@vibecheckai/cli 3.2.2 → 3.2.4

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 (170) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  6. package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
  7. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
  8. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
  9. package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
  10. package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
  11. package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
  12. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
  14. package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
  15. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  16. package/bin/runners/lib/analyzers.js +606 -325
  17. package/bin/runners/lib/auth-truth.js +193 -193
  18. package/bin/runners/lib/backup.js +62 -62
  19. package/bin/runners/lib/billing.js +107 -107
  20. package/bin/runners/lib/claims.js +118 -118
  21. package/bin/runners/lib/cli-ui.js +540 -540
  22. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  23. package/bin/runners/lib/contracts/env-contract.js +181 -181
  24. package/bin/runners/lib/contracts/external-contract.js +206 -206
  25. package/bin/runners/lib/contracts/guard.js +168 -168
  26. package/bin/runners/lib/contracts/index.js +89 -89
  27. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  28. package/bin/runners/lib/contracts/route-contract.js +199 -199
  29. package/bin/runners/lib/contracts.js +804 -804
  30. package/bin/runners/lib/detect.js +89 -89
  31. package/bin/runners/lib/doctor/autofix.js +254 -254
  32. package/bin/runners/lib/doctor/index.js +37 -37
  33. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  34. package/bin/runners/lib/doctor/modules/index.js +46 -46
  35. package/bin/runners/lib/doctor/modules/network.js +250 -250
  36. package/bin/runners/lib/doctor/modules/project.js +312 -312
  37. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  38. package/bin/runners/lib/doctor/modules/security.js +348 -348
  39. package/bin/runners/lib/doctor/modules/system.js +213 -213
  40. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  41. package/bin/runners/lib/doctor/reporter.js +262 -262
  42. package/bin/runners/lib/doctor/service.js +262 -262
  43. package/bin/runners/lib/doctor/types.js +113 -113
  44. package/bin/runners/lib/doctor/ui.js +263 -263
  45. package/bin/runners/lib/doctor-v2.js +608 -608
  46. package/bin/runners/lib/drift.js +425 -425
  47. package/bin/runners/lib/enforcement.js +72 -72
  48. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  49. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  50. package/bin/runners/lib/engines/ast-cache.js +99 -0
  51. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  52. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  53. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  54. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  55. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  56. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  57. package/bin/runners/lib/engines/file-filter.js +131 -0
  58. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  59. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  60. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  61. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  62. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  63. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  64. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  65. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  66. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  67. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  68. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  69. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  70. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  71. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  72. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  73. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  74. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  75. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  76. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  77. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  78. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  79. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  80. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  81. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  82. package/bin/runners/lib/enterprise-detect.js +603 -603
  83. package/bin/runners/lib/enterprise-init.js +942 -942
  84. package/bin/runners/lib/env-resolver.js +417 -417
  85. package/bin/runners/lib/env-template.js +66 -66
  86. package/bin/runners/lib/env.js +189 -189
  87. package/bin/runners/lib/extractors/client-calls.js +990 -990
  88. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  89. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  90. package/bin/runners/lib/extractors/index.js +363 -363
  91. package/bin/runners/lib/extractors/next-routes.js +524 -524
  92. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  93. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  94. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  95. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  96. package/bin/runners/lib/findings-schema.js +281 -281
  97. package/bin/runners/lib/firewall-prompt.js +50 -50
  98. package/bin/runners/lib/global-flags.js +213 -213
  99. package/bin/runners/lib/graph/graph-builder.js +265 -265
  100. package/bin/runners/lib/graph/html-renderer.js +413 -413
  101. package/bin/runners/lib/graph/index.js +32 -32
  102. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  103. package/bin/runners/lib/graph/static-extractor.js +518 -518
  104. package/bin/runners/lib/html-report.js +650 -650
  105. package/bin/runners/lib/interactive-menu.js +1496 -1496
  106. package/bin/runners/lib/llm.js +75 -75
  107. package/bin/runners/lib/meter.js +61 -61
  108. package/bin/runners/lib/missions/evidence.js +126 -126
  109. package/bin/runners/lib/patch.js +40 -40
  110. package/bin/runners/lib/permissions/auth-model.js +213 -213
  111. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  112. package/bin/runners/lib/permissions/index.js +45 -45
  113. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  114. package/bin/runners/lib/pkgjson.js +28 -28
  115. package/bin/runners/lib/policy.js +295 -295
  116. package/bin/runners/lib/preflight.js +142 -142
  117. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  118. package/bin/runners/lib/reality/index.js +318 -318
  119. package/bin/runners/lib/reality/request-hashing.js +416 -416
  120. package/bin/runners/lib/reality/request-mapper.js +453 -453
  121. package/bin/runners/lib/reality/safety-rails.js +463 -463
  122. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  123. package/bin/runners/lib/reality/toast-detector.js +393 -393
  124. package/bin/runners/lib/reality-findings.js +84 -84
  125. package/bin/runners/lib/receipts.js +179 -179
  126. package/bin/runners/lib/redact.js +29 -29
  127. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  128. package/bin/runners/lib/replay/index.js +263 -263
  129. package/bin/runners/lib/replay/player.js +348 -348
  130. package/bin/runners/lib/replay/recorder.js +331 -331
  131. package/bin/runners/lib/report-output.js +187 -187
  132. package/bin/runners/lib/report.js +135 -135
  133. package/bin/runners/lib/route-detection.js +1140 -1140
  134. package/bin/runners/lib/sandbox/index.js +59 -59
  135. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  136. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  137. package/bin/runners/lib/sandbox/worktree.js +174 -174
  138. package/bin/runners/lib/scan-output.js +525 -190
  139. package/bin/runners/lib/schema-validator.js +350 -350
  140. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  141. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  142. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  143. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  144. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  145. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  146. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  147. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  148. package/bin/runners/lib/schemas/validator.js +438 -438
  149. package/bin/runners/lib/score-history.js +282 -282
  150. package/bin/runners/lib/share-pack.js +239 -239
  151. package/bin/runners/lib/snippets.js +67 -67
  152. package/bin/runners/lib/status-output.js +253 -253
  153. package/bin/runners/lib/terminal-ui.js +351 -271
  154. package/bin/runners/lib/upsell.js +510 -510
  155. package/bin/runners/lib/usage.js +153 -153
  156. package/bin/runners/lib/validate-patch.js +156 -156
  157. package/bin/runners/lib/verdict-engine.js +628 -628
  158. package/bin/runners/reality/engine.js +917 -917
  159. package/bin/runners/reality/flows.js +122 -122
  160. package/bin/runners/reality/report.js +378 -378
  161. package/bin/runners/reality/session.js +193 -193
  162. package/bin/runners/runGuard.js +168 -168
  163. package/bin/runners/runProof.zip +0 -0
  164. package/bin/runners/runProve.js +8 -0
  165. package/bin/runners/runReality.js +14 -0
  166. package/bin/runners/runScan.js +17 -1
  167. package/bin/runners/runTruth.js +15 -3
  168. package/mcp-server/tier-auth.js +4 -4
  169. package/mcp-server/tools/index.js +72 -72
  170. package/package.json +1 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Hardcoded Secrets Engine
