@sanity/ailf 3.4.1 → 3.5.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.
- package/bin/ailf.js +16 -1
- package/config/airbyte/ai_literacy_framework.connector.yaml +114 -0
- package/config/bigquery/README.md +44 -8
- package/config/bigquery/views/official_area_scores.sql +20 -0
- package/config/bigquery/views/official_runs.sql +31 -0
- package/config/bigquery/views/reports.sql +19 -0
- package/config/bigquery/views/team_runs_template.sql +17 -0
- 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 +25 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +23 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.js +59 -1
- package/dist/_vendor/ailf-shared/index.d.ts +2 -0
- package/dist/_vendor/ailf-shared/index.js +2 -0
- package/dist/_vendor/ailf-shared/owner-teams.d.ts +26 -0
- package/dist/_vendor/ailf-shared/owner-teams.js +52 -0
- package/dist/_vendor/ailf-shared/run-classification.d.ts +100 -0
- package/dist/_vendor/ailf-shared/run-classification.js +28 -0
- package/dist/_vendor/ailf-shared/run-context.d.ts +23 -0
- package/dist/adapters/api-client/build-request.d.ts +42 -0
- package/dist/adapters/api-client/build-request.js +188 -10
- package/dist/adapters/api-client/index.d.ts +1 -1
- package/dist/adapters/api-client/index.js +1 -1
- package/dist/commands/explain-handler.js +5 -0
- package/dist/commands/pipeline-action.d.ts +6 -0
- package/dist/commands/pipeline-action.js +13 -1
- package/dist/commands/pipeline.d.ts +5 -0
- package/dist/commands/pipeline.js +16 -2
- package/dist/commands/remote-pipeline.js +13 -1
- package/dist/orchestration/steps/finalize-run-step.js +1 -0
- package/dist/orchestration/steps/publish-report-step.js +1 -0
- package/dist/pipeline/map-request-to-config.js +18 -0
- package/dist/pipeline/run-context.d.ts +63 -0
- package/dist/pipeline/run-context.js +166 -0
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known owner-team slugs and soft-normalization helper.
|
|
3
|
+
*
|
|
4
|
+
* `RunOwner.team` is free-form by design (external teams name themselves
|
|
5
|
+
* and internal names drift). This module provides two things to keep UX
|
|
6
|
+
* polished without blocking new entrants:
|
|
7
|
+
*
|
|
8
|
+
* - `KNOWN_OWNER_TEAMS` — a seed list of canonical slugs that populates
|
|
9
|
+
* Studio filter comboboxes as suggestions. Not a closed enum.
|
|
10
|
+
* - `normalizeOwnerTeam()` — maps a handful of common aliases to
|
|
11
|
+
* canonical slugs. Warn-only: returns the original string when no
|
|
12
|
+
* mapping applies. Adding an alias here is a one-liner.
|
|
13
|
+
*
|
|
14
|
+
* @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
|
|
15
|
+
*/
|
|
16
|
+
export const KNOWN_OWNER_TEAMS = [
|
|
17
|
+
"content-lake",
|
|
18
|
+
"core-docs",
|
|
19
|
+
"growth",
|
|
20
|
+
"media",
|
|
21
|
+
"platform",
|
|
22
|
+
"studio",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Lowercase alias → canonical slug. Lightweight — only entries where
|
|
26
|
+
* drift has been observed belong here. Unknown values pass through.
|
|
27
|
+
*/
|
|
28
|
+
const OWNER_TEAM_ALIASES = {
|
|
29
|
+
coredocs: "core-docs",
|
|
30
|
+
docs: "core-docs",
|
|
31
|
+
studio_team: "studio",
|
|
32
|
+
"studio-team": "studio",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a free-form team slug to its canonical form.
|
|
36
|
+
*
|
|
37
|
+
* - Trims and lowercases.
|
|
38
|
+
* - Maps known aliases to canonical slugs.
|
|
39
|
+
* - Passes unknown values through unchanged (warn-only at the UI layer).
|
|
40
|
+
* - Returns `"unknown"` for empty input.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeOwnerTeam(value) {
|
|
43
|
+
if (!value)
|
|
44
|
+
return "unknown";
|
|
45
|
+
const trimmed = value.trim().toLowerCase();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
return "unknown";
|
|
48
|
+
return OWNER_TEAM_ALIASES[trimmed] ?? trimmed;
|
|
49
|
+
}
|
|
50
|
+
export function isKnownOwnerTeam(value) {
|
|
51
|
+
return KNOWN_OWNER_TEAMS.includes(value);
|
|
52
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run classification, ownership, executor, and environment metadata.
|
|
3
|
+
*
|
|
4
|
+
* These fields extend `RunContext` to capture run *intent*, *attribution*,
|
|
5
|
+
* and *reproducibility* — orthogonal to the *mechanism* captured by
|
|
6
|
+
* `RunTrigger`. A scheduled run can be experimental; a manual run can be
|
|
7
|
+
* official; a PR-triggered run is executed by GH Actions but attributable
|
|
8
|
+
* to the PR author.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
|
|
11
|
+
* @see docs/design-docs/run-classification-and-ownership.md
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* How a run should be treated for reporting and trend tracking.
|
|
15
|
+
*
|
|
16
|
+
* Orthogonal to `RunTrigger` (mechanism). Defaults to `"ad-hoc"` when
|
|
17
|
+
* unannotated so pre-taxonomy runs never leak into the canonical series.
|
|
18
|
+
*/
|
|
19
|
+
export type RunClassification = "official" | "ad-hoc" | "experimental" | "test" | "external";
|
|
20
|
+
export declare const RUN_CLASSIFICATIONS: readonly RunClassification[];
|
|
21
|
+
export declare function isRunClassification(value: unknown): value is RunClassification;
|
|
22
|
+
/**
|
|
23
|
+
* Attribution — which team and (optionally) individual the run *belongs to*.
|
|
24
|
+
*
|
|
25
|
+
* `team` is a free-form slug, not a closed enum: external teams name
|
|
26
|
+
* themselves and internal names drift. A soft-normalization layer under
|
|
27
|
+
* `config/owners.ts` maps aliases to canonical slugs (warn-only).
|
|
28
|
+
*/
|
|
29
|
+
export interface RunOwner {
|
|
30
|
+
team: string;
|
|
31
|
+
individual?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Who or what actually invoked the run.
|
|
35
|
+
*
|
|
36
|
+
* Separate from `RunOwner` because they diverge for automated surfaces:
|
|
37
|
+
* a PR gate is *executed by* GH Actions but *attributable to* the PR
|
|
38
|
+
* author. Both variants expose a `name` field so consumers can format
|
|
39
|
+
* them with one template.
|
|
40
|
+
*
|
|
41
|
+
* Every detectable identity field is optional — a misconfigured shell,
|
|
42
|
+
* a container without `git`, or a CI provider that doesn't expose actor
|
|
43
|
+
* metadata can all still produce a valid run with thin provenance.
|
|
44
|
+
*/
|
|
45
|
+
export type RunExecutor = RunExecutorUser | RunExecutorSystem;
|
|
46
|
+
export interface RunExecutorUser {
|
|
47
|
+
type: "user";
|
|
48
|
+
/** Detected from `git config user.name`, `os.userInfo().username`, or GH actor. */
|
|
49
|
+
name?: string;
|
|
50
|
+
/** From `git config user.email`. Subject to the `AILF_CAPTURE_EMAIL` opt-out. */
|
|
51
|
+
email?: string;
|
|
52
|
+
/** Where the invocation originated. Always knowable. */
|
|
53
|
+
surface: RunExecutorSurface;
|
|
54
|
+
/** GH actor when the user invoked via a GH surface (PR, manual dispatch). */
|
|
55
|
+
githubActor?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface RunExecutorSystem {
|
|
58
|
+
type: "system";
|
|
59
|
+
/** e.g. `"github-actions"`, `"vercel-cron"`, `"sanity-webhook"`. */
|
|
60
|
+
name: string;
|
|
61
|
+
workflow?: string;
|
|
62
|
+
runId?: string;
|
|
63
|
+
}
|
|
64
|
+
export type RunExecutorSurface = "cli" | "studio" | "api";
|
|
65
|
+
export declare const RUN_EXECUTOR_SURFACES: readonly RunExecutorSurface[];
|
|
66
|
+
/**
|
|
67
|
+
* Links to related runs. Fills the gap where the Studio report schema
|
|
68
|
+
* already carried these fields but `RunContext` did not.
|
|
69
|
+
*/
|
|
70
|
+
export interface RunLineage {
|
|
71
|
+
/** Prior `RunId` this run re-executes. */
|
|
72
|
+
rerunOf?: string;
|
|
73
|
+
/** Sibling `RunId` this run is intentionally compared against. */
|
|
74
|
+
comparedAgainst?: string;
|
|
75
|
+
/** API-gateway job ID that dispatched this run. */
|
|
76
|
+
parentJobId?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reproducibility metadata — which AILF/Node ran the eval.
|
|
80
|
+
*
|
|
81
|
+
* Required on every new run so cross-version trend comparisons can
|
|
82
|
+
* isolate framework changes from doc changes.
|
|
83
|
+
*/
|
|
84
|
+
export interface RunTool {
|
|
85
|
+
ailfVersion: string;
|
|
86
|
+
nodeVersion: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Platform + CI-provider metadata for debugging flakes. Hostname is
|
|
90
|
+
* intentionally excluded — it leaks machine/user identity without
|
|
91
|
+
* filtering benefit.
|
|
92
|
+
*/
|
|
93
|
+
export interface RunHost {
|
|
94
|
+
/** `os.platform()` — `"darwin"` | `"linux"` | `"win32"`. */
|
|
95
|
+
platform: string;
|
|
96
|
+
/** `os.arch()` — `"x64"` | `"arm64"`. */
|
|
97
|
+
arch: string;
|
|
98
|
+
/** CI provider when running under one, e.g. `"github-actions"`. */
|
|
99
|
+
ci?: string;
|
|
100
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run classification, ownership, executor, and environment metadata.
|
|
3
|
+
*
|
|
4
|
+
* These fields extend `RunContext` to capture run *intent*, *attribution*,
|
|
5
|
+
* and *reproducibility* — orthogonal to the *mechanism* captured by
|
|
6
|
+
* `RunTrigger`. A scheduled run can be experimental; a manual run can be
|
|
7
|
+
* official; a PR-triggered run is executed by GH Actions but attributable
|
|
8
|
+
* to the PR author.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
|
|
11
|
+
* @see docs/design-docs/run-classification-and-ownership.md
|
|
12
|
+
*/
|
|
13
|
+
export const RUN_CLASSIFICATIONS = [
|
|
14
|
+
"official",
|
|
15
|
+
"ad-hoc",
|
|
16
|
+
"experimental",
|
|
17
|
+
"test",
|
|
18
|
+
"external",
|
|
19
|
+
];
|
|
20
|
+
export function isRunClassification(value) {
|
|
21
|
+
return (typeof value === "string" &&
|
|
22
|
+
RUN_CLASSIFICATIONS.includes(value));
|
|
23
|
+
}
|
|
24
|
+
export const RUN_EXECUTOR_SURFACES = [
|
|
25
|
+
"cli",
|
|
26
|
+
"studio",
|
|
27
|
+
"api",
|
|
28
|
+
];
|
|
@@ -15,15 +15,26 @@
|
|
|
15
15
|
* @see docs/design-docs/run-artifact-store.md (§ Drift Prevention)
|
|
16
16
|
*/
|
|
17
17
|
import type { EvalMode } from "./eval-modes.js";
|
|
18
|
+
import type { RunClassification, RunExecutor, RunHost, RunLineage, RunOwner, RunTool } from "./run-classification.js";
|
|
18
19
|
import type { RunTrigger } from "./run-trigger.js";
|
|
19
20
|
export interface RunContext {
|
|
20
21
|
/** Which feature areas were evaluated */
|
|
21
22
|
areas: string[];
|
|
23
|
+
/**
|
|
24
|
+
* How this run should be treated for reporting and trend tracking.
|
|
25
|
+
* Orthogonal to `trigger` (mechanism). Defaults to `"ad-hoc"` when
|
|
26
|
+
* unannotated — only the scheduled workflow mints `"official"`.
|
|
27
|
+
*
|
|
28
|
+
* @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
|
|
29
|
+
*/
|
|
30
|
+
classification: RunClassification;
|
|
22
31
|
/**
|
|
23
32
|
* Evaluation fingerprint — SHA-256 of all inputs that affect eval output.
|
|
24
33
|
* Used for cross-environment cache lookup (CI → Content Lake).
|
|
25
34
|
*/
|
|
26
35
|
evalFingerprint?: string;
|
|
36
|
+
/** Who/what actually invoked the run. May or may not match `owner`. */
|
|
37
|
+
executor: RunExecutor;
|
|
27
38
|
/** Git metadata (when run from CI) */
|
|
28
39
|
git?: {
|
|
29
40
|
branch: string;
|
|
@@ -33,6 +44,12 @@ export interface RunContext {
|
|
|
33
44
|
};
|
|
34
45
|
/** Grader model used for scoring */
|
|
35
46
|
graderModel: string;
|
|
47
|
+
/** Platform/CI metadata for debugging flakes. */
|
|
48
|
+
host?: RunHost;
|
|
49
|
+
/** Free-form searchable tags — release IDs, regression hunts, experiments. */
|
|
50
|
+
labels?: string[];
|
|
51
|
+
/** Links to related runs (re-runs, comparison partners, API parent job). */
|
|
52
|
+
lineage?: RunLineage;
|
|
36
53
|
/** Evaluation mode */
|
|
37
54
|
mode: EvalMode;
|
|
38
55
|
/** Models under evaluation */
|
|
@@ -40,6 +57,10 @@ export interface RunContext {
|
|
|
40
57
|
id: string;
|
|
41
58
|
label: string;
|
|
42
59
|
}[];
|
|
60
|
+
/** Which team (and optionally individual) this run is attributable to. */
|
|
61
|
+
owner: RunOwner;
|
|
62
|
+
/** Human-authored "why I ran this" — useful for Content Lake archaeology. */
|
|
63
|
+
purpose?: string;
|
|
43
64
|
/** Documentation source configuration */
|
|
44
65
|
source: {
|
|
45
66
|
baseUrl: string;
|
|
@@ -50,6 +71,8 @@ export interface RunContext {
|
|
|
50
71
|
};
|
|
51
72
|
/** Specific task IDs evaluated when scoped to a subset */
|
|
52
73
|
taskIds?: string[];
|
|
74
|
+
/** Which AILF/Node ran the eval — for cross-version trend compatibility. */
|
|
75
|
+
tool?: RunTool;
|
|
53
76
|
/** What initiated this run */
|
|
54
77
|
trigger: RunTrigger;
|
|
55
78
|
}
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* @see packages/eval/src/adapters/task-sources/repo-task-source.ts
|
|
14
14
|
*/
|
|
15
15
|
import { type PipelineRequest } from "../../_vendor/ailf-core/index.d.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when `buildRemoteRequest` can't find any runnable tasks.
|
|
18
|
+
*
|
|
19
|
+
* The CLI catches this separately from ZodError so it can print the
|
|
20
|
+
* message without an accompanying stack trace — the message is already
|
|
21
|
+
* the whole story for the user.
|
|
22
|
+
*/
|
|
23
|
+
export declare class NoRunnableTasksError extends Error {
|
|
24
|
+
readonly name = "NoRunnableTasksError";
|
|
25
|
+
}
|
|
16
26
|
/** Options for building a remote pipeline request. */
|
|
17
27
|
export interface BuildRequestOptions {
|
|
18
28
|
/** Path to .ailf/tasks/ directory. */
|
|
@@ -27,6 +37,7 @@ export interface BuildRequestOptions {
|
|
|
27
37
|
*/
|
|
28
38
|
export interface RemoteConfigSlice {
|
|
29
39
|
mode?: string;
|
|
40
|
+
variant?: string;
|
|
30
41
|
debug?: {
|
|
31
42
|
enabled?: boolean;
|
|
32
43
|
firstN?: number;
|
|
@@ -51,6 +62,18 @@ export interface RemoteConfigSlice {
|
|
|
51
62
|
readinessEnabled?: boolean;
|
|
52
63
|
discoveryReportEnabled?: boolean;
|
|
53
64
|
noRemoteCache?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* D0037 / W0069 — CLI-flag overrides for the caller envelope. These
|
|
67
|
+
* take precedence over the equivalent env vars when set. When both a
|
|
68
|
+
* flag and its env var are unset the field is omitted from the
|
|
69
|
+
* request (server applies its own defaults).
|
|
70
|
+
*/
|
|
71
|
+
classificationOption?: string;
|
|
72
|
+
ownerTeamOption?: string;
|
|
73
|
+
ownerIndividualOption?: string;
|
|
74
|
+
purposeOption?: string;
|
|
75
|
+
/** Repeatable --label values; appended to AILF_LABELS env values. */
|
|
76
|
+
labelOptions?: string[];
|
|
54
77
|
}
|
|
55
78
|
/**
|
|
56
79
|
* Build a PipelineRequest from local tasks and config.
|
|
@@ -75,3 +98,22 @@ export declare function buildRemoteRequest(options: BuildRequestOptions): Promis
|
|
|
75
98
|
* Returns the resolved path or throws if not found.
|
|
76
99
|
*/
|
|
77
100
|
export declare function resolveTasksDir(rootDir: string, explicitPath?: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Build the D0037 caller envelope payload from CLI flags + env vars.
|
|
103
|
+
*
|
|
104
|
+
* Precedence, highest first:
|
|
105
|
+
* 1. Explicit CLI flag (--classification, --owner-team, --purpose, …)
|
|
106
|
+
* 2. Env var (AILF_CLASSIFICATION, AILF_OWNER_TEAM, AILF_PURPOSE, …)
|
|
107
|
+
* 3. Omit — server applies its own defaults (ad-hoc / unknown).
|
|
108
|
+
*
|
|
109
|
+
* Labels are additive: --label values concatenate with AILF_LABELS.
|
|
110
|
+
*
|
|
111
|
+
* `executor` is always set on remote submissions because we know the
|
|
112
|
+
* invocation is a user-driven CLI call. Surface defaults to `"cli"`
|
|
113
|
+
* unless AILF_EXECUTOR_SURFACE explicitly overrides; name falls back to
|
|
114
|
+
* GITHUB_ACTOR when available.
|
|
115
|
+
*
|
|
116
|
+
* Returns partial `PipelineRequest` fields only. Omits any key whose
|
|
117
|
+
* source (flag + env) was unset.
|
|
118
|
+
*/
|
|
119
|
+
export declare function buildCallerEnvelope(config: RemoteConfigSlice): Partial<PipelineRequest>;
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
import { existsSync } from "fs";
|
|
16
16
|
import { resolve } from "path";
|
|
17
17
|
import { PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
|
|
18
|
-
import { LEGACY_EVAL_MODE_ALIASES } from "../../_vendor/ailf-shared/index.js";
|
|
19
|
-
import { LiteracyVariant } from "../../pipeline/normalize-mode.js";
|
|
18
|
+
import { LEGACY_EVAL_MODE_ALIASES, isRunClassification, } from "../../_vendor/ailf-shared/index.js";
|
|
20
19
|
import { RepoTaskSource } from "../task-sources/repo-task-source.js";
|
|
21
20
|
const LEGACY_LITERACY_VARIANT_SET = new Set(LEGACY_EVAL_MODE_ALIASES);
|
|
22
21
|
/**
|
|
@@ -27,6 +26,16 @@ const LEGACY_LITERACY_VARIANT_SET = new Set(LEGACY_EVAL_MODE_ALIASES);
|
|
|
27
26
|
function resolveCanonicalTaskMode(configMode) {
|
|
28
27
|
return LEGACY_LITERACY_VARIANT_SET.has(configMode) ? "literacy" : configMode;
|
|
29
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when `buildRemoteRequest` can't find any runnable tasks.
|
|
31
|
+
*
|
|
32
|
+
* The CLI catches this separately from ZodError so it can print the
|
|
33
|
+
* message without an accompanying stack trace — the message is already
|
|
34
|
+
* the whole story for the user.
|
|
35
|
+
*/
|
|
36
|
+
export class NoRunnableTasksError extends Error {
|
|
37
|
+
name = "NoRunnableTasksError";
|
|
38
|
+
}
|
|
30
39
|
// ---------------------------------------------------------------------------
|
|
31
40
|
// Public API
|
|
32
41
|
// ---------------------------------------------------------------------------
|
|
@@ -56,11 +65,13 @@ export async function buildRemoteRequest(options) {
|
|
|
56
65
|
? allTasks.filter((t) => t.mode === taskModeFilter)
|
|
57
66
|
: allTasks;
|
|
58
67
|
if (tasks.length === 0) {
|
|
59
|
-
throw
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
throw await emptyTasksError({
|
|
69
|
+
taskSource,
|
|
70
|
+
tasksDir,
|
|
71
|
+
config,
|
|
72
|
+
filterOptions,
|
|
73
|
+
taskModeFilter,
|
|
74
|
+
});
|
|
64
75
|
}
|
|
65
76
|
// 2. Convert tasks to inline format
|
|
66
77
|
const inlineTasks = tasks.map(taskToInlineFormat);
|
|
@@ -69,10 +80,14 @@ export async function buildRemoteRequest(options) {
|
|
|
69
80
|
taskMode: "inline",
|
|
70
81
|
inlineTasks,
|
|
71
82
|
};
|
|
72
|
-
// Mode
|
|
73
|
-
|
|
83
|
+
// Mode + variant — send both when set so the server sees the caller's
|
|
84
|
+
// canonical intent. Legacy aliases ("full", "baseline", …) are accepted
|
|
85
|
+
// by `PipelineRequestSchema.mode` for back-compat but the CLI now emits
|
|
86
|
+
// the canonical form (`mode: "literacy"` + explicit `variant`).
|
|
87
|
+
if (config.mode)
|
|
74
88
|
raw.mode = config.mode;
|
|
75
|
-
|
|
89
|
+
if (config.variant)
|
|
90
|
+
raw.variant = config.variant;
|
|
76
91
|
// Debug
|
|
77
92
|
if (config.debug?.enabled) {
|
|
78
93
|
raw.debug = config.debug;
|
|
@@ -127,6 +142,10 @@ export async function buildRemoteRequest(options) {
|
|
|
127
142
|
const callerGit = detectCallerGit();
|
|
128
143
|
if (callerGit)
|
|
129
144
|
raw.callerGit = callerGit;
|
|
145
|
+
// D0037 caller envelope — merge CLI flags + env vars and attach each
|
|
146
|
+
// populated field. Flags override env. Skipped fields are omitted so
|
|
147
|
+
// the server applies its own defaults.
|
|
148
|
+
Object.assign(raw, buildCallerEnvelope(config));
|
|
130
149
|
// 4. Validate the assembled request
|
|
131
150
|
const parsed = PipelineRequestSchema.parse(raw);
|
|
132
151
|
return { request: parsed, taskCount: tasks.length };
|
|
@@ -202,6 +221,88 @@ function taskToInlineFormat(task) {
|
|
|
202
221
|
}
|
|
203
222
|
return inline;
|
|
204
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Build a descriptive error when the task list is empty after filtering.
|
|
226
|
+
*
|
|
227
|
+
* Loads the full task list a second time with `includeDrafts: true` so we
|
|
228
|
+
* can distinguish the two common failure modes:
|
|
229
|
+
*
|
|
230
|
+
* 1. Every discovered task is non-active (`status: "draft"` from
|
|
231
|
+
* `ailf init` scaffolding, or `status: "paused"`). Tell the user how
|
|
232
|
+
* to opt a task in.
|
|
233
|
+
* 2. The tasks directory is genuinely empty for this filter combination.
|
|
234
|
+
* Echo the filters back so the mismatch is obvious.
|
|
235
|
+
*
|
|
236
|
+
* The directory-missing and file-missing cases are already surfaced
|
|
237
|
+
* earlier by `RepoTaskSource.loadTasks()`, so we never reach this helper
|
|
238
|
+
* for those.
|
|
239
|
+
*/
|
|
240
|
+
async function emptyTasksError(args) {
|
|
241
|
+
const { taskSource, tasksDir, config, filterOptions, taskModeFilter } = args;
|
|
242
|
+
// Re-load without the status gate to categorize what got filtered.
|
|
243
|
+
let relaxed = [];
|
|
244
|
+
try {
|
|
245
|
+
relaxed = await taskSource.loadTasks({
|
|
246
|
+
...(filterOptions ?? {}),
|
|
247
|
+
includeDrafts: true,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Fall through to the generic message if re-loading fails for any
|
|
252
|
+
// reason (e.g. directory removed mid-run).
|
|
253
|
+
}
|
|
254
|
+
const modeMatched = taskModeFilter
|
|
255
|
+
? relaxed.filter((t) => t.mode === taskModeFilter)
|
|
256
|
+
: relaxed;
|
|
257
|
+
const drafts = modeMatched.filter((t) => (t.status ?? "active") === "draft");
|
|
258
|
+
const paused = modeMatched.filter((t) => t.status === "paused");
|
|
259
|
+
const filtersBlock = (config.areas?.length
|
|
260
|
+
? ` Area filter: ${config.areas.join(", ")}\n`
|
|
261
|
+
: "") +
|
|
262
|
+
(config.tasks?.length
|
|
263
|
+
? ` Task filter: ${config.tasks.join(", ")}\n`
|
|
264
|
+
: "") +
|
|
265
|
+
(config.tags?.length ? ` Tag filter: ${config.tags.join(", ")}\n` : "") +
|
|
266
|
+
(taskModeFilter ? ` Mode filter: ${taskModeFilter}\n` : "");
|
|
267
|
+
if (modeMatched.length === 0) {
|
|
268
|
+
return new NoRunnableTasksError("No tasks matched your filters.\n" +
|
|
269
|
+
` Tasks directory: ${tasksDir}\n` +
|
|
270
|
+
filtersBlock +
|
|
271
|
+
" Check that your .ailf/tasks/ YAML or .task.ts files define tasks\n" +
|
|
272
|
+
" matching these filters.");
|
|
273
|
+
}
|
|
274
|
+
// All matched tasks were excluded by the status gate.
|
|
275
|
+
const draftIds = drafts.map((t) => t.id);
|
|
276
|
+
const pausedIds = paused.map((t) => t.id);
|
|
277
|
+
const draftSample = draftIds.slice(0, 3).join(", ");
|
|
278
|
+
const draftMore = draftIds.length > 3 ? `, +${draftIds.length - 3} more` : "";
|
|
279
|
+
const pausedSample = pausedIds.slice(0, 3).join(", ");
|
|
280
|
+
const pausedMore = pausedIds.length > 3 ? `, +${pausedIds.length - 3} more` : "";
|
|
281
|
+
const lines = [];
|
|
282
|
+
lines.push("No runnable tasks after applying filters.");
|
|
283
|
+
lines.push(` Tasks directory: ${tasksDir}`);
|
|
284
|
+
if (filtersBlock)
|
|
285
|
+
lines.push(filtersBlock.trimEnd());
|
|
286
|
+
if (drafts.length > 0) {
|
|
287
|
+
lines.push(` ${drafts.length} task(s) skipped because status: "draft": ${draftSample}${draftMore}`);
|
|
288
|
+
}
|
|
289
|
+
if (paused.length > 0) {
|
|
290
|
+
lines.push(` ${paused.length} task(s) skipped because status: "paused": ${pausedSample}${pausedMore}`);
|
|
291
|
+
}
|
|
292
|
+
lines.push("");
|
|
293
|
+
lines.push(" To run one of these anyway, either:");
|
|
294
|
+
if (drafts.length > 0) {
|
|
295
|
+
lines.push(` • Change the task's status field from "draft" to "active", or`);
|
|
296
|
+
lines.push(` • Target it explicitly: --task ${drafts[0]?.id ?? "<id>"}`);
|
|
297
|
+
}
|
|
298
|
+
else if (paused.length > 0) {
|
|
299
|
+
lines.push(` • Target it explicitly by id: --task ${paused[0]?.id ?? "<id>"}, or`);
|
|
300
|
+
lines.push(` • Flip its status from "paused" to "active"`);
|
|
301
|
+
}
|
|
302
|
+
lines.push(" Tasks scaffolded by `ailf init` ship as drafts so you can edit");
|
|
303
|
+
lines.push(" them before they start contributing to your literacy score.");
|
|
304
|
+
return new NoRunnableTasksError(lines.join("\n"));
|
|
305
|
+
}
|
|
205
306
|
function buildFilterOptions(config) {
|
|
206
307
|
const areas = config.areas?.length ? config.areas : undefined;
|
|
207
308
|
const taskIds = config.tasks?.length ? config.tasks : undefined;
|
|
@@ -210,6 +311,83 @@ function buildFilterOptions(config) {
|
|
|
210
311
|
return undefined;
|
|
211
312
|
return { areas, taskIds, tags };
|
|
212
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Build the D0037 caller envelope payload from CLI flags + env vars.
|
|
316
|
+
*
|
|
317
|
+
* Precedence, highest first:
|
|
318
|
+
* 1. Explicit CLI flag (--classification, --owner-team, --purpose, …)
|
|
319
|
+
* 2. Env var (AILF_CLASSIFICATION, AILF_OWNER_TEAM, AILF_PURPOSE, …)
|
|
320
|
+
* 3. Omit — server applies its own defaults (ad-hoc / unknown).
|
|
321
|
+
*
|
|
322
|
+
* Labels are additive: --label values concatenate with AILF_LABELS.
|
|
323
|
+
*
|
|
324
|
+
* `executor` is always set on remote submissions because we know the
|
|
325
|
+
* invocation is a user-driven CLI call. Surface defaults to `"cli"`
|
|
326
|
+
* unless AILF_EXECUTOR_SURFACE explicitly overrides; name falls back to
|
|
327
|
+
* GITHUB_ACTOR when available.
|
|
328
|
+
*
|
|
329
|
+
* Returns partial `PipelineRequest` fields only. Omits any key whose
|
|
330
|
+
* source (flag + env) was unset.
|
|
331
|
+
*/
|
|
332
|
+
export function buildCallerEnvelope(config) {
|
|
333
|
+
const out = {};
|
|
334
|
+
// Classification: flag > env. Validated against the closed enum.
|
|
335
|
+
const rawClassification = config.classificationOption ??
|
|
336
|
+
process.env.AILF_CLASSIFICATION?.trim() ??
|
|
337
|
+
undefined;
|
|
338
|
+
if (rawClassification) {
|
|
339
|
+
if (isRunClassification(rawClassification)) {
|
|
340
|
+
out.classification = rawClassification;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Surface the invalid value so downstream Zod validation gives a
|
|
344
|
+
// clear error message pointing at the flag, not the inner enum.
|
|
345
|
+
out.classification =
|
|
346
|
+
rawClassification;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Owner: flag > env. Team required, individual optional.
|
|
350
|
+
const team = config.ownerTeamOption ?? process.env.AILF_OWNER_TEAM?.trim() ?? undefined;
|
|
351
|
+
const individual = config.ownerIndividualOption ??
|
|
352
|
+
process.env.AILF_OWNER_INDIVIDUAL?.trim() ??
|
|
353
|
+
process.env.GITHUB_ACTOR?.trim() ??
|
|
354
|
+
undefined;
|
|
355
|
+
if (team) {
|
|
356
|
+
out.owner = individual ? { team, individual } : { team };
|
|
357
|
+
}
|
|
358
|
+
// Purpose: flag > env.
|
|
359
|
+
const purpose = config.purposeOption ?? process.env.AILF_PURPOSE?.trim() ?? undefined;
|
|
360
|
+
if (purpose)
|
|
361
|
+
out.purpose = purpose;
|
|
362
|
+
// Labels: flag AND env are additive (dedup + trim).
|
|
363
|
+
const flagLabels = config.labelOptions ?? [];
|
|
364
|
+
const envLabels = (process.env.AILF_LABELS ?? "")
|
|
365
|
+
.split(",")
|
|
366
|
+
.map((s) => s.trim())
|
|
367
|
+
.filter(Boolean);
|
|
368
|
+
const mergedLabels = Array.from(new Set([...envLabels, ...flagLabels]));
|
|
369
|
+
if (mergedLabels.length > 0)
|
|
370
|
+
out.labels = mergedLabels;
|
|
371
|
+
// Executor: always set on remote submissions — we know this is a CLI
|
|
372
|
+
// user. Only omit when absolutely nothing identifying is available.
|
|
373
|
+
const surfaceEnv = process.env.AILF_EXECUTOR_SURFACE?.trim();
|
|
374
|
+
const surface = surfaceEnv === "studio" || surfaceEnv === "api" ? surfaceEnv : "cli";
|
|
375
|
+
const githubActor = process.env.GITHUB_ACTOR?.trim() || undefined;
|
|
376
|
+
const nameFromIndividual = config.ownerIndividualOption ??
|
|
377
|
+
process.env.AILF_OWNER_INDIVIDUAL?.trim() ??
|
|
378
|
+
undefined;
|
|
379
|
+
const executorName = githubActor ?? nameFromIndividual;
|
|
380
|
+
const executor = {
|
|
381
|
+
type: "user",
|
|
382
|
+
surface,
|
|
383
|
+
};
|
|
384
|
+
if (executorName)
|
|
385
|
+
executor.name = executorName;
|
|
386
|
+
if (githubActor)
|
|
387
|
+
executor.githubActor = githubActor;
|
|
388
|
+
out.executor = executor;
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
213
391
|
/**
|
|
214
392
|
* Auto-detect caller git metadata from GitHub Actions environment variables.
|
|
215
393
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
|
|
6
6
|
*/
|
|
7
7
|
export { ApiClient } from "./api-client.js";
|
|
8
|
-
export { buildRemoteRequest, resolveTasksDir, type BuildRequestOptions, type RemoteConfigSlice, } from "./build-request.js";
|
|
8
|
+
export { buildCallerEnvelope, buildRemoteRequest, NoRunnableTasksError, resolveTasksDir, type BuildRequestOptions, type RemoteConfigSlice, } from "./build-request.js";
|
|
9
9
|
export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
|
|
10
10
|
export { formatJobError } from "./format-error.js";
|
|
11
11
|
export { createProgressDisplay } from "./progress.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import { ApiClient, buildRemoteRequest, resolveTasksDir } from "./adapters/api-client/index.js"
|
|
6
6
|
*/
|
|
7
7
|
export { ApiClient } from "./api-client.js";
|
|
8
|
-
export { buildRemoteRequest, resolveTasksDir, } from "./build-request.js";
|
|
8
|
+
export { buildCallerEnvelope, buildRemoteRequest, NoRunnableTasksError, resolveTasksDir, } from "./build-request.js";
|
|
9
9
|
export { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
|
|
10
10
|
export { formatJobError } from "./format-error.js";
|
|
11
11
|
export { createProgressDisplay } from "./progress.js";
|
|
@@ -727,6 +727,11 @@ async function buildPipelineExplainPlan(actionCommand, rootDir) {
|
|
|
727
727
|
artifactsDir: raw.artifactsDir,
|
|
728
728
|
artifactsDryRun: raw.artifactsDryRun ?? false,
|
|
729
729
|
artifactsExclude: raw.artifactsExclude,
|
|
730
|
+
classification: raw.classification,
|
|
731
|
+
ownerTeam: raw.ownerTeam,
|
|
732
|
+
ownerIndividual: raw.ownerIndividual,
|
|
733
|
+
purpose: raw.purpose,
|
|
734
|
+
label: raw.label ?? [],
|
|
730
735
|
};
|
|
731
736
|
const resolved = computeResolvedOptions(withDefaults);
|
|
732
737
|
const planOpts = {
|
|
@@ -68,6 +68,12 @@ export interface ResolvedOptions {
|
|
|
68
68
|
artifactsDir?: string;
|
|
69
69
|
artifactsDryRun: boolean;
|
|
70
70
|
artifactsExclude?: readonly string[];
|
|
71
|
+
/** D0037 / W0069 caller envelope — surfaces only on --remote today. */
|
|
72
|
+
classificationOption?: string;
|
|
73
|
+
ownerTeamOption?: string;
|
|
74
|
+
ownerIndividualOption?: string;
|
|
75
|
+
purposeOption?: string;
|
|
76
|
+
labelOptions: string[];
|
|
71
77
|
}
|
|
72
78
|
/**
|
|
73
79
|
* Pure option resolution — computes ResolvedOptions from CLI flags without
|
|
@@ -14,7 +14,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
14
14
|
import { dirname, resolve } from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import { classifyUrls } from "../pipeline/classify-url.js";
|
|
17
|
-
import { normalizeMode } from "../pipeline/normalize-mode.js";
|
|
17
|
+
import { LiteracyVariant, normalizeMode } from "../pipeline/normalize-mode.js";
|
|
18
18
|
import { assessImpact, buildReverseMapping, } from "../pipeline/reverse-mapping.js";
|
|
19
19
|
import { buildAppContext, parseArtifactUploadEnv, } from "../orchestration/build-app-context.js";
|
|
20
20
|
import { buildStepSequence } from "../orchestration/build-step-sequence.js";
|
|
@@ -47,6 +47,13 @@ export function computeResolvedOptions(opts) {
|
|
|
47
47
|
mode = normalized.mode;
|
|
48
48
|
// Explicit --variant flag takes precedence over what normalizeMode inferred
|
|
49
49
|
variant = opts.variant ?? normalized.variant;
|
|
50
|
+
// Canonical mode "literacy" with no variant defaults to the full variant
|
|
51
|
+
// (standard + agentic). This preserves the pre-canonical CLI behavior
|
|
52
|
+
// where `--mode full` was the default, without emitting the legacy alias
|
|
53
|
+
// deprecation warning for users who pass no flags at all.
|
|
54
|
+
if (mode === "literacy" && !variant) {
|
|
55
|
+
variant = LiteracyVariant.FULL;
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
catch (err) {
|
|
52
59
|
console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -269,6 +276,11 @@ export function computeResolvedOptions(opts) {
|
|
|
269
276
|
artifactsDir: resolveArtifactsDir(opts),
|
|
270
277
|
artifactsDryRun: opts.artifactsDryRun,
|
|
271
278
|
artifactsExclude: parseArtifactsExcludeList(opts.artifactsExclude),
|
|
279
|
+
classificationOption: opts.classification?.trim() || undefined,
|
|
280
|
+
ownerTeamOption: opts.ownerTeam?.trim() || undefined,
|
|
281
|
+
ownerIndividualOption: opts.ownerIndividual?.trim() || undefined,
|
|
282
|
+
purposeOption: opts.purpose?.trim() || undefined,
|
|
283
|
+
labelOptions: opts.label ?? [],
|
|
272
284
|
};
|
|
273
285
|
}
|
|
274
286
|
/**
|
|
@@ -68,5 +68,10 @@ export interface PipelineCliOptions {
|
|
|
68
68
|
artifactsDir?: string;
|
|
69
69
|
artifactsDryRun: boolean;
|
|
70
70
|
artifactsExclude?: string;
|
|
71
|
+
classification?: string;
|
|
72
|
+
ownerTeam?: string;
|
|
73
|
+
ownerIndividual?: string;
|
|
74
|
+
purpose?: string;
|
|
75
|
+
label: string[];
|
|
71
76
|
}
|
|
72
77
|
export declare function createPipelineCommand(): Command;
|