@monoes/monograph 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (460) hide show
  1. package/__tests__/mcp-tools/cypher.test.ts +10 -6
  2. package/__tests__/mcp-tools/detect-changes.test.ts +14 -11
  3. package/__tests__/pipeline/phases/wildcard-synthesis.test.ts +4 -4
  4. package/__tests__/search/embed-batch-config.test.ts +7 -7
  5. package/__tests__/search/hybrid-query.test.ts +3 -1
  6. package/__tests__/web/api.test.ts +1 -1
  7. package/dist/src/analysis/churn-cache.d.ts.map +1 -1
  8. package/dist/src/analysis/churn-cache.js +19 -6
  9. package/dist/src/analysis/churn-cache.js.map +1 -1
  10. package/dist/src/analysis/churn-trend.d.ts.map +1 -1
  11. package/dist/src/analysis/churn-trend.js +50 -6
  12. package/dist/src/analysis/churn-trend.js.map +1 -1
  13. package/dist/src/analysis/cluster-quality.d.ts +3 -0
  14. package/dist/src/analysis/cluster-quality.d.ts.map +1 -1
  15. package/dist/src/analysis/cluster-quality.js +18 -12
  16. package/dist/src/analysis/cluster-quality.js.map +1 -1
  17. package/dist/src/analysis/coverage-gaps.d.ts +3 -0
  18. package/dist/src/analysis/coverage-gaps.d.ts.map +1 -1
  19. package/dist/src/analysis/coverage-gaps.js +40 -1
  20. package/dist/src/analysis/coverage-gaps.js.map +1 -1
  21. package/dist/src/analysis/dep-classification.d.ts +2 -0
  22. package/dist/src/analysis/dep-classification.d.ts.map +1 -1
  23. package/dist/src/analysis/dep-classification.js +30 -0
  24. package/dist/src/analysis/dep-classification.js.map +1 -1
  25. package/dist/src/analysis/dependency-health.d.ts +4 -0
  26. package/dist/src/analysis/dependency-health.d.ts.map +1 -1
  27. package/dist/src/analysis/dependency-health.js +28 -0
  28. package/dist/src/analysis/dependency-health.js.map +1 -1
  29. package/dist/src/analysis/duplicate-exports.d.ts +9 -5
  30. package/dist/src/analysis/duplicate-exports.d.ts.map +1 -1
  31. package/dist/src/analysis/duplicate-exports.js +26 -2
  32. package/dist/src/analysis/duplicate-exports.js.map +1 -1
  33. package/dist/src/analysis/feature-flags.d.ts.map +1 -1
  34. package/dist/src/analysis/feature-flags.js +36 -17
  35. package/dist/src/analysis/feature-flags.js.map +1 -1
  36. package/dist/src/analysis/node-similarity.d.ts +19 -0
  37. package/dist/src/analysis/node-similarity.d.ts.map +1 -1
  38. package/dist/src/analysis/node-similarity.js +34 -3
  39. package/dist/src/analysis/node-similarity.js.map +1 -1
  40. package/dist/src/analysis/private-type-leaks.d.ts +4 -0
  41. package/dist/src/analysis/private-type-leaks.d.ts.map +1 -1
  42. package/dist/src/analysis/private-type-leaks.js +43 -2
  43. package/dist/src/analysis/private-type-leaks.js.map +1 -1
  44. package/dist/src/analysis/project-state.d.ts +2 -0
  45. package/dist/src/analysis/project-state.d.ts.map +1 -1
  46. package/dist/src/analysis/project-state.js +18 -7
  47. package/dist/src/analysis/project-state.js.map +1 -1
  48. package/dist/src/analysis/suppressions.d.ts.map +1 -1
  49. package/dist/src/analysis/suppressions.js +3 -1
  50. package/dist/src/analysis/suppressions.js.map +1 -1
  51. package/dist/src/analysis/trace.d.ts +5 -3
  52. package/dist/src/analysis/trace.d.ts.map +1 -1
  53. package/dist/src/analysis/trace.js +59 -53
  54. package/dist/src/analysis/trace.js.map +1 -1
  55. package/dist/src/analysis/unused-class-members.d.ts.map +1 -1
  56. package/dist/src/analysis/unused-class-members.js +7 -1
  57. package/dist/src/analysis/unused-class-members.js.map +1 -1
  58. package/dist/src/analysis/worker-pool.d.ts +3 -0
  59. package/dist/src/analysis/worker-pool.d.ts.map +1 -1
  60. package/dist/src/analysis/worker-pool.js +30 -8
  61. package/dist/src/analysis/worker-pool.js.map +1 -1
  62. package/dist/src/cli/augment.d.ts.map +1 -1
  63. package/dist/src/cli/augment.js +10 -3
  64. package/dist/src/cli/augment.js.map +1 -1
  65. package/dist/src/export/sarif.js +5 -5
  66. package/dist/src/export/sarif.js.map +1 -1
  67. package/dist/src/graph/analyzer.d.ts.map +1 -1
  68. package/dist/src/graph/analyzer.js +17 -2
  69. package/dist/src/graph/analyzer.js.map +1 -1
  70. package/dist/src/graph/author-analytics.d.ts +2 -0
  71. package/dist/src/graph/author-analytics.d.ts.map +1 -1
  72. package/dist/src/graph/author-analytics.js +39 -5
  73. package/dist/src/graph/author-analytics.js.map +1 -1
  74. package/dist/src/graph/baseline.d.ts +5 -0
  75. package/dist/src/graph/baseline.d.ts.map +1 -1
  76. package/dist/src/graph/baseline.js +65 -18
  77. package/dist/src/graph/baseline.js.map +1 -1
  78. package/dist/src/graph/clone-detector.d.ts.map +1 -1
  79. package/dist/src/graph/clone-detector.js +19 -33
  80. package/dist/src/graph/clone-detector.js.map +1 -1
  81. package/dist/src/graph/clone-families.d.ts +2 -0
  82. package/dist/src/graph/clone-families.d.ts.map +1 -1
  83. package/dist/src/graph/clone-families.js +38 -10
  84. package/dist/src/graph/clone-families.js.map +1 -1
  85. package/dist/src/graph/codeowners-extended.d.ts +17 -0
  86. package/dist/src/graph/codeowners-extended.d.ts.map +1 -1
  87. package/dist/src/graph/codeowners-extended.js +42 -0
  88. package/dist/src/graph/codeowners-extended.js.map +1 -1
  89. package/dist/src/graph/codeowners-gitlab.d.ts +21 -0
  90. package/dist/src/graph/codeowners-gitlab.d.ts.map +1 -1
  91. package/dist/src/graph/codeowners-gitlab.js +37 -0
  92. package/dist/src/graph/codeowners-gitlab.js.map +1 -1
  93. package/dist/src/graph/codeowners.d.ts +10 -0
  94. package/dist/src/graph/codeowners.d.ts.map +1 -1
  95. package/dist/src/graph/codeowners.js +24 -2
  96. package/dist/src/graph/codeowners.js.map +1 -1
  97. package/dist/src/graph/complexity.d.ts +10 -0
  98. package/dist/src/graph/complexity.d.ts.map +1 -1
  99. package/dist/src/graph/complexity.js +93 -43
  100. package/dist/src/graph/complexity.js.map +1 -1
  101. package/dist/src/graph/cross-reference-findings.d.ts.map +1 -1
  102. package/dist/src/graph/cross-reference-findings.js +19 -2
  103. package/dist/src/graph/cross-reference-findings.js.map +1 -1
  104. package/dist/src/graph/cross-reference.d.ts +4 -0
  105. package/dist/src/graph/cross-reference.d.ts.map +1 -1
  106. package/dist/src/graph/cross-reference.js +23 -0
  107. package/dist/src/graph/cross-reference.js.map +1 -1
  108. package/dist/src/graph/cycle-detection.d.ts +2 -0
  109. package/dist/src/graph/cycle-detection.d.ts.map +1 -1
  110. package/dist/src/graph/cycle-detection.js +26 -3
  111. package/dist/src/graph/cycle-detection.js.map +1 -1
  112. package/dist/src/graph/cycles.d.ts +11 -3
  113. package/dist/src/graph/cycles.d.ts.map +1 -1
  114. package/dist/src/graph/cycles.js +121 -83
  115. package/dist/src/graph/cycles.js.map +1 -1
  116. package/dist/src/graph/dead-code.d.ts +23 -0
  117. package/dist/src/graph/dead-code.d.ts.map +1 -1
  118. package/dist/src/graph/dead-code.js +53 -13
  119. package/dist/src/graph/dead-code.js.map +1 -1
  120. package/dist/src/graph/dependency-closure.d.ts.map +1 -1
  121. package/dist/src/graph/dependency-closure.js +20 -4
  122. package/dist/src/graph/dependency-closure.js.map +1 -1
  123. package/dist/src/graph/dfs.d.ts +14 -0
  124. package/dist/src/graph/dfs.d.ts.map +1 -1
  125. package/dist/src/graph/dfs.js +61 -0
  126. package/dist/src/graph/dfs.js.map +1 -1
  127. package/dist/src/graph/duplication-stats.d.ts.map +1 -1
  128. package/dist/src/graph/duplication-stats.js +19 -3
  129. package/dist/src/graph/duplication-stats.js.map +1 -1
  130. package/dist/src/graph/edge-filter.d.ts +2 -0
  131. package/dist/src/graph/edge-filter.d.ts.map +1 -1
  132. package/dist/src/graph/edge-filter.js +26 -12
  133. package/dist/src/graph/edge-filter.js.map +1 -1
  134. package/dist/src/graph/health-score.d.ts +8 -0
  135. package/dist/src/graph/health-score.d.ts.map +1 -1
  136. package/dist/src/graph/health-score.js +48 -17
  137. package/dist/src/graph/health-score.js.map +1 -1
  138. package/dist/src/graph/hotspots.d.ts +7 -0
  139. package/dist/src/graph/hotspots.d.ts.map +1 -1
  140. package/dist/src/graph/hotspots.js +50 -5
  141. package/dist/src/graph/hotspots.js.map +1 -1
  142. package/dist/src/graph/import-chain.d.ts +17 -1
  143. package/dist/src/graph/import-chain.d.ts.map +1 -1
  144. package/dist/src/graph/import-chain.js +68 -19
  145. package/dist/src/graph/import-chain.js.map +1 -1
  146. package/dist/src/graph/loader.d.ts.map +1 -1
  147. package/dist/src/graph/loader.js +9 -10
  148. package/dist/src/graph/loader.js.map +1 -1
  149. package/dist/src/graph/maintainability.d.ts +8 -0
  150. package/dist/src/graph/maintainability.d.ts.map +1 -1
  151. package/dist/src/graph/maintainability.js +65 -10
  152. package/dist/src/graph/maintainability.js.map +1 -1
  153. package/dist/src/graph/mirrored-dirs.d.ts +4 -0
  154. package/dist/src/graph/mirrored-dirs.d.ts.map +1 -1
  155. package/dist/src/graph/mirrored-dirs.js +48 -13
  156. package/dist/src/graph/mirrored-dirs.js.map +1 -1
  157. package/dist/src/graph/narrowing.d.ts +15 -0
  158. package/dist/src/graph/narrowing.d.ts.map +1 -1
  159. package/dist/src/graph/narrowing.js +33 -0
  160. package/dist/src/graph/narrowing.js.map +1 -1
  161. package/dist/src/graph/node-search.d.ts +3 -0
  162. package/dist/src/graph/node-search.d.ts.map +1 -1
  163. package/dist/src/graph/node-search.js +37 -4
  164. package/dist/src/graph/node-search.js.map +1 -1
  165. package/dist/src/graph/normalize-id.d.ts +4 -0
  166. package/dist/src/graph/normalize-id.d.ts.map +1 -1
  167. package/dist/src/graph/normalize-id.js +25 -2
  168. package/dist/src/graph/normalize-id.js.map +1 -1
  169. package/dist/src/graph/pagerank.d.ts +7 -0
  170. package/dist/src/graph/pagerank.d.ts.map +1 -1
  171. package/dist/src/graph/pagerank.js +49 -2
  172. package/dist/src/graph/pagerank.js.map +1 -1
  173. package/dist/src/graph/prune.d.ts +31 -0
  174. package/dist/src/graph/prune.d.ts.map +1 -1
  175. package/dist/src/graph/prune.js +65 -0
  176. package/dist/src/graph/prune.js.map +1 -1
  177. package/dist/src/graph/regex-search.d.ts +10 -2
  178. package/dist/src/graph/regex-search.d.ts.map +1 -1
  179. package/dist/src/graph/regex-search.js +33 -4
  180. package/dist/src/graph/regex-search.js.map +1 -1
  181. package/dist/src/graph/regression.d.ts +5 -0
  182. package/dist/src/graph/regression.d.ts.map +1 -1
  183. package/dist/src/graph/regression.js +45 -23
  184. package/dist/src/graph/regression.js.map +1 -1
  185. package/dist/src/graph/ripple-impact.d.ts +27 -0
  186. package/dist/src/graph/ripple-impact.d.ts.map +1 -1
  187. package/dist/src/graph/ripple-impact.js +55 -7
  188. package/dist/src/graph/ripple-impact.js.map +1 -1
  189. package/dist/src/graph/risk-profile.d.ts +7 -0
  190. package/dist/src/graph/risk-profile.d.ts.map +1 -1
  191. package/dist/src/graph/risk-profile.js +82 -48
  192. package/dist/src/graph/risk-profile.js.map +1 -1
  193. package/dist/src/graph/shingle-filter.d.ts.map +1 -1
  194. package/dist/src/graph/shingle-filter.js +25 -26
  195. package/dist/src/graph/shingle-filter.js.map +1 -1
  196. package/dist/src/graph/statistics.d.ts +19 -0
  197. package/dist/src/graph/statistics.d.ts.map +1 -1
  198. package/dist/src/graph/statistics.js +53 -40
  199. package/dist/src/graph/statistics.js.map +1 -1
  200. package/dist/src/graph/stats.d.ts +2 -0
  201. package/dist/src/graph/stats.d.ts.map +1 -1
  202. package/dist/src/graph/stats.js +54 -8
  203. package/dist/src/graph/stats.js.map +1 -1
  204. package/dist/src/graph/subgraph.d.ts +5 -0
  205. package/dist/src/graph/subgraph.d.ts.map +1 -1
  206. package/dist/src/graph/subgraph.js +73 -9
  207. package/dist/src/graph/subgraph.js.map +1 -1
  208. package/dist/src/graph/topo-sort.d.ts +15 -0
  209. package/dist/src/graph/topo-sort.d.ts.map +1 -1
  210. package/dist/src/graph/topo-sort.js +83 -10
  211. package/dist/src/graph/topo-sort.js.map +1 -1
  212. package/dist/src/graph/wcc.d.ts +29 -0
  213. package/dist/src/graph/wcc.d.ts.map +1 -1
  214. package/dist/src/graph/wcc.js +122 -39
  215. package/dist/src/graph/wcc.js.map +1 -1
  216. package/dist/src/index.js +1 -1
  217. package/dist/src/index.js.map +1 -1
  218. package/dist/src/mcp-tools/api-impact.d.ts.map +1 -1
  219. package/dist/src/mcp-tools/api-impact.js +13 -6
  220. package/dist/src/mcp-tools/api-impact.js.map +1 -1
  221. package/dist/src/mcp-tools/context.d.ts.map +1 -1
  222. package/dist/src/mcp-tools/context.js +19 -29
  223. package/dist/src/mcp-tools/context.js.map +1 -1
  224. package/dist/src/mcp-tools/cypher.d.ts +11 -0
  225. package/dist/src/mcp-tools/cypher.d.ts.map +1 -1
  226. package/dist/src/mcp-tools/cypher.js +13 -1
  227. package/dist/src/mcp-tools/cypher.js.map +1 -1
  228. package/dist/src/mcp-tools/detect-changes.d.ts.map +1 -1
  229. package/dist/src/mcp-tools/detect-changes.js +35 -20
  230. package/dist/src/mcp-tools/detect-changes.js.map +1 -1
  231. package/dist/src/mcp-tools/explain.d.ts.map +1 -1
  232. package/dist/src/mcp-tools/explain.js +4 -1
  233. package/dist/src/mcp-tools/explain.js.map +1 -1
  234. package/dist/src/mcp-tools/graph-query.d.ts +2 -0
  235. package/dist/src/mcp-tools/graph-query.d.ts.map +1 -1
  236. package/dist/src/mcp-tools/graph-query.js +20 -3
  237. package/dist/src/mcp-tools/graph-query.js.map +1 -1
  238. package/dist/src/mcp-tools/group-list.d.ts +1 -0
  239. package/dist/src/mcp-tools/group-list.d.ts.map +1 -1
  240. package/dist/src/mcp-tools/group-list.js +7 -9
  241. package/dist/src/mcp-tools/group-list.js.map +1 -1
  242. package/dist/src/mcp-tools/impact.d.ts.map +1 -1
  243. package/dist/src/mcp-tools/impact.js +20 -59
  244. package/dist/src/mcp-tools/impact.js.map +1 -1
  245. package/dist/src/mcp-tools/neighbors.d.ts.map +1 -1
  246. package/dist/src/mcp-tools/neighbors.js +24 -33
  247. package/dist/src/mcp-tools/neighbors.js.map +1 -1
  248. package/dist/src/mcp-tools/query.d.ts +2 -0
  249. package/dist/src/mcp-tools/query.d.ts.map +1 -1
  250. package/dist/src/mcp-tools/query.js +1 -0
  251. package/dist/src/mcp-tools/query.js.map +1 -1
  252. package/dist/src/mcp-tools/rename.d.ts.map +1 -1
  253. package/dist/src/mcp-tools/rename.js +12 -7
  254. package/dist/src/mcp-tools/rename.js.map +1 -1
  255. package/dist/src/mcp-tools/route-map.d.ts.map +1 -1
  256. package/dist/src/mcp-tools/route-map.js +19 -23
  257. package/dist/src/mcp-tools/route-map.js.map +1 -1
  258. package/dist/src/mcp-tools/shape-check.d.ts.map +1 -1
  259. package/dist/src/mcp-tools/shape-check.js +17 -13
  260. package/dist/src/mcp-tools/shape-check.js.map +1 -1
  261. package/dist/src/parsers/loader.js +6 -5
  262. package/dist/src/parsers/loader.js.map +1 -1
  263. package/dist/src/pipeline/cluster-only.d.ts.map +1 -1
  264. package/dist/src/pipeline/cluster-only.js +8 -4
  265. package/dist/src/pipeline/cluster-only.js.map +1 -1
  266. package/dist/src/pipeline/orchestrator.d.ts.map +1 -1
  267. package/dist/src/pipeline/orchestrator.js +19 -0
  268. package/dist/src/pipeline/orchestrator.js.map +1 -1
  269. package/dist/src/pipeline/phases/boundary.d.ts +2 -0
  270. package/dist/src/pipeline/phases/boundary.d.ts.map +1 -1
  271. package/dist/src/pipeline/phases/boundary.js +31 -2
  272. package/dist/src/pipeline/phases/boundary.js.map +1 -1
  273. package/dist/src/pipeline/phases/communities.d.ts +14 -0
  274. package/dist/src/pipeline/phases/communities.d.ts.map +1 -1
  275. package/dist/src/pipeline/phases/communities.js +58 -5
  276. package/dist/src/pipeline/phases/communities.js.map +1 -1
  277. package/dist/src/pipeline/phases/contextual-proximity.d.ts.map +1 -1
  278. package/dist/src/pipeline/phases/contextual-proximity.js +21 -14
  279. package/dist/src/pipeline/phases/contextual-proximity.js.map +1 -1
  280. package/dist/src/pipeline/phases/cross-file.d.ts.map +1 -1
  281. package/dist/src/pipeline/phases/cross-file.js +35 -14
  282. package/dist/src/pipeline/phases/cross-file.js.map +1 -1
  283. package/dist/src/pipeline/phases/field-access.d.ts.map +1 -1
  284. package/dist/src/pipeline/phases/field-access.js +9 -4
  285. package/dist/src/pipeline/phases/field-access.js.map +1 -1
  286. package/dist/src/pipeline/phases/god-nodes.d.ts +4 -0
  287. package/dist/src/pipeline/phases/god-nodes.d.ts.map +1 -1
  288. package/dist/src/pipeline/phases/god-nodes.js +43 -9
  289. package/dist/src/pipeline/phases/god-nodes.js.map +1 -1
  290. package/dist/src/pipeline/phases/import-resolver.d.ts +11 -0
  291. package/dist/src/pipeline/phases/import-resolver.d.ts.map +1 -1
  292. package/dist/src/pipeline/phases/import-resolver.js +34 -7
  293. package/dist/src/pipeline/phases/import-resolver.js.map +1 -1
  294. package/dist/src/pipeline/phases/leiden.d.ts.map +1 -1
  295. package/dist/src/pipeline/phases/leiden.js +27 -19
  296. package/dist/src/pipeline/phases/leiden.js.map +1 -1
  297. package/dist/src/pipeline/phases/markdown.d.ts.map +1 -1
  298. package/dist/src/pipeline/phases/markdown.js +10 -5
  299. package/dist/src/pipeline/phases/markdown.js.map +1 -1
  300. package/dist/src/pipeline/phases/pdf-parse.d.ts.map +1 -1
  301. package/dist/src/pipeline/phases/pdf-parse.js +18 -3
  302. package/dist/src/pipeline/phases/pdf-parse.js.map +1 -1
  303. package/dist/src/pipeline/phases/processes.d.ts.map +1 -1
  304. package/dist/src/pipeline/phases/processes.js +26 -9
  305. package/dist/src/pipeline/phases/processes.js.map +1 -1
  306. package/dist/src/pipeline/phases/re-export-propagation.d.ts.map +1 -1
  307. package/dist/src/pipeline/phases/re-export-propagation.js +33 -14
  308. package/dist/src/pipeline/phases/re-export-propagation.js.map +1 -1
  309. package/dist/src/pipeline/phases/reachability.d.ts.map +1 -1
  310. package/dist/src/pipeline/phases/reachability.js +42 -26
  311. package/dist/src/pipeline/phases/reachability.js.map +1 -1
  312. package/dist/src/pipeline/phases/routes.d.ts.map +1 -1
  313. package/dist/src/pipeline/phases/routes.js +30 -21
  314. package/dist/src/pipeline/phases/routes.js.map +1 -1
  315. package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
  316. package/dist/src/pipeline/phases/scope-resolution.js +93 -63
  317. package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
  318. package/dist/src/pipeline/runner.d.ts +4 -0
  319. package/dist/src/pipeline/runner.d.ts.map +1 -1
  320. package/dist/src/pipeline/runner.js +30 -11
  321. package/dist/src/pipeline/runner.js.map +1 -1
  322. package/dist/src/query/cypher-parser.d.ts +20 -0
  323. package/dist/src/query/cypher-parser.d.ts.map +1 -1
  324. package/dist/src/query/cypher-parser.js +73 -0
  325. package/dist/src/query/cypher-parser.js.map +1 -1
  326. package/dist/src/search/embed-batch.d.ts.map +1 -1
  327. package/dist/src/search/embed-batch.js +4 -1
  328. package/dist/src/search/embed-batch.js.map +1 -1
  329. package/dist/src/search/hybrid-query.d.ts +4 -0
  330. package/dist/src/search/hybrid-query.d.ts.map +1 -1
  331. package/dist/src/search/hybrid-query.js +5 -1
  332. package/dist/src/search/hybrid-query.js.map +1 -1
  333. package/dist/src/staleness/git-staleness.js +3 -3
  334. package/dist/src/staleness/git-staleness.js.map +1 -1
  335. package/dist/src/storage/db.d.ts.map +1 -1
  336. package/dist/src/storage/db.js +12 -0
  337. package/dist/src/storage/db.js.map +1 -1
  338. package/dist/src/storage/edge-store.d.ts +2 -0
  339. package/dist/src/storage/edge-store.d.ts.map +1 -1
  340. package/dist/src/storage/edge-store.js +26 -1
  341. package/dist/src/storage/edge-store.js.map +1 -1
  342. package/dist/src/storage/embedding-store.d.ts +16 -0
  343. package/dist/src/storage/embedding-store.d.ts.map +1 -1
  344. package/dist/src/storage/embedding-store.js +26 -2
  345. package/dist/src/storage/embedding-store.js.map +1 -1
  346. package/dist/src/storage/file-cache.d.ts +5 -0
  347. package/dist/src/storage/file-cache.d.ts.map +1 -1
  348. package/dist/src/storage/file-cache.js +35 -9
  349. package/dist/src/storage/file-cache.js.map +1 -1
  350. package/dist/src/storage/fts-store.d.ts +4 -0
  351. package/dist/src/storage/fts-store.d.ts.map +1 -1
  352. package/dist/src/storage/fts-store.js +8 -3
  353. package/dist/src/storage/fts-store.js.map +1 -1
  354. package/dist/src/storage/node-store.d.ts +2 -0
  355. package/dist/src/storage/node-store.d.ts.map +1 -1
  356. package/dist/src/storage/node-store.js +28 -1
  357. package/dist/src/storage/node-store.js.map +1 -1
  358. package/dist/tsconfig.tsbuildinfo +1 -1
  359. package/package.json +1 -1
  360. package/src/__tests__/pipeline/runner.parallel.test.ts +2 -2
  361. package/src/analysis/churn-cache.ts +18 -7
  362. package/src/analysis/churn-trend.ts +35 -6
  363. package/src/analysis/cluster-quality.ts +20 -11
  364. package/src/analysis/coverage-gaps.ts +44 -1
  365. package/src/analysis/dep-classification.ts +34 -0
  366. package/src/analysis/dependency-health.ts +25 -0
  367. package/src/analysis/duplicate-exports.ts +40 -4
  368. package/src/analysis/feature-flags.ts +37 -17
  369. package/src/analysis/node-similarity.ts +45 -4
  370. package/src/analysis/private-type-leaks.ts +49 -2
  371. package/src/analysis/project-state.ts +19 -9
  372. package/src/analysis/suppressions.ts +4 -1
  373. package/src/analysis/trace.ts +73 -54
  374. package/src/analysis/unused-class-members.ts +8 -1
  375. package/src/analysis/worker-pool.ts +40 -8
  376. package/src/cli/augment.ts +10 -3
  377. package/src/export/sarif.ts +5 -5
  378. package/src/graph/analyzer.ts +26 -2
  379. package/src/graph/author-analytics.ts +45 -5
  380. package/src/graph/baseline.ts +78 -24
  381. package/src/graph/clone-detector.ts +22 -36
  382. package/src/graph/clone-families.ts +34 -10
  383. package/src/graph/codeowners-extended.ts +54 -0
  384. package/src/graph/codeowners-gitlab.ts +46 -0
  385. package/src/graph/codeowners.ts +33 -2
  386. package/src/graph/complexity.ts +102 -47
  387. package/src/graph/cross-reference-findings.ts +23 -2
  388. package/src/graph/cross-reference.ts +26 -0
  389. package/src/graph/cycle-detection.ts +22 -3
  390. package/src/graph/cycles.ts +155 -96
  391. package/src/graph/dead-code.ts +76 -17
  392. package/src/graph/dependency-closure.ts +24 -7
  393. package/src/graph/dfs.ts +78 -0
  394. package/src/graph/duplication-stats.ts +17 -6
  395. package/src/graph/edge-filter.ts +26 -12
  396. package/src/graph/health-score.ts +47 -17
  397. package/src/graph/hotspots.ts +57 -8
  398. package/src/graph/import-chain.ts +89 -19
  399. package/src/graph/loader.ts +10 -7
  400. package/src/graph/maintainability.ts +72 -22
  401. package/src/graph/mirrored-dirs.ts +45 -14
  402. package/src/graph/narrowing.ts +44 -0
  403. package/src/graph/node-search.ts +49 -4
  404. package/src/graph/normalize-id.ts +26 -2
  405. package/src/graph/pagerank.ts +69 -2
  406. package/src/graph/prune.ts +76 -0
  407. package/src/graph/regex-search.ts +38 -4
  408. package/src/graph/regression.ts +48 -23
  409. package/src/graph/ripple-impact.ts +71 -11
  410. package/src/graph/risk-profile.ts +98 -53
  411. package/src/graph/shingle-filter.ts +33 -27
  412. package/src/graph/statistics.ts +73 -45
  413. package/src/graph/stats.ts +50 -16
  414. package/src/graph/subgraph.ts +96 -25
  415. package/src/graph/topo-sort.ts +101 -16
  416. package/src/graph/wcc.ts +146 -37
  417. package/src/index.ts +1 -1
  418. package/src/mcp-tools/api-impact.ts +13 -6
  419. package/src/mcp-tools/context.ts +31 -40
  420. package/src/mcp-tools/cypher.ts +14 -1
  421. package/src/mcp-tools/detect-changes.ts +38 -23
  422. package/src/mcp-tools/explain.ts +4 -1
  423. package/src/mcp-tools/graph-query.ts +24 -3
  424. package/src/mcp-tools/group-list.ts +9 -11
  425. package/src/mcp-tools/impact.ts +26 -66
  426. package/src/mcp-tools/neighbors.ts +34 -39
  427. package/src/mcp-tools/query.ts +3 -0
  428. package/src/mcp-tools/rename.ts +9 -8
  429. package/src/mcp-tools/route-map.ts +21 -23
  430. package/src/mcp-tools/shape-check.ts +18 -14
  431. package/src/parsers/loader.ts +5 -5
  432. package/src/pipeline/cluster-only.ts +8 -4
  433. package/src/pipeline/orchestrator.ts +22 -0
  434. package/src/pipeline/phases/boundary.ts +40 -2
  435. package/src/pipeline/phases/communities.ts +66 -5
  436. package/src/pipeline/phases/contextual-proximity.ts +15 -14
  437. package/src/pipeline/phases/cross-file.ts +35 -14
  438. package/src/pipeline/phases/field-access.ts +11 -4
  439. package/src/pipeline/phases/god-nodes.ts +47 -10
  440. package/src/pipeline/phases/import-resolver.ts +37 -5
  441. package/src/pipeline/phases/leiden.ts +25 -18
  442. package/src/pipeline/phases/markdown.ts +12 -7
  443. package/src/pipeline/phases/pdf-parse.ts +19 -2
  444. package/src/pipeline/phases/processes.ts +27 -15
  445. package/src/pipeline/phases/re-export-propagation.ts +40 -18
  446. package/src/pipeline/phases/reachability.ts +39 -24
  447. package/src/pipeline/phases/routes.ts +32 -21
  448. package/src/pipeline/phases/scope-resolution.ts +110 -71
  449. package/src/pipeline/runner.ts +28 -11
  450. package/src/query/cypher-parser.ts +83 -0
  451. package/src/search/embed-batch.ts +4 -1
  452. package/src/search/hybrid-query.ts +11 -1
  453. package/src/staleness/git-staleness.ts +3 -3
  454. package/src/storage/db.ts +11 -0
  455. package/src/storage/edge-store.ts +34 -1
  456. package/src/storage/embedding-store.ts +38 -6
  457. package/src/storage/file-cache.ts +35 -21
  458. package/src/storage/fts-store.ts +12 -3
  459. package/src/storage/node-store.ts +39 -1
  460. package/src/coverage/runtime.ts +0 -249
