popeye-cli 1.8.0 → 1.9.1

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 (68) hide show
  1. package/README.md +47 -3
  2. package/cheatsheet.md +33 -0
  3. package/dist/cli/commands/index.d.ts +1 -0
  4. package/dist/cli/commands/index.d.ts.map +1 -1
  5. package/dist/cli/commands/index.js +1 -0
  6. package/dist/cli/commands/index.js.map +1 -1
  7. package/dist/cli/commands/review.d.ts +31 -0
  8. package/dist/cli/commands/review.d.ts.map +1 -0
  9. package/dist/cli/commands/review.js +156 -0
  10. package/dist/cli/commands/review.js.map +1 -0
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +2 -1
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +122 -61
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/types/audit.d.ts +623 -0
  18. package/dist/types/audit.d.ts.map +1 -0
  19. package/dist/types/audit.js +240 -0
  20. package/dist/types/audit.js.map +1 -0
  21. package/dist/types/workflow.d.ts +15 -0
  22. package/dist/types/workflow.d.ts.map +1 -1
  23. package/dist/types/workflow.js +5 -0
  24. package/dist/types/workflow.js.map +1 -1
  25. package/dist/workflow/audit-analyzer.d.ts +58 -0
  26. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  27. package/dist/workflow/audit-analyzer.js +438 -0
  28. package/dist/workflow/audit-analyzer.js.map +1 -0
  29. package/dist/workflow/audit-mode.d.ts +28 -0
  30. package/dist/workflow/audit-mode.d.ts.map +1 -0
  31. package/dist/workflow/audit-mode.js +169 -0
  32. package/dist/workflow/audit-mode.js.map +1 -0
  33. package/dist/workflow/audit-recovery.d.ts +61 -0
  34. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  35. package/dist/workflow/audit-recovery.js +242 -0
  36. package/dist/workflow/audit-recovery.js.map +1 -0
  37. package/dist/workflow/audit-reporter.d.ts +65 -0
  38. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  39. package/dist/workflow/audit-reporter.js +301 -0
  40. package/dist/workflow/audit-reporter.js.map +1 -0
  41. package/dist/workflow/audit-scanner.d.ts +87 -0
  42. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  43. package/dist/workflow/audit-scanner.js +768 -0
  44. package/dist/workflow/audit-scanner.js.map +1 -0
  45. package/dist/workflow/index.d.ts +5 -0
  46. package/dist/workflow/index.d.ts.map +1 -1
  47. package/dist/workflow/index.js +5 -0
  48. package/dist/workflow/index.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/cli/commands/index.ts +1 -0
  51. package/src/cli/commands/review.ts +187 -0
  52. package/src/cli/index.ts +2 -0
  53. package/src/cli/interactive.ts +72 -4
  54. package/src/types/audit.ts +294 -0
  55. package/src/types/workflow.ts +15 -0
  56. package/src/workflow/audit-analyzer.ts +510 -0
  57. package/src/workflow/audit-mode.ts +240 -0
  58. package/src/workflow/audit-recovery.ts +284 -0
  59. package/src/workflow/audit-reporter.ts +370 -0
  60. package/src/workflow/audit-scanner.ts +873 -0
  61. package/src/workflow/index.ts +5 -0
  62. package/tests/cli/commands/review.test.ts +52 -0
  63. package/tests/types/audit.test.ts +250 -0
  64. package/tests/workflow/audit-analyzer.test.ts +281 -0
  65. package/tests/workflow/audit-mode.test.ts +114 -0
  66. package/tests/workflow/audit-recovery.test.ts +237 -0
  67. package/tests/workflow/audit-reporter.test.ts +254 -0
  68. package/tests/workflow/audit-scanner.test.ts +270 -0
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Report generation for the audit system.
3
+ *
4
+ * Builds summary + full reports, writes markdown and JSON artifacts
5
+ * to the project's .popeye/ directory.
6
+ */
7
+
8
+ import { promises as fs } from 'node:fs';
9
+ import path from 'node:path';
10
+ import type { ProjectState } from '../types/workflow.js';
11
+ import type {
12
+ AuditCategory,
13
+ AuditFinding,
14
+ AuditModeOptions,
15
+ AuditRecommendation,
16
+ ProjectAuditReport,
17
+ ProjectScanResult,
18
+ ProjectSummaryReport,
19
+ RecoveryPlan,
20
+ SearchMetadata,
21
+ } from '../types/audit.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Summary report
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Build a summary report from the scan result and project state.
29
+ *
30
+ * @param scan - The project scan result.
31
+ * @param state - Current project state.
32
+ * @param aiOverview - Optional AI-generated overview text.
33
+ * @returns A structured summary report.
34
+ */
35
+ export function buildSummaryReport(
36
+ scan: ProjectScanResult,
37
+ state: ProjectState,
38
+ aiOverview?: string
39
+ ): ProjectSummaryReport {
40
+ const depCount = scan.dependencies.reduce((sum, d) => {
41
+ const deps = d.dependencies ? Object.keys(d.dependencies).length : 0;
42
+ const devDeps = d.devDependencies ? Object.keys(d.devDependencies).length : 0;
43
+ return sum + deps + devDeps;
44
+ }, 0);
45
+
46
+ return {
47
+ projectName: state.name,
48
+ language: scan.language,
49
+ totalSourceFiles: scan.totalSourceFiles,
50
+ totalTestFiles: scan.totalTestFiles,
51
+ totalLinesOfCode: scan.totalLinesOfCode,
52
+ totalLinesOfTests: scan.totalLinesOfTests,
53
+ componentCount: scan.components.length,
54
+ detectedComposition: [...scan.detectedComposition],
55
+ entryPointCount: scan.entryPoints.length,
56
+ routeCount: scan.routeFiles.length,
57
+ dependencyCount: depCount,
58
+ hasDocker: scan.configFiles.includes('docker-compose.yml')
59
+ || scan.configFiles.includes('docker-compose.yaml')
60
+ || scan.configFiles.includes('Dockerfile'),
61
+ hasEnvExample: !!scan.envExampleContent,
62
+ hasCiConfig: scan.configFiles.some((f) =>
63
+ f.includes('.github') || f.includes('Jenkinsfile') || f.includes('.gitlab-ci')
64
+ ),
65
+ aiOverview,
66
+ };
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Audit report
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Derive the overall recommendation from finding counts and score.
75
+ *
76
+ * @param criticalCount - Number of critical findings.
77
+ * @param majorCount - Number of major findings.
78
+ * @param overallScore - The overall audit score (0-100).
79
+ * @param strict - Whether strict mode is enabled.
80
+ * @returns The recommendation string.
81
+ */
82
+ function deriveRecommendation(
83
+ criticalCount: number,
84
+ majorCount: number,
85
+ overallScore: number,
86
+ strict: boolean
87
+ ): AuditRecommendation {
88
+ if (criticalCount >= 3 || overallScore < 50) return 'major-rework';
89
+ if (criticalCount === 0 && (strict ? majorCount === 0 : majorCount <= 2)) return 'pass';
90
+ return 'fix-and-recheck';
91
+ }
92
+
93
+ /**
94
+ * Build the full audit report from all analysis results.
95
+ *
96
+ * @param summary - The summary report.
97
+ * @param findings - All audit findings.
98
+ * @param scores - Overall and category scores.
99
+ * @param searchMeta - Serena search tracking metadata.
100
+ * @param options - Audit options (for strict mode, etc.).
101
+ * @param auditRunId - Unique run identifier.
102
+ * @returns The complete audit report.
103
+ */
104
+ export function buildAuditReport(
105
+ summary: ProjectSummaryReport,
106
+ findings: AuditFinding[],
107
+ scores: { overallScore: number; categoryScores: Record<AuditCategory, number> },
108
+ searchMeta: SearchMetadata,
109
+ options: Pick<AuditModeOptions, 'strict'>,
110
+ auditRunId: string
111
+ ): ProjectAuditReport {
112
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length;
113
+ const majorCount = findings.filter((f) => f.severity === 'major').length;
114
+ const minorCount = findings.filter((f) => f.severity === 'minor').length;
115
+ const infoCount = findings.filter((f) => f.severity === 'info').length;
116
+
117
+ // Determine passed checks — categories with no findings
118
+ const categoriesWithFindings = new Set(findings.map((f) => f.category));
119
+ const allCategories: AuditCategory[] = [
120
+ 'feature-completeness', 'integration-wiring', 'test-coverage',
121
+ 'config-deployment', 'dependency-sanity', 'consistency', 'security', 'documentation',
122
+ ];
123
+ const passedChecks = allCategories
124
+ .filter((c) => !categoriesWithFindings.has(c))
125
+ .map((c) => `${c}: no issues found`);
126
+
127
+ return {
128
+ projectName: summary.projectName,
129
+ language: summary.language,
130
+ auditedAt: new Date().toISOString(),
131
+ auditRunId,
132
+ summary,
133
+ findings,
134
+ overallScore: scores.overallScore,
135
+ categoryScores: scores.categoryScores,
136
+ criticalCount,
137
+ majorCount,
138
+ minorCount,
139
+ infoCount,
140
+ passedChecks,
141
+ searchMetadata: searchMeta,
142
+ recommendation: deriveRecommendation(
143
+ criticalCount, majorCount, scores.overallScore, options.strict
144
+ ),
145
+ };
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Markdown rendering
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Render the audit report as a markdown string.
154
+ *
155
+ * @param report - The audit report.
156
+ * @returns Markdown string.
157
+ */
158
+ function renderAuditMarkdown(report: ProjectAuditReport): string {
159
+ const lines: string[] = [];
160
+
161
+ lines.push(`# Audit Report: ${report.projectName}`);
162
+ lines.push('');
163
+ lines.push(`**Language:** ${report.language}`);
164
+ lines.push(`**Audited:** ${report.auditedAt}`);
165
+ lines.push(`**Run ID:** ${report.auditRunId}`);
166
+ lines.push(`**Overall Score:** ${report.overallScore}/100`);
167
+ lines.push(`**Recommendation:** ${report.recommendation}`);
168
+ lines.push('');
169
+
170
+ // Summary
171
+ lines.push('## Summary');
172
+ lines.push('');
173
+ lines.push(`| Metric | Value |`);
174
+ lines.push(`| --- | --- |`);
175
+ lines.push(`| Source files | ${report.summary.totalSourceFiles} |`);
176
+ lines.push(`| Test files | ${report.summary.totalTestFiles} |`);
177
+ lines.push(`| Lines of code | ${report.summary.totalLinesOfCode} |`);
178
+ lines.push(`| Lines of tests | ${report.summary.totalLinesOfTests} |`);
179
+ lines.push(`| Components | ${report.summary.componentCount} |`);
180
+ lines.push(`| Entry points | ${report.summary.entryPointCount} |`);
181
+ lines.push(`| Routes | ${report.summary.routeCount} |`);
182
+ lines.push(`| Dependencies | ${report.summary.dependencyCount} |`);
183
+ lines.push('');
184
+
185
+ // Category scores
186
+ lines.push('## Category Scores');
187
+ lines.push('');
188
+ for (const [cat, score] of Object.entries(report.categoryScores)) {
189
+ lines.push(`- **${cat}:** ${score}/100`);
190
+ }
191
+ lines.push('');
192
+
193
+ // Findings
194
+ lines.push(`## Findings (${report.findings.length} total)`);
195
+ lines.push('');
196
+ lines.push(`- Critical: ${report.criticalCount}`);
197
+ lines.push(`- Major: ${report.majorCount}`);
198
+ lines.push(`- Minor: ${report.minorCount}`);
199
+ lines.push(`- Info: ${report.infoCount}`);
200
+ lines.push('');
201
+
202
+ // Group findings by severity
203
+ const severityOrder = ['critical', 'major', 'minor', 'info'] as const;
204
+ for (const sev of severityOrder) {
205
+ const sevFindings = report.findings.filter((f) => f.severity === sev);
206
+ if (sevFindings.length === 0) continue;
207
+ lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)}`);
208
+ lines.push('');
209
+ for (const f of sevFindings) {
210
+ lines.push(`#### ${f.id}: ${f.title}`);
211
+ lines.push('');
212
+ lines.push(f.description);
213
+ if (f.evidence.length > 0) {
214
+ lines.push('');
215
+ lines.push('**Evidence:**');
216
+ for (const e of f.evidence) {
217
+ const loc = e.line ? `${e.file}:${e.line}` : e.file;
218
+ lines.push(`- \`${loc}\`${e.description ? ` - ${e.description}` : ''}`);
219
+ }
220
+ }
221
+ lines.push('');
222
+ lines.push(`**Recommendation:** ${f.recommendation}`);
223
+ lines.push(`**Auto-fixable:** ${f.autoFixable ? 'Yes' : 'No'}`);
224
+ lines.push('');
225
+ }
226
+ }
227
+
228
+ // Passed checks
229
+ if (report.passedChecks.length > 0) {
230
+ lines.push('## Passed Checks');
231
+ lines.push('');
232
+ for (const check of report.passedChecks) {
233
+ lines.push(`- ${check}`);
234
+ }
235
+ lines.push('');
236
+ }
237
+
238
+ return lines.join('\n');
239
+ }
240
+
241
+ /**
242
+ * Render the recovery plan as a markdown string.
243
+ *
244
+ * @param recovery - The recovery plan.
245
+ * @returns Markdown string.
246
+ */
247
+ function renderRecoveryMarkdown(recovery: RecoveryPlan): string {
248
+ const lines: string[] = [];
249
+
250
+ lines.push('# Recovery Plan');
251
+ lines.push('');
252
+ lines.push(`**Generated:** ${recovery.generatedAt}`);
253
+ lines.push(`**Audit Score:** ${recovery.auditScore}/100`);
254
+ lines.push(`**Run ID:** ${recovery.auditRunId}`);
255
+ lines.push(`**Total Findings:** ${recovery.totalFindings}`);
256
+ lines.push(`**Critical Findings:** ${recovery.criticalFindings}`);
257
+ lines.push(`**Estimated Effort:** ${recovery.estimatedEffort}`);
258
+ lines.push('');
259
+
260
+ for (const milestone of recovery.milestones) {
261
+ lines.push(`## ${milestone.name}`);
262
+ lines.push('');
263
+ lines.push(milestone.description);
264
+ lines.push('');
265
+
266
+ for (const task of milestone.tasks) {
267
+ lines.push(`### ${task.name} (target: ${task.appTarget})`);
268
+ lines.push('');
269
+ lines.push(task.description);
270
+ lines.push('');
271
+ lines.push('**Finding IDs:** ' + task.findingIds.join(', '));
272
+ lines.push('');
273
+ lines.push('**Acceptance Criteria:**');
274
+ for (const ac of task.acceptanceCriteria) {
275
+ lines.push(`- [ ] ${ac}`);
276
+ }
277
+ if (task.testPlan) {
278
+ lines.push('');
279
+ lines.push(`**Test Plan:** ${task.testPlan}`);
280
+ }
281
+ lines.push('');
282
+ }
283
+ }
284
+
285
+ return lines.join('\n');
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // File writers
290
+ // ---------------------------------------------------------------------------
291
+
292
+ /**
293
+ * Ensure .popeye directory exists.
294
+ *
295
+ * @param projectDir - Project root directory.
296
+ * @returns Path to .popeye directory.
297
+ */
298
+ async function ensurePopeyeDir(projectDir: string): Promise<string> {
299
+ const dir = path.join(projectDir, '.popeye');
300
+ await fs.mkdir(dir, { recursive: true });
301
+ return dir;
302
+ }
303
+
304
+ /**
305
+ * Write the audit report as markdown.
306
+ *
307
+ * @param projectDir - Project root directory.
308
+ * @param report - The audit report.
309
+ * @returns Path to the written file.
310
+ */
311
+ export async function writeAuditMarkdown(
312
+ projectDir: string,
313
+ report: ProjectAuditReport
314
+ ): Promise<string> {
315
+ const dir = await ensurePopeyeDir(projectDir);
316
+ const filePath = path.join(dir, 'popeye.audit.md');
317
+ await fs.writeFile(filePath, renderAuditMarkdown(report), 'utf-8');
318
+ return filePath;
319
+ }
320
+
321
+ /**
322
+ * Write the audit report as JSON.
323
+ *
324
+ * @param projectDir - Project root directory.
325
+ * @param report - The audit report.
326
+ * @returns Path to the written file.
327
+ */
328
+ export async function writeAuditJson(
329
+ projectDir: string,
330
+ report: ProjectAuditReport
331
+ ): Promise<string> {
332
+ const dir = await ensurePopeyeDir(projectDir);
333
+ const filePath = path.join(dir, 'popeye.audit.json');
334
+ await fs.writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8');
335
+ return filePath;
336
+ }
337
+
338
+ /**
339
+ * Write the recovery plan as markdown.
340
+ *
341
+ * @param projectDir - Project root directory.
342
+ * @param recovery - The recovery plan.
343
+ * @returns Path to the written file.
344
+ */
345
+ export async function writeRecoveryMarkdown(
346
+ projectDir: string,
347
+ recovery: RecoveryPlan
348
+ ): Promise<string> {
349
+ const dir = await ensurePopeyeDir(projectDir);
350
+ const filePath = path.join(dir, 'popeye.recovery.md');
351
+ await fs.writeFile(filePath, renderRecoveryMarkdown(recovery), 'utf-8');
352
+ return filePath;
353
+ }
354
+
355
+ /**
356
+ * Write the recovery plan as JSON.
357
+ *
358
+ * @param projectDir - Project root directory.
359
+ * @param recovery - The recovery plan.
360
+ * @returns Path to the written file.
361
+ */
362
+ export async function writeRecoveryJson(
363
+ projectDir: string,
364
+ recovery: RecoveryPlan
365
+ ): Promise<string> {
366
+ const dir = await ensurePopeyeDir(projectDir);
367
+ const filePath = path.join(dir, 'popeye.recovery.json');
368
+ await fs.writeFile(filePath, JSON.stringify(recovery, null, 2), 'utf-8');
369
+ return filePath;
370
+ }