scene-capability-engine 3.4.6 → 3.5.1
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 +35 -0
- package/README.md +8 -2
- package/README.zh.md +8 -2
- package/docs/command-reference.md +53 -1
- package/lib/adoption/adoption-strategy.js +1 -0
- package/lib/adoption/detection-engine.js +1 -0
- package/lib/adoption/file-classifier.js +2 -1
- package/lib/adoption/smart-orchestrator.js +1 -0
- package/lib/commands/studio.js +334 -18
- package/lib/spec/related-specs.js +10 -2
- package/lib/spec/scene-binding-overrides.js +115 -0
- package/lib/studio/spec-intake-governor.js +1315 -0
- package/lib/workspace/takeover-baseline.js +81 -0
- package/package.json +1 -1
- package/template/.sce/config/studio-intake-policy.json +154 -0
- package/template/.sce/config/takeover-baseline.json +88 -1
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { DraftGenerator } = require('../spec/bootstrap/draft-generator');
|
|
4
|
+
const { ensureSpecDomainArtifacts } = require('../spec/domain-modeling');
|
|
5
|
+
const {
|
|
6
|
+
DEFAULT_SPEC_SCENE_OVERRIDE_PATH,
|
|
7
|
+
loadSceneBindingOverrides,
|
|
8
|
+
normalizeSceneBindingOverrides,
|
|
9
|
+
resolveSceneIdFromOverrides
|
|
10
|
+
} = require('../spec/scene-binding-overrides');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_STUDIO_INTAKE_POLICY_PATH = '.sce/config/studio-intake-policy.json';
|
|
13
|
+
const DEFAULT_STUDIO_GOVERNANCE_DIR = '.sce/spec-governance';
|
|
14
|
+
const DEFAULT_STUDIO_PORTFOLIO_REPORT = `${DEFAULT_STUDIO_GOVERNANCE_DIR}/scene-portfolio.latest.json`;
|
|
15
|
+
const DEFAULT_STUDIO_SCENE_INDEX = `${DEFAULT_STUDIO_GOVERNANCE_DIR}/scene-index.json`;
|
|
16
|
+
const DEFAULT_STUDIO_SCENE_OVERRIDE_PATH = DEFAULT_SPEC_SCENE_OVERRIDE_PATH;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STUDIO_SCENE_BACKFILL_RULES = Object.freeze([
|
|
19
|
+
{
|
|
20
|
+
id: 'moqui-core',
|
|
21
|
+
scene_id: 'scene.moqui-core',
|
|
22
|
+
keywords: ['moqui']
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'orchestration',
|
|
26
|
+
scene_id: 'scene.sce-orchestration',
|
|
27
|
+
keywords: ['orchestrate', 'runtime', 'controller', 'batch', 'parallel']
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'template-registry',
|
|
31
|
+
scene_id: 'scene.sce-template-registry',
|
|
32
|
+
keywords: ['template', 'scene-package', 'registry', 'catalog', 'scene-template']
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'spec-governance',
|
|
36
|
+
scene_id: 'scene.sce-spec-governance',
|
|
37
|
+
keywords: ['spec', 'gate', 'ontology', 'governance', 'policy']
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'quality',
|
|
41
|
+
scene_id: 'scene.sce-quality',
|
|
42
|
+
keywords: ['test', 'quality', 'stability', 'jest', 'coverage']
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'docs',
|
|
46
|
+
scene_id: 'scene.sce-docs',
|
|
47
|
+
keywords: ['document', 'documentation', 'onboarding', 'guide']
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'platform',
|
|
51
|
+
scene_id: 'scene.sce-platform',
|
|
52
|
+
keywords: ['adopt', 'upgrade', 'workspace', 'repo', 'environment', 'devops', 'release', 'github', 'npm']
|
|
53
|
+
}
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const DEFAULT_STUDIO_INTAKE_POLICY = Object.freeze({
|
|
57
|
+
schema_version: '1.0',
|
|
58
|
+
enabled: true,
|
|
59
|
+
auto_create_spec: true,
|
|
60
|
+
force_spec_for_studio_plan: true,
|
|
61
|
+
allow_manual_spec_override: false,
|
|
62
|
+
prefer_existing_scene_spec: true,
|
|
63
|
+
related_spec_min_score: 45,
|
|
64
|
+
allow_new_spec_when_goal_diverges: true,
|
|
65
|
+
divergence_similarity_threshold: 0.2,
|
|
66
|
+
goal_missing_strategy: 'create_for_tracking',
|
|
67
|
+
question_only_patterns: [
|
|
68
|
+
'how', 'what', 'why', 'when', 'where', 'which', 'can', 'could', 'should', 'would',
|
|
69
|
+
'是否', '怎么', '如何', '为什么', '吗', '么'
|
|
70
|
+
],
|
|
71
|
+
change_intent_patterns: [
|
|
72
|
+
'implement', 'build', 'create', 'add', 'update', 'upgrade', 'refactor', 'fix', 'stabilize',
|
|
73
|
+
'optimize', 'deliver', 'release', 'bootstrap', 'repair', 'patch',
|
|
74
|
+
'新增', '增加', '实现', '构建', '开发', '修复', '优化', '重构', '发布', '改造', '完善', '增强'
|
|
75
|
+
],
|
|
76
|
+
spec_id: {
|
|
77
|
+
prefix: 'auto',
|
|
78
|
+
max_goal_slug_tokens: 6
|
|
79
|
+
},
|
|
80
|
+
governance: {
|
|
81
|
+
auto_run_on_plan: true,
|
|
82
|
+
require_auto_on_plan: true,
|
|
83
|
+
max_active_specs_per_scene: 3,
|
|
84
|
+
stale_days: 14,
|
|
85
|
+
duplicate_similarity_threshold: 0.66
|
|
86
|
+
},
|
|
87
|
+
backfill: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
active_only_default: true,
|
|
90
|
+
default_scene_id: 'scene.sce-core',
|
|
91
|
+
override_file: DEFAULT_STUDIO_SCENE_OVERRIDE_PATH,
|
|
92
|
+
rules: DEFAULT_STUDIO_SCENE_BACKFILL_RULES
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function normalizeText(value) {
|
|
97
|
+
if (typeof value !== 'string') {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
return value.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeNumber(value, fallback = 0) {
|
|
104
|
+
const parsed = Number(value);
|
|
105
|
+
if (!Number.isFinite(parsed)) {
|
|
106
|
+
return fallback;
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeInteger(value, fallback = 0, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
112
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
113
|
+
if (!Number.isFinite(parsed)) {
|
|
114
|
+
return fallback;
|
|
115
|
+
}
|
|
116
|
+
return Math.max(min, Math.min(max, parsed));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeBoolean(value, fallback = false) {
|
|
120
|
+
if (typeof value === 'boolean') {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === 'string') {
|
|
124
|
+
const lowered = value.trim().toLowerCase();
|
|
125
|
+
if (['1', 'true', 'yes', 'on'].includes(lowered)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (['0', 'false', 'no', 'off'].includes(lowered)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return fallback;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeTextList(value = []) {
|
|
136
|
+
if (!Array.isArray(value)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return value
|
|
140
|
+
.map((item) => normalizeText(`${item}`))
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeBackfillRules(value = []) {
|
|
145
|
+
if (!Array.isArray(value)) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const rules = [];
|
|
149
|
+
for (const item of value) {
|
|
150
|
+
if (!item || typeof item !== 'object') {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const id = normalizeText(item.id);
|
|
154
|
+
const sceneId = normalizeText(item.scene_id || item.sceneId);
|
|
155
|
+
const keywords = normalizeTextList(item.keywords || item.match_any_keywords || item.matchAnyKeywords);
|
|
156
|
+
if (!id || !sceneId || keywords.length === 0) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
rules.push({
|
|
160
|
+
id,
|
|
161
|
+
scene_id: sceneId,
|
|
162
|
+
keywords
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return rules;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toRelativePosix(projectPath, absolutePath) {
|
|
169
|
+
return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function tokenizeText(value) {
|
|
173
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
174
|
+
if (!normalized) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return Array.from(new Set(
|
|
178
|
+
normalized
|
|
179
|
+
.split(/[^a-z0-9\u4e00-\u9fff]+/i)
|
|
180
|
+
.map((item) => item.trim())
|
|
181
|
+
.filter((item) => item.length >= 2 || /[\u4e00-\u9fff]/.test(item))
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function computeJaccard(leftTokens = [], rightTokens = []) {
|
|
186
|
+
const left = new Set(leftTokens);
|
|
187
|
+
const right = new Set(rightTokens);
|
|
188
|
+
if (left.size === 0 && right.size === 0) {
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
if (left.size === 0 || right.size === 0) {
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
let intersection = 0;
|
|
195
|
+
for (const token of left) {
|
|
196
|
+
if (right.has(token)) {
|
|
197
|
+
intersection += 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const union = left.size + right.size - intersection;
|
|
201
|
+
if (union <= 0) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
return Number((intersection / union).toFixed(3));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function slugifyText(value, fallback = 'spec') {
|
|
208
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
209
|
+
if (!normalized) {
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
const slug = normalized
|
|
213
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
|
214
|
+
.replace(/-+/g, '-')
|
|
215
|
+
.replace(/^-|-$/g, '');
|
|
216
|
+
return slug || fallback;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildGoalSlug(goal, maxTokens = 6) {
|
|
220
|
+
const tokens = tokenizeText(goal).slice(0, Math.max(1, maxTokens));
|
|
221
|
+
if (tokens.length === 0) {
|
|
222
|
+
return 'work';
|
|
223
|
+
}
|
|
224
|
+
return slugifyText(tokens.join('-'), 'work');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeSceneSlug(sceneId) {
|
|
228
|
+
const normalized = normalizeText(sceneId).replace(/^scene[._-]?/i, '');
|
|
229
|
+
return slugifyText(normalized, 'scene');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseTasksProgress(tasksContent) {
|
|
233
|
+
const content = typeof tasksContent === 'string' ? tasksContent : '';
|
|
234
|
+
const taskLines = content.match(/^- \[[ xX]\] .+$/gm) || [];
|
|
235
|
+
const doneLines = content.match(/^- \[[xX]\] .+$/gm) || [];
|
|
236
|
+
const total = taskLines.length;
|
|
237
|
+
const done = doneLines.length;
|
|
238
|
+
const ratio = total > 0 ? Number((done / total).toFixed(3)) : 0;
|
|
239
|
+
return {
|
|
240
|
+
total,
|
|
241
|
+
done,
|
|
242
|
+
ratio
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeStudioIntakePolicy(raw = {}) {
|
|
247
|
+
const payload = raw && typeof raw === 'object' ? raw : {};
|
|
248
|
+
const specId = payload.spec_id && typeof payload.spec_id === 'object' ? payload.spec_id : {};
|
|
249
|
+
const governance = payload.governance && typeof payload.governance === 'object' ? payload.governance : {};
|
|
250
|
+
const backfill = payload.backfill && typeof payload.backfill === 'object' ? payload.backfill : {};
|
|
251
|
+
const normalizedBackfillRules = normalizeBackfillRules(backfill.rules);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
schema_version: normalizeText(payload.schema_version) || DEFAULT_STUDIO_INTAKE_POLICY.schema_version,
|
|
255
|
+
enabled: normalizeBoolean(payload.enabled, DEFAULT_STUDIO_INTAKE_POLICY.enabled),
|
|
256
|
+
auto_create_spec: normalizeBoolean(payload.auto_create_spec, DEFAULT_STUDIO_INTAKE_POLICY.auto_create_spec),
|
|
257
|
+
force_spec_for_studio_plan: normalizeBoolean(
|
|
258
|
+
payload.force_spec_for_studio_plan,
|
|
259
|
+
DEFAULT_STUDIO_INTAKE_POLICY.force_spec_for_studio_plan
|
|
260
|
+
),
|
|
261
|
+
allow_manual_spec_override: normalizeBoolean(
|
|
262
|
+
payload.allow_manual_spec_override,
|
|
263
|
+
DEFAULT_STUDIO_INTAKE_POLICY.allow_manual_spec_override
|
|
264
|
+
),
|
|
265
|
+
prefer_existing_scene_spec: normalizeBoolean(
|
|
266
|
+
payload.prefer_existing_scene_spec,
|
|
267
|
+
DEFAULT_STUDIO_INTAKE_POLICY.prefer_existing_scene_spec
|
|
268
|
+
),
|
|
269
|
+
related_spec_min_score: normalizeInteger(
|
|
270
|
+
payload.related_spec_min_score,
|
|
271
|
+
DEFAULT_STUDIO_INTAKE_POLICY.related_spec_min_score,
|
|
272
|
+
0,
|
|
273
|
+
1000
|
|
274
|
+
),
|
|
275
|
+
allow_new_spec_when_goal_diverges: normalizeBoolean(
|
|
276
|
+
payload.allow_new_spec_when_goal_diverges,
|
|
277
|
+
DEFAULT_STUDIO_INTAKE_POLICY.allow_new_spec_when_goal_diverges
|
|
278
|
+
),
|
|
279
|
+
divergence_similarity_threshold: Math.max(
|
|
280
|
+
0,
|
|
281
|
+
Math.min(1, normalizeNumber(
|
|
282
|
+
payload.divergence_similarity_threshold,
|
|
283
|
+
DEFAULT_STUDIO_INTAKE_POLICY.divergence_similarity_threshold
|
|
284
|
+
))
|
|
285
|
+
),
|
|
286
|
+
goal_missing_strategy: ['create_for_tracking', 'bind_existing', 'skip'].includes(normalizeText(payload.goal_missing_strategy))
|
|
287
|
+
? normalizeText(payload.goal_missing_strategy)
|
|
288
|
+
: DEFAULT_STUDIO_INTAKE_POLICY.goal_missing_strategy,
|
|
289
|
+
question_only_patterns: (() => {
|
|
290
|
+
const values = normalizeTextList(payload.question_only_patterns);
|
|
291
|
+
return values.length > 0 ? values : [...DEFAULT_STUDIO_INTAKE_POLICY.question_only_patterns];
|
|
292
|
+
})(),
|
|
293
|
+
change_intent_patterns: (() => {
|
|
294
|
+
const values = normalizeTextList(payload.change_intent_patterns);
|
|
295
|
+
return values.length > 0 ? values : [...DEFAULT_STUDIO_INTAKE_POLICY.change_intent_patterns];
|
|
296
|
+
})(),
|
|
297
|
+
spec_id: {
|
|
298
|
+
prefix: normalizeText(specId.prefix) || DEFAULT_STUDIO_INTAKE_POLICY.spec_id.prefix,
|
|
299
|
+
max_goal_slug_tokens: normalizeInteger(
|
|
300
|
+
specId.max_goal_slug_tokens,
|
|
301
|
+
DEFAULT_STUDIO_INTAKE_POLICY.spec_id.max_goal_slug_tokens,
|
|
302
|
+
1,
|
|
303
|
+
12
|
|
304
|
+
)
|
|
305
|
+
},
|
|
306
|
+
governance: {
|
|
307
|
+
auto_run_on_plan: normalizeBoolean(
|
|
308
|
+
governance.auto_run_on_plan,
|
|
309
|
+
DEFAULT_STUDIO_INTAKE_POLICY.governance.auto_run_on_plan
|
|
310
|
+
),
|
|
311
|
+
require_auto_on_plan: normalizeBoolean(
|
|
312
|
+
governance.require_auto_on_plan,
|
|
313
|
+
DEFAULT_STUDIO_INTAKE_POLICY.governance.require_auto_on_plan
|
|
314
|
+
),
|
|
315
|
+
max_active_specs_per_scene: normalizeInteger(
|
|
316
|
+
governance.max_active_specs_per_scene,
|
|
317
|
+
DEFAULT_STUDIO_INTAKE_POLICY.governance.max_active_specs_per_scene,
|
|
318
|
+
1,
|
|
319
|
+
200
|
|
320
|
+
),
|
|
321
|
+
stale_days: normalizeInteger(
|
|
322
|
+
governance.stale_days,
|
|
323
|
+
DEFAULT_STUDIO_INTAKE_POLICY.governance.stale_days,
|
|
324
|
+
1,
|
|
325
|
+
3650
|
|
326
|
+
),
|
|
327
|
+
duplicate_similarity_threshold: Math.max(
|
|
328
|
+
0,
|
|
329
|
+
Math.min(1, normalizeNumber(
|
|
330
|
+
governance.duplicate_similarity_threshold,
|
|
331
|
+
DEFAULT_STUDIO_INTAKE_POLICY.governance.duplicate_similarity_threshold
|
|
332
|
+
))
|
|
333
|
+
)
|
|
334
|
+
},
|
|
335
|
+
backfill: {
|
|
336
|
+
enabled: normalizeBoolean(
|
|
337
|
+
backfill.enabled,
|
|
338
|
+
DEFAULT_STUDIO_INTAKE_POLICY.backfill.enabled
|
|
339
|
+
),
|
|
340
|
+
active_only_default: normalizeBoolean(
|
|
341
|
+
backfill.active_only_default,
|
|
342
|
+
DEFAULT_STUDIO_INTAKE_POLICY.backfill.active_only_default
|
|
343
|
+
),
|
|
344
|
+
default_scene_id: normalizeText(backfill.default_scene_id)
|
|
345
|
+
|| DEFAULT_STUDIO_INTAKE_POLICY.backfill.default_scene_id,
|
|
346
|
+
override_file: normalizeText(backfill.override_file)
|
|
347
|
+
|| DEFAULT_STUDIO_INTAKE_POLICY.backfill.override_file,
|
|
348
|
+
rules: normalizedBackfillRules.length > 0
|
|
349
|
+
? normalizedBackfillRules
|
|
350
|
+
: normalizeBackfillRules(DEFAULT_STUDIO_INTAKE_POLICY.backfill.rules)
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function loadStudioIntakePolicy(projectPath = process.cwd(), fileSystem = fs) {
|
|
356
|
+
const policyPath = path.join(projectPath, DEFAULT_STUDIO_INTAKE_POLICY_PATH);
|
|
357
|
+
let policyPayload = {};
|
|
358
|
+
let loadedFrom = 'default';
|
|
359
|
+
if (await fileSystem.pathExists(policyPath)) {
|
|
360
|
+
try {
|
|
361
|
+
policyPayload = await fileSystem.readJson(policyPath);
|
|
362
|
+
loadedFrom = 'file';
|
|
363
|
+
} catch (_error) {
|
|
364
|
+
policyPayload = {};
|
|
365
|
+
loadedFrom = 'default';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const policy = normalizeStudioIntakePolicy(policyPayload);
|
|
369
|
+
return {
|
|
370
|
+
policy,
|
|
371
|
+
policy_path: DEFAULT_STUDIO_INTAKE_POLICY_PATH,
|
|
372
|
+
loaded_from: loadedFrom
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function classifyStudioGoalIntent(goal = '', policy = DEFAULT_STUDIO_INTAKE_POLICY) {
|
|
377
|
+
const normalizedGoal = normalizeText(goal);
|
|
378
|
+
const loweredGoal = normalizedGoal.toLowerCase();
|
|
379
|
+
const changePatterns = Array.isArray(policy.change_intent_patterns) ? policy.change_intent_patterns : [];
|
|
380
|
+
const questionPatterns = Array.isArray(policy.question_only_patterns) ? policy.question_only_patterns : [];
|
|
381
|
+
|
|
382
|
+
let changeHits = 0;
|
|
383
|
+
for (const pattern of changePatterns) {
|
|
384
|
+
const keyword = normalizeText(pattern).toLowerCase();
|
|
385
|
+
if (keyword && loweredGoal.includes(keyword)) {
|
|
386
|
+
changeHits += 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let questionHits = 0;
|
|
391
|
+
for (const pattern of questionPatterns) {
|
|
392
|
+
const keyword = normalizeText(pattern).toLowerCase();
|
|
393
|
+
if (keyword && loweredGoal.includes(keyword)) {
|
|
394
|
+
questionHits += 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (/[??]\s*$/.test(normalizedGoal)) {
|
|
399
|
+
questionHits += 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!normalizedGoal) {
|
|
403
|
+
return {
|
|
404
|
+
intent_type: 'unknown',
|
|
405
|
+
requires_spec: false,
|
|
406
|
+
confidence: 'low',
|
|
407
|
+
signals: {
|
|
408
|
+
change_hits: 0,
|
|
409
|
+
question_hits: 0,
|
|
410
|
+
goal_missing: true
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (changeHits > 0 && changeHits >= questionHits) {
|
|
416
|
+
return {
|
|
417
|
+
intent_type: 'change_request',
|
|
418
|
+
requires_spec: true,
|
|
419
|
+
confidence: changeHits >= 2 ? 'high' : 'medium',
|
|
420
|
+
signals: {
|
|
421
|
+
change_hits: changeHits,
|
|
422
|
+
question_hits: questionHits,
|
|
423
|
+
goal_missing: false
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (questionHits > 0 && changeHits === 0) {
|
|
429
|
+
return {
|
|
430
|
+
intent_type: 'analysis_only',
|
|
431
|
+
requires_spec: false,
|
|
432
|
+
confidence: 'medium',
|
|
433
|
+
signals: {
|
|
434
|
+
change_hits: changeHits,
|
|
435
|
+
question_hits: questionHits,
|
|
436
|
+
goal_missing: false
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
intent_type: 'ambiguous',
|
|
443
|
+
requires_spec: false,
|
|
444
|
+
confidence: 'low',
|
|
445
|
+
signals: {
|
|
446
|
+
change_hits: changeHits,
|
|
447
|
+
question_hits: questionHits,
|
|
448
|
+
goal_missing: false
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function listExistingSpecIds(projectPath, fileSystem = fs) {
|
|
454
|
+
const specsRoot = path.join(projectPath, '.sce', 'specs');
|
|
455
|
+
if (!await fileSystem.pathExists(specsRoot)) {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
const entries = await fileSystem.readdir(specsRoot);
|
|
459
|
+
const specIds = [];
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
const candidatePath = path.join(specsRoot, entry);
|
|
462
|
+
try {
|
|
463
|
+
const stat = await fileSystem.stat(candidatePath);
|
|
464
|
+
if (stat && stat.isDirectory()) {
|
|
465
|
+
specIds.push(entry);
|
|
466
|
+
}
|
|
467
|
+
} catch (_error) {
|
|
468
|
+
// ignore unreadable entry
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
specIds.sort();
|
|
472
|
+
return specIds;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function createAutoSpecId(sceneId, goal, existingSpecIds = [], policy = DEFAULT_STUDIO_INTAKE_POLICY) {
|
|
476
|
+
const now = new Date();
|
|
477
|
+
const timestamp = now.toISOString().replace(/[-:TZ.]/g, '').slice(2, 14);
|
|
478
|
+
const sceneSlug = normalizeSceneSlug(sceneId);
|
|
479
|
+
const goalSlug = buildGoalSlug(goal, policy?.spec_id?.max_goal_slug_tokens || 6);
|
|
480
|
+
const prefix = slugifyText(normalizeText(policy?.spec_id?.prefix) || 'auto', 'auto');
|
|
481
|
+
const base = `${prefix}-${sceneSlug}-${goalSlug}-${timestamp}`.slice(0, 96);
|
|
482
|
+
const existing = new Set(existingSpecIds);
|
|
483
|
+
if (!existing.has(base)) {
|
|
484
|
+
return base;
|
|
485
|
+
}
|
|
486
|
+
for (let index = 2; index <= 99; index += 1) {
|
|
487
|
+
const candidate = `${base}-${index}`;
|
|
488
|
+
if (!existing.has(candidate)) {
|
|
489
|
+
return candidate;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return `${base}-${Math.random().toString(36).slice(2, 7)}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function materializeIntakeSpec(projectPath, payload = {}, dependencies = {}) {
|
|
496
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
497
|
+
const sceneId = normalizeText(payload.scene_id);
|
|
498
|
+
const goal = normalizeText(payload.goal);
|
|
499
|
+
const fromChat = normalizeText(payload.from_chat);
|
|
500
|
+
const specId = normalizeText(payload.spec_id);
|
|
501
|
+
if (!specId) {
|
|
502
|
+
throw new Error('spec_id is required for intake spec creation');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const specRoot = path.join(projectPath, '.sce', 'specs', specId);
|
|
506
|
+
if (await fileSystem.pathExists(specRoot)) {
|
|
507
|
+
return {
|
|
508
|
+
created: false,
|
|
509
|
+
spec_id: specId,
|
|
510
|
+
reason: 'already_exists',
|
|
511
|
+
spec_path: toRelativePosix(projectPath, specRoot)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const allSpecs = await listExistingSpecIds(projectPath, fileSystem);
|
|
516
|
+
const draftGenerator = dependencies.draftGenerator || new DraftGenerator();
|
|
517
|
+
const problemStatement = goal || `Studio intake request from ${fromChat || 'chat-session'}`;
|
|
518
|
+
const draft = draftGenerator.generate({
|
|
519
|
+
specName: specId,
|
|
520
|
+
profile: 'studio-intake',
|
|
521
|
+
template: 'default',
|
|
522
|
+
context: {
|
|
523
|
+
projectPath,
|
|
524
|
+
totalSpecs: allSpecs.length
|
|
525
|
+
},
|
|
526
|
+
answers: {
|
|
527
|
+
problemStatement,
|
|
528
|
+
primaryFlow: `Scene ${sceneId || 'unknown'} iterative capability evolution`,
|
|
529
|
+
verificationPlan: 'Run spec gate + studio verify/release with closure gates'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const requirementsPath = path.join(specRoot, 'requirements.md');
|
|
534
|
+
const designPath = path.join(specRoot, 'design.md');
|
|
535
|
+
const tasksPath = path.join(specRoot, 'tasks.md');
|
|
536
|
+
await fileSystem.ensureDir(specRoot);
|
|
537
|
+
await fileSystem.writeFile(requirementsPath, draft.requirements, 'utf8');
|
|
538
|
+
await fileSystem.writeFile(designPath, draft.design, 'utf8');
|
|
539
|
+
await fileSystem.writeFile(tasksPath, draft.tasks, 'utf8');
|
|
540
|
+
const domainArtifacts = await ensureSpecDomainArtifacts(projectPath, specId, {
|
|
541
|
+
fileSystem,
|
|
542
|
+
force: true,
|
|
543
|
+
sceneId,
|
|
544
|
+
problemStatement,
|
|
545
|
+
primaryFlow: `Scene ${sceneId || 'unknown'} delivery`,
|
|
546
|
+
verificationPlan: 'spec gate + studio verify + problem-closure gate'
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
created: true,
|
|
551
|
+
spec_id: specId,
|
|
552
|
+
spec_path: toRelativePosix(projectPath, specRoot),
|
|
553
|
+
files: {
|
|
554
|
+
requirements: toRelativePosix(projectPath, requirementsPath),
|
|
555
|
+
design: toRelativePosix(projectPath, designPath),
|
|
556
|
+
tasks: toRelativePosix(projectPath, tasksPath),
|
|
557
|
+
domain_map: toRelativePosix(projectPath, domainArtifacts.paths.domain_map),
|
|
558
|
+
scene_spec: toRelativePosix(projectPath, domainArtifacts.paths.scene_spec),
|
|
559
|
+
domain_chain: toRelativePosix(projectPath, domainArtifacts.paths.domain_chain),
|
|
560
|
+
problem_contract: toRelativePosix(projectPath, domainArtifacts.paths.problem_contract)
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function normalizeRelatedCandidates(relatedSpecLookup = {}) {
|
|
566
|
+
const items = Array.isArray(relatedSpecLookup.related_specs)
|
|
567
|
+
? relatedSpecLookup.related_specs
|
|
568
|
+
: [];
|
|
569
|
+
return items
|
|
570
|
+
.map((item) => ({
|
|
571
|
+
spec_id: normalizeText(item.spec_id),
|
|
572
|
+
score: normalizeNumber(item.score, 0),
|
|
573
|
+
scene_id: normalizeText(item.scene_id) || null,
|
|
574
|
+
problem_statement: normalizeText(item.problem_statement) || '',
|
|
575
|
+
reasons: Array.isArray(item.reasons) ? item.reasons : []
|
|
576
|
+
}))
|
|
577
|
+
.filter((item) => item.spec_id);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function resolveStudioSpecIntakeDecision(context = {}, policy = DEFAULT_STUDIO_INTAKE_POLICY) {
|
|
581
|
+
const goal = normalizeText(context.goal);
|
|
582
|
+
const explicitSpecId = normalizeText(context.explicit_spec_id);
|
|
583
|
+
const domainChainBinding = context.domain_chain_binding && typeof context.domain_chain_binding === 'object'
|
|
584
|
+
? context.domain_chain_binding
|
|
585
|
+
: {};
|
|
586
|
+
const relatedCandidates = normalizeRelatedCandidates(context.related_specs);
|
|
587
|
+
const intent = context.intent && typeof context.intent === 'object'
|
|
588
|
+
? context.intent
|
|
589
|
+
: classifyStudioGoalIntent(goal, policy);
|
|
590
|
+
|
|
591
|
+
if (!policy.enabled) {
|
|
592
|
+
return {
|
|
593
|
+
action: 'disabled',
|
|
594
|
+
reason: 'policy_disabled',
|
|
595
|
+
confidence: 'high',
|
|
596
|
+
spec_id: explicitSpecId || null,
|
|
597
|
+
source: explicitSpecId ? 'explicit-spec' : 'none',
|
|
598
|
+
intent
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (explicitSpecId) {
|
|
603
|
+
return {
|
|
604
|
+
action: 'bind_existing',
|
|
605
|
+
reason: 'explicit_spec',
|
|
606
|
+
confidence: 'high',
|
|
607
|
+
spec_id: explicitSpecId,
|
|
608
|
+
source: 'explicit-spec',
|
|
609
|
+
intent
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const preferredRelated = relatedCandidates.find((item) => item.score >= policy.related_spec_min_score) || null;
|
|
614
|
+
const hasBoundDomainSpec = domainChainBinding.resolved === true && normalizeText(domainChainBinding.spec_id).length > 0;
|
|
615
|
+
const domainSpecId = hasBoundDomainSpec ? normalizeText(domainChainBinding.spec_id) : '';
|
|
616
|
+
const domainProblem = normalizeText(domainChainBinding?.summary?.problem_statement);
|
|
617
|
+
const goalSimilarityToDomain = computeJaccard(tokenizeText(goal), tokenizeText(domainProblem));
|
|
618
|
+
|
|
619
|
+
if (hasBoundDomainSpec && policy.prefer_existing_scene_spec) {
|
|
620
|
+
const shouldDivergeCreate = Boolean(
|
|
621
|
+
policy.allow_new_spec_when_goal_diverges
|
|
622
|
+
&& intent.requires_spec
|
|
623
|
+
&& goal
|
|
624
|
+
&& goalSimilarityToDomain < policy.divergence_similarity_threshold
|
|
625
|
+
);
|
|
626
|
+
if (!shouldDivergeCreate) {
|
|
627
|
+
return {
|
|
628
|
+
action: 'bind_existing',
|
|
629
|
+
reason: 'prefer_existing_scene_spec',
|
|
630
|
+
confidence: 'high',
|
|
631
|
+
spec_id: domainSpecId,
|
|
632
|
+
source: 'scene-domain-chain',
|
|
633
|
+
similarity: goalSimilarityToDomain,
|
|
634
|
+
intent
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (preferredRelated) {
|
|
640
|
+
return {
|
|
641
|
+
action: 'bind_existing',
|
|
642
|
+
reason: 'related_spec_match',
|
|
643
|
+
confidence: preferredRelated.score >= (policy.related_spec_min_score + 20) ? 'high' : 'medium',
|
|
644
|
+
spec_id: preferredRelated.spec_id,
|
|
645
|
+
source: 'related-spec',
|
|
646
|
+
matched_score: preferredRelated.score,
|
|
647
|
+
intent
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const goalMissing = normalizeText(goal).length === 0;
|
|
652
|
+
const shouldCreateByMissingGoal = goalMissing && policy.goal_missing_strategy === 'create_for_tracking';
|
|
653
|
+
const shouldCreateByIntent = intent.requires_spec || policy.force_spec_for_studio_plan;
|
|
654
|
+
const shouldCreate = policy.auto_create_spec && (shouldCreateByIntent || shouldCreateByMissingGoal);
|
|
655
|
+
|
|
656
|
+
if (shouldCreate) {
|
|
657
|
+
return {
|
|
658
|
+
action: 'create_spec',
|
|
659
|
+
reason: goalMissing ? 'goal_missing_tracking' : 'intent_requires_spec',
|
|
660
|
+
confidence: intent.requires_spec ? intent.confidence : 'medium',
|
|
661
|
+
spec_id: null,
|
|
662
|
+
source: 'auto-create',
|
|
663
|
+
intent
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
action: 'none',
|
|
669
|
+
reason: 'no_spec_required',
|
|
670
|
+
confidence: 'low',
|
|
671
|
+
spec_id: null,
|
|
672
|
+
source: 'none',
|
|
673
|
+
intent
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function runStudioAutoIntake(options = {}, dependencies = {}) {
|
|
678
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
679
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
680
|
+
const sceneId = normalizeText(options.scene_id || options.sceneId);
|
|
681
|
+
const goal = normalizeText(options.goal);
|
|
682
|
+
const fromChat = normalizeText(options.from_chat || options.fromChat);
|
|
683
|
+
const explicitSpecId = normalizeText(options.explicit_spec_id || options.spec_id || options.specId);
|
|
684
|
+
const apply = options.apply === true;
|
|
685
|
+
const skip = options.skip === true;
|
|
686
|
+
|
|
687
|
+
const loadedPolicy = options.policy && typeof options.policy === 'object'
|
|
688
|
+
? { policy: normalizeStudioIntakePolicy(options.policy), policy_path: '(inline)', loaded_from: 'inline' }
|
|
689
|
+
: await loadStudioIntakePolicy(projectPath, fileSystem);
|
|
690
|
+
|
|
691
|
+
const policy = loadedPolicy.policy;
|
|
692
|
+
const intent = classifyStudioGoalIntent(goal, policy);
|
|
693
|
+
const decision = resolveStudioSpecIntakeDecision({
|
|
694
|
+
goal,
|
|
695
|
+
explicit_spec_id: explicitSpecId,
|
|
696
|
+
domain_chain_binding: options.domain_chain_binding || {},
|
|
697
|
+
related_specs: options.related_specs || {},
|
|
698
|
+
intent
|
|
699
|
+
}, policy);
|
|
700
|
+
|
|
701
|
+
const payload = {
|
|
702
|
+
mode: 'studio-auto-intake',
|
|
703
|
+
success: true,
|
|
704
|
+
enabled: policy.enabled === true && !skip,
|
|
705
|
+
policy_path: loadedPolicy.policy_path,
|
|
706
|
+
policy_loaded_from: loadedPolicy.loaded_from,
|
|
707
|
+
policy,
|
|
708
|
+
scene_id: sceneId || null,
|
|
709
|
+
from_chat: fromChat || null,
|
|
710
|
+
goal: goal || null,
|
|
711
|
+
intent,
|
|
712
|
+
decision: {
|
|
713
|
+
...decision
|
|
714
|
+
},
|
|
715
|
+
selected_spec_id: decision.spec_id || null,
|
|
716
|
+
created_spec: null
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
if (skip && policy.allow_manual_spec_override !== true) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
'manual spec override is disabled by studio intake policy (allow_manual_spec_override=false)'
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (skip) {
|
|
726
|
+
payload.enabled = false;
|
|
727
|
+
payload.decision = {
|
|
728
|
+
action: 'disabled',
|
|
729
|
+
reason: 'manual_override',
|
|
730
|
+
confidence: 'high',
|
|
731
|
+
spec_id: explicitSpecId || null,
|
|
732
|
+
source: explicitSpecId ? 'explicit-spec' : 'none',
|
|
733
|
+
intent
|
|
734
|
+
};
|
|
735
|
+
payload.selected_spec_id = payload.decision.spec_id || null;
|
|
736
|
+
return payload;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (decision.action === 'create_spec') {
|
|
740
|
+
const existingSpecIds = await listExistingSpecIds(projectPath, fileSystem);
|
|
741
|
+
const autoSpecId = createAutoSpecId(sceneId, goal, existingSpecIds, policy);
|
|
742
|
+
payload.decision.spec_id = autoSpecId;
|
|
743
|
+
payload.selected_spec_id = autoSpecId;
|
|
744
|
+
if (apply) {
|
|
745
|
+
const createdSpec = await materializeIntakeSpec(projectPath, {
|
|
746
|
+
scene_id: sceneId,
|
|
747
|
+
from_chat: fromChat,
|
|
748
|
+
goal,
|
|
749
|
+
spec_id: autoSpecId
|
|
750
|
+
}, {
|
|
751
|
+
fileSystem
|
|
752
|
+
});
|
|
753
|
+
payload.created_spec = createdSpec;
|
|
754
|
+
payload.decision.created = createdSpec.created === true;
|
|
755
|
+
} else {
|
|
756
|
+
payload.decision.created = false;
|
|
757
|
+
}
|
|
758
|
+
return payload;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
payload.selected_spec_id = decision.spec_id || null;
|
|
762
|
+
return payload;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function readJsonSafe(filePath, fileSystem = fs) {
|
|
766
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
return await fileSystem.readJson(filePath);
|
|
771
|
+
} catch (_error) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function readFileSafe(filePath, fileSystem = fs) {
|
|
777
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
778
|
+
return '';
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
return await fileSystem.readFile(filePath, 'utf8');
|
|
782
|
+
} catch (_error) {
|
|
783
|
+
return '';
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function classifySpecLifecycleState(record = {}, staleDays = 14) {
|
|
788
|
+
const nowMs = Date.now();
|
|
789
|
+
const updatedMs = Date.parse(record.updated_at || 0);
|
|
790
|
+
const ageDays = Number.isFinite(updatedMs)
|
|
791
|
+
? Number(((nowMs - updatedMs) / (1000 * 60 * 60 * 24)).toFixed(2))
|
|
792
|
+
: null;
|
|
793
|
+
|
|
794
|
+
if (record.tasks_total > 0 && record.tasks_done >= record.tasks_total) {
|
|
795
|
+
return {
|
|
796
|
+
state: 'completed',
|
|
797
|
+
age_days: ageDays
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (ageDays !== null && ageDays > staleDays) {
|
|
801
|
+
return {
|
|
802
|
+
state: 'stale',
|
|
803
|
+
age_days: ageDays
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
state: 'active',
|
|
808
|
+
age_days: ageDays
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function scanSpecPortfolio(projectPath = process.cwd(), options = {}, dependencies = {}) {
|
|
813
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
814
|
+
const specsRoot = path.join(projectPath, '.sce', 'specs');
|
|
815
|
+
if (!await fileSystem.pathExists(specsRoot)) {
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
const staleDays = normalizeInteger(options.stale_days, 14, 1, 3650);
|
|
819
|
+
const overrideContext = await loadSceneBindingOverrides(projectPath, {
|
|
820
|
+
overridePath: options.override_file || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH
|
|
821
|
+
}, fileSystem);
|
|
822
|
+
const sceneOverrides = normalizeSceneBindingOverrides(overrideContext.overrides || {});
|
|
823
|
+
const entries = await fileSystem.readdir(specsRoot);
|
|
824
|
+
const records = [];
|
|
825
|
+
|
|
826
|
+
for (const entry of entries) {
|
|
827
|
+
const specRoot = path.join(specsRoot, entry);
|
|
828
|
+
let stat = null;
|
|
829
|
+
try {
|
|
830
|
+
stat = await fileSystem.stat(specRoot);
|
|
831
|
+
} catch (_error) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (!stat || !stat.isDirectory()) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const domainChainPath = path.join(specRoot, 'custom', 'problem-domain-chain.json');
|
|
839
|
+
const problemContractPath = path.join(specRoot, 'custom', 'problem-contract.json');
|
|
840
|
+
const requirementsPath = path.join(specRoot, 'requirements.md');
|
|
841
|
+
const designPath = path.join(specRoot, 'design.md');
|
|
842
|
+
const tasksPath = path.join(specRoot, 'tasks.md');
|
|
843
|
+
const [chain, contract, requirements, design, tasks] = await Promise.all([
|
|
844
|
+
readJsonSafe(domainChainPath, fileSystem),
|
|
845
|
+
readJsonSafe(problemContractPath, fileSystem),
|
|
846
|
+
readFileSafe(requirementsPath, fileSystem),
|
|
847
|
+
readFileSafe(designPath, fileSystem),
|
|
848
|
+
readFileSafe(tasksPath, fileSystem)
|
|
849
|
+
]);
|
|
850
|
+
|
|
851
|
+
const sceneFromChain = normalizeText(chain && chain.scene_id ? chain.scene_id : '');
|
|
852
|
+
const sceneFromOverride = resolveSceneIdFromOverrides(entry, sceneOverrides);
|
|
853
|
+
const sceneId = sceneFromChain || sceneFromOverride || 'scene.unassigned';
|
|
854
|
+
const sceneSource = sceneFromChain
|
|
855
|
+
? 'domain-chain'
|
|
856
|
+
: (sceneFromOverride ? 'override' : 'unassigned');
|
|
857
|
+
const problemStatement = normalizeText(
|
|
858
|
+
(chain && chain.problem && chain.problem.statement)
|
|
859
|
+
|| (contract && contract.issue_statement)
|
|
860
|
+
|| ''
|
|
861
|
+
);
|
|
862
|
+
const taskProgress = parseTasksProgress(tasks);
|
|
863
|
+
const lifecycle = classifySpecLifecycleState({
|
|
864
|
+
updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
|
|
865
|
+
tasks_total: taskProgress.total,
|
|
866
|
+
tasks_done: taskProgress.done
|
|
867
|
+
}, staleDays);
|
|
868
|
+
const searchSeed = [
|
|
869
|
+
entry,
|
|
870
|
+
sceneId,
|
|
871
|
+
problemStatement,
|
|
872
|
+
normalizeText(requirements).slice(0, 1600),
|
|
873
|
+
normalizeText(design).slice(0, 1600),
|
|
874
|
+
normalizeText(tasks).slice(0, 1000)
|
|
875
|
+
].join('\n');
|
|
876
|
+
const tokens = tokenizeText(searchSeed);
|
|
877
|
+
|
|
878
|
+
records.push({
|
|
879
|
+
spec_id: entry,
|
|
880
|
+
scene_id: sceneId,
|
|
881
|
+
problem_statement: problemStatement || null,
|
|
882
|
+
updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
|
|
883
|
+
tasks_total: taskProgress.total,
|
|
884
|
+
tasks_done: taskProgress.done,
|
|
885
|
+
tasks_progress: taskProgress.ratio,
|
|
886
|
+
lifecycle_state: lifecycle.state,
|
|
887
|
+
age_days: lifecycle.age_days,
|
|
888
|
+
scene_source: sceneSource,
|
|
889
|
+
tokens
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
records.sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')));
|
|
894
|
+
return records;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function buildSceneGovernanceReport(records = [], policy = DEFAULT_STUDIO_INTAKE_POLICY) {
|
|
898
|
+
const governance = policy.governance || DEFAULT_STUDIO_INTAKE_POLICY.governance;
|
|
899
|
+
const threshold = normalizeNumber(governance.duplicate_similarity_threshold, 0.66);
|
|
900
|
+
const maxActive = normalizeInteger(governance.max_active_specs_per_scene, 3, 1, 200);
|
|
901
|
+
|
|
902
|
+
const sceneMap = new Map();
|
|
903
|
+
for (const record of records) {
|
|
904
|
+
const sceneId = normalizeText(record.scene_id) || 'scene.unassigned';
|
|
905
|
+
if (!sceneMap.has(sceneId)) {
|
|
906
|
+
sceneMap.set(sceneId, []);
|
|
907
|
+
}
|
|
908
|
+
sceneMap.get(sceneId).push(record);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const scenes = [];
|
|
912
|
+
const mergeCandidates = [];
|
|
913
|
+
const archiveCandidates = [];
|
|
914
|
+
let duplicatePairs = 0;
|
|
915
|
+
|
|
916
|
+
for (const [sceneId, sceneSpecs] of sceneMap.entries()) {
|
|
917
|
+
const sortedSpecs = [...sceneSpecs].sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')));
|
|
918
|
+
const activeSpecs = sortedSpecs.filter((item) => item.lifecycle_state === 'active');
|
|
919
|
+
const staleSpecs = sortedSpecs.filter((item) => item.lifecycle_state === 'stale');
|
|
920
|
+
const completedSpecs = sortedSpecs.filter((item) => item.lifecycle_state === 'completed');
|
|
921
|
+
|
|
922
|
+
const duplicates = [];
|
|
923
|
+
for (let i = 0; i < sortedSpecs.length; i += 1) {
|
|
924
|
+
for (let j = i + 1; j < sortedSpecs.length; j += 1) {
|
|
925
|
+
const left = sortedSpecs[i];
|
|
926
|
+
const right = sortedSpecs[j];
|
|
927
|
+
const similarity = computeJaccard(left.tokens, right.tokens);
|
|
928
|
+
if (similarity >= threshold) {
|
|
929
|
+
duplicatePairs += 1;
|
|
930
|
+
duplicates.push({
|
|
931
|
+
spec_a: left.spec_id,
|
|
932
|
+
spec_b: right.spec_id,
|
|
933
|
+
similarity
|
|
934
|
+
});
|
|
935
|
+
mergeCandidates.push({
|
|
936
|
+
scene_id: sceneId,
|
|
937
|
+
spec_primary: left.spec_id,
|
|
938
|
+
spec_secondary: right.spec_id,
|
|
939
|
+
similarity
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const overflow = activeSpecs.length > maxActive
|
|
946
|
+
? activeSpecs.slice(maxActive).map((item) => item.spec_id)
|
|
947
|
+
: [];
|
|
948
|
+
for (const specId of overflow) {
|
|
949
|
+
archiveCandidates.push({
|
|
950
|
+
scene_id: sceneId,
|
|
951
|
+
spec_id: specId,
|
|
952
|
+
reason: `active spec count exceeds limit ${maxActive}`
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
scenes.push({
|
|
957
|
+
scene_id: sceneId,
|
|
958
|
+
total_specs: sortedSpecs.length,
|
|
959
|
+
active_specs: activeSpecs.length,
|
|
960
|
+
completed_specs: completedSpecs.length,
|
|
961
|
+
stale_specs: staleSpecs.length,
|
|
962
|
+
active_limit: maxActive,
|
|
963
|
+
active_overflow_count: overflow.length,
|
|
964
|
+
active_overflow_specs: overflow,
|
|
965
|
+
duplicate_pairs: duplicates,
|
|
966
|
+
specs: sortedSpecs.map((item) => ({
|
|
967
|
+
spec_id: item.spec_id,
|
|
968
|
+
lifecycle_state: item.lifecycle_state,
|
|
969
|
+
updated_at: item.updated_at,
|
|
970
|
+
age_days: item.age_days,
|
|
971
|
+
tasks_total: item.tasks_total,
|
|
972
|
+
tasks_done: item.tasks_done,
|
|
973
|
+
tasks_progress: item.tasks_progress,
|
|
974
|
+
problem_statement: item.problem_statement
|
|
975
|
+
}))
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
scenes.sort((left, right) => {
|
|
980
|
+
if (right.total_specs !== left.total_specs) {
|
|
981
|
+
return right.total_specs - left.total_specs;
|
|
982
|
+
}
|
|
983
|
+
return String(left.scene_id).localeCompare(String(right.scene_id));
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const totalSpecs = records.length;
|
|
987
|
+
const activeTotal = records.filter((item) => item.lifecycle_state === 'active').length;
|
|
988
|
+
const staleTotal = records.filter((item) => item.lifecycle_state === 'stale').length;
|
|
989
|
+
const completedTotal = records.filter((item) => item.lifecycle_state === 'completed').length;
|
|
990
|
+
const overflowScenes = scenes.filter((item) => item.active_overflow_count > 0).length;
|
|
991
|
+
const alertCount = duplicatePairs + overflowScenes + staleTotal;
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
scene_count: scenes.length,
|
|
995
|
+
total_specs: totalSpecs,
|
|
996
|
+
active_specs: activeTotal,
|
|
997
|
+
completed_specs: completedTotal,
|
|
998
|
+
stale_specs: staleTotal,
|
|
999
|
+
duplicate_pairs: duplicatePairs,
|
|
1000
|
+
overflow_scenes: overflowScenes,
|
|
1001
|
+
alert_count: alertCount,
|
|
1002
|
+
status: alertCount > 0 ? 'attention' : 'healthy',
|
|
1003
|
+
scenes,
|
|
1004
|
+
actions: {
|
|
1005
|
+
merge_candidates: mergeCandidates,
|
|
1006
|
+
archive_candidates: archiveCandidates
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function classifyBackfillRule(record = {}, backfillPolicy = {}) {
|
|
1012
|
+
const rules = Array.isArray(backfillPolicy.rules) ? backfillPolicy.rules : [];
|
|
1013
|
+
const defaultSceneId = normalizeText(backfillPolicy.default_scene_id) || 'scene.sce-core';
|
|
1014
|
+
const searchText = [
|
|
1015
|
+
normalizeText(record.spec_id).toLowerCase(),
|
|
1016
|
+
normalizeText(record.problem_statement).toLowerCase()
|
|
1017
|
+
].join(' ');
|
|
1018
|
+
const searchTokens = new Set(tokenizeText(searchText));
|
|
1019
|
+
let bestMatch = null;
|
|
1020
|
+
|
|
1021
|
+
for (const rule of rules) {
|
|
1022
|
+
const ruleId = normalizeText(rule.id);
|
|
1023
|
+
const sceneId = normalizeText(rule.scene_id);
|
|
1024
|
+
const keywords = normalizeTextList(rule.keywords).map((item) => item.toLowerCase());
|
|
1025
|
+
if (!ruleId || !sceneId || keywords.length === 0) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const matchedKeywords = [];
|
|
1030
|
+
for (const keyword of keywords) {
|
|
1031
|
+
if (!keyword) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (searchText.includes(keyword) || searchTokens.has(keyword)) {
|
|
1035
|
+
matchedKeywords.push(keyword);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (matchedKeywords.length === 0) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const score = matchedKeywords.length;
|
|
1043
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
1044
|
+
bestMatch = {
|
|
1045
|
+
rule_id: ruleId,
|
|
1046
|
+
scene_id: sceneId,
|
|
1047
|
+
matched_keywords: matchedKeywords,
|
|
1048
|
+
score
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (!bestMatch) {
|
|
1054
|
+
return {
|
|
1055
|
+
scene_id: defaultSceneId,
|
|
1056
|
+
rule_id: 'default',
|
|
1057
|
+
matched_keywords: [],
|
|
1058
|
+
confidence: 'low',
|
|
1059
|
+
source: 'default'
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
scene_id: bestMatch.scene_id,
|
|
1065
|
+
rule_id: bestMatch.rule_id,
|
|
1066
|
+
matched_keywords: bestMatch.matched_keywords,
|
|
1067
|
+
confidence: bestMatch.score >= 2 ? 'high' : 'medium',
|
|
1068
|
+
source: 'rule'
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function clampBackfillLimit(value, fallback = 0, max = 1000) {
|
|
1073
|
+
if (value === undefined || value === null || value === '') {
|
|
1074
|
+
return fallback;
|
|
1075
|
+
}
|
|
1076
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
1077
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1078
|
+
return fallback;
|
|
1079
|
+
}
|
|
1080
|
+
return Math.min(parsed, max);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function runStudioSceneBackfill(options = {}, dependencies = {}) {
|
|
1084
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1085
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1086
|
+
const loaded = options.policy && typeof options.policy === 'object'
|
|
1087
|
+
? { policy: normalizeStudioIntakePolicy(options.policy), policy_path: '(inline)', loaded_from: 'inline' }
|
|
1088
|
+
: await loadStudioIntakePolicy(projectPath, fileSystem);
|
|
1089
|
+
const policy = loaded.policy;
|
|
1090
|
+
const backfillPolicy = policy.backfill || DEFAULT_STUDIO_INTAKE_POLICY.backfill;
|
|
1091
|
+
const apply = options.apply === true;
|
|
1092
|
+
const refreshGovernance = options.refresh_governance !== false && options.refreshGovernance !== false;
|
|
1093
|
+
const sourceScene = normalizeText(options.source_scene || options.sourceScene || options.scene) || 'scene.unassigned';
|
|
1094
|
+
const includeAll = options.all === true || options.active_only === false || options.activeOnly === false;
|
|
1095
|
+
const activeOnly = includeAll ? false : (options.active_only === true || options.activeOnly === true || backfillPolicy.active_only_default !== false);
|
|
1096
|
+
const limit = clampBackfillLimit(options.limit, 0, 2000);
|
|
1097
|
+
const overrideFile = normalizeText(backfillPolicy.override_file) || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH;
|
|
1098
|
+
|
|
1099
|
+
const records = await scanSpecPortfolio(projectPath, {
|
|
1100
|
+
stale_days: policy.governance && policy.governance.stale_days,
|
|
1101
|
+
override_file: overrideFile
|
|
1102
|
+
}, {
|
|
1103
|
+
fileSystem
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
let candidates = records.filter((item) => normalizeText(item.scene_id) === sourceScene);
|
|
1107
|
+
if (activeOnly) {
|
|
1108
|
+
candidates = candidates.filter((item) => item.lifecycle_state === 'active');
|
|
1109
|
+
}
|
|
1110
|
+
if (limit > 0) {
|
|
1111
|
+
candidates = candidates.slice(0, limit);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const assignmentPlan = candidates.map((record) => {
|
|
1115
|
+
const decision = classifyBackfillRule(record, backfillPolicy);
|
|
1116
|
+
return {
|
|
1117
|
+
spec_id: record.spec_id,
|
|
1118
|
+
from_scene_id: sourceScene,
|
|
1119
|
+
to_scene_id: decision.scene_id,
|
|
1120
|
+
lifecycle_state: record.lifecycle_state,
|
|
1121
|
+
rule_id: decision.rule_id,
|
|
1122
|
+
source: decision.source,
|
|
1123
|
+
confidence: decision.confidence,
|
|
1124
|
+
matched_keywords: decision.matched_keywords
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
const overrideContext = await loadSceneBindingOverrides(projectPath, {
|
|
1129
|
+
overridePath: overrideFile
|
|
1130
|
+
}, fileSystem);
|
|
1131
|
+
const existingOverrides = normalizeSceneBindingOverrides(overrideContext.overrides || {});
|
|
1132
|
+
const nextOverrides = normalizeSceneBindingOverrides(existingOverrides);
|
|
1133
|
+
const now = new Date().toISOString();
|
|
1134
|
+
let changedCount = 0;
|
|
1135
|
+
|
|
1136
|
+
for (const item of assignmentPlan) {
|
|
1137
|
+
const existing = existingOverrides.mappings[item.spec_id];
|
|
1138
|
+
const currentScene = normalizeText(existing && existing.scene_id);
|
|
1139
|
+
if (currentScene === item.to_scene_id) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
nextOverrides.mappings[item.spec_id] = {
|
|
1143
|
+
scene_id: item.to_scene_id,
|
|
1144
|
+
source: 'scene-backfill',
|
|
1145
|
+
rule_id: item.rule_id,
|
|
1146
|
+
updated_at: now
|
|
1147
|
+
};
|
|
1148
|
+
changedCount += 1;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const totalsByTargetScene = {};
|
|
1152
|
+
for (const item of assignmentPlan) {
|
|
1153
|
+
totalsByTargetScene[item.to_scene_id] = (totalsByTargetScene[item.to_scene_id] || 0) + 1;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const payload = {
|
|
1157
|
+
mode: 'studio-scene-backfill',
|
|
1158
|
+
success: true,
|
|
1159
|
+
generated_at: now,
|
|
1160
|
+
policy_path: loaded.policy_path,
|
|
1161
|
+
policy_loaded_from: loaded.loaded_from,
|
|
1162
|
+
source_scene: sourceScene,
|
|
1163
|
+
active_only: activeOnly,
|
|
1164
|
+
apply,
|
|
1165
|
+
refresh_governance: refreshGovernance,
|
|
1166
|
+
override_file: overrideFile,
|
|
1167
|
+
summary: {
|
|
1168
|
+
candidate_count: assignmentPlan.length,
|
|
1169
|
+
changed_count: changedCount,
|
|
1170
|
+
target_scene_count: Object.keys(totalsByTargetScene).length
|
|
1171
|
+
},
|
|
1172
|
+
targets: totalsByTargetScene,
|
|
1173
|
+
assignments: assignmentPlan
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
if (apply) {
|
|
1177
|
+
const overrideAbsolutePath = path.join(projectPath, overrideFile);
|
|
1178
|
+
await fileSystem.ensureDir(path.dirname(overrideAbsolutePath));
|
|
1179
|
+
const serialized = {
|
|
1180
|
+
schema_version: '1.0',
|
|
1181
|
+
generated_at: nextOverrides.generated_at || now,
|
|
1182
|
+
updated_at: now,
|
|
1183
|
+
source: 'studio-scene-backfill',
|
|
1184
|
+
mappings: nextOverrides.mappings
|
|
1185
|
+
};
|
|
1186
|
+
await fileSystem.writeJson(overrideAbsolutePath, serialized, { spaces: 2 });
|
|
1187
|
+
payload.override_written = overrideFile;
|
|
1188
|
+
if (refreshGovernance) {
|
|
1189
|
+
const refreshed = await runStudioSpecGovernance({
|
|
1190
|
+
apply: true
|
|
1191
|
+
}, {
|
|
1192
|
+
projectPath,
|
|
1193
|
+
fileSystem
|
|
1194
|
+
});
|
|
1195
|
+
payload.governance = {
|
|
1196
|
+
status: refreshed.summary ? refreshed.summary.status : null,
|
|
1197
|
+
alert_count: refreshed.summary ? Number(refreshed.summary.alert_count || 0) : 0,
|
|
1198
|
+
report_file: refreshed.report_file || null,
|
|
1199
|
+
scene_index_file: refreshed.scene_index_file || null
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return payload;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function runStudioSpecGovernance(options = {}, dependencies = {}) {
|
|
1208
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1209
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1210
|
+
const loaded = options.policy && typeof options.policy === 'object'
|
|
1211
|
+
? { policy: normalizeStudioIntakePolicy(options.policy), policy_path: '(inline)', loaded_from: 'inline' }
|
|
1212
|
+
: await loadStudioIntakePolicy(projectPath, fileSystem);
|
|
1213
|
+
const policy = loaded.policy;
|
|
1214
|
+
const governance = policy.governance || DEFAULT_STUDIO_INTAKE_POLICY.governance;
|
|
1215
|
+
const backfill = policy.backfill || DEFAULT_STUDIO_INTAKE_POLICY.backfill;
|
|
1216
|
+
const apply = options.apply !== false;
|
|
1217
|
+
const sceneFilter = normalizeText(options.scene_id || options.sceneId || options.scene);
|
|
1218
|
+
const overrideFile = normalizeText(backfill.override_file) || DEFAULT_STUDIO_SCENE_OVERRIDE_PATH;
|
|
1219
|
+
|
|
1220
|
+
const records = await scanSpecPortfolio(projectPath, {
|
|
1221
|
+
stale_days: governance.stale_days,
|
|
1222
|
+
override_file: overrideFile
|
|
1223
|
+
}, {
|
|
1224
|
+
fileSystem
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const filteredRecords = sceneFilter
|
|
1228
|
+
? records.filter((item) => normalizeText(item.scene_id) === sceneFilter)
|
|
1229
|
+
: records;
|
|
1230
|
+
|
|
1231
|
+
const summary = buildSceneGovernanceReport(filteredRecords, policy);
|
|
1232
|
+
const generatedAt = new Date().toISOString();
|
|
1233
|
+
const reportPayload = {
|
|
1234
|
+
mode: 'studio-spec-governance',
|
|
1235
|
+
success: true,
|
|
1236
|
+
generated_at: generatedAt,
|
|
1237
|
+
scene_filter: sceneFilter || null,
|
|
1238
|
+
policy_path: loaded.policy_path,
|
|
1239
|
+
policy_loaded_from: loaded.loaded_from,
|
|
1240
|
+
policy: {
|
|
1241
|
+
governance,
|
|
1242
|
+
backfill: {
|
|
1243
|
+
override_file: overrideFile
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
summary: {
|
|
1247
|
+
scene_count: summary.scene_count,
|
|
1248
|
+
total_specs: summary.total_specs,
|
|
1249
|
+
active_specs: summary.active_specs,
|
|
1250
|
+
completed_specs: summary.completed_specs,
|
|
1251
|
+
stale_specs: summary.stale_specs,
|
|
1252
|
+
duplicate_pairs: summary.duplicate_pairs,
|
|
1253
|
+
overflow_scenes: summary.overflow_scenes,
|
|
1254
|
+
alert_count: summary.alert_count,
|
|
1255
|
+
status: summary.status
|
|
1256
|
+
},
|
|
1257
|
+
scenes: summary.scenes,
|
|
1258
|
+
actions: summary.actions
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
if (apply) {
|
|
1262
|
+
const reportPath = path.join(projectPath, DEFAULT_STUDIO_PORTFOLIO_REPORT);
|
|
1263
|
+
const indexPath = path.join(projectPath, DEFAULT_STUDIO_SCENE_INDEX);
|
|
1264
|
+
await fileSystem.ensureDir(path.dirname(reportPath));
|
|
1265
|
+
await fileSystem.writeJson(reportPath, reportPayload, { spaces: 2 });
|
|
1266
|
+
const sceneIndex = {
|
|
1267
|
+
schema_version: '1.0',
|
|
1268
|
+
generated_at: generatedAt,
|
|
1269
|
+
scene_filter: sceneFilter || null,
|
|
1270
|
+
scenes: {}
|
|
1271
|
+
};
|
|
1272
|
+
for (const scene of summary.scenes) {
|
|
1273
|
+
sceneIndex.scenes[scene.scene_id] = {
|
|
1274
|
+
total_specs: scene.total_specs,
|
|
1275
|
+
active_specs: scene.active_specs,
|
|
1276
|
+
completed_specs: scene.completed_specs,
|
|
1277
|
+
stale_specs: scene.stale_specs,
|
|
1278
|
+
spec_ids: scene.specs.map((item) => item.spec_id),
|
|
1279
|
+
active_spec_ids: scene.specs
|
|
1280
|
+
.filter((item) => item.lifecycle_state === 'active')
|
|
1281
|
+
.map((item) => item.spec_id),
|
|
1282
|
+
stale_spec_ids: scene.specs
|
|
1283
|
+
.filter((item) => item.lifecycle_state === 'stale')
|
|
1284
|
+
.map((item) => item.spec_id)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
await fileSystem.writeJson(indexPath, sceneIndex, { spaces: 2 });
|
|
1288
|
+
reportPayload.report_file = DEFAULT_STUDIO_PORTFOLIO_REPORT;
|
|
1289
|
+
reportPayload.scene_index_file = DEFAULT_STUDIO_SCENE_INDEX;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return reportPayload;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
module.exports = {
|
|
1296
|
+
DEFAULT_STUDIO_INTAKE_POLICY_PATH,
|
|
1297
|
+
DEFAULT_STUDIO_INTAKE_POLICY,
|
|
1298
|
+
DEFAULT_STUDIO_SCENE_OVERRIDE_PATH,
|
|
1299
|
+
DEFAULT_STUDIO_PORTFOLIO_REPORT,
|
|
1300
|
+
DEFAULT_STUDIO_SCENE_INDEX,
|
|
1301
|
+
normalizeStudioIntakePolicy,
|
|
1302
|
+
loadStudioIntakePolicy,
|
|
1303
|
+
classifyStudioGoalIntent,
|
|
1304
|
+
resolveStudioSpecIntakeDecision,
|
|
1305
|
+
createAutoSpecId,
|
|
1306
|
+
materializeIntakeSpec,
|
|
1307
|
+
runStudioAutoIntake,
|
|
1308
|
+
parseTasksProgress,
|
|
1309
|
+
scanSpecPortfolio,
|
|
1310
|
+
buildSceneGovernanceReport,
|
|
1311
|
+
runStudioSceneBackfill,
|
|
1312
|
+
runStudioSpecGovernance,
|
|
1313
|
+
tokenizeText,
|
|
1314
|
+
computeJaccard
|
|
1315
|
+
};
|