@monoes/monograph 1.2.0 → 1.2.2

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 (469) hide show
  1. package/.monomind/loops/mastermind-review-1782162423447.json +16 -0
  2. package/__tests__/mcp-tools/cypher.test.ts +10 -6
  3. package/__tests__/mcp-tools/detect-changes.test.ts +14 -11
  4. package/__tests__/pipeline/phases/wildcard-synthesis.test.ts +4 -4
  5. package/__tests__/search/embed-batch-config.test.ts +7 -7
  6. package/__tests__/search/hybrid-query.test.ts +3 -1
  7. package/__tests__/web/api.test.ts +1 -1
  8. package/dist/src/analysis/churn-cache.d.ts.map +1 -1
  9. package/dist/src/analysis/churn-cache.js +19 -6
  10. package/dist/src/analysis/churn-cache.js.map +1 -1
  11. package/dist/src/analysis/churn-trend.d.ts.map +1 -1
  12. package/dist/src/analysis/churn-trend.js +50 -6
  13. package/dist/src/analysis/churn-trend.js.map +1 -1
  14. package/dist/src/analysis/cluster-quality.d.ts +3 -0
  15. package/dist/src/analysis/cluster-quality.d.ts.map +1 -1
  16. package/dist/src/analysis/cluster-quality.js +18 -12
  17. package/dist/src/analysis/cluster-quality.js.map +1 -1
  18. package/dist/src/analysis/coverage-gaps.d.ts +3 -0
  19. package/dist/src/analysis/coverage-gaps.d.ts.map +1 -1
  20. package/dist/src/analysis/coverage-gaps.js +40 -1
  21. package/dist/src/analysis/coverage-gaps.js.map +1 -1
  22. package/dist/src/analysis/dep-classification.d.ts +2 -0
  23. package/dist/src/analysis/dep-classification.d.ts.map +1 -1
  24. package/dist/src/analysis/dep-classification.js +30 -0
  25. package/dist/src/analysis/dep-classification.js.map +1 -1
  26. package/dist/src/analysis/dependency-health.d.ts +4 -0
  27. package/dist/src/analysis/dependency-health.d.ts.map +1 -1
  28. package/dist/src/analysis/dependency-health.js +28 -0
  29. package/dist/src/analysis/dependency-health.js.map +1 -1
  30. package/dist/src/analysis/duplicate-exports.d.ts +9 -5
  31. package/dist/src/analysis/duplicate-exports.d.ts.map +1 -1
  32. package/dist/src/analysis/duplicate-exports.js +26 -2
  33. package/dist/src/analysis/duplicate-exports.js.map +1 -1
  34. package/dist/src/analysis/feature-flags.d.ts.map +1 -1
  35. package/dist/src/analysis/feature-flags.js +36 -17
  36. package/dist/src/analysis/feature-flags.js.map +1 -1
  37. package/dist/src/analysis/node-similarity.d.ts +19 -0
  38. package/dist/src/analysis/node-similarity.d.ts.map +1 -1
  39. package/dist/src/analysis/node-similarity.js +34 -3
  40. package/dist/src/analysis/node-similarity.js.map +1 -1
  41. package/dist/src/analysis/private-type-leaks.d.ts +4 -0
  42. package/dist/src/analysis/private-type-leaks.d.ts.map +1 -1
  43. package/dist/src/analysis/private-type-leaks.js +43 -2
  44. package/dist/src/analysis/private-type-leaks.js.map +1 -1
  45. package/dist/src/analysis/project-state.d.ts +2 -0
  46. package/dist/src/analysis/project-state.d.ts.map +1 -1
  47. package/dist/src/analysis/project-state.js +18 -7
  48. package/dist/src/analysis/project-state.js.map +1 -1
  49. package/dist/src/analysis/suppressions.d.ts.map +1 -1
  50. package/dist/src/analysis/suppressions.js +3 -1
  51. package/dist/src/analysis/suppressions.js.map +1 -1
  52. package/dist/src/analysis/trace.d.ts +5 -3
  53. package/dist/src/analysis/trace.d.ts.map +1 -1
  54. package/dist/src/analysis/trace.js +59 -53
  55. package/dist/src/analysis/trace.js.map +1 -1
  56. package/dist/src/analysis/unused-class-members.d.ts.map +1 -1
  57. package/dist/src/analysis/unused-class-members.js +7 -1
  58. package/dist/src/analysis/unused-class-members.js.map +1 -1
  59. package/dist/src/analysis/worker-pool.d.ts +3 -0
  60. package/dist/src/analysis/worker-pool.d.ts.map +1 -1
  61. package/dist/src/analysis/worker-pool.js +30 -8
  62. package/dist/src/analysis/worker-pool.js.map +1 -1
  63. package/dist/src/cli/augment.d.ts.map +1 -1
  64. package/dist/src/cli/augment.js +10 -3
  65. package/dist/src/cli/augment.js.map +1 -1
  66. package/dist/src/cli/skill-gen.js +3 -0
  67. package/dist/src/cli/skill-gen.js.map +1 -1
  68. package/dist/src/export/sarif.js +5 -5
  69. package/dist/src/export/sarif.js.map +1 -1
  70. package/dist/src/graph/analyzer.d.ts.map +1 -1
  71. package/dist/src/graph/analyzer.js +17 -2
  72. package/dist/src/graph/analyzer.js.map +1 -1
  73. package/dist/src/graph/author-analytics.d.ts +2 -0
  74. package/dist/src/graph/author-analytics.d.ts.map +1 -1
  75. package/dist/src/graph/author-analytics.js +39 -5
  76. package/dist/src/graph/author-analytics.js.map +1 -1
  77. package/dist/src/graph/baseline.d.ts +5 -0
  78. package/dist/src/graph/baseline.d.ts.map +1 -1
  79. package/dist/src/graph/baseline.js +65 -18
  80. package/dist/src/graph/baseline.js.map +1 -1
  81. package/dist/src/graph/clone-detector.d.ts.map +1 -1
  82. package/dist/src/graph/clone-detector.js +19 -33
  83. package/dist/src/graph/clone-detector.js.map +1 -1
  84. package/dist/src/graph/clone-families.d.ts +2 -0
  85. package/dist/src/graph/clone-families.d.ts.map +1 -1
  86. package/dist/src/graph/clone-families.js +38 -10
  87. package/dist/src/graph/clone-families.js.map +1 -1
  88. package/dist/src/graph/codeowners-extended.d.ts +17 -0
  89. package/dist/src/graph/codeowners-extended.d.ts.map +1 -1
  90. package/dist/src/graph/codeowners-extended.js +42 -0
  91. package/dist/src/graph/codeowners-extended.js.map +1 -1
  92. package/dist/src/graph/codeowners-gitlab.d.ts +21 -0
  93. package/dist/src/graph/codeowners-gitlab.d.ts.map +1 -1
  94. package/dist/src/graph/codeowners-gitlab.js +37 -0
  95. package/dist/src/graph/codeowners-gitlab.js.map +1 -1
  96. package/dist/src/graph/codeowners.d.ts +10 -0
  97. package/dist/src/graph/codeowners.d.ts.map +1 -1
  98. package/dist/src/graph/codeowners.js +24 -2
  99. package/dist/src/graph/codeowners.js.map +1 -1
  100. package/dist/src/graph/complexity.d.ts +10 -0
  101. package/dist/src/graph/complexity.d.ts.map +1 -1
  102. package/dist/src/graph/complexity.js +93 -43
  103. package/dist/src/graph/complexity.js.map +1 -1
  104. package/dist/src/graph/cross-reference-findings.d.ts.map +1 -1
  105. package/dist/src/graph/cross-reference-findings.js +19 -2
  106. package/dist/src/graph/cross-reference-findings.js.map +1 -1
  107. package/dist/src/graph/cross-reference.d.ts +4 -0
  108. package/dist/src/graph/cross-reference.d.ts.map +1 -1
  109. package/dist/src/graph/cross-reference.js +23 -0
  110. package/dist/src/graph/cross-reference.js.map +1 -1
  111. package/dist/src/graph/cycle-detection.d.ts +2 -0
  112. package/dist/src/graph/cycle-detection.d.ts.map +1 -1
  113. package/dist/src/graph/cycle-detection.js +26 -3
  114. package/dist/src/graph/cycle-detection.js.map +1 -1
  115. package/dist/src/graph/cycles.d.ts +11 -3
  116. package/dist/src/graph/cycles.d.ts.map +1 -1
  117. package/dist/src/graph/cycles.js +121 -83
  118. package/dist/src/graph/cycles.js.map +1 -1
  119. package/dist/src/graph/dead-code.d.ts +23 -0
  120. package/dist/src/graph/dead-code.d.ts.map +1 -1
  121. package/dist/src/graph/dead-code.js +53 -13
  122. package/dist/src/graph/dead-code.js.map +1 -1
  123. package/dist/src/graph/dependency-closure.d.ts.map +1 -1
  124. package/dist/src/graph/dependency-closure.js +20 -4
  125. package/dist/src/graph/dependency-closure.js.map +1 -1
  126. package/dist/src/graph/dfs.d.ts +14 -0
  127. package/dist/src/graph/dfs.d.ts.map +1 -1
  128. package/dist/src/graph/dfs.js +61 -0
  129. package/dist/src/graph/dfs.js.map +1 -1
  130. package/dist/src/graph/duplication-stats.d.ts.map +1 -1
  131. package/dist/src/graph/duplication-stats.js +19 -3
  132. package/dist/src/graph/duplication-stats.js.map +1 -1
  133. package/dist/src/graph/edge-filter.d.ts +2 -0
  134. package/dist/src/graph/edge-filter.d.ts.map +1 -1
  135. package/dist/src/graph/edge-filter.js +26 -12
  136. package/dist/src/graph/edge-filter.js.map +1 -1
  137. package/dist/src/graph/health-score.d.ts +8 -0
  138. package/dist/src/graph/health-score.d.ts.map +1 -1
  139. package/dist/src/graph/health-score.js +48 -17
  140. package/dist/src/graph/health-score.js.map +1 -1
  141. package/dist/src/graph/hotspots.d.ts +7 -0
  142. package/dist/src/graph/hotspots.d.ts.map +1 -1
  143. package/dist/src/graph/hotspots.js +50 -5
  144. package/dist/src/graph/hotspots.js.map +1 -1
  145. package/dist/src/graph/import-chain.d.ts +17 -1
  146. package/dist/src/graph/import-chain.d.ts.map +1 -1
  147. package/dist/src/graph/import-chain.js +68 -19
  148. package/dist/src/graph/import-chain.js.map +1 -1
  149. package/dist/src/graph/loader.d.ts.map +1 -1
  150. package/dist/src/graph/loader.js +9 -10
  151. package/dist/src/graph/loader.js.map +1 -1
  152. package/dist/src/graph/maintainability.d.ts +8 -0
  153. package/dist/src/graph/maintainability.d.ts.map +1 -1
  154. package/dist/src/graph/maintainability.js +65 -10
  155. package/dist/src/graph/maintainability.js.map +1 -1
  156. package/dist/src/graph/mirrored-dirs.d.ts +4 -0
  157. package/dist/src/graph/mirrored-dirs.d.ts.map +1 -1
  158. package/dist/src/graph/mirrored-dirs.js +48 -13
  159. package/dist/src/graph/mirrored-dirs.js.map +1 -1
  160. package/dist/src/graph/narrowing.d.ts +15 -0
  161. package/dist/src/graph/narrowing.d.ts.map +1 -1
  162. package/dist/src/graph/narrowing.js +33 -0
  163. package/dist/src/graph/narrowing.js.map +1 -1
  164. package/dist/src/graph/node-search.d.ts +3 -0
  165. package/dist/src/graph/node-search.d.ts.map +1 -1
  166. package/dist/src/graph/node-search.js +37 -4
  167. package/dist/src/graph/node-search.js.map +1 -1
  168. package/dist/src/graph/normalize-id.d.ts +4 -0
  169. package/dist/src/graph/normalize-id.d.ts.map +1 -1
  170. package/dist/src/graph/normalize-id.js +25 -2
  171. package/dist/src/graph/normalize-id.js.map +1 -1
  172. package/dist/src/graph/pagerank.d.ts +7 -0
  173. package/dist/src/graph/pagerank.d.ts.map +1 -1
  174. package/dist/src/graph/pagerank.js +49 -2
  175. package/dist/src/graph/pagerank.js.map +1 -1
  176. package/dist/src/graph/prune.d.ts +31 -0
  177. package/dist/src/graph/prune.d.ts.map +1 -1
  178. package/dist/src/graph/prune.js +65 -0
  179. package/dist/src/graph/prune.js.map +1 -1
  180. package/dist/src/graph/regex-search.d.ts +10 -2
  181. package/dist/src/graph/regex-search.d.ts.map +1 -1
  182. package/dist/src/graph/regex-search.js +33 -4
  183. package/dist/src/graph/regex-search.js.map +1 -1
  184. package/dist/src/graph/regression.d.ts +5 -0
  185. package/dist/src/graph/regression.d.ts.map +1 -1
  186. package/dist/src/graph/regression.js +45 -23
  187. package/dist/src/graph/regression.js.map +1 -1
  188. package/dist/src/graph/ripple-impact.d.ts +27 -0
  189. package/dist/src/graph/ripple-impact.d.ts.map +1 -1
  190. package/dist/src/graph/ripple-impact.js +55 -7
  191. package/dist/src/graph/ripple-impact.js.map +1 -1
  192. package/dist/src/graph/risk-profile.d.ts +7 -0
  193. package/dist/src/graph/risk-profile.d.ts.map +1 -1
  194. package/dist/src/graph/risk-profile.js +82 -48
  195. package/dist/src/graph/risk-profile.js.map +1 -1
  196. package/dist/src/graph/shingle-filter.d.ts.map +1 -1
  197. package/dist/src/graph/shingle-filter.js +25 -26
  198. package/dist/src/graph/shingle-filter.js.map +1 -1
  199. package/dist/src/graph/statistics.d.ts +19 -0
  200. package/dist/src/graph/statistics.d.ts.map +1 -1
  201. package/dist/src/graph/statistics.js +53 -40
  202. package/dist/src/graph/statistics.js.map +1 -1
  203. package/dist/src/graph/stats.d.ts +2 -0
  204. package/dist/src/graph/stats.d.ts.map +1 -1
  205. package/dist/src/graph/stats.js +54 -8
  206. package/dist/src/graph/stats.js.map +1 -1
  207. package/dist/src/graph/subgraph.d.ts +5 -0
  208. package/dist/src/graph/subgraph.d.ts.map +1 -1
  209. package/dist/src/graph/subgraph.js +73 -9
  210. package/dist/src/graph/subgraph.js.map +1 -1
  211. package/dist/src/graph/topo-sort.d.ts +15 -0
  212. package/dist/src/graph/topo-sort.d.ts.map +1 -1
  213. package/dist/src/graph/topo-sort.js +83 -10
  214. package/dist/src/graph/topo-sort.js.map +1 -1
  215. package/dist/src/graph/wcc.d.ts +29 -0
  216. package/dist/src/graph/wcc.d.ts.map +1 -1
  217. package/dist/src/graph/wcc.js +122 -39
  218. package/dist/src/graph/wcc.js.map +1 -1
  219. package/dist/src/index.js +1 -1
  220. package/dist/src/index.js.map +1 -1
  221. package/dist/src/mcp-tools/api-impact.d.ts.map +1 -1
  222. package/dist/src/mcp-tools/api-impact.js +13 -6
  223. package/dist/src/mcp-tools/api-impact.js.map +1 -1
  224. package/dist/src/mcp-tools/context.d.ts.map +1 -1
  225. package/dist/src/mcp-tools/context.js +19 -29
  226. package/dist/src/mcp-tools/context.js.map +1 -1
  227. package/dist/src/mcp-tools/cypher.d.ts +11 -0
  228. package/dist/src/mcp-tools/cypher.d.ts.map +1 -1
  229. package/dist/src/mcp-tools/cypher.js +13 -1
  230. package/dist/src/mcp-tools/cypher.js.map +1 -1
  231. package/dist/src/mcp-tools/detect-changes.d.ts.map +1 -1
  232. package/dist/src/mcp-tools/detect-changes.js +35 -20
  233. package/dist/src/mcp-tools/detect-changes.js.map +1 -1
  234. package/dist/src/mcp-tools/explain.d.ts.map +1 -1
  235. package/dist/src/mcp-tools/explain.js +4 -1
  236. package/dist/src/mcp-tools/explain.js.map +1 -1
  237. package/dist/src/mcp-tools/graph-query.d.ts +2 -0
  238. package/dist/src/mcp-tools/graph-query.d.ts.map +1 -1
  239. package/dist/src/mcp-tools/graph-query.js +20 -3
  240. package/dist/src/mcp-tools/graph-query.js.map +1 -1
  241. package/dist/src/mcp-tools/group-list.d.ts +1 -0
  242. package/dist/src/mcp-tools/group-list.d.ts.map +1 -1
  243. package/dist/src/mcp-tools/group-list.js +7 -9
  244. package/dist/src/mcp-tools/group-list.js.map +1 -1
  245. package/dist/src/mcp-tools/impact.d.ts.map +1 -1
  246. package/dist/src/mcp-tools/impact.js +20 -59
  247. package/dist/src/mcp-tools/impact.js.map +1 -1
  248. package/dist/src/mcp-tools/neighbors.d.ts.map +1 -1
  249. package/dist/src/mcp-tools/neighbors.js +24 -33
  250. package/dist/src/mcp-tools/neighbors.js.map +1 -1
  251. package/dist/src/mcp-tools/query.d.ts +2 -0
  252. package/dist/src/mcp-tools/query.d.ts.map +1 -1
  253. package/dist/src/mcp-tools/query.js +1 -0
  254. package/dist/src/mcp-tools/query.js.map +1 -1
  255. package/dist/src/mcp-tools/rename.d.ts.map +1 -1
  256. package/dist/src/mcp-tools/rename.js +12 -7
  257. package/dist/src/mcp-tools/rename.js.map +1 -1
  258. package/dist/src/mcp-tools/route-map.d.ts.map +1 -1
  259. package/dist/src/mcp-tools/route-map.js +19 -23
  260. package/dist/src/mcp-tools/route-map.js.map +1 -1
  261. package/dist/src/mcp-tools/shape-check.d.ts.map +1 -1
  262. package/dist/src/mcp-tools/shape-check.js +17 -13
  263. package/dist/src/mcp-tools/shape-check.js.map +1 -1
  264. package/dist/src/parsers/loader.js +6 -5
  265. package/dist/src/parsers/loader.js.map +1 -1
  266. package/dist/src/pipeline/cluster-only.d.ts.map +1 -1
  267. package/dist/src/pipeline/cluster-only.js +8 -4
  268. package/dist/src/pipeline/cluster-only.js.map +1 -1
  269. package/dist/src/pipeline/orchestrator.d.ts +2 -0
  270. package/dist/src/pipeline/orchestrator.d.ts.map +1 -1
  271. package/dist/src/pipeline/orchestrator.js +19 -0
  272. package/dist/src/pipeline/orchestrator.js.map +1 -1
  273. package/dist/src/pipeline/phases/boundary.d.ts +2 -0
  274. package/dist/src/pipeline/phases/boundary.d.ts.map +1 -1
  275. package/dist/src/pipeline/phases/boundary.js +31 -2
  276. package/dist/src/pipeline/phases/boundary.js.map +1 -1
  277. package/dist/src/pipeline/phases/communities.d.ts +14 -0
  278. package/dist/src/pipeline/phases/communities.d.ts.map +1 -1
  279. package/dist/src/pipeline/phases/communities.js +58 -5
  280. package/dist/src/pipeline/phases/communities.js.map +1 -1
  281. package/dist/src/pipeline/phases/contextual-proximity.d.ts.map +1 -1
  282. package/dist/src/pipeline/phases/contextual-proximity.js +21 -14
  283. package/dist/src/pipeline/phases/contextual-proximity.js.map +1 -1
  284. package/dist/src/pipeline/phases/cross-file.d.ts.map +1 -1
  285. package/dist/src/pipeline/phases/cross-file.js +35 -14
  286. package/dist/src/pipeline/phases/cross-file.js.map +1 -1
  287. package/dist/src/pipeline/phases/field-access.d.ts.map +1 -1
  288. package/dist/src/pipeline/phases/field-access.js +9 -4
  289. package/dist/src/pipeline/phases/field-access.js.map +1 -1
  290. package/dist/src/pipeline/phases/god-nodes.d.ts +4 -0
  291. package/dist/src/pipeline/phases/god-nodes.d.ts.map +1 -1
  292. package/dist/src/pipeline/phases/god-nodes.js +43 -9
  293. package/dist/src/pipeline/phases/god-nodes.js.map +1 -1
  294. package/dist/src/pipeline/phases/import-resolver.d.ts +11 -0
  295. package/dist/src/pipeline/phases/import-resolver.d.ts.map +1 -1
  296. package/dist/src/pipeline/phases/import-resolver.js +34 -7
  297. package/dist/src/pipeline/phases/import-resolver.js.map +1 -1
  298. package/dist/src/pipeline/phases/leiden.d.ts.map +1 -1
  299. package/dist/src/pipeline/phases/leiden.js +27 -19
  300. package/dist/src/pipeline/phases/leiden.js.map +1 -1
  301. package/dist/src/pipeline/phases/markdown.d.ts.map +1 -1
  302. package/dist/src/pipeline/phases/markdown.js +10 -5
  303. package/dist/src/pipeline/phases/markdown.js.map +1 -1
  304. package/dist/src/pipeline/phases/pdf-parse.d.ts.map +1 -1
  305. package/dist/src/pipeline/phases/pdf-parse.js +18 -3
  306. package/dist/src/pipeline/phases/pdf-parse.js.map +1 -1
  307. package/dist/src/pipeline/phases/processes.d.ts.map +1 -1
  308. package/dist/src/pipeline/phases/processes.js +26 -9
  309. package/dist/src/pipeline/phases/processes.js.map +1 -1
  310. package/dist/src/pipeline/phases/re-export-propagation.d.ts.map +1 -1
  311. package/dist/src/pipeline/phases/re-export-propagation.js +33 -14
  312. package/dist/src/pipeline/phases/re-export-propagation.js.map +1 -1
  313. package/dist/src/pipeline/phases/reachability.d.ts.map +1 -1
  314. package/dist/src/pipeline/phases/reachability.js +42 -26
  315. package/dist/src/pipeline/phases/reachability.js.map +1 -1
  316. package/dist/src/pipeline/phases/routes.d.ts.map +1 -1
  317. package/dist/src/pipeline/phases/routes.js +30 -21
  318. package/dist/src/pipeline/phases/routes.js.map +1 -1
  319. package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
  320. package/dist/src/pipeline/phases/scope-resolution.js +93 -63
  321. package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
  322. package/dist/src/pipeline/runner.d.ts +4 -0
  323. package/dist/src/pipeline/runner.d.ts.map +1 -1
  324. package/dist/src/pipeline/runner.js +30 -11
  325. package/dist/src/pipeline/runner.js.map +1 -1
  326. package/dist/src/query/cypher-parser.d.ts +20 -0
  327. package/dist/src/query/cypher-parser.d.ts.map +1 -1
  328. package/dist/src/query/cypher-parser.js +73 -0
  329. package/dist/src/query/cypher-parser.js.map +1 -1
  330. package/dist/src/search/embed-batch.d.ts.map +1 -1
  331. package/dist/src/search/embed-batch.js +4 -1
  332. package/dist/src/search/embed-batch.js.map +1 -1
  333. package/dist/src/search/hybrid-query.d.ts +4 -0
  334. package/dist/src/search/hybrid-query.d.ts.map +1 -1
  335. package/dist/src/search/hybrid-query.js +5 -1
  336. package/dist/src/search/hybrid-query.js.map +1 -1
  337. package/dist/src/staleness/git-staleness.js +3 -3
  338. package/dist/src/staleness/git-staleness.js.map +1 -1
  339. package/dist/src/storage/db.d.ts.map +1 -1
  340. package/dist/src/storage/db.js +12 -0
  341. package/dist/src/storage/db.js.map +1 -1
  342. package/dist/src/storage/edge-store.d.ts +2 -0
  343. package/dist/src/storage/edge-store.d.ts.map +1 -1
  344. package/dist/src/storage/edge-store.js +26 -1
  345. package/dist/src/storage/edge-store.js.map +1 -1
  346. package/dist/src/storage/embedding-store.d.ts +16 -0
  347. package/dist/src/storage/embedding-store.d.ts.map +1 -1
  348. package/dist/src/storage/embedding-store.js +26 -2
  349. package/dist/src/storage/embedding-store.js.map +1 -1
  350. package/dist/src/storage/file-cache.d.ts +5 -0
  351. package/dist/src/storage/file-cache.d.ts.map +1 -1
  352. package/dist/src/storage/file-cache.js +35 -9
  353. package/dist/src/storage/file-cache.js.map +1 -1
  354. package/dist/src/storage/fts-store.d.ts +4 -0
  355. package/dist/src/storage/fts-store.d.ts.map +1 -1
  356. package/dist/src/storage/fts-store.js +8 -3
  357. package/dist/src/storage/fts-store.js.map +1 -1
  358. package/dist/src/storage/node-store.d.ts +2 -0
  359. package/dist/src/storage/node-store.d.ts.map +1 -1
  360. package/dist/src/storage/node-store.js +28 -1
  361. package/dist/src/storage/node-store.js.map +1 -1
  362. package/dist/src/wiki/wiki-generator.d.ts.map +1 -1
  363. package/dist/src/wiki/wiki-generator.js +5 -1
  364. package/dist/src/wiki/wiki-generator.js.map +1 -1
  365. package/dist/tsconfig.tsbuildinfo +1 -1
  366. package/package.json +1 -1
  367. package/src/__tests__/pipeline/runner.parallel.test.ts +2 -2
  368. package/src/analysis/churn-cache.ts +18 -7
  369. package/src/analysis/churn-trend.ts +35 -6
  370. package/src/analysis/cluster-quality.ts +20 -11
  371. package/src/analysis/coverage-gaps.ts +44 -1
  372. package/src/analysis/dep-classification.ts +34 -0
  373. package/src/analysis/dependency-health.ts +25 -0
  374. package/src/analysis/duplicate-exports.ts +40 -4
  375. package/src/analysis/feature-flags.ts +37 -17
  376. package/src/analysis/node-similarity.ts +45 -4
  377. package/src/analysis/private-type-leaks.ts +49 -2
  378. package/src/analysis/project-state.ts +19 -9
  379. package/src/analysis/suppressions.ts +4 -1
  380. package/src/analysis/trace.ts +73 -54
  381. package/src/analysis/unused-class-members.ts +8 -1
  382. package/src/analysis/worker-pool.ts +40 -8
  383. package/src/cli/augment.ts +10 -3
  384. package/src/cli/skill-gen.ts +3 -0
  385. package/src/export/sarif.ts +5 -5
  386. package/src/graph/analyzer.ts +26 -2
  387. package/src/graph/author-analytics.ts +45 -5
  388. package/src/graph/baseline.ts +78 -24
  389. package/src/graph/clone-detector.ts +22 -36
  390. package/src/graph/clone-families.ts +34 -10
  391. package/src/graph/codeowners-extended.ts +54 -0
  392. package/src/graph/codeowners-gitlab.ts +46 -0
  393. package/src/graph/codeowners.ts +33 -2
  394. package/src/graph/complexity.ts +102 -47
  395. package/src/graph/cross-reference-findings.ts +23 -2
  396. package/src/graph/cross-reference.ts +26 -0
  397. package/src/graph/cycle-detection.ts +22 -3
  398. package/src/graph/cycles.ts +155 -96
  399. package/src/graph/dead-code.ts +76 -17
  400. package/src/graph/dependency-closure.ts +24 -7
  401. package/src/graph/dfs.ts +78 -0
  402. package/src/graph/duplication-stats.ts +17 -6
  403. package/src/graph/edge-filter.ts +26 -12
  404. package/src/graph/health-score.ts +47 -17
  405. package/src/graph/hotspots.ts +57 -8
  406. package/src/graph/import-chain.ts +89 -19
  407. package/src/graph/loader.ts +10 -7
  408. package/src/graph/maintainability.ts +72 -22
  409. package/src/graph/mirrored-dirs.ts +45 -14
  410. package/src/graph/narrowing.ts +44 -0
  411. package/src/graph/node-search.ts +50 -4
  412. package/src/graph/normalize-id.ts +26 -2
  413. package/src/graph/pagerank.ts +70 -2
  414. package/src/graph/prune.ts +76 -0
  415. package/src/graph/regex-search.ts +38 -4
  416. package/src/graph/regression.ts +48 -23
  417. package/src/graph/ripple-impact.ts +71 -11
  418. package/src/graph/risk-profile.ts +98 -53
  419. package/src/graph/shingle-filter.ts +33 -27
  420. package/src/graph/statistics.ts +73 -45
  421. package/src/graph/stats.ts +50 -16
  422. package/src/graph/subgraph.ts +97 -25
  423. package/src/graph/topo-sort.ts +101 -16
  424. package/src/graph/wcc.ts +146 -37
  425. package/src/index.ts +1 -1
  426. package/src/mcp-tools/api-impact.ts +13 -6
  427. package/src/mcp-tools/context.ts +31 -40
  428. package/src/mcp-tools/cypher.ts +14 -1
  429. package/src/mcp-tools/detect-changes.ts +38 -23
  430. package/src/mcp-tools/explain.ts +4 -1
  431. package/src/mcp-tools/graph-query.ts +24 -3
  432. package/src/mcp-tools/group-list.ts +9 -11
  433. package/src/mcp-tools/impact.ts +26 -66
  434. package/src/mcp-tools/neighbors.ts +34 -39
  435. package/src/mcp-tools/query.ts +3 -0
  436. package/src/mcp-tools/rename.ts +9 -8
  437. package/src/mcp-tools/route-map.ts +21 -23
  438. package/src/mcp-tools/shape-check.ts +18 -14
  439. package/src/parsers/loader.ts +5 -5
  440. package/src/pipeline/cluster-only.ts +8 -4
  441. package/src/pipeline/orchestrator.ts +22 -0
  442. package/src/pipeline/phases/boundary.ts +40 -2
  443. package/src/pipeline/phases/communities.ts +66 -5
  444. package/src/pipeline/phases/contextual-proximity.ts +15 -14
  445. package/src/pipeline/phases/cross-file.ts +35 -14
  446. package/src/pipeline/phases/field-access.ts +11 -4
  447. package/src/pipeline/phases/god-nodes.ts +47 -10
  448. package/src/pipeline/phases/import-resolver.ts +37 -5
  449. package/src/pipeline/phases/leiden.ts +25 -18
  450. package/src/pipeline/phases/markdown.ts +12 -7
  451. package/src/pipeline/phases/pdf-parse.ts +19 -2
  452. package/src/pipeline/phases/processes.ts +27 -15
  453. package/src/pipeline/phases/re-export-propagation.ts +40 -18
  454. package/src/pipeline/phases/reachability.ts +39 -24
  455. package/src/pipeline/phases/routes.ts +32 -21
  456. package/src/pipeline/phases/scope-resolution.ts +110 -71
  457. package/src/pipeline/runner.ts +28 -11
  458. package/src/query/cypher-parser.ts +83 -0
  459. package/src/search/embed-batch.ts +4 -1
  460. package/src/search/hybrid-query.ts +11 -1
  461. package/src/staleness/git-staleness.ts +3 -3
  462. package/src/storage/db.ts +11 -0
  463. package/src/storage/edge-store.ts +34 -1
  464. package/src/storage/embedding-store.ts +38 -6
  465. package/src/storage/file-cache.ts +35 -21
  466. package/src/storage/fts-store.ts +12 -3
  467. package/src/storage/node-store.ts +39 -1
  468. package/src/wiki/wiki-generator.ts +5 -1
  469. package/src/coverage/runtime.ts +0 -249
