@girardmedia/bootspring 2.0.21 → 2.0.23
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/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Audit Workflow Engine
|
|
3
|
+
*
|
|
4
|
+
* Quality, security, and best practices audit for codebases.
|
|
5
|
+
* Generates prioritized recommendations and remediation tasks.
|
|
6
|
+
*
|
|
7
|
+
* @package bootspring
|
|
8
|
+
* @module core/audit-workflow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
// Import analyzers from JS modules
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const securityModule = require('../../analyzers/security-scanner') as {
|
|
17
|
+
SecurityScanner: new (projectRoot: string) => {
|
|
18
|
+
scan: () => SecurityScanResult;
|
|
19
|
+
};
|
|
20
|
+
SEVERITY: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
const { SecurityScanner, SEVERITY } = securityModule;
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const { QualityAnalyzer } = require('../../analyzers/quality-analyzer') as {
|
|
26
|
+
QualityAnalyzer: new (projectRoot: string) => {
|
|
27
|
+
analyze: () => QualityAnalysisResult;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const { DependencyAnalyzer } = require('../../analyzers/dependency-analyzer') as {
|
|
33
|
+
DependencyAnalyzer: new (projectRoot: string) => {
|
|
34
|
+
buildGraph: () => void;
|
|
35
|
+
findUnusedDependencies: () => UnusedDependency[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Types
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
export type AuditPhaseStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
|
|
44
|
+
export type AuditSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
45
|
+
|
|
46
|
+
export interface AuditPhaseDefinition {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
order: number;
|
|
50
|
+
required: boolean;
|
|
51
|
+
dependencies?: string[] | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AuditPhaseState {
|
|
55
|
+
status: AuditPhaseStatus;
|
|
56
|
+
startedAt: string | null;
|
|
57
|
+
completedAt: string | null;
|
|
58
|
+
result: unknown | null;
|
|
59
|
+
error: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SeverityLevel {
|
|
63
|
+
label: string;
|
|
64
|
+
color: string;
|
|
65
|
+
action: string;
|
|
66
|
+
priority: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AuditFinding {
|
|
70
|
+
id?: string | undefined;
|
|
71
|
+
phase: string;
|
|
72
|
+
category: string;
|
|
73
|
+
severity: AuditSeverity;
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
file?: string | null | undefined;
|
|
77
|
+
line?: number | undefined;
|
|
78
|
+
function?: string | null | undefined;
|
|
79
|
+
remediation?: string | undefined;
|
|
80
|
+
timestamp?: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AuditSummary {
|
|
84
|
+
reportPath: string;
|
|
85
|
+
totalFindings: number;
|
|
86
|
+
critical: number;
|
|
87
|
+
high: number;
|
|
88
|
+
medium: number;
|
|
89
|
+
low: number;
|
|
90
|
+
recommendations: number;
|
|
91
|
+
generatedAt: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AuditWorkflowState {
|
|
95
|
+
version: string;
|
|
96
|
+
startedAt: string | null;
|
|
97
|
+
lastUpdated: string | null;
|
|
98
|
+
currentPhase: string | null;
|
|
99
|
+
phases: Record<string, AuditPhaseState>;
|
|
100
|
+
findings: AuditFinding[];
|
|
101
|
+
summary: AuditSummary | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface QualityMetricsResult {
|
|
105
|
+
score: number;
|
|
106
|
+
breakdown: Record<string, number>;
|
|
107
|
+
totalFiles: number;
|
|
108
|
+
totalIssues: number;
|
|
109
|
+
totalSmells: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SecurityScanResultSummary {
|
|
113
|
+
passed: boolean;
|
|
114
|
+
total: number;
|
|
115
|
+
critical: number;
|
|
116
|
+
high: number;
|
|
117
|
+
medium: number;
|
|
118
|
+
low: number;
|
|
119
|
+
info?: number | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SecurityFinding {
|
|
123
|
+
id: string;
|
|
124
|
+
type: string;
|
|
125
|
+
severity: AuditSeverity;
|
|
126
|
+
name: string;
|
|
127
|
+
message: string;
|
|
128
|
+
file: string;
|
|
129
|
+
line?: number;
|
|
130
|
+
remediation?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface SecurityScanResult {
|
|
134
|
+
findings: SecurityFinding[];
|
|
135
|
+
bySeverity: Record<string, SecurityFinding[]>;
|
|
136
|
+
byType: {
|
|
137
|
+
secret: SecurityFinding[];
|
|
138
|
+
[key: string]: SecurityFinding[];
|
|
139
|
+
};
|
|
140
|
+
summary: SecurityScanResultSummary;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface QualityAnalysisResult {
|
|
144
|
+
scores: {
|
|
145
|
+
overall: number;
|
|
146
|
+
breakdown: {
|
|
147
|
+
complexity: number;
|
|
148
|
+
maintainability: number;
|
|
149
|
+
documentation: number;
|
|
150
|
+
codeSmells: number;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
summary: {
|
|
154
|
+
totalFiles: number;
|
|
155
|
+
totalLines: number;
|
|
156
|
+
averageComplexity: number;
|
|
157
|
+
totalIssues: number;
|
|
158
|
+
totalSmells: number;
|
|
159
|
+
};
|
|
160
|
+
worstFiles: Array<{
|
|
161
|
+
path: string;
|
|
162
|
+
complexity: number;
|
|
163
|
+
issues: Array<{
|
|
164
|
+
type: string;
|
|
165
|
+
message: string;
|
|
166
|
+
function?: string;
|
|
167
|
+
}>;
|
|
168
|
+
}>;
|
|
169
|
+
smells: Array<{
|
|
170
|
+
id: string;
|
|
171
|
+
name: string;
|
|
172
|
+
severity: AuditSeverity;
|
|
173
|
+
count: number;
|
|
174
|
+
file: string;
|
|
175
|
+
}>;
|
|
176
|
+
smellsByCategory?: Record<string, Array<{
|
|
177
|
+
name: string;
|
|
178
|
+
file: string;
|
|
179
|
+
count: number;
|
|
180
|
+
}>>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface UnusedDependency {
|
|
184
|
+
name: string;
|
|
185
|
+
type: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface PerformanceResult {
|
|
189
|
+
recommendations: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface BestPracticesResult {
|
|
193
|
+
passed: number;
|
|
194
|
+
total: number;
|
|
195
|
+
checks: Record<string, CheckResult>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface CheckResult {
|
|
199
|
+
passed: boolean;
|
|
200
|
+
issues: CheckIssue[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface CheckIssue {
|
|
204
|
+
title: string;
|
|
205
|
+
description: string;
|
|
206
|
+
file?: string;
|
|
207
|
+
severity?: AuditSeverity;
|
|
208
|
+
remediation?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface TechDebtResult {
|
|
212
|
+
todos: number;
|
|
213
|
+
unusedDeps: number;
|
|
214
|
+
testCoverage: number;
|
|
215
|
+
depAnalysisSkipped: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface Recommendation {
|
|
219
|
+
priority: string;
|
|
220
|
+
severity: AuditSeverity;
|
|
221
|
+
title: string;
|
|
222
|
+
description: string;
|
|
223
|
+
file?: string | undefined;
|
|
224
|
+
remediation?: string | undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface RecommendationsResult {
|
|
228
|
+
reportPath: string;
|
|
229
|
+
recommendations: number;
|
|
230
|
+
passed: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface AuditPhaseProgress {
|
|
234
|
+
id: string;
|
|
235
|
+
name: string;
|
|
236
|
+
description: string;
|
|
237
|
+
order: number;
|
|
238
|
+
required: boolean;
|
|
239
|
+
status: AuditPhaseStatus;
|
|
240
|
+
dependenciesMet: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface AuditWorkflowProgress {
|
|
244
|
+
currentPhase: string | null;
|
|
245
|
+
startedAt: string | null;
|
|
246
|
+
lastUpdated: string | null;
|
|
247
|
+
phases: AuditPhaseProgress[];
|
|
248
|
+
overall: {
|
|
249
|
+
completed: number;
|
|
250
|
+
total: number;
|
|
251
|
+
percentage: number;
|
|
252
|
+
};
|
|
253
|
+
findings: {
|
|
254
|
+
total: number;
|
|
255
|
+
critical: number;
|
|
256
|
+
high: number;
|
|
257
|
+
medium: number;
|
|
258
|
+
low: number;
|
|
259
|
+
info: number;
|
|
260
|
+
};
|
|
261
|
+
isComplete: boolean;
|
|
262
|
+
summary: AuditSummary | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface AuditResumePoint {
|
|
266
|
+
phase: string;
|
|
267
|
+
phaseName: string | undefined;
|
|
268
|
+
phaseStatus: AuditPhaseStatus | undefined;
|
|
269
|
+
lastUpdated: string | null;
|
|
270
|
+
findingsCount: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface AuditWorkflowOptions {
|
|
274
|
+
severityFilter?: string | null | undefined;
|
|
275
|
+
autoFix?: boolean | undefined;
|
|
276
|
+
ciMode?: boolean | undefined;
|
|
277
|
+
[key: string]: unknown;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Constants
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Workflow phase status
|
|
286
|
+
*/
|
|
287
|
+
export const PHASE_STATUS: Record<string, AuditPhaseStatus> = {
|
|
288
|
+
PENDING: 'pending',
|
|
289
|
+
IN_PROGRESS: 'in_progress',
|
|
290
|
+
COMPLETED: 'completed',
|
|
291
|
+
SKIPPED: 'skipped',
|
|
292
|
+
FAILED: 'failed'
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Audit phases
|
|
297
|
+
*/
|
|
298
|
+
export const AUDIT_PHASES: Record<string, AuditPhaseDefinition> = {
|
|
299
|
+
quality: {
|
|
300
|
+
name: 'Quality Metrics',
|
|
301
|
+
description: 'Complexity, duplication, comments, naming',
|
|
302
|
+
order: 1,
|
|
303
|
+
required: true
|
|
304
|
+
},
|
|
305
|
+
security: {
|
|
306
|
+
name: 'Security Scan',
|
|
307
|
+
description: 'Secrets, vulnerabilities, OWASP, injection',
|
|
308
|
+
order: 2,
|
|
309
|
+
required: true
|
|
310
|
+
},
|
|
311
|
+
performance: {
|
|
312
|
+
name: 'Performance Analysis',
|
|
313
|
+
description: 'Bundle size, lazy loading, N+1 queries',
|
|
314
|
+
order: 3,
|
|
315
|
+
required: false,
|
|
316
|
+
dependencies: ['quality']
|
|
317
|
+
},
|
|
318
|
+
practices: {
|
|
319
|
+
name: 'Best Practices',
|
|
320
|
+
description: 'TypeScript strict, error handling, env vars',
|
|
321
|
+
order: 4,
|
|
322
|
+
required: true
|
|
323
|
+
},
|
|
324
|
+
techDebt: {
|
|
325
|
+
name: 'Tech Debt Inventory',
|
|
326
|
+
description: 'TODOs, deprecated APIs, dead code, missing tests',
|
|
327
|
+
order: 5,
|
|
328
|
+
required: true,
|
|
329
|
+
dependencies: ['quality']
|
|
330
|
+
},
|
|
331
|
+
recommendations: {
|
|
332
|
+
name: 'Recommendations',
|
|
333
|
+
description: 'Prioritize (P0-P3), estimate effort, generate tasks',
|
|
334
|
+
order: 6,
|
|
335
|
+
required: true,
|
|
336
|
+
dependencies: ['quality', 'security', 'practices', 'techDebt']
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Finding severity levels with actions
|
|
342
|
+
*/
|
|
343
|
+
export const SEVERITY_LEVELS: Record<AuditSeverity, SeverityLevel> = {
|
|
344
|
+
critical: {
|
|
345
|
+
label: 'CRITICAL',
|
|
346
|
+
color: 'red',
|
|
347
|
+
action: 'Immediate fix required',
|
|
348
|
+
priority: 0
|
|
349
|
+
},
|
|
350
|
+
high: {
|
|
351
|
+
label: 'HIGH',
|
|
352
|
+
color: 'orange',
|
|
353
|
+
action: 'Fix within 1-2 days',
|
|
354
|
+
priority: 1
|
|
355
|
+
},
|
|
356
|
+
medium: {
|
|
357
|
+
label: 'MEDIUM',
|
|
358
|
+
color: 'yellow',
|
|
359
|
+
action: 'Plan fix within 1-2 weeks',
|
|
360
|
+
priority: 2
|
|
361
|
+
},
|
|
362
|
+
low: {
|
|
363
|
+
label: 'LOW',
|
|
364
|
+
color: 'blue',
|
|
365
|
+
action: 'Add to backlog',
|
|
366
|
+
priority: 3
|
|
367
|
+
},
|
|
368
|
+
info: {
|
|
369
|
+
label: 'INFO',
|
|
370
|
+
color: 'gray',
|
|
371
|
+
action: 'Consider when convenient',
|
|
372
|
+
priority: 4
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Default workflow state
|
|
378
|
+
*/
|
|
379
|
+
export const DEFAULT_STATE: AuditWorkflowState = {
|
|
380
|
+
version: '1.0.0',
|
|
381
|
+
startedAt: null,
|
|
382
|
+
lastUpdated: null,
|
|
383
|
+
currentPhase: null,
|
|
384
|
+
phases: {},
|
|
385
|
+
findings: [],
|
|
386
|
+
summary: null
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// AuditWorkflowEngine Class
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* AuditWorkflowEngine - Manages audit workflow
|
|
395
|
+
*/
|
|
396
|
+
export class AuditWorkflowEngine {
|
|
397
|
+
readonly projectRoot: string;
|
|
398
|
+
readonly workflowDir: string;
|
|
399
|
+
readonly stateFile: string;
|
|
400
|
+
readonly reportsDir: string;
|
|
401
|
+
readonly findingsDir: string;
|
|
402
|
+
readonly options: AuditWorkflowOptions;
|
|
403
|
+
state: AuditWorkflowState | null;
|
|
404
|
+
|
|
405
|
+
constructor(projectRoot: string, options: AuditWorkflowOptions = {}) {
|
|
406
|
+
this.projectRoot = projectRoot;
|
|
407
|
+
this.workflowDir = path.join(projectRoot, '.bootspring', 'audit');
|
|
408
|
+
this.stateFile = path.join(this.workflowDir, 'workflow-state.json');
|
|
409
|
+
this.reportsDir = path.join(this.workflowDir, 'reports');
|
|
410
|
+
this.findingsDir = path.join(this.workflowDir, 'findings');
|
|
411
|
+
this.options = {
|
|
412
|
+
severityFilter: options.severityFilter ?? null,
|
|
413
|
+
autoFix: options.autoFix ?? false,
|
|
414
|
+
ciMode: options.ciMode ?? false,
|
|
415
|
+
...options
|
|
416
|
+
};
|
|
417
|
+
this.state = null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Setup directories
|
|
422
|
+
*/
|
|
423
|
+
setupDirectories(): void {
|
|
424
|
+
const dirs = [this.workflowDir, this.reportsDir, this.findingsDir];
|
|
425
|
+
|
|
426
|
+
for (const dir of dirs) {
|
|
427
|
+
if (!fs.existsSync(dir)) {
|
|
428
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Load workflow state
|
|
435
|
+
*/
|
|
436
|
+
loadState(): boolean {
|
|
437
|
+
if (fs.existsSync(this.stateFile)) {
|
|
438
|
+
try {
|
|
439
|
+
this.state = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8')) as AuditWorkflowState;
|
|
440
|
+
return true;
|
|
441
|
+
} catch {
|
|
442
|
+
this.state = { ...DEFAULT_STATE, phases: {}, findings: [] };
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.state = { ...DEFAULT_STATE, phases: {}, findings: [] };
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Save workflow state
|
|
452
|
+
*/
|
|
453
|
+
saveState(): void {
|
|
454
|
+
if (!this.state) return;
|
|
455
|
+
this.setupDirectories();
|
|
456
|
+
this.state.lastUpdated = new Date().toISOString();
|
|
457
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Initialize workflow
|
|
462
|
+
*/
|
|
463
|
+
initializeWorkflow(): AuditWorkflowState {
|
|
464
|
+
this.setupDirectories();
|
|
465
|
+
this.state = {
|
|
466
|
+
...DEFAULT_STATE,
|
|
467
|
+
startedAt: new Date().toISOString(),
|
|
468
|
+
lastUpdated: new Date().toISOString(),
|
|
469
|
+
phases: {},
|
|
470
|
+
findings: []
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Initialize phase states
|
|
474
|
+
for (const phaseId of Object.keys(AUDIT_PHASES)) {
|
|
475
|
+
this.state.phases[phaseId] = {
|
|
476
|
+
status: 'pending',
|
|
477
|
+
startedAt: null,
|
|
478
|
+
completedAt: null,
|
|
479
|
+
result: null,
|
|
480
|
+
error: null
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.state.currentPhase = 'quality';
|
|
485
|
+
this.saveState();
|
|
486
|
+
return this.state;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check if workflow exists
|
|
491
|
+
*/
|
|
492
|
+
hasWorkflow(): boolean {
|
|
493
|
+
return fs.existsSync(this.stateFile);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Reset workflow
|
|
498
|
+
*/
|
|
499
|
+
resetWorkflow(): boolean {
|
|
500
|
+
if (fs.existsSync(this.stateFile)) {
|
|
501
|
+
fs.unlinkSync(this.stateFile);
|
|
502
|
+
}
|
|
503
|
+
this.state = null;
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Check if phase dependencies are met
|
|
509
|
+
*/
|
|
510
|
+
arePhaseDependenciesMet(phaseId: string): boolean {
|
|
511
|
+
const phase = AUDIT_PHASES[phaseId];
|
|
512
|
+
if (!phase || !phase.dependencies || phase.dependencies.length === 0) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const depPhaseId of phase.dependencies) {
|
|
517
|
+
const depPhase = this.state?.phases[depPhaseId];
|
|
518
|
+
if (!depPhase || (depPhase.status !== 'completed' && depPhase.status !== 'skipped')) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get next phase
|
|
528
|
+
*/
|
|
529
|
+
getNextPhase(): string | null {
|
|
530
|
+
if (!this.state) return null;
|
|
531
|
+
|
|
532
|
+
const phaseOrder = Object.keys(AUDIT_PHASES).sort((a, b) => {
|
|
533
|
+
const phaseA = AUDIT_PHASES[a];
|
|
534
|
+
const phaseB = AUDIT_PHASES[b];
|
|
535
|
+
return (phaseA?.order ?? 0) - (phaseB?.order ?? 0);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
for (const phaseId of phaseOrder) {
|
|
539
|
+
const phase = this.state.phases[phaseId];
|
|
540
|
+
if (phase?.status === 'pending' && this.arePhaseDependenciesMet(phaseId)) {
|
|
541
|
+
return phaseId;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Start a phase
|
|
550
|
+
*/
|
|
551
|
+
startPhase(phaseId: string): void {
|
|
552
|
+
if (!this.state) {
|
|
553
|
+
throw new Error('Workflow not initialized');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const phase = this.state.phases[phaseId];
|
|
557
|
+
if (!phase) {
|
|
558
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
phase.status = 'in_progress';
|
|
562
|
+
phase.startedAt = new Date().toISOString();
|
|
563
|
+
this.state.currentPhase = phaseId;
|
|
564
|
+
this.saveState();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Complete a phase
|
|
569
|
+
*/
|
|
570
|
+
completePhase(phaseId: string, result: unknown = null): void {
|
|
571
|
+
if (!this.state) {
|
|
572
|
+
throw new Error('Workflow not initialized');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const phase = this.state.phases[phaseId];
|
|
576
|
+
if (!phase) {
|
|
577
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
phase.status = 'completed';
|
|
581
|
+
phase.completedAt = new Date().toISOString();
|
|
582
|
+
phase.result = result;
|
|
583
|
+
this.saveState();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Fail a phase
|
|
588
|
+
*/
|
|
589
|
+
failPhase(phaseId: string, error: string): void {
|
|
590
|
+
if (!this.state) {
|
|
591
|
+
throw new Error('Workflow not initialized');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const phase = this.state.phases[phaseId];
|
|
595
|
+
if (!phase) {
|
|
596
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
phase.status = 'failed';
|
|
600
|
+
phase.completedAt = new Date().toISOString();
|
|
601
|
+
phase.error = error;
|
|
602
|
+
this.saveState();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Skip a phase
|
|
607
|
+
*/
|
|
608
|
+
skipPhase(phaseId: string): void {
|
|
609
|
+
if (!this.state) {
|
|
610
|
+
throw new Error('Workflow not initialized');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const phase = this.state.phases[phaseId];
|
|
614
|
+
if (!phase) {
|
|
615
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
phase.status = 'skipped';
|
|
619
|
+
phase.completedAt = new Date().toISOString();
|
|
620
|
+
this.saveState();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Add finding
|
|
625
|
+
*/
|
|
626
|
+
addFinding(finding: Omit<AuditFinding, 'id' | 'timestamp'>): void {
|
|
627
|
+
if (!this.state) return;
|
|
628
|
+
|
|
629
|
+
this.state.findings.push({
|
|
630
|
+
...finding,
|
|
631
|
+
id: `F${this.state.findings.length + 1}`,
|
|
632
|
+
timestamp: new Date().toISOString()
|
|
633
|
+
});
|
|
634
|
+
this.saveState();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Run quality metrics phase
|
|
639
|
+
*/
|
|
640
|
+
async runQualityMetrics(): Promise<QualityMetricsResult> {
|
|
641
|
+
const analyzer = new QualityAnalyzer(this.projectRoot);
|
|
642
|
+
const result = analyzer.analyze();
|
|
643
|
+
|
|
644
|
+
// Add findings for quality issues
|
|
645
|
+
for (const file of result.worstFiles) {
|
|
646
|
+
for (const issue of file.issues) {
|
|
647
|
+
this.addFinding({
|
|
648
|
+
phase: 'quality',
|
|
649
|
+
category: 'quality',
|
|
650
|
+
severity: issue.type === 'high-complexity' ? 'medium' : 'low',
|
|
651
|
+
title: issue.type,
|
|
652
|
+
description: issue.message,
|
|
653
|
+
file: file.path,
|
|
654
|
+
function: issue.function ?? null,
|
|
655
|
+
remediation: this.getQualityRemediation(issue.type)
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Add findings for code smells
|
|
661
|
+
for (const smell of result.smells) {
|
|
662
|
+
this.addFinding({
|
|
663
|
+
phase: 'quality',
|
|
664
|
+
category: 'code-smell',
|
|
665
|
+
severity: smell.severity,
|
|
666
|
+
title: smell.name,
|
|
667
|
+
description: `Found ${smell.count} instance(s)`,
|
|
668
|
+
file: smell.file,
|
|
669
|
+
remediation: this.getSmellRemediation(smell.id)
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Save report
|
|
674
|
+
const report = this.generateQualityReport(result);
|
|
675
|
+
fs.writeFileSync(
|
|
676
|
+
path.join(this.reportsDir, 'quality.md'),
|
|
677
|
+
report
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
score: result.scores.overall,
|
|
682
|
+
breakdown: result.scores.breakdown,
|
|
683
|
+
totalFiles: result.summary.totalFiles,
|
|
684
|
+
totalIssues: result.summary.totalIssues,
|
|
685
|
+
totalSmells: result.summary.totalSmells
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Get remediation for quality issue
|
|
691
|
+
*/
|
|
692
|
+
getQualityRemediation(issueType: string): string {
|
|
693
|
+
const remediations: Record<string, string> = {
|
|
694
|
+
'long-function': 'Break function into smaller, focused functions',
|
|
695
|
+
'high-complexity': 'Simplify logic by extracting helper functions or using early returns',
|
|
696
|
+
'too-many-params': 'Use an options object or break into multiple functions',
|
|
697
|
+
'long-file': 'Split into multiple modules with clear responsibilities',
|
|
698
|
+
'low-comments': 'Add JSDoc comments for public functions and complex logic'
|
|
699
|
+
};
|
|
700
|
+
return remediations[issueType] ?? 'Review and refactor as needed';
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get remediation for code smell
|
|
705
|
+
*/
|
|
706
|
+
getSmellRemediation(smellId: string): string {
|
|
707
|
+
const remediations: Record<string, string> = {
|
|
708
|
+
'todo-comment': 'Convert to a tracked issue or complete the TODO',
|
|
709
|
+
'fixme-comment': 'Address the issue and remove the comment',
|
|
710
|
+
'console-log': 'Remove console.log or replace with proper logging',
|
|
711
|
+
'debugger': 'Remove debugger statement before committing',
|
|
712
|
+
'empty-catch': 'Handle the error appropriately or add a comment explaining why it is safe to ignore',
|
|
713
|
+
'any-type': 'Add proper TypeScript types',
|
|
714
|
+
'ts-ignore': 'Fix the underlying TypeScript error',
|
|
715
|
+
'eslint-disable': 'Fix the underlying ESLint error or add specific disable reason'
|
|
716
|
+
};
|
|
717
|
+
return remediations[smellId] ?? 'Review and address as appropriate';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Generate quality report
|
|
722
|
+
*/
|
|
723
|
+
generateQualityReport(analysis: QualityAnalysisResult): string {
|
|
724
|
+
const lines: string[] = [
|
|
725
|
+
'# Quality Analysis Report',
|
|
726
|
+
'',
|
|
727
|
+
`Generated: ${new Date().toISOString()}`,
|
|
728
|
+
'',
|
|
729
|
+
'## Quality Score',
|
|
730
|
+
'',
|
|
731
|
+
`**Overall**: ${analysis.scores.overall}/100`,
|
|
732
|
+
'',
|
|
733
|
+
'| Category | Score |',
|
|
734
|
+
'|----------|-------|',
|
|
735
|
+
`| Complexity | ${analysis.scores.breakdown.complexity}/100 |`,
|
|
736
|
+
`| Maintainability | ${analysis.scores.breakdown.maintainability}/100 |`,
|
|
737
|
+
`| Documentation | ${analysis.scores.breakdown.documentation}/100 |`,
|
|
738
|
+
`| Code Smells | ${analysis.scores.breakdown.codeSmells}/100 |`,
|
|
739
|
+
'',
|
|
740
|
+
'## Summary',
|
|
741
|
+
'',
|
|
742
|
+
`- **Total Files**: ${analysis.summary.totalFiles}`,
|
|
743
|
+
`- **Total Lines**: ${analysis.summary.totalLines}`,
|
|
744
|
+
`- **Average Complexity**: ${analysis.summary.averageComplexity}`,
|
|
745
|
+
`- **Total Issues**: ${analysis.summary.totalIssues}`,
|
|
746
|
+
`- **Code Smells**: ${analysis.summary.totalSmells}`,
|
|
747
|
+
''
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
if (analysis.worstFiles.length > 0) {
|
|
751
|
+
lines.push('## Files Needing Attention');
|
|
752
|
+
lines.push('');
|
|
753
|
+
lines.push('| File | Issues | Complexity |');
|
|
754
|
+
lines.push('|------|--------|------------|');
|
|
755
|
+
for (const file of analysis.worstFiles.slice(0, 10)) {
|
|
756
|
+
lines.push(`| \`${file.path}\` | ${file.issues.length} | ${file.complexity} |`);
|
|
757
|
+
}
|
|
758
|
+
lines.push('');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (analysis.smellsByCategory && Object.keys(analysis.smellsByCategory).length > 0) {
|
|
762
|
+
lines.push('## Code Smells by Category');
|
|
763
|
+
lines.push('');
|
|
764
|
+
for (const [category, smells] of Object.entries(analysis.smellsByCategory)) {
|
|
765
|
+
lines.push(`### ${category}`);
|
|
766
|
+
for (const smell of smells.slice(0, 5)) {
|
|
767
|
+
lines.push(`- **${smell.name}** in \`${smell.file}\`: ${smell.count} instances`);
|
|
768
|
+
}
|
|
769
|
+
lines.push('');
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return lines.join('\n');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Run security scan phase
|
|
778
|
+
*/
|
|
779
|
+
async runSecurityScan(): Promise<SecurityScanResultSummary> {
|
|
780
|
+
const scanner = new SecurityScanner(this.projectRoot);
|
|
781
|
+
const result = scanner.scan();
|
|
782
|
+
|
|
783
|
+
// Add findings
|
|
784
|
+
for (const finding of result.findings) {
|
|
785
|
+
this.addFinding({
|
|
786
|
+
phase: 'security',
|
|
787
|
+
category: finding.type,
|
|
788
|
+
severity: finding.severity,
|
|
789
|
+
title: finding.name,
|
|
790
|
+
description: finding.message,
|
|
791
|
+
file: finding.file,
|
|
792
|
+
line: finding.line,
|
|
793
|
+
remediation: finding.remediation ?? this.getSecurityRemediation(finding.id)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Save findings by severity
|
|
798
|
+
const criticalFindings = result.bySeverity['critical'] ?? [];
|
|
799
|
+
const highFindings = result.bySeverity['high'] ?? [];
|
|
800
|
+
|
|
801
|
+
fs.writeFileSync(
|
|
802
|
+
path.join(this.findingsDir, 'critical.json'),
|
|
803
|
+
JSON.stringify(criticalFindings, null, 2)
|
|
804
|
+
);
|
|
805
|
+
fs.writeFileSync(
|
|
806
|
+
path.join(this.findingsDir, 'high.json'),
|
|
807
|
+
JSON.stringify(highFindings, null, 2)
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
// Save report
|
|
811
|
+
const report = this.generateSecurityReport(result);
|
|
812
|
+
fs.writeFileSync(
|
|
813
|
+
path.join(this.reportsDir, 'security.md'),
|
|
814
|
+
report
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
passed: result.summary.passed,
|
|
819
|
+
total: result.summary.total,
|
|
820
|
+
critical: result.summary.critical,
|
|
821
|
+
high: result.summary.high,
|
|
822
|
+
medium: result.summary.medium,
|
|
823
|
+
low: result.summary.low
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Get security remediation
|
|
829
|
+
*/
|
|
830
|
+
getSecurityRemediation(findingId: string): string {
|
|
831
|
+
const remediations: Record<string, string> = {
|
|
832
|
+
'aws-key': 'Remove AWS key and rotate credentials immediately',
|
|
833
|
+
'api-key': 'Move API key to environment variables',
|
|
834
|
+
'private-key': 'Remove private key from repository and rotate',
|
|
835
|
+
'sql-injection': 'Use parameterized queries',
|
|
836
|
+
'command-injection': 'Sanitize input and use spawn with array arguments',
|
|
837
|
+
'xss': 'Sanitize output and use content security policy'
|
|
838
|
+
};
|
|
839
|
+
return remediations[findingId] ?? 'Review and remediate security issue';
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Generate security report
|
|
844
|
+
*/
|
|
845
|
+
generateSecurityReport(analysis: SecurityScanResult): string {
|
|
846
|
+
const lines: string[] = [
|
|
847
|
+
'# Security Audit Report',
|
|
848
|
+
'',
|
|
849
|
+
`Generated: ${new Date().toISOString()}`,
|
|
850
|
+
'',
|
|
851
|
+
'## Summary',
|
|
852
|
+
'',
|
|
853
|
+
`**Status**: ${analysis.summary.passed ? 'PASSED' : 'FAILED'}`,
|
|
854
|
+
'',
|
|
855
|
+
'| Severity | Count |',
|
|
856
|
+
'|----------|-------|',
|
|
857
|
+
`| Critical | ${analysis.summary.critical} |`,
|
|
858
|
+
`| High | ${analysis.summary.high} |`,
|
|
859
|
+
`| Medium | ${analysis.summary.medium} |`,
|
|
860
|
+
`| Low | ${analysis.summary.low} |`,
|
|
861
|
+
`| Info | ${analysis.summary.info ?? 0} |`,
|
|
862
|
+
''
|
|
863
|
+
];
|
|
864
|
+
|
|
865
|
+
const criticalFindings = analysis.bySeverity['critical'] ?? [];
|
|
866
|
+
if (analysis.summary.critical > 0) {
|
|
867
|
+
lines.push('## Critical Findings');
|
|
868
|
+
lines.push('');
|
|
869
|
+
lines.push('> These require immediate attention.');
|
|
870
|
+
lines.push('');
|
|
871
|
+
for (const finding of criticalFindings) {
|
|
872
|
+
lines.push(`### ${finding.name}`);
|
|
873
|
+
lines.push(`- **File**: \`${finding.file}:${finding.line ?? 0}\``);
|
|
874
|
+
lines.push(`- **Message**: ${finding.message}`);
|
|
875
|
+
lines.push(`- **Remediation**: ${finding.remediation ?? 'Review and fix'}`);
|
|
876
|
+
lines.push('');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const highFindings = analysis.bySeverity['high'] ?? [];
|
|
881
|
+
if (analysis.summary.high > 0) {
|
|
882
|
+
lines.push('## High Severity Findings');
|
|
883
|
+
lines.push('');
|
|
884
|
+
for (const finding of highFindings) {
|
|
885
|
+
lines.push(`- **${finding.name}** in \`${finding.file}\`: ${finding.message}`);
|
|
886
|
+
}
|
|
887
|
+
lines.push('');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (analysis.byType.secret.length > 0) {
|
|
891
|
+
lines.push('## Potential Secrets');
|
|
892
|
+
lines.push('');
|
|
893
|
+
lines.push('> Remove these from version control and rotate credentials.');
|
|
894
|
+
lines.push('');
|
|
895
|
+
for (const finding of analysis.byType.secret) {
|
|
896
|
+
lines.push(`- **${finding.name}** in \`${finding.file}\``);
|
|
897
|
+
}
|
|
898
|
+
lines.push('');
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return lines.join('\n');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Run performance analysis phase
|
|
906
|
+
*/
|
|
907
|
+
async runPerformanceAnalysis(): Promise<PerformanceResult> {
|
|
908
|
+
const recommendations: Array<{ id: string; title: string; description: string }> = [];
|
|
909
|
+
|
|
910
|
+
// Check for bundle analysis config
|
|
911
|
+
const nextConfigPath = path.join(this.projectRoot, 'next.config.js');
|
|
912
|
+
if (fs.existsSync(nextConfigPath)) {
|
|
913
|
+
const content = fs.readFileSync(nextConfigPath, 'utf-8');
|
|
914
|
+
if (!content.includes('bundle-analyzer')) {
|
|
915
|
+
recommendations.push({
|
|
916
|
+
id: 'add-bundle-analyzer',
|
|
917
|
+
title: 'Add Bundle Analyzer',
|
|
918
|
+
description: 'Consider adding @next/bundle-analyzer to monitor bundle size'
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Check for dynamic imports
|
|
924
|
+
const hasDynamicImports = await this.checkForDynamicImports();
|
|
925
|
+
if (!hasDynamicImports) {
|
|
926
|
+
recommendations.push({
|
|
927
|
+
id: 'use-dynamic-imports',
|
|
928
|
+
title: 'Use Dynamic Imports',
|
|
929
|
+
description: 'Consider using dynamic imports for large components'
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Add findings
|
|
934
|
+
for (const rec of recommendations) {
|
|
935
|
+
this.addFinding({
|
|
936
|
+
phase: 'performance',
|
|
937
|
+
category: 'performance',
|
|
938
|
+
severity: 'low',
|
|
939
|
+
title: rec.title,
|
|
940
|
+
description: rec.description,
|
|
941
|
+
remediation: 'Implement the suggested optimization'
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
recommendations: recommendations.length
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Check for dynamic imports
|
|
952
|
+
*/
|
|
953
|
+
async checkForDynamicImports(): Promise<boolean> {
|
|
954
|
+
const patterns = [
|
|
955
|
+
/import\s*\(\s*['"][^'"]+['"]\s*\)/g,
|
|
956
|
+
/dynamic\s*\(\s*\(\)\s*=>\s*import/g,
|
|
957
|
+
/lazy\s*\(\s*\(\)\s*=>\s*import/g
|
|
958
|
+
];
|
|
959
|
+
|
|
960
|
+
const sourceDirs = ['src', 'app', 'pages', 'components'];
|
|
961
|
+
|
|
962
|
+
for (const dir of sourceDirs) {
|
|
963
|
+
const dirPath = path.join(this.projectRoot, dir);
|
|
964
|
+
if (fs.existsSync(dirPath)) {
|
|
965
|
+
const found = await this.searchForPatterns(dirPath, patterns);
|
|
966
|
+
if (found) return true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Search for patterns in directory
|
|
975
|
+
*/
|
|
976
|
+
async searchForPatterns(dir: string, patterns: RegExp[]): Promise<boolean> {
|
|
977
|
+
try {
|
|
978
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
979
|
+
|
|
980
|
+
for (const item of items) {
|
|
981
|
+
const fullPath = path.join(dir, item.name);
|
|
982
|
+
|
|
983
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
984
|
+
if (await this.searchForPatterns(fullPath, patterns)) {
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
} else if (item.isFile() && /\.(js|jsx|ts|tsx)$/.test(item.name)) {
|
|
988
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
989
|
+
for (const pattern of patterns) {
|
|
990
|
+
if (pattern.test(content)) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
// Skip
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Run best practices phase
|
|
1005
|
+
*/
|
|
1006
|
+
async runBestPractices(): Promise<BestPracticesResult> {
|
|
1007
|
+
const checks: Record<string, CheckResult> = {
|
|
1008
|
+
typescript: await this.checkTypeScriptConfig(),
|
|
1009
|
+
errorHandling: await this.checkErrorHandling(),
|
|
1010
|
+
envVars: await this.checkEnvVars(),
|
|
1011
|
+
gitignore: await this.checkGitignore()
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// Add findings for failed checks
|
|
1015
|
+
for (const [_check, result] of Object.entries(checks)) {
|
|
1016
|
+
if (!result.passed) {
|
|
1017
|
+
for (const issue of result.issues) {
|
|
1018
|
+
this.addFinding({
|
|
1019
|
+
phase: 'practices',
|
|
1020
|
+
category: 'best-practice',
|
|
1021
|
+
severity: issue.severity ?? 'medium',
|
|
1022
|
+
title: issue.title,
|
|
1023
|
+
description: issue.description,
|
|
1024
|
+
file: issue.file,
|
|
1025
|
+
remediation: issue.remediation
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const passedCount = Object.values(checks).filter(c => c.passed).length;
|
|
1032
|
+
|
|
1033
|
+
return {
|
|
1034
|
+
passed: passedCount,
|
|
1035
|
+
total: Object.keys(checks).length,
|
|
1036
|
+
checks
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Check TypeScript configuration
|
|
1042
|
+
*/
|
|
1043
|
+
async checkTypeScriptConfig(): Promise<CheckResult> {
|
|
1044
|
+
const result: CheckResult = { passed: true, issues: [] };
|
|
1045
|
+
const tsconfigPath = path.join(this.projectRoot, 'tsconfig.json');
|
|
1046
|
+
|
|
1047
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
1048
|
+
result.passed = false;
|
|
1049
|
+
result.issues.push({
|
|
1050
|
+
title: 'TypeScript Not Configured',
|
|
1051
|
+
description: 'No tsconfig.json found',
|
|
1052
|
+
severity: 'low',
|
|
1053
|
+
remediation: 'Consider using TypeScript for better type safety'
|
|
1054
|
+
});
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8')) as Record<string, unknown>;
|
|
1060
|
+
const compilerOptions = (tsconfig.compilerOptions ?? {}) as Record<string, unknown>;
|
|
1061
|
+
|
|
1062
|
+
if (!compilerOptions.strict) {
|
|
1063
|
+
result.passed = false;
|
|
1064
|
+
result.issues.push({
|
|
1065
|
+
title: 'TypeScript Strict Mode Disabled',
|
|
1066
|
+
description: 'strict mode is not enabled in tsconfig.json',
|
|
1067
|
+
file: 'tsconfig.json',
|
|
1068
|
+
severity: 'low',
|
|
1069
|
+
remediation: 'Enable "strict": true for better type safety'
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (compilerOptions.noImplicitAny === false) {
|
|
1074
|
+
result.passed = false;
|
|
1075
|
+
result.issues.push({
|
|
1076
|
+
title: 'Implicit Any Allowed',
|
|
1077
|
+
description: 'noImplicitAny is disabled',
|
|
1078
|
+
file: 'tsconfig.json',
|
|
1079
|
+
severity: 'low',
|
|
1080
|
+
remediation: 'Enable noImplicitAny for stricter typing'
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
// Invalid tsconfig
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Check error handling patterns
|
|
1092
|
+
*/
|
|
1093
|
+
async checkErrorHandling(): Promise<CheckResult> {
|
|
1094
|
+
const result: CheckResult = { passed: true, issues: [] };
|
|
1095
|
+
|
|
1096
|
+
// Check for global error handler in Next.js
|
|
1097
|
+
const errorPagePaths = [
|
|
1098
|
+
'app/error.tsx', 'app/error.js',
|
|
1099
|
+
'pages/_error.tsx', 'pages/_error.js',
|
|
1100
|
+
'src/app/error.tsx', 'src/pages/_error.tsx'
|
|
1101
|
+
];
|
|
1102
|
+
|
|
1103
|
+
let hasErrorPage = false;
|
|
1104
|
+
for (const errorPath of errorPagePaths) {
|
|
1105
|
+
if (fs.existsSync(path.join(this.projectRoot, errorPath))) {
|
|
1106
|
+
hasErrorPage = true;
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (!hasErrorPage) {
|
|
1112
|
+
result.passed = false;
|
|
1113
|
+
result.issues.push({
|
|
1114
|
+
title: 'No Global Error Handler',
|
|
1115
|
+
description: 'No error page found for handling runtime errors',
|
|
1116
|
+
severity: 'medium',
|
|
1117
|
+
remediation: 'Create an error.tsx (App Router) or _error.tsx (Pages Router)'
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return result;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Check environment variable handling
|
|
1126
|
+
*/
|
|
1127
|
+
async checkEnvVars(): Promise<CheckResult> {
|
|
1128
|
+
const result: CheckResult = { passed: true, issues: [] };
|
|
1129
|
+
|
|
1130
|
+
const envPath = path.join(this.projectRoot, '.env');
|
|
1131
|
+
const envExamplePath = path.join(this.projectRoot, '.env.example');
|
|
1132
|
+
const envLocalPath = path.join(this.projectRoot, '.env.local');
|
|
1133
|
+
|
|
1134
|
+
// Check for .env.example
|
|
1135
|
+
if (!fs.existsSync(envExamplePath)) {
|
|
1136
|
+
if (fs.existsSync(envPath) || fs.existsSync(envLocalPath)) {
|
|
1137
|
+
result.passed = false;
|
|
1138
|
+
result.issues.push({
|
|
1139
|
+
title: 'Missing .env.example',
|
|
1140
|
+
description: 'No .env.example file to document required environment variables',
|
|
1141
|
+
severity: 'low',
|
|
1142
|
+
remediation: 'Create .env.example with all required variables (without values)'
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Check .gitignore includes .env
|
|
1148
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
1149
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1150
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1151
|
+
if (!gitignore.includes('.env')) {
|
|
1152
|
+
result.passed = false;
|
|
1153
|
+
result.issues.push({
|
|
1154
|
+
title: '.env Not in .gitignore',
|
|
1155
|
+
description: '.env files should be excluded from version control',
|
|
1156
|
+
file: '.gitignore',
|
|
1157
|
+
severity: 'high',
|
|
1158
|
+
remediation: 'Add .env* to .gitignore'
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Check .gitignore configuration
|
|
1168
|
+
*/
|
|
1169
|
+
async checkGitignore(): Promise<CheckResult> {
|
|
1170
|
+
const result: CheckResult = { passed: true, issues: [] };
|
|
1171
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
1172
|
+
|
|
1173
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1174
|
+
result.passed = false;
|
|
1175
|
+
result.issues.push({
|
|
1176
|
+
title: 'Missing .gitignore',
|
|
1177
|
+
description: 'No .gitignore file found',
|
|
1178
|
+
severity: 'medium',
|
|
1179
|
+
remediation: 'Create a .gitignore file with appropriate exclusions'
|
|
1180
|
+
});
|
|
1181
|
+
return result;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1185
|
+
const requiredEntries = ['node_modules', '.env'];
|
|
1186
|
+
|
|
1187
|
+
for (const entry of requiredEntries) {
|
|
1188
|
+
if (!gitignore.includes(entry)) {
|
|
1189
|
+
result.passed = false;
|
|
1190
|
+
result.issues.push({
|
|
1191
|
+
title: `Missing ${entry} in .gitignore`,
|
|
1192
|
+
description: `${entry} should be in .gitignore`,
|
|
1193
|
+
file: '.gitignore',
|
|
1194
|
+
severity: entry === '.env' ? 'high' : 'low',
|
|
1195
|
+
remediation: `Add ${entry} to .gitignore`
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Run tech debt inventory phase
|
|
1205
|
+
*/
|
|
1206
|
+
async runTechDebtInventory(): Promise<TechDebtResult> {
|
|
1207
|
+
const techDebt: {
|
|
1208
|
+
todos: AuditFinding[];
|
|
1209
|
+
deprecated: unknown[];
|
|
1210
|
+
deadCode: unknown[];
|
|
1211
|
+
missingTests: unknown[];
|
|
1212
|
+
} = {
|
|
1213
|
+
todos: [],
|
|
1214
|
+
deprecated: [],
|
|
1215
|
+
deadCode: [],
|
|
1216
|
+
missingTests: []
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// Scan for TODOs (already captured in quality phase)
|
|
1220
|
+
const qualityFindings = this.state?.findings.filter(
|
|
1221
|
+
f => f.phase === 'quality' && f.category === 'code-smell'
|
|
1222
|
+
) ?? [];
|
|
1223
|
+
|
|
1224
|
+
techDebt.todos = qualityFindings.filter(
|
|
1225
|
+
f => f.title.toLowerCase().includes('todo') || f.title.toLowerCase().includes('fixme')
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
// Check for missing tests
|
|
1229
|
+
const srcDirs = ['src', 'lib', 'app', 'components'];
|
|
1230
|
+
let sourceFiles = 0;
|
|
1231
|
+
let testFiles = 0;
|
|
1232
|
+
|
|
1233
|
+
for (const dir of srcDirs) {
|
|
1234
|
+
const dirPath = path.join(this.projectRoot, dir);
|
|
1235
|
+
if (fs.existsSync(dirPath)) {
|
|
1236
|
+
const counts = await this.countSourceAndTestFiles(dirPath);
|
|
1237
|
+
sourceFiles += counts.source;
|
|
1238
|
+
testFiles += counts.test;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const testRatio = sourceFiles > 0 ? testFiles / sourceFiles : 0;
|
|
1243
|
+
if (testRatio < 0.2) {
|
|
1244
|
+
this.addFinding({
|
|
1245
|
+
phase: 'techDebt',
|
|
1246
|
+
category: 'tech-debt',
|
|
1247
|
+
severity: 'medium',
|
|
1248
|
+
title: 'Low Test Coverage',
|
|
1249
|
+
description: `Only ${Math.round(testRatio * 100)}% test file ratio`,
|
|
1250
|
+
remediation: 'Add unit and integration tests for critical paths'
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Check for unused dependencies (skip for large codebases)
|
|
1255
|
+
const LARGE_CODEBASE_THRESHOLD = 500;
|
|
1256
|
+
let unusedDeps: UnusedDependency[] = [];
|
|
1257
|
+
let depAnalysisSkipped = false;
|
|
1258
|
+
|
|
1259
|
+
// Estimate file count from source files scanned
|
|
1260
|
+
const estimatedFileCount = sourceFiles + testFiles;
|
|
1261
|
+
|
|
1262
|
+
if (estimatedFileCount < LARGE_CODEBASE_THRESHOLD) {
|
|
1263
|
+
const depAnalyzer = new DependencyAnalyzer(this.projectRoot);
|
|
1264
|
+
depAnalyzer.buildGraph();
|
|
1265
|
+
unusedDeps = depAnalyzer.findUnusedDependencies();
|
|
1266
|
+
|
|
1267
|
+
for (const dep of unusedDeps.slice(0, 10)) {
|
|
1268
|
+
this.addFinding({
|
|
1269
|
+
phase: 'techDebt',
|
|
1270
|
+
category: 'tech-debt',
|
|
1271
|
+
severity: 'low',
|
|
1272
|
+
title: 'Potentially Unused Dependency',
|
|
1273
|
+
description: `${dep.name} may be unused`,
|
|
1274
|
+
remediation: `Remove ${dep.name} if not needed, or verify it is used`
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
} else {
|
|
1278
|
+
depAnalysisSkipped = true;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Save tech debt report
|
|
1282
|
+
const report = this.generateTechDebtReport(techDebt, testRatio, unusedDeps, depAnalysisSkipped);
|
|
1283
|
+
fs.writeFileSync(
|
|
1284
|
+
path.join(this.reportsDir, 'tech-debt.md'),
|
|
1285
|
+
report
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
todos: techDebt.todos.length,
|
|
1290
|
+
unusedDeps: unusedDeps.length,
|
|
1291
|
+
testCoverage: Math.round(testRatio * 100),
|
|
1292
|
+
depAnalysisSkipped
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Count source and test files
|
|
1298
|
+
*/
|
|
1299
|
+
async countSourceAndTestFiles(dir: string): Promise<{ source: number; test: number }> {
|
|
1300
|
+
let source = 0;
|
|
1301
|
+
let test = 0;
|
|
1302
|
+
|
|
1303
|
+
try {
|
|
1304
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1305
|
+
|
|
1306
|
+
for (const item of items) {
|
|
1307
|
+
const fullPath = path.join(dir, item.name);
|
|
1308
|
+
|
|
1309
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
1310
|
+
const counts = await this.countSourceAndTestFiles(fullPath);
|
|
1311
|
+
source += counts.source;
|
|
1312
|
+
test += counts.test;
|
|
1313
|
+
} else if (item.isFile() && /\.(js|jsx|ts|tsx)$/.test(item.name)) {
|
|
1314
|
+
if (/\.(test|spec)\.(js|jsx|ts|tsx)$/.test(item.name)) {
|
|
1315
|
+
test++;
|
|
1316
|
+
} else {
|
|
1317
|
+
source++;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
// Skip
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return { source, test };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Generate tech debt report
|
|
1330
|
+
*/
|
|
1331
|
+
generateTechDebtReport(
|
|
1332
|
+
techDebt: { todos: AuditFinding[] },
|
|
1333
|
+
testRatio: number,
|
|
1334
|
+
unusedDeps: UnusedDependency[],
|
|
1335
|
+
depAnalysisSkipped: boolean = false
|
|
1336
|
+
): string {
|
|
1337
|
+
const lines: string[] = [
|
|
1338
|
+
'# Technical Debt Report',
|
|
1339
|
+
'',
|
|
1340
|
+
`Generated: ${new Date().toISOString()}`,
|
|
1341
|
+
'',
|
|
1342
|
+
'## Summary',
|
|
1343
|
+
'',
|
|
1344
|
+
`- **TODO/FIXME Comments**: ${techDebt.todos.length}`,
|
|
1345
|
+
`- **Test Coverage**: ${Math.round(testRatio * 100)}%`,
|
|
1346
|
+
`- **Unused Dependencies**: ${depAnalysisSkipped ? 'Skipped (large codebase)' : unusedDeps.length}`,
|
|
1347
|
+
''
|
|
1348
|
+
];
|
|
1349
|
+
|
|
1350
|
+
if (techDebt.todos.length > 0) {
|
|
1351
|
+
lines.push('## TODO/FIXME Items');
|
|
1352
|
+
lines.push('');
|
|
1353
|
+
for (const todo of techDebt.todos.slice(0, 20)) {
|
|
1354
|
+
lines.push(`- \`${todo.file ?? 'unknown'}\`: ${todo.title}`);
|
|
1355
|
+
}
|
|
1356
|
+
lines.push('');
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (unusedDeps.length > 0) {
|
|
1360
|
+
lines.push('## Potentially Unused Dependencies');
|
|
1361
|
+
lines.push('');
|
|
1362
|
+
for (const dep of unusedDeps) {
|
|
1363
|
+
lines.push(`- \`${dep.name}\` (${dep.type})`);
|
|
1364
|
+
}
|
|
1365
|
+
lines.push('');
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return lines.join('\n');
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Run recommendations phase
|
|
1373
|
+
*/
|
|
1374
|
+
async runRecommendations(): Promise<RecommendationsResult> {
|
|
1375
|
+
if (!this.state) {
|
|
1376
|
+
throw new Error('Workflow not initialized');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Group findings by severity
|
|
1380
|
+
const bySeverity: Record<AuditSeverity, AuditFinding[]> = {
|
|
1381
|
+
critical: [],
|
|
1382
|
+
high: [],
|
|
1383
|
+
medium: [],
|
|
1384
|
+
low: [],
|
|
1385
|
+
info: []
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
for (const finding of this.state.findings) {
|
|
1389
|
+
const severity = finding.severity ?? 'info';
|
|
1390
|
+
const arr = bySeverity[severity];
|
|
1391
|
+
if (arr) {
|
|
1392
|
+
arr.push(finding);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Generate prioritized recommendations
|
|
1397
|
+
const recommendations: Recommendation[] = [];
|
|
1398
|
+
|
|
1399
|
+
// P0: Critical findings
|
|
1400
|
+
for (const finding of bySeverity.critical) {
|
|
1401
|
+
recommendations.push({
|
|
1402
|
+
priority: 'P0',
|
|
1403
|
+
severity: 'critical',
|
|
1404
|
+
title: finding.title,
|
|
1405
|
+
description: finding.description,
|
|
1406
|
+
file: finding.file ?? undefined,
|
|
1407
|
+
remediation: finding.remediation
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// P1: High findings
|
|
1412
|
+
for (const finding of bySeverity.high.slice(0, 10)) {
|
|
1413
|
+
recommendations.push({
|
|
1414
|
+
priority: 'P1',
|
|
1415
|
+
severity: 'high',
|
|
1416
|
+
title: finding.title,
|
|
1417
|
+
description: finding.description,
|
|
1418
|
+
file: finding.file ?? undefined,
|
|
1419
|
+
remediation: finding.remediation
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// P2: Medium findings
|
|
1424
|
+
for (const finding of bySeverity.medium.slice(0, 10)) {
|
|
1425
|
+
recommendations.push({
|
|
1426
|
+
priority: 'P2',
|
|
1427
|
+
severity: 'medium',
|
|
1428
|
+
title: finding.title,
|
|
1429
|
+
description: finding.description,
|
|
1430
|
+
file: finding.file ?? undefined,
|
|
1431
|
+
remediation: finding.remediation
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// P3: Low findings (just summary)
|
|
1436
|
+
recommendations.push({
|
|
1437
|
+
priority: 'P3',
|
|
1438
|
+
severity: 'low',
|
|
1439
|
+
title: `${bySeverity.low.length} Low Priority Items`,
|
|
1440
|
+
description: 'Address when convenient',
|
|
1441
|
+
remediation: 'Review low priority findings in the audit report'
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
// Generate final report
|
|
1445
|
+
const report = this.generateFinalReport(bySeverity, recommendations);
|
|
1446
|
+
const reportPath = path.join(this.projectRoot, 'planning', 'AUDIT_REPORT.md');
|
|
1447
|
+
const planningDir = path.join(this.projectRoot, 'planning');
|
|
1448
|
+
|
|
1449
|
+
if (!fs.existsSync(planningDir)) {
|
|
1450
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
fs.writeFileSync(reportPath, report);
|
|
1454
|
+
|
|
1455
|
+
// Also save in workflow directory
|
|
1456
|
+
fs.writeFileSync(
|
|
1457
|
+
path.join(this.reportsDir, 'AUDIT_REPORT.md'),
|
|
1458
|
+
report
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
this.state.summary = {
|
|
1462
|
+
reportPath,
|
|
1463
|
+
totalFindings: this.state.findings.length,
|
|
1464
|
+
critical: bySeverity.critical.length,
|
|
1465
|
+
high: bySeverity.high.length,
|
|
1466
|
+
medium: bySeverity.medium.length,
|
|
1467
|
+
low: bySeverity.low.length,
|
|
1468
|
+
recommendations: recommendations.length,
|
|
1469
|
+
generatedAt: new Date().toISOString()
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
return {
|
|
1473
|
+
reportPath,
|
|
1474
|
+
recommendations: recommendations.length,
|
|
1475
|
+
passed: bySeverity.critical.length === 0
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Generate final audit report
|
|
1481
|
+
*/
|
|
1482
|
+
generateFinalReport(
|
|
1483
|
+
bySeverity: Record<AuditSeverity, AuditFinding[]>,
|
|
1484
|
+
recommendations: Recommendation[]
|
|
1485
|
+
): string {
|
|
1486
|
+
const totalFindings = Object.values(bySeverity).reduce((sum, arr) => sum + arr.length, 0);
|
|
1487
|
+
|
|
1488
|
+
const lines: string[] = [
|
|
1489
|
+
'# Audit Report',
|
|
1490
|
+
'',
|
|
1491
|
+
'**Generated by**: Bootspring Audit',
|
|
1492
|
+
`**Date**: ${new Date().toISOString().split('T')[0]}`,
|
|
1493
|
+
`**Status**: ${bySeverity.critical.length === 0 ? 'PASSED' : 'NEEDS ATTENTION'}`,
|
|
1494
|
+
'',
|
|
1495
|
+
'## Executive Summary',
|
|
1496
|
+
'',
|
|
1497
|
+
`This audit found **${totalFindings}** findings across quality, security, and best practices checks.`,
|
|
1498
|
+
'',
|
|
1499
|
+
'### Findings by Severity',
|
|
1500
|
+
'',
|
|
1501
|
+
'| Severity | Count | Action Required |',
|
|
1502
|
+
'|----------|-------|-----------------|',
|
|
1503
|
+
`| Critical | ${bySeverity.critical.length} | Immediate |`,
|
|
1504
|
+
`| High | ${bySeverity.high.length} | 1-2 days |`,
|
|
1505
|
+
`| Medium | ${bySeverity.medium.length} | 1-2 weeks |`,
|
|
1506
|
+
`| Low | ${bySeverity.low.length} | Backlog |`,
|
|
1507
|
+
''
|
|
1508
|
+
];
|
|
1509
|
+
|
|
1510
|
+
// Critical findings
|
|
1511
|
+
if (bySeverity.critical.length > 0) {
|
|
1512
|
+
lines.push('## Critical Findings');
|
|
1513
|
+
lines.push('');
|
|
1514
|
+
lines.push('> These require immediate attention.');
|
|
1515
|
+
lines.push('');
|
|
1516
|
+
for (const finding of bySeverity.critical) {
|
|
1517
|
+
lines.push(`### ${finding.title}`);
|
|
1518
|
+
lines.push(`- **Category**: ${finding.category}`);
|
|
1519
|
+
lines.push(`- **File**: ${finding.file ? `\`${finding.file}\`` : 'N/A'}`);
|
|
1520
|
+
lines.push(`- **Description**: ${finding.description}`);
|
|
1521
|
+
lines.push(`- **Remediation**: ${finding.remediation ?? 'Review and fix'}`);
|
|
1522
|
+
lines.push('');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// High findings
|
|
1527
|
+
if (bySeverity.high.length > 0) {
|
|
1528
|
+
lines.push('## High Priority Findings');
|
|
1529
|
+
lines.push('');
|
|
1530
|
+
for (const finding of bySeverity.high) {
|
|
1531
|
+
lines.push(`- **${finding.title}** ${finding.file ? `(\`${finding.file}\`)` : ''}: ${finding.description}`);
|
|
1532
|
+
}
|
|
1533
|
+
lines.push('');
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Recommendations
|
|
1537
|
+
lines.push('## Prioritized Recommendations');
|
|
1538
|
+
lines.push('');
|
|
1539
|
+
lines.push('| Priority | Title | Severity |');
|
|
1540
|
+
lines.push('|----------|-------|----------|');
|
|
1541
|
+
for (const rec of recommendations.slice(0, 15)) {
|
|
1542
|
+
lines.push(`| ${rec.priority} | ${rec.title} | ${rec.severity} |`);
|
|
1543
|
+
}
|
|
1544
|
+
lines.push('');
|
|
1545
|
+
|
|
1546
|
+
// Quality metrics
|
|
1547
|
+
const qualityPhase = this.state?.phases.quality;
|
|
1548
|
+
const qualityResult = qualityPhase?.result as { score?: number; totalFiles?: number; totalIssues?: number } | undefined;
|
|
1549
|
+
if (qualityResult) {
|
|
1550
|
+
lines.push('## Quality Metrics');
|
|
1551
|
+
lines.push('');
|
|
1552
|
+
lines.push(`- **Quality Score**: ${qualityResult.score ?? 0}/100`);
|
|
1553
|
+
lines.push(`- **Total Files**: ${qualityResult.totalFiles ?? 0}`);
|
|
1554
|
+
lines.push(`- **Total Issues**: ${qualityResult.totalIssues ?? 0}`);
|
|
1555
|
+
lines.push('');
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Security summary
|
|
1559
|
+
const securityPhase = this.state?.phases.security;
|
|
1560
|
+
const securityResult = securityPhase?.result as { passed?: boolean; critical?: number; high?: number } | undefined;
|
|
1561
|
+
if (securityResult) {
|
|
1562
|
+
lines.push('## Security Summary');
|
|
1563
|
+
lines.push('');
|
|
1564
|
+
lines.push(`- **Status**: ${securityResult.passed ? 'PASSED' : 'NEEDS ATTENTION'}`);
|
|
1565
|
+
lines.push(`- **Critical Issues**: ${securityResult.critical ?? 0}`);
|
|
1566
|
+
lines.push(`- **High Issues**: ${securityResult.high ?? 0}`);
|
|
1567
|
+
lines.push('');
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
lines.push('---');
|
|
1571
|
+
lines.push('');
|
|
1572
|
+
lines.push('*Generated by [Bootspring](https://bootspring.com)*');
|
|
1573
|
+
|
|
1574
|
+
return lines.join('\n');
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Get workflow progress
|
|
1579
|
+
*/
|
|
1580
|
+
getProgress(): AuditWorkflowProgress | null {
|
|
1581
|
+
if (!this.state) return null;
|
|
1582
|
+
|
|
1583
|
+
const phases: AuditPhaseProgress[] = Object.entries(AUDIT_PHASES).map(([phaseId, phase]) => {
|
|
1584
|
+
const phaseState = this.state?.phases[phaseId];
|
|
1585
|
+
const status: AuditPhaseStatus = phaseState?.status ?? 'pending';
|
|
1586
|
+
|
|
1587
|
+
return {
|
|
1588
|
+
id: phaseId,
|
|
1589
|
+
name: phase.name,
|
|
1590
|
+
description: phase.description,
|
|
1591
|
+
order: phase.order,
|
|
1592
|
+
required: phase.required,
|
|
1593
|
+
status,
|
|
1594
|
+
dependenciesMet: this.arePhaseDependenciesMet(phaseId)
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
const completedCount = phases.filter(p => p.status === 'completed').length;
|
|
1599
|
+
const activeCount = phases.filter(p => p.status !== 'skipped').length;
|
|
1600
|
+
|
|
1601
|
+
// Count findings by severity
|
|
1602
|
+
const findingsBySeverity = {
|
|
1603
|
+
critical: 0,
|
|
1604
|
+
high: 0,
|
|
1605
|
+
medium: 0,
|
|
1606
|
+
low: 0,
|
|
1607
|
+
info: 0
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
for (const finding of this.state.findings) {
|
|
1611
|
+
const severity = finding.severity ?? 'info';
|
|
1612
|
+
const current = findingsBySeverity[severity];
|
|
1613
|
+
if (current !== undefined) {
|
|
1614
|
+
findingsBySeverity[severity] = current + 1;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
return {
|
|
1619
|
+
currentPhase: this.state.currentPhase,
|
|
1620
|
+
startedAt: this.state.startedAt,
|
|
1621
|
+
lastUpdated: this.state.lastUpdated,
|
|
1622
|
+
phases,
|
|
1623
|
+
overall: {
|
|
1624
|
+
completed: completedCount,
|
|
1625
|
+
total: activeCount,
|
|
1626
|
+
percentage: activeCount > 0 ? Math.round((completedCount / activeCount) * 100) : 0
|
|
1627
|
+
},
|
|
1628
|
+
findings: {
|
|
1629
|
+
total: this.state.findings.length,
|
|
1630
|
+
...findingsBySeverity
|
|
1631
|
+
},
|
|
1632
|
+
isComplete: completedCount === activeCount,
|
|
1633
|
+
summary: this.state.summary
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Get resume point
|
|
1639
|
+
*/
|
|
1640
|
+
getResumePoint(): AuditResumePoint | null {
|
|
1641
|
+
if (!this.state || !this.state.currentPhase) {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const phase = AUDIT_PHASES[this.state.currentPhase];
|
|
1646
|
+
const phaseState = this.state.phases[this.state.currentPhase];
|
|
1647
|
+
|
|
1648
|
+
return {
|
|
1649
|
+
phase: this.state.currentPhase,
|
|
1650
|
+
phaseName: phase?.name,
|
|
1651
|
+
phaseStatus: phaseState?.status,
|
|
1652
|
+
lastUpdated: this.state.lastUpdated,
|
|
1653
|
+
findingsCount: this.state.findings.length
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Get exit code for CI mode
|
|
1659
|
+
*/
|
|
1660
|
+
getExitCode(): number {
|
|
1661
|
+
if (!this.state) return 1;
|
|
1662
|
+
|
|
1663
|
+
const criticalCount = this.state.findings.filter(f => f.severity === 'critical').length;
|
|
1664
|
+
const highCount = this.state.findings.filter(f => f.severity === 'high').length;
|
|
1665
|
+
|
|
1666
|
+
if (criticalCount > 0) return 2;
|
|
1667
|
+
if (highCount > 0) return 1;
|
|
1668
|
+
return 0;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// ============================================================================
|
|
1673
|
+
// Factory Function
|
|
1674
|
+
// ============================================================================
|
|
1675
|
+
|
|
1676
|
+
export function createAuditWorkflowEngine(
|
|
1677
|
+
projectRoot: string,
|
|
1678
|
+
options: AuditWorkflowOptions = {}
|
|
1679
|
+
): AuditWorkflowEngine {
|
|
1680
|
+
return new AuditWorkflowEngine(projectRoot, options);
|
|
1681
|
+
}
|