@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,366 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getProfile } from "../../language/profile";
6
+ import {
7
+ detectCommentedCode,
8
+ detectConsoleLogs,
9
+ detectEmptyBodies,
10
+ detectHallucinatedImports,
11
+ detectSlop,
12
+ detectTodosWithoutTickets,
13
+ } from "../slop";
14
+
15
+ // ─── Fixtures ──────────────────────────────────────────────────────────────
16
+
17
+ const TMP_DIR = join(tmpdir(), `maina-slop-test-${Date.now()}`);
18
+
19
+ beforeAll(() => {
20
+ mkdirSync(TMP_DIR, { recursive: true });
21
+ });
22
+
23
+ afterAll(() => {
24
+ rmSync(TMP_DIR, { recursive: true, force: true });
25
+ });
26
+
27
+ function writeFixture(name: string, content: string): string {
28
+ const filePath = join(TMP_DIR, name);
29
+ writeFileSync(filePath, content, "utf-8");
30
+ return filePath;
31
+ }
32
+
33
+ // ─── Empty Bodies ────────────────────────────────────────────────────────────
34
+
35
+ describe("SlopDetector", () => {
36
+ describe("detectEmptyBodies", () => {
37
+ it("should detect empty function bodies via AST", () => {
38
+ const content = `function doNothing() {}
39
+
40
+ function hasBody() {
41
+ return 42;
42
+ }
43
+
44
+ const arrow = () => {};
45
+
46
+ class Foo {
47
+ method() {}
48
+ }`;
49
+ const findings = detectEmptyBodies(content, "src/foo.ts");
50
+ expect(findings.length).toBe(3);
51
+ expect(findings.every((f) => f.ruleId === "slop/empty-body")).toBe(true);
52
+ expect(findings.every((f) => f.tool === "slop")).toBe(true);
53
+ expect(findings.every((f) => f.severity === "warning")).toBe(true);
54
+ });
55
+
56
+ it("should not flag function bodies with comments", () => {
57
+ const content = `function placeholder() {
58
+ // TODO(#123): implement later
59
+ }`;
60
+ const findings = detectEmptyBodies(content, "src/foo.ts");
61
+ expect(findings.length).toBe(0);
62
+ });
63
+
64
+ it("should not flag empty object literals or arrays", () => {
65
+ const content = `const obj = {};
66
+ const arr: string[] = [];
67
+ const map = new Map();`;
68
+ const findings = detectEmptyBodies(content, "src/foo.ts");
69
+ expect(findings.length).toBe(0);
70
+ });
71
+ });
72
+
73
+ // ─── Hallucinated Imports ────────────────────────────────────────────────
74
+
75
+ describe("detectHallucinatedImports", () => {
76
+ it("should detect hallucinated imports", () => {
77
+ const content = `import { foo } from "./nonexistent-module";
78
+ import { bar } from "../does-not-exist";`;
79
+ const findings = detectHallucinatedImports(
80
+ content,
81
+ join(TMP_DIR, "test.ts"),
82
+ TMP_DIR,
83
+ );
84
+ expect(findings.length).toBe(2);
85
+ expect(
86
+ findings.every((f) => f.ruleId === "slop/hallucinated-import"),
87
+ ).toBe(true);
88
+ expect(findings.every((f) => f.severity === "error")).toBe(true);
89
+ });
90
+
91
+ it("should not flag node_modules imports", () => {
92
+ const content = `import { describe } from "bun:test";
93
+ import path from "node:path";
94
+ import React from "react";
95
+ import { z } from "zod";`;
96
+ const findings = detectHallucinatedImports(
97
+ content,
98
+ join(TMP_DIR, "test.ts"),
99
+ TMP_DIR,
100
+ );
101
+ expect(findings.length).toBe(0);
102
+ });
103
+
104
+ it("should not flag existing relative imports", () => {
105
+ // Create the imported file
106
+ writeFixture("real-module.ts", "export const x = 1;\n");
107
+ const content = `import { x } from "./real-module";`;
108
+ const findings = detectHallucinatedImports(
109
+ content,
110
+ join(TMP_DIR, "importer.ts"),
111
+ TMP_DIR,
112
+ );
113
+ expect(findings.length).toBe(0);
114
+ });
115
+ });
116
+
117
+ // ─── Console Logs ────────────────────────────────────────────────────────
118
+
119
+ describe("detectConsoleLogs", () => {
120
+ it("should detect console.log in production code", () => {
121
+ const content = `function greet(name: string): void {
122
+ console.log("Hello", name);
123
+ console.warn("deprecated");
124
+ console.error("something broke");
125
+ console.debug("trace info");
126
+ console.info("status update");
127
+ }`;
128
+ const findings = detectConsoleLogs(content, "src/app.ts");
129
+ expect(findings.length).toBe(5);
130
+ expect(findings.every((f) => f.ruleId === "slop/console-log")).toBe(true);
131
+ expect(findings.every((f) => f.severity === "warning")).toBe(true);
132
+ });
133
+
134
+ it("should not flag console.log in test files", () => {
135
+ const content = `console.log("debugging test");`;
136
+ const findingsTest = detectConsoleLogs(content, "src/app.test.ts");
137
+ const findingsSpec = detectConsoleLogs(content, "src/app.spec.ts");
138
+ expect(findingsTest.length).toBe(0);
139
+ expect(findingsSpec.length).toBe(0);
140
+ });
141
+
142
+ it("should not flag files without console usage", () => {
143
+ const content = `function add(a: number, b: number): number {
144
+ return a + b;
145
+ }`;
146
+ const findings = detectConsoleLogs(content, "src/math.ts");
147
+ expect(findings.length).toBe(0);
148
+ });
149
+ });
150
+
151
+ // ─── Bare TODOs missing ticket ──────────────────────────────────────────
152
+
153
+ describe("detectTodosWithoutTickets", () => {
154
+ it("should detect TODO without ticket reference", () => {
155
+ const content = `// TODO: fix this later
156
+ /* TODO implement error handling */
157
+ // TODO add caching`;
158
+ const findings = detectTodosWithoutTickets(content, "src/app.ts");
159
+ expect(findings.length).toBe(3);
160
+ expect(
161
+ findings.every((f) => f.ruleId === "slop/todo-without-ticket"),
162
+ ).toBe(true);
163
+ expect(findings.every((f) => f.severity === "info")).toBe(true);
164
+ });
165
+
166
+ it("should not flag TODO with ticket references", () => {
167
+ const content = `// TODO(#123): fix this later
168
+ /* TODO PROJ-456: implement error handling */
169
+ // TODO [#789] add caching
170
+ // TODO(MAINA-42): refactor`;
171
+ const findings = detectTodosWithoutTickets(content, "src/app.ts");
172
+ expect(findings.length).toBe(0);
173
+ });
174
+ });
175
+
176
+ // ─── Commented-out code ──────────────────────────────────────────────────
177
+
178
+ describe("detectCommentedCode", () => {
179
+ it("should detect commented-out code blocks > 3 lines", () => {
180
+ const content = `function active() {
181
+ return 1;
182
+ }
183
+
184
+ // const old = require("old-module");
185
+ // function deprecated() {
186
+ // return old.doStuff();
187
+ // }
188
+
189
+ function alsoActive() {
190
+ return 2;
191
+ }`;
192
+ const findings = detectCommentedCode(content, "src/app.ts");
193
+ expect(findings.length).toBe(1);
194
+ expect(findings[0]?.ruleId).toBe("slop/commented-code");
195
+ expect(findings[0]?.severity).toBe("warning");
196
+ });
197
+
198
+ it("should not flag short comment blocks", () => {
199
+ const content = `// This is a normal comment
200
+ // that spans two lines
201
+ function foo() {
202
+ return 1;
203
+ }`;
204
+ const findings = detectCommentedCode(content, "src/app.ts");
205
+ expect(findings.length).toBe(0);
206
+ });
207
+
208
+ it("should not flag documentation comments", () => {
209
+ const content = `/**
210
+ * This function does something important.
211
+ * It takes a number and returns it doubled.
212
+ * @param n - the number to double
213
+ * @returns the doubled number
214
+ */
215
+ function double(n: number): number {
216
+ return n * 2;
217
+ }`;
218
+ const findings = detectCommentedCode(content, "src/app.ts");
219
+ expect(findings.length).toBe(0);
220
+ });
221
+ });
222
+
223
+ // ─── Cache integration ───────────────────────────────────────────────────
224
+
225
+ describe("cache integration", () => {
226
+ it("should cache results for unchanged files", async () => {
227
+ const filePath = writeFixture(
228
+ "cached.ts",
229
+ `function empty() {}\nconsole.log("hello");\n`,
230
+ );
231
+
232
+ // Create a mock cache manager
233
+ const store = new Map<string, { value: string }>();
234
+ const mockCache = {
235
+ get(key: string) {
236
+ const entry = store.get(key);
237
+ if (!entry) return null;
238
+ return {
239
+ key,
240
+ value: entry.value,
241
+ createdAt: Date.now(),
242
+ ttl: 0,
243
+ };
244
+ },
245
+ set(key: string, value: string) {
246
+ store.set(key, { value });
247
+ },
248
+ has(key: string) {
249
+ return store.has(key);
250
+ },
251
+ invalidate(key: string) {
252
+ store.delete(key);
253
+ },
254
+ clear() {
255
+ store.clear();
256
+ },
257
+ stats() {
258
+ return {
259
+ l1Hits: 0,
260
+ l2Hits: 0,
261
+ misses: 0,
262
+ totalQueries: 0,
263
+ entriesL1: 0,
264
+ entriesL2: 0,
265
+ };
266
+ },
267
+ };
268
+
269
+ // First call — should not be cached
270
+ const result1 = await detectSlop([filePath], {
271
+ cache: mockCache,
272
+ cwd: TMP_DIR,
273
+ });
274
+ expect(result1.cached).toBe(false);
275
+ expect(result1.findings.length).toBeGreaterThan(0);
276
+
277
+ // Second call — same file, should be cached
278
+ const result2 = await detectSlop([filePath], {
279
+ cache: mockCache,
280
+ cwd: TMP_DIR,
281
+ });
282
+ expect(result2.cached).toBe(true);
283
+ expect(result2.findings.length).toBe(result1.findings.length);
284
+ });
285
+ });
286
+
287
+ // ─── Integration: detectSlop ─────────────────────────────────────────────
288
+
289
+ describe("detectSlop integration", () => {
290
+ it("should detect console.log in a file", async () => {
291
+ const filePath = writeFixture(
292
+ "with-console.ts",
293
+ `export function greet(): void {\n\tconsole.log("hello");\n}\n`,
294
+ );
295
+ const result = await detectSlop([filePath], { cwd: TMP_DIR });
296
+ expect(result.findings.length).toBeGreaterThan(0);
297
+ expect(result.findings.some((f) => f.ruleId === "slop/console-log")).toBe(
298
+ true,
299
+ );
300
+ });
301
+
302
+ it("should return clean for a file without slop", async () => {
303
+ const filePath = writeFixture(
304
+ "clean.ts",
305
+ `export function add(a: number, b: number): number {\n\treturn a + b;\n}\n`,
306
+ );
307
+ const result = await detectSlop([filePath], { cwd: TMP_DIR });
308
+ expect(result.findings.length).toBe(0);
309
+ });
310
+ });
311
+
312
+ // ─── Language-aware slop detection ──────────────────────────────────────
313
+
314
+ describe("language-aware slop detection", () => {
315
+ it("should detect print() in Python files", () => {
316
+ const findings = detectConsoleLogs(
317
+ "x = 1\nprint('debug')\ny = 2",
318
+ "app.py",
319
+ getProfile("python"),
320
+ );
321
+ expect(findings).toHaveLength(1);
322
+ expect(findings[0]?.ruleId).toBe("slop/console-log");
323
+ });
324
+
325
+ it("should detect fmt.Println in Go files", () => {
326
+ const findings = detectConsoleLogs(
327
+ "package main\nfmt.Println(x)\n",
328
+ "main.go",
329
+ getProfile("go"),
330
+ );
331
+ expect(findings).toHaveLength(1);
332
+ });
333
+
334
+ it("should detect println! in Rust files", () => {
335
+ const findings = detectConsoleLogs(
336
+ 'fn main() {\n println!("debug");\n}',
337
+ "main.rs",
338
+ getProfile("rust"),
339
+ );
340
+ expect(findings).toHaveLength(1);
341
+ });
342
+
343
+ it("should skip Python test files", () => {
344
+ const findings = detectConsoleLogs(
345
+ "print('ok')",
346
+ "test_app.py",
347
+ getProfile("python"),
348
+ );
349
+ expect(findings).toHaveLength(0);
350
+ });
351
+
352
+ it("should skip Go test files", () => {
353
+ const findings = detectConsoleLogs(
354
+ "fmt.Println(x)",
355
+ "app_test.go",
356
+ getProfile("go"),
357
+ );
358
+ expect(findings).toHaveLength(0);
359
+ });
360
+
361
+ it("should still work without profile (backward compatible)", () => {
362
+ const findings = detectConsoleLogs("console.log('test')", "app.ts");
363
+ expect(findings).toHaveLength(1);
364
+ });
365
+ });
366
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseSonarReport, runSonar } from "../sonar";
3
+
4
+ describe("SonarQube Integration", () => {
5
+ describe("parseSonarReport", () => {
6
+ it("should parse SonarQube JSON issues into findings", () => {
7
+ const json = JSON.stringify({
8
+ issues: [
9
+ {
10
+ rule: "typescript:S1854",
11
+ severity: "MAJOR",
12
+ component: "src/app.ts",
13
+ line: 42,
14
+ message: 'Remove this useless assignment to local variable "x".',
15
+ },
16
+ {
17
+ rule: "typescript:S3776",
18
+ severity: "CRITICAL",
19
+ component: "src/utils.ts",
20
+ line: 10,
21
+ message:
22
+ "Refactor this function to reduce its Cognitive Complexity.",
23
+ },
24
+ ],
25
+ });
26
+
27
+ const findings = parseSonarReport(json);
28
+
29
+ expect(findings).toHaveLength(2);
30
+ expect(findings[0]?.tool).toBe("sonarqube");
31
+ expect(findings[0]?.file).toBe("src/app.ts");
32
+ expect(findings[0]?.line).toBe(42);
33
+ expect(findings[0]?.severity).toBe("warning");
34
+ expect(findings[0]?.ruleId).toBe("typescript:S1854");
35
+ expect(findings[1]?.severity).toBe("error");
36
+ });
37
+
38
+ it("should handle empty issues array", () => {
39
+ const json = JSON.stringify({ issues: [] });
40
+ expect(parseSonarReport(json)).toHaveLength(0);
41
+ });
42
+
43
+ it("should handle malformed JSON", () => {
44
+ expect(parseSonarReport("not json")).toHaveLength(0);
45
+ });
46
+
47
+ it("should handle missing fields gracefully", () => {
48
+ const json = JSON.stringify({
49
+ issues: [{ rule: "test:rule" }],
50
+ });
51
+ const findings = parseSonarReport(json);
52
+ expect(findings).toHaveLength(1);
53
+ expect(findings[0]?.file).toBe("");
54
+ expect(findings[0]?.line).toBe(0);
55
+ });
56
+
57
+ it("should map SonarQube severities correctly", () => {
58
+ const json = JSON.stringify({
59
+ issues: [
60
+ {
61
+ rule: "r1",
62
+ severity: "BLOCKER",
63
+ component: "a.ts",
64
+ line: 1,
65
+ message: "blocker",
66
+ },
67
+ {
68
+ rule: "r2",
69
+ severity: "CRITICAL",
70
+ component: "a.ts",
71
+ line: 2,
72
+ message: "critical",
73
+ },
74
+ {
75
+ rule: "r3",
76
+ severity: "MAJOR",
77
+ component: "a.ts",
78
+ line: 3,
79
+ message: "major",
80
+ },
81
+ {
82
+ rule: "r4",
83
+ severity: "MINOR",
84
+ component: "a.ts",
85
+ line: 4,
86
+ message: "minor",
87
+ },
88
+ {
89
+ rule: "r5",
90
+ severity: "INFO",
91
+ component: "a.ts",
92
+ line: 5,
93
+ message: "info",
94
+ },
95
+ ],
96
+ });
97
+ const findings = parseSonarReport(json);
98
+ expect(findings[0]?.severity).toBe("error"); // BLOCKER
99
+ expect(findings[1]?.severity).toBe("error"); // CRITICAL
100
+ expect(findings[2]?.severity).toBe("warning"); // MAJOR
101
+ expect(findings[3]?.severity).toBe("warning"); // MINOR
102
+ expect(findings[4]?.severity).toBe("info"); // INFO
103
+ });
104
+ });
105
+
106
+ describe("runSonar", () => {
107
+ it("should skip when sonarqube is not available", async () => {
108
+ const result = await runSonar({ available: false });
109
+ expect(result.skipped).toBe(true);
110
+ expect(result.findings).toHaveLength(0);
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,227 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getProfile } from "../../language/profile";
6
+ import { parseBiomeOutput, syntaxGuard } from "../syntax-guard";
7
+
8
+ // ─── Fixtures ──────────────────────────────────────────────────────────────
9
+
10
+ const TMP_DIR = join(tmpdir(), `maina-syntax-guard-test-${Date.now()}`);
11
+
12
+ const VALID_TS = `export const greeting = "hello";\n`;
13
+
14
+ const INVALID_TS_MISSING_BRACKET = `export function broken( {
15
+ return 1;
16
+ }\n`;
17
+
18
+ const INVALID_TS_PARSE_ERROR = `export function bad(): number {
19
+ const x = [1, 2, 3
20
+ return x.length;
21
+ }\n`;
22
+
23
+ beforeAll(() => {
24
+ mkdirSync(TMP_DIR, { recursive: true });
25
+ });
26
+
27
+ afterAll(() => {
28
+ rmSync(TMP_DIR, { recursive: true, force: true });
29
+ });
30
+
31
+ function writeFixture(name: string, content: string): string {
32
+ const filePath = join(TMP_DIR, name);
33
+ writeFileSync(filePath, content, "utf-8");
34
+ return filePath;
35
+ }
36
+
37
+ // ─── SyntaxGuard ───────────────────────────────────────────────────────────
38
+
39
+ describe("SyntaxGuard", () => {
40
+ it("should pass valid TypeScript files", async () => {
41
+ const file = writeFixture("valid.ts", VALID_TS);
42
+ const result = await syntaxGuard([file]);
43
+ expect(result.ok).toBe(true);
44
+ });
45
+
46
+ it("should reject files with syntax errors", async () => {
47
+ const file = writeFixture("broken.ts", INVALID_TS_MISSING_BRACKET);
48
+ const result = await syntaxGuard([file]);
49
+ expect(result.ok).toBe(false);
50
+ if (!result.ok) {
51
+ expect(result.error.length).toBeGreaterThan(0);
52
+ const hasError = result.error.some((e) => e.severity === "error");
53
+ expect(hasError).toBe(true);
54
+ }
55
+ });
56
+
57
+ it("should complete in < 500ms for 10 files", async () => {
58
+ const files: string[] = [];
59
+ for (let i = 0; i < 10; i++) {
60
+ files.push(
61
+ writeFixture(`perf_${i}.ts`, `export const val${i} = ${i};\n`),
62
+ );
63
+ }
64
+ const start = performance.now();
65
+ const result = await syntaxGuard(files);
66
+ const elapsed = performance.now() - start;
67
+ expect(result.ok).toBe(true);
68
+ expect(elapsed).toBeLessThan(500);
69
+ });
70
+
71
+ it("should return structured error with file + line + message", async () => {
72
+ const file = writeFixture("structured.ts", INVALID_TS_PARSE_ERROR);
73
+ const result = await syntaxGuard([file]);
74
+ expect(result.ok).toBe(false);
75
+ if (!result.ok) {
76
+ expect(result.error.length).toBeGreaterThan(0);
77
+ const first = result.error[0] as NonNullable<(typeof result.error)[0]>;
78
+ expect(typeof first.file).toBe("string");
79
+ expect(first.file).toContain("structured.ts");
80
+ expect(typeof first.line).toBe("number");
81
+ expect(first.line).toBeGreaterThan(0);
82
+ expect(typeof first.column).toBe("number");
83
+ expect(typeof first.message).toBe("string");
84
+ expect(first.message.length).toBeGreaterThan(0);
85
+ expect(["error", "warning"]).toContain(first.severity);
86
+ }
87
+ });
88
+
89
+ it("should return Ok immediately for empty file list", async () => {
90
+ const result = await syntaxGuard([]);
91
+ expect(result.ok).toBe(true);
92
+ });
93
+
94
+ it("should include warnings in output but only reject on errors", async () => {
95
+ // A file with only a warning (unused variable) but no parse errors
96
+ const file = writeFixture(
97
+ "warn_only.ts",
98
+ `const used = 1;\nexport const result = used + 1;\n`,
99
+ );
100
+ const result = await syntaxGuard([file]);
101
+ // This should pass because there are no errors (only possible warnings)
102
+ expect(result.ok).toBe(true);
103
+ });
104
+ });
105
+
106
+ // ─── syntaxGuard with language profile ────────────────────────────────────
107
+
108
+ describe("syntaxGuard with language profile", () => {
109
+ it("should accept a language profile parameter", async () => {
110
+ const profile = getProfile("typescript");
111
+ const result = await syntaxGuard([], undefined, profile);
112
+ expect(result.ok).toBe(true);
113
+ });
114
+
115
+ it("should use biome for typescript profile (default behavior)", async () => {
116
+ const result = await syntaxGuard(["nonexistent.ts"]);
117
+ expect(result).toBeDefined();
118
+ });
119
+
120
+ it("should attempt ruff for python profile", async () => {
121
+ const profile = getProfile("python");
122
+ // ruff likely not installed — should fail gracefully
123
+ const result = await syntaxGuard(["test.py"], undefined, profile);
124
+ expect(result).toBeDefined();
125
+ // Either ok (if ruff found nothing) or error (if ruff not installed)
126
+ });
127
+
128
+ it("should attempt go vet for go profile", async () => {
129
+ const profile = getProfile("go");
130
+ const result = await syntaxGuard(["test.go"], undefined, profile);
131
+ expect(result).toBeDefined();
132
+ });
133
+
134
+ it("should attempt clippy for rust profile", async () => {
135
+ const profile = getProfile("rust");
136
+ const result = await syntaxGuard(["test.rs"], undefined, profile);
137
+ expect(result).toBeDefined();
138
+ });
139
+ });
140
+
141
+ // ─── parseBiomeOutput ──────────────────────────────────────────────────────
142
+
143
+ describe("parseBiomeOutput", () => {
144
+ it("should return empty array for empty diagnostics", () => {
145
+ const json = JSON.stringify({
146
+ summary: { errors: 0, warnings: 0 },
147
+ diagnostics: [],
148
+ command: "check",
149
+ });
150
+ const errors = parseBiomeOutput(json);
151
+ expect(errors).toEqual([]);
152
+ });
153
+
154
+ it("should parse diagnostics from biome JSON output", () => {
155
+ const json = JSON.stringify({
156
+ summary: { errors: 1, warnings: 0 },
157
+ diagnostics: [
158
+ {
159
+ severity: "error",
160
+ message: "expected `:` but instead found `x`",
161
+ category: "parse",
162
+ location: {
163
+ path: "/tmp/test.ts",
164
+ start: { line: 3, column: 10 },
165
+ end: { line: 3, column: 11 },
166
+ },
167
+ advices: [],
168
+ },
169
+ ],
170
+ command: "check",
171
+ });
172
+
173
+ const errors = parseBiomeOutput(json);
174
+ expect(errors.length).toBe(1);
175
+ const first = errors[0] as NonNullable<(typeof errors)[0]>;
176
+ expect(first.file).toBe("/tmp/test.ts");
177
+ expect(first.line).toBe(3);
178
+ expect(first.column).toBe(10);
179
+ expect(first.message).toContain("expected `:`");
180
+ expect(first.severity).toBe("error");
181
+ });
182
+
183
+ it("should handle multiple diagnostics", () => {
184
+ const json = JSON.stringify({
185
+ summary: { errors: 2, warnings: 1 },
186
+ diagnostics: [
187
+ {
188
+ severity: "warning",
189
+ message: "Unused variable",
190
+ category: "lint/correctness/noUnusedVariables",
191
+ location: {
192
+ path: "/tmp/a.ts",
193
+ start: { line: 1, column: 5 },
194
+ end: { line: 1, column: 6 },
195
+ },
196
+ advices: [],
197
+ },
198
+ {
199
+ severity: "error",
200
+ message: "Parse error",
201
+ category: "parse",
202
+ location: {
203
+ path: "/tmp/a.ts",
204
+ start: { line: 5, column: 1 },
205
+ end: { line: 5, column: 1 },
206
+ },
207
+ advices: [],
208
+ },
209
+ ],
210
+ command: "check",
211
+ });
212
+
213
+ const errors = parseBiomeOutput(json);
214
+ expect(errors.length).toBe(2);
215
+ expect((errors[0] as NonNullable<(typeof errors)[0]>).severity).toBe(
216
+ "warning",
217
+ );
218
+ expect((errors[1] as NonNullable<(typeof errors)[0]>).severity).toBe(
219
+ "error",
220
+ );
221
+ });
222
+
223
+ it("should return empty array for invalid JSON", () => {
224
+ const errors = parseBiomeOutput("not json at all");
225
+ expect(errors).toEqual([]);
226
+ });
227
+ });