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,543 @@
1
+ const path = require('path');
2
+
3
+ function normalizeBatchFormat(formatCandidate) {
4
+ const normalized = typeof formatCandidate === 'string'
5
+ ? formatCandidate.trim().toLowerCase()
6
+ : 'auto';
7
+ if (!['auto', 'json', 'lines'].includes(normalized)) {
8
+ throw new Error('--format must be one of: auto, json, lines');
9
+ }
10
+ return normalized;
11
+ }
12
+
13
+ function parseGoalsFromJsonPayload(payload) {
14
+ if (Array.isArray(payload)) {
15
+ return payload;
16
+ }
17
+ if (payload && typeof payload === 'object' && Array.isArray(payload.goals)) {
18
+ return payload.goals;
19
+ }
20
+ throw new Error('JSON goals file must be an array of strings or an object with a "goals" array.');
21
+ }
22
+
23
+ function parseGoalsFromLines(content) {
24
+ return `${content || ''}`
25
+ .split(/\r?\n/)
26
+ .map(line => line.trim())
27
+ .filter(line => line && !line.startsWith('#'));
28
+ }
29
+
30
+ async function loadCloseLoopBatchGoals(projectPath, goalsFile, formatCandidate, dependencies = {}) {
31
+ const { fs, path: pathModule = path } = dependencies;
32
+ const resolvedFile = pathModule.isAbsolute(goalsFile)
33
+ ? goalsFile
34
+ : pathModule.join(projectPath, goalsFile);
35
+ if (!(await fs.pathExists(resolvedFile))) {
36
+ throw new Error(`Goals file not found: ${resolvedFile}`);
37
+ }
38
+
39
+ const format = normalizeBatchFormat(formatCandidate);
40
+ const isJsonByExtension = resolvedFile.toLowerCase().endsWith('.json');
41
+ const useJson = format === 'json' || (format === 'auto' && isJsonByExtension);
42
+ let goals = [];
43
+
44
+ if (useJson) {
45
+ let payload = null;
46
+ try {
47
+ payload = await fs.readJson(resolvedFile);
48
+ } catch (error) {
49
+ throw new Error(`Invalid JSON goals file: ${resolvedFile} (${error.message})`);
50
+ }
51
+ goals = parseGoalsFromJsonPayload(payload);
52
+ } else {
53
+ const content = await fs.readFile(resolvedFile, 'utf8');
54
+ goals = parseGoalsFromLines(content);
55
+ }
56
+
57
+ const normalizedGoals = goals
58
+ .map(item => `${item || ''}`.trim())
59
+ .filter(Boolean);
60
+ if (normalizedGoals.length === 0) {
61
+ throw new Error(`No valid goals found in file: ${resolvedFile}`);
62
+ }
63
+
64
+ return {
65
+ file: resolvedFile,
66
+ goals: normalizedGoals
67
+ };
68
+ }
69
+
70
+ const PROGRAM_CATEGORY_GOAL_LIBRARY = {
71
+ closeLoop: 'Build automatic closed-loop progression without manual confirmation waits for the program scope.',
72
+ decomposition: 'Split broad functional scope into coordinated master/sub specs with explicit dependency ownership.',
73
+ orchestration: 'Harden orchestration scheduling, parallel execution, and shared resource governance for multi-spec delivery.',
74
+ quality: 'Enforce quality gates, tests, and observability evidence across all autonomous execution tracks.',
75
+ docs: 'Complete documentation and rollout guidance so autonomous workflows can be repeatedly operated at scale.'
76
+ };
77
+ const DEFAULT_PROGRAM_DECOMPOSITION_MIN_QUALITY_SCORE = 70;
78
+
79
+ function normalizeProgramGoalCount(programGoalsCandidate, fallbackCount) {
80
+ if (programGoalsCandidate === undefined || programGoalsCandidate === null) {
81
+ return fallbackCount;
82
+ }
83
+
84
+ const parsed = Number(programGoalsCandidate);
85
+ if (!Number.isInteger(parsed) || parsed < 2 || parsed > 12) {
86
+ throw new Error('--program-goals must be an integer between 2 and 12.');
87
+ }
88
+ return parsed;
89
+ }
90
+
91
+ function inferProgramGoalCount(semantic) {
92
+ const clauseCount = Array.isArray(semantic && semantic.clauses) ? semantic.clauses.length : 0;
93
+ const activeCategories = semantic && semantic.categoryScores
94
+ ? Object.values(semantic.categoryScores).filter(score => score > 0).length
95
+ : 0;
96
+
97
+ if (clauseCount >= 8 || activeCategories >= 4) {
98
+ return 5;
99
+ }
100
+ if (clauseCount >= 5 || activeCategories >= 3) {
101
+ return 4;
102
+ }
103
+ return 3;
104
+ }
105
+
106
+ function normalizeProgramMinQualityScore(scoreCandidate) {
107
+ if (scoreCandidate === undefined || scoreCandidate === null) {
108
+ return DEFAULT_PROGRAM_DECOMPOSITION_MIN_QUALITY_SCORE;
109
+ }
110
+ const parsed = Number(scoreCandidate);
111
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 100) {
112
+ throw new Error('--program-min-quality-score must be a number between 0 and 100.');
113
+ }
114
+ return Number(parsed.toFixed(2));
115
+ }
116
+
117
+ function scoreProgramGoalClause(clause) {
118
+ const text = `${clause || ''}`.trim().toLowerCase();
119
+ if (!text) {
120
+ return 0;
121
+ }
122
+
123
+ const words = text.split(/\s+/).filter(Boolean).length;
124
+ const connectorSignals = (text.match(/,|;| and | with | plus |并且|以及|并行|同时/g) || []).length;
125
+ const domainSignals = (text.match(
126
+ /orchestrat|integration|migration|observability|quality|security|performance|resilience|compliance|governance|闭环|主从|并行|重规划/g
127
+ ) || []).length;
128
+ return words + (connectorSignals * 2) + (domainSignals * 3);
129
+ }
130
+
131
+ function buildProgramGoalDecompositionQuality(semantic, generatedGoals, targetGoalCount) {
132
+ const goals = Array.isArray(generatedGoals) ? generatedGoals : [];
133
+ const rankedCategories = Array.isArray(semantic && semantic.rankedCategories)
134
+ ? semantic.rankedCategories
135
+ : [];
136
+ const categoryScores = semantic && semantic.categoryScores && typeof semantic.categoryScores === 'object'
137
+ ? semantic.categoryScores
138
+ : {};
139
+ const activeCategoryCount = Object.values(categoryScores)
140
+ .filter(value => Number(value) > 0)
141
+ .length;
142
+ const averageGoalWords = goals.length === 0
143
+ ? 0
144
+ : Number((
145
+ goals
146
+ .map(goal => `${goal || ''}`.trim().split(/\s+/).filter(Boolean).length)
147
+ .reduce((sum, value) => sum + value, 0) / goals.length
148
+ ).toFixed(2));
149
+ const normalizedGoalSeeds = goals
150
+ .map(goal => `${goal || ''}`.toLowerCase().replace(/[0-9]+/g, '#').replace(/[^a-z\u4e00-\u9fff# ]+/g, ' '))
151
+ .map(goal => goal.split(/\s+/).filter(Boolean).slice(0, 8).join(' '))
152
+ .filter(Boolean);
153
+ const uniqueGoalSeeds = new Set(normalizedGoalSeeds);
154
+ const diversityRatio = goals.length === 0
155
+ ? 1
156
+ : Math.min(1, uniqueGoalSeeds.size / goals.length);
157
+ const coverageRatio = targetGoalCount <= 0
158
+ ? 1
159
+ : Math.min(1, goals.length / targetGoalCount);
160
+ const categoryCoverageRatio = activeCategoryCount <= 0
161
+ ? 1
162
+ : Math.min(1, rankedCategories.length / activeCategoryCount);
163
+ const warnings = [];
164
+ if (goals.length < targetGoalCount) {
165
+ warnings.push('under-produced-goals');
166
+ }
167
+ if (averageGoalWords < 6) {
168
+ warnings.push('goals-too-short');
169
+ }
170
+ if (activeCategoryCount >= 3 && rankedCategories.length < 2) {
171
+ warnings.push('category-coverage-low');
172
+ }
173
+ if (diversityRatio < 0.6) {
174
+ warnings.push('goal-diversity-low');
175
+ }
176
+
177
+ const score = Number((
178
+ (coverageRatio * 45) +
179
+ (categoryCoverageRatio * 25) +
180
+ (Math.min(1, averageGoalWords / 12) * 20) +
181
+ (diversityRatio * 10)
182
+ ).toFixed(2));
183
+ return {
184
+ score,
185
+ coverage_ratio_percent: Number((coverageRatio * 100).toFixed(2)),
186
+ category_coverage_ratio_percent: Number((categoryCoverageRatio * 100).toFixed(2)),
187
+ diversity_ratio_percent: Number((diversityRatio * 100).toFixed(2)),
188
+ average_goal_words: averageGoalWords,
189
+ warnings
190
+ };
191
+ }
192
+
193
+ function buildRefinedProgramGoalFromClause(clause, contextGoal) {
194
+ const normalizedClause = `${clause || ''}`.replace(/\s+/g, ' ').trim().replace(/[.。;;]+$/g, '');
195
+ if (!normalizedClause) {
196
+ return null;
197
+ }
198
+ return (
199
+ `Deliver ${normalizedClause} as a dedicated execution track with implementation tasks, ` +
200
+ `automated validation, and rollout evidence aligned to: ${contextGoal}`
201
+ );
202
+ }
203
+
204
+ function buildRefinedProgramGoalFromCategory(category, contextGoal) {
205
+ const template = PROGRAM_CATEGORY_GOAL_LIBRARY[category];
206
+ if (!template) {
207
+ return null;
208
+ }
209
+ return (
210
+ `${template} Ensure cross-spec coordination, measurable acceptance criteria, ` +
211
+ `and audit-ready output for: ${contextGoal}`
212
+ );
213
+ }
214
+
215
+ function shouldRefineProgramGoalQuality(quality, minQualityScore) {
216
+ const safeQuality = quality && typeof quality === 'object' ? quality : {};
217
+ const warnings = Array.isArray(safeQuality.warnings) ? safeQuality.warnings : [];
218
+ const score = Number(safeQuality.score);
219
+ if (Number.isFinite(score) && score < minQualityScore) {
220
+ return true;
221
+ }
222
+ return warnings.includes('goals-too-short') || warnings.includes('under-produced-goals');
223
+ }
224
+
225
+ function buildCloseLoopBatchGoalsFromGoal(goalCandidate, programGoalsCandidate, settings = {}, dependencies = {}) {
226
+ const { analyzeGoalSemantics } = dependencies;
227
+ const normalizedGoal = `${goalCandidate || ''}`.trim();
228
+ if (!normalizedGoal) {
229
+ throw new Error('--decompose-goal requires a non-empty goal string.');
230
+ }
231
+
232
+ const semantic = analyzeGoalSemantics(normalizedGoal);
233
+ const targetGoalCount = normalizeProgramGoalCount(
234
+ programGoalsCandidate,
235
+ inferProgramGoalCount(semantic)
236
+ );
237
+ const minQualityScore = normalizeProgramMinQualityScore(settings.minQualityScore);
238
+ const enforceQualityGate = Boolean(settings.enforceQualityGate);
239
+
240
+ const seenGoals = new Set();
241
+ const generatedGoals = [];
242
+ const pushGoal = goal => {
243
+ const normalized = `${goal || ''}`.trim();
244
+ if (!normalized) {
245
+ return;
246
+ }
247
+ const dedupeKey = normalized.toLowerCase();
248
+ if (seenGoals.has(dedupeKey)) {
249
+ return;
250
+ }
251
+ seenGoals.add(dedupeKey);
252
+ generatedGoals.push(normalized);
253
+ };
254
+
255
+ const scoredClauses = (semantic.clauses || [])
256
+ .map(clause => `${clause || ''}`.trim())
257
+ .filter(clause => clause.length >= 8)
258
+ .map(clause => ({
259
+ clause,
260
+ score: scoreProgramGoalClause(clause)
261
+ }))
262
+ .sort((left, right) => right.score - left.score);
263
+
264
+ for (const item of scoredClauses) {
265
+ if (generatedGoals.length >= targetGoalCount) {
266
+ break;
267
+ }
268
+ pushGoal(item.clause);
269
+ }
270
+
271
+ for (const category of semantic.rankedCategories || []) {
272
+ if (generatedGoals.length >= targetGoalCount) {
273
+ break;
274
+ }
275
+
276
+ const template = PROGRAM_CATEGORY_GOAL_LIBRARY[category];
277
+ if (!template) {
278
+ continue;
279
+ }
280
+ pushGoal(`${template} Program goal context: ${normalizedGoal}`);
281
+ }
282
+
283
+ if (generatedGoals.length === 0) {
284
+ pushGoal(normalizedGoal);
285
+ }
286
+ let finalGoals = generatedGoals.slice(0, targetGoalCount);
287
+ const initialQuality = buildProgramGoalDecompositionQuality(semantic, finalGoals, targetGoalCount);
288
+ let finalQuality = initialQuality;
289
+ let refinementApplied = false;
290
+ let refinementReason = null;
291
+
292
+ if (shouldRefineProgramGoalQuality(initialQuality, minQualityScore)) {
293
+ refinementReason = Number(initialQuality.score) < minQualityScore
294
+ ? 'score-below-threshold'
295
+ : 'quality-warning-triggered';
296
+ const refinedGoals = [];
297
+ const refinedSeen = new Set();
298
+ const pushRefinedGoal = goal => {
299
+ const normalized = `${goal || ''}`.trim();
300
+ if (!normalized) {
301
+ return;
302
+ }
303
+ const dedupeKey = normalized.toLowerCase();
304
+ if (refinedSeen.has(dedupeKey)) {
305
+ return;
306
+ }
307
+ refinedSeen.add(dedupeKey);
308
+ refinedGoals.push(normalized);
309
+ };
310
+
311
+ for (const item of scoredClauses) {
312
+ if (refinedGoals.length >= targetGoalCount) {
313
+ break;
314
+ }
315
+ pushRefinedGoal(buildRefinedProgramGoalFromClause(item.clause, normalizedGoal));
316
+ }
317
+
318
+ for (const category of semantic.rankedCategories || []) {
319
+ if (refinedGoals.length >= targetGoalCount) {
320
+ break;
321
+ }
322
+ pushRefinedGoal(buildRefinedProgramGoalFromCategory(category, normalizedGoal));
323
+ }
324
+
325
+ if (refinedGoals.length === 0) {
326
+ pushRefinedGoal(
327
+ `Execute ${normalizedGoal} with coordinated master/sub specs, quality gates, and completion evidence.`
328
+ );
329
+ }
330
+
331
+ while (refinedGoals.length < targetGoalCount) {
332
+ pushRefinedGoal(
333
+ `Track ${refinedGoals.length + 1}: Deliver ${normalizedGoal} with implementation tasks, ` +
334
+ 'integration checks, and operational handoff evidence.'
335
+ );
336
+ }
337
+
338
+ const refinedFinalGoals = refinedGoals.slice(0, targetGoalCount);
339
+ const refinedQuality = buildProgramGoalDecompositionQuality(semantic, refinedFinalGoals, targetGoalCount);
340
+ const refinedWarnings = Array.isArray(refinedQuality.warnings) ? refinedQuality.warnings.length : 0;
341
+ const initialWarnings = Array.isArray(initialQuality.warnings) ? initialQuality.warnings.length : 0;
342
+ if (
343
+ Number(refinedQuality.score) > Number(initialQuality.score) ||
344
+ (Number(refinedQuality.score) === Number(initialQuality.score) && refinedWarnings < initialWarnings)
345
+ ) {
346
+ finalGoals = refinedFinalGoals;
347
+ finalQuality = refinedQuality;
348
+ refinementApplied = true;
349
+ }
350
+ }
351
+
352
+ const quality = {
353
+ ...finalQuality,
354
+ refinement: {
355
+ attempted: shouldRefineProgramGoalQuality(initialQuality, minQualityScore),
356
+ applied: refinementApplied,
357
+ min_score: minQualityScore,
358
+ reason: refinementReason,
359
+ before_score: initialQuality.score,
360
+ after_score: finalQuality.score,
361
+ before_warnings: initialQuality.warnings,
362
+ after_warnings: finalQuality.warnings
363
+ }
364
+ };
365
+ if (enforceQualityGate && Number(quality.score) < minQualityScore) {
366
+ const warningText = Array.isArray(quality.warnings) && quality.warnings.length > 0
367
+ ? ` Warnings: ${quality.warnings.join(', ')}.`
368
+ : '';
369
+ throw new Error(
370
+ `Decomposition quality score ${quality.score} is below required ${minQualityScore}.${warningText}`
371
+ );
372
+ }
373
+
374
+ return {
375
+ file: '(generated-from-goal)',
376
+ goals: finalGoals,
377
+ generatedFromGoal: {
378
+ goal: normalizedGoal,
379
+ strategy: 'semantic-clause-and-category',
380
+ target_goal_count: targetGoalCount,
381
+ produced_goal_count: finalGoals.length,
382
+ clauses_considered: Array.isArray(semantic.clauses) ? semantic.clauses.length : 0,
383
+ category_scores: semantic.categoryScores || {},
384
+ ranked_categories: semantic.rankedCategories || [],
385
+ quality
386
+ }
387
+ };
388
+ }
389
+
390
+ function normalizeResumeStrategy(resumeStrategyCandidate) {
391
+ const normalized = typeof resumeStrategyCandidate === 'string'
392
+ ? resumeStrategyCandidate.trim().toLowerCase()
393
+ : 'pending';
394
+ if (!['pending', 'failed-only'].includes(normalized)) {
395
+ throw new Error('--resume-strategy must be one of: pending, failed-only');
396
+ }
397
+ return normalized;
398
+ }
399
+
400
+ async function buildCloseLoopBatchGoalsFromSummaryPayload(
401
+ summary,
402
+ summaryFile,
403
+ projectPath,
404
+ formatCandidate,
405
+ resumeStrategyCandidate,
406
+ dependencies = {}
407
+ ) {
408
+ const { fs, path: pathModule = path, loadCloseLoopBatchGoals } = dependencies;
409
+ const resumeStrategy = normalizeResumeStrategy(resumeStrategyCandidate);
410
+ if (!summary || typeof summary !== 'object') {
411
+ throw new Error(`Invalid batch summary payload: ${summaryFile}`);
412
+ }
413
+ if (!Array.isArray(summary.results)) {
414
+ throw new Error(`Batch summary missing "results" array: ${summaryFile}`);
415
+ }
416
+
417
+ const retryStatuses = resumeStrategy === 'failed-only'
418
+ ? new Set(['failed', 'error'])
419
+ : new Set(['failed', 'error', 'unknown', 'stopped', 'planned', 'prepared']);
420
+ const pendingByIndex = new Map();
421
+ for (const item of summary.results) {
422
+ if (!item || typeof item !== 'object') {
423
+ continue;
424
+ }
425
+ const status = typeof item.status === 'string' ? item.status.trim().toLowerCase() : '';
426
+ const goal = typeof item.goal === 'string' ? item.goal.trim() : '';
427
+ const index = Number(item.index);
428
+ if (!goal || !retryStatuses.has(status)) {
429
+ continue;
430
+ }
431
+ if (Number.isInteger(index) && index > 0) {
432
+ pendingByIndex.set(index, goal);
433
+ } else {
434
+ pendingByIndex.set(pendingByIndex.size + 1, goal);
435
+ }
436
+ }
437
+
438
+ let sourceGoals = null;
439
+ let resolvedGoalsFile = null;
440
+ if (typeof summary.goals_file === 'string' && summary.goals_file.trim()) {
441
+ const goalsFileCandidate = summary.goals_file.trim();
442
+ const isSyntheticGoalsFile = goalsFileCandidate.startsWith('(') && goalsFileCandidate.endsWith(')');
443
+ if (!isSyntheticGoalsFile) {
444
+ const resolvedGoalsCandidate = pathModule.isAbsolute(goalsFileCandidate)
445
+ ? goalsFileCandidate
446
+ : pathModule.join(projectPath, goalsFileCandidate);
447
+ if (await fs.pathExists(resolvedGoalsCandidate)) {
448
+ const loadedSource = await loadCloseLoopBatchGoals(projectPath, goalsFileCandidate, formatCandidate, { fs, path: pathModule });
449
+ sourceGoals = loadedSource.goals;
450
+ resolvedGoalsFile = loadedSource.file;
451
+ }
452
+ }
453
+ }
454
+
455
+ const totalGoals = Number(summary.total_goals);
456
+ const processedGoals = Number(summary.processed_goals);
457
+ if (
458
+ resumeStrategy === 'pending' &&
459
+ sourceGoals &&
460
+ Number.isInteger(totalGoals) &&
461
+ Number.isInteger(processedGoals) &&
462
+ processedGoals < totalGoals
463
+ ) {
464
+ const seenIndexes = new Set(
465
+ summary.results
466
+ .map(item => Number(item && item.index))
467
+ .filter(index => Number.isInteger(index) && index > 0)
468
+ );
469
+ for (let index = 1; index <= sourceGoals.length; index += 1) {
470
+ if (!seenIndexes.has(index)) {
471
+ pendingByIndex.set(index, sourceGoals[index - 1]);
472
+ }
473
+ }
474
+ }
475
+
476
+ const orderedPendingEntries = [...pendingByIndex.entries()]
477
+ .sort((a, b) => a[0] - b[0])
478
+ .map(([sourceIndex, goal]) => ({
479
+ goal,
480
+ sourceIndex: Math.max(0, sourceIndex - 1)
481
+ }));
482
+ if (orderedPendingEntries.length === 0) {
483
+ throw new Error(`No pending goals found in batch summary: ${summaryFile}`);
484
+ }
485
+
486
+ return {
487
+ file: resolvedGoalsFile || summary.goals_file || '(derived-from-summary)',
488
+ goals: orderedPendingEntries.map(item => item.goal),
489
+ goal_entries: orderedPendingEntries,
490
+ resumedFromSummary: {
491
+ file: summaryFile,
492
+ strategy: resumeStrategy,
493
+ previous_status: summary.status || null,
494
+ previous_total_goals: Number.isInteger(totalGoals) ? totalGoals : null,
495
+ previous_processed_goals: Number.isInteger(processedGoals) ? processedGoals : null
496
+ }
497
+ };
498
+ }
499
+
500
+ async function loadCloseLoopBatchGoalsFromSummary(
501
+ projectPath,
502
+ summaryCandidate,
503
+ formatCandidate,
504
+ resumeStrategyCandidate,
505
+ dependencies = {}
506
+ ) {
507
+ const { fs, resolveCloseLoopBatchSummaryFile, buildCloseLoopBatchGoalsFromSummaryPayload } = dependencies;
508
+ const summaryFile = await resolveCloseLoopBatchSummaryFile(projectPath, summaryCandidate);
509
+ if (!(await fs.pathExists(summaryFile))) {
510
+ throw new Error(`Batch summary file not found: ${summaryFile}`);
511
+ }
512
+
513
+ let summary = null;
514
+ try {
515
+ summary = await fs.readJson(summaryFile);
516
+ } catch (error) {
517
+ throw new Error(`Invalid batch summary JSON: ${summaryFile} (${error.message})`);
518
+ }
519
+
520
+ return buildCloseLoopBatchGoalsFromSummaryPayload(
521
+ summary,
522
+ summaryFile,
523
+ projectPath,
524
+ formatCandidate,
525
+ resumeStrategyCandidate,
526
+ {
527
+ ...dependencies,
528
+ fs,
529
+ loadCloseLoopBatchGoals
530
+ }
531
+ );
532
+ }
533
+
534
+ module.exports = {
535
+ normalizeBatchFormat,
536
+ parseGoalsFromJsonPayload,
537
+ parseGoalsFromLines,
538
+ loadCloseLoopBatchGoals,
539
+ buildCloseLoopBatchGoalsFromGoal,
540
+ normalizeResumeStrategy,
541
+ buildCloseLoopBatchGoalsFromSummaryPayload,
542
+ loadCloseLoopBatchGoalsFromSummary
543
+ };