@lesliechan721/aw 0.1.0-beta.50 → 0.1.0-beta.51

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.
@@ -1,13 +1,10 @@
1
1
  import path from 'node:path';
2
- import { canonicalJson, 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';
2
+ import { canonicalJson, computeSkillDigest } from '@lesliechan721/agent-skill-eval';
3
+ import { DEFAULT_CATALOG_OUTPUT_PATH, GENERATED_OUTPUT_MARKER_FILENAME, validateCatalogSnapshotComplete, } from '@lesliechan721/aw-plugin-core';
4
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, resolvePortablePluginProfileConfig, resolveEvalProfileBindingInput, selectAwSourceBindingEvalCases, validateAwSourceBindingForWorkspace, } from '../review/source-binding.js';
10
- import { resolveReviewDynamicSkillSource, resolveReviewSelection, reviewBindingMatchesSelection, } from '../review/review-selection.js';
5
+ import { readAwSkillEvalManifest } from '../eval/manifest-v1.js';
6
+ import { readAwSourceBinding, resolvePortablePluginProfileConfig, validateAwSourceBindingForWorkspace, } from '../review/source-binding.js';
7
+ import { resolveReviewSelection, reviewBindingMatchesSelection, } from '../review/review-selection.js';
11
8
  import { resolveEffectivePluginConfigFromStores, } from '../utils/plugin-config.js';
12
9
  export class ReviewInspectionInputError extends Error {
13
10
  code;
@@ -31,84 +28,6 @@ class ReviewInspectionResolvedTargetError extends Error {
31
28
  }
32
29
  }
33
30
  const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