@@ -14,6 +14,7 @@ export interface BaselineFinding {
14
14
  nodeId: string;
15
15
  nodeName: string;
16
16
  filePath: string | null;
17
+ startLine: number | null;
17
18
  savedAt: string; // ISO timestamp
18
19
  /** 16-hex SHA-256 fingerprint for stable deduplication across runs */
19
20
  fingerprint?: string;
@@ -103,12 +104,12 @@ export function extractFindingsFromDb(
103
104
 
104
105
  // Isolated nodes (no incoming or outgoing edges)
105
106
  const isolated = db.prepare(`
106
- SELECT n.id, n.name, n.file_path, n.label
107
+ SELECT n.id, n.name, n.file_path, n.start_line, n.label
107
108
  FROM nodes n
108
109
  WHERE NOT EXISTS (SELECT 1 FROM edges e WHERE e.source_id = n.id OR e.target_id = n.id)
109
110
  AND n.label IN ('Function', 'Class', 'Method', 'Interface', 'Variable', 'Module', 'File')
110
111
  LIMIT 500
111
- `).all() as { id: string; name: string; file_path: string | null; label: string }[];
112
+ `).all() as { id: string; name: string; file_path: string | null; start_line: number | null; label: string }[];
112
113
 
113
114
  for (const n of isolated) {
114
115
  findings.push({
@@ -117,6 +118,7 @@ export function extractFindingsFromDb(
117
118
  nodeId: n.id,
118
119
  nodeName: n.name,
119
120
  filePath: n.file_path,
121
+ startLine: n.start_line ?? null,
120
122
  savedAt: new Date().toISOString(),
121
123
  fingerprint: fingerprintFinding('isolated_node', n.file_path ?? '', n.id),
122
124
  });
@@ -124,7 +126,7 @@ export function extractFindingsFromDb(
124
126
 
125
127
  // God nodes (degree > 50)
126
128
  const gods = db.prepare(`
127
- SELECT n.id, n.name, n.file_path,
129
+ SELECT n.id, n.name, n.file_path, n.start_line,
128
130
  COUNT(DISTINCT e1.id) + COUNT(DISTINCT e2.id) as degree
129
131
  FROM nodes n
130
132
  LEFT JOIN edges e1 ON e1.source_id = n.id
@@ -133,7 +135,7 @@ export function extractFindingsFromDb(
133
135
  HAVING degree > 50
134
136
  ORDER BY degree DESC
135
137
  LIMIT 100
136
- `).all() as { id: string; name: string; file_path: string | null; degree: number }[];
138
+ `).all() as { id: string; name: string; file_path: string | null; start_line: number | null; degree: number }[];
137
139
 
138
140
  for (const n of gods) {
139
141
  findings.push({
@@ -142,6 +144,7 @@ export function extractFindingsFromDb(
142
144
  nodeId: n.id,
143
145
  nodeName: n.name,
144
146
  filePath: n.file_path,
147
+ startLine: n.start_line ?? null,
145
148
  savedAt: new Date().toISOString(),
146
149
  fingerprint: fingerprintFinding('god_node', n.file_path ?? '', n.id),
147
150
  });
@@ -150,12 +153,12 @@ export function extractFindingsFromDb(
150
153
  // Surprise edges: non-EXTRACTED confidence (cross-community low-confidence edges)
151
154
  const surprises = db.prepare(`
152
155
  SELECT DISTINCT e.id as edge_id, e.source_id, e.target_id, e.confidence,
153
- n.name as src_name, n.file_path as src_file
156
+ n.name as src_name, n.file_path as src_file, n.start_line as src_line
154
157
  FROM edges e
155
158
  JOIN nodes n ON n.id = e.source_id
156
159
  WHERE e.confidence != 'EXTRACTED'
157
160
  LIMIT 200
158
- `).all() as { edge_id: string; source_id: string; target_id: string; confidence: string; src_name: string; src_file: string | null }[];
161
+ `).all() as { edge_id: string; source_id: string; target_id: string; confidence: string; src_name: string; src_file: string | null; src_line: number | null }[];
159
162
 
160
163
  for (const e of surprises) {
161
164
  findings.push({
@@ -164,30 +167,28 @@ export function extractFindingsFromDb(
164
167
  nodeId: e.source_id,
165
168
  nodeName: e.src_name,
166
169
  filePath: e.src_file,
170
+ startLine: e.src_line ?? null,
167
171
  savedAt: new Date().toISOString(),
168
172
  fingerprint: fingerprintFinding('surprise', e.source_id, e.target_id),
169
173
  });
170
174
  }
171
175
 
172
176
  // Bridge nodes: nodes whose edges span more than one community_id
177
+ // Use JOIN+GROUP BY instead of correlated subqueries to avoid N*2 subquery executions
173
178
  const bridges = db.prepare(`
174
- SELECT n.id, n.name, n.file_path
179
+ SELECT n.id, n.name, n.file_path, n.start_line,
180
+ COUNT(DISTINCT CASE WHEN e_out.source_id = n.id AND n_tgt.community_id != n.community_id THEN n_tgt.community_id END) +
181
+ COUNT(DISTINCT CASE WHEN e_in.target_id = n.id AND n_src.community_id != n.community_id THEN n_src.community_id END) AS cross_communities
175
182
  FROM nodes n
183
+ LEFT JOIN edges e_out ON e_out.source_id = n.id
184
+ LEFT JOIN nodes n_tgt ON n_tgt.id = e_out.target_id AND n_tgt.community_id IS NOT NULL
185
+ LEFT JOIN edges e_in ON e_in.target_id = n.id
186
+ LEFT JOIN nodes n_src ON n_src.id = e_in.source_id AND n_src.community_id IS NOT NULL
176
187
  WHERE n.community_id IS NOT NULL
177
- AND (
178
- SELECT COUNT(DISTINCT n_tgt.community_id)
179
- FROM edges e_out
180
- JOIN nodes n_tgt ON n_tgt.id = e_out.target_id
181
- WHERE e_out.source_id = n.id AND n_tgt.community_id != n.community_id
182
- ) +
183
- (
184
- SELECT COUNT(DISTINCT n_src.community_id)
185
- FROM edges e_in
186
- JOIN nodes n_src ON n_src.id = e_in.source_id
187
- WHERE e_in.target_id = n.id AND n_src.community_id != n.community_id
188
- ) >= 2
188
+ GROUP BY n.id
189
+ HAVING cross_communities >= 2
189
190
  LIMIT 100
190
- `).all() as { id: string; name: string; file_path: string | null }[];
191
+ `).all() as { id: string; name: string; file_path: string | null; start_line: number | null }[];
191
192
 
192
193
  for (const n of bridges) {
193
194
  findings.push({
@@ -196,6 +197,7 @@ export function extractFindingsFromDb(
196
197
  nodeId: n.id,
197
198
  nodeName: n.name,
198
199
  filePath: n.file_path,
200
+ startLine: n.start_line ?? null,
199
201
  savedAt: new Date().toISOString(),
200
202
  fingerprint: fingerprintFinding('bridge_node', n.file_path ?? '', n.id),
201
203
  });
@@ -203,13 +205,13 @@ export function extractFindingsFromDb(
203
205
 
204
206
  // Unreachable exports: exported nodes with no incoming edges
205
207
  const unreachableExports = db.prepare(`
206
- SELECT n.id, n.name, n.file_path
208
+ SELECT n.id, n.name, n.file_path, n.start_line
207
209
  FROM nodes n
208
210
  WHERE n.is_exported = 1
209
211
  AND n.label IN ('Function', 'Class', 'Method', 'Interface', 'Variable')
210
212
  AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.target_id = n.id)
211
213
  LIMIT 300
212
- `).all() as { id: string; name: string; file_path: string | null }[];
214
+ `).all() as { id: string; name: string; file_path: string | null; start_line: number | null }[];
213
215
 
214
216
  for (const n of unreachableExports) {
215
217
  findings.push({
@@ -218,6 +220,7 @@ export function extractFindingsFromDb(
218
220
  nodeId: n.id,
219
221
  nodeName: n.name,
220
222
  filePath: n.file_path,
223
+ startLine: n.start_line ?? null,
221
224
  savedAt: new Date().toISOString(),
222
225
  fingerprint: fingerprintFinding('unreachable_export', n.file_path ?? '', n.id),
223
226
  });
@@ -226,12 +229,12 @@ export function extractFindingsFromDb(
226
229
  // Ambiguous edges: edges with AMBIGUOUS confidence
227
230
  const ambiguousEdges = db.prepare(`
228
231
  SELECT DISTINCT e.id as edge_id, e.source_id, e.target_id,
229
- n.name as src_name, n.file_path as src_file
232
+ n.name as src_name, n.file_path as src_file, n.start_line as src_line
230
233
  FROM edges e
231
234
  JOIN nodes n ON n.id = e.source_id
232
235
  WHERE e.confidence = 'AMBIGUOUS'
233
236
  LIMIT 200
234
- `).all() as { edge_id: string; source_id: string; target_id: string; src_name: string; src_file: string | null }[];
237
+ `).all() as { edge_id: string; source_id: string; target_id: string; src_name: string; src_file: string | null; src_line: number | null }[];
235
238
 
236
239
  for (const e of ambiguousEdges) {
237
240
  findings.push({
@@ -240,6 +243,7 @@ export function extractFindingsFromDb(
240
243
  nodeId: e.source_id,
241
244
  nodeName: e.src_name,
242
245
  filePath: e.src_file,
246
+ startLine: e.src_line ?? null,
243
247
  savedAt: new Date().toISOString(),
244
248
  fingerprint: fingerprintFinding('ambiguous_edge', e.source_id, e.target_id),
245
249
  });
@@ -367,3 +371,53 @@ export function computeTrend(
367
371
 
368
372
  return { metrics, overallDirection };
369
373
  }
374
+
375
+ /** Format a list of ComparedFindings as structured text with file:line hints. */
376
+ export function formatComparedFindings(findings: ComparedFinding[], showAll = false): string {
377
+ if (findings.length === 0) return 'Baseline comparison: no findings.';
378
+
379
+ const newFindings = findings.filter(f => f.introduced);
380
+ const existing = findings.filter(f => !f.introduced);
381
+
382
+ const lines: string[] = [
383
+ `Baseline comparison: ${findings.length} total finding(s) — ${newFindings.length} new, ${existing.length} known.`,
384
+ '',
385
+ ];
386
+
387
+ if (newFindings.length > 0) {
388
+ lines.push(`New findings (${newFindings.length}):`);
389
+ for (const f of newFindings) {
390
+ const ref = f.filePath
391
+ ? f.startLine != null ? `${f.filePath}:${f.startLine}` : f.filePath
392
+ : f.nodeId;
393
+ lines.push(` [${f.type}] ${f.nodeName} ${ref}`);
394
+ }
395
+ lines.push('');
396
+ }
397
+
398
+ if (showAll && existing.length > 0) {
399
+ lines.push(`Known findings (${existing.length}):`);
400
+ for (const f of existing) {
401
+ const ref = f.filePath
402
+ ? f.startLine != null ? `${f.filePath}:${f.startLine}` : f.filePath
403
+ : f.nodeId;
404
+ lines.push(` [${f.type}] ${f.nodeName} ${ref}`);
405
+ }
406
+ lines.push('');
407
+ }
408
+
409
+ return lines.join('\n').trimEnd();
410
+ }
411
+
412
+ /** Format a TrendReport as structured text for LLM consumption. */
413
+ export function formatTrendReport(report: TrendReport): string {
414
+ const lines: string[] = [
415
+ `Graph trend: ${report.overallDirection}`,
416
+ '',
417
+ ];
418
+ for (const m of report.metrics) {
419
+ const deltaStr = m.delta > 0 ? `+${m.delta}` : `${m.delta}`;
420
+ lines.push(` ${m.symbol} ${m.metric}: ${m.previous} → ${m.current} (${deltaStr}) [${m.direction}]`);
421
+ }
422
+ return lines.join('\n');
423
+ }
@@ -29,39 +29,29 @@ function baseName(filePath: string): string {
29
29
  return filePath.replace(/^.*[\\/]/, '');
30
30
  }
31
31
 
32
- /** Compute Jaccard similarity between two multisets (represented as token arrays). */
33
- function jaccardSimilarity(a: string[], b: string[]): number {
32
+ /**
33
+ * Compute both Jaccard similarity and shared token count in a single pass.
34
+ * Avoids building the countA Map twice (previously done separately in
35
+ * jaccardSimilarity and sharedTokenCount).
36
+ */
37
+ function computeSimilarityAndShared(a: string[], b: string[]): { similarity: number; shared: number } {
34
38
  const countA = new Map<string, number>();
35
- const countB = new Map<string, number>();
36
39
  for (const t of a) countA.set(t, (countA.get(t) ?? 0) + 1);
37
- for (const t of b) countB.set(t, (countB.get(t) ?? 0) + 1);
38
40
 
39
41
  let intersection = 0;
40
- for (const [token, ca] of countA) {
41
- const cb = countB.get(token) ?? 0;
42
- intersection += Math.min(ca, cb);
43
- }
44
-
45
- const union = a.length + b.length - intersection;
46
- return union === 0 ? 1 : intersection / union;
47
- }
48
-
49
- /** Count tokens shared between two multisets. */
50
- function sharedTokenCount(a: string[], b: string[]): number {
51
- const countA = new Map<string, number>();
52
- for (const t of a) countA.set(t, (countA.get(t) ?? 0) + 1);
53
-
54
- let shared = 0;
55
42
  const seen = new Map<string, number>();
56
43
  for (const t of b) {
57
44
  const limit = countA.get(t) ?? 0;
58
45
  const used = seen.get(t) ?? 0;
59
46
  if (used < limit) {
60
- shared++;
47
+ intersection++;
61
48
  seen.set(t, used + 1);
62
49
  }
63
50
  }
64
- return shared;
51
+
52
+ const union = a.length + b.length - intersection;
53
+ const similarity = union === 0 ? 1 : intersection / union;
54
+ return { similarity, shared: intersection };
65
55
  }
66
56
 
67
57
  export function detectClones(
@@ -78,10 +68,12 @@ export function detectClones(
78
68
  const pairs: ClonePair[] = [];
79
69
  const involvedFiles = new Set<string>();
80
70
 
81
- // Pre-compute tokens for each file
82
- const tokenMap = new Map<string, string[]>();
71
+ // Pre-compute path tokens and name tokens for each file
72
+ const tokenMap = new Map<string, { pathTokens: string[]; nameTokens: string[] }>();
83
73
  for (const row of rows) {
84
- tokenMap.set(row.file_path, tokenizePath(row.file_path));
74
+ const pathTokens = tokenizePath(row.file_path);
75
+ const nameTokens = tokenizePath(baseName(row.file_path));
76
+ tokenMap.set(row.file_path, { pathTokens, nameTokens });
85
77
  }
86
78
 
87
79
  // Compare all pairs
@@ -89,22 +81,16 @@ export function detectClones(
89
81
  for (let j = i + 1; j < rows.length; j++) {
90
82
  const pathA = rows[i].file_path;
91
83
  const pathB = rows[j].file_path;
92
- const tokensA = tokenMap.get(pathA)!;
93
- const tokensB = tokenMap.get(pathB)!;
84
+ const entryA = tokenMap.get(pathA)!;
85
+ const entryB = tokenMap.get(pathB)!;
94
86
 
95
- const sim = jaccardSimilarity(tokensA, tokensB);
96
- const shared = sharedTokenCount(tokensA, tokensB);
87
+ const { similarity: sim, shared } = computeSimilarityAndShared(entryA.pathTokens, entryB.pathTokens);
97
88
 
98
89
  if (sim < minSimilarity || shared < minTokens) {
99
90
  // Also check near-duplicate file names (same name, different dir)
100
- const nameA = baseName(pathA);
101
- const nameB = baseName(pathB);
102
- const nameTokensA = tokenizePath(nameA);
103
- const nameTokensB = tokenizePath(nameB);
104
- const nameSim = jaccardSimilarity(nameTokensA, nameTokensB);
105
- const nameShared = sharedTokenCount(nameTokensA, nameTokensB);
106
-
107
- if (nameSim >= minSimilarity && nameShared >= Math.min(minTokens, nameTokensA.length)) {
91
+ const { similarity: nameSim, shared: nameShared } = computeSimilarityAndShared(entryA.nameTokens, entryB.nameTokens);
92
+
93
+ if (nameSim >= minSimilarity && nameShared >= Math.min(minTokens, entryA.nameTokens.length)) {
108
94
  // Same-name files in different directories → structural similarity
109
95
  const pair: ClonePair = {
110
96
  fileA: pathA,
@@ -30,7 +30,8 @@ export interface CloneFamily {
30
30
  }
31
31
 
32
32
  function fileSetKey(files: string[]): string {
33
- return [...files].sort().join('|');
33
+ // Avoid spread allocation: sort a copy directly
34
+ return files.slice().sort().join('|');
34
35
  }
35
36
 
36
37
  function generateSuggestions(family: Omit<CloneFamily, 'suggestions'>): RefactoringSuggestion[] {
@@ -46,10 +47,14 @@ function generateSuggestions(family: Omit<CloneFamily, 'suggestions'>): Refactor
46
47
  });
47
48
  } else {
48
49
  // Check if all files are in the same directory → MergeDirectories
49
- const dirs = [...new Set(family.files.map(f => {
50
- const parts = f.replace(/\\/g, '/').split('/');
51
- return parts.slice(0, -1).join('/');
52
- }))];
50
+ // Use Set directly to avoid spread+map intermediate array
51
+ const dirSet = new Set<string>();
52
+ for (const f of family.files) {
53
+ const slash = f.replace(/\\/g, '/');
54
+ const lastSlash = slash.lastIndexOf('/');
55
+ dirSet.add(lastSlash >= 0 ? slash.slice(0, lastSlash) : '.');
56
+ }
57
+ const dirs = [...dirSet];
53
58
  if (dirs.length > 1 && family.files.length >= 3) {
54
59
  suggestions.push({
55
60
  kind: 'MergeDirectories',
@@ -81,12 +86,29 @@ export function groupIntoFamilies(groups: CloneGroup[]): CloneFamily[] {
81
86
  familyMap.get(key)!.groups.push(group);
82
87
  }
83
88
 
84
- return [...familyMap.values()].map(({ files, groups }) => {
89
+ // Build output array without intermediate spread + map allocation
90
+ const result: CloneFamily[] = [];
91
+ for (const { files, groups } of familyMap.values()) {
85
92
  const fileList = [...files].sort();
86
- const totalDuplicatedLines = groups.reduce((s, g) => s + g.duplicatedLines, 0);
93
+ let totalDuplicatedLines = 0;
94
+ for (const g of groups) totalDuplicatedLines += g.duplicatedLines;
87
95
  const base = { files: fileList, groups, totalDuplicatedLines };
88
- return { ...base, suggestions: generateSuggestions(base) };
89
- }).sort((a, b) => b.totalDuplicatedLines - a.totalDuplicatedLines);
96
+ result.push({ ...base, suggestions: generateSuggestions(base) });
97
+ }
98
+ return result.sort((a, b) => b.totalDuplicatedLines - a.totalDuplicatedLines);
99
+ }
100
+
101
+ /** Format clone families as structured text for LLM consumption. */
102
+ export function formatCloneFamilies(families: CloneFamily[]): string {
103
+ if (families.length === 0) return 'No clone families detected.';
104
+ const lines: string[] = [`Clone Families (${families.length} total):`];
105
+ for (const f of families.slice(0, 20)) {
106
+ lines.push(`\n ${f.totalDuplicatedLines} duplicated lines across ${f.files.length} files:`);
107
+ for (const file of f.files) lines.push(` ${file}`);
108
+ for (const s of f.suggestions) lines.push(` -> [${s.kind}] ${s.description}`);
109
+ }
110
+ if (families.length > 20) lines.push(`\n ... and ${families.length - 20} more families`);
111
+ return lines.join('\n');
90
112
  }
91
113
 
92
114
  export function cloneFamilySummary(families: CloneFamily[]): {
@@ -97,12 +119,14 @@ export function cloneFamilySummary(families: CloneFamily[]): {
97
119
  const byKind: Record<RefactoringKind, number> = {
98
120
  ExtractFunction: 0, ExtractModule: 0, MergeDirectories: 0,
99
121
  };
122
+ let totalDuplicatedLines = 0;
100
123
  for (const f of families) {
124
+ totalDuplicatedLines += f.totalDuplicatedLines;
101
125
  for (const s of f.suggestions) byKind[s.kind]++;
102
126
  }
103
127
  return {
104
128
  totalFamilies: families.length,
105
- totalDuplicatedLines: families.reduce((s, f) => s + f.totalDuplicatedLines, 0),
129
+ totalDuplicatedLines,
106
130
  byKind,
107
131
  };
108
132
  }
@@ -58,3 +58,57 @@ export function ownerLabel(co: CodeOwnersLike, relativePath: string): string {
58
58
  }
59
59
  return owners[0];
60
60
  }
61
+
62
+ export interface OwnershipAggregate {
63
+ /** Paths that have no owner. */
64
+ unowned: string[];
65
+ /** Map from owner handle → list of owned file paths. */
66
+ byOwner: Map<string, string[]>;
67
+ /** Total files analyzed. */
68
+ totalFiles: number;
69
+ }
70
+
71
+ /**
72
+ * Aggregate ownership across a list of relative paths in a single pass.
73
+ * Each file is attributed to its primary owner (first in the owners list).
74
+ */
75
+ export function aggregateOwnership(
76
+ co: CodeOwnersLike,
77
+ relativePaths: string[],
78
+ ): OwnershipAggregate {
79
+ const unowned: string[] = [];
80
+ const byOwner = new Map<string, string[]>();
81
+
82
+ for (const path of relativePaths) {
83
+ const owners = co.ownersOf(path);
84
+ if (!owners || owners.length === 0) {
85
+ unowned.push(path);
86
+ } else {
87
+ const primary = owners[0];
88
+ let bucket = byOwner.get(primary);
89
+ if (!bucket) { bucket = []; byOwner.set(primary, bucket); }
90
+ bucket.push(path);
91
+ }
92
+ }
93
+
94
+ return { unowned, byOwner, totalFiles: relativePaths.length };
95
+ }
96
+
97
+ /**
98
+ * Format an OwnershipAggregate as structured text for LLM consumption.
99
+ */
100
+ export function formatOwnershipReport(agg: OwnershipAggregate): string {
101
+ const lines: string[] = [
102
+ `Ownership report: ${agg.totalFiles} files, ${agg.byOwner.size} owner(s), ${agg.unowned.length} unowned`,
103
+ '',
104
+ ];
105
+ // Sort owners by file count descending
106
+ const sorted = Array.from(agg.byOwner.entries()).sort((a, b) => b[1].length - a[1].length);
107
+ for (const [owner, files] of sorted) {
108
+ lines.push(` ${owner}: ${files.length} file(s)`);
109
+ }
110
+ if (agg.unowned.length > 0) {
111
+ lines.push(` ${UNOWNED_LABEL}: ${agg.unowned.length} file(s)`);
112
+ }
113
+ return lines.join('\n');
114
+ }
@@ -86,6 +86,12 @@ function pathMatchesPattern(filePath: string, pattern: string): boolean {
86
86
  return norm === pat || norm.endsWith('/' + pat) || norm.startsWith(pat + '/');
87
87
  }
88
88
 
89
+ /** Single-call helper — avoids triple matchOwners invocations when callers need all three fields. */
90
+ export function singleFileOwnership(entries: CodeownersEntry[], filePath: string): { section: string; owners: string[]; ownerCount: number } {
91
+ const { owners, section } = matchOwners(entries, filePath);
92
+ return { section: section ?? NO_SECTION_LABEL, owners, ownerCount: owners.length };
93
+ }
94
+
89
95
  export function ownerCountOf(entries: CodeownersEntry[], filePath: string): number {
90
96
  return matchOwners(entries, filePath).owners.length;
91
97
  }
@@ -102,3 +108,43 @@ export function sectionAndOwnersOf(entries: CodeownersEntry[], filePath: string)
102
108
  export function hasSections(entries: CodeownersEntry[]): boolean {
103
109
  return entries.some(e => e.section !== undefined);
104
110
  }
111
+
112
+ export interface CodeownersGitlabReport {
113
+ totalEntries: number;
114
+ hasSections: boolean;
115
+ sectionNames: string[];
116
+ ownershipSummary: Array<{ file: string; section: string; owners: string[] }>;
117
+ }
118
+
119
+ /**
120
+ * Format a batch file-ownership report for LLM consumption.
121
+ * Uses singleFileOwnership() to avoid N*3 matchOwners scans.
122
+ */
123
+ export function formatCodeownersGitlab(entries: CodeownersEntry[], filePaths: string[]): string {
124
+ if (entries.length === 0) return 'No CODEOWNERS entries found.';
125
+
126
+ const sections = new Set<string>();
127
+ const rows: Array<{ file: string; section: string; owners: string[] }> = [];
128
+ for (const fp of filePaths) {
129
+ const { section, owners } = singleFileOwnership(entries, fp);
130
+ sections.add(section);
131
+ rows.push({ file: fp, section, owners });
132
+ }
133
+
134
+ const unowned = rows.filter(r => r.owners.length === 0);
135
+ const lines: string[] = [
136
+ `CODEOWNERS (GitLab) — ${entries.length} entries, ${sections.size} section(s)`,
137
+ `Sections: ${[...sections].join(', ') || '(none)'}`,
138
+ `Files scanned: ${filePaths.length} Unowned: ${unowned.length}`,
139
+ '',
140
+ ];
141
+ for (const r of rows) {
142
+ const ownerStr = r.owners.length > 0 ? r.owners.join(' ') : UNOWNED_LABEL;
143
+ lines.push(` ${r.file} [${r.section}] ${ownerStr}`);
144
+ }
145
+ if (unowned.length > 0) {
146
+ lines.push('', `Unowned files (${unowned.length}):`);
147
+ for (const r of unowned) lines.push(` ${r.file}`);
148
+ }
149
+ return lines.join('\n');
150
+ }
@@ -98,6 +98,32 @@ export function parseCodeowners(repoRoot: string): CodeownersEntry[] {
98
98
  return entries;
99
99
  }
100
100
 
101
+ // ── Compiled entry (regex precompiled once) ────────────────────────────────────
102
+
103
+ export interface CompiledEntry {
104
+ owners: string[];
105
+ re: RegExp;
106
+ }
107
+
108
+ /**
109
+ * Compile an array of CodeownersEntry into CompiledEntry objects so that
110
+ * globToRegex() runs exactly once per pattern instead of once per file lookup.
111
+ */
112
+ export function compileEntries(entries: CodeownersEntry[]): CompiledEntry[] {
113
+ return entries.map(e => ({ owners: e.owners, re: globToRegex(e.pattern) }));
114
+ }
115
+
116
+ // Resolve owner for a single file path using precompiled regexes.
117
+ // Last matching entry wins (GitHub semantics).
118
+ export function resolveOwnerCompiled(compiled: CompiledEntry[], filePath: string): string[] {
119
+ const normalised = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
120
+ let lastMatch: CompiledEntry | null = null;
121
+ for (const entry of compiled) {
122
+ if (entry.re.test(normalised)) lastMatch = entry;
123
+ }
124
+ return lastMatch ? lastMatch.owners : [];
125
+ }
126
+
101
127
  // Resolve owner for a single file path (relative to repoRoot)
102
128
  // Last matching entry wins
103
129
  export function resolveOwner(entries: CodeownersEntry[], filePath: string): string[] {
@@ -121,6 +147,9 @@ export function annotateOwnership(
121
147
  repoRoot: string,
122
148
  ): { annotated: number; unowned: number } {
123
149
  const entries = parseCodeowners(repoRoot);
150
+ // Compile regexes once for all file nodes — avoids O(files * patterns) re-compilations.
151
+ const compiled = compileEntries(entries);
152
+
124
153
  const fileNodes = db
125
154
  .prepare(`SELECT id, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`)
126
155
  .all() as { id: string; file_path: string; properties: string | null }[];
@@ -133,7 +162,7 @@ export function annotateOwnership(
133
162
  const updateAll = db.transaction(() => {
134
163
  for (const node of fileNodes) {
135
164
  const relPath = relative(repoRoot, node.file_path).replace(/\\/g, '/');
136
- const owners = resolveOwner(entries, relPath);
165
+ const owners = resolveOwnerCompiled(compiled, relPath);
137
166
  const existing = node.properties ? JSON.parse(node.properties) : {};
138
167
  const updated = { ...existing, codeowners: owners };
139
168
  update.run(JSON.stringify(updated), node.id);
@@ -158,6 +187,8 @@ export function groupByOwner<T extends { filePath?: string | null }>(
158
187
  findings: T[],
159
188
  entries: CodeownersEntry[],
160
189
  ): Map<string, T[]> {
190
+ // Compile patterns once for all findings to avoid O(findings * patterns) regex constructions.
191
+ const compiled = compileEntries(entries);
161
192
  const result = new Map<string, T[]>();
162
193
 
163
194
  for (const finding of findings) {
@@ -168,7 +199,7 @@ export function groupByOwner<T extends { filePath?: string | null }>(
168
199
  continue;
169
200
  }
170
201
 
171
- const owners = resolveOwner(entries, finding.filePath);
202
+ const owners = resolveOwnerCompiled(compiled, finding.filePath);
172
203
  if (owners.length === 0) {
173
204
  const bucket = result.get('unowned') ?? [];
174
205
  bucket.push(finding);