scene-capability-engine 3.6.9 → 3.6.11
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 +27 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/bin/scene-capability-engine.js +2 -0
- package/docs/agent-runtime/capability-iteration-ui.schema.json +226 -0
- package/docs/command-reference.md +41 -0
- package/docs/magicball-capability-iteration-api.md +154 -0
- package/docs/magicball-capability-iteration-ui.md +172 -0
- package/docs/ontology/capability-mapping.schema.json +54 -0
- package/lib/commands/capability.js +634 -0
- package/lib/commands/task.js +271 -0
- package/lib/task/task-quality-policy.js +109 -0
- package/lib/task/task-quality.js +378 -0
- package/package.json +1 -1
- package/template/.sce/config/task-quality-policy.json +8 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://scene-capability-engine.dev/contracts/capability-mapping.schema.json",
|
|
4
|
+
"title": "Capability Ontology Mapping",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": [
|
|
7
|
+
"ontology_scope"
|
|
8
|
+
],
|
|
9
|
+
"properties": {
|
|
10
|
+
"ontology_scope": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": [
|
|
13
|
+
"domains",
|
|
14
|
+
"entities",
|
|
15
|
+
"relations",
|
|
16
|
+
"business_rules",
|
|
17
|
+
"decisions"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"domains": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
},
|
|
24
|
+
"entities": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": { "type": "string" }
|
|
27
|
+
},
|
|
28
|
+
"relations": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": { "type": "string" }
|
|
31
|
+
},
|
|
32
|
+
"business_rules": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "string" }
|
|
35
|
+
},
|
|
36
|
+
"decisions": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": false
|
|
42
|
+
},
|
|
43
|
+
"notes": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"source": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"version": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": false
|
|
54
|
+
}
|
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Iteration Commands
|
|
3
|
+
*
|
|
4
|
+
* Extracts capability candidates from scene/spec/task history,
|
|
5
|
+
* scores candidates, maps them to ontology scope, and exports
|
|
6
|
+
* registry-ready capability template packages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const TaskClaimer = require('../task/task-claimer');
|
|
13
|
+
const { runStudioSpecGovernance } = require('../studio/spec-intake-governor');
|
|
14
|
+
const { SceStateStore } = require('../state/sce-state-store');
|
|
15
|
+
const packageJson = require('../../package.json');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ITERATION_DIR = '.sce/reports/capability-iteration';
|
|
18
|
+
const DEFAULT_EXPORT_ROOT = '.sce/templates/exports';
|
|
19
|
+
|
|
20
|
+
function normalizeText(value) {
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeStringArray(value) {
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return value.map((item) => normalizeText(item)).filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeBoolean(value, fallback = false) {
|
|
35
|
+
if (typeof value === 'boolean') {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toPositiveInteger(value, fallback) {
|
|
52
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
53
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildDefaultCandidatePath(sceneId) {
|
|
60
|
+
const safeScene = normalizeText(sceneId).replace(/[^\w.-]+/g, '_') || 'scene';
|
|
61
|
+
return path.join(DEFAULT_ITERATION_DIR, `${safeScene}.candidate.json`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildDefaultScorePath(sceneId) {
|
|
65
|
+
const safeScene = normalizeText(sceneId).replace(/[^\w.-]+/g, '_') || 'scene';
|
|
66
|
+
return path.join(DEFAULT_ITERATION_DIR, `${safeScene}.score.json`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildDefaultTemplatePath(sceneId) {
|
|
70
|
+
const safeScene = normalizeText(sceneId).replace(/[^\w.-]+/g, '_') || 'scene';
|
|
71
|
+
return path.join(DEFAULT_ITERATION_DIR, `${safeScene}.template.json`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildDefaultExportDir(templateId) {
|
|
75
|
+
const safeId = normalizeText(templateId).replace(/[^\w.-]+/g, '_') || 'capability';
|
|
76
|
+
return path.join(DEFAULT_EXPORT_ROOT, `capability-${safeId}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildSceneIdFromCandidate(candidate) {
|
|
80
|
+
return normalizeText(candidate && candidate.scene_id) || 'scene.unknown';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadSceneIndexFromFile(projectPath, fileSystem) {
|
|
84
|
+
const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
|
|
85
|
+
if (!await fileSystem.pathExists(indexPath)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const data = await fileSystem.readJson(indexPath);
|
|
90
|
+
return {
|
|
91
|
+
source: indexPath,
|
|
92
|
+
data
|
|
93
|
+
};
|
|
94
|
+
} catch (_error) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function loadSceneIndexFromState(projectPath, fileSystem, env) {
|
|
100
|
+
try {
|
|
101
|
+
const stateStore = new SceStateStore(projectPath, {
|
|
102
|
+
fileSystem,
|
|
103
|
+
env
|
|
104
|
+
});
|
|
105
|
+
const records = await stateStore.listGovernanceSceneIndexRecords({ limit: 500 });
|
|
106
|
+
if (!Array.isArray(records)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const scenes = {};
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (!record || !record.scene_id) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
scenes[record.scene_id] = {
|
|
115
|
+
total_specs: record.total_specs,
|
|
116
|
+
active_specs: record.active_specs,
|
|
117
|
+
completed_specs: record.completed_specs,
|
|
118
|
+
stale_specs: record.stale_specs,
|
|
119
|
+
spec_ids: Array.isArray(record.spec_ids) ? record.spec_ids : [],
|
|
120
|
+
active_spec_ids: Array.isArray(record.active_spec_ids) ? record.active_spec_ids : [],
|
|
121
|
+
stale_spec_ids: Array.isArray(record.stale_spec_ids) ? record.stale_spec_ids : []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
source: 'sqlite:governance_scene_index_registry',
|
|
126
|
+
data: {
|
|
127
|
+
schema_version: '1.0',
|
|
128
|
+
generated_at: new Date().toISOString(),
|
|
129
|
+
scene_filter: null,
|
|
130
|
+
scenes
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
} catch (_error) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function resolveSceneSpecs(sceneId, options, dependencies) {
|
|
139
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
140
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
141
|
+
const env = dependencies.env || process.env;
|
|
142
|
+
|
|
143
|
+
const explicitSpecs = normalizeStringArray(options && options.specs);
|
|
144
|
+
if (explicitSpecs.length > 0) {
|
|
145
|
+
return {
|
|
146
|
+
scene_id: sceneId,
|
|
147
|
+
spec_ids: explicitSpecs,
|
|
148
|
+
source: 'options.specs'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const indexFile = await loadSceneIndexFromFile(projectPath, fileSystem);
|
|
153
|
+
if (indexFile && indexFile.data && indexFile.data.scenes && indexFile.data.scenes[sceneId]) {
|
|
154
|
+
const record = indexFile.data.scenes[sceneId];
|
|
155
|
+
return {
|
|
156
|
+
scene_id: sceneId,
|
|
157
|
+
spec_ids: Array.isArray(record.spec_ids) ? record.spec_ids : [],
|
|
158
|
+
source: indexFile.source
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const indexState = await loadSceneIndexFromState(projectPath, fileSystem, env);
|
|
163
|
+
if (indexState && indexState.data && indexState.data.scenes && indexState.data.scenes[sceneId]) {
|
|
164
|
+
const record = indexState.data.scenes[sceneId];
|
|
165
|
+
return {
|
|
166
|
+
scene_id: sceneId,
|
|
167
|
+
spec_ids: Array.isArray(record.spec_ids) ? record.spec_ids : [],
|
|
168
|
+
source: indexState.source
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const governanceReport = await runStudioSpecGovernance({
|
|
173
|
+
apply: false,
|
|
174
|
+
scene: sceneId
|
|
175
|
+
}, {
|
|
176
|
+
projectPath,
|
|
177
|
+
fileSystem
|
|
178
|
+
});
|
|
179
|
+
if (governanceReport && Array.isArray(governanceReport.scenes)) {
|
|
180
|
+
const target = governanceReport.scenes.find((scene) => normalizeText(scene.scene_id) === sceneId);
|
|
181
|
+
if (target) {
|
|
182
|
+
return {
|
|
183
|
+
scene_id: sceneId,
|
|
184
|
+
spec_ids: Array.isArray(target.specs)
|
|
185
|
+
? target.specs.map((item) => normalizeText(item.spec_id)).filter(Boolean)
|
|
186
|
+
: [],
|
|
187
|
+
source: 'studio-spec-governance'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
scene_id: sceneId,
|
|
194
|
+
spec_ids: [],
|
|
195
|
+
source: 'unknown'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function summarizeTasks(tasks) {
|
|
200
|
+
const summary = {
|
|
201
|
+
total: 0,
|
|
202
|
+
completed: 0,
|
|
203
|
+
in_progress: 0,
|
|
204
|
+
queued: 0,
|
|
205
|
+
not_started: 0,
|
|
206
|
+
unknown: 0
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (!Array.isArray(tasks)) {
|
|
210
|
+
return summary;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
summary.total = tasks.length;
|
|
214
|
+
tasks.forEach((task) => {
|
|
215
|
+
const status = normalizeText(task && task.status);
|
|
216
|
+
if (status === 'completed') {
|
|
217
|
+
summary.completed += 1;
|
|
218
|
+
} else if (status === 'in-progress') {
|
|
219
|
+
summary.in_progress += 1;
|
|
220
|
+
} else if (status === 'queued') {
|
|
221
|
+
summary.queued += 1;
|
|
222
|
+
} else if (status === 'not-started') {
|
|
223
|
+
summary.not_started += 1;
|
|
224
|
+
} else {
|
|
225
|
+
summary.unknown += 1;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return summary;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildCandidateSummary(specs) {
|
|
233
|
+
const summary = {
|
|
234
|
+
spec_count: specs.length,
|
|
235
|
+
task_total: 0,
|
|
236
|
+
task_completed: 0,
|
|
237
|
+
task_pending: 0
|
|
238
|
+
};
|
|
239
|
+
specs.forEach((spec) => {
|
|
240
|
+
const taskSummary = spec.task_summary || {};
|
|
241
|
+
summary.task_total += Number(taskSummary.total || 0);
|
|
242
|
+
summary.task_completed += Number(taskSummary.completed || 0);
|
|
243
|
+
});
|
|
244
|
+
summary.task_pending = Math.max(0, summary.task_total - summary.task_completed);
|
|
245
|
+
return summary;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildScoreFromCandidate(candidate) {
|
|
249
|
+
const summary = candidate && candidate.summary ? candidate.summary : {};
|
|
250
|
+
const taskTotal = Number(summary.task_total || 0);
|
|
251
|
+
const taskCompleted = Number(summary.task_completed || 0);
|
|
252
|
+
const specCount = Number(summary.spec_count || 0);
|
|
253
|
+
const completionRate = taskTotal > 0 ? taskCompleted / taskTotal : 0;
|
|
254
|
+
const reuseScore = Math.min(100, Math.round((specCount / 3) * 100));
|
|
255
|
+
const stabilityScore = Math.round(completionRate * 100);
|
|
256
|
+
const riskScore = Math.min(100, Math.round((1 - completionRate) * 100));
|
|
257
|
+
const valueScore = Math.round((stabilityScore * 0.5) + (reuseScore * 0.3) + ((100 - riskScore) * 0.2));
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
completion_rate: Number(completionRate.toFixed(3)),
|
|
261
|
+
reuse_score: reuseScore,
|
|
262
|
+
stability_score: stabilityScore,
|
|
263
|
+
risk_score: riskScore,
|
|
264
|
+
value_score: valueScore
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildTemplateCandidate(candidate, mapping, options) {
|
|
269
|
+
const sceneId = buildSceneIdFromCandidate(candidate);
|
|
270
|
+
const templateId = normalizeText(options && options.template_id)
|
|
271
|
+
|| normalizeText(options && options.id)
|
|
272
|
+
|| sceneId.replace(/[^\w.-]+/g, '_');
|
|
273
|
+
const name = normalizeText(options && options.name)
|
|
274
|
+
|| `Capability template: ${sceneId}`;
|
|
275
|
+
const description = normalizeText(options && options.description)
|
|
276
|
+
|| `Capability template derived from ${sceneId}`;
|
|
277
|
+
const category = normalizeText(options && options.category) || 'capability';
|
|
278
|
+
const tags = normalizeStringArray(options && options.tags);
|
|
279
|
+
const ontologyScope = (mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
|
|
280
|
+
? mapping.ontology_scope
|
|
281
|
+
: {
|
|
282
|
+
domains: [],
|
|
283
|
+
entities: [],
|
|
284
|
+
relations: [],
|
|
285
|
+
business_rules: [],
|
|
286
|
+
decisions: []
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
mode: 'capability-template',
|
|
291
|
+
template_id: templateId,
|
|
292
|
+
name,
|
|
293
|
+
description,
|
|
294
|
+
category,
|
|
295
|
+
template_type: 'capability-template',
|
|
296
|
+
scene_id: sceneId,
|
|
297
|
+
source_candidate: candidate,
|
|
298
|
+
ontology_scope: ontologyScope,
|
|
299
|
+
tags,
|
|
300
|
+
created_at: new Date().toISOString()
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildRegistryEntry(templateCandidate, options) {
|
|
305
|
+
const riskLevel = normalizeText(options && options.risk_level) || 'medium';
|
|
306
|
+
const difficulty = normalizeText(options && options.difficulty) || 'intermediate';
|
|
307
|
+
const applicable = normalizeStringArray(options && options.applicable_scenarios);
|
|
308
|
+
const tags = normalizeStringArray(options && options.tags);
|
|
309
|
+
const sceneId = buildSceneIdFromCandidate(templateCandidate);
|
|
310
|
+
const safeTags = tags.length > 0 ? tags : ['capability', sceneId];
|
|
311
|
+
const safeApplicable = applicable.length > 0 ? applicable : [sceneId];
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
id: templateCandidate.template_id,
|
|
315
|
+
name: templateCandidate.name,
|
|
316
|
+
category: templateCandidate.category,
|
|
317
|
+
description: templateCandidate.description,
|
|
318
|
+
difficulty,
|
|
319
|
+
tags: safeTags,
|
|
320
|
+
applicable_scenarios: safeApplicable,
|
|
321
|
+
files: ['capability-template.json'],
|
|
322
|
+
template_type: 'capability-template',
|
|
323
|
+
min_sce_version: packageJson.version,
|
|
324
|
+
max_sce_version: null,
|
|
325
|
+
risk_level: riskLevel,
|
|
326
|
+
rollback_contract: {
|
|
327
|
+
supported: false,
|
|
328
|
+
strategy: 'n/a'
|
|
329
|
+
},
|
|
330
|
+
ontology_scope: templateCandidate.ontology_scope
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
|
|
335
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
336
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
337
|
+
const env = dependencies.env || process.env;
|
|
338
|
+
const sceneId = normalizeText(options.scene || options.sceneId || options.scene_id);
|
|
339
|
+
const writeOutput = normalizeBoolean(options.write, true);
|
|
340
|
+
|
|
341
|
+
if (!sceneId) {
|
|
342
|
+
throw new Error('scene is required for capability extract');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const specResolution = await resolveSceneSpecs(sceneId, {
|
|
346
|
+
specs: options.specs
|
|
347
|
+
}, { projectPath, fileSystem, env });
|
|
348
|
+
const specIds = Array.isArray(specResolution.spec_ids) ? specResolution.spec_ids : [];
|
|
349
|
+
|
|
350
|
+
const taskClaimer = new TaskClaimer();
|
|
351
|
+
const specs = [];
|
|
352
|
+
|
|
353
|
+
for (const specId of specIds) {
|
|
354
|
+
const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
|
|
355
|
+
let tasks = [];
|
|
356
|
+
let taskError = null;
|
|
357
|
+
if (await fileSystem.pathExists(tasksPath)) {
|
|
358
|
+
try {
|
|
359
|
+
tasks = await taskClaimer.parseTasks(tasksPath, { preferStatusMarkers: true });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
taskError = error.message;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
taskError = 'tasks.md missing';
|
|
365
|
+
}
|
|
366
|
+
const taskSummary = summarizeTasks(tasks);
|
|
367
|
+
specs.push({
|
|
368
|
+
spec_id: specId,
|
|
369
|
+
tasks_path: path.relative(projectPath, tasksPath),
|
|
370
|
+
task_summary: taskSummary,
|
|
371
|
+
task_sample: tasks.slice(0, toPositiveInteger(options.sample_limit, 5)).map((task) => ({
|
|
372
|
+
id: task.taskId,
|
|
373
|
+
title: task.title,
|
|
374
|
+
status: task.status
|
|
375
|
+
})),
|
|
376
|
+
task_error: taskError
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const payload = {
|
|
381
|
+
mode: 'capability-extract',
|
|
382
|
+
scene_id: sceneId,
|
|
383
|
+
generated_at: new Date().toISOString(),
|
|
384
|
+
source: {
|
|
385
|
+
scene_index_source: specResolution.source || 'unknown',
|
|
386
|
+
spec_count: specIds.length
|
|
387
|
+
},
|
|
388
|
+
specs,
|
|
389
|
+
summary: buildCandidateSummary(specs)
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const outputPath = normalizeText(options.out) || buildDefaultCandidatePath(sceneId);
|
|
393
|
+
if (writeOutput) {
|
|
394
|
+
await fileSystem.ensureDir(path.dirname(path.join(projectPath, outputPath)));
|
|
395
|
+
await fileSystem.writeJson(path.join(projectPath, outputPath), payload, { spaces: 2 });
|
|
396
|
+
payload.output_file = outputPath;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!normalizeBoolean(options.json, false)) {
|
|
400
|
+
console.log(chalk.green('✅ Capability candidate extracted'));
|
|
401
|
+
console.log(chalk.gray(` Scene: ${sceneId}`));
|
|
402
|
+
console.log(chalk.gray(` Specs: ${payload.summary.spec_count}`));
|
|
403
|
+
console.log(chalk.gray(` Tasks: ${payload.summary.task_total}`));
|
|
404
|
+
if (payload.output_file) {
|
|
405
|
+
console.log(chalk.gray(` Output: ${payload.output_file}`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return payload;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function runCapabilityScoreCommand(options = {}, dependencies = {}) {
|
|
413
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
414
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
415
|
+
const inputPath = normalizeText(options.input || options.file);
|
|
416
|
+
if (!inputPath) {
|
|
417
|
+
throw new Error('input candidate file is required for capability score');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const candidate = await fileSystem.readJson(path.join(projectPath, inputPath));
|
|
421
|
+
const sceneId = buildSceneIdFromCandidate(candidate);
|
|
422
|
+
const scores = buildScoreFromCandidate(candidate);
|
|
423
|
+
const payload = {
|
|
424
|
+
mode: 'capability-score',
|
|
425
|
+
scene_id: sceneId,
|
|
426
|
+
generated_at: new Date().toISOString(),
|
|
427
|
+
input: inputPath,
|
|
428
|
+
scores,
|
|
429
|
+
summary: candidate && candidate.summary ? candidate.summary : null
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const outputPath = normalizeText(options.out) || buildDefaultScorePath(sceneId);
|
|
433
|
+
if (normalizeBoolean(options.write, true)) {
|
|
434
|
+
await fileSystem.ensureDir(path.dirname(path.join(projectPath, outputPath)));
|
|
435
|
+
await fileSystem.writeJson(path.join(projectPath, outputPath), payload, { spaces: 2 });
|
|
436
|
+
payload.output_file = outputPath;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!normalizeBoolean(options.json, false)) {
|
|
440
|
+
console.log(chalk.green('✅ Capability score generated'));
|
|
441
|
+
console.log(chalk.gray(` Scene: ${sceneId}`));
|
|
442
|
+
console.log(chalk.gray(` Value score: ${scores.value_score}`));
|
|
443
|
+
if (payload.output_file) {
|
|
444
|
+
console.log(chalk.gray(` Output: ${payload.output_file}`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return payload;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function runCapabilityMapCommand(options = {}, dependencies = {}) {
|
|
452
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
453
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
454
|
+
const inputPath = normalizeText(options.input || options.file);
|
|
455
|
+
if (!inputPath) {
|
|
456
|
+
throw new Error('input candidate file is required for capability map');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const mappingPath = normalizeText(options.mapping);
|
|
460
|
+
const candidate = await fileSystem.readJson(path.join(projectPath, inputPath));
|
|
461
|
+
const mapping = mappingPath
|
|
462
|
+
? await fileSystem.readJson(path.join(projectPath, mappingPath))
|
|
463
|
+
: { ontology_scope: { domains: [], entities: [], relations: [], business_rules: [], decisions: [] } };
|
|
464
|
+
|
|
465
|
+
const templateCandidate = buildTemplateCandidate(candidate, mapping, options);
|
|
466
|
+
const sceneId = buildSceneIdFromCandidate(candidate);
|
|
467
|
+
const payload = {
|
|
468
|
+
mode: 'capability-map',
|
|
469
|
+
scene_id: sceneId,
|
|
470
|
+
generated_at: new Date().toISOString(),
|
|
471
|
+
input: inputPath,
|
|
472
|
+
mapping: mappingPath || null,
|
|
473
|
+
template: templateCandidate
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const outputPath = normalizeText(options.out) || buildDefaultTemplatePath(sceneId);
|
|
477
|
+
if (normalizeBoolean(options.write, true)) {
|
|
478
|
+
await fileSystem.ensureDir(path.dirname(path.join(projectPath, outputPath)));
|
|
479
|
+
await fileSystem.writeJson(path.join(projectPath, outputPath), payload, { spaces: 2 });
|
|
480
|
+
payload.output_file = outputPath;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!normalizeBoolean(options.json, false)) {
|
|
484
|
+
console.log(chalk.green('✅ Capability ontology mapping prepared'));
|
|
485
|
+
console.log(chalk.gray(` Scene: ${sceneId}`));
|
|
486
|
+
if (payload.output_file) {
|
|
487
|
+
console.log(chalk.gray(` Output: ${payload.output_file}`));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return payload;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
|
|
495
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
496
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
497
|
+
const inputPath = normalizeText(options.input || options.file);
|
|
498
|
+
if (!inputPath) {
|
|
499
|
+
throw new Error('input template file is required for capability register');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const payload = await fileSystem.readJson(path.join(projectPath, inputPath));
|
|
503
|
+
const templateCandidate = payload.template || payload;
|
|
504
|
+
if (!templateCandidate || !templateCandidate.template_id) {
|
|
505
|
+
throw new Error('template_id missing in capability template candidate');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const exportDir = normalizeText(options.out) || buildDefaultExportDir(templateCandidate.template_id);
|
|
509
|
+
const outputDirAbs = path.join(projectPath, exportDir);
|
|
510
|
+
await fileSystem.ensureDir(outputDirAbs);
|
|
511
|
+
|
|
512
|
+
const registryEntry = buildRegistryEntry(templateCandidate, options);
|
|
513
|
+
const registryPayload = {
|
|
514
|
+
version: '1.0',
|
|
515
|
+
templates: [registryEntry]
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
await fileSystem.writeJson(path.join(outputDirAbs, 'capability-template.json'), templateCandidate, { spaces: 2 });
|
|
519
|
+
await fileSystem.writeJson(path.join(outputDirAbs, 'template-registry.json'), registryPayload, { spaces: 2 });
|
|
520
|
+
|
|
521
|
+
const result = {
|
|
522
|
+
mode: 'capability-register',
|
|
523
|
+
template_id: templateCandidate.template_id,
|
|
524
|
+
output_dir: exportDir,
|
|
525
|
+
files: [
|
|
526
|
+
path.join(exportDir, 'capability-template.json'),
|
|
527
|
+
path.join(exportDir, 'template-registry.json')
|
|
528
|
+
]
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
if (!normalizeBoolean(options.json, false)) {
|
|
532
|
+
console.log(chalk.green('✅ Capability template package exported'));
|
|
533
|
+
console.log(chalk.gray(` Template: ${templateCandidate.template_id}`));
|
|
534
|
+
console.log(chalk.gray(` Output: ${exportDir}`));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function registerCapabilityCommands(program) {
|
|
541
|
+
const capabilityCmd = program
|
|
542
|
+
.command('capability')
|
|
543
|
+
.description('Extract and manage capability templates from scene/spec/task history');
|
|
544
|
+
|
|
545
|
+
capabilityCmd
|
|
546
|
+
.command('extract')
|
|
547
|
+
.description('Extract capability candidate from a scene')
|
|
548
|
+
.requiredOption('--scene <scene-id>', 'Scene identifier')
|
|
549
|
+
.option('--specs <spec-ids>', 'Comma-separated spec identifiers')
|
|
550
|
+
.option('--out <path>', 'Output JSON path')
|
|
551
|
+
.option('--sample-limit <n>', 'Max tasks per spec in sample', '5')
|
|
552
|
+
.option('--no-write', 'Skip writing output file')
|
|
553
|
+
.option('--json', 'Output JSON to stdout')
|
|
554
|
+
.action(async (options) => {
|
|
555
|
+
const specs = normalizeText(options.specs)
|
|
556
|
+
? normalizeText(options.specs).split(',').map((item) => normalizeText(item)).filter(Boolean)
|
|
557
|
+
: [];
|
|
558
|
+
await runCapabilityExtractCommand({
|
|
559
|
+
scene: options.scene,
|
|
560
|
+
specs,
|
|
561
|
+
out: options.out,
|
|
562
|
+
sample_limit: options.sampleLimit,
|
|
563
|
+
write: options.write,
|
|
564
|
+
json: options.json
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
capabilityCmd
|
|
569
|
+
.command('score')
|
|
570
|
+
.description('Score a capability candidate')
|
|
571
|
+
.requiredOption('--input <path>', 'Input candidate JSON')
|
|
572
|
+
.option('--out <path>', 'Output JSON path')
|
|
573
|
+
.option('--no-write', 'Skip writing output file')
|
|
574
|
+
.option('--json', 'Output JSON to stdout')
|
|
575
|
+
.action(async (options) => {
|
|
576
|
+
await runCapabilityScoreCommand(options);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
capabilityCmd
|
|
580
|
+
.command('map')
|
|
581
|
+
.description('Attach ontology mapping to a capability candidate')
|
|
582
|
+
.requiredOption('--input <path>', 'Input candidate JSON')
|
|
583
|
+
.option('--mapping <path>', 'Ontology mapping JSON')
|
|
584
|
+
.option('--template-id <id>', 'Template identifier')
|
|
585
|
+
.option('--name <name>', 'Template name')
|
|
586
|
+
.option('--description <desc>', 'Template description')
|
|
587
|
+
.option('--category <category>', 'Template category')
|
|
588
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
589
|
+
.option('--out <path>', 'Output JSON path')
|
|
590
|
+
.option('--no-write', 'Skip writing output file')
|
|
591
|
+
.option('--json', 'Output JSON to stdout')
|
|
592
|
+
.action(async (options) => {
|
|
593
|
+
const tags = normalizeText(options.tags)
|
|
594
|
+
? normalizeText(options.tags).split(',').map((item) => normalizeText(item)).filter(Boolean)
|
|
595
|
+
: [];
|
|
596
|
+
await runCapabilityMapCommand({
|
|
597
|
+
...options,
|
|
598
|
+
tags
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
capabilityCmd
|
|
603
|
+
.command('register')
|
|
604
|
+
.description('Export a registry-ready capability template package')
|
|
605
|
+
.requiredOption('--input <path>', 'Input template JSON (output of capability map)')
|
|
606
|
+
.option('--out <path>', 'Output directory')
|
|
607
|
+
.option('--difficulty <level>', 'Difficulty (beginner|intermediate|advanced)')
|
|
608
|
+
.option('--risk-level <level>', 'Risk level (low|medium|high|critical)')
|
|
609
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
610
|
+
.option('--applicable-scenarios <scenes>', 'Comma-separated applicable scenarios')
|
|
611
|
+
.option('--json', 'Output JSON to stdout')
|
|
612
|
+
.action(async (options) => {
|
|
613
|
+
const tags = normalizeText(options.tags)
|
|
614
|
+
? normalizeText(options.tags).split(',').map((item) => normalizeText(item)).filter(Boolean)
|
|
615
|
+
: [];
|
|
616
|
+
const applicable = normalizeText(options.applicableScenarios)
|
|
617
|
+
? normalizeText(options.applicableScenarios).split(',').map((item) => normalizeText(item)).filter(Boolean)
|
|
618
|
+
: [];
|
|
619
|
+
await runCapabilityRegisterCommand({
|
|
620
|
+
...options,
|
|
621
|
+
risk_level: options.riskLevel,
|
|
622
|
+
applicable_scenarios: applicable,
|
|
623
|
+
tags
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
module.exports = {
|
|
629
|
+
registerCapabilityCommands,
|
|
630
|
+
runCapabilityExtractCommand,
|
|
631
|
+
runCapabilityScoreCommand,
|
|
632
|
+
runCapabilityMapCommand,
|
|
633
|
+
runCapabilityRegisterCommand
|
|
634
|
+
};
|