@lesliechan721/aw 0.1.0-beta.23 → 0.1.0-beta.25

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.
Files changed (51) hide show
  1. package/dist/scripts/aw-skill-eval-materializer.d.ts +3 -1
  2. package/dist/scripts/aw-skill-eval-materializer.js +76 -79
  3. package/dist/scripts/aw-skill-eval-materializer.js.map +1 -1
  4. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  5. package/dist/src/commands/aw-capabilities.js +2 -1
  6. package/dist/src/commands/aw-capabilities.js.map +1 -1
  7. package/dist/src/commands/rebuild-integration.d.ts +26 -0
  8. package/dist/src/commands/rebuild-integration.js +163 -6
  9. package/dist/src/commands/rebuild-integration.js.map +1 -1
  10. package/dist/src/commands/review-inspection.d.ts +71 -12
  11. package/dist/src/commands/review-inspection.js +485 -61
  12. package/dist/src/commands/review-inspection.js.map +1 -1
  13. package/dist/src/commands/review-preparation.d.ts +49 -0
  14. package/dist/src/commands/review-preparation.js +427 -0
  15. package/dist/src/commands/review-preparation.js.map +1 -0
  16. package/dist/src/commands/skill-eval.js +2 -0
  17. package/dist/src/commands/skill-eval.js.map +1 -1
  18. package/dist/src/eval/manifest-v1.d.ts +4 -0
  19. package/dist/src/eval/manifest-v1.js +34 -6
  20. package/dist/src/eval/manifest-v1.js.map +1 -1
  21. package/dist/src/generators/local-plugin.d.ts +2 -1
  22. package/dist/src/generators/local-plugin.js +40 -34
  23. package/dist/src/generators/local-plugin.js.map +1 -1
  24. package/dist/src/index.js +146 -20
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/portable/materializer.d.ts +1 -0
  27. package/dist/src/portable/materializer.js +51 -11
  28. package/dist/src/portable/materializer.js.map +1 -1
  29. package/dist/src/review/review-profile.d.ts +17 -0
  30. package/dist/src/review/review-profile.js +177 -0
  31. package/dist/src/review/review-profile.js.map +1 -0
  32. package/dist/src/review/review-selection.d.ts +30 -0
  33. package/dist/src/review/review-selection.js +527 -0
  34. package/dist/src/review/review-selection.js.map +1 -0
  35. package/dist/src/review/review-types.d.ts +68 -0
  36. package/dist/src/review/review-types.js +16 -0
  37. package/dist/src/review/review-types.js.map +1 -0
  38. package/dist/src/review/skill-materialization.d.ts +13 -0
  39. package/dist/src/review/skill-materialization.js +75 -0
  40. package/dist/src/review/skill-materialization.js.map +1 -0
  41. package/dist/src/review/source-binding.d.ts +21 -9
  42. package/dist/src/review/source-binding.js +84 -24
  43. package/dist/src/review/source-binding.js.map +1 -1
  44. package/dist/src/tui/console-app-v4.js +8 -2
  45. package/dist/src/tui/console-app-v4.js.map +1 -1
  46. package/dist/src/tui/controller-runtime.js +5 -2
  47. package/dist/src/tui/controller-runtime.js.map +1 -1
  48. package/dist/src/tui/model.d.ts +1 -0
  49. package/dist/src/tui/model.js +32 -3
  50. package/dist/src/tui/model.js.map +1 -1
  51. package/package.json +3 -3
@@ -1,12 +1,35 @@
1
1
  import path from 'node:path';
2
- import { computeCaseCorpusDigest, computeSkillDigest, } from '@lesliechan721/agent-skill-eval';
2
+ import { canonicalJson, computeCaseCorpusDigest, computeSkillDigest, } from '@lesliechan721/agent-skill-eval';
3
3
  import { calculateCatalogWorkspaceSourceIdentity, DEFAULT_CATALOG_OUTPUT_PATH, GENERATED_OUTPUT_MARKER_FILENAME, readCatalogWorkspace, resolveSupportedPlatforms, validateCatalogSnapshotComplete, } from '@lesliechan721/aw-plugin-core';
