agentxchain 2.110.0 → 2.112.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.
@@ -0,0 +1,195 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { loadAllChainReports, loadChainReport, loadLatestChainReport } from './chain-reports.js';
4
+ import { getActiveRepoDecisions } from './repo-decisions.js';
5
+
6
+ const MISSION_ATTENTION_TERMINALS = new Set(['operator_abort', 'parent_validation_failed']);
7
+ const MISSION_ATTENTION_RUN_STATUSES = new Set(['blocked', 'failed']);
8
+
9
+ export function getMissionsDir(root) {
10
+ return join(root, '.agentxchain', 'missions');
11
+ }
12
+
13
+ export function buildMissionId(input) {
14
+ const slug = String(input || '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')
18
+ .slice(0, 64);
19
+ const base = slug || 'untitled';
20
+ return base.startsWith('mission-') ? base : `mission-${base}`;
21
+ }
22
+
23
+ export function createMission(root, { missionId, title, goal }) {
24
+ const normalizedId = buildMissionId(missionId || title);
25
+ const filePath = join(getMissionsDir(root), `${normalizedId}.json`);
26
+ if (existsSync(filePath)) {
27
+ return { ok: false, error: `Mission already exists: ${normalizedId}` };
28
+ }
29
+
30
+ const now = new Date().toISOString();
31
+ const artifact = {
32
+ mission_id: normalizedId,
33
+ title,
34
+ goal,
35
+ status: 'active',
36
+ created_at: now,
37
+ updated_at: now,
38
+ chain_ids: [],
39
+ };
40
+
41
+ mkdirSync(getMissionsDir(root), { recursive: true });
42
+ writeFileSync(filePath, JSON.stringify(artifact, null, 2));
43
+ return { ok: true, mission: artifact };
44
+ }
45
+
46
+ export function loadAllMissionArtifacts(root) {
47
+ const missionsDir = getMissionsDir(root);
48
+ if (!existsSync(missionsDir)) return [];
49
+
50
+ const missions = [];
51
+ for (const file of readdirSync(missionsDir).filter((entry) => entry.endsWith('.json')).sort()) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(join(missionsDir, file), 'utf8'));
54
+ if (parsed && parsed.mission_id) {
55
+ missions.push(parsed);
56
+ }
57
+ } catch {
58
+ // Advisory surface only. Skip malformed mission files.
59
+ }
60
+ }
61
+
62
+ missions.sort((left, right) => {
63
+ const leftTime = new Date(left.updated_at || left.created_at || 0).getTime();
64
+ const rightTime = new Date(right.updated_at || right.created_at || 0).getTime();
65
+ return rightTime - leftTime;
66
+ });
67
+
68
+ return missions;
69
+ }
70
+
71
+ export function loadMissionArtifact(root, missionId) {
72
+ const filePath = join(getMissionsDir(root), `${missionId}.json`);
73
+ if (existsSync(filePath)) {
74
+ try {
75
+ const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
76
+ if (parsed?.mission_id) return parsed;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ return loadAllMissionArtifacts(root).find((mission) => mission.mission_id === missionId) || null;
83
+ }
84
+
85
+ export function loadLatestMissionArtifact(root) {
86
+ const missions = loadAllMissionArtifacts(root);
87
+ return missions.length > 0 ? missions[0] : null;
88
+ }
89
+
90
+ export function attachChainToMission(root, missionId, chainRef = 'latest') {
91
+ const mission = loadMissionArtifact(root, missionId);
92
+ if (!mission) {
93
+ return { ok: false, error: `Mission not found: ${missionId}` };
94
+ }
95
+
96
+ const chain = chainRef === 'latest'
97
+ ? loadLatestChainReport(root)
98
+ : loadChainReport(root, chainRef);
99
+ if (!chain) {
100
+ return { ok: false, error: chainRef === 'latest' ? 'No chain reports found.' : `Chain report not found: ${chainRef}` };
101
+ }
102
+
103
+ const nextChainIds = Array.isArray(mission.chain_ids) ? [...mission.chain_ids] : [];
104
+ if (!nextChainIds.includes(chain.chain_id)) {
105
+ nextChainIds.push(chain.chain_id);
106
+ }
107
+
108
+ const updated = {
109
+ ...mission,
110
+ chain_ids: nextChainIds,
111
+ updated_at: new Date().toISOString(),
112
+ };
113
+
114
+ mkdirSync(getMissionsDir(root), { recursive: true });
115
+ writeFileSync(join(getMissionsDir(root), `${updated.mission_id}.json`), JSON.stringify(updated, null, 2));
116
+ return { ok: true, mission: updated, chain };
117
+ }
118
+
119
+ export function buildMissionSnapshot(root, missionArtifact) {
120
+ const chainIds = Array.isArray(missionArtifact.chain_ids) ? missionArtifact.chain_ids : [];
121
+ const chains = [];
122
+ const missingChainIds = [];
123
+
124
+ for (const chainId of chainIds) {
125
+ const report = loadChainReport(root, chainId);
126
+ if (report) {
127
+ chains.push(report);
128
+ } else {
129
+ missingChainIds.push(chainId);
130
+ }
131
+ }
132
+
133
+ chains.sort((left, right) => {
134
+ const leftTime = new Date(left.started_at || 0).getTime();
135
+ const rightTime = new Date(right.started_at || 0).getTime();
136
+ return rightTime - leftTime;
137
+ });
138
+
139
+ const totalRuns = chains.reduce((sum, chain) => sum + (chain.runs?.length || 0), 0);
140
+ const totalTurns = chains.reduce((sum, chain) => sum + (chain.total_turns || 0), 0);
141
+ const latestChain = chains[0] || null;
142
+ const activeRepoDecisions = getActiveRepoDecisions(root);
143
+
144
+ return {
145
+ ...missionArtifact,
146
+ derived_status: deriveMissionStatus(missionArtifact, chains, missingChainIds),
147
+ chain_count: chainIds.length,
148
+ attached_chain_count: chains.length,
149
+ missing_chain_ids: missingChainIds,
150
+ total_runs: totalRuns,
151
+ total_turns: totalTurns,
152
+ latest_chain_id: latestChain?.chain_id || null,
153
+ latest_terminal_reason: latestChain?.terminal_reason || null,
154
+ active_repo_decisions_count: activeRepoDecisions.length,
155
+ chains,
156
+ };
157
+ }
158
+
159
+ export function loadAllMissionSnapshots(root) {
160
+ return loadAllMissionArtifacts(root).map((mission) => buildMissionSnapshot(root, mission));
161
+ }
162
+
163
+ function deriveMissionStatus(missionArtifact, chains, missingChainIds) {
164
+ if (missionArtifact.status && missionArtifact.status !== 'active') {
165
+ return missionArtifact.status;
166
+ }
167
+ if (missingChainIds.length > 0) return 'degraded';
168
+ if (chains.length === 0) return 'planned';
169
+ if (chains.some((chain) => (
170
+ MISSION_ATTENTION_TERMINALS.has(chain.terminal_reason)
171
+ || (chain.runs || []).some((run) => MISSION_ATTENTION_RUN_STATUSES.has(run.status))
172
+ ))) {
173
+ return 'needs_attention';
174
+ }
175
+ return 'progressing';
176
+ }
177
+
178
+ export function loadLatestMissionSnapshot(root) {
179
+ const artifact = loadLatestMissionArtifact(root);
180
+ return artifact ? buildMissionSnapshot(root, artifact) : null;
181
+ }
182
+
183
+ export function loadMissionSnapshot(root, missionId) {
184
+ const artifact = loadMissionArtifact(root, missionId);
185
+ return artifact ? buildMissionSnapshot(root, artifact) : null;
186
+ }
187
+
188
+ export function buildMissionListSummary(root, limit = 20) {
189
+ return loadAllMissionSnapshots(root).slice(0, limit);
190
+ }
191
+
192
+ export function loadMissionAttachmentTarget(root, missionId) {
193
+ if (missionId) return loadMissionArtifact(root, missionId);
194
+ return loadLatestMissionArtifact(root);
195
+ }
@@ -0,0 +1,336 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export const RELEASE_ALIGNMENT_SCOPES = {
5
+ PREBUMP: 'prebump',
6
+ CURRENT: 'current',
7
+ };
8
+
9
+ function read(repoRoot, relativePath) {
10
+ return readFileSync(join(repoRoot, relativePath), 'utf8');
11
+ }
12
+
13
+ function readJson(repoRoot, relativePath) {
14
+ return JSON.parse(read(repoRoot, relativePath));
15
+ }
16
+
17
+ export function escapeRegExp(value) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
+ }
20
+
21
+ export function formatCount(value) {
22
+ return new Intl.NumberFormat('en-US').format(value);
23
+ }
24
+
25
+ function normalizeEvidenceText(value) {
26
+ return value
27
+ .replace(/^\s*-\s*/, '')
28
+ .replace(/\.$/, '')
29
+ .replace(/,/g, '')
30
+ .trim();
31
+ }
32
+
33
+ export function extractTopReleaseSection(changelog, version) {
34
+ const heading = `## ${version}`;
35
+ const start = changelog.indexOf(heading);
36
+ if (start === -1) {
37
+ return null;
38
+ }
39
+ const afterStart = changelog.slice(start + heading.length);
40
+ const nextHeadingOffset = afterStart.search(/\n##\s+\d+\.\d+\.\d+/);
41
+ return nextHeadingOffset === -1 ? afterStart : afterStart.slice(0, nextHeadingOffset);
42
+ }
43
+
44
+ export function extractAggregateEvidenceLine(text) {
45
+ const matches = [...text.matchAll(/(^-\s+.*\b\d[\d,]*\s+tests\b.*\b0 failures\b.*$)/gm)];
46
+ if (matches.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ const aggregate = matches.reduce((best, match) => {
51
+ const line = match[1].replace(/\*\*/g, '').replace(/`/g, '').trim();
52
+ const countMatch = line.match(/\b(\d[\d,]*)\s+tests\b/);
53
+ const count = countMatch ? Number(countMatch[1].replace(/,/g, '')) : 0;
54
+ if (!best || count > best.count) {
55
+ return { count, line };
56
+ }
57
+ return best;
58
+ }, null);
59
+
60
+ return aggregate?.line ?? null;
61
+ }
62
+
63
+ function extractAggregateEvidenceCount(line) {
64
+ const match = line?.match(/\b(\d[\d,]*)\s+tests\b/);
65
+ if (!match) {
66
+ return null;
67
+ }
68
+ return Number(match[1].replace(/,/g, ''));
69
+ }
70
+
71
+ export function getReleaseAlignmentContext(repoRoot, { targetVersion } = {}) {
72
+ const pkg = readJson(repoRoot, 'cli/package.json');
73
+ const version = targetVersion || pkg.version;
74
+ const releaseDocId = `v${version.replace(/\./g, '-')}`;
75
+ const releaseDocPath = `website-v2/docs/releases/${releaseDocId}.mdx`;
76
+ const releaseRoute = `/docs/releases/${releaseDocId}`;
77
+ const tarballUrl = `https://registry.npmjs.org/agentxchain/-/agentxchain-${version}.tgz`;
78
+ const changelog = read(repoRoot, 'cli/CHANGELOG.md');
79
+ const changelogSection = extractTopReleaseSection(changelog, version);
80
+ const aggregateEvidenceLine = changelogSection ? extractAggregateEvidenceLine(changelogSection) : null;
81
+ const aggregateEvidenceCount = extractAggregateEvidenceCount(aggregateEvidenceLine);
82
+
83
+ return {
84
+ packageVersion: pkg.version,
85
+ targetVersion: version,
86
+ releaseDocId,
87
+ releaseDocPath,
88
+ releaseRoute,
89
+ tarballUrl,
90
+ changelog,
91
+ changelogSection,
92
+ aggregateEvidenceLine,
93
+ aggregateEvidenceText: aggregateEvidenceLine ? normalizeEvidenceText(aggregateEvidenceLine) : null,
94
+ aggregateEvidenceCount,
95
+ };
96
+ }
97
+
98
+ function validateCurrentReleaseDoc(ctx, repoRoot) {
99
+ const errors = [];
100
+ const fullPath = join(repoRoot, ctx.releaseDocPath);
101
+ if (!existsSync(fullPath)) {
102
+ errors.push(`release notes page missing: ${ctx.releaseDocPath}`);
103
+ return errors;
104
+ }
105
+
106
+ const releaseDoc = read(repoRoot, ctx.releaseDocPath);
107
+ const heading = `# AgentXchain v${ctx.targetVersion}`;
108
+ if (!releaseDoc.includes(heading)) {
109
+ errors.push(`${ctx.releaseDocPath} must contain ${heading}`);
110
+ }
111
+
112
+ if (!releaseDoc.match(/## Evidence/i)) {
113
+ errors.push(`${ctx.releaseDocPath} must contain an Evidence section`);
114
+ }
115
+
116
+ if (ctx.aggregateEvidenceText && !normalizeEvidenceText(releaseDoc).includes(ctx.aggregateEvidenceText)) {
117
+ errors.push(`${ctx.releaseDocPath} must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
118
+ }
119
+
120
+ return errors;
121
+ }
122
+
123
+ function validateTextIncludesVersionAndEvidence(relativePath, label) {
124
+ return {
125
+ id: label,
126
+ check(ctx, repoRoot) {
127
+ const content = read(repoRoot, relativePath);
128
+ const errors = [];
129
+ if (!content.includes(`v${ctx.targetVersion}`)) {
130
+ errors.push(`${relativePath} must mention v${ctx.targetVersion}`);
131
+ }
132
+ if (ctx.aggregateEvidenceText && !normalizeEvidenceText(content).includes(ctx.aggregateEvidenceText)) {
133
+ errors.push(`${relativePath} must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
134
+ }
135
+ return errors;
136
+ },
137
+ };
138
+ }
139
+
140
+ export const RELEASE_ALIGNMENT_SURFACES = [
141
+ {
142
+ id: 'changelog',
143
+ label: 'cli/CHANGELOG.md top target section',
144
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
145
+ check(ctx) {
146
+ const errors = [];
147
+ if (!ctx.changelogSection) {
148
+ errors.push(`cli/CHANGELOG.md is missing top heading ## ${ctx.targetVersion}`);
149
+ }
150
+ if (!ctx.aggregateEvidenceLine) {
151
+ errors.push(`cli/CHANGELOG.md section ## ${ctx.targetVersion} must contain an aggregate evidence line with 0 failures`);
152
+ }
153
+ return errors;
154
+ },
155
+ },
156
+ {
157
+ id: 'release_notes',
158
+ label: 'current release notes page',
159
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
160
+ check: validateCurrentReleaseDoc,
161
+ },
162
+ {
163
+ id: 'release_sidebar',
164
+ label: 'website-v2/sidebars.ts release autogen',
165
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
166
+ check(_ctx, repoRoot) {
167
+ const sidebars = read(repoRoot, 'website-v2/sidebars.ts');
168
+ return /label:\s*'Release Notes'[\s\S]*dirName:\s*'releases'/.test(sidebars)
169
+ ? []
170
+ : [`website-v2/sidebars.ts must keep Release Notes auto-generated from dirName: 'releases'`];
171
+ },
172
+ },
173
+ {
174
+ id: 'homepage_badge',
175
+ label: 'homepage hero badge version',
176
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
177
+ check(ctx, repoRoot) {
178
+ const home = read(repoRoot, 'website-v2/src/pages/index.tsx');
179
+ return home.includes(`v${ctx.targetVersion}`)
180
+ ? []
181
+ : [`website-v2/src/pages/index.tsx must mention v${ctx.targetVersion}`];
182
+ },
183
+ },
184
+ {
185
+ id: 'homepage_proof_stat',
186
+ label: 'homepage proof stat',
187
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
188
+ check(ctx, repoRoot) {
189
+ if (ctx.aggregateEvidenceCount == null) {
190
+ return ['homepage proof stat cannot be validated because CHANGELOG aggregate evidence is missing'];
191
+ }
192
+ const home = read(repoRoot, 'website-v2/src/pages/index.tsx');
193
+ const formatted = formatCount(ctx.aggregateEvidenceCount);
194
+ const errors = [];
195
+ if (!home.includes(`stat-number">${formatted}<`)) {
196
+ errors.push(`website-v2/src/pages/index.tsx must show homepage proof stat ${formatted}`);
197
+ }
198
+ if (!home.includes('Tests / 0 failures')) {
199
+ errors.push('website-v2/src/pages/index.tsx must keep the "Tests / 0 failures" proof label');
200
+ }
201
+ return errors;
202
+ },
203
+ },
204
+ {
205
+ id: 'capabilities_version',
206
+ label: '.agentxchain-conformance/capabilities.json version',
207
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
208
+ check(ctx, repoRoot) {
209
+ const capabilities = readJson(repoRoot, '.agentxchain-conformance/capabilities.json');
210
+ return capabilities.version === ctx.targetVersion
211
+ ? []
212
+ : [`.agentxchain-conformance/capabilities.json version is "${capabilities.version}", expected "${ctx.targetVersion}"`];
213
+ },
214
+ },
215
+ {
216
+ id: 'implementor_guide_version',
217
+ label: 'protocol implementor guide example version',
218
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
219
+ check(ctx, repoRoot) {
220
+ const guide = read(repoRoot, 'website-v2/docs/protocol-implementor-guide.mdx');
221
+ return guide.includes(`"version": "${ctx.targetVersion}"`)
222
+ ? []
223
+ : [`website-v2/docs/protocol-implementor-guide.mdx must contain "version": "${ctx.targetVersion}"`];
224
+ },
225
+ },
226
+ {
227
+ id: 'launch_evidence_report',
228
+ label: 'launch evidence report',
229
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
230
+ check(ctx, repoRoot) {
231
+ const report = read(repoRoot, '.planning/LAUNCH_EVIDENCE_REPORT.md');
232
+ const errors = [];
233
+ if (!report.match(new RegExp(`^# Launch Evidence Report — AgentXchain v${escapeRegExp(ctx.targetVersion)}`, 'm'))) {
234
+ errors.push(`.planning/LAUNCH_EVIDENCE_REPORT.md title must carry v${ctx.targetVersion}`);
235
+ }
236
+ if (ctx.aggregateEvidenceText && !normalizeEvidenceText(report).includes(ctx.aggregateEvidenceText)) {
237
+ errors.push(`.planning/LAUNCH_EVIDENCE_REPORT.md must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
238
+ }
239
+ return errors;
240
+ },
241
+ },
242
+ {
243
+ id: 'show_hn_draft',
244
+ label: 'show hn draft',
245
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
246
+ check: validateTextIncludesVersionAndEvidence('.planning/SHOW_HN_DRAFT.md', 'show hn draft').check,
247
+ },
248
+ {
249
+ id: 'twitter_thread',
250
+ label: 'twitter thread draft',
251
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
252
+ check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/TWITTER_THREAD.md', 'twitter thread draft').check,
253
+ },
254
+ {
255
+ id: 'reddit_posts',
256
+ label: 'reddit posts draft',
257
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
258
+ check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/REDDIT_POSTS.md', 'reddit posts draft').check,
259
+ },
260
+ {
261
+ id: 'hn_submission',
262
+ label: 'hn submission draft',
263
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
264
+ check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/HN_SUBMISSION.md', 'hn submission draft').check,
265
+ },
266
+ {
267
+ id: 'llms_release_route',
268
+ label: 'llms current release route',
269
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
270
+ check(ctx, repoRoot) {
271
+ const llms = read(repoRoot, 'website-v2/static/llms.txt');
272
+ return llms.includes(ctx.releaseRoute)
273
+ ? []
274
+ : [`website-v2/static/llms.txt must list ${ctx.releaseRoute}`];
275
+ },
276
+ },
277
+ {
278
+ id: 'homebrew_formula_url',
279
+ label: 'homebrew mirror formula url',
280
+ scopes: [RELEASE_ALIGNMENT_SCOPES.CURRENT],
281
+ check(ctx, repoRoot) {
282
+ const formula = read(repoRoot, 'cli/homebrew/agentxchain.rb');
283
+ return formula.includes(`url "${ctx.tarballUrl}"`)
284
+ ? []
285
+ : [`cli/homebrew/agentxchain.rb must point at ${ctx.tarballUrl}`];
286
+ },
287
+ },
288
+ {
289
+ id: 'homebrew_readme',
290
+ label: 'homebrew mirror readme',
291
+ scopes: [RELEASE_ALIGNMENT_SCOPES.CURRENT],
292
+ check(ctx, repoRoot) {
293
+ const readme = read(repoRoot, 'cli/homebrew/README.md');
294
+ const errors = [];
295
+ if (!readme.includes(`- version: \`${ctx.targetVersion}\``)) {
296
+ errors.push(`cli/homebrew/README.md must carry version ${ctx.targetVersion}`);
297
+ }
298
+ if (!readme.includes(`- source tarball: \`${ctx.tarballUrl}\``)) {
299
+ errors.push(`cli/homebrew/README.md must carry tarball ${ctx.tarballUrl}`);
300
+ }
301
+ return errors;
302
+ },
303
+ },
304
+ ];
305
+
306
+ export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELEASE_ALIGNMENT_SCOPES.CURRENT } = {}) {
307
+ if (!Object.values(RELEASE_ALIGNMENT_SCOPES).includes(scope)) {
308
+ throw new Error(`Unsupported release alignment scope "${scope}"`);
309
+ }
310
+
311
+ const context = getReleaseAlignmentContext(repoRoot, { targetVersion });
312
+ const surfaces = RELEASE_ALIGNMENT_SURFACES.filter((surface) => surface.scopes.includes(scope));
313
+ const errors = [];
314
+
315
+ for (const surface of surfaces) {
316
+ const surfaceErrors = surface.check(context, repoRoot) || [];
317
+ for (const error of surfaceErrors) {
318
+ errors.push({
319
+ surface_id: surface.id,
320
+ label: surface.label,
321
+ message: error,
322
+ });
323
+ }
324
+ }
325
+
326
+ return {
327
+ ok: errors.length === 0,
328
+ scope,
329
+ targetVersion: context.targetVersion,
330
+ packageVersion: context.packageVersion,
331
+ aggregateEvidenceLine: context.aggregateEvidenceLine,
332
+ checkedSurfaceCount: surfaces.length,
333
+ checkedSurfaceIds: surfaces.map((surface) => surface.id),
334
+ errors,
335
+ };
336
+ }