@sanity/ailf 2.5.0 → 2.7.0
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/ports/context.d.ts +18 -1
- package/dist/cli.js +2 -0
- package/dist/commands/check-staleness.d.ts +14 -0
- package/dist/commands/check-staleness.js +74 -0
- package/dist/commands/pipeline-action.js +2 -1
- package/dist/composition-root.d.ts +9 -2
- package/dist/composition-root.js +28 -15
- package/dist/orchestration/build-app-context.d.ts +8 -0
- package/dist/orchestration/build-app-context.js +18 -0
- package/package.json +1 -1
|
@@ -164,8 +164,25 @@ export interface ResolvedConfig {
|
|
|
164
164
|
captureGcsBucket?: string;
|
|
165
165
|
/** GCS object prefix for capture uploads (default: "captures/") */
|
|
166
166
|
captureGcsPrefix?: string;
|
|
167
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* GCS bucket for report artifact uploads. Defaults to "ailf-artifacts"
|
|
169
|
+
* at the composition root — only set this to override (e.g., self-hosted
|
|
170
|
+
* deployment with a different bucket). Read access is governed by the
|
|
171
|
+
* gateway's signing credentials, so alternate bucket names require
|
|
172
|
+
* reconfiguring the gateway as well (D0030).
|
|
173
|
+
*/
|
|
168
174
|
artifactGcsBucket?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Controls whether the ArtifactUploader is constructed.
|
|
177
|
+
*
|
|
178
|
+
* - `undefined` (default): auto — construct when credentials are available
|
|
179
|
+
* (ADC for direct GCS, or AILF_API_KEY for gateway-signed URLs).
|
|
180
|
+
* - `true`: force-enable — still a no-op if no credentials are present (P5).
|
|
181
|
+
* - `false`: force-disable — skip artifact upload even when credentials exist.
|
|
182
|
+
*
|
|
183
|
+
* Sourced from AILF_ARTIFACT_UPLOAD env var or `artifactUpload` in ailf.config.ts.
|
|
184
|
+
*/
|
|
185
|
+
artifactUpload?: boolean;
|
|
169
186
|
}
|
|
170
187
|
/**
|
|
171
188
|
* Application context — the complete dependency carrier.
|
package/dist/cli.js
CHANGED
|
@@ -147,6 +147,8 @@ import { createAgentReportCommand } from "./commands/agent-report.js";
|
|
|
147
147
|
program.addCommand(createAgentReportCommand().helpGroup(CommandGroup.AnalysisReports));
|
|
148
148
|
import { createWeeklyDigestCommand } from "./commands/weekly-digest.js";
|
|
149
149
|
program.addCommand(createWeeklyDigestCommand().helpGroup(CommandGroup.AnalysisReports));
|
|
150
|
+
import { createCheckStalenessCommand } from "./commands/check-staleness.js";
|
|
151
|
+
program.addCommand(createCheckStalenessCommand().helpGroup(CommandGroup.AnalysisReports));
|
|
150
152
|
// ── Grader Reliability ────────────────────────────────────────────────
|
|
151
153
|
import { createGraderCommand } from "./commands/grader/index.js";
|
|
152
154
|
program.addCommand(createGraderCommand().helpGroup(CommandGroup.GraderReliability));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-staleness command — verifies recent evaluation reports exist.
|
|
3
|
+
*
|
|
4
|
+
* Exits 0 when the most recent report in the Sanity Content Lake is within
|
|
5
|
+
* the max-age window, 1 otherwise (including "no reports at all"). Emits a
|
|
6
|
+
* single JSON line on stdout summarizing the decision so CI can pipe it
|
|
7
|
+
* directly into an alert payload.
|
|
8
|
+
*
|
|
9
|
+
* Used by the scheduled staleness workflow to detect silent pipeline
|
|
10
|
+
* failures — cases where scheduled evaluations stop producing reports but
|
|
11
|
+
* no workflow run fails loudly enough to be noticed.
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
export declare function createCheckStalenessCommand(): Command;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-staleness command — verifies recent evaluation reports exist.
|
|
3
|
+
*
|
|
4
|
+
* Exits 0 when the most recent report in the Sanity Content Lake is within
|
|
5
|
+
* the max-age window, 1 otherwise (including "no reports at all"). Emits a
|
|
6
|
+
* single JSON line on stdout summarizing the decision so CI can pipe it
|
|
7
|
+
* directly into an alert payload.
|
|
8
|
+
*
|
|
9
|
+
* Used by the scheduled staleness workflow to detect silent pipeline
|
|
10
|
+
* failures — cases where scheduled evaluations stop producing reports but
|
|
11
|
+
* no workflow run fails loudly enough to be noticed.
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
export function createCheckStalenessCommand() {
|
|
15
|
+
return new Command("check-staleness")
|
|
16
|
+
.description("Exit 1 if no evaluation report has been produced within the max-age window")
|
|
17
|
+
.option("--max-age <days>", "Max age in days before reports are considered stale", (v) => Number.parseInt(v, 10), 3)
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const { getSanityClient } = await import("../sanity/client.js");
|
|
20
|
+
// Resolve report-store credentials with the same precedence as
|
|
21
|
+
// weekly-digest.ts and composition-root.ts — AILF_REPORT_* wins over
|
|
22
|
+
// the evaluated-source SANITY_* defaults so the staleness probe tracks
|
|
23
|
+
// the actual report dataset even when it diverges from the eval source.
|
|
24
|
+
const client = getSanityClient({
|
|
25
|
+
dataset: process.env.AILF_REPORT_DATASET,
|
|
26
|
+
projectId: process.env.AILF_REPORT_PROJECT_ID,
|
|
27
|
+
token: process.env.AILF_REPORT_SANITY_API_TOKEN ??
|
|
28
|
+
process.env.SANITY_API_TOKEN,
|
|
29
|
+
});
|
|
30
|
+
const maxAgeDays = opts.maxAge;
|
|
31
|
+
// Bound the GROQ sort with a `completedAt > $floor` filter. Beyond
|
|
32
|
+
// ~10,000 reports the unbounded `order(completedAt desc)[0]` scan
|
|
33
|
+
// becomes a noticeable cost; a floor proportional to the max-age
|
|
34
|
+
// window keeps the scan cheap regardless of corpus size. The factor
|
|
35
|
+
// of 10× max-age gives plenty of headroom — if the last report
|
|
36
|
+
// predates the floor, the absence of any result still yields the
|
|
37
|
+
// correct "stale" verdict.
|
|
38
|
+
const floorDays = Math.max(maxAgeDays * 10, 30);
|
|
39
|
+
const floor = new Date(Date.now() - floorDays * 24 * 60 * 60 * 1000).toISOString();
|
|
40
|
+
const QUERY = `*[_type == "ailf.report" && completedAt > $floor] | order(completedAt desc)[0]{
|
|
41
|
+
"reportId": reportId,
|
|
42
|
+
"completedAt": completedAt,
|
|
43
|
+
"tag": tag
|
|
44
|
+
}`;
|
|
45
|
+
const latest = await client.fetch(QUERY, { floor });
|
|
46
|
+
// Use `process.exitCode` + `return` rather than `process.exit()` so
|
|
47
|
+
// stdout flushes cleanly when the caller captures via `$(...)` — a
|
|
48
|
+
// hard exit can drop buffered output on piped captures. Matches the
|
|
49
|
+
// pattern used by agent-report.ts, capture-list.ts, etc.
|
|
50
|
+
if (!latest || !latest.completedAt) {
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
floorDays,
|
|
53
|
+
maxAgeDays,
|
|
54
|
+
reason: "no-reports",
|
|
55
|
+
stale: true,
|
|
56
|
+
}));
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const ageMs = Date.now() - new Date(latest.completedAt).getTime();
|
|
61
|
+
const ageDays = Number((ageMs / (24 * 60 * 60 * 1000)).toFixed(2));
|
|
62
|
+
const stale = ageDays > maxAgeDays;
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
ageDays,
|
|
65
|
+
floorDays,
|
|
66
|
+
latestCompletedAt: latest.completedAt,
|
|
67
|
+
latestReportId: latest.reportId,
|
|
68
|
+
latestTag: latest.tag,
|
|
69
|
+
maxAgeDays,
|
|
70
|
+
stale,
|
|
71
|
+
}));
|
|
72
|
+
process.exitCode = stale ? 1 : 0;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "url";
|
|
|
16
16
|
import { classifyUrls } from "../pipeline/classify-url.js";
|
|
17
17
|
import { normalizeMode } from "../pipeline/normalize-mode.js";
|
|
18
18
|
import { assessImpact, buildReverseMapping, } from "../pipeline/reverse-mapping.js";
|
|
19
|
-
import { buildAppContext } from "../orchestration/build-app-context.js";
|
|
19
|
+
import { buildAppContext, parseArtifactUploadEnv, } from "../orchestration/build-app-context.js";
|
|
20
20
|
import { buildStepSequence } from "../orchestration/build-step-sequence.js";
|
|
21
21
|
import { orchestratePipeline } from "../orchestration/pipeline-orchestrator.js";
|
|
22
22
|
import { load } from "js-yaml";
|
|
@@ -329,6 +329,7 @@ export async function executePipeline(cliOpts) {
|
|
|
329
329
|
config.captureGcsBucket ??= process.env.AILF_CAPTURE_GCS_BUCKET;
|
|
330
330
|
config.captureGcsPrefix ??= process.env.AILF_CAPTURE_GCS_PREFIX;
|
|
331
331
|
config.artifactGcsBucket ??= process.env.AILF_GCS_ARTIFACT_BUCKET;
|
|
332
|
+
config.artifactUpload ??= parseArtifactUploadEnv(process.env.AILF_ARTIFACT_UPLOAD);
|
|
332
333
|
// Create AppContext directly from the merged config so adapters
|
|
333
334
|
// (especially taskSource) are wired from the file config's
|
|
334
335
|
// taskSourceType — not from CLI defaults.
|
|
@@ -26,8 +26,15 @@ export declare function createAppContext(config: ResolvedConfig): AppContext;
|
|
|
26
26
|
/**
|
|
27
27
|
* Selects an ArtifactUploader implementation based on available credentials.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Selection order:
|
|
30
|
+
* 1. config.artifactUpload === false → always skip (explicit opt-out)
|
|
31
|
+
* 2. GOOGLE_APPLICATION_CREDENTIALS or GCLOUD_PROJECT present → direct GCS
|
|
32
|
+
* 3. apiKey + apiUrl present → gateway-signed PUT URL
|
|
33
|
+
* 4. Neither → skip silently (P5)
|
|
34
|
+
*
|
|
35
|
+
* The bucket defaults to DEFAULT_ARTIFACT_BUCKET when not explicitly set —
|
|
36
|
+
* users only need to override for self-hosted deployments with a different
|
|
37
|
+
* bucket (and matching gateway signing credentials).
|
|
31
38
|
*
|
|
32
39
|
* Exported for unit-test access; not part of the public package API.
|
|
33
40
|
*/
|
package/dist/composition-root.js
CHANGED
|
@@ -83,13 +83,9 @@ export function createAppContext(config) {
|
|
|
83
83
|
: fsCollector;
|
|
84
84
|
}
|
|
85
85
|
// Report artifact uploader — uploads structured files to GCS at known
|
|
86
|
-
// paths for Studio to fetch via signed URLs (D0030).
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// 1. Direct GCS upload when ADC env vars are present (CI / GCP runtime)
|
|
90
|
-
// 2. API Gateway signed-URL upload when only an AILF API key is present
|
|
91
|
-
// (local dev — no GCS credentials needed)
|
|
92
|
-
// 3. Skipped silently when neither is configured
|
|
86
|
+
// paths for Studio to fetch via signed URLs (D0030). Auto-detects the
|
|
87
|
+
// right adapter from available credentials; defaults bucket to
|
|
88
|
+
// "ailf-artifacts". Set artifactUpload: false to opt out entirely.
|
|
93
89
|
const artifactUploader = createArtifactUploader(config, logger);
|
|
94
90
|
return {
|
|
95
91
|
artifactUploader,
|
|
@@ -120,34 +116,51 @@ function createLogger() {
|
|
|
120
116
|
process.env.AILF_VERBOSE === "1",
|
|
121
117
|
});
|
|
122
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Shared GCS bucket for report artifacts. Matches the gateway default at
|
|
121
|
+
* packages/api/src/routes/artifacts.ts — both sides assume ailf-artifacts
|
|
122
|
+
* unless explicitly overridden. The gateway's signing credentials are scoped
|
|
123
|
+
* to this bucket, so alternate names require reconfiguring the gateway.
|
|
124
|
+
*/
|
|
125
|
+
const DEFAULT_ARTIFACT_BUCKET = "ailf-artifacts";
|
|
123
126
|
/**
|
|
124
127
|
* Selects an ArtifactUploader implementation based on available credentials.
|
|
125
128
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
129
|
+
* Selection order:
|
|
130
|
+
* 1. config.artifactUpload === false → always skip (explicit opt-out)
|
|
131
|
+
* 2. GOOGLE_APPLICATION_CREDENTIALS or GCLOUD_PROJECT present → direct GCS
|
|
132
|
+
* 3. apiKey + apiUrl present → gateway-signed PUT URL
|
|
133
|
+
* 4. Neither → skip silently (P5)
|
|
134
|
+
*
|
|
135
|
+
* The bucket defaults to DEFAULT_ARTIFACT_BUCKET when not explicitly set —
|
|
136
|
+
* users only need to override for self-hosted deployments with a different
|
|
137
|
+
* bucket (and matching gateway signing credentials).
|
|
128
138
|
*
|
|
129
139
|
* Exported for unit-test access; not part of the public package API.
|
|
130
140
|
*/
|
|
131
141
|
export function createArtifactUploader(config, logger) {
|
|
132
|
-
if (
|
|
142
|
+
if (config.artifactUpload === false) {
|
|
143
|
+
logger.debug("Artifact upload explicitly disabled via artifactUpload=false");
|
|
133
144
|
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const bucket = config.artifactGcsBucket ?? DEFAULT_ARTIFACT_BUCKET;
|
|
134
147
|
// CI / GCP runtime — direct GCS upload (fastest, no extra hop).
|
|
135
148
|
// We treat the presence of either env var as the user opting in to ADC.
|
|
136
149
|
const hasGcsCredentials = Boolean(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GCLOUD_PROJECT);
|
|
137
150
|
if (hasGcsCredentials) {
|
|
138
|
-
logger.debug(
|
|
139
|
-
return new GcsReportArtifactUploader({ bucket
|
|
151
|
+
logger.debug(`Artifact uploader: GcsReportArtifactUploader (direct GCS via ADC, bucket=${bucket})`);
|
|
152
|
+
return new GcsReportArtifactUploader({ bucket });
|
|
140
153
|
}
|
|
141
154
|
// Local dev — request signed PUT URLs from the API gateway, no GCS creds needed.
|
|
142
155
|
if (config.apiKey && config.apiUrl) {
|
|
143
|
-
logger.debug(`Artifact uploader: ApiGatewayArtifactUploader (signed URL via ${config.apiUrl})`);
|
|
156
|
+
logger.debug(`Artifact uploader: ApiGatewayArtifactUploader (signed URL via ${config.apiUrl}, bucket=${bucket})`);
|
|
144
157
|
return new ApiGatewayArtifactUploader({
|
|
145
158
|
apiBaseUrl: config.apiUrl,
|
|
146
159
|
apiKey: config.apiKey,
|
|
147
|
-
bucket
|
|
160
|
+
bucket,
|
|
148
161
|
});
|
|
149
162
|
}
|
|
150
|
-
logger.debug("Artifact upload skipped:
|
|
163
|
+
logger.debug("Artifact upload skipped: no GCS credentials or AILF_API_KEY available");
|
|
151
164
|
return undefined;
|
|
152
165
|
}
|
|
153
166
|
function createCache(config) {
|
|
@@ -18,6 +18,14 @@ import type { ResolvedOptions } from "../commands/pipeline-action.js";
|
|
|
18
18
|
* are derived (e.g., areas from areaOption).
|
|
19
19
|
*/
|
|
20
20
|
export declare function mapToResolvedConfig(opts: ResolvedOptions, rootDir: string): ResolvedConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Parse the AILF_ARTIFACT_UPLOAD env var into a tri-state.
|
|
23
|
+
*
|
|
24
|
+
* - "0" | "false" → false (force-disable)
|
|
25
|
+
* - "1" | "true" → true (force-enable; still a no-op without credentials)
|
|
26
|
+
* - unset | other → undefined (auto-detect from credentials)
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseArtifactUploadEnv(value: string | undefined): boolean | undefined;
|
|
21
29
|
/**
|
|
22
30
|
* Build an AppContext from legacy ResolvedOptions.
|
|
23
31
|
*
|
|
@@ -85,8 +85,26 @@ export function mapToResolvedConfig(opts, rootDir) {
|
|
|
85
85
|
captureGcsBucket: process.env.AILF_CAPTURE_GCS_BUCKET,
|
|
86
86
|
captureGcsPrefix: process.env.AILF_CAPTURE_GCS_PREFIX,
|
|
87
87
|
artifactGcsBucket: process.env.AILF_GCS_ARTIFACT_BUCKET,
|
|
88
|
+
artifactUpload: parseArtifactUploadEnv(process.env.AILF_ARTIFACT_UPLOAD),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse the AILF_ARTIFACT_UPLOAD env var into a tri-state.
|
|
93
|
+
*
|
|
94
|
+
* - "0" | "false" → false (force-disable)
|
|
95
|
+
* - "1" | "true" → true (force-enable; still a no-op without credentials)
|
|
96
|
+
* - unset | other → undefined (auto-detect from credentials)
|
|
97
|
+
*/
|
|
98
|
+
export function parseArtifactUploadEnv(value) {
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
const normalized = value.trim().toLowerCase();
|
|
102
|
+
if (normalized === "0" || normalized === "false")
|
|
103
|
+
return false;
|
|
104
|
+
if (normalized === "1" || normalized === "true")
|
|
105
|
+
return true;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
90
108
|
/**
|
|
91
109
|
* Build an AppContext from legacy ResolvedOptions.
|
|
92
110
|
*
|