codesift-mcp 0.1.0 → 0.2.1

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 (299) hide show
  1. package/LICENSE +66 -21
  2. package/README.md +402 -56
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +11 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +177 -67
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/cli/help.d.ts +1 -1
  11. package/dist/cli/help.d.ts.map +1 -1
  12. package/dist/cli/help.js +157 -0
  13. package/dist/cli/help.js.map +1 -1
  14. package/dist/cli/hooks.d.ts +3 -0
  15. package/dist/cli/hooks.d.ts.map +1 -0
  16. package/dist/cli/hooks.js +163 -0
  17. package/dist/cli/hooks.js.map +1 -0
  18. package/dist/cli/setup.d.ts +25 -0
  19. package/dist/cli/setup.d.ts.map +1 -0
  20. package/dist/cli/setup.js +400 -0
  21. package/dist/cli/setup.js.map +1 -0
  22. package/dist/config.d.ts +2 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +2 -0
  25. package/dist/config.js.map +1 -1
  26. package/dist/formatters-shortening.d.ts +7 -0
  27. package/dist/formatters-shortening.d.ts.map +1 -0
  28. package/dist/formatters-shortening.js +68 -0
  29. package/dist/formatters-shortening.js.map +1 -0
  30. package/dist/formatters.d.ts +314 -0
  31. package/dist/formatters.d.ts.map +1 -0
  32. package/dist/formatters.js +396 -0
  33. package/dist/formatters.js.map +1 -0
  34. package/dist/instructions.d.ts +6 -0
  35. package/dist/instructions.d.ts.map +1 -0
  36. package/dist/instructions.js +72 -0
  37. package/dist/instructions.js.map +1 -0
  38. package/dist/lsp/lsp-client.d.ts +21 -0
  39. package/dist/lsp/lsp-client.d.ts.map +1 -0
  40. package/dist/lsp/lsp-client.js +122 -0
  41. package/dist/lsp/lsp-client.js.map +1 -0
  42. package/dist/lsp/lsp-manager.d.ts +12 -0
  43. package/dist/lsp/lsp-manager.d.ts.map +1 -0
  44. package/dist/lsp/lsp-manager.js +82 -0
  45. package/dist/lsp/lsp-manager.js.map +1 -0
  46. package/dist/lsp/lsp-servers.d.ts +13 -0
  47. package/dist/lsp/lsp-servers.d.ts.map +1 -0
  48. package/dist/lsp/lsp-servers.js +57 -0
  49. package/dist/lsp/lsp-servers.js.map +1 -0
  50. package/dist/lsp/lsp-tools.d.ts +67 -0
  51. package/dist/lsp/lsp-tools.d.ts.map +1 -0
  52. package/dist/lsp/lsp-tools.js +359 -0
  53. package/dist/lsp/lsp-tools.js.map +1 -0
  54. package/dist/parser/extractors/_shared.d.ts +11 -0
  55. package/dist/parser/extractors/_shared.d.ts.map +1 -0
  56. package/dist/parser/extractors/_shared.js +38 -0
  57. package/dist/parser/extractors/_shared.js.map +1 -0
  58. package/dist/parser/extractors/astro.d.ts +15 -0
  59. package/dist/parser/extractors/astro.d.ts.map +1 -0
  60. package/dist/parser/extractors/astro.js +104 -0
  61. package/dist/parser/extractors/astro.js.map +1 -0
  62. package/dist/parser/extractors/conversation.d.ts +16 -0
  63. package/dist/parser/extractors/conversation.d.ts.map +1 -0
  64. package/dist/parser/extractors/conversation.js +196 -0
  65. package/dist/parser/extractors/conversation.js.map +1 -0
  66. package/dist/parser/extractors/go.d.ts.map +1 -1
  67. package/dist/parser/extractors/go.js +22 -45
  68. package/dist/parser/extractors/go.js.map +1 -1
  69. package/dist/parser/extractors/python.d.ts +1 -1
  70. package/dist/parser/extractors/python.d.ts.map +1 -1
  71. package/dist/parser/extractors/python.js +19 -50
  72. package/dist/parser/extractors/python.js.map +1 -1
  73. package/dist/parser/extractors/rust.d.ts +1 -1
  74. package/dist/parser/extractors/rust.d.ts.map +1 -1
  75. package/dist/parser/extractors/rust.js +7 -34
  76. package/dist/parser/extractors/rust.js.map +1 -1
  77. package/dist/parser/extractors/typescript.d.ts +1 -1
  78. package/dist/parser/extractors/typescript.d.ts.map +1 -1
  79. package/dist/parser/extractors/typescript.js +99 -68
  80. package/dist/parser/extractors/typescript.js.map +1 -1
  81. package/dist/parser/parser-manager.d.ts.map +1 -1
  82. package/dist/parser/parser-manager.js +12 -2
  83. package/dist/parser/parser-manager.js.map +1 -1
  84. package/dist/parser/symbol-extractor.d.ts +2 -0
  85. package/dist/parser/symbol-extractor.d.ts.map +1 -1
  86. package/dist/parser/symbol-extractor.js +2 -0
  87. package/dist/parser/symbol-extractor.js.map +1 -1
  88. package/dist/register-tools.d.ts +127 -0
  89. package/dist/register-tools.d.ts.map +1 -0
  90. package/dist/register-tools.js +1453 -0
  91. package/dist/register-tools.js.map +1 -0
  92. package/dist/retrieval/codebase-retrieval.d.ts +4 -26
  93. package/dist/retrieval/codebase-retrieval.d.ts.map +1 -1
  94. package/dist/retrieval/codebase-retrieval.js +105 -403
  95. package/dist/retrieval/codebase-retrieval.js.map +1 -1
  96. package/dist/retrieval/retrieval-constants.d.ts +27 -0
  97. package/dist/retrieval/retrieval-constants.d.ts.map +1 -0
  98. package/dist/retrieval/retrieval-constants.js +27 -0
  99. package/dist/retrieval/retrieval-constants.js.map +1 -0
  100. package/dist/retrieval/retrieval-schemas.d.ts +107 -0
  101. package/dist/retrieval/retrieval-schemas.d.ts.map +1 -0
  102. package/dist/retrieval/retrieval-schemas.js +102 -0
  103. package/dist/retrieval/retrieval-schemas.js.map +1 -0
  104. package/dist/retrieval/retrieval-utils.d.ts +40 -0
  105. package/dist/retrieval/retrieval-utils.d.ts.map +1 -0
  106. package/dist/retrieval/retrieval-utils.js +139 -0
  107. package/dist/retrieval/retrieval-utils.js.map +1 -0
  108. package/dist/retrieval/semantic-handlers.d.ts +8 -0
  109. package/dist/retrieval/semantic-handlers.d.ts.map +1 -0
  110. package/dist/retrieval/semantic-handlers.js +152 -0
  111. package/dist/retrieval/semantic-handlers.js.map +1 -0
  112. package/dist/search/bm25.d.ts +6 -1
  113. package/dist/search/bm25.d.ts.map +1 -1
  114. package/dist/search/bm25.js +95 -32
  115. package/dist/search/bm25.js.map +1 -1
  116. package/dist/search/chunker.d.ts +10 -0
  117. package/dist/search/chunker.d.ts.map +1 -1
  118. package/dist/search/chunker.js +63 -11
  119. package/dist/search/chunker.js.map +1 -1
  120. package/dist/search/reranker.d.ts +15 -0
  121. package/dist/search/reranker.d.ts.map +1 -0
  122. package/dist/search/reranker.js +126 -0
  123. package/dist/search/reranker.js.map +1 -0
  124. package/dist/search/semantic.d.ts +1 -1
  125. package/dist/search/semantic.d.ts.map +1 -1
  126. package/dist/search/semantic.js +40 -45
  127. package/dist/search/semantic.js.map +1 -1
  128. package/dist/server-helpers.d.ts +29 -0
  129. package/dist/server-helpers.d.ts.map +1 -0
  130. package/dist/server-helpers.js +312 -0
  131. package/dist/server-helpers.js.map +1 -0
  132. package/dist/server.d.ts +1 -1
  133. package/dist/server.d.ts.map +1 -1
  134. package/dist/server.js +11 -271
  135. package/dist/server.js.map +1 -1
  136. package/dist/storage/_shared.d.ts +9 -0
  137. package/dist/storage/_shared.d.ts.map +1 -0
  138. package/dist/storage/_shared.js +26 -0
  139. package/dist/storage/_shared.js.map +1 -0
  140. package/dist/storage/chunk-store.d.ts.map +1 -1
  141. package/dist/storage/chunk-store.js +23 -63
  142. package/dist/storage/chunk-store.js.map +1 -1
  143. package/dist/storage/embedding-store.d.ts +6 -3
  144. package/dist/storage/embedding-store.d.ts.map +1 -1
  145. package/dist/storage/embedding-store.js +54 -30
  146. package/dist/storage/embedding-store.js.map +1 -1
  147. package/dist/storage/graph-store.d.ts +48 -0
  148. package/dist/storage/graph-store.d.ts.map +1 -0
  149. package/dist/storage/graph-store.js +52 -0
  150. package/dist/storage/graph-store.js.map +1 -0
  151. package/dist/storage/index-store.d.ts +5 -0
  152. package/dist/storage/index-store.d.ts.map +1 -1
  153. package/dist/storage/index-store.js +28 -16
  154. package/dist/storage/index-store.js.map +1 -1
  155. package/dist/storage/registry.d.ts +4 -0
  156. package/dist/storage/registry.d.ts.map +1 -1
  157. package/dist/storage/registry.js +16 -16
  158. package/dist/storage/registry.js.map +1 -1
  159. package/dist/storage/usage-stats.d.ts +6 -0
  160. package/dist/storage/usage-stats.d.ts.map +1 -1
  161. package/dist/storage/usage-stats.js +59 -11
  162. package/dist/storage/usage-stats.js.map +1 -1
  163. package/dist/storage/usage-tracker.d.ts +3 -0
  164. package/dist/storage/usage-tracker.d.ts.map +1 -1
  165. package/dist/storage/usage-tracker.js +50 -132
  166. package/dist/storage/usage-tracker.js.map +1 -1
  167. package/dist/storage/watcher.d.ts +2 -1
  168. package/dist/storage/watcher.d.ts.map +1 -1
  169. package/dist/storage/watcher.js +16 -16
  170. package/dist/storage/watcher.js.map +1 -1
  171. package/dist/tools/ast-query-tools.d.ts +29 -0
  172. package/dist/tools/ast-query-tools.d.ts.map +1 -0
  173. package/dist/tools/ast-query-tools.js +110 -0
  174. package/dist/tools/ast-query-tools.js.map +1 -0
  175. package/dist/tools/boundary-tools.d.ts +31 -0
  176. package/dist/tools/boundary-tools.d.ts.map +1 -0
  177. package/dist/tools/boundary-tools.js +62 -0
  178. package/dist/tools/boundary-tools.js.map +1 -0
  179. package/dist/tools/clone-tools.d.ts +35 -0
  180. package/dist/tools/clone-tools.d.ts.map +1 -0
  181. package/dist/tools/clone-tools.js +181 -0
  182. package/dist/tools/clone-tools.js.map +1 -0
  183. package/dist/tools/community-tools.d.ts +23 -0
  184. package/dist/tools/community-tools.d.ts.map +1 -0
  185. package/dist/tools/community-tools.js +297 -0
  186. package/dist/tools/community-tools.js.map +1 -0
  187. package/dist/tools/complexity-tools.d.ts +34 -0
  188. package/dist/tools/complexity-tools.d.ts.map +1 -0
  189. package/dist/tools/complexity-tools.js +135 -0
  190. package/dist/tools/complexity-tools.js.map +1 -0
  191. package/dist/tools/context-tools.d.ts +44 -3
  192. package/dist/tools/context-tools.d.ts.map +1 -1
  193. package/dist/tools/context-tools.js +329 -99
  194. package/dist/tools/context-tools.js.map +1 -1
  195. package/dist/tools/conversation-tools.d.ts +107 -0
  196. package/dist/tools/conversation-tools.d.ts.map +1 -0
  197. package/dist/tools/conversation-tools.js +419 -0
  198. package/dist/tools/conversation-tools.js.map +1 -0
  199. package/dist/tools/coordinator-tools.d.ts +73 -0
  200. package/dist/tools/coordinator-tools.d.ts.map +1 -0
  201. package/dist/tools/coordinator-tools.js +153 -0
  202. package/dist/tools/coordinator-tools.js.map +1 -0
  203. package/dist/tools/cross-repo-tools.d.ts +43 -0
  204. package/dist/tools/cross-repo-tools.d.ts.map +1 -0
  205. package/dist/tools/cross-repo-tools.js +55 -0
  206. package/dist/tools/cross-repo-tools.js.map +1 -0
  207. package/dist/tools/diff-tools.d.ts +4 -1
  208. package/dist/tools/diff-tools.d.ts.map +1 -1
  209. package/dist/tools/diff-tools.js +23 -5
  210. package/dist/tools/diff-tools.js.map +1 -1
  211. package/dist/tools/frequency-tools.d.ts +46 -0
  212. package/dist/tools/frequency-tools.d.ts.map +1 -0
  213. package/dist/tools/frequency-tools.js +184 -0
  214. package/dist/tools/frequency-tools.js.map +1 -0
  215. package/dist/tools/generate-tools.d.ts.map +1 -1
  216. package/dist/tools/generate-tools.js +13 -2
  217. package/dist/tools/generate-tools.js.map +1 -1
  218. package/dist/tools/graph-tools.d.ts +44 -11
  219. package/dist/tools/graph-tools.d.ts.map +1 -1
  220. package/dist/tools/graph-tools.js +147 -104
  221. package/dist/tools/graph-tools.js.map +1 -1
  222. package/dist/tools/hotspot-tools.d.ts +24 -0
  223. package/dist/tools/hotspot-tools.d.ts.map +1 -0
  224. package/dist/tools/hotspot-tools.js +122 -0
  225. package/dist/tools/hotspot-tools.js.map +1 -0
  226. package/dist/tools/impact-tools.d.ts +13 -0
  227. package/dist/tools/impact-tools.d.ts.map +1 -0
  228. package/dist/tools/impact-tools.js +238 -0
  229. package/dist/tools/impact-tools.js.map +1 -0
  230. package/dist/tools/index-tools.d.ts +44 -3
  231. package/dist/tools/index-tools.d.ts.map +1 -1
  232. package/dist/tools/index-tools.js +530 -222
  233. package/dist/tools/index-tools.js.map +1 -1
  234. package/dist/tools/memory-tools.d.ts +35 -0
  235. package/dist/tools/memory-tools.d.ts.map +1 -0
  236. package/dist/tools/memory-tools.js +229 -0
  237. package/dist/tools/memory-tools.js.map +1 -0
  238. package/dist/tools/outline-tools.d.ts +24 -13
  239. package/dist/tools/outline-tools.d.ts.map +1 -1
  240. package/dist/tools/outline-tools.js +113 -87
  241. package/dist/tools/outline-tools.js.map +1 -1
  242. package/dist/tools/pattern-tools.d.ts +32 -0
  243. package/dist/tools/pattern-tools.d.ts.map +1 -0
  244. package/dist/tools/pattern-tools.js +116 -0
  245. package/dist/tools/pattern-tools.js.map +1 -0
  246. package/dist/tools/report-tools.d.ts +5 -0
  247. package/dist/tools/report-tools.d.ts.map +1 -0
  248. package/dist/tools/report-tools.js +167 -0
  249. package/dist/tools/report-tools.js.map +1 -0
  250. package/dist/tools/review-diff-tools.d.ts +148 -0
  251. package/dist/tools/review-diff-tools.d.ts.map +1 -0
  252. package/dist/tools/review-diff-tools.js +852 -0
  253. package/dist/tools/review-diff-tools.js.map +1 -0
  254. package/dist/tools/route-tools.d.ts +32 -0
  255. package/dist/tools/route-tools.d.ts.map +1 -0
  256. package/dist/tools/route-tools.js +276 -0
  257. package/dist/tools/route-tools.js.map +1 -0
  258. package/dist/tools/search-ranker.d.ts +5 -0
  259. package/dist/tools/search-ranker.d.ts.map +1 -0
  260. package/dist/tools/search-ranker.js +142 -0
  261. package/dist/tools/search-ranker.js.map +1 -0
  262. package/dist/tools/search-tools.d.ts +24 -1
  263. package/dist/tools/search-tools.d.ts.map +1 -1
  264. package/dist/tools/search-tools.js +459 -225
  265. package/dist/tools/search-tools.js.map +1 -1
  266. package/dist/tools/secret-tools.d.ts +104 -0
  267. package/dist/tools/secret-tools.d.ts.map +1 -0
  268. package/dist/tools/secret-tools.js +410 -0
  269. package/dist/tools/secret-tools.js.map +1 -0
  270. package/dist/tools/symbol-tools.d.ts +90 -2
  271. package/dist/tools/symbol-tools.d.ts.map +1 -1
  272. package/dist/tools/symbol-tools.js +576 -42
  273. package/dist/tools/symbol-tools.js.map +1 -1
  274. package/dist/types.d.ts +34 -1
  275. package/dist/types.d.ts.map +1 -1
  276. package/dist/utils/framework-detect.d.ts +5 -0
  277. package/dist/utils/framework-detect.d.ts.map +1 -0
  278. package/dist/utils/framework-detect.js +36 -0
  279. package/dist/utils/framework-detect.js.map +1 -0
  280. package/dist/utils/glob.d.ts +19 -0
  281. package/dist/utils/glob.d.ts.map +1 -0
  282. package/dist/utils/glob.js +74 -0
  283. package/dist/utils/glob.js.map +1 -0
  284. package/dist/utils/import-graph.d.ts +29 -0
  285. package/dist/utils/import-graph.d.ts.map +1 -0
  286. package/dist/utils/import-graph.js +125 -0
  287. package/dist/utils/import-graph.js.map +1 -0
  288. package/dist/utils/test-file.d.ts.map +1 -1
  289. package/dist/utils/test-file.js +1 -0
  290. package/dist/utils/test-file.js.map +1 -1
  291. package/dist/utils/walk.d.ts +45 -0
  292. package/dist/utils/walk.d.ts.map +1 -0
  293. package/dist/utils/walk.js +87 -0
  294. package/dist/utils/walk.js.map +1 -0
  295. package/package.json +12 -5
  296. package/rules/codesift.md +187 -0
  297. package/rules/codesift.mdc +192 -0
  298. package/rules/codex.md +187 -0
  299. package/rules/gemini.md +187 -0
