midas-mcp 3.2.0 → 3.3.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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +93 -49
- package/dist/analyzer.js.map +1 -1
- package/dist/docs/DEPLOYMENT.md +190 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/monitoring.d.ts +97 -0
- package/dist/monitoring.d.ts.map +1 -0
- package/dist/monitoring.js +258 -0
- package/dist/monitoring.js.map +1 -0
- package/dist/prompts/grow.d.ts.map +1 -1
- package/dist/prompts/grow.js +155 -47
- package/dist/prompts/grow.js.map +1 -1
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +77 -15
- package/dist/providers.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -3
- package/dist/server.js.map +1 -1
- package/dist/state/phase.d.ts +19 -4
- package/dist/state/phase.d.ts.map +1 -1
- package/dist/state/phase.js +19 -10
- package/dist/state/phase.js.map +1 -1
- package/dist/tools/analyze.d.ts.map +1 -1
- package/dist/tools/analyze.js +21 -9
- package/dist/tools/analyze.js.map +1 -1
- package/dist/tools/completeness.d.ts +36 -0
- package/dist/tools/completeness.d.ts.map +1 -0
- package/dist/tools/completeness.js +838 -0
- package/dist/tools/completeness.js.map +1 -0
- package/dist/tools/grow.d.ts +157 -0
- package/dist/tools/grow.d.ts.map +1 -0
- package/dist/tools/grow.js +532 -0
- package/dist/tools/grow.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/validate.d.ts +60 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +234 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/verify.d.ts +4 -4
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +66 -12
- package/dist/tui.js.map +1 -1
- package/docs/DEPLOYMENT.md +190 -0
- 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
|