scene-capability-engine 3.6.56 → 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.
- package/CHANGELOG.md +21 -0
- package/README.md +6 -3
- package/README.zh.md +6 -3
- package/bin/scene-capability-engine.js +2 -0
- package/docs/autonomous-control-guide.md +2 -0
- package/docs/command-reference.md +76 -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.57.md +19 -0
- package/docs/releases/v3.6.58.md +27 -0
- package/docs/spec-workflow.md +2 -0
- package/docs/zh/release-checklist.md +8 -0
- package/docs/zh/releases/README.md +2 -0
- package/docs/zh/releases/v3.6.57.md +19 -0
- package/docs/zh/releases/v3.6.58.md +27 -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/commands/spec-gate.js +57 -6
- package/lib/commands/spec-pipeline.js +13 -4
- package/lib/problem/project-problem-projection.js +43 -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
|
@@ -15,9 +15,11 @@ const { ResultEmitter } = require('../spec-gate/result-emitter');
|
|
|
15
15
|
const { SessionStore } = require('../runtime/session-store');
|
|
16
16
|
const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
|
|
17
17
|
const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
|
|
18
|
+
const { assessComplexityStrategy } = require('../spec/complexity-strategy');
|
|
18
19
|
|
|
19
20
|
async function runSpecGate(options = {}, dependencies = {}) {
|
|
20
21
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
22
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
21
23
|
const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
|
|
22
24
|
const specTargets = parseSpecTargets(options);
|
|
23
25
|
|
|
@@ -43,7 +45,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
43
45
|
})
|
|
44
46
|
}, {
|
|
45
47
|
projectPath,
|
|
46
|
-
fileSystem
|
|
48
|
+
fileSystem,
|
|
47
49
|
sessionStore
|
|
48
50
|
});
|
|
49
51
|
}
|
|
@@ -51,7 +53,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
51
53
|
const specId = specTargets[0];
|
|
52
54
|
|
|
53
55
|
const specPath = path.join(projectPath, '.sce', 'specs', specId);
|
|
54
|
-
if (!await
|
|
56
|
+
if (!await fileSystem.pathExists(specPath)) {
|
|
55
57
|
throw new Error(`Spec not found: ${specId}`);
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -60,7 +62,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
60
62
|
allowNoScene: false
|
|
61
63
|
}, {
|
|
62
64
|
projectPath,
|
|
63
|
-
fileSystem
|
|
65
|
+
fileSystem,
|
|
64
66
|
sessionStore
|
|
65
67
|
});
|
|
66
68
|
const linked = await sessionStore.startSpecSession({
|
|
@@ -83,13 +85,22 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
83
85
|
policy
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
const
|
|
88
|
+
const gateResult = await engine.evaluate({ specId });
|
|
89
|
+
const result = {
|
|
90
|
+
...gateResult,
|
|
91
|
+
strategy_assessment: await buildStrategyAssessment(specId, {
|
|
92
|
+
projectPath,
|
|
93
|
+
fileSystem,
|
|
94
|
+
strategyAssessor: dependencies.strategyAssessor
|
|
95
|
+
})
|
|
96
|
+
};
|
|
87
97
|
const emitter = dependencies.emitter || new ResultEmitter(projectPath);
|
|
88
98
|
const emitted = await emitter.emit(result, {
|
|
89
99
|
json: options.json,
|
|
90
100
|
out: options.out,
|
|
91
101
|
silent: options.silent
|
|
92
102
|
});
|
|
103
|
+
emitStrategyAdvisory(result.strategy_assessment, options);
|
|
93
104
|
|
|
94
105
|
const decisionStatus = result.decision === 'no-go' ? 'failed' : 'completed';
|
|
95
106
|
await sessionStore.completeSpecSession({
|
|
@@ -101,7 +112,8 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
101
112
|
spec: specId,
|
|
102
113
|
decision: result.decision,
|
|
103
114
|
score: result.score,
|
|
104
|
-
report_path: emitted.outputPath || null
|
|
115
|
+
report_path: emitted.outputPath || null,
|
|
116
|
+
strategy_decision: result.strategy_assessment ? result.strategy_assessment.decision : null
|
|
105
117
|
}
|
|
106
118
|
});
|
|
107
119
|
|
|
@@ -132,6 +144,43 @@ async function runSpecGate(options = {}, dependencies = {}) {
|
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
async function buildStrategyAssessment(specId, dependencies = {}) {
|
|
148
|
+
const assess = dependencies.strategyAssessor || assessComplexityStrategy;
|
|
149
|
+
try {
|
|
150
|
+
return await assess({
|
|
151
|
+
spec: specId
|
|
152
|
+
}, {
|
|
153
|
+
projectPath: dependencies.projectPath,
|
|
154
|
+
fileSystem: dependencies.fileSystem
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
decision: 'assessment-unavailable',
|
|
159
|
+
decision_reason: error.message,
|
|
160
|
+
advisory_only: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function emitStrategyAdvisory(strategyAssessment, options = {}) {
|
|
166
|
+
if (!strategyAssessment || options.json || options.silent) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!['multi-spec-program', 'research-program'].includes(strategyAssessment.decision)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(chalk.yellow('⚠ Strategy Advisory'));
|
|
176
|
+
console.log(` ${strategyAssessment.decision}: ${strategyAssessment.decision_reason}`);
|
|
177
|
+
if (Array.isArray(strategyAssessment.next_actions)) {
|
|
178
|
+
strategyAssessment.next_actions.forEach(action => {
|
|
179
|
+
console.log(` - ${action}`);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
135
184
|
async function generateSpecGatePolicyTemplate(options = {}, dependencies = {}) {
|
|
136
185
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
137
186
|
const loader = dependencies.policyLoader || new PolicyLoader(projectPath);
|
|
@@ -220,5 +269,7 @@ module.exports = {
|
|
|
220
269
|
registerSpecGateCommand,
|
|
221
270
|
runSpecGate,
|
|
222
271
|
generateSpecGatePolicyTemplate,
|
|
223
|
-
_parseSpecTargets
|
|
272
|
+
_parseSpecTargets,
|
|
273
|
+
buildStrategyAssessment,
|
|
274
|
+
emitStrategyAdvisory
|
|
224
275
|
};
|
|
@@ -13,9 +13,11 @@ const { createDefaultStageAdapters } = require('../spec/pipeline/stage-adapters'
|
|
|
13
13
|
const { SessionStore } = require('../runtime/session-store');
|
|
14
14
|
const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
|
|
15
15
|
const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
|
|
16
|
+
const { buildStrategyAssessment, emitStrategyAdvisory } = require('./spec-gate');
|
|
16
17
|
|
|
17
18
|
async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
18
19
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
20
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
19
21
|
const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
|
|
20
22
|
const specTargets = parseSpecTargets(options);
|
|
21
23
|
if (specTargets.length === 0) {
|
|
@@ -40,7 +42,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
40
42
|
})
|
|
41
43
|
}, {
|
|
42
44
|
projectPath,
|
|
43
|
-
fileSystem
|
|
45
|
+
fileSystem,
|
|
44
46
|
sessionStore
|
|
45
47
|
});
|
|
46
48
|
}
|
|
@@ -48,7 +50,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
48
50
|
const specId = specTargets[0];
|
|
49
51
|
|
|
50
52
|
const specPath = path.join(projectPath, '.sce', 'specs', specId);
|
|
51
|
-
if (!await
|
|
53
|
+
if (!await fileSystem.pathExists(specPath)) {
|
|
52
54
|
throw new Error(`Spec not found: ${specId}`);
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -57,7 +59,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
57
59
|
allowNoScene: false
|
|
58
60
|
}, {
|
|
59
61
|
projectPath,
|
|
60
|
-
fileSystem
|
|
62
|
+
fileSystem,
|
|
61
63
|
sessionStore
|
|
62
64
|
});
|
|
63
65
|
|
|
@@ -130,6 +132,11 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
130
132
|
status: execution.status,
|
|
131
133
|
stage_results: execution.stageResults,
|
|
132
134
|
failure: execution.failure,
|
|
135
|
+
strategy_assessment: await buildStrategyAssessment(specId, {
|
|
136
|
+
projectPath,
|
|
137
|
+
fileSystem,
|
|
138
|
+
strategyAssessor: dependencies.strategyAssessor
|
|
139
|
+
}),
|
|
133
140
|
next_actions: buildNextActions(execution),
|
|
134
141
|
state_file: path.relative(projectPath, stateStore.getRunPath(specId, state.run_id)),
|
|
135
142
|
scene_session: sceneBinding
|
|
@@ -156,7 +163,8 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
156
163
|
spec: specId,
|
|
157
164
|
run_id: state.run_id,
|
|
158
165
|
pipeline_status: execution.status,
|
|
159
|
-
failure: execution.failure || null
|
|
166
|
+
failure: execution.failure || null,
|
|
167
|
+
strategy_decision: result.strategy_assessment ? result.strategy_assessment.decision : null
|
|
160
168
|
}
|
|
161
169
|
});
|
|
162
170
|
}
|
|
@@ -174,6 +182,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
|
|
|
174
182
|
console.log(JSON.stringify(result, null, 2));
|
|
175
183
|
} else {
|
|
176
184
|
printResult(result);
|
|
185
|
+
emitStrategyAdvisory(result.strategy_assessment, options);
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
return result;
|
|
@@ -82,6 +82,31 @@ function toRelativePosix(projectPath, absolutePath) {
|
|
|
82
82
|
return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function sortKeysDeep(value) {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.map((item) => sortKeysDeep(item));
|
|
88
|
+
}
|
|
89
|
+
if (!value || typeof value !== 'object') {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
const sorted = {};
|
|
93
|
+
Object.keys(value).sort().forEach((key) => {
|
|
94
|
+
sorted[key] = sortKeysDeep(value[key]);
|
|
95
|
+
});
|
|
96
|
+
return sorted;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toComparableProjection(payload) {
|
|
100
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const clone = {
|
|
104
|
+
...payload
|
|
105
|
+
};
|
|
106
|
+
delete clone.generated_at;
|
|
107
|
+
return sortKeysDeep(clone);
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
async function buildProjectSharedProblemProjection(projectPath = process.cwd(), options = {}, dependencies = {}) {
|
|
86
111
|
const fileSystem = dependencies.fileSystem || fs;
|
|
87
112
|
const studioIntakePolicy = dependencies.studioIntakePolicy || await loadStudioIntakePolicy(projectPath, fileSystem);
|
|
@@ -212,6 +237,24 @@ async function syncProjectSharedProblemProjection(projectPath = process.cwd(), o
|
|
|
212
237
|
...dependencies,
|
|
213
238
|
problemClosurePolicyBundle: closurePolicy
|
|
214
239
|
});
|
|
240
|
+
const existing = await readJsonSafe(absolutePath, fileSystem);
|
|
241
|
+
const existingComparable = toComparableProjection(existing);
|
|
242
|
+
const nextComparable = toComparableProjection(payload);
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
existingComparable
|
|
246
|
+
&& nextComparable
|
|
247
|
+
&& JSON.stringify(existingComparable) === JSON.stringify(nextComparable)
|
|
248
|
+
) {
|
|
249
|
+
return {
|
|
250
|
+
mode: 'project-problem-projection-sync',
|
|
251
|
+
enabled: true,
|
|
252
|
+
file: absolutePath,
|
|
253
|
+
scope: projectionConfig.scope,
|
|
254
|
+
total_entries: Number(payload.summary.total_entries || payload.entries.length || 0),
|
|
255
|
+
refreshed: false
|
|
256
|
+
};
|
|
257
|
+
}
|
|
215
258
|
|
|
216
259
|
await fileSystem.ensureDir(path.dirname(absolutePath));
|
|
217
260
|
await fileSystem.writeJson(absolutePath, payload, { spaces: 2 });
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const WorkspaceStateManager = require('../workspace/multi/workspace-state-manager');
|
|
5
|
+
const { SessionStore } = require('../runtime/session-store');
|
|
6
|
+
const { getCurrentDeviceProfile } = require('../device/current-device');
|
|
7
|
+
|
|
8
|
+
function normalizeString(value) {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizePath(value) {
|
|
16
|
+
return normalizeString(value).replace(/\\/g, '/');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildWorkspaceProjectId(workspaceId) {
|
|
20
|
+
return `workspace:${workspaceId}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildLocalProjectId(projectRoot) {
|
|
24
|
+
const hash = crypto
|
|
25
|
+
.createHash('sha1')
|
|
26
|
+
.update(normalizePath(projectRoot))
|
|
27
|
+
.digest('hex')
|
|
28
|
+
.slice(0, 12);
|
|
29
|
+
return `local:${hash}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeIsoAt(value) {
|
|
33
|
+
const normalized = normalizeString(value);
|
|
34
|
+
if (!normalized) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const time = Date.parse(normalized);
|
|
38
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectRecordActivityTimestamps(sceneRecords = []) {
|
|
42
|
+
const timestamps = [];
|
|
43
|
+
for (const record of sceneRecords) {
|
|
44
|
+
const updatedAt = safeIsoAt(record && record.updated_at);
|
|
45
|
+
if (updatedAt) {
|
|
46
|
+
timestamps.push(updatedAt);
|
|
47
|
+
}
|
|
48
|
+
const cycles = Array.isArray(record && record.cycles) ? record.cycles : [];
|
|
49
|
+
for (const cycle of cycles) {
|
|
50
|
+
const startedAt = safeIsoAt(cycle && cycle.started_at);
|
|
51
|
+
const completedAt = safeIsoAt(cycle && cycle.completed_at);
|
|
52
|
+
if (startedAt) {
|
|
53
|
+
timestamps.push(startedAt);
|
|
54
|
+
}
|
|
55
|
+
if (completedAt) {
|
|
56
|
+
timestamps.push(completedAt);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
timestamps.sort((left, right) => right.localeCompare(left));
|
|
61
|
+
return timestamps[0] || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function isValidSceProjectRoot(projectRoot, fileSystem = fs) {
|
|
65
|
+
const sceRoot = path.join(projectRoot, '.sce');
|
|
66
|
+
if (!await fileSystem.pathExists(sceRoot)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const stats = await fileSystem.stat(sceRoot);
|
|
71
|
+
return stats.isDirectory();
|
|
72
|
+
} catch (_error) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function countSpecDirectories(projectRoot, fileSystem = fs) {
|
|
78
|
+
const specsRoot = path.join(projectRoot, '.sce', 'specs');
|
|
79
|
+
if (!await fileSystem.pathExists(specsRoot)) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const entries = await fileSystem.readdir(specsRoot);
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
try {
|
|
86
|
+
const stats = await fileSystem.stat(path.join(specsRoot, entry));
|
|
87
|
+
if (stats.isDirectory()) {
|
|
88
|
+
count += 1;
|
|
89
|
+
}
|
|
90
|
+
} catch (_error) {
|
|
91
|
+
// Ignore unreadable spec directories and keep reporting partial state elsewhere.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deriveAvailability({ accessible, partial }) {
|
|
98
|
+
if (!accessible) {
|
|
99
|
+
return 'inaccessible';
|
|
100
|
+
}
|
|
101
|
+
if (partial) {
|
|
102
|
+
return 'degraded';
|
|
103
|
+
}
|
|
104
|
+
return 'accessible';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deriveReadiness({ accessible, partial, sceneCount, specCount }) {
|
|
108
|
+
if (!accessible) {
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
if (partial) {
|
|
112
|
+
return 'partial';
|
|
113
|
+
}
|
|
114
|
+
if (sceneCount === 0 && specCount === 0) {
|
|
115
|
+
return 'pending';
|
|
116
|
+
}
|
|
117
|
+
return 'ready';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function deriveStatus({ accessible, isCurrentProject, activeSessionCount }) {
|
|
121
|
+
if (!accessible) {
|
|
122
|
+
return 'inaccessible';
|
|
123
|
+
}
|
|
124
|
+
if (isCurrentProject) {
|
|
125
|
+
return 'active';
|
|
126
|
+
}
|
|
127
|
+
if (activeSessionCount > 0) {
|
|
128
|
+
return 'background';
|
|
129
|
+
}
|
|
130
|
+
return 'idle';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readSceneRecords(projectRoot, dependencies = {}) {
|
|
134
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
135
|
+
const env = dependencies.env || process.env;
|
|
136
|
+
const sqliteModule = dependencies.sqliteModule;
|
|
137
|
+
const sessionStore = new SessionStore(projectRoot, null, {
|
|
138
|
+
fileSystem,
|
|
139
|
+
env,
|
|
140
|
+
sqliteModule
|
|
141
|
+
});
|
|
142
|
+
return sessionStore.listSceneRecords();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function buildRegisteredWorkspaceRecord(workspace, context = {}, dependencies = {}) {
|
|
146
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
147
|
+
const workspaceId = normalizeString(workspace && workspace.name);
|
|
148
|
+
const projectRoot = normalizePath(workspace && workspace.path);
|
|
149
|
+
const partialReasons = [];
|
|
150
|
+
const projectId = buildWorkspaceProjectId(workspaceId);
|
|
151
|
+
let accessible = false;
|
|
152
|
+
let partial = false;
|
|
153
|
+
let sceneRecords = [];
|
|
154
|
+
let specCount = 0;
|
|
155
|
+
|
|
156
|
+
const sceProject = await isValidSceProjectRoot(projectRoot, fileSystem);
|
|
157
|
+
if (!sceProject) {
|
|
158
|
+
partial = true;
|
|
159
|
+
partialReasons.push('workspace_root_unavailable');
|
|
160
|
+
} else {
|
|
161
|
+
accessible = true;
|
|
162
|
+
try {
|
|
163
|
+
sceneRecords = await readSceneRecords(projectRoot, dependencies);
|
|
164
|
+
} catch (_error) {
|
|
165
|
+
partial = true;
|
|
166
|
+
partialReasons.push('scene_records_unavailable');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
specCount = await countSpecDirectories(projectRoot, fileSystem);
|
|
170
|
+
} catch (_error) {
|
|
171
|
+
partial = true;
|
|
172
|
+
partialReasons.push('spec_inventory_unavailable');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sceneCount = Array.isArray(sceneRecords) ? sceneRecords.length : 0;
|
|
177
|
+
const activeSessionCount = Array.isArray(sceneRecords)
|
|
178
|
+
? sceneRecords.filter((record) => normalizeString(record && record.active_session_id)).length
|
|
179
|
+
: 0;
|
|
180
|
+
const lastActivityAt = collectRecordActivityTimestamps(sceneRecords);
|
|
181
|
+
const uniquePartialReasons = Array.from(new Set(partialReasons));
|
|
182
|
+
const isCurrentProject = context.projectId === projectId;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
projectId,
|
|
186
|
+
workspaceId,
|
|
187
|
+
projectRoot,
|
|
188
|
+
projectName: path.basename(projectRoot) || workspaceId,
|
|
189
|
+
provenance: 'registered',
|
|
190
|
+
readiness: deriveReadiness({
|
|
191
|
+
accessible,
|
|
192
|
+
partial,
|
|
193
|
+
sceneCount,
|
|
194
|
+
specCount
|
|
195
|
+
}),
|
|
196
|
+
status: deriveStatus({
|
|
197
|
+
accessible,
|
|
198
|
+
isCurrentProject,
|
|
199
|
+
activeSessionCount
|
|
200
|
+
}),
|
|
201
|
+
availability: deriveAvailability({
|
|
202
|
+
accessible,
|
|
203
|
+
partial
|
|
204
|
+
}),
|
|
205
|
+
activeSessionCount,
|
|
206
|
+
...(lastActivityAt ? { lastActivityAt } : {}),
|
|
207
|
+
summary: {
|
|
208
|
+
sceneCount,
|
|
209
|
+
specCount
|
|
210
|
+
},
|
|
211
|
+
partial,
|
|
212
|
+
partialReasons: uniquePartialReasons
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function buildLocalProjectRecord(projectRoot, context = {}, dependencies = {}) {
|
|
217
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
218
|
+
const normalizedRoot = normalizePath(projectRoot);
|
|
219
|
+
const projectId = buildLocalProjectId(normalizedRoot);
|
|
220
|
+
let sceneRecords = [];
|
|
221
|
+
let specCount = 0;
|
|
222
|
+
const partialReasons = ['unregistered_project'];
|
|
223
|
+
let partial = true;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
sceneRecords = await readSceneRecords(normalizedRoot, dependencies);
|
|
227
|
+
} catch (_error) {
|
|
228
|
+
partialReasons.push('scene_records_unavailable');
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
specCount = await countSpecDirectories(normalizedRoot, fileSystem);
|
|
232
|
+
} catch (_error) {
|
|
233
|
+
partialReasons.push('spec_inventory_unavailable');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sceneCount = Array.isArray(sceneRecords) ? sceneRecords.length : 0;
|
|
237
|
+
const activeSessionCount = Array.isArray(sceneRecords)
|
|
238
|
+
? sceneRecords.filter((record) => normalizeString(record && record.active_session_id)).length
|
|
239
|
+
: 0;
|
|
240
|
+
const lastActivityAt = collectRecordActivityTimestamps(sceneRecords);
|
|
241
|
+
const uniquePartialReasons = Array.from(new Set(partialReasons));
|
|
242
|
+
const isCurrentProject = context.projectId === projectId;
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
projectId,
|
|
246
|
+
projectRoot: normalizedRoot,
|
|
247
|
+
projectName: path.basename(normalizedRoot) || 'local-project',
|
|
248
|
+
provenance: 'discovered',
|
|
249
|
+
readiness: deriveReadiness({
|
|
250
|
+
accessible: true,
|
|
251
|
+
partial,
|
|
252
|
+
sceneCount,
|
|
253
|
+
specCount
|
|
254
|
+
}),
|
|
255
|
+
status: deriveStatus({
|
|
256
|
+
accessible: true,
|
|
257
|
+
isCurrentProject,
|
|
258
|
+
activeSessionCount
|
|
259
|
+
}),
|
|
260
|
+
availability: deriveAvailability({
|
|
261
|
+
accessible: true,
|
|
262
|
+
partial
|
|
263
|
+
}),
|
|
264
|
+
activeSessionCount,
|
|
265
|
+
...(lastActivityAt ? { lastActivityAt } : {}),
|
|
266
|
+
summary: {
|
|
267
|
+
sceneCount,
|
|
268
|
+
specCount
|
|
269
|
+
},
|
|
270
|
+
partial,
|
|
271
|
+
partialReasons: uniquePartialReasons
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function resolveCurrentProjectContext(options = {}, dependencies = {}) {
|
|
276
|
+
const stateManager = dependencies.stateManager || new WorkspaceStateManager(dependencies.workspaceStatePath);
|
|
277
|
+
const currentDir = normalizePath(dependencies.projectPath || process.cwd());
|
|
278
|
+
const explicitWorkspace = normalizeString(options.workspace);
|
|
279
|
+
|
|
280
|
+
if (explicitWorkspace) {
|
|
281
|
+
const workspace = await stateManager.getWorkspace(explicitWorkspace);
|
|
282
|
+
if (!workspace) {
|
|
283
|
+
throw new Error(`workspace not found: ${explicitWorkspace}`);
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
workspaceId: workspace.name,
|
|
287
|
+
projectId: buildWorkspaceProjectId(workspace.name),
|
|
288
|
+
projectRoot: normalizePath(workspace.path),
|
|
289
|
+
source: 'explicit-workspace'
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const matchedWorkspace = await stateManager.findWorkspaceByPath(currentDir);
|
|
294
|
+
if (matchedWorkspace) {
|
|
295
|
+
return {
|
|
296
|
+
workspaceId: matchedWorkspace.name,
|
|
297
|
+
projectId: buildWorkspaceProjectId(matchedWorkspace.name),
|
|
298
|
+
projectRoot: normalizePath(matchedWorkspace.path),
|
|
299
|
+
source: 'current-directory'
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const activeWorkspace = await stateManager.getActiveWorkspace();
|
|
304
|
+
if (activeWorkspace) {
|
|
305
|
+
return {
|
|
306
|
+
workspaceId: activeWorkspace.name,
|
|
307
|
+
projectId: buildWorkspaceProjectId(activeWorkspace.name),
|
|
308
|
+
projectRoot: normalizePath(activeWorkspace.path),
|
|
309
|
+
source: 'active-workspace'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (await isValidSceProjectRoot(currentDir, dependencies.fileSystem || fs)) {
|
|
314
|
+
return {
|
|
315
|
+
workspaceId: null,
|
|
316
|
+
projectId: buildLocalProjectId(currentDir),
|
|
317
|
+
projectRoot: currentDir,
|
|
318
|
+
source: 'local-project'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
workspaceId: null,
|
|
324
|
+
projectId: null,
|
|
325
|
+
projectRoot: null,
|
|
326
|
+
source: 'none'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function buildProjectPortfolioProjection(options = {}, dependencies = {}) {
|
|
331
|
+
const stateManager = dependencies.stateManager || new WorkspaceStateManager(dependencies.workspaceStatePath);
|
|
332
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
333
|
+
const currentContext = await resolveCurrentProjectContext(options, {
|
|
334
|
+
...dependencies,
|
|
335
|
+
stateManager
|
|
336
|
+
});
|
|
337
|
+
let currentDevice = null;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
currentDevice = await getCurrentDeviceProfile(dependencies.projectPath || process.cwd(), {
|
|
341
|
+
fileSystem,
|
|
342
|
+
persistIfMissing: false
|
|
343
|
+
});
|
|
344
|
+
} catch (_error) {
|
|
345
|
+
currentDevice = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const workspaces = await stateManager.listWorkspaces();
|
|
349
|
+
const records = [];
|
|
350
|
+
for (const workspace of workspaces) {
|
|
351
|
+
records.push(await buildRegisteredWorkspaceRecord(workspace, currentContext, dependencies));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const localRoot = normalizeString(currentContext.projectRoot);
|
|
355
|
+
const currentDirWorkspace = localRoot ? await stateManager.findWorkspaceByPath(localRoot) : null;
|
|
356
|
+
const shouldAddLocalProject = currentContext.source === 'local-project'
|
|
357
|
+
&& localRoot
|
|
358
|
+
&& !currentDirWorkspace;
|
|
359
|
+
if (shouldAddLocalProject) {
|
|
360
|
+
records.push(await buildLocalProjectRecord(localRoot, currentContext, dependencies));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
records.sort((left, right) => {
|
|
364
|
+
const leftCurrent = left.projectId === currentContext.projectId ? 0 : 1;
|
|
365
|
+
const rightCurrent = right.projectId === currentContext.projectId ? 0 : 1;
|
|
366
|
+
if (leftCurrent !== rightCurrent) {
|
|
367
|
+
return leftCurrent - rightCurrent;
|
|
368
|
+
}
|
|
369
|
+
return `${left.projectName || left.projectId}`.localeCompare(`${right.projectName || right.projectId}`);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
generatedAt: new Date().toISOString(),
|
|
374
|
+
callerContext: {
|
|
375
|
+
...(currentContext.workspaceId ? { workspaceId: currentContext.workspaceId } : {}),
|
|
376
|
+
...(currentContext.projectId ? { projectId: currentContext.projectId } : {}),
|
|
377
|
+
...(currentDevice && currentDevice.device_id ? { deviceId: currentDevice.device_id } : {})
|
|
378
|
+
},
|
|
379
|
+
...(currentContext.projectId ? { activeProjectId: currentContext.projectId } : {}),
|
|
380
|
+
projects: records
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
buildProjectPortfolioProjection,
|
|
386
|
+
buildWorkspaceProjectId,
|
|
387
|
+
buildLocalProjectId,
|
|
388
|
+
resolveCurrentProjectContext
|
|
389
|
+
};
|