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.
- package/README.md +47 -3
- package/cheatsheet.md +33 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -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 +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +122 -61
- package/dist/cli/interactive.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/workflow.d.ts +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +5 -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 +438 -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/index.d.ts +5 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +5 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/index.ts +1 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/interactive.ts +72 -4
- package/src/types/audit.ts +294 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/audit-analyzer.ts +510 -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/index.ts +5 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/types/audit.test.ts +250 -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
|
@@ -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
|
+
}
|