3
+ * Detects API keys, tokens, passwords, and private keys in code.
4
+ */
5
+
6
+ const { getAST, parseCode } = require("./ast-cache");
7
+ const traverse = require("@babel/traverse").default;
8
+ const t = require("@babel/types");
9
+ const { shouldExcludeFile, isTestContext, hasIgnoreDirective } = require("./file-filter");
10
+
11
+ // Enhanced patterns with more comprehensive detection
12
+ const SECRET_PATTERNS = [
13
+ { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/ },
14
+ { name: "AWS Secret Key", pattern: /[0-9a-zA-Z/+]{40}/ },
15
+ { name: "GitHub Token", pattern: /ghp_[0-9a-zA-Z]{36}/ },
16
+ { name: "GitLab Token", pattern: /glpat-[0-9a-zA-Z\-_]{20,}/ },
17
+ { name: "Slack Token", pattern: /xox[baprs]-[0-9a-zA-Z]{10,48}/ },
18
+ { name: "Stripe Key", pattern: /sk_(live|test)_[0-9a-zA-Z]{24,}/ },
19
+ { name: "Generic API Key", pattern: /[a-zA-Z0-9]{32,}/ },
20
+ { name: "JWT Token", pattern: /eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/ },
21
+ { name: "Private Key", pattern: /-----BEGIN (RSA |EC )?PRIVATE KEY-----/ },
22
+ { name: "Database URL", pattern: /(mongodb|postgresql|mysql):\/\/[^\\s]+/ },
23
+ { name: "Password", pattern: /(password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/i },
24
+ { name: "Bearer Token", pattern: /Bearer\s+[a-zA-Z0-9\-_\.]+/i },
25
+ { name: "OAuth Token", pattern: /ya29\.[0-9A-Za-z\-_]+/ },
26
+ { name: "Twilio Key", pattern: /SK[0-9a-fA-F]{32}/ },
27
+ ];
28
+
29
+ function getContext(code, start, length = 80) {
30
+ const contextStart = Math.max(0, start - length / 2);
31
+ const contextEnd = Math.min(code.length, start + length / 2);
32
+ return code.slice(contextStart, contextEnd).replace(/\n/g, " ").trim();
33
+ }
34
+
35
+ function isLikelyFalsePositive(value) {
36
+ // Exclude obvious placeholders
37
+ const placeholders = [
38
+ "YOUR_API_KEY",
39
+ "API_KEY_HERE",
40
+ "REPLACE_ME",
41
+ "CHANGE_ME",
42
+ "INSERT_KEY",
43
+ "TODO",
44
+ "EXAMPLE",
45
+ "SAMPLE",
46
+ "TEST",
47
+ "DUMMY",
48
+ ];
49
+ const upper = String(value).toUpperCase();
50
+ return placeholders.some((p) => upper.includes(p));
51
+ }
52
+
53
+ function analyzeHardcodedSecrets(code, filePath) {
54
+ const findings = [];
55
+
56
+ if (shouldExcludeFile(filePath)) return findings;
57
+ if (isTestContext(code, filePath)) return findings;
58
+ if (hasIgnoreDirective(code, "hardcoded-secrets")) return findings;
59
+
60
+ const ast = getAST(code, filePath);
61
+ if (!ast) return findings;
62
+
63
+ const lines = code.split("\n");
64
+
65
+ // Check string literals for secrets
66
+ traverse(ast, {
67
+ StringLiteral(path) {
68
+ const value = path.node.value;
69
+ if (!value || value.length < 12) return;
70
+ if (isLikelyFalsePositive(value)) return;
71
+
72
+ for (const secretPattern of SECRET_PATTERNS) {
73
+ const match = value.match(secretPattern.pattern);
74
+ if (!match) continue;
75
+
76
+ const loc = path.node.loc?.start;
77
+ if (!loc) continue;
78
+
79
+ const line = loc.line;
80
+ const context = getContext(code, path.node.start || 0);
81
+
82
+ findings.push({
83
+ type: "hardcoded_secret",
84
+ severity: "BLOCK",
85
+ category: "Security",
86
+ file: filePath,
87
+ line,
88
+ column: loc.column,
89
+ title: `Hardcoded ${secretPattern.name} detected`,
90
+ message: `Found potential ${secretPattern.name} in string literal. Move to environment variables or secret manager.`,
91
+ codeSnippet: lines[line - 1]?.trim(),
92
+ evidence: context,
93
+ confidence: secretPattern.name === "Generic API Key" ? "low" : "high",
94
+ });
95
+ }
96
+ },
97
+
98
+ TemplateLiteral(path) {
99
+ // Template literals may contain secrets in raw strings (rare), but catch if a quasi looks like a token.
100
+ for (const quasi of path.node.quasis || []) {
101
+ const value = quasi.value?.cooked || "";
102
+ if (!value || value.length < 12) continue;
103
+ if (isLikelyFalsePositive(value)) continue;
104
+
105
+ for (const secretPattern of SECRET_PATTERNS) {
106
+ const match = value.match(secretPattern.pattern);
107
+ if (!match) continue;
108
+
109
+ const loc = quasi.loc?.start || path.node.loc?.start;
110
+ if (!loc) continue;
111
+
112
+ const line = loc.line;
113
+ const context = getContext(code, quasi.start || path.node.start || 0);
114
+
115
+ findings.push({
116
+ type: "hardcoded_secret",
117
+ severity: "BLOCK",
118
+ category: "Security",
119
+ file: filePath,
120
+ line,
121
+ column: loc.column,
122
+ title: `Hardcoded ${secretPattern.name} detected`,
123
+ message: `Found potential ${secretPattern.name} in template literal. Move to environment variables or secret manager.`,
124
+ codeSnippet: lines[line - 1]?.trim(),
125
+ evidence: context,
126
+ confidence: secretPattern.name === "Generic API Key" ? "low" : "high",
127
+ });
128
+ }
129
+ }
130
+ },
131
+ });
132
+
133
+ return findings;
134
+ }
135
+
136
+ module.exports = {
137
+ analyzeHardcodedSecrets,
138
+ parseCode,
139
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Mock Data Engine
3
+ * Detects mock, fake, or placeholder data in production code.
4
+ */
5
+
6
+ const { getAST, parseCode } = require("./ast-cache");
7
+ const traverse = require("@babel/traverse").default;
8
+ const { shouldExcludeFile, isTestContext, hasIgnoreDirective } = require("./file-filter");
9
+
10
+ // Enhanced mock data patterns
11
+ const MOCK_PATTERNS = [
12
+ { pattern: /mock/i, type: "mock_reference", message: "Mock reference found" },
13
+ { pattern: /fake/i, type: "fake_data", message: "Fake data reference found" },
14
+ { pattern: /dummy/i, type: "dummy_data", message: "Dummy data reference found" },
15
+ { pattern: /test.*data/i, type: "test_data", message: "Test data reference found" },
16
+ { pattern: /lorem ipsum/i, type: "lorem_ipsum", message: "Lorem ipsum placeholder found" },
17
+ { pattern: /todo|fixme/i, type: "todo_fixme", message: "TODO/FIXME found in code" },
18
+ { pattern: /placeholder/i, type: "placeholder", message: "Placeholder reference found" },
19
+ { pattern: /example/i, type: "example_data", message: "Example reference found" },
20
+ { pattern: /sample/i, type: "sample_data", message: "Sample data reference found" },
21
+ { pattern: /hardcode/i, type: "hardcoded", message: "Hardcoded data reference found" },
22
+ ];
23
+
24
+ // Common mock data values
25
+ const MOCK_VALUES = [
26
+ "test",
27
+ "demo",
28
+ "example",
29
+ "sample",
30
+ "placeholder",
31
+ "lorem",
32
+ "ipsum",
33
+ "foo",
34
+ "bar",
35
+ "baz",
36
+ "mock",
37
+ "fake",
38
+ "dummy",
39
+ "12345",
40
+ "password",
41
+ "admin",
42
+ "user",
43
+ ];
44
+
45
+ function analyzeMockData(code, filePath) {
46
+ const findings = [];
47
+
48
+ if (shouldExcludeFile(filePath)) return findings;
49
+ if (isTestContext(code, filePath)) return findings;
50
+ if (hasIgnoreDirective(code, "mock-data")) return findings;
51
+
52
+ const ast = getAST(code, filePath);
53
+ if (!ast) return findings;
54
+
55
+ const lines = code.split("\n");
56
+
57
+ // Check string literals for mock data
58
+ traverse(ast, {
59
+ StringLiteral(path) {
60
+ const value = path.node.value;
61
+ if (!value || value.length < 2) return;
62
+
63
+ // Check for common mock values (case-insensitive)
64
+ const lower = value.toLowerCase();
65
+ if (MOCK_VALUES.includes(lower)) {
66
+ const loc = path.node.loc?.start;
67
+ if (!loc) return;
68
+
69
+ findings.push({
70
+ type: "mock_value",
71
+ severity: "INFO",
72
+ category: "CodeQuality",
73
+ file: filePath,
74
+ line: loc.line,
75
+ column: loc.column,
76
+ title: "Potential mock value in string literal",
77
+ message: `String literal "${value}" looks like mock/placeholder data.`,
78
+ codeSnippet: lines[loc.line - 1]?.trim(),
79
+ confidence: "low",
80
+ });
81
+ }
82
+
83
+ // Check for mock patterns
84
+ for (const mockPattern of MOCK_PATTERNS) {
85
+ if (mockPattern.pattern.test(value)) {
86
+ const loc = path.node.loc?.start;
87
+ if (!loc) continue;
88
+
89
+ findings.push({
90
+ type: mockPattern.type,
91
+ severity: mockPattern.type === "todo_fixme" ? "WARN" : "INFO",
92
+ category: "CodeQuality",
93
+ file: filePath,
94
+ line: loc.line,
95
+ column: loc.column,
96
+ title: mockPattern.message,
97
+ message: `Found "${mockPattern.pattern}" in string literal: "${value}"`,
98
+ codeSnippet: lines[loc.line - 1]?.trim(),
99
+ confidence: "low",
100
+ });
101
+ }
102
+ }
103
+ },
104
+
105
+ Identifier(path) {
106
+ const name = path.node.name;
107
+ if (!name) return;
108
+
109
+ // Check for mock patterns in variable names (avoid huge noise: only longer names)
110
+ if (name.length < 5) return;
111
+
112
+ for (const mockPattern of MOCK_PATTERNS) {
113
+ if (mockPattern.pattern.test(name)) {
114
+ const loc = path.node.loc?.start;
115
+ if (!loc) continue;
116
+
117
+ findings.push({
118
+ type: mockPattern.type,
119
+ severity: mockPattern.type === "todo_fixme" ? "WARN" : "INFO",
120
+ category: "CodeQuality",
121
+ file: filePath,
122
+ line: loc.line,
123
+ column: loc.column,
124
+ title: `Mock-like identifier: ${name}`,
125
+ message: `Identifier name suggests mock/placeholder data: "${name}"`,
126
+ codeSnippet: lines[loc.line - 1]?.trim(),
127
+ confidence: "low",
128
+ });
129
+ }
130
+ }
131
+ },
132
+ });
133
+
134
+ return findings;
135
+ }
136
+
137
+ module.exports = {
138
+ analyzeMockData,
139
+ parseCode,
140
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Parallel Processor
3
+ * Utility to process a list of files concurrently with bounded parallelism.
4
+ *
5
+ * NOTE: The caller provides `processor(filePath)` which may return any value.
6
+ * We collect both results and errors without crashing the whole run.
7
+ */
8
+
9
+ const os = require("os");
10
+
11
+ /**
12
+ * Run a promise with an optional timeout.
13
+ */
14
+ async function withTimeout(promise, timeoutMs, label = "operation") {
15
+ if (!timeoutMs || timeoutMs <= 0) return promise;
16
+
17
+ let timer = null;
18
+ const timeout = new Promise((_, reject) => {
19
+ timer = setTimeout(() => {
20
+ const err = new Error(`${label} timed out after ${timeoutMs}ms`);
21
+ err.code = "ETIMEDOUT";
22
+ reject(err);
23
+ }, timeoutMs);
24
+ });
25
+
26
+ try {
27
+ return await Promise.race([promise, timeout]);
28
+ } finally {
29
+ if (timer) clearTimeout(timer);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Process files in parallel with bounded concurrency.
35
+ *
36
+ * @param {string[]} files
37
+ * @param {(filePath: string) => Promise<any>} processor
38
+ * @param {object} options
39
+ * @returns {Promise<{results: Array<{file: string, result: any}>, errors: Array<{file: string, error: Error}>, stats: object}>}
40
+ */
41
+ async function processFilesInParallel(files, processor, options = {}) {
42
+ const concurrency =
43
+ Number.isFinite(options.concurrency) && options.concurrency > 0
44
+ ? Math.floor(options.concurrency)
45
+ : Math.max(1, Math.min(os.cpus().length, 8));
46
+
47
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
48
+ const stopOnError = Boolean(options.stopOnError);
49
+ const onProgress = typeof options.onProgress === "function" ? options.onProgress : null;
50
+ const signal = options.signal;
51
+
52
+ const results = [];
53
+ const errors = [];
54
+
55
+ let cursor = 0;
56
+ let completed = 0;
57
+ const startedAt = Date.now();
58
+
59
+ function reportProgress(file, kind) {
60
+ if (!onProgress) return;
61
+ try {
62
+ onProgress({
63
+ kind,
64
+ file,
65
+ completed,
66
+ total: files.length,
67
+ errors: errors.length,
68
+ elapsedMs: Date.now() - startedAt,
69
+ });
70
+ } catch {
71
+ // ignore progress handler failures
72
+ }
73
+ }
74
+
75
+ async function runFile(filePath) {
76
+ if (signal?.aborted) {
77
+ const err = new Error("Aborted");
78
+ err.code = "ABORTED";
79
+ throw err;
80
+ }
81
+
82
+ const label = `process(${filePath})`;
83
+ const result = await withTimeout(Promise.resolve().then(() => processor(filePath)), timeoutMs, label);
84
+ return result;
85
+ }
86
+
87
+ async function worker(workerId) {
88
+ // eslint-disable-next-line no-constant-condition
89
+ while (true) {
90
+ if (signal?.aborted) break;
91
+ if (stopOnError && errors.length) break;
92
+
93
+ const i = cursor++;
94
+ if (i >= files.length) break;
95
+
96
+ const filePath = files[i];
97
+ reportProgress(filePath, "start");
98
+
99
+ try {
100
+ const result = await runFile(filePath);
101
+ results.push({ file: filePath, result });
102
+ } catch (error) {
103
+ errors.push({ file: filePath, error });
104
+ reportProgress(filePath, "error");
105
+ } finally {
106
+ completed++;
107
+ reportProgress(filePath, "done");
108
+ }
109
+ }
110
+ }
111
+
112
+ const workers = Array.from({ length: Math.min(concurrency, files.length) }, (_, i) => worker(i));
113
+ await Promise.all(workers);
114
+
115
+ return {
116
+ results,
117
+ errors,
118
+ stats: {
119
+ totalFiles: files.length,
120
+ processed: completed,
121
+ results: results.length,
122
+ errors: errors.length,
123
+ concurrency,
124
+ timeoutMs,
125
+ elapsedMs: Date.now() - startedAt,
126
+ aborted: Boolean(signal?.aborted),
127
+ },
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Process in sequential batches (useful when you want deterministic ordering).
133
+ *
134
+ * @param {string[]} files
135
+ * @param {(filePath: string) => Promise<any>} processor
136
+ * @param {number} batchSize
137
+ */
138
+ async function processFilesInBatches(files, processor, batchSize = 10) {
139
+ const results = [];
140
+ const errors = [];
141
+ const size = Math.max(1, Number(batchSize) || 10);
142
+
143
+ for (let i = 0; i < files.length; i += size) {
144
+ const batch = files.slice(i, i + size);
145
+ const batchResults = await Promise.allSettled(batch.map((f) => Promise.resolve().then(() => processor(f))));
146
+
147
+ for (let j = 0; j < batchResults.length; j++) {
148
+ const file = batch[j];
149
+ const r = batchResults[j];
150
+ if (r.status === "fulfilled") {
151
+ results.push({ file, result: r.value });
152
+ } else {
153
+ errors.push({ file, error: r.reason });
154
+ }
155
+ }
156
+ }
157
+
158
+ return { results, errors };
159
+ }
160
+
161
+ module.exports = {
162
+ processFilesInParallel,
163
+ processFilesInBatches,
164
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Performance Issues Engine
3
+ * Detects potential performance problems:
4
+ * - Nested loops
5
+ * - Potential memory leaks (event listeners without cleanup)
6
+ * - Expensive operations in render-like contexts (heuristic)
7
+ */
8
+
9
+ const { getAST, parseCode } = require("./ast-cache");
10
+ const traverse = require("@babel/traverse").default;
11
+ const t = require("@babel/types");
12
+ const { shouldExcludeFile, isTestContext, hasIgnoreDirective } = require("./file-filter");
13
+
14
+ function snippetForLine(lines, line) {
15
+ return lines[line - 1] ? lines[line - 1].trim() : "";
16
+ }
17
+
18
+ function isLoopStatement(node) {
19
+ return (
20
+ t.isForStatement(node) ||
21
+ t.isForInStatement(node) ||
22
+ t.isForOfStatement(node) ||
23
+ t.isWhileStatement(node) ||
24
+ t.isDoWhileStatement(node)
25
+ );
26
+ }
27
+
28
+ function getEventName(callNode) {
29
+ const arg0 = callNode.arguments?.[0];
30
+ if (t.isStringLiteral(arg0)) return arg0.value;
31
+ if (t.isIdentifier(arg0)) return arg0.name;
32
+ return null;
33
+ }
34
+
35
+ function calleePropertyName(callNode) {
36
+ const callee = callNode.callee;
37
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && !callee.computed) return callee.property.name;
38
+ if (t.isIdentifier(callee)) return callee.name;
39
+ return null;
40
+ }
41
+
42
+ function hasMatchingRemoveEventListener(fnPath, eventName) {
43
+ let found = false;
44
+
45
+ fnPath.traverse({
46
+ Function(inner) {
47
+ if (inner !== fnPath) inner.skip();
48
+ },
49
+ CallExpression(p) {
50
+ const name = calleePropertyName(p.node);
51
+ if (name !== "removeEventListener") return;
52
+ const ev = getEventName(p.node);
53
+ if (!eventName || !ev) {
54
+ // no event info: still counts as cleanup
55
+ found = true;
56
+ return;
57
+ }
58
+ if (ev === eventName) found = true;
59
+ },
60
+ });
61
+
62
+ return found;
63
+ }
64
+
65
+ function isLikelyRenderContext(path) {
66
+ // Heuristic: inside a function named render, or a React component (PascalCase) returning JSX
67
+ const fn = path.getFunctionParent();
68
+ if (!fn) return false;
69
+
70
+ const node = fn.node;
71
+ let name = null;
72
+
73
+ if (t.isFunctionDeclaration(node) && node.id?.name) name = node.id.name;
74
+ if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) && t.isVariableDeclarator(fn.parent) && t.isIdentifier(fn.parent.id)) {
75
+ name = fn.parent.id.name;
76
+ }
77
+
78
+ if (name === "render") return true;
79
+ if (name && /^[A-Z]/.test(name)) return true; // component-ish
80
+
81
+ // If function returns JSX somewhere
82
+ let returnsJSX = false;
83
+ fn.traverse({
84
+ Function(inner) {
85
+ if (inner !== fn) inner.skip();
86
+ },
87
+ ReturnStatement(r) {
88
+ if (t.isJSXElement(r.node.argument) || t.isJSXFragment(r.node.argument)) returnsJSX = true;
89
+ },
90
+ });
91
+
92
+ return returnsJSX;
93
+ }
94
+
95
+ function analyzePerformanceIssues(code, filePath) {
96
+ const findings = [];
97
+
98
+ if (shouldExcludeFile(filePath)) return findings;
99
+ if (isTestContext(code, filePath)) return findings;
100
+ if (hasIgnoreDirective(code, "performance")) return findings;
101
+
102
+ const ast = getAST(code, filePath);
103
+ if (!ast) return findings;
104
+
105
+ const lines = code.split("\n");
106
+
107
+ traverse(ast, {
108
+ // Nested loops - only flag 3+ levels deep
109
+ enter(path) {
110
+ if (!isLoopStatement(path.node)) return;
111
+
112
+ // Count nesting depth
113
+ let depth = 0;
114
+ let current = path;
115
+ while (current) {
116
+ if (isLoopStatement(current.node)) {
117
+ depth++;
118
+ }
119
+ current = current.parentPath;
120
+ }
121
+
122
+ // Only flag if 3 or more levels deep
123
+ if (depth < 3) return;
124
+
125
+ const loc = path.node.loc?.start;
126
+ if (!loc) return;
127
+
128
+ findings.push({
129
+ type: "nested_loop",
130
+ severity: "WARN",
131
+ category: "Performance",
132
+ file: filePath,
133
+ line: loc.line,
134
+ column: loc.column,
135
+ title: `Deeply nested loop (${depth} levels)`,
136
+ message: "Deeply nested loops can be expensive. Consider optimizing or using better data structures.",
137
+ codeSnippet: snippetForLine(lines, loc.line),
138
+ confidence: "med",
139
+ });
140
+ path.skip(); // avoid multiple nested loop reports in same subtree
141
+ },
142
+
143
+ // Event listeners without cleanup - only flag in component-like contexts
144
+ CallExpression(path) {
145
+ const callee = path.node.callee;
146
+ if (!t.isMemberExpression(callee)) return;
147
+ if (callee.computed) return;
148
+ if (!t.isIdentifier(callee.property, { name: "addEventListener" })) return;
149
+
150
+ const loc = path.node.loc?.start;
151
+ if (!loc) return;
152
+
153
+ // Skip if not in a component-like context (React, Vue, etc.)
154
+ const fn = path.getFunctionParent();
155
+ if (!fn) return;
156
+
157
+ // Only flag in component-like functions or useEffect-like hooks
158
+ const fnName = fn.node.id?.name || (fn.parent?.id?.name);
159
+ const isComponent = fnName && /^[A-Z]/.test(fnName);
160
+ const isEffectHook = fnName && /useEffect|useLayoutEffect|componentDidMount|componentWillUnmount/i.test(fnName);
161
+
162
+ if (!isComponent && !isEffectHook) return;
163
+
164
+ const evName = getEventName(path.node);
165
+ const hasCleanup = hasMatchingRemoveEventListener(fn, evName);
166
+
167
+ if (hasCleanup) return;
168
+
169
+ findings.push({
170
+ type: "potential_memory_leak",
171
+ severity: "WARN",
172
+ category: "Performance",
173
+ file: filePath,
174
+ line: loc.line,
175
+ column: loc.column,
176
+ title: "Event listener without cleanup",
177
+ message: "addEventListener() found without a matching removeEventListener() in the same function scope.",
178
+ codeSnippet: snippetForLine(lines, loc.line),
179
+ confidence: "low",
180
+ });
181
+ },
182
+
183
+ // Expensive operations in render-like contexts
184
+ CallExpression(path) {
185
+ const callee = path.node.callee;
186
+ const loc = path.node.loc?.start;
187
+ if (!loc) return;
188
+
189
+ if (!isLikelyRenderContext(path)) return;
190
+
191
+ // JSON.parse / stringify in render
192
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: "JSON" }) && t.isIdentifier(callee.property)) {
193
+ const m = callee.property.name;
194
+ if (m === "parse" || m === "stringify") {
195
+ findings.push({
196
+ type: "expensive_render_op",
197
+ severity: "INFO",
198
+ category: "Performance",
199
+ file: filePath,
200
+ line: loc.line,
201
+ column: loc.column,
202
+ title: `JSON.${m} in render context`,
203
+ message: "Consider moving JSON parsing/serialization out of render to avoid repeated work.",
204
+ codeSnippet: snippetForLine(lines, loc.line),
205
+ confidence: "low",
206
+ });
207
+ }
208
+ }
209
+
210
+ // Array.sort in render (mutates + expensive)
211
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: "sort" })) {
212
+ findings.push({
213
+ type: "expensive_render_op",
214
+ severity: "INFO",
215
+ category: "Performance",
216
+ file: filePath,
217
+ line: loc.line,
218
+ column: loc.column,
219
+ title: "Array.sort in render context",
220
+ message: "Sorting during render can be expensive and mutates arrays. Consider sorting in memoized/selectors.",
221
+ codeSnippet: snippetForLine(lines, loc.line),
222
+ confidence: "low",
223
+ });
224
+ }
225
+ },
226
+ });
227
+
228
+ return findings;
229
+ }
230
+
231
+ module.exports = {
232
+ analyzePerformanceIssues,
233
+ parseCode,
234
+ };