scene-capability-engine 3.6.57 → 3.6.58

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 (34) hide show
  1. package/CHANGELOG.md +12 -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 +1 -0
  19. package/docs/releases/v3.6.58.md +27 -0
  20. package/docs/zh/release-checklist.md +8 -0
  21. package/docs/zh/releases/README.md +1 -0
  22. package/docs/zh/releases/v3.6.58.md +27 -0
  23. package/lib/app/engineering-scaffold-service.js +154 -0
  24. package/lib/commands/app.js +442 -13
  25. package/lib/commands/project.js +105 -0
  26. package/lib/commands/scene.js +16 -0
  27. package/lib/project/portfolio-projection-service.js +389 -0
  28. package/lib/project/supervision-projection-service.js +329 -0
  29. package/lib/project/target-resolution-service.js +180 -0
  30. package/lib/scene/delivery-projection-service.js +650 -0
  31. package/package.json +6 -2
  32. package/scripts/magicball-engineering-contract-audit.js +347 -0
  33. package/scripts/magicball-project-contract-audit.js +254 -0
  34. package/template/.sce/README.md +2 -2
@@ -0,0 +1,329 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const TaskClaimer = require('../task/task-claimer');
5
+ const { SessionStore } = require('../runtime/session-store');
6
+ const { buildProjectPortfolioProjection } = require('./portfolio-projection-service');
7
+
8
+ const SPEC_GOVERNANCE_SCENE_INDEX = path.join('.sce', 'spec-governance', 'scene-index.json');
9
+ const STUDIO_REPORT_DIR = path.join('.sce', 'reports', 'studio');
10
+ const HANDOFF_REPORT_DIR = path.join('.sce', 'reports', 'handoff-runs');
11
+
12
+ function normalizeString(value) {
13
+ if (typeof value !== 'string') {
14
+ return '';
15
+ }
16
+ return value.trim();
17
+ }
18
+
19
+ function safeIsoAt(value) {
20
+ const normalized = normalizeString(value);
21
+ if (!normalized) {
22
+ return null;
23
+ }
24
+ const time = Date.parse(normalized);
25
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
26
+ }
27
+
28
+ function listObjectValues(value) {
29
+ if (!value || typeof value !== 'object') {
30
+ return [];
31
+ }
32
+ if (Array.isArray(value)) {
33
+ return value;
34
+ }
35
+ return Object.values(value);
36
+ }
37
+
38
+ async function readJsonIfExists(filePath, fileSystem = fs) {
39
+ if (!await fileSystem.pathExists(filePath)) {
40
+ return null;
41
+ }
42
+ try {
43
+ return await fileSystem.readJson(filePath);
44
+ } catch (_error) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ async function listJsonFiles(dirPath, fileSystem = fs) {
50
+ if (!await fileSystem.pathExists(dirPath)) {
51
+ return [];
52
+ }
53
+ const entries = await fileSystem.readdir(dirPath);
54
+ return entries
55
+ .filter((entry) => entry.toLowerCase().endsWith('.json'))
56
+ .map((entry) => path.join(dirPath, entry))
57
+ .sort((left, right) => left.localeCompare(right));
58
+ }
59
+
60
+ function collectLatestTimestamp(items = []) {
61
+ const timestamps = items
62
+ .map((item) => safeIsoAt(item && item.updatedAt))
63
+ .filter(Boolean)
64
+ .sort((left, right) => right.localeCompare(left));
65
+ return timestamps[0] || null;
66
+ }
67
+
68
+ function createCursor(projectId, latestEventAt, itemCount) {
69
+ const hash = crypto
70
+ .createHash('sha1')
71
+ .update(`${projectId}::${latestEventAt || 'none'}::${itemCount}`)
72
+ .digest('hex')
73
+ .slice(0, 12);
74
+ return `${latestEventAt || 'none'}::${itemCount}::${hash}`;
75
+ }
76
+
77
+ function resolveReportSceneId(report = {}) {
78
+ return normalizeString(report.scene_id)
79
+ || normalizeString(report?.domain_chain?.context?.scene_id)
80
+ || normalizeString(report?.domain_chain?.problem_contract?.scene_id);
81
+ }
82
+
83
+ function resolveReportSpecId(report = {}) {
84
+ return normalizeString(report.spec_id)
85
+ || normalizeString(report?.domain_chain?.spec_id);
86
+ }
87
+
88
+ function summarizeTaskCounts(tasks = []) {
89
+ const total = tasks.length;
90
+ const completed = tasks.filter((item) => item.status === 'completed').length;
91
+ const inProgress = tasks.filter((item) => item.status === 'in-progress').length;
92
+ const queued = tasks.filter((item) => item.status === 'queued').length;
93
+ const notStarted = tasks.filter((item) => item.status === 'not-started').length;
94
+ return {
95
+ total,
96
+ completed,
97
+ inProgress,
98
+ queued,
99
+ notStarted,
100
+ active: Math.max(total - completed, 0)
101
+ };
102
+ }
103
+
104
+ async function collectSpecTaskSummary(projectRoot, fileSystem = fs, taskClaimer = new TaskClaimer()) {
105
+ const specsRoot = path.join(projectRoot, '.sce', 'specs');
106
+ if (!await fileSystem.pathExists(specsRoot)) {
107
+ return {
108
+ activeSpecCount: 0,
109
+ activeTaskCount: 0
110
+ };
111
+ }
112
+
113
+ const entries = await fileSystem.readdir(specsRoot);
114
+ let activeSpecCount = 0;
115
+ let activeTaskCount = 0;
116
+
117
+ for (const entry of entries) {
118
+ const specRoot = path.join(specsRoot, entry);
119
+ try {
120
+ const stats = await fileSystem.stat(specRoot);
121
+ if (!stats.isDirectory()) {
122
+ continue;
123
+ }
124
+ const tasksPath = path.join(specRoot, 'tasks.md');
125
+ if (!await fileSystem.pathExists(tasksPath)) {
126
+ continue;
127
+ }
128
+ const tasks = await taskClaimer.parseTasks(tasksPath, { preferStatusMarkers: true });
129
+ const counts = summarizeTaskCounts(tasks);
130
+ if (counts.active > 0) {
131
+ activeSpecCount += 1;
132
+ activeTaskCount += counts.active;
133
+ }
134
+ } catch (_error) {
135
+ // Keep the projection resilient and skip unreadable spec entries.
136
+ }
137
+ }
138
+
139
+ return {
140
+ activeSpecCount,
141
+ activeTaskCount
142
+ };
143
+ }
144
+
145
+ async function buildActiveItems(projectRoot, sceneRecords = []) {
146
+ const items = [];
147
+ for (const record of sceneRecords) {
148
+ const sceneId = normalizeString(record && record.scene_id);
149
+ const sessionId = normalizeString(record && record.active_session_id);
150
+ if (!sceneId || !sessionId) {
151
+ continue;
152
+ }
153
+ const cycles = Array.isArray(record && record.cycles) ? record.cycles : [];
154
+ const activeCycle = cycles.find((item) => normalizeString(item && item.session_id) === sessionId) || {};
155
+ const updatedAt = safeIsoAt(activeCycle.started_at) || safeIsoAt(record.updated_at) || new Date().toISOString();
156
+ items.push({
157
+ id: `active:${sceneId}:${sessionId}`,
158
+ kind: 'active',
159
+ state: 'active',
160
+ sceneId,
161
+ eventId: sessionId,
162
+ updatedAt,
163
+ summary: `Scene ${sceneId} has active session ${sessionId}.`
164
+ });
165
+ }
166
+ return items;
167
+ }
168
+
169
+ async function buildRiskItems(projectRoot, fileSystem = fs) {
170
+ const payload = await readJsonIfExists(path.join(projectRoot, SPEC_GOVERNANCE_SCENE_INDEX), fileSystem);
171
+ const items = [];
172
+ for (const record of listObjectValues(payload && payload.scenes)) {
173
+ const sceneId = normalizeString(record && record.scene_id);
174
+ const staleSpecs = Number(record && record.stale_specs || 0);
175
+ if (!sceneId || staleSpecs <= 0) {
176
+ continue;
177
+ }
178
+ items.push({
179
+ id: `risk:${sceneId}:stale-specs`,
180
+ kind: 'risk',
181
+ state: 'at-risk',
182
+ reasonCode: 'project.stale_specs_present',
183
+ sceneId,
184
+ updatedAt: safeIsoAt(record && record.updated_at) || safeIsoAt(payload && payload.updated_at),
185
+ summary: `Scene ${sceneId} has ${staleSpecs} stale spec(s).`
186
+ });
187
+ }
188
+ return items;
189
+ }
190
+
191
+ async function buildHandoffItems(projectRoot, fileSystem = fs) {
192
+ const files = await listJsonFiles(path.join(projectRoot, HANDOFF_REPORT_DIR), fileSystem);
193
+ const items = [];
194
+ for (const filePath of files) {
195
+ const report = await readJsonIfExists(filePath, fileSystem);
196
+ if (!report || typeof report !== 'object') {
197
+ continue;
198
+ }
199
+ const sceneId = resolveReportSceneId(report) || null;
200
+ const specId = resolveReportSpecId(report) || null;
201
+ const sessionId = normalizeString(report.session_id) || path.basename(filePath, '.json');
202
+ const updatedAt = safeIsoAt(report.generated_at || report.completed_at || report.updated_at) || new Date().toISOString();
203
+ items.push({
204
+ id: `handoff:${sessionId}:${specId || sceneId || 'project'}`,
205
+ kind: 'handoff',
206
+ state: normalizeString(report.status) || 'handoff',
207
+ ...(sceneId ? { sceneId } : {}),
208
+ ...(specId ? { specId } : {}),
209
+ requestId: sessionId,
210
+ updatedAt,
211
+ summary: `Handoff session ${sessionId} is recorded for ${specId || sceneId || 'project scope'}.`
212
+ });
213
+ }
214
+ return items;
215
+ }
216
+
217
+ async function buildBlockedItems(projectRoot, fileSystem = fs) {
218
+ const files = await listJsonFiles(path.join(projectRoot, STUDIO_REPORT_DIR), fileSystem);
219
+ const items = [];
220
+ for (const filePath of files) {
221
+ const report = await readJsonIfExists(filePath, fileSystem);
222
+ if (!report || typeof report !== 'object' || report.passed !== false) {
223
+ continue;
224
+ }
225
+ const sceneId = resolveReportSceneId(report) || null;
226
+ const specId = resolveReportSpecId(report) || null;
227
+ const reportId = path.basename(filePath, '.json');
228
+ const updatedAt = safeIsoAt(report.completed_at || report.updated_at || report.started_at) || new Date().toISOString();
229
+ items.push({
230
+ id: `blocked:${reportId}`,
231
+ kind: 'blocked',
232
+ state: normalizeString(report.mode) || 'blocked',
233
+ ...(sceneId ? { sceneId } : {}),
234
+ ...(specId ? { specId } : {}),
235
+ eventId: reportId,
236
+ updatedAt,
237
+ summary: `Studio report ${reportId} is blocked.`,
238
+ ...(Array.isArray(report.steps) && report.steps.length > 0
239
+ ? {
240
+ reasonCode: normalizeString(report.steps.find((step) => /fail|error|block/i.test(normalizeString(step && step.status)))?.id) || undefined
241
+ }
242
+ : {})
243
+ });
244
+ }
245
+ return items;
246
+ }
247
+
248
+ async function resolveVisibleProject(projectId, dependencies = {}) {
249
+ const portfolio = await buildProjectPortfolioProjection({}, dependencies);
250
+ const record = (portfolio.projects || []).find((item) => item.projectId === projectId) || null;
251
+ return {
252
+ portfolio,
253
+ record
254
+ };
255
+ }
256
+
257
+ async function buildProjectSupervisionProjection(options = {}, dependencies = {}) {
258
+ const projectId = normalizeString(options.project);
259
+ if (!projectId) {
260
+ throw new Error('--project is required');
261
+ }
262
+
263
+ const fileSystem = dependencies.fileSystem || fs;
264
+ const taskClaimer = dependencies.taskClaimer || new TaskClaimer();
265
+ const { record } = await resolveVisibleProject(projectId, dependencies);
266
+ if (!record) {
267
+ throw new Error(`project not visible: ${projectId}`);
268
+ }
269
+
270
+ if (record.availability === 'inaccessible') {
271
+ const generatedAt = new Date().toISOString();
272
+ return {
273
+ generatedAt,
274
+ projectId,
275
+ cursor: createCursor(projectId, generatedAt, 0),
276
+ summary: {
277
+ blockedCount: 0,
278
+ handoffCount: 0,
279
+ riskCount: 0
280
+ },
281
+ items: [],
282
+ partial: true,
283
+ partialReasons: ['project_inaccessible']
284
+ };
285
+ }
286
+
287
+ const projectRoot = record.projectRoot;
288
+ const sessionStore = new SessionStore(projectRoot, null, {
289
+ fileSystem,
290
+ env: dependencies.env || process.env,
291
+ sqliteModule: dependencies.sqliteModule
292
+ });
293
+ const sceneRecords = await sessionStore.listSceneRecords();
294
+ const activeItems = await buildActiveItems(projectRoot, sceneRecords);
295
+ const riskItems = await buildRiskItems(projectRoot, fileSystem);
296
+ const handoffItems = await buildHandoffItems(projectRoot, fileSystem);
297
+ const blockedItems = await buildBlockedItems(projectRoot, fileSystem);
298
+ const taskSummary = await collectSpecTaskSummary(projectRoot, fileSystem, taskClaimer);
299
+ const items = [
300
+ ...blockedItems,
301
+ ...handoffItems,
302
+ ...riskItems,
303
+ ...activeItems
304
+ ].sort((left, right) => `${right.updatedAt || ''}`.localeCompare(`${left.updatedAt || ''}`));
305
+ const latestEventAt = collectLatestTimestamp(items);
306
+ const generatedAt = new Date().toISOString();
307
+
308
+ return {
309
+ generatedAt,
310
+ projectId,
311
+ cursor: createCursor(projectId, latestEventAt || generatedAt, items.length),
312
+ summary: {
313
+ blockedCount: blockedItems.length,
314
+ handoffCount: handoffItems.length,
315
+ riskCount: riskItems.length,
316
+ activeSceneCount: activeItems.length,
317
+ activeSpecCount: taskSummary.activeSpecCount,
318
+ activeTaskCount: taskSummary.activeTaskCount,
319
+ ...(latestEventAt ? { latestEventAt } : {})
320
+ },
321
+ items,
322
+ partial: record.partial === true,
323
+ partialReasons: Array.isArray(record.partialReasons) ? record.partialReasons : []
324
+ };
325
+ }
326
+
327
+ module.exports = {
328
+ buildProjectSupervisionProjection
329
+ };
@@ -0,0 +1,180 @@
1
+ const path = require('path');
2
+ const { buildProjectPortfolioProjection } = require('./portfolio-projection-service');
3
+
4
+ function normalizeString(value) {
5
+ if (typeof value !== 'string') {
6
+ return '';
7
+ }
8
+ return value.trim();
9
+ }
10
+
11
+ function normalizeText(value) {
12
+ return normalizeString(value).toLowerCase();
13
+ }
14
+
15
+ function collectProjectAliases(project = {}) {
16
+ const aliases = [];
17
+ const fields = [
18
+ project.projectId,
19
+ project.workspaceId,
20
+ project.projectName,
21
+ project.appKey,
22
+ project.projectRoot ? path.basename(project.projectRoot) : null
23
+ ];
24
+ for (const field of fields) {
25
+ const normalized = normalizeText(field);
26
+ if (!normalized || aliases.includes(normalized)) {
27
+ continue;
28
+ }
29
+ aliases.push(normalized);
30
+ }
31
+ return aliases;
32
+ }
33
+
34
+ function scoreProjectMatch(requestText, project = {}) {
35
+ const normalizedRequest = normalizeText(requestText);
36
+ if (!normalizedRequest) {
37
+ return null;
38
+ }
39
+
40
+ const aliases = collectProjectAliases(project);
41
+ let bestScore = 0;
42
+ let reasonCode = 'target.request_match';
43
+
44
+ for (const alias of aliases) {
45
+ if (normalizedRequest === alias) {
46
+ bestScore = Math.max(bestScore, 1);
47
+ reasonCode = 'target.alias_exact_match';
48
+ continue;
49
+ }
50
+ if (normalizedRequest.includes(alias) && alias.length >= 2) {
51
+ bestScore = Math.max(bestScore, 0.96);
52
+ reasonCode = 'target.alias_contained_match';
53
+ continue;
54
+ }
55
+ if (alias.includes(normalizedRequest) && normalizedRequest.length >= 3) {
56
+ bestScore = Math.max(bestScore, 0.84);
57
+ reasonCode = 'target.alias_prefix_match';
58
+ }
59
+ }
60
+
61
+ if (bestScore <= 0) {
62
+ return null;
63
+ }
64
+
65
+ return {
66
+ projectId: project.projectId,
67
+ workspaceId: project.workspaceId || null,
68
+ projectName: project.projectName || null,
69
+ appKey: project.appKey || null,
70
+ confidence: Number(bestScore.toFixed(2)),
71
+ reasonCode
72
+ };
73
+ }
74
+
75
+ function buildResolutionCallerContext(options = {}, portfolio = {}) {
76
+ const portfolioContext = portfolio && portfolio.callerContext && typeof portfolio.callerContext === 'object'
77
+ ? portfolio.callerContext
78
+ : {};
79
+ const explicitCurrentProject = normalizeString(options.currentProject);
80
+ const explicitDeviceId = normalizeString(options.device);
81
+ const explicitToolInstanceId = normalizeString(options.toolInstanceId);
82
+
83
+ return {
84
+ ...(explicitCurrentProject || portfolioContext.projectId
85
+ ? { currentProjectId: explicitCurrentProject || portfolioContext.projectId }
86
+ : {}),
87
+ ...(portfolioContext.workspaceId ? { workspaceId: portfolioContext.workspaceId } : {}),
88
+ ...(explicitDeviceId || portfolioContext.deviceId
89
+ ? { deviceId: explicitDeviceId || portfolioContext.deviceId }
90
+ : {}),
91
+ ...(explicitToolInstanceId ? { toolInstanceId: explicitToolInstanceId } : {})
92
+ };
93
+ }
94
+
95
+ async function resolveProjectTarget(options = {}, dependencies = {}) {
96
+ const requestText = normalizeString(options.request);
97
+ const portfolio = await buildProjectPortfolioProjection({
98
+ workspace: options.workspace
99
+ }, dependencies);
100
+ const callerContext = buildResolutionCallerContext(options, portfolio);
101
+ const currentProjectId = normalizeString(callerContext.currentProjectId);
102
+ const visibleProjects = Array.isArray(portfolio.projects) ? portfolio.projects : [];
103
+ const currentProject = visibleProjects.find((project) => project.projectId === currentProjectId) || null;
104
+
105
+ if (!requestText) {
106
+ if (currentProject) {
107
+ return {
108
+ resolvedAt: new Date().toISOString(),
109
+ callerContext,
110
+ status: 'current-project',
111
+ currentProjectId,
112
+ resolvedProjectId: currentProjectId,
113
+ confidence: 1,
114
+ reasonCode: 'target.current_project'
115
+ };
116
+ }
117
+ return {
118
+ resolvedAt: new Date().toISOString(),
119
+ callerContext,
120
+ status: 'unresolved',
121
+ ...(currentProjectId ? { currentProjectId } : {}),
122
+ reasonCode: currentProjectId
123
+ ? 'target.current_project_unavailable'
124
+ : 'target.no_request_or_current_project'
125
+ };
126
+ }
127
+
128
+ const matches = visibleProjects
129
+ .map((project) => scoreProjectMatch(requestText, project))
130
+ .filter(Boolean)
131
+ .sort((left, right) => {
132
+ if (right.confidence !== left.confidence) {
133
+ return right.confidence - left.confidence;
134
+ }
135
+ return `${left.projectId}`.localeCompare(`${right.projectId}`);
136
+ });
137
+
138
+ if (matches.length === 0) {
139
+ return {
140
+ resolvedAt: new Date().toISOString(),
141
+ callerContext,
142
+ status: 'unresolved',
143
+ ...(currentProjectId ? { currentProjectId } : {}),
144
+ reasonCode: 'target.no_match'
145
+ };
146
+ }
147
+
148
+ const best = matches[0];
149
+ const second = matches[1];
150
+ const ambiguous = second && Math.abs(best.confidence - second.confidence) < 0.05;
151
+
152
+ if (ambiguous) {
153
+ return {
154
+ resolvedAt: new Date().toISOString(),
155
+ callerContext,
156
+ status: 'ambiguous',
157
+ ...(currentProjectId ? { currentProjectId } : {}),
158
+ confidence: best.confidence,
159
+ reasonCode: 'target.ambiguous',
160
+ candidates: matches.slice(0, 5)
161
+ };
162
+ }
163
+
164
+ return {
165
+ resolvedAt: new Date().toISOString(),
166
+ callerContext,
167
+ status: currentProjectId && best.projectId === currentProjectId
168
+ ? 'current-project'
169
+ : 'resolved-other-project',
170
+ ...(currentProjectId ? { currentProjectId } : {}),
171
+ resolvedProjectId: best.projectId,
172
+ confidence: best.confidence,
173
+ reasonCode: best.reasonCode,
174
+ candidates: [best]
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ resolveProjectTarget
180
+ };