4
4
  import fs from 'fs-extra';
5
5
  import { readAwSkillEvalManifest, } from '../eval/manifest-v1.js';
6
6
  import { CLI_RESERVED_IMPORTS } from '../utils/manifest.js';
7
7
  import { resolveCatalogWorkspaceRoot, } from './catalog-workspace-resolution.js';
8
8
  import { computeReviewComponentDigest } from './review-publication.js';
9
- import { computeAwSourceBindingSelectedCasesDigest, computeAwSourceBindingSuiteDefaultsDigest, readAwSourceBinding, resolveEvalProfileBindingInput, selectAwSourceBindingEvalCases, validateAwSourceBindingForWorkspace, } from '../review/source-binding.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';
11
+ import { resolveEffectivePluginConfigFromStores, } from '../utils/plugin-config.js';
12
+ export class ReviewInspectionInputError extends Error {
13
+ code;
14
+ exitCode = 2;
15
+ constructor(code, message) {
16
+ super(`${code}: ${message}`);
17
+ this.name = 'ReviewInspectionInputError';
18
+ this.code = code;
19
+ }
20
+ }
21
+ class ReviewInspectionResolvedTargetError extends Error {
22
+ code;
23
+ exitCode;
24
+ reviewTarget;
25
+ constructor(error, target) {
26
+ super(error instanceof Error ? error.message : String(error), { cause: error });
27
+ this.name = 'ReviewInspectionResolvedTargetError';
28
+ this.code = getReviewInspectionErrorCode(error);
29
+ this.exitCode = getReviewInspectionExitCode(error);
30
+ this.reviewTarget = target;
31
+ }
32
+ }
10
33
  const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
