scene-capability-engine 3.3.13 → 3.3.14

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.
@@ -5,12 +5,19 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs-extra');
8
- const path = require('path');
8
+ const semver = require('semver');
9
9
  const { ValidationError } = require('./template-error');
10
10
 
11
11
  class RegistryParser {
12
12
  constructor() {
13
13
  this.registryCache = new Map();
14
+ this.templateTypes = [
15
+ 'spec-scaffold',
16
+ 'capability-template',
17
+ 'runtime-playbook'
18
+ ];
19
+ this.validDifficulties = ['beginner', 'intermediate', 'advanced'];
20
+ this.validRiskLevels = ['low', 'medium', 'high', 'critical'];
14
21
  }
15
22
 
16
23
  /**
@@ -46,13 +53,14 @@ class RegistryParser {
46
53
  );
47
54
  }
48
55
 
49
- // Validate schema
56
+ // Validate schema and normalize metadata contract
50
57
  this.validateRegistrySchema(registry);
58
+ const normalized = this.normalizeRegistry(registry);
51
59
 
52
60
  // Cache parsed registry
53
- this.registryCache.set(registryPath, registry);
61
+ this.registryCache.set(registryPath, normalized);
54
62
 
55
- return registry;
63
+ return normalized;
56
64
  }
57
65
 
58
66
  /**
@@ -93,6 +101,45 @@ class RegistryParser {
93
101
  }
94
102
  }
95
103
 
104
+ /**
105
+ * Normalizes registry entries to the typed template contract
106
+ *
107
+ * @param {Object} registry - Registry object
108
+ * @returns {Object} Normalized registry
109
+ */
110
+ normalizeRegistry(registry) {
111
+ const templates = Array.isArray(registry.templates) ? registry.templates : [];
112
+
113
+ return {
114
+ ...registry,
115
+ templates: templates.map((template) => this.normalizeTemplateEntry(template))
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Normalizes a single template entry
121
+ *
122
+ * @param {Object} template - Template entry
123
+ * @returns {Object} Normalized template
124
+ */
125
+ normalizeTemplateEntry(template = {}) {
126
+ const templateType = this._resolveTemplateType(template);
127
+ const normalized = {
128
+ ...template,
129
+ template_type: templateType,
130
+ min_sce_version: template.min_sce_version ?? null,
131
+ max_sce_version: template.max_sce_version ?? null,
132
+ ontology_scope: this._normalizeOntologyScope(template.ontology_scope),
133
+ risk_level: template.risk_level ?? null,
134
+ rollback_contract: this._normalizeRollbackContract(template.rollback_contract),
135
+ applicable_scenarios: Array.isArray(template.applicable_scenarios) ? template.applicable_scenarios : [],
136
+ tags: Array.isArray(template.tags) ? template.tags : [],
137
+ files: Array.isArray(template.files) ? template.files : []
138
+ };
139
+
140
+ return normalized;
141
+ }
142
+
96
143
  /**
97
144
  * Validates a single template entry
98
145
  *
@@ -103,11 +150,12 @@ class RegistryParser {
103
150
  validateTemplateEntry(template, index) {
104
151
  const errors = [];
105
152
  const prefix = `Template[${index}]`;
153
+ const templateType = this._resolveTemplateType(template);
106
154
 
107
155
  // Required fields
108
156
  const requiredFields = [
109
- 'id', 'name', 'category', 'description',
110
- 'difficulty', 'tags', 'applicable_scenarios', 'files'
157
+ 'id', 'name', 'category', 'description',
158
+ 'difficulty', 'tags', 'files'
111
159
  ];
112
160
 
113
161
  for (const field of requiredFields) {
@@ -129,18 +177,74 @@ class RegistryParser {
129
177
  errors.push(`${prefix}: Field "files" must be an array`);
130
178
  }
131
179
 
180
+ if (template.template_type && !this.templateTypes.includes(template.template_type)) {
181
+ errors.push(`${prefix}: Invalid template_type "${template.template_type}". Must be one of: ${this.templateTypes.join(', ')}`);
182
+ }
183
+
184
+ if (template.min_sce_version !== undefined && template.min_sce_version !== null) {
185
+ if (typeof template.min_sce_version !== 'string' || !semver.valid(template.min_sce_version)) {
186
+ errors.push(`${prefix}: Field "min_sce_version" must be a valid semver version (e.g. 3.3.13)`);
187
+ }
188
+ }
189
+
190
+ if (template.max_sce_version !== undefined && template.max_sce_version !== null) {
191
+ if (typeof template.max_sce_version !== 'string' || !semver.valid(template.max_sce_version)) {
192
+ errors.push(`${prefix}: Field "max_sce_version" must be a valid semver version (e.g. 3.3.13)`);
193
+ }
194
+ }
195
+
196
+ if (template.min_sce_version && template.max_sce_version) {
197
+ if (semver.gt(template.min_sce_version, template.max_sce_version)) {
198
+ errors.push(`${prefix}: Field "min_sce_version" must be less than or equal to "max_sce_version"`);
199
+ }
200
+ }
201
+
202
+ if (template.ontology_scope !== undefined && template.ontology_scope !== null) {
203
+ if (!this._isPlainObject(template.ontology_scope)) {
204
+ errors.push(`${prefix}: Field "ontology_scope" must be an object`);
205
+ } else {
206
+ const ontologyArrayFields = ['domains', 'entities', 'relations', 'business_rules', 'decisions'];
207
+ for (const fieldName of ontologyArrayFields) {
208
+ if (template.ontology_scope[fieldName] !== undefined &&
209
+ !Array.isArray(template.ontology_scope[fieldName])) {
210
+ errors.push(`${prefix}: Field "ontology_scope.${fieldName}" must be an array when present`);
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ if (template.risk_level !== undefined && template.risk_level !== null) {
217
+ if (!this.validRiskLevels.includes(template.risk_level)) {
218
+ errors.push(`${prefix}: Invalid risk_level "${template.risk_level}". Must be one of: ${this.validRiskLevels.join(', ')}`);
219
+ }
220
+ }
221
+
222
+ if (template.rollback_contract !== undefined && template.rollback_contract !== null) {
223
+ if (!this._isPlainObject(template.rollback_contract)) {
224
+ errors.push(`${prefix}: Field "rollback_contract" must be an object`);
225
+ } else {
226
+ if (typeof template.rollback_contract.supported !== 'boolean') {
227
+ errors.push(`${prefix}: Field "rollback_contract.supported" must be boolean`);
228
+ }
229
+ if (template.rollback_contract.strategy !== undefined &&
230
+ template.rollback_contract.strategy !== null &&
231
+ typeof template.rollback_contract.strategy !== 'string') {
232
+ errors.push(`${prefix}: Field "rollback_contract.strategy" must be a string when present`);
233
+ }
234
+ }
235
+ }
236
+
132
237
  // Validate difficulty
133
- const validDifficulties = ['beginner', 'intermediate', 'advanced'];
134
- if (template.difficulty && !validDifficulties.includes(template.difficulty)) {
135
- errors.push(`${prefix}: Invalid difficulty "${template.difficulty}". Must be one of: ${validDifficulties.join(', ')}`);
238
+ if (template.difficulty && !this.validDifficulties.includes(template.difficulty)) {
239
+ errors.push(`${prefix}: Invalid difficulty "${template.difficulty}". Must be one of: ${this.validDifficulties.join(', ')}`);
136
240
  }
137
241
 
138
- // Validate files array
139
- if (template.files && Array.isArray(template.files)) {
242
+ // Validate files array (legacy requirement only for spec scaffolds)
243
+ if (templateType === 'spec-scaffold' && template.files && Array.isArray(template.files)) {
140
244
  const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
141
245
  for (const file of requiredFiles) {
142
246
  if (!template.files.includes(file)) {
143
- errors.push(`${prefix}: Missing required file "${file}" in files array`);
247
+ errors.push(`${prefix}: spec-scaffold missing required file "${file}" in files array`);
144
248
  }
145
249
  }
146
250
  }
@@ -160,6 +264,8 @@ class RegistryParser {
160
264
  byCategory: {},
161
265
  byTag: {},
162
266
  byDifficulty: {},
267
+ byTemplateType: {},
268
+ byRiskLevel: {},
163
269
  all: []
164
270
  };
165
271
 
@@ -187,6 +293,20 @@ class RegistryParser {
187
293
  }
188
294
  index.byDifficulty[template.difficulty].push(template);
189
295
 
296
+ // Index by template type
297
+ const templateType = this._resolveTemplateType(template);
298
+ if (!index.byTemplateType[templateType]) {
299
+ index.byTemplateType[templateType] = [];
300
+ }
301
+ index.byTemplateType[templateType].push(template);
302
+
303
+ // Index by risk level
304
+ const riskLevel = template.risk_level || 'unspecified';
305
+ if (!index.byRiskLevel[riskLevel]) {
306
+ index.byRiskLevel[riskLevel] = [];
307
+ }
308
+ index.byRiskLevel[riskLevel].push(template);
309
+
190
310
  // Add to all templates
191
311
  index.all.push(template);
192
312
  }
@@ -216,7 +336,7 @@ class RegistryParser {
216
336
  }
217
337
 
218
338
  seenIds.add(template.id);
219
- merged.templates.push(template);
339
+ merged.templates.push(this.normalizeTemplateEntry(template));
220
340
  }
221
341
  }
222
342
 
@@ -274,6 +394,17 @@ class RegistryParser {
274
394
  return index.byDifficulty[difficulty] || [];
275
395
  }
276
396
 
397
+ /**
398
+ * Gets templates by template type
399
+ *
400
+ * @param {Object} index - Registry index
401
+ * @param {string} templateType - Template type
402
+ * @returns {Object[]} Array of templates
403
+ */
404
+ getTemplatesByTemplateType(index, templateType) {
405
+ return index.byTemplateType[templateType] || [];
406
+ }
407
+
277
408
  /**
278
409
  * Gets all categories
279
410
  *
@@ -302,6 +433,8 @@ class RegistryParser {
302
433
  * @param {Object} filters - Optional filters
303
434
  * @param {string} filters.category - Filter by category
304
435
  * @param {string} filters.difficulty - Filter by difficulty
436
+ * @param {string} filters.templateType - Filter by template type
437
+ * @param {string} filters.riskLevel - Filter by risk level
305
438
  * @param {string[]} filters.tags - Filter by tags (any match)
306
439
  * @returns {Object[]} Array of matching templates
307
440
  */
@@ -327,6 +460,17 @@ class RegistryParser {
327
460
  continue;
328
461
  }
329
462
 
463
+ if (filters.templateType && this._resolveTemplateType(template) !== filters.templateType) {
464
+ continue;
465
+ }
466
+
467
+ if (filters.riskLevel) {
468
+ const riskLevel = template.risk_level || 'unspecified';
469
+ if (riskLevel !== filters.riskLevel) {
470
+ continue;
471
+ }
472
+ }
473
+
330
474
  if (filters.tags && filters.tags.length > 0) {
331
475
  const hasMatchingTag = filters.tags.some(tag =>
332
476
  template.tags.includes(tag)
@@ -375,6 +519,21 @@ class RegistryParser {
375
519
  }
376
520
  }
377
521
 
522
+ // Search in template type
523
+ if (this._resolveTemplateType(template).includes(keywordLower)) {
524
+ return true;
525
+ }
526
+
527
+ // Search in ontology scope values
528
+ const ontologyScope = template.ontology_scope || {};
529
+ for (const field of ['domains', 'entities', 'relations', 'business_rules', 'decisions']) {
530
+ for (const value of ontologyScope[field] || []) {
531
+ if (String(value).toLowerCase().includes(keywordLower)) {
532
+ return true;
533
+ }
534
+ }
535
+ }
536
+
378
537
  return false;
379
538
  }
380
539
 
@@ -385,6 +544,8 @@ class RegistryParser {
385
544
  * @param {Object} filters - Filters
386
545
  * @param {string} filters.category - Filter by category
387
546
  * @param {string} filters.difficulty - Filter by difficulty
547
+ * @param {string} filters.templateType - Filter by template type
548
+ * @param {string} filters.riskLevel - Filter by risk level
388
549
  * @param {string[]} filters.tags - Filter by tags (any match)
389
550
  * @returns {Object[]} Array of matching templates
390
551
  */
@@ -401,6 +562,16 @@ class RegistryParser {
401
562
  results = results.filter(t => t.difficulty === filters.difficulty);
402
563
  }
403
564
 
565
+ // Filter by template type
566
+ if (filters.templateType) {
567
+ results = results.filter(t => this._resolveTemplateType(t) === filters.templateType);
568
+ }
569
+
570
+ // Filter by risk level
571
+ if (filters.riskLevel) {
572
+ results = results.filter((t) => (t.risk_level || 'unspecified') === filters.riskLevel);
573
+ }
574
+
404
575
  // Filter by tags (any match)
405
576
  if (filters.tags && filters.tags.length > 0) {
406
577
  results = results.filter(t =>
@@ -411,6 +582,106 @@ class RegistryParser {
411
582
  return results;
412
583
  }
413
584
 
585
+ /**
586
+ * Evaluates whether a template is compatible with a target SCE version
587
+ *
588
+ * @param {Object} template - Template object
589
+ * @param {string} sceVersion - Target SCE version
590
+ * @returns {boolean} Compatibility result
591
+ */
592
+ isTemplateCompatible(template, sceVersion) {
593
+ if (!sceVersion) {
594
+ return true;
595
+ }
596
+
597
+ if (!semver.valid(sceVersion)) {
598
+ return false;
599
+ }
600
+
601
+ const minVersion = template.min_sce_version;
602
+ const maxVersion = template.max_sce_version;
603
+
604
+ if (minVersion && semver.valid(minVersion) && semver.lt(sceVersion, minVersion)) {
605
+ return false;
606
+ }
607
+
608
+ if (maxVersion && semver.valid(maxVersion) && semver.gt(sceVersion, maxVersion)) {
609
+ return false;
610
+ }
611
+
612
+ return true;
613
+ }
614
+
615
+ /**
616
+ * Resolves template type with backward-compatible default
617
+ *
618
+ * @param {Object} template - Template object
619
+ * @returns {string} Template type
620
+ * @private
621
+ */
622
+ _resolveTemplateType(template = {}) {
623
+ const candidate = String(template.template_type || '').trim();
624
+ return this.templateTypes.includes(candidate) ? candidate : 'spec-scaffold';
625
+ }
626
+
627
+ /**
628
+ * Normalizes ontology scope structure
629
+ *
630
+ * @param {Object|null|undefined} ontologyScope - Ontology scope
631
+ * @returns {Object|null} Normalized ontology scope
632
+ * @private
633
+ */
634
+ _normalizeOntologyScope(ontologyScope) {
635
+ if (!this._isPlainObject(ontologyScope)) {
636
+ return null;
637
+ }
638
+
639
+ const normalized = {};
640
+ for (const field of ['domains', 'entities', 'relations', 'business_rules', 'decisions']) {
641
+ if (Array.isArray(ontologyScope[field])) {
642
+ normalized[field] = ontologyScope[field];
643
+ }
644
+ }
645
+
646
+ return Object.keys(normalized).length > 0 ? normalized : null;
647
+ }
648
+
649
+ /**
650
+ * Normalizes rollback contract
651
+ *
652
+ * @param {Object|null|undefined} rollbackContract - rollback contract
653
+ * @returns {Object|null} Normalized rollback contract
654
+ * @private
655
+ */
656
+ _normalizeRollbackContract(rollbackContract) {
657
+ if (!this._isPlainObject(rollbackContract)) {
658
+ return null;
659
+ }
660
+
661
+ const supported = rollbackContract.supported;
662
+ if (typeof supported !== 'boolean') {
663
+ return null;
664
+ }
665
+
666
+ const normalized = { supported };
667
+ if (typeof rollbackContract.strategy === 'string' && rollbackContract.strategy.trim().length > 0) {
668
+ normalized.strategy = rollbackContract.strategy.trim();
669
+ }
670
+
671
+ return normalized;
672
+ }
673
+
674
+ /**
675
+ * Checks if value is a plain object
676
+ *
677
+ * @param {*} value - Value to check
678
+ * @returns {boolean} Is plain object
679
+ * @private
680
+ */
681
+ _isPlainObject(value) {
682
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
683
+ }
684
+
414
685
  /**
415
686
  * Sorts templates
416
687
  *
@@ -497,6 +768,14 @@ class RegistryParser {
497
768
  byDifficulty: Object.keys(index.byDifficulty).reduce((acc, diff) => {
498
769
  acc[diff] = index.byDifficulty[diff].length;
499
770
  return acc;
771
+ }, {}),
772
+ byTemplateType: Object.keys(index.byTemplateType).reduce((acc, templateType) => {
773
+ acc[templateType] = index.byTemplateType[templateType].length;
774
+ return acc;
775
+ }, {}),
776
+ byRiskLevel: Object.keys(index.byRiskLevel).reduce((acc, riskLevel) => {
777
+ acc[riskLevel] = index.byRiskLevel[riskLevel].length;
778
+ return acc;
500
779
  }, {})
501
780
  };
502
781
  }
@@ -8,6 +8,12 @@ const path = require('path');
8
8
  class TemplateExporter {
9
9
  constructor() {
10
10
  this.defaultOutputDir = '.sce/templates/exports';
11
+ this.templateRepositoryUrl =
12
+ process.env.SCE_TEMPLATE_REPOSITORY_URL ||
13
+ 'https://github.com/heguangyong/scene-capability-engine-templates';
14
+ this.templateRepositoryName = this.templateRepositoryUrl
15
+ .replace(/^https?:\/\/github\.com\//, '')
16
+ .replace(/\.git$/, '');
11
17
  }
12
18
 
13
19
  /**
@@ -134,14 +140,28 @@ class TemplateExporter {
134
140
  * @returns {Object} Registry entry
135
141
  */
136
142
  generateRegistryEntry(metadata) {
143
+ const templateType = metadata.template_type || 'spec-scaffold';
144
+ const files = Array.isArray(metadata.files) && metadata.files.length > 0
145
+ ? metadata.files
146
+ : ['requirements.md', 'design.md', 'tasks.md'];
147
+
137
148
  return {
149
+ id: `${metadata.category}/${metadata.name}`,
138
150
  name: metadata.name,
151
+ template_type: templateType,
139
152
  category: metadata.category,
140
153
  description: metadata.description,
154
+ difficulty: metadata.difficulty || 'intermediate',
141
155
  tags: metadata.tags || [],
156
+ applicable_scenarios: metadata.applicable_scenarios || [],
157
+ files,
158
+ min_sce_version: metadata.min_sce_version,
159
+ max_sce_version: metadata.max_sce_version || null,
160
+ ontology_scope: metadata.ontology_scope || null,
161
+ risk_level: metadata.risk_level || null,
162
+ rollback_contract: metadata.rollback_contract || null,
142
163
  author: metadata.author,
143
164
  version: metadata.version,
144
- kse_version: metadata.kse_version,
145
165
  created_at: metadata.created_at,
146
166
  updated_at: metadata.updated_at,
147
167
  path: `${metadata.category}/${metadata.name}`
@@ -158,7 +178,7 @@ class TemplateExporter {
158
178
 
159
179
  ## Template: ${metadata.name}
160
180
 
161
- Congratulations! Your template has been generated successfully. Follow these steps to submit it to the scene-capability-engine-templates repository.
181
+ Congratulations! Your template has been generated successfully. Follow these steps to submit it to the ${this.templateRepositoryName} repository.
162
182
 
163
183
  ## Next Steps
164
184
 
@@ -186,10 +206,10 @@ cd .sce/specs/test-spec
186
206
  **Option A: Fork + Pull Request (Recommended)**
187
207
 
188
208
  \`\`\`bash
189
- # 1. Fork the scene-capability-engine-templates repository on GitHub
209
+ # 1. Fork the ${this.templateRepositoryName} repository on GitHub
190
210
  # 2. Clone your fork
191
- git clone https://github.com/YOUR_USERNAME/scene-capability-engine-templates.git
192
- cd scene-capability-engine-templates
211
+ git clone https://github.com/YOUR_USERNAME/${this.templateRepositoryName.split('/').pop()}.git
212
+ cd ${this.templateRepositoryName.split('/').pop()}
193
213
 
194
214
  # 3. Create a branch
195
215
  git checkout -b add-${metadata.name}
@@ -214,7 +234,7 @@ git push origin add-${metadata.name}
214
234
 
215
235
  If you're not familiar with Git:
216
236
 
217
- 1. Go to https://github.com/heguangyong/scene-capability-engine-templates/issues
237
+ 1. Go to ${this.templateRepositoryUrl.replace(/\.git$/, '')}/issues
218
238
  2. Create a new issue with title: \`[Template Submission] ${metadata.name}\`
219
239
  3. Attach the template files or paste their contents
220
240
  4. A maintainer will review and add your template
@@ -234,7 +254,7 @@ If you're not familiar with Git:
234
254
  ## Questions?
235
255
 
236
256
  If you have questions about the submission process, please:
237
- - Check the scene-capability-engine-templates repository README
257
+ - Check the ${this.templateRepositoryName} repository README
238
258
  - Open an issue in the repository
239
259
  - Contact the maintainers
240
260
 
@@ -261,7 +281,7 @@ ${metadata.description}
261
281
  - **Tags**: ${metadata.tags.join(', ')}
262
282
  - **Author**: ${metadata.author}
263
283
  - **Version**: ${metadata.version}
264
- - **Minimum SCE Version**: ${metadata.kse_version}
284
+ - **Minimum SCE Version**: ${metadata.min_sce_version}
265
285
 
266
286
  ## Files Included
267
287
 
@@ -427,7 +447,7 @@ ${metadata.description}
427
447
 
428
448
  ## Prerequisites
429
449
 
430
- - sce version ${metadata.kse_version} or higher
450
+ - sce version ${metadata.min_sce_version} or higher
431
451
  - Basic understanding of Spec-driven development
432
452
 
433
453
  ## Tags
@@ -443,7 +463,7 @@ After applying this template, you'll have a complete Spec structure ready to cus
443
463
  If you have questions about using this template:
444
464
  - Check the sce documentation
445
465
  - Review the generated Spec files
446
- - Open an issue in the scene-capability-engine-templates repository
466
+ - Open an issue in the ${this.templateRepositoryName} repository
447
467
  `;
448
468
  }
449
469
 
@@ -7,6 +7,7 @@
7
7
  const fs = require('fs-extra');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
+ const semver = require('semver');
10
11
  const GitHandler = require('./git-handler');
11
12
  const CacheManager = require('./cache-manager');
12
13
  const RegistryParser = require('./registry-parser');
@@ -16,7 +17,7 @@ const { ValidationError, NetworkError } = require('./template-error');
16
17
 
17
18
  class TemplateManager {
18
19
  constructor(options = {}) {
19
- this.cacheDir = options.cacheDir || path.join(os.homedir(), '.kse', 'templates');
20
+ this.cacheDir = options.cacheDir || path.join(os.homedir(), '.sce', 'templates');
20
21
 
21
22
  // Initialize components
22
23
  this.gitHandler = new GitHandler();
@@ -222,10 +223,26 @@ class TemplateManager {
222
223
  * @param {Object} options - List options
223
224
  * @param {string} options.category - Filter by category
224
225
  * @param {string} options.source - Filter by source
226
+ * @param {string} options.templateType - Filter by template type
227
+ * @param {string} options.compatibleWith - Filter by SCE version compatibility
228
+ * @param {string} options.riskLevel - Filter by risk level
225
229
  * @returns {Promise<Object[]>} Array of templates
226
230
  */
227
231
  async listTemplates(options = {}) {
228
- const { category = null, source = null } = options;
232
+ const {
233
+ category = null,
234
+ source = null,
235
+ templateType = null,
236
+ compatibleWith = null,
237
+ riskLevel = null
238
+ } = options;
239
+
240
+ if (compatibleWith && !semver.valid(compatibleWith)) {
241
+ throw new ValidationError(
242
+ 'Invalid --compatible-with version. Expected semver like 3.3.13.',
243
+ { compatibleWith }
244
+ );
245
+ }
229
246
 
230
247
  const sources = await this.getSources();
231
248
  const enabledSources = sources.filter(s => s.enabled);
@@ -257,10 +274,18 @@ class TemplateManager {
257
274
  const registry = await this.registryParser.parseRegistry(registryPath);
258
275
  const index = this.registryParser.buildIndex(registry);
259
276
 
260
- // Get templates
261
- let templates = category
262
- ? this.registryParser.getTemplatesByCategory(index, category)
263
- : index.all;
277
+ // Apply filters
278
+ let templates = this.registryParser.filterTemplates(index, {
279
+ category,
280
+ templateType,
281
+ riskLevel
282
+ });
283
+
284
+ if (compatibleWith) {
285
+ templates = templates.filter((template) =>
286
+ this.registryParser.isTemplateCompatible(template, compatibleWith)
287
+ );
288
+ }
264
289
 
265
290
  // Add source information
266
291
  templates = templates.map(t => ({
@@ -283,15 +308,36 @@ class TemplateManager {
283
308
  *
284
309
  * @param {string} keyword - Search keyword
285
310
  * @param {Object} filters - Search filters
311
+ * @param {string} filters.source - Filter by source
312
+ * @param {string} filters.category - Filter by category
313
+ * @param {string} filters.templateType - Filter by template type
314
+ * @param {string} filters.compatibleWith - Filter by SCE version compatibility
315
+ * @param {string} filters.riskLevel - Filter by risk level
286
316
  * @returns {Promise<Object[]>} Array of matching templates
287
317
  */
288
318
  async searchTemplates(keyword, filters = {}) {
319
+ const {
320
+ source = null,
321
+ compatibleWith = null,
322
+ ...searchFilters
323
+ } = filters;
324
+
325
+ if (compatibleWith && !semver.valid(compatibleWith)) {
326
+ throw new ValidationError(
327
+ 'Invalid --compatible-with version. Expected semver like 3.3.13.',
328
+ { compatibleWith }
329
+ );
330
+ }
331
+
289
332
  const sources = await this.getSources();
290
333
  const enabledSources = sources.filter(s => s.enabled);
334
+ const targetSources = source
335
+ ? enabledSources.filter((entry) => entry.name === source)
336
+ : enabledSources;
291
337
 
292
338
  const allResults = [];
293
339
 
294
- for (const src of enabledSources) {
340
+ for (const src of targetSources) {
295
341
  if (!await this.cacheManager.cacheExists(src.name)) {
296
342
  continue;
297
343
  }
@@ -309,7 +355,12 @@ class TemplateManager {
309
355
  const registry = await this.registryParser.parseRegistry(registryPath);
310
356
  const index = this.registryParser.buildIndex(registry);
311
357
 
312
- const results = this.registryParser.searchTemplates(index, keyword, filters);
358
+ let results = this.registryParser.searchTemplates(index, keyword, searchFilters);
359
+ if (compatibleWith) {
360
+ results = results.filter((template) =>
361
+ this.registryParser.isTemplateCompatible(template, compatibleWith)
362
+ );
363
+ }
313
364
 
314
365
  // Add source information
315
366
  const resultsWithSource = results.map(t => ({
@@ -577,6 +628,9 @@ class TemplateManager {
577
628
  );
578
629
  }
579
630
 
631
+ // Registry file may have changed after pull/checkout; refresh parser cache.
632
+ this.registryParser.clearCache();
633
+
580
634
  // Get new template list
581
635
  const newRegistry = await this.registryParser.parseRegistry(oldRegistryPath);
582
636
  const newTemplates = new Map(