popeye-cli 1.7.0 → 1.9.0

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 (174) hide show
  1. package/README.md +148 -7
  2. package/cheatsheet.md +440 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +3 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +3 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/commands/review.d.ts +31 -0
  16. package/dist/cli/commands/review.d.ts.map +1 -0
  17. package/dist/cli/commands/review.js +156 -0
  18. package/dist/cli/commands/review.js.map +1 -0
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +4 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/interactive.d.ts.map +1 -1
  23. package/dist/cli/interactive.js +218 -61
  24. package/dist/cli/interactive.js.map +1 -1
  25. package/dist/generators/admin-wizard.d.ts +25 -0
  26. package/dist/generators/admin-wizard.d.ts.map +1 -0
  27. package/dist/generators/admin-wizard.js +123 -0
  28. package/dist/generators/admin-wizard.js.map +1 -0
  29. package/dist/generators/all.d.ts.map +1 -1
  30. package/dist/generators/all.js +10 -3
  31. package/dist/generators/all.js.map +1 -1
  32. package/dist/generators/database.d.ts +58 -0
  33. package/dist/generators/database.d.ts.map +1 -0
  34. package/dist/generators/database.js +229 -0
  35. package/dist/generators/database.js.map +1 -0
  36. package/dist/generators/fullstack.d.ts.map +1 -1
  37. package/dist/generators/fullstack.js +23 -7
  38. package/dist/generators/fullstack.js.map +1 -1
  39. package/dist/generators/index.d.ts +2 -0
  40. package/dist/generators/index.d.ts.map +1 -1
  41. package/dist/generators/index.js +2 -0
  42. package/dist/generators/index.js.map +1 -1
  43. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  44. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-python.js +425 -0
  46. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  47. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  48. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  49. package/dist/generators/templates/admin-wizard-react.js +554 -0
  50. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  51. package/dist/generators/templates/database-docker.d.ts +23 -0
  52. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  53. package/dist/generators/templates/database-docker.js +221 -0
  54. package/dist/generators/templates/database-docker.js.map +1 -0
  55. package/dist/generators/templates/database-python.d.ts +54 -0
  56. package/dist/generators/templates/database-python.d.ts.map +1 -0
  57. package/dist/generators/templates/database-python.js +723 -0
  58. package/dist/generators/templates/database-python.js.map +1 -0
  59. package/dist/generators/templates/database-typescript.d.ts +34 -0
  60. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  61. package/dist/generators/templates/database-typescript.js +232 -0
  62. package/dist/generators/templates/database-typescript.js.map +1 -0
  63. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  64. package/dist/generators/templates/fullstack.js +29 -0
  65. package/dist/generators/templates/fullstack.js.map +1 -1
  66. package/dist/generators/templates/index.d.ts +5 -0
  67. package/dist/generators/templates/index.d.ts.map +1 -1
  68. package/dist/generators/templates/index.js +5 -0
  69. package/dist/generators/templates/index.js.map +1 -1
  70. package/dist/state/index.d.ts +10 -0
  71. package/dist/state/index.d.ts.map +1 -1
  72. package/dist/state/index.js +21 -0
  73. package/dist/state/index.js.map +1 -1
  74. package/dist/types/audit.d.ts +623 -0
  75. package/dist/types/audit.d.ts.map +1 -0
  76. package/dist/types/audit.js +240 -0
  77. package/dist/types/audit.js.map +1 -0
  78. package/dist/types/database-runtime.d.ts +86 -0
  79. package/dist/types/database-runtime.d.ts.map +1 -0
  80. package/dist/types/database-runtime.js +61 -0
  81. package/dist/types/database-runtime.js.map +1 -0
  82. package/dist/types/database.d.ts +85 -0
  83. package/dist/types/database.d.ts.map +1 -0
  84. package/dist/types/database.js +71 -0
  85. package/dist/types/database.js.map +1 -0
  86. package/dist/types/index.d.ts +2 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +4 -0
  89. package/dist/types/index.js.map +1 -1
  90. package/dist/types/workflow.d.ts +36 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +7 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/audit-analyzer.d.ts +58 -0
  95. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  96. package/dist/workflow/audit-analyzer.js +420 -0
  97. package/dist/workflow/audit-analyzer.js.map +1 -0
  98. package/dist/workflow/audit-mode.d.ts +28 -0
  99. package/dist/workflow/audit-mode.d.ts.map +1 -0
  100. package/dist/workflow/audit-mode.js +169 -0
  101. package/dist/workflow/audit-mode.js.map +1 -0
  102. package/dist/workflow/audit-recovery.d.ts +61 -0
  103. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  104. package/dist/workflow/audit-recovery.js +242 -0
  105. package/dist/workflow/audit-recovery.js.map +1 -0
  106. package/dist/workflow/audit-reporter.d.ts +65 -0
  107. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  108. package/dist/workflow/audit-reporter.js +301 -0
  109. package/dist/workflow/audit-reporter.js.map +1 -0
  110. package/dist/workflow/audit-scanner.d.ts +87 -0
  111. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  112. package/dist/workflow/audit-scanner.js +768 -0
  113. package/dist/workflow/audit-scanner.js.map +1 -0
  114. package/dist/workflow/db-setup-runner.d.ts +63 -0
  115. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  116. package/dist/workflow/db-setup-runner.js +336 -0
  117. package/dist/workflow/db-setup-runner.js.map +1 -0
  118. package/dist/workflow/db-state-machine.d.ts +30 -0
  119. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  120. package/dist/workflow/db-state-machine.js +51 -0
  121. package/dist/workflow/db-state-machine.js.map +1 -0
  122. package/dist/workflow/index.d.ts +7 -0
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +7 -0
  125. package/dist/workflow/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/commands/db.ts +281 -0
  128. package/src/cli/commands/doctor.ts +273 -0
  129. package/src/cli/commands/index.ts +3 -0
  130. package/src/cli/commands/review.ts +187 -0
  131. package/src/cli/index.ts +6 -0
  132. package/src/cli/interactive.ts +174 -4
  133. package/src/generators/admin-wizard.ts +146 -0
  134. package/src/generators/all.ts +10 -3
  135. package/src/generators/database.ts +286 -0
  136. package/src/generators/fullstack.ts +26 -9
  137. package/src/generators/index.ts +12 -0
  138. package/src/generators/templates/admin-wizard-python.ts +431 -0
  139. package/src/generators/templates/admin-wizard-react.ts +560 -0
  140. package/src/generators/templates/database-docker.ts +227 -0
  141. package/src/generators/templates/database-python.ts +734 -0
  142. package/src/generators/templates/database-typescript.ts +238 -0
  143. package/src/generators/templates/fullstack.ts +29 -0
  144. package/src/generators/templates/index.ts +5 -0
  145. package/src/state/index.ts +28 -0
  146. package/src/types/audit.ts +294 -0
  147. package/src/types/database-runtime.ts +69 -0
  148. package/src/types/database.ts +84 -0
  149. package/src/types/index.ts +29 -0
  150. package/src/types/workflow.ts +20 -0
  151. package/src/workflow/audit-analyzer.ts +491 -0
  152. package/src/workflow/audit-mode.ts +240 -0
  153. package/src/workflow/audit-recovery.ts +284 -0
  154. package/src/workflow/audit-reporter.ts +370 -0
  155. package/src/workflow/audit-scanner.ts +873 -0
  156. package/src/workflow/db-setup-runner.ts +391 -0
  157. package/src/workflow/db-state-machine.ts +58 -0
  158. package/src/workflow/index.ts +7 -0
  159. package/tests/cli/commands/review.test.ts +52 -0
  160. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  161. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  162. package/tests/generators/cross-phase-integration.test.ts +383 -0
  163. package/tests/generators/database.test.ts +456 -0
  164. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  165. package/tests/types/audit.test.ts +250 -0
  166. package/tests/types/database-runtime.test.ts +158 -0
  167. package/tests/types/database.test.ts +187 -0
  168. package/tests/workflow/audit-analyzer.test.ts +281 -0
  169. package/tests/workflow/audit-mode.test.ts +114 -0
  170. package/tests/workflow/audit-recovery.test.ts +237 -0
  171. package/tests/workflow/audit-reporter.test.ts +254 -0
  172. package/tests/workflow/audit-scanner.test.ts +270 -0
  173. package/tests/workflow/db-setup-runner.test.ts +211 -0
  174. package/tests/workflow/db-state-machine.test.ts +117 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Audit mode orchestrator.
