popeye-cli 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +148 -7
  2. package/cheatsheet.md +440 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +3 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +3 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/commands/review.d.ts +31 -0
  16. package/dist/cli/commands/review.d.ts.map +1 -0
  17. package/dist/cli/commands/review.js +156 -0
  18. package/dist/cli/commands/review.js.map +1 -0
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +4 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/interactive.d.ts.map +1 -1
  23. package/dist/cli/interactive.js +218 -61
  24. package/dist/cli/interactive.js.map +1 -1
  25. package/dist/generators/admin-wizard.d.ts +25 -0
  26. package/dist/generators/admin-wizard.d.ts.map +1 -0
  27. package/dist/generators/admin-wizard.js +123 -0
  28. package/dist/generators/admin-wizard.js.map +1 -0
  29. package/dist/generators/all.d.ts.map +1 -1
  30. package/dist/generators/all.js +10 -3
  31. package/dist/generators/all.js.map +1 -1
  32. package/dist/generators/database.d.ts +58 -0
  33. package/dist/generators/database.d.ts.map +1 -0
  34. package/dist/generators/database.js +229 -0
  35. package/dist/generators/database.js.map +1 -0
  36. package/dist/generators/fullstack.d.ts.map +1 -1
  37. package/dist/generators/fullstack.js +23 -7
  38. package/dist/generators/fullstack.js.map +1 -1
  39. package/dist/generators/index.d.ts +2 -0
  40. package/dist/generators/index.d.ts.map +1 -1
  41. package/dist/generators/index.js +2 -0
  42. package/dist/generators/index.js.map +1 -1
  43. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  44. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-python.js +425 -0
  46. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  47. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  48. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  49. package/dist/generators/templates/admin-wizard-react.js +554 -0
  50. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  51. package/dist/generators/templates/database-docker.d.ts +23 -0
  52. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  53. package/dist/generators/templates/database-docker.js +221 -0
  54. package/dist/generators/templates/database-docker.js.map +1 -0
  55. package/dist/generators/templates/database-python.d.ts +54 -0
  56. package/dist/generators/templates/database-python.d.ts.map +1 -0
  57. package/dist/generators/templates/database-python.js +723 -0
  58. package/dist/generators/templates/database-python.js.map +1 -0
  59. package/dist/generators/templates/database-typescript.d.ts +34 -0
  60. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  61. package/dist/generators/templates/database-typescript.js +232 -0
  62. package/dist/generators/templates/database-typescript.js.map +1 -0
  63. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  64. package/dist/generators/templates/fullstack.js +29 -0
  65. package/dist/generators/templates/fullstack.js.map +1 -1
  66. package/dist/generators/templates/index.d.ts +5 -0
  67. package/dist/generators/templates/index.d.ts.map +1 -1
  68. package/dist/generators/templates/index.js +5 -0
  69. package/dist/generators/templates/index.js.map +1 -1
  70. package/dist/state/index.d.ts +10 -0
  71. package/dist/state/index.d.ts.map +1 -1
  72. package/dist/state/index.js +21 -0
  73. package/dist/state/index.js.map +1 -1
  74. package/dist/types/audit.d.ts +623 -0
  75. package/dist/types/audit.d.ts.map +1 -0
  76. package/dist/types/audit.js +240 -0
  77. package/dist/types/audit.js.map +1 -0
  78. package/dist/types/database-runtime.d.ts +86 -0
  79. package/dist/types/database-runtime.d.ts.map +1 -0
  80. package/dist/types/database-runtime.js +61 -0
  81. package/dist/types/database-runtime.js.map +1 -0
  82. package/dist/types/database.d.ts +85 -0
  83. package/dist/types/database.d.ts.map +1 -0
  84. package/dist/types/database.js +71 -0
  85. package/dist/types/database.js.map +1 -0
  86. package/dist/types/index.d.ts +2 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +4 -0
  89. package/dist/types/index.js.map +1 -1
  90. package/dist/types/workflow.d.ts +36 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +7 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/audit-analyzer.d.ts +58 -0
  95. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  96. package/dist/workflow/audit-analyzer.js +420 -0
  97. package/dist/workflow/audit-analyzer.js.map +1 -0
  98. package/dist/workflow/audit-mode.d.ts +28 -0
  99. package/dist/workflow/audit-mode.d.ts.map +1 -0
  100. package/dist/workflow/audit-mode.js +169 -0
  101. package/dist/workflow/audit-mode.js.map +1 -0
  102. package/dist/workflow/audit-recovery.d.ts +61 -0
  103. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  104. package/dist/workflow/audit-recovery.js +242 -0
  105. package/dist/workflow/audit-recovery.js.map +1 -0
  106. package/dist/workflow/audit-reporter.d.ts +65 -0
  107. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  108. package/dist/workflow/audit-reporter.js +301 -0
  109. package/dist/workflow/audit-reporter.js.map +1 -0
  110. package/dist/workflow/audit-scanner.d.ts +87 -0
  111. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  112. package/dist/workflow/audit-scanner.js +768 -0
  113. package/dist/workflow/audit-scanner.js.map +1 -0
  114. package/dist/workflow/db-setup-runner.d.ts +63 -0
  115. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  116. package/dist/workflow/db-setup-runner.js +336 -0
  117. package/dist/workflow/db-setup-runner.js.map +1 -0
  118. package/dist/workflow/db-state-machine.d.ts +30 -0
  119. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  120. package/dist/workflow/db-state-machine.js +51 -0
  121. package/dist/workflow/db-state-machine.js.map +1 -0
  122. package/dist/workflow/index.d.ts +7 -0
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +7 -0
  125. package/dist/workflow/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/commands/db.ts +281 -0
  128. package/src/cli/commands/doctor.ts +273 -0
  129. package/src/cli/commands/index.ts +3 -0
  130. package/src/cli/commands/review.ts +187 -0
  131. package/src/cli/index.ts +6 -0
  132. package/src/cli/interactive.ts +174 -4
  133. package/src/generators/admin-wizard.ts +146 -0
  134. package/src/generators/all.ts +10 -3
  135. package/src/generators/database.ts +286 -0
  136. package/src/generators/fullstack.ts +26 -9
  137. package/src/generators/index.ts +12 -0
  138. package/src/generators/templates/admin-wizard-python.ts +431 -0
  139. package/src/generators/templates/admin-wizard-react.ts +560 -0
  140. package/src/generators/templates/database-docker.ts +227 -0
  141. package/src/generators/templates/database-python.ts +734 -0
  142. package/src/generators/templates/database-typescript.ts +238 -0
  143. package/src/generators/templates/fullstack.ts +29 -0
  144. package/src/generators/templates/index.ts +5 -0
  145. package/src/state/index.ts +28 -0
  146. package/src/types/audit.ts +294 -0
  147. package/src/types/database-runtime.ts +69 -0
  148. package/src/types/database.ts +84 -0
  149. package/src/types/index.ts +29 -0
  150. package/src/types/workflow.ts +20 -0
  151. package/src/workflow/audit-analyzer.ts +491 -0
  152. package/src/workflow/audit-mode.ts +240 -0
  153. package/src/workflow/audit-recovery.ts +284 -0
  154. package/src/workflow/audit-reporter.ts +370 -0
  155. package/src/workflow/audit-scanner.ts +873 -0
  156. package/src/workflow/db-setup-runner.ts +391 -0
  157. package/src/workflow/db-state-machine.ts +58 -0
  158. package/src/workflow/index.ts +7 -0
  159. package/tests/cli/commands/review.test.ts +52 -0
  160. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  161. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  162. package/tests/generators/cross-phase-integration.test.ts +383 -0
  163. package/tests/generators/database.test.ts +456 -0
  164. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  165. package/tests/types/audit.test.ts +250 -0
  166. package/tests/types/database-runtime.test.ts +158 -0
  167. package/tests/types/database.test.ts +187 -0
  168. package/tests/workflow/audit-analyzer.test.ts +281 -0
  169. package/tests/workflow/audit-mode.test.ts +114 -0
  170. package/tests/workflow/audit-recovery.test.ts +237 -0
  171. package/tests/workflow/audit-reporter.test.ts +254 -0
  172. package/tests/workflow/audit-scanner.test.ts +270 -0
  173. package/tests/workflow/db-setup-runner.test.ts +211 -0
  174. package/tests/workflow/db-state-machine.test.ts +117 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Database configuration types and Zod schemas
