scene-capability-engine 3.6.15 → 3.6.17

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/CHANGELOG.md CHANGED
@@ -7,10 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.17] - 2026-03-06
11
+
12
+ ### Added
13
+ - Capability catalog/list/search/show/match/use payloads now expose `ontology_core_ui` for direct UI badge rendering.
14
+ - Magicball capability library guide now documents direct triad badge fields for list and match views.
15
+
16
+ ## [3.6.16] - 2026-03-06
17
+
18
+ ### Added
19
+ - Capability registry validation now enforces ontology core triads for all capability templates.
20
+ - Capability catalog, match, and use payloads now expose `ontology_core` for direct UI rendering.
21
+ - Capability extract now reports triad readiness in candidate summaries.
22
+
10
23
  ## [3.6.15] - 2026-03-06
11
24
 
12
25
  ### Added
13
26
  - Capability plan apply result now includes preview lines + skipped titles for UI feedback.
27
+ - Capability iteration now enforces ontology core triads by default and exposes triad readiness in catalog/match/use payloads:
28
+ - entity + relation
29
+ - business rule
30
+ - decision strategy
14
31
 
15
32
  ## [3.6.14] - 2026-03-06
16
33
 
package/README.md CHANGED
@@ -218,5 +218,5 @@ MIT. See [LICENSE](LICENSE).
218
218
 
219
219
  ---
220
220
 
221
- **Version**: 3.6.15
221
+ **Version**: 3.6.17
222
222
  **Last Updated**: 2026-03-05
package/README.zh.md CHANGED
@@ -218,5 +218,5 @@ MIT,见 [LICENSE](LICENSE)。
218
218
 
219
219
  ---
220
220
 
221
- **版本**:3.6.15
221
+ **版本**:3.6.17
222
222
  **最后更新**:2026-03-05
@@ -1869,6 +1869,12 @@ sce scene template-render --package scene-erp --values '{"entity_name":"Order"}'
1869
1869
 
1870
1870
  ### Capability Iteration (scene -> template -> ontology)
1871
1871
 
1872
+ Capability candidates are now evaluated against the ontology core triad by default:
1873
+ - entity + relation
1874
+ - business rule
1875
+ - decision strategy
1876
+
1877
+
1872
1878
  ```bash
1873
1879
  # 1) Extract capability candidate from scene history
1874
1880
  sce capability extract --scene scene.customer-order --json
@@ -1890,6 +1896,8 @@ Schema references:
1890
1896
 
1891
1897
  ### Capability Library Reuse (query -> match -> use)
1892
1898
 
1899
+ `catalog/list/search/show/match/use` responses now include `ontology_core` and `ontology_core_ui` so UI can render triad readiness directly.
1900
+
1893
1901
  ```bash
1894
1902
  # List capability templates
1895
1903
  sce capability catalog list --json
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## 1. 能力库复用流程
6
6
 
7
+ 能力模板进入能力库前,默认要求补齐本体三项核心能力:
8
+ - 实体关系
9
+ - 业务规则
10
+ - 决策策略
11
+
12
+
7
13
  1. **查询**能力库(列表/搜索)
8
14
  2. **匹配**当前 spec 的问题域(基于 problem-domain-chain 里的 ontology)
9
15
  3. **使用**能力模板(生成可执行的 usage plan)
@@ -72,10 +78,10 @@ sce capability use --template <template-id> --spec <spec-id> --apply --json
72
78
 
73
79
  ### 4.2 能力库列表
74
80
  - 支持筛选:category / risk / source
75
- - 展示关键信息:`name` / `description` / `ontology_scope`
81
+ - 展示关键信息:`name` / `description` / `ontology_scope` / `ontology_core`
76
82
 
77
83
  ### 4.3 匹配结果
78
- - 展示 `score` + `score_components`
84
+ - 展示 `score` + `score_components` + `ontology_core.ready`
79
85
  - 支持一键生成 usage plan
80
86
 
81
87
  ### 4.4 使用计划
