scene-capability-engine 3.6.60 → 3.6.62
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 +15 -0
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/docs/command-reference.md +37 -4
- package/docs/magicball-adaptation-task-checklist-v1.md +14 -0
- package/docs/magicball-cli-invocation-examples.md +2 -0
- package/docs/magicball-frontend-state-and-command-mapping.md +5 -0
- package/docs/magicball-integration-issue-tracker.md +2 -2
- package/docs/magicball-project-portfolio-contract.md +60 -4
- package/docs/magicball-sce-adaptation-guide.md +2 -0
- package/docs/magicball-task-feedback-timeline-guide.md +0 -1
- package/docs/magicball-ui-surface-checklist.md +2 -0
- package/docs/release-checklist.md +1 -1
- package/docs/releases/README.md +2 -0
- package/docs/releases/v3.6.61.md +21 -0
- package/docs/releases/v3.6.62.md +32 -0
- package/docs/zh/release-checklist.md +1 -1
- package/docs/zh/releases/README.md +2 -0
- package/docs/zh/releases/v3.6.61.md +21 -0
- package/docs/zh/releases/v3.6.62.md +32 -0
- package/lib/commands/project.js +52 -0
- package/lib/commands/studio.js +1 -319
- package/lib/project/candidate-inspection-service.js +216 -0
- package/lib/project/root-onboarding-service.js +350 -0
- package/package.json +1 -1
- package/scripts/magicball-project-contract-audit.js +17 -4
- package/template/.sce/README.md +2 -2
package/lib/commands/studio.js
CHANGED
|
@@ -921,273 +921,6 @@ function extractEventArrayFromPayload(payload) {
|
|
|
921
921
|
return [payload];
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
-
async function readOpenHandsEventsFile(openhandsEventsPath, fileSystem = fs) {
|
|
925
|
-
const normalizedPath = normalizeString(openhandsEventsPath);
|
|
926
|
-
if (!normalizedPath) {
|
|
927
|
-
return [];
|
|
928
|
-
}
|
|
929
|
-
const exists = await fileSystem.pathExists(normalizedPath);
|
|
930
|
-
if (!exists) {
|
|
931
|
-
throw new Error(`OpenHands events file not found: ${normalizedPath}`);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const content = await fileSystem.readFile(normalizedPath, 'utf8');
|
|
935
|
-
const trimmed = `${content || ''}`.trim();
|
|
936
|
-
if (!trimmed) {
|
|
937
|
-
return [];
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
try {
|
|
941
|
-
const parsed = JSON.parse(trimmed);
|
|
942
|
-
return extractEventArrayFromPayload(parsed);
|
|
943
|
-
} catch (_error) {
|
|
944
|
-
const lines = trimmed
|
|
945
|
-
.split(/\r?\n/)
|
|
946
|
-
.map((line) => line.trim())
|
|
947
|
-
.filter(Boolean);
|
|
948
|
-
const parsedLines = [];
|
|
949
|
-
for (const line of lines) {
|
|
950
|
-
try {
|
|
951
|
-
parsedLines.push(JSON.parse(line));
|
|
952
|
-
} catch (_innerError) {
|
|
953
|
-
// Skip malformed JSONL lines to keep ingestion robust.
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return parsedLines;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function normalizeOpenHandsEventRecord(rawEvent, index, jobId) {
|
|
961
|
-
const raw = rawEvent && typeof rawEvent === 'object'
|
|
962
|
-
? rawEvent
|
|
963
|
-
: { value: rawEvent };
|
|
964
|
-
const eventId = pickFirstString([
|
|
965
|
-
raw.event_id,
|
|
966
|
-
raw.eventId,
|
|
967
|
-
raw.id,
|
|
968
|
-
raw.uuid
|
|
969
|
-
]) || `oh-evt-${index + 1}`;
|
|
970
|
-
const eventType = pickFirstString([
|
|
971
|
-
raw.event_type,
|
|
972
|
-
raw.eventType,
|
|
973
|
-
raw.type,
|
|
974
|
-
raw.kind,
|
|
975
|
-
raw.action
|
|
976
|
-
]) || 'unknown';
|
|
977
|
-
const timestamp = pickFirstString([
|
|
978
|
-
raw.timestamp,
|
|
979
|
-
raw.time,
|
|
980
|
-
raw.created_at,
|
|
981
|
-
raw.createdAt,
|
|
982
|
-
raw.ts
|
|
983
|
-
]) || nowIso();
|
|
984
|
-
return {
|
|
985
|
-
api_version: STUDIO_EVENT_API_VERSION,
|
|
986
|
-
event_id: eventId,
|
|
987
|
-
job_id: jobId,
|
|
988
|
-
event_type: `openhands.${eventType}`,
|
|
989
|
-
timestamp,
|
|
990
|
-
metadata: {
|
|
991
|
-
source: 'openhands',
|
|
992
|
-
raw
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function tryParseJsonText(value) {
|
|
998
|
-
if (typeof value !== 'string') {
|
|
999
|
-
return null;
|
|
1000
|
-
}
|
|
1001
|
-
const trimmed = value.trim();
|
|
1002
|
-
if (!trimmed) {
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
1006
|
-
return null;
|
|
1007
|
-
}
|
|
1008
|
-
try {
|
|
1009
|
-
return JSON.parse(trimmed);
|
|
1010
|
-
} catch (_error) {
|
|
1011
|
-
return null;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function stringifyCompact(value) {
|
|
1016
|
-
if (value == null) {
|
|
1017
|
-
return '';
|
|
1018
|
-
}
|
|
1019
|
-
if (typeof value === 'string') {
|
|
1020
|
-
return value;
|
|
1021
|
-
}
|
|
1022
|
-
try {
|
|
1023
|
-
return JSON.stringify(value);
|
|
1024
|
-
} catch (_error) {
|
|
1025
|
-
return `${value}`;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function extractOpenHandsCommandCandidates(raw = {}) {
|
|
1030
|
-
const command = pickFirstString([
|
|
1031
|
-
raw.command,
|
|
1032
|
-
raw.cmd,
|
|
1033
|
-
raw.input && raw.input.command,
|
|
1034
|
-
raw.action && raw.action.command,
|
|
1035
|
-
raw.observation && raw.observation.command,
|
|
1036
|
-
raw.tool_input && raw.tool_input.command
|
|
1037
|
-
]);
|
|
1038
|
-
|
|
1039
|
-
const toolName = pickFirstString([
|
|
1040
|
-
raw.tool_name,
|
|
1041
|
-
raw.toolName,
|
|
1042
|
-
raw.tool && raw.tool.name,
|
|
1043
|
-
raw.name
|
|
1044
|
-
]);
|
|
1045
|
-
const toolArgs = raw.arguments || raw.tool_input || raw.input || null;
|
|
1046
|
-
|
|
1047
|
-
const cmd = command || (toolName
|
|
1048
|
-
? `tool:${toolName}${toolArgs ? ` ${stringifyCompact(toolArgs)}` : ''}`
|
|
1049
|
-
: '');
|
|
1050
|
-
|
|
1051
|
-
return {
|
|
1052
|
-
cmd,
|
|
1053
|
-
exit_code: pickFirstNumber([
|
|
1054
|
-
raw.exit_code,
|
|
1055
|
-
raw.exitCode,
|
|
1056
|
-
raw.result && (raw.result.exit_code ?? raw.result.exitCode)
|
|
1057
|
-
]),
|
|
1058
|
-
stdout: pickFirstString([
|
|
1059
|
-
raw.stdout,
|
|
1060
|
-
raw.output,
|
|
1061
|
-
raw.result && raw.result.stdout,
|
|
1062
|
-
raw.observation && raw.observation.stdout
|
|
1063
|
-
]),
|
|
1064
|
-
stderr: pickFirstString([
|
|
1065
|
-
raw.stderr,
|
|
1066
|
-
raw.result && raw.result.stderr,
|
|
1067
|
-
raw.error && raw.error.message,
|
|
1068
|
-
typeof raw.error === 'string' ? raw.error : ''
|
|
1069
|
-
]),
|
|
1070
|
-
log_path: pickFirstString([
|
|
1071
|
-
raw.log_path,
|
|
1072
|
-
raw.logPath,
|
|
1073
|
-
raw.log && raw.log.path
|
|
1074
|
-
])
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function collectOpenHandsFileChangesFromRaw(raw = {}) {
|
|
1079
|
-
const changes = [];
|
|
1080
|
-
const addPath = (pathValue, lineValue, diffRefValue = '') => {
|
|
1081
|
-
const pathRef = normalizeString(pathValue);
|
|
1082
|
-
if (!pathRef) {
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const line = Number.parseInt(`${lineValue != null ? lineValue : 1}`, 10);
|
|
1086
|
-
const normalizedLine = Number.isFinite(line) && line > 0 ? line : 1;
|
|
1087
|
-
const diffRef = normalizeString(diffRefValue) || `${pathRef}:${normalizedLine}`;
|
|
1088
|
-
changes.push({
|
|
1089
|
-
path: pathRef,
|
|
1090
|
-
line: normalizedLine,
|
|
1091
|
-
diffRef
|
|
1092
|
-
});
|
|
1093
|
-
};
|
|
1094
|
-
|
|
1095
|
-
if (typeof raw.path === 'string') {
|
|
1096
|
-
addPath(raw.path, raw.line || raw.line_number, raw.diff_ref);
|
|
1097
|
-
}
|
|
1098
|
-
if (raw.file && typeof raw.file === 'object') {
|
|
1099
|
-
addPath(raw.file.path || raw.file.name, raw.file.line || raw.file.line_number, raw.file.diffRef || raw.file.diff_ref);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
const arrayKeys = ['files', 'changed_files', 'modified_files', 'created_files', 'deleted_files'];
|
|
1103
|
-
for (const key of arrayKeys) {
|
|
1104
|
-
const values = Array.isArray(raw[key]) ? raw[key] : [];
|
|
1105
|
-
for (const value of values) {
|
|
1106
|
-
if (typeof value === 'string') {
|
|
1107
|
-
addPath(value, 1);
|
|
1108
|
-
} else if (value && typeof value === 'object') {
|
|
1109
|
-
addPath(value.path || value.file || value.name, value.line || value.line_number, value.diffRef || value.diff_ref);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
const patchText = pickFirstString([raw.patch, raw.diff]);
|
|
1115
|
-
if (patchText) {
|
|
1116
|
-
const parsedJson = tryParseJsonText(patchText);
|
|
1117
|
-
if (parsedJson && typeof parsedJson === 'object') {
|
|
1118
|
-
const nested = collectOpenHandsFileChangesFromRaw(parsedJson);
|
|
1119
|
-
changes.push(...nested);
|
|
1120
|
-
} else {
|
|
1121
|
-
const lines = patchText.split(/\r?\n/);
|
|
1122
|
-
for (const line of lines) {
|
|
1123
|
-
const match = line.match(/^\+\+\+\s+b\/(.+)$/);
|
|
1124
|
-
if (match && match[1]) {
|
|
1125
|
-
addPath(match[1], 1);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
return changes;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function mapOpenHandsEventsToTaskSignals(openhandsEvents = [], context = {}) {
|
|
1135
|
-
const normalizedEvents = openhandsEvents.map((item, index) =>
|
|
1136
|
-
normalizeOpenHandsEventRecord(item, index, context.jobId)
|
|
1137
|
-
);
|
|
1138
|
-
|
|
1139
|
-
const commands = [];
|
|
1140
|
-
const fileChanges = [];
|
|
1141
|
-
const errors = [];
|
|
1142
|
-
const evidence = [];
|
|
1143
|
-
|
|
1144
|
-
for (const event of normalizedEvents) {
|
|
1145
|
-
const raw = event && event.metadata ? event.metadata.raw || {} : {};
|
|
1146
|
-
const commandCandidate = extractOpenHandsCommandCandidates(raw);
|
|
1147
|
-
if (commandCandidate.cmd || commandCandidate.stdout || commandCandidate.stderr || commandCandidate.exit_code != null) {
|
|
1148
|
-
commands.push(commandCandidate);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
fileChanges.push(...collectOpenHandsFileChangesFromRaw(raw));
|
|
1152
|
-
|
|
1153
|
-
const eventType = normalizeString(event.event_type).toLowerCase();
|
|
1154
|
-
const failedByType = eventType.includes('error') || eventType.includes('fail');
|
|
1155
|
-
const failedByExit = commandCandidate.exit_code != null && Number(commandCandidate.exit_code) !== 0;
|
|
1156
|
-
const failedByStderr = commandCandidate.stderr.length > 0 && !commandCandidate.stdout;
|
|
1157
|
-
if (failedByType || failedByExit || failedByStderr) {
|
|
1158
|
-
const message = pickFirstString([
|
|
1159
|
-
raw.message,
|
|
1160
|
-
raw.error && raw.error.message,
|
|
1161
|
-
typeof raw.error === 'string' ? raw.error : '',
|
|
1162
|
-
failedByExit ? `OpenHands command failed (exit ${commandCandidate.exit_code})` : '',
|
|
1163
|
-
'OpenHands event indicates failure'
|
|
1164
|
-
]);
|
|
1165
|
-
errors.push({
|
|
1166
|
-
message,
|
|
1167
|
-
step_id: event.event_type,
|
|
1168
|
-
cmd: commandCandidate.cmd,
|
|
1169
|
-
exit_code: commandCandidate.exit_code,
|
|
1170
|
-
stderr: commandCandidate.stderr,
|
|
1171
|
-
stdout: commandCandidate.stdout
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
evidence.push({
|
|
1176
|
-
type: 'openhands-event',
|
|
1177
|
-
ref: event.event_id,
|
|
1178
|
-
detail: event.event_type
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
return {
|
|
1183
|
-
event: normalizedEvents,
|
|
1184
|
-
commands: normalizeTaskCommands(commands),
|
|
1185
|
-
file_changes: normalizeTaskFileChanges(fileChanges),
|
|
1186
|
-
errors: normalizeTaskErrors(errors),
|
|
1187
|
-
evidence: normalizeTaskEvidence(evidence)
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
924
|
function extractCommandsFromStageMetadata(stageMetadata = {}) {
|
|
1192
925
|
const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
|
|
1193
926
|
? stageMetadata.gate_steps
|
|
@@ -2919,21 +2652,7 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
|
|
|
2919
2652
|
|
|
2920
2653
|
const limit = normalizePositiveInteger(options.limit, 50);
|
|
2921
2654
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
2922
|
-
const openhandsEventsPath = normalizeString(options.openhandsEvents);
|
|
2923
|
-
let sourceStream = 'studio';
|
|
2924
2655
|
let events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
|
|
2925
|
-
let openhandsSignals = null;
|
|
2926
|
-
if (openhandsEventsPath) {
|
|
2927
|
-
const absoluteOpenhandsPath = path.isAbsolute(openhandsEventsPath)
|
|
2928
|
-
? openhandsEventsPath
|
|
2929
|
-
: path.join(projectPath, openhandsEventsPath);
|
|
2930
|
-
const rawOpenhandsEvents = await readOpenHandsEventsFile(absoluteOpenhandsPath, fileSystem);
|
|
2931
|
-
openhandsSignals = mapOpenHandsEventsToTaskSignals(rawOpenhandsEvents, {
|
|
2932
|
-
jobId: job.job_id
|
|
2933
|
-
});
|
|
2934
|
-
events = openhandsSignals.event;
|
|
2935
|
-
sourceStream = 'openhands';
|
|
2936
|
-
}
|
|
2937
2656
|
|
|
2938
2657
|
const payload = await buildCommandPayload('studio-events', job, {
|
|
2939
2658
|
events,
|
|
@@ -2941,47 +2660,11 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
|
|
|
2941
2660
|
fileSystem
|
|
2942
2661
|
});
|
|
2943
2662
|
payload.limit = limit;
|
|
2944
|
-
payload.source_stream =
|
|
2945
|
-
if (sourceStream === 'openhands') {
|
|
2946
|
-
payload.openhands_events_file = path.relative(projectPath, path.isAbsolute(openhandsEventsPath)
|
|
2947
|
-
? openhandsEventsPath
|
|
2948
|
-
: path.join(projectPath, openhandsEventsPath)).replace(/\\/g, '/');
|
|
2949
|
-
payload.task = {
|
|
2950
|
-
...payload.task,
|
|
2951
|
-
summary: [
|
|
2952
|
-
`OpenHands events: ${events.length} | commands: ${openhandsSignals.commands.length} | errors: ${openhandsSignals.errors.length}`,
|
|
2953
|
-
`File changes: ${openhandsSignals.file_changes.length} | source: ${payload.openhands_events_file}`,
|
|
2954
|
-
`Next: ${payload.task.next_action}`
|
|
2955
|
-
],
|
|
2956
|
-
handoff: {
|
|
2957
|
-
...(payload.task.handoff || {}),
|
|
2958
|
-
source_stream: 'openhands',
|
|
2959
|
-
openhands_event_count: events.length,
|
|
2960
|
-
openhands_command_count: openhandsSignals.commands.length,
|
|
2961
|
-
openhands_error_count: openhandsSignals.errors.length
|
|
2962
|
-
},
|
|
2963
|
-
commands: openhandsSignals.commands,
|
|
2964
|
-
file_changes: openhandsSignals.file_changes,
|
|
2965
|
-
errors: openhandsSignals.errors,
|
|
2966
|
-
evidence: normalizeTaskEvidence([
|
|
2967
|
-
...(Array.isArray(payload.task.evidence) ? payload.task.evidence : []),
|
|
2968
|
-
...openhandsSignals.evidence,
|
|
2969
|
-
{
|
|
2970
|
-
type: 'openhands-event-file',
|
|
2971
|
-
ref: payload.openhands_events_file,
|
|
2972
|
-
detail: 'mapped'
|
|
2973
|
-
}
|
|
2974
|
-
])
|
|
2975
|
-
};
|
|
2976
|
-
payload.eventId = events.length > 0
|
|
2977
|
-
? events[events.length - 1].event_id
|
|
2978
|
-
: null;
|
|
2979
|
-
}
|
|
2663
|
+
payload.source_stream = 'studio';
|
|
2980
2664
|
payload.events = events;
|
|
2981
2665
|
const normalizedPayload = attachTaskFeedbackModel(payload);
|
|
2982
2666
|
printStudioEventsPayload(normalizedPayload, options);
|
|
2983
2667
|
return normalizedPayload;
|
|
2984
|
-
return payload;
|
|
2985
2668
|
}
|
|
2986
2669
|
|
|
2987
2670
|
function printStudioIntakePayload(payload, options = {}) {
|
|
@@ -3267,7 +2950,6 @@ function registerStudioCommands(program) {
|
|
|
3267
2950
|
.description('Show studio job event stream')
|
|
3268
2951
|
.option('--job <job-id>', 'Studio job id (defaults to latest)')
|
|
3269
2952
|
.option('--limit <number>', 'Maximum number of recent events to return', '50')
|
|
3270
|
-
.option('--openhands-events <path>', 'Optional OpenHands raw events file (.json/.jsonl) mapped to task stream')
|
|
3271
2953
|
.option('--json', 'Print machine-readable JSON output')
|
|
3272
2954
|
.action(async (options) => runStudioCommand(runStudioEventsCommand, options, 'events'));
|
|
3273
2955
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const WorkspaceStateManager = require('../workspace/multi/workspace-state-manager');
|
|
4
|
+
const {
|
|
5
|
+
buildLocalProjectId,
|
|
6
|
+
buildWorkspaceProjectId
|
|
7
|
+
} = require('./portfolio-projection-service');
|
|
8
|
+
|
|
9
|
+
const PROJECT_CANDIDATE_REASON_CODES = {
|
|
10
|
+
ROOT_ACCESSIBLE: 'project.root.accessible',
|
|
11
|
+
ROOT_INACCESSIBLE: 'project.root.inaccessible',
|
|
12
|
+
ROOT_INVALID_TYPE: 'project.root.invalid_type',
|
|
13
|
+
WORKSPACE_REGISTERED: 'project.workspace.registered',
|
|
14
|
+
SCE_PRESENT: 'project.sce.present',
|
|
15
|
+
ROOT_NOT_INITIALIZED: 'project.root.not_initialized',
|
|
16
|
+
INVALID_PROJECT_METADATA: 'project.metadata.invalid',
|
|
17
|
+
UNREGISTERED_PROJECT: 'project.sce.unregistered'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function normalizeString(value) {
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePath(value) {
|
|
28
|
+
return normalizeString(value).replace(/\\/g, '/');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildProjectName(rootDir) {
|
|
32
|
+
return path.basename(rootDir) || 'project';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolveExactWorkspaceByRoot(rootDir, stateManager) {
|
|
36
|
+
const workspaces = await stateManager.listWorkspaces();
|
|
37
|
+
const normalizedRoot = normalizePath(rootDir);
|
|
38
|
+
return workspaces.find((workspace) => normalizePath(workspace && workspace.path) === normalizedRoot) || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function inspectSceMetadata(rootDir, fileSystem = fs) {
|
|
42
|
+
const sceRoot = path.join(rootDir, '.sce');
|
|
43
|
+
if (!await fileSystem.pathExists(sceRoot)) {
|
|
44
|
+
return {
|
|
45
|
+
scePresent: false,
|
|
46
|
+
metadataValid: true,
|
|
47
|
+
metadataReasonCodes: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const sceStats = await fileSystem.stat(sceRoot);
|
|
53
|
+
if (!sceStats.isDirectory()) {
|
|
54
|
+
return {
|
|
55
|
+
scePresent: false,
|
|
56
|
+
metadataValid: false,
|
|
57
|
+
metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INVALID_TYPE]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
} catch (_error) {
|
|
61
|
+
return {
|
|
62
|
+
scePresent: false,
|
|
63
|
+
metadataValid: false,
|
|
64
|
+
metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const versionPath = path.join(sceRoot, 'version.json');
|
|
69
|
+
if (!await fileSystem.pathExists(versionPath)) {
|
|
70
|
+
return {
|
|
71
|
+
scePresent: true,
|
|
72
|
+
metadataValid: true,
|
|
73
|
+
metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await fileSystem.readJson(versionPath);
|
|
79
|
+
return {
|
|
80
|
+
scePresent: true,
|
|
81
|
+
metadataValid: true,
|
|
82
|
+
metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT]
|
|
83
|
+
};
|
|
84
|
+
} catch (_error) {
|
|
85
|
+
return {
|
|
86
|
+
scePresent: true,
|
|
87
|
+
metadataValid: false,
|
|
88
|
+
metadataReasonCodes: [
|
|
89
|
+
PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT,
|
|
90
|
+
PROJECT_CANDIDATE_REASON_CODES.INVALID_PROJECT_METADATA
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dedupeReasonCodes(reasonCodes = []) {
|
|
97
|
+
return Array.from(new Set(reasonCodes.filter(Boolean)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function inspectProjectCandidate(options = {}, dependencies = {}) {
|
|
101
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
102
|
+
const stateManager = dependencies.stateManager || new WorkspaceStateManager(dependencies.workspaceStatePath);
|
|
103
|
+
const requestedRoot = normalizeString(options.root || options.rootDir);
|
|
104
|
+
if (!requestedRoot) {
|
|
105
|
+
throw new Error('--root is required');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const absoluteRoot = path.resolve(requestedRoot);
|
|
109
|
+
const rootDir = normalizePath(absoluteRoot);
|
|
110
|
+
const inspectedAt = new Date().toISOString();
|
|
111
|
+
const projectName = buildProjectName(rootDir);
|
|
112
|
+
|
|
113
|
+
if (!await fileSystem.pathExists(absoluteRoot)) {
|
|
114
|
+
return {
|
|
115
|
+
inspectedAt,
|
|
116
|
+
rootDir,
|
|
117
|
+
kind: 'invalid',
|
|
118
|
+
projectName,
|
|
119
|
+
readiness: 'blocked',
|
|
120
|
+
availability: 'inaccessible',
|
|
121
|
+
localCandidate: false,
|
|
122
|
+
reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let rootStats = null;
|
|
127
|
+
try {
|
|
128
|
+
rootStats = await fileSystem.stat(absoluteRoot);
|
|
129
|
+
} catch (_error) {
|
|
130
|
+
return {
|
|
131
|
+
inspectedAt,
|
|
132
|
+
rootDir,
|
|
133
|
+
kind: 'invalid',
|
|
134
|
+
projectName,
|
|
135
|
+
readiness: 'blocked',
|
|
136
|
+
availability: 'inaccessible',
|
|
137
|
+
localCandidate: false,
|
|
138
|
+
reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!rootStats.isDirectory()) {
|
|
143
|
+
return {
|
|
144
|
+
inspectedAt,
|
|
145
|
+
rootDir,
|
|
146
|
+
kind: 'invalid',
|
|
147
|
+
projectName,
|
|
148
|
+
readiness: 'blocked',
|
|
149
|
+
availability: 'degraded',
|
|
150
|
+
localCandidate: false,
|
|
151
|
+
reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INVALID_TYPE]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const workspace = await resolveExactWorkspaceByRoot(absoluteRoot, stateManager);
|
|
156
|
+
const sceInspection = await inspectSceMetadata(absoluteRoot, fileSystem);
|
|
157
|
+
const metadataBlocked = sceInspection.scePresent && !sceInspection.metadataValid;
|
|
158
|
+
|
|
159
|
+
if (workspace) {
|
|
160
|
+
const workspaceBlocked = !sceInspection.scePresent || metadataBlocked;
|
|
161
|
+
return {
|
|
162
|
+
inspectedAt,
|
|
163
|
+
rootDir,
|
|
164
|
+
kind: 'workspace-backed',
|
|
165
|
+
projectId: buildWorkspaceProjectId(workspace.name),
|
|
166
|
+
workspaceId: workspace.name,
|
|
167
|
+
projectName,
|
|
168
|
+
readiness: workspaceBlocked ? 'blocked' : 'ready',
|
|
169
|
+
availability: workspaceBlocked ? 'degraded' : 'accessible',
|
|
170
|
+
localCandidate: false,
|
|
171
|
+
reasonCodes: dedupeReasonCodes([
|
|
172
|
+
PROJECT_CANDIDATE_REASON_CODES.WORKSPACE_REGISTERED,
|
|
173
|
+
PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
|
|
174
|
+
...(!sceInspection.scePresent ? [PROJECT_CANDIDATE_REASON_CODES.ROOT_NOT_INITIALIZED] : []),
|
|
175
|
+
...sceInspection.metadataReasonCodes
|
|
176
|
+
])
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (sceInspection.scePresent) {
|
|
181
|
+
return {
|
|
182
|
+
inspectedAt,
|
|
183
|
+
rootDir,
|
|
184
|
+
kind: 'local-sce-candidate',
|
|
185
|
+
projectId: buildLocalProjectId(rootDir),
|
|
186
|
+
projectName,
|
|
187
|
+
readiness: metadataBlocked ? 'blocked' : 'partial',
|
|
188
|
+
availability: metadataBlocked ? 'degraded' : 'degraded',
|
|
189
|
+
localCandidate: true,
|
|
190
|
+
reasonCodes: dedupeReasonCodes([
|
|
191
|
+
PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
|
|
192
|
+
PROJECT_CANDIDATE_REASON_CODES.UNREGISTERED_PROJECT,
|
|
193
|
+
...sceInspection.metadataReasonCodes
|
|
194
|
+
])
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
inspectedAt,
|
|
200
|
+
rootDir,
|
|
201
|
+
kind: 'directory-candidate',
|
|
202
|
+
projectName,
|
|
203
|
+
readiness: 'pending',
|
|
204
|
+
availability: 'accessible',
|
|
205
|
+
localCandidate: true,
|
|
206
|
+
reasonCodes: [
|
|
207
|
+
PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
|
|
208
|
+
PROJECT_CANDIDATE_REASON_CODES.ROOT_NOT_INITIALIZED
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
PROJECT_CANDIDATE_REASON_CODES,
|
|
215
|
+
inspectProjectCandidate
|
|
216
|
+
};
|