3
+ * Defines DB lifecycle states, provisioning modes, and config tracking
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Database lifecycle status
10
+ */
11
+ export const DbStatusSchema = z.enum([
12
+ 'unconfigured',
13
+ 'configured',
14
+ 'applying',
15
+ 'ready',
16
+ 'error',
17
+ ]);
18
+ export type DbStatus = z.infer<typeof DbStatusSchema>;
19
+
20
+ /**
21
+ * Database provisioning mode
22
+ * - local_docker: PostgreSQL runs in Docker Compose
23
+ * - managed: External managed database (Neon, Supabase, etc.)
24
+ */
25
+ export const DbModeSchema = z.enum(['local_docker', 'managed']);
26
+ export type DbMode = z.infer<typeof DbModeSchema>;
27
+
28
+ /**
29
+ * Database provider (informational only)
30
+ */
31
+ export const DbProviderSchema = z.enum(['neon', 'supabase', 'other']);
32
+ export type DbProvider = z.infer<typeof DbProviderSchema>;
33
+
34
+ /**
35
+ * Backend ORM choice
36
+ */
37
+ export const BackendOrmSchema = z.enum(['sqlalchemy', 'prisma', 'drizzle']);
38
+ export type BackendOrm = z.infer<typeof BackendOrmSchema>;
39
+
40
+ /**
41
+ * Setup pipeline steps (forward compat for Phase 2)
42
+ */
43
+ export const DbSetupStepSchema = z.enum([
44
+ 'check_connection',
45
+ 'ensure_extensions',
46
+ 'apply_migrations',
47
+ 'seed_minimal',
48
+ 'readiness_tests',
49
+ 'mark_ready',
50
+ ]);
51
+ export type DbSetupStep = z.infer<typeof DbSetupStepSchema>;
52
+
53
+ /**
54
+ * Main database configuration tracking object
55
+ */
56
+ export const DbConfigSchema = z.object({
57
+ /** Whether DB layer was generated */
58
+ designed: z.boolean(),
59
+ /** Provisioning mode - unset until user configures */
60
+ mode: DbModeSchema.optional(),
61
+ /** Whether pgvector is included */
62
+ vectorRequired: z.boolean(),
63
+ /** Current lifecycle state */
64
+ status: DbStatusSchema,
65
+ /** Last error message */
66
+ lastError: z.string().optional(),
67
+ /** Number of migrations applied (updated by runner, not generator) */
68
+ migrationsApplied: z.number(),
69
+ /** ISO timestamp of last readiness check */
70
+ readinessCheckedAt: z.string().optional(),
71
+ });
72
+ export type DbConfig = z.infer<typeof DbConfigSchema>;
73
+
74
+ /**
75
+ * Default DB config for new fullstack/all projects
76
+ * vectorRequired: true because fullstack/all projects get pgvector by default
77
+ * mode is intentionally absent (unset until user configures provisioning)
78
+ */
79
+ export const DEFAULT_DB_CONFIG: DbConfig = {
80
+ designed: true,
81
+ status: 'unconfigured',
82
+ vectorRequired: true,
83
+ migrationsApplied: 0,
84
+ };
@@ -101,6 +101,35 @@ export {
101
101
  type DiscoveredTestCommands,
102
102
  } from './tester.js';
