@optave/codegraph 3.4.0 → 3.4.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 (410) hide show
  1. package/README.md +7 -7
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +3 -9
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/shared.d.ts.map +1 -1
  6. package/dist/ast-analysis/shared.js +0 -1
  7. package/dist/ast-analysis/shared.js.map +1 -1
  8. package/dist/ast-analysis/visitors/cfg-conditionals.d.ts +5 -0
  9. package/dist/ast-analysis/visitors/cfg-conditionals.d.ts.map +1 -0
  10. package/dist/ast-analysis/visitors/cfg-conditionals.js +166 -0
  11. package/dist/ast-analysis/visitors/cfg-conditionals.js.map +1 -0
  12. package/dist/ast-analysis/visitors/cfg-loops.d.ts +7 -0
  13. package/dist/ast-analysis/visitors/cfg-loops.d.ts.map +1 -0
  14. package/dist/ast-analysis/visitors/cfg-loops.js +73 -0
  15. package/dist/ast-analysis/visitors/cfg-loops.js.map +1 -0
  16. package/dist/ast-analysis/visitors/cfg-shared.d.ts +56 -0
  17. package/dist/ast-analysis/visitors/cfg-shared.d.ts.map +1 -0
  18. package/dist/ast-analysis/visitors/cfg-shared.js +107 -0
  19. package/dist/ast-analysis/visitors/cfg-shared.js.map +1 -0
  20. package/dist/ast-analysis/visitors/cfg-try-catch.d.ts +4 -0
  21. package/dist/ast-analysis/visitors/cfg-try-catch.d.ts.map +1 -0
  22. package/dist/ast-analysis/visitors/cfg-try-catch.js +100 -0
  23. package/dist/ast-analysis/visitors/cfg-try-catch.js.map +1 -0
  24. package/dist/ast-analysis/visitors/cfg-visitor.d.ts +2 -2
  25. package/dist/ast-analysis/visitors/cfg-visitor.d.ts.map +1 -1
  26. package/dist/ast-analysis/visitors/cfg-visitor.js +11 -445
  27. package/dist/ast-analysis/visitors/cfg-visitor.js.map +1 -1
  28. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  29. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  30. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  31. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  32. package/dist/cli/commands/batch.d.ts.map +1 -1
  33. package/dist/cli/commands/batch.js +4 -3
  34. package/dist/cli/commands/batch.js.map +1 -1
  35. package/dist/cli/commands/branch-compare.js +1 -1
  36. package/dist/cli/commands/branch-compare.js.map +1 -1
  37. package/dist/cli/commands/build.js +1 -1
  38. package/dist/cli/commands/build.js.map +1 -1
  39. package/dist/cli/commands/info.d.ts.map +1 -1
  40. package/dist/cli/commands/info.js +1 -2
  41. package/dist/cli/commands/info.js.map +1 -1
  42. package/dist/cli/commands/path.d.ts.map +1 -1
  43. package/dist/cli/commands/path.js +7 -2
  44. package/dist/cli/commands/path.js.map +1 -1
  45. package/dist/cli/commands/plot.d.ts.map +1 -1
  46. package/dist/cli/commands/plot.js +2 -2
  47. package/dist/cli/commands/plot.js.map +1 -1
  48. package/dist/cli/commands/watch.js +1 -1
  49. package/dist/cli/commands/watch.js.map +1 -1
  50. package/dist/cli/index.js +2 -2
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/cli/shared/open-graph.d.ts +2 -2
  53. package/dist/cli/shared/open-graph.d.ts.map +1 -1
  54. package/dist/cli/shared/open-graph.js.map +1 -1
  55. package/dist/cli/types.d.ts +1 -1
  56. package/dist/cli/types.d.ts.map +1 -1
  57. package/dist/cli.js +2 -3
  58. package/dist/cli.js.map +1 -1
  59. package/dist/db/connection.d.ts +17 -0
  60. package/dist/db/connection.d.ts.map +1 -1
  61. package/dist/db/connection.js +91 -2
  62. package/dist/db/connection.js.map +1 -1
  63. package/dist/db/index.d.ts +1 -1
  64. package/dist/db/index.d.ts.map +1 -1
  65. package/dist/db/index.js +1 -1
  66. package/dist/db/index.js.map +1 -1
  67. package/dist/db/migrations.d.ts.map +1 -1
  68. package/dist/db/migrations.js +7 -0
  69. package/dist/db/migrations.js.map +1 -1
  70. package/dist/domain/analysis/brief.d.ts.map +1 -1
  71. package/dist/domain/analysis/brief.js +1 -3
  72. package/dist/domain/analysis/brief.js.map +1 -1
  73. package/dist/domain/analysis/context.d.ts.map +1 -1
  74. package/dist/domain/analysis/context.js +2 -4
  75. package/dist/domain/analysis/context.js.map +1 -1
  76. package/dist/domain/analysis/dependencies.d.ts +49 -0
  77. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  78. package/dist/domain/analysis/dependencies.js +145 -0
  79. package/dist/domain/analysis/dependencies.js.map +1 -1
  80. package/dist/domain/analysis/diff-impact.d.ts +76 -0
  81. package/dist/domain/analysis/diff-impact.d.ts.map +1 -0
  82. package/dist/domain/analysis/diff-impact.js +282 -0
  83. package/dist/domain/analysis/diff-impact.js.map +1 -0
  84. package/dist/domain/analysis/exports.d.ts.map +1 -1
  85. package/dist/domain/analysis/exports.js +0 -1
  86. package/dist/domain/analysis/exports.js.map +1 -1
  87. package/dist/domain/analysis/fn-impact.d.ts +66 -0
  88. package/dist/domain/analysis/fn-impact.d.ts.map +1 -0
  89. package/dist/domain/analysis/fn-impact.js +189 -0
  90. package/dist/domain/analysis/fn-impact.js.map +1 -0
  91. package/dist/domain/analysis/impact.d.ts +8 -148
  92. package/dist/domain/analysis/impact.d.ts.map +1 -1
  93. package/dist/domain/analysis/impact.js +8 -568
  94. package/dist/domain/analysis/impact.js.map +1 -1
  95. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  96. package/dist/domain/analysis/module-map.js +1 -3
  97. package/dist/domain/analysis/module-map.js.map +1 -1
  98. package/dist/domain/graph/builder/context.d.ts +2 -3
  99. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  100. package/dist/domain/graph/builder/context.js.map +1 -1
  101. package/dist/domain/graph/builder/helpers.d.ts +4 -5
  102. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  103. package/dist/domain/graph/builder/helpers.js +1 -2
  104. package/dist/domain/graph/builder/helpers.js.map +1 -1
  105. package/dist/domain/graph/builder/incremental.d.ts +2 -3
  106. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  107. package/dist/domain/graph/builder/incremental.js.map +1 -1
  108. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  109. package/dist/domain/graph/builder/pipeline.js +6 -0
  110. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  111. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  112. package/dist/domain/graph/builder/stages/build-edges.js +12 -2
  113. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  114. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  115. package/dist/domain/graph/builder/stages/build-structure.js +155 -59
  116. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  117. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  118. package/dist/domain/graph/builder/stages/detect-changes.js +6 -6
  119. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  120. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  121. package/dist/domain/graph/builder/stages/finalize.js +85 -61
  122. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  123. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  124. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  125. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  126. package/dist/domain/graph/builder/stages/resolve-imports.js +58 -11
  127. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  128. package/dist/domain/graph/cycles.js +2 -2
  129. package/dist/domain/graph/cycles.js.map +1 -1
  130. package/dist/domain/graph/resolve.d.ts.map +1 -1
  131. package/dist/domain/graph/resolve.js +10 -8
  132. package/dist/domain/graph/resolve.js.map +1 -1
  133. package/dist/domain/graph/watcher.d.ts.map +1 -1
  134. package/dist/domain/graph/watcher.js +1 -3
  135. package/dist/domain/graph/watcher.js.map +1 -1
  136. package/dist/domain/parser.d.ts.map +1 -1
  137. package/dist/domain/parser.js +11 -12
  138. package/dist/domain/parser.js.map +1 -1
  139. package/dist/domain/queries.d.ts +3 -2
  140. package/dist/domain/queries.d.ts.map +1 -1
  141. package/dist/domain/queries.js +3 -2
  142. package/dist/domain/queries.js.map +1 -1
  143. package/dist/domain/search/generator.d.ts.map +1 -1
  144. package/dist/domain/search/generator.js.map +1 -1
  145. package/dist/extractors/csharp.js +2 -2
  146. package/dist/extractors/csharp.js.map +1 -1
  147. package/dist/extractors/go.js +2 -2
  148. package/dist/extractors/go.js.map +1 -1
  149. package/dist/extractors/helpers.d.ts +5 -0
  150. package/dist/extractors/helpers.d.ts.map +1 -1
  151. package/dist/extractors/helpers.js +5 -0
  152. package/dist/extractors/helpers.js.map +1 -1
  153. package/dist/extractors/javascript.js +58 -60
  154. package/dist/extractors/javascript.js.map +1 -1
  155. package/dist/extractors/php.js +2 -2
  156. package/dist/extractors/php.js.map +1 -1
  157. package/dist/extractors/python.js +2 -2
  158. package/dist/extractors/python.js.map +1 -1
  159. package/dist/extractors/rust.js +2 -2
  160. package/dist/extractors/rust.js.map +1 -1
  161. package/dist/features/audit.d.ts.map +1 -1
  162. package/dist/features/audit.js +1 -2
  163. package/dist/features/audit.js.map +1 -1
  164. package/dist/features/branch-compare.d.ts.map +1 -1
  165. package/dist/features/branch-compare.js +2 -3
  166. package/dist/features/branch-compare.js.map +1 -1
  167. package/dist/features/cfg.d.ts.map +1 -1
  168. package/dist/features/cfg.js +2 -4
  169. package/dist/features/cfg.js.map +1 -1
  170. package/dist/features/cochange.js +4 -4
  171. package/dist/features/cochange.js.map +1 -1
  172. package/dist/features/communities.js +4 -4
  173. package/dist/features/communities.js.map +1 -1
  174. package/dist/features/complexity-query.d.ts +37 -0
  175. package/dist/features/complexity-query.d.ts.map +1 -0
  176. package/dist/features/complexity-query.js +263 -0
  177. package/dist/features/complexity-query.js.map +1 -0
  178. package/dist/features/complexity.d.ts +2 -30
  179. package/dist/features/complexity.d.ts.map +1 -1
  180. package/dist/features/complexity.js +7 -261
  181. package/dist/features/complexity.js.map +1 -1
  182. package/dist/features/dataflow.d.ts.map +1 -1
  183. package/dist/features/dataflow.js +8 -24
  184. package/dist/features/dataflow.js.map +1 -1
  185. package/dist/features/export.d.ts +7 -8
  186. package/dist/features/export.d.ts.map +1 -1
  187. package/dist/features/export.js.map +1 -1
  188. package/dist/features/flow.d.ts.map +1 -1
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +1 -3
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/manifesto.js +8 -8
  194. package/dist/features/manifesto.js.map +1 -1
  195. package/dist/features/snapshot.d.ts.map +1 -1
  196. package/dist/features/snapshot.js +0 -1
  197. package/dist/features/snapshot.js.map +1 -1
  198. package/dist/features/structure-query.d.ts +76 -0
  199. package/dist/features/structure-query.d.ts.map +1 -0
  200. package/dist/features/structure-query.js +245 -0
  201. package/dist/features/structure-query.js.map +1 -0
  202. package/dist/features/structure.d.ts +12 -67
  203. package/dist/features/structure.d.ts.map +1 -1
  204. package/dist/features/structure.js +188 -244
  205. package/dist/features/structure.js.map +1 -1
  206. package/dist/features/triage.js +2 -2
  207. package/dist/features/triage.js.map +1 -1
  208. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  209. package/dist/graph/algorithms/leiden/adapter.js +2 -9
  210. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  211. package/dist/graph/classifiers/roles.d.ts +5 -1
  212. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  213. package/dist/graph/classifiers/roles.js +20 -12
  214. package/dist/graph/classifiers/roles.js.map +1 -1
  215. package/dist/index.d.ts +1 -0
  216. package/dist/index.d.ts.map +1 -1
  217. package/dist/index.js.map +1 -1
  218. package/dist/infrastructure/config.d.ts.map +1 -1
  219. package/dist/infrastructure/config.js +12 -11
  220. package/dist/infrastructure/config.js.map +1 -1
  221. package/dist/infrastructure/native.d.ts.map +1 -1
  222. package/dist/infrastructure/native.js +7 -3
  223. package/dist/infrastructure/native.js.map +1 -1
  224. package/dist/infrastructure/registry.d.ts.map +1 -1
  225. package/dist/infrastructure/registry.js +1 -1
  226. package/dist/infrastructure/registry.js.map +1 -1
  227. package/dist/infrastructure/update-check.js +3 -3
  228. package/dist/infrastructure/update-check.js.map +1 -1
  229. package/dist/mcp/server.d.ts.map +1 -1
  230. package/dist/mcp/server.js +2 -8
  231. package/dist/mcp/server.js.map +1 -1
  232. package/dist/mcp/tool-registry.d.ts.map +1 -1
  233. package/dist/mcp/tool-registry.js +9 -4
  234. package/dist/mcp/tool-registry.js.map +1 -1
  235. package/dist/mcp/tools/audit.js +1 -1
  236. package/dist/mcp/tools/audit.js.map +1 -1
  237. package/dist/mcp/tools/cfg.js +1 -1
  238. package/dist/mcp/tools/cfg.js.map +1 -1
  239. package/dist/mcp/tools/check.js +2 -2
  240. package/dist/mcp/tools/check.js.map +1 -1
  241. package/dist/mcp/tools/dataflow.js +2 -2
  242. package/dist/mcp/tools/dataflow.js.map +1 -1
  243. package/dist/mcp/tools/export-graph.js +1 -1
  244. package/dist/mcp/tools/export-graph.js.map +1 -1
  245. package/dist/mcp/tools/index.d.ts.map +1 -1
  246. package/dist/mcp/tools/index.js.map +1 -1
  247. package/dist/mcp/tools/path.d.ts +1 -0
  248. package/dist/mcp/tools/path.d.ts.map +1 -1
  249. package/dist/mcp/tools/path.js +9 -0
  250. package/dist/mcp/tools/path.js.map +1 -1
  251. package/dist/mcp/tools/query.js +1 -1
  252. package/dist/mcp/tools/query.js.map +1 -1
  253. package/dist/mcp/tools/semantic-search.js +1 -1
  254. package/dist/mcp/tools/semantic-search.js.map +1 -1
  255. package/dist/mcp/tools/sequence.js +1 -1
  256. package/dist/mcp/tools/sequence.js.map +1 -1
  257. package/dist/mcp/tools/symbol-children.js +1 -1
  258. package/dist/mcp/tools/symbol-children.js.map +1 -1
  259. package/dist/mcp/tools/triage.js +1 -1
  260. package/dist/mcp/tools/triage.js.map +1 -1
  261. package/dist/presentation/audit.d.ts.map +1 -1
  262. package/dist/presentation/audit.js +0 -1
  263. package/dist/presentation/audit.js.map +1 -1
  264. package/dist/presentation/diff-impact-mermaid.d.ts +11 -0
  265. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -0
  266. package/dist/presentation/diff-impact-mermaid.js +105 -0
  267. package/dist/presentation/diff-impact-mermaid.js.map +1 -0
  268. package/dist/presentation/flow.d.ts.map +1 -1
  269. package/dist/presentation/flow.js +0 -2
  270. package/dist/presentation/flow.js.map +1 -1
  271. package/dist/presentation/manifesto.d.ts.map +1 -1
  272. package/dist/presentation/manifesto.js +0 -1
  273. package/dist/presentation/manifesto.js.map +1 -1
  274. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  275. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  276. package/dist/presentation/queries-cli/path.d.ts.map +1 -1
  277. package/dist/presentation/queries-cli/path.js +45 -1
  278. package/dist/presentation/queries-cli/path.js.map +1 -1
  279. package/dist/presentation/result-formatter.d.ts.map +1 -1
  280. package/dist/presentation/result-formatter.js +1 -3
  281. package/dist/presentation/result-formatter.js.map +1 -1
  282. package/dist/presentation/sequence.d.ts.map +1 -1
  283. package/dist/presentation/sequence.js +0 -1
  284. package/dist/presentation/sequence.js.map +1 -1
  285. package/dist/presentation/structure.d.ts.map +1 -1
  286. package/dist/presentation/structure.js.map +1 -1
  287. package/dist/presentation/triage.d.ts.map +1 -1
  288. package/dist/presentation/triage.js +0 -1
  289. package/dist/presentation/triage.js.map +1 -1
  290. package/dist/shared/constants.d.ts +9 -3
  291. package/dist/shared/constants.d.ts.map +1 -1
  292. package/dist/shared/constants.js +6 -3
  293. package/dist/shared/constants.js.map +1 -1
  294. package/dist/shared/errors.d.ts +2 -0
  295. package/dist/shared/errors.d.ts.map +1 -1
  296. package/dist/shared/errors.js +4 -0
  297. package/dist/shared/errors.js.map +1 -1
  298. package/dist/shared/version.d.ts +2 -0
  299. package/dist/shared/version.d.ts.map +1 -0
  300. package/dist/shared/version.js +5 -0
  301. package/dist/shared/version.js.map +1 -0
  302. package/dist/types.d.ts +2 -2
  303. package/dist/types.d.ts.map +1 -1
  304. package/package.json +8 -7
  305. package/src/ast-analysis/engine.ts +3 -9
  306. package/src/ast-analysis/shared.ts +0 -1
  307. package/src/ast-analysis/visitors/cfg-conditionals.ts +227 -0
  308. package/src/ast-analysis/visitors/cfg-loops.ts +136 -0
  309. package/src/ast-analysis/visitors/cfg-shared.ts +196 -0
  310. package/src/ast-analysis/visitors/cfg-try-catch.ts +142 -0
  311. package/src/ast-analysis/visitors/cfg-visitor.ts +34 -655
  312. package/src/ast-analysis/visitors/complexity-visitor.ts +0 -1
  313. package/src/ast-analysis/visitors/dataflow-visitor.ts +0 -1
  314. package/src/cli/commands/batch.ts +4 -3
  315. package/src/cli/commands/branch-compare.ts +1 -1
  316. package/src/cli/commands/build.ts +1 -1
  317. package/src/cli/commands/info.ts +1 -2
  318. package/src/cli/commands/path.ts +7 -2
  319. package/src/cli/commands/plot.ts +2 -2
  320. package/src/cli/commands/watch.ts +1 -1
  321. package/src/cli/index.ts +2 -2
  322. package/src/cli/shared/open-graph.ts +2 -2
  323. package/src/cli/types.ts +1 -1
  324. package/src/cli.ts +2 -3
  325. package/src/db/connection.ts +97 -13
  326. package/src/db/index.ts +2 -0
  327. package/src/db/migrations.ts +7 -0
  328. package/src/domain/analysis/brief.ts +0 -1
  329. package/src/domain/analysis/context.ts +2 -6
  330. package/src/domain/analysis/dependencies.ts +165 -0
  331. package/src/domain/analysis/diff-impact.ts +354 -0
  332. package/src/domain/analysis/exports.ts +0 -2
  333. package/src/domain/analysis/fn-impact.ts +241 -0
  334. package/src/domain/analysis/impact.ts +8 -718
  335. package/src/domain/analysis/module-map.ts +1 -5
  336. package/src/domain/graph/builder/context.ts +2 -2
  337. package/src/domain/graph/builder/helpers.ts +14 -11
  338. package/src/domain/graph/builder/incremental.ts +33 -28
  339. package/src/domain/graph/builder/pipeline.ts +8 -0
  340. package/src/domain/graph/builder/stages/build-edges.ts +17 -4
  341. package/src/domain/graph/builder/stages/build-structure.ts +205 -76
  342. package/src/domain/graph/builder/stages/detect-changes.ts +11 -12
  343. package/src/domain/graph/builder/stages/finalize.ts +100 -81
  344. package/src/domain/graph/builder/stages/insert-nodes.ts +12 -8
  345. package/src/domain/graph/builder/stages/resolve-imports.ts +75 -10
  346. package/src/domain/graph/cycles.ts +2 -2
  347. package/src/domain/graph/resolve.ts +14 -8
  348. package/src/domain/graph/watcher.ts +2 -4
  349. package/src/domain/parser.ts +11 -13
  350. package/src/domain/queries.ts +2 -2
  351. package/src/domain/search/generator.ts +3 -4
  352. package/src/extractors/csharp.ts +2 -2
  353. package/src/extractors/go.ts +2 -2
  354. package/src/extractors/helpers.ts +6 -0
  355. package/src/extractors/javascript.ts +58 -61
  356. package/src/extractors/php.ts +2 -2
  357. package/src/extractors/python.ts +2 -2
  358. package/src/extractors/rust.ts +2 -2
  359. package/src/features/audit.ts +1 -2
  360. package/src/features/branch-compare.ts +3 -9
  361. package/src/features/cfg.ts +2 -4
  362. package/src/features/cochange.ts +4 -4
  363. package/src/features/communities.ts +4 -4
  364. package/src/features/complexity-query.ts +370 -0
  365. package/src/features/complexity.ts +6 -365
  366. package/src/features/dataflow.ts +48 -70
  367. package/src/features/export.ts +12 -16
  368. package/src/features/flow.ts +0 -1
  369. package/src/features/graph-enrichment.ts +1 -3
  370. package/src/features/manifesto.ts +8 -8
  371. package/src/features/snapshot.ts +1 -2
  372. package/src/features/structure-query.ts +387 -0
  373. package/src/features/structure.ts +231 -376
  374. package/src/features/triage.ts +2 -2
  375. package/src/graph/algorithms/leiden/adapter.ts +2 -9
  376. package/src/graph/classifiers/roles.ts +22 -13
  377. package/src/index.ts +1 -0
  378. package/src/infrastructure/config.ts +12 -13
  379. package/src/infrastructure/native.ts +7 -3
  380. package/src/infrastructure/registry.ts +1 -1
  381. package/src/infrastructure/update-check.ts +3 -3
  382. package/src/mcp/server.ts +2 -10
  383. package/src/mcp/tool-registry.ts +11 -4
  384. package/src/mcp/tools/audit.ts +1 -1
  385. package/src/mcp/tools/cfg.ts +1 -1
  386. package/src/mcp/tools/check.ts +2 -2
  387. package/src/mcp/tools/dataflow.ts +2 -2
  388. package/src/mcp/tools/export-graph.ts +1 -1
  389. package/src/mcp/tools/index.ts +0 -1
  390. package/src/mcp/tools/path.ts +10 -0
  391. package/src/mcp/tools/query.ts +1 -1
  392. package/src/mcp/tools/semantic-search.ts +1 -1
  393. package/src/mcp/tools/sequence.ts +1 -1
  394. package/src/mcp/tools/symbol-children.ts +1 -1
  395. package/src/mcp/tools/triage.ts +1 -1
  396. package/src/presentation/audit.ts +0 -1
  397. package/src/presentation/diff-impact-mermaid.ts +127 -0
  398. package/src/presentation/flow.ts +0 -2
  399. package/src/presentation/manifesto.ts +0 -1
  400. package/src/presentation/queries-cli/inspect.ts +0 -1
  401. package/src/presentation/queries-cli/path.ts +71 -1
  402. package/src/presentation/result-formatter.ts +0 -1
  403. package/src/presentation/sequence.ts +0 -1
  404. package/src/presentation/structure.ts +0 -12
  405. package/src/presentation/triage.ts +0 -1
  406. package/src/shared/constants.ts +33 -19
  407. package/src/shared/errors.ts +5 -0
  408. package/src/shared/version.ts +10 -0
  409. package/src/types.ts +4 -10
  410. package/src/vendor.d.ts +0 -39
