@sanity/ailf 0.1.34 → 0.3.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/LICENSE +21 -0
- package/config/airbyte/ai_literacy_framework.connector.yaml +6 -0
- package/config/bigquery/views/reports.sql +1 -0
- package/dist/_vendor/ailf-core/examples/index.d.ts +10 -20
- package/dist/_vendor/ailf-core/examples/index.js +10 -20
- package/dist/_vendor/ailf-core/ports/task-source.d.ts +2 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +65 -0
- package/dist/_vendor/ailf-tasks/schemas.d.ts +12 -0
- package/dist/_vendor/ailf-tasks/schemas.js +4 -0
- package/dist/adapters/task-sources/content-lake-task-source.js +9 -1
- package/dist/adapters/task-sources/repo-task-source.js +19 -4
- package/dist/commands/calculate-scores.js +5 -1
- package/dist/commands/publish.js +3 -0
- package/dist/composition-root.js +7 -2
- package/dist/orchestration/pipeline-orchestrator.js +27 -2
- package/dist/orchestration/step-runner.js +8 -0
- package/dist/orchestration/steps/calculate-scores-step.js +22 -19
- package/dist/orchestration/steps/generate-configs-step.js +1 -0
- package/dist/orchestration/steps/grader-consistency-step.js +1 -0
- package/dist/orchestration/steps/mirror-repo-tasks-step.js +2 -1
- package/dist/orchestration/steps/publish-report-step.js +3 -0
- package/dist/pipeline/calculate-scores.d.ts +11 -1
- package/dist/pipeline/calculate-scores.js +222 -157
- package/dist/pipeline/coverage-audit.d.ts +2 -1
- package/dist/pipeline/coverage-audit.js +5 -3
- package/dist/pipeline/expand-tasks.d.ts +2 -1
- package/dist/pipeline/expand-tasks.js +33 -2
- package/dist/pipeline/generate-configs.d.ts +3 -1
- package/dist/pipeline/generate-configs.js +51 -37
- package/dist/pipeline/grader-api.d.ts +2 -1
- package/dist/pipeline/grader-api.js +11 -9
- package/dist/pipeline/grader-compare-runner.d.ts +3 -0
- package/dist/pipeline/grader-compare-runner.js +21 -19
- package/dist/pipeline/grader-consistency-runner.d.ts +3 -0
- package/dist/pipeline/grader-consistency-runner.js +16 -14
- package/dist/pipeline/grader-sensitivity-runner.d.ts +3 -0
- package/dist/pipeline/grader-sensitivity-runner.js +18 -16
- package/dist/pipeline/grader-validate-runner.d.ts +3 -0
- package/dist/pipeline/grader-validate-runner.js +16 -14
- package/dist/pipeline/mirror-repo-tasks.d.ts +80 -1
- package/dist/pipeline/mirror-repo-tasks.js +148 -32
- package/dist/pipeline/provenance.d.ts +3 -0
- package/dist/pipeline/provenance.js +25 -3
- package/dist/pipeline/report-title.d.ts +66 -0
- package/dist/pipeline/report-title.js +118 -0
- package/dist/report-store.js +2 -0
- package/dist/sinks/bigquery/index.d.ts +1 -0
- package/dist/sinks/bigquery/index.js +1 -0
- package/dist/sources.d.ts +2 -1
- package/dist/sources.js +28 -1
- package/package.json +23 -23
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* @see docs/exec-plans/tasks-as-content/phase-5-content-lake-mirroring.md
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from "crypto";
|
|
16
|
-
import {
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { isIdRef, isPathRef, isPerspectiveRef, isSlugRef, } from "../_vendor/ailf-core/index.js";
|
|
18
|
+
import { ConsoleLogger } from "../adapters/loggers/index.js";
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
// Public API
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
@@ -29,7 +31,8 @@ import { isSlugRef, } from "../_vendor/ailf-core/index.js";
|
|
|
29
31
|
* 6. Upsert the ailf.task document with origin block
|
|
30
32
|
*/
|
|
31
33
|
export async function mirrorRepoTasks(options) {
|
|
32
|
-
const { client, tasks, git, dryRun = false } = options;
|
|
34
|
+
const { client, tasks, git, dryRun = false, logger } = options;
|
|
35
|
+
const log = logger ?? new ConsoleLogger();
|
|
33
36
|
const result = {
|
|
34
37
|
total: tasks.length,
|
|
35
38
|
upserted: 0,
|
|
@@ -54,29 +57,37 @@ export async function mirrorRepoTasks(options) {
|
|
|
54
57
|
}
|
|
55
58
|
// Ensure all feature areas exist
|
|
56
59
|
const areas = [...new Set(tasks.map((t) => t.featureArea))];
|
|
57
|
-
const createdAreas = await ensureFeatureAreas(client, areas, dryRun);
|
|
60
|
+
const createdAreas = await ensureFeatureAreas(client, areas, dryRun, log);
|
|
58
61
|
result.areasCreated = createdAreas;
|
|
59
|
-
// Fetch existing mirror document
|
|
62
|
+
// Fetch existing mirror document state for change detection + ownership check
|
|
60
63
|
const mirrorIds = tasks.map((t) => mirrorDocId(git.owner, git.name, t.id));
|
|
61
|
-
const
|
|
64
|
+
const existingDocState = await fetchExistingDocState(client, mirrorIds);
|
|
62
65
|
// Mirror each task
|
|
63
66
|
for (const task of tasks) {
|
|
64
67
|
try {
|
|
65
68
|
const docId = mirrorDocId(git.owner, git.name, task.id);
|
|
69
|
+
const existing = existingDocState.get(docId);
|
|
70
|
+
// Skip graduated tasks — ownership was changed to "studio"
|
|
71
|
+
if (existing?.ownership === "studio") {
|
|
72
|
+
log.info(` ℹ️ Skipping "${task.id}" — graduated to Studio ownership`);
|
|
73
|
+
result.skipped++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
66
76
|
const contentHash = computeTaskHash(task);
|
|
67
77
|
// Skip unchanged
|
|
68
|
-
if (
|
|
78
|
+
if (existing?.hash === contentHash) {
|
|
69
79
|
result.skipped++;
|
|
70
80
|
continue;
|
|
71
81
|
}
|
|
72
82
|
const doc = buildMirrorDocument(task, {
|
|
73
83
|
contentHash,
|
|
74
84
|
docId,
|
|
85
|
+
existingAuthor: existing?.existingAuthor,
|
|
75
86
|
git,
|
|
76
87
|
slugToDocId,
|
|
77
88
|
});
|
|
78
89
|
if (dryRun) {
|
|
79
|
-
|
|
90
|
+
log.info(` [dry-run] Would upsert: ${docId}`);
|
|
80
91
|
result.upserted++;
|
|
81
92
|
continue;
|
|
82
93
|
}
|
|
@@ -106,12 +117,15 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
106
117
|
if (ghRepo) {
|
|
107
118
|
const [owner, name] = ghRepo.split("/");
|
|
108
119
|
const branch = ghHeadRef || ghRef.replace("refs/heads/", "").replace("refs/tags/", "");
|
|
120
|
+
// Extract author from GitHub Actions environment
|
|
121
|
+
const author = detectGitHubActionsAuthor();
|
|
109
122
|
return {
|
|
110
123
|
repo: ghRepo,
|
|
111
124
|
owner: owner ?? "unknown",
|
|
112
125
|
name: name ?? "unknown",
|
|
113
126
|
branch: branch || "unknown",
|
|
114
127
|
commitSha: ghSha || "unknown",
|
|
128
|
+
author,
|
|
115
129
|
};
|
|
116
130
|
}
|
|
117
131
|
// Fallback: try git CLI
|
|
@@ -134,12 +148,15 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
134
148
|
remote.match(/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
135
149
|
const owner = match?.[1] ?? "unknown";
|
|
136
150
|
const name = match?.[2] ?? "unknown";
|
|
151
|
+
// Extract author from git log
|
|
152
|
+
const author = detectGitCliAuthor(repoTasksPath, execSync);
|
|
137
153
|
return {
|
|
138
154
|
repo: `${owner}/${name}`,
|
|
139
155
|
owner,
|
|
140
156
|
name,
|
|
141
157
|
branch,
|
|
142
158
|
commitSha,
|
|
159
|
+
author,
|
|
143
160
|
};
|
|
144
161
|
}
|
|
145
162
|
catch {
|
|
@@ -149,10 +166,63 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
149
166
|
name: "unknown",
|
|
150
167
|
branch: "unknown",
|
|
151
168
|
commitSha: "unknown",
|
|
169
|
+
author: {},
|
|
152
170
|
};
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
// ---------------------------------------------------------------------------
|
|
174
|
+
// Author detection helpers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
/**
|
|
177
|
+
* Extract author info from GitHub Actions environment variables and
|
|
178
|
+
* the webhook event payload (GITHUB_EVENT_PATH).
|
|
179
|
+
*/
|
|
180
|
+
function detectGitHubActionsAuthor() {
|
|
181
|
+
const ghActor = process.env.GITHUB_ACTOR ?? undefined;
|
|
182
|
+
const author = { githubUsername: ghActor };
|
|
183
|
+
// Try to read richer author info from the event payload
|
|
184
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
185
|
+
if (eventPath) {
|
|
186
|
+
try {
|
|
187
|
+
const event = JSON.parse(readFileSync(eventPath, "utf-8"));
|
|
188
|
+
// Push event: head_commit.author has name + email
|
|
189
|
+
if (event.head_commit?.author) {
|
|
190
|
+
author.gitName = event.head_commit.author.name ?? undefined;
|
|
191
|
+
author.gitEmail = event.head_commit.author.email ?? undefined;
|
|
192
|
+
}
|
|
193
|
+
// PR event: pull_request.user.login is the PR author
|
|
194
|
+
if (event.pull_request?.user?.login) {
|
|
195
|
+
author.githubUsername = event.pull_request.user.login;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Event payload parsing is best-effort — fall through with GITHUB_ACTOR only
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return author;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract author info from git CLI (local fallback when not in GitHub Actions).
|
|
206
|
+
*/
|
|
207
|
+
function detectGitCliAuthor(cwd, execSyncFn) {
|
|
208
|
+
try {
|
|
209
|
+
const authorLine = execSyncFn('git log -1 --format="%an|%ae"', {
|
|
210
|
+
encoding: "utf-8",
|
|
211
|
+
cwd,
|
|
212
|
+
})
|
|
213
|
+
.toString()
|
|
214
|
+
.trim();
|
|
215
|
+
const [gitName, gitEmail] = authorLine.split("|");
|
|
216
|
+
return {
|
|
217
|
+
gitName: gitName || undefined,
|
|
218
|
+
gitEmail: gitEmail || undefined,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
156
226
|
// Document ID scheme
|
|
157
227
|
// ---------------------------------------------------------------------------
|
|
158
228
|
/**
|
|
@@ -219,7 +289,7 @@ async function batchResolveDocSlugs(client, slugs) {
|
|
|
219
289
|
* Ensure ailf.featureArea documents exist for all referenced areas.
|
|
220
290
|
* Returns the list of newly created area IDs.
|
|
221
291
|
*/
|
|
222
|
-
async function ensureFeatureAreas(client, areas, dryRun) {
|
|
292
|
+
async function ensureFeatureAreas(client, areas, dryRun, log) {
|
|
223
293
|
if (areas.length === 0)
|
|
224
294
|
return [];
|
|
225
295
|
// Check which areas already exist
|
|
@@ -230,7 +300,7 @@ async function ensureFeatureAreas(client, areas, dryRun) {
|
|
|
230
300
|
return [];
|
|
231
301
|
if (dryRun) {
|
|
232
302
|
for (const area of missing) {
|
|
233
|
-
|
|
303
|
+
log.info(` [dry-run] Would create feature area: ${area}`);
|
|
234
304
|
}
|
|
235
305
|
return missing;
|
|
236
306
|
}
|
|
@@ -247,42 +317,82 @@ async function ensureFeatureAreas(client, areas, dryRun) {
|
|
|
247
317
|
await transaction.commit();
|
|
248
318
|
return missing;
|
|
249
319
|
}
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
// Fetch existing content hashes
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
320
|
/**
|
|
254
|
-
* Fetch existing mirror documents' content hashes
|
|
255
|
-
*
|
|
321
|
+
* Fetch existing mirror documents' ownership and content hashes.
|
|
322
|
+
*
|
|
323
|
+
* The ownership field determines whether the mirror step is allowed to
|
|
324
|
+
* update the document:
|
|
325
|
+
* - `"repo"` or absent → safe to update (active mirror)
|
|
326
|
+
* - `"studio"` → skip (graduated to Studio ownership)
|
|
327
|
+
*
|
|
328
|
+
* The hash is stored in origin.contentHash for change detection.
|
|
256
329
|
*/
|
|
257
|
-
async function
|
|
330
|
+
async function fetchExistingDocState(client, docIds) {
|
|
258
331
|
if (docIds.length === 0)
|
|
259
332
|
return new Map();
|
|
260
|
-
const query = `*[_id in $ids] {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
333
|
+
const query = `*[_id in $ids] {
|
|
334
|
+
_id,
|
|
335
|
+
"hash": origin.contentHash,
|
|
336
|
+
ownership,
|
|
337
|
+
"existingAuthor": origin.author
|
|
338
|
+
}`;
|
|
339
|
+
const results = await client.fetch(query, { ids: docIds });
|
|
264
340
|
const map = new Map();
|
|
265
341
|
for (const r of results) {
|
|
266
|
-
|
|
267
|
-
|
|
342
|
+
map.set(r._id, {
|
|
343
|
+
hash: r.hash ?? undefined,
|
|
344
|
+
ownership: r.ownership ?? undefined,
|
|
345
|
+
existingAuthor: r.existingAuthor ?? undefined,
|
|
346
|
+
});
|
|
268
347
|
}
|
|
269
348
|
return map;
|
|
270
349
|
}
|
|
271
350
|
// ---------------------------------------------------------------------------
|
|
272
351
|
// Build mirror document
|
|
273
352
|
// ---------------------------------------------------------------------------
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
//
|
|
353
|
+
/** @internal Exported for testing — not part of the public API. */
|
|
354
|
+
export function buildMirrorDocument(task, opts) {
|
|
355
|
+
const { contentHash, docId, existingAuthor, git, slugToDocId } = opts;
|
|
356
|
+
// Build canonical docs with resolved references and correct refType.
|
|
357
|
+
// Each ref type gets the appropriate resolution fields set on the
|
|
358
|
+
// mirror document so Studio can display them correctly.
|
|
279
359
|
const canonicalDocs = task.canonicalDocs.map((ref, i) => {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
360
|
+
const base = { _key: `cd${i}`, reason: ref.reason ?? "" };
|
|
361
|
+
if (isSlugRef(ref)) {
|
|
362
|
+
const resolvedId = slugToDocId.get(ref.slug);
|
|
363
|
+
// When a slug resolves to a document, store as "id" ref with
|
|
364
|
+
// the resolved article reference. When unresolved, store as
|
|
365
|
+
// "slug" so Studio knows the resolution strategy even if the
|
|
366
|
+
// article doesn't exist yet.
|
|
367
|
+
return {
|
|
368
|
+
...base,
|
|
369
|
+
refType: resolvedId ? "id" : "slug",
|
|
370
|
+
...(resolvedId
|
|
371
|
+
? { doc: { _ref: resolvedId, _type: "reference" } }
|
|
372
|
+
: {}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (isPathRef(ref)) {
|
|
376
|
+
return { ...base, refType: "path", path: ref.path };
|
|
377
|
+
}
|
|
378
|
+
if (isIdRef(ref)) {
|
|
379
|
+
return {
|
|
380
|
+
...base,
|
|
381
|
+
refType: "id",
|
|
382
|
+
...(ref.id
|
|
383
|
+
? { doc: { _ref: ref.id, _type: "reference" }, docId: ref.id }
|
|
384
|
+
: {}),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (isPerspectiveRef(ref)) {
|
|
388
|
+
return {
|
|
389
|
+
...base,
|
|
390
|
+
refType: "perspective",
|
|
391
|
+
perspective: ref.perspective,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Unknown ref type — store with reason only
|
|
395
|
+
return base;
|
|
286
396
|
});
|
|
287
397
|
// Build assertions
|
|
288
398
|
const assertArray = task.assertions.map((a, i) => {
|
|
@@ -315,6 +425,8 @@ function buildMirrorDocument(task, opts) {
|
|
|
315
425
|
return {
|
|
316
426
|
_id: docId,
|
|
317
427
|
_type: "ailf.task",
|
|
428
|
+
ownership: "repo",
|
|
429
|
+
status: task.status ?? "active",
|
|
318
430
|
assert: assertArray,
|
|
319
431
|
canonicalDocs,
|
|
320
432
|
description: task.description,
|
|
@@ -325,6 +437,7 @@ function buildMirrorDocument(task, opts) {
|
|
|
325
437
|
},
|
|
326
438
|
id: { _type: "slug", current: task.id },
|
|
327
439
|
origin: {
|
|
440
|
+
// Existing provenance fields
|
|
328
441
|
branch: git.branch,
|
|
329
442
|
commitSha: git.commitSha,
|
|
330
443
|
contentHash,
|
|
@@ -334,6 +447,9 @@ function buildMirrorDocument(task, opts) {
|
|
|
334
447
|
repoName: git.name,
|
|
335
448
|
repoOwner: git.owner,
|
|
336
449
|
type: "repo",
|
|
450
|
+
// Authorship: author is write-once (preserve existing), lastEditor always updates
|
|
451
|
+
author: existingAuthor ?? git.author,
|
|
452
|
+
lastEditor: git.author,
|
|
337
453
|
},
|
|
338
454
|
taskPrompt: task.taskPrompt,
|
|
339
455
|
...(task.baseline
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
* @see docs/design-docs/report-store/domain-model.md
|
|
12
12
|
* @see docs/design-docs/report-store/architecture.md — Provenance collection
|
|
13
13
|
*/
|
|
14
|
+
import type { Logger } from "../_vendor/ailf-core/index.d.ts";
|
|
14
15
|
import type { ResolvedSourceConfig } from "../sources.js";
|
|
15
16
|
import type { EvalMode, PromptfooUrlEntry, ReportAutoScope, ReportProvenance } from "./types.js";
|
|
16
17
|
export interface ProvenanceInput {
|
|
17
18
|
/** Feature areas that were evaluated */
|
|
18
19
|
areas: string[];
|
|
20
|
+
/** Logger instance (defaults to ConsoleLogger) */
|
|
21
|
+
logger?: Logger;
|
|
19
22
|
/** Release auto-scope metadata (when perspective evaluation was scoped) */
|
|
20
23
|
autoScope?: ReportAutoScope;
|
|
21
24
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFileSync } from "fs";
|
|
15
15
|
import { resolve } from "path";
|
|
16
16
|
import { load } from "js-yaml";
|
|
17
|
+
import { ConsoleLogger } from "../adapters/loggers/index.js";
|
|
17
18
|
/**
|
|
18
19
|
* Build a ReportProvenance object from pipeline context.
|
|
19
20
|
*
|
|
@@ -24,7 +25,20 @@ import { load } from "js-yaml";
|
|
|
24
25
|
* - Optional metadata (context hash, Promptfoo URL)
|
|
25
26
|
*/
|
|
26
27
|
export function buildProvenance(input) {
|
|
27
|
-
const
|
|
28
|
+
const log = input.logger ?? new ConsoleLogger();
|
|
29
|
+
const models = loadModelsConfig(input.rootDir, log);
|
|
30
|
+
log.debug("Assembling provenance input", {
|
|
31
|
+
mode: input.mode,
|
|
32
|
+
sourceName: input.source.name,
|
|
33
|
+
sourceBaseUrl: input.source.baseUrl,
|
|
34
|
+
areas: input.areas,
|
|
35
|
+
taskIds: input.taskIds,
|
|
36
|
+
hasContextHash: Boolean(input.contextHash),
|
|
37
|
+
hasEvalFingerprint: Boolean(input.evalFingerprint),
|
|
38
|
+
hasCallerGit: Boolean(input.callerGit),
|
|
39
|
+
hasSourceReportId: Boolean(input.sourceReportId),
|
|
40
|
+
modelCount: models.models.length,
|
|
41
|
+
});
|
|
28
42
|
// Cross-repo evaluations: prefer explicit caller git metadata over
|
|
29
43
|
// CI env vars (which always reflect the AILF core repo).
|
|
30
44
|
const git = input.callerGit
|
|
@@ -39,6 +53,14 @@ export function buildProvenance(input) {
|
|
|
39
53
|
const lineage = input.sourceReportId
|
|
40
54
|
? { rerunOf: input.sourceReportId }
|
|
41
55
|
: undefined;
|
|
56
|
+
const trigger = detectTrigger();
|
|
57
|
+
log.debug("Provenance computed", {
|
|
58
|
+
triggerType: trigger.type,
|
|
59
|
+
gitRepo: git?.repo,
|
|
60
|
+
gitBranch: git?.branch,
|
|
61
|
+
evalFingerprint: input.evalFingerprint,
|
|
62
|
+
hasLineage: Boolean(lineage),
|
|
63
|
+
});
|
|
42
64
|
return {
|
|
43
65
|
areas: input.areas,
|
|
44
66
|
autoScope: input.autoScope,
|
|
@@ -149,13 +171,13 @@ function detectTrigger() {
|
|
|
149
171
|
* Load config/models.yaml to extract model list and grader info.
|
|
150
172
|
* Falls back to a minimal config if the file can't be read.
|
|
151
173
|
*/
|
|
152
|
-
function loadModelsConfig(rootDir) {
|
|
174
|
+
function loadModelsConfig(rootDir, log) {
|
|
153
175
|
try {
|
|
154
176
|
const content = readFileSync(resolve(rootDir, "config", "models.yaml"), "utf-8");
|
|
155
177
|
return load(content);
|
|
156
178
|
}
|
|
157
179
|
catch {
|
|
158
|
-
|
|
180
|
+
log.warn("Could not read config/models.yaml for provenance");
|
|
159
181
|
return {
|
|
160
182
|
defaults: {},
|
|
161
183
|
grader: { id: "unknown" },
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline/report-title.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure function that generates descriptive report titles from provenance
|
|
5
|
+
* metadata. The title is the primary display string shown in dashboards,
|
|
6
|
+
* Slack digests, and Studio views — it conveys trigger context, evaluated
|
|
7
|
+
* areas, source/perspective, and document scope at a glance.
|
|
8
|
+
*
|
|
9
|
+
* Score is intentionally omitted from the title since it is surfaced
|
|
10
|
+
* heavily elsewhere in the UI. The `tag` field (on Report) is preserved
|
|
11
|
+
* as a secondary label; the title is the primary display string.
|
|
12
|
+
*
|
|
13
|
+
* Segments are joined with ` · ` (middle dot with spaces).
|
|
14
|
+
*
|
|
15
|
+
* @see docs/design-docs/report-store/domain-model.md
|
|
16
|
+
* @see packages/eval/src/pipeline/provenance.ts — builds the provenance input
|
|
17
|
+
*/
|
|
18
|
+
import type { EvalMode, ReportTrigger } from "./types.js";
|
|
19
|
+
/** Input required to generate a human-readable report title. */
|
|
20
|
+
export interface ReportTitleInput {
|
|
21
|
+
provenance: {
|
|
22
|
+
/** Feature areas that were evaluated */
|
|
23
|
+
areas: string[];
|
|
24
|
+
/** Evaluation mode */
|
|
25
|
+
mode: EvalMode;
|
|
26
|
+
/** Resolved documentation source */
|
|
27
|
+
source: {
|
|
28
|
+
name: string;
|
|
29
|
+
perspective?: string;
|
|
30
|
+
};
|
|
31
|
+
/** Sanity document IDs targeted (when scoped to specific documents) */
|
|
32
|
+
targetDocuments?: string[];
|
|
33
|
+
/** What triggered the evaluation */
|
|
34
|
+
trigger: ReportTrigger;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Total number of known feature areas in the system.
|
|
38
|
+
* Used to determine whether to show "All areas" vs "N areas"
|
|
39
|
+
* when more than 3 areas are evaluated.
|
|
40
|
+
*/
|
|
41
|
+
totalAreaCount?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate a descriptive report title from provenance metadata.
|
|
45
|
+
*
|
|
46
|
+
* The title is composed of up to four segments separated by ` · `:
|
|
47
|
+
*
|
|
48
|
+
* 1. **Trigger context** — what initiated the evaluation (always present)
|
|
49
|
+
* 2. **Areas** — which feature areas were evaluated (omitted if empty)
|
|
50
|
+
* 3. **Source context** — non-default source or perspective (omitted if default)
|
|
51
|
+
* 4. **Target documents** — scoped document IDs (omitted if not scoped)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* generateReportTitle({
|
|
56
|
+
* provenance: {
|
|
57
|
+
* areas: ["GROQ", "Mutations"],
|
|
58
|
+
* mode: "baseline",
|
|
59
|
+
* source: { name: "production" },
|
|
60
|
+
* trigger: { type: "manual" },
|
|
61
|
+
* },
|
|
62
|
+
* })
|
|
63
|
+
* // → "Manual eval · GROQ, Mutations"
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function generateReportTitle(input: ReportTitleInput): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline/report-title.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure function that generates descriptive report titles from provenance
|
|
5
|
+
* metadata. The title is the primary display string shown in dashboards,
|
|
6
|
+
* Slack digests, and Studio views — it conveys trigger context, evaluated
|
|
7
|
+
* areas, source/perspective, and document scope at a glance.
|
|
8
|
+
*
|
|
9
|
+
* Score is intentionally omitted from the title since it is surfaced
|
|
10
|
+
* heavily elsewhere in the UI. The `tag` field (on Report) is preserved
|
|
11
|
+
* as a secondary label; the title is the primary display string.
|
|
12
|
+
*
|
|
13
|
+
* Segments are joined with ` · ` (middle dot with spaces).
|
|
14
|
+
*
|
|
15
|
+
* @see docs/design-docs/report-store/domain-model.md
|
|
16
|
+
* @see packages/eval/src/pipeline/provenance.ts — builds the provenance input
|
|
17
|
+
*/
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Segment builders
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const SEPARATOR = " · ";
|
|
22
|
+
/** Segment 1 — human-readable trigger context */
|
|
23
|
+
function triggerSegment(trigger) {
|
|
24
|
+
switch (trigger.type) {
|
|
25
|
+
case "scheduled": {
|
|
26
|
+
const name = trigger.schedule.replace(/-/g, " ");
|
|
27
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
28
|
+
}
|
|
29
|
+
case "ci":
|
|
30
|
+
return "CI eval";
|
|
31
|
+
case "webhook":
|
|
32
|
+
return "Content change";
|
|
33
|
+
case "cross-repo": {
|
|
34
|
+
// Only show the repo name if callerRepo looks like "owner/repo".
|
|
35
|
+
// Numeric IDs (e.g. GITHUB_REPOSITORY_OWNER_ID fallback) are not useful.
|
|
36
|
+
const repo = trigger.callerRepo;
|
|
37
|
+
if (repo.includes("/")) {
|
|
38
|
+
const shortName = repo.split("/").pop() ?? repo;
|
|
39
|
+
return `Cross-repo (${shortName})`;
|
|
40
|
+
}
|
|
41
|
+
return "Cross-repo";
|
|
42
|
+
}
|
|
43
|
+
case "manual":
|
|
44
|
+
return "Manual eval";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Segment 2 — areas evaluated (omitted when empty) */
|
|
48
|
+
function areasSegment(areas, totalAreaCount) {
|
|
49
|
+
if (areas.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (areas.length <= 3) {
|
|
52
|
+
return areas.join(", ");
|
|
53
|
+
}
|
|
54
|
+
if (totalAreaCount !== undefined && areas.length === totalAreaCount) {
|
|
55
|
+
return "All areas";
|
|
56
|
+
}
|
|
57
|
+
return `${areas.length} areas`;
|
|
58
|
+
}
|
|
59
|
+
/** Segment 3 — source context (omitted when default production, no perspective) */
|
|
60
|
+
function sourceSegment(source) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (source.perspective) {
|
|
63
|
+
parts.push(`perspective: ${source.perspective}`);
|
|
64
|
+
}
|
|
65
|
+
if (source.name !== "production") {
|
|
66
|
+
parts.push(source.name);
|
|
67
|
+
}
|
|
68
|
+
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
69
|
+
}
|
|
70
|
+
/** Segment 4 — target documents (omitted when not scoped) */
|
|
71
|
+
function targetDocumentsSegment(targetDocuments) {
|
|
72
|
+
if (!targetDocuments || targetDocuments.length === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (targetDocuments.length === 1) {
|
|
75
|
+
return targetDocuments[0];
|
|
76
|
+
}
|
|
77
|
+
return `${targetDocuments.length} documents`;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Public API
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Generate a descriptive report title from provenance metadata.
|
|
84
|
+
*
|
|
85
|
+
* The title is composed of up to four segments separated by ` · `:
|
|
86
|
+
*
|
|
87
|
+
* 1. **Trigger context** — what initiated the evaluation (always present)
|
|
88
|
+
* 2. **Areas** — which feature areas were evaluated (omitted if empty)
|
|
89
|
+
* 3. **Source context** — non-default source or perspective (omitted if default)
|
|
90
|
+
* 4. **Target documents** — scoped document IDs (omitted if not scoped)
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* generateReportTitle({
|
|
95
|
+
* provenance: {
|
|
96
|
+
* areas: ["GROQ", "Mutations"],
|
|
97
|
+
* mode: "baseline",
|
|
98
|
+
* source: { name: "production" },
|
|
99
|
+
* trigger: { type: "manual" },
|
|
100
|
+
* },
|
|
101
|
+
* })
|
|
102
|
+
* // → "Manual eval · GROQ, Mutations"
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function generateReportTitle(input) {
|
|
106
|
+
const { provenance, totalAreaCount } = input;
|
|
107
|
+
const segments = [triggerSegment(provenance.trigger)];
|
|
108
|
+
const areas = areasSegment(provenance.areas, totalAreaCount);
|
|
109
|
+
if (areas)
|
|
110
|
+
segments.push(areas);
|
|
111
|
+
const source = sourceSegment(provenance.source);
|
|
112
|
+
if (source)
|
|
113
|
+
segments.push(source);
|
|
114
|
+
const docs = targetDocumentsSegment(provenance.targetDocuments);
|
|
115
|
+
if (docs)
|
|
116
|
+
segments.push(docs);
|
|
117
|
+
return segments.join(SEPARATOR);
|
|
118
|
+
}
|
package/dist/report-store.js
CHANGED
|
@@ -203,6 +203,7 @@ export class ReportStore {
|
|
|
203
203
|
reportId: report.id,
|
|
204
204
|
summary: report.summary,
|
|
205
205
|
tag: report.tag ?? null,
|
|
206
|
+
title: report.title ?? null,
|
|
206
207
|
});
|
|
207
208
|
return report.id;
|
|
208
209
|
}
|
|
@@ -255,5 +256,6 @@ function toReport(doc) {
|
|
|
255
256
|
provenance: doc.provenance,
|
|
256
257
|
summary: doc.summary,
|
|
257
258
|
tag: doc.tag,
|
|
259
|
+
title: doc.title,
|
|
258
260
|
};
|
|
259
261
|
}
|
|
@@ -213,6 +213,7 @@ export function flattenReportRow(report) {
|
|
|
213
213
|
source_name: provenance.source.name,
|
|
214
214
|
source_perspective: provenance.source.perspective ?? null,
|
|
215
215
|
tag: report.tag ?? null,
|
|
216
|
+
title: report.title ?? null,
|
|
216
217
|
total_cost: summary.overall.cost?.total ?? null,
|
|
217
218
|
trigger_caller_repo: provenance.trigger.type === "cross-repo"
|
|
218
219
|
? provenance.trigger.callerRepo
|
package/dist/sources.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Environment variables in config/sources.yaml are resolved via ${{ VAR | default }}
|
|
13
13
|
* interpolation at load time.
|
|
14
14
|
*/
|
|
15
|
+
import type { Logger } from "./_vendor/ailf-core/index.d.ts";
|
|
15
16
|
/**
|
|
16
17
|
* @deprecated Use {@link ResolvedSourceConfig} instead.
|
|
17
18
|
* Kept as a type alias for backward compatibility during the transition.
|
|
@@ -89,7 +90,7 @@ export interface SourceOverrides {
|
|
|
89
90
|
* @param name - Source name from config/sources.yaml. If omitted, uses DOC_SOURCE
|
|
90
91
|
* env var or the first source defined in config/sources.yaml.
|
|
91
92
|
*/
|
|
92
|
-
export declare function loadSource(name?: string, overrides?: SourceOverrides): ResolvedSourceConfig;
|
|
93
|
+
export declare function loadSource(name?: string, overrides?: SourceOverrides, logger?: Logger): ResolvedSourceConfig;
|
|
93
94
|
/**
|
|
94
95
|
* Match a hostname against an origin pattern that may contain `*` wildcards.
|
|
95
96
|
*
|