scene-capability-engine 3.6.14 → 3.6.16

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,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.16] - 2026-03-06
11
+
12
+ ### Added
13
+ - Capability registry validation now enforces ontology core triads for all capability templates.
14
+ - Capability catalog, match, and use payloads now expose `ontology_core` for direct UI rendering.
15
+ - Capability extract now reports triad readiness in candidate summaries.
16
+
17
+ ## [3.6.15] - 2026-03-06
18
+
19
+ ### Added
20
+ - Capability plan apply result now includes preview lines + skipped titles for UI feedback.
21
+ - Capability iteration now enforces ontology core triads by default and exposes triad readiness in catalog/match/use payloads:
22
+ - entity + relation
23
+ - business rule
24
+ - decision strategy
25
+
10
26
  ## [3.6.14] - 2026-03-06
11
27
 
12
28
  ### Added
package/README.md CHANGED
@@ -218,5 +218,5 @@ MIT. See [LICENSE](LICENSE).
218
218
 
219
219
  ---
220
220
 
221
- **Version**: 3.6.14
221
+ **Version**: 3.6.16
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.14
221
+ **版本**:3.6.16
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/show/match/use` responses now include `ontology_core` 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 使用计划
@@ -245,6 +245,7 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
245
245
 
246
246
  const lines = [];
247
247
  const addedTasks = [];
248
+ const skippedTitles = [];
248
249
  let nextTaskId = registry.maxTaskId + 1;
249
250
  let duplicateCount = 0;
250
251
 
@@ -256,6 +257,7 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
256
257
  const titleKey = title.toLowerCase();
257
258
  if (registry.existingTitles.has(titleKey)) {
258
259
  duplicateCount += 1;
260
+ skippedTitles.push(title);
259
261
  continue;
260
262
  }
261
263
  registry.existingTitles.add(titleKey);
@@ -276,8 +278,11 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
276
278
  if (addedTasks.length === 0) {
277
279
  return {
278
280
  tasks_path: tasksPath,
281
+ section_title: sectionTitle,
279
282
  added_count: 0,
280
283
  skipped_duplicates: duplicateCount,
284
+ skipped_titles: skippedTitles,
285
+ preview_lines: [],
281
286
  skipped_reason: recommended.length === 0
282
287
  ? 'no recommended tasks'
283
288
  : 'all recommended tasks already exist in tasks.md',
@@ -300,8 +305,11 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
300
305
 
301
306
  return {
302
307
  tasks_path: tasksPath,
308
+ section_title: sectionTitle,
303
309
  added_count: addedTasks.length,
304
310
  skipped_duplicates: duplicateCount,
311
+ skipped_titles: skippedTitles,
312
+ preview_lines: lines,
305
313
  first_task_id: addedTasks[0].task_id,
306
314
  last_task_id: addedTasks[addedTasks.length - 1].task_id,
307
315
  added_tasks: addedTasks
@@ -322,6 +330,83 @@ async function loadSpecDomainChain(projectPath, specId, fileSystem) {
322
330
  }
323
331
  }
324
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
+
325
410
  async function loadSceneIndexFromFile(projectPath, fileSystem) {
326
411
  const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
327
412
  if (!await fileSystem.pathExists(indexPath)) {
@@ -496,13 +581,22 @@ function buildScoreFromCandidate(candidate) {
496
581
  const reuseScore = Math.min(100, Math.round((specCount / 3) * 100));
497
582
  const stabilityScore = Math.round(completionRate * 100);
498
583
  const riskScore = Math.min(100, Math.round((1 - completionRate) * 100));
499
- const valueScore = Math.round((stabilityScore * 0.5) + (reuseScore * 0.3) + ((100 - riskScore) * 0.2));
584
+ const ontologySummary = buildCoreOntologySummary(candidate && candidate.ontology_scope);
585
+ const ontologyCoreScore = Math.round(ontologySummary.coverage_ratio * 100);
586
+ const valueScore = Math.round(
587
+ (stabilityScore * 0.4) +
588
+ (reuseScore * 0.2) +
589
+ ((100 - riskScore) * 0.1) +
590
+ (ontologyCoreScore * 0.3)
591
+ );
500
592
 
501
593
  return {
502
594
  completion_rate: Number(completionRate.toFixed(3)),
503
595
  reuse_score: reuseScore,
504
596
  stability_score: stabilityScore,
505
597
  risk_score: riskScore,
598
+ ontology_core_score: ontologyCoreScore,
599
+ ontology_core: ontologySummary,
506
600
  value_score: valueScore
507
601
  };
508
602
  }
@@ -518,15 +612,12 @@ function buildTemplateCandidate(candidate, mapping, options) {
518
612
  || `Capability template derived from ${sceneId}`;
519
613
  const category = normalizeText(options && options.category) || 'capability';
520
614
  const tags = normalizeStringArray(options && options.tags);
521
- const ontologyScope = (mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
522
- ? mapping.ontology_scope
523
- : {
524
- domains: [],
525
- entities: [],
526
- relations: [],
527
- business_rules: [],
528
- decisions: []
529
- };
615
+ const ontologyScope = normalizeOntologyScope(
616
+ (mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
617
+ ? mapping.ontology_scope
618
+ : candidate && candidate.ontology_scope
619
+ );
620
+ const ontologyCore = buildCoreOntologySummary(ontologyScope);
530
621
 
531
622
  return {
532
623
  mode: 'capability-template',
@@ -538,6 +629,7 @@ function buildTemplateCandidate(candidate, mapping, options) {
538
629
  scene_id: sceneId,
539
630
  source_candidate: candidate,
540
631
  ontology_scope: ontologyScope,
632
+ ontology_core: ontologyCore,
541
633
  tags,
542
634
  created_at: new Date().toISOString()
543
635
  };
@@ -591,6 +683,8 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
591
683
 
592
684
  const taskClaimer = new TaskClaimer();
593
685
  const specs = [];
686
+ const ontologyScopes = [];
687
+ const ontologyEvidence = [];
594
688
 
595
689
  for (const specId of specIds) {
596
690
  const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
@@ -605,6 +699,16 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
605
699
  } else {
606
700
  taskError = 'tasks.md missing';
607
701
  }
702
+ const domainChain = await loadSpecDomainChain(projectPath, specId, fileSystem);
703
+ const specOntologyScope = domainChain.payload ? buildOntologyScopeFromChain(domainChain.payload) : createEmptyOntologyScope();
704
+ if (domainChain.payload) {
705
+ ontologyScopes.push(specOntologyScope);
706
+ ontologyEvidence.push({
707
+ spec_id: specId,
708
+ source: path.relative(projectPath, domainChain.path),
709
+ triads: buildCoreOntologySummary(specOntologyScope)
710
+ });
711
+ }
608
712
  const taskSummary = summarizeTasks(tasks);
609
713
  specs.push({
610
714
  spec_id: specId,
@@ -615,10 +719,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
615
719
  title: task.title,
616
720
  status: task.status
617
721
  })),
722
+ ontology_scope: specOntologyScope,
723
+ ontology_source: domainChain.exists ? path.relative(projectPath, domainChain.path) : null,
724
+ ontology_error: domainChain.error || null,
618
725
  task_error: taskError
619
726
  });
620
727
  }
621
728
 
729
+ const ontologyScope = mergeOntologyScopes(ontologyScopes);
730
+ const ontologyCore = buildCoreOntologySummary(ontologyScope);
622
731
  const payload = {
623
732
  mode: 'capability-extract',
624
733
  scene_id: sceneId,
@@ -628,7 +737,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
628
737
  spec_count: specIds.length
629
738
  },
630
739
  specs,
631
- summary: buildCandidateSummary(specs)
740
+ ontology_scope: ontologyScope,
741
+ ontology_core: ontologyCore,
742
+ ontology_evidence: ontologyEvidence,
743
+ summary: {
744
+ ...buildCandidateSummary(specs),
745
+ ontology_triads_ready: ontologyCore.ready,
746
+ ontology_triads_coverage_ratio: ontologyCore.coverage_ratio,
747
+ ontology_missing_triads: ontologyCore.missing
748
+ }
632
749
  };
633
750
 
634
751
  const outputPath = normalizeText(options.out) || buildDefaultCandidatePath(sceneId);
@@ -746,6 +863,11 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
746
863
  if (!templateCandidate || !templateCandidate.template_id) {
747
864
  throw new Error('template_id missing in capability template candidate');
748
865
  }
866
+ const ontologySummary = assertCoreOntologySummary(
867
+ buildCoreOntologySummary(templateCandidate.ontology_scope),
868
+ 'capability template'
869
+ );
870
+ templateCandidate.ontology_scope = ontologySummary.ontology_scope;
749
871
 
750
872
  const exportDir = normalizeText(options.out) || buildDefaultExportDir(templateCandidate.template_id);
751
873
  const outputDirAbs = path.join(projectPath, exportDir);
@@ -764,6 +886,7 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
764
886
  mode: 'capability-register',
765
887
  template_id: templateCandidate.template_id,
766
888
  output_dir: exportDir,
889
+ ontology_core: ontologySummary,
767
890
  files: [
768
891
  path.join(exportDir, 'capability-template.json'),
769
892
  path.join(exportDir, 'template-registry.json')
@@ -914,6 +1037,7 @@ async function matchCapabilityTemplates(options = {}) {
914
1037
  description: template.description,
915
1038
  category: template.category,
916
1039
  risk_level: template.risk_level,
1040
+ ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {}),
917
1041
  score: Math.round(totalScore * 100),
918
1042
  score_components: {
919
1043
  ontology: Number(overlap.score.toFixed(3)),
@@ -998,7 +1122,8 @@ async function useCapabilityTemplate(options = {}) {
998
1122
  name: template.name,
999
1123
  source: template.source,
1000
1124
  description: template.description,
1001
- ontology_scope: template.ontology_scope || {}
1125
+ ontology_scope: template.ontology_scope || {},
1126
+ ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {})
1002
1127
  },
1003
1128
  spec_id: specId,
1004
1129
  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.14",
3
+ "version": "3.6.16",
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": {