@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,288 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ reviewCodeQuality,
4
+ reviewSpecCompliance,
5
+ runTwoStageReview,
6
+ } from "../index";
7
+
8
+ // ── reviewSpecCompliance ────────────────────────────────────────────────────
9
+
10
+ describe("reviewSpecCompliance", () => {
11
+ test("no plan → passed", () => {
12
+ const result = reviewSpecCompliance("+ some code change", null);
13
+
14
+ expect(result.stage).toBe("spec-compliance");
15
+ expect(result.passed).toBe(true);
16
+ expect(result.findings).toHaveLength(0);
17
+ });
18
+
19
+ test("plan with tasks, diff covers all → passed", () => {
20
+ const plan = `## Tasks
21
+ - [ ] Add user authentication to auth.ts
22
+ - [ ] Update database schema in db/schema.ts`;
23
+
24
+ const diff = `diff --git a/src/auth.ts b/src/auth.ts
25
+ --- a/src/auth.ts
26
+ +++ b/src/auth.ts
27
+ @@ -1,3 +1,5 @@
28
+ +export function authenticate() { return true; }
29
+ diff --git a/src/db/schema.ts b/src/db/schema.ts
30
+ --- a/src/db/schema.ts
31
+ +++ b/src/db/schema.ts
32
+ @@ -1,3 +1,5 @@
33
+ +export const usersTable = {};`;
34
+
35
+ const result = reviewSpecCompliance(diff, plan);
36
+
37
+ expect(result.stage).toBe("spec-compliance");
38
+ expect(result.passed).toBe(true);
39
+ expect(result.findings).toHaveLength(0);
40
+ });
41
+
42
+ test("plan with task not in diff → warning (missing implementation)", () => {
43
+ const plan = `## Tasks
44
+ - [ ] Add user authentication to auth.ts
45
+ - [ ] Update database schema in db/schema.ts`;
46
+
47
+ // Only touches auth.ts, not db/schema.ts
48
+ const diff = `diff --git a/src/auth.ts b/src/auth.ts
49
+ --- a/src/auth.ts
50
+ +++ b/src/auth.ts
51
+ @@ -1,3 +1,5 @@
52
+ +export function authenticate() { return true; }`;
53
+
54
+ const result = reviewSpecCompliance(diff, plan);
55
+
56
+ expect(result.stage).toBe("spec-compliance");
57
+ expect(result.passed).toBe(false);
58
+ expect(result.findings.length).toBeGreaterThan(0);
59
+
60
+ const missing = result.findings.find((f) =>
61
+ f.message.toLowerCase().includes("missing"),
62
+ );
63
+ expect(missing).toBeDefined();
64
+ expect(missing?.severity).toBe("warning");
65
+ });
66
+
67
+ test("diff changes not in plan → info (over-building)", () => {
68
+ const plan = `## Tasks
69
+ - [ ] Add user authentication to auth.ts`;
70
+
71
+ // Touches auth.ts (in plan) AND unrelated.ts (not in plan)
72
+ const diff = `diff --git a/src/auth.ts b/src/auth.ts
73
+ --- a/src/auth.ts
74
+ +++ b/src/auth.ts
75
+ @@ -1,3 +1,5 @@
76
+ +export function authenticate() { return true; }
77
+ diff --git a/src/unrelated.ts b/src/unrelated.ts
78
+ --- a/src/unrelated.ts
79
+ +++ b/src/unrelated.ts
80
+ @@ -1,3 +1,5 @@
81
+ +export function unrelated() {}`;
82
+
83
+ const result = reviewSpecCompliance(diff, plan);
84
+
85
+ const overBuilding = result.findings.find((f) =>
86
+ f.message.toLowerCase().includes("over-building"),
87
+ );
88
+ expect(overBuilding).toBeDefined();
89
+ expect(overBuilding?.severity).toBe("info");
90
+ });
91
+ });
92
+
93
+ // ── reviewCodeQuality ───────────────────────────────────────────────────────
94
+
95
+ describe("reviewCodeQuality", () => {
96
+ test("clean diff → passed", () => {
97
+ const diff = `diff --git a/src/index.ts b/src/index.ts
98
+ --- a/src/index.ts
99
+ +++ b/src/index.ts
100
+ @@ -1,3 +1,5 @@
101
+ +export function greet(name: string): string {
102
+ + return name;
103
+ +}`;
104
+
105
+ const result = reviewCodeQuality(diff, null);
106
+
107
+ expect(result.stage).toBe("code-quality");
108
+ expect(result.passed).toBe(true);
109
+ expect(result.findings).toHaveLength(0);
110
+ });
111
+
112
+ test("diff with console.log → finding", () => {
113
+ const diff = `diff --git a/src/index.ts b/src/index.ts
114
+ --- a/src/index.ts
115
+ +++ b/src/index.ts
116
+ @@ -1,3 +1,5 @@
117
+ +export function greet(name: string): string {
118
+ + console.log("hello");
119
+ + return name;
120
+ +}`;
121
+
122
+ const result = reviewCodeQuality(diff, null);
123
+
124
+ expect(result.passed).toBe(false);
125
+ const consoleFinding = result.findings.find((f) =>
126
+ f.message.toLowerCase().includes("console.log"),
127
+ );
128
+ expect(consoleFinding).toBeDefined();
129
+ expect(consoleFinding?.severity).toBe("warning");
130
+ });
131
+
132
+ test("diff with TODO no ticket → finding", () => {
133
+ const diff = `diff --git a/src/index.ts b/src/index.ts
134
+ --- a/src/index.ts
135
+ +++ b/src/index.ts
136
+ @@ -1,3 +1,5 @@
137
+ +// TODO fix this later
138
+ +export function greet() { return "hi"; }`;
139
+
140
+ const result = reviewCodeQuality(diff, null);
141
+
142
+ expect(result.passed).toBe(false);
143
+ const todoFinding = result.findings.find((f) =>
144
+ f.message.toLowerCase().includes("todo"),
145
+ );
146
+ expect(todoFinding).toBeDefined();
147
+ expect(todoFinding?.severity).toBe("warning");
148
+ });
149
+
150
+ test("diff with empty function body → finding", () => {
151
+ const diff = `diff --git a/src/index.ts b/src/index.ts
152
+ --- a/src/index.ts
153
+ +++ b/src/index.ts
154
+ @@ -1,3 +1,5 @@
155
+ +export function greet() {}`;
156
+
157
+ const result = reviewCodeQuality(diff, null);
158
+
159
+ expect(result.passed).toBe(false);
160
+ const emptyFinding = result.findings.find((f) =>
161
+ f.message.toLowerCase().includes("empty"),
162
+ );
163
+ expect(emptyFinding).toBeDefined();
164
+ expect(emptyFinding?.severity).toBe("warning");
165
+ });
166
+
167
+ test("diff with very long line → finding", () => {
168
+ const longLine = `+export const x = "${"a".repeat(130)}";`;
169
+ const diff = `diff --git a/src/index.ts b/src/index.ts
170
+ --- a/src/index.ts
171
+ +++ b/src/index.ts
172
+ @@ -1,3 +1,5 @@
173
+ ${longLine}`;
174
+
175
+ const result = reviewCodeQuality(diff, null);
176
+
177
+ const longFinding = result.findings.find((f) =>
178
+ f.message.toLowerCase().includes("long"),
179
+ );
180
+ expect(longFinding).toBeDefined();
181
+ expect(longFinding?.severity).toBe("info");
182
+ });
183
+
184
+ test("TODO with ticket reference is allowed", () => {
185
+ const diff = `diff --git a/src/index.ts b/src/index.ts
186
+ --- a/src/index.ts
187
+ +++ b/src/index.ts
188
+ @@ -1,3 +1,5 @@
189
+ +// TODO(#123) fix this later
190
+ +export function greet() { return "hi"; }`;
191
+
192
+ const result = reviewCodeQuality(diff, null);
193
+
194
+ const todoFinding = result.findings.find((f) =>
195
+ f.message.toLowerCase().includes("todo"),
196
+ );
197
+ expect(todoFinding).toBeUndefined();
198
+ });
199
+ });
200
+
201
+ // ── runTwoStageReview ───────────────────────────────────────────────────────
202
+
203
+ describe("runTwoStageReview", () => {
204
+ test("both pass → passed", async () => {
205
+ const diff = `diff --git a/src/auth.ts b/src/auth.ts
206
+ --- a/src/auth.ts
207
+ +++ b/src/auth.ts
208
+ @@ -1,3 +1,5 @@
209
+ +export function authenticate() { return true; }`;
210
+
211
+ const plan = `## Tasks
212
+ - [ ] Add user authentication to auth.ts`;
213
+
214
+ const result = await runTwoStageReview({
215
+ diff,
216
+ planContent: plan,
217
+ });
218
+
219
+ expect(result.passed).toBe(true);
220
+ expect(result.stage1.passed).toBe(true);
221
+ expect(result.stage2).not.toBeNull();
222
+ expect(result.stage2?.passed).toBe(true);
223
+ });
224
+
225
+ test("stage 1 fails → stage 2 is null", async () => {
226
+ const plan = `## Tasks
227
+ - [ ] Add user authentication to auth.ts
228
+ - [ ] Update database schema in db/schema.ts`;
229
+
230
+ // Only touches unrelated file
231
+ const diff = `diff --git a/src/unrelated.ts b/src/unrelated.ts
232
+ --- a/src/unrelated.ts
233
+ +++ b/src/unrelated.ts
234
+ @@ -1,3 +1,5 @@
235
+ +export function unrelated() { return true; }`;
236
+
237
+ const result = await runTwoStageReview({
238
+ diff,
239
+ planContent: plan,
240
+ });
241
+
242
+ expect(result.passed).toBe(false);
243
+ expect(result.stage1.passed).toBe(false);
244
+ expect(result.stage2).toBeNull();
245
+ });
246
+
247
+ test("stage 1 passes but stage 2 fails → passed is false", async () => {
248
+ const diff = `diff --git a/src/auth.ts b/src/auth.ts
249
+ --- a/src/auth.ts
250
+ +++ b/src/auth.ts
251
+ @@ -1,3 +1,5 @@
252
+ +export function authenticate() {
253
+ + console.log("authenticating");
254
+ + return true;
255
+ +}`;
256
+
257
+ const plan = `## Tasks
258
+ - [ ] Add user authentication to auth.ts`;
259
+
260
+ const result = await runTwoStageReview({
261
+ diff,
262
+ planContent: plan,
263
+ });
264
+
265
+ expect(result.passed).toBe(false);
266
+ expect(result.stage1.passed).toBe(true);
267
+ expect(result.stage2).not.toBeNull();
268
+ expect(result.stage2?.passed).toBe(false);
269
+ });
270
+
271
+ test("no plan → stage 1 passes automatically", async () => {
272
+ const diff = `diff --git a/src/index.ts b/src/index.ts
273
+ --- a/src/index.ts
274
+ +++ b/src/index.ts
275
+ @@ -1,3 +1,5 @@
276
+ +export function greet(name: string): string {
277
+ + return name;
278
+ +}`;
279
+
280
+ const result = await runTwoStageReview({
281
+ diff,
282
+ planContent: null,
283
+ });
284
+
285
+ expect(result.stage1.passed).toBe(true);
286
+ expect(result.stage2).not.toBeNull();
287
+ });
288
+ });
@@ -0,0 +1,362 @@
1
+ import { relative } from "node:path";
2
+ import type { Finding } from "../verify/diff-filter";
3
+
4
+ // ── Types ────────────────────────────────────────────────────────────────────
5
+
6
+ export type ReviewSeverity = "critical" | "important" | "minor";
7
+
8
+ export interface ComprehensiveReviewFinding {
9
+ severity: ReviewSeverity;
10
+ category:
11
+ | "quality"
12
+ | "architecture"
13
+ | "testing"
14
+ | "requirements"
15
+ | "security";
16
+ file?: string;
17
+ line?: number;
18
+ issue: string;
19
+ why: string;
20
+ fix?: string;
21
+ }
22
+
23
+ export interface ComprehensiveReviewResult {
24
+ strengths: string[];
25
+ findings: ComprehensiveReviewFinding[];
26
+ planAlignment: {
27
+ tasksInPlan: number;
28
+ tasksWithChanges: number;
29
+ overBuilding: string[];
30
+ missingImpl: string[];
31
+ };
32
+ architecture: {
33
+ separationOfConcerns: boolean;
34
+ errorHandling: boolean;
35
+ typeSafety: boolean;
36
+ notes: string[];
37
+ };
38
+ testing: {
39
+ testFiles: number;
40
+ implFiles: number;
41
+ ratio: string;
42
+ gaps: string[];
43
+ };
44
+ verdict: "ready" | "with-fixes" | "not-ready";
45
+ verdictReason: string;
46
+ }
47
+
48
+ export interface ComprehensiveReviewOptions {
49
+ diff: string;
50
+ files: string[];
51
+ repoRoot: string;
52
+ planContent?: string | null;
53
+ pipelineFindings?: Finding[];
54
+ }
55
+
56
+ // ── Helpers ──────────────────────────────────────────────────────────────────
57
+
58
+ function extractAddedLines(
59
+ diff: string,
60
+ ): Array<{ file: string; line: number; content: string }> {
61
+ const lines: Array<{ file: string; line: number; content: string }> = [];
62
+ let currentFile = "";
63
+ let lineNum = 0;
64
+
65
+ for (const raw of diff.split("\n")) {
66
+ if (raw.startsWith("+++ b/")) {
67
+ currentFile = raw.slice(6);
68
+ continue;
69
+ }
70
+ if (raw.startsWith("@@ ")) {
71
+ const match = /@@ -\d+(?:,\d+)? \+(\d+)/.exec(raw);
72
+ lineNum = match ? Number.parseInt(match[1] ?? "0", 10) - 1 : 0;
73
+ continue;
74
+ }
75
+ if (raw.startsWith("+") && !raw.startsWith("+++")) {
76
+ lineNum++;
77
+ lines.push({ file: currentFile, line: lineNum, content: raw.slice(1) });
78
+ } else if (!raw.startsWith("-")) {
79
+ lineNum++;
80
+ }
81
+ }
82
+ return lines;
83
+ }
84
+
85
+ function extractTasksFromPlan(
86
+ planContent: string,
87
+ ): Array<{ id: string; description: string }> {
88
+ const tasks: Array<{ id: string; description: string }> = [];
89
+ const taskPattern = /- \[[ x]\]\s*(T\d+)[:\s]+(.*)/gi;
90
+ let match: RegExpExecArray | null;
91
+ match = taskPattern.exec(planContent);
92
+ while (match !== null) {
93
+ tasks.push({ id: match[1] ?? "", description: match[2] ?? "" });
94
+ match = taskPattern.exec(planContent);
95
+ }
96
+ return tasks;
97
+ }
98
+
99
+ // ── Main Review ──────────────────────────────────────────────────────────────
100
+
101
+ export function comprehensiveReview(
102
+ options: ComprehensiveReviewOptions,
103
+ ): ComprehensiveReviewResult {
104
+ const { diff, files, repoRoot, planContent, pipelineFindings } = options;
105
+ const addedLines = extractAddedLines(diff);
106
+
107
+ const strengths: string[] = [];
108
+ const findings: ComprehensiveReviewFinding[] = [];
109
+
110
+ // ── Quality checks ──────────────────────────────────────────────────
111
+
112
+ // Check for Result<T,E> pattern usage (strength if present)
113
+ const usesResult = addedLines.some(
114
+ (l) => l.content.includes("Result<") || l.content.includes(": Result"),
115
+ );
116
+ if (usesResult) {
117
+ strengths.push("Uses Result<T,E> error handling pattern (no throwing)");
118
+ }
119
+
120
+ // Check for console.log in production code
121
+ for (const line of addedLines) {
122
+ if (
123
+ /console\.(log|warn|error|debug|info)\s*\(/.test(line.content) &&
124
+ !line.file.includes(".test.") &&
125
+ !line.file.includes("__tests__")
126
+ ) {
127
+ findings.push({
128
+ severity: "important",
129
+ category: "quality",
130
+ file: line.file,
131
+ line: line.line,
132
+ issue: `console.${/console\.(\w+)/.exec(line.content)?.[1]} in production code`,
133
+ why: "Constitution forbids console.log in production. Breaks structured logging.",
134
+ fix: "Remove or replace with proper logging/Result error handling",
135
+ });
136
+ }
137
+ }
138
+
139
+ // Check for bare TODO missing ticket reference
140
+ for (const line of addedLines) {
141
+ if (
142
+ /\/\/\s*(?:TO)(?:DO)(?!\s*[(#[])/.test(line.content) &&
143
+ !line.file.includes(".test.")
144
+ ) {
145
+ findings.push({
146
+ severity: "minor",
147
+ category: "quality",
148
+ file: line.file,
149
+ line: line.line,
150
+ issue: "TODO without ticket reference",
151
+ why: "Untracked TODOs get forgotten. Link to a ticket for accountability.",
152
+ fix: "Add ticket reference: // TODO(#123): description",
153
+ });
154
+ }
155
+ }
156
+
157
+ // Check for `any` type usage
158
+ for (const line of addedLines) {
159
+ if (/:\s*any\b|as\s+any\b/.test(line.content)) {
160
+ findings.push({
161
+ severity: "important",
162
+ category: "quality",
163
+ file: line.file,
164
+ line: line.line,
165
+ issue: "Usage of `any` type",
166
+ why: "TypeScript strict mode is required. `any` bypasses all type checking.",
167
+ fix: "Replace with proper type or `unknown`",
168
+ });
169
+ }
170
+ }
171
+
172
+ // Check for empty catch blocks
173
+ for (const line of addedLines) {
174
+ if (/catch\s*\{?\s*$/.test(line.content.trim())) {
175
+ findings.push({
176
+ severity: "minor",
177
+ category: "quality",
178
+ file: line.file,
179
+ line: line.line,
180
+ issue: "Empty catch block",
181
+ why: "Swallowed errors hide bugs. At minimum, log or use Result pattern.",
182
+ });
183
+ }
184
+ }
185
+
186
+ // ── Pipeline findings (from maina verify) ───────────────────────────
187
+
188
+ if (pipelineFindings && pipelineFindings.length > 0) {
189
+ for (const f of pipelineFindings) {
190
+ findings.push({
191
+ severity: f.severity === "error" ? "critical" : "minor",
192
+ category: "quality",
193
+ file: f.file,
194
+ line: f.line,
195
+ issue: `[${f.tool}] ${f.message}`,
196
+ why: `Caught by ${f.tool} verification tool`,
197
+ fix: f.ruleId ? `Fix rule: ${f.ruleId}` : undefined,
198
+ });
199
+ }
200
+ }
201
+
202
+ // ── Architecture checks ─────────────────────────────────────────────
203
+
204
+ const archNotes: string[] = [];
205
+ let separationOk = true;
206
+ let errorHandlingOk = true;
207
+ const typeSafetyOk = true;
208
+
209
+ // Check for cross-package imports
210
+ for (const line of addedLines) {
211
+ if (
212
+ line.file.includes("packages/mcp/") &&
213
+ line.content.includes("packages/cli/")
214
+ ) {
215
+ separationOk = false;
216
+ findings.push({
217
+ severity: "important",
218
+ category: "architecture",
219
+ file: line.file,
220
+ line: line.line,
221
+ issue: "MCP package imports directly from CLI package",
222
+ why: "Violates dependency direction. Both should depend on core, not on each other.",
223
+ fix: "Move shared code to packages/core/",
224
+ });
225
+ }
226
+ }
227
+
228
+ // Check for throw statements (should use Result)
229
+ const throwCount = addedLines.filter(
230
+ (l) => /\bthrow\s/.test(l.content) && !l.file.includes(".test."),
231
+ ).length;
232
+ if (throwCount > 0) {
233
+ errorHandlingOk = false;
234
+ archNotes.push(
235
+ `${throwCount} throw statement(s) found — consider Result<T,E> pattern`,
236
+ );
237
+ }
238
+
239
+ if (separationOk) {
240
+ strengths.push("Clean separation of concerns across packages");
241
+ }
242
+ if (errorHandlingOk) {
243
+ strengths.push("Consistent error handling (no throws in production)");
244
+ }
245
+
246
+ // ── Testing assessment ──────────────────────────────────────────────
247
+
248
+ const testFiles = files.filter(
249
+ (f) => f.includes(".test.") || f.includes("__tests__"),
250
+ );
251
+ const implFiles = files.filter(
252
+ (f) =>
253
+ !f.includes(".test.") &&
254
+ !f.includes("__tests__") &&
255
+ (f.endsWith(".ts") || f.endsWith(".tsx")),
256
+ );
257
+ const testGaps: string[] = [];
258
+
259
+ // Check if impl files have corresponding test files
260
+ for (const impl of implFiles) {
261
+ const baseName = impl.replace(/\.tsx?$/, "");
262
+ const hasTest = testFiles.some(
263
+ (t) =>
264
+ t.includes(baseName.split("/").pop() ?? "") || t.includes("__tests__"),
265
+ );
266
+ if (!hasTest) {
267
+ testGaps.push(relative(repoRoot, impl));
268
+ }
269
+ }
270
+
271
+ if (testFiles.length > 0) {
272
+ strengths.push(`${testFiles.length} test file(s) with TDD approach`);
273
+ }
274
+
275
+ // ── Plan alignment ──────────────────────────────────────────────────
276
+
277
+ let tasksInPlan = 0;
278
+ let tasksWithChanges = 0;
279
+ const overBuilding: string[] = [];
280
+ const missingImpl: string[] = [];
281
+
282
+ if (planContent) {
283
+ const tasks = extractTasksFromPlan(planContent);
284
+ tasksInPlan = tasks.length;
285
+
286
+ const changedFileNames = files.map(
287
+ (f) =>
288
+ f
289
+ .split("/")
290
+ .pop()
291
+ ?.replace(/\.tsx?$/, "")
292
+ .toLowerCase() ?? "",
293
+ );
294
+
295
+ for (const task of tasks) {
296
+ const keywords = task.description.toLowerCase().split(/\s+/);
297
+ const hasChange = keywords.some(
298
+ (kw) => kw.length > 3 && changedFileNames.some((f) => f.includes(kw)),
299
+ );
300
+ if (hasChange) {
301
+ tasksWithChanges++;
302
+ } else {
303
+ missingImpl.push(`${task.id}: ${task.description}`);
304
+ }
305
+ }
306
+
307
+ if (tasksInPlan > 0 && tasksWithChanges === tasksInPlan) {
308
+ strengths.push("All plan tasks have corresponding code changes");
309
+ }
310
+ }
311
+
312
+ // ── Verdict ──────────────────────────────────────────────────────────
313
+
314
+ const criticalCount = findings.filter(
315
+ (f) => f.severity === "critical",
316
+ ).length;
317
+ const importantCount = findings.filter(
318
+ (f) => f.severity === "important",
319
+ ).length;
320
+
321
+ let verdict: "ready" | "with-fixes" | "not-ready";
322
+ let verdictReason: string;
323
+
324
+ if (criticalCount > 0) {
325
+ verdict = "not-ready";
326
+ verdictReason = `${criticalCount} critical issue(s) must be fixed before merge`;
327
+ } else if (importantCount > 0) {
328
+ verdict = "with-fixes";
329
+ verdictReason = `${importantCount} important issue(s) should be addressed`;
330
+ } else {
331
+ verdict = "ready";
332
+ verdictReason = "No critical or important issues found";
333
+ }
334
+
335
+ return {
336
+ strengths,
337
+ findings,
338
+ planAlignment: {
339
+ tasksInPlan,
340
+ tasksWithChanges,
341
+ overBuilding,
342
+ missingImpl,
343
+ },
344
+ architecture: {
345
+ separationOfConcerns: separationOk,
346
+ errorHandling: errorHandlingOk,
347
+ typeSafety: typeSafetyOk,
348
+ notes: archNotes,
349
+ },
350
+ testing: {
351
+ testFiles: testFiles.length,
352
+ implFiles: implFiles.length,
353
+ ratio:
354
+ implFiles.length > 0
355
+ ? `${((testFiles.length / implFiles.length) * 100).toFixed(0)}%`
356
+ : "N/A",
357
+ gaps: testGaps,
358
+ },
359
+ verdict,
360
+ verdictReason,
361
+ };
362
+ }