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.
- package/README.md +9 -3
- package/README.zh.md +9 -3
- package/bin/scene-capability-engine.js +8 -2
- package/docs/command-reference.md +10 -0
- package/lib/commands/scene.js +30 -3
- package/lib/commands/templates.js +93 -17
- package/lib/scene-runtime/moqui-extractor.js +1 -1
- package/lib/templates/cache-manager.js +3 -3
- package/lib/templates/frontmatter-generator.js +11 -1
- package/lib/templates/metadata-collector.js +48 -10
- package/lib/templates/path-utils.js +1 -1
- package/lib/templates/registry-parser.js +292 -13
- package/lib/templates/template-exporter.js +30 -10
- package/lib/templates/template-manager.js +62 -8
- package/lib/templates/template-validator.js +24 -1
- package/lib/workspace/multi/global-config.js +1 -1
- package/lib/workspace/multi/path-utils.js +3 -3
- package/lib/workspace/multi/workspace-registry.js +1 -1
- package/lib/workspace/multi/workspace-state-manager.js +64 -2
- package/package.json +1 -1
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const fs = require('fs-extra');
|
|
8
|
-
const
|
|
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,
|
|
61
|
+
this.registryCache.set(registryPath, normalized);
|
|
54
62
|
|
|
55
|
-
return
|
|
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', '
|
|
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
|
-
|
|
134
|
-
|
|
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}:
|
|
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
|
|
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
|
|
209
|
+
# 1. Fork the ${this.templateRepositoryName} repository on GitHub
|
|
190
210
|
# 2. Clone your fork
|
|
191
|
-
git clone https://github.com/YOUR_USERNAME/
|
|
192
|
-
cd
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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(), '.
|
|
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 {
|
|
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
|
-
//
|
|
261
|
-
let templates =
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
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(
|