@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,191 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseTrivyJson, runTrivy } from "../trivy";
3
+
4
+ // ─── parseTrivyJson ────────────────────────────────────────────────────────
5
+
6
+ describe("parseTrivyJson", () => {
7
+ it("should return empty array for empty results", () => {
8
+ const json = JSON.stringify({ Results: [] });
9
+ const findings = parseTrivyJson(json);
10
+ expect(findings).toEqual([]);
11
+ });
12
+
13
+ it("should parse vulnerabilities from Trivy JSON output", () => {
14
+ const json = JSON.stringify({
15
+ Results: [
16
+ {
17
+ Target: "package-lock.json",
18
+ Type: "npm",
19
+ Vulnerabilities: [
20
+ {
21
+ VulnerabilityID: "CVE-2023-12345",
22
+ PkgName: "lodash",
23
+ InstalledVersion: "4.17.20",
24
+ FixedVersion: "4.17.21",
25
+ Severity: "HIGH",
26
+ Title: "Prototype Pollution in lodash",
27
+ Description: "lodash before 4.17.21 allows prototype pollution.",
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ });
33
+
34
+ const findings = parseTrivyJson(json);
35
+ expect(findings.length).toBe(1);
36
+ expect(findings[0]?.tool).toBe("trivy");
37
+ expect(findings[0]?.file).toBe("package-lock.json");
38
+ expect(findings[0]?.line).toBe(0);
39
+ expect(findings[0]?.severity).toBe("error");
40
+ expect(findings[0]?.ruleId).toBe("CVE-2023-12345");
41
+ expect(findings[0]?.message).toContain("lodash");
42
+ expect(findings[0]?.message).toContain("4.17.20");
43
+ });
44
+
45
+ it("should map Trivy severity levels correctly", () => {
46
+ const makeTrivy = (severity: string) =>
47
+ JSON.stringify({
48
+ Results: [
49
+ {
50
+ Target: "package.json",
51
+ Vulnerabilities: [
52
+ {
53
+ VulnerabilityID: "CVE-0000-0000",
54
+ PkgName: "pkg",
55
+ InstalledVersion: "1.0.0",
56
+ Severity: severity,
57
+ Title: "Test",
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ });
63
+
64
+ expect(parseTrivyJson(makeTrivy("CRITICAL"))[0]?.severity).toBe("error");
65
+ expect(parseTrivyJson(makeTrivy("HIGH"))[0]?.severity).toBe("error");
66
+ expect(parseTrivyJson(makeTrivy("MEDIUM"))[0]?.severity).toBe("warning");
67
+ expect(parseTrivyJson(makeTrivy("LOW"))[0]?.severity).toBe("info");
68
+ expect(parseTrivyJson(makeTrivy("UNKNOWN"))[0]?.severity).toBe("info");
69
+ });
70
+
71
+ it("should handle multiple targets with multiple vulnerabilities", () => {
72
+ const json = JSON.stringify({
73
+ Results: [
74
+ {
75
+ Target: "package-lock.json",
76
+ Vulnerabilities: [
77
+ {
78
+ VulnerabilityID: "CVE-2023-001",
79
+ PkgName: "pkg-a",
80
+ InstalledVersion: "1.0.0",
81
+ Severity: "HIGH",
82
+ Title: "Issue A",
83
+ },
84
+ {
85
+ VulnerabilityID: "CVE-2023-002",
86
+ PkgName: "pkg-b",
87
+ InstalledVersion: "2.0.0",
88
+ Severity: "LOW",
89
+ Title: "Issue B",
90
+ },
91
+ ],
92
+ },
93
+ {
94
+ Target: "yarn.lock",
95
+ Vulnerabilities: [
96
+ {
97
+ VulnerabilityID: "CVE-2023-003",
98
+ PkgName: "pkg-c",
99
+ InstalledVersion: "3.0.0",
100
+ Severity: "CRITICAL",
101
+ Title: "Issue C",
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ });
107
+
108
+ const findings = parseTrivyJson(json);
109
+ expect(findings.length).toBe(3);
110
+ expect(findings[0]?.file).toBe("package-lock.json");
111
+ expect(findings[2]?.file).toBe("yarn.lock");
112
+ });
113
+
114
+ it("should handle targets with null vulnerabilities", () => {
115
+ const json = JSON.stringify({
116
+ Results: [
117
+ {
118
+ Target: "package.json",
119
+ Vulnerabilities: null,
120
+ },
121
+ ],
122
+ });
123
+ const findings = parseTrivyJson(json);
124
+ expect(findings).toEqual([]);
125
+ });
126
+
127
+ it("should return empty array for invalid JSON", () => {
128
+ const findings = parseTrivyJson("not valid json {{{");
129
+ expect(findings).toEqual([]);
130
+ });
131
+
132
+ it("should return empty array for malformed structure", () => {
133
+ const findings = parseTrivyJson(JSON.stringify({ unexpected: true }));
134
+ expect(findings).toEqual([]);
135
+ });
136
+
137
+ it("should include fix version in message when available", () => {
138
+ const json = JSON.stringify({
139
+ Results: [
140
+ {
141
+ Target: "package.json",
142
+ Vulnerabilities: [
143
+ {
144
+ VulnerabilityID: "CVE-2023-999",
145
+ PkgName: "express",
146
+ InstalledVersion: "4.17.0",
147
+ FixedVersion: "4.18.0",
148
+ Severity: "HIGH",
149
+ Title: "Security issue",
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ });
155
+
156
+ const findings = parseTrivyJson(json);
157
+ expect(findings[0]?.message).toContain("4.18.0");
158
+ });
159
+ });
160
+
161
+ // ─── runTrivy ──────────────────────────────────────────────────────────────
162
+
163
+ describe("runTrivy", () => {
164
+ it("should skip when trivy is not installed", async () => {
165
+ const result = await runTrivy();
166
+ if (result.skipped) {
167
+ expect(result.findings).toEqual([]);
168
+ expect(result.skipped).toBe(true);
169
+ } else {
170
+ expect(Array.isArray(result.findings)).toBe(true);
171
+ expect(result.skipped).toBe(false);
172
+ }
173
+ });
174
+
175
+ it("should return correct result shape", async () => {
176
+ const result = await runTrivy();
177
+ expect(result).toHaveProperty("findings");
178
+ expect(result).toHaveProperty("skipped");
179
+ expect(Array.isArray(result.findings)).toBe(true);
180
+ expect(typeof result.skipped).toBe("boolean");
181
+ });
182
+
183
+ it("should accept options without crashing", async () => {
184
+ const result = await runTrivy({
185
+ scanType: "fs",
186
+ cwd: "/tmp",
187
+ });
188
+ expect(result).toHaveProperty("findings");
189
+ expect(result).toHaveProperty("skipped");
190
+ });
191
+ });
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { detectWebProject, loadVisualConfig } from "../visual";
5
+
6
+ describe("detectWebProject", () => {
7
+ const testDir = join(import.meta.dir, "__fixtures__/visual-detect");
8
+
9
+ function setup(pkg: Record<string, unknown>) {
10
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
11
+ mkdirSync(testDir, { recursive: true });
12
+ writeFileSync(join(testDir, "package.json"), JSON.stringify(pkg));
13
+ }
14
+
15
+ function cleanup() {
16
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
17
+ }
18
+
19
+ it("should detect Next.js project", () => {
20
+ setup({ scripts: { dev: "next dev" } });
21
+ expect(detectWebProject(testDir)).toBe(true);
22
+ cleanup();
23
+ });
24
+
25
+ it("should detect Astro project", () => {
26
+ setup({ scripts: { dev: "astro dev" } });
27
+ expect(detectWebProject(testDir)).toBe(true);
28
+ cleanup();
29
+ });
30
+
31
+ it("should detect Vite project", () => {
32
+ setup({ scripts: { dev: "vite" } });
33
+ expect(detectWebProject(testDir)).toBe(true);
34
+ cleanup();
35
+ });
36
+
37
+ it("should detect webpack-dev-server", () => {
38
+ setup({ scripts: { start: "webpack serve" } });
39
+ expect(detectWebProject(testDir)).toBe(true);
40
+ cleanup();
41
+ });
42
+
43
+ it("should return false for non-web project", () => {
44
+ setup({ scripts: { start: "node server.js" } });
45
+ expect(detectWebProject(testDir)).toBe(false);
46
+ cleanup();
47
+ });
48
+
49
+ it("should return false when no package.json", () => {
50
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
51
+ mkdirSync(testDir, { recursive: true });
52
+ expect(detectWebProject(testDir)).toBe(false);
53
+ cleanup();
54
+ });
55
+
56
+ it("should detect from devDependencies", () => {
57
+ setup({ devDependencies: { vite: "^5.0.0" } });
58
+ expect(detectWebProject(testDir)).toBe(true);
59
+ cleanup();
60
+ });
61
+ });
62
+
63
+ describe("loadVisualConfig", () => {
64
+ const testDir = join(import.meta.dir, "__fixtures__/visual-config");
65
+
66
+ function cleanup() {
67
+ if (existsSync(testDir)) rmSync(testDir, { recursive: true });
68
+ }
69
+
70
+ it("should return defaults when no config exists", () => {
71
+ cleanup();
72
+ mkdirSync(testDir, { recursive: true });
73
+ const config = loadVisualConfig(testDir);
74
+ expect(config.threshold).toBe(0.001);
75
+ expect(config.viewport.width).toBe(1280);
76
+ expect(config.viewport.height).toBe(720);
77
+ expect(config.urls).toEqual([]);
78
+ cleanup();
79
+ });
80
+
81
+ it("should load from preferences.json", () => {
82
+ cleanup();
83
+ mkdirSync(testDir, { recursive: true });
84
+ writeFileSync(
85
+ join(testDir, "preferences.json"),
86
+ JSON.stringify({
87
+ visual: {
88
+ urls: ["http://localhost:3000"],
89
+ threshold: 0.005,
90
+ viewport: { width: 1920, height: 1080 },
91
+ },
92
+ }),
93
+ );
94
+ const config = loadVisualConfig(testDir);
95
+ expect(config.urls).toEqual(["http://localhost:3000"]);
96
+ expect(config.threshold).toBe(0.005);
97
+ expect(config.viewport.width).toBe(1920);
98
+ cleanup();
99
+ });
100
+ });
101
+
102
+ describe("captureScreenshot", () => {
103
+ it("should return skipped when playwright is not available", async () => {
104
+ const { captureScreenshot } = await import("../visual");
105
+ const result = await captureScreenshot(
106
+ "http://localhost:9999",
107
+ "/tmp/test-screenshot.png",
108
+ { available: false },
109
+ );
110
+ expect(result.captured).toBe(false);
111
+ expect(result.skipped).toBe(true);
112
+ });
113
+ });
114
+
115
+ describe("compareImages", () => {
116
+ it("should return 0 diff for identical images", async () => {
117
+ const { compareImages } = await import("../visual");
118
+ // Create two identical 2x2 white RGBA buffers
119
+ const img = Buffer.alloc(2 * 2 * 4, 255);
120
+ const result = compareImages(img, img, 2, 2);
121
+ expect(result.diffPixels).toBe(0);
122
+ expect(result.diffPercentage).toBe(0);
123
+ });
124
+
125
+ it("should detect difference between different images", async () => {
126
+ const { compareImages } = await import("../visual");
127
+ // 2x2 white image
128
+ const white = Buffer.alloc(2 * 2 * 4, 255);
129
+ // 2x2 black image
130
+ const black = Buffer.alloc(2 * 2 * 4, 0);
131
+ // Set alpha to 255 for black image
132
+ for (let i = 3; i < black.length; i += 4) {
133
+ black[i] = 255;
134
+ }
135
+ const result = compareImages(white, black, 2, 2);
136
+ expect(result.diffPixels).toBeGreaterThan(0);
137
+ expect(result.diffPercentage).toBeGreaterThan(0);
138
+ });
139
+ });
@@ -0,0 +1,276 @@
1
+ /**
2
+ * AI Review — semantic code review using LLM.
3
+ *
4
+ * Two tiers:
5
+ * - mechanical (always-on): diff + referenced functions, <3s, warnings only
6
+ * - standard (--deep): adds spec/plan context, can emit errors
7
+ */
8
+
9
+ import { tryAIGenerate } from "../ai/try-generate";
10
+ import { buildCacheKey, hashContent } from "../cache/keys";
11
+ import { createCacheManager } from "../cache/manager";
12
+ import type { Finding } from "./diff-filter";
13
+
14
+ // ─── Types ────────────────────────────────────────────────────────────────
15
+
16
+ export interface ReferencedFunction {
17
+ name: string;
18
+ filePath: string;
19
+ body: string;
20
+ }
21
+
22
+ export interface EntityWithBody {
23
+ name: string;
24
+ kind: string;
25
+ startLine: number;
26
+ endLine: number;
27
+ filePath: string;
28
+ body: string;
29
+ }
30
+
31
+ export interface AIReviewOptions {
32
+ diff: string;
33
+ entities: EntityWithBody[];
34
+ deep?: boolean;
35
+ specContext?: string;
36
+ planContext?: string;
37
+ mainaDir: string;
38
+ }
39
+
40
+ export interface AIReviewResult {
41
+ findings: Finding[];
42
+ skipped: boolean;
43
+ tier: "mechanical" | "standard";
44
+ duration: number;
45
+ }
46
+
47
+ const MAX_REFS_PER_FILE = 3;
48
+
49
+ // ─── Referenced Function Resolution ───────────────────────────────────────
50
+
51
+ /**
52
+ * Extract function/method names called in added lines of a diff,
53
+ * then match them against known entities to get their bodies.
54
+ * Capped at MAX_REFS_PER_FILE (3) to bound token usage.
55
+ */
56
+ export function resolveReferencedFunctions(
57
+ diff: string,
58
+ entities: EntityWithBody[],
59
+ ): ReferencedFunction[] {
60
+ // Extract added lines from diff
61
+ const addedLines = diff
62
+ .split("\n")
63
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
64
+ .join("\n");
65
+
66
+ if (!addedLines.trim()) return [];
67
+
68
+ // Extract identifier-like tokens that could be function calls
69
+ // Match word( pattern — likely a function call
70
+ const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
71
+ const calledNames = new Set<string>();
72
+ for (const match of addedLines.matchAll(callPattern)) {
73
+ if (match[1]) calledNames.add(match[1]);
74
+ }
75
+
76
+ // Remove common keywords that match the pattern
77
+ const KEYWORDS = new Set([
78
+ "if",
79
+ "for",
80
+ "while",
81
+ "switch",
82
+ "catch",
83
+ "function",
84
+ "return",
85
+ "new",
86
+ "typeof",
87
+ "instanceof",
88
+ "await",
89
+ "async",
90
+ "import",
91
+ "export",
92
+ "const",
93
+ "let",
94
+ "var",
95
+ "class",
96
+ "throw",
97
+ ]);
98
+ for (const kw of KEYWORDS) calledNames.delete(kw);
99
+
100
+ if (calledNames.size === 0) return [];
101
+
102
+ // Match against known entities
103
+ const matched: ReferencedFunction[] = [];
104
+ for (const entity of entities) {
105
+ if (matched.length >= MAX_REFS_PER_FILE) break;
106
+ if (calledNames.has(entity.name)) {
107
+ matched.push({
108
+ name: entity.name,
109
+ filePath: entity.filePath,
110
+ body: entity.body,
111
+ });
112
+ }
113
+ }
114
+
115
+ return matched;
116
+ }
117
+
118
+ // ─── AI Review Runner ─────────────────────────────────────────────────────
119
+
120
+ const VALID_RULE_IDS = new Set([
121
+ "ai-review/cross-function",
122
+ "ai-review/edge-case",
123
+ "ai-review/dead-code",
124
+ "ai-review/contract",
125
+ "ai-review/spec-compliance",
126
+ "ai-review/architecture",
127
+ "ai-review/coverage-gap",
128
+ ]);
129
+
130
+ /**
131
+ * Parse the AI response JSON into Finding[].
132
+ * Returns null on any parse failure — caller should treat as skip.
133
+ */
134
+ function parseAIResponse(text: string, deep: boolean): Finding[] | null {
135
+ try {
136
+ // Strip markdown fences if present
137
+ const cleaned = text
138
+ .replace(/^```json?\n?/m, "")
139
+ .replace(/\n?```$/m, "")
140
+ .trim();
141
+ const parsed = JSON.parse(cleaned);
142
+
143
+ if (!parsed || !Array.isArray(parsed.findings)) return null;
144
+
145
+ const findings: Finding[] = [];
146
+ for (const f of parsed.findings) {
147
+ if (!f.file || typeof f.line !== "number" || !f.message) continue;
148
+
149
+ let severity: Finding["severity"] =
150
+ f.severity === "error" ? "error" : "warning";
151
+ // Mechanical mode caps at warning
152
+ if (!deep && severity === "error") severity = "warning";
153
+
154
+ const ruleId = VALID_RULE_IDS.has(f.ruleId)
155
+ ? f.ruleId
156
+ : "ai-review/edge-case";
157
+
158
+ findings.push({
159
+ tool: "ai-review",
160
+ file: f.file,
161
+ line: f.line,
162
+ message: f.message,
163
+ severity,
164
+ ruleId,
165
+ });
166
+ }
167
+
168
+ return findings;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Run AI review on a diff.
176
+ *
177
+ * - mechanical (default): uses "code-review" task → mechanical tier model, caps at warning
178
+ * - standard (deep=true): uses "deep-code-review" task → standard tier model, can emit error
179
+ *
180
+ * Gracefully skips on AI failure, host delegation, or malformed response.
181
+ */
182
+ export async function runAIReview(
183
+ options: AIReviewOptions,
184
+ ): Promise<AIReviewResult> {
185
+ const start = performance.now();
186
+ const {
187
+ diff,
188
+ entities,
189
+ deep = false,
190
+ specContext,
191
+ planContext,
192
+ mainaDir,
193
+ } = options;
194
+
195
+ if (!diff.trim()) {
196
+ return {
197
+ findings: [],
198
+ skipped: true,
199
+ tier: deep ? "standard" : "mechanical",
200
+ duration: 0,
201
+ };
202
+ }
203
+
204
+ // Resolve referenced functions from entities
205
+ const refs = resolveReferencedFunctions(diff, entities);
206
+ const refsText =
207
+ refs.length > 0
208
+ ? refs
209
+ .map(
210
+ (r) =>
211
+ `### ${r.name} (${r.filePath})\n\`\`\`typescript\n${r.body}\n\`\`\``,
212
+ )
213
+ .join("\n\n")
214
+ : "None found.";
215
+
216
+ const task = deep ? "deep-code-review" : "code-review";
217
+ const reviewMode = deep
218
+ ? "deep — check everything including spec compliance. May emit error severity."
219
+ : "mechanical — check cross-function consistency and edge cases only. All findings are warning severity.";
220
+
221
+ // ── Cache check ──────────────────────────────────────────────────────
222
+ const cacheKey = await buildCacheKey({
223
+ task,
224
+ extra: hashContent(diff + refsText),
225
+ });
226
+
227
+ const cache = createCacheManager(mainaDir);
228
+ const cached = cache.get(cacheKey);
229
+ if (cached) {
230
+ try {
231
+ const findings = JSON.parse(cached.value) as Finding[];
232
+ const duration = Math.round(performance.now() - start);
233
+ return {
234
+ findings,
235
+ skipped: false,
236
+ tier: deep ? "standard" : "mechanical",
237
+ duration,
238
+ };
239
+ } catch {
240
+ // Corrupted cache entry — fall through to AI
241
+ }
242
+ }
243
+
244
+ // ── AI call ──────────────────────────────────────────────────────────
245
+ const variables: Record<string, string> = {
246
+ diff,
247
+ referencedFunctions: refsText,
248
+ reviewMode,
249
+ };
250
+
251
+ if (deep && specContext) variables.specContext = specContext;
252
+ if (deep && planContext) variables.planContext = planContext;
253
+
254
+ const userPrompt = `Review this diff for semantic issues:\n\n${diff}`;
255
+
256
+ const aiResult = await tryAIGenerate(task, mainaDir, variables, userPrompt);
257
+
258
+ const duration = Math.round(performance.now() - start);
259
+ const tier = deep ? "standard" : "mechanical";
260
+
261
+ // Host delegation or no AI → skip (no cache entry per spec)
262
+ if (!aiResult.text || aiResult.hostDelegation) {
263
+ return { findings: [], skipped: true, tier, duration };
264
+ }
265
+
266
+ // Parse response
267
+ const findings = parseAIResponse(aiResult.text, deep);
268
+ if (findings === null) {
269
+ return { findings: [], skipped: true, tier, duration };
270
+ }
271
+
272
+ // Cache successful result
273
+ cache.set(cacheKey, JSON.stringify(findings));
274
+
275
+ return { findings, skipped: false, tier, duration };
276
+ }