@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,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-stage PR Review.
|
|
3
|
+
*
|
|
4
|
+
* Stage 1 — Spec Compliance: checks diff against implementation plan tasks.
|
|
5
|
+
* Stage 2 — Code Quality: checks added lines for common issues.
|
|
6
|
+
*
|
|
7
|
+
* Deterministic checks only — no AI calls.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface ReviewStageResult {
|
|
13
|
+
stage: "spec-compliance" | "code-quality";
|
|
14
|
+
passed: boolean;
|
|
15
|
+
findings: ReviewFinding[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ReviewFinding {
|
|
19
|
+
stage: "spec-compliance" | "code-quality";
|
|
20
|
+
severity: "error" | "warning" | "info";
|
|
21
|
+
message: string;
|
|
22
|
+
file?: string;
|
|
23
|
+
line?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReviewOptions {
|
|
27
|
+
diff: string;
|
|
28
|
+
planContent?: string | null;
|
|
29
|
+
conventions?: string | null;
|
|
30
|
+
mainaDir?: string; // enables AI review when provided
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReviewResult {
|
|
34
|
+
stage1: ReviewStageResult;
|
|
35
|
+
stage2: ReviewStageResult | null; // null if stage 1 failed
|
|
36
|
+
passed: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract task descriptions from a plan's task list.
|
|
43
|
+
* Matches lines like `- [ ] Do something` or `- [x] Do something`.
|
|
44
|
+
*/
|
|
45
|
+
function extractTasks(planContent: string): string[] {
|
|
46
|
+
const taskPattern = /^[-*]\s+\[[ x]\]\s+(.+)/gim;
|
|
47
|
+
const tasks: string[] = [];
|
|
48
|
+
let match = taskPattern.exec(planContent);
|
|
49
|
+
while (match !== null) {
|
|
50
|
+
if (match[1]) {
|
|
51
|
+
tasks.push(match[1].trim());
|
|
52
|
+
}
|
|
53
|
+
match = taskPattern.exec(planContent);
|
|
54
|
+
}
|
|
55
|
+
return tasks;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract files touched in a unified diff.
|
|
60
|
+
* Looks for `diff --git a/... b/...` headers.
|
|
61
|
+
*/
|
|
62
|
+
function extractDiffFiles(diff: string): string[] {
|
|
63
|
+
const filePattern = /^diff --git a\/(.+?) b\/(.+)$/gm;
|
|
64
|
+
const files: string[] = [];
|
|
65
|
+
let match = filePattern.exec(diff);
|
|
66
|
+
while (match !== null) {
|
|
67
|
+
if (match[2]) {
|
|
68
|
+
files.push(match[2]);
|
|
69
|
+
}
|
|
70
|
+
match = filePattern.exec(diff);
|
|
71
|
+
}
|
|
72
|
+
return [...new Set(files)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract keywords from a task description for matching against file paths.
|
|
77
|
+
* Splits on whitespace and common separators, lowercases, filters noise.
|
|
78
|
+
*/
|
|
79
|
+
function extractKeywords(task: string): string[] {
|
|
80
|
+
const NOISE_WORDS = new Set([
|
|
81
|
+
"a",
|
|
82
|
+
"an",
|
|
83
|
+
"the",
|
|
84
|
+
"to",
|
|
85
|
+
"in",
|
|
86
|
+
"on",
|
|
87
|
+
"for",
|
|
88
|
+
"and",
|
|
89
|
+
"or",
|
|
90
|
+
"with",
|
|
91
|
+
"from",
|
|
92
|
+
"add",
|
|
93
|
+
"update",
|
|
94
|
+
"fix",
|
|
95
|
+
"remove",
|
|
96
|
+
"create",
|
|
97
|
+
"implement",
|
|
98
|
+
"refactor",
|
|
99
|
+
"delete",
|
|
100
|
+
"modify",
|
|
101
|
+
"change",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
return task
|
|
105
|
+
.split(/[\s/\\.,;:()]+/)
|
|
106
|
+
.map((w) => w.toLowerCase().replace(/[^a-z0-9-_]/g, ""))
|
|
107
|
+
.filter((w) => w.length > 1 && !NOISE_WORDS.has(w));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a file path matches any keywords from a task.
|
|
112
|
+
*/
|
|
113
|
+
function fileMatchesTask(file: string, taskKeywords: string[]): boolean {
|
|
114
|
+
const fileLower = file.toLowerCase();
|
|
115
|
+
return taskKeywords.some(
|
|
116
|
+
(keyword) => keyword.length > 2 && fileLower.includes(keyword),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract added lines from a unified diff (lines starting with `+`, excluding `+++` header).
|
|
122
|
+
*/
|
|
123
|
+
function extractAddedLines(
|
|
124
|
+
diff: string,
|
|
125
|
+
): Array<{ text: string; file: string; lineNum: number }> {
|
|
126
|
+
const lines = diff.split("\n");
|
|
127
|
+
const added: Array<{ text: string; file: string; lineNum: number }> = [];
|
|
128
|
+
let currentFile = "";
|
|
129
|
+
let lineNum = 0;
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
// Track current file
|
|
133
|
+
const fileMatch = line.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
134
|
+
if (fileMatch?.[1]) {
|
|
135
|
+
currentFile = fileMatch[1];
|
|
136
|
+
lineNum = 0;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Track line numbers from hunk headers
|
|
141
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
|
|
142
|
+
if (hunkMatch?.[1]) {
|
|
143
|
+
lineNum = Number.parseInt(hunkMatch[1], 10) - 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Count added and context lines for line tracking
|
|
148
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
149
|
+
lineNum++;
|
|
150
|
+
added.push({ text: line.slice(1), file: currentFile, lineNum });
|
|
151
|
+
} else if (line.startsWith("-")) {
|
|
152
|
+
// Removed lines don't increment the new-file line counter
|
|
153
|
+
} else if (!line.startsWith("\\")) {
|
|
154
|
+
// Context line
|
|
155
|
+
lineNum++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return added;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Stage 1: Spec Compliance ────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Review spec compliance by checking if the diff covers all plan tasks.
|
|
166
|
+
*
|
|
167
|
+
* - If planContent is null/empty, skip stage 1 and return passed.
|
|
168
|
+
* - Flags tasks with no corresponding file changes (missing implementation).
|
|
169
|
+
* - Flags changed files that don't map to any task (over-building).
|
|
170
|
+
*/
|
|
171
|
+
export function reviewSpecCompliance(
|
|
172
|
+
diff: string,
|
|
173
|
+
planContent: string | null,
|
|
174
|
+
): ReviewStageResult {
|
|
175
|
+
const findings: ReviewFinding[] = [];
|
|
176
|
+
|
|
177
|
+
if (!planContent) {
|
|
178
|
+
return {
|
|
179
|
+
stage: "spec-compliance",
|
|
180
|
+
passed: true,
|
|
181
|
+
findings: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const tasks = extractTasks(planContent);
|
|
186
|
+
const diffFiles = extractDiffFiles(diff);
|
|
187
|
+
|
|
188
|
+
if (tasks.length === 0) {
|
|
189
|
+
// Plan exists but no tasks extracted — pass through
|
|
190
|
+
return {
|
|
191
|
+
stage: "spec-compliance",
|
|
192
|
+
passed: true,
|
|
193
|
+
findings: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check each task has matching file changes
|
|
198
|
+
const matchedFiles = new Set<string>();
|
|
199
|
+
|
|
200
|
+
for (const task of tasks) {
|
|
201
|
+
const keywords = extractKeywords(task);
|
|
202
|
+
let taskCovered = false;
|
|
203
|
+
|
|
204
|
+
for (const file of diffFiles) {
|
|
205
|
+
if (fileMatchesTask(file, keywords)) {
|
|
206
|
+
taskCovered = true;
|
|
207
|
+
matchedFiles.add(file);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!taskCovered) {
|
|
212
|
+
findings.push({
|
|
213
|
+
stage: "spec-compliance",
|
|
214
|
+
severity: "warning",
|
|
215
|
+
message: `Missing implementation for task: "${task}"`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for files not matching any task (over-building)
|
|
221
|
+
for (const file of diffFiles) {
|
|
222
|
+
if (!matchedFiles.has(file)) {
|
|
223
|
+
findings.push({
|
|
224
|
+
stage: "spec-compliance",
|
|
225
|
+
severity: "info",
|
|
226
|
+
message: `Possible over-building: "${file}" not mapped to any plan task`,
|
|
227
|
+
file,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const hasWarningsOrErrors = findings.some(
|
|
233
|
+
(f) => f.severity === "warning" || f.severity === "error",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
stage: "spec-compliance",
|
|
238
|
+
passed: !hasWarningsOrErrors,
|
|
239
|
+
findings,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Stage 2: Code Quality ───────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Review code quality by checking added lines for common issues.
|
|
247
|
+
*
|
|
248
|
+
* Checks:
|
|
249
|
+
* - console.log in added lines
|
|
250
|
+
* - TODO without ticket reference (e.g., #123) in added lines
|
|
251
|
+
* - Empty function bodies in added lines
|
|
252
|
+
* - Very long lines (>120 chars) in added lines
|
|
253
|
+
*/
|
|
254
|
+
export function reviewCodeQuality(
|
|
255
|
+
diff: string,
|
|
256
|
+
_conventions: string | null,
|
|
257
|
+
): ReviewStageResult {
|
|
258
|
+
const findings: ReviewFinding[] = [];
|
|
259
|
+
const addedLines = extractAddedLines(diff);
|
|
260
|
+
|
|
261
|
+
for (const { text, file, lineNum } of addedLines) {
|
|
262
|
+
// Check for console.log
|
|
263
|
+
if (/console\.log\s*\(/.test(text)) {
|
|
264
|
+
findings.push({
|
|
265
|
+
stage: "code-quality",
|
|
266
|
+
severity: "warning",
|
|
267
|
+
message: "console.log found in added code",
|
|
268
|
+
file,
|
|
269
|
+
line: lineNum,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Bare TODO without ticket ref — case-sensitive to skip identifiers like handleCreateTodo. Allows TODO(#123), TODO(JIRA-456)
|
|
274
|
+
if (
|
|
275
|
+
/\bTODO\b/.test(text) &&
|
|
276
|
+
!/TODO\s*[(#]|TODO\s*\([A-Z]+-\d+\)/.test(text)
|
|
277
|
+
) {
|
|
278
|
+
findings.push({
|
|
279
|
+
stage: "code-quality",
|
|
280
|
+
severity: "warning",
|
|
281
|
+
message: "TODO without ticket reference in added code",
|
|
282
|
+
file,
|
|
283
|
+
line: lineNum,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check for empty function/method bodies
|
|
288
|
+
if (
|
|
289
|
+
/(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}/.test(text) ||
|
|
290
|
+
/\)\s*\{\s*\}/.test(text)
|
|
291
|
+
) {
|
|
292
|
+
findings.push({
|
|
293
|
+
stage: "code-quality",
|
|
294
|
+
severity: "warning",
|
|
295
|
+
message: "Empty function body in added code",
|
|
296
|
+
file,
|
|
297
|
+
line: lineNum,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check for very long lines (>120 chars)
|
|
302
|
+
if (text.length > 120) {
|
|
303
|
+
findings.push({
|
|
304
|
+
stage: "code-quality",
|
|
305
|
+
severity: "info",
|
|
306
|
+
message: `Long line (${text.length} chars) in added code`,
|
|
307
|
+
file,
|
|
308
|
+
line: lineNum,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const hasWarningsOrErrors = findings.some(
|
|
314
|
+
(f) => f.severity === "warning" || f.severity === "error",
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
stage: "code-quality",
|
|
319
|
+
passed: !hasWarningsOrErrors,
|
|
320
|
+
findings,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── AI-Enhanced Code Quality Review ─────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Run code quality review with optional AI enhancement.
|
|
328
|
+
*
|
|
329
|
+
* Always runs deterministic checks first. If an API key is available and
|
|
330
|
+
* mainaDir is provided, also runs an AI-powered review and merges findings.
|
|
331
|
+
* AI failure never blocks the review — deterministic results are always returned.
|
|
332
|
+
*/
|
|
333
|
+
export async function reviewCodeQualityWithAI(
|
|
334
|
+
diff: string,
|
|
335
|
+
conventions: string | null,
|
|
336
|
+
mainaDir: string,
|
|
337
|
+
): Promise<ReviewStageResult> {
|
|
338
|
+
// Always run deterministic checks first
|
|
339
|
+
const deterministicResult = reviewCodeQuality(diff, conventions);
|
|
340
|
+
|
|
341
|
+
// Try AI review if API key available
|
|
342
|
+
try {
|
|
343
|
+
const { tryAIGenerate } = await import("../ai/try-generate");
|
|
344
|
+
const aiResult = await tryAIGenerate(
|
|
345
|
+
"review",
|
|
346
|
+
mainaDir,
|
|
347
|
+
{
|
|
348
|
+
diff,
|
|
349
|
+
conventions: conventions ?? "",
|
|
350
|
+
constitution: "",
|
|
351
|
+
language: "TypeScript",
|
|
352
|
+
},
|
|
353
|
+
`Review this diff:\n\n${diff}`,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Parse AI findings and merge with deterministic ones
|
|
357
|
+
if (aiResult.text && aiResult.fromAI) {
|
|
358
|
+
deterministicResult.findings.push({
|
|
359
|
+
stage: "code-quality",
|
|
360
|
+
severity: "info",
|
|
361
|
+
message: `AI review: ${aiResult.text.slice(0, 200)}${aiResult.text.length > 200 ? "..." : ""}`,
|
|
362
|
+
});
|
|
363
|
+
} else if (aiResult.hostDelegation && aiResult.delegation) {
|
|
364
|
+
// Host mode — include delegation note for the host agent
|
|
365
|
+
deterministicResult.findings.push({
|
|
366
|
+
stage: "code-quality",
|
|
367
|
+
severity: "info",
|
|
368
|
+
message:
|
|
369
|
+
"AI review delegated to host agent. Deterministic checks complete.",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// AI failure should never block review
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return deterministicResult;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Two-Stage Review ────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Run the two-stage PR review pipeline.
|
|
383
|
+
*
|
|
384
|
+
* Stage 1: Spec compliance. If it fails, return without running stage 2.
|
|
385
|
+
* Stage 2: Code quality. Uses AI-enhanced review when mainaDir is provided.
|
|
386
|
+
* Returns combined result.
|
|
387
|
+
*/
|
|
388
|
+
export async function runTwoStageReview(
|
|
389
|
+
options: ReviewOptions,
|
|
390
|
+
): Promise<ReviewResult> {
|
|
391
|
+
const stage1 = reviewSpecCompliance(
|
|
392
|
+
options.diff,
|
|
393
|
+
options.planContent ?? null,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
if (!stage1.passed) {
|
|
397
|
+
return {
|
|
398
|
+
stage1,
|
|
399
|
+
stage2: null,
|
|
400
|
+
passed: false,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const stage2 = options.mainaDir
|
|
405
|
+
? await reviewCodeQualityWithAI(
|
|
406
|
+
options.diff,
|
|
407
|
+
options.conventions ?? null,
|
|
408
|
+
options.mainaDir,
|
|
409
|
+
)
|
|
410
|
+
: reviewCodeQuality(options.diff, options.conventions ?? null);
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
stage1,
|
|
414
|
+
stage2,
|
|
415
|
+
passed: stage1.passed && stage2.passed,
|
|
416
|
+
};
|
|
417
|
+
}
|