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.
- package/README.md +148 -7
- package/cheatsheet.md +440 -0
- package/dist/cli/commands/db.d.ts +10 -0
- package/dist/cli/commands/db.d.ts.map +1 -0
- package/dist/cli/commands/db.js +240 -0
- package/dist/cli/commands/db.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +18 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +255 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +3 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/review.d.ts +31 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +156 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +218 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/admin-wizard.d.ts +25 -0
- package/dist/generators/admin-wizard.d.ts.map +1 -0
- package/dist/generators/admin-wizard.js +123 -0
- package/dist/generators/admin-wizard.js.map +1 -0
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +10 -3
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/database.d.ts +58 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +229 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js +23 -7
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
- package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-python.js +425 -0
- package/dist/generators/templates/admin-wizard-python.js.map +1 -0
- package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
- package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-react.js +554 -0
- package/dist/generators/templates/admin-wizard-react.js.map +1 -0
- package/dist/generators/templates/database-docker.d.ts +23 -0
- package/dist/generators/templates/database-docker.d.ts.map +1 -0
- package/dist/generators/templates/database-docker.js +221 -0
- package/dist/generators/templates/database-docker.js.map +1 -0
- package/dist/generators/templates/database-python.d.ts +54 -0
- package/dist/generators/templates/database-python.d.ts.map +1 -0
- package/dist/generators/templates/database-python.js +723 -0
- package/dist/generators/templates/database-python.js.map +1 -0
- package/dist/generators/templates/database-typescript.d.ts +34 -0
- package/dist/generators/templates/database-typescript.d.ts.map +1 -0
- package/dist/generators/templates/database-typescript.js +232 -0
- package/dist/generators/templates/database-typescript.js.map +1 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -1
- package/dist/generators/templates/fullstack.js +29 -0
- package/dist/generators/templates/fullstack.js.map +1 -1
- package/dist/generators/templates/index.d.ts +5 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +5 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/state/index.d.ts +10 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +21 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/audit.d.ts +623 -0
- package/dist/types/audit.d.ts.map +1 -0
- package/dist/types/audit.js +240 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/database-runtime.d.ts +86 -0
- package/dist/types/database-runtime.d.ts.map +1 -0
- package/dist/types/database-runtime.js +61 -0
- package/dist/types/database-runtime.js.map +1 -0
- package/dist/types/database.d.ts +85 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +71 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow.d.ts +36 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +7 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/audit-analyzer.d.ts +58 -0
- package/dist/workflow/audit-analyzer.d.ts.map +1 -0
- package/dist/workflow/audit-analyzer.js +420 -0
- package/dist/workflow/audit-analyzer.js.map +1 -0
- package/dist/workflow/audit-mode.d.ts +28 -0
- package/dist/workflow/audit-mode.d.ts.map +1 -0
- package/dist/workflow/audit-mode.js +169 -0
- package/dist/workflow/audit-mode.js.map +1 -0
- package/dist/workflow/audit-recovery.d.ts +61 -0
- package/dist/workflow/audit-recovery.d.ts.map +1 -0
- package/dist/workflow/audit-recovery.js +242 -0
- package/dist/workflow/audit-recovery.js.map +1 -0
- package/dist/workflow/audit-reporter.d.ts +65 -0
- package/dist/workflow/audit-reporter.d.ts.map +1 -0
- package/dist/workflow/audit-reporter.js +301 -0
- package/dist/workflow/audit-reporter.js.map +1 -0
- package/dist/workflow/audit-scanner.d.ts +87 -0
- package/dist/workflow/audit-scanner.d.ts.map +1 -0
- package/dist/workflow/audit-scanner.js +768 -0
- package/dist/workflow/audit-scanner.js.map +1 -0
- package/dist/workflow/db-setup-runner.d.ts +63 -0
- package/dist/workflow/db-setup-runner.d.ts.map +1 -0
- package/dist/workflow/db-setup-runner.js +336 -0
- package/dist/workflow/db-setup-runner.js.map +1 -0
- package/dist/workflow/db-state-machine.d.ts +30 -0
- package/dist/workflow/db-state-machine.d.ts.map +1 -0
- package/dist/workflow/db-state-machine.js +51 -0
- package/dist/workflow/db-state-machine.js.map +1 -0
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/db.ts +281 -0
- package/src/cli/commands/doctor.ts +273 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/interactive.ts +174 -4
- package/src/generators/admin-wizard.ts +146 -0
- package/src/generators/all.ts +10 -3
- package/src/generators/database.ts +286 -0
- package/src/generators/fullstack.ts +26 -9
- package/src/generators/index.ts +12 -0
- package/src/generators/templates/admin-wizard-python.ts +431 -0
- package/src/generators/templates/admin-wizard-react.ts +560 -0
- package/src/generators/templates/database-docker.ts +227 -0
- package/src/generators/templates/database-python.ts +734 -0
- package/src/generators/templates/database-typescript.ts +238 -0
- package/src/generators/templates/fullstack.ts +29 -0
- package/src/generators/templates/index.ts +5 -0
- package/src/state/index.ts +28 -0
- package/src/types/audit.ts +294 -0
- package/src/types/database-runtime.ts +69 -0
- package/src/types/database.ts +84 -0
- package/src/types/index.ts +29 -0
- package/src/types/workflow.ts +20 -0
- package/src/workflow/audit-analyzer.ts +491 -0
- package/src/workflow/audit-mode.ts +240 -0
- package/src/workflow/audit-recovery.ts +284 -0
- package/src/workflow/audit-reporter.ts +370 -0
- package/src/workflow/audit-scanner.ts +873 -0
- package/src/workflow/db-setup-runner.ts +391 -0
- package/src/workflow/db-state-machine.ts +58 -0
- package/src/workflow/index.ts +7 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
- package/tests/generators/admin-wizard-templates.test.ts +366 -0
- package/tests/generators/cross-phase-integration.test.ts +383 -0
- package/tests/generators/database.test.ts +456 -0
- package/tests/generators/fe-be-db-integration.test.ts +613 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/types/database-runtime.test.ts +158 -0
- package/tests/types/database.test.ts +187 -0
- package/tests/workflow/audit-analyzer.test.ts +281 -0
- package/tests/workflow/audit-mode.test.ts +114 -0
- package/tests/workflow/audit-recovery.test.ts +237 -0
- package/tests/workflow/audit-reporter.test.ts +254 -0
- package/tests/workflow/audit-scanner.test.ts +270 -0
- package/tests/workflow/db-setup-runner.test.ts +211 -0
- 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
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -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,
|
package/src/types/workflow.ts
CHANGED
|
@@ -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
|
+
}
|