scene-capability-engine 3.6.57 → 3.6.59
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 +18 -0
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/bin/scene-capability-engine.js +2 -0
- package/docs/command-reference.md +72 -0
- package/docs/magicball-adaptation-task-checklist-v1.md +65 -10
- package/docs/magicball-cli-invocation-examples.md +53 -8
- package/docs/magicball-engineering-projection-contract.md +175 -0
- package/docs/magicball-frontend-state-and-command-mapping.md +42 -5
- package/docs/magicball-integration-doc-index.md +19 -5
- package/docs/magicball-integration-issue-tracker.md +15 -5
- package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +13 -5
- package/docs/magicball-project-portfolio-contract.md +216 -0
- package/docs/magicball-sce-adaptation-guide.md +18 -4
- package/docs/magicball-ui-surface-checklist.md +25 -0
- package/docs/magicball-write-auth-adaptation-guide.md +3 -1
- package/docs/release-checklist.md +8 -0
- package/docs/releases/README.md +2 -0
- package/docs/releases/v3.6.58.md +27 -0
- package/docs/releases/v3.6.59.md +18 -0
- package/docs/zh/release-checklist.md +8 -0
- package/docs/zh/releases/README.md +2 -0
- package/docs/zh/releases/v3.6.58.md +27 -0
- package/docs/zh/releases/v3.6.59.md +18 -0
- package/lib/app/engineering-scaffold-service.js +154 -0
- package/lib/commands/app.js +442 -13
- package/lib/commands/project.js +105 -0
- package/lib/commands/scene.js +16 -0
- package/lib/project/portfolio-projection-service.js +389 -0
- package/lib/project/supervision-projection-service.js +329 -0
- package/lib/project/target-resolution-service.js +180 -0
- package/lib/scene/delivery-projection-service.js +650 -0
- package/package.json +6 -2
- package/scripts/magicball-engineering-contract-audit.js +347 -0
- package/scripts/magicball-project-contract-audit.js +254 -0
- package/template/.sce/README.md +2 -2
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const TaskClaimer = require('../task/task-claimer');
|
|
5
|
+
|
|
6
|
+
const SPEC_GOVERNANCE_SCENE_INDEX = path.join('.sce', 'spec-governance', 'scene-index.json');
|
|
7
|
+
const SESSION_GOVERNANCE_SCENE_INDEX = path.join('.sce', 'session-governance', 'scene-index.json');
|
|
8
|
+
const HANDOFF_REPORT_DIR = path.join('.sce', 'reports', 'handoff-runs');
|
|
9
|
+
const STUDIO_REPORT_DIR = path.join('.sce', 'reports', 'studio');
|
|
10
|
+
const SPEC_DOCUMENTS = [
|
|
11
|
+
{ kind: 'requirements', relativePath: 'requirements.md', title: 'Requirements' },
|
|
12
|
+
{ kind: 'design', relativePath: 'design.md', title: 'Design' },
|
|
13
|
+
{ kind: 'tasks', relativePath: 'tasks.md', title: 'Tasks' },
|
|
14
|
+
{ kind: 'problem-contract', relativePath: path.join('custom', 'problem-contract.json'), title: 'Problem Contract' },
|
|
15
|
+
{ kind: 'scene-spec', relativePath: path.join('custom', 'scene-spec.md'), title: 'Scene Spec' },
|
|
16
|
+
{ kind: 'problem-domain-chain', relativePath: path.join('custom', 'problem-domain-chain.json'), title: 'Problem Domain Chain' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function normalizeString(value) {
|
|
20
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toRelativePosix(projectRoot, absolutePath) {
|
|
24
|
+
return path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toArray(value) {
|
|
28
|
+
return Array.isArray(value) ? value : [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isObject(value) {
|
|
32
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readJsonIfExists(filePath, fileSystem = fs) {
|
|
36
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return await fileSystem.readJson(filePath);
|
|
41
|
+
} catch (_error) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function statIfExists(filePath, fileSystem = fs) {
|
|
47
|
+
if (!await fileSystem.pathExists(filePath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await fileSystem.stat(filePath);
|
|
52
|
+
} catch (_error) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pickSceneRecord(payload, sceneId) {
|
|
58
|
+
if (!isObject(payload)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const scenes = payload.scenes;
|
|
62
|
+
if (Array.isArray(scenes)) {
|
|
63
|
+
return scenes.find((item) => normalizeString(item && item.scene_id) === sceneId) || null;
|
|
64
|
+
}
|
|
65
|
+
if (isObject(scenes)) {
|
|
66
|
+
if (isObject(scenes[sceneId])) {
|
|
67
|
+
return scenes[sceneId];
|
|
68
|
+
}
|
|
69
|
+
return Object.values(scenes).find((item) => normalizeString(item && item.scene_id) === sceneId) || null;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadSceneGovernanceRecord(projectRoot, sceneId, fileSystem = fs) {
|
|
75
|
+
const payload = await readJsonIfExists(path.join(projectRoot, SPEC_GOVERNANCE_SCENE_INDEX), fileSystem);
|
|
76
|
+
return pickSceneRecord(payload, sceneId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function loadSceneSessionRecord(projectRoot, sceneId, fileSystem = fs) {
|
|
80
|
+
const payload = await readJsonIfExists(path.join(projectRoot, SESSION_GOVERNANCE_SCENE_INDEX), fileSystem);
|
|
81
|
+
return pickSceneRecord(payload, sceneId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function listSpecDirectoryNames(projectRoot, fileSystem = fs) {
|
|
85
|
+
const specsRoot = path.join(projectRoot, '.sce', 'specs');
|
|
86
|
+
if (!await fileSystem.pathExists(specsRoot)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const entries = await fileSystem.readdir(specsRoot);
|
|
90
|
+
const results = [];
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const absolutePath = path.join(specsRoot, entry);
|
|
93
|
+
try {
|
|
94
|
+
const stat = await fileSystem.stat(absolutePath);
|
|
95
|
+
if (stat && stat.isDirectory()) {
|
|
96
|
+
results.push(entry);
|
|
97
|
+
}
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
// Ignore unreadable entries.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
results.sort((left, right) => left.localeCompare(right));
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readSpecDomainChain(projectRoot, specId, fileSystem = fs) {
|
|
107
|
+
return readJsonIfExists(
|
|
108
|
+
path.join(projectRoot, '.sce', 'specs', specId, 'custom', 'problem-domain-chain.json'),
|
|
109
|
+
fileSystem
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function resolveSceneSpecIds(projectRoot, sceneId, explicitSpecId = '', fileSystem = fs) {
|
|
114
|
+
if (explicitSpecId) {
|
|
115
|
+
return [explicitSpecId];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const governanceRecord = await loadSceneGovernanceRecord(projectRoot, sceneId, fileSystem);
|
|
119
|
+
const governedSpecIds = toArray(governanceRecord && governanceRecord.spec_ids)
|
|
120
|
+
.map((item) => normalizeString(item))
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
if (governedSpecIds.length > 0) {
|
|
123
|
+
return governedSpecIds;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const specNames = await listSpecDirectoryNames(projectRoot, fileSystem);
|
|
127
|
+
const matched = [];
|
|
128
|
+
for (const specId of specNames) {
|
|
129
|
+
const chain = await readSpecDomainChain(projectRoot, specId, fileSystem);
|
|
130
|
+
if (normalizeString(chain && chain.scene_id) === sceneId) {
|
|
131
|
+
matched.push(specId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return matched;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadSpecContext(projectRoot, specId, sceneIdHint = '', fileSystem = fs, taskClaimer = new TaskClaimer()) {
|
|
138
|
+
const specRoot = path.join(projectRoot, '.sce', 'specs', specId);
|
|
139
|
+
if (!await fileSystem.pathExists(specRoot)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const domainChain = await readSpecDomainChain(projectRoot, specId, fileSystem);
|
|
144
|
+
const sceneId = normalizeString(domainChain && domainChain.scene_id) || sceneIdHint || null;
|
|
145
|
+
const files = [];
|
|
146
|
+
for (const item of SPEC_DOCUMENTS) {
|
|
147
|
+
const absolutePath = path.join(specRoot, item.relativePath);
|
|
148
|
+
const stat = await statIfExists(absolutePath, fileSystem);
|
|
149
|
+
if (!stat) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
files.push({
|
|
153
|
+
kind: item.kind,
|
|
154
|
+
title: item.title,
|
|
155
|
+
absolutePath,
|
|
156
|
+
relativePath: toRelativePosix(projectRoot, absolutePath),
|
|
157
|
+
updatedAt: stat.mtime ? stat.mtime.toISOString() : null
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let tasks = [];
|
|
162
|
+
const tasksPath = path.join(specRoot, 'tasks.md');
|
|
163
|
+
if (await fileSystem.pathExists(tasksPath)) {
|
|
164
|
+
tasks = await taskClaimer.parseTasks(tasksPath, { preferStatusMarkers: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const taskCounts = {
|
|
168
|
+
total: tasks.length,
|
|
169
|
+
completed: tasks.filter((item) => item.status === 'completed').length,
|
|
170
|
+
inProgress: tasks.filter((item) => item.status === 'in-progress').length,
|
|
171
|
+
queued: tasks.filter((item) => item.status === 'queued').length,
|
|
172
|
+
notStarted: tasks.filter((item) => item.status === 'not-started').length,
|
|
173
|
+
claimed: tasks.filter((item) => normalizeString(item.claimedBy)).length
|
|
174
|
+
};
|
|
175
|
+
const completionPercent = taskCounts.total > 0
|
|
176
|
+
? Math.round((taskCounts.completed / taskCounts.total) * 100)
|
|
177
|
+
: 0;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
specId,
|
|
181
|
+
sceneId,
|
|
182
|
+
domainChain,
|
|
183
|
+
files,
|
|
184
|
+
taskCounts,
|
|
185
|
+
completionPercent
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildRecordBase({
|
|
190
|
+
id,
|
|
191
|
+
objectType,
|
|
192
|
+
provenance,
|
|
193
|
+
provisional = false,
|
|
194
|
+
sceneId = null,
|
|
195
|
+
specId = null,
|
|
196
|
+
taskRef = null,
|
|
197
|
+
requestId = null,
|
|
198
|
+
eventId = null
|
|
199
|
+
}) {
|
|
200
|
+
return {
|
|
201
|
+
id,
|
|
202
|
+
objectType,
|
|
203
|
+
provenance,
|
|
204
|
+
provisional,
|
|
205
|
+
bound: Boolean(sceneId || specId || taskRef || requestId || eventId),
|
|
206
|
+
...(sceneId ? { sceneId } : {}),
|
|
207
|
+
...(specId ? { specId } : {}),
|
|
208
|
+
...(taskRef ? { taskRef } : {}),
|
|
209
|
+
...(requestId ? { requestId } : {}),
|
|
210
|
+
...(eventId ? { eventId } : {})
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function deriveSpecChecklistStatus(taskCounts = {}) {
|
|
215
|
+
if (taskCounts.total === 0) {
|
|
216
|
+
return 'not_started';
|
|
217
|
+
}
|
|
218
|
+
if (taskCounts.completed === taskCounts.total) {
|
|
219
|
+
return 'completed';
|
|
220
|
+
}
|
|
221
|
+
if (taskCounts.inProgress > 0) {
|
|
222
|
+
return 'in_progress';
|
|
223
|
+
}
|
|
224
|
+
if (taskCounts.queued > 0) {
|
|
225
|
+
return 'queued';
|
|
226
|
+
}
|
|
227
|
+
return 'not_started';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function deriveSceneOverviewStatus(sceneRecord = {}, taskCounts = {}) {
|
|
231
|
+
if (Number(sceneRecord.stale_specs || 0) > 0) {
|
|
232
|
+
return 'at_risk';
|
|
233
|
+
}
|
|
234
|
+
if ((taskCounts.total || 0) > 0 && taskCounts.completed === taskCounts.total) {
|
|
235
|
+
return 'completed';
|
|
236
|
+
}
|
|
237
|
+
if ((taskCounts.inProgress || 0) > 0) {
|
|
238
|
+
return 'in_progress';
|
|
239
|
+
}
|
|
240
|
+
return 'observed';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildOverviewRecords(sceneId, specContexts = [], sceneRecord = null, sessionRecord = null, verifyRecords = [], releaseRecords = []) {
|
|
244
|
+
const taskTotals = specContexts.reduce((acc, item) => ({
|
|
245
|
+
total: acc.total + Number(item.taskCounts.total || 0),
|
|
246
|
+
completed: acc.completed + Number(item.taskCounts.completed || 0),
|
|
247
|
+
inProgress: acc.inProgress + Number(item.taskCounts.inProgress || 0),
|
|
248
|
+
queued: acc.queued + Number(item.taskCounts.queued || 0),
|
|
249
|
+
notStarted: acc.notStarted + Number(item.taskCounts.notStarted || 0)
|
|
250
|
+
}), {
|
|
251
|
+
total: 0,
|
|
252
|
+
completed: 0,
|
|
253
|
+
inProgress: 0,
|
|
254
|
+
queued: 0,
|
|
255
|
+
notStarted: 0
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const latestVerifyAt = verifyRecords
|
|
259
|
+
.map((item) => normalizeString(item.completedAt || item.generatedAt))
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.sort()
|
|
262
|
+
.slice(-1)[0] || null;
|
|
263
|
+
const latestReleaseAt = releaseRecords
|
|
264
|
+
.map((item) => normalizeString(item.completedAt || item.generatedAt))
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.sort()
|
|
267
|
+
.slice(-1)[0] || null;
|
|
268
|
+
|
|
269
|
+
const records = [
|
|
270
|
+
{
|
|
271
|
+
...buildRecordBase({
|
|
272
|
+
id: `overview:scene:${sceneId}`,
|
|
273
|
+
objectType: 'overview',
|
|
274
|
+
provenance: 'derived',
|
|
275
|
+
sceneId
|
|
276
|
+
}),
|
|
277
|
+
status: deriveSceneOverviewStatus(sceneRecord || {}, taskTotals),
|
|
278
|
+
summary: {
|
|
279
|
+
totalSpecs: Number(sceneRecord && sceneRecord.total_specs != null ? sceneRecord.total_specs : specContexts.length),
|
|
280
|
+
activeSpecs: Number(sceneRecord && sceneRecord.active_specs != null
|
|
281
|
+
? sceneRecord.active_specs
|
|
282
|
+
: specContexts.filter((item) => deriveSpecChecklistStatus(item.taskCounts) !== 'completed').length),
|
|
283
|
+
completedSpecs: Number(sceneRecord && sceneRecord.completed_specs != null
|
|
284
|
+
? sceneRecord.completed_specs
|
|
285
|
+
: specContexts.filter((item) => deriveSpecChecklistStatus(item.taskCounts) === 'completed').length),
|
|
286
|
+
staleSpecs: Number(sceneRecord && sceneRecord.stale_specs != null ? sceneRecord.stale_specs : 0),
|
|
287
|
+
totalTasks: taskTotals.total,
|
|
288
|
+
completedTasks: taskTotals.completed,
|
|
289
|
+
inProgressTasks: taskTotals.inProgress,
|
|
290
|
+
queuedTasks: taskTotals.queued,
|
|
291
|
+
pendingTasks: taskTotals.notStarted,
|
|
292
|
+
latestVerifyAt,
|
|
293
|
+
latestReleaseAt
|
|
294
|
+
},
|
|
295
|
+
session: sessionRecord
|
|
296
|
+
? {
|
|
297
|
+
activeSessionId: normalizeString(sessionRecord.active_session_id) || null,
|
|
298
|
+
activeCycle: Number(sessionRecord.active_cycle || 0) || null,
|
|
299
|
+
latestCompletedSessionId: normalizeString(sessionRecord.latest_completed_session_id) || null
|
|
300
|
+
}
|
|
301
|
+
: null
|
|
302
|
+
}
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
for (const item of specContexts) {
|
|
306
|
+
records.push({
|
|
307
|
+
...buildRecordBase({
|
|
308
|
+
id: `overview:spec:${item.specId}`,
|
|
309
|
+
objectType: 'overview',
|
|
310
|
+
provenance: 'derived',
|
|
311
|
+
sceneId: item.sceneId || sceneId,
|
|
312
|
+
specId: item.specId
|
|
313
|
+
}),
|
|
314
|
+
status: deriveSpecChecklistStatus(item.taskCounts),
|
|
315
|
+
summary: {
|
|
316
|
+
documentCount: item.files.length,
|
|
317
|
+
totalTasks: item.taskCounts.total,
|
|
318
|
+
completedTasks: item.taskCounts.completed,
|
|
319
|
+
inProgressTasks: item.taskCounts.inProgress,
|
|
320
|
+
queuedTasks: item.taskCounts.queued,
|
|
321
|
+
pendingTasks: item.taskCounts.notStarted,
|
|
322
|
+
completionPercent: item.completionPercent
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return records;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function buildDocumentRecords(specContexts = []) {
|
|
331
|
+
return specContexts.flatMap((item) => item.files.map((file) => ({
|
|
332
|
+
...buildRecordBase({
|
|
333
|
+
id: `document:${item.specId}:${file.kind}`,
|
|
334
|
+
objectType: 'document',
|
|
335
|
+
provenance: 'engine',
|
|
336
|
+
sceneId: item.sceneId,
|
|
337
|
+
specId: item.specId
|
|
338
|
+
}),
|
|
339
|
+
documentType: file.kind,
|
|
340
|
+
title: file.title,
|
|
341
|
+
path: file.relativePath,
|
|
342
|
+
updatedAt: file.updatedAt
|
|
343
|
+
})));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildChecklistRecords(specContexts = []) {
|
|
347
|
+
return specContexts.map((item) => ({
|
|
348
|
+
...buildRecordBase({
|
|
349
|
+
id: `checklist:${item.specId}:tasks`,
|
|
350
|
+
objectType: 'checklist',
|
|
351
|
+
provenance: 'engine',
|
|
352
|
+
sceneId: item.sceneId,
|
|
353
|
+
specId: item.specId
|
|
354
|
+
}),
|
|
355
|
+
checklistType: 'tasks',
|
|
356
|
+
status: deriveSpecChecklistStatus(item.taskCounts),
|
|
357
|
+
counts: item.taskCounts,
|
|
358
|
+
completionPercent: item.completionPercent
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resolveReportSceneId(report = {}) {
|
|
363
|
+
return normalizeString(report.scene_id)
|
|
364
|
+
|| normalizeString(report?.domain_chain?.context?.scene_id)
|
|
365
|
+
|| normalizeString(report?.domain_chain?.problem_contract?.scene_id)
|
|
366
|
+
|| '';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function resolveReportSpecId(report = {}) {
|
|
370
|
+
return normalizeString(report.spec_id)
|
|
371
|
+
|| normalizeString(report?.domain_chain?.spec_id)
|
|
372
|
+
|| '';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function extractHandoffSpecIds(report = {}) {
|
|
376
|
+
const specs = toArray(report && report.handoff && report.handoff.specs);
|
|
377
|
+
return Array.from(new Set(specs
|
|
378
|
+
.map((item) => {
|
|
379
|
+
if (typeof item === 'string') {
|
|
380
|
+
return normalizeString(item);
|
|
381
|
+
}
|
|
382
|
+
return normalizeString(item && (item.spec_id || item.id || item.spec || item.spec_name));
|
|
383
|
+
})
|
|
384
|
+
.filter(Boolean)));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function summarizeStepStates(steps = []) {
|
|
388
|
+
const failedSteps = steps
|
|
389
|
+
.filter((item) => /fail|error|block/i.test(normalizeString(item && item.status)))
|
|
390
|
+
.map((item) => normalizeString(item && (item.id || item.key || item.name)))
|
|
391
|
+
.filter(Boolean);
|
|
392
|
+
return {
|
|
393
|
+
total: steps.length,
|
|
394
|
+
failed: failedSteps.length,
|
|
395
|
+
failedStepIds: failedSteps
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function listJsonFiles(dirPath, fileSystem = fs) {
|
|
400
|
+
if (!await fileSystem.pathExists(dirPath)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
const entries = await fileSystem.readdir(dirPath);
|
|
404
|
+
return entries
|
|
405
|
+
.filter((entry) => entry.toLowerCase().endsWith('.json'))
|
|
406
|
+
.sort((left, right) => left.localeCompare(right))
|
|
407
|
+
.map((entry) => path.join(dirPath, entry));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function buildHandoffRecords(projectRoot, targetSceneId, targetSpecIds = [], explicitSpecId = '', fileSystem = fs) {
|
|
411
|
+
const files = await listJsonFiles(path.join(projectRoot, HANDOFF_REPORT_DIR), fileSystem);
|
|
412
|
+
const targetSpecSet = new Set(targetSpecIds);
|
|
413
|
+
const records = [];
|
|
414
|
+
|
|
415
|
+
for (const filePath of files) {
|
|
416
|
+
const report = await readJsonIfExists(filePath, fileSystem);
|
|
417
|
+
if (!isObject(report)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const reportSceneId = resolveReportSceneId(report);
|
|
422
|
+
if (reportSceneId && reportSceneId !== targetSceneId) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const reportSpecIds = extractHandoffSpecIds(report);
|
|
427
|
+
const matchedSpecIds = reportSpecIds.filter((specId) => targetSpecSet.has(specId));
|
|
428
|
+
if (reportSpecIds.length > 0 && matchedSpecIds.length === 0) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (explicitSpecId && reportSpecIds.length > 0 && !matchedSpecIds.includes(explicitSpecId)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (!reportSceneId && matchedSpecIds.length === 0) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const targets = matchedSpecIds.length > 0 ? matchedSpecIds : [null];
|
|
439
|
+
for (const specId of targets) {
|
|
440
|
+
records.push({
|
|
441
|
+
...buildRecordBase({
|
|
442
|
+
id: `handoff:${normalizeString(report.session_id) || path.basename(filePath, '.json')}:${specId || 'scene'}`,
|
|
443
|
+
objectType: 'handoff',
|
|
444
|
+
provenance: 'linked-evidence',
|
|
445
|
+
sceneId: reportSceneId || targetSceneId,
|
|
446
|
+
specId
|
|
447
|
+
}),
|
|
448
|
+
sessionId: normalizeString(report.session_id) || path.basename(filePath, '.json'),
|
|
449
|
+
status: normalizeString(report.status) || 'observed',
|
|
450
|
+
manifestPath: normalizeString(report.manifest_path) || null,
|
|
451
|
+
reportFile: toRelativePosix(projectRoot, filePath),
|
|
452
|
+
generatedAt: normalizeString(report.generated_at || report.completed_at || report.updated_at) || null,
|
|
453
|
+
gatePassed: report?.gates?.passed === true,
|
|
454
|
+
reasons: toArray(report?.gates?.reasons).map((item) => `${item}`)
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
records.sort((left, right) => `${right.generatedAt || ''}`.localeCompare(`${left.generatedAt || ''}`));
|
|
460
|
+
return records;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function buildStudioEvidenceRecords(projectRoot, targetSceneId, targetSpecIds = [], explicitSpecId = '', reportPrefix = '', objectType = '', fileSystem = fs) {
|
|
464
|
+
const files = await listJsonFiles(path.join(projectRoot, STUDIO_REPORT_DIR), fileSystem);
|
|
465
|
+
const targetSpecSet = new Set(targetSpecIds);
|
|
466
|
+
const records = [];
|
|
467
|
+
|
|
468
|
+
for (const filePath of files) {
|
|
469
|
+
const baseName = path.basename(filePath).toLowerCase();
|
|
470
|
+
if (!baseName.startsWith(`${reportPrefix.toLowerCase()}-`)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const report = await readJsonIfExists(filePath, fileSystem);
|
|
474
|
+
if (!isObject(report)) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const reportSceneId = resolveReportSceneId(report);
|
|
479
|
+
const reportSpecId = resolveReportSpecId(report);
|
|
480
|
+
|
|
481
|
+
if (reportSceneId && reportSceneId !== targetSceneId) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (reportSpecId && !targetSpecSet.has(reportSpecId)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (explicitSpecId && reportSpecId && reportSpecId !== explicitSpecId) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (!reportSceneId && !reportSpecId) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const steps = toArray(report.steps);
|
|
495
|
+
const stepSummary = summarizeStepStates(steps);
|
|
496
|
+
const resolvedSceneId = reportSceneId || targetSceneId;
|
|
497
|
+
const resolvedSpecId = reportSpecId || null;
|
|
498
|
+
const recordIdSuffix = resolvedSpecId || 'scene';
|
|
499
|
+
const base = buildRecordBase({
|
|
500
|
+
id: `${objectType}:${path.basename(filePath, '.json')}:${recordIdSuffix}`,
|
|
501
|
+
objectType,
|
|
502
|
+
provenance: 'linked-evidence',
|
|
503
|
+
sceneId: resolvedSceneId,
|
|
504
|
+
specId: resolvedSpecId
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (objectType === 'acceptance') {
|
|
508
|
+
records.push({
|
|
509
|
+
...base,
|
|
510
|
+
acceptanceType: 'studio-verify',
|
|
511
|
+
profile: normalizeString(report.profile) || null,
|
|
512
|
+
reportFile: toRelativePosix(projectRoot, filePath),
|
|
513
|
+
startedAt: normalizeString(report.started_at) || null,
|
|
514
|
+
completedAt: normalizeString(report.completed_at) || null,
|
|
515
|
+
passed: report.passed === true,
|
|
516
|
+
status: report.passed === true ? 'accepted' : 'rejected',
|
|
517
|
+
stepSummary
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (objectType === 'release') {
|
|
523
|
+
records.push({
|
|
524
|
+
...base,
|
|
525
|
+
releaseType: 'studio-release',
|
|
526
|
+
channel: normalizeString(report.channel) || null,
|
|
527
|
+
profile: normalizeString(report.profile) || null,
|
|
528
|
+
releaseRef: normalizeString(report.release_ref) || null,
|
|
529
|
+
reportFile: toRelativePosix(projectRoot, filePath),
|
|
530
|
+
startedAt: normalizeString(report.started_at) || null,
|
|
531
|
+
completedAt: normalizeString(report.completed_at) || null,
|
|
532
|
+
passed: report.passed === true,
|
|
533
|
+
status: report.passed === true ? 'released' : 'blocked',
|
|
534
|
+
stepSummary
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
records.sort((left, right) => `${right.completedAt || right.startedAt || ''}`.localeCompare(`${left.completedAt || left.startedAt || ''}`));
|
|
540
|
+
return records;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildSourceSummary(specContexts = [], handoffs = [], releases = [], acceptance = []) {
|
|
544
|
+
return {
|
|
545
|
+
specCount: specContexts.length,
|
|
546
|
+
documentCount: specContexts.reduce((sum, item) => sum + item.files.length, 0),
|
|
547
|
+
handoffCount: handoffs.length,
|
|
548
|
+
releaseCount: releases.length,
|
|
549
|
+
acceptanceCount: acceptance.length
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function runSceneDeliveryShowCommand(rawOptions = {}, dependencies = {}) {
|
|
554
|
+
const options = rawOptions && typeof rawOptions === 'object' ? rawOptions : {};
|
|
555
|
+
const sceneId = normalizeString(options.scene);
|
|
556
|
+
const explicitSpecId = normalizeString(options.spec);
|
|
557
|
+
if (!sceneId) {
|
|
558
|
+
throw new Error('--scene is required');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const projectRoot = dependencies.projectRoot || process.cwd();
|
|
562
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
563
|
+
const taskClaimer = dependencies.taskClaimer || new TaskClaimer();
|
|
564
|
+
const specRoot = explicitSpecId
|
|
565
|
+
? path.join(projectRoot, '.sce', 'specs', explicitSpecId)
|
|
566
|
+
: null;
|
|
567
|
+
if (specRoot && !await fileSystem.pathExists(specRoot)) {
|
|
568
|
+
throw new Error(`spec not found: ${explicitSpecId}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const governanceRecord = await loadSceneGovernanceRecord(projectRoot, sceneId, fileSystem);
|
|
572
|
+
const sessionRecord = await loadSceneSessionRecord(projectRoot, sceneId, fileSystem);
|
|
573
|
+
const specIds = await resolveSceneSpecIds(projectRoot, sceneId, explicitSpecId, fileSystem);
|
|
574
|
+
const specContexts = [];
|
|
575
|
+
for (const specId of specIds) {
|
|
576
|
+
const context = await loadSpecContext(projectRoot, specId, sceneId, fileSystem, taskClaimer);
|
|
577
|
+
if (!context) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (explicitSpecId && context.sceneId && context.sceneId !== sceneId) {
|
|
581
|
+
throw new Error(`spec ${explicitSpecId} is bound to scene ${context.sceneId}, not ${sceneId}`);
|
|
582
|
+
}
|
|
583
|
+
if (!explicitSpecId && context.sceneId && context.sceneId !== sceneId) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
specContexts.push(context);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (explicitSpecId && specContexts.length === 0) {
|
|
590
|
+
throw new Error(`spec ${explicitSpecId} is unavailable for scene ${sceneId}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const targetSpecIds = specContexts.map((item) => item.specId);
|
|
594
|
+
const handoffs = await buildHandoffRecords(projectRoot, sceneId, targetSpecIds, explicitSpecId, fileSystem);
|
|
595
|
+
const acceptance = await buildStudioEvidenceRecords(
|
|
596
|
+
projectRoot,
|
|
597
|
+
sceneId,
|
|
598
|
+
targetSpecIds,
|
|
599
|
+
explicitSpecId,
|
|
600
|
+
'verify',
|
|
601
|
+
'acceptance',
|
|
602
|
+
fileSystem
|
|
603
|
+
);
|
|
604
|
+
const releases = await buildStudioEvidenceRecords(
|
|
605
|
+
projectRoot,
|
|
606
|
+
sceneId,
|
|
607
|
+
targetSpecIds,
|
|
608
|
+
explicitSpecId,
|
|
609
|
+
'release',
|
|
610
|
+
'release',
|
|
611
|
+
fileSystem
|
|
612
|
+
);
|
|
613
|
+
const overview = buildOverviewRecords(sceneId, specContexts, governanceRecord, sessionRecord, acceptance, releases);
|
|
614
|
+
const documents = buildDocumentRecords(specContexts);
|
|
615
|
+
const checklists = buildChecklistRecords(specContexts);
|
|
616
|
+
|
|
617
|
+
const payload = {
|
|
618
|
+
mode: 'scene-delivery-show',
|
|
619
|
+
generated_at: new Date().toISOString(),
|
|
620
|
+
query: {
|
|
621
|
+
scene_id: sceneId,
|
|
622
|
+
spec_id: explicitSpecId || null
|
|
623
|
+
},
|
|
624
|
+
summary: buildSourceSummary(specContexts, handoffs, releases, acceptance),
|
|
625
|
+
overview,
|
|
626
|
+
documents,
|
|
627
|
+
checklists,
|
|
628
|
+
handoffs,
|
|
629
|
+
releases,
|
|
630
|
+
acceptance
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
if (options.json) {
|
|
634
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
635
|
+
} else {
|
|
636
|
+
console.log(chalk.blue('Scene Delivery Show'));
|
|
637
|
+
console.log(` Scene: ${sceneId}`);
|
|
638
|
+
console.log(` Spec: ${explicitSpecId || 'all'}`);
|
|
639
|
+
console.log(` Specs: ${payload.summary.specCount}`);
|
|
640
|
+
console.log(` Documents: ${payload.summary.documentCount}`);
|
|
641
|
+
console.log(` Handoffs: ${payload.summary.handoffCount}`);
|
|
642
|
+
console.log(` Releases: ${payload.summary.releaseCount}`);
|
|
643
|
+
console.log(` Acceptance: ${payload.summary.acceptanceCount}`);
|
|
644
|
+
}
|
|
645
|
+
return payload;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
module.exports = {
|
|
649
|
+
runSceneDeliveryShowCommand
|
|
650
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scene-capability-engine",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.59",
|
|
4
4
|
"description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -44,12 +44,16 @@
|
|
|
44
44
|
"audit:refactor-trigger": "node scripts/refactor-trigger-audit.js",
|
|
45
45
|
"audit:steering": "node scripts/steering-content-audit.js --fail-on-error",
|
|
46
46
|
"audit:clarification-first": "node scripts/clarification-first-audit.js --fail-on-violation",
|
|
47
|
+
"audit:magicball-engineering-contract": "node scripts/magicball-engineering-contract-audit.js --fail-on-violation",
|
|
48
|
+
"audit:magicball-project-contract": "node scripts/magicball-project-contract-audit.js --fail-on-violation",
|
|
47
49
|
"gate:collab-governance": "node scripts/collab-governance-gate.js --fail-on-violation",
|
|
48
50
|
"audit:state-storage": "node scripts/state-storage-tiering-audit.js",
|
|
49
51
|
"report:release-docs": "node scripts/release-doc-version-audit.js --json",
|
|
50
52
|
"report:refactor-trigger": "node scripts/refactor-trigger-audit.js --json",
|
|
51
53
|
"report:steering-audit": "node scripts/steering-content-audit.js --json",
|
|
52
54
|
"report:clarification-first-audit": "node scripts/clarification-first-audit.js --json",
|
|
55
|
+
"report:magicball-engineering-contract": "node scripts/magicball-engineering-contract-audit.js --json",
|
|
56
|
+
"report:magicball-project-contract": "node scripts/magicball-project-contract-audit.js --json",
|
|
53
57
|
"report:collab-governance": "node scripts/collab-governance-gate.js --json",
|
|
54
58
|
"report:state-storage": "node scripts/state-storage-tiering-audit.js --json",
|
|
55
59
|
"report:interactive-approval-projection": "node scripts/interactive-approval-event-projection.js --action doctor --json",
|
|
@@ -92,7 +96,7 @@
|
|
|
92
96
|
"gate:release-asset-integrity": "node scripts/release-asset-integrity-check.js",
|
|
93
97
|
"report:release-risk-remediation": "node scripts/release-risk-remediation-bundle.js --json",
|
|
94
98
|
"report:moqui-core-regression": "node scripts/moqui-core-regression-suite.js --json",
|
|
95
|
-
"prepublishOnly": "npm run test:release && npm run test:skip-audit && npm run test:sce-tracking && npm run gate:npm-runtime-assets && npm run test:brand-consistency && npm run audit:release-docs && npm run audit:steering && npm run audit:clarification-first && npm run gate:collab-governance && npm run gate:git-managed && npm run gate:errorbook-registry-health && npm run gate:errorbook-release && npm run report:interactive-governance -- --fail-on-alert",
|
|
99
|
+
"prepublishOnly": "npm run test:release && npm run test:skip-audit && npm run test:sce-tracking && npm run gate:npm-runtime-assets && npm run test:brand-consistency && npm run audit:release-docs && npm run audit:steering && npm run audit:clarification-first && npm run audit:magicball-engineering-contract && npm run audit:magicball-project-contract && npm run gate:collab-governance && npm run gate:git-managed && npm run gate:errorbook-registry-health && npm run gate:errorbook-release && npm run report:interactive-governance -- --fail-on-alert",
|
|
96
100
|
"publish:manual": "npm publish --access public",
|
|
97
101
|
"install-global": "npm install -g .",
|
|
98
102
|
"uninstall-global": "npm uninstall -g scene-capability-engine"
|