34
- function stripDotSlash(value) {
35
- return value.startsWith('./') ? value.slice(2) : value;
36
- }
37
- function relativeWorkspacePath(workspaceRoot, filePath) {
38
- return path.relative(workspaceRoot, filePath).split(path.sep).join('/') || '.';
39
- }
40
- function sourceKey(options) {
41
- const sourcePaths = options.roots.map((root) => relativeWorkspacePath(options.workspaceRoot, root));
42
- const sourceIdentity = encodeURIComponent(JSON.stringify({
43
- primary: sourcePaths[0],
44
- sources: [...sourcePaths].sort(compareText),
45
- }));
46
- return `source:${options.plugin}:${options.skill}:${options.kind}:${sourceIdentity}`;
47
- }
48
- function requiredFinalKey(seed) {
49
- return [
50
- 'final',
51
- seed.plugin.manifest.name,
52
- seed.skill,
53
- seed.profile,
54
- seed.targetId ?? 'static',
55
- ].join(':');
56
- }
57
- function materializationIdentityKey(options) {
58
- return [
59
- options.plugin,
60
- options.skill,
61
- options.profile,
62
- options.materializationProfile.kind,
63
- options.materializationProfile.id,
64
- options.materializationProfile.inputDigest,
65
- ].join(':');
66
- }
67
- function scopeFromProfile(profile) {
68
- return profile.endsWith('-project') ? 'project' : 'user';
69
- }
70
- function finalProfileFromBinding(profile) {
71
- if (profile.kind === 'eval')
72
- return `codex-${profile.scope}`;
73
- if (profile.kind === 'portable')
74
- return `agent-skills-${profile.scope}`;
75
- return `${profile.platform}-${profile.scope}`;
76
- }
77
- async function directSkillDirectories(root) {
78
- const entries = await fs.readdir(root, { withFileTypes: true });
79
- const result = [];
80
- for (const entry of entries.sort((left, right) => compareText(left.name, right.name))) {
81
- const entryPath = path.join(root, entry.name);
82
- const stat = await fs.lstat(entryPath);
83
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
84
- throw new Error(`Skill collection direct child must be a regular directory: ${entryPath}`);
85
- }
86
- result.push({ name: entry.name, root: await fs.realpath(entryPath) });
87
- }
88
- return result;
89
- }
90
- function pluginImports(plugin) {
91
- return [
92
- ...(plugin.manifest.buildConfig?.buildImports ?? []).map((entry) => ({
93
- purpose: 'build',
94
- provider: entry.plugin,
95
- export: entry.export,
96
- consumer: entry.env,
97
- })),
98
- ...(plugin.manifest.buildConfig?.skillRuntimeFiles ?? []).flatMap((entry) => entry.import ? [{
99
- purpose: 'skill-runtime-file',
100
- provider: entry.import.plugin,
101
- export: entry.import.export,
102
- consumer: `${entry.skill}/${entry.target}`,
103
- }] : []),
104
- ...(plugin.manifest.artifacts?.targets ?? []).flatMap((target) => (target.runtimeImports ?? []).map((entry) => ({
105
- purpose: 'artifact-runtime',
106
- provider: entry.plugin,
107
- export: entry.export,
108
- consumer: `${target.id}/${entry.name}`,
109
- }))),
110
- ].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}`));
111
- }
112
31
  async function inspectSnapshot(workspace, workspaceSourceIdentity) {
113
32
  const snapshotPath = path.join(workspace.workspaceRoot, DEFAULT_CATALOG_OUTPUT_PATH);
114
33
  if (!await fs.pathExists(snapshotPath)) {
@@ -116,12 +35,7 @@ async function inspectSnapshot(workspace, workspaceSourceIdentity) {
116
35
  receipt: {
117
36
  path: snapshotPath,
118
37
  state: 'absent',
119
- catalogVersion: null,
120
- snapshotDigest: null,
121
- workspaceIdentity: null,
122
- sourceIdentity: null,
123
38
  },
124
- entries: new Map(),
125
39
  root: null,
126
40
  };
127
41
  }
@@ -136,12 +50,7 @@ async function inspectSnapshot(workspace, workspaceSourceIdentity) {
136
50
  receipt: {
137
51
  path: snapshotPath,
138
52
  state: current ? 'current' : 'stale',
139
- catalogVersion: validation.manifest.catalogVersion,
140
- snapshotDigest: validation.snapshotDigest,
141
- workspaceIdentity: marker.workspaceIdentity,
142
- sourceIdentity: marker.sourceIdentity,
143
53
  },
144
- entries: new Map(validation.manifest.entries.map((entry) => [entry.name, entry])),
145
54
  root: current ? validation.rootDir : null,
146
55
  };
147
56
  }
@@ -150,497 +59,11 @@ async function inspectSnapshot(workspace, workspaceSourceIdentity) {
150
59
  receipt: {
151
60
  path: snapshotPath,
152
61
  state: 'invalid',
153
- catalogVersion: null,
154
- snapshotDigest: null,
155
- workspaceIdentity: null,
156
- sourceIdentity: null,
157
62
  },
158
- entries: new Map(),
159
63
  root: null,
160
64
  };
161
65
  }
162
66
  }
163
- async function addSourceSubject(options) {
164
- const key = sourceKey({
165
- workspaceRoot: options.workspace.workspaceRoot,
166
- plugin: options.plugin.manifest.name,
167
- skill: options.skill,
168
- kind: options.kind,
169
- roots: options.sourceRoots ?? [options.root],
170
- });
171
- if (options.subjects.some((subject) => subject.key === key))
172
- return key;
173
- const digest = options.kind === 'artifact-source'
174
- ? (await computeReviewComponentDigest({
175
- repoRoot: options.workspace.workspaceRoot,
176
- path: options.root,
177
- })).digest
178
- : await computeSkillDigest(options.root);
179
- options.subjects.push({
180
- key,
181
- kind: options.kind,
182
- plugin: options.plugin.manifest.name,
183
- skill: options.skill,
184
- path: options.root,
185
- profile: null,
186
- digest,
187
- origin: 'workspace',
188
- });
189
- return key;
190
- }
191
- async function inventoryPluginSource(options) {
192
- const { workspace, plugin, subjects } = options;
193
- const nativeSkills = plugin.manifest.skills
194
- ? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.skills)))
195
- : [];
196
- const portableSkills = plugin.manifest.distributions?.agentSkills?.skills
197
- ? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.distributions.agentSkills.skills)))
198
- : [];
199
- const nativeDynamicByPlatform = new Map([
200
- ['claude', new Set()],
201
- ['codex', new Set()],
202
- ]);
203
- const portableDynamic = new Set();
204
- const sourceSubjectKeys = [];
205
- const seeds = [];
206
- for (const target of plugin.manifest.artifacts?.targets ?? []) {
207
- if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
208
- continue;
209
- const skill = target.skillKey;
210
- if (target.target === 'agent-skills.skill')
211
- portableDynamic.add(skill);
212
- else
213
- nativeDynamicByPlatform.get(target.target === 'codex.skill' ? 'codex' : 'claude').add(skill);
214
- }
215
- for (const skill of nativeSkills) {
216
- sourceSubjectKeys.push(await addSourceSubject({
217
- workspace,
218
- subjects,
219
- plugin,
220
- skill: skill.name,
221
- root: skill.root,
222
- kind: 'source-skill',
223
- }));
224
- }
225
- for (const skill of portableSkills) {
226
- sourceSubjectKeys.push(await addSourceSubject({
227
- workspace,
228
- subjects,
229
- plugin,
230
- skill: skill.name,
231
- root: skill.root,
232
- kind: 'portable-source-skill',
233
- }));
234
- }
235
- for (const platform of [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText)) {
236
- for (const skill of nativeSkills) {
237
- if (nativeDynamicByPlatform.get(platform).has(skill.name))
238
- continue;
239
- for (const scope of ['project', 'user']) {
240
- seeds.push({
241
- plugin,
242
- skill: skill.name,
243
- profile: `${platform}-${scope}`,
244
- targetId: null,
245
- sourceSubjectKey: sourceKey({
246
- workspaceRoot: workspace.workspaceRoot,
247
- plugin: plugin.manifest.name,
248
- skill: skill.name,
249
- kind: 'source-skill',
250
- roots: [skill.root],
251
- }),
252
- dynamic: false,
253
- });
254
- }
255
- }
256
- }
257
- if (plugin.manifest.distributions?.agentSkills) {
258
- for (const skill of portableSkills) {
259
- if (portableDynamic.has(skill.name))
260
- continue;
261
- for (const scope of ['project', 'user']) {
262
- seeds.push({
263
- plugin,
264
- skill: skill.name,
265
- profile: `agent-skills-${scope}`,
266
- targetId: null,
267
- sourceSubjectKey: sourceKey({
268
- workspaceRoot: workspace.workspaceRoot,
269
- plugin: plugin.manifest.name,
270
- skill: skill.name,
271
- kind: 'portable-source-skill',
272
- roots: [skill.root],
273
- }),
274
- dynamic: false,
275
- });
276
- }
277
- }
278
- }
279
- for (const target of plugin.manifest.artifacts?.targets ?? []) {
280
- if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
281
- continue;
282
- const skill = target.skillKey;
283
- const platform = target.target === 'agent-skills.skill'
284
- ? 'agent-skills'
285
- : target.target === 'codex.skill' ? 'codex' : 'claude';
286
- const source = await resolveReviewDynamicSkillSource({
287
- plugin,
288
- target,
289
- nativeSkillPaths: nativeSkills.map((entry) => entry.root),
290
- portableSkillPaths: portableSkills.map((entry) => entry.root),
291
- });
292
- const sourceSubjectKey = await addSourceSubject({
293
- workspace,
294
- subjects,
295
- plugin,
296
- skill,
297
- root: source.sourcePath,
298
- sourceRoots: source.sourcePaths,
299
- kind: source.sourceKind,
300
- });
301
- sourceSubjectKeys.push(sourceSubjectKey);
302
- for (const scope of target.scopes) {
303
- seeds.push({
304
- plugin,
305
- skill,
306
- profile: `${platform}-${scope}`,
307
- targetId: target.id,
308
- sourceSubjectKey,
309
- dynamic: true,
310
- });
311
- }
312
- }
313
- return {
314
- seeds: seeds.sort((left, right) => compareText(requiredFinalKey(left), requiredFinalKey(right))),
315
- sourceSubjectKeys: [...new Set(sourceSubjectKeys)].sort(compareText),
316
- };
317
- }
318
- function releaseCases(suite, document, runs) {
319
- const splits = new Set(suite.releaseSplits ?? []);
320
- return document.cases
321
- .filter((entry) => splits.size === 0 || splits.has('all') || splits.has(entry.split))
322
- .map((entry) => ({ id: entry.id, split: entry.split, minimumRuns: runs }))
323
- .sort((left, right) => compareText(left.id, right.id));
324
- }
325
- async function inventoryRequiredEvals(options) {
326
- const required = [];
327
- const caseDigests = new Map();
328
- const bySkill = new Map();
329
- for (const seed of options.seeds) {
330
- const key = `${seed.plugin.manifest.name}\0${seed.skill}`;
331
- bySkill.set(key, [...(bySkill.get(key) ?? []), seed]);
332
- }
333
- for (const seeds of bySkill.values()) {
334
- const first = seeds[0];
335
- const manifestPath = path.join(first.plugin.rootDir, 'skills', first.skill, 'evals', 'manifest.json');
336
- if (!await fs.pathExists(manifestPath))
337
- continue;
338
- let loaded;
339
- try {
340
- loaded = await readAwSkillEvalManifest({ plugin: first.plugin, skill: first.skill });
341
- }
342
- catch (error) {
343
- options.issues.push({
344
- category: 'invalid',
345
- code: 'evalManifestInvalid',
346
- message: error instanceof Error ? error.message : String(error),
347
- blocking: true,
348
- path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
349
- });
350
- continue;
351
- }
352
- const requiredSuites = loaded.manifest.suites.filter((suite) => suite.requiredForRelease === true);
353
- const profileBindings = new Map();
354
- const invalidProfileIds = new Set();
355
- const codexSeeds = seeds.filter((seed) => seed.profile.startsWith('codex-'));
356
- for (const suite of requiredSuites) {
357
- const accepted = suite.profiles ? new Set(suite.profiles) : null;
358
- const suiteProfiles = loaded.manifest.profiles.filter((profile) => !accepted || accepted.has(profile.id));
359
- for (const profile of suiteProfiles) {
360
- if (!profileBindings.has(profile.id) && !invalidProfileIds.has(profile.id)) {
361
- try {
362
- profileBindings.set(profile.id, resolveEvalProfileBindingInput(profile, options.workspace.plugins));
363
- }
364
- catch (error) {
365
- invalidProfileIds.add(profile.id);
366
- options.issues.push({
367
- category: 'invalid',
368
- code: 'evalProfileInvalid',
369
- message: error instanceof Error ? error.message : String(error),
370
- blocking: true,
371
- path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
372
- });
373
- }
374
- }
375
- const profileBinding = profileBindings.get(profile.id);
376
- if (!profileBinding)
377
- continue;
378
- const matchingSeeds = codexSeeds.filter((seed) => scopeFromProfile(seed.profile) === profile.scope);
379
- if (matchingSeeds.length === 0) {
380
- options.issues.push({
381
- category: 'unsupported',
382
- code: 'requiredEvalTargetUnsupported',
383
- message: `Required Eval ${first.plugin.manifest.name}/${first.skill}/${suite.id}/${profile.id} has no declared Codex ${profile.scope} final.`,
384
- blocking: true,
385
- path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
386
- });
387
- continue;
388
- }
389
- const document = loaded.casesBySuite.get(suite.id);
390
- const casesPath = path.join(loaded.evalRoot, suite.cases);
391
- const runs = suite.runs ?? loaded.manifest.defaults.runs ?? 1;
392
- const timeoutMs = suite.timeoutMs ?? loaded.manifest.defaults.timeoutMs ?? null;
393
- let casesDigest = caseDigests.get(casesPath);
394
- if (!casesDigest) {
395
- casesDigest = await computeCaseCorpusDigest([casesPath]);
396
- caseDigests.set(casesPath, casesDigest);
397
- }
398
- const minimumPassRate = typeof document.suite.runtime?.minimumPassRate === 'number'
399
- ? document.suite.runtime.minimumPassRate
400
- : 1;
401
- const selected = releaseCases(suite, document, runs);
402
- const selectedDocument = selected.length === 0
403
- ? { ...document, cases: [] }
404
- : selectAwSourceBindingEvalCases(document, {
405
- caseIds: selected.map((entry) => entry.id),
406
- });
407
- for (const seed of matchingSeeds) {
408
- const finalKey = requiredFinalKey(seed);
409
- required.push({
410
- key: `eval:${finalKey}:${profile.id}:${suite.id}`,
411
- plugin: first.plugin.manifest.name,
412
- skill: first.skill,
413
- finalProfile: seed.profile,
414
- requiredFinalKey: finalKey,
415
- evalProfile: {
416
- id: profile.id,
417
- scope: profile.scope,
418
- inputDigest: profileBinding.profile.inputDigest,
419
- plugins: profileBinding.materializationProfile.plugins
420
- .map((entry) => entry.name)
421
- .sort(compareText),
422
- },
423
- suite: {
424
- id: suite.id,
425
- kind: suite.kind,
426
- provider: suite.provider ?? null,
427
- suiteDefaultsDigest: computeAwSourceBindingSuiteDefaultsDigest(suite, loaded.manifest.defaults),
428
- },
429
- cases: {
430
- sourcePath: relativeWorkspacePath(options.workspace.workspaceRoot, casesPath),
431
- sourceCorpusDigest: casesDigest,
432
- selectedCasesDigest: computeAwSourceBindingSelectedCasesDigest(selectedDocument),
433
- selected,
434
- },
435
- runs,
436
- timeoutMs,
437
- minimumPassRate,
438
- processManagement: suite.releaseProcessManagement ?? 'managed-only',
439
- materializedFinalKeys: [],
440
- verifiedBindingPaths: [],
441
- });
442
- }
443
- }
444
- }
445
- }
446
- return required.sort((left, right) => compareText(left.key, right.key));
447
- }
448
- async function resolveTarget(options) {
449
- if (options.target === 'catalog') {
450
- return {
451
- kind: 'catalog',
452
- id: options.workspace.workspaceIdentity,
453
- path: options.workspace.workspaceRoot,
454
- plugins: options.workspace.plugins,
455
- };
456
- }
457
- const named = options.workspace.plugins.find((plugin) => plugin.manifest.name === options.target);
458
- if (named) {
459
- return { kind: 'plugin', id: named.manifest.name, path: named.rootDir, plugins: [named] };
460
- }
461
- const candidate = path.resolve(options.cwd, options.target);
462
- const stat = await fs.lstat(candidate).catch(() => null);
463
- if (!stat) {
464
- throw new ReviewInspectionInputError('targetNotFound', `Review target was not found: ${candidate}`);
465
- }
466
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
467
- throw new ReviewInspectionInputError('invalidArguments', 'The release view requires a canonical Plugin or Catalog Workspace directory.');
468
- }
469
- const real = await fs.realpath(candidate);
470
- if (real === options.workspace.workspaceRoot) {
471
- return {
472
- kind: 'catalog',
473
- id: options.workspace.workspaceIdentity,
474
- path: real,
475
- plugins: options.workspace.plugins,
476
- };
477
- }
478
- const plugin = options.workspace.plugins.find((entry) => entry.rootDir === real);
479
- if (!plugin) {
480
- throw new ReviewInspectionInputError('invalidArguments', 'The release view requires the complete Plugin or Catalog target.');
481
- }
482
- return { kind: 'plugin', id: plugin.manifest.name, path: real, plugins: [plugin] };
483
- }
484
- async function inspectMaterializations(options) {
485
- const materializations = [];
486
- const finals = new Map();
487
- const targetPluginNames = new Set(options.targetPlugins.map((plugin) => plugin.manifest.name));
488
- const context = {
489
- workspace: options.workspace,
490
- source: {
491
- kind: 'workspace',
492
- workspaceIdentity: options.workspace.workspaceIdentity,
493
- sourceIdentity: options.workspaceSourceIdentity,
494
- },
495
- };
496
- const bindingPaths = [...new Set(options.paths.map((entry) => path.resolve(entry)))].sort(compareText);
497
- for (const bindingPath of bindingPaths) {
498
- let parsed = null;
499
- try {
500
- parsed = await readAwSourceBinding(bindingPath);
501
- }
502
- catch {
503
- // The validator returns the public typed state and safe issue below.
504
- }
505
- const expectedSubject = parsed?.subject ?? {
506
- plugin: options.targetPlugins[0]?.manifest.name ?? 'unknown',
507
- };
508
- const validation = await validateAwSourceBindingForWorkspace({
509
- bindingPath,
510
- context,
511
- expectedSubject,
512
- cwd: options.workspace.workspaceRoot,
513
- env: options.env,
514
- });
515
- if (validation.state !== 'verified') {
516
- materializations.push({
517
- path: bindingPath,
518
- state: validation.state,
519
- plugin: validation.binding?.subject.plugin ?? null,
520
- skill: validation.binding?.subject.skill ?? null,
521
- profile: validation.binding ? finalProfileFromBinding(validation.binding.profile) : null,
522
- finalKeys: [],
523
- verifiedBinding: null,
524
- issues: validation.issues.map((issue) => issue.message),
525
- });
526
- continue;
527
- }
528
- const profile = finalProfileFromBinding(validation.binding.profile);
529
- const finalKeys = [];
530
- for (const output of validation.outputs) {
531
- if (output.kind !== 'eval-target'
532
- && output.kind !== 'portable-skill'
533
- && output.kind !== 'review-skill')
534
- continue;
535
- const outputPlugin = output.kind === 'portable-skill' || output.kind === 'review-skill'
536
- ? output.plugin
537
- : validation.binding.subject.plugin;
538
- if (!targetPluginNames.has(outputPlugin))
539
- continue;
540
- const compatibleRequired = options.requiredFinals.filter((required) => required.plugin === outputPlugin
541
- && required.skill === output.skill
542
- && required.profile === profile
543
- && (validation.binding.profile.kind !== 'eval'
544
- || required.targetId === null
545
- || options.targetPlugins
546
- .find((plugin) => plugin.manifest.name === required.plugin)
547
- ?.manifest.artifacts?.targets
548
- ?.find((target) => target.id === required.targetId)
549
- ?.delivery?.mode !== 'codex-native'));
550
- if (compatibleRequired.length === 0) {
551
- options.issues.push({
552
- category: 'unknown',
553
- code: 'materializedOutputOwnerUnknown',
554
- message: `The verified source binding does not contain a selected leaf for ${outputPlugin}/${output.skill}/${profile}.`,
555
- blocking: false,
556
- path: output.path,
557
- });
558
- continue;
559
- }
560
- const identityKey = materializationIdentityKey({
561
- plugin: outputPlugin,
562
- skill: output.skill,
563
- profile,
564
- materializationProfile: validation.binding.profile,
565
- });
566
- const key = [
567
- 'materialized',
568
- identityKey,
569
- output.digest,
570
- ].join(':');
571
- const existingSubject = options.subjects.find((subject) => subject.key === key);
572
- if (existingSubject) {
573
- if (compareText(output.path, existingSubject.path) < 0)
574
- existingSubject.path = output.path;
575
- }
576
- else {
577
- options.subjects.push({
578
- key,
579
- kind: 'materialized-skill',
580
- plugin: outputPlugin,
581
- skill: output.skill,
582
- path: output.path,
583
- profile,
584
- digest: output.digest,
585
- origin: 'source-binding',
586
- materializationProfile: validation.binding.profile,
587
- });
588
- }
589
- finalKeys.push(key);
590
- const existingFinal = finals.get(key);
591
- if (existingFinal) {
592
- if (compareText(output.path, existingFinal.path) < 0)
593
- existingFinal.path = output.path;
594
- }
595
- else {
596
- finals.set(key, {
597
- key,
598
- plugin: outputPlugin,
599
- skill: output.skill,
600
- profile,
601
- materializationProfile: validation.binding.profile,
602
- identityKey,
603
- path: output.path,
604
- digest: output.digest,
605
- requiredKeys: compatibleRequired.map((required) => required.key).sort(compareText),
606
- });
607
- }
608
- }
609
- const materializationIssues = [];
610
- if (finalKeys.length === 0) {
611
- const message = 'The verified source binding does not contribute a final to the selected inspection target.';
612
- materializationIssues.push(message);
613
- options.issues.push({
614
- category: 'unknown',
615
- code: 'sourceBindingUnused',
616
- message,
617
- blocking: false,
618
- path: bindingPath,
619
- });
620
- }
621
- materializations.push({
622
- path: bindingPath,
623
- state: 'verified',
624
- plugin: validation.binding.subject.plugin,
625
- skill: validation.binding.subject.skill ?? null,
626
- profile,
627
- finalKeys: [...new Set(finalKeys)].sort(compareText),
628
- verifiedBinding: {
629
- source: validation.binding.source,
630
- subject: validation.binding.subject,
631
- profile: validation.binding.profile,
632
- evalInput: validation.binding.evalInput ?? null,
633
- finalDigest: validation.binding.finalDigest,
634
- 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}`)),
635
- },
636
- issues: materializationIssues,
637
- });
638
- }
639
- return {
640
- materializations,
641
- finals: [...finals.values()].sort((left, right) => compareText(left.key, right.key)),
642
- };
643
- }
644
67
  function sameResolvedConfig(left, right) {
645
68
  return canonicalJson({ values: left.values, sources: left.sources })
646
69
  === canonicalJson({ values: right.values, sources: right.sources });
@@ -834,7 +257,7 @@ async function inspectNavigationBindings(options) {
834
257
  function navigationSkillKey(skill, profile) {
835
258
  return `${skill.pluginName}\0${skill.name}\0${profile.platform}\0${profile.scope}`;
836
259
  }
837
- async function inspectNavigationTarget(options) {
260
+ export async function inspectReviewTarget(options) {
838
261
  const selection = await resolveReviewSelection({
839
262
  cwd: options.cwd,
840
263
  target: options.target,
@@ -952,338 +375,6 @@ async function inspectNavigationTarget(options) {
952
375
  });
953
376
  }
954
377
  }
955
- async function inspectReleaseTarget(options) {
956
- const targetCandidate = options.target === 'catalog' ? null : path.resolve(options.cwd, options.target);
957
- const targetCandidateStat = targetCandidate
958
- ? await fs.lstat(targetCandidate).catch(() => null)
959
- : null;
960
- if (targetCandidateStat && (!targetCandidateStat.isDirectory() || targetCandidateStat.isSymbolicLink())) {
961
- throw new ReviewInspectionInputError('invalidArguments', 'The release view requires a canonical Plugin or Catalog Workspace directory.');
962
- }
963
- const resolution = await resolveCatalogWorkspaceRoot({
964
- cwd: options.workspaceRoot === undefined && targetCandidateStat?.isDirectory()
965
- ? targetCandidate
966
- : options.cwd,
967
- ...(options.workspaceRoot === undefined ? {} : { explicitRoot: options.workspaceRoot }),
968
- mode: 'existing',
969
- });
970
- const workspace = await readCatalogWorkspace(resolution.workspaceRoot, {
971
- reservedImports: CLI_RESERVED_IMPORTS,
972
- });
973
- const workspaceSourceIdentity = await calculateCatalogWorkspaceSourceIdentity(workspace);
974
- const [target, snapshot] = await Promise.all([
975
- resolveTarget({ cwd: options.cwd, target: options.target, workspace }),
976
- inspectSnapshot(workspace, workspaceSourceIdentity),
977
- ]);
978
- const issues = [];
979
- const subjects = [{
980
- key: 'catalog:source',
981
- kind: 'catalog-source',
982
- plugin: null,
983
- skill: null,
984
- path: workspace.workspaceRoot,
985
- profile: null,
986
- digest: workspaceSourceIdentity,
987
- origin: 'workspace',
988
- }];
989
- if (snapshot.receipt.state === 'current') {
990
- subjects.push({
991
- key: 'catalog:snapshot',
992
- kind: 'catalog-snapshot',
993
- plugin: null,
994
- skill: null,
995
- path: snapshot.receipt.path,
996
- profile: null,
997
- digest: snapshot.receipt.snapshotDigest,
998
- origin: 'catalog-snapshot',
999
- });
1000
- }
1001
- else if (snapshot.receipt.state !== 'absent') {
1002
- issues.push({
1003
- category: snapshot.receipt.state === 'invalid' ? 'invalid' : 'unknown',
1004
- code: snapshot.receipt.state === 'invalid' ? 'snapshotInvalid' : 'snapshotStale',
1005
- message: snapshot.receipt.state === 'invalid'
1006
- ? 'The existing Catalog snapshot is invalid.'
1007
- : 'The existing Catalog snapshot is not bound to the current Workspace source.',
1008
- blocking: false,
1009
- path: relativeWorkspacePath(workspace.workspaceRoot, snapshot.receipt.path),
1010
- });
1011
- }
1012
- const seeds = [];
1013
- const pluginInputs = [];
1014
- for (const plugin of target.plugins) {
1015
- const sourceContainerKey = `plugin:${plugin.manifest.name}:source`;
1016
- const component = await computeReviewComponentDigest({
1017
- repoRoot: workspace.workspaceRoot,
1018
- path: plugin.rootDir,
1019
- });
1020
- subjects.push({
1021
- key: sourceContainerKey,
1022
- kind: 'aw-plugin-source',
1023
- plugin: plugin.manifest.name,
1024
- skill: null,
1025
- path: plugin.rootDir,
1026
- profile: null,
1027
- digest: component.digest,
1028
- origin: 'workspace',
1029
- });
1030
- const source = await inventoryPluginSource({ workspace, plugin, subjects });
1031
- seeds.push(...source.seeds);
1032
- pluginInputs.push({ plugin, sourceContainerKey, sourceSubjectKeys: source.sourceSubjectKeys });
1033
- }
1034
- const requiredFinals = seeds.map((seed) => ({
1035
- key: requiredFinalKey(seed),
1036
- kind: 'materialized-skill',
1037
- plugin: seed.plugin.manifest.name,
1038
- skill: seed.skill,
1039
- path: null,
1040
- profile: seed.profile,
1041
- digest: null,
1042
- targetId: seed.targetId,
1043
- sourceSubjectKey: seed.sourceSubjectKey,
1044
- status: 'missing',
1045
- matchedFinalKeys: [],
1046
- }));
1047
- const requiredByKey = new Map(requiredFinals.map((entry) => [entry.key, entry]));
1048
- if (snapshot.root) {
1049
- for (const seed of seeds.filter((entry) => !entry.dynamic && !entry.profile.startsWith('agent-skills-'))) {
1050
- const platform = seed.profile.startsWith('codex-') ? 'codex' : 'claude';
1051
- const finalPath = path.join(snapshot.root, 'plugins', seed.plugin.manifest.name, platform, 'skills', seed.skill);
1052
- try {
1053
- const digest = await computeSkillDigest(finalPath);
1054
- const key = `${requiredFinalKey(seed)}:snapshot`;
1055
- subjects.push({
1056
- key,
1057
- kind: 'materialized-skill',
1058
- plugin: seed.plugin.manifest.name,
1059
- skill: seed.skill,
1060
- path: finalPath,
1061
- profile: seed.profile,
1062
- digest,
1063
- origin: 'catalog-snapshot',
1064
- });
1065
- const required = requiredByKey.get(requiredFinalKey(seed));
1066
- required.path = finalPath;
1067
- required.digest = digest;
1068
- required.status = 'resolved';
1069
- required.matchedFinalKeys.push(key);
1070
- }
1071
- catch (error) {
1072
- const required = requiredByKey.get(requiredFinalKey(seed));
1073
- required.status = 'invalid';
1074
- issues.push({
1075
- category: 'invalid',
1076
- code: 'snapshotFinalInvalid',
1077
- message: error instanceof Error ? error.message : String(error),
1078
- blocking: true,
1079
- subjectKey: required.key,
1080
- path: relativeWorkspacePath(workspace.workspaceRoot, finalPath),
1081
- });
1082
- }
1083
- }
1084
- }
1085
- const binding = await inspectMaterializations({
1086
- paths: options.materializations ?? [],
1087
- workspace,
1088
- workspaceSourceIdentity,
1089
- targetPlugins: target.plugins,
1090
- requiredFinals,
1091
- subjects,
1092
- issues,
1093
- env: options.env,
1094
- });
1095
- const bindingFinalsByRequiredKey = new Map();
1096
- for (const final of binding.finals) {
1097
- const candidates = requiredFinals.filter((required) => required.plugin === final.plugin
1098
- && required.skill === final.skill
1099
- && required.profile === final.profile
1100
- && final.requiredKeys.includes(required.key));
1101
- if (candidates.length === 1) {
1102
- const required = candidates[0];
1103
- bindingFinalsByRequiredKey.set(required.key, [...(bindingFinalsByRequiredKey.get(required.key) ?? []), final]);
1104
- }
1105
- else if (candidates.length > 1) {
1106
- issues.push({
1107
- category: 'unknown',
1108
- code: 'materializedFinalAmbiguous',
1109
- message: `Materialized final ${final.plugin}/${final.skill}/${final.profile} matches multiple declarations.`,
1110
- blocking: true,
1111
- subjectKey: final.key,
1112
- });
1113
- }
1114
- }
1115
- for (const required of requiredFinals) {
1116
- const finals = bindingFinalsByRequiredKey.get(required.key) ?? [];
1117
- const byIdentity = new Map();
1118
- for (const final of finals) {
1119
- byIdentity.set(final.identityKey, [...(byIdentity.get(final.identityKey) ?? []), final]);
1120
- }
1121
- const ambiguous = [...byIdentity.entries()]
1122
- .filter(([, entries]) => new Set(entries.map((entry) => entry.digest)).size > 1)
1123
- .sort(([left], [right]) => compareText(left, right));
1124
- required.matchedFinalKeys = [...new Set([
1125
- ...required.matchedFinalKeys,
1126
- ...finals.map((final) => final.key),
1127
- ])].sort(compareText);
1128
- if (ambiguous.length > 0) {
1129
- required.status = 'unknown';
1130
- required.path = null;
1131
- required.digest = null;
1132
- for (const [identity] of ambiguous) {
1133
- issues.push({
1134
- category: 'unknown',
1135
- code: 'materializedFinalDigestAmbiguous',
1136
- message: `One materialization identity produced different target digests for ${required.plugin}/${required.skill}/${required.profile}: ${identity}.`,
1137
- blocking: true,
1138
- subjectKey: required.key,
1139
- });
1140
- }
1141
- continue;
1142
- }
1143
- if (finals.length > 0)
1144
- required.status = 'resolved';
1145
- if (required.matchedFinalKeys.length === 1) {
1146
- const subject = subjects.find((entry) => entry.key === required.matchedFinalKeys[0]);
1147
- required.path = subject.path;
1148
- required.digest = subject.digest;
1149
- }
1150
- else if (required.matchedFinalKeys.length > 1) {
1151
- required.path = null;
1152
- required.digest = null;
1153
- }
1154
- }
1155
- for (const required of requiredFinals.filter((entry) => entry.status === 'missing')) {
1156
- issues.push({
1157
- category: 'missing',
1158
- code: 'requiredFinalMissing',
1159
- message: `No current final is available for ${required.plugin}/${required.skill}/${required.profile}.`,
1160
- blocking: false,
1161
- subjectKey: required.key,
1162
- });
1163
- }
1164
- const requiredEvals = await inventoryRequiredEvals({ workspace, seeds, issues });
1165
- for (const required of requiredEvals) {
1166
- required.materializedFinalKeys = binding.finals
1167
- .filter((final) => final.plugin === required.plugin
1168
- && final.skill === required.skill
1169
- && final.profile === required.finalProfile
1170
- && final.materializationProfile.kind === 'eval'
1171
- && final.materializationProfile.id === required.evalProfile.id
1172
- && final.materializationProfile.inputDigest === required.evalProfile.inputDigest)
1173
- .map((final) => final.key)
1174
- .sort(compareText);
1175
- required.verifiedBindingPaths = [...new Set(binding.materializations
1176
- .filter((materialization) => materialization.state === 'verified'
1177
- && materialization.verifiedBinding?.subject.plugin === required.plugin
1178
- && materialization.verifiedBinding.subject.skill === required.skill
1179
- && materialization.verifiedBinding.subject.suite === required.suite.id
1180
- && materialization.verifiedBinding.profile.kind === 'eval'
1181
- && materialization.verifiedBinding.profile.id === required.evalProfile.id
1182
- && materialization.verifiedBinding.profile.inputDigest === required.evalProfile.inputDigest)
1183
- .map((materialization) => materialization.path))]
1184
- .sort(compareText);
1185
- if (required.materializedFinalKeys.length === 0) {
1186
- const snapshotFinal = (requiredByKey.get(required.requiredFinalKey)?.matchedFinalKeys ?? [])
1187
- .filter((key) => subjects.some((subject) => subject.key === key && subject.origin === 'catalog-snapshot'));
1188
- required.materializedFinalKeys = [...snapshotFinal];
1189
- }
1190
- }
1191
- const plugins = pluginInputs.map(({ plugin, sourceContainerKey, sourceSubjectKeys }) => {
1192
- const entry = snapshot.receipt.state === 'current' ? snapshot.entries.get(plugin.manifest.name) : undefined;
1193
- let finalContainerKey = null;
1194
- if (entry && snapshot.root) {
1195
- finalContainerKey = `plugin:${plugin.manifest.name}:snapshot`;
1196
- subjects.push({
1197
- key: finalContainerKey,
1198
- kind: 'aw-plugin',
1199
- plugin: plugin.manifest.name,
1200
- skill: null,
1201
- path: path.join(snapshot.root, 'plugins', plugin.manifest.name),
1202
- profile: null,
1203
- digest: entry.artifactDigest,
1204
- origin: 'catalog-snapshot',
1205
- });
1206
- }
1207
- const pluginFinals = requiredFinals.filter((required) => required.plugin === plugin.manifest.name);
1208
- const pluginEvals = requiredEvals.filter((required) => required.plugin === plugin.manifest.name);
1209
- return {
1210
- id: plugin.manifest.name,
1211
- name: plugin.manifest.name,
1212
- version: plugin.manifest.version,
1213
- path: plugin.rootDir,
1214
- sourceContainerKey,
1215
- finalContainerKey,
1216
- dependencies: [...(plugin.manifest.dependencies ?? [])].sort(compareText),
1217
- supportedPlatforms: [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText),
1218
- finalProfiles: [...new Set(pluginFinals.map((entry) => entry.profile))].sort(compareText),
1219
- imports: pluginImports(plugin),
1220
- snapshotEntry: entry ? {
1221
- artifactDigest: entry.artifactDigest,
1222
- manifestDigest: entry.manifestDigest,
1223
- contentDigest: entry.contentDigest,
1224
- } : null,
1225
- sourceSubjectKeys,
1226
- requiredFinalKeys: pluginFinals.map((entry) => entry.key).sort(compareText),
1227
- requiredEvalKeys: pluginEvals.map((entry) => entry.key).sort(compareText),
1228
- };
1229
- });
1230
- issues.push(...binding.materializations
1231
- .filter((entry) => entry.state !== 'verified')
1232
- .map((entry) => ({
1233
- category: entry.state === 'incomplete'
1234
- ? 'missing'
1235
- : entry.state === 'unsupported' ? 'unsupported' : entry.state === 'drifted' ? 'unknown' : 'invalid',
1236
- code: `sourceBinding${entry.state[0].toUpperCase()}${entry.state.slice(1)}`,
1237
- message: entry.issues.join('; ') || `Source binding is ${entry.state}.`,
1238
- blocking: true,
1239
- path: entry.path,
1240
- })));
1241
- issues.sort((left, right) => compareText(`${left.code}\0${left.subjectKey ?? ''}\0${left.path ?? ''}`, `${right.code}\0${right.subjectKey ?? ''}\0${right.path ?? ''}`));
1242
- subjects.sort((left, right) => compareText(left.key, right.key));
1243
- const ok = !issues.some((issue) => issue.blocking);
1244
- return {
1245
- schemaVersion: 2,
1246
- command: 'review:inspect',
1247
- view: 'release',
1248
- ok,
1249
- completeness: ok && requiredFinals.every((entry) => entry.status === 'resolved')
1250
- ? 'complete' : 'incomplete',
1251
- target: { kind: target.kind, id: target.id, path: target.path },
1252
- workspace: {
1253
- root: workspace.workspaceRoot,
1254
- resolvedFrom: resolution.workspaceRootResolvedFrom,
1255
- identity: workspace.workspaceIdentity,
1256
- sourceIdentity: workspaceSourceIdentity,
1257
- catalogVersion: workspace.metadata.catalog.version,
1258
- },
1259
- snapshot: snapshot.receipt,
1260
- plugins,
1261
- subjects,
1262
- requiredFinals,
1263
- finalMappings: requiredFinals.map((required) => ({
1264
- requiredFinalKey: required.key,
1265
- sourceSubjectKey: required.sourceSubjectKey,
1266
- finalSubjectKeys: [...required.matchedFinalKeys].sort(compareText),
1267
- status: required.status,
1268
- })),
1269
- requiredEvals,
1270
- materializations: binding.materializations,
1271
- issues,
1272
- };
1273
- }
1274
- export async function inspectReviewTarget(options) {
1275
- const view = options.view ?? 'review';
1276
- if (view === 'release') {
1277
- if ((options.skills !== undefined && options.skills.length > 0)
1278
- || options.platform !== undefined
1279
- || options.scope !== undefined
1280
- || options.configFile !== undefined) {
1281
- throw new ReviewInspectionInputError('invalidArguments', 'The release view requires the complete Plugin or Catalog target and does not accept selection options.');
1282
- }
1283
- return await inspectReleaseTarget(options);
1284
- }
1285
- return await inspectNavigationTarget(options);
1286
- }
1287
378
  export function getReviewInspectionErrorCode(error, fallback = 'reviewInspectionFailed') {
1288
379
  if (error && typeof error === 'object' && 'code' in error) {
1289
380
  const code = error.code;
@@ -1314,23 +405,13 @@ function getReviewInspectionResolvedTarget(error) {
1314
405
  return null;
1315
406
  return { kind, path: targetPath };
1316
407
  }
1317
- export function createReviewInspectionFailure(error, view = 'review', fallbackCode = 'reviewInspectionFailed') {
408
+ export function createReviewInspectionFailure(error, fallbackCode = 'reviewInspectionFailed') {
1318
409
  const code = getReviewInspectionErrorCode(error, fallbackCode);
1319
410
  const message = error instanceof Error ? error.message : String(error);
1320
- if (view === 'release') {
1321
- return {
1322
- schemaVersion: 2,
1323
- command: 'review:inspect',
1324
- view,
1325
- ok: false,
1326
- completeness: 'incomplete',
1327
- issues: [{ category: 'invalid', code, message, blocking: true }],
1328
- };
1329
- }
1330
411
  return {
1331
412
  schemaVersion: 2,
1332
413
  command: 'review:inspect',
1333
- view,
414
+ view: 'review',
1334
415
  ok: false,
1335
416
  target: getReviewInspectionResolvedTarget(error),
1336
417
  sources: [],