@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.
- package/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- 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
|
+
}
|