@sanity/ailf 0.1.34 → 0.2.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 +12 -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/orchestration/steps/calculate-scores-step.js +18 -19
- package/dist/orchestration/steps/publish-report-step.js +3 -0
- package/dist/pipeline/calculate-scores.d.ts +6 -1
- package/dist/pipeline/calculate-scores.js +5 -13
- package/dist/pipeline/generate-configs.js +4 -9
- package/dist/pipeline/mirror-repo-tasks.d.ts +77 -0
- package/dist/pipeline/mirror-repo-tasks.js +141 -27
- 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/package.json +23 -23
|
@@ -63,8 +63,9 @@ export class CalculateScoresStep {
|
|
|
63
63
|
catch {
|
|
64
64
|
// Non-fatal — proceed without source metadata
|
|
65
65
|
}
|
|
66
|
+
let belowCritical = [];
|
|
66
67
|
try {
|
|
67
|
-
calculateAndWriteScores({
|
|
68
|
+
const result = calculateAndWriteScores({
|
|
68
69
|
allowedOrigins: ctx.config.allowedOrigins,
|
|
69
70
|
mode: ctx.config.mode,
|
|
70
71
|
resolvedSource,
|
|
@@ -75,25 +76,14 @@ export class CalculateScoresStep {
|
|
|
75
76
|
searchMode: ctx.config.searchMode,
|
|
76
77
|
source: ctx.config.source,
|
|
77
78
|
});
|
|
79
|
+
belowCritical = result.belowCritical;
|
|
78
80
|
}
|
|
79
81
|
catch (err) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
durationMs: Date.now() - start,
|
|
86
|
-
error: `calculate-scores failed with exit code ${code}`,
|
|
87
|
-
status: "failed",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
if (code === undefined) {
|
|
91
|
-
return {
|
|
92
|
-
durationMs: Date.now() - start,
|
|
93
|
-
error: `calculate-scores failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
94
|
-
status: "failed",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
82
|
+
return {
|
|
83
|
+
durationMs: Date.now() - start,
|
|
84
|
+
error: `calculate-scores failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
+
status: "failed",
|
|
86
|
+
};
|
|
97
87
|
}
|
|
98
88
|
// Postcondition: score summary exists and is valid
|
|
99
89
|
const summaryIssues = checkScoreSummaryValid(ctx.config.rootDir);
|
|
@@ -105,10 +95,19 @@ export class CalculateScoresStep {
|
|
|
105
95
|
status: "failed",
|
|
106
96
|
};
|
|
107
97
|
}
|
|
98
|
+
// Propagate belowCritical into pipeline state for downstream consumers
|
|
99
|
+
// (e.g., orchestrator reporting, publish step metadata).
|
|
100
|
+
// This is informational — the pipeline continues to run subsequent steps.
|
|
101
|
+
if (belowCritical.length > 0) {
|
|
102
|
+
state.belowCritical = belowCritical;
|
|
103
|
+
}
|
|
104
|
+
const criticalSuffix = belowCritical.length > 0
|
|
105
|
+
? ` (${belowCritical.length} area(s) below critical threshold: ${belowCritical.join(", ")})`
|
|
106
|
+
: "";
|
|
108
107
|
return {
|
|
109
108
|
durationMs: Date.now() - start,
|
|
110
109
|
status: "success",
|
|
111
|
-
summary:
|
|
110
|
+
summary: `Scores calculated and summary written${criticalSuffix}`,
|
|
112
111
|
};
|
|
113
112
|
}
|
|
114
113
|
cacheInputs(ctx) {
|
|
@@ -14,6 +14,7 @@ import { readFileSync } from "fs";
|
|
|
14
14
|
import { resolve } from "path";
|
|
15
15
|
import { checkScoreSummaryValid } from "../../pipeline/checks.js";
|
|
16
16
|
import { buildProvenance, } from "../../pipeline/provenance.js";
|
|
17
|
+
import { generateReportTitle } from "../../pipeline/report-title.js";
|
|
17
18
|
import { generateReportId } from "../../report-store.js";
|
|
18
19
|
import { withRetry } from "../../sinks/retry.js";
|
|
19
20
|
export class PublishReportStep {
|
|
@@ -101,6 +102,7 @@ export class PublishReportStep {
|
|
|
101
102
|
comparedAgainst: autoCompareResult.baselineReportId,
|
|
102
103
|
};
|
|
103
104
|
}
|
|
105
|
+
const title = generateReportTitle({ provenance });
|
|
104
106
|
const report = {
|
|
105
107
|
comparison: comparison ?? undefined,
|
|
106
108
|
completedAt: now,
|
|
@@ -109,6 +111,7 @@ export class PublishReportStep {
|
|
|
109
111
|
provenance,
|
|
110
112
|
summary,
|
|
111
113
|
tag: this.options.publishTag ?? ctx.config.publishTag,
|
|
114
|
+
title,
|
|
112
115
|
};
|
|
113
116
|
// Share reportId with downstream steps (CallbackStep + orchestrator job update)
|
|
114
117
|
state.reportId = reportId;
|
|
@@ -99,4 +99,9 @@ export interface CalculateScoresOptions {
|
|
|
99
99
|
/** Documentation source name */
|
|
100
100
|
source?: string;
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
/** Result from calculateAndWriteScores — replaces process.exit() calls. */
|
|
103
|
+
export interface CalculateScoresResult {
|
|
104
|
+
/** Feature areas that scored below the critical threshold (40). */
|
|
105
|
+
belowCritical: string[];
|
|
106
|
+
}
|
|
107
|
+
export declare function calculateAndWriteScores(options: CalculateScoresOptions): CalculateScoresResult;
|
|
@@ -674,15 +674,10 @@ export function calculateAndWriteScores(options) {
|
|
|
674
674
|
const resultsIssues = checkResultsExist(ROOT, baselineResultsPath);
|
|
675
675
|
const resultsErrors = resultsIssues.filter((i) => i.severity === "error");
|
|
676
676
|
if (resultsErrors.length > 0) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
console.error(` at ${e.path}`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
console.error("\nRun 'pnpm eval' first to generate results, then 'pnpm calculate-scores'.");
|
|
685
|
-
process.exit(1);
|
|
677
|
+
const details = resultsErrors
|
|
678
|
+
.map((e) => (e.path ? `${e.message} (at ${e.path})` : e.message))
|
|
679
|
+
.join("; ");
|
|
680
|
+
throw new Error(`Results validation failed: ${details}. Run 'pnpm eval' first to generate results.`);
|
|
686
681
|
}
|
|
687
682
|
console.log(`Reading results from: ${baselineResultsPath}`);
|
|
688
683
|
if (source) {
|
|
@@ -750,10 +745,7 @@ export function calculateAndWriteScores(options) {
|
|
|
750
745
|
writeFileSync(join(outDir, "grader-judgments.json"), JSON.stringify(judgments, null, 2));
|
|
751
746
|
console.log(`Grader judgments written to results/latest/grader-judgments.json (${judgments.length} judgments)`);
|
|
752
747
|
}
|
|
753
|
-
|
|
754
|
-
if (summary.belowCritical.length > 0) {
|
|
755
|
-
process.exit(1);
|
|
756
|
-
}
|
|
748
|
+
return { belowCritical: summary.belowCritical };
|
|
757
749
|
}
|
|
758
750
|
function printPerModelReport(perModel) {
|
|
759
751
|
console.log("-".repeat(80));
|
|
@@ -264,15 +264,10 @@ export function generateConfigs(options) {
|
|
|
264
264
|
const modelIssues = validateModelsYaml(rootDir);
|
|
265
265
|
const modelErrors = modelIssues.filter((i) => i.severity === "error");
|
|
266
266
|
if (modelErrors.length > 0) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.error(` at ${e.path}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
console.error("\nFix config/models.yaml before generating configs. Run 'pnpm validate' for details.");
|
|
275
|
-
process.exit(1);
|
|
267
|
+
const details = modelErrors
|
|
268
|
+
.map((e) => (e.path ? `${e.message} (at ${e.path})` : e.message))
|
|
269
|
+
.join("; ");
|
|
270
|
+
throw new Error(`config/models.yaml validation failed: ${details}. Run 'pnpm validate' for details.`);
|
|
276
271
|
}
|
|
277
272
|
console.log("Loading config/models.yaml...");
|
|
278
273
|
const models = loadModels(rootDir);
|
|
@@ -24,6 +24,15 @@ export interface MirrorOptions {
|
|
|
24
24
|
/** If true, log what would be done without writing */
|
|
25
25
|
dryRun?: boolean;
|
|
26
26
|
}
|
|
27
|
+
/** Authorship info extracted from git context or GitHub Actions environment. */
|
|
28
|
+
export interface GitAuthor {
|
|
29
|
+
/** Git commit author name (e.g., "Jordan Smith") */
|
|
30
|
+
gitName?: string;
|
|
31
|
+
/** Git commit author email (e.g., "jordan@example.com") */
|
|
32
|
+
gitEmail?: string;
|
|
33
|
+
/** GitHub username (from GITHUB_ACTOR or event payload) */
|
|
34
|
+
githubUsername?: string;
|
|
35
|
+
}
|
|
27
36
|
export interface GitContext {
|
|
28
37
|
/** Full repo identifier (e.g., "sanity-io/visual-editing") */
|
|
29
38
|
repo: string;
|
|
@@ -35,6 +44,8 @@ export interface GitContext {
|
|
|
35
44
|
branch: string;
|
|
36
45
|
/** HEAD commit SHA */
|
|
37
46
|
commitSha: string;
|
|
47
|
+
/** Author of the current commit/trigger */
|
|
48
|
+
author: GitAuthor;
|
|
38
49
|
}
|
|
39
50
|
export interface MirrorResult {
|
|
40
51
|
/** Total tasks processed */
|
|
@@ -84,3 +95,69 @@ export declare function mirrorDocId(owner: string, repo: string, taskId: string)
|
|
|
84
95
|
* that's not mirrored.
|
|
85
96
|
*/
|
|
86
97
|
export declare function computeTaskHash(task: TaskDefinition): string;
|
|
98
|
+
/** @internal Exported for testing — not part of the public API. */
|
|
99
|
+
export declare function buildMirrorDocument(task: TaskDefinition, opts: {
|
|
100
|
+
contentHash: string;
|
|
101
|
+
docId: string;
|
|
102
|
+
/** Existing author from the current mirror document (write-once preservation) */
|
|
103
|
+
existingAuthor?: GitAuthor;
|
|
104
|
+
git: GitContext;
|
|
105
|
+
slugToDocId: Map<string, string>;
|
|
106
|
+
}): {
|
|
107
|
+
baseline?: {
|
|
108
|
+
rubric?: "full" | "abbreviated" | "none" | undefined;
|
|
109
|
+
enabled?: boolean | undefined;
|
|
110
|
+
} | undefined;
|
|
111
|
+
_id: string;
|
|
112
|
+
_type: string;
|
|
113
|
+
ownership: string;
|
|
114
|
+
status: "active" | "draft" | "paused" | "archived";
|
|
115
|
+
assert: Record<string, unknown>[];
|
|
116
|
+
canonicalDocs: ({
|
|
117
|
+
_key: string;
|
|
118
|
+
reason: string;
|
|
119
|
+
} | {
|
|
120
|
+
refType: string;
|
|
121
|
+
path: string;
|
|
122
|
+
_key: string;
|
|
123
|
+
reason: string;
|
|
124
|
+
} | {
|
|
125
|
+
doc?: {
|
|
126
|
+
_ref: string;
|
|
127
|
+
_type: string;
|
|
128
|
+
} | undefined;
|
|
129
|
+
docId?: string | undefined;
|
|
130
|
+
refType: string;
|
|
131
|
+
_key: string;
|
|
132
|
+
reason: string;
|
|
133
|
+
} | {
|
|
134
|
+
refType: string;
|
|
135
|
+
perspective: string;
|
|
136
|
+
_key: string;
|
|
137
|
+
reason: string;
|
|
138
|
+
})[];
|
|
139
|
+
description: string;
|
|
140
|
+
docCoverage: boolean;
|
|
141
|
+
featureArea: {
|
|
142
|
+
_ref: string;
|
|
143
|
+
_type: string;
|
|
144
|
+
};
|
|
145
|
+
id: {
|
|
146
|
+
_type: string;
|
|
147
|
+
current: string;
|
|
148
|
+
};
|
|
149
|
+
origin: {
|
|
150
|
+
branch: string;
|
|
151
|
+
commitSha: string;
|
|
152
|
+
contentHash: string;
|
|
153
|
+
lastSyncedAt: string;
|
|
154
|
+
path: string;
|
|
155
|
+
repo: string;
|
|
156
|
+
repoName: string;
|
|
157
|
+
repoOwner: string;
|
|
158
|
+
type: string;
|
|
159
|
+
author: GitAuthor;
|
|
160
|
+
lastEditor: GitAuthor;
|
|
161
|
+
};
|
|
162
|
+
taskPrompt: string;
|
|
163
|
+
};
|
|
@@ -13,7 +13,8 @@
|
|
|
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";
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Public API
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -56,22 +57,30 @@ export async function mirrorRepoTasks(options) {
|
|
|
56
57
|
const areas = [...new Set(tasks.map((t) => t.featureArea))];
|
|
57
58
|
const createdAreas = await ensureFeatureAreas(client, areas, dryRun);
|
|
58
59
|
result.areasCreated = createdAreas;
|
|
59
|
-
// Fetch existing mirror document
|
|
60
|
+
// Fetch existing mirror document state for change detection + ownership check
|
|
60
61
|
const mirrorIds = tasks.map((t) => mirrorDocId(git.owner, git.name, t.id));
|
|
61
|
-
const
|
|
62
|
+
const existingDocState = await fetchExistingDocState(client, mirrorIds);
|
|
62
63
|
// Mirror each task
|
|
63
64
|
for (const task of tasks) {
|
|
64
65
|
try {
|
|
65
66
|
const docId = mirrorDocId(git.owner, git.name, task.id);
|
|
67
|
+
const existing = existingDocState.get(docId);
|
|
68
|
+
// Skip graduated tasks — ownership was changed to "studio"
|
|
69
|
+
if (existing?.ownership === "studio") {
|
|
70
|
+
console.log(` ℹ️ Skipping "${task.id}" — graduated to Studio ownership`);
|
|
71
|
+
result.skipped++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
66
74
|
const contentHash = computeTaskHash(task);
|
|
67
75
|
// Skip unchanged
|
|
68
|
-
if (
|
|
76
|
+
if (existing?.hash === contentHash) {
|
|
69
77
|
result.skipped++;
|
|
70
78
|
continue;
|
|
71
79
|
}
|
|
72
80
|
const doc = buildMirrorDocument(task, {
|
|
73
81
|
contentHash,
|
|
74
82
|
docId,
|
|
83
|
+
existingAuthor: existing?.existingAuthor,
|
|
75
84
|
git,
|
|
76
85
|
slugToDocId,
|
|
77
86
|
});
|
|
@@ -106,12 +115,15 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
106
115
|
if (ghRepo) {
|
|
107
116
|
const [owner, name] = ghRepo.split("/");
|
|
108
117
|
const branch = ghHeadRef || ghRef.replace("refs/heads/", "").replace("refs/tags/", "");
|
|
118
|
+
// Extract author from GitHub Actions environment
|
|
119
|
+
const author = detectGitHubActionsAuthor();
|
|
109
120
|
return {
|
|
110
121
|
repo: ghRepo,
|
|
111
122
|
owner: owner ?? "unknown",
|
|
112
123
|
name: name ?? "unknown",
|
|
113
124
|
branch: branch || "unknown",
|
|
114
125
|
commitSha: ghSha || "unknown",
|
|
126
|
+
author,
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
129
|
// Fallback: try git CLI
|
|
@@ -134,12 +146,15 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
134
146
|
remote.match(/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
135
147
|
const owner = match?.[1] ?? "unknown";
|
|
136
148
|
const name = match?.[2] ?? "unknown";
|
|
149
|
+
// Extract author from git log
|
|
150
|
+
const author = detectGitCliAuthor(repoTasksPath, execSync);
|
|
137
151
|
return {
|
|
138
152
|
repo: `${owner}/${name}`,
|
|
139
153
|
owner,
|
|
140
154
|
name,
|
|
141
155
|
branch,
|
|
142
156
|
commitSha,
|
|
157
|
+
author,
|
|
143
158
|
};
|
|
144
159
|
}
|
|
145
160
|
catch {
|
|
@@ -149,10 +164,63 @@ export async function detectGitContext(repoTasksPath) {
|
|
|
149
164
|
name: "unknown",
|
|
150
165
|
branch: "unknown",
|
|
151
166
|
commitSha: "unknown",
|
|
167
|
+
author: {},
|
|
152
168
|
};
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
171
|
// ---------------------------------------------------------------------------
|
|
172
|
+
// Author detection helpers
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Extract author info from GitHub Actions environment variables and
|
|
176
|
+
* the webhook event payload (GITHUB_EVENT_PATH).
|
|
177
|
+
*/
|
|
178
|
+
function detectGitHubActionsAuthor() {
|
|
179
|
+
const ghActor = process.env.GITHUB_ACTOR ?? undefined;
|
|
180
|
+
const author = { githubUsername: ghActor };
|
|
181
|
+
// Try to read richer author info from the event payload
|
|
182
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
183
|
+
if (eventPath) {
|
|
184
|
+
try {
|
|
185
|
+
const event = JSON.parse(readFileSync(eventPath, "utf-8"));
|
|
186
|
+
// Push event: head_commit.author has name + email
|
|
187
|
+
if (event.head_commit?.author) {
|
|
188
|
+
author.gitName = event.head_commit.author.name ?? undefined;
|
|
189
|
+
author.gitEmail = event.head_commit.author.email ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
// PR event: pull_request.user.login is the PR author
|
|
192
|
+
if (event.pull_request?.user?.login) {
|
|
193
|
+
author.githubUsername = event.pull_request.user.login;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Event payload parsing is best-effort — fall through with GITHUB_ACTOR only
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return author;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Extract author info from git CLI (local fallback when not in GitHub Actions).
|
|
204
|
+
*/
|
|
205
|
+
function detectGitCliAuthor(cwd, execSyncFn) {
|
|
206
|
+
try {
|
|
207
|
+
const authorLine = execSyncFn('git log -1 --format="%an|%ae"', {
|
|
208
|
+
encoding: "utf-8",
|
|
209
|
+
cwd,
|
|
210
|
+
})
|
|
211
|
+
.toString()
|
|
212
|
+
.trim();
|
|
213
|
+
const [gitName, gitEmail] = authorLine.split("|");
|
|
214
|
+
return {
|
|
215
|
+
gitName: gitName || undefined,
|
|
216
|
+
gitEmail: gitEmail || undefined,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
156
224
|
// Document ID scheme
|
|
157
225
|
// ---------------------------------------------------------------------------
|
|
158
226
|
/**
|
|
@@ -247,42 +315,82 @@ async function ensureFeatureAreas(client, areas, dryRun) {
|
|
|
247
315
|
await transaction.commit();
|
|
248
316
|
return missing;
|
|
249
317
|
}
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
// Fetch existing content hashes
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
318
|
/**
|
|
254
|
-
* Fetch existing mirror documents' content hashes
|
|
255
|
-
*
|
|
319
|
+
* Fetch existing mirror documents' ownership and content hashes.
|
|
320
|
+
*
|
|
321
|
+
* The ownership field determines whether the mirror step is allowed to
|
|
322
|
+
* update the document:
|
|
323
|
+
* - `"repo"` or absent → safe to update (active mirror)
|
|
324
|
+
* - `"studio"` → skip (graduated to Studio ownership)
|
|
325
|
+
*
|
|
326
|
+
* The hash is stored in origin.contentHash for change detection.
|
|
256
327
|
*/
|
|
257
|
-
async function
|
|
328
|
+
async function fetchExistingDocState(client, docIds) {
|
|
258
329
|
if (docIds.length === 0)
|
|
259
330
|
return new Map();
|
|
260
|
-
const query = `*[_id in $ids] {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
331
|
+
const query = `*[_id in $ids] {
|
|
332
|
+
_id,
|
|
333
|
+
"hash": origin.contentHash,
|
|
334
|
+
ownership,
|
|
335
|
+
"existingAuthor": origin.author
|
|
336
|
+
}`;
|
|
337
|
+
const results = await client.fetch(query, { ids: docIds });
|
|
264
338
|
const map = new Map();
|
|
265
339
|
for (const r of results) {
|
|
266
|
-
|
|
267
|
-
|
|
340
|
+
map.set(r._id, {
|
|
341
|
+
hash: r.hash ?? undefined,
|
|
342
|
+
ownership: r.ownership ?? undefined,
|
|
343
|
+
existingAuthor: r.existingAuthor ?? undefined,
|
|
344
|
+
});
|
|
268
345
|
}
|
|
269
346
|
return map;
|
|
270
347
|
}
|
|
271
348
|
// ---------------------------------------------------------------------------
|
|
272
349
|
// Build mirror document
|
|
273
350
|
// ---------------------------------------------------------------------------
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
//
|
|
351
|
+
/** @internal Exported for testing — not part of the public API. */
|
|
352
|
+
export function buildMirrorDocument(task, opts) {
|
|
353
|
+
const { contentHash, docId, existingAuthor, git, slugToDocId } = opts;
|
|
354
|
+
// Build canonical docs with resolved references and correct refType.
|
|
355
|
+
// Each ref type gets the appropriate resolution fields set on the
|
|
356
|
+
// mirror document so Studio can display them correctly.
|
|
279
357
|
const canonicalDocs = task.canonicalDocs.map((ref, i) => {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
358
|
+
const base = { _key: `cd${i}`, reason: ref.reason ?? "" };
|
|
359
|
+
if (isSlugRef(ref)) {
|
|
360
|
+
const resolvedId = slugToDocId.get(ref.slug);
|
|
361
|
+
// When a slug resolves to a document, store as "id" ref with
|
|
362
|
+
// the resolved article reference. When unresolved, store as
|
|
363
|
+
// "slug" so Studio knows the resolution strategy even if the
|
|
364
|
+
// article doesn't exist yet.
|
|
365
|
+
return {
|
|
366
|
+
...base,
|
|
367
|
+
refType: resolvedId ? "id" : "slug",
|
|
368
|
+
...(resolvedId
|
|
369
|
+
? { doc: { _ref: resolvedId, _type: "reference" } }
|
|
370
|
+
: {}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (isPathRef(ref)) {
|
|
374
|
+
return { ...base, refType: "path", path: ref.path };
|
|
375
|
+
}
|
|
376
|
+
if (isIdRef(ref)) {
|
|
377
|
+
return {
|
|
378
|
+
...base,
|
|
379
|
+
refType: "id",
|
|
380
|
+
...(ref.id
|
|
381
|
+
? { doc: { _ref: ref.id, _type: "reference" }, docId: ref.id }
|
|
382
|
+
: {}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (isPerspectiveRef(ref)) {
|
|
386
|
+
return {
|
|
387
|
+
...base,
|
|
388
|
+
refType: "perspective",
|
|
389
|
+
perspective: ref.perspective,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// Unknown ref type — store with reason only
|
|
393
|
+
return base;
|
|
286
394
|
});
|
|
287
395
|
// Build assertions
|
|
288
396
|
const assertArray = task.assertions.map((a, i) => {
|
|
@@ -315,6 +423,8 @@ function buildMirrorDocument(task, opts) {
|
|
|
315
423
|
return {
|
|
316
424
|
_id: docId,
|
|
317
425
|
_type: "ailf.task",
|
|
426
|
+
ownership: "repo",
|
|
427
|
+
status: task.status ?? "active",
|
|
318
428
|
assert: assertArray,
|
|
319
429
|
canonicalDocs,
|
|
320
430
|
description: task.description,
|
|
@@ -325,6 +435,7 @@ function buildMirrorDocument(task, opts) {
|
|
|
325
435
|
},
|
|
326
436
|
id: { _type: "slug", current: task.id },
|
|
327
437
|
origin: {
|
|
438
|
+
// Existing provenance fields
|
|
328
439
|
branch: git.branch,
|
|
329
440
|
commitSha: git.commitSha,
|
|
330
441
|
contentHash,
|
|
@@ -334,6 +445,9 @@ function buildMirrorDocument(task, opts) {
|
|
|
334
445
|
repoName: git.name,
|
|
335
446
|
repoOwner: git.owner,
|
|
336
447
|
type: "repo",
|
|
448
|
+
// Authorship: author is write-once (preserve existing), lastEditor always updates
|
|
449
|
+
author: existingAuthor ?? git.author,
|
|
450
|
+
lastEditor: git.author,
|
|
337
451
|
},
|
|
338
452
|
taskPrompt: task.taskPrompt,
|
|
339
453
|
...(task.baseline
|
|
@@ -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;
|