11
34
  function stripDotSlash(value) {
12
35
  return value.startsWith('./') ? value.slice(2) : value;
@@ -14,8 +37,13 @@ function stripDotSlash(value) {
14
37
  function relativeWorkspacePath(workspaceRoot, filePath) {
15
38
  return path.relative(workspaceRoot, filePath).split(path.sep).join('/') || '.';
16
39
  }
17
- function sourceKey(plugin, skill, kind) {
18
- return `source:${plugin}:${skill}:${kind}`;
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}`;
19
47
  }
20
48
  function requiredFinalKey(seed) {
21
49
  return [
@@ -39,6 +67,13 @@ function materializationIdentityKey(options) {
39
67
  function scopeFromProfile(profile) {
40
68
  return profile.endsWith('-project') ? 'project' : 'user';
41
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
+ }
42
77
  async function directSkillDirectories(root) {
43
78
  const entries = await fs.readdir(root, { withFileTypes: true });
44
79
  const result = [];
@@ -48,7 +83,7 @@ async function directSkillDirectories(root) {
48
83
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
49
84
  throw new Error(`Skill collection direct child must be a regular directory: ${entryPath}`);
50
85
  }
51
- result.push({ name: entry.name, root: entryPath });
86
+ result.push({ name: entry.name, root: await fs.realpath(entryPath) });
52
87
  }
53
88
  return result;
54
89
  }
@@ -126,7 +161,13 @@ async function inspectSnapshot(workspace, workspaceSourceIdentity) {
126
161
  }
127
162
  }
128
163
  async function addSourceSubject(options) {
129
- const key = sourceKey(options.plugin.manifest.name, options.skill, options.kind);
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
+ });
130
171
  if (options.subjects.some((subject) => subject.key === key))
131
172
  return key;
132
173
  const digest = options.kind === 'artifact-source'
@@ -155,8 +196,6 @@ async function inventoryPluginSource(options) {
155
196
  const portableSkills = plugin.manifest.distributions?.agentSkills?.skills
156
197
  ? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.distributions.agentSkills.skills)))
157
198
  : [];
158
- const nativeByName = new Map(nativeSkills.map((entry) => [entry.name, entry]));
159
- const portableByName = new Map(portableSkills.map((entry) => [entry.name, entry]));
160
199
  const nativeDynamicByPlatform = new Map([
161
200
  ['claude', new Set()],
162
201
  ['codex', new Set()],
@@ -203,7 +242,13 @@ async function inventoryPluginSource(options) {
203
242
  skill: skill.name,
204
243
  profile: `${platform}-${scope}`,
205
244
  targetId: null,
206
- sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'source-skill'),
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
+ }),
207
252
  dynamic: false,
208
253
  });
209
254
  }
@@ -219,7 +264,13 @@ async function inventoryPluginSource(options) {
219
264
  skill: skill.name,
220
265
  profile: `agent-skills-${scope}`,
221
266
  targetId: null,
222
- sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'portable-source-skill'),
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
+ }),
223
274
  dynamic: false,
224
275
  });
225
276
  }
@@ -232,27 +283,22 @@ async function inventoryPluginSource(options) {
232
283
  const platform = target.target === 'agent-skills.skill'
233
284
  ? 'agent-skills'
234
285
  : 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
- }
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);
256
302
  for (const scope of target.scopes) {
257
303
  seeds.push({
258
304
  plugin,
@@ -414,8 +460,11 @@ async function resolveTarget(options) {
414
460
  }
415
461
  const candidate = path.resolve(options.cwd, options.target);
416
462
  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.');
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.');
419
468
  }
420
469
  const real = await fs.realpath(candidate);
421
470
  if (real === options.workspace.workspaceRoot) {
@@ -428,7 +477,7 @@ async function resolveTarget(options) {
428
477
  }
429
478
  const plugin = options.workspace.plugins.find((entry) => entry.rootDir === real);
430
479
  if (!plugin) {
431
- throw new Error('Review inspection target is not a canonical Plugin in the selected Catalog Workspace.');
480
+ throw new ReviewInspectionInputError('invalidArguments', 'The release view requires the complete Plugin or Catalog target.');
432
481
  }
433
482
  return { kind: 'plugin', id: plugin.manifest.name, path: real, plugins: [plugin] };
434
483
  }
@@ -444,7 +493,8 @@ async function inspectMaterializations(options) {
444
493
  sourceIdentity: options.workspaceSourceIdentity,
445
494
  },
446
495
  };
447
- for (const bindingPath of options.paths) {
496
+ const bindingPaths = [...new Set(options.paths.map((entry) => path.resolve(entry)))].sort(compareText);
497
+ for (const bindingPath of bindingPaths) {
448
498
  let parsed = null;
449
499
  try {
450
500
  parsed = await readAwSourceBinding(bindingPath);
@@ -468,33 +518,36 @@ async function inspectMaterializations(options) {
468
518
  state: validation.state,
469
519
  plugin: validation.binding?.subject.plugin ?? null,
470
520
  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,
521
+ profile: validation.binding ? finalProfileFromBinding(validation.binding.profile) : null,
476
522
  finalKeys: [],
477
523
  verifiedBinding: null,
478
524
  issues: validation.issues.map((issue) => issue.message),
479
525
  });
480
526
  continue;
481
527
  }
482
- const profile = validation.binding.profile.kind === 'eval'
483
- ? `codex-${validation.binding.profile.scope}`
484
- : `agent-skills-${validation.binding.profile.scope}`;
528
+ const profile = finalProfileFromBinding(validation.binding.profile);
485
529
  const finalKeys = [];
486
530
  for (const output of validation.outputs) {
487
- if (output.kind !== 'eval-target' && output.kind !== 'portable-skill')
531
+ if (output.kind !== 'eval-target'
532
+ && output.kind !== 'portable-skill'
533
+ && output.kind !== 'review-skill')
488
534
  continue;
489
- const outputPlugin = output.kind === 'portable-skill'
535
+ const outputPlugin = output.kind === 'portable-skill' || output.kind === 'review-skill'
490
536
  ? output.plugin
491
537
  : validation.binding.subject.plugin;
492
538
  if (!targetPluginNames.has(outputPlugin))
493
539
  continue;
494
- const belongsToSelectedPlugin = options.requiredFinals.some((required) => required.plugin === outputPlugin
540
+ const compatibleRequired = options.requiredFinals.filter((required) => required.plugin === outputPlugin
495
541
  && required.skill === output.skill
496
- && required.profile === profile);
497
- if (!belongsToSelectedPlugin) {
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) {
498
551
  options.issues.push({
499
552
  category: 'unknown',
500
553
  code: 'materializedOutputOwnerUnknown',
@@ -549,6 +602,7 @@ async function inspectMaterializations(options) {
549
602
  identityKey,
550
603
  path: output.path,
551
604
  digest: output.digest,
605
+ requiredKeys: compatibleRequired.map((required) => required.key).sort(compareText),
552
606
  });
553
607
  }
554
608
  }
@@ -587,11 +641,325 @@ async function inspectMaterializations(options) {
587
641
  finals: [...finals.values()].sort((left, right) => compareText(left.key, right.key)),
588
642
  };
589
643
  }
590
- export async function inspectReviewTarget(options) {
644
+ function sameResolvedConfig(left, right) {
645
+ return canonicalJson({ values: left.values, sources: left.sources })
646
+ === canonicalJson({ values: right.values, sources: right.sources });
647
+ }
648
+ function selectResolvedConfig(config, keys) {
649
+ return {
650
+ values: Object.fromEntries(keys.flatMap((key) => Object.hasOwn(config.values, key) ? [[key, config.values[key]]] : [])),
651
+ sources: Object.fromEntries(keys.flatMap((key) => Object.hasOwn(config.sources, key) ? [[key, config.sources[key]]] : [])),
652
+ };
653
+ }
654
+ function selectedSkillConfig(profile, skill) {
655
+ const entries = profile.pluginConfigs.filter((entry) => entry.pluginName === skill.pluginName);
656
+ if (entries.length === 0)
657
+ return skill.refreshKeys.length === 0 ? { values: {}, sources: {} } : null;
658
+ const values = Object.assign({}, ...entries.map((entry) => entry.values));
659
+ const sources = Object.assign({}, ...entries.map((entry) => entry.sources));
660
+ return selectResolvedConfig({ values, sources }, skill.refreshKeys);
661
+ }
662
+ function legacyProfileStore(scope, values) {
663
+ return {
664
+ path: `<legacy-${scope}-profile>`,
665
+ exists: Object.keys(values).length > 0,
666
+ state: 'ready',
667
+ values,
668
+ issues: [],
669
+ };
670
+ }
671
+ async function legacyBindingMatchesSelection(options) {
672
+ const { binding, requestedProfile, skill } = options;
673
+ if (binding.profile.scope !== requestedProfile.scope)
674
+ return false;
675
+ if (binding.profile.kind === 'eval') {
676
+ if (requestedProfile.platform !== 'codex'
677
+ || !binding.subject.skill
678
+ || skill.delivery === 'codex-native')
679
+ return false;
680
+ const plugin = options.selection.workspace.plugins.find((entry) => entry.manifest.name === binding.subject.plugin);
681
+ if (!plugin)
682
+ return false;
683
+ const loaded = await readAwSkillEvalManifest({ plugin, skill: binding.subject.skill });
684
+ const evalProfile = loaded.manifest.profiles.find((entry) => entry.id === binding.profile.id);
685
+ if (!evalProfile)
686
+ return false;
687
+ const config = Object.fromEntries(evalProfile.plugins.map((entry) => [entry.name, entry.config]));
688
+ try {
689
+ const requested = selectedSkillConfig(requestedProfile, skill);
690
+ if (!requested)
691
+ return false;
692
+ const overrides = config[skill.pluginName] ?? {};
693
+ const userValues = binding.profile.scope === 'user' ? overrides : {};
694
+ const effective = await resolveEffectivePluginConfigFromStores({
695
+ pluginName: skill.pluginName,
696
+ manifest: skill.plugin.manifest,
697
+ targetScope: binding.profile.scope,
698
+ projectRoot: options.selection.workspace.workspaceRoot,
699
+ stores: {
700
+ user: legacyProfileStore('user', userValues),
701
+ ...(binding.profile.scope === 'project'
702
+ ? { project: legacyProfileStore('project', overrides) }
703
+ : {}),
704
+ },
705
+ });
706
+ return sameResolvedConfig(requested, selectResolvedConfig(effective, skill.refreshKeys));
707
+ }
708
+ catch {
709
+ return false;
710
+ }
711
+ }
712
+ if (binding.profile.kind !== 'portable' || requestedProfile.platform !== 'agent-skills') {
713
+ return false;
714
+ }
715
+ try {
716
+ const requested = selectedSkillConfig(requestedProfile, skill);
717
+ if (!requested)
718
+ return false;
719
+ const current = await resolvePortablePluginProfileConfig({
720
+ plugin: skill.plugin,
721
+ scope: skill.configScope,
722
+ cwd: options.selection.workspace.workspaceRoot,
723
+ env: options.env,
724
+ });
725
+ return sameResolvedConfig(requested, selectResolvedConfig(current.config, skill.refreshKeys));
726
+ }
727
+ catch {
728
+ return false;
729
+ }
730
+ }
731
+ function bindingOutputIdentity(binding, artifact) {
732
+ if (artifact.kind === 'eval-target') {
733
+ return { plugin: binding.subject.plugin, skill: artifact.skill };
734
+ }
735
+ if (artifact.kind === 'portable-skill' || artifact.kind === 'review-skill') {
736
+ return { plugin: artifact.plugin, skill: artifact.skill };
737
+ }
738
+ return null;
739
+ }
740
+ async function inspectNavigationBindings(options) {
741
+ const candidates = new Map();
742
+ const issues = [];
743
+ const context = {
744
+ workspace: options.selection.workspace,
745
+ source: {
746
+ kind: 'workspace',
747
+ workspaceIdentity: options.selection.workspace.workspaceIdentity,
748
+ sourceIdentity: options.selection.sourceIdentity,
749
+ },
750
+ };
751
+ const bindingPaths = [...new Set(options.materializations.map((entry) => path.resolve(entry)))]
752
+ .sort(compareText);
753
+ for (const bindingPath of bindingPaths) {
754
+ let binding;
755
+ try {
756
+ binding = await readAwSourceBinding(bindingPath);
757
+ }
758
+ catch (error) {
759
+ throw new ReviewInspectionInputError('sourceBindingInvalid', error instanceof Error ? error.message : String(error));
760
+ }
761
+ const validation = await validateAwSourceBindingForWorkspace({
762
+ bindingPath,
763
+ context,
764
+ expectedSubject: binding.subject,
765
+ cwd: options.selection.workspace.workspaceRoot,
766
+ env: options.env,
767
+ });
768
+ if (validation.state === 'invalid' || validation.state === 'incomplete') {
769
+ throw new ReviewInspectionInputError('sourceBindingInvalid', validation.issues.map((entry) => entry.message).join('; '));
770
+ }
771
+ let selected = false;
772
+ for (const requestedProfile of options.selection.profiles) {
773
+ const profileMatches = binding.profile.kind === 'review'
774
+ ? binding.profile.platform === requestedProfile.platform
775
+ && binding.profile.scope === requestedProfile.scope
776
+ : binding.profile.kind === 'eval'
777
+ ? requestedProfile.platform === 'codex'
778
+ : requestedProfile.platform === 'agent-skills';
779
+ if (!profileMatches)
780
+ continue;
781
+ for (const artifact of binding.artifacts) {
782
+ const identity = bindingOutputIdentity(binding, artifact);
783
+ if (!identity)
784
+ continue;
785
+ const skill = requestedProfile.skills.find((entry) => entry.pluginName === identity.plugin && entry.name === identity.skill);
786
+ if (!skill)
787
+ continue;
788
+ if (binding.profile.kind === 'review'
789
+ && !await reviewBindingMatchesSelection({
790
+ workspace: options.selection.workspace,
791
+ bindingProfile: binding.profile,
792
+ requestedProfile,
793
+ pluginName: identity.plugin,
794
+ skill: identity.skill,
795
+ }))
796
+ continue;
797
+ if (binding.profile.kind !== 'review'
798
+ && !await legacyBindingMatchesSelection({
799
+ selection: options.selection,
800
+ requestedProfile,
801
+ skill,
802
+ binding,
803
+ env: options.env,
804
+ }))
805
+ continue;
806
+ selected = true;
807
+ const key = `${identity.plugin}\0${identity.skill}\0${requestedProfile.platform}\0${requestedProfile.scope}`;
808
+ const candidate = {
809
+ path: path.resolve(path.dirname(bindingPath), ...artifact.path.split('/')),
810
+ digest: artifact.digest,
811
+ evidencePath: bindingPath,
812
+ state: validation.state === 'verified' ? 'ready' : 'stale',
813
+ };
814
+ candidates.set(key, [...(candidates.get(key) ?? []), candidate]);
815
+ }
816
+ }
817
+ if (!selected) {
818
+ issues.push({
819
+ code: 'materializationNotSelected',
820
+ path: bindingPath,
821
+ message: 'The source binding does not match the selected Skill, profile, and config input.',
822
+ });
823
+ }
824
+ else if (validation.state === 'drifted') {
825
+ issues.push({
826
+ code: 'sourceBindingDrifted',
827
+ path: bindingPath,
828
+ message: validation.issues.map((entry) => entry.message).join('; '),
829
+ });
830
+ }
831
+ }
832
+ return { candidates, issues };
833
+ }
834
+ function navigationSkillKey(skill, profile) {
835
+ return `${skill.pluginName}\0${skill.name}\0${profile.platform}\0${profile.scope}`;
836
+ }
837
+ async function inspectNavigationTarget(options) {
838
+ const selection = await resolveReviewSelection({
839
+ cwd: options.cwd,
840
+ target: options.target,
841
+ ...(options.workspaceRoot === undefined ? {} : { workspaceRoot: options.workspaceRoot }),
842
+ ...(options.skills === undefined ? {} : { skills: options.skills }),
843
+ ...(options.platform === undefined ? {} : { platform: options.platform }),
844
+ ...(options.scope === undefined ? {} : { scope: options.scope }),
845
+ ...(options.configFile === undefined ? {} : { configFile: options.configFile }),
846
+ ...(options.env === undefined ? {} : { env: options.env }),
847
+ });
848
+ try {
849
+ const snapshot = await inspectSnapshot(selection.workspace, selection.sourceIdentity);
850
+ const binding = await inspectNavigationBindings({
851
+ selection,
852
+ materializations: options.materializations ?? [],
853
+ env: options.env,
854
+ });
855
+ const skills = [];
856
+ const issues = [...binding.issues];
857
+ if (snapshot.receipt.state === 'invalid' || snapshot.receipt.state === 'stale') {
858
+ issues.push({
859
+ code: snapshot.receipt.state === 'invalid' ? 'snapshotInvalid' : 'snapshotStale',
860
+ path: snapshot.receipt.path,
861
+ message: snapshot.receipt.state === 'invalid'
862
+ ? 'The existing Catalog snapshot is invalid.'
863
+ : 'The existing Catalog snapshot is not bound to the current Workspace source.',
864
+ });
865
+ }
866
+ for (const profile of selection.profiles) {
867
+ for (const skill of profile.skills) {
868
+ const key = navigationSkillKey(skill, profile);
869
+ const candidates = [...(binding.candidates.get(key) ?? [])];
870
+ if (snapshot.root
871
+ && !skill.dynamic
872
+ && profile.platform !== 'agent-skills') {
873
+ const finalPath = path.join(snapshot.root, 'plugins', skill.pluginName, profile.platform, 'skills', skill.name);
874
+ try {
875
+ candidates.push({
876
+ path: finalPath,
877
+ digest: await computeSkillDigest(finalPath),
878
+ evidencePath: snapshot.receipt.path,
879
+ state: 'ready',
880
+ });
881
+ }
882
+ catch {
883
+ // Complete snapshot validation reports invalid final content through its own state.
884
+ }
885
+ }
886
+ const ready = candidates.filter((entry) => entry.state === 'ready');
887
+ const readyDigests = new Set(ready.map((entry) => entry.digest));
888
+ let state = 'missing';
889
+ let chosen = null;
890
+ if (readyDigests.size > 1) {
891
+ state = 'invalid';
892
+ issues.push({
893
+ code: 'materializationAmbiguous',
894
+ path: null,
895
+ message: `Current materializations have different digests for ${skill.pluginName}/${skill.name}/${profile.profile.id}.`,
896
+ });
897
+ }
898
+ else if (ready.length > 0) {
899
+ state = 'ready';
900
+ chosen = ready.sort((left, right) => compareText(`${left.evidencePath}\0${left.path}`, `${right.evidencePath}\0${right.path}`))[0];
901
+ }
902
+ else {
903
+ const stale = candidates.filter((entry) => entry.state === 'stale')
904
+ .sort((left, right) => compareText(left.evidencePath, right.evidencePath));
905
+ if (stale.length > 0) {
906
+ state = 'stale';
907
+ chosen = stale[0];
908
+ }
909
+ }
910
+ if (state === 'missing') {
911
+ issues.push({
912
+ code: 'requiredFinalMissing',
913
+ path: skill.sourcePath,
914
+ message: `No current final is available for ${skill.pluginName}/${skill.name}/${profile.profile.id}.`,
915
+ });
916
+ }
917
+ skills.push({
918
+ skill: skill.name,
919
+ sourcePath: skill.sourcePath,
920
+ profile: {
921
+ platform: profile.platform,
922
+ scope: profile.scope,
923
+ inputDigest: profile.profile.inputDigest,
924
+ },
925
+ finalPath: chosen?.path ?? null,
926
+ digest: chosen?.digest ?? null,
927
+ state,
928
+ evidencePath: chosen?.evidencePath ?? null,
929
+ });
930
+ }
931
+ }
932
+ skills.sort((left, right) => compareText(`${left.skill}\0${left.profile.platform}\0${left.profile.scope}\0${left.sourcePath}`, `${right.skill}\0${right.profile.platform}\0${right.profile.scope}\0${right.sourcePath}`));
933
+ issues.sort((left, right) => compareText(`${left.code}\0${left.path ?? ''}\0${left.message}`, `${right.code}\0${right.path ?? ''}\0${right.message}`));
934
+ return {
935
+ schemaVersion: 2,
936
+ command: 'review:inspect',
937
+ view: 'review',
938
+ ok: true,
939
+ target: { kind: selection.target.kind, path: selection.target.path },
940
+ sources: [...new Set(selection.sources.map((entry) => path.resolve(entry)))].sort(compareText),
941
+ ...(selection.target.kind === 'catalog'
942
+ ? { snapshot: { path: snapshot.receipt.path, state: snapshot.receipt.state } }
943
+ : {}),
944
+ skills,
945
+ issues,
946
+ };
947
+ }
948
+ catch (error) {
949
+ throw new ReviewInspectionResolvedTargetError(error, {
950
+ kind: selection.target.kind,
951
+ path: selection.target.path,
952
+ });
953
+ }
954
+ }
955
+ async function inspectReleaseTarget(options) {
591
956
  const targetCandidate = options.target === 'catalog' ? null : path.resolve(options.cwd, options.target);
592
957
  const targetCandidateStat = targetCandidate
593
958
  ? await fs.lstat(targetCandidate).catch(() => null)
594
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
+ }
595
963
  const resolution = await resolveCatalogWorkspaceRoot({
596
964
  cwd: options.workspaceRoot === undefined && targetCandidateStat?.isDirectory()
597
965
  ? targetCandidate
@@ -728,7 +1096,8 @@ export async function inspectReviewTarget(options) {
728
1096
  for (const final of binding.finals) {
729
1097
  const candidates = requiredFinals.filter((required) => required.plugin === final.plugin
730
1098
  && required.skill === final.skill
731
- && required.profile === final.profile);
1099
+ && required.profile === final.profile
1100
+ && final.requiredKeys.includes(required.key));
732
1101
  if (candidates.length === 1) {
733
1102
  const required = candidates[0];
734
1103
  bindingFinalsByRequiredKey.set(required.key, [...(bindingFinalsByRequiredKey.get(required.key) ?? []), final]);
@@ -814,7 +1183,8 @@ export async function inspectReviewTarget(options) {
814
1183
  .map((materialization) => materialization.path))]
815
1184
  .sort(compareText);
816
1185
  if (required.materializedFinalKeys.length === 0) {
817
- const snapshotFinal = requiredByKey.get(required.requiredFinalKey)?.matchedFinalKeys ?? [];
1186
+ const snapshotFinal = (requiredByKey.get(required.requiredFinalKey)?.matchedFinalKeys ?? [])
1187
+ .filter((key) => subjects.some((subject) => subject.key === key && subject.origin === 'catalog-snapshot'));
818
1188
  required.materializedFinalKeys = [...snapshotFinal];
819
1189
  }
820
1190
  }
@@ -872,8 +1242,9 @@ export async function inspectReviewTarget(options) {
872
1242
  subjects.sort((left, right) => compareText(left.key, right.key));
873
1243
  const ok = !issues.some((issue) => issue.blocking);
874
1244
  return {
875
- schemaVersion: 1,
1245
+ schemaVersion: 2,
876
1246
  command: 'review:inspect',
1247
+ view: 'release',
877
1248
  ok,
878
1249
  completeness: ok && requiredFinals.every((entry) => entry.status === 'resolved')
879
1250
  ? 'complete' : 'incomplete',
@@ -900,18 +1271,71 @@ export async function inspectReviewTarget(options) {
900
1271
  issues,
901
1272
  };
902
1273
  }
903
- export function createReviewInspectionFailure(error, code = 'reviewInspectionFailed') {
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
+ export function getReviewInspectionErrorCode(error, fallback = 'reviewInspectionFailed') {
1288
+ if (error && typeof error === 'object' && 'code' in error) {
1289
+ const code = error.code;
1290
+ if (typeof code === 'string' && code !== '')
1291
+ return code;
1292
+ }
1293
+ return fallback;
1294
+ }
1295
+ export function getReviewInspectionExitCode(error) {
1296
+ if (error && typeof error === 'object' && 'exitCode' in error) {
1297
+ const exitCode = error.exitCode;
1298
+ if (exitCode === 2 || exitCode === 3)
1299
+ return exitCode;
1300
+ }
1301
+ return 1;
1302
+ }
1303
+ function getReviewInspectionResolvedTarget(error) {
1304
+ if (!error || typeof error !== 'object' || !('reviewTarget' in error))
1305
+ return null;
1306
+ const target = error.reviewTarget;
1307
+ if (!target || typeof target !== 'object')
1308
+ return null;
1309
+ const kind = target.kind;
1310
+ const targetPath = target.path;
1311
+ if ((kind !== 'skill' && kind !== 'plugin' && kind !== 'catalog')
1312
+ || typeof targetPath !== 'string'
1313
+ || targetPath === '')
1314
+ return null;
1315
+ return { kind, path: targetPath };
1316
+ }
1317
+ export function createReviewInspectionFailure(error, view = 'review', fallbackCode = 'reviewInspectionFailed') {
1318
+ const code = getReviewInspectionErrorCode(error, fallbackCode);
1319
+ 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
+ }
904
1330
  return {
905
- schemaVersion: 1,
1331
+ schemaVersion: 2,
906
1332
  command: 'review:inspect',
1333
+ view,
907
1334
  ok: false,
908
- completeness: 'incomplete',
909
- issues: [{
910
- category: 'invalid',
911
- code,
912
- message: error instanceof Error ? error.message : String(error),
913
- blocking: true,
914
- }],
1335
+ target: getReviewInspectionResolvedTarget(error),
1336
+ sources: [],
1337
+ skills: [],
1338
+ issues: [{ code, path: null, message }],
915
1339
  };
916
1340
  }
917
1341
  //# sourceMappingURL=review-inspection.js.map