scene-capability-engine 3.6.11 → 3.6.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/CHANGELOG.md CHANGED
@@ -7,6 +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.14] - 2026-03-06
11
+
12
+ ### Added
13
+ - `sce capability use --apply` to append recommended tasks to spec tasks.md.
14
+
15
+ ## [3.6.13] - 2026-03-06
16
+
17
+ ### Added
18
+ - Capability library reuse commands:
19
+ - `sce capability catalog list/search/show`
20
+ - `sce capability match`
21
+ - `sce capability use`
22
+ - Magicball capability library integration guide:
23
+ - `docs/magicball-capability-library.md`
24
+
25
+ ## [3.6.12] - 2026-03-06
26
+
27
+ ### Added
28
+ - Magicball task quality governance integration guide:
29
+ - `docs/magicball-task-quality-governance.md`
30
+
10
31
  ## [3.6.11] - 2026-03-05
11
32
 
12
33
  ### Added
package/README.md CHANGED
@@ -218,5 +218,5 @@ MIT. See [LICENSE](LICENSE).
218
218
 
219
219
  ---
220
220
 
221
- **Version**: 3.6.11
221
+ **Version**: 3.6.14
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.11
221
+ **版本**:3.6.14
222
222
  **最后更新**:2026-03-05
@@ -1888,6 +1888,29 @@ Schema references:
1888
1888
  - UI contract: `docs/agent-runtime/capability-iteration-ui.schema.json`
1889
1889
  - Ontology mapping: `docs/ontology/capability-mapping.schema.json`
1890
1890
 
1891
+ ### Capability Library Reuse (query -> match -> use)
1892
+
1893
+ ```bash
1894
+ # List capability templates
1895
+ sce capability catalog list --json
1896
+
1897
+ # Search capability templates
1898
+ sce capability catalog search "customer order" --json
1899
+
1900
+ # Show capability template metadata + payload
1901
+ sce capability catalog show customer-order-core --json
1902
+
1903
+ # Match capability templates to a spec (uses problem-domain-chain.json)
1904
+ sce capability match --spec 01-02-customer-order --json
1905
+ sce capability match --spec 01-02-customer-order --query "订单 库存" --limit 5 --json
1906
+
1907
+ # Generate a usage plan for a spec
1908
+ sce capability use --template customer-order-core --spec 01-02-customer-order --json
1909
+
1910
+ # Generate usage plan + append tasks to tasks.md
1911
+ sce capability use --template customer-order-core --spec 01-02-customer-order --apply --json
1912
+ ```
1913
+
1891
1914
  ### Scene Package Batch Publish
1892
1915
 
