@sanity/ailf 0.1.6 → 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-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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/build-request.ts — Build a PipelineRequest from local state.
|
|
3
|
+
*
|
|
4
|
+
* Translates the user's local `.ailf/tasks/` directory and resolved CLI
|
|
5
|
+
* config into a `PipelineRequest` payload ready for the API. Reuses the
|
|
6
|
+
* existing `RepoTaskSource` adapter for YAML parsing — no duplicated
|
|
7
|
+
* parsing logic.
|
|
8
|
+
*
|
|
9
|
+
* The built request is validated against `PipelineRequestSchema` before
|
|
10
|
+
* return (fail-fast on malformed payloads).
|
|
11
|
+
*
|
|
12
|
+
* @see packages/core/src/schemas/pipeline-request.ts — shared schema
|
|
13
|
+
* @see packages/eval/src/adapters/task-sources/repo-task-source.ts
|
|
14
|
+
*/
|
|
15
|
+
import { type PipelineRequest } from "../../_vendor/ailf-core/index.d.ts";
|
|
16
|
+
/** Options for building a remote pipeline request. */
|
|
17
|
+
export interface BuildRequestOptions {
|
|
18
|
+
/** Path to .ailf/tasks/ directory. */
|
|
19
|
+
tasksDir: string;
|
|
20
|
+
/** Resolved CLI config for mode, scoping, and source overrides. */
|
|
21
|
+
config: RemoteConfigSlice;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Subset of ResolvedConfig fields needed for building a remote request.
|
|
25
|
+
* Defined as a standalone interface so the build function doesn't depend
|
|
26
|
+
* on the full ResolvedConfig type (keeps the api-client adapter decoupled).
|
|
27
|
+
*/
|
|
28
|
+
export interface RemoteConfigSlice {
|
|
29
|
+
mode?: string;
|
|
30
|
+
debug?: {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
firstN?: number;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
sample?: number;
|
|
35
|
+
};
|
|
36
|
+
areas?: string[];
|
|
37
|
+
tasks?: string[];
|
|
38
|
+
changedDocs?: string[];
|
|
39
|
+
source?: string;
|
|
40
|
+
compareEnabled?: boolean;
|
|
41
|
+
compareThreshold?: number;
|
|
42
|
+
publishEnabled?: boolean;
|
|
43
|
+
publishTag?: string;
|
|
44
|
+
concurrency?: number;
|
|
45
|
+
datasetOverride?: string;
|
|
46
|
+
projectIdOverride?: string;
|
|
47
|
+
perspectiveOverride?: string;
|
|
48
|
+
graderReplications?: number;
|
|
49
|
+
gapAnalysisEnabled?: boolean;
|
|
50
|
+
readinessEnabled?: boolean;
|
|
51
|
+
discoveryReportEnabled?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build a PipelineRequest from local tasks and config.
|
|
55
|
+
*
|
|
56
|
+
* 1. Loads tasks via RepoTaskSource (validates via Zod automatically)
|
|
57
|
+
* 2. Applies filter options (areas, task IDs) from config
|
|
58
|
+
* 3. Maps tasks to inline format
|
|
59
|
+
* 4. Builds the full PipelineRequest
|
|
60
|
+
* 5. Validates against PipelineRequestSchema
|
|
61
|
+
*
|
|
62
|
+
* Throws on validation errors (from RepoTaskSource or PipelineRequestSchema).
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildRemoteRequest(options: BuildRequestOptions): Promise<{
|
|
65
|
+
request: PipelineRequest;
|
|
66
|
+
taskCount: number;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Find the tasks directory, trying in order:
|
|
70
|
+
* 1. Explicit path (from --repo-tasks-path)
|
|
71
|
+
* 2. .ailf/tasks/ relative to rootDir
|
|
72
|
+
*
|
|
73
|
+
* Returns the resolved path or throws if not found.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveTasksDir(rootDir: string, explicitPath?: string): string;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/build-request.ts — Build a PipelineRequest from local state.
|
|
3
|
+
*
|
|
4
|
+
* Translates the user's local `.ailf/tasks/` directory and resolved CLI
|
|
5
|
+
* config into a `PipelineRequest` payload ready for the API. Reuses the
|
|
6
|
+
* existing `RepoTaskSource` adapter for YAML parsing — no duplicated
|
|
7
|
+
* parsing logic.
|
|
8
|
+
*
|
|
9
|
+
* The built request is validated against `PipelineRequestSchema` before
|
|
10
|
+
* return (fail-fast on malformed payloads).
|
|
11
|
+
*
|
|
12
|
+
* @see packages/core/src/schemas/pipeline-request.ts — shared schema
|
|
13
|
+
* @see packages/eval/src/adapters/task-sources/repo-task-source.ts
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { resolve } from "path";
|
|
17
|
+
import { PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
|
|
18
|
+
import { RepoTaskSource } from "../task-sources/repo-task-source.js";
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Public API
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Build a PipelineRequest from local tasks and config.
|
|
24
|
+
*
|
|
25
|
+
* 1. Loads tasks via RepoTaskSource (validates via Zod automatically)
|
|
26
|
+
* 2. Applies filter options (areas, task IDs) from config
|
|
27
|
+
* 3. Maps tasks to inline format
|
|
28
|
+
* 4. Builds the full PipelineRequest
|
|
29
|
+
* 5. Validates against PipelineRequestSchema
|
|
30
|
+
*
|
|
31
|
+
* Throws on validation errors (from RepoTaskSource or PipelineRequestSchema).
|
|
32
|
+
*/
|
|
33
|
+
export async function buildRemoteRequest(options) {
|
|
34
|
+
const { tasksDir, config } = options;
|
|
35
|
+
// 1. Load and validate local tasks
|
|
36
|
+
const taskSource = new RepoTaskSource(tasksDir);
|
|
37
|
+
const filterOptions = buildFilterOptions(config);
|
|
38
|
+
const tasks = await taskSource.loadTasks(filterOptions);
|
|
39
|
+
if (tasks.length === 0) {
|
|
40
|
+
throw new Error("No tasks found after applying filters.\n" +
|
|
41
|
+
` Tasks directory: ${tasksDir}\n` +
|
|
42
|
+
(config.areas ? ` Area filter: ${config.areas.join(", ")}\n` : "") +
|
|
43
|
+
(config.tasks ? ` Task filter: ${config.tasks.join(", ")}\n` : "") +
|
|
44
|
+
" Check that your .ailf/tasks/ YAML files define tasks matching these filters.");
|
|
45
|
+
}
|
|
46
|
+
// 2. Convert tasks to inline format
|
|
47
|
+
const inlineTasks = tasks.map(taskToInlineFormat);
|
|
48
|
+
// 3. Build the PipelineRequest
|
|
49
|
+
const raw = {
|
|
50
|
+
taskMode: "inline",
|
|
51
|
+
inlineTasks,
|
|
52
|
+
};
|
|
53
|
+
// Mode
|
|
54
|
+
if (config.mode && config.mode !== "full") {
|
|
55
|
+
raw.mode = config.mode;
|
|
56
|
+
}
|
|
57
|
+
// Debug
|
|
58
|
+
if (config.debug?.enabled) {
|
|
59
|
+
raw.debug = config.debug;
|
|
60
|
+
}
|
|
61
|
+
// Scoping
|
|
62
|
+
if (config.areas?.length)
|
|
63
|
+
raw.areas = config.areas;
|
|
64
|
+
if (config.tasks?.length)
|
|
65
|
+
raw.tasks = config.tasks;
|
|
66
|
+
if (config.changedDocs?.length)
|
|
67
|
+
raw.changedDocs = config.changedDocs;
|
|
68
|
+
// Source
|
|
69
|
+
if (config.source)
|
|
70
|
+
raw.source = config.source;
|
|
71
|
+
// Comparison
|
|
72
|
+
if (config.compareEnabled)
|
|
73
|
+
raw.compare = true;
|
|
74
|
+
if (config.compareThreshold !== undefined) {
|
|
75
|
+
raw.compareThreshold = config.compareThreshold;
|
|
76
|
+
}
|
|
77
|
+
// Publishing
|
|
78
|
+
if (config.publishEnabled !== undefined)
|
|
79
|
+
raw.publish = config.publishEnabled;
|
|
80
|
+
if (config.publishTag)
|
|
81
|
+
raw.publishTag = config.publishTag;
|
|
82
|
+
// Concurrency
|
|
83
|
+
if (config.concurrency)
|
|
84
|
+
raw.concurrency = config.concurrency;
|
|
85
|
+
// Source overrides
|
|
86
|
+
if (config.datasetOverride)
|
|
87
|
+
raw.dataset = config.datasetOverride;
|
|
88
|
+
if (config.projectIdOverride)
|
|
89
|
+
raw.projectId = config.projectIdOverride;
|
|
90
|
+
if (config.perspectiveOverride)
|
|
91
|
+
raw.perspective = config.perspectiveOverride;
|
|
92
|
+
// Advanced
|
|
93
|
+
if (config.graderReplications) {
|
|
94
|
+
raw.graderReplications = config.graderReplications;
|
|
95
|
+
}
|
|
96
|
+
if (config.gapAnalysisEnabled)
|
|
97
|
+
raw.gapAnalysis = true;
|
|
98
|
+
if (config.readinessEnabled)
|
|
99
|
+
raw.readiness = true;
|
|
100
|
+
if (config.discoveryReportEnabled)
|
|
101
|
+
raw.discoveryReport = true;
|
|
102
|
+
// 4. Validate the assembled request
|
|
103
|
+
const parsed = PipelineRequestSchema.parse(raw);
|
|
104
|
+
return { request: parsed, taskCount: tasks.length };
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Task directory resolution
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Find the tasks directory, trying in order:
|
|
111
|
+
* 1. Explicit path (from --repo-tasks-path)
|
|
112
|
+
* 2. .ailf/tasks/ relative to rootDir
|
|
113
|
+
*
|
|
114
|
+
* Returns the resolved path or throws if not found.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveTasksDir(rootDir, explicitPath) {
|
|
117
|
+
if (explicitPath) {
|
|
118
|
+
if (!existsSync(explicitPath)) {
|
|
119
|
+
throw new Error(`Tasks directory not found: ${explicitPath}\n` +
|
|
120
|
+
" Provide a valid path via --repo-tasks-path");
|
|
121
|
+
}
|
|
122
|
+
return explicitPath;
|
|
123
|
+
}
|
|
124
|
+
const defaultPath = resolve(rootDir, ".ailf", "tasks");
|
|
125
|
+
if (existsSync(defaultPath)) {
|
|
126
|
+
return defaultPath;
|
|
127
|
+
}
|
|
128
|
+
throw new Error("No tasks directory found.\n" +
|
|
129
|
+
` Looked for: ${defaultPath}\n` +
|
|
130
|
+
" Remote mode reads task definitions from .ailf/tasks/ in your repo.\n" +
|
|
131
|
+
" Run 'ailf init' to create example tasks, or specify a path:\n" +
|
|
132
|
+
" ailf eval --remote --repo-tasks-path ./my-tasks/");
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Helpers
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/**
|
|
138
|
+
* Convert a TaskDefinition back to the camelCase inline format expected
|
|
139
|
+
* by the API. This is essentially the inverse of RepoTaskSource's
|
|
140
|
+
* `mapToTaskDefinition()`.
|
|
141
|
+
*/
|
|
142
|
+
function taskToInlineFormat(task) {
|
|
143
|
+
const inline = {
|
|
144
|
+
id: task.id,
|
|
145
|
+
description: task.description,
|
|
146
|
+
featureArea: task.featureArea,
|
|
147
|
+
assert: task.assertions,
|
|
148
|
+
};
|
|
149
|
+
if (task.canonicalDocs?.length) {
|
|
150
|
+
inline.canonicalDocs = task.canonicalDocs;
|
|
151
|
+
}
|
|
152
|
+
if (task.taskPrompt) {
|
|
153
|
+
inline.vars = {
|
|
154
|
+
task: task.taskPrompt,
|
|
155
|
+
docs: "",
|
|
156
|
+
...(task.extraVars ?? {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (task.docCoverage) {
|
|
160
|
+
inline.docCoverage = true;
|
|
161
|
+
}
|
|
162
|
+
if (task.referenceSolution) {
|
|
163
|
+
inline.referenceSolution = task.referenceSolution;
|
|
164
|
+
}
|
|
165
|
+
if (task.baseline) {
|
|
166
|
+
inline.baseline = task.baseline;
|
|
167
|
+
}
|
|
168
|
+
return inline;
|
|
169
|
+
}
|
|
170
|
+
function buildFilterOptions(config) {
|
|
171
|
+
const areas = config.areas?.length ? config.areas : undefined;
|
|
172
|
+
const taskIds = config.tasks?.length ? config.tasks : undefined;
|
|
173
|
+
if (!areas && !taskIds)
|
|
174
|
+
return undefined;
|
|
175
|
+
return { areas, taskIds };
|
|
176
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/errors.ts — Typed error classes for API client failures.
|
|
3
|
+
*
|
|
4
|
+
* Each error class corresponds to a distinct failure mode so callers can
|
|
5
|
+
* match on `instanceof` for control flow. Error messages are always
|
|
6
|
+
* human-readable; machine-readable details are in typed fields.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The API returned a well-formed error response (JSON envelope with
|
|
10
|
+
* `object: "error"`). The HTTP status code and error details are preserved.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ApiError extends Error {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly statusCode: number;
|
|
15
|
+
readonly param?: string | undefined;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
constructor(message: string, code: string, statusCode: number, param?: string | undefined);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The API key is missing, invalid, or lacks the required scope.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ApiAuthError extends ApiError {
|
|
23
|
+
readonly name = "ApiAuthError";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The API could not be reached — DNS failure, connection refused,
|
|
27
|
+
* timeout, or a non-JSON response (e.g., 502 from a CDN).
|
|
28
|
+
*/
|
|
29
|
+
export declare class ApiConnectionError extends Error {
|
|
30
|
+
readonly url: string;
|
|
31
|
+
readonly cause?: unknown | undefined;
|
|
32
|
+
readonly name = "ApiConnectionError";
|
|
33
|
+
constructor(message: string, url: string, cause?: unknown | undefined);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The job did not reach a terminal state within the allotted time.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ApiTimeoutError extends Error {
|
|
39
|
+
readonly jobId: string;
|
|
40
|
+
readonly timeoutMs: number;
|
|
41
|
+
readonly name = "ApiTimeoutError";
|
|
42
|
+
constructor(jobId: string, timeoutMs: number);
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/errors.ts — Typed error classes for API client failures.
|
|
3
|
+
*
|
|
4
|
+
* Each error class corresponds to a distinct failure mode so callers can
|
|
5
|
+
* match on `instanceof` for control flow. Error messages are always
|
|
6
|
+
* human-readable; machine-readable details are in typed fields.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// ApiError — API returned a non-2xx response with a structured error body
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* The API returned a well-formed error response (JSON envelope with
|
|
13
|
+
* `object: "error"`). The HTTP status code and error details are preserved.
|
|
14
|
+
*/
|
|
15
|
+
export class ApiError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
statusCode;
|
|
18
|
+
param;
|
|
19
|
+
name = "ApiError";
|
|
20
|
+
constructor(message, code, statusCode, param) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
this.param = param;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// ApiAuthError — 401/403 from the API
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* The API key is missing, invalid, or lacks the required scope.
|
|
32
|
+
*/
|
|
33
|
+
export class ApiAuthError extends ApiError {
|
|
34
|
+
name = "ApiAuthError";
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// ApiConnectionError — network-level failure
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* The API could not be reached — DNS failure, connection refused,
|
|
41
|
+
* timeout, or a non-JSON response (e.g., 502 from a CDN).
|
|
42
|
+
*/
|
|
43
|
+
export class ApiConnectionError extends Error {
|
|
44
|
+
url;
|
|
45
|
+
cause;
|
|
46
|
+
name = "ApiConnectionError";
|
|
47
|
+
constructor(message, url, cause) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.url = url;
|
|
50
|
+
this.cause = cause;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// ApiTimeoutError — polling deadline exceeded
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* The job did not reach a terminal state within the allotted time.
|
|
58
|
+
*/
|
|
59
|
+
export class ApiTimeoutError extends Error {
|
|
60
|
+
jobId;
|
|
61
|
+
timeoutMs;
|
|
62
|
+
name = "ApiTimeoutError";
|
|
63
|
+
constructor(jobId, timeoutMs) {
|
|
64
|
+
super(`Job ${jobId} did not complete within ${Math.round(timeoutMs / 1000)}s`);
|
|
65
|
+
this.jobId = jobId;
|
|
66
|
+
this.timeoutMs = timeoutMs;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/format-error.ts — Human-readable error formatting for failed jobs.
|
|
3
|
+
*
|
|
4
|
+
* Produces structured console output that shows the failed step, error
|
|
5
|
+
* message, and (when available) a remediation hint. The output is designed
|
|
6
|
+
* to be useful both in interactive terminals and in CI log output.
|
|
7
|
+
*/
|
|
8
|
+
import type { JobResponse } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Format a failed job response into a multi-line error string.
|
|
11
|
+
*
|
|
12
|
+
* Does NOT call `process.exit()` — the caller decides what to do.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```
|
|
16
|
+
* ❌ Pipeline failed at step 'fetch-docs'
|
|
17
|
+
* Postcondition failed: Canonical context for task "foo" is empty.
|
|
18
|
+
*
|
|
19
|
+
* 💡 One or more canonicalDocs slugs in your task definitions don't match ...
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatJobError(job: JobResponse): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/format-error.ts — Human-readable error formatting for failed jobs.
|
|
3
|
+
*
|
|
4
|
+
* Produces structured console output that shows the failed step, error
|
|
5
|
+
* message, and (when available) a remediation hint. The output is designed
|
|
6
|
+
* to be useful both in interactive terminals and in CI log output.
|
|
7
|
+
*/
|
|
8
|
+
import { getRemediationHint } from "./remediation.js";
|
|
9
|
+
/**
|
|
10
|
+
* Format a failed job response into a multi-line error string.
|
|
11
|
+
*
|
|
12
|
+
* Does NOT call `process.exit()` — the caller decides what to do.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```
|
|
16
|
+
* ❌ Pipeline failed at step 'fetch-docs'
|
|
17
|
+
* Postcondition failed: Canonical context for task "foo" is empty.
|
|
18
|
+
*
|
|
19
|
+
* 💡 One or more canonicalDocs slugs in your task definitions don't match ...
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function formatJobError(job) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
if (job.error?.step && job.error?.message) {
|
|
25
|
+
lines.push(`❌ Pipeline failed at step '${job.error.step}'`);
|
|
26
|
+
lines.push(` ${job.error.message}`);
|
|
27
|
+
}
|
|
28
|
+
else if (job.error?.message) {
|
|
29
|
+
lines.push(`❌ Evaluation failed`);
|
|
30
|
+
lines.push(` ${job.error.message}`);
|
|
31
|
+
}
|
|
32
|
+
else if (job.status === "timed-out") {
|
|
33
|
+
lines.push(`❌ Evaluation timed out`);
|
|
34
|
+
lines.push(" The job did not complete within the server-side deadline.");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(`❌ Evaluation ${job.status}`);
|
|
38
|
+
}
|
|
39
|
+
// Remediation hint
|
|
40
|
+
if (job.error) {
|
|
41
|
+
const hint = getRemediationHint(job.error);
|
|
42
|
+
if (hint) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(`💡 ${hint}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/ — Barrel exports for the AILF API client adapter.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
|
|
6
|
+
*/
|
|
7
|
+
export { ApiClient } from "./api-client.js";
|
|
8
|
+
export { buildRemoteRequest, resolveTasksDir, type BuildRequestOptions, type RemoteConfigSlice, } from "./build-request.js";
|
|
9
|
+
export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
|
|
10
|
+
export { formatJobError } from "./format-error.js";
|
|
11
|
+
export { createProgressDisplay } from "./progress.js";
|
|
12
|
+
export { getRemediationHint } from "./remediation.js";
|
|
13
|
+
export type { ApiClientOptions, ApiEnvelope, ApiErrorDetail, ApiErrorResponse, JobError, JobExecution, JobProgress, JobResponse, JobStatus, SubmitResponse, ValidationIssue, ValidationResponse, WaitOptions, } from "./types.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/ — Barrel exports for the AILF API client adapter.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
|
|
6
|
+
*/
|
|
7
|
+
export { ApiClient } from "./api-client.js";
|
|
8
|
+
export { buildRemoteRequest, resolveTasksDir, } from "./build-request.js";
|
|
9
|
+
export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
|
|
10
|
+
export { formatJobError } from "./format-error.js";
|
|
11
|
+
export { createProgressDisplay } from "./progress.js";
|
|
12
|
+
export { getRemediationHint } from "./remediation.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/progress.ts — Polling progress display for the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Creates a callback function that the ApiClient's `waitForCompletion()`
|
|
5
|
+
* calls after each poll. Tracks elapsed time and emits single-line status
|
|
6
|
+
* updates so the user sees real-time feedback during remote evaluation.
|
|
7
|
+
*/
|
|
8
|
+
import type { JobResponse } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Create a progress display callback for `waitForCompletion()`.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const job = await client.waitForCompletion(jobId, {
|
|
15
|
+
* onProgress: createProgressDisplay(),
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Output example:
|
|
20
|
+
* ```
|
|
21
|
+
* ⏳ [queued] Waiting for runner...
|
|
22
|
+
* ⏳ [running] Step 2/8: fetch-docs (1m 12s)
|
|
23
|
+
* ⏳ [running] Step 5/8: run-eval (3m 45s)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createProgressDisplay(): (job: JobResponse) => void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/progress.ts — Polling progress display for the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Creates a callback function that the ApiClient's `waitForCompletion()`
|
|
5
|
+
* calls after each poll. Tracks elapsed time and emits single-line status
|
|
6
|
+
* updates so the user sees real-time feedback during remote evaluation.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Create a progress display callback for `waitForCompletion()`.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* const job = await client.waitForCompletion(jobId, {
|
|
14
|
+
* onProgress: createProgressDisplay(),
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Output example:
|
|
19
|
+
* ```
|
|
20
|
+
* ⏳ [queued] Waiting for runner...
|
|
21
|
+
* ⏳ [running] Step 2/8: fetch-docs (1m 12s)
|
|
22
|
+
* ⏳ [running] Step 5/8: run-eval (3m 45s)
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createProgressDisplay() {
|
|
26
|
+
const startMs = Date.now();
|
|
27
|
+
let lastLine = "";
|
|
28
|
+
return (job) => {
|
|
29
|
+
const elapsed = formatElapsed(Date.now() - startMs);
|
|
30
|
+
let line;
|
|
31
|
+
switch (job.status) {
|
|
32
|
+
case "received":
|
|
33
|
+
line = `⏳ [received] Submitted, waiting for API... (${elapsed})`;
|
|
34
|
+
break;
|
|
35
|
+
case "queued":
|
|
36
|
+
line = `⏳ [queued] Waiting for runner... (${elapsed})`;
|
|
37
|
+
break;
|
|
38
|
+
case "running": {
|
|
39
|
+
if (job.progress) {
|
|
40
|
+
const { step, current, total } = job.progress;
|
|
41
|
+
line = `⏳ [running] Step ${current}/${total}: ${step} (${elapsed})`;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
line = `⏳ [running] Evaluating... (${elapsed})`;
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
// Terminal states — don't display; waitForCompletion handles them
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Only print if the line changed (avoids duplicates during long-poll)
|
|
53
|
+
if (line !== lastLine) {
|
|
54
|
+
console.log(line);
|
|
55
|
+
lastLine = line;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function formatElapsed(ms) {
|
|
63
|
+
const seconds = Math.floor(ms / 1000);
|
|
64
|
+
if (seconds < 60)
|
|
65
|
+
return `${seconds}s`;
|
|
66
|
+
const minutes = Math.floor(seconds / 60);
|
|
67
|
+
const remaining = seconds % 60;
|
|
68
|
+
return `${minutes}m ${remaining}s`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/remediation.ts — Error-to-hint matching for common failures.
|
|
3
|
+
*
|
|
4
|
+
* When the pipeline fails remotely, the CLI extracts the structured error
|
|
5
|
+
* (step + message) and matches it against known patterns to provide
|
|
6
|
+
* actionable remediation advice. This replaces the inline JavaScript
|
|
7
|
+
* hints that were previously embedded in the generated workflow YAML.
|
|
8
|
+
*
|
|
9
|
+
* Keeping hints in TypeScript means they're:
|
|
10
|
+
* - Testable (unit tests per pattern)
|
|
11
|
+
* - Version-locked to the CLI (updated automatically)
|
|
12
|
+
* - Not duplicated across workflow templates
|
|
13
|
+
*/
|
|
14
|
+
import type { JobError } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Match an error against known patterns and return a remediation hint,
|
|
17
|
+
* or `undefined` if no pattern matches.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getRemediationHint(error: JobError): string | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/remediation.ts — Error-to-hint matching for common failures.
|
|
3
|
+
*
|
|
4
|
+
* When the pipeline fails remotely, the CLI extracts the structured error
|
|
5
|
+
* (step + message) and matches it against known patterns to provide
|
|
6
|
+
* actionable remediation advice. This replaces the inline JavaScript
|
|
7
|
+
* hints that were previously embedded in the generated workflow YAML.
|
|
8
|
+
*
|
|
9
|
+
* Keeping hints in TypeScript means they're:
|
|
10
|
+
* - Testable (unit tests per pattern)
|
|
11
|
+
* - Version-locked to the CLI (updated automatically)
|
|
12
|
+
* - Not duplicated across workflow templates
|
|
13
|
+
*/
|
|
14
|
+
const HINTS = [
|
|
15
|
+
{
|
|
16
|
+
match: (e) => /canonical context.*empty/i.test(e.message) ||
|
|
17
|
+
/no article found for slug/i.test(e.message),
|
|
18
|
+
hint: "One or more `canonicalDocs` slugs in your task definitions don't match " +
|
|
19
|
+
"any article in the documentation. Check the `slug` values in " +
|
|
20
|
+
"`.ailf/tasks/*.yaml` and ensure they correspond to real articles.\n" +
|
|
21
|
+
" Run `ailf validate` to check your task definitions locally.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
match: (e) => /no tasks found/i.test(e.message),
|
|
25
|
+
hint: "No evaluation tasks were found. Add task YAML files to `.ailf/tasks/`.\n" +
|
|
26
|
+
" Run `ailf init` to generate example tasks.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
match: (e) => /rate_limit_exceeded/i.test(e.message),
|
|
30
|
+
hint: "Your daily evaluation budget has been reached. Try again tomorrow,\n" +
|
|
31
|
+
" or use `--debug` to run a smaller subset of tests.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
match: (e) => /concurrent_limit_exceeded/i.test(e.message),
|
|
35
|
+
hint: "Too many evaluations are running simultaneously.\n" +
|
|
36
|
+
" Wait for the current jobs to finish before submitting a new one.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
match: (e) => /authentication_failed/i.test(e.message) ||
|
|
40
|
+
/invalid.*api.?key/i.test(e.message),
|
|
41
|
+
hint: "Check that `AILF_API_KEY` is set correctly in your environment.\n" +
|
|
42
|
+
" The key should start with `ailf_live_sk_` or `ailf_test_sk_`.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
match: (e) => /validation_error/i.test(e.message) && /inlineTasks/i.test(e.message),
|
|
46
|
+
hint: "Your task definitions have validation errors.\n" +
|
|
47
|
+
" Run `ailf validate` locally to see detailed error messages.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
match: (e) => e.step === "fetch-docs" && /postcondition/i.test(e.message),
|
|
51
|
+
hint: "The documentation fetch step completed but one or more tasks had " +
|
|
52
|
+
"empty context. This usually means a `canonicalDocs` slug doesn't " +
|
|
53
|
+
"match any article.\n" +
|
|
54
|
+
" Check the slug values in `.ailf/tasks/*.yaml`.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
match: (e) => e.step === "dispatch" && /dispatch failed/i.test(e.message),
|
|
58
|
+
hint: "The evaluation could not be dispatched to the execution backend.\n" +
|
|
59
|
+
" This is usually a transient infrastructure issue — try again in a few minutes.",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Public API
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Match an error against known patterns and return a remediation hint,
|
|
67
|
+
* or `undefined` if no pattern matches.
|
|
68
|
+
*/
|
|
69
|
+
export function getRemediationHint(error) {
|
|
70
|
+
for (const entry of HINTS) {
|
|
71
|
+
if (entry.match(error)) {
|
|
72
|
+
return entry.hint;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|