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.
Files changed (83) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +119 -122
  3. package/README.zh.md +123 -121
  4. package/bin/scene-capability-engine.js +11 -0
  5. package/docs/README.md +21 -32
  6. package/docs/auto-refactor-index.md +384 -0
  7. package/docs/command-reference.md +94 -2
  8. package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
  9. package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
  10. package/docs/magicball-capability-iteration-api.md +2 -0
  11. package/docs/magicball-capability-iteration-ui.md +2 -0
  12. package/docs/magicball-capability-library.md +2 -0
  13. package/docs/magicball-cli-invocation-examples.md +336 -0
  14. package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
  15. package/docs/magicball-integration-doc-index.md +137 -0
  16. package/docs/magicball-integration-issue-tracker.md +218 -0
  17. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
  18. package/docs/magicball-sce-adaptation-guide.md +203 -0
  19. package/docs/magicball-three-mode-alignment-plan.md +551 -0
  20. package/docs/magicball-ui-surface-checklist.md +126 -0
  21. package/docs/magicball-write-auth-adaptation-guide.md +328 -0
  22. package/docs/refactor-completion-roadmap.md +116 -0
  23. package/docs/zh/README.md +27 -30
  24. package/docs/zh/refactor-completion-roadmap.md +116 -0
  25. package/lib/app/registry-config.js +73 -0
  26. package/lib/app/registry-sync-service.js +228 -0
  27. package/lib/auto/archive-schema-service.js +276 -0
  28. package/lib/auto/archive-summary.js +60 -0
  29. package/lib/auto/batch-goal-input-service.js +543 -0
  30. package/lib/auto/batch-output.js +201 -0
  31. package/lib/auto/batch-summary-storage-service.js +110 -0
  32. package/lib/auto/close-loop-batch-service.js +116 -0
  33. package/lib/auto/close-loop-controller-service.js +287 -0
  34. package/lib/auto/close-loop-program-service.js +283 -0
  35. package/lib/auto/close-loop-recovery-service.js +191 -0
  36. package/lib/auto/close-loop-session-storage-service.js +50 -0
  37. package/lib/auto/controller-lock-service.js +55 -0
  38. package/lib/auto/controller-output.js +32 -0
  39. package/lib/auto/controller-queue-service.js +127 -0
  40. package/lib/auto/controller-session-storage-service.js +105 -0
  41. package/lib/auto/governance-advisory-service.js +208 -0
  42. package/lib/auto/governance-close-loop-service.js +411 -0
  43. package/lib/auto/governance-maintenance-presenter.js +162 -0
  44. package/lib/auto/governance-maintenance-service.js +112 -0
  45. package/lib/auto/governance-session-presenter.js +70 -0
  46. package/lib/auto/governance-session-storage-service.js +198 -0
  47. package/lib/auto/governance-signals.js +139 -0
  48. package/lib/auto/governance-stats-presenter.js +337 -0
  49. package/lib/auto/governance-stats-service.js +115 -0
  50. package/lib/auto/governance-summary.js +703 -0
  51. package/lib/auto/handoff-capability-matrix-service.js +281 -0
  52. package/lib/auto/handoff-evidence-review-service.js +251 -0
  53. package/lib/auto/handoff-release-evidence-service.js +190 -0
  54. package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
  55. package/lib/auto/handoff-release-gate-history-service.js +257 -0
  56. package/lib/auto/handoff-reporting-service.js +1407 -0
  57. package/lib/auto/handoff-run-service.js +486 -0
  58. package/lib/auto/handoff-snapshots-service.js +645 -0
  59. package/lib/auto/observability-service.js +132 -0
  60. package/lib/auto/output-writer.js +34 -0
  61. package/lib/auto/program-auto-remediation-service.js +130 -0
  62. package/lib/auto/program-diagnostics.js +138 -0
  63. package/lib/auto/program-governance-helpers.js +306 -0
  64. package/lib/auto/program-governance-loop-service.js +413 -0
  65. package/lib/auto/program-output.js +106 -0
  66. package/lib/auto/program-summary.js +183 -0
  67. package/lib/auto/recovery-memory-service.js +684 -0
  68. package/lib/auto/recovery-selection-service.js +52 -0
  69. package/lib/auto/retention-policy.js +98 -0
  70. package/lib/auto/session-persistence-service.js +106 -0
  71. package/lib/auto/session-presenter.js +105 -0
  72. package/lib/auto/session-prune-service.js +190 -0
  73. package/lib/auto/session-query-service.js +249 -0
  74. package/lib/auto/spec-protection.js +141 -0
  75. package/lib/commands/app.js +911 -0
  76. package/lib/commands/assurance.js +212 -0
  77. package/lib/commands/auto.js +1091 -11063
  78. package/lib/commands/mode.js +321 -0
  79. package/lib/commands/ontology.js +415 -0
  80. package/lib/commands/pm.js +422 -0
  81. package/lib/ontology/seed-profiles.js +160 -0
  82. package/lib/state/sce-state-store.js +3369 -1200
  83. package/package.json +1 -1