103
103
 
104
+ // Database types
105
+ export {
106
+ DbStatusSchema,
107
+ DbModeSchema,
108
+ DbProviderSchema,
109
+ BackendOrmSchema,
110
+ DbSetupStepSchema,
111
+ DbConfigSchema,
112
+ DEFAULT_DB_CONFIG,
113
+ type DbStatus,
114
+ type DbMode,
115
+ type DbProvider,
116
+ type BackendOrm,
117
+ type DbSetupStep,
118
+ type DbConfig,
119
+ } from './database.js';
120
+
121
+ // Database runtime types
122
+ export {
123
+ SetupStepResultSchema,
124
+ SetupResultSchema,
125
+ ReadinessCheckSchema,
126
+ ReadinessResultSchema,
127
+ type SetupStepResult,
128
+ type SetupResult,
129
+ type ReadinessCheck,
130
+ type ReadinessResult,
131
+ } from './database-runtime.js';
132
+
104
133
  // CLI types
105
134
  export {
106
135
  EXIT_CODES,
@@ -9,6 +9,8 @@ import type { OutputLanguage, OpenAIModel } from './project.js';
9
9
  import type { ConsensusIteration } from './consensus.js';
10
10
  import type { TestPlanOutput } from './tester.js';
11
11
  import { TestPlanOutputSchema, TestVerdictSchema } from './tester.js';
12
+ import type { DbConfig } from './database.js';
13
+ import { DbConfigSchema } from './database.js';
12
14
 
13
15
  /**
14
16
  * Workflow phases
@@ -230,6 +232,18 @@ export interface ProjectState {
230
232
  sourceDocPaths?: string[];
231
233
  /** Whether QA Tester skill is active (default: true for new projects, undefined/false for existing) */
232
234
  qaEnabled?: boolean;
235
+ /** Database configuration tracking (workspace projects only) */
236
+ dbConfig?: DbConfig;
237
+ /** Path to most recent audit report JSON (relative to .popeye/) */
238
+ auditReportPath?: string;
239
+ /** Path to most recent audit summary JSON */
240
+ auditSummaryPath?: string;
241
+ /** Whether recovery milestones from audit are being executed */
242
+ auditRecoveryInProgress?: boolean;
243
+ /** ISO timestamp of last audit run */
244
+ auditLastRunAt?: string;
245
+ /** Unique identifier for the audit run */
246
+ auditRunId?: string;
233
247
  }
