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.
- package/README.md +148 -7
- package/cheatsheet.md +440 -0
- package/dist/cli/commands/db.d.ts +10 -0
- package/dist/cli/commands/db.d.ts.map +1 -0
- package/dist/cli/commands/db.js +240 -0
- package/dist/cli/commands/db.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +18 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +255 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +3 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/review.d.ts +31 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +156 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +218 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/admin-wizard.d.ts +25 -0
- package/dist/generators/admin-wizard.d.ts.map +1 -0
- package/dist/generators/admin-wizard.js +123 -0
- package/dist/generators/admin-wizard.js.map +1 -0
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +10 -3
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/database.d.ts +58 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +229 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js +23 -7
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
- package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-python.js +425 -0
- package/dist/generators/templates/admin-wizard-python.js.map +1 -0
- package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
- package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-react.js +554 -0
- package/dist/generators/templates/admin-wizard-react.js.map +1 -0
- package/dist/generators/templates/database-docker.d.ts +23 -0
- package/dist/generators/templates/database-docker.d.ts.map +1 -0
- package/dist/generators/templates/database-docker.js +221 -0
- package/dist/generators/templates/database-docker.js.map +1 -0
- package/dist/generators/templates/database-python.d.ts +54 -0
- package/dist/generators/templates/database-python.d.ts.map +1 -0
- package/dist/generators/templates/database-python.js +723 -0
- package/dist/generators/templates/database-python.js.map +1 -0
- package/dist/generators/templates/database-typescript.d.ts +34 -0
- package/dist/generators/templates/database-typescript.d.ts.map +1 -0
- package/dist/generators/templates/database-typescript.js +232 -0
- package/dist/generators/templates/database-typescript.js.map +1 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -1
- package/dist/generators/templates/fullstack.js +29 -0
- package/dist/generators/templates/fullstack.js.map +1 -1
- package/dist/generators/templates/index.d.ts +5 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +5 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/state/index.d.ts +10 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +21 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/audit.d.ts +623 -0
- package/dist/types/audit.d.ts.map +1 -0
- package/dist/types/audit.js +240 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/database-runtime.d.ts +86 -0
- package/dist/types/database-runtime.d.ts.map +1 -0
- package/dist/types/database-runtime.js +61 -0
- package/dist/types/database-runtime.js.map +1 -0
- package/dist/types/database.d.ts +85 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +71 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow.d.ts +36 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +7 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/audit-analyzer.d.ts +58 -0
- package/dist/workflow/audit-analyzer.d.ts.map +1 -0
- package/dist/workflow/audit-analyzer.js +420 -0
- package/dist/workflow/audit-analyzer.js.map +1 -0
- package/dist/workflow/audit-mode.d.ts +28 -0
- package/dist/workflow/audit-mode.d.ts.map +1 -0
- package/dist/workflow/audit-mode.js +169 -0
- package/dist/workflow/audit-mode.js.map +1 -0
- package/dist/workflow/audit-recovery.d.ts +61 -0
- package/dist/workflow/audit-recovery.d.ts.map +1 -0
- package/dist/workflow/audit-recovery.js +242 -0
- package/dist/workflow/audit-recovery.js.map +1 -0
- package/dist/workflow/audit-reporter.d.ts +65 -0
- package/dist/workflow/audit-reporter.d.ts.map +1 -0
- package/dist/workflow/audit-reporter.js +301 -0
- package/dist/workflow/audit-reporter.js.map +1 -0
- package/dist/workflow/audit-scanner.d.ts +87 -0
- package/dist/workflow/audit-scanner.d.ts.map +1 -0
- package/dist/workflow/audit-scanner.js +768 -0
- package/dist/workflow/audit-scanner.js.map +1 -0
- package/dist/workflow/db-setup-runner.d.ts +63 -0
- package/dist/workflow/db-setup-runner.d.ts.map +1 -0
- package/dist/workflow/db-setup-runner.js +336 -0
- package/dist/workflow/db-setup-runner.js.map +1 -0
- package/dist/workflow/db-state-machine.d.ts +30 -0
- package/dist/workflow/db-state-machine.d.ts.map +1 -0
- package/dist/workflow/db-state-machine.js +51 -0
- package/dist/workflow/db-state-machine.js.map +1 -0
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/db.ts +281 -0
- package/src/cli/commands/doctor.ts +273 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/interactive.ts +174 -4
- package/src/generators/admin-wizard.ts +146 -0
- package/src/generators/all.ts +10 -3
- package/src/generators/database.ts +286 -0
- package/src/generators/fullstack.ts +26 -9
- package/src/generators/index.ts +12 -0
- package/src/generators/templates/admin-wizard-python.ts +431 -0
- package/src/generators/templates/admin-wizard-react.ts +560 -0
- package/src/generators/templates/database-docker.ts +227 -0
- package/src/generators/templates/database-python.ts +734 -0
- package/src/generators/templates/database-typescript.ts +238 -0
- package/src/generators/templates/fullstack.ts +29 -0
- package/src/generators/templates/index.ts +5 -0
- package/src/state/index.ts +28 -0
- package/src/types/audit.ts +294 -0
- package/src/types/database-runtime.ts +69 -0
- package/src/types/database.ts +84 -0
- package/src/types/index.ts +29 -0
- package/src/types/workflow.ts +20 -0
- package/src/workflow/audit-analyzer.ts +491 -0
- package/src/workflow/audit-mode.ts +240 -0
- package/src/workflow/audit-recovery.ts +284 -0
- package/src/workflow/audit-reporter.ts +370 -0
- package/src/workflow/audit-scanner.ts +873 -0
- package/src/workflow/db-setup-runner.ts +391 -0
- package/src/workflow/db-state-machine.ts +58 -0
- package/src/workflow/index.ts +7 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
- package/tests/generators/admin-wizard-templates.test.ts +366 -0
- package/tests/generators/cross-phase-integration.test.ts +383 -0
- package/tests/generators/database.test.ts +456 -0
- package/tests/generators/fe-be-db-integration.test.ts +613 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/types/database-runtime.test.ts +158 -0
- package/tests/types/database.test.ts +187 -0
- package/tests/workflow/audit-analyzer.test.ts +281 -0
- package/tests/workflow/audit-mode.test.ts +114 -0
- package/tests/workflow/audit-recovery.test.ts +237 -0
- package/tests/workflow/audit-reporter.test.ts +254 -0
- package/tests/workflow/audit-scanner.test.ts +270 -0
- package/tests/workflow/db-setup-runner.test.ts +211 -0
- 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
|
+
}
|