1893
1916
  ```bash
@@ -0,0 +1,123 @@
1
+ # Magicball 能力库复用对接说明(SCE)
2
+
3
+ > 目标:在 Magicball UI 中提供“能力库检索/匹配/使用”闭环,加速场景能力落地。
4
+
5
+ ## 1. 能力库复用流程
6
+
7
+ 1. **查询**能力库(列表/搜索)
8
+ 2. **匹配**当前 spec 的问题域(基于 problem-domain-chain 里的 ontology)
9
+ 3. **使用**能力模板(生成可执行的 usage plan)
10
+
11
+ ## 2. SCE CLI 支持(对 Magicball 的最小封装)
12
+
13
+ ### 2.1 查询与搜索
14
+
15
+ ```bash
16
+ sce capability catalog list --json
17
+ sce capability catalog search "customer order" --json
18
+ sce capability catalog show <template-id> --json
19
+ ```
20
+
21
+ ### 2.2 匹配(基于 spec ontology)
22
+
23
+ ```bash
24
+ sce capability match --spec <spec-id> --json
25
+ sce capability match --spec <spec-id> --query "订单 库存" --limit 5 --json
26
+ ```
27
+
28
+ 匹配会读取:
29
+ - `.sce/specs/<spec>/custom/problem-domain-chain.json`
30
+
31
+ 若该文件缺失,默认仍返回结果,但 `warnings` 中会标记缺失。
32
+
33
+ ### 2.3 使用(生成 usage plan)
34
+
35
+ ```bash
36
+ sce capability use --template <template-id> --spec <spec-id> --json
37
+ ```
38
+
39
+ 输出:`capability-use-plan`(用于 UI 展示和后续手工应用)。
40
+
41
+ 如需直接写入 spec 任务:
42
+
43
+ ```bash
44
+ sce capability use --template <template-id> --spec <spec-id> --apply --json
45
+ ```
46
+
47
+ ## 3. API 封装建议(CLI -> HTTP)
48
+
49
+ 建议 Magicball 后端封装为:
50
+
51
+ - `POST /api/sce/capability/catalog/list`
52
+ - `POST /api/sce/capability/catalog/search`
53
+ - `POST /api/sce/capability/catalog/show`
54
+ - `POST /api/sce/capability/match`
55
+ - `POST /api/sce/capability/use`
56
+
57
+ 请求体示例:
58
+
59
+ ```json
60
+ {
61
+ "specId": "01-02-customer-order",
62
+ "query": "订单 库存",
63
+ "limit": 5
64
+ }
65
+ ```
66
+
67
+ ## 4. UI 行为建议(Magicball)
68
+
69
+ ### 4.1 能力库入口
70
+ - 顶部图标入口
71
+ - Tabs:`能力库` / `匹配结果` / `使用计划`
72
+
73
+ ### 4.2 能力库列表
74
+ - 支持筛选:category / risk / source
75
+ - 展示关键信息:`name` / `description` / `ontology_scope`
76
+
77
+ ### 4.3 匹配结果
78
+ - 展示 `score` + `score_components`
79
+ - 支持一键生成 usage plan
80
+
81
+ ### 4.4 使用计划
82
+ - 展示 `recommended_tasks`
83
+ - 可手工转为当前 spec 的任务
84
+
85
+ ## 5. 输出结构(关键字段)
86
+
87
+ ### capability-match
88
+ ```json
89
+ {
90
+ "mode": "capability-match",
91
+ "spec_id": "01-02-customer-order",
92
+ "scene_id": "scene.customer-order",
93
+ "match_count": 12,
94
+ "matches": [
95
+ {
96
+ "template_id": "customer-order-core",
97
+ "score": 82,
98
+ "score_components": {
99
+ "ontology": 0.72,
100
+ "scenario": 1,
101
+ "keyword": 0.35
102
+ }
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ ### capability-use-plan
109
+ ```json
110
+ {
111
+ "mode": "capability-use-plan",
112
+ "template": {"id": "customer-order-core", "name": "Customer Order Core"},
113
+ "spec_id": "01-02-customer-order",
114
+ "recommended_tasks": [
115
+ {"title": "Define order entity"},
116
+ {"title": "Implement order lifecycle"}
117
+ ]
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ 若需要“自动落地写入 spec 任务”的强制执行模式,可以在后续版本加 `--apply` 开关。
@@ -0,0 +1,301 @@
1
+ # Magicball 任务质量治理对接说明(SCE)
2
+
3
+ > 适用于:Magicball AI 助手任务卡 UI 的质量治理增强与 SCE Task 质量闭环对接。
4
+
5
+ ## 1. 背景与目标
6
+
7
+ 任务由对话生成,容易出现「多事项混杂、目标不清晰、验收缺失」等问题,导致执行偏移或兜底编程。
8
+ SCE 新增「任务质量治理」闭环能力,保证每个任务可执行、可验收、可追踪。
9
+
10
+ 目标:
11
+ - 让 Magicball UI 在**同一任务卡**内完成“草案 -> 评分 -> 修正 -> Promote”的闭环
12
+ - 通过质量门禁(Policy)强制任务可执行、可验收
13
+ - 保留用户原始输入,避免信息丢失
14
+
15
+ ## 2. SCE 新增能力概览
16
+
17
+ 新增 CLI:
18
+ - `sce task draft`
19
+ - `sce task consolidate`
20
+ - `sce task score`
21
+ - `sce task promote`
22
+
23
+ 新增策略文件:
24
+ - `.sce/config/task-quality-policy.json`
25
+ - 支持 `--policy <path>` 覆盖
26
+
27
+ 默认门禁(可配置):
28
+ - acceptance_criteria 必须存在
29
+ - needs_split 必须为 false
30
+ - min_score >= 70
31
+
32
+ ## 3. 推荐交互流程(最短闭环)
33
+
34
+ 1. 用户输入 -> `sce task draft`
35
+ 2. 多轮输入合并 -> `sce task consolidate`
36
+ 3. 评分 -> `sce task score`
37
+ 4. 通过门禁 -> `sce task promote`
38
+ 5. 成功写入 `tasks.md`
39
+
40
+ ## 4. 字段契约(建议 Magicball 消费)
41
+
42
+ 核心字段:
43
+ - `task_ref`(草案阶段可为空)
44
+ - `title_norm`
45
+ - `raw_request`
46
+ - `goal`
47
+ - `sub_goals`
48
+ - `acceptance_criteria`
49
+ - `needs_split`
50
+ - `confidence`
51
+ - `next_action`
52
+ - `handoff`
53
+ - `score`
54
+
55
+ 示例(草案阶段):
56
+ ```json
57
+ {
58
+ "task_ref": null,
59
+ "title_norm": "生成客户-订单-库存演示数据流程",
60
+ "raw_request": "帮我做一个客户订单库存的demo",
61
+ "goal": "生成可运行的客户-订单-库存演示流程",
62
+ "sub_goals": ["定义实体关系", "生成测试数据", "配置页面展示"],
63
+ "acceptance_criteria": [],
64
+ "needs_split": true,
65
+ "confidence": 0.68,
66
+ "next_action": "split",
67
+ "handoff": "needs_split=true, acceptance_criteria empty"
68
+ }
69
+ ```
70
+
71
+ 评分示例:
72
+ ```json
73
+ {
74
+ "score": 62,
75
+ "missing_items": ["acceptance_criteria", "split_required"]
76
+ }
77
+ ```
78
+
79
+ Promote 成功:
80
+ ```json
81
+ {
82
+ "success": true,
83
+ "task_ref": "01.02.03",
84
+ "message": "promoted"
85
+ }
86
+ ```
87
+
88
+ Promote 失败:
89
+ ```json
90
+ {
91
+ "success": false,
92
+ "message": "quality gate failed",
93
+ "reasons": ["acceptance_criteria missing", "needs_split=true"]
94
+ }
95
+ ```
96
+
97
+ ## 5. UI 行为规范(强制)
98
+
99
+ - 草案页:默认显示**评分卡 + 缺失项**
100
+ - `needs_split=true`:必须拆分或补充,**禁止 promote**
101
+ - `acceptance_criteria` 为空:**阻断 promote**
102
+ - promote 失败时提示固定文案:**“质量门禁未通过”**
103
+
104
+ ## 6. 基于现有任务 UI 的最小改动建议
105
+
106
+ 当前 UI:单卡片 + 事件流 + 文件变更 + 错误信息。
107
+ 在不改整体布局的前提下,建议:
108
+
109
+ 1) 任务头部
110
+ - 显示 `title_norm`
111
+ - `raw_request` 置于标题下方(可折叠)
112
+ - 状态徽标:Draft / Needs Split / Missing Acceptance / Ready / Failed Gate
113
+
114
+ 2) 评分卡(插入到事件流上方)
115
+ - 展示 `score / missing_items / next_action`
116
+ - 点击展开查看策略阈值与建议
117
+
118
+ 3) 强制阻断逻辑
119
+ - needs_split 或 acceptance 缺失 => promote 按钮置灰
120
+ - 展示原因与修复入口
121
+
122
+ 4) Promote 失败提示
123
+ - 固定文案 “质量门禁未通过”
124
+ - 展示失败原因列表
125
+
126
+ 5) 原有事件流保留,增强复制能力
127
+ - 错误日志一键复制,便于诊断
128
+
129
+ ## 7. 参考命令(Magicball 封装为 API 即可)
130
+
131
+ ```bash
132
+ sce task draft --spec <specPath> --input "<user text>"
133
+ sce task consolidate --spec <specPath>
134
+ sce task score --spec <specPath>
135
+ sce task promote --spec <specPath>
136
+ ```
137
+
138
+ ## 8. 版本与发布说明
139
+
140
+ 该能力自 **SCE v3.6.11** 开始提供,若 Magicball 需要兼容老版本,请做版本检测与能力降级处理。
141
+
142
+ ---
143
+
144
+ 如需进一步输出 UI 页面原型或字段映射表,可直接在此文档上增补。
145
+ ## 9. 字段映射表(SCE -> Magicball UI)
146
+
147
+ | SCE 字段 | UI 位置 | 展示方式 | 规则 |
148
+ | --- | --- | --- | --- |
149
+ | task_ref | 任务卡标题前 | 小号标签 | 无则隐藏 |
150
+ | title_norm | 任务卡标题 | 主标题 | 必填 |
151
+ | raw_request | 标题下方 | 灰色可折叠 | 保留原文 |
152
+ | goal | 详情区 | 主目标段落 | 若空提示补齐 |
153
+ | sub_goals | 详情区 | 列表 | needs_split=true 时高亮 |
154
+ | acceptance_criteria | 详情区 | 验收列表 | 为空阻断 promote |
155
+ | needs_split | 状态徽标 | Needs Split | true 则阻断 |
156
+ | confidence | 评分卡 | 低/中/高 | <0.6 高亮 |
157
+ | score | 评分卡 | 数值 + 色阶 | <阈值红色 |
158
+ | missing_items | 评分卡 | 缺失列表 | 点击展开 |
159
+ | next_action | 评分卡 | 下一步 | 生成操作建议 |
160
+ | handoff | 详情区 | 灰色提示 | 展示门禁原因 |
161
+ | errors | 事件流 | 错误块 | 一键复制 |
162
+ | file_changes | 文件变更区 | 文件列表 | diff 快捷入口 |
163
+
164
+ ## 10. 前端组件建议
165
+
166
+ ### 10.1 TaskCard(现有卡片增强)
167
+ - Header:`task_ref + title_norm`,`raw_request` 折叠显示。
168
+ - StatusBadge:Draft / Needs Split / Missing Acceptance / Ready / Failed Gate。
169
+ - QuickActions:Score / Promote / Split / Fix Acceptance。
170
+
171
+ ### 10.2 QualityScorePanel
172
+ - 固定显示 `score / missing_items / next_action`。
173
+ - 展开显示 policy 阈值与建议。
174
+ - 禁止 promote 时显示红色提示条。
175
+
176
+ ### 10.3 AcceptanceEditor
177
+ - 为空时自动提示“请补齐验收标准”。
178
+ - 提供“自动补齐建议”按钮(由 SCE 生成)。
179
+
180
+ ### 10.4 PromoteGuard
181
+ - 拦截条件:needs_split=true 或 acceptance_criteria 为空。
182
+ - 拦截弹窗文案固定:“质量门禁未通过”。
183
+
184
+ ### 10.5 ErrorStream
185
+ - 事件流右侧增加复制按钮。
186
+ - 错误日志折叠/展开。
187
+
188
+ ## 11. 建议的调用顺序(前端按钮)
189
+
190
+ - 点击“生成草案”:`sce task draft`
191
+ - 点击“合并输入”:`sce task consolidate`
192
+ - 点击“评分”:`sce task score`
193
+ - 点击“Promote”:`sce task promote`
194
+
195
+ ---
196
+ ## 12. 示例 UI 结构(JSON/DSL)
197
+
198
+ ```json
199
+ {
200
+ "type": "TaskCard",
201
+ "props": {
202
+ "taskRef": "01.02.03",
203
+ "titleNorm": "生成客户-订单-库存演示数据流程",
204
+ "rawRequest": "帮我做一个客户订单库存的demo",
205
+ "status": "NeedsSplit",
206
+ "score": 62,
207
+ "missingItems": ["acceptance_criteria", "split_required"],
208
+ "nextAction": "split"
209
+ },
210
+ "children": [
211
+ {
212
+ "type": "QualityScorePanel",
213
+ "props": {
214
+ "score": 62,
215
+ "minScore": 70,
216
+ "missingItems": ["acceptance_criteria", "split_required"],
217
+ "nextAction": "split"
218
+ }
219
+ },
220
+ {
221
+ "type": "TaskDetails",
222
+ "props": {
223
+ "goal": "生成可运行的客户-订单-库存演示流程",
224
+ "subGoals": ["定义实体关系", "生成测试数据", "配置页面展示"],
225
+ "acceptanceCriteria": [],
226
+ "handoff": "needs_split=true, acceptance_criteria empty"
227
+ }
228
+ },
229
+ {
230
+ "type": "EventStream",
231
+ "props": {
232
+ "events": [
233
+ {"ts": "23:59:01", "level": "error", "message": "auto-fix blocked", "copyable": true},
234
+ {"ts": "23:59:02", "level": "info", "message": "Task completed"}
235
+ ]
236
+ }
237
+ },
238
+ {
239
+ "type": "FileChanges",
240
+ "props": {
241
+ "files": [
242
+ {"path": "src/app.ts", "diffRef": "diff:123", "reason": "adjust task acceptance"}
243
+ ]
244
+ }
245
+ }
246
+ ]
247
+ }
248
+ ```
249
+
250
+ ## 13. 前端 API 封装建议(CLI -> HTTP)
251
+
252
+ 建议 Magicball 在后端封装 SCE CLI,前端统一调用 HTTP。
253
+
254
+ ### 13.1 路由定义
255
+
256
+ - `POST /api/sce/task/draft`
257
+ - `POST /api/sce/task/consolidate`
258
+ - `POST /api/sce/task/score`
259
+ - `POST /api/sce/task/promote`
260
+
261
+ ### 13.2 请求体
262
+
263
+ ```json
264
+ {
265
+ "specPath": "scenes/01/specs/02/spec.md",
266
+ "input": "帮我做一个客户订单库存的demo",
267
+ "policyPath": ".sce/config/task-quality-policy.json"
268
+ }
269
+ ```
270
+
271
+ ### 13.3 响应体(统一包裹)
272
+
273
+ ```json
274
+ {
275
+ "success": true,
276
+ "data": {
277
+ "task_ref": "01.02.03",
278
+ "title_norm": "生成客户-订单-库存演示数据流程",
279
+ "raw_request": "帮我做一个客户订单库存的demo",
280
+ "goal": "生成可运行的客户-订单-库存演示流程",
281
+ "sub_goals": ["定义实体关系", "生成测试数据", "配置页面展示"],
282
+ "acceptance_criteria": ["前端能展示客户列表"],
283
+ "needs_split": false,
284
+ "confidence": 0.81,
285
+ "score": 78,
286
+ "missing_items": [],
287
+ "next_action": "promote",
288
+ "handoff": "ready"
289
+ },
290
+ "errors": []
291
+ }
292
+ ```
293
+
294
+ ### 13.4 前端调用顺序(建议)
295
+
296
+ 1. `draft` -> 渲染草案 + 评分卡
297
+ 2. `consolidate` -> 合并多轮输入(可选)
298
+ 3. `score` -> 质量评分
299
+ 4. `promote` -> 写入 tasks.md
300
+
301
+ ---
@@ -11,7 +11,10 @@ const path = require('path');
11
11
  const chalk = require('chalk');
12
12
  const TaskClaimer = require('../task/task-claimer');
13
13
  const { runStudioSpecGovernance } = require('../studio/spec-intake-governor');
14
+ const { DOMAIN_CHAIN_RELATIVE_PATH } = require('../spec/domain-modeling');
14
15
  const { SceStateStore } = require('../state/sce-state-store');
16
+ const TemplateManager = require('../templates/template-manager');
17
+ const { TemplateError } = require('../templates/template-error');
15
18
  const packageJson = require('../../package.json');
16
19
 
17
20
  const DEFAULT_ITERATION_DIR = '.sce/reports/capability-iteration';
@@ -31,6 +34,20 @@ function normalizeStringArray(value) {
31
34
  return value.map((item) => normalizeText(item)).filter(Boolean);
32
35
  }
33
36
 
37
+ function normalizeTokenList(value) {
38
+ if (Array.isArray(value)) {
39
+ return normalizeStringArray(value).map((item) => item.toLowerCase());
40
+ }
41
+ const text = normalizeText(value);
42
+ if (!text) {
43
+ return [];
44
+ }
45
+ return text
46
+ .split(/[^a-zA-Z0-9._-]+/g)
47
+ .map((item) => item.trim().toLowerCase())
48
+ .filter(Boolean);
49
+ }
50
+
34
51
  function normalizeBoolean(value, fallback = false) {
35
52
  if (typeof value === 'boolean') {
36
53
  return value;
@@ -71,6 +88,12 @@ function buildDefaultTemplatePath(sceneId) {
71
88
  return path.join(DEFAULT_ITERATION_DIR, `${safeScene}.template.json`);
72
89
  }
73
90
 
91
+ function buildDefaultUsePlanPath(specId, templateId) {
92
+ const safeSpec = normalizeText(specId).replace(/[^\w.-]+/g, '_') || 'spec';
93
+ const safeTemplate = normalizeText(templateId).replace(/[^\w.-]+/g, '_') || 'template';
94
+ return path.join(DEFAULT_ITERATION_DIR, 'usage', `${safeSpec}.${safeTemplate}.plan.json`);
95
+ }
96
+
74
97
  function buildDefaultExportDir(templateId) {
75
98
  const safeId = normalizeText(templateId).replace(/[^\w.-]+/g, '_') || 'capability';
76
99
  return path.join(DEFAULT_EXPORT_ROOT, `capability-${safeId}`);
@@ -80,6 +103,225 @@ function buildSceneIdFromCandidate(candidate) {
80
103
  return normalizeText(candidate && candidate.scene_id) || 'scene.unknown';
81
104
  }
82
105
 
106
+ function parseTemplatePath(templatePath) {
107
+ const normalized = normalizeText(templatePath);
108
+ if (!normalized) {
109
+ return { sourceName: 'official', templateId: '' };
110
+ }
111
+ if (normalized.includes(':')) {
112
+ const [sourceName, templateId] = normalized.split(':', 2);
113
+ return { sourceName: normalizeText(sourceName) || 'official', templateId: normalizeText(templateId) };
114
+ }
115
+ return { sourceName: 'official', templateId: normalized };
116
+ }
117
+
118
+ function buildOntologyScopeFromChain(domainChain) {
119
+ const ontology = domainChain && domainChain.ontology ? domainChain.ontology : {};
120
+ return {
121
+ domains: normalizeStringArray(domainChain && domainChain.scene_id ? [domainChain.scene_id] : []),
122
+ entities: normalizeStringArray(ontology.entity),
123
+ relations: normalizeStringArray(ontology.relation),
124
+ business_rules: normalizeStringArray(ontology.business_rule),
125
+ decisions: normalizeStringArray(ontology.decision_policy)
126
+ };
127
+ }
128
+
129
+ function buildOntologyOverlap(specScope, templateScope) {
130
+ const fields = ['domains', 'entities', 'relations', 'business_rules', 'decisions'];
131
+ const details = {};
132
+ let weightedTotal = 0;
133
+ let weightedMatched = 0;
134
+ let bucketCount = 0;
135
+
136
+ fields.forEach((field) => {
137
+ const expected = normalizeTokenList(specScope && specScope[field]);
138
+ const provided = normalizeTokenList(templateScope && templateScope[field]);
139
+ const providedSet = new Set(provided);
140
+ const matched = expected.filter((item) => providedSet.has(item));
141
+ const expectedCount = expected.length;
142
+ const matchedCount = matched.length;
143
+ const coverage = expectedCount > 0 ? matchedCount / expectedCount : 0;
144
+ if (expectedCount > 0) {
145
+ weightedTotal += 1;
146
+ weightedMatched += coverage;
147
+ bucketCount += 1;
148
+ }
149
+ details[field] = {
150
+ expected,
151
+ provided,
152
+ matched,
153
+ expected_count: expectedCount,
154
+ matched_count: matchedCount,
155
+ coverage_ratio: Number(coverage.toFixed(3))
156
+ };
157
+ });
158
+
159
+ const score = bucketCount > 0 ? weightedMatched / weightedTotal : 0;
160
+ return {
161
+ score,
162
+ details
163
+ };
164
+ }
165
+
166
+ function buildKeywordScore(template, queryTokens) {
167
+ if (!queryTokens || queryTokens.length === 0) {
168
+ return 0;
169
+ }
170
+ const haystack = [
171
+ template.id,
172
+ template.name,
173
+ template.description,
174
+ ...(template.tags || []),
175
+ ...(template.applicable_scenarios || [])
176
+ ].map((item) => `${item || ''}`.toLowerCase());
177
+ const hits = queryTokens.filter((token) => haystack.some((value) => value.includes(token))).length;
178
+ return hits / queryTokens.length;
179
+ }
180
+
181
+ function collectExistingTaskRegistry(tasksContent) {
182
+ const taskPattern = /^-\s*\[[ x~-]\]\*?\s+(\d+(?:\.\d+)*)\s+(.+)$/;
183
+ const lines = String(tasksContent || '').split('\n');
184
+ const existingTitles = new Set();
185
+ let maxTaskId = 0;
186
+
187
+ for (const line of lines) {
188
+ const match = line.match(taskPattern);
189
+ if (!match) {
190
+ continue;
191
+ }
192
+
193
+ const rawId = match[1];
194
+ const rawTitle = match[2];
195
+ const taskId = Number.parseInt(String(rawId).split('.')[0], 10);
196
+
197
+ if (Number.isFinite(taskId)) {
198
+ maxTaskId = Math.max(maxTaskId, taskId);
199
+ }
200
+
201
+ const normalizedTitle = String(rawTitle || '')
202
+ .replace(/\s+\[[^\]]+\]$/, '')
203
+ .trim()
204
+ .toLowerCase();
205
+
206
+ if (normalizedTitle) {
207
+ existingTitles.add(normalizedTitle);
208
+ }
209
+ }
210
+
211
+ return {
212
+ maxTaskId,
213
+ existingTitles
214
+ };
215
+ }
216
+
217
+ function createCapabilityTaskLine(taskId, title, metadata = {}) {
218
+ const suffixParts = [];
219
+ if (metadata.templateId) {
220
+ suffixParts.push(`capability_ref=${metadata.templateId}`);
221
+ }
222
+ if (metadata.templateSource) {
223
+ suffixParts.push(`template_source=${metadata.templateSource}`);
224
+ }
225
+ const suffix = suffixParts.length > 0 ? ` [${suffixParts.join(' ')}]` : '';
226
+ return `- [ ] ${taskId} ${title}${suffix}`;
227
+ }
228
+
229
+ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
230
+ const projectPath = options.projectPath || process.cwd();
231
+ const specId = normalizeText(options.spec || options.specId);
232
+ if (!specId) {
233
+ throw new Error('spec is required to apply capability plan');
234
+ }
235
+ const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
236
+ const tasksExists = await fileSystem.pathExists(tasksPath);
237
+ if (!tasksExists) {
238
+ throw new Error(`target spec tasks.md not found: ${tasksPath}`);
239
+ }
240
+ const currentContent = await fileSystem.readFile(tasksPath, 'utf8');
241
+ const registry = collectExistingTaskRegistry(currentContent);
242
+ const recommended = Array.isArray(plan.recommended_tasks) ? plan.recommended_tasks : [];
243
+ const sectionTitle = normalizeText(options.sectionTitle)
244
+ || `## Capability Template Tasks (${plan.template.id} - ${new Date().toISOString()})`;
245
+
246
+ const lines = [];
247
+ const addedTasks = [];
248
+ let nextTaskId = registry.maxTaskId + 1;
249
+ let duplicateCount = 0;
250
+
251
+ for (const entry of recommended) {
252
+ const title = normalizeText(entry && entry.title);
253
+ if (!title) {
254
+ continue;
255
+ }
256
+ const titleKey = title.toLowerCase();
257
+ if (registry.existingTitles.has(titleKey)) {
258
+ duplicateCount += 1;
259
+ continue;
260
+ }
261
+ registry.existingTitles.add(titleKey);
262
+
263
+ lines.push(createCapabilityTaskLine(nextTaskId, title, {
264
+ templateId: plan.template.id,
265
+ templateSource: plan.template.source
266
+ }));
267
+
268
+ addedTasks.push({
269
+ task_id: nextTaskId,
270
+ title
271
+ });
272
+
273
+ nextTaskId += 1;
274
+ }
275
+
276
+ if (addedTasks.length === 0) {
277
+ return {
278
+ tasks_path: tasksPath,
279
+ added_count: 0,
280
+ skipped_duplicates: duplicateCount,
281
+ skipped_reason: recommended.length === 0
282
+ ? 'no recommended tasks'
283
+ : 'all recommended tasks already exist in tasks.md',
284
+ added_tasks: []
285
+ };
286
+ }
287
+
288
+ const prefix = currentContent.trimEnd();
289
+ const chunks = [
290
+ prefix,
291
+ '',
292
+ sectionTitle,
293
+ '',
294
+ ...lines,
295
+ ''
296
+ ];
297
+
298
+ const nextContent = chunks.join('\n');
299
+ await fileSystem.writeFile(tasksPath, nextContent, 'utf8');
300
+
301
+ return {
302
+ tasks_path: tasksPath,
303
+ added_count: addedTasks.length,
304
+ skipped_duplicates: duplicateCount,
305
+ first_task_id: addedTasks[0].task_id,
306
+ last_task_id: addedTasks[addedTasks.length - 1].task_id,
307
+ added_tasks: addedTasks
308
+ };
309
+ }
310
+
311
+ async function loadSpecDomainChain(projectPath, specId, fileSystem) {
312
+ const specPath = path.join(projectPath, '.sce', 'specs', specId);
313
+ const domainChainPath = path.join(specPath, DOMAIN_CHAIN_RELATIVE_PATH);
314
+ if (!await fileSystem.pathExists(domainChainPath)) {
315
+ return { exists: false, path: domainChainPath, payload: null };
316
+ }
317
+ try {
318
+ const payload = await fileSystem.readJson(domainChainPath);
319
+ return { exists: true, path: domainChainPath, payload };
320
+ } catch (error) {
321
+ return { exists: true, path: domainChainPath, payload: null, error: error.message };
322
+ }
323
+ }
324
+
83
325
  async function loadSceneIndexFromFile(projectPath, fileSystem) {
84
326
  const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
85
327
  if (!await fileSystem.pathExists(indexPath)) {
@@ -537,6 +779,263 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
537
779
  return result;
538
780
  }
539
781
 
782
+ function displayCapabilityCatalog(templates, options = {}) {
783
+ const total = Array.isArray(templates) ? templates.length : 0;
784
+ console.log(chalk.red('🔥') + ' Capability Library');
785
+ if (total === 0) {
786
+ console.log(chalk.yellow('No capability templates found.'));
787
+ if (options.source) {
788
+ console.log(chalk.gray(`Try removing filters or run ${chalk.cyan('sce templates update')}.`));
789
+ }
790
+ return;
791
+ }
792
+ templates.forEach((template) => {
793
+ const sourcePrefix = template.source && template.source !== 'official'
794
+ ? chalk.gray(`[${template.source}] `)
795
+ : '';
796
+ console.log(`${sourcePrefix}${chalk.cyan(template.id)} ${chalk.gray(`(${template.category})`)}`);
797
+ console.log(` ${template.name}`);
798
+ console.log(` ${chalk.gray(template.description)}`);
799
+ console.log();
800
+ });
801
+ console.log(chalk.gray(`Total: ${total} capability template(s)`));
802
+ }
803
+
804
+ async function listCapabilityCatalog(options = {}) {
805
+ const manager = new TemplateManager();
806
+ const templates = await manager.listTemplates({
807
+ category: options.category,
808
+ source: options.source,
809
+ templateType: 'capability-template',
810
+ compatibleWith: options.compatibleWith,
811
+ riskLevel: options.risk
812
+ });
813
+ if (normalizeBoolean(options.json, false)) {
814
+ return {
815
+ mode: 'capability-catalog-list',
816
+ templates
817
+ };
818
+ }
819
+ displayCapabilityCatalog(templates, options);
820
+ return { templates };
821
+ }
822
+
823
+ async function searchCapabilityCatalog(keyword, options = {}) {
824
+ const manager = new TemplateManager();
825
+ const templates = await manager.searchTemplates(keyword, {
826
+ category: options.category,
827
+ source: options.source,
828
+ templateType: 'capability-template',
829
+ compatibleWith: options.compatibleWith,
830
+ riskLevel: options.risk
831
+ });
832
+ if (normalizeBoolean(options.json, false)) {
833
+ return {
834
+ mode: 'capability-catalog-search',
835
+ keyword,
836
+ templates
837
+ };
838
+ }
839
+ displayCapabilityCatalog(templates, options);
840
+ return { templates };
841
+ }
842
+
843
+ async function showCapabilityTemplate(templatePath, options = {}) {
844
+ const manager = new TemplateManager();
845
+ const template = await manager.showTemplate(templatePath);
846
+ const { sourceName, templateId } = parseTemplatePath(templatePath);
847
+ await manager.ensureCached(sourceName);
848
+ const sourcePath = manager.cacheManager.getSourceCachePath(sourceName);
849
+ const templateDir = path.join(sourcePath, templateId);
850
+ const capabilityFile = path.join(templateDir, 'capability-template.json');
851
+ let templatePayload = null;
852
+ if (await fs.pathExists(capabilityFile)) {
853
+ try {
854
+ templatePayload = await fs.readJson(capabilityFile);
855
+ } catch (_error) {
856
+ templatePayload = null;
857
+ }
858
+ }
859
+ const result = {
860
+ mode: 'capability-catalog-show',
861
+ template,
862
+ template_file: await fs.pathExists(capabilityFile) ? capabilityFile : null,
863
+ payload: templatePayload
864
+ };
865
+ if (normalizeBoolean(options.json, false)) {
866
+ return result;
867
+ }
868
+ console.log(chalk.green('✅ Capability template loaded'));
869
+ console.log(chalk.gray(` ID: ${template.id}`));
870
+ console.log(chalk.gray(` Name: ${template.name}`));
871
+ if (templatePayload) {
872
+ console.log(chalk.gray(' Payload: capability-template.json loaded'));
873
+ }
874
+ return result;
875
+ }
876
+
877
+ async function matchCapabilityTemplates(options = {}) {
878
+ const projectPath = options.projectPath || process.cwd();
879
+ const fileSystem = options.fileSystem || fs;
880
+ const specId = normalizeText(options.spec || options.specId);
881
+ if (!specId) {
882
+ throw new Error('spec is required for capability match');
883
+ }
884
+ const chain = await loadSpecDomainChain(projectPath, specId, fileSystem);
885
+ if (!chain.exists && normalizeBoolean(options.strict, false)) {
886
+ throw new Error(`problem-domain-chain missing for spec ${specId}`);
887
+ }
888
+ if (chain.error && normalizeBoolean(options.strict, false)) {
889
+ throw new Error(`problem-domain-chain invalid: ${chain.error}`);
890
+ }
891
+ const domainChain = chain.payload || {};
892
+ const specScope = buildOntologyScopeFromChain(domainChain);
893
+ const queryTokens = normalizeTokenList(options.query)
894
+ .concat(normalizeTokenList(domainChain.problem && domainChain.problem.statement))
895
+ .concat(normalizeTokenList(domainChain.scene_id));
896
+ const manager = new TemplateManager();
897
+ const templates = await manager.listTemplates({
898
+ source: options.source,
899
+ templateType: 'capability-template',
900
+ compatibleWith: options.compatibleWith,
901
+ riskLevel: options.risk
902
+ });
903
+ const matches = templates.map((template) => {
904
+ const overlap = buildOntologyOverlap(specScope, template.ontology_scope || {});
905
+ const scenarioScore = template.applicable_scenarios && domainChain.scene_id
906
+ ? (template.applicable_scenarios.includes(domainChain.scene_id) ? 1 : 0)
907
+ : 0;
908
+ const keywordScore = buildKeywordScore(template, queryTokens);
909
+ const totalScore = (overlap.score * 0.6) + (scenarioScore * 0.2) + (keywordScore * 0.2);
910
+ return {
911
+ template_id: template.id,
912
+ source: template.source,
913
+ name: template.name,
914
+ description: template.description,
915
+ category: template.category,
916
+ risk_level: template.risk_level,
917
+ score: Math.round(totalScore * 100),
918
+ score_components: {
919
+ ontology: Number(overlap.score.toFixed(3)),
920
+ scenario: scenarioScore,
921
+ keyword: Number(keywordScore.toFixed(3))
922
+ },
923
+ overlap
924
+ };
925
+ }).sort((a, b) => b.score - a.score);
926
+
927
+ const limit = toPositiveInteger(options.limit, 10);
928
+ const payload = {
929
+ mode: 'capability-match',
930
+ spec_id: specId,
931
+ scene_id: domainChain.scene_id || null,
932
+ query: normalizeText(options.query) || null,
933
+ ontology_source: chain.exists ? chain.path : null,
934
+ match_count: matches.length,
935
+ matches: matches.slice(0, limit),
936
+ warnings: chain.exists ? [] : ['problem-domain-chain missing; ontology-based match unavailable']
937
+ };
938
+ if (normalizeBoolean(options.json, false)) {
939
+ return payload;
940
+ }
941
+ console.log(chalk.green('✅ Capability match completed'));
942
+ console.log(chalk.gray(` Spec: ${specId}`));
943
+ console.log(chalk.gray(` Matches: ${payload.matches.length}`));
944
+ return payload;
945
+ }
946
+
947
+ async function useCapabilityTemplate(options = {}) {
948
+ const projectPath = options.projectPath || process.cwd();
949
+ const fileSystem = options.fileSystem || fs;
950
+ const templateId = normalizeText(options.template || options.id);
951
+ if (!templateId) {
952
+ throw new Error('template is required for capability use');
953
+ }
954
+ if (normalizeBoolean(options.apply, false) && normalizeBoolean(options.write, true) === false) {
955
+ throw new Error('cannot use --apply with --no-write');
956
+ }
957
+ const specId = normalizeText(options.spec || options.specId) || null;
958
+ const manager = new TemplateManager();
959
+ const template = await manager.showTemplate(templateId);
960
+ const { sourceName, templateId: parsedTemplateId } = parseTemplatePath(templateId);
961
+ await manager.ensureCached(sourceName);
962
+ const sourcePath = manager.cacheManager.getSourceCachePath(sourceName);
963
+ const templateDir = path.join(sourcePath, parsedTemplateId);
964
+ const capabilityFile = path.join(templateDir, 'capability-template.json');
965
+ let templatePayload = null;
966
+ if (await fileSystem.pathExists(capabilityFile)) {
967
+ try {
968
+ templatePayload = await fileSystem.readJson(capabilityFile);
969
+ } catch (_error) {
970
+ templatePayload = null;
971
+ }
972
+ }
973
+
974
+ const recommendedTasks = [];
975
+ if (templatePayload && templatePayload.source_candidate && Array.isArray(templatePayload.source_candidate.specs)) {
976
+ templatePayload.source_candidate.specs.forEach((spec) => {
977
+ const sample = Array.isArray(spec.task_sample) ? spec.task_sample : [];
978
+ sample.forEach((task) => {
979
+ if (task && task.title) {
980
+ recommendedTasks.push({
981
+ title: task.title,
982
+ source_spec_id: spec.spec_id || null,
983
+ source_task_id: task.id || null
984
+ });
985
+ }
986
+ });
987
+ });
988
+ }
989
+ if (recommendedTasks.length === 0) {
990
+ recommendedTasks.push({ title: `Implement capability scope: ${template.name || parsedTemplateId}` });
991
+ }
992
+
993
+ const plan = {
994
+ mode: 'capability-use-plan',
995
+ generated_at: new Date().toISOString(),
996
+ template: {
997
+ id: template.id,
998
+ name: template.name,
999
+ source: template.source,
1000
+ description: template.description,
1001
+ ontology_scope: template.ontology_scope || {}
1002
+ },
1003
+ spec_id: specId,
1004
+ recommended_tasks: recommendedTasks
1005
+ };
1006
+
1007
+ const outputPath = normalizeText(options.out) || buildDefaultUsePlanPath(specId || 'spec', template.id);
1008
+ if (normalizeBoolean(options.write, true)) {
1009
+ await fileSystem.ensureDir(path.dirname(path.join(projectPath, outputPath)));
1010
+ await fileSystem.writeJson(path.join(projectPath, outputPath), plan, { spaces: 2 });
1011
+ plan.output_file = outputPath;
1012
+ }
1013
+
1014
+ if (normalizeBoolean(options.apply, false)) {
1015
+ if (!specId) {
1016
+ throw new Error('spec is required for --apply');
1017
+ }
1018
+ plan.apply = await appendCapabilityPlanToSpecTasks({
1019
+ projectPath,
1020
+ spec: specId,
1021
+ sectionTitle: options.sectionTitle
1022
+ }, plan, fileSystem);
1023
+ }
1024
+
1025
+ if (!normalizeBoolean(options.json, false)) {
1026
+ console.log(chalk.green('✅ Capability use plan generated'));
1027
+ console.log(chalk.gray(` Template: ${template.id}`));
1028
+ if (specId) {
1029
+ console.log(chalk.gray(` Spec: ${specId}`));
1030
+ }
1031
+ if (plan.output_file) {
1032
+ console.log(chalk.gray(` Output: ${plan.output_file}`));
1033
+ }
1034
+ }
1035
+
1036
+ return plan;
1037
+ }
1038
+
540
1039
  function registerCapabilityCommands(program) {
541
1040
  const capabilityCmd = program
542
1041
  .command('capability')
@@ -623,6 +1122,143 @@ function registerCapabilityCommands(program) {
623
1122
  tags
624
1123
  });
625
1124
  });
1125
+
1126
+ const catalogCmd = capabilityCmd
1127
+ .command('catalog')
1128
+ .description('Browse and reuse capability templates');
1129
+
1130
+ catalogCmd
1131
+ .command('list')
1132
+ .description('List capability templates')
1133
+ .option('--source <name>', 'Template source name')
1134
+ .option('--category <name>', 'Template category filter')
1135
+ .option('--compatible-with <semver>', 'SCE version compatibility')
1136
+ .option('--risk <level>', 'Risk level filter')
1137
+ .option('--json', 'Output JSON to stdout')
1138
+ .action(async (options) => {
1139
+ try {
1140
+ const payload = await listCapabilityCatalog({
1141
+ source: options.source,
1142
+ category: options.category,
1143
+ compatibleWith: options.compatibleWith,
1144
+ risk: options.risk,
1145
+ json: options.json
1146
+ });
1147
+ if (options.json) {
1148
+ console.log(JSON.stringify(payload, null, 2));
1149
+ }
1150
+ } catch (error) {
1151
+ console.log();
1152
+ console.log(chalk.red('❌ Error:'), error.message);
1153
+ if (error instanceof TemplateError && error.suggestions) {
1154
+ console.log();
1155
+ console.log(chalk.yellow('💡 Suggestions:'));
1156
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1157
+ }
1158
+ process.exit(1);
1159
+ }
1160
+ });
1161
+
1162
+ catalogCmd
1163
+ .command('search <keyword>')
1164
+ .description('Search capability templates')
1165
+ .option('--source <name>', 'Template source name')
1166
+ .option('--category <name>', 'Template category filter')
1167
+ .option('--compatible-with <semver>', 'SCE version compatibility')
1168
+ .option('--risk <level>', 'Risk level filter')
1169
+ .option('--json', 'Output JSON to stdout')
1170
+ .action(async (keyword, options) => {
1171
+ try {
1172
+ const payload = await searchCapabilityCatalog(keyword, {
1173
+ source: options.source,
1174
+ category: options.category,
1175
+ compatibleWith: options.compatibleWith,
1176
+ risk: options.risk,
1177
+ json: options.json
1178
+ });
1179
+ if (options.json) {
1180
+ console.log(JSON.stringify(payload, null, 2));
1181
+ }
1182
+ } catch (error) {
1183
+ console.log();
1184
+ console.log(chalk.red('❌ Error:'), error.message);
1185
+ if (error instanceof TemplateError && error.suggestions) {
1186
+ console.log();
1187
+ console.log(chalk.yellow('💡 Suggestions:'));
1188
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1189
+ }
1190
+ process.exit(1);
1191
+ }
1192
+ });
1193
+
1194
+ catalogCmd
1195
+ .command('show <template-id>')
1196
+ .description('Show capability template details')
1197
+ .option('--json', 'Output JSON to stdout')
1198
+ .action(async (templateId, options) => {
1199
+ try {
1200
+ const payload = await showCapabilityTemplate(templateId, { json: options.json });
1201
+ if (options.json) {
1202
+ console.log(JSON.stringify(payload, null, 2));
1203
+ }
1204
+ } catch (error) {
1205
+ console.log();
1206
+ console.log(chalk.red('❌ Error:'), error.message);
1207
+ if (error instanceof TemplateError && error.suggestions) {
1208
+ console.log();
1209
+ console.log(chalk.yellow('💡 Suggestions:'));
1210
+ error.suggestions.forEach((suggestion) => console.log(` • ${suggestion}`));
1211
+ }
1212
+ process.exit(1);
1213
+ }
1214
+ });
1215
+
1216
+ capabilityCmd
1217
+ .command('match')
1218
+ .description('Match capability templates to a spec using ontology scope')
1219
+ .requiredOption('--spec <spec-id>', 'Spec identifier')
1220
+ .option('--query <text>', 'Additional keyword query')
1221
+ .option('--source <name>', 'Template source name')
1222
+ .option('--compatible-with <semver>', 'SCE version compatibility')
1223
+ .option('--risk <level>', 'Risk level filter')
1224
+ .option('--limit <n>', 'Max match results', '10')
1225
+ .option('--strict', 'Fail if domain-chain missing or invalid')
1226
+ .option('--json', 'Output JSON to stdout')
1227
+ .action(async (options) => {
1228
+ try {
1229
+ const payload = await matchCapabilityTemplates(options);
1230
+ if (options.json) {
1231
+ console.log(JSON.stringify(payload, null, 2));
1232
+ }
1233
+ } catch (error) {
1234
+ console.log();
1235
+ console.log(chalk.red('❌ Error:'), error.message);
1236
+ process.exit(1);
1237
+ }
1238
+ });
1239
+
1240
+ capabilityCmd
1241
+ .command('use')
1242
+ .description('Generate a capability usage plan for a spec')
1243
+ .requiredOption('--template <template-id>', 'Capability template identifier')
1244
+ .option('--spec <spec-id>', 'Spec identifier')
1245
+ .option('--out <path>', 'Output JSON path')
1246
+ .option('--apply', 'Append recommended tasks to spec tasks.md')
1247
+ .option('--section-title <title>', 'Custom section title for tasks.md')
1248
+ .option('--no-write', 'Skip writing output file')
1249
+ .option('--json', 'Output JSON to stdout')
1250
+ .action(async (options) => {
1251
+ try {
1252
+ const payload = await useCapabilityTemplate(options);
1253
+ if (options.json) {
1254
+ console.log(JSON.stringify(payload, null, 2));
1255
+ }
1256
+ } catch (error) {
1257
+ console.log();
1258
+ console.log(chalk.red('❌ Error:'), error.message);
1259
+ process.exit(1);
1260
+ }
1261
+ });
626
1262
  }
627
1263
 
628
1264
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.11",
3
+ "version": "3.6.14",
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": {