@@ -0,0 +1,684 @@
1
+ const path = require('path');
2
+
3
+ function getCloseLoopRecoveryMemoryFile(projectPath, dependencies = {}) {
4
+ const pathModule = dependencies.path || path;
5
+ return pathModule.join(projectPath, '.sce', 'auto', 'close-loop-recovery-memory.json');
6
+ }
7
+
8
+ async function loadCloseLoopRecoveryMemory(projectPath, dependencies = {}) {
9
+ const { fs, getCloseLoopRecoveryMemoryFile } = dependencies;
10
+ const memoryFile = getCloseLoopRecoveryMemoryFile(projectPath);
11
+ const fallbackPayload = {
12
+ version: 1,
13
+ signatures: {}
14
+ };
15
+ if (!(await fs.pathExists(memoryFile))) {
16
+ return {
17
+ file: memoryFile,
18
+ payload: fallbackPayload
19
+ };
20
+ }
21
+
22
+ let payload = null;
23
+ try {
24
+ payload = await fs.readJson(memoryFile);
25
+ } catch (error) {
26
+ return {
27
+ file: memoryFile,
28
+ payload: fallbackPayload
29
+ };
30
+ }
31
+
32
+ if (!payload || typeof payload !== 'object') {
33
+ return {
34
+ file: memoryFile,
35
+ payload: fallbackPayload
36
+ };
37
+ }
38
+
39
+ return {
40
+ file: memoryFile,
41
+ payload: {
42
+ version: Number(payload.version) || 1,
43
+ signatures: payload.signatures && typeof payload.signatures === 'object'
44
+ ? payload.signatures
45
+ : {}
46
+ }
47
+ };
48
+ }
49
+
50
+ function normalizeRecoveryMemoryToken(value) {
51
+ return `${value || ''}`
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9]+/g, '-')
54
+ .replace(/^-+|-+$/g, '')
55
+ .slice(0, 120);
56
+ }
57
+
58
+ async function resolveRecoveryMemoryScope(projectPath, scopeCandidate, dependencies = {}) {
59
+ const { path: pathModule = path, normalizeRecoveryMemoryToken, resolveGitBranchToken } = dependencies;
60
+ const explicitScope = `${scopeCandidate || ''}`.trim();
61
+ if (explicitScope && explicitScope.toLowerCase() !== 'auto') {
62
+ return normalizeRecoveryMemoryToken(explicitScope) || 'default-scope';
63
+ }
64
+
65
+ const projectToken = normalizeRecoveryMemoryToken(pathModule.basename(pathModule.resolve(projectPath || '.'))) || 'project';
66
+ const branchToken = await resolveGitBranchToken(projectPath);
67
+ return `${projectToken}|${branchToken || 'default'}`;
68
+ }
69
+
70
+ async function resolveGitBranchToken(projectPath, dependencies = {}) {
71
+ const { path: pathModule = path, fs, normalizeRecoveryMemoryToken } = dependencies;
72
+ try {
73
+ const gitMetadataPath = pathModule.join(projectPath, '.git');
74
+ if (!(await fs.pathExists(gitMetadataPath))) {
75
+ return 'no-git';
76
+ }
77
+
78
+ let gitDir = gitMetadataPath;
79
+ const gitStat = await fs.stat(gitMetadataPath);
80
+ if (gitStat.isFile()) {
81
+ const pointer = await fs.readFile(gitMetadataPath, 'utf8');
82
+ const match = pointer.match(/gitdir:\s*(.+)/i);
83
+ if (match && match[1]) {
84
+ gitDir = pathModule.resolve(projectPath, match[1].trim());
85
+ }
86
+ }
87
+
88
+ const headFile = pathModule.join(gitDir, 'HEAD');
89
+ if (!(await fs.pathExists(headFile))) {
90
+ return 'no-head';
91
+ }
92
+ const headContent = `${await fs.readFile(headFile, 'utf8')}`.trim();
93
+ const refMatch = headContent.match(/^ref:\s+refs\/heads\/(.+)$/i);
94
+ if (refMatch && refMatch[1]) {
95
+ return normalizeRecoveryMemoryToken(refMatch[1]) || 'unknown-branch';
96
+ }
97
+ if (/^[a-f0-9]{7,40}$/i.test(headContent)) {
98
+ return `detached-${headContent.slice(0, 8).toLowerCase()}`;
99
+ }
100
+ return 'unknown-branch';
101
+ } catch (error) {
102
+ return 'unknown-branch';
103
+ }
104
+ }
105
+
106
+ function buildRecoveryMemorySignature(summaryPayload, context = {}, dependencies = {}) {
107
+ const { buildProgramDiagnostics } = dependencies;
108
+ const safeSummary = summaryPayload && typeof summaryPayload === 'object'
109
+ ? summaryPayload
110
+ : {};
111
+ const diagnostics = safeSummary.program_diagnostics && typeof safeSummary.program_diagnostics === 'object'
112
+ ? safeSummary.program_diagnostics
113
+ : buildProgramDiagnostics(safeSummary);
114
+ const clusters = Array.isArray(diagnostics.failure_clusters)
115
+ ? diagnostics.failure_clusters
116
+ : [];
117
+ const clusterSignature = clusters
118
+ .slice(0, 3)
119
+ .map(cluster => normalizeRecoveryMemoryToken(cluster && cluster.signature))
120
+ .filter(Boolean)
121
+ .join('|');
122
+ const scopeToken = normalizeRecoveryMemoryToken(context.scope || 'default-scope') || 'default-scope';
123
+ const modeToken = normalizeRecoveryMemoryToken(safeSummary.mode || 'unknown-mode');
124
+ const failedCount = Number(safeSummary.failed_goals) || 0;
125
+ const seed = clusterSignature || 'no-failure-cluster';
126
+ return `scope-${scopeToken}|${modeToken}|failed-${failedCount}|${seed}`;
127
+ }
128
+
129
+ function getRecoveryActionMemoryKey(action, index) {
130
+ const actionToken = normalizeRecoveryMemoryToken(action && action.action);
131
+ const commandToken = normalizeRecoveryMemoryToken(action && action.suggested_command);
132
+ const fallback = `action-${index}`;
133
+ return actionToken || commandToken
134
+ ? `${fallback}|${actionToken || 'none'}|${commandToken || 'none'}`
135
+ : fallback;
136
+ }
137
+
138
+ function selectRecoveryActionFromMemory(availableActions, recoveryMemoryEntry) {
139
+ if (
140
+ !recoveryMemoryEntry ||
141
+ typeof recoveryMemoryEntry !== 'object' ||
142
+ !recoveryMemoryEntry.actions ||
143
+ typeof recoveryMemoryEntry.actions !== 'object'
144
+ ) {
145
+ return null;
146
+ }
147
+
148
+ const candidates = [];
149
+ for (let index = 1; index <= availableActions.length; index += 1) {
150
+ const action = availableActions[index - 1];
151
+ const key = getRecoveryActionMemoryKey(action, index);
152
+ const stats = recoveryMemoryEntry.actions[key];
153
+ if (!stats || typeof stats !== 'object') {
154
+ continue;
155
+ }
156
+ const attempts = Number(stats.attempts) || 0;
157
+ const successes = Number(stats.successes) || 0;
158
+ if (attempts <= 0) {
159
+ continue;
160
+ }
161
+ const successRate = successes / attempts;
162
+ const score = (successRate * 100) + Math.min(25, attempts);
163
+ candidates.push({
164
+ index,
165
+ key,
166
+ score,
167
+ attempts,
168
+ successes,
169
+ failures: Number(stats.failures) || 0,
170
+ success_rate_percent: Number((successRate * 100).toFixed(2))
171
+ });
172
+ }
173
+
174
+ if (candidates.length === 0) {
175
+ return null;
176
+ }
177
+ candidates.sort((left, right) => {
178
+ if (right.score !== left.score) {
179
+ return right.score - left.score;
180
+ }
181
+ if (right.attempts !== left.attempts) {
182
+ return right.attempts - left.attempts;
183
+ }
184
+ return left.index - right.index;
185
+ });
186
+ const best = candidates[0];
187
+ return {
188
+ ...best,
189
+ selection_reason: 'highest memory score: success_rate_percent + bounded_attempt_bonus',
190
+ top_candidates: candidates.slice(0, 5)
191
+ };
192
+ }
193
+
194
+ function getRecoveryMemoryEntry(recoveryMemoryPayload, signature) {
195
+ if (
196
+ !recoveryMemoryPayload ||
197
+ typeof recoveryMemoryPayload !== 'object' ||
198
+ !recoveryMemoryPayload.signatures ||
199
+ typeof recoveryMemoryPayload.signatures !== 'object'
200
+ ) {
201
+ return null;
202
+ }
203
+ const signatureKey = `${signature || ''}`.trim();
204
+ if (!signatureKey) {
205
+ return null;
206
+ }
207
+ const entry = recoveryMemoryPayload.signatures[signatureKey];
208
+ return entry && typeof entry === 'object' ? entry : null;
209
+ }
210
+
211
+ async function updateCloseLoopRecoveryMemory(
212
+ projectPath,
213
+ recoveryMemory,
214
+ signature,
215
+ selectedIndex,
216
+ selectedAction,
217
+ finalStatus,
218
+ metadata = {},
219
+ dependencies = {}
220
+ ) {
221
+ const {
222
+ fs,
223
+ path: pathModule = path,
224
+ getCloseLoopRecoveryMemoryFile,
225
+ getRecoveryActionMemoryKey,
226
+ normalizeRecoveryMemoryToken
227
+ } = dependencies;
228
+ const memoryFile = recoveryMemory && typeof recoveryMemory.file === 'string'
229
+ ? recoveryMemory.file
230
+ : getCloseLoopRecoveryMemoryFile(projectPath);
231
+ const memoryPayload = recoveryMemory && recoveryMemory.payload && typeof recoveryMemory.payload === 'object'
232
+ ? recoveryMemory.payload
233
+ : {
234
+ version: 1,
235
+ signatures: {}
236
+ };
237
+ if (!memoryPayload.signatures || typeof memoryPayload.signatures !== 'object') {
238
+ memoryPayload.signatures = {};
239
+ }
240
+
241
+ const signatureKey = `${signature || ''}`.trim() || 'unknown-signature';
242
+ const selected = Number.isInteger(selectedIndex) && selectedIndex > 0 ? selectedIndex : 1;
243
+ const actionKey = getRecoveryActionMemoryKey(selectedAction || {}, selected);
244
+ const now = new Date().toISOString();
245
+ const scope = normalizeRecoveryMemoryToken(metadata.scope || '') || null;
246
+
247
+ if (!memoryPayload.signatures[signatureKey] || typeof memoryPayload.signatures[signatureKey] !== 'object') {
248
+ memoryPayload.signatures[signatureKey] = {
249
+ attempts: 0,
250
+ successes: 0,
251
+ failures: 0,
252
+ scope,
253
+ last_used_at: null,
254
+ last_selected_index: null,
255
+ actions: {}
256
+ };
257
+ }
258
+
259
+ const signatureEntry = memoryPayload.signatures[signatureKey];
260
+ if (!signatureEntry.actions || typeof signatureEntry.actions !== 'object') {
261
+ signatureEntry.actions = {};
262
+ }
263
+ if (!signatureEntry.actions[actionKey] || typeof signatureEntry.actions[actionKey] !== 'object') {
264
+ signatureEntry.actions[actionKey] = {
265
+ attempts: 0,
266
+ successes: 0,
267
+ failures: 0,
268
+ last_status: null,
269
+ last_used_at: null,
270
+ last_selected_index: selected
271
+ };
272
+ }
273
+
274
+ const actionEntry = signatureEntry.actions[actionKey];
275
+ const succeeded = `${finalStatus || ''}`.trim().toLowerCase() === 'completed';
276
+
277
+ signatureEntry.attempts = (Number(signatureEntry.attempts) || 0) + 1;
278
+ signatureEntry.successes = (Number(signatureEntry.successes) || 0) + (succeeded ? 1 : 0);
279
+ signatureEntry.failures = (Number(signatureEntry.failures) || 0) + (succeeded ? 0 : 1);
280
+ signatureEntry.scope = signatureEntry.scope || scope;
281
+ signatureEntry.last_used_at = now;
282
+ signatureEntry.last_selected_index = selected;
283
+
284
+ actionEntry.attempts = (Number(actionEntry.attempts) || 0) + 1;
285
+ actionEntry.successes = (Number(actionEntry.successes) || 0) + (succeeded ? 1 : 0);
286
+ actionEntry.failures = (Number(actionEntry.failures) || 0) + (succeeded ? 0 : 1);
287
+ actionEntry.last_status = `${finalStatus || 'unknown'}`;
288
+ actionEntry.last_used_at = now;
289
+ actionEntry.last_selected_index = selected;
290
+
291
+ await fs.ensureDir(pathModule.dirname(memoryFile));
292
+ await fs.writeJson(memoryFile, memoryPayload, { spaces: 2 });
293
+
294
+ return {
295
+ file: memoryFile,
296
+ signature: signatureKey,
297
+ action_key: actionKey,
298
+ scope: signatureEntry.scope || scope,
299
+ entry: actionEntry
300
+ };
301
+ }
302
+
303
+ function summarizeRecoveryMemory(memoryPayload) {
304
+ const signatures = memoryPayload && memoryPayload.signatures && typeof memoryPayload.signatures === 'object'
305
+ ? memoryPayload.signatures
306
+ : {};
307
+ const signatureKeys = Object.keys(signatures);
308
+ let actionCount = 0;
309
+ const scopeCounts = {};
310
+ for (const key of signatureKeys) {
311
+ const entry = signatures[key];
312
+ if (entry && entry.actions && typeof entry.actions === 'object') {
313
+ actionCount += Object.keys(entry.actions).length;
314
+ }
315
+ const scope = normalizeRecoveryMemoryToken(entry && entry.scope ? entry.scope : 'default-scope') || 'default-scope';
316
+ scopeCounts[scope] = (Number(scopeCounts[scope]) || 0) + 1;
317
+ }
318
+ return {
319
+ signature_count: signatureKeys.length,
320
+ action_count: actionCount,
321
+ scope_count: Object.keys(scopeCounts).length,
322
+ scopes: scopeCounts
323
+ };
324
+ }
325
+
326
+ function filterRecoveryMemoryByScope(memoryPayload, scopeCandidate) {
327
+ const normalizedScope = normalizeRecoveryMemoryToken(scopeCandidate);
328
+ if (!normalizedScope) {
329
+ return {
330
+ scope: null,
331
+ payload: memoryPayload
332
+ };
333
+ }
334
+
335
+ const source = memoryPayload && typeof memoryPayload === 'object'
336
+ ? memoryPayload
337
+ : { version: 1, signatures: {} };
338
+ const signatures = source.signatures && typeof source.signatures === 'object'
339
+ ? source.signatures
340
+ : {};
341
+ const filteredSignatures = {};
342
+ for (const [signature, entryRaw] of Object.entries(signatures)) {
343
+ const entry = entryRaw && typeof entryRaw === 'object' ? entryRaw : null;
344
+ if (!entry) {
345
+ continue;
346
+ }
347
+ const entryScope = normalizeRecoveryMemoryToken(entry.scope || 'default-scope') || 'default-scope';
348
+ if (entryScope !== normalizedScope) {
349
+ continue;
350
+ }
351
+ filteredSignatures[signature] = entry;
352
+ }
353
+
354
+ return {
355
+ scope: normalizedScope,
356
+ payload: {
357
+ version: Number(source.version) || 1,
358
+ signatures: filteredSignatures
359
+ }
360
+ };
361
+ }
362
+
363
+ function buildRecoveryMemoryScopeStats(memoryPayload) {
364
+ const signatures = memoryPayload && memoryPayload.signatures && typeof memoryPayload.signatures === 'object'
365
+ ? memoryPayload.signatures
366
+ : {};
367
+ const aggregates = new Map();
368
+ for (const entryRaw of Object.values(signatures)) {
369
+ const entry = entryRaw && typeof entryRaw === 'object' ? entryRaw : null;
370
+ if (!entry) {
371
+ continue;
372
+ }
373
+ const scope = normalizeRecoveryMemoryToken(entry.scope || 'default-scope') || 'default-scope';
374
+ if (!aggregates.has(scope)) {
375
+ aggregates.set(scope, {
376
+ scope,
377
+ signature_count: 0,
378
+ action_count: 0,
379
+ attempts: 0,
380
+ successes: 0,
381
+ failures: 0
382
+ });
383
+ }
384
+ const aggregate = aggregates.get(scope);
385
+ aggregate.signature_count += 1;
386
+ aggregate.attempts += Number(entry.attempts) || 0;
387
+ aggregate.successes += Number(entry.successes) || 0;
388
+ aggregate.failures += Number(entry.failures) || 0;
389
+ if (entry.actions && typeof entry.actions === 'object') {
390
+ aggregate.action_count += Object.keys(entry.actions).length;
391
+ }
392
+ }
393
+
394
+ return [...aggregates.values()]
395
+ .map(item => ({
396
+ ...item,
397
+ success_rate_percent: item.attempts > 0
398
+ ? Number(((item.successes / item.attempts) * 100).toFixed(2))
399
+ : 0
400
+ }))
401
+ .sort((left, right) => {
402
+ if (right.signature_count !== left.signature_count) {
403
+ return right.signature_count - left.signature_count;
404
+ }
405
+ return `${left.scope}`.localeCompare(`${right.scope}`);
406
+ });
407
+ }
408
+
409
+ function isIsoTimestampOlderThan(timestamp, cutoffMs) {
410
+ if (cutoffMs === null) {
411
+ return false;
412
+ }
413
+ if (typeof timestamp !== 'string' || !timestamp.trim()) {
414
+ return true;
415
+ }
416
+ const parsed = Date.parse(timestamp);
417
+ if (Number.isNaN(parsed)) {
418
+ return true;
419
+ }
420
+ return parsed < cutoffMs;
421
+ }
422
+
423
+ async function showCloseLoopRecoveryMemory(projectPath, options = {}, dependencies = {}) {
424
+ const { loadCloseLoopRecoveryMemory, filterRecoveryMemoryByScope, summarizeRecoveryMemory } = dependencies;
425
+ const recoveryMemory = await loadCloseLoopRecoveryMemory(projectPath);
426
+ const filtered = filterRecoveryMemoryByScope(recoveryMemory.payload, options.scope);
427
+ return {
428
+ mode: 'auto-recovery-memory-show',
429
+ file: recoveryMemory.file,
430
+ scope: filtered.scope,
431
+ stats: summarizeRecoveryMemory(filtered.payload),
432
+ payload: filtered.payload
433
+ };
434
+ }
435
+
436
+ async function showCloseLoopRecoveryMemoryScopes(projectPath, dependencies = {}) {
437
+ const { loadCloseLoopRecoveryMemory, buildRecoveryMemoryScopeStats } = dependencies;
438
+ const recoveryMemory = await loadCloseLoopRecoveryMemory(projectPath);
439
+ const scopes = buildRecoveryMemoryScopeStats(recoveryMemory.payload);
440
+ return {
441
+ mode: 'auto-recovery-memory-scopes',
442
+ file: recoveryMemory.file,
443
+ total_scopes: scopes.length,
444
+ scopes
445
+ };
446
+ }
447
+
448
+ async function pruneCloseLoopRecoveryMemory(projectPath, options = {}, dependencies = {}) {
449
+ const {
450
+ fs,
451
+ path: pathModule = path,
452
+ Date: DateCtor = Date,
453
+ normalizeRecoveryMemoryTtlDays,
454
+ loadCloseLoopRecoveryMemory,
455
+ filterRecoveryMemoryByScope,
456
+ summarizeRecoveryMemory
457
+ } = dependencies;
458
+ const olderThanDays = normalizeRecoveryMemoryTtlDays(options.olderThanDays === undefined ? 30 : options.olderThanDays);
459
+ const scope = normalizeRecoveryMemoryToken(options.scope || '') || null;
460
+ const dryRun = Boolean(options.dryRun);
461
+ const recoveryMemory = await loadCloseLoopRecoveryMemory(projectPath);
462
+ const memoryPayload = recoveryMemory.payload && typeof recoveryMemory.payload === 'object'
463
+ ? recoveryMemory.payload
464
+ : { version: 1, signatures: {} };
465
+ if (!memoryPayload.signatures || typeof memoryPayload.signatures !== 'object') {
466
+ memoryPayload.signatures = {};
467
+ }
468
+
469
+ const cutoffMs = olderThanDays === null
470
+ ? null
471
+ : DateCtor.now() - (olderThanDays * 24 * 60 * 60 * 1000);
472
+ const filteredBeforePayload = scope
473
+ ? filterRecoveryMemoryByScope(memoryPayload, scope).payload
474
+ : memoryPayload;
475
+ const signaturesBefore = summarizeRecoveryMemory(filteredBeforePayload).signature_count;
476
+ const actionBefore = summarizeRecoveryMemory(filteredBeforePayload).action_count;
477
+
478
+ const retainedSignatures = {};
479
+ for (const [signature, entryRaw] of Object.entries(memoryPayload.signatures)) {
480
+ const entry = entryRaw && typeof entryRaw === 'object' ? { ...entryRaw } : null;
481
+ if (!entry) {
482
+ continue;
483
+ }
484
+ const entryScope = normalizeRecoveryMemoryToken(entry.scope || 'default-scope') || 'default-scope';
485
+ if (scope && entryScope !== scope) {
486
+ retainedSignatures[signature] = entry;
487
+ continue;
488
+ }
489
+ const actions = entry.actions && typeof entry.actions === 'object' ? entry.actions : {};
490
+ const retainedActions = {};
491
+ for (const [actionKey, actionStatsRaw] of Object.entries(actions)) {
492
+ const actionStats = actionStatsRaw && typeof actionStatsRaw === 'object' ? actionStatsRaw : null;
493
+ if (!actionStats) {
494
+ continue;
495
+ }
496
+ if (!isIsoTimestampOlderThan(actionStats.last_used_at, cutoffMs)) {
497
+ retainedActions[actionKey] = actionStats;
498
+ }
499
+ }
500
+
501
+ if (Object.keys(retainedActions).length > 0 || !isIsoTimestampOlderThan(entry.last_used_at, cutoffMs)) {
502
+ retainedSignatures[signature] = {
503
+ ...entry,
504
+ actions: retainedActions
505
+ };
506
+ }
507
+ }
508
+
509
+ const nextPayload = {
510
+ version: Number(memoryPayload.version) || 1,
511
+ signatures: retainedSignatures
512
+ };
513
+ const filteredAfterPayload = scope
514
+ ? filterRecoveryMemoryByScope(nextPayload, scope).payload
515
+ : nextPayload;
516
+ const signaturesAfter = summarizeRecoveryMemory(filteredAfterPayload).signature_count;
517
+ const actionAfter = summarizeRecoveryMemory(filteredAfterPayload).action_count;
518
+ if (!dryRun) {
519
+ await fs.ensureDir(pathModule.dirname(recoveryMemory.file));
520
+ await fs.writeJson(recoveryMemory.file, nextPayload, { spaces: 2 });
521
+ }
522
+
523
+ return {
524
+ mode: 'auto-recovery-memory-prune',
525
+ file: recoveryMemory.file,
526
+ scope,
527
+ dry_run: dryRun,
528
+ older_than_days: olderThanDays,
529
+ signatures_before: signaturesBefore,
530
+ signatures_after: signaturesAfter,
531
+ actions_before: actionBefore,
532
+ actions_after: actionAfter,
533
+ signatures_removed: Math.max(0, signaturesBefore - signaturesAfter),
534
+ actions_removed: Math.max(0, actionBefore - actionAfter)
535
+ };
536
+ }
537
+
538
+ async function clearCloseLoopRecoveryMemory(projectPath, dependencies = {}) {
539
+ const { fs, getCloseLoopRecoveryMemoryFile } = dependencies;
540
+ const recoveryMemoryFile = getCloseLoopRecoveryMemoryFile(projectPath);
541
+ const existed = await fs.pathExists(recoveryMemoryFile);
542
+ if (existed) {
543
+ await fs.remove(recoveryMemoryFile);
544
+ }
545
+ return {
546
+ mode: 'auto-recovery-memory-clear',
547
+ file: recoveryMemoryFile,
548
+ existed,
549
+ cleared: true
550
+ };
551
+ }
552
+
553
+ function resolveRecoveryActionSelection(summaryPayload, actionCandidate, context = {}, dependencies = {}) {
554
+ const {
555
+ buildProgramDiagnostics,
556
+ buildProgramRemediationActions,
557
+ normalizeRecoveryActionIndex,
558
+ selectRecoveryActionFromMemory
559
+ } = dependencies;
560
+ const diagnostics = summaryPayload && summaryPayload.program_diagnostics && typeof summaryPayload.program_diagnostics === 'object'
561
+ ? summaryPayload.program_diagnostics
562
+ : buildProgramDiagnostics(summaryPayload || {});
563
+ const availableActions = Array.isArray(diagnostics.remediation_actions) && diagnostics.remediation_actions.length > 0
564
+ ? diagnostics.remediation_actions
565
+ : buildProgramRemediationActions(summaryPayload || {}, []);
566
+ const optionLabel = typeof context.optionLabel === 'string' && context.optionLabel.trim()
567
+ ? context.optionLabel.trim()
568
+ : '--use-action';
569
+ let selectedIndex = null;
570
+ let selectionSource = 'default';
571
+ let memorySelection = null;
572
+ let selectionExplain = null;
573
+ if (actionCandidate !== undefined && actionCandidate !== null) {
574
+ selectedIndex = normalizeRecoveryActionIndex(actionCandidate, availableActions.length, optionLabel);
575
+ selectionSource = 'explicit';
576
+ selectionExplain = {
577
+ mode: 'explicit',
578
+ reason: `${optionLabel} provided`,
579
+ selected_index: selectedIndex
580
+ };
581
+ } else {
582
+ memorySelection = selectRecoveryActionFromMemory(availableActions, context.recoveryMemoryEntry);
583
+ if (memorySelection) {
584
+ selectedIndex = memorySelection.index;
585
+ selectionSource = 'memory';
586
+ selectionExplain = {
587
+ mode: 'memory',
588
+ reason: memorySelection.selection_reason,
589
+ selected_index: selectedIndex,
590
+ candidate_count: memorySelection.top_candidates.length,
591
+ top_candidates: memorySelection.top_candidates
592
+ };
593
+ } else {
594
+ selectedIndex = normalizeRecoveryActionIndex(undefined, availableActions.length);
595
+ selectionExplain = {
596
+ mode: 'default',
597
+ reason: 'no matching memory entry found for current signature',
598
+ selected_index: selectedIndex
599
+ };
600
+ }
601
+ }
602
+ const selectedAction = availableActions[selectedIndex - 1] || null;
603
+ const appliedPatch = selectedAction && selectedAction.strategy_patch && typeof selectedAction.strategy_patch === 'object'
604
+ ? { ...selectedAction.strategy_patch }
605
+ : {};
606
+
607
+ return {
608
+ selectedIndex,
609
+ selectedAction,
610
+ availableActions,
611
+ appliedPatch,
612
+ selectionSource,
613
+ memorySelection,
614
+ selectionExplain
615
+ };
616
+ }
617
+
618
+ function applyRecoveryActionPatch(options, selectedAction) {
619
+ const baseOptions = { ...options };
620
+ const patch = selectedAction && selectedAction.strategy_patch && typeof selectedAction.strategy_patch === 'object'
621
+ ? selectedAction.strategy_patch
622
+ : {};
623
+ const merged = { ...baseOptions };
624
+
625
+ if (patch.batchAutonomous !== undefined) {
626
+ merged.batchAutonomous = Boolean(patch.batchAutonomous);
627
+ }
628
+ if (patch.continueOnError !== undefined && merged.continueOnError === undefined) {
629
+ merged.continueOnError = Boolean(patch.continueOnError);
630
+ }
631
+ if (patch.batchParallel !== undefined && (merged.batchParallel === undefined || merged.batchParallel === null)) {
632
+ merged.batchParallel = Number(patch.batchParallel);
633
+ }
634
+ if (patch.batchAgentBudget !== undefined && (merged.batchAgentBudget === undefined || merged.batchAgentBudget === null)) {
635
+ merged.batchAgentBudget = Number(patch.batchAgentBudget);
636
+ }
637
+ if (patch.batchPriority !== undefined && (!merged.batchPriority || `${merged.batchPriority}`.trim().toLowerCase() === 'fifo')) {
638
+ merged.batchPriority = patch.batchPriority;
639
+ }
640
+ if (patch.batchAgingFactor !== undefined && (merged.batchAgingFactor === undefined || merged.batchAgingFactor === null)) {
641
+ merged.batchAgingFactor = Number(patch.batchAgingFactor);
642
+ }
643
+ if (patch.batchRetryRounds !== undefined && (merged.batchRetryRounds === undefined || merged.batchRetryRounds === null)) {
644
+ merged.batchRetryRounds = Number(patch.batchRetryRounds);
645
+ }
646
+ if (patch.batchRetryUntilComplete !== undefined && merged.batchRetryUntilComplete === undefined) {
647
+ merged.batchRetryUntilComplete = Boolean(patch.batchRetryUntilComplete);
648
+ }
649
+ if (patch.batchRetryMaxRounds !== undefined && (merged.batchRetryMaxRounds === undefined || merged.batchRetryMaxRounds === null)) {
650
+ merged.batchRetryMaxRounds = Number(patch.batchRetryMaxRounds);
651
+ }
652
+ if (patch.dodTests !== undefined && !merged.dodTests) {
653
+ merged.dodTests = patch.dodTests;
654
+ }
655
+ if (patch.dodTasksClosed !== undefined && merged.dodTasksClosed === undefined) {
656
+ merged.dodTasksClosed = Boolean(patch.dodTasksClosed);
657
+ }
658
+
659
+ return merged;
660
+ }
661
+
662
+
663
+
664
+ module.exports = {
665
+ getCloseLoopRecoveryMemoryFile,
666
+ loadCloseLoopRecoveryMemory,
667
+ normalizeRecoveryMemoryToken,
668
+ resolveRecoveryMemoryScope,
669
+ resolveGitBranchToken,
670
+ buildRecoveryMemorySignature,
671
+ getRecoveryActionMemoryKey,
672
+ selectRecoveryActionFromMemory,
673
+ getRecoveryMemoryEntry,
674
+ updateCloseLoopRecoveryMemory,
675
+ summarizeRecoveryMemory,
676
+ filterRecoveryMemoryByScope,
677
+ buildRecoveryMemoryScopeStats,
678
+ showCloseLoopRecoveryMemory,
679
+ showCloseLoopRecoveryMemoryScopes,
680
+ pruneCloseLoopRecoveryMemory,
681
+ clearCloseLoopRecoveryMemory,
682
+ resolveRecoveryActionSelection,
683
+ applyRecoveryActionPatch
684
+ };