@lesliechan721/aw 0.1.0-beta.14 → 0.1.0-beta.16
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/src/commands/aw-capabilities.d.ts +1 -1
- package/dist/src/commands/aw-capabilities.js +1 -0
- package/dist/src/commands/aw-capabilities.js.map +1 -1
- package/dist/src/commands/review-inspection.d.ts +169 -0
- package/dist/src/commands/review-inspection.js +916 -0
- package/dist/src/commands/review-inspection.js.map +1 -0
- package/dist/src/commands/skill-eval.d.ts +5 -0
- package/dist/src/commands/skill-eval.js +86 -63
- package/dist/src/commands/skill-eval.js.map +1 -1
- package/dist/src/commands/status-v6.d.ts +2 -1
- package/dist/src/commands/status-v6.js +7 -1
- package/dist/src/commands/status-v6.js.map +1 -1
- package/dist/src/eval/manifest-v1.js +0 -5
- package/dist/src/eval/manifest-v1.js.map +1 -1
- package/dist/src/index.js +39 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/portable/commands.d.ts +1 -0
- package/dist/src/portable/commands.js +43 -1
- package/dist/src/portable/commands.js.map +1 -1
- package/dist/src/portable/materializer.d.ts +12 -0
- package/dist/src/portable/materializer.js +67 -41
- package/dist/src/portable/materializer.js.map +1 -1
- package/dist/src/review/eval-source-input.d.ts +43 -0
- package/dist/src/review/eval-source-input.js +207 -0
- package/dist/src/review/eval-source-input.js.map +1 -0
- package/dist/src/review/source-binding.d.ts +170 -0
- package/dist/src/review/source-binding.js +622 -0
- package/dist/src/review/source-binding.js.map +1 -0
- package/dist/src/tui/console-app-v4.js +523 -133
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +59 -37
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +15 -1
- package/dist/src/tui/controller.js +86 -3
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/model.js +2 -3
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-review-planner.js +22 -1
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/plugin-write-task.d.ts +2 -1
- package/dist/src/tui/plugin-write-task.js +17 -0
- package/dist/src/tui/plugin-write-task.js.map +1 -1
- package/dist/src/tui/router.d.ts +1 -0
- package/dist/src/tui/router.js +13 -5
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +21 -1
- package/dist/src/tui/screen-registry.js +5 -1
- package/dist/src/tui/screen-registry.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { computeCaseCorpusDigest, computeSkillDigest, } from '@lesliechan721/agent-skill-eval';
|
|
3
|
+
import { calculateCatalogWorkspaceSourceIdentity, DEFAULT_CATALOG_OUTPUT_PATH, GENERATED_OUTPUT_MARKER_FILENAME, readCatalogWorkspace, resolveSupportedPlatforms, validateCatalogSnapshotComplete, } from '@lesliechan721/aw-plugin-core';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { readAwSkillEvalManifest, } from '../eval/manifest-v1.js';
|
|
6
|
+
import { CLI_RESERVED_IMPORTS } from '../utils/manifest.js';
|
|
7
|
+
import { resolveCatalogWorkspaceRoot, } from './catalog-workspace-resolution.js';
|
|
8
|
+
import { computeReviewComponentDigest } from './review-publication.js';
|
|
9
|
+
import { computeAwSourceBindingSelectedCasesDigest, computeAwSourceBindingSuiteDefaultsDigest, readAwSourceBinding, resolveEvalProfileBindingInput, selectAwSourceBindingEvalCases, validateAwSourceBindingForWorkspace, } from '../review/source-binding.js';
|
|
10
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
11
|
+
function stripDotSlash(value) {
|
|
12
|
+
return value.startsWith('./') ? value.slice(2) : value;
|
|
13
|
+
}
|
|
14
|
+
function relativeWorkspacePath(workspaceRoot, filePath) {
|
|
15
|
+
return path.relative(workspaceRoot, filePath).split(path.sep).join('/') || '.';
|
|
16
|
+
}
|
|
17
|
+
function sourceKey(plugin, skill, kind) {
|
|
18
|
+
return `source:${plugin}:${skill}:${kind}`;
|
|
19
|
+
}
|
|
20
|
+
function requiredFinalKey(seed) {
|
|
21
|
+
return [
|
|
22
|
+
'final',
|
|
23
|
+
seed.plugin.manifest.name,
|
|
24
|
+
seed.skill,
|
|
25
|
+
seed.profile,
|
|
26
|
+
seed.targetId ?? 'static',
|
|
27
|
+
].join(':');
|
|
28
|
+
}
|
|
29
|
+
function materializationIdentityKey(options) {
|
|
30
|
+
return [
|
|
31
|
+
options.plugin,
|
|
32
|
+
options.skill,
|
|
33
|
+
options.profile,
|
|
34
|
+
options.materializationProfile.kind,
|
|
35
|
+
options.materializationProfile.id,
|
|
36
|
+
options.materializationProfile.inputDigest,
|
|
37
|
+
].join(':');
|
|
38
|
+
}
|
|
39
|
+
function scopeFromProfile(profile) {
|
|
40
|
+
return profile.endsWith('-project') ? 'project' : 'user';
|
|
41
|
+
}
|
|
42
|
+
async function directSkillDirectories(root) {
|
|
43
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const entry of entries.sort((left, right) => compareText(left.name, right.name))) {
|
|
46
|
+
const entryPath = path.join(root, entry.name);
|
|
47
|
+
const stat = await fs.lstat(entryPath);
|
|
48
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
49
|
+
throw new Error(`Skill collection direct child must be a regular directory: ${entryPath}`);
|
|
50
|
+
}
|
|
51
|
+
result.push({ name: entry.name, root: entryPath });
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function pluginImports(plugin) {
|
|
56
|
+
return [
|
|
57
|
+
...(plugin.manifest.buildConfig?.buildImports ?? []).map((entry) => ({
|
|
58
|
+
purpose: 'build',
|
|
59
|
+
provider: entry.plugin,
|
|
60
|
+
export: entry.export,
|
|
61
|
+
consumer: entry.env,
|
|
62
|
+
})),
|
|
63
|
+
...(plugin.manifest.buildConfig?.skillRuntimeFiles ?? []).flatMap((entry) => entry.import ? [{
|
|
64
|
+
purpose: 'skill-runtime-file',
|
|
65
|
+
provider: entry.import.plugin,
|
|
66
|
+
export: entry.import.export,
|
|
67
|
+
consumer: `${entry.skill}/${entry.target}`,
|
|
68
|
+
}] : []),
|
|
69
|
+
...(plugin.manifest.artifacts?.targets ?? []).flatMap((target) => (target.runtimeImports ?? []).map((entry) => ({
|
|
70
|
+
purpose: 'artifact-runtime',
|
|
71
|
+
provider: entry.plugin,
|
|
72
|
+
export: entry.export,
|
|
73
|
+
consumer: `${target.id}/${entry.name}`,
|
|
74
|
+
}))),
|
|
75
|
+
].sort((left, right) => compareText(`${left.purpose}\0${left.consumer}\0${left.provider}\0${left.export}`, `${right.purpose}\0${right.consumer}\0${right.provider}\0${right.export}`));
|
|
76
|
+
}
|
|
77
|
+
async function inspectSnapshot(workspace, workspaceSourceIdentity) {
|
|
78
|
+
const snapshotPath = path.join(workspace.workspaceRoot, DEFAULT_CATALOG_OUTPUT_PATH);
|
|
79
|
+
if (!await fs.pathExists(snapshotPath)) {
|
|
80
|
+
return {
|
|
81
|
+
receipt: {
|
|
82
|
+
path: snapshotPath,
|
|
83
|
+
state: 'absent',
|
|
84
|
+
catalogVersion: null,
|
|
85
|
+
snapshotDigest: null,
|
|
86
|
+
workspaceIdentity: null,
|
|
87
|
+
sourceIdentity: null,
|
|
88
|
+
},
|
|
89
|
+
entries: new Map(),
|
|
90
|
+
root: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const [validation, marker] = await Promise.all([
|
|
95
|
+
validateCatalogSnapshotComplete(snapshotPath),
|
|
96
|
+
fs.readJson(path.join(snapshotPath, GENERATED_OUTPUT_MARKER_FILENAME)),
|
|
97
|
+
]);
|
|
98
|
+
const current = marker.workspaceIdentity === workspace.workspaceIdentity
|
|
99
|
+
&& marker.sourceIdentity === workspaceSourceIdentity;
|
|
100
|
+
return {
|
|
101
|
+
receipt: {
|
|
102
|
+
path: snapshotPath,
|
|
103
|
+
state: current ? 'current' : 'stale',
|
|
104
|
+
catalogVersion: validation.manifest.catalogVersion,
|
|
105
|
+
snapshotDigest: validation.snapshotDigest,
|
|
106
|
+
workspaceIdentity: marker.workspaceIdentity,
|
|
107
|
+
sourceIdentity: marker.sourceIdentity,
|
|
108
|
+
},
|
|
109
|
+
entries: new Map(validation.manifest.entries.map((entry) => [entry.name, entry])),
|
|
110
|
+
root: current ? validation.rootDir : null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {
|
|
115
|
+
receipt: {
|
|
116
|
+
path: snapshotPath,
|
|
117
|
+
state: 'invalid',
|
|
118
|
+
catalogVersion: null,
|
|
119
|
+
snapshotDigest: null,
|
|
120
|
+
workspaceIdentity: null,
|
|
121
|
+
sourceIdentity: null,
|
|
122
|
+
},
|
|
123
|
+
entries: new Map(),
|
|
124
|
+
root: null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function addSourceSubject(options) {
|
|
129
|
+
const key = sourceKey(options.plugin.manifest.name, options.skill, options.kind);
|
|
130
|
+
if (options.subjects.some((subject) => subject.key === key))
|
|
131
|
+
return key;
|
|
132
|
+
const digest = options.kind === 'artifact-source'
|
|
133
|
+
? (await computeReviewComponentDigest({
|
|
134
|
+
repoRoot: options.workspace.workspaceRoot,
|
|
135
|
+
path: options.root,
|
|
136
|
+
})).digest
|
|
137
|
+
: await computeSkillDigest(options.root);
|
|
138
|
+
options.subjects.push({
|
|
139
|
+
key,
|
|
140
|
+
kind: options.kind,
|
|
141
|
+
plugin: options.plugin.manifest.name,
|
|
142
|
+
skill: options.skill,
|
|
143
|
+
path: options.root,
|
|
144
|
+
profile: null,
|
|
145
|
+
digest,
|
|
146
|
+
origin: 'workspace',
|
|
147
|
+
});
|
|
148
|
+
return key;
|
|
149
|
+
}
|
|
150
|
+
async function inventoryPluginSource(options) {
|
|
151
|
+
const { workspace, plugin, subjects } = options;
|
|
152
|
+
const nativeSkills = plugin.manifest.skills
|
|
153
|
+
? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.skills)))
|
|
154
|
+
: [];
|
|
155
|
+
const portableSkills = plugin.manifest.distributions?.agentSkills?.skills
|
|
156
|
+
? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.distributions.agentSkills.skills)))
|
|
157
|
+
: [];
|
|
158
|
+
const nativeByName = new Map(nativeSkills.map((entry) => [entry.name, entry]));
|
|
159
|
+
const portableByName = new Map(portableSkills.map((entry) => [entry.name, entry]));
|
|
160
|
+
const nativeDynamicByPlatform = new Map([
|
|
161
|
+
['claude', new Set()],
|
|
162
|
+
['codex', new Set()],
|
|
163
|
+
]);
|
|
164
|
+
const portableDynamic = new Set();
|
|
165
|
+
const sourceSubjectKeys = [];
|
|
166
|
+
const seeds = [];
|
|
167
|
+
for (const target of plugin.manifest.artifacts?.targets ?? []) {
|
|
168
|
+
if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
|
|
169
|
+
continue;
|
|
170
|
+
const skill = target.skillKey;
|
|
171
|
+
if (target.target === 'agent-skills.skill')
|
|
172
|
+
portableDynamic.add(skill);
|
|
173
|
+
else
|
|
174
|
+
nativeDynamicByPlatform.get(target.target === 'codex.skill' ? 'codex' : 'claude').add(skill);
|
|
175
|
+
}
|
|
176
|
+
for (const skill of nativeSkills) {
|
|
177
|
+
sourceSubjectKeys.push(await addSourceSubject({
|
|
178
|
+
workspace,
|
|
179
|
+
subjects,
|
|
180
|
+
plugin,
|
|
181
|
+
skill: skill.name,
|
|
182
|
+
root: skill.root,
|
|
183
|
+
kind: 'source-skill',
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
for (const skill of portableSkills) {
|
|
187
|
+
sourceSubjectKeys.push(await addSourceSubject({
|
|
188
|
+
workspace,
|
|
189
|
+
subjects,
|
|
190
|
+
plugin,
|
|
191
|
+
skill: skill.name,
|
|
192
|
+
root: skill.root,
|
|
193
|
+
kind: 'portable-source-skill',
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
for (const platform of [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText)) {
|
|
197
|
+
for (const skill of nativeSkills) {
|
|
198
|
+
if (nativeDynamicByPlatform.get(platform).has(skill.name))
|
|
199
|
+
continue;
|
|
200
|
+
for (const scope of ['project', 'user']) {
|
|
201
|
+
seeds.push({
|
|
202
|
+
plugin,
|
|
203
|
+
skill: skill.name,
|
|
204
|
+
profile: `${platform}-${scope}`,
|
|
205
|
+
targetId: null,
|
|
206
|
+
sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'source-skill'),
|
|
207
|
+
dynamic: false,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (plugin.manifest.distributions?.agentSkills) {
|
|
213
|
+
for (const skill of portableSkills) {
|
|
214
|
+
if (portableDynamic.has(skill.name))
|
|
215
|
+
continue;
|
|
216
|
+
for (const scope of ['project', 'user']) {
|
|
217
|
+
seeds.push({
|
|
218
|
+
plugin,
|
|
219
|
+
skill: skill.name,
|
|
220
|
+
profile: `agent-skills-${scope}`,
|
|
221
|
+
targetId: null,
|
|
222
|
+
sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'portable-source-skill'),
|
|
223
|
+
dynamic: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const target of plugin.manifest.artifacts?.targets ?? []) {
|
|
229
|
+
if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
|
|
230
|
+
continue;
|
|
231
|
+
const skill = target.skillKey;
|
|
232
|
+
const platform = target.target === 'agent-skills.skill'
|
|
233
|
+
? 'agent-skills'
|
|
234
|
+
: target.target === 'codex.skill' ? 'codex' : 'claude';
|
|
235
|
+
const sourceEntry = target.target === 'agent-skills.skill'
|
|
236
|
+
? portableByName.get(skill) ?? nativeByName.get(skill)
|
|
237
|
+
: nativeByName.get(skill);
|
|
238
|
+
let sourceSubjectKey = target.target === 'agent-skills.skill' && portableByName.has(skill)
|
|
239
|
+
? sourceKey(plugin.manifest.name, skill, 'portable-source-skill')
|
|
240
|
+
: sourceKey(plugin.manifest.name, skill, 'source-skill');
|
|
241
|
+
if (!sourceEntry) {
|
|
242
|
+
const declaredSource = target.sourceDirs?.[0];
|
|
243
|
+
const sourceRoot = declaredSource
|
|
244
|
+
? path.join(plugin.rootDir, stripDotSlash(declaredSource))
|
|
245
|
+
: path.join(plugin.rootDir, stripDotSlash(plugin.manifest.scripts?.artifacts?.render?.path ?? '.agents-plugin'));
|
|
246
|
+
sourceSubjectKey = await addSourceSubject({
|
|
247
|
+
workspace,
|
|
248
|
+
subjects,
|
|
249
|
+
plugin,
|
|
250
|
+
skill,
|
|
251
|
+
root: sourceRoot,
|
|
252
|
+
kind: 'artifact-source',
|
|
253
|
+
});
|
|
254
|
+
sourceSubjectKeys.push(sourceSubjectKey);
|
|
255
|
+
}
|
|
256
|
+
for (const scope of target.scopes) {
|
|
257
|
+
seeds.push({
|
|
258
|
+
plugin,
|
|
259
|
+
skill,
|
|
260
|
+
profile: `${platform}-${scope}`,
|
|
261
|
+
targetId: target.id,
|
|
262
|
+
sourceSubjectKey,
|
|
263
|
+
dynamic: true,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
seeds: seeds.sort((left, right) => compareText(requiredFinalKey(left), requiredFinalKey(right))),
|
|
269
|
+
sourceSubjectKeys: [...new Set(sourceSubjectKeys)].sort(compareText),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function releaseCases(suite, document, runs) {
|
|
273
|
+
const splits = new Set(suite.releaseSplits ?? []);
|
|
274
|
+
return document.cases
|
|
275
|
+
.filter((entry) => splits.size === 0 || splits.has('all') || splits.has(entry.split))
|
|
276
|
+
.map((entry) => ({ id: entry.id, split: entry.split, minimumRuns: runs }))
|
|
277
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
278
|
+
}
|
|
279
|
+
async function inventoryRequiredEvals(options) {
|
|
280
|
+
const required = [];
|
|
281
|
+
const caseDigests = new Map();
|
|
282
|
+
const bySkill = new Map();
|
|
283
|
+
for (const seed of options.seeds) {
|
|
284
|
+
const key = `${seed.plugin.manifest.name}\0${seed.skill}`;
|
|
285
|
+
bySkill.set(key, [...(bySkill.get(key) ?? []), seed]);
|
|
286
|
+
}
|
|
287
|
+
for (const seeds of bySkill.values()) {
|
|
288
|
+
const first = seeds[0];
|
|
289
|
+
const manifestPath = path.join(first.plugin.rootDir, 'skills', first.skill, 'evals', 'manifest.json');
|
|
290
|
+
if (!await fs.pathExists(manifestPath))
|
|
291
|
+
continue;
|
|
292
|
+
let loaded;
|
|
293
|
+
try {
|
|
294
|
+
loaded = await readAwSkillEvalManifest({ plugin: first.plugin, skill: first.skill });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
options.issues.push({
|
|
298
|
+
category: 'invalid',
|
|
299
|
+
code: 'evalManifestInvalid',
|
|
300
|
+
message: error instanceof Error ? error.message : String(error),
|
|
301
|
+
blocking: true,
|
|
302
|
+
path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const requiredSuites = loaded.manifest.suites.filter((suite) => suite.requiredForRelease === true);
|
|
307
|
+
const profileBindings = new Map();
|
|
308
|
+
const invalidProfileIds = new Set();
|
|
309
|
+
const codexSeeds = seeds.filter((seed) => seed.profile.startsWith('codex-'));
|
|
310
|
+
for (const suite of requiredSuites) {
|
|
311
|
+
const accepted = suite.profiles ? new Set(suite.profiles) : null;
|
|
312
|
+
const suiteProfiles = loaded.manifest.profiles.filter((profile) => !accepted || accepted.has(profile.id));
|
|
313
|
+
for (const profile of suiteProfiles) {
|
|
314
|
+
if (!profileBindings.has(profile.id) && !invalidProfileIds.has(profile.id)) {
|
|
315
|
+
try {
|
|
316
|
+
profileBindings.set(profile.id, resolveEvalProfileBindingInput(profile, options.workspace.plugins));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
invalidProfileIds.add(profile.id);
|
|
320
|
+
options.issues.push({
|
|
321
|
+
category: 'invalid',
|
|
322
|
+
code: 'evalProfileInvalid',
|
|
323
|
+
message: error instanceof Error ? error.message : String(error),
|
|
324
|
+
blocking: true,
|
|
325
|
+
path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const profileBinding = profileBindings.get(profile.id);
|
|
330
|
+
if (!profileBinding)
|
|
331
|
+
continue;
|
|
332
|
+
const matchingSeeds = codexSeeds.filter((seed) => scopeFromProfile(seed.profile) === profile.scope);
|
|
333
|
+
if (matchingSeeds.length === 0) {
|
|
334
|
+
options.issues.push({
|
|
335
|
+
category: 'unsupported',
|
|
336
|
+
code: 'requiredEvalTargetUnsupported',
|
|
337
|
+
message: `Required Eval ${first.plugin.manifest.name}/${first.skill}/${suite.id}/${profile.id} has no declared Codex ${profile.scope} final.`,
|
|
338
|
+
blocking: true,
|
|
339
|
+
path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const document = loaded.casesBySuite.get(suite.id);
|
|
344
|
+
const casesPath = path.join(loaded.evalRoot, suite.cases);
|
|
345
|
+
const runs = suite.runs ?? loaded.manifest.defaults.runs ?? 1;
|
|
346
|
+
const timeoutMs = suite.timeoutMs ?? loaded.manifest.defaults.timeoutMs ?? null;
|
|
347
|
+
let casesDigest = caseDigests.get(casesPath);
|
|
348
|
+
if (!casesDigest) {
|
|
349
|
+
casesDigest = await computeCaseCorpusDigest([casesPath]);
|
|
350
|
+
caseDigests.set(casesPath, casesDigest);
|
|
351
|
+
}
|
|
352
|
+
const minimumPassRate = typeof document.suite.runtime?.minimumPassRate === 'number'
|
|
353
|
+
? document.suite.runtime.minimumPassRate
|
|
354
|
+
: 1;
|
|
355
|
+
const selected = releaseCases(suite, document, runs);
|
|
356
|
+
const selectedDocument = selected.length === 0
|
|
357
|
+
? { ...document, cases: [] }
|
|
358
|
+
: selectAwSourceBindingEvalCases(document, {
|
|
359
|
+
caseIds: selected.map((entry) => entry.id),
|
|
360
|
+
});
|
|
361
|
+
for (const seed of matchingSeeds) {
|
|
362
|
+
const finalKey = requiredFinalKey(seed);
|
|
363
|
+
required.push({
|
|
364
|
+
key: `eval:${finalKey}:${profile.id}:${suite.id}`,
|
|
365
|
+
plugin: first.plugin.manifest.name,
|
|
366
|
+
skill: first.skill,
|
|
367
|
+
finalProfile: seed.profile,
|
|
368
|
+
requiredFinalKey: finalKey,
|
|
369
|
+
evalProfile: {
|
|
370
|
+
id: profile.id,
|
|
371
|
+
scope: profile.scope,
|
|
372
|
+
inputDigest: profileBinding.profile.inputDigest,
|
|
373
|
+
plugins: profileBinding.materializationProfile.plugins
|
|
374
|
+
.map((entry) => entry.name)
|
|
375
|
+
.sort(compareText),
|
|
376
|
+
},
|
|
377
|
+
suite: {
|
|
378
|
+
id: suite.id,
|
|
379
|
+
kind: suite.kind,
|
|
380
|
+
provider: suite.provider ?? null,
|
|
381
|
+
suiteDefaultsDigest: computeAwSourceBindingSuiteDefaultsDigest(suite, loaded.manifest.defaults),
|
|
382
|
+
},
|
|
383
|
+
cases: {
|
|
384
|
+
sourcePath: relativeWorkspacePath(options.workspace.workspaceRoot, casesPath),
|
|
385
|
+
sourceCorpusDigest: casesDigest,
|
|
386
|
+
selectedCasesDigest: computeAwSourceBindingSelectedCasesDigest(selectedDocument),
|
|
387
|
+
selected,
|
|
388
|
+
},
|
|
389
|
+
runs,
|
|
390
|
+
timeoutMs,
|
|
391
|
+
minimumPassRate,
|
|
392
|
+
processManagement: suite.releaseProcessManagement ?? 'managed-only',
|
|
393
|
+
materializedFinalKeys: [],
|
|
394
|
+
verifiedBindingPaths: [],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return required.sort((left, right) => compareText(left.key, right.key));
|
|
401
|
+
}
|
|
402
|
+
async function resolveTarget(options) {
|
|
403
|
+
if (options.target === 'catalog') {
|
|
404
|
+
return {
|
|
405
|
+
kind: 'catalog',
|
|
406
|
+
id: options.workspace.workspaceIdentity,
|
|
407
|
+
path: options.workspace.workspaceRoot,
|
|
408
|
+
plugins: options.workspace.plugins,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const named = options.workspace.plugins.find((plugin) => plugin.manifest.name === options.target);
|
|
412
|
+
if (named) {
|
|
413
|
+
return { kind: 'plugin', id: named.manifest.name, path: named.rootDir, plugins: [named] };
|
|
414
|
+
}
|
|
415
|
+
const candidate = path.resolve(options.cwd, options.target);
|
|
416
|
+
const stat = await fs.lstat(candidate).catch(() => null);
|
|
417
|
+
if (!stat?.isDirectory() || stat.isSymbolicLink()) {
|
|
418
|
+
throw new Error('Review inspection target must be a canonical Plugin or Catalog Workspace directory.');
|
|
419
|
+
}
|
|
420
|
+
const real = await fs.realpath(candidate);
|
|
421
|
+
if (real === options.workspace.workspaceRoot) {
|
|
422
|
+
return {
|
|
423
|
+
kind: 'catalog',
|
|
424
|
+
id: options.workspace.workspaceIdentity,
|
|
425
|
+
path: real,
|
|
426
|
+
plugins: options.workspace.plugins,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const plugin = options.workspace.plugins.find((entry) => entry.rootDir === real);
|
|
430
|
+
if (!plugin) {
|
|
431
|
+
throw new Error('Review inspection target is not a canonical Plugin in the selected Catalog Workspace.');
|
|
432
|
+
}
|
|
433
|
+
return { kind: 'plugin', id: plugin.manifest.name, path: real, plugins: [plugin] };
|
|
434
|
+
}
|
|
435
|
+
async function inspectMaterializations(options) {
|
|
436
|
+
const materializations = [];
|
|
437
|
+
const finals = new Map();
|
|
438
|
+
const targetPluginNames = new Set(options.targetPlugins.map((plugin) => plugin.manifest.name));
|
|
439
|
+
const context = {
|
|
440
|
+
workspace: options.workspace,
|
|
441
|
+
source: {
|
|
442
|
+
kind: 'workspace',
|
|
443
|
+
workspaceIdentity: options.workspace.workspaceIdentity,
|
|
444
|
+
sourceIdentity: options.workspaceSourceIdentity,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
for (const bindingPath of options.paths) {
|
|
448
|
+
let parsed = null;
|
|
449
|
+
try {
|
|
450
|
+
parsed = await readAwSourceBinding(bindingPath);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// The validator returns the public typed state and safe issue below.
|
|
454
|
+
}
|
|
455
|
+
const expectedSubject = parsed?.subject ?? {
|
|
456
|
+
plugin: options.targetPlugins[0]?.manifest.name ?? 'unknown',
|
|
457
|
+
};
|
|
458
|
+
const validation = await validateAwSourceBindingForWorkspace({
|
|
459
|
+
bindingPath,
|
|
460
|
+
context,
|
|
461
|
+
expectedSubject,
|
|
462
|
+
cwd: options.workspace.workspaceRoot,
|
|
463
|
+
env: options.env,
|
|
464
|
+
});
|
|
465
|
+
if (validation.state !== 'verified') {
|
|
466
|
+
materializations.push({
|
|
467
|
+
path: bindingPath,
|
|
468
|
+
state: validation.state,
|
|
469
|
+
plugin: validation.binding?.subject.plugin ?? null,
|
|
470
|
+
skill: validation.binding?.subject.skill ?? null,
|
|
471
|
+
profile: validation.binding
|
|
472
|
+
? validation.binding.profile.kind === 'eval'
|
|
473
|
+
? `codex-${validation.binding.profile.scope}`
|
|
474
|
+
: `agent-skills-${validation.binding.profile.scope}`
|
|
475
|
+
: null,
|
|
476
|
+
finalKeys: [],
|
|
477
|
+
verifiedBinding: null,
|
|
478
|
+
issues: validation.issues.map((issue) => issue.message),
|
|
479
|
+
});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const profile = validation.binding.profile.kind === 'eval'
|
|
483
|
+
? `codex-${validation.binding.profile.scope}`
|
|
484
|
+
: `agent-skills-${validation.binding.profile.scope}`;
|
|
485
|
+
const finalKeys = [];
|
|
486
|
+
for (const output of validation.outputs) {
|
|
487
|
+
if (output.kind !== 'eval-target' && output.kind !== 'portable-skill')
|
|
488
|
+
continue;
|
|
489
|
+
const outputPlugin = output.kind === 'portable-skill'
|
|
490
|
+
? output.plugin
|
|
491
|
+
: validation.binding.subject.plugin;
|
|
492
|
+
if (!targetPluginNames.has(outputPlugin))
|
|
493
|
+
continue;
|
|
494
|
+
const belongsToSelectedPlugin = options.requiredFinals.some((required) => required.plugin === outputPlugin
|
|
495
|
+
&& required.skill === output.skill
|
|
496
|
+
&& required.profile === profile);
|
|
497
|
+
if (!belongsToSelectedPlugin) {
|
|
498
|
+
options.issues.push({
|
|
499
|
+
category: 'unknown',
|
|
500
|
+
code: 'materializedOutputOwnerUnknown',
|
|
501
|
+
message: `The verified source binding does not contain a selected leaf for ${outputPlugin}/${output.skill}/${profile}.`,
|
|
502
|
+
blocking: false,
|
|
503
|
+
path: output.path,
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const identityKey = materializationIdentityKey({
|
|
508
|
+
plugin: outputPlugin,
|
|
509
|
+
skill: output.skill,
|
|
510
|
+
profile,
|
|
511
|
+
materializationProfile: validation.binding.profile,
|
|
512
|
+
});
|
|
513
|
+
const key = [
|
|
514
|
+
'materialized',
|
|
515
|
+
identityKey,
|
|
516
|
+
output.digest,
|
|
517
|
+
].join(':');
|
|
518
|
+
const existingSubject = options.subjects.find((subject) => subject.key === key);
|
|
519
|
+
if (existingSubject) {
|
|
520
|
+
if (compareText(output.path, existingSubject.path) < 0)
|
|
521
|
+
existingSubject.path = output.path;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
options.subjects.push({
|
|
525
|
+
key,
|
|
526
|
+
kind: 'materialized-skill',
|
|
527
|
+
plugin: outputPlugin,
|
|
528
|
+
skill: output.skill,
|
|
529
|
+
path: output.path,
|
|
530
|
+
profile,
|
|
531
|
+
digest: output.digest,
|
|
532
|
+
origin: 'source-binding',
|
|
533
|
+
materializationProfile: validation.binding.profile,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
finalKeys.push(key);
|
|
537
|
+
const existingFinal = finals.get(key);
|
|
538
|
+
if (existingFinal) {
|
|
539
|
+
if (compareText(output.path, existingFinal.path) < 0)
|
|
540
|
+
existingFinal.path = output.path;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
finals.set(key, {
|
|
544
|
+
key,
|
|
545
|
+
plugin: outputPlugin,
|
|
546
|
+
skill: output.skill,
|
|
547
|
+
profile,
|
|
548
|
+
materializationProfile: validation.binding.profile,
|
|
549
|
+
identityKey,
|
|
550
|
+
path: output.path,
|
|
551
|
+
digest: output.digest,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const materializationIssues = [];
|
|
556
|
+
if (finalKeys.length === 0) {
|
|
557
|
+
const message = 'The verified source binding does not contribute a final to the selected inspection target.';
|
|
558
|
+
materializationIssues.push(message);
|
|
559
|
+
options.issues.push({
|
|
560
|
+
category: 'unknown',
|
|
561
|
+
code: 'sourceBindingUnused',
|
|
562
|
+
message,
|
|
563
|
+
blocking: false,
|
|
564
|
+
path: bindingPath,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
materializations.push({
|
|
568
|
+
path: bindingPath,
|
|
569
|
+
state: 'verified',
|
|
570
|
+
plugin: validation.binding.subject.plugin,
|
|
571
|
+
skill: validation.binding.subject.skill ?? null,
|
|
572
|
+
profile,
|
|
573
|
+
finalKeys: [...new Set(finalKeys)].sort(compareText),
|
|
574
|
+
verifiedBinding: {
|
|
575
|
+
source: validation.binding.source,
|
|
576
|
+
subject: validation.binding.subject,
|
|
577
|
+
profile: validation.binding.profile,
|
|
578
|
+
evalInput: validation.binding.evalInput ?? null,
|
|
579
|
+
finalDigest: validation.binding.finalDigest,
|
|
580
|
+
artifacts: [...validation.outputs].sort((left, right) => compareText(`${left.kind}\0${'plugin' in left ? left.plugin : ''}\0${'skill' in left ? left.skill : ''}\0${left.path}`, `${right.kind}\0${'plugin' in right ? right.plugin : ''}\0${'skill' in right ? right.skill : ''}\0${right.path}`)),
|
|
581
|
+
},
|
|
582
|
+
issues: materializationIssues,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
materializations,
|
|
587
|
+
finals: [...finals.values()].sort((left, right) => compareText(left.key, right.key)),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
export async function inspectReviewTarget(options) {
|
|
591
|
+
const targetCandidate = options.target === 'catalog' ? null : path.resolve(options.cwd, options.target);
|
|
592
|
+
const targetCandidateStat = targetCandidate
|
|
593
|
+
? await fs.lstat(targetCandidate).catch(() => null)
|
|
594
|
+
: null;
|
|
595
|
+
const resolution = await resolveCatalogWorkspaceRoot({
|
|
596
|
+
cwd: options.workspaceRoot === undefined && targetCandidateStat?.isDirectory()
|
|
597
|
+
? targetCandidate
|
|
598
|
+
: options.cwd,
|
|
599
|
+
...(options.workspaceRoot === undefined ? {} : { explicitRoot: options.workspaceRoot }),
|
|
600
|
+
mode: 'existing',
|
|
601
|
+
});
|
|
602
|
+
const workspace = await readCatalogWorkspace(resolution.workspaceRoot, {
|
|
603
|
+
reservedImports: CLI_RESERVED_IMPORTS,
|
|
604
|
+
});
|
|
605
|
+
const workspaceSourceIdentity = await calculateCatalogWorkspaceSourceIdentity(workspace);
|
|
606
|
+
const [target, snapshot] = await Promise.all([
|
|
607
|
+
resolveTarget({ cwd: options.cwd, target: options.target, workspace }),
|
|
608
|
+
inspectSnapshot(workspace, workspaceSourceIdentity),
|
|
609
|
+
]);
|
|
610
|
+
const issues = [];
|
|
611
|
+
const subjects = [{
|
|
612
|
+
key: 'catalog:source',
|
|
613
|
+
kind: 'catalog-source',
|
|
614
|
+
plugin: null,
|
|
615
|
+
skill: null,
|
|
616
|
+
path: workspace.workspaceRoot,
|
|
617
|
+
profile: null,
|
|
618
|
+
digest: workspaceSourceIdentity,
|
|
619
|
+
origin: 'workspace',
|
|
620
|
+
}];
|
|
621
|
+
if (snapshot.receipt.state === 'current') {
|
|
622
|
+
subjects.push({
|
|
623
|
+
key: 'catalog:snapshot',
|
|
624
|
+
kind: 'catalog-snapshot',
|
|
625
|
+
plugin: null,
|
|
626
|
+
skill: null,
|
|
627
|
+
path: snapshot.receipt.path,
|
|
628
|
+
profile: null,
|
|
629
|
+
digest: snapshot.receipt.snapshotDigest,
|
|
630
|
+
origin: 'catalog-snapshot',
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
else if (snapshot.receipt.state !== 'absent') {
|
|
634
|
+
issues.push({
|
|
635
|
+
category: snapshot.receipt.state === 'invalid' ? 'invalid' : 'unknown',
|
|
636
|
+
code: snapshot.receipt.state === 'invalid' ? 'snapshotInvalid' : 'snapshotStale',
|
|
637
|
+
message: snapshot.receipt.state === 'invalid'
|
|
638
|
+
? 'The existing Catalog snapshot is invalid.'
|
|
639
|
+
: 'The existing Catalog snapshot is not bound to the current Workspace source.',
|
|
640
|
+
blocking: false,
|
|
641
|
+
path: relativeWorkspacePath(workspace.workspaceRoot, snapshot.receipt.path),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const seeds = [];
|
|
645
|
+
const pluginInputs = [];
|
|
646
|
+
for (const plugin of target.plugins) {
|
|
647
|
+
const sourceContainerKey = `plugin:${plugin.manifest.name}:source`;
|
|
648
|
+
const component = await computeReviewComponentDigest({
|
|
649
|
+
repoRoot: workspace.workspaceRoot,
|
|
650
|
+
path: plugin.rootDir,
|
|
651
|
+
});
|
|
652
|
+
subjects.push({
|
|
653
|
+
key: sourceContainerKey,
|
|
654
|
+
kind: 'aw-plugin-source',
|
|
655
|
+
plugin: plugin.manifest.name,
|
|
656
|
+
skill: null,
|
|
657
|
+
path: plugin.rootDir,
|
|
658
|
+
profile: null,
|
|
659
|
+
digest: component.digest,
|
|
660
|
+
origin: 'workspace',
|
|
661
|
+
});
|
|
662
|
+
const source = await inventoryPluginSource({ workspace, plugin, subjects });
|
|
663
|
+
seeds.push(...source.seeds);
|
|
664
|
+
pluginInputs.push({ plugin, sourceContainerKey, sourceSubjectKeys: source.sourceSubjectKeys });
|
|
665
|
+
}
|
|
666
|
+
const requiredFinals = seeds.map((seed) => ({
|
|
667
|
+
key: requiredFinalKey(seed),
|
|
668
|
+
kind: 'materialized-skill',
|
|
669
|
+
plugin: seed.plugin.manifest.name,
|
|
670
|
+
skill: seed.skill,
|
|
671
|
+
path: null,
|
|
672
|
+
profile: seed.profile,
|
|
673
|
+
digest: null,
|
|
674
|
+
targetId: seed.targetId,
|
|
675
|
+
sourceSubjectKey: seed.sourceSubjectKey,
|
|
676
|
+
status: 'missing',
|
|
677
|
+
matchedFinalKeys: [],
|
|
678
|
+
}));
|
|
679
|
+
const requiredByKey = new Map(requiredFinals.map((entry) => [entry.key, entry]));
|
|
680
|
+
if (snapshot.root) {
|
|
681
|
+
for (const seed of seeds.filter((entry) => !entry.dynamic && !entry.profile.startsWith('agent-skills-'))) {
|
|
682
|
+
const platform = seed.profile.startsWith('codex-') ? 'codex' : 'claude';
|
|
683
|
+
const finalPath = path.join(snapshot.root, 'plugins', seed.plugin.manifest.name, platform, 'skills', seed.skill);
|
|
684
|
+
try {
|
|
685
|
+
const digest = await computeSkillDigest(finalPath);
|
|
686
|
+
const key = `${requiredFinalKey(seed)}:snapshot`;
|
|
687
|
+
subjects.push({
|
|
688
|
+
key,
|
|
689
|
+
kind: 'materialized-skill',
|
|
690
|
+
plugin: seed.plugin.manifest.name,
|
|
691
|
+
skill: seed.skill,
|
|
692
|
+
path: finalPath,
|
|
693
|
+
profile: seed.profile,
|
|
694
|
+
digest,
|
|
695
|
+
origin: 'catalog-snapshot',
|
|
696
|
+
});
|
|
697
|
+
const required = requiredByKey.get(requiredFinalKey(seed));
|
|
698
|
+
required.path = finalPath;
|
|
699
|
+
required.digest = digest;
|
|
700
|
+
required.status = 'resolved';
|
|
701
|
+
required.matchedFinalKeys.push(key);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
const required = requiredByKey.get(requiredFinalKey(seed));
|
|
705
|
+
required.status = 'invalid';
|
|
706
|
+
issues.push({
|
|
707
|
+
category: 'invalid',
|
|
708
|
+
code: 'snapshotFinalInvalid',
|
|
709
|
+
message: error instanceof Error ? error.message : String(error),
|
|
710
|
+
blocking: true,
|
|
711
|
+
subjectKey: required.key,
|
|
712
|
+
path: relativeWorkspacePath(workspace.workspaceRoot, finalPath),
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const binding = await inspectMaterializations({
|
|
718
|
+
paths: options.materializations ?? [],
|
|
719
|
+
workspace,
|
|
720
|
+
workspaceSourceIdentity,
|
|
721
|
+
targetPlugins: target.plugins,
|
|
722
|
+
requiredFinals,
|
|
723
|
+
subjects,
|
|
724
|
+
issues,
|
|
725
|
+
env: options.env,
|
|
726
|
+
});
|
|
727
|
+
const bindingFinalsByRequiredKey = new Map();
|
|
728
|
+
for (const final of binding.finals) {
|
|
729
|
+
const candidates = requiredFinals.filter((required) => required.plugin === final.plugin
|
|
730
|
+
&& required.skill === final.skill
|
|
731
|
+
&& required.profile === final.profile);
|
|
732
|
+
if (candidates.length === 1) {
|
|
733
|
+
const required = candidates[0];
|
|
734
|
+
bindingFinalsByRequiredKey.set(required.key, [...(bindingFinalsByRequiredKey.get(required.key) ?? []), final]);
|
|
735
|
+
}
|
|
736
|
+
else if (candidates.length > 1) {
|
|
737
|
+
issues.push({
|
|
738
|
+
category: 'unknown',
|
|
739
|
+
code: 'materializedFinalAmbiguous',
|
|
740
|
+
message: `Materialized final ${final.plugin}/${final.skill}/${final.profile} matches multiple declarations.`,
|
|
741
|
+
blocking: true,
|
|
742
|
+
subjectKey: final.key,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const required of requiredFinals) {
|
|
747
|
+
const finals = bindingFinalsByRequiredKey.get(required.key) ?? [];
|
|
748
|
+
const byIdentity = new Map();
|
|
749
|
+
for (const final of finals) {
|
|
750
|
+
byIdentity.set(final.identityKey, [...(byIdentity.get(final.identityKey) ?? []), final]);
|
|
751
|
+
}
|
|
752
|
+
const ambiguous = [...byIdentity.entries()]
|
|
753
|
+
.filter(([, entries]) => new Set(entries.map((entry) => entry.digest)).size > 1)
|
|
754
|
+
.sort(([left], [right]) => compareText(left, right));
|
|
755
|
+
required.matchedFinalKeys = [...new Set([
|
|
756
|
+
...required.matchedFinalKeys,
|
|
757
|
+
...finals.map((final) => final.key),
|
|
758
|
+
])].sort(compareText);
|
|
759
|
+
if (ambiguous.length > 0) {
|
|
760
|
+
required.status = 'unknown';
|
|
761
|
+
required.path = null;
|
|
762
|
+
required.digest = null;
|
|
763
|
+
for (const [identity] of ambiguous) {
|
|
764
|
+
issues.push({
|
|
765
|
+
category: 'unknown',
|
|
766
|
+
code: 'materializedFinalDigestAmbiguous',
|
|
767
|
+
message: `One materialization identity produced different target digests for ${required.plugin}/${required.skill}/${required.profile}: ${identity}.`,
|
|
768
|
+
blocking: true,
|
|
769
|
+
subjectKey: required.key,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (finals.length > 0)
|
|
775
|
+
required.status = 'resolved';
|
|
776
|
+
if (required.matchedFinalKeys.length === 1) {
|
|
777
|
+
const subject = subjects.find((entry) => entry.key === required.matchedFinalKeys[0]);
|
|
778
|
+
required.path = subject.path;
|
|
779
|
+
required.digest = subject.digest;
|
|
780
|
+
}
|
|
781
|
+
else if (required.matchedFinalKeys.length > 1) {
|
|
782
|
+
required.path = null;
|
|
783
|
+
required.digest = null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for (const required of requiredFinals.filter((entry) => entry.status === 'missing')) {
|
|
787
|
+
issues.push({
|
|
788
|
+
category: 'missing',
|
|
789
|
+
code: 'requiredFinalMissing',
|
|
790
|
+
message: `No current final is available for ${required.plugin}/${required.skill}/${required.profile}.`,
|
|
791
|
+
blocking: true,
|
|
792
|
+
subjectKey: required.key,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const requiredEvals = await inventoryRequiredEvals({ workspace, seeds, issues });
|
|
796
|
+
for (const required of requiredEvals) {
|
|
797
|
+
required.materializedFinalKeys = binding.finals
|
|
798
|
+
.filter((final) => final.plugin === required.plugin
|
|
799
|
+
&& final.skill === required.skill
|
|
800
|
+
&& final.profile === required.finalProfile
|
|
801
|
+
&& final.materializationProfile.kind === 'eval'
|
|
802
|
+
&& final.materializationProfile.id === required.evalProfile.id
|
|
803
|
+
&& final.materializationProfile.inputDigest === required.evalProfile.inputDigest)
|
|
804
|
+
.map((final) => final.key)
|
|
805
|
+
.sort(compareText);
|
|
806
|
+
required.verifiedBindingPaths = [...new Set(binding.materializations
|
|
807
|
+
.filter((materialization) => materialization.state === 'verified'
|
|
808
|
+
&& materialization.verifiedBinding?.subject.plugin === required.plugin
|
|
809
|
+
&& materialization.verifiedBinding.subject.skill === required.skill
|
|
810
|
+
&& materialization.verifiedBinding.subject.suite === required.suite.id
|
|
811
|
+
&& materialization.verifiedBinding.profile.kind === 'eval'
|
|
812
|
+
&& materialization.verifiedBinding.profile.id === required.evalProfile.id
|
|
813
|
+
&& materialization.verifiedBinding.profile.inputDigest === required.evalProfile.inputDigest)
|
|
814
|
+
.map((materialization) => materialization.path))]
|
|
815
|
+
.sort(compareText);
|
|
816
|
+
if (required.materializedFinalKeys.length === 0) {
|
|
817
|
+
const snapshotFinal = requiredByKey.get(required.requiredFinalKey)?.matchedFinalKeys ?? [];
|
|
818
|
+
required.materializedFinalKeys = [...snapshotFinal];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const plugins = pluginInputs.map(({ plugin, sourceContainerKey, sourceSubjectKeys }) => {
|
|
822
|
+
const entry = snapshot.receipt.state === 'current' ? snapshot.entries.get(plugin.manifest.name) : undefined;
|
|
823
|
+
let finalContainerKey = null;
|
|
824
|
+
if (entry && snapshot.root) {
|
|
825
|
+
finalContainerKey = `plugin:${plugin.manifest.name}:snapshot`;
|
|
826
|
+
subjects.push({
|
|
827
|
+
key: finalContainerKey,
|
|
828
|
+
kind: 'aw-plugin',
|
|
829
|
+
plugin: plugin.manifest.name,
|
|
830
|
+
skill: null,
|
|
831
|
+
path: path.join(snapshot.root, 'plugins', plugin.manifest.name),
|
|
832
|
+
profile: null,
|
|
833
|
+
digest: entry.artifactDigest,
|
|
834
|
+
origin: 'catalog-snapshot',
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
const pluginFinals = requiredFinals.filter((required) => required.plugin === plugin.manifest.name);
|
|
838
|
+
const pluginEvals = requiredEvals.filter((required) => required.plugin === plugin.manifest.name);
|
|
839
|
+
return {
|
|
840
|
+
id: plugin.manifest.name,
|
|
841
|
+
name: plugin.manifest.name,
|
|
842
|
+
version: plugin.manifest.version,
|
|
843
|
+
path: plugin.rootDir,
|
|
844
|
+
sourceContainerKey,
|
|
845
|
+
finalContainerKey,
|
|
846
|
+
dependencies: [...(plugin.manifest.dependencies ?? [])].sort(compareText),
|
|
847
|
+
supportedPlatforms: [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText),
|
|
848
|
+
finalProfiles: [...new Set(pluginFinals.map((entry) => entry.profile))].sort(compareText),
|
|
849
|
+
imports: pluginImports(plugin),
|
|
850
|
+
snapshotEntry: entry ? {
|
|
851
|
+
artifactDigest: entry.artifactDigest,
|
|
852
|
+
manifestDigest: entry.manifestDigest,
|
|
853
|
+
contentDigest: entry.contentDigest,
|
|
854
|
+
} : null,
|
|
855
|
+
sourceSubjectKeys,
|
|
856
|
+
requiredFinalKeys: pluginFinals.map((entry) => entry.key).sort(compareText),
|
|
857
|
+
requiredEvalKeys: pluginEvals.map((entry) => entry.key).sort(compareText),
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
issues.push(...binding.materializations
|
|
861
|
+
.filter((entry) => entry.state !== 'verified')
|
|
862
|
+
.map((entry) => ({
|
|
863
|
+
category: entry.state === 'incomplete'
|
|
864
|
+
? 'missing'
|
|
865
|
+
: entry.state === 'unsupported' ? 'unsupported' : entry.state === 'drifted' ? 'unknown' : 'invalid',
|
|
866
|
+
code: `sourceBinding${entry.state[0].toUpperCase()}${entry.state.slice(1)}`,
|
|
867
|
+
message: entry.issues.join('; ') || `Source binding is ${entry.state}.`,
|
|
868
|
+
blocking: true,
|
|
869
|
+
path: entry.path,
|
|
870
|
+
})));
|
|
871
|
+
issues.sort((left, right) => compareText(`${left.code}\0${left.subjectKey ?? ''}\0${left.path ?? ''}`, `${right.code}\0${right.subjectKey ?? ''}\0${right.path ?? ''}`));
|
|
872
|
+
subjects.sort((left, right) => compareText(left.key, right.key));
|
|
873
|
+
const ok = !issues.some((issue) => issue.blocking);
|
|
874
|
+
return {
|
|
875
|
+
schemaVersion: 1,
|
|
876
|
+
command: 'review:inspect',
|
|
877
|
+
ok,
|
|
878
|
+
completeness: ok ? 'complete' : 'incomplete',
|
|
879
|
+
target: { kind: target.kind, id: target.id, path: target.path },
|
|
880
|
+
workspace: {
|
|
881
|
+
root: workspace.workspaceRoot,
|
|
882
|
+
resolvedFrom: resolution.workspaceRootResolvedFrom,
|
|
883
|
+
identity: workspace.workspaceIdentity,
|
|
884
|
+
sourceIdentity: workspaceSourceIdentity,
|
|
885
|
+
catalogVersion: workspace.metadata.catalog.version,
|
|
886
|
+
},
|
|
887
|
+
snapshot: snapshot.receipt,
|
|
888
|
+
plugins,
|
|
889
|
+
subjects,
|
|
890
|
+
requiredFinals,
|
|
891
|
+
finalMappings: requiredFinals.map((required) => ({
|
|
892
|
+
requiredFinalKey: required.key,
|
|
893
|
+
sourceSubjectKey: required.sourceSubjectKey,
|
|
894
|
+
finalSubjectKeys: [...required.matchedFinalKeys].sort(compareText),
|
|
895
|
+
status: required.status,
|
|
896
|
+
})),
|
|
897
|
+
requiredEvals,
|
|
898
|
+
materializations: binding.materializations,
|
|
899
|
+
issues,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
export function createReviewInspectionFailure(error, code = 'reviewInspectionFailed') {
|
|
903
|
+
return {
|
|
904
|
+
schemaVersion: 1,
|
|
905
|
+
command: 'review:inspect',
|
|
906
|
+
ok: false,
|
|
907
|
+
completeness: 'incomplete',
|
|
908
|
+
issues: [{
|
|
909
|
+
category: 'invalid',
|
|
910
|
+
code,
|
|
911
|
+
message: error instanceof Error ? error.message : String(error),
|
|
912
|
+
blocking: true,
|
|
913
|
+
}],
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
//# sourceMappingURL=review-inspection.js.map
|