scene-capability-engine 3.6.10 → 3.6.13

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.
@@ -11,7 +11,10 @@ const path = require('path');
11
11
  const chalk = require('chalk');
12
12
  const TaskClaimer = require('../task/task-claimer');
13
13
  const { runStudioSpecGovernance } = require('../studio/spec-intake-governor');
14
+ const { DOMAIN_CHAIN_RELATIVE_PATH } = require('../spec/domain-modeling');
14
15
  const { SceStateStore } = require('../state/sce-state-store');
16
+ const TemplateManager = require('../templates/template-manager');
17
+ const { TemplateError } = require('../templates/template-error');
15
18
  const packageJson = require('../../package.json');
16
19
 
17
20
  const DEFAULT_ITERATION_DIR = '.sce/reports/capability-iteration';
@@ -31,6 +34,20 @@ function normalizeStringArray(value) {
31
34
  return value.map((item) => normalizeText(item)).filter(Boolean);
32
35
  }
33
36
 
37
+ function normalizeTokenList(value) {
38
+ if (Array.isArray(value)) {
39
+ return normalizeStringArray(value).map((item) => item.toLowerCase());
40
+ }
41
+ const text = normalizeText(value);
42
+ if (!text) {
43
+ return [];
44
+ }
45
+ return text
46
+ .split(/[^a-zA-Z0-9._-]+/g)
47
+ .map((item) => item.trim().toLowerCase())
48
+ .filter(Boolean);
49
+ }
50
+
34
51
  function normalizeBoolean(value, fallback = false) {
35
52
  if (typeof value === 'boolean') {
36
53
  return value;
@@ -71,6 +88,12 @@ function buildDefaultTemplatePath(sceneId) {
71
88
  return path.join(DEFAULT_ITERATION_DIR, `${safeScene}.template.json`);
72
89
  }
73
90
 
91
+ function buildDefaultUsePlanPath(specId, templateId) {
92
+ const safeSpec = normalizeText(specId).replace(/[^\w.-]+/g, '_') || 'spec';
93
+ const safeTemplate = normalizeText(templateId).replace(/[^\w.-]+/g, '_') || 'template';
94
+ return path.join(DEFAULT_ITERATION_DIR, 'usage', `${safeSpec}.${safeTemplate}.plan.json`);
95
+ }
96
+
74
97
  function buildDefaultExportDir(templateId) {
75
98
  const safeId = normalizeText(templateId).replace(/[^\w.-]+/g, '_') || 'capability';
76
99
  return path.join(DEFAULT_EXPORT_ROOT, `capability-${safeId}`);
@@ -80,6 +103,95 @@ function buildSceneIdFromCandidate(candidate) {
80
103
  return normalizeText(candidate && candidate.scene_id) || 'scene.unknown';
81
104
  }
82
105
 
106
+ function parseTemplatePath(templatePath) {
107
+ const normalized = normalizeText(templatePath);
108
+ if (!normalized) {
109
+ return { sourceName: 'official', templateId: '' };
110
+ }
111
+ if (normalized.includes(':')) {
112
+ const [sourceName, templateId] = normalized.split(':', 2);
113
+ return { sourceName: normalizeText(sourceName) || 'official', templateId: normalizeText(templateId) };
114
+ }
115
+ return { sourceName: 'official', templateId: normalized };
116
+ }
117
+
118
+ function buildOntologyScopeFromChain(domainChain) {
119
+ const ontology = domainChain && domainChain.ontology ? domainChain.ontology : {};
120
+ return {
121
+ domains: normalizeStringArray(domainChain && domainChain.scene_id ? [domainChain.scene_id] : []),
122
+ entities: normalizeStringArray(ontology.entity),
123
+ relations: normalizeStringArray(ontology.relation),
124
+ business_rules: normalizeStringArray(ontology.business_rule),
125
+ decisions: normalizeStringArray(ontology.decision_policy)
126
+ };
127
+ }
128
+
129
+ function buildOntologyOverlap(specScope, templateScope) {
130
+ const fields = ['domains', 'entities', 'relations', 'business_rules', 'decisions'];
131
+ const details = {};
132
+ let weightedTotal = 0;
133
+ let weightedMatched = 0;
134
+ let bucketCount = 0;
135
+
136
+ fields.forEach((field) => {
137
+ const expected = normalizeTokenList(specScope && specScope[field]);
138
+ const provided = normalizeTokenList(templateScope && templateScope[field]);
139
+ const providedSet = new Set(provided);
140
+ const matched = expected.filter((item) => providedSet.has(item));
141
+ const expectedCount = expected.length;
142
+ const matchedCount = matched.length;
143
+ const coverage = expectedCount > 0 ? matchedCount / expectedCount : 0;
144
+ if (expectedCount > 0) {
145
+ weightedTotal += 1;
146
+ weightedMatched += coverage;
147
+ bucketCount += 1;
148
+ }
149
+ details[field] = {
150
+ expected,
151
+ provided,
152
+ matched,
153
+ expected_count: expectedCount,
154
+ matched_count: matchedCount,
155
+ coverage_ratio: Number(coverage.toFixed(3))
156
+ };
157
+ });
158
+
159
+ const score = bucketCount > 0 ? weightedMatched / weightedTotal : 0;
160
+ return {
161
+ score,
162
+ details
163
+ };
164
+ }
165
+
166
+ function buildKeywordScore(template, queryTokens) {
167
+ if (!queryTokens || queryTokens.length === 0) {
168
+ return 0;
169
+ }
170
+ const haystack = [
171
+ template.id,
172
+ template.name,
173
+ template.description,
174
+ ...(template.tags || []),
175
+ ...(template.applicable_scenarios || [])
176
+ ].map((item) => `${item || ''}`.toLowerCase());
177
+ const hits = queryTokens.filter((token) => haystack.some((value) => value.includes(token))).length;
178
+ return hits / queryTokens.length;
179
+ }
180
+
181
+ async function loadSpecDomainChain(projectPath, specId, fileSystem) {
182
+ const specPath = path.join(projectPath, '.sce', 'specs', specId);
183
+ const domainChainPath = path.join(specPath, DOMAIN_CHAIN_RELATIVE_PATH);
184
+ if (!await fileSystem.pathExists(domainChainPath)) {
185
+ return { exists: false, path: domainChainPath, payload: null };
186
+ }
187
+ try {
188
+ const payload = await fileSystem.readJson(domainChainPath);
189
+ return { exists: true, path: domainChainPath, payload };
190
+ } catch (error) {
191
+ return { exists: true, path: domainChainPath, payload: null, error: error.message };
192
+ }
193
+ }
194
+
83
195
  async function loadSceneIndexFromFile(projectPath, fileSystem) {
84
196
  const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
85
197
  if (!await fileSystem.pathExists(indexPath)) {
@@ -537,6 +649,249 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
537
649
  return result;
538
650
  }
539
651
 
652
+ function displayCapabilityCatalog(templates, options = {}) {
653
+ const total = Array.isArray(templates) ? templates.length : 0;
654
+ console.log(chalk.red('🔥') + ' Capability Library');
655
+ if (total === 0) {
656
+ console.log(chalk.yellow('No capability templates found.'));
657
+ if (options.source) {
658
+ console.log(chalk.gray(`Try removing filters or run ${chalk.cyan('sce templates update')}.`));
659
+ }
660
+ return;
661
+ }
662
+ templates.forEach((template) => {
663
+ const sourcePrefix = template.source && template.source !== 'official'
664
+ ? chalk.gray(`[${template.source}] `)
665
+ : '';
666
+ console.log(`${sourcePrefix}${chalk.cyan(template.id)} ${chalk.gray(`(${template.category})`)}`);
667
+ console.log(` ${template.name}`);
668
+ console.log(` ${chalk.gray(template.description)}`);
669
+ console.log();
670
+ });
671
+ console.log(chalk.gray(`Total: ${total} capability template(s)`));
672
+ }
673
+
674
+ async function listCapabilityCatalog(options = {}) {
675
+ const manager = new TemplateManager();
676
+ const templates = await manager.listTemplates({
677
+ category: options.category,
678
+ source: options.source,
679
+ templateType: 'capability-template',
680
+ compatibleWith: options.compatibleWith,
681
+ riskLevel: options.risk
682
+ });
683
+ if (normalizeBoolean(options.json, false)) {
684
+ return {
685
+ mode: 'capability-catalog-list',
686
+ templates
687
+ };
688
+ }
689
+ displayCapabilityCatalog(templates, options);
690
+ return { templates };
691
+ }
692
+
693
+ async function searchCapabilityCatalog(keyword, options = {}) {
694
+ const manager = new TemplateManager();
695
+ const templates = await manager.searchTemplates(keyword, {
696
+ category: options.category,
697
+ source: options.source,
698
+ templateType: 'capability-template',
699
+ compatibleWith: options.compatibleWith,
700
+ riskLevel: options.risk
701
+ });
702
+ if (normalizeBoolean(options.json, false)) {
703
+ return {
704
+ mode: 'capability-catalog-search',
705
+ keyword,
706
+ templates
707
+ };
708
+ }
709
+ displayCapabilityCatalog(templates, options);
710
+ return { templates };
711
+ }
712
+
713
+ async function showCapabilityTemplate(templatePath, options = {}) {
714
+ const manager = new TemplateManager();
715
+ const template = await manager.showTemplate(templatePath);
716
+ const { sourceName, templateId } = parseTemplatePath(templatePath);
717
+ await manager.ensureCached(sourceName);
718
+ const sourcePath = manager.cacheManager.getSourceCachePath(sourceName);
719
+ const templateDir = path.join(sourcePath, templateId);
720
+ const capabilityFile = path.join(templateDir, 'capability-template.json');
721
+ let templatePayload = null;
722
+ if (await fs.pathExists(capabilityFile)) {
723
+ try {
724
+ templatePayload = await fs.readJson(capabilityFile);
725
+ } catch (_error) {
726
+ templatePayload = null;
727
+ }
728
+ }
729
+ const result = {
730
+ mode: 'capability-catalog-show',
731
+ template,
732
+ template_file: await fs.pathExists(capabilityFile) ? capabilityFile : null,
733
+ payload: templatePayload
734
+ };
735
+ if (normalizeBoolean(options.json, false)) {
736
+ return result;
737
+ }
738
+ console.log(chalk.green('✅ Capability template loaded'));
739
+ console.log(chalk.gray(` ID: ${template.id}`));
740
+ console.log(chalk.gray(` Name: ${template.name}`));
741
+ if (templatePayload) {
742
+ console.log(chalk.gray(' Payload: capability-template.json loaded'));
743
+ }
744
+ return result;
745
+ }
746
+
747
+ async function matchCapabilityTemplates(options = {}) {
748
+ const projectPath = options.projectPath || process.cwd();
749
+ const fileSystem = options.fileSystem || fs;
750
+ const specId = normalizeText(options.spec || options.specId);
751
+ if (!specId) {
752
+ throw new Error('spec is required for capability match');
753
+ }
754
+ const chain = await loadSpecDomainChain(projectPath, specId, fileSystem);
755
+ if (!chain.exists && normalizeBoolean(options.strict, false)) {
756
+ throw new Error(`problem-domain-chain missing for spec ${specId}`);
757
+ }
758
+ if (chain.error && normalizeBoolean(options.strict, false)) {
759
+ throw new Error(`problem-domain-chain invalid: ${chain.error}`);
760
+ }
761
+ const domainChain = chain.payload || {};
762
+ const specScope = buildOntologyScopeFromChain(domainChain);
763
+ const queryTokens = normalizeTokenList(options.query)
764
+ .concat(normalizeTokenList(domainChain.problem && domainChain.problem.statement))
765
+ .concat(normalizeTokenList(domainChain.scene_id));
766
+ const manager = new TemplateManager();
767
+ const templates = await manager.listTemplates({
768
+ source: options.source,
769
+ templateType: 'capability-template',
770
+ compatibleWith: options.compatibleWith,
771
+ riskLevel: options.risk
772
+ });
773
+ const matches = templates.map((template) => {
774
+ const overlap = buildOntologyOverlap(specScope, template.ontology_scope || {});
775
+ const scenarioScore = template.applicable_scenarios && domainChain.scene_id
776
+ ? (template.applicable_scenarios.includes(domainChain.scene_id) ? 1 : 0)
777
+ : 0;
778
+ const keywordScore = buildKeywordScore(template, queryTokens);
779
+ const totalScore = (overlap.score * 0.6) + (scenarioScore * 0.2) + (keywordScore * 0.2);
780
+ return {
781
+ template_id: template.id,
782
+ source: template.source,
783
+ name: template.name,
784
+ description: template.description,
785
+ category: template.category,
786
+ risk_level: template.risk_level,
787
+ score: Math.round(totalScore * 100),
788
+ score_components: {
789
+ ontology: Number(overlap.score.toFixed(3)),
790
+ scenario: scenarioScore,
791
+ keyword: Number(keywordScore.toFixed(3))
792
+ },
793
+ overlap
794
+ };
795
+ }).sort((a, b) => b.score - a.score);
796
+
797
+ const limit = toPositiveInteger(options.limit, 10);
798
+ const payload = {
799
+ mode: 'capability-match',
800
+ spec_id: specId,
801
+ scene_id: domainChain.scene_id || null,
802
+ query: normalizeText(options.query) || null,
803
+ ontology_source: chain.exists ? chain.path : null,
804
+ match_count: matches.length,
805
+ matches: matches.slice(0, limit),
806
+ warnings: chain.exists ? [] : ['problem-domain-chain missing; ontology-based match unavailable']
807
+ };
808
+ if (normalizeBoolean(options.json, false)) {
809
+ return payload;
810
+ }
811
+ console.log(chalk.green('✅ Capability match completed'));
812
+ console.log(chalk.gray(` Spec: ${specId}`));
813
+ console.log(chalk.gray(` Matches: ${payload.matches.length}`));
814
+ return payload;
815
+ }
816
+
817
+ async function useCapabilityTemplate(options = {}) {
818
+ const projectPath = options.projectPath || process.cwd();
819
+ const fileSystem = options.fileSystem || fs;
820
+ const templateId = normalizeText(options.template || options.id);
821
+ if (!templateId) {
822
+ throw new Error('template is required for capability use');
823
+ }
824
+ const specId = normalizeText(options.spec || options.specId) || null;
825
+ const manager = new TemplateManager();
826
+ const template = await manager.showTemplate(templateId);
827
+ const { sourceName, templateId: parsedTemplateId } = parseTemplatePath(templateId);
828
+ await manager.ensureCached(sourceName);
829
+ const sourcePath = manager.cacheManager.getSourceCachePath(sourceName);
830
+ const templateDir = path.join(sourcePath, parsedTemplateId);
831
+ const capabilityFile = path.join(templateDir, 'capability-template.json');
832
+ let templatePayload = null;
833
+ if (await fileSystem.pathExists(capabilityFile)) {
834
+ try {
835
+ templatePayload = await fileSystem.readJson(capabilityFile);
836
+ } catch (_error) {
837
+ templatePayload = null;
838
+ }
839
+ }
840
+
841
+ const recommendedTasks = [];
842
+ if (templatePayload && templatePayload.source_candidate && Array.isArray(templatePayload.source_candidate.specs)) {
843
+ templatePayload.source_candidate.specs.forEach((spec) => {
844
+ const sample = Array.isArray(spec.task_sample) ? spec.task_sample : [];
845
+ sample.forEach((task) => {
846
+ if (task && task.title) {
847
+ recommendedTasks.push({
848
+ title: task.title,
849
+ source_spec_id: spec.spec_id || null,
850
+ source_task_id: task.id || null
851
+ });
852
+ }
853
+ });
854
+ });
855
+ }
856
+ if (recommendedTasks.length === 0) {
857
+ recommendedTasks.push({ title: `Implement capability scope: ${template.name || parsedTemplateId}` });
858
+ }
859
+
860
+ const plan = {
861
+ mode: 'capability-use-plan',
862
+ generated_at: new Date().toISOString(),
863
+ template: {
864
+ id: template.id,
865
+ name: template.name,
866
+ source: template.source,
867
+ description: template.description,
868
+ ontology_scope: template.ontology_scope || {}
869
+ },
870
+ spec_id: specId,
871
+ recommended_tasks: recommendedTasks
872
+ };
873
+
874
+ const outputPath = normalizeText(options.out) || buildDefaultUsePlanPath(specId || 'spec', template.id);
875
+ if (normalizeBoolean(options.write, true)) {
876
+ await fileSystem.ensureDir(path.dirname(path.join(projectPath, outputPath)));
877
+ await fileSystem.writeJson(path.join(projectPath, outputPath), plan, { spaces: 2 });
878
+ plan.output_file = outputPath;
879
+ }
880
+
881
+ if (!normalizeBoolean(options.json, false)) {
882
+ console.log(chalk.green('✅ Capability use plan generated'));
883
+ console.log(chalk.gray(` Template: ${template.id}`));
884
+ if (specId) {
885
+ console.log(chalk.gray(` Spec: ${specId}`));
886
+ }
887
+ if (plan.output_file) {
888
+ console.log(chalk.gray(` Output: ${plan.output_file}`));
889
+ }
890
+ }
891
+
892
+ return plan;
893
+ }
894
+
540
895
  function registerCapabilityCommands(program) {
541
896
  const capabilityCmd = program
542
897
  .command('capability')
@@ -623,6 +978,141 @@ function registerCapabilityCommands(program) {
623
978
  tags
624
979
  });
625
980
  });
981
+
982
+ const catalogCmd = capabilityCmd
983
+ .command('catalog')
984
+ .description('Browse and reuse capability templates');
985
+
986
+ catalogCmd
987
+ .command('list')
988
+ .description('List capability templates')
989
+ .option('--source <name>', 'Template source name')
990
+ .option('--category <name>', 'Template category filter')
991
+ .option('--compatible-with <semver>', 'SCE version compatibility')
992
+ .option('--risk <level>', 'Risk level filter')
993
+ .option('--json', 'Output JSON to stdout')
994
+ .action(async (options) => {
995
+ try {
996
+ const payload = await listCapabilityCatalog({
997
+ source: options.source,
998
+ category: options.category,
999
+ compatibleWith: options.compatibleWith,
1000
+ risk: options.risk,
1001
+ json: options.json
1002
+ });
1003
+ if (options.json) {
1004
+ console.log(JSON.stringify(payload, null, 2));
1005
+ }
1006
+ } catch (error) {
1007
+ console.log();
1008
+ console.log(chalk.red('❌ Error:'), error.message);
1009
+ if (error instanceof TemplateError && error.suggestions) {
1010
+ console.log();
1011
+ console.log(chalk.yellow('💡 Suggestions:'));
1012
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1013
+ }
1014
+ process.exit(1);
1015
+ }
1016
+ });
1017
+
1018
+ catalogCmd
1019
+ .command('search <keyword>')
1020
+ .description('Search capability templates')
1021
+ .option('--source <name>', 'Template source name')
1022
+ .option('--category <name>', 'Template category filter')
1023
+ .option('--compatible-with <semver>', 'SCE version compatibility')
1024
+ .option('--risk <level>', 'Risk level filter')
1025
+ .option('--json', 'Output JSON to stdout')
1026
+ .action(async (keyword, options) => {
1027
+ try {
1028
+ const payload = await searchCapabilityCatalog(keyword, {
1029
+ source: options.source,
1030
+ category: options.category,
1031
+ compatibleWith: options.compatibleWith,
1032
+ risk: options.risk,
1033
+ json: options.json
1034
+ });
1035
+ if (options.json) {
1036
+ console.log(JSON.stringify(payload, null, 2));
1037
+ }
1038
+ } catch (error) {
1039
+ console.log();
1040
+ console.log(chalk.red('❌ Error:'), error.message);
1041
+ if (error instanceof TemplateError && error.suggestions) {
1042
+ console.log();
1043
+ console.log(chalk.yellow('💡 Suggestions:'));
1044
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1045
+ }
1046
+ process.exit(1);
1047
+ }
1048
+ });
1049
+
1050
+ catalogCmd
1051
+ .command('show <template-id>')
1052
+ .description('Show capability template details')
1053
+ .option('--json', 'Output JSON to stdout')
1054
+ .action(async (templateId, options) => {
1055
+ try {
1056
+ const payload = await showCapabilityTemplate(templateId, { json: options.json });
1057
+ if (options.json) {
1058
+ console.log(JSON.stringify(payload, null, 2));
1059
+ }
1060
+ } catch (error) {
1061
+ console.log();
1062
+ console.log(chalk.red('❌ Error:'), error.message);
1063
+ if (error instanceof TemplateError && error.suggestions) {
1064
+ console.log();
1065
+ console.log(chalk.yellow('💡 Suggestions:'));
1066
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1067
+ }
1068
+ process.exit(1);
1069
+ }
1070
+ });
1071
+
1072
+ capabilityCmd
1073
+ .command('match')
1074
+ .description('Match capability templates to a spec using ontology scope')
1075
+ .requiredOption('--spec <spec-id>', 'Spec identifier')
1076
+ .option('--query <text>', 'Additional keyword query')
1077
+ .option('--source <name>', 'Template source name')
1078
+ .option('--compatible-with <semver>', 'SCE version compatibility')
1079
+ .option('--risk <level>', 'Risk level filter')
1080
+ .option('--limit <n>', 'Max match results', '10')
1081
+ .option('--strict', 'Fail if domain-chain missing or invalid')
1082
+ .option('--json', 'Output JSON to stdout')
1083
+ .action(async (options) => {
1084
+ try {
1085
+ const payload = await matchCapabilityTemplates(options);
1086
+ if (options.json) {
1087
+ console.log(JSON.stringify(payload, null, 2));
1088
+ }
1089
+ } catch (error) {
1090
+ console.log();
1091
+ console.log(chalk.red('❌ Error:'), error.message);
1092
+ process.exit(1);
1093
+ }
1094
+ });
1095
+
1096
+ capabilityCmd
1097
+ .command('use')
1098
+ .description('Generate a capability usage plan for a spec')
1099
+ .requiredOption('--template <template-id>', 'Capability template identifier')
1100
+ .option('--spec <spec-id>', 'Spec identifier')
1101
+ .option('--out <path>', 'Output JSON path')
1102
+ .option('--no-write', 'Skip writing output file')
1103
+ .option('--json', 'Output JSON to stdout')
1104
+ .action(async (options) => {
1105
+ try {
1106
+ const payload = await useCapabilityTemplate(options);
1107
+ if (options.json) {
1108
+ console.log(JSON.stringify(payload, null, 2));
1109
+ }
1110
+ } catch (error) {
1111
+ console.log();
1112
+ console.log(chalk.red('❌ Error:'), error.message);
1113
+ process.exit(1);
1114
+ }
1115
+ });
626
1116
  }
627
1117
 
628
1118
  module.exports = {