@@ -11,35 +11,94 @@ import type { MonographDb } from '../storage/db.js';
11
11
  * @returns Array of node ids that are considered dead code.
12
12
  */
13
13
  export function detectDeadCode(db: MonographDb): string[] {
14
- const nodeRows = db.prepare('SELECT id, is_exported FROM nodes').all() as {
14
+ return detectDeadCodeNodes(db).map(n => n.id);
15
+ }
16
+
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface DeadCodeNode {
20
+ id: string;
21
+ name: string;
22
+ filePath: string | null;
23
+ startLine: number | null;
24
+ label: string;
25
+ }
26
+
27
+ /**
28
+ * Like detectDeadCode() but returns rich node objects with filePath and
29
+ * startLine so callers can render file:line navigation hints.
30
+ *
31
+ * @param db - The MonographDb instance
32
+ * @returns Array of dead-code nodes with location metadata.
33
+ */
34
+ export function detectDeadCodeNodes(db: MonographDb): DeadCodeNode[] {
35
+ const nodeRows = db
36
+ .prepare('SELECT id, name, file_path, start_line, label, is_exported FROM nodes')
37
+ .all() as {
15
38
  id: string;
39
+ name: string;
40
+ file_path: string | null;
41
+ start_line: number | null;
42
+ label: string;
16
43
  is_exported: number;
17
44
  }[];
18
45
 
19
46
  if (nodeRows.length === 0) return [];
20
47
 
21
- // Count in-degrees
48
+ // Count in-degrees via a single SQL aggregation — avoids loading all edges into JS
49
+ const inDegreeRows = db
50
+ .prepare(
51
+ 'SELECT target_id, COUNT(*) AS cnt FROM edges WHERE source_id != target_id GROUP BY target_id',
52
+ )
53
+ .all() as { target_id: string; cnt: number }[];
54
+
22
55
  const inDegree = new Map<string, number>();
23
- for (const { id } of nodeRows) {
24
- inDegree.set(id, 0);
56
+ for (const { target_id, cnt } of inDegreeRows) {
57
+ inDegree.set(target_id, cnt);
25
58
  }
26
59
 
27
- const edgeRows = db.prepare('SELECT target_id FROM edges WHERE source_id != target_id').all() as {
28
- target_id: string;
29
- }[];
30
-
31
- for (const { target_id } of edgeRows) {
32
- if (inDegree.has(target_id)) {
33
- inDegree.set(target_id, (inDegree.get(target_id) ?? 0) + 1);
60
+ const dead: DeadCodeNode[] = [];
61
+ for (const row of nodeRows) {
62
+ if ((inDegree.get(row.id) ?? 0) === 0 && row.is_exported !== 1) {
63
+ dead.push({
64
+ id: row.id,
65
+ name: row.name,
66
+ filePath: row.file_path,
67
+ startLine: row.start_line,
68
+ label: row.label,
69
+ });
34
70
  }
35
71
  }
36
72
 
37
- const dead: string[] = [];
38
- for (const { id, is_exported } of nodeRows) {
39
- if ((inDegree.get(id) ?? 0) === 0 && is_exported !== 1) {
40
- dead.push(id);
41
- }
73
+ return dead;
74
+ }
75
+
76
+ /**
77
+ * Format dead-code nodes as structured text for LLM consumption.
78
+ * Each entry includes a file:line navigation hint where available.
79
+ *
80
+ * @param nodes - Result from detectDeadCodeNodes()
81
+ * @returns Multi-line string suitable for injection into LLM context
82
+ */
83
+ export function formatDeadCode(nodes: DeadCodeNode[]): string {
84
+ if (nodes.length === 0) {
85
+ return 'Dead code: none detected.';
42
86
  }
43
87
 
44
- return dead;
88
+ const lines: string[] = [
89
+ `Dead code: ${nodes.length} unreachable node${nodes.length === 1 ? '' : 's'} detected`,
90
+ '',
91
+ ];
92
+
93
+ for (const node of nodes) {
94
+ const loc =
95
+ node.filePath != null
96
+ ? node.startLine != null
97
+ ? `${node.filePath}:${node.startLine}`
98
+ : node.filePath
99
+ : '(unknown location)';
100
+ lines.push(` [${node.label}] ${node.name} — ${loc}`);
101
+ }
102
+
103
+ return lines.join('\n');
45
104
  }
@@ -17,6 +17,8 @@ export interface DepClosureReport {
17
17
  deepDependencyFiles: DepClosureResult[]; // files with depDepth > 5
18
18
  }
19
19
 
20
+ const SQLITE_VAR_LIMIT = 200; // safe batch size for IN clauses
21
+
20
22
  export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepClosureReport {
21
23
  // Get File nodes sorted by degree (most connected first)
22
24
  const fileNodes = (db.prepare(`
@@ -38,6 +40,12 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
38
40
  `).all() as Array<{ file_path: string }>).map(r => r.file_path)
39
41
  );
40
42
 
43
+ // Hoist prepared statement outside the per-node BFS loop to avoid recompiling
44
+ // on every iteration (N×maxNodes calls previously).
45
+ const edgeStmt = db.prepare(
46
+ `SELECT target_id FROM edges WHERE source_id = ? AND relation = 'IMPORTS'`,
47
+ );
48
+
41
49
  const results: DepClosureResult[] = [];
42
50
 
43
51
  for (const fileNode of fileNodes) {
@@ -53,10 +61,7 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
53
61
  depth++;
54
62
  const next: string[] = [];
55
63
  for (const id of frontier) {
56
- const edges = db.prepare(
57
- `SELECT target_id FROM edges WHERE source_id = ? AND relation = 'IMPORTS'`
58
- ).all(id) as Array<{ target_id: string }>;
59
-
64
+ const edges = edgeStmt.all(id) as Array<{ target_id: string }>;
60
65
  for (const e of edges) {
61
66
  if (!visited.has(e.target_id)) {
62
67
  visited.add(e.target_id);
@@ -73,11 +78,23 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
73
78
  frontier = next;
74
79
  }
75
80
 
81
+ // Batch-resolve file_paths for all transitive deps in one chunked SQL query
82
+ // instead of one SELECT per dep (N+1 → O(deps/CHUNK) queries).
83
+ const depFilePathMap = new Map<string, string | null>();
84
+ for (let i = 0; i < transitiveDeps.length; i += SQLITE_VAR_LIMIT) {
85
+ const chunk = transitiveDeps.slice(i, i + SQLITE_VAR_LIMIT);
86
+ const ph = chunk.map(() => '?').join(',');
87
+ const rows = db
88
+ .prepare(`SELECT id, file_path FROM nodes WHERE id IN (${ph})`)
89
+ .all(...chunk) as Array<{ id: string; file_path: string | null }>;
90
+ for (const row of rows) depFilePathMap.set(row.id, row.file_path);
91
+ }
92
+
76
93
  // unusedTransitiveDeps: transitive deps whose file_path is NOT in importedFilePaths
77
94
  const unusedTransitiveDeps = transitiveDeps.filter(depId => {
78
- const depNode = db.prepare('SELECT file_path FROM nodes WHERE id = ?').get(depId) as { file_path: string | null } | undefined;
79
- if (!depNode?.file_path) return false;
80
- return !importedFilePaths.has(depNode.file_path);
95
+ const fp = depFilePathMap.get(depId);
96
+ if (!fp) return false;
97
+ return !importedFilePaths.has(fp);
81
98
  });
82
99
 
83
100
  results.push({
package/src/graph/dfs.ts CHANGED
@@ -7,6 +7,12 @@ export interface DfsOptions {
7
7
  maxDepth?: number;
8
8
  }
9
9
 
10
+ export interface DfsResult {
11
+ visited: DfsNode[];
12
+ maxDepth: number;
13
+ nodeCount: number;
14
+ }
15
+
10
16
  export function dfsTraversal(
11
17
  startId: string,
12
18
  adjacency: Map<string, string[]>,
@@ -38,3 +44,75 @@ export function dfsTraversal(
38
44
  }
39
45
  }
40
46
  }
47
+
48
+ /** Collect all reachable nodes from startId via DFS, returning structured result. */
49
+ export function dfsCollect(
50
+ startId: string,
51
+ adjacency: Map<string, string[]>,
52
+ options: DfsOptions = {},
53
+ ): DfsResult {
54
+ const visited: DfsNode[] = [];
55
+ let maxReachedDepth = 0;
56
+ dfsTraversal(startId, adjacency, (node) => {
57
+ visited.push(node);
58
+ if (node.depth > maxReachedDepth) maxReachedDepth = node.depth;
59
+ }, options);
60
+ return { visited, maxDepth: maxReachedDepth, nodeCount: visited.length };
61
+ }
62
+
63
+ /** BFS level-order traversal — useful when shortest-path distance matters more than DFS order. */
64
+ export function bfsTraversal(
65
+ startId: string,
66
+ adjacency: Map<string, string[]>,
67
+ visitor: (node: DfsNode) => void,
68
+ options: DfsOptions = {},
69
+ ): void {
70
+ if (!adjacency.has(startId)) return;
71
+
72
+ const { maxDepth = Infinity } = options;
73
+ const visited = new Set<string>([startId]);
74
+ const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
75
+
76
+ while (queue.length > 0) {
77
+ const { id, depth } = queue.shift()!;
78
+ visitor({ id, depth });
79
+
80
+ if (depth < maxDepth) {
81
+ const neighbors = adjacency.get(id) ?? [];
82
+ for (let i = 0; i < neighbors.length; i++) {
83
+ const neighbor = neighbors[i]!;
84
+ if (!visited.has(neighbor)) {
85
+ visited.add(neighbor);
86
+ queue.push({ id: neighbor, depth: depth + 1 });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Format a DfsResult as structured text for LLM consumption.
95
+ * Groups nodes by depth level for easy reading.
96
+ */
97
+ export function formatDfsResult(result: DfsResult, startId: string): string {
98
+ if (result.nodeCount === 0) return `No nodes reachable from "${startId}".`;
99
+
100
+ const byDepth = new Map<number, string[]>();
101
+ for (const node of result.visited) {
102
+ let bucket = byDepth.get(node.depth);
103
+ if (!bucket) { bucket = []; byDepth.set(node.depth, bucket); }
104
+ bucket.push(node.id);
105
+ }
106
+
107
+ const lines: string[] = [
108
+ `DFS from "${startId}": ${result.nodeCount} nodes, max depth ${result.maxDepth}`,
109
+ '',
110
+ ];
111
+ // Iterate in depth order
112
+ const depths = Array.from(byDepth.keys()).sort((a, b) => a - b);
113
+ for (const depth of depths) {
114
+ const ids = byDepth.get(depth)!;
115
+ lines.push(` depth ${depth}: ${ids.join(', ')}`);
116
+ }
117
+ return lines.join('\n');
118
+ }
@@ -39,11 +39,15 @@ export function computeDuplicationStats(
39
39
  }
40
40
 
41
41
  const filesWithClones = fileLines.size;
42
- const duplicatedLines = [...fileLines.values()].reduce((s, set) => s + set.size, 0);
43
- const duplicatedTokens = Math.min(
44
- totalTokens,
45
- groups.reduce((s, g) => s + g.instances.reduce((t, i) => t + (i.tokenCount ?? 0), 0), 0),
46
- );
42
+ // Single-pass line count avoids spread allocation over all file Sets
43
+ let duplicatedLines = 0;
44
+ for (const set of fileLines.values()) duplicatedLines += set.size;
45
+ // Single-pass token sum avoids nested reduce and intermediate arrays
46
+ let rawTokens = 0;
47
+ for (const group of groups) {
48
+ for (const inst of group.instances) rawTokens += inst.tokenCount ?? 0;
49
+ }
50
+ const duplicatedTokens = Math.min(totalTokens, rawTokens);
47
51
 
48
52
  return {
49
53
  totalFiles: allFilePaths.length,
@@ -59,10 +63,17 @@ export function computeDuplicationStats(
59
63
  }
60
64
 
61
65
  export function formatDuplicationStats(stats: DuplicationStats): string {
66
+ const pct = stats.duplicationPct;
67
+ const grade = pct < 5 ? 'A' : pct < 10 ? 'B' : pct < 20 ? 'C' : pct < 30 ? 'D' : 'F';
68
+ const tokenPct = stats.totalTokens > 0
69
+ ? ((stats.duplicatedTokens / stats.totalTokens) * 100).toFixed(1)
70
+ : '0.0';
62
71
  return [
72
+ `Duplication grade: ${grade} (${pct.toFixed(1)}% duplicated lines)`,
63
73
  `Clone groups: ${stats.cloneGroups}`,
64
74
  `Clone instances: ${stats.cloneInstances}`,
65
75
  `Files with clones: ${stats.filesWithClones} / ${stats.totalFiles}`,
66
- `Duplicated lines: ${stats.duplicatedLines} / ${stats.totalLines} (${stats.duplicationPct.toFixed(1)}%)`,
76
+ `Duplicated lines: ${stats.duplicatedLines} / ${stats.totalLines} (${pct.toFixed(1)}%)`,
77
+ `Duplicated tokens: ${stats.duplicatedTokens} / ${stats.totalTokens} (${tokenPct}%)`,
67
78
  ].join('\n');
68
79
  }
@@ -85,19 +85,33 @@ export function filterEdgesInMemory(
85
85
  edges: MonographEdge[],
86
86
  options: EdgeFilterOptions = {},
87
87
  ): MonographEdge[] {
88
+ // Convert arrays to Sets once before the loop for O(1) membership checks
89
+ const relationSet = options.relations && options.relations.length > 0
90
+ ? new Set(options.relations) : null;
91
+ const confidenceSet = options.confidences && options.confidences.length > 0
92
+ ? new Set(options.confidences) : null;
93
+ const minScore = options.minConfidenceScore;
94
+ const maxScore = options.maxConfidenceScore;
95
+
88
96
  return edges.filter(e => {
89
- if (options.relations && options.relations.length > 0) {
90
- if (!options.relations.includes(e.relation)) return false;
91
- }
92
- if (options.confidences && options.confidences.length > 0) {
93
- if (!options.confidences.includes(e.confidence)) return false;
94
- }
95
- if (options.minConfidenceScore !== undefined && e.confidenceScore < options.minConfidenceScore) {
96
- return false;
97
- }
98
- if (options.maxConfidenceScore !== undefined && e.confidenceScore > options.maxConfidenceScore) {
99
- return false;
100
- }
97
+ if (relationSet && !relationSet.has(e.relation)) return false;
98
+ if (confidenceSet && !confidenceSet.has(e.confidence)) return false;
99
+ if (minScore !== undefined && e.confidenceScore < minScore) return false;
100
+ if (maxScore !== undefined && e.confidenceScore > maxScore) return false;
101
101
  return true;
102
102
  });
103
103
  }
104
+
105
+ // ── Formatting ─────────────────────────────────────────────────────────────────
106
+
107
+ /** Format a list of filtered edges as structured text for LLM consumption. */
108
+ export function formatFilteredEdges(edges: MonographEdge[]): string {
109
+ if (edges.length === 0) return 'No edges match the filter.';
110
+ const lines: string[] = [`Filtered edges (${edges.length}):`];
111
+ for (const e of edges) {
112
+ const score = e.confidenceScore !== undefined ? ` [${e.confidenceScore.toFixed(2)}]` : '';
113
+ const reason = e.reason ? ` — ${e.reason}` : '';
114
+ lines.push(` ${e.sourceId} -[${e.relation}/${e.confidence}${score}]-> ${e.targetId}${reason}`);
115
+ }
116
+ return lines.join('\n');
117
+ }
@@ -75,23 +75,13 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
75
75
  const circularEdges = circularRow.c * 2;
76
76
 
77
77
  // ── Hotspot files (churnScore > 0.5 in properties) ───────────────────────
78
- // Files where properties contain a churnScore > 0.5
79
- // We detect presence of churnScore via LIKE and then evaluate in JS
80
- const hotspotCandidates = db.prepare(
81
- "SELECT properties FROM nodes WHERE label = 'File' AND properties LIKE '%churnScore%'"
82
- ).all() as { properties: string | null }[];
83
-
84
- let hotspotCount = 0;
85
- for (const row of hotspotCandidates) {
86
- if (!row.properties) continue;
87
- try {
88
- const props = JSON.parse(row.properties) as Record<string, unknown>;
89
- const churn = props['churnScore'];
90
- if (typeof churn === 'number' && churn > 0.5) hotspotCount++;
91
- } catch {
92
- // ignore malformed JSON
93
- }
94
- }
78
+ // Push the numeric filter into SQLite via json_extract() — avoids O(N) JS JSON.parse loop
79
+ const hotspotRow = db.prepare(`
80
+ SELECT COUNT(*) as c FROM nodes
81
+ WHERE label = 'File'
82
+ AND CAST(json_extract(properties, '$.churnScore') AS REAL) > 0.5
83
+ `).get() as { c: number };
84
+ const hotspotCount = hotspotRow.c;
95
85
 
96
86
  // ── Isolated nodes (degree = 0) ───────────────────────────────────────────
97
87
  const isolatedRow = db.prepare(`
@@ -150,3 +140,43 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
150
140
 
151
141
  return { score, grade, penalties, summary };
152
142
  }
143
+
144
+ /**
145
+ * Format a HealthScoreResult as structured text for LLM navigation.
146
+ * Provides a concise grade breakdown that an LLM can parse and act on.
147
+ *
148
+ * @param result - HealthScoreResult from computeHealthScore()
149
+ * @returns structured text suitable for LLM consumption
150
+ */
151
+ export function formatHealthScore(result: HealthScoreResult): string {
152
+ const { score, grade, penalties } = result;
153
+
154
+ const fmt = (val: number): string => val.toFixed(1);
155
+
156
+ const lines: string[] = [
157
+ `health_score: ${score}/100 grade: ${grade}`,
158
+ '',
159
+ 'penalties:',
160
+ ` unreachable_files: ${fmt(penalties.unreachableFilePct)}% (max -25)`,
161
+ ` god_nodes: ${fmt(penalties.godNodePct)}% (max -20)`,
162
+ ` circular_edges: ${fmt(penalties.circularEdgePct)}% (max -20)`,
163
+ ` hotspot_files: ${fmt(penalties.hotspotPct)}% (max -15)`,
164
+ ` isolated_nodes: ${fmt(penalties.isolatedNodePct)}% (max -10)`,
165
+ ` cross_community_edges: ${fmt(penalties.crossCommunityEdgePct)}% (max -10)`,
166
+ '',
167
+ ];
168
+
169
+ // Actionable guidance for grades below B
170
+ if (grade === 'F' || grade === 'D') {
171
+ lines.push('action_required: yes');
172
+ if (penalties.circularEdgePct > 5) lines.push(' - break circular import cycles (monograph_context for file dependencies)');
173
+ if (penalties.godNodePct > 5) lines.push(' - split god nodes (monograph_god_nodes for candidates)');
174
+ if (penalties.unreachableFilePct > 10) lines.push(' - investigate unreachable files (monograph_detect_changes to surface dead paths)');
175
+ } else if (grade === 'C') {
176
+ lines.push('action_suggested: yes');
177
+ if (penalties.hotspotPct > 10) lines.push(' - reduce churn on hotspot files');
178
+ if (penalties.isolatedNodePct > 10) lines.push(' - connect or remove isolated nodes');
179
+ }
180
+
181
+ return lines.join('\n');
182
+ }
@@ -1,6 +1,11 @@
1
1
  import { execSync } from 'child_process';
2
2
  import type Database from 'better-sqlite3';
3
3
 
4
+ // ── Process-scoped TTL cache for expensive git log calls ─────────────────────
5
+ // Key: `${projectDir}:${windowDays}` → { output, ts }
6
+ const GIT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
7
+ const gitLogCache = new Map<string, { output: string; ts: number }>();
8
+
4
9
  export interface HotspotResult {
5
10
  nodeId: string;
6
11
  nodeName: string;
@@ -47,16 +52,23 @@ export function computeHotspots(
47
52
  const now = Date.now();
48
53
  const HALF_LIFE_DAYS = 90;
49
54
 
50
- // ── Step 1: Get git log with dates ────────────────────────────────────────
55
+ // ── Step 1: Get git log with dates (TTL-cached per projectDir+window) ────
51
56
  // Format: ISO-date\tfile-path (one per changed file per commit)
52
57
  let gitOutput = '';
53
- try {
54
- gitOutput = execSync(
55
- `git log --since="${windowDays} days ago" --name-only --pretty=format:"%ci" -- .`,
56
- { cwd: projectDir, maxBuffer: 10 * 1024 * 1024 },
57
- ).toString();
58
- } catch {
59
- return []; // not a git repo or git not available
58
+ const cacheKey = `${projectDir}:${windowDays}`;
59
+ const cached = gitLogCache.get(cacheKey);
60
+ if (cached && now - cached.ts < GIT_CACHE_TTL_MS) {
61
+ gitOutput = cached.output;
62
+ } else {
63
+ try {
64
+ gitOutput = execSync(
65
+ `git log --since="${windowDays} days ago" --name-only --pretty=format:"%ci" -- .`,
66
+ { cwd: projectDir, maxBuffer: 10 * 1024 * 1024 },
67
+ ).toString();
68
+ gitLogCache.set(cacheKey, { output: gitOutput, ts: now });
69
+ } catch {
70
+ return []; // not a git repo or git not available
71
+ }
60
72
  }
61
73
 
62
74
  // ── Step 2: Parse git log and compute churn scores ────────────────────────
@@ -164,3 +176,40 @@ export function computeHotspots(
164
176
  .sort((a, b) => b.hotspotScore - a.hotspotScore)
165
177
  .slice(0, limit);
166
178
  }
179
+
180
+ /**
181
+ * Format hotspot results as structured text with file:line hints for LLM navigation.
182
+ *
183
+ * @param hotspots - Results from computeHotspots()
184
+ * @returns structured text suitable for LLM consumption
185
+ */
186
+ export function formatHotspots(hotspots: HotspotResult[]): string {
187
+ if (hotspots.length === 0) {
188
+ return 'hotspots: none found (insufficient git history or no matching file nodes)\n';
189
+ }
190
+
191
+ const trendMark = (t: HotspotResult['trend']): string => {
192
+ if (t === 'accelerating') return '+';
193
+ if (t === 'cooling') return '-';
194
+ return '~';
195
+ };
196
+
197
+ const lines: string[] = [
198
+ `hotspots: ${hotspots.length} high-risk files (churn x centrality)`,
199
+ '',
200
+ ];
201
+
202
+ hotspots.forEach((h, i) => {
203
+ lines.push(`[${i + 1}] ${h.nodeName} trend:${trendMark(h.trend)}`);
204
+ lines.push(` file: ${h.filePath}:1`);
205
+ lines.push(` score: ${h.hotspotScore.toFixed(2)} churn: ${h.churnScore.toFixed(2)} centrality: ${h.centralityScore.toFixed(2)}`);
206
+ lines.push(` commits: ${h.rawCommitCount} last: ${h.lastCommitDate ?? 'unknown'}`);
207
+ lines.push('');
208
+ });
209
+
210
+ const accelerating = hotspots.filter(h => h.trend === 'accelerating').length;
211
+ const cooling = hotspots.filter(h => h.trend === 'cooling').length;
212
+ lines.push(`summary: ${accelerating} accelerating, ${cooling} cooling, ${hotspots.length - accelerating - cooling} stable`);
213
+
214
+ return lines.join('\n');
215
+ }
@@ -10,7 +10,9 @@ export interface ImportChainOptions {
10
10
  /**
11
11
  * Trace all import chains (paths) from `sourceId` to `targetId`.
12
12
  *
13
- * Uses BFS/DFS with cycle detection. Returns all simple paths up to `maxDepth`.
13
+ * Uses frontier-based BFS with lazy edge loading: only fetches outgoing edges
14
+ * for nodes as they are visited, avoiding loading the full edge table when
15
+ * the graph is large. Returns all simple paths up to `maxDepth`.
14
16
  *
15
17
  * @param db - The MonographDb instance
16
18
  * @param sourceId - Starting node id
@@ -26,28 +28,27 @@ export function traceImportChain(
26
28
  ): string[][] {
27
29
  const { maxDepth = 10, maxPaths = 100 } = options;
28
30
 
29
- const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
30
- if (nodeRows.length === 0) return [];
31
-
32
- const nodeSet = new Set(nodeRows.map(r => r.id));
33
- if (!nodeSet.has(sourceId) || !nodeSet.has(targetId)) return [];
31
+ // Quick existence check avoids loading any edge data for invalid IDs
32
+ const exists = db.prepare('SELECT 1 FROM nodes WHERE id = ? LIMIT 1');
33
+ if (!exists.get(sourceId) || !exists.get(targetId)) return [];
34
34
  if (sourceId === targetId) return [[sourceId]];
35
35
 
36
- const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
37
- source_id: string;
38
- target_id: string;
39
- }[];
36
+ // Lazy adjacency cache: only loaded for nodes we actually visit
37
+ const adjCache = new Map<string, string[]>();
38
+ const edgeStmt = db.prepare(
39
+ 'SELECT target_id FROM edges WHERE source_id = ? AND source_id != target_id',
40
+ );
40
41
 
41
- // Build adjacency
42
- const adj = new Map<string, string[]>();
43
- for (const n of nodeSet) adj.set(n, []);
44
- for (const { source_id: src, target_id: tgt } of edgeRows) {
45
- if (src === tgt) continue;
46
- if (!adj.has(src) || !adj.has(tgt)) continue;
47
- adj.get(src)!.push(tgt);
42
+ function getNeighbors(nodeId: string): string[] {
43
+ let neighbors = adjCache.get(nodeId);
44
+ if (neighbors === undefined) {
45
+ neighbors = (edgeStmt.all(nodeId) as { target_id: string }[]).map(r => r.target_id);
46
+ adjCache.set(nodeId, neighbors);
47
+ }
48
+ return neighbors;
48
49
  }
49
50
 
50
- // DFS with path tracking
51
+ // DFS with path tracking (iterative to avoid stack overflow on deep graphs)
51
52
  const results: string[][] = [];
52
53
  const stack: Array<{ node: string; path: string[]; visited: Set<string> }> = [
53
54
  { node: sourceId, path: [sourceId], visited: new Set([sourceId]) },
@@ -59,7 +60,7 @@ export function traceImportChain(
59
60
  // the target would require one more edge which would exceed maxDepth.
60
61
  if (path.length - 1 >= maxDepth) continue;
61
62
 
62
- for (const neighbor of adj.get(node) ?? []) {
63
+ for (const neighbor of getNeighbors(node)) {
63
64
  if (visited.has(neighbor)) continue;
64
65
  if (neighbor === targetId) {
65
66
  results.push([...path, neighbor]);
@@ -76,3 +77,72 @@ export function traceImportChain(
76
77
 
77
78
  return results;
78
79
  }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Structured text formatter for LLM consumption
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Format import-chain paths as structured text.
87
+ *
88
+ * Resolves node IDs to human-readable names (name + file_path) for LLM
89
+ * context injection. Each path is printed as an arrow chain with file:line
90
+ * hints where available.
91
+ *
92
+ * @param db - The MonographDb instance (used for name resolution)
93
+ * @param paths - Result of traceImportChain()
94
+ * @param sourceId - Source node id (for summary line)
95
+ * @param targetId - Target node id (for summary line)
96
+ * @returns Structured text string
97
+ */
98
+ export function formatImportChain(
99
+ db: MonographDb,
100
+ paths: string[][],
101
+ sourceId: string,
102
+ targetId: string,
103
+ ): string {
104
+ if (paths.length === 0) {
105
+ return `Import chain: no path found from ${sourceId} to ${targetId}.`;
106
+ }
107
+
108
+ // Batch-resolve all node IDs in the result set
109
+ const allIds = [...new Set(paths.flat())];
110
+ const placeholders = allIds.map(() => '?').join(',');
111
+ const nodeRows = db
112
+ .prepare(
113
+ `SELECT id, name, file_path, start_line FROM nodes WHERE id IN (${placeholders})`,
114
+ )
115
+ .all(...allIds) as {
116
+ id: string;
117
+ name: string;
118
+ file_path: string | null;
119
+ start_line: number | null;
120
+ }[];
121
+
122
+ const nodeInfo = new Map<string, { name: string; loc: string }>();
123
+ for (const row of nodeRows) {
124
+ const loc =
125
+ row.file_path != null
126
+ ? row.start_line != null
127
+ ? `${row.file_path}:${row.start_line}`
128
+ : row.file_path
129
+ : row.id;
130
+ nodeInfo.set(row.id, { name: row.name ?? row.id, loc });
131
+ }
132
+
133
+ const lines: string[] = [
134
+ `Import chain: ${paths.length} path${paths.length === 1 ? '' : 's'} from ${nodeInfo.get(sourceId)?.name ?? sourceId} to ${nodeInfo.get(targetId)?.name ?? targetId}`,
135
+ '',
136
+ ];
137
+
138
+ for (let i = 0; i < paths.length; i++) {
139
+ lines.push(`Path ${i + 1} (${paths[i].length} node${paths[i].length === 1 ? '' : 's'}):`);
140
+ for (const id of paths[i]) {
141
+ const info = nodeInfo.get(id);
142
+ lines.push(` ${info?.name ?? id} — ${info?.loc ?? id}`);
143
+ }
144
+ if (i < paths.length - 1) lines.push('');
145
+ }
146
+
147
+ return lines.join('\n');
148
+ }