@@ -1,11 +1,8 @@
1
1
  import path from 'node:path';
2
- import { getNodeId, openReadonlyOrFail, testFilterSQL } from '../db/index.js';
3
- import { loadConfig } from '../infrastructure/config.js';
2
+ import { getNodeId, testFilterSQL } from '../db/index.js';
4
3
  import { debug } from '../infrastructure/logger.js';
5
- import { isTestFile } from '../infrastructure/test-filter.js';
6
4
  import { normalizePath } from '../shared/constants.js';
7
- import { paginateResult } from '../shared/paginate.js';
8
- import type { BetterSqlite3Database, CodegraphConfig } from '../types.js';
5
+ import type { BetterSqlite3Database } from '../types.js';
9
6
 
10
7
  // ─── Build-time helpers ───────────────────────────────────────────────
11
8
 
@@ -367,7 +364,7 @@ export function buildStructure(
367
364
  // Re-export from classifier for backward compatibility
368
365
  export { FRAMEWORK_ENTRY_PREFIXES } from '../graph/classifiers/roles.js';
369
366
 
370
- import { classifyRoles } from '../graph/classifiers/roles.js';
367
+ import { classifyRoles, median } from '../graph/classifiers/roles.js';
371
368
 
372
369
  interface RoleSummary {
373
370
  entry: number;
@@ -384,7 +381,53 @@ interface RoleSummary {
384
381
  [key: string]: number;
385
382
  }
386
383
 
387
- export function classifyNodeRoles(db: BetterSqlite3Database): RoleSummary {
384
+ /**
385
+ * Classify every node in the graph into a role (core, entry, utility, etc.).
386
+ *
387
+ * When `changedFiles` is provided, only nodes from those files (and their
388
+ * edge neighbours) are reclassified. The returned `RoleSummary` in that case
389
+ * reflects **only the affected subset**, not the entire graph. Callers that
390
+ * need graph-wide totals should perform a full classification (omit
391
+ * `changedFiles`) or query the DB directly.
392
+ */
393
+ export function classifyNodeRoles(
394
+ db: BetterSqlite3Database,
395
+ changedFiles?: string[] | null,
396
+ ): RoleSummary {
397
+ const emptySummary: RoleSummary = {
398
+ entry: 0,
399
+ core: 0,
400
+ utility: 0,
401
+ adapter: 0,
402
+ dead: 0,
403
+ 'dead-leaf': 0,
404
+ 'dead-entry': 0,
405
+ 'dead-ffi': 0,
406
+ 'dead-unresolved': 0,
407
+ 'test-only': 0,
408
+ leaf: 0,
409
+ };
410
+
411
+ // Incremental path: only reclassify nodes from affected files
412
+ if (changedFiles && changedFiles.length > 0) {
413
+ return classifyNodeRolesIncremental(db, changedFiles, emptySummary);
414
+ }
415
+
416
+ return classifyNodeRolesFull(db, emptySummary);
417
+ }
418
+
419
+ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSummary): RoleSummary {
420
+ // Leaf kinds (parameter, property) can never have callers/callees.
421
+ // Classify them directly as dead-leaf without the expensive fan-in/fan-out JOINs.
422
+ const leafRows = db
423
+ .prepare(
424
+ `SELECT n.id
425
+ FROM nodes n
426
+ WHERE n.kind IN ('parameter', 'property')`,
427
+ )
428
+ .all() as { id: number }[];
429
+
430
+ // Only compute fan-in/fan-out for callable/classifiable nodes
388
431
  const rows = db
389
432
  .prepare(
390
433
  `SELECT n.id, n.name, n.kind, n.file,
@@ -397,7 +440,7 @@ export function classifyNodeRoles(db: BetterSqlite3Database): RoleSummary {
397
440
  LEFT JOIN (
398
441
  SELECT source_id, COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY source_id
399
442
  ) fo ON n.id = fo.source_id
400
- WHERE n.kind NOT IN ('file', 'directory')`,
443
+ WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')`,
401
444
  )
402
445
  .all() as {
403
446
  id: number;
@@ -408,21 +451,7 @@ export function classifyNodeRoles(db: BetterSqlite3Database): RoleSummary {
408
451
  fan_out: number;
409
452
  }[];
410
453
 
411
- const emptySummary: RoleSummary = {
412
- entry: 0,
413
- core: 0,
414
- utility: 0,
415
- adapter: 0,
416
- dead: 0,
417
- 'dead-leaf': 0,
418
- 'dead-entry': 0,
419
- 'dead-ffi': 0,
420
- 'dead-unresolved': 0,
421
- 'test-only': 0,
422
- leaf: 0,
423
- };
424
-
425
- if (rows.length === 0) return emptySummary;
454
+ if (rows.length === 0 && leafRows.length === 0) return emptySummary;
426
455
 
427
456
  const exportedIds = new Set(
428
457
  (
@@ -471,6 +500,16 @@ export function classifyNodeRoles(db: BetterSqlite3Database): RoleSummary {
471
500
  // Build summary and group updates by role for batch UPDATE
472
501
  const summary: RoleSummary = { ...emptySummary };
473
502
  const idsByRole = new Map<string, number[]>();
503
+
504
+ // Leaf kinds are always dead-leaf -- skip classifier
505
+ if (leafRows.length > 0) {
506
+ const leafIds: number[] = [];
507
+ for (const row of leafRows) leafIds.push(row.id);
508
+ idsByRole.set('dead-leaf', leafIds);
509
+ summary.dead += leafRows.length;
510
+ summary['dead-leaf'] += leafRows.length;
511
+ }
512
+
474
513
  for (const row of rows) {
475
514
  const role = roleMap.get(String(row.id)) || 'leaf';
476
515
  if (role.startsWith('dead')) summary.dead++;
@@ -508,372 +547,188 @@ export function classifyNodeRoles(db: BetterSqlite3Database): RoleSummary {
508
547
  return summary;
509
548
  }
510
549
 
511
- // ─── Query functions (read-only) ──────────────────────────────────────
512
-
513
- interface DirRow {
514
- id: number;
515
- name: string;
516
- file: string;
517
- symbol_count: number | null;
518
- fan_in: number | null;
519
- fan_out: number | null;
520
- cohesion: number | null;
521
- file_count: number | null;
522
- }
523
-
524
- interface FileMetricRow {
525
- name: string;
526
- line_count: number | null;
527
- symbol_count: number | null;
528
- import_count: number | null;
529
- export_count: number | null;
530
- fan_in: number | null;
531
- fan_out: number | null;
532
- }
533
-
534
- interface StructureDataOpts {
535
- directory?: string;
536
- depth?: number;
537
- sort?: string;
538
- noTests?: boolean;
539
- full?: boolean;
540
- fileLimit?: number;
541
- limit?: number;
542
- offset?: number;
543
- }
550
+ /**
551
+ * Incremental role classification: only reclassify nodes from changed files
552
+ * plus their immediate edge neighbours (callers and callees in other files).
553
+ *
554
+ * Uses indexed point lookups for fan-in/fan-out instead of full table scans.
555
+ * Global medians are computed from edge distribution (fast GROUP BY on index).
556
+ * Unchanged files not connected to changed files keep their roles from the
557
+ * previous build.
558
+ */
559
+ function classifyNodeRolesIncremental(
560
+ db: BetterSqlite3Database,
561
+ changedFiles: string[],
562
+ emptySummary: RoleSummary,
563
+ ): RoleSummary {
564
+ // Expand affected set: include files containing nodes that are edge neighbours
565
+ // of changed-file nodes. This ensures that removing a call from file A to a
566
+ // node in file B causes B's roles to be recalculated (fan_in changed).
567
+ const seedPlaceholders = changedFiles.map(() => '?').join(',');
568
+ const neighbourFiles = db
569
+ .prepare(
570
+ `SELECT DISTINCT n2.file FROM edges e
571
+ JOIN nodes n1 ON (e.source_id = n1.id OR e.target_id = n1.id)
572
+ JOIN nodes n2 ON (e.source_id = n2.id OR e.target_id = n2.id)
573
+ WHERE e.kind = 'calls'
574
+ AND n1.file IN (${seedPlaceholders})
575
+ AND n2.file NOT IN (${seedPlaceholders})
576
+ AND n2.kind NOT IN ('file', 'directory')`,
577
+ )
578
+ .all(...changedFiles, ...changedFiles) as { file: string }[];
579
+ const allAffectedFiles = [...changedFiles, ...neighbourFiles.map((r) => r.file)];
580
+ const placeholders = allAffectedFiles.map(() => '?').join(',');
581
+
582
+ // 1. Compute global medians from edge distribution (fast: scans edge index, no node join)
583
+ const fanInDist = (
584
+ db
585
+ .prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY target_id`)
586
+ .all() as { cnt: number }[]
587
+ )
588
+ .map((r) => r.cnt)
589
+ .sort((a, b) => a - b);
590
+ const fanOutDist = (
591
+ db
592
+ .prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY source_id`)
593
+ .all() as { cnt: number }[]
594
+ )
595
+ .map((r) => r.cnt)
596
+ .sort((a, b) => a - b);
597
+
598
+ const globalMedians = { fanIn: median(fanInDist), fanOut: median(fanOutDist) };
599
+
600
+ // 2a. Leaf kinds (parameter, property) in affected files — always dead-leaf
601
+ const leafRows = db
602
+ .prepare(
603
+ `SELECT n.id FROM nodes n
604
+ WHERE n.kind IN ('parameter', 'property')
605
+ AND n.file IN (${placeholders})`,
606
+ )
607
+ .all(...allAffectedFiles) as { id: number }[];
544
608
 
545
- interface DirectoryEntry {
546
- directory: string;
547
- fileCount: number;
548
- symbolCount: number;
549
- fanIn: number;
550
- fanOut: number;
551
- cohesion: number | null;
552
- density: number;
553
- files: {
609
+ // 2b. Get callable nodes using indexed correlated subqueries (fast point lookups)
610
+ const rows = db
611
+ .prepare(
612
+ `SELECT n.id, n.name, n.kind, n.file,
613
+ (SELECT COUNT(*) FROM edges WHERE kind = 'calls' AND target_id = n.id) AS fan_in,
614
+ (SELECT COUNT(*) FROM edges WHERE kind = 'calls' AND source_id = n.id) AS fan_out
615
+ FROM nodes n
616
+ WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')
617
+ AND n.file IN (${placeholders})`,
618
+ )
619
+ .all(...allAffectedFiles) as {
620
+ id: number;
621
+ name: string;
622
+ kind: string;
554
623
  file: string;
555
- lineCount: number;
556
- symbolCount: number;
557
- importCount: number;
558
- exportCount: number;
559
- fanIn: number;
560
- fanOut: number;
624
+ fan_in: number;
625
+ fan_out: number;
561
626
  }[];
562
- subdirectories: string[];
563
- }
564
-
565
- export function structureData(
566
- customDbPath?: string,
567
- opts: StructureDataOpts = {},
568
- ): {
569
- directories: DirectoryEntry[];
570
- count: number;
571
- suppressed?: number;
572
- warning?: string;
573
- } {
574
- const db = openReadonlyOrFail(customDbPath);
575
- try {
576
- const rawDir = opts.directory || null;
577
- const filterDir = rawDir && normalizePath(rawDir) !== '.' ? rawDir : null;
578
- const maxDepth = opts.depth || null;
579
- const sortBy = opts.sort || 'files';
580
- const noTests = opts.noTests || false;
581
- const full = opts.full || false;
582
- const fileLimit = opts.fileLimit || 25;
583
-
584
- // Get all directory nodes with their metrics
585
- let dirs = db
586
- .prepare(`
587
- SELECT n.id, n.name, n.file, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
588
- FROM nodes n
589
- LEFT JOIN node_metrics nm ON n.id = nm.node_id
590
- WHERE n.kind = 'directory'
591
- `)
592
- .all() as DirRow[];
593
-
594
- if (filterDir) {
595
- const norm = normalizePath(filterDir);
596
- dirs = dirs.filter((d) => d.name === norm || d.name.startsWith(`${norm}/`));
597
- }
598
627
 
599
- if (maxDepth) {
600
- const baseDepth = filterDir ? normalizePath(filterDir).split('/').length : 0;
601
- dirs = dirs.filter((d) => {
602
- const depth = d.name.split('/').length - baseDepth;
603
- return depth <= maxDepth;
604
- });
605
- }
606
-
607
- // Sort
608
- const sortFn = getSortFn(sortBy);
609
- dirs.sort(sortFn);
628
+ if (rows.length === 0 && leafRows.length === 0) return emptySummary;
610
629
 
611
- // Get file metrics for each directory
612
- const result: DirectoryEntry[] = dirs.map((d) => {
613
- let files = db
614
- .prepare(`
615
- SELECT n.name, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count, nm.fan_in, nm.fan_out
616
- FROM edges e
617
- JOIN nodes n ON e.target_id = n.id
618
- LEFT JOIN node_metrics nm ON n.id = nm.node_id
619
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
620
- `)
621
- .all(d.id) as FileMetricRow[];
622
- if (noTests) files = files.filter((f) => !isTestFile(f.name));
623
-
624
- const subdirs = db
625
- .prepare(`
626
- SELECT n.name
630
+ // 3. Get exported status for affected nodes only (scoped to changed files)
631
+ const exportedIds = new Set(
632
+ (
633
+ db
634
+ .prepare(
635
+ `SELECT DISTINCT e.target_id
627
636
  FROM edges e
628
- JOIN nodes n ON e.target_id = n.id
629
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'directory'
630
- `)
631
- .all(d.id) as { name: string }[];
632
-
633
- const fileCount = noTests ? files.length : d.file_count || 0;
634
- return {
635
- directory: d.name,
636
- fileCount,
637
- symbolCount: d.symbol_count || 0,
638
- fanIn: d.fan_in || 0,
639
- fanOut: d.fan_out || 0,
640
- cohesion: d.cohesion,
641
- density: fileCount > 0 ? (d.symbol_count || 0) / fileCount : 0,
642
- files: files.map((f) => ({
643
- file: f.name,
644
- lineCount: f.line_count || 0,
645
- symbolCount: f.symbol_count || 0,
646
- importCount: f.import_count || 0,
647
- exportCount: f.export_count || 0,
648
- fanIn: f.fan_in || 0,
649
- fanOut: f.fan_out || 0,
650
- })),
651
- subdirectories: subdirs.map((s) => s.name),
652
- };
653
- });
654
-
655
- // Apply global file limit unless full mode
656
- if (!full) {
657
- const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
658
- if (totalFiles > fileLimit) {
659
- let shown = 0;
660
- for (const d of result) {
661
- const remaining = fileLimit - shown;
662
- if (remaining <= 0) {
663
- d.files = [];
664
- } else if (d.files.length > remaining) {
665
- d.files = d.files.slice(0, remaining);
666
- shown = fileLimit;
667
- } else {
668
- shown += d.files.length;
669
- }
670
- }
671
- const suppressed = totalFiles - fileLimit;
672
- return {
673
- directories: result,
674
- count: result.length,
675
- suppressed,
676
- warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
677
- };
678
- }
679
- }
637
+ JOIN nodes caller ON e.source_id = caller.id
638
+ JOIN nodes target ON e.target_id = target.id
639
+ WHERE e.kind = 'calls' AND caller.file != target.file
640
+ AND target.file IN (${placeholders})`,
641
+ )
642
+ .all(...allAffectedFiles) as { target_id: number }[]
643
+ ).map((r) => r.target_id),
644
+ );
680
645
 
681
- const base = { directories: result, count: result.length };
682
- return paginateResult(base, 'directories', { limit: opts.limit, offset: opts.offset });
683
- } finally {
684
- db.close();
646
+ // 4. Production fan-in for affected nodes only
647
+ const prodFanInMap = new Map<number, number>();
648
+ const prodRows = db
649
+ .prepare(
650
+ `SELECT e.target_id, COUNT(*) AS cnt
651
+ FROM edges e
652
+ JOIN nodes caller ON e.source_id = caller.id
653
+ JOIN nodes target ON e.target_id = target.id
654
+ WHERE e.kind = 'calls'
655
+ AND target.file IN (${placeholders})
656
+ ${testFilterSQL('caller.file')}
657
+ GROUP BY e.target_id`,
658
+ )
659
+ .all(...allAffectedFiles) as { target_id: number; cnt: number }[];
660
+ for (const r of prodRows) {
661
+ prodFanInMap.set(r.target_id, r.cnt);
685
662
  }
686
- }
687
-
688
- interface HotspotRow {
689
- name: string;
690
- kind: string;
691
- line_count: number | null;
692
- symbol_count: number | null;
693
- import_count: number | null;
694
- export_count: number | null;
695
- fan_in: number | null;
696
- fan_out: number | null;
697
- cohesion: number | null;
698
- file_count: number | null;
699
- }
700
663
 
701
- interface HotspotsDataOpts {
702
- metric?: string;
703
- level?: string;
704
- limit?: number;
705
- offset?: number;
706
- noTests?: boolean;
707
- }
664
+ // 5. Classify affected nodes using global medians
665
+ const classifierInput = rows.map((r) => ({
666
+ id: String(r.id),
667
+ name: r.name,
668
+ kind: r.kind,
669
+ file: r.file,
670
+ fanIn: r.fan_in,
671
+ fanOut: r.fan_out,
672
+ isExported: exportedIds.has(r.id),
673
+ productionFanIn: prodFanInMap.get(r.id) || 0,
674
+ }));
708
675
 
709
- export function hotspotsData(
710
- customDbPath?: string,
711
- opts: HotspotsDataOpts = {},
712
- ): {
713
- metric: string;
714
- level: string;
715
- limit: number;
716
- hotspots: unknown[];
717
- } {
718
- const db = openReadonlyOrFail(customDbPath);
719
- try {
720
- const metric = opts.metric || 'fan-in';
721
- const level = opts.level || 'file';
722
- const limit = opts.limit || 10;
723
- const noTests = opts.noTests || false;
724
-
725
- const kind = level === 'directory' ? 'directory' : 'file';
726
-
727
- const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
728
-
729
- const HOTSPOT_QUERIES: Record<string, { all(...params: unknown[]): HotspotRow[] }> = {
730
- 'fan-in': db.prepare(`
731
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
732
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
733
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
734
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
735
- 'fan-out': db.prepare(`
736
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
737
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
738
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
739
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
740
- density: db.prepare(`
741
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
742
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
743
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
744
- WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
745
- coupling: db.prepare(`
746
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
747
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
748
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
749
- WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
750
- };
751
-
752
- const stmt = HOTSPOT_QUERIES[metric] ?? HOTSPOT_QUERIES['fan-in'];
753
- // stmt is always defined: metric is a valid key or the fallback is a concrete property
754
- const rows = stmt!.all(kind, limit);
755
-
756
- const hotspots = rows.map((r) => ({
757
- name: r.name,
758
- kind: r.kind,
759
- lineCount: r.line_count,
760
- symbolCount: r.symbol_count,
761
- importCount: r.import_count,
762
- exportCount: r.export_count,
763
- fanIn: r.fan_in,
764
- fanOut: r.fan_out,
765
- cohesion: r.cohesion,
766
- fileCount: r.file_count,
767
- density:
768
- (r.file_count ?? 0) > 0
769
- ? (r.symbol_count || 0) / (r.file_count ?? 1)
770
- : (r.line_count ?? 0) > 0
771
- ? (r.symbol_count || 0) / (r.line_count ?? 1)
772
- : 0,
773
- coupling: (r.fan_in || 0) + (r.fan_out || 0),
774
- }));
775
-
776
- const base = { metric, level, limit, hotspots };
777
- return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
778
- } finally {
779
- db.close();
780
- }
781
- }
676
+ const roleMap = classifyRoles(classifierInput, globalMedians);
782
677
 
783
- interface ModuleBoundariesOpts {
784
- threshold?: number;
785
- config?: CodegraphConfig;
786
- }
678
+ // 6. Build summary (only for affected nodes) and update only those nodes
679
+ const summary: RoleSummary = { ...emptySummary };
680
+ const idsByRole = new Map<string, number[]>();
787
681
 
788
- export function moduleBoundariesData(
789
- customDbPath?: string,
790
- opts: ModuleBoundariesOpts = {},
791
- ): {
792
- threshold: number;
793
- modules: {
794
- directory: string;
795
- cohesion: number | null;
796
- fileCount: number;
797
- symbolCount: number;
798
- fanIn: number;
799
- fanOut: number;
800
- files: string[];
801
- }[];
802
- count: number;
803
- } {
804
- const db = openReadonlyOrFail(customDbPath);
805
- try {
806
- const config = opts.config || loadConfig();
807
- const threshold =
808
- opts.threshold ??
809
- (config as unknown as { structure?: { cohesionThreshold?: number } }).structure
810
- ?.cohesionThreshold ??
811
- 0.3;
812
-
813
- const dirs = db
814
- .prepare(`
815
- SELECT n.id, n.name, nm.symbol_count, nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
816
- FROM nodes n
817
- JOIN node_metrics nm ON n.id = nm.node_id
818
- WHERE n.kind = 'directory' AND nm.cohesion IS NOT NULL AND nm.cohesion >= ?
819
- ORDER BY nm.cohesion DESC
820
- `)
821
- .all(threshold) as {
822
- id: number;
823
- name: string;
824
- symbol_count: number | null;
825
- fan_in: number | null;
826
- fan_out: number | null;
827
- cohesion: number | null;
828
- file_count: number | null;
829
- }[];
830
-
831
- const modules = dirs.map((d) => {
832
- // Get files inside this directory
833
- const files = (
834
- db
835
- .prepare(`
836
- SELECT n.name FROM edges e
837
- JOIN nodes n ON e.target_id = n.id
838
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
839
- `)
840
- .all(d.id) as { name: string }[]
841
- ).map((f) => f.name);
842
-
843
- return {
844
- directory: d.name,
845
- cohesion: d.cohesion,
846
- fileCount: d.file_count || 0,
847
- symbolCount: d.symbol_count || 0,
848
- fanIn: d.fan_in || 0,
849
- fanOut: d.fan_out || 0,
850
- files,
851
- };
852
- });
853
-
854
- return { threshold, modules, count: modules.length };
855
- } finally {
856
- db.close();
682
+ // Leaf kinds are always dead-leaf -- skip classifier
683
+ if (leafRows.length > 0) {
684
+ const leafIds: number[] = [];
685
+ for (const row of leafRows) leafIds.push(row.id);
686
+ idsByRole.set('dead-leaf', leafIds);
687
+ summary.dead += leafRows.length;
688
+ summary['dead-leaf'] += leafRows.length;
857
689
  }
858
- }
859
690
 
860
- // ─── Helpers ──────────────────────────────────────────────────────────
861
-
862
- function getSortFn(sortBy: string): (a: DirRow, b: DirRow) => number {
863
- switch (sortBy) {
864
- case 'cohesion':
865
- return (a, b) => (b.cohesion ?? -1) - (a.cohesion ?? -1);
866
- case 'fan-in':
867
- return (a, b) => (b.fan_in || 0) - (a.fan_in || 0);
868
- case 'fan-out':
869
- return (a, b) => (b.fan_out || 0) - (a.fan_out || 0);
870
- case 'density':
871
- return (a, b) => {
872
- const da = (a.file_count ?? 0) > 0 ? (a.symbol_count || 0) / (a.file_count ?? 1) : 0;
873
- const db_ = (b.file_count ?? 0) > 0 ? (b.symbol_count || 0) / (b.file_count ?? 1) : 0;
874
- return db_ - da;
875
- };
876
- default:
877
- return (a, b) => a.name.localeCompare(b.name);
691
+ for (const row of rows) {
692
+ const role = roleMap.get(String(row.id)) || 'leaf';
693
+ if (role.startsWith('dead')) summary.dead++;
694
+ summary[role] = (summary[role] || 0) + 1;
695
+ let ids = idsByRole.get(role);
696
+ if (!ids) {
697
+ ids = [];
698
+ idsByRole.set(role, ids);
699
+ }
700
+ ids.push(row.id);
878
701
  }
702
+
703
+ // Only update affected nodes — no global NULL reset
704
+ const ROLE_CHUNK = 500;
705
+ const roleStmtCache = new Map<number, SqliteStatement>();
706
+ db.transaction(() => {
707
+ // Reset roles only for affected files' nodes
708
+ db.prepare(
709
+ `UPDATE nodes SET role = NULL WHERE file IN (${placeholders}) AND kind NOT IN ('file', 'directory')`,
710
+ ).run(...allAffectedFiles);
711
+ for (const [role, ids] of idsByRole) {
712
+ for (let i = 0; i < ids.length; i += ROLE_CHUNK) {
713
+ const end = Math.min(i + ROLE_CHUNK, ids.length);
714
+ const chunkSize = end - i;
715
+ let stmt = roleStmtCache.get(chunkSize);
716
+ if (!stmt) {
717
+ const ph = Array.from({ length: chunkSize }, () => '?').join(',');
718
+ stmt = db.prepare(`UPDATE nodes SET role = ? WHERE id IN (${ph})`);
719
+ roleStmtCache.set(chunkSize, stmt);
720
+ }
721
+ const vals: unknown[] = [role];
722
+ for (let j = i; j < end; j++) vals.push(ids[j]);
723
+ stmt.run(...vals);
724
+ }
725
+ }
726
+ })();
727
+
728
+ return summary;
879
729
  }
730
+
731
+ // ─── Query functions (re-exported from structure-query.ts) ────────────
732
+ // Split to separate query-time concerns (DB reads, sorting, pagination)
733
+ // from build-time concerns (directory insertion, metrics computation, role classification).
734
+ export { hotspotsData, moduleBoundariesData, structureData } from './structure-query.js';
@@ -126,7 +126,7 @@ export function triageData(
126
126
  const minScore = opts.minScore != null ? Number(opts.minScore) : null;
127
127
  const sort = opts.sort || 'risk';
128
128
  const config = opts.config || loadConfig();
129
- const riskConfig = ((config as unknown as Record<string, unknown>)['risk'] || {}) as {
129
+ const riskConfig = ((config as unknown as Record<string, unknown>).risk || {}) as {
130
130
  weights?: Partial<RiskWeights>;
131
131
  roleWeights?: Record<string, number>;
132
132
  defaultRoleWeight?: number;
@@ -163,7 +163,7 @@ export function triageData(
163
163
  const items = buildTriageItems(filtered, riskMetrics);
164
164
 
165
165
  const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
166
- scored.sort(SORT_FNS[sort] || SORT_FNS['risk']!);
166
+ scored.sort(SORT_FNS[sort] || SORT_FNS.risk!);
167
167
 
168
168
  const result = {
169
169
  items: scored,