scene-capability-engine 3.6.32 → 3.6.36
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 +86 -1
- package/README.md +119 -122
- package/README.zh.md +123 -121
- package/bin/scene-capability-engine.js +11 -0
- package/docs/README.md +21 -32
- package/docs/auto-refactor-index.md +384 -0
- package/docs/command-reference.md +94 -2
- package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
- package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
- package/docs/magicball-capability-iteration-api.md +2 -0
- package/docs/magicball-capability-iteration-ui.md +2 -0
- package/docs/magicball-capability-library.md +2 -0
- package/docs/magicball-cli-invocation-examples.md +336 -0
- package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
- package/docs/magicball-integration-doc-index.md +137 -0
- package/docs/magicball-integration-issue-tracker.md +218 -0
- package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
- package/docs/magicball-sce-adaptation-guide.md +203 -0
- package/docs/magicball-three-mode-alignment-plan.md +551 -0
- package/docs/magicball-ui-surface-checklist.md +126 -0
- package/docs/magicball-write-auth-adaptation-guide.md +328 -0
- package/docs/refactor-completion-roadmap.md +116 -0
- package/docs/zh/README.md +27 -30
- package/docs/zh/refactor-completion-roadmap.md +116 -0
- package/lib/app/registry-config.js +73 -0
- package/lib/app/registry-sync-service.js +228 -0
- package/lib/auto/archive-schema-service.js +276 -0
- package/lib/auto/archive-summary.js +60 -0
- package/lib/auto/batch-goal-input-service.js +543 -0
- package/lib/auto/batch-output.js +201 -0
- package/lib/auto/batch-summary-storage-service.js +110 -0
- package/lib/auto/close-loop-batch-service.js +116 -0
- package/lib/auto/close-loop-controller-service.js +287 -0
- package/lib/auto/close-loop-program-service.js +283 -0
- package/lib/auto/close-loop-recovery-service.js +191 -0
- package/lib/auto/close-loop-session-storage-service.js +50 -0
- package/lib/auto/controller-lock-service.js +55 -0
- package/lib/auto/controller-output.js +32 -0
- package/lib/auto/controller-queue-service.js +127 -0
- package/lib/auto/controller-session-storage-service.js +105 -0
- package/lib/auto/governance-advisory-service.js +208 -0
- package/lib/auto/governance-close-loop-service.js +411 -0
- package/lib/auto/governance-maintenance-presenter.js +162 -0
- package/lib/auto/governance-maintenance-service.js +112 -0
- package/lib/auto/governance-session-presenter.js +70 -0
- package/lib/auto/governance-session-storage-service.js +198 -0
- package/lib/auto/governance-signals.js +139 -0
- package/lib/auto/governance-stats-presenter.js +337 -0
- package/lib/auto/governance-stats-service.js +115 -0
- package/lib/auto/governance-summary.js +703 -0
- package/lib/auto/handoff-capability-matrix-service.js +281 -0
- package/lib/auto/handoff-evidence-review-service.js +251 -0
- package/lib/auto/handoff-release-evidence-service.js +190 -0
- package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
- package/lib/auto/handoff-release-gate-history-service.js +257 -0
- package/lib/auto/handoff-reporting-service.js +1407 -0
- package/lib/auto/handoff-run-service.js +486 -0
- package/lib/auto/handoff-snapshots-service.js +645 -0
- package/lib/auto/observability-service.js +132 -0
- package/lib/auto/output-writer.js +34 -0
- package/lib/auto/program-auto-remediation-service.js +130 -0
- package/lib/auto/program-diagnostics.js +138 -0
- package/lib/auto/program-governance-helpers.js +306 -0
- package/lib/auto/program-governance-loop-service.js +413 -0
- package/lib/auto/program-output.js +106 -0
- package/lib/auto/program-summary.js +183 -0
- package/lib/auto/recovery-memory-service.js +684 -0
- package/lib/auto/recovery-selection-service.js +52 -0
- package/lib/auto/retention-policy.js +98 -0
- package/lib/auto/session-persistence-service.js +106 -0
- package/lib/auto/session-presenter.js +105 -0
- package/lib/auto/session-prune-service.js +190 -0
- package/lib/auto/session-query-service.js +249 -0
- package/lib/auto/spec-protection.js +141 -0
- package/lib/commands/app.js +911 -0
- package/lib/commands/assurance.js +212 -0
- package/lib/commands/auto.js +1091 -11063
- package/lib/commands/mode.js +321 -0
- package/lib/commands/ontology.js +415 -0
- package/lib/commands/pm.js +422 -0
- package/lib/ontology/seed-profiles.js +160 -0
- package/lib/state/sce-state-store.js +3369 -1200
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
async function executeCloseLoopRecoveryCycle(input = {}, dependencies = {}) {
|
|
2
|
+
const {
|
|
3
|
+
projectPath,
|
|
4
|
+
sourceSummary,
|
|
5
|
+
baseOptions,
|
|
6
|
+
recoverAutonomousEnabled,
|
|
7
|
+
resumeStrategy,
|
|
8
|
+
recoverUntilComplete,
|
|
9
|
+
recoverMaxRounds,
|
|
10
|
+
recoverMaxDurationMs,
|
|
11
|
+
recoveryMemoryTtlDays,
|
|
12
|
+
recoveryMemoryScope,
|
|
13
|
+
actionCandidate
|
|
14
|
+
} = input;
|
|
15
|
+
const {
|
|
16
|
+
pruneCloseLoopRecoveryMemory,
|
|
17
|
+
loadCloseLoopRecoveryMemory,
|
|
18
|
+
normalizeRecoveryMemoryToken,
|
|
19
|
+
buildRecoveryMemorySignature,
|
|
20
|
+
getRecoveryMemoryEntry,
|
|
21
|
+
resolveRecoveryActionSelection,
|
|
22
|
+
applyRecoveryActionPatch,
|
|
23
|
+
buildCloseLoopBatchGoalsFromSummaryPayload,
|
|
24
|
+
executeCloseLoopBatch,
|
|
25
|
+
loadCloseLoopBatchSummaryPayload,
|
|
26
|
+
updateCloseLoopRecoveryMemory,
|
|
27
|
+
now = () => Date.now()
|
|
28
|
+
} = dependencies;
|
|
29
|
+
|
|
30
|
+
let resolvedSourceSummary = sourceSummary && typeof sourceSummary === 'object'
|
|
31
|
+
? {
|
|
32
|
+
file: typeof sourceSummary.file === 'string' && sourceSummary.file.trim()
|
|
33
|
+
? sourceSummary.file
|
|
34
|
+
: '(in-memory-summary)',
|
|
35
|
+
payload: sourceSummary.payload && typeof sourceSummary.payload === 'object'
|
|
36
|
+
? sourceSummary.payload
|
|
37
|
+
: {}
|
|
38
|
+
}
|
|
39
|
+
: {
|
|
40
|
+
file: '(in-memory-summary)',
|
|
41
|
+
payload: {}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (recoveryMemoryTtlDays !== null && recoveryMemoryTtlDays !== undefined) {
|
|
45
|
+
await pruneCloseLoopRecoveryMemory(projectPath, {
|
|
46
|
+
olderThanDays: recoveryMemoryTtlDays,
|
|
47
|
+
dryRun: false
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const recoveryMemory = await loadCloseLoopRecoveryMemory(projectPath);
|
|
51
|
+
const resolvedRecoveryScope = normalizeRecoveryMemoryToken(recoveryMemoryScope || '') || 'default-scope';
|
|
52
|
+
const recoverySignature = buildRecoveryMemorySignature(resolvedSourceSummary.payload, {
|
|
53
|
+
scope: resolvedRecoveryScope
|
|
54
|
+
});
|
|
55
|
+
const recoveryMemoryEntry = getRecoveryMemoryEntry(recoveryMemory.payload, recoverySignature);
|
|
56
|
+
const pinnedActionSelection = resolveRecoveryActionSelection(
|
|
57
|
+
resolvedSourceSummary.payload,
|
|
58
|
+
actionCandidate,
|
|
59
|
+
{ recoveryMemoryEntry }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
let finalSummary = null;
|
|
63
|
+
let finalRecoveryOptions = null;
|
|
64
|
+
const recoveryHistory = [];
|
|
65
|
+
const startedAt = Number(now());
|
|
66
|
+
let budgetExhausted = false;
|
|
67
|
+
for (let round = 1; round <= recoverMaxRounds; round += 1) {
|
|
68
|
+
if (recoverMaxDurationMs !== null && recoverMaxDurationMs !== undefined) {
|
|
69
|
+
const elapsedBeforeRound = Number(now()) - startedAt;
|
|
70
|
+
if (elapsedBeforeRound >= recoverMaxDurationMs && finalSummary) {
|
|
71
|
+
budgetExhausted = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const recoveryOptions = applyRecoveryActionPatch({
|
|
77
|
+
...baseOptions,
|
|
78
|
+
batchAutonomous: recoverAutonomousEnabled
|
|
79
|
+
}, pinnedActionSelection.selectedAction);
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
recoverUntilComplete &&
|
|
83
|
+
typeof recoveryOptions.batchSessionId === 'string' &&
|
|
84
|
+
recoveryOptions.batchSessionId.trim()
|
|
85
|
+
) {
|
|
86
|
+
recoveryOptions.batchSessionId = `${recoveryOptions.batchSessionId.trim()}-r${round}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const goalsResult = await buildCloseLoopBatchGoalsFromSummaryPayload(
|
|
90
|
+
resolvedSourceSummary.payload,
|
|
91
|
+
resolvedSourceSummary.file,
|
|
92
|
+
projectPath,
|
|
93
|
+
'auto',
|
|
94
|
+
resumeStrategy
|
|
95
|
+
);
|
|
96
|
+
const summary = await executeCloseLoopBatch(
|
|
97
|
+
goalsResult,
|
|
98
|
+
recoveryOptions,
|
|
99
|
+
projectPath,
|
|
100
|
+
'auto-close-loop-recover'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
summary.recovered_from_summary = {
|
|
104
|
+
file: resolvedSourceSummary.file,
|
|
105
|
+
source_mode: resolvedSourceSummary.payload.mode || null,
|
|
106
|
+
source_status: resolvedSourceSummary.payload.status || null,
|
|
107
|
+
resume_strategy: resumeStrategy,
|
|
108
|
+
selected_action_index: pinnedActionSelection.selectedIndex,
|
|
109
|
+
selected_action: pinnedActionSelection.selectedAction || null,
|
|
110
|
+
round
|
|
111
|
+
};
|
|
112
|
+
summary.recovery_plan = {
|
|
113
|
+
remediation_actions: pinnedActionSelection.availableActions,
|
|
114
|
+
applied_patch: pinnedActionSelection.appliedPatch,
|
|
115
|
+
selection_source: pinnedActionSelection.selectionSource,
|
|
116
|
+
selection_explain: pinnedActionSelection.selectionExplain || null
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
recoveryHistory.push({
|
|
120
|
+
round,
|
|
121
|
+
source_summary: resolvedSourceSummary.file,
|
|
122
|
+
status: summary.status,
|
|
123
|
+
processed_goals: summary.processed_goals,
|
|
124
|
+
completed_goals: summary.completed_goals,
|
|
125
|
+
failed_goals: summary.failed_goals,
|
|
126
|
+
batch_session_file: summary.batch_session && summary.batch_session.file
|
|
127
|
+
? summary.batch_session.file
|
|
128
|
+
: null
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
finalSummary = summary;
|
|
132
|
+
finalRecoveryOptions = recoveryOptions;
|
|
133
|
+
|
|
134
|
+
if (!recoverUntilComplete || summary.status === 'completed') {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
resolvedSourceSummary = summary.batch_session && summary.batch_session.file
|
|
139
|
+
? await loadCloseLoopBatchSummaryPayload(projectPath, summary.batch_session.file)
|
|
140
|
+
: {
|
|
141
|
+
file: '(derived-from-summary)',
|
|
142
|
+
payload: summary
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!finalSummary) {
|
|
147
|
+
throw new Error('Recovery cycle did not produce a summary.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
finalSummary.recovery_cycle = {
|
|
151
|
+
enabled: recoverUntilComplete,
|
|
152
|
+
max_rounds: recoverMaxRounds,
|
|
153
|
+
performed_rounds: recoveryHistory.length,
|
|
154
|
+
converged: finalSummary.status === 'completed',
|
|
155
|
+
exhausted: ((recoverUntilComplete && recoveryHistory.length >= recoverMaxRounds && finalSummary.status !== 'completed') || budgetExhausted),
|
|
156
|
+
time_budget_minutes: recoverMaxDurationMs ? Number((recoverMaxDurationMs / 60000).toFixed(2)) : null,
|
|
157
|
+
elapsed_ms: Number(now()) - startedAt,
|
|
158
|
+
budget_exhausted: budgetExhausted,
|
|
159
|
+
history: recoveryHistory
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const memoryUpdate = await updateCloseLoopRecoveryMemory(
|
|
163
|
+
projectPath,
|
|
164
|
+
recoveryMemory,
|
|
165
|
+
recoverySignature,
|
|
166
|
+
pinnedActionSelection.selectedIndex,
|
|
167
|
+
pinnedActionSelection.selectedAction,
|
|
168
|
+
finalSummary.status,
|
|
169
|
+
{ scope: resolvedRecoveryScope }
|
|
170
|
+
);
|
|
171
|
+
finalSummary.recovery_memory = {
|
|
172
|
+
file: memoryUpdate.file,
|
|
173
|
+
signature: memoryUpdate.signature,
|
|
174
|
+
scope: memoryUpdate.scope,
|
|
175
|
+
action_key: memoryUpdate.action_key,
|
|
176
|
+
selected_action_index: pinnedActionSelection.selectedIndex,
|
|
177
|
+
selection_source: pinnedActionSelection.selectionSource,
|
|
178
|
+
selection_explain: pinnedActionSelection.selectionExplain || null,
|
|
179
|
+
action_stats: memoryUpdate.entry
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
summary: finalSummary,
|
|
184
|
+
options: finalRecoveryOptions || baseOptions,
|
|
185
|
+
pinnedActionSelection
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
executeCloseLoopRecoveryCycle
|
|
191
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function getCloseLoopSessionDir(projectPath) {
|
|
4
|
+
return path.join(projectPath, '.sce', 'auto', 'close-loop-sessions');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function readCloseLoopSessionEntries(projectPath, dependencies = {}) {
|
|
8
|
+
const { fs } = dependencies;
|
|
9
|
+
const sessionDir = getCloseLoopSessionDir(projectPath);
|
|
10
|
+
if (!(await fs.pathExists(sessionDir))) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const files = (await fs.readdir(sessionDir)).filter((item) => item.toLowerCase().endsWith('.json'));
|
|
15
|
+
const sessions = [];
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const filePath = path.join(sessionDir, file);
|
|
18
|
+
const stats = await fs.stat(filePath);
|
|
19
|
+
const fallbackTimestamp = new Date(stats.mtimeMs).toISOString();
|
|
20
|
+
const fallbackId = path.basename(file, '.json');
|
|
21
|
+
let payload = null;
|
|
22
|
+
let parseError = null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
payload = await fs.readJson(filePath);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
parseError = error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sessions.push({
|
|
31
|
+
id: payload && payload.session_id ? payload.session_id : fallbackId,
|
|
32
|
+
file: filePath,
|
|
33
|
+
status: payload && typeof payload.status === 'string' ? payload.status : (parseError ? 'invalid' : 'unknown'),
|
|
34
|
+
goal: payload && typeof payload.goal === 'string' ? payload.goal : null,
|
|
35
|
+
master_spec: payload && payload.portfolio && typeof payload.portfolio.master_spec === 'string' ? payload.portfolio.master_spec : null,
|
|
36
|
+
sub_spec_count: payload && payload.portfolio && Array.isArray(payload.portfolio.sub_specs) ? payload.portfolio.sub_specs.length : null,
|
|
37
|
+
updated_at: payload && typeof payload.updated_at === 'string' ? payload.updated_at : fallbackTimestamp,
|
|
38
|
+
parse_error: parseError ? parseError.message : null,
|
|
39
|
+
mtime_ms: stats.mtimeMs
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
sessions.sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
44
|
+
return sessions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
getCloseLoopSessionDir,
|
|
49
|
+
readCloseLoopSessionEntries
|
|
50
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function buildControllerLockPayload(lockToken) {
|
|
2
|
+
return {
|
|
3
|
+
token: lockToken,
|
|
4
|
+
pid: process.pid,
|
|
5
|
+
host: process.env.COMPUTERNAME || process.env.HOSTNAME || null,
|
|
6
|
+
acquired_at: new Date().toISOString(),
|
|
7
|
+
touched_at: new Date().toISOString()
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveControllerLockFile(pathModule, projectPath, queueFilePath, lockFileCandidate) {
|
|
12
|
+
const normalized = typeof lockFileCandidate === 'string' && lockFileCandidate.trim()
|
|
13
|
+
? lockFileCandidate.trim()
|
|
14
|
+
: `${queueFilePath}.lock`;
|
|
15
|
+
return pathModule.isAbsolute(normalized)
|
|
16
|
+
? normalized
|
|
17
|
+
: pathModule.join(projectPath, normalized);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readControllerLockPayload(fs, lockFile) {
|
|
21
|
+
if (!(await fs.pathExists(lockFile))) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return await fs.readJson(lockFile);
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function writeControllerLockPayload(pathModule, fs, lockFile, payload, mode = 'overwrite') {
|
|
32
|
+
await fs.ensureDir(pathModule.dirname(lockFile));
|
|
33
|
+
if (mode === 'create') {
|
|
34
|
+
await fs.writeFile(lockFile, JSON.stringify(payload, null, 2), {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
flag: 'wx'
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await fs.writeJson(lockFile, payload, { spaces: 2 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isControllerLockStale(stats, ttlSeconds, now = Date.now()) {
|
|
44
|
+
const mtimeMs = Number(stats && stats.mtimeMs) || 0;
|
|
45
|
+
const ttlMs = Math.max(1, ttlSeconds) * 1000;
|
|
46
|
+
return mtimeMs > 0 && (now - mtimeMs) > ttlMs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
buildControllerLockPayload,
|
|
51
|
+
resolveControllerLockFile,
|
|
52
|
+
readControllerLockPayload,
|
|
53
|
+
writeControllerLockPayload,
|
|
54
|
+
isControllerLockStale
|
|
55
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function printCloseLoopControllerSummary(chalk, summary) {
|
|
2
|
+
console.log(chalk.blue('Autonomous close-loop controller summary'));
|
|
3
|
+
console.log(chalk.gray(` Status: ${summary.status}`));
|
|
4
|
+
console.log(chalk.gray(` Cycles: ${summary.cycles_performed}/${summary.max_cycles}`));
|
|
5
|
+
console.log(chalk.gray(` Processed goals: ${summary.processed_goals}`));
|
|
6
|
+
console.log(chalk.gray(` Completed: ${summary.completed_goals}`));
|
|
7
|
+
console.log(chalk.gray(` Failed: ${summary.failed_goals}`));
|
|
8
|
+
console.log(chalk.gray(` Pending queue goals: ${summary.pending_goals}`));
|
|
9
|
+
if (summary.dedupe_enabled) {
|
|
10
|
+
console.log(chalk.gray(` Dedupe dropped: ${summary.dedupe_dropped_goals || 0}`));
|
|
11
|
+
}
|
|
12
|
+
console.log(chalk.gray(` Stop reason: ${summary.stop_reason}`));
|
|
13
|
+
if (summary.lock_enabled && summary.lock_file) {
|
|
14
|
+
console.log(chalk.gray(` Lock: ${summary.lock_file}`));
|
|
15
|
+
}
|
|
16
|
+
if (summary.controller_session && summary.controller_session.file) {
|
|
17
|
+
console.log(chalk.gray(` Session: ${summary.controller_session.file}`));
|
|
18
|
+
}
|
|
19
|
+
if (summary.done_archive_file) {
|
|
20
|
+
console.log(chalk.gray(` Done archive: ${summary.done_archive_file}`));
|
|
21
|
+
}
|
|
22
|
+
if (summary.failed_archive_file) {
|
|
23
|
+
console.log(chalk.gray(` Failed archive: ${summary.failed_archive_file}`));
|
|
24
|
+
}
|
|
25
|
+
if (summary.output_file) {
|
|
26
|
+
console.log(chalk.gray(` Output: ${summary.output_file}`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
printCloseLoopControllerSummary
|
|
32
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
function resolveControllerQueueFile(pathModule, projectPath, queueFileCandidate) {
|
|
2
|
+
const normalized = typeof queueFileCandidate === 'string' && queueFileCandidate.trim()
|
|
3
|
+
? queueFileCandidate.trim()
|
|
4
|
+
: '.sce/auto/close-loop-controller-goals.lines';
|
|
5
|
+
return pathModule.isAbsolute(normalized)
|
|
6
|
+
? normalized
|
|
7
|
+
: pathModule.join(projectPath, normalized);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveControllerQueueFormat(normalizeBatchFormat, resolvedQueueFile, formatCandidate) {
|
|
11
|
+
const normalized = normalizeBatchFormat(formatCandidate);
|
|
12
|
+
if (normalized !== 'auto') {
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
return `${resolvedQueueFile}`.toLowerCase().endsWith('.json')
|
|
16
|
+
? 'json'
|
|
17
|
+
: 'lines';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dedupeControllerGoals(goals) {
|
|
21
|
+
const uniqueGoals = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
let duplicateCount = 0;
|
|
24
|
+
for (const item of Array.isArray(goals) ? goals : []) {
|
|
25
|
+
const normalized = `${item || ''}`.trim();
|
|
26
|
+
if (!normalized) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const fingerprint = normalized.toLowerCase();
|
|
30
|
+
if (seen.has(fingerprint)) {
|
|
31
|
+
duplicateCount += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
seen.add(fingerprint);
|
|
35
|
+
uniqueGoals.push(normalized);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
goals: uniqueGoals,
|
|
39
|
+
duplicate_count: duplicateCount
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadControllerGoalQueue(pathModule, fs, parseGoalsFromJsonPayload, parseGoalsFromLines, projectPath, queueFileCandidate, formatCandidate, options = {}) {
|
|
44
|
+
const file = resolveControllerQueueFile(pathModule, projectPath, queueFileCandidate);
|
|
45
|
+
const format = resolveControllerQueueFormat(options.normalizeBatchFormat || ((value) => value || 'auto'), file, formatCandidate);
|
|
46
|
+
const dedupe = options.dedupe === true;
|
|
47
|
+
if (!(await fs.pathExists(file))) {
|
|
48
|
+
await fs.ensureDir(pathModule.dirname(file));
|
|
49
|
+
if (format === 'json') {
|
|
50
|
+
await fs.writeJson(file, { goals: [] }, { spaces: 2 });
|
|
51
|
+
} else {
|
|
52
|
+
await fs.writeFile(file, '', 'utf8');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let goals = [];
|
|
57
|
+
if (format === 'json') {
|
|
58
|
+
let payload = null;
|
|
59
|
+
try {
|
|
60
|
+
payload = await fs.readJson(file);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Invalid controller queue JSON: ${file} (${error.message})`);
|
|
63
|
+
}
|
|
64
|
+
goals = parseGoalsFromJsonPayload(payload || {});
|
|
65
|
+
} else {
|
|
66
|
+
const content = await fs.readFile(file, 'utf8');
|
|
67
|
+
goals = parseGoalsFromLines(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizedGoals = goals.map((item) => `${item || ''}`.trim()).filter(Boolean);
|
|
71
|
+
const dedupeResult = dedupe
|
|
72
|
+
? dedupeControllerGoals(normalizedGoals)
|
|
73
|
+
: { goals: normalizedGoals, duplicate_count: 0 };
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
file,
|
|
77
|
+
format,
|
|
78
|
+
goals: dedupeResult.goals,
|
|
79
|
+
duplicate_count: dedupeResult.duplicate_count,
|
|
80
|
+
dedupe_applied: dedupe
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeControllerGoalQueue(pathModule, fs, file, format, goals) {
|
|
85
|
+
const normalizedGoals = Array.isArray(goals)
|
|
86
|
+
? goals.map((item) => `${item || ''}`.trim()).filter(Boolean)
|
|
87
|
+
: [];
|
|
88
|
+
await fs.ensureDir(pathModule.dirname(file));
|
|
89
|
+
if (format === 'json') {
|
|
90
|
+
await fs.writeJson(file, { goals: normalizedGoals }, { spaces: 2 });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const content = normalizedGoals.length > 0
|
|
94
|
+
? `${normalizedGoals.join('\n')}\n`
|
|
95
|
+
: '';
|
|
96
|
+
await fs.writeFile(file, content, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function appendControllerGoalArchive(pathModule, fs, fileCandidate, projectPath, goal, metadata = {}) {
|
|
100
|
+
if (!fileCandidate) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const resolvedFile = pathModule.isAbsolute(fileCandidate)
|
|
104
|
+
? fileCandidate
|
|
105
|
+
: pathModule.join(projectPath, fileCandidate);
|
|
106
|
+
await fs.ensureDir(pathModule.dirname(resolvedFile));
|
|
107
|
+
const timestamp = new Date().toISOString();
|
|
108
|
+
const normalizedGoal = `${goal || ''}`.replace(/\r?\n/g, ' ').trim();
|
|
109
|
+
const fields = [
|
|
110
|
+
timestamp,
|
|
111
|
+
`${metadata.status || ''}`.trim() || 'unknown',
|
|
112
|
+
`${metadata.program_status || ''}`.trim() || 'unknown',
|
|
113
|
+
`${metadata.gate_passed === true ? 'gate-pass' : 'gate-fail'}`,
|
|
114
|
+
normalizedGoal
|
|
115
|
+
];
|
|
116
|
+
await fs.appendFile(resolvedFile, `${fields.join('\t')}\n`, 'utf8');
|
|
117
|
+
return resolvedFile;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
resolveControllerQueueFile,
|
|
122
|
+
resolveControllerQueueFormat,
|
|
123
|
+
dedupeControllerGoals,
|
|
124
|
+
loadControllerGoalQueue,
|
|
125
|
+
writeControllerGoalQueue,
|
|
126
|
+
appendControllerGoalArchive
|
|
127
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
async function readCloseLoopControllerSessionEntries(projectPath, dependencies = {}) {
|
|
4
|
+
const { getCloseLoopControllerSessionDir, fs } = dependencies;
|
|
5
|
+
const sessionDir = getCloseLoopControllerSessionDir(projectPath);
|
|
6
|
+
if (!(await fs.pathExists(sessionDir))) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const files = (await fs.readdir(sessionDir)).filter((item) => item.toLowerCase().endsWith('.json'));
|
|
11
|
+
const sessions = [];
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const filePath = path.join(sessionDir, file);
|
|
14
|
+
const stats = await fs.stat(filePath);
|
|
15
|
+
const fallbackTimestamp = new Date(stats.mtimeMs).toISOString();
|
|
16
|
+
const fallbackId = path.basename(file, '.json');
|
|
17
|
+
let payload = null;
|
|
18
|
+
let parseError = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
payload = await fs.readJson(filePath);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
parseError = error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
sessions.push({
|
|
27
|
+
id: payload && typeof payload.controller_session === 'object' && typeof payload.controller_session.id === 'string'
|
|
28
|
+
? payload.controller_session.id
|
|
29
|
+
: fallbackId,
|
|
30
|
+
file: filePath,
|
|
31
|
+
status: payload && typeof payload.status === 'string'
|
|
32
|
+
? payload.status
|
|
33
|
+
: parseError
|
|
34
|
+
? 'invalid'
|
|
35
|
+
: 'unknown',
|
|
36
|
+
queue_file: payload && typeof payload.queue_file === 'string' ? payload.queue_file : null,
|
|
37
|
+
queue_format: payload && typeof payload.queue_format === 'string' ? payload.queue_format : null,
|
|
38
|
+
processed_goals: payload && Number.isFinite(Number(payload.processed_goals)) ? Number(payload.processed_goals) : null,
|
|
39
|
+
pending_goals: payload && Number.isFinite(Number(payload.pending_goals)) ? Number(payload.pending_goals) : null,
|
|
40
|
+
updated_at: payload && typeof payload.updated_at === 'string' ? payload.updated_at : fallbackTimestamp,
|
|
41
|
+
parse_error: parseError ? parseError.message : null,
|
|
42
|
+
mtime_ms: stats.mtimeMs
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
sessions.sort((a, b) => b.mtime_ms - a.mtime_ms);
|
|
47
|
+
return sessions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function resolveCloseLoopControllerSessionFile(projectPath, sessionCandidate, dependencies = {}) {
|
|
51
|
+
const { readCloseLoopControllerSessionEntries, getCloseLoopControllerSessionDir, sanitizeBatchSessionId, fs } = dependencies;
|
|
52
|
+
if (typeof sessionCandidate !== 'string' || !sessionCandidate.trim()) {
|
|
53
|
+
throw new Error('--controller-resume requires a session id/file or "latest".');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalizedCandidate = sessionCandidate.trim();
|
|
57
|
+
if (normalizedCandidate.toLowerCase() === 'latest') {
|
|
58
|
+
const sessions = await readCloseLoopControllerSessionEntries(projectPath, dependencies);
|
|
59
|
+
if (sessions.length === 0) {
|
|
60
|
+
throw new Error(`No controller sessions found in: ${getCloseLoopControllerSessionDir(projectPath)}`);
|
|
61
|
+
}
|
|
62
|
+
return sessions[0].file;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (path.isAbsolute(normalizedCandidate)) {
|
|
66
|
+
return normalizedCandidate;
|
|
67
|
+
}
|
|
68
|
+
if (normalizedCandidate.includes('/') || normalizedCandidate.includes('\\') || normalizedCandidate.toLowerCase().endsWith('.json')) {
|
|
69
|
+
return path.join(projectPath, normalizedCandidate);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const byId = path.join(getCloseLoopControllerSessionDir(projectPath), `${sanitizeBatchSessionId(normalizedCandidate)}.json`);
|
|
73
|
+
if (await fs.pathExists(byId)) {
|
|
74
|
+
return byId;
|
|
75
|
+
}
|
|
76
|
+
return path.join(projectPath, normalizedCandidate);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function loadCloseLoopControllerSessionPayload(projectPath, sessionCandidate, dependencies = {}) {
|
|
80
|
+
const { fs } = dependencies;
|
|
81
|
+
const sessionFile = await resolveCloseLoopControllerSessionFile(projectPath, sessionCandidate, dependencies);
|
|
82
|
+
if (!(await fs.pathExists(sessionFile))) {
|
|
83
|
+
throw new Error(`Controller session file not found: ${sessionFile}`);
|
|
84
|
+
}
|
|
85
|
+
let payload = null;
|
|
86
|
+
try {
|
|
87
|
+
payload = await fs.readJson(sessionFile);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Invalid controller session JSON: ${sessionFile} (${error.message})`);
|
|
90
|
+
}
|
|
91
|
+
const sessionId = payload && payload.controller_session && payload.controller_session.id
|
|
92
|
+
? payload.controller_session.id
|
|
93
|
+
: path.basename(sessionFile, '.json');
|
|
94
|
+
return {
|
|
95
|
+
id: sessionId,
|
|
96
|
+
file: sessionFile,
|
|
97
|
+
payload: payload || {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
readCloseLoopControllerSessionEntries,
|
|
103
|
+
resolveCloseLoopControllerSessionFile,
|
|
104
|
+
loadCloseLoopControllerSessionPayload
|
|
105
|
+
};
|