@@ -0,0 +1,852 @@
1
+ /**
2
+ * review-diff-tools.ts
3
+ *
4
+ * Types, tier assignment, scoring, verdict, and orchestrator for the review_diff MCP tool.
5
+ * Pure functions + one async orchestrator that fans out sub-checks.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import path from "node:path";
9
+ import { changedSymbols } from "./diff-tools.js";
10
+ import { getCodeIndex } from "./index-tools.js";
11
+ import { impactAnalysis } from "./impact-tools.js";
12
+ import { scanSecrets } from "./secret-tools.js";
13
+ import { findDeadCode } from "./symbol-tools.js";
14
+ import { searchPatterns, listPatterns } from "./pattern-tools.js";
15
+ import { analyzeHotspots } from "./hotspot-tools.js";
16
+ import { analyzeComplexity } from "./complexity-tools.js";
17
+ import { validateGitRef } from "../utils/git-validation.js";
18
+ import { isTestFile } from "../utils/test-file.js";
19
+ import picomatch from "picomatch";
20
+ // ---------------------------------------------------------------------------
21
+ // Tier assignment
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * Returns the tier (1 | 2 | 3) for a given check name.
25
+ *
26
+ * Tier 1 — critical (−20 per finding): secrets, breaking
27
+ * Tier 2 — important (−5 per finding): coupling, complexity, dead-code,
28
+ * blast-radius, bug-patterns
29
+ * Tier 3 — advisory (−1 per finding, only if no T1/T2): test-gaps, hotspots
30
+ */
31
+ export function findingTier(check) {
32
+ switch (check) {
33
+ case "secrets":
34
+ case "breaking":
35
+ return 1;
36
+ case "coupling":
37
+ case "complexity":
38
+ case "dead-code":
39
+ case "blast-radius":
40
+ case "bug-patterns":
41
+ return 2;
42
+ default:
43
+ return 3;
44
+ }
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Score calculation
48
+ // ---------------------------------------------------------------------------
49
+ /**
50
+ * Calculates a 0-100 quality score from findings and check results.
51
+ *
52
+ * Deductions:
53
+ * - T1 findings: −20 each, floor at 0
54
+ * - T2 findings: −5 each, floor at 20 (overridden by T1 floor)
55
+ * - T3 findings: −1 each, floor at 50 (only applied when there are no T1/T2 findings)
56
+ * - Errored checks: −3 each
57
+ * - Final floor: 0
58
+ */
59
+ export function calculateScore(findings, checks) {
60
+ const t1Count = findings.filter((f) => findingTier(f.check) === 1).length;
61
+ const t2Count = findings.filter((f) => findingTier(f.check) === 2).length;
62
+ const t3Count = findings.filter((f) => findingTier(f.check) === 3).length;
63
+ const errorCount = checks.filter((c) => c.status === "error").length;
64
+ let score = 100;
65
+ // Tier 1 deductions
66
+ score -= t1Count * 20;
67
+ if (score < 0)
68
+ score = 0;
69
+ // Tier 2 deductions (floor 20, but T1 can override below 20)
70
+ const afterT2 = score - t2Count * 5;
71
+ if (t1Count === 0) {
72
+ // T2 floor is 20 only when no T1 findings
73
+ score = Math.max(afterT2, 20);
74
+ }
75
+ else {
76
+ // T1 already applied; T2 can further reduce but overall floor is 0
77
+ score = Math.max(afterT2, 0);
78
+ }
79
+ // Tier 3 deductions (floor 50, only when no T1/T2 findings)
80
+ if (t1Count === 0 && t2Count === 0) {
81
+ const afterT3 = score - t3Count * 1;
82
+ score = Math.max(afterT3, 50);
83
+ }
84
+ // Error deductions
85
+ score -= errorCount * 3;
86
+ // Final floor
87
+ return Math.max(score, 0);
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Verdict determination
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Determines the overall verdict from check statuses.
94
+ *
95
+ * - Any "fail" → "fail"
96
+ * - Any "warn" (and no "fail") → "warn"
97
+ * - Otherwise → "pass"
98
+ * - "timeout" and "error" do not affect verdict direction
99
+ */
100
+ export function determineVerdict(checks) {
101
+ const hasFail = checks.some((c) => c.status === "fail");
102
+ if (hasFail)
103
+ return "fail";
104
+ const hasWarn = checks.some((c) => c.status === "warn");
105
+ if (hasWarn)
106
+ return "warn";
107
+ return "pass";
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // All known check names
111
+ // ---------------------------------------------------------------------------
112
+ const ALL_CHECKS = [
113
+ "secrets",
114
+ "breaking",
115
+ "coupling",
116
+ "complexity",
117
+ "dead-code",
118
+ "blast-radius",
119
+ "bug-patterns",
120
+ "test-gaps",
121
+ "hotspots",
122
+ ];
123
+ const DEFAULT_MAX_FILES = 50;
124
+ const DEFAULT_CHECK_TIMEOUT_MS = 30_000;
125
+ const HEAD_TILDE_PATTERN = /^HEAD~\d+$/;
126
+ function withTimeout(promise, ms) {
127
+ return Promise.race([
128
+ promise,
129
+ new Promise((resolve) => setTimeout(() => resolve({ status: "timeout" }), ms)),
130
+ ]);
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Check adapters
134
+ // ---------------------------------------------------------------------------
135
+ /**
136
+ * Blast-radius check: run impactAnalysis and map affected_symbols to T2 findings.
137
+ */
138
+ export async function checkBlastRadius(index, since, until) {
139
+ const start = Date.now();
140
+ try {
141
+ const result = await impactAnalysis(index.repo, since, { until });
142
+ const MAX_BLAST_FINDINGS = 10;
143
+ const allFindings = result.affected_symbols.map((sym) => ({
144
+ check: "blast-radius",
145
+ severity: "warn",
146
+ message: `Symbol "${sym.name}" in ${sym.file} is affected by changes`,
147
+ file: sym.file,
148
+ symbol: sym.name,
149
+ }));
150
+ const findings = allFindings.slice(0, MAX_BLAST_FINDINGS);
151
+ const totalCount = allFindings.length;
152
+ return {
153
+ check: "blast-radius",
154
+ status: findings.length > 0 ? "warn" : "pass",
155
+ findings,
156
+ duration_ms: Date.now() - start,
157
+ summary: totalCount > 0
158
+ ? `${totalCount} affected symbol(s) found${totalCount > MAX_BLAST_FINDINGS ? ` (showing ${MAX_BLAST_FINDINGS})` : ""}`
159
+ : "No blast radius detected",
160
+ };
161
+ }
162
+ catch (err) {
163
+ return {
164
+ check: "blast-radius",
165
+ status: "error",
166
+ findings: [],
167
+ duration_ms: Date.now() - start,
168
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
169
+ };
170
+ }
171
+ }
172
+ /**
173
+ * Secrets check: run scanSecrets scoped to changedFiles and map findings to T1.
174
+ */
175
+ export async function checkSecrets(index, changedFiles) {
176
+ const start = Date.now();
177
+ try {
178
+ // Build a glob pattern that matches any of the changed files
179
+ const filePattern = changedFiles.length === 1
180
+ ? changedFiles[0]
181
+ : `{${changedFiles.join(",")}}`;
182
+ const result = await scanSecrets(index.repo, { file_pattern: filePattern, min_confidence: "high" });
183
+ const findings = result.findings.map((f) => ({
184
+ check: "secrets",
185
+ severity: "error",
186
+ message: `Secret detected: ${f.rule} (${f.severity}) — ${f.masked_secret}`,
187
+ file: f.file,
188
+ line: f.line,
189
+ }));
190
+ return {
191
+ check: "secrets",
192
+ status: findings.length > 0 ? "fail" : "pass",
193
+ findings,
194
+ duration_ms: Date.now() - start,
195
+ summary: findings.length > 0
196
+ ? `${findings.length} secret(s) detected`
197
+ : "No secrets detected",
198
+ };
199
+ }
200
+ catch (err) {
201
+ return {
202
+ check: "secrets",
203
+ status: "error",
204
+ findings: [],
205
+ duration_ms: Date.now() - start,
206
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
207
+ };
208
+ }
209
+ }
210
+ /**
211
+ * Dead-code check: run findDeadCode scoped to changedFiles and map candidates to T2 findings.
212
+ */
213
+ export async function checkDeadCode(index, changedFiles) {
214
+ const start = Date.now();
215
+ try {
216
+ // Build a glob pattern that matches any of the changed files
217
+ const filePattern = changedFiles.length === 1
218
+ ? changedFiles[0]
219
+ : `{${changedFiles.join(",")}}`;
220
+ const result = await findDeadCode(index.repo, { file_pattern: filePattern });
221
+ const findings = result.candidates.map((c) => ({
222
+ check: "dead-code",
223
+ severity: "warn",
224
+ message: `"${c.name}" appears unused — ${c.reason}`,
225
+ file: c.file,
226
+ line: c.start_line,
227
+ symbol: c.name,
228
+ }));
229
+ return {
230
+ check: "dead-code",
231
+ status: findings.length > 0 ? "warn" : "pass",
232
+ findings,
233
+ duration_ms: Date.now() - start,
234
+ summary: findings.length > 0
235
+ ? `${findings.length} dead-code candidate(s) found`
236
+ : "No dead code detected",
237
+ };
238
+ }
239
+ catch (err) {
240
+ return {
241
+ check: "dead-code",
242
+ status: "error",
243
+ findings: [],
244
+ duration_ms: Date.now() - start,
245
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Bug-patterns check: run all BUILTIN_PATTERNS via searchPatterns, merge and
251
+ * deduplicate findings across patterns.
252
+ */
253
+ export async function checkBugPatterns(index, changedFiles) {
254
+ const start = Date.now();
255
+ try {
256
+ // Build a file_pattern covering all changed files
257
+ const filePattern = changedFiles.length === 1
258
+ ? changedFiles[0]
259
+ : `{${changedFiles.join(",")}}`;
260
+ // Get all built-in pattern names
261
+ const patterns = listPatterns().map((p) => p.name);
262
+ // Run all patterns in parallel
263
+ const results = await Promise.all(patterns.map((p) => searchPatterns(index.repo, p, { file_pattern: filePattern })));
264
+ // Merge matches, dedup by (file, start_line, matched_pattern)
265
+ const seen = new Set();
266
+ const findings = [];
267
+ for (const result of results) {
268
+ for (const match of result.matches) {
269
+ const key = `${match.file}:${match.start_line}:${match.matched_pattern}`;
270
+ if (seen.has(key))
271
+ continue;
272
+ seen.add(key);
273
+ findings.push({
274
+ check: "bug-patterns",
275
+ severity: "warn",
276
+ message: `Pattern match: ${match.matched_pattern} — "${match.context}"`,
277
+ file: match.file,
278
+ line: match.start_line,
279
+ symbol: match.name,
280
+ });
281
+ }
282
+ }
283
+ return {
284
+ check: "bug-patterns",
285
+ status: findings.length > 0 ? "warn" : "pass",
286
+ findings,
287
+ duration_ms: Date.now() - start,
288
+ summary: findings.length > 0
289
+ ? `${findings.length} bug pattern(s) found`
290
+ : "No bug patterns detected",
291
+ };
292
+ }
293
+ catch (err) {
294
+ return {
295
+ check: "bug-patterns",
296
+ status: "error",
297
+ findings: [],
298
+ duration_ms: Date.now() - start,
299
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
300
+ };
301
+ }
302
+ }
303
+ /**
304
+ * Hotspots check: run analyzeHotspots and filter to files in changedFiles.
305
+ * Maps high-churn files to T3 advisory findings.
306
+ */
307
+ export async function checkHotspots(index, changedFiles) {
308
+ const start = Date.now();
309
+ try {
310
+ const changedSet = new Set(changedFiles);
311
+ const result = await analyzeHotspots(index.repo);
312
+ const findings = result.hotspots
313
+ .filter((h) => changedSet.has(h.file))
314
+ .map((h) => ({
315
+ check: "hotspots",
316
+ severity: "warn",
317
+ message: `High churn file: ${h.file} — hotspot_score ${h.hotspot_score} (${h.commits} commits, ${h.lines_changed} lines changed)`,
318
+ file: h.file,
319
+ }));
320
+ return {
321
+ check: "hotspots",
322
+ status: findings.length > 0 ? "warn" : "pass",
323
+ findings,
324
+ duration_ms: Date.now() - start,
325
+ summary: findings.length > 0
326
+ ? `${findings.length} hotspot file(s) in diff`
327
+ : "No hotspot files in diff",
328
+ };
329
+ }
330
+ catch (err) {
331
+ return {
332
+ check: "hotspots",
333
+ status: "error",
334
+ findings: [],
335
+ duration_ms: Date.now() - start,
336
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
337
+ };
338
+ }
339
+ }
340
+ /**
341
+ * Complexity delta check: run analyzeComplexity and filter to functions in
342
+ * changedFiles with cyclomatic complexity > 10. Maps to T2 findings.
343
+ */
344
+ export async function checkComplexityDelta(index, changedFiles) {
345
+ const start = Date.now();
346
+ try {
347
+ const changedSet = new Set(changedFiles);
348
+ const result = await analyzeComplexity(index.repo, { top_n: 50 });
349
+ const findings = result.functions
350
+ .filter((fn) => changedSet.has(fn.file) && fn.cyclomatic_complexity > 10)
351
+ .map((fn) => ({
352
+ check: "complexity",
353
+ severity: "warn",
354
+ message: `High complexity: "${fn.name}" in ${fn.file} — cyclomatic complexity ${fn.cyclomatic_complexity} (>${10})`,
355
+ file: fn.file,
356
+ line: fn.start_line,
357
+ symbol: fn.name,
358
+ }));
359
+ return {
360
+ check: "complexity",
361
+ status: findings.length > 0 ? "warn" : "pass",
362
+ findings,
363
+ duration_ms: Date.now() - start,
364
+ summary: findings.length > 0
365
+ ? `${findings.length} high-complexity function(s) in diff`
366
+ : "No high-complexity functions in diff",
367
+ };
368
+ }
369
+ catch (err) {
370
+ return {
371
+ check: "complexity",
372
+ status: "error",
373
+ findings: [],
374
+ duration_ms: Date.now() - start,
375
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
376
+ };
377
+ }
378
+ }
379
+ /**
380
+ * Coupling gaps check: parse git log for co-change pairs, compute Jaccard
381
+ * similarity, and flag coupled files that are missing from the diff.
382
+ */
383
+ export async function checkCouplingGaps(repoRoot, changedFiles) {
384
+ const start = Date.now();
385
+ const MIN_SUPPORT = 3;
386
+ const MIN_JACCARD = 0.5;
387
+ const MAX_FILES_PER_COMMIT = 50;
388
+ try {
389
+ const raw = execFileSync("git", [
390
+ "log",
391
+ "--name-only",
392
+ "--no-merges",
393
+ "--diff-filter=AMRC",
394
+ "--since=180 days ago",
395
+ "--pretty=format:%H",
396
+ ], { cwd: repoRoot, encoding: "utf-8", timeout: 15000 });
397
+ // Parse commits: git log --pretty=format:%H --name-only outputs:
398
+ // SHA\n\nfile1\nfile2\n\nSHA\n\nfile1\nfile2
399
+ // Split by \n\n yields alternating blocks: [SHA, files, SHA, files, ...]
400
+ const blocks = raw.split("\n\n").filter((b) => b.trim().length > 0);
401
+ const fileCommitCounts = new Map();
402
+ const pairCounts = new Map();
403
+ // Process pairs: blocks[i] = SHA, blocks[i+1] = file list
404
+ for (let i = 0; i < blocks.length - 1; i += 2) {
405
+ const fileBlock = blocks[i + 1];
406
+ const files = fileBlock.split("\n").filter((l) => l.trim().length > 0);
407
+ // Skip bulk commits
408
+ if (files.length > MAX_FILES_PER_COMMIT)
409
+ continue;
410
+ // Count file appearances
411
+ for (const file of files) {
412
+ fileCommitCounts.set(file, (fileCommitCounts.get(file) ?? 0) + 1);
413
+ }
414
+ // Count pairs (canonical: sorted alphabetically)
415
+ for (let i = 0; i < files.length; i++) {
416
+ for (let j = i + 1; j < files.length; j++) {
417
+ const pair = [files[i], files[j]].sort().join("\0");
418
+ pairCounts.set(pair, (pairCounts.get(pair) ?? 0) + 1);
419
+ }
420
+ }
421
+ }
422
+ // For each changed file, find partners with high Jaccard that are NOT in the diff
423
+ const changedSet = new Set(changedFiles);
424
+ const findings = [];
425
+ for (const changedFile of changedFiles) {
426
+ for (const [pair, coCount] of pairCounts) {
427
+ if (coCount < MIN_SUPPORT)
428
+ continue;
429
+ const [fileA, fileB] = pair.split("\0");
430
+ let partner;
431
+ if (fileA === changedFile)
432
+ partner = fileB;
433
+ else if (fileB === changedFile)
434
+ partner = fileA;
435
+ else
436
+ continue;
437
+ // Skip if partner is already in the diff
438
+ if (changedSet.has(partner))
439
+ continue;
440
+ const countA = fileCommitCounts.get(fileA) ?? 0;
441
+ const countB = fileCommitCounts.get(fileB) ?? 0;
442
+ const jaccard = coCount / (countA + countB - coCount);
443
+ if (jaccard >= MIN_JACCARD) {
444
+ findings.push({
445
+ check: "coupling",
446
+ severity: "warn",
447
+ message: `"${changedFile}" is frequently co-changed with "${partner}" (Jaccard ${jaccard.toFixed(2)}, ${coCount} co-commits) but "${partner}" is not in this diff`,
448
+ file: changedFile,
449
+ });
450
+ }
451
+ }
452
+ }
453
+ return {
454
+ check: "coupling",
455
+ status: findings.length > 0 ? "warn" : "pass",
456
+ findings,
457
+ duration_ms: Date.now() - start,
458
+ summary: findings.length > 0
459
+ ? `${findings.length} coupling gap(s) detected`
460
+ : "No coupling gaps detected",
461
+ };
462
+ }
463
+ catch (err) {
464
+ return {
465
+ check: "coupling",
466
+ status: "error",
467
+ findings: [],
468
+ duration_ms: Date.now() - start,
469
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
470
+ };
471
+ }
472
+ }
473
+ /**
474
+ * Breaking changes check: detect exported symbols removed between `since` and
475
+ * current index. For each changed .ts/.js file, `git show` retrieves the old
476
+ * source and a regex extracts export names. These are compared against the
477
+ * current index symbols. Missing exports → T1 "breaking" findings.
478
+ *
479
+ * File-level renames (detected via `git diff --find-renames`) are suppressed
480
+ * because renames naturally lose old export names.
481
+ */
482
+ export async function checkBreakingChanges(index, repoRoot, changedFiles, since, until) {
483
+ const start = Date.now();
484
+ const TS_JS_RE = /\.(tsx?|jsx?)$/;
485
+ const EXPORT_NAMED_RE = /export\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g;
486
+ const EXPORT_DEFAULT_RE = /export\s+default/g;
487
+ try {
488
+ // 1. Detect renames so we can suppress findings for renamed files
489
+ let renameRaw = "";
490
+ try {
491
+ renameRaw = execFileSync("git", [
492
+ "diff",
493
+ "--find-renames",
494
+ "--name-status",
495
+ `${since}..${until || "HEAD"}`,
496
+ ], { cwd: repoRoot, encoding: "utf-8", timeout: 10_000 });
497
+ }
498
+ catch {
499
+ // If rename detection fails, proceed without suppression
500
+ }
501
+ const renamedFiles = new Set();
502
+ for (const line of renameRaw.split("\n")) {
503
+ if (line.startsWith("R")) {
504
+ // R100\told-path\tnew-path (tab-separated)
505
+ const parts = line.split("\t");
506
+ if (parts[1])
507
+ renamedFiles.add(parts[1]);
508
+ if (parts[2])
509
+ renamedFiles.add(parts[2]);
510
+ }
511
+ }
512
+ // 2. Filter to TS/JS files, exclude renames
513
+ const tsJsFiles = changedFiles.filter((f) => TS_JS_RE.test(f) && !renamedFiles.has(f));
514
+ const findings = [];
515
+ // 3. For each file, compare old exports vs current exports
516
+ for (const file of tsJsFiles) {
517
+ try {
518
+ const oldSource = execFileSync("git", ["show", `${since}:${file}`], { cwd: repoRoot, encoding: "utf-8", timeout: 10_000 });
519
+ // Extract old export names
520
+ const oldExports = new Set();
521
+ let match;
522
+ while ((match = EXPORT_NAMED_RE.exec(oldSource)) !== null) {
523
+ oldExports.add(match[1]);
524
+ }
525
+ while ((match = EXPORT_DEFAULT_RE.exec(oldSource)) !== null) {
526
+ oldExports.add("default");
527
+ }
528
+ if (oldExports.size === 0)
529
+ continue;
530
+ // Get current exports from index: top-level symbols in this file
531
+ const currentExports = new Set(index.symbols
532
+ .filter((s) => s.file === file && !s.parent)
533
+ .map((s) => s.name));
534
+ // Removed = in old but not in current
535
+ for (const name of oldExports) {
536
+ if (!currentExports.has(name)) {
537
+ findings.push({
538
+ check: "breaking",
539
+ severity: "error",
540
+ message: `Removed export "${name}" from ${file} — may break downstream consumers`,
541
+ file,
542
+ symbol: name,
543
+ });
544
+ }
545
+ }
546
+ }
547
+ catch {
548
+ // git show failed → file didn't exist at `since` (new file), skip
549
+ }
550
+ }
551
+ return {
552
+ check: "breaking",
553
+ status: findings.length > 0 ? "fail" : "pass",
554
+ findings,
555
+ duration_ms: Date.now() - start,
556
+ summary: findings.length > 0
557
+ ? `${findings.length} removed export(s) detected`
558
+ : "No breaking changes detected",
559
+ };
560
+ }
561
+ catch (err) {
562
+ return {
563
+ check: "breaking",
564
+ status: "error",
565
+ findings: [],
566
+ duration_ms: Date.now() - start,
567
+ summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
568
+ };
569
+ }
570
+ }
571
+ /**
572
+ * Test-gaps check: for each changed non-test source file, verify that at least
573
+ * one test file covers it — either by naming convention or by import reference.
574
+ *
575
+ * Naming convention candidates:
576
+ * foo.ts → foo.test.ts, foo.spec.ts, __tests__/foo.ts, __tests__/foo.test.ts
577
+ *
578
+ * Import graph: search index symbols from test files whose source imports the
579
+ * source file's base name (without extension).
580
+ *
581
+ * If BOTH pathways find 0 tests → T3 advisory finding.
582
+ */
583
+ export async function checkTestGaps(index, changedFiles) {
584
+ const start = Date.now();
585
+ const SOURCE_EXTENSIONS = /\.(tsx?|jsx?)$/;
586
+ // Only process non-test source files
587
+ const sourceFiles = changedFiles.filter((f) => SOURCE_EXTENSIONS.test(f) && !isTestFile(f));
588
+ const indexFilePaths = new Set(index.files.map((f) => f.path));
589
+ const findings = [];
590
+ for (const sourceFile of sourceFiles) {
591
+ // -----------------------------------------------------------------------
592
+ // 1. Naming check
593
+ // -----------------------------------------------------------------------
594
+ const dir = path.dirname(sourceFile);
595
+ const base = path.basename(sourceFile).replace(SOURCE_EXTENSIONS, "");
596
+ // Check co-located tests, __tests__/ dir, AND tests/ mirror directory
597
+ // e.g., src/tools/foo.ts → tests/tools/foo.test.ts
598
+ const testsDir = dir.replace(/^src\//, "tests/");
599
+ const candidates = [
600
+ path.join(dir, `${base}.test.ts`),
601
+ path.join(dir, `${base}.spec.ts`),
602
+ path.join(dir, `${base}.test.tsx`),
603
+ path.join(dir, `${base}.spec.tsx`),
604
+ path.join(dir, `${base}.test.js`),
605
+ path.join(dir, `${base}.spec.js`),
606
+ path.join(dir, "__tests__", `${base}.ts`),
607
+ path.join(dir, "__tests__", `${base}.test.ts`),
608
+ // Mirror in tests/ directory (common layout)
609
+ path.join(testsDir, `${base}.test.ts`),
610
+ path.join(testsDir, `${base}.spec.ts`),
611
+ path.join(testsDir, `${base}.test.tsx`),
612
+ path.join(testsDir, `${base}.test.js`),
613
+ ];
614
+ const foundByNaming = candidates.some((c) => indexFilePaths.has(c));
615
+ if (foundByNaming)
616
+ continue;
617
+ // -----------------------------------------------------------------------
618
+ // 2. Import graph check: look for test file symbols that import sourceFile
619
+ // -----------------------------------------------------------------------
620
+ const foundByImport = index.symbols.some((sym) => {
621
+ if (!isTestFile(sym.file))
622
+ return false;
623
+ if (!sym.source)
624
+ return false;
625
+ // Check if source mentions the file base name in an import
626
+ return sym.source.includes(base);
627
+ });
628
+ if (foundByImport)
629
+ continue;
630
+ // -----------------------------------------------------------------------
631
+ // 3. Neither pathway found a test → T3 finding
632
+ // -----------------------------------------------------------------------
633
+ findings.push({
634
+ check: "test-gaps",
635
+ severity: "warn",
636
+ message: `No test found for "${sourceFile}" — add a test file matching naming convention or import it from a test`,
637
+ file: sourceFile,
638
+ });
639
+ }
640
+ return {
641
+ check: "test-gaps",
642
+ status: findings.length > 0 ? "warn" : "pass",
643
+ findings,
644
+ duration_ms: Date.now() - start,
645
+ summary: findings.length > 0
646
+ ? `${findings.length} source file(s) with no test coverage found`
647
+ : "All changed source files have test coverage",
648
+ };
649
+ }
650
+ // ---------------------------------------------------------------------------
651
+ // Check runner — dispatches to real adapters or stubs for unimplemented checks
652
+ // ---------------------------------------------------------------------------
653
+ async function runCheck(checkName, _repo, changedFiles, index, since, until) {
654
+ switch (checkName) {
655
+ case "blast-radius":
656
+ return checkBlastRadius(index, since, until);
657
+ case "secrets":
658
+ return checkSecrets(index, changedFiles);
659
+ case "dead-code":
660
+ return checkDeadCode(index, changedFiles);
661
+ case "bug-patterns":
662
+ return checkBugPatterns(index, changedFiles);
663
+ case "hotspots":
664
+ return checkHotspots(index, changedFiles);
665
+ case "complexity":
666
+ return checkComplexityDelta(index, changedFiles);
667
+ case "coupling":
668
+ return checkCouplingGaps(index.root, changedFiles);
669
+ case "breaking":
670
+ return checkBreakingChanges(index, index.root, changedFiles, since, until);
671
+ case "test-gaps":
672
+ return checkTestGaps(index, changedFiles);
673
+ default: {
674
+ const tier = findingTier(checkName);
675
+ return {
676
+ check: checkName,
677
+ status: "pass",
678
+ tier,
679
+ findings: [],
680
+ duration_ms: 0,
681
+ summary: "No findings",
682
+ };
683
+ }
684
+ }
685
+ }
686
+ // ---------------------------------------------------------------------------
687
+ // Orchestrator
688
+ // ---------------------------------------------------------------------------
689
+ export async function reviewDiff(repo, opts) {
690
+ const startTime = Date.now();
691
+ const since = opts.since ?? "HEAD~1";
692
+ const until = opts.until;
693
+ const maxFiles = opts.max_files ?? DEFAULT_MAX_FILES;
694
+ const checkTimeoutMs = opts.check_timeout_ms ?? DEFAULT_CHECK_TIMEOUT_MS;
695
+ // -----------------------------------------------------------------------
696
+ // Pre-flight: validate refs
697
+ // -----------------------------------------------------------------------
698
+ try {
699
+ validateGitRef(since);
700
+ if (until && until !== "WORKING" && until !== "STAGED") {
701
+ validateGitRef(until);
702
+ }
703
+ }
704
+ catch (err) {
705
+ const msg = err instanceof Error ? err.message : String(err);
706
+ return {
707
+ repo,
708
+ since,
709
+ checks: [],
710
+ findings: [],
711
+ score: 0,
712
+ verdict: "fail",
713
+ duration_ms: Date.now() - startTime,
714
+ diff_stats: { files_changed: 0, files_reviewed: 0 },
715
+ metadata: {},
716
+ error: `invalid_ref: ${msg}`,
717
+ };
718
+ }
719
+ // -----------------------------------------------------------------------
720
+ // Pre-flight: validate repo exists
721
+ // -----------------------------------------------------------------------
722
+ const index = await getCodeIndex(repo);
723
+ if (!index) {
724
+ return {
725
+ repo,
726
+ since,
727
+ checks: [],
728
+ findings: [],
729
+ score: 0,
730
+ verdict: "fail",
731
+ duration_ms: Date.now() - startTime,
732
+ diff_stats: { files_changed: 0, files_reviewed: 0 },
733
+ metadata: {},
734
+ error: `Repository not found: ${repo}`,
735
+ };
736
+ }
737
+ // -----------------------------------------------------------------------
738
+ // Parse diff
739
+ // -----------------------------------------------------------------------
740
+ const diffResult = await changedSymbols(repo, since, until ?? "HEAD", undefined);
741
+ let changedFiles = diffResult.map((f) => f.file);
742
+ // -----------------------------------------------------------------------
743
+ // Exclude patterns
744
+ // -----------------------------------------------------------------------
745
+ if (opts.exclude_patterns && opts.exclude_patterns.length > 0) {
746
+ const isExcluded = picomatch(opts.exclude_patterns);
747
+ changedFiles = changedFiles.filter((f) => !isExcluded(f));
748
+ }
749
+ const totalFilesChanged = changedFiles.length;
750
+ // -----------------------------------------------------------------------
751
+ // Early return: empty diff
752
+ // -----------------------------------------------------------------------
753
+ if (changedFiles.length === 0) {
754
+ return {
755
+ repo,
756
+ since,
757
+ checks: [],
758
+ findings: [],
759
+ score: 100,
760
+ verdict: "pass",
761
+ duration_ms: Date.now() - startTime,
762
+ diff_stats: { files_changed: 0, files_reviewed: 0 },
763
+ metadata: {},
764
+ };
765
+ }
766
+ // -----------------------------------------------------------------------
767
+ // Large diff: cap files and add advisory finding
768
+ // -----------------------------------------------------------------------
769
+ const allFindings = [];
770
+ const metadata = {};
771
+ if (changedFiles.length > maxFiles) {
772
+ metadata.files_capped = true;
773
+ allFindings.push({
774
+ check: "large-diff",
775
+ severity: "info",
776
+ message: `Large diff: ${changedFiles.length} files changed, reviewing first ${maxFiles}. Consider smaller commits.`,
777
+ });
778
+ changedFiles = changedFiles.slice(0, maxFiles);
779
+ }
780
+ // -----------------------------------------------------------------------
781
+ // Index warning: non-HEAD~N ref may mean stale index
782
+ // -----------------------------------------------------------------------
783
+ if (!HEAD_TILDE_PATTERN.test(since)) {
784
+ metadata.index_warning =
785
+ `Ref "${since}" is not a HEAD~N pattern. Index may not reflect this commit range.`;
786
+ }
787
+ // -----------------------------------------------------------------------
788
+ // Check enablement
789
+ // -----------------------------------------------------------------------
790
+ const requestedChecks = opts.checks
791
+ ? opts.checks.split(",").map((c) => c.trim())
792
+ : [...ALL_CHECKS];
793
+ const enabledChecks = requestedChecks.filter((c) => ALL_CHECKS.includes(c));
794
+ // -----------------------------------------------------------------------
795
+ // Fan-out: run checks with timeout
796
+ // -----------------------------------------------------------------------
797
+ const checkPromises = enabledChecks.map((checkName) => withTimeout(runCheck(checkName, repo, changedFiles, index, since, until ?? "HEAD"), checkTimeoutMs));
798
+ const settled = await Promise.allSettled(checkPromises);
799
+ const checkResults = [];
800
+ for (let i = 0; i < settled.length; i++) {
801
+ const outcome = settled[i];
802
+ const checkName = enabledChecks[i] ?? `check_${i}`;
803
+ if (!outcome || outcome.status === "rejected") {
804
+ checkResults.push({
805
+ check: checkName,
806
+ status: "error",
807
+ findings: [],
808
+ duration_ms: 0,
809
+ summary: `Error: ${outcome && outcome.status === "rejected" && outcome.reason instanceof Error ? outcome.reason.message : String(outcome?.status === "rejected" ? outcome.reason : "unknown")}`,
810
+ });
811
+ }
812
+ else if (outcome.status === "fulfilled" &&
813
+ outcome.value &&
814
+ typeof outcome.value === "object" &&
815
+ "status" in outcome.value &&
816
+ outcome.value.status === "timeout") {
817
+ checkResults.push({
818
+ check: checkName,
819
+ status: "timeout",
820
+ findings: [],
821
+ duration_ms: checkTimeoutMs,
822
+ summary: `Timed out after ${checkTimeoutMs}ms`,
823
+ });
824
+ }
825
+ else if (outcome.status === "fulfilled") {
826
+ checkResults.push(outcome.value);
827
+ }
828
+ }
829
+ // -----------------------------------------------------------------------
830
+ // Assembly: collect findings, score, verdict
831
+ // -----------------------------------------------------------------------
832
+ for (const cr of checkResults) {
833
+ allFindings.push(...cr.findings);
834
+ }
835
+ const score = calculateScore(allFindings, checkResults);
836
+ const verdict = determineVerdict(checkResults);
837
+ return {
838
+ repo,
839
+ since,
840
+ checks: checkResults,
841
+ findings: allFindings,
842
+ score,
843
+ verdict,
844
+ duration_ms: Date.now() - startTime,
845
+ diff_stats: {
846
+ files_changed: totalFilesChanged,
847
+ files_reviewed: changedFiles.length,
848
+ },
849
+ metadata,
850
+ };
851
+ }
852
+ //# sourceMappingURL=review-diff-tools.js.map