@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.
Files changed (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. 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
+ }