@@ -84,6 +90,14 @@ sce capability use --template <template-id> --spec <spec-id> --apply --json
84
90
 
85
91
  ## 5. 输出结构(关键字段)
86
92
 
93
+ 列表、详情、匹配、使用计划里的模板对象都应直接消费:
94
+ - `ontology_core`
95
+ - `ontology_core_ui.ready`
96
+ - `ontology_core_ui.coverage_percent`
97
+ - `ontology_core_ui.missing`
98
+ - `ontology_core_ui.triads.entity_relation|business_rules|decision_strategy`
99
+
100
+
87
101
  ### capability-match
88
102
  ```json
89
103
  {
@@ -94,6 +108,16 @@ sce capability use --template <template-id> --spec <spec-id> --apply --json
94
108
  "matches": [
95
109
  {
96
110
  "template_id": "customer-order-core",
111
+ "ontology_core_ui": {
112
+ "ready": true,
113
+ "coverage_percent": 100,
114
+ "missing": [],
115
+ "triads": {
116
+ "entity_relation": true,
117
+ "business_rules": true,
118
+ "decision_strategy": true
119
+ }
120
+ },
97
121
  "score": 82,
98
122
  "score_components": {
99
123
  "ontology": 0.72,
@@ -330,6 +330,115 @@ async function loadSpecDomainChain(projectPath, specId, fileSystem) {
330
330
  }
331
331
  }
332
332
 
333
+ function createEmptyOntologyScope() {
334
+ return {
335
+ domains: [],
336
+ entities: [],
337
+ relations: [],
338
+ business_rules: [],
339
+ decisions: []
340
+ };
341
+ }
342
+
343
+ function normalizeOntologyScope(scope) {
344
+ const candidate = scope && typeof scope === 'object' ? scope : {};
345
+ return {
346
+ domains: normalizeStringArray(candidate.domains),
347
+ entities: normalizeStringArray(candidate.entities),
348
+ relations: normalizeStringArray(candidate.relations),
349
+ business_rules: normalizeStringArray(candidate.business_rules),
350
+ decisions: normalizeStringArray(candidate.decisions)
351
+ };
352
+ }
353
+
354
+ function mergeOntologyScopes(scopes) {
355
+ const merged = createEmptyOntologyScope();
356
+ for (const scope of Array.isArray(scopes) ? scopes : []) {
357
+ const normalized = normalizeOntologyScope(scope);
358
+ for (const field of Object.keys(merged)) {
359
+ const combined = new Set([...(merged[field] || []), ...(normalized[field] || [])]);
360
+ merged[field] = Array.from(combined);
361
+ }
362
+ }
363
+ return merged;
364
+ }
365
+
366
+ function buildCoreOntologySummary(scope) {
367
+ const normalized = normalizeOntologyScope(scope);
368
+ const triads = {
369
+ entity_relation: {
370
+ required_fields: ['entities', 'relations'],
371
+ entity_count: normalized.entities.length,
372
+ relation_count: normalized.relations.length,
373
+ passed: normalized.entities.length > 0 && normalized.relations.length > 0
374
+ },
375
+ business_rules: {
376
+ required_fields: ['business_rules'],
377
+ count: normalized.business_rules.length,
378
+ passed: normalized.business_rules.length > 0
379
+ },
380
+ decision_strategy: {
381
+ required_fields: ['decisions'],
382
+ count: normalized.decisions.length,
383
+ passed: normalized.decisions.length > 0
384
+ }
385
+ };
386
+ const passedCount = Object.values(triads).filter((item) => item.passed).length;
387
+ return {
388
+ ontology_scope: normalized,
389
+ triads,
390
+ passed_count: passedCount,
391
+ total_count: 3,
392
+ coverage_ratio: Number((passedCount / 3).toFixed(3)),
393
+ ready: passedCount === 3,
394
+ missing: Object.entries(triads)
395
+ .filter(([, value]) => !value.passed)
396
+ .map(([key]) => key)
397
+ };
398
+ }
399
+
400
+ function assertCoreOntologySummary(summary, contextLabel = 'capability template') {
401
+ const details = summary || buildCoreOntologySummary(createEmptyOntologyScope());
402
+ if (details.ready) {
403
+ return details;
404
+ }
405
+ throw new Error(
406
+ `${contextLabel} missing required ontology triads: ${details.missing.join(', ')}`
407
+ );
408
+ }
409
+
410
+ function buildOntologyCoreUiState(summary) {
411
+ const details = summary || buildCoreOntologySummary(createEmptyOntologyScope());
412
+ const labels = {
413
+ entity_relation: 'entity_relation',
414
+ business_rules: 'business_rules',
415
+ decision_strategy: 'decision_strategy'
416
+ };
417
+ return {
418
+ ready: details.ready === true,
419
+ coverage_ratio: Number(details.coverage_ratio || 0),
420
+ coverage_percent: Math.round(Number(details.coverage_ratio || 0) * 100),
421
+ missing: Array.isArray(details.missing) ? details.missing : [],
422
+ missing_labels: (Array.isArray(details.missing) ? details.missing : []).map((key) => labels[key] || key),
423
+ triads: {
424
+ entity_relation: Boolean(details.triads && details.triads.entity_relation && details.triads.entity_relation.passed),
425
+ business_rules: Boolean(details.triads && details.triads.business_rules && details.triads.business_rules.passed),
426
+ decision_strategy: Boolean(details.triads && details.triads.decision_strategy && details.triads.decision_strategy.passed)
427
+ }
428
+ };
429
+ }
430
+
431
+ function enrichCapabilityTemplateForUi(template) {
432
+ const ontologyCore = template && template.ontology_core
433
+ ? template.ontology_core
434
+ : buildCoreOntologySummary(template && template.ontology_scope ? template.ontology_scope : createEmptyOntologyScope());
435
+ return {
436
+ ...template,
437
+ ontology_core: ontologyCore,
438
+ ontology_core_ui: buildOntologyCoreUiState(ontologyCore)
439
+ };
440
+ }
441
+
333
442
  async function loadSceneIndexFromFile(projectPath, fileSystem) {
334
443
  const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
335
444
  if (!await fileSystem.pathExists(indexPath)) {
@@ -504,13 +613,22 @@ function buildScoreFromCandidate(candidate) {
504
613
  const reuseScore = Math.min(100, Math.round((specCount / 3) * 100));
505
614
  const stabilityScore = Math.round(completionRate * 100);
506
615
  const riskScore = Math.min(100, Math.round((1 - completionRate) * 100));
507
- const valueScore = Math.round((stabilityScore * 0.5) + (reuseScore * 0.3) + ((100 - riskScore) * 0.2));
616
+ const ontologySummary = buildCoreOntologySummary(candidate && candidate.ontology_scope);
617
+ const ontologyCoreScore = Math.round(ontologySummary.coverage_ratio * 100);
618
+ const valueScore = Math.round(
619
+ (stabilityScore * 0.4) +
620
+ (reuseScore * 0.2) +
621
+ ((100 - riskScore) * 0.1) +
622
+ (ontologyCoreScore * 0.3)
623
+ );
508
624
 
509
625
  return {
510
626
  completion_rate: Number(completionRate.toFixed(3)),
511
627
  reuse_score: reuseScore,
512
628
  stability_score: stabilityScore,
513
629
  risk_score: riskScore,
630
+ ontology_core_score: ontologyCoreScore,
631
+ ontology_core: ontologySummary,
514
632
  value_score: valueScore
515
633
  };
516
634
  }
@@ -526,15 +644,12 @@ function buildTemplateCandidate(candidate, mapping, options) {
526
644
  || `Capability template derived from ${sceneId}`;
527
645
  const category = normalizeText(options && options.category) || 'capability';
528
646
  const tags = normalizeStringArray(options && options.tags);
529
- const ontologyScope = (mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
530
- ? mapping.ontology_scope
531
- : {
532
- domains: [],
533
- entities: [],
534
- relations: [],
535
- business_rules: [],
536
- decisions: []
537
- };
647
+ const ontologyScope = normalizeOntologyScope(
648
+ (mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
649
+ ? mapping.ontology_scope
650
+ : candidate && candidate.ontology_scope
651
+ );
652
+ const ontologyCore = buildCoreOntologySummary(ontologyScope);
538
653
 
539
654
  return {
540
655
  mode: 'capability-template',
@@ -546,6 +661,7 @@ function buildTemplateCandidate(candidate, mapping, options) {
546
661
  scene_id: sceneId,
547
662
  source_candidate: candidate,
548
663
  ontology_scope: ontologyScope,
664
+ ontology_core: ontologyCore,
549
665
  tags,
550
666
  created_at: new Date().toISOString()
551
667
  };
@@ -599,6 +715,8 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
599
715
 
600
716
  const taskClaimer = new TaskClaimer();
601
717
  const specs = [];
718
+ const ontologyScopes = [];
719
+ const ontologyEvidence = [];
602
720
 
603
721
  for (const specId of specIds) {
604
722
  const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
@@ -613,6 +731,16 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
613
731
  } else {
614
732
  taskError = 'tasks.md missing';
615
733
  }
734
+ const domainChain = await loadSpecDomainChain(projectPath, specId, fileSystem);
735
+ const specOntologyScope = domainChain.payload ? buildOntologyScopeFromChain(domainChain.payload) : createEmptyOntologyScope();
736
+ if (domainChain.payload) {
737
+ ontologyScopes.push(specOntologyScope);
738
+ ontologyEvidence.push({
739
+ spec_id: specId,
740
+ source: path.relative(projectPath, domainChain.path),
741
+ triads: buildCoreOntologySummary(specOntologyScope)
742
+ });
743
+ }
616
744
  const taskSummary = summarizeTasks(tasks);
617
745
  specs.push({
618
746
  spec_id: specId,
@@ -623,10 +751,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
623
751
  title: task.title,
624
752
  status: task.status
625
753
  })),
754
+ ontology_scope: specOntologyScope,
755
+ ontology_source: domainChain.exists ? path.relative(projectPath, domainChain.path) : null,
756
+ ontology_error: domainChain.error || null,
626
757
  task_error: taskError
627
758
  });
628
759
  }
629
760
 
761
+ const ontologyScope = mergeOntologyScopes(ontologyScopes);
762
+ const ontologyCore = buildCoreOntologySummary(ontologyScope);
630
763
  const payload = {
631
764
  mode: 'capability-extract',
632
765
  scene_id: sceneId,
@@ -636,7 +769,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
636
769
  spec_count: specIds.length
637
770
  },
638
771
  specs,
639
- summary: buildCandidateSummary(specs)
772
+ ontology_scope: ontologyScope,
773
+ ontology_core: ontologyCore,
774
+ ontology_evidence: ontologyEvidence,
775
+ summary: {
776
+ ...buildCandidateSummary(specs),
777
+ ontology_triads_ready: ontologyCore.ready,
778
+ ontology_triads_coverage_ratio: ontologyCore.coverage_ratio,
779
+ ontology_missing_triads: ontologyCore.missing
780
+ }
640
781
  };
641
782
 
642
783
  const outputPath = normalizeText(options.out) || buildDefaultCandidatePath(sceneId);
@@ -754,6 +895,11 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
754
895
  if (!templateCandidate || !templateCandidate.template_id) {
755
896
  throw new Error('template_id missing in capability template candidate');
756
897
  }
898
+ const ontologySummary = assertCoreOntologySummary(
899
+ buildCoreOntologySummary(templateCandidate.ontology_scope),
900
+ 'capability template'
901
+ );
902
+ templateCandidate.ontology_scope = ontologySummary.ontology_scope;
757
903
 
758
904
  const exportDir = normalizeText(options.out) || buildDefaultExportDir(templateCandidate.template_id);
759
905
  const outputDirAbs = path.join(projectPath, exportDir);
@@ -772,6 +918,7 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
772
918
  mode: 'capability-register',
773
919
  template_id: templateCandidate.template_id,
774
920
  output_dir: exportDir,
921
+ ontology_core: ontologySummary,
775
922
  files: [
776
923
  path.join(exportDir, 'capability-template.json'),
777
924
  path.join(exportDir, 'template-registry.json')
@@ -797,7 +944,8 @@ function displayCapabilityCatalog(templates, options = {}) {
797
944
  }
798
945
  return;
799
946
  }
800
- templates.forEach((template) => {
947
+ templates.forEach((rawTemplate) => {
948
+ const template = enrichCapabilityTemplateForUi(rawTemplate);
801
949
  const sourcePrefix = template.source && template.source !== 'official'
802
950
  ? chalk.gray(`[${template.source}] `)
803
951
  : '';
@@ -811,13 +959,13 @@ function displayCapabilityCatalog(templates, options = {}) {
811
959
 
812
960
  async function listCapabilityCatalog(options = {}) {
813
961
  const manager = new TemplateManager();
814
- const templates = await manager.listTemplates({
962
+ const templates = (await manager.listTemplates({
815
963
  category: options.category,
816
964
  source: options.source,
817
965
  templateType: 'capability-template',
818
966
  compatibleWith: options.compatibleWith,
819
967
  riskLevel: options.risk
820
- });
968
+ })).map((template) => enrichCapabilityTemplateForUi(template));
821
969
  if (normalizeBoolean(options.json, false)) {
822
970
  return {
823
971
  mode: 'capability-catalog-list',
@@ -830,13 +978,13 @@ async function listCapabilityCatalog(options = {}) {
830
978
 
831
979
  async function searchCapabilityCatalog(keyword, options = {}) {
832
980
  const manager = new TemplateManager();
833
- const templates = await manager.searchTemplates(keyword, {
981
+ const templates = (await manager.searchTemplates(keyword, {
834
982
  category: options.category,
835
983
  source: options.source,
836
984
  templateType: 'capability-template',
837
985
  compatibleWith: options.compatibleWith,
838
986
  riskLevel: options.risk
839
- });
987
+ })).map((template) => enrichCapabilityTemplateForUi(template));
840
988
  if (normalizeBoolean(options.json, false)) {
841
989
  return {
842
990
  mode: 'capability-catalog-search',
@@ -850,7 +998,7 @@ async function searchCapabilityCatalog(keyword, options = {}) {
850
998
 
851
999
  async function showCapabilityTemplate(templatePath, options = {}) {
852
1000
  const manager = new TemplateManager();
853
- const template = await manager.showTemplate(templatePath);
1001
+ const template = enrichCapabilityTemplateForUi(await manager.showTemplate(templatePath));
854
1002
  const { sourceName, templateId } = parseTemplatePath(templatePath);
855
1003
  await manager.ensureCached(sourceName);
856
1004
  const sourcePath = manager.cacheManager.getSourceCachePath(sourceName);
@@ -922,6 +1070,8 @@ async function matchCapabilityTemplates(options = {}) {
922
1070
  description: template.description,
923
1071
  category: template.category,
924
1072
  risk_level: template.risk_level,
1073
+ ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {}),
1074
+ ontology_core_ui: buildOntologyCoreUiState(template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {})),
925
1075
  score: Math.round(totalScore * 100),
926
1076
  score_components: {
927
1077
  ontology: Number(overlap.score.toFixed(3)),
@@ -1006,7 +1156,9 @@ async function useCapabilityTemplate(options = {}) {
1006
1156
  name: template.name,
1007
1157
  source: template.source,
1008
1158
  description: template.description,
1009
- ontology_scope: template.ontology_scope || {}
1159
+ ontology_scope: template.ontology_scope || {},
1160
+ ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {}),
1161
+ ontology_core_ui: buildOntologyCoreUiState(template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {}))
1010
1162
  },
1011
1163
  spec_id: specId,
1012
1164
  recommended_tasks: recommendedTasks
@@ -124,12 +124,16 @@ class RegistryParser {
124
124
  */
125
125
  normalizeTemplateEntry(template = {}) {
126
126
  const templateType = this._resolveTemplateType(template);
127
+ const ontologyScope = this._normalizeOntologyScope(template.ontology_scope);
127
128
  const normalized = {
128
129
  ...template,
129
130
  template_type: templateType,
130
131
  min_sce_version: template.min_sce_version ?? null,
131
132
  max_sce_version: template.max_sce_version ?? null,
132
- ontology_scope: this._normalizeOntologyScope(template.ontology_scope),
133
+ ontology_scope: ontologyScope,
134
+ ontology_core: templateType === 'capability-template'
135
+ ? this._buildCapabilityOntologyCore(ontologyScope)
136
+ : null,
133
137
  risk_level: template.risk_level ?? null,
134
138
  rollback_contract: this._normalizeRollbackContract(template.rollback_contract),
135
139
  applicable_scenarios: Array.isArray(template.applicable_scenarios) ? template.applicable_scenarios : [],
@@ -235,10 +239,10 @@ class RegistryParser {
235
239
  if (!this._isPlainObject(ontologyScope)) {
236
240
  errors.push(`${prefix}: capability-template requires "ontology_scope" object`);
237
241
  } else {
238
- const hasAnyScope = ['domains', 'entities', 'relations', 'business_rules', 'decisions']
239
- .some((fieldName) => Array.isArray(ontologyScope[fieldName]) && ontologyScope[fieldName].length > 0);
240
- if (!hasAnyScope) {
241
- errors.push(`${prefix}: capability-template ontology_scope must include at least one non-empty scope array`);
242
+ const normalizedOntologyScope = this._normalizeOntologyScope(ontologyScope);
243
+ const ontologyCore = this._buildCapabilityOntologyCore(normalizedOntologyScope);
244
+ if (!ontologyCore.ready) {
245
+ errors.push(`${prefix}: capability-template missing required ontology triads: ${ontologyCore.missing.join(', ')}`);
242
246
  }
243
247
  }
244
248
  }
@@ -655,6 +659,42 @@ class RegistryParser {
655
659
  return Object.keys(normalized).length > 0 ? normalized : null;
656
660
  }
657
661
 
662
+ _buildCapabilityOntologyCore(ontologyScope) {
663
+ const normalized = this._normalizeOntologyScope(ontologyScope) || {};
664
+ const entities = Array.isArray(normalized.entities) ? normalized.entities : [];
665
+ const relations = Array.isArray(normalized.relations) ? normalized.relations : [];
666
+ const businessRules = Array.isArray(normalized.business_rules) ? normalized.business_rules : [];
667
+ const decisions = Array.isArray(normalized.decisions) ? normalized.decisions : [];
668
+ const triads = {
669
+ entity_relation: {
670
+ required_fields: ['entities', 'relations'],
671
+ entity_count: entities.length,
672
+ relation_count: relations.length,
673
+ passed: entities.length > 0 && relations.length > 0
674
+ },
675
+ business_rules: {
676
+ required_fields: ['business_rules'],
677
+ count: businessRules.length,
678
+ passed: businessRules.length > 0
679
+ },
680
+ decision_strategy: {
681
+ required_fields: ['decisions'],
682
+ count: decisions.length,
683
+ passed: decisions.length > 0
684
+ }
685
+ };
686
+ const missing = Object.entries(triads)
687
+ .filter(([, value]) => !value.passed)
688
+ .map(([key]) => key);
689
+
690
+ return {
691
+ triads,
692
+ ready: missing.length === 0,
693
+ missing,
694
+ coverage_ratio: Number(((3 - missing.length) / 3).toFixed(3))
695
+ };
696
+ }
697
+
658
698
  /**
659
699
  * Normalizes rollback contract
660
700
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.15",
3
+ "version": "3.6.17",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {