midas-mcp 3.2.0 → 3.4.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 (48) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +93 -49
  3. package/dist/analyzer.js.map +1 -1
  4. package/dist/docs/DEPLOYMENT.md +190 -0
  5. package/dist/index.js +4 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/monitoring.d.ts +97 -0
  8. package/dist/monitoring.d.ts.map +1 -0
  9. package/dist/monitoring.js +258 -0
  10. package/dist/monitoring.js.map +1 -0
  11. package/dist/prompts/grow.d.ts.map +1 -1
  12. package/dist/prompts/grow.js +155 -47
  13. package/dist/prompts/grow.js.map +1 -1
  14. package/dist/providers.d.ts.map +1 -1
  15. package/dist/providers.js +77 -15
  16. package/dist/providers.js.map +1 -1
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +21 -3
  19. package/dist/server.js.map +1 -1
  20. package/dist/state/phase.d.ts +19 -4
  21. package/dist/state/phase.d.ts.map +1 -1
  22. package/dist/state/phase.js +19 -10
  23. package/dist/state/phase.js.map +1 -1
  24. package/dist/tools/analyze.d.ts.map +1 -1
  25. package/dist/tools/analyze.js +21 -9
  26. package/dist/tools/analyze.js.map +1 -1
  27. package/dist/tools/completeness.d.ts +36 -0
  28. package/dist/tools/completeness.d.ts.map +1 -0
  29. package/dist/tools/completeness.js +838 -0
  30. package/dist/tools/completeness.js.map +1 -0
  31. package/dist/tools/grow.d.ts +157 -0
  32. package/dist/tools/grow.d.ts.map +1 -0
  33. package/dist/tools/grow.js +532 -0
  34. package/dist/tools/grow.js.map +1 -0
  35. package/dist/tools/index.d.ts +3 -0
  36. package/dist/tools/index.d.ts.map +1 -1
  37. package/dist/tools/index.js +6 -0
  38. package/dist/tools/index.js.map +1 -1
  39. package/dist/tools/validate.d.ts +60 -0
  40. package/dist/tools/validate.d.ts.map +1 -0
  41. package/dist/tools/validate.js +234 -0
  42. package/dist/tools/validate.js.map +1 -0
  43. package/dist/tools/verify.d.ts +4 -4
  44. package/dist/tui.d.ts.map +1 -1
  45. package/dist/tui.js +66 -12
  46. package/dist/tui.js.map +1 -1
  47. package/docs/DEPLOYMENT.md +190 -0
  48. package/package.json +1 -1
