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.
Files changed (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -3
  3. package/README.zh.md +5 -3
  4. package/bin/scene-capability-engine.js +2 -0
  5. package/docs/command-reference.md +72 -0
  6. package/docs/magicball-adaptation-task-checklist-v1.md +65 -10
  7. package/docs/magicball-cli-invocation-examples.md +53 -8
  8. package/docs/magicball-engineering-projection-contract.md +175 -0
  9. package/docs/magicball-frontend-state-and-command-mapping.md +42 -5
  10. package/docs/magicball-integration-doc-index.md +19 -5
  11. package/docs/magicball-integration-issue-tracker.md +15 -5
  12. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +13 -5
  13. package/docs/magicball-project-portfolio-contract.md +216 -0
  14. package/docs/magicball-sce-adaptation-guide.md +18 -4
  15. package/docs/magicball-ui-surface-checklist.md +25 -0
  16. package/docs/magicball-write-auth-adaptation-guide.md +3 -1
  17. package/docs/release-checklist.md +8 -0
  18. package/docs/releases/README.md +2 -0
  19. package/docs/releases/v3.6.58.md +27 -0
  20. package/docs/releases/v3.6.59.md +18 -0
  21. package/docs/zh/release-checklist.md +8 -0
  22. package/docs/zh/releases/README.md +2 -0
  23. package/docs/zh/releases/v3.6.58.md +27 -0
  24. package/docs/zh/releases/v3.6.59.md +18 -0
  25. package/lib/app/engineering-scaffold-service.js +154 -0
  26. package/lib/commands/app.js +442 -13
  27. package/lib/commands/project.js +105 -0
  28. package/lib/commands/scene.js +16 -0
  29. package/lib/project/portfolio-projection-service.js +389 -0
  30. package/lib/project/supervision-projection-service.js +329 -0
  31. package/lib/project/target-resolution-service.js +180 -0
  32. package/lib/scene/delivery-projection-service.js +650 -0
  33. package/package.json +6 -2
  34. package/scripts/magicball-engineering-contract-audit.js +347 -0
  35. package/scripts/magicball-project-contract-audit.js +254 -0
  36. 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.57",
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"