@mainahq/core 0.2.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 (156) hide show
  1. package/README.md +31 -0
  2. package/package.json +37 -0
  3. package/src/ai/__tests__/ai.test.ts +207 -0
  4. package/src/ai/__tests__/design-approaches.test.ts +192 -0
  5. package/src/ai/__tests__/spec-questions.test.ts +191 -0
  6. package/src/ai/__tests__/tiers.test.ts +110 -0
  7. package/src/ai/commit-msg.ts +28 -0
  8. package/src/ai/design-approaches.ts +76 -0
  9. package/src/ai/index.ts +205 -0
  10. package/src/ai/pr-summary.ts +60 -0
  11. package/src/ai/spec-questions.ts +74 -0
  12. package/src/ai/tiers.ts +52 -0
  13. package/src/ai/try-generate.ts +89 -0
  14. package/src/ai/validate.ts +66 -0
  15. package/src/benchmark/__tests__/reporter.test.ts +525 -0
  16. package/src/benchmark/__tests__/runner.test.ts +113 -0
  17. package/src/benchmark/__tests__/story-loader.test.ts +152 -0
  18. package/src/benchmark/reporter.ts +332 -0
  19. package/src/benchmark/runner.ts +91 -0
  20. package/src/benchmark/story-loader.ts +88 -0
  21. package/src/benchmark/types.ts +95 -0
  22. package/src/cache/__tests__/keys.test.ts +97 -0
  23. package/src/cache/__tests__/manager.test.ts +312 -0
  24. package/src/cache/__tests__/ttl.test.ts +94 -0
  25. package/src/cache/keys.ts +44 -0
  26. package/src/cache/manager.ts +231 -0
  27. package/src/cache/ttl.ts +77 -0
  28. package/src/config/__tests__/config.test.ts +376 -0
  29. package/src/config/index.ts +198 -0
  30. package/src/context/__tests__/budget.test.ts +179 -0
  31. package/src/context/__tests__/engine.test.ts +163 -0
  32. package/src/context/__tests__/episodic.test.ts +291 -0
  33. package/src/context/__tests__/relevance.test.ts +323 -0
  34. package/src/context/__tests__/retrieval.test.ts +143 -0
  35. package/src/context/__tests__/selector.test.ts +174 -0
  36. package/src/context/__tests__/semantic.test.ts +252 -0
  37. package/src/context/__tests__/treesitter.test.ts +229 -0
  38. package/src/context/__tests__/working.test.ts +236 -0
  39. package/src/context/budget.ts +130 -0
  40. package/src/context/engine.ts +394 -0
  41. package/src/context/episodic.ts +251 -0
  42. package/src/context/relevance.ts +325 -0
  43. package/src/context/retrieval.ts +325 -0
  44. package/src/context/selector.ts +93 -0
  45. package/src/context/semantic.ts +331 -0
  46. package/src/context/treesitter.ts +216 -0
  47. package/src/context/working.ts +192 -0
  48. package/src/db/__tests__/db.test.ts +151 -0
  49. package/src/db/index.ts +211 -0
  50. package/src/db/schema.ts +84 -0
  51. package/src/design/__tests__/design.test.ts +310 -0
  52. package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
  53. package/src/design/__tests__/review.test.ts +561 -0
  54. package/src/design/index.ts +297 -0
  55. package/src/design/review.ts +327 -0
  56. package/src/explain/__tests__/explain.test.ts +173 -0
  57. package/src/explain/index.ts +181 -0
  58. package/src/features/__tests__/analyzer.test.ts +358 -0
  59. package/src/features/__tests__/checklist.test.ts +454 -0
  60. package/src/features/__tests__/numbering.test.ts +319 -0
  61. package/src/features/__tests__/quality.test.ts +295 -0
  62. package/src/features/__tests__/traceability.test.ts +147 -0
  63. package/src/features/analyzer.ts +445 -0
  64. package/src/features/checklist.ts +366 -0
  65. package/src/features/index.ts +18 -0
  66. package/src/features/numbering.ts +404 -0
  67. package/src/features/quality.ts +349 -0
  68. package/src/features/test-stubs.ts +157 -0
  69. package/src/features/traceability.ts +260 -0
  70. package/src/feedback/__tests__/async-feedback.test.ts +52 -0
  71. package/src/feedback/__tests__/collector.test.ts +219 -0
  72. package/src/feedback/__tests__/compress.test.ts +150 -0
  73. package/src/feedback/__tests__/preferences.test.ts +169 -0
  74. package/src/feedback/collector.ts +135 -0
  75. package/src/feedback/compress.ts +92 -0
  76. package/src/feedback/preferences.ts +108 -0
  77. package/src/git/__tests__/git.test.ts +62 -0
  78. package/src/git/index.ts +110 -0
  79. package/src/hooks/__tests__/runner.test.ts +266 -0
  80. package/src/hooks/index.ts +8 -0
  81. package/src/hooks/runner.ts +130 -0
  82. package/src/index.ts +356 -0
  83. package/src/init/__tests__/init.test.ts +228 -0
  84. package/src/init/index.ts +364 -0
  85. package/src/language/__tests__/detect.test.ts +77 -0
  86. package/src/language/__tests__/profile.test.ts +51 -0
  87. package/src/language/detect.ts +70 -0
  88. package/src/language/profile.ts +110 -0
  89. package/src/prompts/__tests__/defaults.test.ts +52 -0
  90. package/src/prompts/__tests__/engine.test.ts +183 -0
  91. package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
  92. package/src/prompts/__tests__/evolution.test.ts +187 -0
  93. package/src/prompts/__tests__/loader.test.ts +105 -0
  94. package/src/prompts/candidates/review-v2.md +55 -0
  95. package/src/prompts/defaults/ai-review.md +49 -0
  96. package/src/prompts/defaults/commit.md +30 -0
  97. package/src/prompts/defaults/context.md +26 -0
  98. package/src/prompts/defaults/design-approaches.md +57 -0
  99. package/src/prompts/defaults/design-hld-lld.md +55 -0
  100. package/src/prompts/defaults/design.md +53 -0
  101. package/src/prompts/defaults/explain.md +31 -0
  102. package/src/prompts/defaults/fix.md +32 -0
  103. package/src/prompts/defaults/index.ts +38 -0
  104. package/src/prompts/defaults/review.md +41 -0
  105. package/src/prompts/defaults/spec-questions.md +59 -0
  106. package/src/prompts/defaults/tests.md +72 -0
  107. package/src/prompts/engine.ts +137 -0
  108. package/src/prompts/evolution.ts +409 -0
  109. package/src/prompts/loader.ts +71 -0
  110. package/src/review/__tests__/review.test.ts +288 -0
  111. package/src/review/comprehensive.ts +362 -0
  112. package/src/review/index.ts +417 -0
  113. package/src/stats/__tests__/tracker.test.ts +323 -0
  114. package/src/stats/index.ts +11 -0
  115. package/src/stats/tracker.ts +492 -0
  116. package/src/ticket/__tests__/ticket.test.ts +273 -0
  117. package/src/ticket/index.ts +185 -0
  118. package/src/utils.ts +87 -0
  119. package/src/verify/__tests__/ai-review.test.ts +242 -0
  120. package/src/verify/__tests__/coverage.test.ts +83 -0
  121. package/src/verify/__tests__/detect.test.ts +175 -0
  122. package/src/verify/__tests__/diff-filter.test.ts +338 -0
  123. package/src/verify/__tests__/fix.test.ts +478 -0
  124. package/src/verify/__tests__/linters/clippy.test.ts +45 -0
  125. package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
  126. package/src/verify/__tests__/linters/ruff.test.ts +64 -0
  127. package/src/verify/__tests__/mutation.test.ts +141 -0
  128. package/src/verify/__tests__/pipeline.test.ts +553 -0
  129. package/src/verify/__tests__/proof.test.ts +97 -0
  130. package/src/verify/__tests__/secretlint.test.ts +190 -0
  131. package/src/verify/__tests__/semgrep.test.ts +217 -0
  132. package/src/verify/__tests__/slop.test.ts +366 -0
  133. package/src/verify/__tests__/sonar.test.ts +113 -0
  134. package/src/verify/__tests__/syntax-guard.test.ts +227 -0
  135. package/src/verify/__tests__/trivy.test.ts +191 -0
  136. package/src/verify/__tests__/visual.test.ts +139 -0
  137. package/src/verify/ai-review.ts +276 -0
  138. package/src/verify/coverage.ts +134 -0
  139. package/src/verify/detect.ts +171 -0
  140. package/src/verify/diff-filter.ts +183 -0
  141. package/src/verify/fix.ts +317 -0
  142. package/src/verify/linters/clippy.ts +52 -0
  143. package/src/verify/linters/go-vet.ts +32 -0
  144. package/src/verify/linters/ruff.ts +47 -0
  145. package/src/verify/mutation.ts +143 -0
  146. package/src/verify/pipeline.ts +328 -0
  147. package/src/verify/proof.ts +277 -0
  148. package/src/verify/secretlint.ts +168 -0
  149. package/src/verify/semgrep.ts +170 -0
  150. package/src/verify/slop.ts +493 -0
  151. package/src/verify/sonar.ts +146 -0
  152. package/src/verify/syntax-guard.ts +251 -0
  153. package/src/verify/trivy.ts +161 -0
  154. package/src/verify/visual.ts +460 -0
  155. package/src/workflow/__tests__/context.test.ts +110 -0
  156. package/src/workflow/context.ts +81 -0
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Slop Detector — catches common AI-generated code patterns.
3
+ *
4
+ * Detects patterns that slip through linters: empty function bodies,
5
+ * hallucinated imports, console.log in production code, TODOs without
6
+ * ticket references, and large blocks of commented-out code.
7
+ *
8
+ * Pattern/regex-based detection. AST-based detection (tree-sitter) is
9
+ * a future improvement — the key is detecting the patterns correctly.
10
+ */
11
+
12
+ import { existsSync } from "node:fs";
13
+ import { dirname, isAbsolute, join, resolve } from "node:path";
14
+ import type { CacheManager } from "../cache/manager";
15
+ import type { LanguageProfile } from "../language/profile";
16
+ import { TYPESCRIPT_PROFILE } from "../language/profile";
17
+ import type { Finding } from "./diff-filter";
18
+
19
+ // ─── Types ────────────────────────────────────────────────────────────────
20
+
21
+ export type SlopRule =
22
+ | "empty-body"
23
+ | "hallucinated-import"
24
+ | "console-log"
25
+ | "todo-without-ticket"
26
+ | "commented-code";
27
+
28
+ export interface SlopResult {
29
+ findings: Finding[];
30
+ cached: boolean;
31
+ }
32
+
33
+ // ─── Helpers ──────────────────────────────────────────────────────────────
34
+
35
+ function hashContent(content: string): string {
36
+ const hasher = new Bun.CryptoHasher("sha256");
37
+ hasher.update(content);
38
+ return hasher.digest("hex");
39
+ }
40
+
41
+ // Bump this when detection logic changes to invalidate stale cache entries
42
+ const SLOP_CACHE_VERSION = 2;
43
+
44
+ function cacheKey(fileHash: string): string {
45
+ return `slop:v${SLOP_CACHE_VERSION}:${fileHash}`;
46
+ }
47
+
48
+ // ─── Individual Detectors ─────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Detect empty function/method/arrow bodies.
52
+ *
53
+ * Looks for patterns like `function name() { }`, `() => { }`, `method() { }`.
54
+ * Does NOT flag bodies that contain comments.
55
+ * Does NOT flag object literals or array literals.
56
+ */
57
+ export function detectEmptyBodies(
58
+ content: string,
59
+ file: string,
60
+ profile?: LanguageProfile,
61
+ ): Finding[] {
62
+ const lang = profile ?? TYPESCRIPT_PROFILE;
63
+ // Skip test files — mocks/stubs intentionally use empty bodies
64
+ if (lang.testFilePattern.test(file)) {
65
+ return [];
66
+ }
67
+
68
+ const findings: Finding[] = [];
69
+ const lines = content.split("\n");
70
+
71
+ // Strategy: find lines with `{}` or multiline open/close brace patterns
72
+ // that are part of function/method/arrow declarations.
73
+
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const line = lines[i] ?? "";
76
+ const trimmed = line.trim();
77
+
78
+ // Skip obvious non-function empty braces
79
+ if (
80
+ /(?:const|let|var|type|interface|enum)\s+\w+.*=\s*\{/.test(trimmed) &&
81
+ !trimmed.includes("=>")
82
+ ) {
83
+ continue;
84
+ }
85
+
86
+ // Check for single-line empty function body
87
+ const emptyBraces = /\{\s*\}/;
88
+ if (emptyBraces.test(trimmed)) {
89
+ // Skip lines where the empty braces are inside a regex literal or string
90
+ if (
91
+ /\/.*\{\\s\*\}.*\//.test(trimmed) ||
92
+ /['"`].*\{\s*\}.*['"`]/.test(trimmed)
93
+ ) {
94
+ continue;
95
+ }
96
+
97
+ const fnDeclPattern =
98
+ /function\s+\w+\s*\([^)]*\)\s*(?::\s*[^{]+)?\{\s*\}/;
99
+ const arrowPattern = /=>\s*\{\s*\}/;
100
+ const methodPattern =
101
+ /^\s*(?:(?:public|private|protected|static|async|get|set|override)\s+)*\w+\s*\([^)]*\)\s*(?::\s*[^{]+)?\{\s*\}/;
102
+ const nonFnPattern =
103
+ /(?:const|let|var|type|interface|enum|import|export\s+(?:type|interface))\s/;
104
+
105
+ const isFunctionLike =
106
+ fnDeclPattern.test(trimmed) ||
107
+ arrowPattern.test(trimmed) ||
108
+ (methodPattern.test(trimmed) && !nonFnPattern.test(trimmed));
109
+
110
+ if (isFunctionLike) {
111
+ findings.push({
112
+ tool: "slop",
113
+ file,
114
+ line: i + 1,
115
+ message: "Empty function/method body detected",
116
+ severity: "warning",
117
+ ruleId: "slop/empty-body",
118
+ });
119
+ }
120
+ continue;
121
+ }
122
+
123
+ // Multi-line empty body: opening brace on one line, closing on next,
124
+ // with nothing in between
125
+ if (trimmed.endsWith("{")) {
126
+ const nextLine = lines[i + 1]?.trim() ?? "";
127
+ if (nextLine === "}") {
128
+ // Check if this line is a function/method declaration
129
+ const isFunctionLike =
130
+ /function\s+\w+\s*\(/.test(trimmed) ||
131
+ /=>\s*\{$/.test(trimmed) ||
132
+ (/^\s*(?:(?:public|private|protected|static|async|get|set|override)\s+)*\w+\s*\([^)]*\)\s*(?::\s*[^{]+)?\{$/.test(
133
+ trimmed,
134
+ ) &&
135
+ !/(?:const|let|var|type|interface|enum|import|class|if|else|for|while|switch|try|catch)\s/.test(
136
+ trimmed,
137
+ ));
138
+
139
+ if (isFunctionLike) {
140
+ findings.push({
141
+ tool: "slop",
142
+ file,
143
+ line: i + 1,
144
+ message: "Empty function/method body detected",
145
+ severity: "warning",
146
+ ruleId: "slop/empty-body",
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ return findings;
154
+ }
155
+
156
+ /**
157
+ * Detect hallucinated imports — imports that reference non-existent modules.
158
+ *
159
+ * Only checks relative imports (./foo, ../bar). Package imports (react, zod,
160
+ * node:path, bun:test) are skipped since they could be valid packages.
161
+ */
162
+ export function detectHallucinatedImports(
163
+ content: string,
164
+ file: string,
165
+ cwd: string,
166
+ profile?: LanguageProfile,
167
+ ): Finding[] {
168
+ const lang = profile ?? TYPESCRIPT_PROFILE;
169
+ // Skip test files and markdown files — code blocks in .md trigger false positives
170
+ if (lang.testFilePattern.test(file) || file.endsWith(".md")) {
171
+ return [];
172
+ }
173
+
174
+ const findings: Finding[] = [];
175
+ const lines = content.split("\n");
176
+
177
+ // Determine the directory of the file being checked
178
+ const fileDir = dirname(isAbsolute(file) ? file : resolve(cwd, file));
179
+
180
+ // Match import statements with relative paths
181
+ const importPattern =
182
+ /(?:import\s+.*\s+from\s+|import\s+|require\s*\()['"](\.[^'"]+)['"]/;
183
+
184
+ for (let i = 0; i < lines.length; i++) {
185
+ const line = lines[i] ?? "";
186
+ const match = importPattern.exec(line);
187
+ if (!match) continue;
188
+
189
+ const importPath = match[1];
190
+ if (!importPath) continue;
191
+
192
+ // Only check relative imports
193
+ if (!importPath.startsWith(".")) continue;
194
+
195
+ // Skip placeholder/ellipsis imports (e.g. "..." in dynamic import docs)
196
+ if (/^\.{2,}$/.test(importPath)) continue;
197
+
198
+ const resolvedBase = resolve(fileDir, importPath);
199
+
200
+ // Check common extensions and index files
201
+ const candidates = [
202
+ resolvedBase,
203
+ `${resolvedBase}.ts`,
204
+ `${resolvedBase}.tsx`,
205
+ `${resolvedBase}.js`,
206
+ `${resolvedBase}.jsx`,
207
+ `${resolvedBase}.json`,
208
+ join(resolvedBase, "index.ts"),
209
+ join(resolvedBase, "index.tsx"),
210
+ join(resolvedBase, "index.js"),
211
+ join(resolvedBase, "index.jsx"),
212
+ ];
213
+
214
+ const found = candidates.some((candidate) => existsSync(candidate));
215
+
216
+ if (!found) {
217
+ findings.push({
218
+ tool: "slop",
219
+ file,
220
+ line: i + 1,
221
+ message: `Import "${importPath}" does not resolve to an existing file`,
222
+ severity: "error",
223
+ ruleId: "slop/hallucinated-import",
224
+ });
225
+ }
226
+ }
227
+
228
+ return findings;
229
+ }
230
+
231
+ /**
232
+ * Detect console.log/warn/error/debug/info in production code.
233
+ *
234
+ * Skips test files (*.test.ts, *.spec.ts).
235
+ * Accepts an optional LanguageProfile for language-specific patterns.
236
+ * Defaults to TYPESCRIPT_PROFILE for backward compatibility.
237
+ */
238
+ export function detectConsoleLogs(
239
+ content: string,
240
+ file: string,
241
+ profile?: LanguageProfile,
242
+ ): Finding[] {
243
+ const lang = profile ?? TYPESCRIPT_PROFILE;
244
+
245
+ // Skip test files using language-specific pattern
246
+ if (lang.testFilePattern.test(file)) {
247
+ return [];
248
+ }
249
+
250
+ const findings: Finding[] = [];
251
+ const lines = content.split("\n");
252
+
253
+ for (let i = 0; i < lines.length; i++) {
254
+ const line = lines[i] ?? "";
255
+ // Respect lint-ignore directives on preceding line
256
+ const prevLine = i > 0 ? (lines[i - 1] ?? "") : "";
257
+ if (lang.lintIgnorePattern.test(prevLine)) {
258
+ continue;
259
+ }
260
+ const match = lang.printPattern.exec(line);
261
+ if (match) {
262
+ findings.push({
263
+ tool: "slop",
264
+ file,
265
+ line: i + 1,
266
+ column: (match.index ?? 0) + 1,
267
+ message: "Print/log statement found in production code",
268
+ severity: "warning",
269
+ ruleId: "slop/console-log",
270
+ });
271
+ }
272
+ }
273
+
274
+ return findings;
275
+ }
276
+
277
+ /**
278
+ * Detect TODO/FIXME comments without a ticket reference.
279
+ *
280
+ * A ticket reference is a pattern like #123, PROJ-123, or [#123].
281
+ */
282
+ export function detectTodosWithoutTickets(
283
+ content: string,
284
+ file: string,
285
+ profile?: LanguageProfile,
286
+ ): Finding[] {
287
+ const lang = profile ?? TYPESCRIPT_PROFILE;
288
+ // Skip test files — fixtures legitimately contain TODO patterns as test data
289
+ if (lang.testFilePattern.test(file)) {
290
+ return [];
291
+ }
292
+
293
+ const findings: Finding[] = [];
294
+ const lines = content.split("\n");
295
+
296
+ // Match TODO or FIXME in comments (case-sensitive — these are always uppercase)
297
+ const todoPattern = /(?:\/\/|\/\*|\*)\s*(?:TODO|FIXME)\b/;
298
+ // Ticket reference patterns: #123, PROJ-123, [#123], (PROJ-123)
299
+ const ticketPattern = /#\d+|\b[A-Z][A-Z0-9]+-\d+/;
300
+
301
+ for (let i = 0; i < lines.length; i++) {
302
+ const line = lines[i] ?? "";
303
+ if (todoPattern.test(line) && !ticketPattern.test(line)) {
304
+ findings.push({
305
+ tool: "slop",
306
+ file,
307
+ line: i + 1,
308
+ message: "TODO/FIXME without ticket reference",
309
+ severity: "info",
310
+ ruleId: "slop/todo-without-ticket",
311
+ });
312
+ }
313
+ }
314
+
315
+ return findings;
316
+ }
317
+
318
+ /**
319
+ * Detect commented-out code blocks (3+ consecutive comment lines with code patterns).
320
+ *
321
+ * Distinguishes code comments from documentation comments by looking for
322
+ * code-like patterns: keywords, semicolons, brackets, import/export, assignments.
323
+ *
324
+ * JSDoc-style comments (starting with /**) are treated as documentation and skipped.
325
+ */
326
+ export function detectCommentedCode(
327
+ content: string,
328
+ file: string,
329
+ profile?: LanguageProfile,
330
+ ): Finding[] {
331
+ const lang = profile ?? TYPESCRIPT_PROFILE;
332
+ // Skip test files — fixtures contain intentional commented-out code as test data
333
+ if (lang.testFilePattern.test(file)) {
334
+ return [];
335
+ }
336
+
337
+ const findings: Finding[] = [];
338
+ const lines = content.split("\n");
339
+
340
+ // Code-like patterns in comments
341
+ const codePatterns = [
342
+ /(?:const|let|var|function|class|import|export|return|if|else|for|while|switch|case|break|continue|throw|try|catch)\s/,
343
+ /[=;{}()[\]]/,
344
+ /=>/,
345
+ /require\s*\(/,
346
+ /\.\w+\s*\(/,
347
+ ];
348
+
349
+ function looksLikeCode(line: string): boolean {
350
+ // Strip the comment prefix
351
+ const stripped = line
352
+ .replace(/^\s*\/\/\s?/, "")
353
+ .replace(/^\s*\*\s?/, "")
354
+ .replace(/^\s*\/\*\s?/, "")
355
+ .trim();
356
+ if (stripped.length === 0) return false;
357
+
358
+ return codePatterns.some((p) => p.test(stripped));
359
+ }
360
+
361
+ let blockStart = -1;
362
+ let blockCount = 0;
363
+ let inJsDoc = false;
364
+
365
+ for (let i = 0; i < lines.length; i++) {
366
+ const trimmed = (lines[i] ?? "").trim();
367
+
368
+ // Track JSDoc blocks
369
+ if (trimmed.startsWith("/**")) {
370
+ inJsDoc = true;
371
+ blockStart = -1;
372
+ blockCount = 0;
373
+ continue;
374
+ }
375
+ if (inJsDoc) {
376
+ if (trimmed.includes("*/")) {
377
+ inJsDoc = false;
378
+ }
379
+ continue;
380
+ }
381
+
382
+ // Single-line comment
383
+ const isSingleLineComment = trimmed.startsWith("//");
384
+
385
+ if (isSingleLineComment && looksLikeCode(trimmed)) {
386
+ if (blockStart === -1) {
387
+ blockStart = i;
388
+ blockCount = 1;
389
+ } else {
390
+ blockCount++;
391
+ }
392
+ } else {
393
+ // End of consecutive comment block
394
+ if (blockCount >= 3) {
395
+ findings.push({
396
+ tool: "slop",
397
+ file,
398
+ line: blockStart + 1,
399
+ message: `${blockCount} consecutive lines of commented-out code`,
400
+ severity: "warning",
401
+ ruleId: "slop/commented-code",
402
+ });
403
+ }
404
+ blockStart = -1;
405
+ blockCount = 0;
406
+ }
407
+ }
408
+
409
+ // Check trailing block
410
+ if (blockCount >= 3) {
411
+ findings.push({
412
+ tool: "slop",
413
+ file,
414
+ line: blockStart + 1,
415
+ message: `${blockCount} consecutive lines of commented-out code`,
416
+ severity: "warning",
417
+ ruleId: "slop/commented-code",
418
+ });
419
+ }
420
+
421
+ return findings;
422
+ }
423
+
424
+ // ─── Main Entry Point ─────────────────────────────────────────────────────
425
+
426
+ /**
427
+ * Run slop detection on the given files.
428
+ *
429
+ * Checks for: empty function bodies, hallucinated imports, console.log,
430
+ * bare TODOs missing ticket references, and commented-out code blocks.
431
+ *
432
+ * Results are cached by file content hash when a CacheManager is provided.
433
+ */
434
+ export async function detectSlop(
435
+ files: string[],
436
+ options?: {
437
+ cache?: CacheManager;
438
+ cwd?: string;
439
+ },
440
+ ): Promise<SlopResult> {
441
+ const cwd = options?.cwd ?? process.cwd();
442
+ const cache = options?.cache;
443
+
444
+ const allFindings: Finding[] = [];
445
+ let allCached = files.length > 0;
446
+
447
+ for (const file of files) {
448
+ const filePath = isAbsolute(file) ? file : resolve(cwd, file);
449
+ let content: string;
450
+ try {
451
+ content = await Bun.file(filePath).text();
452
+ } catch {
453
+ // File doesn't exist or can't be read — skip
454
+ allCached = false;
455
+ continue;
456
+ }
457
+
458
+ const hash = hashContent(content);
459
+ const key = cacheKey(hash);
460
+
461
+ // Check cache
462
+ if (cache) {
463
+ const cached = cache.get(key);
464
+ if (cached) {
465
+ const cachedFindings: Finding[] = JSON.parse(cached.value);
466
+ allFindings.push(...cachedFindings);
467
+ continue;
468
+ }
469
+ }
470
+
471
+ // Not cached — run all detectors
472
+ allCached = false;
473
+ const fileFindings: Finding[] = [
474
+ ...detectEmptyBodies(content, file),
475
+ ...detectHallucinatedImports(content, file, cwd),
476
+ ...detectConsoleLogs(content, file),
477
+ ...detectTodosWithoutTickets(content, file),
478
+ ...detectCommentedCode(content, file),
479
+ ];
480
+
481
+ // Store in cache
482
+ if (cache) {
483
+ cache.set(key, JSON.stringify(fileFindings));
484
+ }
485
+
486
+ allFindings.push(...fileFindings);
487
+ }
488
+
489
+ return {
490
+ findings: allFindings,
491
+ cached: allCached,
492
+ };
493
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * SonarQube Integration for the Verify Engine.
3
+ *
4
+ * Runs sonar-scanner and parses the JSON report into unified Findings.
5
+ * Gracefully skips if sonar-scanner is not installed.
6
+ */
7
+
8
+ import { isToolAvailable } from "./detect";
9
+ import type { Finding } from "./diff-filter";
10
+
11
+ // ─── Types ────────────────────────────────────────────────────────────────
12
+
13
+ export interface SonarOptions {
14
+ cwd?: string;
15
+ /** Pre-resolved availability — skips redundant detection if provided. */
16
+ available?: boolean;
17
+ }
18
+
19
+ export interface SonarResult {
20
+ findings: Finding[];
21
+ skipped: boolean;
22
+ }
23
+
24
+ // ─── JSON Parsing ─────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Map SonarQube severity to unified severity.
28
+ */
29
+ function mapSonarSeverity(severity: string): "error" | "warning" | "info" {
30
+ switch (severity.toUpperCase()) {
31
+ case "BLOCKER":
32
+ case "CRITICAL":
33
+ return "error";
34
+ case "MAJOR":
35
+ case "MINOR":
36
+ return "warning";
37
+ case "INFO":
38
+ return "info";
39
+ default:
40
+ return "warning";
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Parse SonarQube JSON report into Finding[].
46
+ *
47
+ * Expected format:
48
+ * ```json
49
+ * {
50
+ * "issues": [{
51
+ * "rule": "typescript:S1854",
52
+ * "severity": "MAJOR",
53
+ * "component": "src/app.ts",
54
+ * "line": 42,
55
+ * "message": "Description of the issue"
56
+ * }]
57
+ * }
58
+ * ```
59
+ *
60
+ * Handles malformed JSON and unexpected structures gracefully.
61
+ */
62
+ export function parseSonarReport(json: string): Finding[] {
63
+ let parsed: Record<string, unknown>;
64
+ try {
65
+ parsed = JSON.parse(json) as Record<string, unknown>;
66
+ } catch {
67
+ return [];
68
+ }
69
+
70
+ const issues = parsed.issues;
71
+ if (!Array.isArray(issues)) {
72
+ return [];
73
+ }
74
+
75
+ const findings: Finding[] = [];
76
+
77
+ for (const issue of issues) {
78
+ const i = issue as Record<string, unknown>;
79
+ const rule = (i.rule as string) ?? undefined;
80
+ const severity = (i.severity as string) ?? "MAJOR";
81
+ const component = (i.component as string) ?? "";
82
+ const line = (i.line as number) ?? 0;
83
+ const message = (i.message as string) ?? "";
84
+
85
+ findings.push({
86
+ tool: "sonarqube",
87
+ file: component,
88
+ line,
89
+ message,
90
+ severity: mapSonarSeverity(severity),
91
+ ruleId: rule,
92
+ });
93
+ }
94
+
95
+ return findings;
96
+ }
97
+
98
+ // ─── Runner ───────────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Run SonarQube scanner and return parsed findings.
102
+ *
103
+ * If sonar-scanner is not installed, returns `{ findings: [], skipped: true }`.
104
+ * If sonar-scanner fails, returns `{ findings: [], skipped: false }`.
105
+ */
106
+ export async function runSonar(options?: SonarOptions): Promise<SonarResult> {
107
+ const toolAvailable =
108
+ options?.available ?? (await isToolAvailable("sonarqube"));
109
+ if (!toolAvailable) {
110
+ return { findings: [], skipped: true };
111
+ }
112
+
113
+ const cwd = options?.cwd ?? process.cwd();
114
+
115
+ const args = [
116
+ "sonar-scanner",
117
+ "-Dsonar.analysis.mode=issues",
118
+ "-Dsonar.report.export.path=sonar-report.json",
119
+ ];
120
+
121
+ try {
122
+ const proc = Bun.spawn(args, {
123
+ cwd,
124
+ stdout: "pipe",
125
+ stderr: "pipe",
126
+ });
127
+
128
+ await new Response(proc.stdout).text();
129
+ await new Response(proc.stderr).text();
130
+ await proc.exited;
131
+
132
+ // Read the generated report file
133
+ const reportPath = `${cwd}/.scannerwork/sonar-report.json`;
134
+ const reportFile = Bun.file(reportPath);
135
+ const exists = await reportFile.exists();
136
+ if (!exists) {
137
+ return { findings: [], skipped: false };
138
+ }
139
+
140
+ const reportJson = await reportFile.text();
141
+ const findings = parseSonarReport(reportJson);
142
+ return { findings, skipped: false };
143
+ } catch {
144
+ return { findings: [], skipped: false };
145
+ }
146
+ }