ai-code-reviewer-plus 0.1.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/dist/index.js ADDED
@@ -0,0 +1,713 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ConfigError: () => ConfigError,
24
+ GitCommandError: () => GitCommandError,
25
+ ParseError: () => ParseError,
26
+ VERSION: () => VERSION,
27
+ analyzeCode: () => analyzeCode,
28
+ analyzeDiffs: () => analyzeDiffs,
29
+ collectCommitDiff: () => collectCommitDiff,
30
+ collectDiff: () => collectDiff,
31
+ detectProject: () => detectProject,
32
+ formatDiffSummary: () => formatDiffSummary,
33
+ formatDuration: () => formatDuration,
34
+ formatFinding: () => formatFinding,
35
+ formatReviewReport: () => formatReviewReport,
36
+ formatSummary: () => formatSummary,
37
+ getAvailableRules: () => getAvailableRules,
38
+ getDefaultConfig: () => getDefaultConfig,
39
+ loadConfigFile: () => loadConfigFile,
40
+ mergeConfig: () => mergeConfig
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // src/analyzers/index.ts
45
+ var RULES = [
46
+ // Security rules
47
+ {
48
+ id: "SEC-001",
49
+ dimension: "security",
50
+ severity: "BLOCKER",
51
+ pattern: /\.innerHTML\s*=\s*/,
52
+ title: "XSS: Direct innerHTML assignment",
53
+ description: "Assigning to innerHTML with unsanitized input creates an XSS vulnerability.",
54
+ suggestion: "Use textContent or sanitize input before rendering.",
55
+ fixExample: "element.textContent = sanitize(userInput)"
56
+ },
57
+ {
58
+ id: "SEC-002",
59
+ dimension: "security",
60
+ severity: "BLOCKER",
61
+ pattern: /eval\s*\(/,
62
+ title: "XSS: Use of eval()",
63
+ description: "eval() executes arbitrary code, creating a severe security risk.",
64
+ suggestion: "Avoid eval(). Use JSON.parse() for data or Function constructor for controlled logic.",
65
+ fixExample: "const data = JSON.parse(jsonString)"
66
+ },
67
+ {
68
+ id: "SEC-003",
69
+ dimension: "security",
70
+ severity: "HIGH",
71
+ pattern: /document\.write\s*\(/,
72
+ title: "XSS: Use of document.write()",
73
+ description: "document.write() can inject arbitrary HTML, enabling XSS attacks.",
74
+ suggestion: "Use DOM manipulation methods like createElement and appendChild."
75
+ },
76
+ // Correctness rules
77
+ {
78
+ id: "COR-001",
79
+ dimension: "correctness",
80
+ severity: "HIGH",
81
+ pattern: /\.\w+\s*\(\s*\)\s*\.\s*\w+\s*\(\s*\)\s*\.\s*\w+\s*\(\s*\)/,
82
+ title: "Potential null reference: Long method chain",
83
+ description: "Chained method calls without null checks can throw TypeError at runtime.",
84
+ suggestion: "Add null checks or use optional chaining (?.).",
85
+ fixExample: "obj?.method1()?.method2()?.method3()"
86
+ },
87
+ {
88
+ id: "COR-002",
89
+ dimension: "correctness",
90
+ severity: "MEDIUM",
91
+ pattern: /==(?!=)/,
92
+ title: "Loose equality comparison",
93
+ description: "Using == instead of === can produce unexpected type coercion results.",
94
+ suggestion: "Use === for strict equality comparison.",
95
+ fixExample: "value === expected"
96
+ },
97
+ // Performance rules
98
+ {
99
+ id: "PER-001",
100
+ dimension: "performance",
101
+ severity: "HIGH",
102
+ pattern: /for\s*\(.*await/,
103
+ title: "Sequential async in loop",
104
+ description: "Using await inside a for loop processes operations sequentially, often causing N+1 latency.",
105
+ suggestion: "Use Promise.all() for parallel execution, or process in batches.",
106
+ fixExample: "await Promise.all(items.map(i => fetch(i)))"
107
+ },
108
+ {
109
+ id: "PER-002",
110
+ dimension: "performance",
111
+ severity: "MEDIUM",
112
+ pattern: /console\.log\s*\(/,
113
+ title: "Console.log in production code",
114
+ description: "Leftover console.log calls add overhead and may leak sensitive data.",
115
+ suggestion: "Remove console.log or use a proper logging library."
116
+ },
117
+ // Maintainability rules
118
+ {
119
+ id: "MAIN-001",
120
+ dimension: "maintainability",
121
+ severity: "LOW",
122
+ pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*HACK/i,
123
+ title: "Unresolved TODO/FIXME comment",
124
+ description: "TODO/FIXME/HACK comments indicate unfinished or problematic code.",
125
+ suggestion: "Resolve the comment or create a tracked issue."
126
+ },
127
+ {
128
+ id: "MAIN-002",
129
+ dimension: "maintainability",
130
+ severity: "LOW",
131
+ pattern: /:\s*any\b/,
132
+ title: "Use of `any` type",
133
+ description: "Using any bypasses TypeScript type checking, reducing type safety.",
134
+ suggestion: "Replace any with a specific type or unknown.",
135
+ fixExample: "const data: unknown = value"
136
+ },
137
+ // Best practices rules
138
+ {
139
+ id: "BP-001",
140
+ dimension: "best-practices",
141
+ severity: "MEDIUM",
142
+ pattern: /var\s+\w/,
143
+ title: "Use of var instead of let/const",
144
+ description: "var has function scope and hoisting, which can cause subtle bugs.",
145
+ suggestion: "Use const for values that don't change, let for those that do.",
146
+ fixExample: "const value = 42"
147
+ }
148
+ ];
149
+ function analyzeDiffs(diffs, enabledRules, disabledRules, severityOverrides) {
150
+ const findings = [];
151
+ const activeRules = filterRules(RULES, enabledRules, disabledRules);
152
+ for (const diff of diffs) {
153
+ for (const hunk of diff.hunks) {
154
+ for (const rule of activeRules) {
155
+ for (const line of hunk.lines) {
156
+ if (!line.startsWith("+")) continue;
157
+ const content = line.slice(1);
158
+ if (!rule.pattern.test(content)) continue;
159
+ const severity = severityOverrides?.[rule.id] ?? rule.severity;
160
+ findings.push({
161
+ id: rule.id,
162
+ dimension: rule.dimension,
163
+ severity,
164
+ title: rule.title,
165
+ description: rule.description,
166
+ file: diff.file,
167
+ line: hunk.newStart + countAddedLinesBefore(hunk, line),
168
+ codeSnippet: content.trim(),
169
+ suggestion: rule.suggestion,
170
+ fixExample: rule.fixExample,
171
+ confidence: "high"
172
+ });
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return findings;
178
+ }
179
+ function analyzeCode(code, file, enabledRules, disabledRules, severityOverrides) {
180
+ const findings = [];
181
+ const activeRules = filterRules(RULES, enabledRules, disabledRules);
182
+ const lines = code.split("\n");
183
+ for (let i = 0; i < lines.length; i++) {
184
+ for (const rule of activeRules) {
185
+ if (!rule.pattern.test(lines[i])) continue;
186
+ const severity = severityOverrides?.[rule.id] ?? rule.severity;
187
+ findings.push({
188
+ id: rule.id,
189
+ dimension: rule.dimension,
190
+ severity,
191
+ title: rule.title,
192
+ description: rule.description,
193
+ file,
194
+ line: i + 1,
195
+ codeSnippet: lines[i].trim(),
196
+ suggestion: rule.suggestion,
197
+ fixExample: rule.fixExample,
198
+ confidence: "high"
199
+ });
200
+ }
201
+ }
202
+ return findings;
203
+ }
204
+ function getAvailableRules() {
205
+ const grouped = {};
206
+ for (const rule of RULES) {
207
+ ;
208
+ (grouped[rule.dimension] ??= []).push(rule);
209
+ }
210
+ return grouped;
211
+ }
212
+ function filterRules(allRules, enabled, disabled) {
213
+ if (enabled && enabled.length > 0) {
214
+ const set = new Set(enabled);
215
+ return allRules.filter((r) => set.has(r.id));
216
+ }
217
+ if (disabled && disabled.length > 0) {
218
+ const set = new Set(disabled);
219
+ return allRules.filter((r) => !set.has(r.id));
220
+ }
221
+ return allRules;
222
+ }
223
+ function countAddedLinesBefore(hunk, targetLine) {
224
+ let count = 0;
225
+ for (const line of hunk.lines) {
226
+ if (line === targetLine) return count;
227
+ if (line.startsWith("+")) count++;
228
+ }
229
+ return count;
230
+ }
231
+
232
+ // src/collectors/diff-collector.ts
233
+ var import_node_child_process = require("child_process");
234
+ var import_node_util = require("util");
235
+
236
+ // src/errors.ts
237
+ var GitCommandError = class _GitCommandError extends Error {
238
+ constructor(options) {
239
+ const msg = options.message ?? `git ${[options.command, ...options.args ?? []].join(" ")} failed${options.exitCode != null ? ` (exit ${options.exitCode})` : ""}`;
240
+ super(msg);
241
+ this.name = "GitCommandError";
242
+ this.command = options.command ?? "git";
243
+ this.args = options.args ?? [];
244
+ this.exitCode = options.exitCode;
245
+ this.cwd = options.cwd;
246
+ if (options.cause && Error.captureStackTrace) {
247
+ Error.captureStackTrace(this, _GitCommandError);
248
+ }
249
+ }
250
+ };
251
+ var ParseError = class extends Error {
252
+ constructor(options) {
253
+ const msg = options.message ?? `Failed to parse ${options.parser} output`;
254
+ super(msg);
255
+ this.name = "ParseError";
256
+ this.parser = options.parser;
257
+ this.rawInput = options.rawInput;
258
+ }
259
+ };
260
+ var ConfigError = class extends Error {
261
+ constructor(options) {
262
+ const msg = options.message ?? `Invalid configuration${options.field ? ` in field '${options.field}'` : ""}`;
263
+ super(msg);
264
+ this.name = "ConfigError";
265
+ this.filePath = options.filePath;
266
+ this.field = options.field;
267
+ }
268
+ };
269
+
270
+ // src/collectors/diff-collector.ts
271
+ var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
272
+ async function collectDiff(options) {
273
+ const { root, targetBranch = "main", commitHash } = options;
274
+ const args = ["diff", "--no-color"];
275
+ if (commitHash) {
276
+ args.push(commitHash);
277
+ } else {
278
+ args.push(targetBranch, "HEAD");
279
+ }
280
+ try {
281
+ const { stdout } = await exec("git", args, { cwd: root, maxBuffer: 50 * 1024 * 1024 });
282
+ return parseDiffOutput(stdout);
283
+ } catch (err) {
284
+ throw new GitCommandError({
285
+ command: "diff",
286
+ args,
287
+ cwd: root,
288
+ message: "Failed to collect git diff",
289
+ cause: err instanceof Error ? err : void 0
290
+ });
291
+ }
292
+ }
293
+ async function collectCommitDiff(options) {
294
+ const { root, hash } = options;
295
+ const args = ["diff", "--no-color", hash];
296
+ try {
297
+ const { stdout } = await exec("git", args, { cwd: root });
298
+ return parseDiffOutput(stdout);
299
+ } catch (err) {
300
+ throw new GitCommandError({
301
+ command: "diff",
302
+ args,
303
+ cwd: root,
304
+ message: `Failed to collect diff for commit ${hash}`,
305
+ cause: err instanceof Error ? err : void 0
306
+ });
307
+ }
308
+ }
309
+ function parseDiffOutput(stdout) {
310
+ if (!stdout.trim()) return [];
311
+ const diffs = [];
312
+ const fileSections = stdout.split(/^diff --git /m).filter((s) => s.trim());
313
+ for (const section of fileSections) {
314
+ const diff = parseFileSection(section);
315
+ if (diff) diffs.push(diff);
316
+ }
317
+ return diffs;
318
+ }
319
+ function parseFileSection(section) {
320
+ const lines = section.split("\n");
321
+ const headerLine = lines[0];
322
+ const pathMatch = headerLine?.match(/^a\/(.+) b\/(.+)/);
323
+ if (!pathMatch) return null;
324
+ const file = pathMatch[2];
325
+ const from = pathMatch[1];
326
+ let status = "modified", currentHunk = null, additions = 0, deletions = 0;
327
+ if (lines.some((l) => l.startsWith("new file mode"))) {
328
+ status = "added";
329
+ } else if (lines.some((l) => l.startsWith("deleted file mode"))) {
330
+ status = "deleted";
331
+ } else if (lines.some((l) => l.startsWith("rename from "))) {
332
+ status = "renamed";
333
+ }
334
+ const hunks = [];
335
+ for (const line of lines) {
336
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
337
+ if (hunkMatch) {
338
+ if (currentHunk) hunks.push(currentHunk);
339
+ currentHunk = {
340
+ header: line,
341
+ oldStart: Number(hunkMatch[1]),
342
+ oldCount: Number(hunkMatch[2] ?? 1),
343
+ newStart: Number(hunkMatch[3]),
344
+ newCount: Number(hunkMatch[4] ?? 1),
345
+ lines: []
346
+ };
347
+ continue;
348
+ }
349
+ if (line.startsWith("+") && currentHunk) {
350
+ additions++;
351
+ currentHunk.lines.push(line);
352
+ } else if (line.startsWith("-") && currentHunk && !line.startsWith("---")) {
353
+ deletions++;
354
+ currentHunk.lines.push(line);
355
+ } else if (line.startsWith(" ") && currentHunk) {
356
+ currentHunk.lines.push(line);
357
+ }
358
+ }
359
+ if (currentHunk) hunks.push(currentHunk);
360
+ return {
361
+ file,
362
+ from: status === "renamed" || status === "deleted" ? from : void 0,
363
+ status,
364
+ additions,
365
+ deletions,
366
+ hunks
367
+ };
368
+ }
369
+
370
+ // src/collectors/project-detector.ts
371
+ var import_node_fs = require("fs");
372
+ var import_node_path = require("path");
373
+ async function detectProject(root) {
374
+ const markers = [
375
+ { type: "node", file: "package.json" },
376
+ { type: "go", file: "go.mod" },
377
+ { type: "python", file: "requirements.txt" },
378
+ { type: "rust", file: "Cargo.toml" }
379
+ ];
380
+ for (const marker of markers) {
381
+ const filePath = (0, import_node_path.join)(root, marker.file);
382
+ if ((0, import_node_fs.existsSync)(filePath)) {
383
+ if (marker.type === "node") {
384
+ return detectNodeProject(root, filePath);
385
+ }
386
+ if (marker.type === "go") {
387
+ return {
388
+ type: "go",
389
+ language: "go",
390
+ packageManager: "go mod",
391
+ root
392
+ };
393
+ }
394
+ if (marker.type === "python") {
395
+ return {
396
+ type: "python",
397
+ language: "python",
398
+ packageManager: "pip",
399
+ root
400
+ };
401
+ }
402
+ }
403
+ }
404
+ return {
405
+ type: "unknown",
406
+ language: "unknown",
407
+ root
408
+ };
409
+ }
410
+ function detectNodeProject(root, packageJsonPath) {
411
+ try {
412
+ const content = (0, import_node_fs.readFileSync)(packageJsonPath, "utf-8");
413
+ const pkg = JSON.parse(content);
414
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
415
+ if (deps.vue || deps.Vue) {
416
+ const version = deps.vue || deps.Vue || "";
417
+ const isVue3 = version.startsWith("^3") || version.startsWith("3");
418
+ return {
419
+ type: "vue",
420
+ framework: isVue3 ? "vue3" : "vue2",
421
+ language: "typescript",
422
+ packageManager: detectPackageManager(root),
423
+ root
424
+ };
425
+ }
426
+ if (deps.react || deps.React) {
427
+ const version = deps.react || deps.React || "";
428
+ const isReact18 = version.startsWith("^18") || version.startsWith("18");
429
+ return {
430
+ type: "react",
431
+ framework: isReact18 ? "react18" : "react",
432
+ language: "typescript",
433
+ packageManager: detectPackageManager(root),
434
+ root
435
+ };
436
+ }
437
+ return {
438
+ type: "node",
439
+ language: "javascript",
440
+ packageManager: detectPackageManager(root),
441
+ root
442
+ };
443
+ } catch {
444
+ return {
445
+ type: "node",
446
+ language: "javascript",
447
+ packageManager: "npm",
448
+ root
449
+ };
450
+ }
451
+ }
452
+ function detectPackageManager(root) {
453
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(root, "pnpm-lock.yaml"))) return "pnpm";
454
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(root, "yarn.lock"))) return "yarn";
455
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(root, "package-lock.json"))) return "npm";
456
+ return "npm";
457
+ }
458
+
459
+ // src/types.ts
460
+ var VERSION = "0.1.0";
461
+
462
+ // src/utils/config.ts
463
+ var import_node_fs2 = require("fs");
464
+ var import_node_path2 = require("path");
465
+ var DEFAULT_CONFIG = {
466
+ rules: {
467
+ enabled: ["COR-001", "SEC-001", "SEC-002", "PER-001", "MAIN-001"],
468
+ disabled: [],
469
+ severityOverrides: {}
470
+ },
471
+ excludePaths: ["node_modules/", "dist/", "coverage/", "vendor/", ".git/"],
472
+ maxFindingsPerFile: 20,
473
+ outputFormat: "markdown",
474
+ targetBranch: "main"
475
+ };
476
+ function mergeConfig(user) {
477
+ return {
478
+ ...DEFAULT_CONFIG,
479
+ ...user,
480
+ rules: {
481
+ ...DEFAULT_CONFIG.rules,
482
+ ...user.rules,
483
+ enabled: user.rules?.enabled ?? DEFAULT_CONFIG.rules.enabled,
484
+ disabled: user.rules?.disabled ?? DEFAULT_CONFIG.rules.disabled,
485
+ severityOverrides: user.rules?.severityOverrides ?? DEFAULT_CONFIG.rules.severityOverrides
486
+ },
487
+ excludePaths: user.excludePaths ?? DEFAULT_CONFIG.excludePaths,
488
+ maxFindingsPerFile: user.maxFindingsPerFile ?? DEFAULT_CONFIG.maxFindingsPerFile,
489
+ outputFormat: user.outputFormat ?? DEFAULT_CONFIG.outputFormat
490
+ };
491
+ }
492
+ function getDefaultConfig() {
493
+ return structuredClone(DEFAULT_CONFIG);
494
+ }
495
+ function loadConfigFile(root, fileName = ".ai-code-reviewer-plus.yml") {
496
+ const filePath = (0, import_node_path2.join)(root, fileName);
497
+ if (!(0, import_node_fs2.existsSync)(filePath)) {
498
+ return {};
499
+ }
500
+ try {
501
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
502
+ return parseSimpleYAML(content);
503
+ } catch (err) {
504
+ throw new ConfigError({
505
+ filePath,
506
+ message: `Failed to load config file: ${err instanceof Error ? err.message : "Unknown error"}`
507
+ });
508
+ }
509
+ }
510
+ function parseSimpleYAML(content) {
511
+ const config = {
512
+ rules: {
513
+ enabled: [],
514
+ disabled: [],
515
+ severityOverrides: {}
516
+ },
517
+ excludePaths: []
518
+ };
519
+ const lines = content.split("\n");
520
+ let currentSection = "";
521
+ for (const line of lines) {
522
+ const trimmed = line.trim();
523
+ if (trimmed.startsWith("#") || !trimmed) continue;
524
+ if (trimmed.startsWith("rules:")) {
525
+ currentSection = "rules";
526
+ } else if (trimmed.startsWith("excludePaths:")) {
527
+ currentSection = "excludePaths";
528
+ } else if (trimmed.startsWith("maxFindingsPerFile:")) {
529
+ const value = trimmed.split(":")[1]?.trim();
530
+ if (value) config.maxFindingsPerFile = Number(value);
531
+ } else if (trimmed.startsWith("outputFormat:")) {
532
+ const value = trimmed.split(":")[1]?.trim();
533
+ if (value && (value === "markdown" || value === "json")) {
534
+ config.outputFormat = value;
535
+ }
536
+ } else if (trimmed.startsWith("- ") && currentSection) {
537
+ const value = trimmed.slice(2);
538
+ if (currentSection === "rules") {
539
+ const ruleId = value.split(" ")[0];
540
+ if (ruleId) {
541
+ config.rules.enabled.push(ruleId);
542
+ }
543
+ } else if (currentSection === "excludePaths") {
544
+ config.excludePaths.push(value);
545
+ }
546
+ }
547
+ }
548
+ return config;
549
+ }
550
+
551
+ // src/utils/format.ts
552
+ function formatFinding(finding) {
553
+ const severityEmoji = {
554
+ BLOCKER: "\u{1F6AB}",
555
+ HIGH: "\u{1F534}",
556
+ MEDIUM: "\u{1F7E1}",
557
+ LOW: "\u{1F7E2}",
558
+ SUGGESTION: "\u{1F4A1}"
559
+ };
560
+ const emoji = severityEmoji[finding.severity];
561
+ const location = finding.line ? `:${finding.line}` : "";
562
+ let output = `- ${emoji} **${finding.id}** [${finding.severity}] \`${finding.file}${location}\`
563
+ `;
564
+ output += ` **${finding.title}**
565
+ `;
566
+ output += ` ${finding.description}
567
+ `;
568
+ if (finding.codeSnippet) {
569
+ output += ` \`\`\`
570
+ ${finding.codeSnippet}
571
+ \`\`\`
572
+ `;
573
+ }
574
+ if (finding.suggestion) {
575
+ output += ` \u{1F4A1} ${finding.suggestion}
576
+ `;
577
+ }
578
+ if (finding.fixExample) {
579
+ output += ` **Fix:**
580
+ \`\`\`
581
+ ${finding.fixExample}
582
+ \`\`\`
583
+ `;
584
+ }
585
+ const confidenceEmoji = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
586
+ output += ` Confidence: ${confidenceEmoji[finding.confidence]}
587
+ `;
588
+ return output;
589
+ }
590
+ function formatSummary(result) {
591
+ const { summary, duration } = result;
592
+ let output = `## \u{1F4CA} Review Summary
593
+
594
+ `;
595
+ output += `- **Total findings**: ${summary.total}
596
+ `;
597
+ output += `- **\u{1F6AB} Blockers**: ${summary.blockers}
598
+ `;
599
+ output += `- **\u{1F534} High**: ${summary.high}
600
+ `;
601
+ output += `- **\u{1F7E1} Medium**: ${summary.medium}
602
+ `;
603
+ output += `- **\u{1F7E2} Low**: ${summary.low}
604
+ `;
605
+ output += `- **\u{1F4A1} Suggestions**: ${summary.suggestions}
606
+ `;
607
+ output += `- **Files reviewed**: ${result.filesReviewed.length}
608
+ `;
609
+ output += `- **Duration**: ${formatDuration(duration)}
610
+ `;
611
+ return output;
612
+ }
613
+ function formatReviewReport(result) {
614
+ let output = `# \u{1F50D} Code Review Report
615
+
616
+ `;
617
+ output += formatSummary(result);
618
+ output += `
619
+ ---
620
+
621
+ `;
622
+ const blockers = result.findings.filter((f) => f.severity === "BLOCKER");
623
+ const high = result.findings.filter((f) => f.severity === "HIGH");
624
+ const medium = result.findings.filter((f) => f.severity === "MEDIUM");
625
+ const low = result.findings.filter((f) => f.severity === "LOW");
626
+ const suggestions = result.findings.filter((f) => f.severity === "SUGGESTION");
627
+ if (blockers.length > 0) {
628
+ output += `## \u{1F6AB} Blockers (Must Fix)
629
+
630
+ `;
631
+ for (const f of blockers) {
632
+ output += `${formatFinding(f)}
633
+ `;
634
+ }
635
+ }
636
+ if (high.length > 0) {
637
+ output += `## \u{1F534} High Priority
638
+
639
+ `;
640
+ for (const f of high) {
641
+ output += `${formatFinding(f)}
642
+ `;
643
+ }
644
+ }
645
+ if (medium.length > 0) {
646
+ output += `## \u{1F7E1} Medium Priority
647
+
648
+ `;
649
+ for (const f of medium) {
650
+ output += `${formatFinding(f)}
651
+ `;
652
+ }
653
+ }
654
+ if (low.length > 0) {
655
+ output += `## \u{1F7E2} Low Priority
656
+
657
+ `;
658
+ for (const f of low) {
659
+ output += `${formatFinding(f)}
660
+ `;
661
+ }
662
+ }
663
+ if (suggestions.length > 0) {
664
+ output += `## \u{1F4A1} Suggestions
665
+
666
+ `;
667
+ for (const f of suggestions) {
668
+ output += `${formatFinding(f)}
669
+ `;
670
+ }
671
+ }
672
+ return output;
673
+ }
674
+ function formatDiffSummary(diff) {
675
+ const statusEmoji = {
676
+ added: "\u2795",
677
+ deleted: "\u274C",
678
+ modified: "\u{1F4DD}",
679
+ renamed: "\u{1F4E6}"
680
+ };
681
+ const emoji = statusEmoji[diff.status];
682
+ const renameInfo = diff.status === "renamed" && diff.from ? ` (${diff.from} \u2192 ${diff.file})` : "";
683
+ return `- ${emoji} \`${diff.file}\` +${diff.additions}/-${diff.deletions}${renameInfo}`;
684
+ }
685
+ function formatDuration(ms) {
686
+ if (ms < 1e3) return `${ms}ms`;
687
+ const seconds = Math.round(ms / 1e3);
688
+ if (seconds < 60) return `${seconds}s`;
689
+ const minutes = Math.floor(seconds / 60);
690
+ const secs = seconds % 60;
691
+ return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
692
+ }
693
+ // Annotate the CommonJS export names for ESM import in node:
694
+ 0 && (module.exports = {
695
+ ConfigError,
696
+ GitCommandError,
697
+ ParseError,
698
+ VERSION,
699
+ analyzeCode,
700
+ analyzeDiffs,
701
+ collectCommitDiff,
702
+ collectDiff,
703
+ detectProject,
704
+ formatDiffSummary,
705
+ formatDuration,
706
+ formatFinding,
707
+ formatReviewReport,
708
+ formatSummary,
709
+ getAvailableRules,
710
+ getDefaultConfig,
711
+ loadConfigFile,
712
+ mergeConfig
713
+ });