forgedev 1.0.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 (99) hide show
  1. package/CLAUDE.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/bin/devforge.js +4 -0
  5. package/package.json +33 -0
  6. package/src/claude-configurator.js +260 -0
  7. package/src/cli.js +119 -0
  8. package/src/composer.js +214 -0
  9. package/src/doctor-checks.js +743 -0
  10. package/src/doctor-prompts.js +295 -0
  11. package/src/doctor.js +281 -0
  12. package/src/guided.js +315 -0
  13. package/src/index.js +148 -0
  14. package/src/init-mode.js +134 -0
  15. package/src/prompts.js +155 -0
  16. package/src/recommender.js +186 -0
  17. package/src/scanner.js +368 -0
  18. package/src/uat-generator.js +189 -0
  19. package/src/utils.js +57 -0
  20. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
  21. package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
  22. package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
  23. package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
  24. package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
  25. package/templates/auth/nextauth/src/middleware.ts.template +14 -0
  26. package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
  27. package/templates/backend/fastapi/backend/app/__init__.py +0 -0
  28. package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
  29. package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
  30. package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
  31. package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
  32. package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
  33. package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
  34. package/templates/backend/fastapi/backend/app/main.py.template +58 -0
  35. package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
  36. package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
  37. package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
  38. package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
  39. package/templates/base/.gitignore.template +29 -0
  40. package/templates/base/README.md.template +25 -0
  41. package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
  42. package/templates/claude-code/agents/production-readiness.md +55 -0
  43. package/templates/claude-code/agents/security-reviewer.md +41 -0
  44. package/templates/claude-code/agents/spec-validator.md +34 -0
  45. package/templates/claude-code/agents/uat-validator.md +37 -0
  46. package/templates/claude-code/claude-md/base.md +33 -0
  47. package/templates/claude-code/claude-md/fastapi.md +12 -0
  48. package/templates/claude-code/claude-md/fullstack.md +12 -0
  49. package/templates/claude-code/claude-md/nextjs.md +11 -0
  50. package/templates/claude-code/commands/audit-security.md +11 -0
  51. package/templates/claude-code/commands/audit-spec.md +9 -0
  52. package/templates/claude-code/commands/audit-wiring.md +17 -0
  53. package/templates/claude-code/commands/done.md +19 -0
  54. package/templates/claude-code/commands/generate-prd.md +45 -0
  55. package/templates/claude-code/commands/generate-uat.md +35 -0
  56. package/templates/claude-code/commands/help.md +26 -0
  57. package/templates/claude-code/commands/next.md +20 -0
  58. package/templates/claude-code/commands/optimize-claude-md.md +31 -0
  59. package/templates/claude-code/commands/pre-pr.md +19 -0
  60. package/templates/claude-code/commands/run-uat.md +21 -0
  61. package/templates/claude-code/commands/status.md +24 -0
  62. package/templates/claude-code/commands/verify-all.md +11 -0
  63. package/templates/claude-code/hooks/polyglot.json +36 -0
  64. package/templates/claude-code/hooks/python.json +36 -0
  65. package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
  66. package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
  67. package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
  68. package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
  69. package/templates/claude-code/hooks/typescript.json +36 -0
  70. package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
  71. package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
  72. package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
  73. package/templates/claude-code/skills/playwright/SKILL.md +37 -0
  74. package/templates/claude-code/skills/security-api/SKILL.md +47 -0
  75. package/templates/claude-code/skills/security-web/SKILL.md +41 -0
  76. package/templates/database/prisma-postgres/.env.example +1 -0
  77. package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
  78. package/templates/database/sqlalchemy-postgres/.env.example +1 -0
  79. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
  80. package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
  81. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
  82. package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
  83. package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
  84. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
  85. package/templates/frontend/nextjs/next.config.ts.template +7 -0
  86. package/templates/frontend/nextjs/package.json.template +41 -0
  87. package/templates/frontend/nextjs/postcss.config.mjs +7 -0
  88. package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
  89. package/templates/frontend/nextjs/src/app/globals.css +1 -0
  90. package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
  91. package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
  92. package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
  93. package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
  94. package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
  95. package/templates/frontend/nextjs/tsconfig.json +23 -0
  96. package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
  97. package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
  98. package/templates/testing/playwright/playwright.config.ts.template +22 -0
  99. package/templates/testing/vitest/src/__tests__/example.test.ts.template +12 -0
@@ -0,0 +1,743 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function runAllChecks(projectDir, scan) {
5
+ const issues = [];
6
+
7
+ issues.push(...checkClaudeMdLength(projectDir, scan));
8
+ issues.push(...checkUnauthEndpoints(projectDir, scan));
9
+ issues.push(...checkFlakyTestPatterns(projectDir));
10
+ issues.push(...checkCrossDomainImports(projectDir, scan));
11
+ issues.push(...checkBareExcepts(projectDir));
12
+ issues.push(...checkMissingHealthEndpoint(projectDir, scan));
13
+ issues.push(...checkMissingGracefulShutdown(projectDir, scan));
14
+ issues.push(...checkMissingUAT(scan));
15
+ issues.push(...checkScatteredAPICalls(projectDir, scan));
16
+ issues.push(...checkLargeFiles(projectDir));
17
+ issues.push(...checkMissingScopedClaudeMd(projectDir, scan));
18
+ issues.push(...checkUnusedDependencies(projectDir, scan));
19
+ issues.push(...checkHardcodedValues(projectDir));
20
+ issues.push(...checkUnusedExports(projectDir, scan));
21
+ issues.push(...checkDuplicateCode(projectDir));
22
+ issues.push(...checkDeadFeatures(projectDir, scan));
23
+ issues.push(...checkAIPrompts(projectDir));
24
+
25
+ return issues.sort((a, b) => {
26
+ const order = { critical: 0, warning: 1, info: 2 };
27
+ return (order[a.severity] ?? 3) - (order[b.severity] ?? 3);
28
+ });
29
+ }
30
+
31
+ function checkClaudeMdLength(projectDir, scan) {
32
+ if (!scan.infrastructure.hasClaudeMd) return [];
33
+ const lines = scan.infrastructure.claudeMdLines;
34
+ if (lines <= 150) return [];
35
+
36
+ return [{
37
+ severity: lines > 500 ? 'critical' : 'warning',
38
+ title: `CLAUDE.md is ${lines} lines (recommended limit: 150)`,
39
+ impact: 'Instructions are being dropped — Claude Code ignores rules randomly when context is too large',
40
+ files: ['CLAUDE.md'],
41
+ autoFixable: false,
42
+ promptId: 'CLAUDE_MD_TOO_LONG',
43
+ effort: 'medium',
44
+ }];
45
+ }
46
+
47
+ function checkUnauthEndpoints(projectDir, scan) {
48
+ const issues = [];
49
+
50
+ // FastAPI: check for routes missing Depends(get_current_user)
51
+ if (scan.backend.detected && scan.backend.framework === 'fastapi') {
52
+ const apiDir = path.join(projectDir, scan.backend.directory || 'backend', 'app', 'api');
53
+ if (!fs.existsSync(apiDir)) return [];
54
+
55
+ const pyFiles = findFiles(apiDir, '.py');
56
+ for (const file of pyFiles) {
57
+ if (path.basename(file) === '__init__.py' || path.basename(file) === 'auth.py') continue;
58
+ const content = fs.readFileSync(file, 'utf-8');
59
+ const lines = content.split('\n');
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ if (/@router\.(get|post|put|delete|patch)\s*\(/.test(line)) {
64
+ // Look ahead for the function definition and check for auth dependency
65
+ const fnBlock = lines.slice(i, Math.min(i + 10, lines.length)).join('\n');
66
+ if (!fnBlock.includes('get_current_user') && !fnBlock.includes('current_user') && !fnBlock.includes('Depends(')) {
67
+ const relPath = path.relative(projectDir, file);
68
+ issues.push({
69
+ severity: 'critical',
70
+ title: `Unauthenticated endpoint: ${relPath}:${i + 1}`,
71
+ impact: 'Security vulnerability — data accessible without login',
72
+ files: [`${relPath}:${i + 1}`],
73
+ autoFixable: false,
74
+ promptId: 'UNAUTH_ENDPOINT',
75
+ effort: 'quick',
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // Next.js API routes: check for missing auth
84
+ if (scan.frontend.detected && scan.frontend.framework === 'nextjs') {
85
+ const srcDir = scan.frontend.directory || 'src';
86
+ const apiDir = path.join(projectDir, srcDir, 'app', 'api');
87
+ if (!fs.existsSync(apiDir)) return issues;
88
+
89
+ const routeFiles = findFiles(apiDir, '.ts').concat(findFiles(apiDir, '.tsx'));
90
+ for (const file of routeFiles) {
91
+ if (path.basename(file) === 'route.ts' || path.basename(file) === 'route.tsx') {
92
+ // Skip health endpoints
93
+ if (file.includes('/health/')) continue;
94
+
95
+ const content = fs.readFileSync(file, 'utf-8');
96
+ if (!content.includes('getServerSession') && !content.includes('auth(') &&
97
+ !content.includes('getToken') && !content.includes('currentUser') &&
98
+ !content.includes('session')) {
99
+ const relPath = path.relative(projectDir, file);
100
+ issues.push({
101
+ severity: 'warning',
102
+ title: `API route may lack auth: ${relPath}`,
103
+ impact: 'Endpoint may be accessible without authentication',
104
+ files: [relPath],
105
+ autoFixable: false,
106
+ promptId: 'UNAUTH_ENDPOINT',
107
+ effort: 'quick',
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ // Consolidate multiple unauth endpoints into one issue
115
+ if (issues.length > 1) {
116
+ const allFiles = issues.flatMap(i => i.files);
117
+ const severity = issues.some(i => i.severity === 'critical') ? 'critical' : 'warning';
118
+ return [{
119
+ severity,
120
+ title: `${issues.length} endpoints have no authentication`,
121
+ impact: 'Security vulnerability — data accessible without login',
122
+ files: allFiles,
123
+ autoFixable: false,
124
+ promptId: 'UNAUTH_ENDPOINT',
125
+ effort: 'quick',
126
+ }];
127
+ }
128
+
129
+ return issues;
130
+ }
131
+
132
+ function checkFlakyTestPatterns(projectDir) {
133
+ const issues = [];
134
+ const testDirs = ['tests', 'e2e', '__tests__', 'test', 'frontend/e2e', 'frontend/tests'];
135
+ const testFiles = [];
136
+
137
+ for (const dir of testDirs) {
138
+ const fullDir = path.join(projectDir, dir);
139
+ if (fs.existsSync(fullDir)) {
140
+ testFiles.push(...findFiles(fullDir, '.ts'), ...findFiles(fullDir, '.js'), ...findFiles(fullDir, '.py'));
141
+ }
142
+ }
143
+
144
+ // Also find test files in src
145
+ const srcDir = path.join(projectDir, 'src');
146
+ if (fs.existsSync(srcDir)) {
147
+ const allSrc = findFiles(srcDir, '.ts').concat(findFiles(srcDir, '.js'));
148
+ testFiles.push(...allSrc.filter(f => f.includes('.test.') || f.includes('.spec.')));
149
+ }
150
+
151
+ const flakyFiles = [];
152
+ for (const file of testFiles) {
153
+ const content = fs.readFileSync(file, 'utf-8');
154
+ if (content.includes('waitForTimeout') || content.includes('page.waitForTimeout') ||
155
+ /cy\.wait\(\d+\)/.test(content) || /setTimeout.*\d{3,}/.test(content)) {
156
+ flakyFiles.push(path.relative(projectDir, file));
157
+ }
158
+ }
159
+
160
+ if (flakyFiles.length > 0) {
161
+ issues.push({
162
+ severity: 'critical',
163
+ title: `${flakyFiles.length} tests use waitForTimeout() or hardcoded delays`,
164
+ impact: 'Tests fail randomly — unreliable CI',
165
+ files: flakyFiles,
166
+ autoFixable: false,
167
+ promptId: 'FLAKY_TESTS',
168
+ effort: 'medium',
169
+ });
170
+ }
171
+
172
+ return issues;
173
+ }
174
+
175
+ function checkCrossDomainImports(projectDir, scan) {
176
+ if (!scan.frontend.detected || !scan.backend.detected) return [];
177
+ if (scan.stackId !== 'polyglot-fullstack') return [];
178
+
179
+ const issues = [];
180
+ const frontendDir = scan.frontend.directory || 'frontend';
181
+ const backendDir = scan.backend.directory || 'backend';
182
+
183
+ // Check frontend files importing from backend
184
+ const frontendPath = path.join(projectDir, frontendDir);
185
+ if (fs.existsSync(frontendPath)) {
186
+ const frontFiles = findFiles(frontendPath, '.ts').concat(findFiles(frontendPath, '.tsx'), findFiles(frontendPath, '.js'));
187
+ for (const file of frontFiles) {
188
+ const content = fs.readFileSync(file, 'utf-8');
189
+ if (content.includes(`from '../../${backendDir}`) || content.includes(`from '../${backendDir}`) ||
190
+ content.includes(`import '../../${backendDir}`) || content.includes(`require('../../${backendDir}`)) {
191
+ issues.push({
192
+ severity: 'critical',
193
+ title: `Cross-domain import: ${path.relative(projectDir, file)} imports from ${backendDir}`,
194
+ impact: 'Build will break or bundle server code into client — security risk',
195
+ files: [path.relative(projectDir, file)],
196
+ autoFixable: false,
197
+ promptId: 'CROSS_DOMAIN_IMPORT',
198
+ effort: 'medium',
199
+ });
200
+ }
201
+ }
202
+ }
203
+
204
+ if (issues.length > 1) {
205
+ const allFiles = issues.flatMap(i => i.files);
206
+ return [{
207
+ severity: 'critical',
208
+ title: `${issues.length} cross-domain imports (frontend importing backend or vice versa)`,
209
+ impact: 'Build will break or bundle server code into client — security risk',
210
+ files: allFiles,
211
+ autoFixable: false,
212
+ promptId: 'CROSS_DOMAIN_IMPORT',
213
+ effort: 'medium',
214
+ }];
215
+ }
216
+
217
+ return issues;
218
+ }
219
+
220
+ function checkBareExcepts(projectDir) {
221
+ const pyFiles = [];
222
+ const dirs = ['backend', 'app', 'src', '.'];
223
+ for (const dir of dirs) {
224
+ const fullDir = path.join(projectDir, dir);
225
+ if (fs.existsSync(fullDir) && fs.statSync(fullDir).isDirectory()) {
226
+ pyFiles.push(...findFiles(fullDir, '.py'));
227
+ }
228
+ }
229
+
230
+ const bareExceptFiles = [];
231
+ for (const file of pyFiles) {
232
+ const content = fs.readFileSync(file, 'utf-8');
233
+ const lines = content.split('\n');
234
+ for (let i = 0; i < lines.length; i++) {
235
+ const trimmed = lines[i].trim();
236
+ if (trimmed === 'except:' || trimmed === 'except :') {
237
+ bareExceptFiles.push(`${path.relative(projectDir, file)}:${i + 1}`);
238
+ }
239
+ }
240
+ }
241
+
242
+ if (bareExceptFiles.length === 0) return [];
243
+
244
+ return [{
245
+ severity: 'warning',
246
+ title: `${bareExceptFiles.length} bare except: blocks`,
247
+ impact: 'Silently swallows real errors, makes debugging impossible',
248
+ files: bareExceptFiles,
249
+ autoFixable: false,
250
+ promptId: 'BARE_EXCEPT',
251
+ effort: 'quick',
252
+ }];
253
+ }
254
+
255
+ function checkMissingHealthEndpoint(projectDir, scan) {
256
+ // Search for health endpoints by file path or content
257
+ const patterns = ['/health', '/healthz', '/api/health'];
258
+ const searchDirs = ['src', 'backend', 'app', 'frontend/src'];
259
+
260
+ for (const dir of searchDirs) {
261
+ const fullDir = path.join(projectDir, dir);
262
+ if (!fs.existsSync(fullDir)) continue;
263
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.js'), findFiles(fullDir, '.py'));
264
+ for (const file of files) {
265
+ // Check file path (e.g., src/app/api/health/route.ts)
266
+ const relPath = path.relative(projectDir, file).replace(/\\/g, '/');
267
+ if (relPath.includes('/health/') || relPath.includes('/healthz/')) {
268
+ return [];
269
+ }
270
+ // Check file content
271
+ const content = fs.readFileSync(file, 'utf-8');
272
+ if (patterns.some(p => content.includes(p))) {
273
+ return [];
274
+ }
275
+ }
276
+ }
277
+
278
+ return [{
279
+ severity: 'warning',
280
+ title: 'No health check endpoint',
281
+ impact: 'Load balancers and monitoring can\'t verify app is running',
282
+ files: [],
283
+ autoFixable: false,
284
+ promptId: 'MISSING_HEALTH',
285
+ effort: 'quick',
286
+ }];
287
+ }
288
+
289
+ function checkMissingGracefulShutdown(projectDir, scan) {
290
+ const searchDirs = ['src', 'backend', 'app', 'frontend/src'];
291
+ const signals = ['SIGTERM', 'SIGINT', 'process.on', 'signal.signal', 'lifespan'];
292
+
293
+ for (const dir of searchDirs) {
294
+ const fullDir = path.join(projectDir, dir);
295
+ if (!fs.existsSync(fullDir)) continue;
296
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.js'), findFiles(fullDir, '.py'));
297
+ for (const file of files) {
298
+ const content = fs.readFileSync(file, 'utf-8');
299
+ if (signals.some(s => content.includes(s))) {
300
+ return [];
301
+ }
302
+ }
303
+ }
304
+
305
+ return [{
306
+ severity: 'warning',
307
+ title: 'No graceful shutdown handler',
308
+ impact: 'Deployments may drop in-flight requests',
309
+ files: [],
310
+ autoFixable: false,
311
+ promptId: 'MISSING_SHUTDOWN',
312
+ effort: 'quick',
313
+ }];
314
+ }
315
+
316
+ function checkMissingUAT(scan) {
317
+ if (scan.infrastructure.hasUAT) return [];
318
+ return [{
319
+ severity: 'warning',
320
+ title: 'No UAT scenarios',
321
+ impact: 'No formal acceptance criteria — features may not work as intended',
322
+ files: [],
323
+ autoFixable: false,
324
+ promptId: 'MISSING_UAT',
325
+ effort: 'medium',
326
+ }];
327
+ }
328
+
329
+ function checkScatteredAPICalls(projectDir, scan) {
330
+ if (!scan.frontend.detected) return [];
331
+
332
+ const srcDir = path.join(projectDir, scan.frontend.directory || 'src');
333
+ if (!fs.existsSync(srcDir)) return [];
334
+
335
+ const files = findFiles(srcDir, '.tsx').concat(findFiles(srcDir, '.jsx'));
336
+ const scattered = [];
337
+
338
+ for (const file of files) {
339
+ const basename = path.basename(file);
340
+ // Skip files that ARE the API client
341
+ if (['api.ts', 'api.js', 'apiClient.ts', 'apiClient.js', 'fetcher.ts', 'fetcher.js'].includes(basename)) continue;
342
+ // Skip non-component files
343
+ if (file.includes('/lib/') || file.includes('/services/') || file.includes('/utils/')) continue;
344
+
345
+ const content = fs.readFileSync(file, 'utf-8');
346
+ if (/fetch\s*\(\s*['"`]\/api\//.test(content) || /fetch\s*\(\s*['"`]http/.test(content) ||
347
+ /axios\.(get|post|put|delete|patch)\s*\(/.test(content)) {
348
+ scattered.push(path.relative(projectDir, file));
349
+ }
350
+ }
351
+
352
+ if (scattered.length <= 3) return [];
353
+
354
+ return [{
355
+ severity: 'warning',
356
+ title: `${scattered.length} components make direct API calls instead of using a centralized client`,
357
+ impact: 'API changes require updating every component instead of one file',
358
+ files: scattered,
359
+ autoFixable: false,
360
+ promptId: 'SCATTERED_API_CALLS',
361
+ effort: 'medium',
362
+ }];
363
+ }
364
+
365
+ function checkLargeFiles(projectDir) {
366
+ const sourceDirs = ['src', 'backend', 'app', 'frontend/src', 'frontend'];
367
+ const largeFiles = [];
368
+
369
+ for (const dir of sourceDirs) {
370
+ const fullDir = path.join(projectDir, dir);
371
+ if (!fs.existsSync(fullDir)) continue;
372
+ const files = findFiles(fullDir, '.ts')
373
+ .concat(findFiles(fullDir, '.tsx'), findFiles(fullDir, '.js'), findFiles(fullDir, '.jsx'), findFiles(fullDir, '.py'));
374
+
375
+ for (const file of files) {
376
+ // Skip generated/vendor files
377
+ if (file.includes('node_modules') || file.includes('.next') || file.includes('__pycache__')) continue;
378
+ const content = fs.readFileSync(file, 'utf-8');
379
+ const lineCount = content.split('\n').length;
380
+ if (lineCount > 500) {
381
+ largeFiles.push({ file: path.relative(projectDir, file), lines: lineCount });
382
+ }
383
+ }
384
+ }
385
+
386
+ if (largeFiles.length === 0) return [];
387
+
388
+ return [{
389
+ severity: 'info',
390
+ title: `${largeFiles.length} files over 500 lines — candidates for splitting`,
391
+ impact: 'Large files are hard to navigate, review, and test',
392
+ files: largeFiles.map(f => `${f.file} (${f.lines} lines)`),
393
+ autoFixable: false,
394
+ promptId: 'LARGE_FILES',
395
+ effort: 'large',
396
+ }];
397
+ }
398
+
399
+ function checkMissingScopedClaudeMd(projectDir, scan) {
400
+ if (!scan.frontend.detected || !scan.backend.detected) return [];
401
+
402
+ const issues = [];
403
+ const frontendDir = scan.frontend.directory || 'src';
404
+ const backendDir = scan.backend.directory || 'backend';
405
+
406
+ if (!fs.existsSync(path.join(projectDir, frontendDir, 'CLAUDE.md'))) {
407
+ issues.push({
408
+ severity: 'info',
409
+ title: `No ${frontendDir}/CLAUDE.md — frontend rules not scoped`,
410
+ impact: 'Claude Code loads all rules even when working only on frontend',
411
+ files: [],
412
+ autoFixable: false,
413
+ promptId: 'MISSING_SCOPED_CLAUDE_MD',
414
+ effort: 'quick',
415
+ });
416
+ }
417
+
418
+ if (!fs.existsSync(path.join(projectDir, backendDir, 'CLAUDE.md'))) {
419
+ issues.push({
420
+ severity: 'info',
421
+ title: `No ${backendDir}/CLAUDE.md — backend rules not scoped`,
422
+ impact: 'Claude Code loads all rules even when working only on backend',
423
+ files: [],
424
+ autoFixable: false,
425
+ promptId: 'MISSING_SCOPED_CLAUDE_MD',
426
+ effort: 'quick',
427
+ });
428
+ }
429
+
430
+ return issues;
431
+ }
432
+
433
+ function checkUnusedDependencies(projectDir, scan) {
434
+ const pkgPath = path.join(projectDir, 'package.json');
435
+ if (!fs.existsSync(pkgPath)) return [];
436
+
437
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
438
+ const deps = Object.keys(pkg.dependencies || {});
439
+ if (deps.length === 0) return [];
440
+
441
+ // Gather all source file contents
442
+ const srcDirs = ['src', 'app', 'frontend/src', 'pages', 'components', 'lib'];
443
+ let allContent = '';
444
+ for (const dir of srcDirs) {
445
+ const fullDir = path.join(projectDir, dir);
446
+ if (!fs.existsSync(fullDir)) continue;
447
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.tsx'), findFiles(fullDir, '.js'), findFiles(fullDir, '.jsx'));
448
+ for (const file of files) {
449
+ allContent += fs.readFileSync(file, 'utf-8') + '\n';
450
+ }
451
+ }
452
+
453
+ // Also check config files at root
454
+ const configFiles = ['next.config.ts', 'next.config.js', 'next.config.mjs', 'tailwind.config.ts', 'tailwind.config.js', 'postcss.config.js', 'postcss.config.mjs'];
455
+ for (const cf of configFiles) {
456
+ const cfPath = path.join(projectDir, cf);
457
+ if (fs.existsSync(cfPath)) {
458
+ allContent += fs.readFileSync(cfPath, 'utf-8') + '\n';
459
+ }
460
+ }
461
+
462
+ // Skip framework deps that are used implicitly
463
+ const implicitDeps = ['react', 'react-dom', 'next', '@prisma/client', 'typescript'];
464
+ const unused = deps.filter(dep => {
465
+ if (implicitDeps.includes(dep)) return false;
466
+ // Check if the package name appears in imports
467
+ const shortName = dep.startsWith('@') ? dep : dep.split('/')[0];
468
+ return !allContent.includes(shortName);
469
+ });
470
+
471
+ if (unused.length === 0) return [];
472
+
473
+ return [{
474
+ severity: 'info',
475
+ title: `${unused.length} potentially unused dependencies`,
476
+ impact: 'Bloats install size and increases supply chain risk',
477
+ files: unused.map(d => `package.json → ${d}`),
478
+ autoFixable: false,
479
+ promptId: 'UNUSED_DEPENDENCIES',
480
+ effort: 'quick',
481
+ }];
482
+ }
483
+
484
+ function checkHardcodedValues(projectDir) {
485
+ const sourceDirs = ['src', 'backend', 'app', 'frontend/src'];
486
+ const hardcoded = [];
487
+
488
+ const patterns = [
489
+ { regex: /['"]http:\/\/localhost:\d+/, label: 'hardcoded localhost URL' },
490
+ { regex: /['"]postgresql:\/\//, label: 'hardcoded database URL' },
491
+ { regex: /['"]mongodb:\/\//, label: 'hardcoded database URL' },
492
+ { regex: /['"]redis:\/\//, label: 'hardcoded redis URL' },
493
+ { regex: /['"]sk-[a-zA-Z0-9]{20,}['"]/, label: 'possible API key' },
494
+ { regex: /port\s*[:=]\s*\d{4,5}\b/i, label: 'hardcoded port number' },
495
+ ];
496
+
497
+ for (const dir of sourceDirs) {
498
+ const fullDir = path.join(projectDir, dir);
499
+ if (!fs.existsSync(fullDir)) continue;
500
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.tsx'), findFiles(fullDir, '.js'), findFiles(fullDir, '.jsx'), findFiles(fullDir, '.py'));
501
+
502
+ for (const file of files) {
503
+ // Skip config and env files
504
+ const basename = path.basename(file);
505
+ if (basename.includes('.config.') || basename.includes('.env') || basename.includes('.test.') || basename.includes('.spec.')) continue;
506
+
507
+ const content = fs.readFileSync(file, 'utf-8');
508
+ const lines = content.split('\n');
509
+
510
+ for (let i = 0; i < lines.length; i++) {
511
+ const line = lines[i];
512
+ // Skip comments
513
+ if (line.trim().startsWith('//') || line.trim().startsWith('#')) continue;
514
+ for (const pat of patterns) {
515
+ if (pat.regex.test(line)) {
516
+ hardcoded.push(`${path.relative(projectDir, file)}:${i + 1} (${pat.label})`);
517
+ break;
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+
524
+ if (hardcoded.length === 0) return [];
525
+
526
+ return [{
527
+ severity: 'warning',
528
+ title: `${hardcoded.length} hardcoded values that should be in environment variables`,
529
+ impact: 'Cannot change config without code changes — breaks deployment',
530
+ files: hardcoded,
531
+ autoFixable: false,
532
+ promptId: 'HARDCODED_VALUES',
533
+ effort: 'medium',
534
+ }];
535
+ }
536
+
537
+ function checkUnusedExports(projectDir, scan) {
538
+ const srcDir = path.join(projectDir, scan.frontend?.directory || 'src');
539
+ if (!fs.existsSync(srcDir)) return [];
540
+
541
+ const files = findFiles(srcDir, '.ts').concat(findFiles(srcDir, '.tsx'), findFiles(srcDir, '.js'), findFiles(srcDir, '.jsx'));
542
+ if (files.length === 0) return [];
543
+
544
+ // Collect all exports and imports
545
+ const exportMap = new Map(); // name -> file
546
+ const importedNames = new Set();
547
+
548
+ for (const file of files) {
549
+ const content = fs.readFileSync(file, 'utf-8');
550
+ const relPath = path.relative(projectDir, file);
551
+
552
+ // Skip barrel files and index files
553
+ if (path.basename(file).startsWith('index.')) continue;
554
+
555
+ // Find named exports
556
+ const exportMatches = content.matchAll(/export\s+(?:function|const|class|type|interface|enum)\s+(\w+)/g);
557
+ for (const m of exportMatches) {
558
+ // Skip default page/layout/route exports (Next.js convention)
559
+ if (['default', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'generateMetadata', 'generateStaticParams'].includes(m[1])) continue;
560
+ exportMap.set(`${relPath}:${m[1]}`, relPath);
561
+ }
562
+
563
+ // Find imports
564
+ const importMatches = content.matchAll(/import\s+\{([^}]+)\}/g);
565
+ for (const m of importMatches) {
566
+ const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim());
567
+ names.forEach(n => importedNames.add(n));
568
+ }
569
+ }
570
+
571
+ const unused = [];
572
+ for (const [key] of exportMap) {
573
+ const name = key.substring(key.lastIndexOf(':') + 1);
574
+ if (!importedNames.has(name)) {
575
+ unused.push(key);
576
+ }
577
+ }
578
+
579
+ // Only report if there's a meaningful number
580
+ if (unused.length < 5) return [];
581
+
582
+ return [{
583
+ severity: 'info',
584
+ title: `${unused.length} exported symbols may be unused`,
585
+ impact: 'Dead exports add confusion and increase bundle size',
586
+ files: unused.slice(0, 20),
587
+ autoFixable: false,
588
+ promptId: 'UNUSED_EXPORTS',
589
+ effort: 'medium',
590
+ }];
591
+ }
592
+
593
+ function checkDuplicateCode(projectDir) {
594
+ const sourceDirs = ['src', 'backend', 'app', 'frontend/src'];
595
+ const functionBodies = new Map(); // hash -> [{file, line, name}]
596
+
597
+ for (const dir of sourceDirs) {
598
+ const fullDir = path.join(projectDir, dir);
599
+ if (!fs.existsSync(fullDir)) continue;
600
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.tsx'), findFiles(fullDir, '.js'), findFiles(fullDir, '.jsx'), findFiles(fullDir, '.py'));
601
+
602
+ for (const file of files) {
603
+ const content = fs.readFileSync(file, 'utf-8');
604
+ const lines = content.split('\n');
605
+
606
+ // Look for consecutive blocks of 6+ identical lines
607
+ for (let i = 0; i <= lines.length - 6; i++) {
608
+ const block = lines.slice(i, i + 6).map(l => l.trim()).filter(l => l.length > 0);
609
+ if (block.length < 4) continue;
610
+ const key = block.join('|');
611
+ if (key.length < 50) continue; // Skip trivial blocks
612
+
613
+ if (!functionBodies.has(key)) {
614
+ functionBodies.set(key, []);
615
+ }
616
+ functionBodies.get(key).push({ file: path.relative(projectDir, file), line: i + 1 });
617
+ }
618
+ }
619
+ }
620
+
621
+ const duplicates = [];
622
+ for (const [, locations] of functionBodies) {
623
+ // Must appear in different files
624
+ const uniqueFiles = new Set(locations.map(l => l.file));
625
+ if (uniqueFiles.size > 1) {
626
+ duplicates.push(locations.map(l => `${l.file}:${l.line}`));
627
+ }
628
+ }
629
+
630
+ if (duplicates.length === 0) return [];
631
+
632
+ const flatFiles = [...new Set(duplicates.flat())].slice(0, 15);
633
+ return [{
634
+ severity: 'info',
635
+ title: `${duplicates.length} code blocks appear duplicated across files`,
636
+ impact: 'Bug fixes need to be applied in multiple places',
637
+ files: flatFiles,
638
+ autoFixable: false,
639
+ promptId: 'DUPLICATE_CODE',
640
+ effort: 'medium',
641
+ }];
642
+ }
643
+
644
+ function checkDeadFeatures(projectDir, scan) {
645
+ // Look for route/page files that aren't linked from anywhere
646
+ if (!scan.frontend.detected) return [];
647
+
648
+ const srcDir = path.join(projectDir, scan.frontend.directory || 'src');
649
+ if (!fs.existsSync(srcDir)) return [];
650
+
651
+ const allFiles = findFiles(srcDir, '.ts').concat(findFiles(srcDir, '.tsx'), findFiles(srcDir, '.js'), findFiles(srcDir, '.jsx'));
652
+ const dead = [];
653
+
654
+ for (const file of allFiles) {
655
+ const content = fs.readFileSync(file, 'utf-8');
656
+
657
+ // Check for disabled/commented-out route handlers
658
+ if (content.includes('// export') && (content.includes('function GET') || content.includes('function POST'))) {
659
+ dead.push(`${path.relative(projectDir, file)} (commented-out route handler)`);
660
+ }
661
+
662
+ // Check for components with only a TODO placeholder
663
+ if (content.includes('TODO') && content.split('\n').length < 10) {
664
+ dead.push(`${path.relative(projectDir, file)} (stub/placeholder only)`);
665
+ }
666
+ }
667
+
668
+ if (dead.length === 0) return [];
669
+
670
+ return [{
671
+ severity: 'info',
672
+ title: `${dead.length} potentially dead or stub features`,
673
+ impact: 'Dead code adds confusion and maintenance burden',
674
+ files: dead,
675
+ autoFixable: false,
676
+ promptId: 'DEAD_FEATURES',
677
+ effort: 'quick',
678
+ }];
679
+ }
680
+
681
+ function checkAIPrompts(projectDir) {
682
+ const sourceDirs = ['src', 'backend', 'app', 'frontend/src', 'lib'];
683
+ const issues = [];
684
+
685
+ for (const dir of sourceDirs) {
686
+ const fullDir = path.join(projectDir, dir);
687
+ if (!fs.existsSync(fullDir)) continue;
688
+ const files = findFiles(fullDir, '.ts').concat(findFiles(fullDir, '.tsx'), findFiles(fullDir, '.js'), findFiles(fullDir, '.jsx'), findFiles(fullDir, '.py'));
689
+
690
+ for (const file of files) {
691
+ const content = fs.readFileSync(file, 'utf-8');
692
+
693
+ // Check for inline prompts (strings containing common prompt patterns)
694
+ if (/(?:system|user)\s*(?:message|prompt|role)\s*[:=]/i.test(content) ||
695
+ content.includes('openai.chat.completions') || content.includes('anthropic.messages') ||
696
+ content.includes('ChatCompletion') || content.includes('messages=[')) {
697
+
698
+ // Check if prompts are inline strings rather than imported from a prompts file
699
+ const basename = path.basename(file);
700
+ if (!basename.includes('prompt') && !basename.includes('template')) {
701
+ const relPath = path.relative(projectDir, file);
702
+ issues.push(relPath);
703
+ }
704
+ }
705
+ }
706
+ }
707
+
708
+ if (issues.length === 0) return [];
709
+
710
+ return [{
711
+ severity: 'info',
712
+ title: `${issues.length} files have inline AI prompts — consider a prompts file`,
713
+ impact: 'Inline prompts are hard to version, test, and iterate on',
714
+ files: issues,
715
+ autoFixable: false,
716
+ promptId: 'INLINE_AI_PROMPTS',
717
+ effort: 'medium',
718
+ }];
719
+ }
720
+
721
+ // Helpers
722
+
723
+ function findFiles(dir, ext) {
724
+ const results = [];
725
+ if (!fs.existsSync(dir)) return results;
726
+
727
+ try {
728
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
729
+ for (const entry of entries) {
730
+ const fullPath = path.join(dir, entry.name);
731
+ if (entry.isDirectory()) {
732
+ if (['node_modules', '.next', '__pycache__', '.git', 'venv', '.venv', 'dist', 'build'].includes(entry.name)) continue;
733
+ results.push(...findFiles(fullPath, ext));
734
+ } else if (entry.name.endsWith(ext)) {
735
+ results.push(fullPath);
736
+ }
737
+ }
738
+ } catch {
739
+ // Permission errors, etc.
740
+ }
741
+
742
+ return results;
743
+ }