@@ -0,0 +1,838 @@
1
+ /**
2
+ * 12-Category Completeness Model
3
+ *
4
+ * Production readiness scoring across critical dimensions:
5
+ * - Testing, Security, Documentation, Monitoring, etc.
6
+ */
7
+ import { z } from 'zod';
8
+ import { existsSync, readFileSync, readdirSync } from 'fs';
9
+ import { join, extname } from 'path';
10
+ import { sanitizePath } from '../security.js';
11
+ import { execSync } from 'child_process';
12
+ // ============================================================================
13
+ // SCHEMA
14
+ // ============================================================================
15
+ export const completenessSchema = z.object({
16
+ projectPath: z.string().optional().describe('Path to project root'),
17
+ detailed: z.boolean().optional().describe('Include detailed recommendations'),
18
+ });
19
+ // ============================================================================
20
+ // CATEGORY CHECKERS
21
+ // ============================================================================
22
+ function checkTesting(projectPath) {
23
+ const findings = [];
24
+ const recommendations = [];
25
+ let score = 0;
26
+ // Check for test files
27
+ const testPatterns = ['.test.', '.spec.', '__tests__'];
28
+ let testFiles = 0;
29
+ let srcFiles = 0;
30
+ function scan(dir, depth = 0) {
31
+ if (depth > 4)
32
+ return;
33
+ try {
34
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
35
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
36
+ continue;
37
+ const path = join(dir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ scan(path, depth + 1);
40
+ }
41
+ else {
42
+ const ext = extname(entry.name);
43
+ if (['.ts', '.tsx', '.js', '.jsx', '.py'].includes(ext)) {
44
+ if (testPatterns.some(p => entry.name.includes(p))) {
45
+ testFiles++;
46
+ }
47
+ else {
48
+ srcFiles++;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ catch {
55
+ // Skip inaccessible
56
+ }
57
+ }
58
+ scan(projectPath);
59
+ // Calculate coverage ratio
60
+ const testRatio = srcFiles > 0 ? testFiles / srcFiles : 0;
61
+ if (testFiles === 0) {
62
+ findings.push('No test files found');
63
+ recommendations.push('Add unit tests for critical functions');
64
+ }
65
+ else {
66
+ findings.push(`${testFiles} test files for ${srcFiles} source files`);
67
+ score += 30;
68
+ }
69
+ if (testRatio >= 0.5) {
70
+ score += 30;
71
+ findings.push('Good test coverage ratio');
72
+ }
73
+ else if (testRatio >= 0.2) {
74
+ score += 15;
75
+ recommendations.push('Increase test coverage to at least 50%');
76
+ }
77
+ // Check for test config
78
+ const hasJest = existsSync(join(projectPath, 'jest.config.js')) ||
79
+ existsSync(join(projectPath, 'jest.config.ts'));
80
+ const hasVitest = existsSync(join(projectPath, 'vitest.config.ts'));
81
+ const hasPytest = existsSync(join(projectPath, 'pytest.ini')) ||
82
+ existsSync(join(projectPath, 'pyproject.toml'));
83
+ if (hasJest || hasVitest || hasPytest) {
84
+ score += 20;
85
+ findings.push('Test framework configured');
86
+ }
87
+ // Check CI runs tests
88
+ const ciPath = join(projectPath, '.github', 'workflows');
89
+ if (existsSync(ciPath)) {
90
+ try {
91
+ const workflows = readdirSync(ciPath);
92
+ for (const wf of workflows) {
93
+ const content = readFileSync(join(ciPath, wf), 'utf-8');
94
+ if (content.includes('test') || content.includes('jest') || content.includes('vitest')) {
95
+ score += 20;
96
+ findings.push('Tests run in CI');
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ // Skip
103
+ }
104
+ }
105
+ return {
106
+ score: Math.min(100, score),
107
+ weight: 3,
108
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
109
+ findings,
110
+ recommendations,
111
+ };
112
+ }
113
+ function checkSecurity(projectPath) {
114
+ const findings = [];
115
+ const recommendations = [];
116
+ let score = 0;
117
+ // Check .gitignore
118
+ const gitignorePath = join(projectPath, '.gitignore');
119
+ if (existsSync(gitignorePath)) {
120
+ const content = readFileSync(gitignorePath, 'utf-8');
121
+ const patterns = ['.env', 'node_modules', '*.key', '*.pem', 'secrets'];
122
+ const found = patterns.filter(p => content.includes(p));
123
+ if (found.length >= 3) {
124
+ score += 25;
125
+ findings.push('.gitignore covers sensitive files');
126
+ }
127
+ else {
128
+ recommendations.push('Add more sensitive patterns to .gitignore');
129
+ }
130
+ }
131
+ else {
132
+ recommendations.push('Create .gitignore to exclude sensitive files');
133
+ }
134
+ // Check for hardcoded secrets (basic check)
135
+ const secretPatterns = [
136
+ /api[_-]?key\s*[:=]\s*['"][a-zA-Z0-9]{20,}/i,
137
+ /secret\s*[:=]\s*['"][a-zA-Z0-9]{20,}/i,
138
+ /password\s*[:=]\s*['"][^'"]+['"]/i,
139
+ ];
140
+ let foundSecrets = false;
141
+ function scanForSecrets(dir, depth = 0) {
142
+ if (depth > 3 || foundSecrets)
143
+ return;
144
+ try {
145
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
146
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
147
+ continue;
148
+ const path = join(dir, entry.name);
149
+ if (entry.isDirectory()) {
150
+ scanForSecrets(path, depth + 1);
151
+ }
152
+ else if (['.ts', '.js', '.py', '.json'].includes(extname(entry.name))) {
153
+ try {
154
+ const content = readFileSync(path, 'utf-8').slice(0, 10000);
155
+ for (const pattern of secretPatterns) {
156
+ if (pattern.test(content)) {
157
+ foundSecrets = true;
158
+ findings.push(`Potential secret in ${entry.name}`);
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ // Skip unreadable
165
+ }
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ // Skip
171
+ }
172
+ }
173
+ scanForSecrets(projectPath);
174
+ if (!foundSecrets) {
175
+ score += 25;
176
+ findings.push('No hardcoded secrets detected');
177
+ }
178
+ else {
179
+ recommendations.push('Remove hardcoded secrets and use environment variables');
180
+ }
181
+ // Check npm audit
182
+ try {
183
+ const audit = execSync('npm audit --json 2>/dev/null || echo "{}"', {
184
+ cwd: projectPath,
185
+ encoding: 'utf-8',
186
+ timeout: 30000,
187
+ stdio: 'pipe',
188
+ });
189
+ const result = JSON.parse(audit || '{}');
190
+ const vulns = result.metadata?.vulnerabilities || {};
191
+ const critical = vulns.critical || 0;
192
+ const high = vulns.high || 0;
193
+ if (critical === 0 && high === 0) {
194
+ score += 25;
195
+ findings.push('No critical/high vulnerabilities');
196
+ }
197
+ else {
198
+ findings.push(`${critical} critical, ${high} high vulnerabilities`);
199
+ recommendations.push('Run npm audit fix to address vulnerabilities');
200
+ }
201
+ }
202
+ catch {
203
+ findings.push('npm audit skipped');
204
+ score += 10; // Neutral
205
+ }
206
+ // Check for security headers / rate limiting mentions
207
+ const srcDir = join(projectPath, 'src');
208
+ if (existsSync(srcDir)) {
209
+ try {
210
+ const files = readdirSync(srcDir);
211
+ for (const f of files.slice(0, 10)) {
212
+ try {
213
+ const content = readFileSync(join(srcDir, f), 'utf-8');
214
+ if (content.includes('rate-limit') || content.includes('rateLimit') ||
215
+ content.includes('helmet') || content.includes('cors')) {
216
+ score += 25;
217
+ findings.push('Security middleware detected');
218
+ break;
219
+ }
220
+ }
221
+ catch {
222
+ // Skip
223
+ }
224
+ }
225
+ }
226
+ catch {
227
+ // Skip
228
+ }
229
+ }
230
+ return {
231
+ score: Math.min(100, score),
232
+ weight: 3,
233
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
234
+ findings,
235
+ recommendations,
236
+ };
237
+ }
238
+ function checkDocumentation(projectPath) {
239
+ const findings = [];
240
+ const recommendations = [];
241
+ let score = 0;
242
+ // README
243
+ if (existsSync(join(projectPath, 'README.md'))) {
244
+ const content = readFileSync(join(projectPath, 'README.md'), 'utf-8');
245
+ const wordCount = content.split(/\s+/).length;
246
+ if (wordCount > 200) {
247
+ score += 30;
248
+ findings.push('README with substantial content');
249
+ }
250
+ else {
251
+ score += 15;
252
+ recommendations.push('Expand README with installation and usage');
253
+ }
254
+ // Check for key sections
255
+ const sections = ['install', 'usage', 'api', 'example', 'license'];
256
+ const found = sections.filter(s => content.toLowerCase().includes(s));
257
+ if (found.length >= 3) {
258
+ score += 20;
259
+ findings.push('README has key sections');
260
+ }
261
+ }
262
+ else {
263
+ recommendations.push('Create README.md with project overview');
264
+ }
265
+ // API docs / JSDoc
266
+ let hasApiDocs = false;
267
+ const docsDir = join(projectPath, 'docs');
268
+ if (existsSync(docsDir)) {
269
+ score += 20;
270
+ findings.push('docs/ directory exists');
271
+ hasApiDocs = true;
272
+ }
273
+ // Check for inline documentation
274
+ const srcDir = join(projectPath, 'src');
275
+ if (existsSync(srcDir)) {
276
+ try {
277
+ const files = readdirSync(srcDir).filter(f => f.endsWith('.ts'));
278
+ let jsdocCount = 0;
279
+ for (const f of files.slice(0, 5)) {
280
+ const content = readFileSync(join(srcDir, f), 'utf-8');
281
+ if (content.includes('/**') || content.includes('* @')) {
282
+ jsdocCount++;
283
+ }
284
+ }
285
+ if (jsdocCount >= 2) {
286
+ score += 20;
287
+ findings.push('JSDoc comments present');
288
+ }
289
+ else {
290
+ recommendations.push('Add JSDoc comments to exported functions');
291
+ }
292
+ }
293
+ catch {
294
+ // Skip
295
+ }
296
+ }
297
+ // CHANGELOG
298
+ if (existsSync(join(projectPath, 'CHANGELOG.md'))) {
299
+ score += 10;
300
+ findings.push('CHANGELOG.md exists');
301
+ }
302
+ else {
303
+ recommendations.push('Create CHANGELOG.md to track versions');
304
+ }
305
+ return {
306
+ score: Math.min(100, score),
307
+ weight: 2,
308
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
309
+ findings,
310
+ recommendations,
311
+ };
312
+ }
313
+ function checkMonitoring(projectPath) {
314
+ const findings = [];
315
+ const recommendations = [];
316
+ let score = 0;
317
+ // Check for logging
318
+ const srcDir = join(projectPath, 'src');
319
+ let hasLogging = false;
320
+ let hasMetrics = false;
321
+ let hasSentry = false;
322
+ if (existsSync(srcDir)) {
323
+ try {
324
+ const files = readdirSync(srcDir);
325
+ for (const f of files.slice(0, 10)) {
326
+ try {
327
+ const content = readFileSync(join(srcDir, f), 'utf-8');
328
+ if (content.includes('logger') || content.includes('winston') || content.includes('pino')) {
329
+ hasLogging = true;
330
+ }
331
+ if (content.includes('metrics') || content.includes('prometheus') || content.includes('opentelemetry')) {
332
+ hasMetrics = true;
333
+ }
334
+ if (content.includes('sentry') || content.includes('Sentry')) {
335
+ hasSentry = true;
336
+ }
337
+ }
338
+ catch {
339
+ // Skip
340
+ }
341
+ }
342
+ }
343
+ catch {
344
+ // Skip
345
+ }
346
+ }
347
+ if (hasLogging) {
348
+ score += 30;
349
+ findings.push('Logging configured');
350
+ }
351
+ else {
352
+ recommendations.push('Add structured logging (winston, pino)');
353
+ }
354
+ if (hasMetrics) {
355
+ score += 30;
356
+ findings.push('Metrics/observability present');
357
+ }
358
+ else {
359
+ recommendations.push('Add metrics export (OpenTelemetry, Prometheus)');
360
+ }
361
+ if (hasSentry) {
362
+ score += 20;
363
+ findings.push('Error tracking (Sentry) detected');
364
+ }
365
+ // Health check endpoint
366
+ try {
367
+ const files = readdirSync(srcDir);
368
+ for (const f of files) {
369
+ if (f.includes('health') || f.includes('status')) {
370
+ score += 20;
371
+ findings.push('Health check endpoint found');
372
+ break;
373
+ }
374
+ }
375
+ }
376
+ catch {
377
+ // Skip
378
+ }
379
+ if (findings.length === 0) {
380
+ recommendations.push('Add monitoring for production visibility');
381
+ }
382
+ return {
383
+ score: Math.min(100, score),
384
+ weight: 2,
385
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
386
+ findings,
387
+ recommendations,
388
+ };
389
+ }
390
+ function checkCI(projectPath) {
391
+ const findings = [];
392
+ const recommendations = [];
393
+ let score = 0;
394
+ const ciPath = join(projectPath, '.github', 'workflows');
395
+ const hasGithubActions = existsSync(ciPath);
396
+ const hasGitlab = existsSync(join(projectPath, '.gitlab-ci.yml'));
397
+ const hasCircle = existsSync(join(projectPath, '.circleci'));
398
+ if (hasGithubActions || hasGitlab || hasCircle) {
399
+ score += 40;
400
+ findings.push('CI/CD pipeline configured');
401
+ // Check workflow content
402
+ if (hasGithubActions) {
403
+ try {
404
+ const workflows = readdirSync(ciPath);
405
+ for (const wf of workflows) {
406
+ const content = readFileSync(join(ciPath, wf), 'utf-8');
407
+ if (content.includes('npm test') || content.includes('jest') || content.includes('vitest')) {
408
+ score += 20;
409
+ findings.push('CI runs tests');
410
+ }
411
+ if (content.includes('npm run build') || content.includes('tsc')) {
412
+ score += 20;
413
+ findings.push('CI runs build');
414
+ }
415
+ if (content.includes('npm run lint') || content.includes('eslint')) {
416
+ score += 20;
417
+ findings.push('CI runs linting');
418
+ }
419
+ }
420
+ }
421
+ catch {
422
+ // Skip
423
+ }
424
+ }
425
+ }
426
+ else {
427
+ recommendations.push('Add CI/CD pipeline (GitHub Actions recommended)');
428
+ }
429
+ return {
430
+ score: Math.min(100, score),
431
+ weight: 2,
432
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
433
+ findings,
434
+ recommendations,
435
+ };
436
+ }
437
+ function checkDeployment(projectPath) {
438
+ const findings = [];
439
+ const recommendations = [];
440
+ let score = 0;
441
+ // Docker
442
+ if (existsSync(join(projectPath, 'Dockerfile')) ||
443
+ existsSync(join(projectPath, 'docker-compose.yml'))) {
444
+ score += 30;
445
+ findings.push('Docker configuration present');
446
+ }
447
+ // Infrastructure as code
448
+ if (existsSync(join(projectPath, 'terraform')) ||
449
+ existsSync(join(projectPath, 'pulumi')) ||
450
+ existsSync(join(projectPath, 'cdk.json'))) {
451
+ score += 30;
452
+ findings.push('Infrastructure as code configured');
453
+ }
454
+ // Deployment docs
455
+ if (existsSync(join(projectPath, 'docs', 'DEPLOYMENT.md')) ||
456
+ existsSync(join(projectPath, 'DEPLOYMENT.md'))) {
457
+ score += 20;
458
+ findings.push('Deployment documentation exists');
459
+ }
460
+ else {
461
+ recommendations.push('Document deployment process');
462
+ }
463
+ // Environment config
464
+ if (existsSync(join(projectPath, '.env.example')) ||
465
+ existsSync(join(projectPath, '.env.template'))) {
466
+ score += 20;
467
+ findings.push('Environment template provided');
468
+ }
469
+ else {
470
+ recommendations.push('Create .env.example for required variables');
471
+ }
472
+ if (score === 0) {
473
+ recommendations.push('Add deployment configuration (Docker, CI/CD)');
474
+ }
475
+ return {
476
+ score: Math.min(100, score),
477
+ weight: 2,
478
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
479
+ findings,
480
+ recommendations,
481
+ };
482
+ }
483
+ function checkErrorHandling(projectPath) {
484
+ const findings = [];
485
+ const recommendations = [];
486
+ let score = 0;
487
+ const srcDir = join(projectPath, 'src');
488
+ let tryCatchCount = 0;
489
+ let errorBoundaries = 0;
490
+ let fileCount = 0;
491
+ if (existsSync(srcDir)) {
492
+ function scan(dir, depth = 0) {
493
+ if (depth > 3)
494
+ return;
495
+ try {
496
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
497
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
498
+ continue;
499
+ const path = join(dir, entry.name);
500
+ if (entry.isDirectory()) {
501
+ scan(path, depth + 1);
502
+ }
503
+ else if (['.ts', '.tsx', '.js', '.jsx'].includes(extname(entry.name))) {
504
+ fileCount++;
505
+ try {
506
+ const content = readFileSync(path, 'utf-8');
507
+ const catches = (content.match(/catch\s*\(/g) || []).length;
508
+ tryCatchCount += catches;
509
+ if (content.includes('ErrorBoundary') || content.includes('componentDidCatch')) {
510
+ errorBoundaries++;
511
+ }
512
+ }
513
+ catch {
514
+ // Skip
515
+ }
516
+ }
517
+ }
518
+ }
519
+ catch {
520
+ // Skip
521
+ }
522
+ }
523
+ scan(srcDir);
524
+ }
525
+ const catchRatio = fileCount > 0 ? tryCatchCount / fileCount : 0;
526
+ if (catchRatio >= 0.5) {
527
+ score += 40;
528
+ findings.push('Good error handling coverage');
529
+ }
530
+ else if (catchRatio >= 0.2) {
531
+ score += 20;
532
+ recommendations.push('Add try/catch to more async operations');
533
+ }
534
+ else {
535
+ recommendations.push('Add error handling to critical operations');
536
+ }
537
+ if (errorBoundaries > 0) {
538
+ score += 30;
539
+ findings.push('React error boundaries present');
540
+ }
541
+ // Check for global error handlers
542
+ try {
543
+ const indexFile = join(srcDir, 'index.ts');
544
+ if (existsSync(indexFile)) {
545
+ const content = readFileSync(indexFile, 'utf-8');
546
+ if (content.includes('uncaughtException') || content.includes('unhandledRejection')) {
547
+ score += 30;
548
+ findings.push('Global error handlers configured');
549
+ }
550
+ }
551
+ }
552
+ catch {
553
+ // Skip
554
+ }
555
+ return {
556
+ score: Math.min(100, score),
557
+ weight: 2,
558
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
559
+ findings,
560
+ recommendations,
561
+ };
562
+ }
563
+ function checkCodeQuality(projectPath) {
564
+ const findings = [];
565
+ const recommendations = [];
566
+ let score = 0;
567
+ // TypeScript
568
+ if (existsSync(join(projectPath, 'tsconfig.json'))) {
569
+ score += 25;
570
+ findings.push('TypeScript configured');
571
+ try {
572
+ const config = JSON.parse(readFileSync(join(projectPath, 'tsconfig.json'), 'utf-8'));
573
+ if (config.compilerOptions?.strict) {
574
+ score += 15;
575
+ findings.push('Strict mode enabled');
576
+ }
577
+ else {
578
+ recommendations.push('Enable TypeScript strict mode');
579
+ }
580
+ }
581
+ catch {
582
+ // Skip
583
+ }
584
+ }
585
+ // Linting
586
+ if (existsSync(join(projectPath, '.eslintrc.json')) ||
587
+ existsSync(join(projectPath, '.eslintrc.js')) ||
588
+ existsSync(join(projectPath, 'eslint.config.js'))) {
589
+ score += 25;
590
+ findings.push('ESLint configured');
591
+ }
592
+ else {
593
+ recommendations.push('Add ESLint for code quality');
594
+ }
595
+ // Formatting
596
+ if (existsSync(join(projectPath, '.prettierrc')) ||
597
+ existsSync(join(projectPath, '.prettierrc.json'))) {
598
+ score += 15;
599
+ findings.push('Prettier configured');
600
+ }
601
+ // Pre-commit hooks
602
+ if (existsSync(join(projectPath, '.husky'))) {
603
+ score += 20;
604
+ findings.push('Pre-commit hooks configured');
605
+ }
606
+ return {
607
+ score: Math.min(100, score),
608
+ weight: 2,
609
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
610
+ findings,
611
+ recommendations,
612
+ };
613
+ }
614
+ function checkPerformance(projectPath) {
615
+ const findings = [];
616
+ const recommendations = [];
617
+ let score = 30; // Base score - hard to detect without running
618
+ // Bundle optimization
619
+ const pkgPath = join(projectPath, 'package.json');
620
+ if (existsSync(pkgPath)) {
621
+ try {
622
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
623
+ const deps = Object.keys(pkg.dependencies || {});
624
+ // Check for known performance helpers
625
+ if (deps.includes('compression') || deps.includes('zlib')) {
626
+ score += 20;
627
+ findings.push('Compression middleware present');
628
+ }
629
+ // Check for caching
630
+ if (deps.includes('redis') || deps.includes('ioredis') || deps.includes('memcached')) {
631
+ score += 20;
632
+ findings.push('Caching layer configured');
633
+ }
634
+ // Lazy loading indicators
635
+ const srcDir = join(projectPath, 'src');
636
+ if (existsSync(srcDir)) {
637
+ try {
638
+ const content = readdirSync(srcDir).map(f => {
639
+ try {
640
+ return readFileSync(join(srcDir, f), 'utf-8');
641
+ }
642
+ catch {
643
+ return '';
644
+ }
645
+ }).join('');
646
+ if (content.includes('lazy(') || content.includes('React.lazy')) {
647
+ score += 15;
648
+ findings.push('Lazy loading detected');
649
+ }
650
+ if (content.includes('useMemo') || content.includes('useCallback')) {
651
+ score += 15;
652
+ findings.push('React memoization used');
653
+ }
654
+ }
655
+ catch {
656
+ // Skip
657
+ }
658
+ }
659
+ }
660
+ catch {
661
+ // Skip
662
+ }
663
+ }
664
+ return {
665
+ score: Math.min(100, score),
666
+ weight: 1,
667
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
668
+ findings,
669
+ recommendations,
670
+ };
671
+ }
672
+ function checkAccessibility(projectPath) {
673
+ const findings = [];
674
+ const recommendations = [];
675
+ let score = 50; // Base - needs runtime testing
676
+ // Check for a11y testing libraries
677
+ const pkgPath = join(projectPath, 'package.json');
678
+ if (existsSync(pkgPath)) {
679
+ try {
680
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
681
+ const allDeps = {
682
+ ...pkg.dependencies,
683
+ ...pkg.devDependencies,
684
+ };
685
+ if (allDeps['@axe-core/react'] || allDeps['jest-axe'] || allDeps['@testing-library/jest-dom']) {
686
+ score += 25;
687
+ findings.push('Accessibility testing library present');
688
+ }
689
+ if (allDeps['@headlessui/react'] || allDeps['@radix-ui/react-accessible-icon']) {
690
+ score += 25;
691
+ findings.push('Accessible component library used');
692
+ }
693
+ }
694
+ catch {
695
+ // Skip
696
+ }
697
+ }
698
+ // Check for aria usage
699
+ const srcDir = join(projectPath, 'src');
700
+ if (existsSync(srcDir)) {
701
+ try {
702
+ const files = readdirSync(srcDir).filter(f => f.endsWith('.tsx'));
703
+ for (const f of files.slice(0, 5)) {
704
+ const content = readFileSync(join(srcDir, f), 'utf-8');
705
+ if (content.includes('aria-') || content.includes('role=')) {
706
+ findings.push('ARIA attributes used');
707
+ break;
708
+ }
709
+ }
710
+ }
711
+ catch {
712
+ // Skip
713
+ }
714
+ }
715
+ return {
716
+ score: Math.min(100, score),
717
+ weight: 1,
718
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
719
+ findings,
720
+ recommendations,
721
+ };
722
+ }
723
+ function checkDataIntegrity(projectPath) {
724
+ const findings = [];
725
+ const recommendations = [];
726
+ let score = 30;
727
+ const pkgPath = join(projectPath, 'package.json');
728
+ if (existsSync(pkgPath)) {
729
+ try {
730
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
731
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
732
+ // Validation libraries
733
+ if (deps.zod || deps.joi || deps.yup || deps['class-validator']) {
734
+ score += 30;
735
+ findings.push('Input validation library present');
736
+ }
737
+ else {
738
+ recommendations.push('Add validation library (zod recommended)');
739
+ }
740
+ // Database migrations
741
+ if (deps.prisma || deps.knex || deps.typeorm || deps.sequelize) {
742
+ score += 20;
743
+ findings.push('Database ORM configured');
744
+ }
745
+ // Backup/transaction handling
746
+ const srcDir = join(projectPath, 'src');
747
+ if (existsSync(srcDir)) {
748
+ try {
749
+ const content = readdirSync(srcDir).slice(0, 10).map(f => {
750
+ try {
751
+ return readFileSync(join(srcDir, f), 'utf-8');
752
+ }
753
+ catch {
754
+ return '';
755
+ }
756
+ }).join('');
757
+ if (content.includes('transaction') || content.includes('$transaction')) {
758
+ score += 20;
759
+ findings.push('Database transactions used');
760
+ }
761
+ }
762
+ catch {
763
+ // Skip
764
+ }
765
+ }
766
+ }
767
+ catch {
768
+ // Skip
769
+ }
770
+ }
771
+ return {
772
+ score: Math.min(100, score),
773
+ weight: 2,
774
+ status: score >= 70 ? 'pass' : score >= 40 ? 'warn' : 'fail',
775
+ findings,
776
+ recommendations,
777
+ };
778
+ }
779
+ // ============================================================================
780
+ // MAIN FUNCTION
781
+ // ============================================================================
782
+ export function checkCompleteness(input) {
783
+ const projectPath = sanitizePath(input.projectPath || process.cwd());
784
+ const detailed = input.detailed ?? true;
785
+ // Run all category checks
786
+ const categories = {
787
+ testing: checkTesting(projectPath),
788
+ security: checkSecurity(projectPath),
789
+ documentation: checkDocumentation(projectPath),
790
+ monitoring: checkMonitoring(projectPath),
791
+ ci_cd: checkCI(projectPath),
792
+ deployment: checkDeployment(projectPath),
793
+ error_handling: checkErrorHandling(projectPath),
794
+ code_quality: checkCodeQuality(projectPath),
795
+ performance: checkPerformance(projectPath),
796
+ accessibility: checkAccessibility(projectPath),
797
+ data_integrity: checkDataIntegrity(projectPath),
798
+ };
799
+ // Calculate weighted score
800
+ let totalWeight = 0;
801
+ let weightedScore = 0;
802
+ const blockers = [];
803
+ const allRecommendations = [];
804
+ for (const [name, category] of Object.entries(categories)) {
805
+ totalWeight += category.weight;
806
+ weightedScore += category.score * category.weight;
807
+ if (category.status === 'fail') {
808
+ blockers.push(`${name}: ${category.findings[0] || 'Needs attention'}`);
809
+ }
810
+ for (const rec of category.recommendations) {
811
+ allRecommendations.push(`[${name}] ${rec}`);
812
+ }
813
+ }
814
+ const overallScore = Math.round(weightedScore / totalWeight);
815
+ // Determine grade
816
+ let grade;
817
+ if (overallScore >= 90)
818
+ grade = 'A';
819
+ else if (overallScore >= 80)
820
+ grade = 'B';
821
+ else if (overallScore >= 70)
822
+ grade = 'C';
823
+ else if (overallScore >= 60)
824
+ grade = 'D';
825
+ else
826
+ grade = 'F';
827
+ // Top recommendations (prioritized by category weight)
828
+ const topRecommendations = allRecommendations.slice(0, 5);
829
+ return {
830
+ overallScore,
831
+ grade,
832
+ categories: detailed ? categories : Object.fromEntries(Object.entries(categories).map(([k, v]) => [k, { ...v, findings: [], recommendations: [] }])),
833
+ blockers,
834
+ topRecommendations,
835
+ productionReady: grade !== 'F' && blockers.length === 0,
836
+ };
837
+ }
838
+ //# sourceMappingURL=completeness.js.map