@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
@@ -32,6 +32,12 @@ export function computeCrapScore(cc: number, coverage: number): number {
32
32
  return cc * cc * Math.pow(1 - cov, 3) + cc;
33
33
  }
34
34
 
35
+ function percentile(arr: number[], p: number): number {
36
+ if (arr.length === 0) return 0;
37
+ const idx = Math.floor((p / 100) * (arr.length - 1));
38
+ return arr[idx];
39
+ }
40
+
35
41
  /**
36
42
  * Compute cyclomatic and cognitive complexity for all Function/Method nodes
37
43
  * in the knowledge graph. Uses graph degree as a proxy for decision points
@@ -41,9 +47,11 @@ export function computeCrapScore(cc: number, coverage: number): number {
41
47
  * - Cognitive complexity proxy: (endLine - startLine) / 10, capped at 20
42
48
  * - LOC: endLine - startLine + 1 (or 1 if missing)
43
49
  * - CRAP: computed with coverage = 0 (worst-case, no test data available)
50
+ *
51
+ * Batches all SQL work to avoid N+1 queries (2 GROUP BY queries replace 2×N individual queries).
44
52
  */
45
53
  export function computeComplexity(db: MonographDb): ComplexityReport {
46
- // Query all function/method nodes
54
+ // Fetch all function/method nodes in one query
47
55
  const rows = db.prepare(`
48
56
  SELECT id, name, file_path, start_line, end_line, properties
49
57
  FROM nodes
@@ -57,52 +65,53 @@ export function computeComplexity(db: MonographDb): ComplexityReport {
57
65
  properties: string | null;
58
66
  }>;
59
67
 
68
+ if (rows.length === 0) {
69
+ return { functions: [], p50cc: 0, p90cc: 0, p95cc: 0, highComplexityCount: 0, criticalCount: 0 };
70
+ }
71
+
72
+ // ── Batch edge counts: 2 GROUP BY queries replace 2×N individual queries ─
73
+ const callsMap = new Map<string, number>();
74
+ for (const { source_id, c } of db.prepare(`
75
+ SELECT source_id, COUNT(*) as c FROM edges WHERE relation = 'CALLS' GROUP BY source_id
76
+ `).all() as { source_id: string; c: number }[]) {
77
+ callsMap.set(source_id, c);
78
+ }
79
+
80
+ const accessesMap = new Map<string, number>();
81
+ for (const { source_id, c } of db.prepare(`
82
+ SELECT source_id, COUNT(*) as c FROM edges WHERE relation = 'ACCESSES' GROUP BY source_id
83
+ `).all() as { source_id: string; c: number }[]) {
84
+ accessesMap.set(source_id, c);
85
+ }
86
+
87
+ // ── Compute metrics for each function ────────────────────────────────────
60
88
  const functions: FunctionComplexity[] = [];
89
+ const propUpdates: Array<{ id: string; props: string }> = [];
61
90
 
62
91
  for (const row of rows) {
63
92
  const props = row.properties ? JSON.parse(row.properties) as Record<string, unknown> : {};
64
93
 
65
- // Outgoing CALLS edges (decision points proxy)
66
- const callsCount = (db.prepare(`
67
- SELECT COUNT(*) as c FROM edges
68
- WHERE source_id = ? AND relation = 'CALLS'
69
- `).get(row.id) as { c: number }).c;
70
-
71
- // Outgoing ACCESSES edges
72
- const accessesCount = (db.prepare(`
73
- SELECT COUNT(*) as c FROM edges
74
- WHERE source_id = ? AND relation = 'ACCESSES'
75
- `).get(row.id) as { c: number }).c;
94
+ const callsCount = callsMap.get(row.id) ?? 0;
95
+ const accessesCount = accessesMap.get(row.id) ?? 0;
76
96
 
77
- // Cyclomatic complexity proxy: decision points + 1
78
97
  const cyclomaticComplexity = Math.round(callsCount * 0.5 + accessesCount * 0.2 + 1);
79
98
 
80
- // Cognitive complexity proxy: nesting depth approximation capped at 20
81
99
  const lineSpan = (row.start_line != null && row.end_line != null)
82
100
  ? row.end_line - row.start_line
83
101
  : 0;
84
102
  const cognitiveComplexity = Math.min(Math.round(lineSpan / 10), 20);
85
-
86
- // LOC
87
103
  const linesOfCode = (row.start_line != null && row.end_line != null)
88
104
  ? row.end_line - row.start_line + 1
89
105
  : 1;
90
106
 
91
- // Param count from stored properties
92
107
  const paramCount = typeof props.paramCount === 'number' ? props.paramCount : 0;
93
-
94
- // CRAP score with no coverage data (worst-case: coverage = 0)
95
108
  const crapScore = computeCrapScore(cyclomaticComplexity, 0);
96
109
 
97
- // Store computed metrics back onto the node properties
98
- const updatedProps = {
99
- ...props,
100
- cyclomaticComplexity,
101
- cognitiveComplexity,
102
- crapScore,
103
- };
104
- db.prepare('UPDATE nodes SET properties = ? WHERE id = ?')
105
- .run(JSON.stringify(updatedProps), row.id);
110
+ // Stage property update batched in a single transaction below
111
+ propUpdates.push({
112
+ id: row.id,
113
+ props: JSON.stringify({ ...props, cyclomaticComplexity, cognitiveComplexity, crapScore }),
114
+ });
106
115
 
107
116
  functions.push({
108
117
  nodeId: row.id,
@@ -118,28 +127,74 @@ export function computeComplexity(db: MonographDb): ComplexityReport {
118
127
  });
119
128
  }
120
129
 
121
- // Compute percentiles for cyclomatic complexity
122
- const ccValues = functions.map(f => f.cyclomaticComplexity).sort((a, b) => a - b);
123
- const len = ccValues.length;
124
-
125
- function percentile(arr: number[], p: number): number {
126
- if (arr.length === 0) return 0;
127
- const idx = Math.floor((p / 100) * (arr.length - 1));
128
- return arr[idx];
129
- }
130
+ // ── Batch UPDATE all property writes in one transaction ───────────────────
131
+ const updateStmt = db.prepare('UPDATE nodes SET properties = ? WHERE id = ?');
132
+ db.transaction((updates: typeof propUpdates) => {
133
+ for (const { id, props } of updates) updateStmt.run(props, id);
134
+ })(propUpdates);
130
135
 
131
- const p50cc = percentile(ccValues, 50);
132
- const p90cc = percentile(ccValues, 90);
133
- const p95cc = percentile(ccValues, 95);
134
- const highComplexityCount = functions.filter(f => f.cyclomaticComplexity > 10).length;
135
- const criticalCount = functions.filter(f => f.cyclomaticComplexity > 20).length;
136
+ // ── Compute percentiles ───────────────────────────────────────────────────
137
+ const ccValues = functions.map(f => f.cyclomaticComplexity).sort((a, b) => a - b);
136
138
 
137
139
  return {
138
140
  functions,
139
- p50cc,
140
- p90cc,
141
- p95cc,
142
- highComplexityCount,
143
- criticalCount,
141
+ p50cc: percentile(ccValues, 50),
142
+ p90cc: percentile(ccValues, 90),
143
+ p95cc: percentile(ccValues, 95),
144
+ highComplexityCount: functions.filter(f => f.cyclomaticComplexity > 10).length,
145
+ criticalCount: functions.filter(f => f.cyclomaticComplexity > 20).length,
144
146
  };
145
147
  }
148
+
149
+ /**
150
+ * Format a ComplexityReport as structured text with file:line hints for LLM navigation.
151
+ *
152
+ * @param report - ComplexityReport from computeComplexity()
153
+ * @param topN - number of worst offenders to list (default 10)
154
+ * @returns structured text suitable for LLM consumption
155
+ */
156
+ export function formatComplexity(report: ComplexityReport, topN = 10): string {
157
+ const { functions, p50cc, p90cc, p95cc, highComplexityCount, criticalCount } = report;
158
+
159
+ if (functions.length === 0) {
160
+ return 'complexity: no Function/Method nodes found in graph\n';
161
+ }
162
+
163
+ const lines: string[] = [
164
+ `complexity: ${functions.length} functions analysed`,
165
+ ` p50_cc: ${p50cc} p90_cc: ${p90cc} p95_cc: ${p95cc}`,
166
+ ` high(cc>10): ${highComplexityCount} critical(cc>20): ${criticalCount}`,
167
+ '',
168
+ ];
169
+
170
+ // Sort by crapScore descending (worst first)
171
+ const worst = [...functions]
172
+ .sort((a, b) => b.crapScore - a.crapScore)
173
+ .slice(0, topN);
174
+
175
+ if (worst.length > 0) {
176
+ lines.push(`top_${topN}_worst_crap:`);
177
+ for (const fn of worst) {
178
+ const loc = fn.filePath
179
+ ? `${fn.filePath}:${fn.startLine ?? 1}`
180
+ : `<unknown>:1`;
181
+ lines.push(` - ${fn.name}`);
182
+ lines.push(` file: ${loc}`);
183
+ lines.push(` cc: ${fn.cyclomaticComplexity} crap: ${fn.crapScore.toFixed(1)} loc: ${fn.linesOfCode}`);
184
+ }
185
+ lines.push('');
186
+ }
187
+
188
+ const critical = functions.filter(f => f.cyclomaticComplexity > 20);
189
+ if (critical.length > 0) {
190
+ lines.push(`critical_functions(cc>20): ${critical.length}`);
191
+ for (const fn of critical.slice(0, 5)) {
192
+ const loc = fn.filePath ? `${fn.filePath}:${fn.startLine ?? 1}` : `<unknown>:1`;
193
+ lines.push(` - ${fn.name} file: ${loc} cc: ${fn.cyclomaticComplexity}`);
194
+ }
195
+ if (critical.length > 5) lines.push(` ... and ${critical.length - 5} more`);
196
+ lines.push('');
197
+ }
198
+
199
+ return lines.join('\n');
200
+ }
@@ -36,6 +36,23 @@ export function crossReference(duplication: CrossRefDuplicationReport, deadCode:
36
36
  const findings: CombinedFinding[] = [];
37
37
  let clonesInUnusedFiles = 0, clonesWithUnusedExports = 0;
38
38
 
39
+ // Preindex unusedExports and unusedTypes by file path to avoid O(N) Array.find
40
+ // per clone instance. Each lookup becomes an O(1) Map get + a short in-range scan
41
+ // over the candidates for that specific file.
42
+ const exportsByPath = new Map<string, Array<{ exportName: string; line: number }>>();
43
+ for (const e of deadCode.unusedExports) {
44
+ const bucket = exportsByPath.get(e.path) ?? [];
45
+ bucket.push({ exportName: e.exportName, line: e.line });
46
+ exportsByPath.set(e.path, bucket);
47
+ }
48
+
49
+ const typesByPath = new Map<string, Array<{ typeName: string; line: number }>>();
50
+ for (const t of deadCode.unusedTypes) {
51
+ const bucket = typesByPath.get(t.path) ?? [];
52
+ bucket.push({ typeName: t.typeName, line: t.line });
53
+ typesByPath.set(t.path, bucket);
54
+ }
55
+
39
56
  for (let gi = 0; gi < duplication.cloneGroups.length; gi++) {
40
57
  for (const inst of duplication.cloneGroups[gi].instances) {
41
58
  if (deadCode.unusedFiles.has(inst.file)) {
@@ -43,13 +60,17 @@ export function crossReference(duplication: CrossRefDuplicationReport, deadCode:
43
60
  clonesInUnusedFiles++;
44
61
  continue;
45
62
  }
46
- const overlap = deadCode.unusedExports.find(e => e.path === inst.file && e.line >= inst.startLine && e.line <= inst.endLine);
63
+
64
+ const exportsForFile = exportsByPath.get(inst.file);
65
+ const overlap = exportsForFile?.find(e => e.line >= inst.startLine && e.line <= inst.endLine);
47
66
  if (overlap) {
48
67
  findings.push({ cloneInstance: inst, deadCodeKind: { type: 'unused-export', exportName: overlap.exportName }, groupIndex: gi });
49
68
  clonesWithUnusedExports++;
50
69
  continue;
51
70
  }
52
- const typeOverlap = deadCode.unusedTypes.find(t => t.path === inst.file && t.line >= inst.startLine && t.line <= inst.endLine);
71
+
72
+ const typesForFile = typesByPath.get(inst.file);
73
+ const typeOverlap = typesForFile?.find(t => t.line >= inst.startLine && t.line <= inst.endLine);
53
74
  if (typeOverlap) {
54
75
  findings.push({ cloneInstance: inst, deadCodeKind: { type: 'unused-type', typeName: typeOverlap.typeName }, groupIndex: gi });
55
76
  clonesWithUnusedExports++;
@@ -111,3 +111,29 @@ export function crossReferenceDuplicatesAndDeadCode(db: MonographDb): CrossRefer
111
111
  crossCount: crossIds.length,
112
112
  };
113
113
  }
114
+
115
+ /**
116
+ * Format a CrossReferenceReport as structured text for LLM consumption.
117
+ */
118
+ export function formatCrossReferenceReport(report: CrossReferenceReport): string {
119
+ const lines: string[] = [
120
+ `Cross-reference analysis: dead code × structural duplicates`,
121
+ ` Dead (unreachable) file nodes : ${report.deadCount}`,
122
+ ` Duplicated file nodes : ${report.duplicateCount}`,
123
+ ` Both dead AND duplicated : ${report.crossCount} (highest-confidence safe-delete candidates)`,
124
+ ];
125
+
126
+ if (report.crossCount === 0) {
127
+ lines.push(' No cross-referenced candidates found.');
128
+ return lines.join('\n');
129
+ }
130
+
131
+ lines.push('');
132
+ lines.push('Safe-delete candidates:');
133
+ for (const f of report.findings) {
134
+ const loc = f.filePath ? ` ${f.filePath}` : ` (no path) ${f.nodeName}`;
135
+ lines.push(loc);
136
+ lines.push(` ${f.description}`);
137
+ }
138
+ return lines.join('\n');
139
+ }
@@ -25,7 +25,12 @@ function canonicalCycle(cycle: number[], idToPath: Map<number, string>): number[
25
25
  minPos = i;
26
26
  }
27
27
  }
28
- return [...cycle.slice(minPos), ...cycle.slice(0, minPos)];
28
+ if (minPos === 0) return cycle.slice(); // already canonical — single allocation
29
+ // Single-pass rotation without 3 intermediate arrays (2x slice + spread)
30
+ const n = cycle.length;
31
+ const result = new Array<number>(n);
32
+ for (let i = 0; i < n; i++) result[i] = cycle[(minPos + i) % n];
33
+ return result;
29
34
  }
30
35
 
31
36
  function tryRecordCycle(
@@ -103,7 +108,8 @@ function enumerateElementaryCycles(
103
108
  const cycles: number[][] = [];
104
109
  const seen = new Set<string>();
105
110
 
106
- const sortedNodes = [...sccNodes].sort((a, b) => {
111
+ // Use slice() rather than spread to avoid the iterator allocation overhead
112
+ const sortedNodes = sccNodes.slice().sort((a, b) => {
107
113
  const pa = idToPath.get(a) ?? '';
108
114
  const pb = idToPath.get(b) ?? '';
109
115
  return pa < pb ? -1 : pa > pb ? 1 : 0;
@@ -284,9 +290,22 @@ export function findCycles(
284
290
  });
285
291
 
286
292
  return result.map(cycle => ({
287
- files: [...cycle]
293
+ // slice() instead of spread to avoid iterator overhead
294
+ files: cycle.slice()
288
295
  .sort((a, b) => (idToPath.get(a) ?? '').localeCompare(idToPath.get(b) ?? ''))
289
296
  .map(id => idToPath.get(id) ?? String(id)),
290
297
  length: cycle.length,
291
298
  }));
292
299
  }
300
+
301
+ /** Format cycle groups as structured text for LLM consumption. */
302
+ export function formatCycleGroups(groups: CycleGroup[]): string {
303
+ if (groups.length === 0) return 'No import cycles detected.';
304
+ const lines: string[] = [`Import Cycles (${groups.length} total):`];
305
+ for (const g of groups.slice(0, 30)) {
306
+ lines.push(`\n Cycle (length ${g.length}):`);
307
+ for (const f of g.files) lines.push(` ${f}`);
308
+ }
309
+ if (groups.length > 30) lines.push(`\n ... and ${groups.length - 30} more cycles`);
310
+ return lines.join('\n');
311
+ }
@@ -1,71 +1,127 @@
1
1
  import type { MonographDb } from '../storage/db.js';
2
2
 
3
- /**
4
- * Find ALL strongly connected components (SCCs) using Kosaraju's algorithm.
5
- *
6
- * Returns every SCC, including singleton components (single nodes with no self-loop).
7
- * This is the full SCC decomposition of the graph.
8
- *
9
- * @param db - The MonographDb instance
10
- * @returns Array of SCCs; each SCC is an array of node ids in that component.
11
- */
12
- export function findStronglyConnectedComponents(db: MonographDb): string[][] {
13
- const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
14
- const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
15
- source_id: string;
16
- target_id: string;
17
- }[];
18
-
19
- if (nodeRows.length === 0) return [];
20
-
21
- const nodes = nodeRows.map(r => r.id);
3
+ /** Build adjacency maps from DB rows (shared by both exports). */
4
+ function buildAdjacency(
5
+ nodes: string[],
6
+ edgeRows: { source_id: string; target_id: string }[],
7
+ ): {
8
+ adj: Map<string, string[]>;
9
+ radj: Map<string, string[]>;
10
+ selfLoops: Set<string>;
11
+ } {
22
12
  const nodeSet = new Set(nodes);
23
-
24
13
  const adj = new Map<string, string[]>();
25
14
  const radj = new Map<string, string[]>();
26
15
  for (const n of nodes) {
27
16
  adj.set(n, []);
28
17
  radj.set(n, []);
29
18
  }
30
-
19
+ const selfLoops = new Set<string>();
31
20
  for (const { source_id: src, target_id: tgt } of edgeRows) {
32
21
  if (!nodeSet.has(src) || !nodeSet.has(tgt)) continue;
33
- if (src === tgt) continue;
22
+ if (src === tgt) {
23
+ selfLoops.add(src);
24
+ continue;
25
+ }
34
26
  adj.get(src)!.push(tgt);
35
27
  radj.get(tgt)!.push(src);
36
28
  }
29
+ return { adj, radj, selfLoops };
30
+ }
37
31
 
38
- const visited = new Set<string>();
39
- const finishOrder: string[] = [];
40
-
41
- function dfs1(node: string): void {
42
- visited.add(node);
43
- for (const neighbor of adj.get(node) ?? []) {
44
- if (!visited.has(neighbor)) dfs1(neighbor);
32
+ /** Iterative DFS pass 1: populate finish order (replaces recursive dfs1). */
33
+ function iterativeDfs1(
34
+ roots: string[],
35
+ adj: Map<string, string[]>,
36
+ visited: Set<string>,
37
+ finishOrder: string[],
38
+ ): void {
39
+ for (const root of roots) {
40
+ if (visited.has(root)) continue;
41
+ // Stack entries: [node, neighborIndex]
42
+ const stack: [string, number][] = [[root, 0]];
43
+ visited.add(root);
44
+ while (stack.length > 0) {
45
+ const frame = stack[stack.length - 1];
46
+ const [node, idx] = frame;
47
+ const neighbors = adj.get(node) ?? [];
48
+ if (idx < neighbors.length) {
49
+ frame[1]++;
50
+ const neighbor = neighbors[idx];
51
+ if (!visited.has(neighbor)) {
52
+ visited.add(neighbor);
53
+ stack.push([neighbor, 0]);
54
+ }
55
+ } else {
56
+ stack.pop();
57
+ finishOrder.push(node);
58
+ }
45
59
  }
46
- finishOrder.push(node);
47
60
  }
61
+ }
48
62
 
49
- for (const node of nodes) {
50
- if (!visited.has(node)) dfs1(node);
51
- }
52
-
53
- const assigned = new Set<string>();
54
- const sccs: string[][] = [];
55
-
56
- function dfs2(node: string, component: string[]): void {
57
- assigned.add(node);
63
+ /** Iterative DFS pass 2: collect SCC component (replaces recursive dfs2). */
64
+ function iterativeDfs2(
65
+ root: string,
66
+ radj: Map<string, string[]>,
67
+ assigned: Set<string>,
68
+ component: string[],
69
+ ): void {
70
+ const stack: string[] = [root];
71
+ assigned.add(root);
72
+ while (stack.length > 0) {
73
+ const node = stack.pop()!;
58
74
  component.push(node);
59
75
  for (const neighbor of radj.get(node) ?? []) {
60
- if (!assigned.has(neighbor)) dfs2(neighbor, component);
76
+ if (!assigned.has(neighbor)) {
77
+ assigned.add(neighbor);
78
+ stack.push(neighbor);
79
+ }
61
80
  }
62
81
  }
82
+ }
83
+
84
+ /**
85
+ * Fetch nodes and edges once and return raw rows (shared helper).
86
+ */
87
+ function fetchGraphRows(db: MonographDb): {
88
+ nodes: string[];
89
+ edgeRows: { source_id: string; target_id: string }[];
90
+ } {
91
+ const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
92
+ const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
93
+ source_id: string;
94
+ target_id: string;
95
+ }[];
96
+ return { nodes: nodeRows.map(r => r.id), edgeRows };
97
+ }
98
+
99
+ /**
100
+ * Find ALL strongly connected components (SCCs) using iterative Kosaraju's algorithm.
101
+ *
102
+ * Returns every SCC, including singleton components (single nodes with no self-loop).
103
+ * This is the full SCC decomposition of the graph.
104
+ *
105
+ * @param db - The MonographDb instance
106
+ * @returns Array of SCCs; each SCC is an array of node ids in that component.
107
+ */
108
+ export function findStronglyConnectedComponents(db: MonographDb): string[][] {
109
+ const { nodes, edgeRows } = fetchGraphRows(db);
110
+ if (nodes.length === 0) return [];
63
111
 
112
+ const { adj, radj } = buildAdjacency(nodes, edgeRows);
113
+
114
+ const visited = new Set<string>();
115
+ const finishOrder: string[] = [];
116
+ iterativeDfs1(nodes, adj, visited, finishOrder);
117
+
118
+ const assigned = new Set<string>();
119
+ const sccs: string[][] = [];
64
120
  for (let i = finishOrder.length - 1; i >= 0; i--) {
65
121
  const node = finishOrder[i];
66
122
  if (!assigned.has(node)) {
67
123
  const component: string[] = [];
68
- dfs2(node, component);
124
+ iterativeDfs2(node, radj, assigned, component);
69
125
  sccs.push(component);
70
126
  }
71
127
  }
@@ -77,77 +133,31 @@ export function findStronglyConnectedComponents(db: MonographDb): string[][] {
77
133
  * Find all strongly connected components (SCCs) with more than 1 node,
78
134
  * or self-loops, and return them as cycle node lists.
79
135
  *
80
- * Uses Kosaraju's algorithm to find SCCs. Each SCC with size > 1 represents
81
- * a cycle. Self-loops (node -> itself) are also detected separately.
136
+ * Uses iterative Kosaraju's algorithm to avoid stack overflow on large graphs.
137
+ * Each SCC with size > 1 represents a cycle. Self-loops are also detected.
82
138
  *
83
139
  * @param db - The MonographDb instance
84
140
  * @returns Array of cycles; each cycle is an array of node ids participating in it.
85
141
  */
86
142
  export function findCycles(db: MonographDb): string[][] {
87
- const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
88
- const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
89
- source_id: string;
90
- target_id: string;
91
- }[];
92
-
93
- if (nodeRows.length === 0) return [];
94
-
95
- const nodes = nodeRows.map(r => r.id);
96
- const nodeSet = new Set(nodes);
143
+ const { nodes, edgeRows } = fetchGraphRows(db);
144
+ if (nodes.length === 0) return [];
97
145
 
98
- // Build adjacency list and reverse adjacency list
99
- const adj = new Map<string, string[]>();
100
- const radj = new Map<string, string[]>();
101
- for (const n of nodes) {
102
- adj.set(n, []);
103
- radj.set(n, []);
104
- }
105
-
106
- const selfLoops = new Set<string>();
107
-
108
- for (const { source_id: src, target_id: tgt } of edgeRows) {
109
- if (!nodeSet.has(src) || !nodeSet.has(tgt)) continue;
110
- if (src === tgt) {
111
- selfLoops.add(src);
112
- continue;
113
- }
114
- adj.get(src)!.push(tgt);
115
- radj.get(tgt)!.push(src);
116
- }
146
+ const { adj, radj, selfLoops } = buildAdjacency(nodes, edgeRows);
117
147
 
118
- // Kosaraju's algorithm — pass 1: DFS on original graph, build finish order
148
+ // Pass 1: iterative DFS on original graph, build finish order
119
149
  const visited = new Set<string>();
120
150
  const finishOrder: string[] = [];
151
+ iterativeDfs1(nodes, adj, visited, finishOrder);
121
152
 
122
- function dfs1(node: string): void {
123
- visited.add(node);
124
- for (const neighbor of adj.get(node) ?? []) {
125
- if (!visited.has(neighbor)) dfs1(neighbor);
126
- }
127
- finishOrder.push(node);
128
- }
129
-
130
- for (const node of nodes) {
131
- if (!visited.has(node)) dfs1(node);
132
- }
133
-
134
- // Kosaraju's algorithm — pass 2: DFS on reverse graph in reverse finish order
153
+ // Pass 2: iterative DFS on reverse graph in reverse finish order
135
154
  const assigned = new Set<string>();
136
155
  const sccs: string[][] = [];
137
-
138
- function dfs2(node: string, component: string[]): void {
139
- assigned.add(node);
140
- component.push(node);
141
- for (const neighbor of radj.get(node) ?? []) {
142
- if (!assigned.has(neighbor)) dfs2(neighbor, component);
143
- }
144
- }
145
-
146
156
  for (let i = finishOrder.length - 1; i >= 0; i--) {
147
157
  const node = finishOrder[i];
148
158
  if (!assigned.has(node)) {
149
159
  const component: string[] = [];
150
- dfs2(node, component);
160
+ iterativeDfs2(node, radj, assigned, component);
151
161
  if (component.length > 1) {
152
162
  sccs.push(component);
153
163
  }
@@ -161,3 +171,52 @@ export function findCycles(db: MonographDb): string[][] {
161
171
 
162
172
  return sccs;
163
173
  }
174
+
175
+ /**
176
+ * Format cycle results as structured text with file:line hints for LLM navigation.
177
+ *
178
+ * @param cycles - Array of cycle arrays (node ids / file paths)
179
+ * @param nodeToFile - Optional map from node id to file path for resolving paths
180
+ * @returns Structured text output suitable for LLM consumption
181
+ */
182
+ export function formatCycles(
183
+ cycles: string[][],
184
+ nodeToFile?: Map<string, string>,
185
+ ): string {
186
+ if (cycles.length === 0) {
187
+ return 'cycles: none\nstatus: no circular dependencies detected\n';
188
+ }
189
+
190
+ const resolve = (id: string): string => nodeToFile?.get(id) ?? id;
191
+
192
+ const lines: string[] = [
193
+ `cycles: ${cycles.length} circular dependency group(s) found`,
194
+ '',
195
+ ];
196
+
197
+ // Sort by cycle length ascending, then alphabetically
198
+ const sorted = [...cycles].sort((a, b) => {
199
+ if (a.length !== b.length) return a.length - b.length;
200
+ return resolve(a[0]).localeCompare(resolve(b[0]));
201
+ });
202
+
203
+ sorted.forEach((cycle, i) => {
204
+ const isSelfLoop = cycle.length === 1;
205
+ const label = isSelfLoop ? 'self-loop' : `cycle(${cycle.length})`;
206
+ lines.push(`[${i + 1}] ${label}`);
207
+ for (const node of cycle) {
208
+ lines.push(` file: ${resolve(node)}:1`);
209
+ }
210
+ if (!isSelfLoop) {
211
+ // Show the closing edge
212
+ lines.push(` closes: ${resolve(cycle[cycle.length - 1])} → ${resolve(cycle[0])}`);
213
+ }
214
+ lines.push('');
215
+ });
216
+
217
+ const selfLoopCount = sorted.filter(c => c.length === 1).length;
218
+ const multiNodeCount = sorted.length - selfLoopCount;
219
+ lines.push(`summary: ${multiNodeCount} multi-node cycle(s), ${selfLoopCount} self-loop(s)`);
220
+
221
+ return lines.join('\n');
222
+ }