234
248
 
235
249
  /**
@@ -276,6 +290,12 @@ export const ProjectStateSchema = z.object({
276
290
  strategyError: z.string().optional(),
277
291
  sourceDocPaths: z.array(z.string()).optional(),
278
292
  qaEnabled: z.boolean().optional(),
293
+ dbConfig: DbConfigSchema.optional(),
294
+ auditReportPath: z.string().optional(),
295
+ auditSummaryPath: z.string().optional(),
296
+ auditRecoveryInProgress: z.boolean().optional(),
297
+ auditLastRunAt: z.string().optional(),
298
+ auditRunId: z.string().optional(),
279
299
  });
280
300
 
281
301
  /**
@@ -0,0 +1,491 @@
1
+ /**
2
+ * AI-powered project analyzer for the audit system.
3
+ *
4
+ * Constructs analysis prompts, executes them through Claude with Serena-first
5
+ * search strategy (with retries and fallback), parses findings, and scores.
6
+ */
7
+
8
+ import { executePrompt, type ClaudeExecuteResult } from '../adapters/claude.js';
9
+ import type { ProjectState } from '../types/workflow.js';
10
+ import type {
11
+ AuditCategory,
12
+ AuditFinding,
13
+ AuditModeOptions,
14
+ ProjectScanResult,
15
+ SearchMetadata,
16
+ WiringMismatch,
17
+ } from '../types/audit.js';
18
+ import { AuditFindingSchema } from '../types/audit.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const SERENA_TOOLS = [
25
+ 'mcp__serena__find_symbol',
26
+ 'mcp__serena__get_symbols_overview',
27
+ 'mcp__serena__search_symbol',
28
+ 'mcp__serena__get_file_symbols',
29
+ ];
30
+
31
+ const FALLBACK_TOOLS = [
32
+ 'Read', 'Glob', 'Grep',
33
+ ];
34
+
35
+ const ALL_AUDIT_TOOLS = [...SERENA_TOOLS, ...FALLBACK_TOOLS];
36
+
37
+ const MAX_SERENA_RETRIES = 2;
38
+
39
+ const CATEGORY_WEIGHTS: Record<AuditCategory, number> = {
40
+ 'feature-completeness': 25,
41
+ 'integration-wiring': 15,
42
+ 'test-coverage': 15,
43
+ 'config-deployment': 10,
44
+ 'dependency-sanity': 10,
45
+ 'consistency': 10,
46
+ 'security': 10,
47
+ 'documentation': 5,
48
+ };
49
+
50
+ const SEVERITY_DEDUCTIONS: Record<string, number> = {
51
+ critical: 20,
52
+ major: 10,
53
+ minor: 3,
54
+ info: 0,
55
+ };
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Prompt construction
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Build the analysis prompt for Claude, embedding scan results and context.
63
+ *
64
+ * @param scan - The project scan result.
65
+ * @param state - Current project state.
66
+ * @param depth - Audit depth (1=shallow, 2=standard, 3=deep).
67
+ * @param strict - Whether to use strict scoring.
68
+ * @returns The complete analysis prompt string.
69
+ */
70
+ export function buildAnalysisPrompt(
71
+ scan: ProjectScanResult,
72
+ state: ProjectState,
73
+ depth: number,
74
+ strict: boolean
75
+ ): string {
76
+ const sections: string[] = [];
77
+
78
+ sections.push(`# Project Audit Analysis
79
+
80
+ You are auditing the project "${state.name}" (language: ${scan.language}).
81
+ Depth level: ${depth} (1=shallow checks, 2=standard, 3=deep investigation).
82
+ ${strict ? 'STRICT MODE: Apply higher standards for all checks.' : ''}
83
+
84
+ ## Instructions
85
+ Analyze the project and return findings as a JSON array of objects. Each finding must have:
86
+ - id: string (format "AUD-NNN")
87
+ - category: one of "feature-completeness", "integration-wiring", "test-coverage", "config-deployment", "dependency-sanity", "consistency", "security", "documentation"
88
+ - severity: one of "critical", "major", "minor", "info"
89
+ - title: short summary
90
+ - description: detailed explanation
91
+ - evidence: array of { file: string, line?: number, snippet?: string, description?: string }
92
+ - recommendation: actionable fix
93
+ - autoFixable: boolean
94
+
95
+ Use Serena tools first for code navigation. If they fail, fall back to Read/Glob/Grep.
96
+
97
+ Return ONLY a JSON array wrapped in \`\`\`json fences. No other text.`);
98
+
99
+ // Component structure
100
+ sections.push(`## Component Structure
101
+ Components detected: ${scan.detectedComposition.join(', ')}
102
+ State language: ${scan.stateLanguage}
103
+ Composition mismatch: ${scan.compositionMismatch}
104
+
105
+ ${scan.components.map((c) =>
106
+ `### ${c.kind} (${c.rootDir})
107
+ - Language: ${c.language}${c.framework ? `, Framework: ${c.framework}` : ''}
108
+ - Source files: ${c.sourceFiles.length}
109
+ - Test files: ${c.testFiles.length}
110
+ - Entry points: ${c.entryPoints.join(', ') || 'none'}
111
+ - Route files: ${c.routeFiles.join(', ') || 'none'}
112
+ - Dependencies: ${c.dependencyManifests.map((d) => d.file).join(', ') || 'none'}`
113
+ ).join('\n\n')}`);
114
+
115
+ // File tree
116
+ sections.push(`## Project Tree (truncated)
117
+ \`\`\`
118
+ ${scan.tree}
119
+ \`\`\``);
120
+
121
+ // Totals
122
+ sections.push(`## Totals
123
+ - Source files: ${scan.totalSourceFiles}
124
+ - Test files: ${scan.totalTestFiles}
125
+ - Lines of code: ${scan.totalLinesOfCode}
126
+ - Lines of tests: ${scan.totalLinesOfTests}
127
+ - Config files: ${scan.configFiles.join(', ') || 'none'}`);
128
+
129
+ // CLAUDE.md context
130
+ if (scan.claudeMdContent) {
131
+ sections.push(`## CLAUDE.md (project instructions)
132
+ \`\`\`
133
+ ${scan.claudeMdContent.slice(0, 3000)}
134
+ \`\`\``);
135
+ }
136
+
137
+ // README
138
+ if (scan.readmeContent) {
139
+ sections.push(`## README.md
140
+ \`\`\`
141
+ ${scan.readmeContent.slice(0, 3000)}
142
+ \`\`\``);
143
+ }
144
+
145
+ // Specification from state
146
+ if (state.specification) {
147
+ sections.push(`## Project Specification
148
+ \`\`\`
149
+ ${state.specification.slice(0, 3000)}
150
+ \`\`\``);
151
+ }
152
+
153
+ // Milestone status
154
+ if (state.milestones && state.milestones.length > 0) {
155
+ sections.push(`## Milestone Status
156
+ ${state.milestones.map((m) =>
157
+ `- ${m.name}: ${m.status} (${m.tasks.filter((t) => t.status === 'complete').length}/${m.tasks.length} tasks)`
158
+ ).join('\n')}`);
159
+ }
160
+
161
+ // Wiring matrix pre-analysis
162
+ if (scan.wiring) {
163
+ sections.push(`## FE<->BE Wiring Matrix
164
+ - Frontend API env keys: ${scan.wiring.frontendApiBaseEnvKeys.join(', ') || 'none'}
165
+ - Frontend API resolved: ${scan.wiring.frontendApiBaseResolved || 'not set'}
166
+ - Backend CORS origins: ${scan.wiring.backendCorsOrigins?.join(', ') || 'not found'}
167
+ - Backend API prefix: ${scan.wiring.backendApiPrefix || 'not found'}
168
+ - Detected mismatches: ${scan.wiring.potentialMismatches.length}`);
169
+ }
170
+
171
+ // Env + Docker
172
+ if (scan.envExampleContent) {
173
+ sections.push(`## .env.example
174
+ \`\`\`
175
+ ${scan.envExampleContent}
176
+ \`\`\``);
177
+ }
178
+ if (scan.dockerComposeContent) {
179
+ sections.push(`## docker-compose.yml
180
+ \`\`\`
181
+ ${scan.dockerComposeContent}
182
+ \`\`\``);
183
+ }
184
+
185
+ // Depth-specific instructions
186
+ if (depth >= 2) {
187
+ sections.push(`## Depth-2 Checks
188
+ - Verify test coverage for all route handlers
189
+ - Check for missing error boundaries / error handling
190
+ - Validate dependency versions are not wildly outdated
191
+ - Confirm env variables used in code match .env.example`);
192
+ }
193
+ if (depth >= 3) {
194
+ sections.push(`## Depth-3 Checks
195
+ - Trace data flow from API endpoints to database
196
+ - Check for security issues (OWASP Top 10)
197
+ - Verify all imports resolve correctly
198
+ - Check for dead code and unused exports`);
199
+ }
200
+
201
+ return sections.join('\n\n');
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // JSON parsing
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /**
209
+ * Parse AI response into validated AuditFinding objects.
210
+ *
211
+ * Handles JSON wrapped in code fences, partial JSON, and malformed responses.
212
+ *
213
+ * @param rawResponse - Raw AI response text.
214
+ * @returns Array of validated findings.
215
+ */
216
+ export function parseAuditFindings(rawResponse: string): AuditFinding[] {
217
+ // Extract JSON from code fences
218
+ const jsonMatch = rawResponse.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
219
+ const jsonStr = jsonMatch ? jsonMatch[1].trim() : rawResponse.trim();
220
+
221
+ let parsed: unknown;
222
+ try {
223
+ parsed = JSON.parse(jsonStr);
224
+ } catch {
225
+ // Attempt to find a JSON array in the response
226
+ const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
227
+ if (arrayMatch) {
228
+ try {
229
+ parsed = JSON.parse(arrayMatch[0]);
230
+ } catch {
231
+ return [];
232
+ }
233
+ } else {
234
+ return [];
235
+ }
236
+ }
237
+
238
+ if (!Array.isArray(parsed)) {
239
+ return [];
240
+ }
241
+
242
+ const findings: AuditFinding[] = [];
243
+ for (const item of parsed) {
244
+ try {
245
+ const finding = AuditFindingSchema.parse(item);
246
+ findings.push(finding);
247
+ } catch {
248
+ // Skip malformed findings — partial results better than none
249
+ }
250
+ }
251
+
252
+ return findings;
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Deterministic findings from wiring/composition
257
+ // ---------------------------------------------------------------------------
258
+
259
+ /**
260
+ * Generate deterministic findings from wiring mismatches and composition issues.
261
+ *
262
+ * @param scan - The project scan result.
263
+ * @returns Array of deterministic findings.
264
+ */
265
+ function generateDeterministicFindings(scan: ProjectScanResult): AuditFinding[] {
266
+ const findings: AuditFinding[] = [];
267
+ let counter = 900; // Reason: Start at 900 to avoid ID collision with AI findings
268
+
269
+ // Composition mismatch
270
+ if (scan.compositionMismatch) {
271
+ findings.push({
272
+ id: `AUD-${counter++}`,
273
+ category: 'consistency',
274
+ severity: 'major',
275
+ title: 'Workspace composition mismatch',
276
+ description: `State language is "${scan.stateLanguage}" but filesystem shows components: [${scan.detectedComposition.join(', ')}]. This may indicate an incomplete workspace setup or a stale state file.`,
277
+ evidence: [
278
+ { file: '.popeye/state.json', description: `language: "${scan.stateLanguage}"` },
279
+ ],
280
+ recommendation: 'Verify that all expected workspace apps are created and update project language if needed.',
281
+ autoFixable: false,
282
+ });
283
+ }
284
+
285
+ // Wiring mismatches
286
+ if (scan.wiring) {
287
+ for (const mismatch of scan.wiring.potentialMismatches) {
288
+ findings.push(wiringMismatchToFinding(mismatch, counter++));
289
+ }
290
+ }
291
+
292
+ // No test files at all
293
+ if (scan.totalTestFiles === 0 && scan.totalSourceFiles > 0) {
294
+ findings.push({
295
+ id: `AUD-${counter++}`,
296
+ category: 'test-coverage',
297
+ severity: 'critical',
298
+ title: 'No test files found',
299
+ description: `Project has ${scan.totalSourceFiles} source files but zero test files.`,
300
+ evidence: [],
301
+ recommendation: 'Add unit tests for critical paths.',
302
+ autoFixable: false,
303
+ });
304
+ }
305
+
306
+ // No README
307
+ if (!scan.readmeContent) {
308
+ findings.push({
309
+ id: `AUD-${counter++}`,
310
+ category: 'documentation',
311
+ severity: 'minor',
312
+ title: 'Missing README.md',
313
+ description: 'No README.md file found in project root.',
314
+ evidence: [],
315
+ recommendation: 'Add a README.md with project overview, setup, and usage instructions.',
316
+ autoFixable: true,
317
+ });
318
+ }
319
+
320
+ // No .env.example for multi-component projects
321
+ if (!scan.envExampleContent && scan.components.length > 1) {
322
+ findings.push({
323
+ id: `AUD-${counter++}`,
324
+ category: 'config-deployment',
325
+ severity: 'major',
326
+ title: 'Missing .env.example for workspace project',
327
+ description: 'Multi-component project should have a .env.example documenting required environment variables.',
328
+ evidence: [],
329
+ recommendation: 'Create .env.example with all required env variables and comments.',
330
+ autoFixable: true,
331
+ });
332
+ }
333
+
334
+ return findings;
335
+ }
336
+
337
+ /**
338
+ * Convert a wiring mismatch into an audit finding.
339
+ *
340
+ * @param mismatch - The wiring mismatch.
341
+ * @param counter - Finding ID counter.
342
+ * @returns An audit finding.
343
+ */
344
+ function wiringMismatchToFinding(mismatch: WiringMismatch, counter: number): AuditFinding {
345
+ return {
346
+ id: `AUD-${counter}`,
347
+ category: 'integration-wiring',
348
+ severity: 'major',
349
+ title: `Wiring issue: ${mismatch.type}`,
350
+ description: mismatch.details,
351
+ evidence: mismatch.evidence,
352
+ recommendation: 'Update CORS or API base URL configuration to ensure frontend and backend can communicate.',
353
+ autoFixable: true,
354
+ };
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Scoring
359
+ // ---------------------------------------------------------------------------
360
+
361
+ /**
362
+ * Calculate audit scores from findings and scan data.
363
+ *
364
+ * @param findings - All audit findings (AI + deterministic).
365
+ * @param scan - The project scan result.
366
+ * @returns Overall score (0-100) and per-category scores.
367
+ */
368
+ export function calculateAuditScores(
369
+ findings: AuditFinding[],
370
+ _scan: ProjectScanResult
371
+ ): { overallScore: number; categoryScores: Record<AuditCategory, number> } {
372
+ const categories = Object.keys(CATEGORY_WEIGHTS) as AuditCategory[];
373
+ const categoryScores: Record<string, number> = {};
374
+
375
+ for (const cat of categories) {
376
+ const catFindings = findings.filter((f) => f.category === cat);
377
+ let score = 100;
378
+ for (const f of catFindings) {
379
+ score -= SEVERITY_DEDUCTIONS[f.severity] ?? 0;
380
+ }
381
+ categoryScores[cat] = Math.max(0, Math.min(100, score));
382
+ }
383
+
384
+ // Weighted average
385
+ let overallScore = 0;
386
+ let totalWeight = 0;
387
+ for (const cat of categories) {
388
+ overallScore += (categoryScores[cat] ?? 100) * CATEGORY_WEIGHTS[cat];
389
+ totalWeight += CATEGORY_WEIGHTS[cat];
390
+ }
391
+ overallScore = Math.round(overallScore / totalWeight);
392
+
393
+ return {
394
+ overallScore: Math.max(0, Math.min(100, overallScore)),
395
+ categoryScores: categoryScores as Record<AuditCategory, number>,
396
+ };
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Main analyzer
401
+ // ---------------------------------------------------------------------------
402
+
403
+ /**
404
+ * Analyze the project using AI with Serena-first search strategy.
405
+ *
406
+ * The analysis flow:
407
+ * 1. Build prompt from scan data and state
408
+ * 2. Execute through Claude with Serena tools + fallback tools
409
+ * 3. Parse AI findings from the response
410
+ * 4. Merge with deterministic findings (wiring, composition, missing tests)
411
+ * 5. Track Serena usage in SearchMetadata
412
+ *
413
+ * @param scan - The project scan result from audit-scanner.
414
+ * @param state - Current project state.
415
+ * @param options - Audit mode options (depth, strict, etc.).
416
+ * @returns Findings array and search metadata.
417
+ */
418
+ export async function analyzeProject(
419
+ scan: ProjectScanResult,
420
+ state: ProjectState,
421
+ options: Pick<AuditModeOptions, 'depth' | 'strict' | 'projectDir'>
422
+ ): Promise<{ findings: AuditFinding[]; searchMetadata: SearchMetadata }> {
423
+ const metadata: SearchMetadata = {
424
+ serenaUsed: false,
425
+ serenaRetries: 0,
426
+ serenaErrors: [],
427
+ fallbackUsed: false,
428
+ fallbackTool: '',
429
+ searchQueries: [],
430
+ };
431
+
432
+ const prompt = buildAnalysisPrompt(scan, state, options.depth, options.strict);
433
+ metadata.searchQueries.push('audit-analysis-prompt');
434
+
435
+ // Attempt execution with Serena tools
436
+ let result: ClaudeExecuteResult | undefined;
437
+ let serenaAttempt = 0;
438
+
439
+ while (serenaAttempt <= MAX_SERENA_RETRIES) {
440
+ try {
441
+ result = await executePrompt(prompt, {
442
+ cwd: options.projectDir,
443
+ allowedTools: ALL_AUDIT_TOOLS,
444
+ permissionMode: 'bypassPermissions',
445
+ timeout: 120_000,
446
+ });
447
+
448
+ if (result.success) {
449
+ metadata.serenaUsed = true;
450
+ break;
451
+ }
452
+
453
+ // Serena failure: retry with alternate approach
454
+ metadata.serenaRetries++;
455
+ metadata.serenaErrors.push(result.error ?? 'Unknown error');
456
+ serenaAttempt++;
457
+ } catch (err) {
458
+ metadata.serenaRetries++;
459
+ metadata.serenaErrors.push(err instanceof Error ? err.message : 'Unknown error');
460
+ serenaAttempt++;
461
+ }
462
+ }
463
+
464
+ // Fallback: use only Read/Glob/Grep if Serena failed
465
+ if (!result?.success) {
466
+ metadata.fallbackUsed = true;
467
+ metadata.fallbackTool = 'grep';
468
+ try {
469
+ result = await executePrompt(prompt, {
470
+ cwd: options.projectDir,
471
+ allowedTools: FALLBACK_TOOLS,
472
+ permissionMode: 'bypassPermissions',
473
+ timeout: 120_000,
474
+ });
475
+ } catch {
476
+ // Complete failure — proceed with deterministic findings only
477
+ }
478
+ }
479
+
480
+ // Parse AI findings
481
+ let aiFindings: AuditFinding[] = [];
482
+ if (result?.success && result.response) {
483
+ aiFindings = parseAuditFindings(result.response);
484
+ }
485
+
486
+ // Merge with deterministic findings
487
+ const deterministicFindings = generateDeterministicFindings(scan);
488
+ const allFindings = [...aiFindings, ...deterministicFindings];
489
+
490
+ return { findings: allFindings, searchMetadata: metadata };
491
+ }