3
+ *
4
+ * Coordinates the three audit stages:
5
+ * Stage 1: Scan + Summary
6
+ * Stage 2: Analyze + Report
7
+ * Stage 3: Recovery (evidence-based trigger)
8
+ *
9
+ * Pattern follows plan-mode.ts orchestration style.
10
+ */
11
+
12
+ import { randomUUID } from 'node:crypto';
13
+ import { loadProject, updateState } from '../state/index.js';
14
+ import type { ConsensusConfig } from '../types/consensus.js';
15
+ import type { ProjectState } from '../types/workflow.js';
16
+ import type {
17
+ AuditModeOptions,
18
+ AuditModeResult,
19
+ RecoveryPlan,
20
+ } from '../types/audit.js';
21
+ import { scanProject } from './audit-scanner.js';
22
+ import { analyzeProject, calculateAuditScores } from './audit-analyzer.js';
23
+ import {
24
+ buildSummaryReport,
25
+ buildAuditReport,
26
+ writeAuditMarkdown,
27
+ writeAuditJson,
28
+ writeRecoveryMarkdown,
29
+ writeRecoveryJson,
30
+ } from './audit-reporter.js';
31
+ import {
32
+ shouldTriggerRecovery,
33
+ generateRecoveryPlan,
34
+ recoveryToMilestones,
35
+ injectRecoveryIntoState,
36
+ } from './audit-recovery.js';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Options
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface AuditModeRunOptions extends AuditModeOptions {
43
+ consensusConfig?: Partial<ConsensusConfig>;
44
+ onProgress?: (stage: string, message: string) => void;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Orchestrator
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Run a full audit of the project.
53
+ *
54
+ * Stage 1: Scan the filesystem deterministically
55
+ * Stage 2: Analyze with AI (Serena-first + fallback)
56
+ * Stage 3: Generate recovery plan if evidence warrants it
57
+ *
58
+ * @param options - Audit configuration options.
59
+ * @returns The complete audit result.
60
+ */
61
+ export async function runAuditMode(options: AuditModeRunOptions): Promise<AuditModeResult> {
62
+ const { projectDir, onProgress } = options;
63
+ const depth = options.depth ?? 2;
64
+ const strict = options.strict ?? false;
65
+ const format = options.format ?? 'both';
66
+ const autoRecover = options.autoRecover ?? true;
67
+ const auditRunId = randomUUID();
68
+
69
+ let state: ProjectState;
70
+ try {
71
+ state = await loadProject(projectDir);
72
+ } catch (err) {
73
+ const errorMsg = err instanceof Error ? err.message : 'Failed to load project';
74
+ return makeErrorResult(errorMsg);
75
+ }
76
+
77
+ // -----------------------------------------------------------------------
78
+ // Stage 1: Scan + Summary
79
+ // -----------------------------------------------------------------------
80
+ onProgress?.('stage-1', 'Starting project scan...');
81
+
82
+ const scan = await scanProject(
83
+ projectDir,
84
+ state.language,
85
+ (msg) => onProgress?.('stage-1', msg)
86
+ );
87
+
88
+ const summary = buildSummaryReport(scan, state);
89
+
90
+ onProgress?.(
91
+ 'stage-1',
92
+ `Scan complete: ${scan.totalSourceFiles} source files, ${scan.totalLinesOfCode} LOC, ${scan.components.length} component(s)`
93
+ );
94
+
95
+ // -----------------------------------------------------------------------
96
+ // Stage 2: Analyze + Report
97
+ // -----------------------------------------------------------------------
98
+ onProgress?.('stage-2', 'Starting AI analysis...');
99
+
100
+ const { findings, searchMetadata } = await analyzeProject(scan, state, {
101
+ depth,
102
+ strict,
103
+ projectDir,
104
+ });
105
+
106
+ const scores = calculateAuditScores(findings, scan);
107
+
108
+ const auditReport = buildAuditReport(
109
+ summary,
110
+ findings,
111
+ scores,
112
+ searchMetadata,
113
+ { strict },
114
+ auditRunId
115
+ );
116
+
117
+ // Write report artifacts
118
+ const reportPaths: AuditModeResult['reportPaths'] = {};
119
+
120
+ if (format === 'md' || format === 'both') {
121
+ reportPaths.auditMd = await writeAuditMarkdown(projectDir, auditReport);
122
+ }
123
+ if (format === 'json' || format === 'both') {
124
+ reportPaths.auditJson = await writeAuditJson(projectDir, auditReport);
125
+ }
126
+
127
+ // Update state with audit report path
128
+ await updateState(projectDir, {
129
+ auditReportPath: reportPaths.auditJson ?? reportPaths.auditMd,
130
+ auditLastRunAt: new Date().toISOString(),
131
+ auditRunId,
132
+ } as Partial<ProjectState>);
133
+
134
+ onProgress?.(
135
+ 'stage-2',
136
+ `Analysis complete: score ${scores.overallScore}%, ${findings.length} findings (serena: ${searchMetadata.serenaUsed ? 'used' : 'fallback'})`
137
+ );
138
+
139
+ // -----------------------------------------------------------------------
140
+ // Stage 3: Recovery (evidence-based trigger)
141
+ // -----------------------------------------------------------------------
142
+ let recovery: RecoveryPlan | undefined;
143
+
144
+ if (shouldTriggerRecovery(auditReport, strict)) {
145
+ onProgress?.('stage-3', 'Generating recovery plan...');
146
+
147
+ recovery = generateRecoveryPlan(auditReport);
148
+
149
+ // Write recovery artifacts
150
+ if (format === 'md' || format === 'both') {
151
+ reportPaths.recoveryMd = await writeRecoveryMarkdown(projectDir, recovery);
152
+ }
153
+ if (format === 'json' || format === 'both') {
154
+ reportPaths.recoveryJson = await writeRecoveryJson(projectDir, recovery);
155
+ }
156
+
157
+ // Auto-recover: inject milestones and switch to execution
158
+ if (autoRecover && recovery.milestones.length > 0) {
159
+ onProgress?.('stage-3', 'Injecting recovery milestones...');
160
+ const milestones = recoveryToMilestones(recovery, state.language);
161
+ await injectRecoveryIntoState(projectDir, milestones, auditRunId);
162
+ onProgress?.('stage-3', `Injected ${milestones.length} recovery milestone(s) — ready for execution`);
163
+ }
164
+
165
+ onProgress?.(
166
+ 'stage-3',
167
+ `Recovery plan: ${recovery.milestones.length} milestone(s), estimated ${recovery.estimatedEffort}`
168
+ );
169
+ } else {
170
+ onProgress?.('stage-3', 'No recovery needed based on findings.');
171
+ }
172
+
173
+ return {
174
+ success: true,
175
+ summary,
176
+ audit: auditReport,
177
+ recovery,
178
+ reportPaths,
179
+ };
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /**
187
+ * Create an error result with minimal valid structure.
188
+ *
189
+ * @param error - Error message.
190
+ * @returns An AuditModeResult indicating failure.
191
+ */
192
+ function makeErrorResult(error: string): AuditModeResult {
193
+ const emptySummary = {
194
+ projectName: 'unknown',
195
+ language: 'unknown',
196
+ totalSourceFiles: 0,
197
+ totalTestFiles: 0,
198
+ totalLinesOfCode: 0,
199
+ totalLinesOfTests: 0,
200
+ componentCount: 0,
201
+ detectedComposition: [],
202
+ entryPointCount: 0,
203
+ routeCount: 0,
204
+ dependencyCount: 0,
205
+ hasDocker: false,
206
+ hasEnvExample: false,
207
+ hasCiConfig: false,
208
+ };
209
+
210
+ return {
211
+ success: false,
212
+ summary: emptySummary,
213
+ audit: {
214
+ projectName: 'unknown',
215
+ language: 'unknown',
216
+ auditedAt: new Date().toISOString(),
217
+ auditRunId: 'error',
218
+ summary: emptySummary,
219
+ findings: [],
220
+ overallScore: 0,
221
+ categoryScores: {} as any,
222
+ criticalCount: 0,
223
+ majorCount: 0,
224
+ minorCount: 0,
225
+ infoCount: 0,
226
+ passedChecks: [],
227
+ searchMetadata: {
228
+ serenaUsed: false,
229
+ serenaRetries: 0,
230
+ serenaErrors: [],
231
+ fallbackUsed: false,
232
+ fallbackTool: '',
233
+ searchQueries: [],
234
+ },
235
+ recommendation: 'major-rework',
236
+ },
237
+ reportPaths: {},
238
+ error,
239
+ };
240
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Recovery system for the audit feature.
3
+ *
4
+ * Evidence-based trigger logic, finding-to-milestone conversion,
5
+ * and safe state injection for recovery execution.
6
+ */
7
+
8
+ import { addMilestones, updateState } from '../state/index.js';
9
+ import type { Milestone, ProjectState, Task } from '../types/workflow.js';
10
+ import type {
11
+ AuditFinding,
12
+ ComponentKind,
13
+ ProjectAuditReport,
14
+ RecoveryMilestone,
15
+ RecoveryPlan,
16
+ RecoveryTask,
17
+ } from '../types/audit.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const RECOVERY_PREFIX = '[RECOVERY]';
24
+ const ACTIONABLE_CATEGORIES = new Set([
25
+ 'integration-wiring',
26
+ 'test-coverage',
27
+ 'config-deployment',
28
+ ]);
29
+ const DEFAULT_SCORE_THRESHOLD = 70;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Trigger logic
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Determine if recovery should be triggered based on evidence in the report.
37
+ *
38
+ * Trigger conditions (any one suffices):
39
+ * - criticalCount > 0
40
+ * - strict mode AND majorCount > 0
41
+ * - overallScore < threshold AND at least one finding is autoFixable
42
+ * or belongs to an actionable category
43
+ *
44
+ * Does NOT trigger on info-only or purely cosmetic issues.
45
+ *
46
+ * @param report - The audit report.
47
+ * @param strict - Whether strict mode is active.
48
+ * @returns True if recovery should be triggered.
49
+ */
50
+ export function shouldTriggerRecovery(
51
+ report: ProjectAuditReport,
52
+ strict: boolean
53
+ ): boolean {
54
+ if (report.criticalCount > 0) return true;
55
+ if (strict && report.majorCount > 0) return true;
56
+
57
+ if (report.overallScore < DEFAULT_SCORE_THRESHOLD) {
58
+ const hasActionable = report.findings.some(
59
+ (f) => f.autoFixable || ACTIONABLE_CATEGORIES.has(f.category)
60
+ );
61
+ if (hasActionable) return true;
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Recovery plan generation
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Infer the target component from a finding's evidence file paths.
73
+ *
74
+ * @param finding - An audit finding.
75
+ * @returns The most likely component kind.
76
+ */
77
+ function inferAppTarget(finding: AuditFinding): ComponentKind {
78
+ const paths = finding.evidence.map((e) => e.file);
79
+ for (const p of paths) {
80
+ if (p.includes('frontend') || p.includes('web') || p.includes('client')) return 'frontend';
81
+ if (p.includes('backend') || p.includes('api') || p.includes('server')) return 'backend';
82
+ if (p.includes('website') || p.includes('landing')) return 'website';
83
+ if (p.includes('infra') || p.includes('docker') || p.includes('deploy')) return 'infra';
84
+ }
85
+
86
+ // Reason: Fall back based on category
87
+ if (finding.category === 'integration-wiring') return 'backend';
88
+ if (finding.category === 'test-coverage') return 'shared';
89
+ if (finding.category === 'config-deployment') return 'infra';
90
+ return 'shared';
91
+ }
92
+
93
+ /**
94
+ * Convert a finding into a recovery task.
95
+ *
96
+ * @param finding - An audit finding.
97
+ * @returns A recovery task.
98
+ */
99
+ function findingToRecoveryTask(finding: AuditFinding): RecoveryTask {
100
+ return {
101
+ name: finding.title,
102
+ description: `${finding.description}\n\nRecommendation: ${finding.recommendation}`,
103
+ findingIds: [finding.id],
104
+ acceptanceCriteria: [
105
+ finding.recommendation,
106
+ `Verify finding ${finding.id} is resolved`,
107
+ ],
108
+ testPlan: finding.category === 'test-coverage'
109
+ ? 'Add missing tests and verify they pass'
110
+ : undefined,
111
+ appTarget: inferAppTarget(finding),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Generate a recovery plan from audit report findings.
117
+ *
118
+ * Groups findings into milestones by severity:
119
+ * - Critical -> [RECOVERY] Critical Fixes
120
+ * - Major -> [RECOVERY] Major Improvements (grouped by category)
121
+ * - Minor + autoFixable -> [RECOVERY] Polish
122
+ *
123
+ * @param report - The audit report.
124
+ * @returns A structured recovery plan.
125
+ */
126
+ export function generateRecoveryPlan(report: ProjectAuditReport): RecoveryPlan {
127
+ const milestones: RecoveryMilestone[] = [];
128
+
129
+ // Critical findings -> separate milestone
130
+ const criticalFindings = report.findings.filter((f) => f.severity === 'critical');
131
+ if (criticalFindings.length > 0) {
132
+ milestones.push({
133
+ name: `${RECOVERY_PREFIX} Critical Fixes`,
134
+ description: 'Address all critical audit findings that block production readiness.',
135
+ tasks: criticalFindings.map(findingToRecoveryTask),
136
+ });
137
+ }
138
+
139
+ // Major findings -> grouped by category
140
+ const majorFindings = report.findings.filter((f) => f.severity === 'major');
141
+ if (majorFindings.length > 0) {
142
+ // Group by category
143
+ const byCategory = new Map<string, AuditFinding[]>();
144
+ for (const f of majorFindings) {
145
+ const existing = byCategory.get(f.category) ?? [];
146
+ existing.push(f);
147
+ byCategory.set(f.category, existing);
148
+ }
149
+
150
+ const majorTasks: RecoveryTask[] = [];
151
+ for (const [, findings] of byCategory) {
152
+ majorTasks.push(...findings.map(findingToRecoveryTask));
153
+ }
154
+
155
+ milestones.push({
156
+ name: `${RECOVERY_PREFIX} Major Improvements`,
157
+ description: 'Address major audit findings that significantly affect quality.',
158
+ tasks: majorTasks,
159
+ });
160
+ }
161
+
162
+ // Minor + autoFixable -> polish milestone
163
+ const polishFindings = report.findings.filter(
164
+ (f) => f.severity === 'minor' && f.autoFixable
165
+ );
166
+ if (polishFindings.length > 0) {
167
+ milestones.push({
168
+ name: `${RECOVERY_PREFIX} Polish`,
169
+ description: 'Address minor auto-fixable findings for polish.',
170
+ tasks: polishFindings.map(findingToRecoveryTask),
171
+ });
172
+ }
173
+
174
+ // Estimate effort based on finding count and severity
175
+ const effortHours = criticalFindings.length * 2 + majorFindings.length * 1 + polishFindings.length * 0.5;
176
+ const estimatedEffort = effortHours <= 2
177
+ ? '1-2 hours'
178
+ : effortHours <= 8
179
+ ? `${Math.ceil(effortHours / 2)}-${Math.ceil(effortHours)} hours`
180
+ : `${Math.ceil(effortHours / 8)} days`;
181
+
182
+ return {
183
+ generatedAt: new Date().toISOString(),
184
+ auditScore: report.overallScore,
185
+ auditRunId: report.auditRunId,
186
+ totalFindings: report.findings.length,
187
+ criticalFindings: report.criticalCount,
188
+ milestones,
189
+ estimatedEffort,
190
+ };
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Recovery -> milestone conversion
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /**
198
+ * Convert a recovery plan into milestone objects compatible with addMilestones().
199
+ *
200
+ * Each recovery milestone becomes an Omit<Milestone, 'id'> with tasks
201
+ * that have the standard Task shape.
202
+ *
203
+ * @param recovery - The recovery plan.
204
+ * @param language - Project language (for task context).
205
+ * @returns Array of milestone objects ready for addMilestones().
206
+ */
207
+ export function recoveryToMilestones(
208
+ recovery: RecoveryPlan,
209
+ _language: string
210
+ ): Omit<Milestone, 'id'>[] {
211
+ return recovery.milestones.map((rm) => ({
212
+ name: rm.name,
213
+ description: rm.description,
214
+ status: 'pending' as const,
215
+ tasks: rm.tasks.map((rt): Task => ({
216
+ id: '', // Reason: addMilestones() auto-assigns IDs
217
+ name: rt.name,
218
+ description: buildTaskDescription(rt),
219
+ status: 'pending' as const,
220
+ })),
221
+ }));
222
+ }
223
+
224
+ /**
225
+ * Build a detailed task description for execution.
226
+ *
227
+ * @param task - Recovery task.
228
+ * @param language - Project language.
229
+ * @returns Formatted description string.
230
+ */
231
+ function buildTaskDescription(task: RecoveryTask): string {
232
+ const parts = [task.description];
233
+
234
+ parts.push(`\nTarget component: ${task.appTarget}`);
235
+ parts.push(`Related findings: ${task.findingIds.join(', ')}`);
236
+
237
+ if (task.acceptanceCriteria.length > 0) {
238
+ parts.push('\nAcceptance criteria:');
239
+ for (const ac of task.acceptanceCriteria) {
240
+ parts.push(`- ${ac}`);
241
+ }
242
+ }
243
+
244
+ if (task.testPlan) {
245
+ parts.push(`\nTest plan: ${task.testPlan}`);
246
+ }
247
+
248
+ return parts.join('\n');
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // State injection
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /**
256
+ * Inject recovery milestones into the project state.
257
+ *
258
+ * This APPENDS recovery milestones — it never clobbers existing milestones.
259
+ * Milestone names are prefixed with [RECOVERY] for visibility.
260
+ * Sets audit tracking fields and switches phase to 'execution'.
261
+ *
262
+ * @param projectDir - Project root directory.
263
+ * @param milestones - Recovery milestones (without IDs).
264
+ * @param auditRunId - The audit run identifier for lineage.
265
+ * @returns Updated project state.
266
+ */
267
+ export async function injectRecoveryIntoState(
268
+ projectDir: string,
269
+ milestones: Omit<Milestone, 'id'>[],
270
+ auditRunId: string
271
+ ): Promise<ProjectState> {
272
+ // Reason: addMilestones appends to existing milestones, no clobbering
273
+ await addMilestones(projectDir, milestones);
274
+
275
+ const updatedState = await updateState(projectDir, {
276
+ phase: 'execution',
277
+ status: 'in-progress',
278
+ auditRecoveryInProgress: true,
279
+ auditRunId,
280
+ auditLastRunAt: new Date().toISOString(),
281
+ } as Partial<ProjectState>);
282
+
283
+ return updatedState;
284
+ }