scene-capability-engine 3.6.10 → 3.6.13
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 +29 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/command-reference.md +40 -0
- package/docs/magicball-capability-library.md +117 -0
- package/docs/magicball-task-quality-governance.md +301 -0
- package/lib/commands/capability.js +490 -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
package/lib/commands/task.js
CHANGED
|
@@ -12,6 +12,16 @@ const TaskClaimer = require('../task/task-claimer');
|
|
|
12
12
|
const WorkspaceManager = require('../workspace/workspace-manager');
|
|
13
13
|
const { TaskRefRegistry } = require('../task/task-ref-registry');
|
|
14
14
|
const { ensureWriteAuthorization } = require('../security/write-authorization');
|
|
15
|
+
const {
|
|
16
|
+
buildDraft,
|
|
17
|
+
scoreDraft,
|
|
18
|
+
appendDraft,
|
|
19
|
+
updateDraft,
|
|
20
|
+
consolidateDrafts,
|
|
21
|
+
loadDraftStore,
|
|
22
|
+
promoteDraftToTasks
|
|
23
|
+
} = require('../task/task-quality');
|
|
24
|
+
const { loadTaskQualityPolicy } = require('../task/task-quality-policy');
|
|
15
25
|
|
|
16
26
|
function normalizeString(value) {
|
|
17
27
|
if (typeof value !== 'string') {
|
|
@@ -32,6 +42,195 @@ function resolveStudioStageFromTaskKey(taskKey) {
|
|
|
32
42
|
return normalizeString(normalized.slice('studio:'.length));
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
async function runTaskDraftCommand(options = {}) {
|
|
46
|
+
const projectPath = process.cwd();
|
|
47
|
+
const sceneId = normalizeString(options.scene);
|
|
48
|
+
const specId = normalizeString(options.spec);
|
|
49
|
+
const inputText = normalizeString(options.input);
|
|
50
|
+
const inputFile = normalizeString(options.inputFile);
|
|
51
|
+
const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
|
|
52
|
+
const policy = policyConfig.policy;
|
|
53
|
+
|
|
54
|
+
if (!sceneId) {
|
|
55
|
+
throw new Error('scene is required');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let rawRequest = inputText;
|
|
59
|
+
if (!rawRequest && inputFile) {
|
|
60
|
+
rawRequest = await fs.readFile(inputFile, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
if (!rawRequest) {
|
|
63
|
+
throw new Error('input text is required (use --input or --input-file)');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const draft = buildDraft(rawRequest, {
|
|
67
|
+
scene_id: sceneId,
|
|
68
|
+
spec_id: specId,
|
|
69
|
+
acceptance_criteria: options.acceptance ? String(options.acceptance).split('|') : [],
|
|
70
|
+
confidence: options.confidence,
|
|
71
|
+
policy
|
|
72
|
+
});
|
|
73
|
+
const quality = scoreDraft(draft, policy);
|
|
74
|
+
draft.quality_score = quality.score;
|
|
75
|
+
draft.quality_breakdown = quality.breakdown;
|
|
76
|
+
draft.quality_issues = quality.issues;
|
|
77
|
+
draft.quality_passed = quality.passed;
|
|
78
|
+
|
|
79
|
+
const result = await appendDraft(projectPath, draft, fs);
|
|
80
|
+
const payload = {
|
|
81
|
+
mode: 'task-draft',
|
|
82
|
+
draft: draft,
|
|
83
|
+
store_path: result.store_path
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (options.json) {
|
|
87
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green('✅ Draft created'));
|
|
92
|
+
console.log(chalk.gray(` id: ${draft.draft_id}`));
|
|
93
|
+
console.log(chalk.gray(` score: ${draft.quality_score}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runTaskConsolidateCommand(options = {}) {
|
|
97
|
+
const projectPath = process.cwd();
|
|
98
|
+
const sceneId = normalizeString(options.scene);
|
|
99
|
+
const specId = normalizeString(options.spec);
|
|
100
|
+
if (!sceneId) {
|
|
101
|
+
throw new Error('scene is required');
|
|
102
|
+
}
|
|
103
|
+
const result = await consolidateDrafts(projectPath, {
|
|
104
|
+
scene_id: sceneId,
|
|
105
|
+
spec_id: specId
|
|
106
|
+
}, fs);
|
|
107
|
+
|
|
108
|
+
const payload = {
|
|
109
|
+
mode: 'task-consolidate',
|
|
110
|
+
scene_id: sceneId,
|
|
111
|
+
spec_id: specId || null,
|
|
112
|
+
merged: result.merged,
|
|
113
|
+
drafts: result.drafts,
|
|
114
|
+
store_path: result.store_path
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (options.json) {
|
|
118
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(chalk.green('✅ Drafts consolidated'));
|
|
123
|
+
console.log(chalk.gray(` merged: ${result.merged.length}`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runTaskScoreCommand(options = {}) {
|
|
127
|
+
const projectPath = process.cwd();
|
|
128
|
+
const draftId = normalizeString(options.draft);
|
|
129
|
+
const all = Boolean(options.all);
|
|
130
|
+
const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
|
|
131
|
+
const policy = policyConfig.policy;
|
|
132
|
+
const store = await loadDraftStore(projectPath, fs);
|
|
133
|
+
const drafts = Array.isArray(store.payload.drafts) ? store.payload.drafts : [];
|
|
134
|
+
|
|
135
|
+
const targets = all
|
|
136
|
+
? drafts
|
|
137
|
+
: drafts.filter((draft) => draft.draft_id === draftId);
|
|
138
|
+
|
|
139
|
+
if (!all && targets.length === 0) {
|
|
140
|
+
throw new Error(`draft not found: ${draftId}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const scored = [];
|
|
144
|
+
for (const draft of targets) {
|
|
145
|
+
const quality = scoreDraft(draft, policy);
|
|
146
|
+
const updated = await updateDraft(projectPath, draft.draft_id, (current) => ({
|
|
147
|
+
...current,
|
|
148
|
+
quality_score: quality.score,
|
|
149
|
+
quality_breakdown: quality.breakdown,
|
|
150
|
+
quality_issues: quality.issues,
|
|
151
|
+
quality_passed: quality.passed
|
|
152
|
+
}), fs);
|
|
153
|
+
if (updated) {
|
|
154
|
+
scored.push(updated);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const payload = {
|
|
159
|
+
mode: 'task-score',
|
|
160
|
+
draft_id: draftId || null,
|
|
161
|
+
total: scored.length,
|
|
162
|
+
drafts: scored
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (options.json) {
|
|
166
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(chalk.green('✅ Draft scoring complete'));
|
|
171
|
+
console.log(chalk.gray(` scored: ${scored.length}`));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runTaskPromoteCommand(options = {}) {
|
|
175
|
+
const projectPath = process.cwd();
|
|
176
|
+
const draftId = normalizeString(options.draft);
|
|
177
|
+
const specId = normalizeString(options.spec);
|
|
178
|
+
const policyConfig = await loadTaskQualityPolicy(projectPath, options.policy, fs);
|
|
179
|
+
const policy = policyConfig.policy;
|
|
180
|
+
if (!draftId) {
|
|
181
|
+
throw new Error('draft id is required');
|
|
182
|
+
}
|
|
183
|
+
if (!specId) {
|
|
184
|
+
throw new Error('spec is required to promote draft');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const store = await loadDraftStore(projectPath, fs);
|
|
188
|
+
const drafts = Array.isArray(store.payload.drafts) ? store.payload.drafts : [];
|
|
189
|
+
const draft = drafts.find((item) => item.draft_id === draftId);
|
|
190
|
+
if (!draft) {
|
|
191
|
+
throw new Error(`draft not found: ${draftId}`);
|
|
192
|
+
}
|
|
193
|
+
const quality = scoreDraft(draft, policy);
|
|
194
|
+
if (!quality.passed && !options.force) {
|
|
195
|
+
throw new Error('draft quality gate failed; use --force to override');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const promoted = await promoteDraftToTasks(projectPath, {
|
|
199
|
+
...draft,
|
|
200
|
+
spec_id: specId
|
|
201
|
+
}, fs);
|
|
202
|
+
|
|
203
|
+
const updated = await updateDraft(projectPath, draftId, (current) => ({
|
|
204
|
+
...current,
|
|
205
|
+
spec_id: specId,
|
|
206
|
+
status: 'promoted',
|
|
207
|
+
quality_score: quality.score,
|
|
208
|
+
quality_breakdown: quality.breakdown,
|
|
209
|
+
quality_issues: quality.issues,
|
|
210
|
+
quality_passed: quality.passed,
|
|
211
|
+
promoted_at: new Date().toISOString(),
|
|
212
|
+
promoted_task_id: promoted.task_id,
|
|
213
|
+
promoted_tasks_path: promoted.tasks_path
|
|
214
|
+
}), fs);
|
|
215
|
+
|
|
216
|
+
const payload = {
|
|
217
|
+
mode: 'task-promote',
|
|
218
|
+
draft_id: draftId,
|
|
219
|
+
spec_id: specId,
|
|
220
|
+
task_id: promoted.task_id,
|
|
221
|
+
tasks_path: toRelativePosix(projectPath, promoted.tasks_path),
|
|
222
|
+
draft: updated
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (options.json) {
|
|
226
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(chalk.green('✅ Draft promoted to tasks.md'));
|
|
231
|
+
console.log(chalk.gray(` task: ${promoted.task_id}`));
|
|
232
|
+
}
|
|
233
|
+
|
|
35
234
|
function isStudioTaskRef(lookup = {}) {
|
|
36
235
|
const source = normalizeString(lookup.source);
|
|
37
236
|
if (source === 'studio-stage') {
|
|
@@ -771,6 +970,74 @@ function registerTaskCommands(program) {
|
|
|
771
970
|
process.exitCode = 1;
|
|
772
971
|
}
|
|
773
972
|
});
|
|
973
|
+
|
|
974
|
+
task
|
|
975
|
+
.command('draft')
|
|
976
|
+
.description('Create a task draft from dialogue input')
|
|
977
|
+
.requiredOption('--scene <scene-id>', 'Scene identifier')
|
|
978
|
+
.option('--spec <spec-id>', 'Spec identifier')
|
|
979
|
+
.option('--input <text>', 'Raw task input text')
|
|
980
|
+
.option('--input-file <path>', 'File containing raw task input')
|
|
981
|
+
.option('--acceptance <items>', 'Pipe-delimited acceptance criteria')
|
|
982
|
+
.option('--confidence <score>', 'Manual confidence (0-1)')
|
|
983
|
+
.option('--policy <path>', 'Task quality policy path override')
|
|
984
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
985
|
+
.action(async (options) => {
|
|
986
|
+
try {
|
|
987
|
+
await runTaskDraftCommand(options);
|
|
988
|
+
} catch (error) {
|
|
989
|
+
console.error(chalk.red(`Task draft failed: ${error.message}`));
|
|
990
|
+
process.exitCode = 1;
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
task
|
|
995
|
+
.command('consolidate')
|
|
996
|
+
.description('Consolidate task drafts by scene/spec')
|
|
997
|
+
.requiredOption('--scene <scene-id>', 'Scene identifier')
|
|
998
|
+
.option('--spec <spec-id>', 'Spec identifier')
|
|
999
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1000
|
+
.action(async (options) => {
|
|
1001
|
+
try {
|
|
1002
|
+
await runTaskConsolidateCommand(options);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
console.error(chalk.red(`Task consolidate failed: ${error.message}`));
|
|
1005
|
+
process.exitCode = 1;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
task
|
|
1010
|
+
.command('score')
|
|
1011
|
+
.description('Score task draft quality')
|
|
1012
|
+
.option('--draft <draft-id>', 'Draft identifier')
|
|
1013
|
+
.option('--all', 'Score all drafts')
|
|
1014
|
+
.option('--policy <path>', 'Task quality policy path override')
|
|
1015
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1016
|
+
.action(async (options) => {
|
|
1017
|
+
try {
|
|
1018
|
+
await runTaskScoreCommand(options);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
console.error(chalk.red(`Task score failed: ${error.message}`));
|
|
1021
|
+
process.exitCode = 1;
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
task
|
|
1026
|
+
.command('promote')
|
|
1027
|
+
.description('Promote a task draft into spec tasks.md')
|
|
1028
|
+
.requiredOption('--draft <draft-id>', 'Draft identifier')
|
|
1029
|
+
.requiredOption('--spec <spec-id>', 'Spec identifier')
|
|
1030
|
+
.option('--force', 'Override quality gate')
|
|
1031
|
+
.option('--policy <path>', 'Task quality policy path override')
|
|
1032
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
1033
|
+
.action(async (options) => {
|
|
1034
|
+
try {
|
|
1035
|
+
await runTaskPromoteCommand(options);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
console.error(chalk.red(`Task promote failed: ${error.message}`));
|
|
1038
|
+
process.exitCode = 1;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
774
1041
|
}
|
|
775
1042
|
|
|
776
1043
|
module.exports = {
|
|
@@ -780,5 +1047,9 @@ module.exports = {
|
|
|
780
1047
|
runTaskRefCommand,
|
|
781
1048
|
runTaskShowCommand,
|
|
782
1049
|
runTaskRerunCommand,
|
|
1050
|
+
runTaskDraftCommand,
|
|
1051
|
+
runTaskConsolidateCommand,
|
|
1052
|
+
runTaskScoreCommand,
|
|
1053
|
+
runTaskPromoteCommand,
|
|
783
1054
|
registerTaskCommands
|
|
784
1055
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_POLICY_PATH = path.join('.sce', 'config', 'task-quality-policy.json');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TASK_QUALITY_POLICY = Object.freeze({
|
|
7
|
+
schema_version: '1.0',
|
|
8
|
+
min_quality_score: 70,
|
|
9
|
+
require_acceptance_criteria: true,
|
|
10
|
+
allow_needs_split: false,
|
|
11
|
+
auto_suggest_acceptance: true,
|
|
12
|
+
max_sub_goals: 3
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function normalizeText(value) {
|
|
16
|
+
if (typeof value !== 'string') {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
return value.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeBoolean(value, fallback) {
|
|
23
|
+
if (typeof value === 'boolean') {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toPositiveInteger(value, fallback) {
|
|
40
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
41
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizePolicy(payload = {}) {
|
|
48
|
+
const policy = payload && typeof payload === 'object' ? payload : {};
|
|
49
|
+
return {
|
|
50
|
+
schema_version: normalizeText(policy.schema_version) || DEFAULT_TASK_QUALITY_POLICY.schema_version,
|
|
51
|
+
min_quality_score: toPositiveInteger(
|
|
52
|
+
policy.min_quality_score,
|
|
53
|
+
DEFAULT_TASK_QUALITY_POLICY.min_quality_score
|
|
54
|
+
),
|
|
55
|
+
require_acceptance_criteria: normalizeBoolean(
|
|
56
|
+
policy.require_acceptance_criteria,
|
|
57
|
+
DEFAULT_TASK_QUALITY_POLICY.require_acceptance_criteria
|
|
58
|
+
),
|
|
59
|
+
allow_needs_split: normalizeBoolean(
|
|
60
|
+
policy.allow_needs_split,
|
|
61
|
+
DEFAULT_TASK_QUALITY_POLICY.allow_needs_split
|
|
62
|
+
),
|
|
63
|
+
auto_suggest_acceptance: normalizeBoolean(
|
|
64
|
+
policy.auto_suggest_acceptance,
|
|
65
|
+
DEFAULT_TASK_QUALITY_POLICY.auto_suggest_acceptance
|
|
66
|
+
),
|
|
67
|
+
max_sub_goals: toPositiveInteger(
|
|
68
|
+
policy.max_sub_goals,
|
|
69
|
+
DEFAULT_TASK_QUALITY_POLICY.max_sub_goals
|
|
70
|
+
)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadTaskQualityPolicy(projectPath, policyPath, fileSystem = fs) {
|
|
75
|
+
const resolvedPath = normalizeText(policyPath) || DEFAULT_POLICY_PATH;
|
|
76
|
+
const absolutePath = path.isAbsolute(resolvedPath)
|
|
77
|
+
? resolvedPath
|
|
78
|
+
: path.join(projectPath, resolvedPath);
|
|
79
|
+
|
|
80
|
+
if (!await fileSystem.pathExists(absolutePath)) {
|
|
81
|
+
return {
|
|
82
|
+
policy: normalizePolicy(DEFAULT_TASK_QUALITY_POLICY),
|
|
83
|
+
path: resolvedPath,
|
|
84
|
+
loaded_from: 'default'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const payload = await fileSystem.readJson(absolutePath);
|
|
90
|
+
return {
|
|
91
|
+
policy: normalizePolicy(payload),
|
|
92
|
+
path: resolvedPath,
|
|
93
|
+
loaded_from: 'file'
|
|
94
|
+
};
|
|
95
|
+
} catch (_error) {
|
|
96
|
+
return {
|
|
97
|
+
policy: normalizePolicy(DEFAULT_TASK_QUALITY_POLICY),
|
|
98
|
+
path: resolvedPath,
|
|
99
|
+
loaded_from: 'default'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
DEFAULT_POLICY_PATH,
|
|
106
|
+
DEFAULT_TASK_QUALITY_POLICY,
|
|
107
|
+
normalizePolicy,
|
|
108
|
+
loadTaskQualityPolicy
|
|
109
|
+
};
|