@optave/codegraph 3.13.0 → 3.15.0

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 (458) hide show
  1. package/README.md +35 -34
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +38 -40
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/b2.d.ts +7 -0
  6. package/dist/ast-analysis/rules/b2.d.ts.map +1 -0
  7. package/dist/ast-analysis/rules/b2.js +240 -0
  8. package/dist/ast-analysis/rules/b2.js.map +1 -0
  9. package/dist/ast-analysis/rules/b3.d.ts +6 -0
  10. package/dist/ast-analysis/rules/b3.d.ts.map +1 -0
  11. package/dist/ast-analysis/rules/b3.js +105 -0
  12. package/dist/ast-analysis/rules/b3.js.map +1 -0
  13. package/dist/ast-analysis/rules/b4.d.ts +9 -0
  14. package/dist/ast-analysis/rules/b4.d.ts.map +1 -0
  15. package/dist/ast-analysis/rules/b4.js +361 -0
  16. package/dist/ast-analysis/rules/b4.js.map +1 -0
  17. package/dist/ast-analysis/rules/b5.d.ts +4 -0
  18. package/dist/ast-analysis/rules/b5.d.ts.map +1 -0
  19. package/dist/ast-analysis/rules/b5.js +52 -0
  20. package/dist/ast-analysis/rules/b5.js.map +1 -0
  21. package/dist/ast-analysis/rules/c.d.ts +4 -0
  22. package/dist/ast-analysis/rules/c.d.ts.map +1 -0
  23. package/dist/ast-analysis/rules/c.js +143 -0
  24. package/dist/ast-analysis/rules/c.js.map +1 -0
  25. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  26. package/dist/ast-analysis/rules/index.js +34 -0
  27. package/dist/ast-analysis/rules/index.js.map +1 -1
  28. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  29. package/dist/ast-analysis/rules/javascript.js +3 -0
  30. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  31. package/dist/ast-analysis/shared.d.ts.map +1 -1
  32. package/dist/ast-analysis/shared.js +2 -0
  33. package/dist/ast-analysis/shared.js.map +1 -1
  34. package/dist/ast-analysis/visitor-utils.d.ts +1 -0
  35. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  36. package/dist/ast-analysis/visitor-utils.js +5 -0
  37. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  38. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  39. package/dist/ast-analysis/visitor.js +60 -47
  40. package/dist/ast-analysis/visitor.js.map +1 -1
  41. package/dist/ast-analysis/visitors/cfg-visitor.d.ts.map +1 -1
  42. package/dist/ast-analysis/visitors/cfg-visitor.js +126 -76
  43. package/dist/ast-analysis/visitors/cfg-visitor.js.map +1 -1
  44. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  45. package/dist/ast-analysis/visitors/complexity-visitor.js +27 -15
  46. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  47. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  48. package/dist/ast-analysis/visitors/dataflow-visitor.js +54 -21
  49. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  50. package/dist/cli/commands/config.d.ts.map +1 -1
  51. package/dist/cli/commands/config.js +137 -134
  52. package/dist/cli/commands/config.js.map +1 -1
  53. package/dist/cli/commands/roles.d.ts.map +1 -1
  54. package/dist/cli/commands/roles.js +6 -1
  55. package/dist/cli/commands/roles.js.map +1 -1
  56. package/dist/db/better-sqlite3.d.ts +2 -1
  57. package/dist/db/better-sqlite3.d.ts.map +1 -1
  58. package/dist/db/better-sqlite3.js.map +1 -1
  59. package/dist/db/connection.d.ts +7 -1
  60. package/dist/db/connection.d.ts.map +1 -1
  61. package/dist/db/connection.js +20 -5
  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 +68 -0
  69. package/dist/db/migrations.js.map +1 -1
  70. package/dist/db/repository/build-stmts.d.ts.map +1 -1
  71. package/dist/db/repository/build-stmts.js +18 -0
  72. package/dist/db/repository/build-stmts.js.map +1 -1
  73. package/dist/db/repository/dataflow.d.ts +5 -0
  74. package/dist/db/repository/dataflow.d.ts.map +1 -1
  75. package/dist/db/repository/dataflow.js +14 -0
  76. package/dist/db/repository/dataflow.js.map +1 -1
  77. package/dist/db/repository/index.d.ts +1 -1
  78. package/dist/db/repository/index.d.ts.map +1 -1
  79. package/dist/db/repository/index.js +1 -1
  80. package/dist/db/repository/index.js.map +1 -1
  81. package/dist/db/repository/native-repository.d.ts.map +1 -1
  82. package/dist/db/repository/native-repository.js +47 -34
  83. package/dist/db/repository/native-repository.js.map +1 -1
  84. package/dist/domain/analysis/context.d.ts +2 -2
  85. package/dist/domain/analysis/dependencies.d.ts +2 -2
  86. package/dist/domain/analysis/diff-impact.d.ts +2 -2
  87. package/dist/domain/analysis/fn-impact.d.ts +3 -1
  88. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  89. package/dist/domain/analysis/fn-impact.js +4 -0
  90. package/dist/domain/analysis/fn-impact.js.map +1 -1
  91. package/dist/domain/analysis/implementations.d.ts +2 -2
  92. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  93. package/dist/domain/analysis/module-map.js +32 -5
  94. package/dist/domain/analysis/module-map.js.map +1 -1
  95. package/dist/domain/analysis/roles.d.ts +7 -1
  96. package/dist/domain/analysis/roles.d.ts.map +1 -1
  97. package/dist/domain/analysis/roles.js +16 -0
  98. package/dist/domain/analysis/roles.js.map +1 -1
  99. package/dist/domain/analysis/symbol-lookup.d.ts +4 -4
  100. package/dist/domain/graph/builder/call-resolver.d.ts +17 -5
  101. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  102. package/dist/domain/graph/builder/call-resolver.js +85 -220
  103. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  104. package/dist/domain/graph/builder/context.d.ts +1 -0
  105. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  106. package/dist/domain/graph/builder/context.js.map +1 -1
  107. package/dist/domain/graph/builder/helpers.d.ts +16 -1
  108. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  109. package/dist/domain/graph/builder/helpers.js +162 -72
  110. package/dist/domain/graph/builder/helpers.js.map +1 -1
  111. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  112. package/dist/domain/graph/builder/incremental.js +166 -97
  113. package/dist/domain/graph/builder/incremental.js.map +1 -1
  114. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  115. package/dist/domain/graph/builder/pipeline.js +10 -4
  116. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  117. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  118. package/dist/domain/graph/builder/stages/build-edges.js +496 -250
  119. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  120. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  121. package/dist/domain/graph/builder/stages/collect-files.js +10 -7
  122. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  123. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  124. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  125. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  126. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  127. package/dist/domain/graph/builder/stages/native-orchestrator.js +895 -545
  128. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  129. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -1
  130. package/dist/domain/graph/resolver/points-to.js +105 -57
  131. package/dist/domain/graph/resolver/points-to.js.map +1 -1
  132. package/dist/domain/graph/resolver/strategy.d.ts +61 -0
  133. package/dist/domain/graph/resolver/strategy.d.ts.map +1 -0
  134. package/dist/domain/graph/resolver/strategy.js +222 -0
  135. package/dist/domain/graph/resolver/strategy.js.map +1 -0
  136. package/dist/domain/graph/watcher.d.ts.map +1 -1
  137. package/dist/domain/graph/watcher.js +16 -9
  138. package/dist/domain/graph/watcher.js.map +1 -1
  139. package/dist/domain/parser.d.ts +12 -0
  140. package/dist/domain/parser.d.ts.map +1 -1
  141. package/dist/domain/parser.js +12 -2
  142. package/dist/domain/parser.js.map +1 -1
  143. package/dist/domain/queries.d.ts +1 -1
  144. package/dist/domain/queries.d.ts.map +1 -1
  145. package/dist/domain/queries.js +1 -1
  146. package/dist/domain/queries.js.map +1 -1
  147. package/dist/domain/wasm-worker-entry.js +3 -0
  148. package/dist/domain/wasm-worker-entry.js.map +1 -1
  149. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  150. package/dist/domain/wasm-worker-pool.js +24 -5
  151. package/dist/domain/wasm-worker-pool.js.map +1 -1
  152. package/dist/domain/wasm-worker-protocol.d.ts +7 -0
  153. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  154. package/dist/extractors/dart.js +48 -3
  155. package/dist/extractors/dart.js.map +1 -1
  156. package/dist/extractors/groovy.js +62 -3
  157. package/dist/extractors/groovy.js.map +1 -1
  158. package/dist/extractors/helpers.d.ts +4 -2
  159. package/dist/extractors/helpers.d.ts.map +1 -1
  160. package/dist/extractors/helpers.js +5 -1
  161. package/dist/extractors/helpers.js.map +1 -1
  162. package/dist/extractors/java.js +77 -1
  163. package/dist/extractors/java.js.map +1 -1
  164. package/dist/extractors/javascript.d.ts.map +1 -1
  165. package/dist/extractors/javascript.js +549 -163
  166. package/dist/extractors/javascript.js.map +1 -1
  167. package/dist/extractors/kotlin.js +58 -3
  168. package/dist/extractors/kotlin.js.map +1 -1
  169. package/dist/extractors/objc.js +25 -2
  170. package/dist/extractors/objc.js.map +1 -1
  171. package/dist/extractors/scala.js +62 -2
  172. package/dist/extractors/scala.js.map +1 -1
  173. package/dist/extractors/swift.js +52 -3
  174. package/dist/extractors/swift.js.map +1 -1
  175. package/dist/features/audit.js +26 -23
  176. package/dist/features/audit.js.map +1 -1
  177. package/dist/features/boundaries.d.ts.map +1 -1
  178. package/dist/features/boundaries.js +12 -9
  179. package/dist/features/boundaries.js.map +1 -1
  180. package/dist/features/cfg.d.ts.map +1 -1
  181. package/dist/features/cfg.js +25 -18
  182. package/dist/features/cfg.js.map +1 -1
  183. package/dist/features/check.d.ts.map +1 -1
  184. package/dist/features/check.js +18 -5
  185. package/dist/features/check.js.map +1 -1
  186. package/dist/features/communities.d.ts +4 -2
  187. package/dist/features/communities.d.ts.map +1 -1
  188. package/dist/features/communities.js +6 -4
  189. package/dist/features/communities.js.map +1 -1
  190. package/dist/features/dataflow.d.ts +60 -0
  191. package/dist/features/dataflow.d.ts.map +1 -1
  192. package/dist/features/dataflow.js +530 -6
  193. package/dist/features/dataflow.js.map +1 -1
  194. package/dist/features/manifesto.d.ts.map +1 -1
  195. package/dist/features/manifesto.js +59 -72
  196. package/dist/features/manifesto.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +27 -22
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/snapshot.d.ts.map +1 -1
  201. package/dist/features/snapshot.js +36 -28
  202. package/dist/features/snapshot.js.map +1 -1
  203. package/dist/features/structure.d.ts.map +1 -1
  204. package/dist/features/structure.js +150 -62
  205. package/dist/features/structure.js.map +1 -1
  206. package/dist/features/triage.d.ts.map +1 -1
  207. package/dist/features/triage.js +18 -11
  208. package/dist/features/triage.js.map +1 -1
  209. package/dist/graph/algorithms/bfs.d.ts +1 -1
  210. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  211. package/dist/graph/algorithms/bfs.js +14 -13
  212. package/dist/graph/algorithms/bfs.js.map +1 -1
  213. package/dist/graph/algorithms/tarjan.d.ts.map +1 -1
  214. package/dist/graph/algorithms/tarjan.js +5 -0
  215. package/dist/graph/algorithms/tarjan.js.map +1 -1
  216. package/dist/graph/builders/dependency.js +28 -22
  217. package/dist/graph/builders/dependency.js.map +1 -1
  218. package/dist/graph/classifiers/roles.d.ts +10 -1
  219. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  220. package/dist/graph/classifiers/roles.js +60 -6
  221. package/dist/graph/classifiers/roles.js.map +1 -1
  222. package/dist/infrastructure/config.d.ts +10 -0
  223. package/dist/infrastructure/config.d.ts.map +1 -1
  224. package/dist/infrastructure/config.js +31 -3
  225. package/dist/infrastructure/config.js.map +1 -1
  226. package/dist/infrastructure/registry.d.ts +0 -7
  227. package/dist/infrastructure/registry.d.ts.map +1 -1
  228. package/dist/infrastructure/registry.js +29 -13
  229. package/dist/infrastructure/registry.js.map +1 -1
  230. package/dist/infrastructure/update-check.d.ts.map +1 -1
  231. package/dist/infrastructure/update-check.js +49 -31
  232. package/dist/infrastructure/update-check.js.map +1 -1
  233. package/dist/mcp/server.d.ts +2 -10
  234. package/dist/mcp/server.d.ts.map +1 -1
  235. package/dist/mcp/server.js.map +1 -1
  236. package/dist/mcp/tools/ast-query.d.ts +1 -1
  237. package/dist/mcp/tools/ast-query.d.ts.map +1 -1
  238. package/dist/mcp/tools/audit.d.ts +1 -1
  239. package/dist/mcp/tools/audit.d.ts.map +1 -1
  240. package/dist/mcp/tools/batch-query.d.ts +1 -1
  241. package/dist/mcp/tools/batch-query.d.ts.map +1 -1
  242. package/dist/mcp/tools/branch-compare.d.ts +1 -1
  243. package/dist/mcp/tools/branch-compare.d.ts.map +1 -1
  244. package/dist/mcp/tools/brief.d.ts +1 -1
  245. package/dist/mcp/tools/brief.d.ts.map +1 -1
  246. package/dist/mcp/tools/cfg.d.ts +1 -1
  247. package/dist/mcp/tools/cfg.d.ts.map +1 -1
  248. package/dist/mcp/tools/check.d.ts +1 -1
  249. package/dist/mcp/tools/check.d.ts.map +1 -1
  250. package/dist/mcp/tools/co-changes.d.ts +1 -1
  251. package/dist/mcp/tools/co-changes.d.ts.map +1 -1
  252. package/dist/mcp/tools/code-owners.d.ts +1 -1
  253. package/dist/mcp/tools/code-owners.d.ts.map +1 -1
  254. package/dist/mcp/tools/communities.d.ts +1 -1
  255. package/dist/mcp/tools/communities.d.ts.map +1 -1
  256. package/dist/mcp/tools/complexity.d.ts +1 -1
  257. package/dist/mcp/tools/complexity.d.ts.map +1 -1
  258. package/dist/mcp/tools/context.d.ts +1 -1
  259. package/dist/mcp/tools/context.d.ts.map +1 -1
  260. package/dist/mcp/tools/dataflow.d.ts +1 -1
  261. package/dist/mcp/tools/dataflow.d.ts.map +1 -1
  262. package/dist/mcp/tools/diff-impact.d.ts +1 -1
  263. package/dist/mcp/tools/diff-impact.d.ts.map +1 -1
  264. package/dist/mcp/tools/execution-flow.d.ts +1 -1
  265. package/dist/mcp/tools/execution-flow.d.ts.map +1 -1
  266. package/dist/mcp/tools/export-graph.d.ts +1 -1
  267. package/dist/mcp/tools/export-graph.d.ts.map +1 -1
  268. package/dist/mcp/tools/file-deps.d.ts +1 -1
  269. package/dist/mcp/tools/file-deps.d.ts.map +1 -1
  270. package/dist/mcp/tools/file-exports.d.ts +1 -1
  271. package/dist/mcp/tools/file-exports.d.ts.map +1 -1
  272. package/dist/mcp/tools/find-cycles.d.ts +1 -1
  273. package/dist/mcp/tools/find-cycles.d.ts.map +1 -1
  274. package/dist/mcp/tools/fn-impact.d.ts +1 -1
  275. package/dist/mcp/tools/fn-impact.d.ts.map +1 -1
  276. package/dist/mcp/tools/impact-analysis.d.ts +1 -1
  277. package/dist/mcp/tools/impact-analysis.d.ts.map +1 -1
  278. package/dist/mcp/tools/implementations.d.ts +1 -1
  279. package/dist/mcp/tools/implementations.d.ts.map +1 -1
  280. package/dist/mcp/tools/index.d.ts +2 -5
  281. package/dist/mcp/tools/index.d.ts.map +1 -1
  282. package/dist/mcp/tools/index.js.map +1 -1
  283. package/dist/mcp/tools/interfaces.d.ts +1 -1
  284. package/dist/mcp/tools/interfaces.d.ts.map +1 -1
  285. package/dist/mcp/tools/list-functions.d.ts +1 -1
  286. package/dist/mcp/tools/list-functions.d.ts.map +1 -1
  287. package/dist/mcp/tools/list-repos.d.ts +1 -1
  288. package/dist/mcp/tools/list-repos.d.ts.map +1 -1
  289. package/dist/mcp/tools/module-map.d.ts +1 -1
  290. package/dist/mcp/tools/module-map.d.ts.map +1 -1
  291. package/dist/mcp/tools/node-roles.d.ts +1 -1
  292. package/dist/mcp/tools/node-roles.d.ts.map +1 -1
  293. package/dist/mcp/tools/path.d.ts +1 -1
  294. package/dist/mcp/tools/path.d.ts.map +1 -1
  295. package/dist/mcp/tools/query.d.ts +1 -1
  296. package/dist/mcp/tools/query.d.ts.map +1 -1
  297. package/dist/mcp/tools/semantic-search.d.ts +1 -1
  298. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  299. package/dist/mcp/tools/sequence.d.ts +1 -1
  300. package/dist/mcp/tools/sequence.d.ts.map +1 -1
  301. package/dist/mcp/tools/structure.d.ts +1 -1
  302. package/dist/mcp/tools/structure.d.ts.map +1 -1
  303. package/dist/mcp/tools/symbol-children.d.ts +1 -1
  304. package/dist/mcp/tools/symbol-children.d.ts.map +1 -1
  305. package/dist/mcp/tools/triage.d.ts +1 -1
  306. package/dist/mcp/tools/triage.d.ts.map +1 -1
  307. package/dist/mcp/tools/where.d.ts +1 -1
  308. package/dist/mcp/tools/where.d.ts.map +1 -1
  309. package/dist/mcp/types.d.ts +19 -0
  310. package/dist/mcp/types.d.ts.map +1 -0
  311. package/dist/mcp/types.js +6 -0
  312. package/dist/mcp/types.js.map +1 -0
  313. package/dist/presentation/queries-cli/index.d.ts +1 -1
  314. package/dist/presentation/queries-cli/index.d.ts.map +1 -1
  315. package/dist/presentation/queries-cli/index.js +1 -1
  316. package/dist/presentation/queries-cli/index.js.map +1 -1
  317. package/dist/presentation/queries-cli/overview.d.ts +1 -0
  318. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  319. package/dist/presentation/queries-cli/overview.js +20 -1
  320. package/dist/presentation/queries-cli/overview.js.map +1 -1
  321. package/dist/presentation/queries-cli.d.ts +1 -1
  322. package/dist/presentation/queries-cli.d.ts.map +1 -1
  323. package/dist/presentation/queries-cli.js +1 -1
  324. package/dist/presentation/queries-cli.js.map +1 -1
  325. package/dist/presentation/viewer.d.ts.map +1 -1
  326. package/dist/presentation/viewer.js +45 -32
  327. package/dist/presentation/viewer.js.map +1 -1
  328. package/dist/shared/constants.d.ts +21 -0
  329. package/dist/shared/constants.d.ts.map +1 -1
  330. package/dist/shared/constants.js +25 -0
  331. package/dist/shared/constants.js.map +1 -1
  332. package/dist/shared/normalize.d.ts.map +1 -1
  333. package/dist/shared/normalize.js +12 -22
  334. package/dist/shared/normalize.js.map +1 -1
  335. package/dist/shared/paginate.d.ts +4 -17
  336. package/dist/shared/paginate.d.ts.map +1 -1
  337. package/dist/shared/paginate.js.map +1 -1
  338. package/dist/types.d.ts +76 -1
  339. package/dist/types.d.ts.map +1 -1
  340. package/grammars/tree-sitter-erlang.wasm +0 -0
  341. package/package.json +7 -7
  342. package/src/ast-analysis/engine.ts +43 -63
  343. package/src/ast-analysis/rules/b2.ts +263 -0
  344. package/src/ast-analysis/rules/b3.ts +127 -0
  345. package/src/ast-analysis/rules/b4.ts +378 -0
  346. package/src/ast-analysis/rules/b5.ts +65 -0
  347. package/src/ast-analysis/rules/c.ts +157 -0
  348. package/src/ast-analysis/rules/index.ts +34 -0
  349. package/src/ast-analysis/rules/javascript.ts +3 -0
  350. package/src/ast-analysis/shared.ts +2 -0
  351. package/src/ast-analysis/visitor-utils.ts +5 -0
  352. package/src/ast-analysis/visitor.ts +82 -52
  353. package/src/ast-analysis/visitors/cfg-visitor.ts +198 -84
  354. package/src/ast-analysis/visitors/complexity-visitor.ts +44 -16
  355. package/src/ast-analysis/visitors/dataflow-visitor.ts +68 -29
  356. package/src/cli/commands/config.ts +184 -184
  357. package/src/cli/commands/roles.ts +6 -1
  358. package/src/db/better-sqlite3.ts +5 -4
  359. package/src/db/connection.ts +23 -5
  360. package/src/db/index.ts +1 -0
  361. package/src/db/migrations.ts +68 -0
  362. package/src/db/repository/build-stmts.ts +30 -0
  363. package/src/db/repository/dataflow.ts +16 -0
  364. package/src/db/repository/index.ts +1 -1
  365. package/src/db/repository/native-repository.ts +56 -40
  366. package/src/domain/analysis/fn-impact.ts +4 -0
  367. package/src/domain/analysis/module-map.ts +38 -6
  368. package/src/domain/analysis/roles.ts +23 -0
  369. package/src/domain/graph/builder/call-resolver.ts +112 -232
  370. package/src/domain/graph/builder/context.ts +1 -0
  371. package/src/domain/graph/builder/helpers.ts +190 -72
  372. package/src/domain/graph/builder/incremental.ts +249 -120
  373. package/src/domain/graph/builder/pipeline.ts +11 -5
  374. package/src/domain/graph/builder/stages/build-edges.ts +696 -296
  375. package/src/domain/graph/builder/stages/collect-files.ts +12 -6
  376. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  377. package/src/domain/graph/builder/stages/native-orchestrator.ts +1102 -590
  378. package/src/domain/graph/resolver/points-to.ts +182 -59
  379. package/src/domain/graph/resolver/strategy.ts +265 -0
  380. package/src/domain/graph/watcher.ts +19 -9
  381. package/src/domain/parser.ts +12 -2
  382. package/src/domain/queries.ts +1 -1
  383. package/src/domain/wasm-worker-entry.ts +3 -0
  384. package/src/domain/wasm-worker-pool.ts +28 -4
  385. package/src/domain/wasm-worker-protocol.ts +4 -0
  386. package/src/extractors/dart.ts +48 -3
  387. package/src/extractors/groovy.ts +62 -2
  388. package/src/extractors/helpers.ts +5 -2
  389. package/src/extractors/java.ts +80 -1
  390. package/src/extractors/javascript.ts +566 -161
  391. package/src/extractors/kotlin.ts +57 -3
  392. package/src/extractors/objc.ts +25 -1
  393. package/src/extractors/scala.ts +63 -1
  394. package/src/extractors/swift.ts +46 -3
  395. package/src/features/audit.ts +43 -34
  396. package/src/features/boundaries.ts +17 -9
  397. package/src/features/cfg.ts +31 -22
  398. package/src/features/check.ts +21 -5
  399. package/src/features/communities.ts +28 -19
  400. package/src/features/dataflow.ts +755 -6
  401. package/src/features/manifesto.ts +76 -75
  402. package/src/features/sequence.ts +29 -23
  403. package/src/features/snapshot.ts +36 -25
  404. package/src/features/structure.ts +185 -55
  405. package/src/features/triage.ts +28 -15
  406. package/src/graph/algorithms/bfs.ts +13 -12
  407. package/src/graph/algorithms/tarjan.ts +5 -0
  408. package/src/graph/builders/dependency.ts +35 -23
  409. package/src/graph/classifiers/roles.ts +74 -7
  410. package/src/infrastructure/config.ts +32 -3
  411. package/src/infrastructure/registry.ts +44 -20
  412. package/src/infrastructure/update-check.ts +55 -33
  413. package/src/mcp/server.ts +2 -8
  414. package/src/mcp/tools/ast-query.ts +1 -1
  415. package/src/mcp/tools/audit.ts +1 -1
  416. package/src/mcp/tools/batch-query.ts +1 -1
  417. package/src/mcp/tools/branch-compare.ts +1 -1
  418. package/src/mcp/tools/brief.ts +1 -1
  419. package/src/mcp/tools/cfg.ts +1 -1
  420. package/src/mcp/tools/check.ts +1 -1
  421. package/src/mcp/tools/co-changes.ts +1 -1
  422. package/src/mcp/tools/code-owners.ts +1 -1
  423. package/src/mcp/tools/communities.ts +1 -1
  424. package/src/mcp/tools/complexity.ts +1 -1
  425. package/src/mcp/tools/context.ts +1 -1
  426. package/src/mcp/tools/dataflow.ts +1 -1
  427. package/src/mcp/tools/diff-impact.ts +1 -1
  428. package/src/mcp/tools/execution-flow.ts +1 -1
  429. package/src/mcp/tools/export-graph.ts +1 -1
  430. package/src/mcp/tools/file-deps.ts +1 -1
  431. package/src/mcp/tools/file-exports.ts +1 -1
  432. package/src/mcp/tools/find-cycles.ts +1 -1
  433. package/src/mcp/tools/fn-impact.ts +1 -1
  434. package/src/mcp/tools/impact-analysis.ts +1 -1
  435. package/src/mcp/tools/implementations.ts +1 -1
  436. package/src/mcp/tools/index.ts +2 -5
  437. package/src/mcp/tools/interfaces.ts +1 -1
  438. package/src/mcp/tools/list-functions.ts +1 -1
  439. package/src/mcp/tools/list-repos.ts +1 -1
  440. package/src/mcp/tools/module-map.ts +1 -1
  441. package/src/mcp/tools/node-roles.ts +1 -1
  442. package/src/mcp/tools/path.ts +1 -1
  443. package/src/mcp/tools/query.ts +1 -1
  444. package/src/mcp/tools/semantic-search.ts +1 -1
  445. package/src/mcp/tools/sequence.ts +1 -1
  446. package/src/mcp/tools/structure.ts +1 -1
  447. package/src/mcp/tools/symbol-children.ts +1 -1
  448. package/src/mcp/tools/triage.ts +1 -1
  449. package/src/mcp/tools/where.ts +1 -1
  450. package/src/mcp/types.ts +21 -0
  451. package/src/presentation/queries-cli/index.ts +1 -1
  452. package/src/presentation/queries-cli/overview.ts +35 -1
  453. package/src/presentation/queries-cli.ts +1 -0
  454. package/src/presentation/viewer.ts +98 -87
  455. package/src/shared/constants.ts +26 -0
  456. package/src/shared/normalize.ts +13 -22
  457. package/src/shared/paginate.ts +4 -18
  458. package/src/types.ts +86 -1
@@ -157,17 +157,27 @@ interface ManifestoOpts {
157
157
  offset?: number;
158
158
  }
159
159
 
160
- function evaluateFunctionRules(
161
- db: BetterSqlite3Database,
160
+ interface EvaluateRulesContext {
161
+ defs: RuleDef[];
162
+ rows: Array<Record<string, unknown>>;
163
+ }
164
+
165
+ /**
166
+ * Shared rule-evaluation loop used by both evaluateFunctionRules and evaluateFileRules.
167
+ * Iterates rows, applies threshold checks for each active rule definition, and
168
+ * accumulates violations + per-rule worst-status tracking into ruleResults.
169
+ */
170
+ function evaluateRules(
171
+ ctx: EvaluateRulesContext,
162
172
  rules: ResolvedRules,
163
- opts: ManifestoOpts,
164
173
  violations: Violation[],
165
174
  ruleResults: RuleResult[],
166
175
  ): void {
167
- const functionDefs = RULE_DEFS.filter((d) => d.level === 'function');
168
- const activeDefs = functionDefs.filter((d) => isEnabled(rules[d.name]!));
176
+ const { defs, rows } = ctx;
177
+ const activeDefs = defs.filter((d) => isEnabled(rules[d.name]!));
178
+
169
179
  if (activeDefs.length === 0) {
170
- for (const def of functionDefs) {
180
+ for (const def of defs) {
171
181
  ruleResults.push({
172
182
  name: def.name,
173
183
  level: def.level,
@@ -179,39 +189,9 @@ function evaluateFunctionRules(
179
189
  return;
180
190
  }
181
191
 
182
- let where = "WHERE n.kind IN ('function','method')";
183
- const params: unknown[] = [];
184
- if (opts.noTests) where += NO_TEST_SQL;
185
- {
186
- const fc = buildFileConditionSQL(opts.file as string, 'n.file');
187
- where += fc.sql;
188
- params.push(...fc.params);
189
- }
190
- if (opts.kind) {
191
- where += ' AND n.kind = ?';
192
- params.push(opts.kind);
193
- }
194
-
195
- let rows: Array<Record<string, unknown>>;
196
- try {
197
- rows = db
198
- .prepare(
199
- `SELECT n.name, n.kind, n.file, n.line,
200
- fc.cognitive, fc.cyclomatic, fc.max_nesting
201
- FROM function_complexity fc
202
- JOIN nodes n ON fc.node_id = n.id
203
- ${where}`,
204
- )
205
- .all(...params) as Array<Record<string, unknown>>;
206
- } catch (err: unknown) {
207
- debug(`manifesto function query failed: ${(err as Error).message}`);
208
- rows = [];
209
- }
210
-
211
- // Track worst status per rule
212
192
  const worst: Record<string, string> = {};
213
193
  const counts: Record<string, number> = {};
214
- for (const def of functionDefs) {
194
+ for (const def of defs) {
215
195
  worst[def.name] = 'pass';
216
196
  counts[def.name] = 0;
217
197
  }
@@ -234,7 +214,7 @@ function evaluateFunctionRules(
234
214
  }
235
215
  }
236
216
 
237
- for (const def of functionDefs) {
217
+ for (const def of defs) {
238
218
  ruleResults.push({
239
219
  name: def.name,
240
220
  level: def.level,
@@ -245,6 +225,60 @@ function evaluateFunctionRules(
245
225
  }
246
226
  }
247
227
 
228
+ function evaluateFunctionRules(
229
+ db: BetterSqlite3Database,
230
+ rules: ResolvedRules,
231
+ opts: ManifestoOpts,
232
+ violations: Violation[],
233
+ ruleResults: RuleResult[],
234
+ ): void {
235
+ const defs = RULE_DEFS.filter((d) => d.level === 'function');
236
+ const activeDefs = defs.filter((d) => isEnabled(rules[d.name]!));
237
+ if (activeDefs.length === 0) {
238
+ for (const def of defs) {
239
+ ruleResults.push({
240
+ name: def.name,
241
+ level: def.level,
242
+ status: 'pass',
243
+ thresholds: rules[def.name]!,
244
+ violationCount: 0,
245
+ });
246
+ }
247
+ return;
248
+ }
249
+
250
+ let where = "WHERE n.kind IN ('function','method')";
251
+ const params: unknown[] = [];
252
+ if (opts.noTests) where += NO_TEST_SQL;
253
+ {
254
+ const fc = buildFileConditionSQL(opts.file as string, 'n.file');
255
+ where += fc.sql;
256
+ params.push(...fc.params);
257
+ }
258
+ if (opts.kind) {
259
+ where += ' AND n.kind = ?';
260
+ params.push(opts.kind);
261
+ }
262
+
263
+ let rows: Array<Record<string, unknown>>;
264
+ try {
265
+ rows = db
266
+ .prepare(
267
+ `SELECT n.name, n.kind, n.file, n.line,
268
+ fc.cognitive, fc.cyclomatic, fc.max_nesting
269
+ FROM function_complexity fc
270
+ JOIN nodes n ON fc.node_id = n.id
271
+ ${where}`,
272
+ )
273
+ .all(...params) as Array<Record<string, unknown>>;
274
+ } catch (err: unknown) {
275
+ debug(`manifesto function query failed: ${(err as Error).message}`);
276
+ rows = [];
277
+ }
278
+
279
+ evaluateRules({ defs, rows }, rules, violations, ruleResults);
280
+ }
281
+
248
282
  function evaluateFileRules(
249
283
  db: BetterSqlite3Database,
250
284
  rules: ResolvedRules,
@@ -252,10 +286,10 @@ function evaluateFileRules(
252
286
  violations: Violation[],
253
287
  ruleResults: RuleResult[],
254
288
  ): void {
255
- const fileDefs = RULE_DEFS.filter((d) => d.level === 'file');
256
- const activeDefs = fileDefs.filter((d) => isEnabled(rules[d.name]!));
289
+ const defs = RULE_DEFS.filter((d) => d.level === 'file');
290
+ const activeDefs = defs.filter((d) => isEnabled(rules[d.name]!));
257
291
  if (activeDefs.length === 0) {
258
- for (const def of fileDefs) {
292
+ for (const def of defs) {
259
293
  ruleResults.push({
260
294
  name: def.name,
261
295
  level: def.level,
@@ -293,40 +327,7 @@ function evaluateFileRules(
293
327
  rows = [];
294
328
  }
295
329
 
296
- const worst: Record<string, string> = {};
297
- const counts: Record<string, number> = {};
298
- for (const def of fileDefs) {
299
- worst[def.name] = 'pass';
300
- counts[def.name] = 0;
301
- }
302
-
303
- for (const row of rows) {
304
- for (const def of activeDefs) {
305
- const value = row[def.metric] as number | null;
306
- if (value == null) continue;
307
- const meta = {
308
- name: row.name as string,
309
- file: row.file as string,
310
- line: row.line as number,
311
- };
312
- const status = checkThreshold(def.name, rules[def.name]!, value, meta, violations);
313
- if (status !== 'pass') {
314
- counts[def.name] = (counts[def.name] ?? 0) + 1;
315
- if (status === 'fail') worst[def.name] = 'fail';
316
- else if (worst[def.name] !== 'fail') worst[def.name] = 'warn';
317
- }
318
- }
319
- }
320
-
321
- for (const def of fileDefs) {
322
- ruleResults.push({
323
- name: def.name,
324
- level: def.level,
325
- status: worst[def.name]!,
326
- thresholds: rules[def.name]!,
327
- violationCount: counts[def.name] ?? 0,
328
- });
329
- }
330
+ evaluateRules({ defs, rows }, rules, violations, ruleResults);
330
331
  }
331
332
 
332
333
  function evaluateGraphRules(
@@ -10,6 +10,34 @@ import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
10
10
 
11
11
  // ─── Alias generation ────────────────────────────────────────────────
12
12
 
13
+ /** Build aliases for a group of paths that share the same basename.
14
+ * Progressively adds parent dirs until all aliases are unique. */
15
+ function resolveCollisionAliases(paths: string[], aliases: Map<string, string>): void {
16
+ for (let depth = 2; depth <= 10; depth++) {
17
+ const trial = new Map<string, string>();
18
+ let allUnique = true;
19
+ const seen = new Set<string>();
20
+
21
+ for (const p of paths) {
22
+ const parts = p.replace(/\.[^.]+$/, '').split('/');
23
+ const alias = parts
24
+ .slice(-depth)
25
+ .join('_')
26
+ .replace(/[^a-zA-Z0-9_-]/g, '_');
27
+ trial.set(p, alias);
28
+ if (seen.has(alias)) allUnique = false;
29
+ seen.add(alias);
30
+ }
31
+
32
+ if (allUnique || depth === 10) {
33
+ for (const [p, alias] of trial) {
34
+ aliases.set(p, alias);
35
+ }
36
+ break;
37
+ }
38
+ }
39
+ }
40
+
13
41
  function buildAliases(files: string[]): Map<string, string> {
14
42
  const aliases = new Map<string, string>();
15
43
  const basenames = new Map<string, string[]>();
@@ -26,29 +54,7 @@ function buildAliases(files: string[]): Map<string, string> {
26
54
  aliases.set(paths[0]!, base);
27
55
  } else {
28
56
  // Collision — progressively add parent dirs until aliases are unique
29
- for (let depth = 2; depth <= 10; depth++) {
30
- const trial = new Map<string, string>();
31
- let allUnique = true;
32
- const seen = new Set<string>();
33
-
34
- for (const p of paths) {
35
- const parts = p.replace(/\.[^.]+$/, '').split('/');
36
- const alias = parts
37
- .slice(-depth)
38
- .join('_')
39
- .replace(/[^a-zA-Z0-9_-]/g, '_');
40
- trial.set(p, alias);
41
- if (seen.has(alias)) allUnique = false;
42
- seen.add(alias);
43
- }
44
-
45
- if (allUnique || depth === 10) {
46
- for (const [p, alias] of trial) {
47
- aliases.set(p, alias);
48
- }
49
- break;
50
- }
51
- }
57
+ resolveCollisionAliases(paths, aliases);
52
58
  }
53
59
  }
54
60
 
@@ -25,6 +25,41 @@ interface SnapshotSaveOptions {
25
25
  force?: boolean;
26
26
  }
27
27
 
28
+ /**
29
+ * Atomically place `tmp` at `dest`.
30
+ * - force=true: renameSync (overwrites any existing file).
31
+ * - force=false: linkSync so EEXIST is detected atomically; then unlink tmp.
32
+ * Callers are responsible for cleaning up `tmp` on any thrown error.
33
+ */
34
+ function atomicPlaceFile(tmp: string, dest: string, name: string, force?: boolean): void {
35
+ if (force) {
36
+ // renameSync overwrites atomically — the correct semantics for --force.
37
+ fs.renameSync(tmp, dest);
38
+ return;
39
+ }
40
+
41
+ // Non-force path: linkSync fails atomically with EEXIST if dest exists,
42
+ // closing the TOCTOU window between existsSync above and the final
43
+ // placement. We then unlink the temp file; on POSIX and NTFS, link
44
+ // creates a second reference so tmp can safely be removed.
45
+ try {
46
+ fs.linkSync(tmp, dest);
47
+ } catch (err) {
48
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
49
+ throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
50
+ }
51
+ throw err;
52
+ }
53
+ try {
54
+ fs.unlinkSync(tmp);
55
+ } catch (cleanupErr) {
56
+ // Best-effort — dest is already in place, so a leftover tmp file is
57
+ // harmless. Log at debug so repeated failures surface during
58
+ // troubleshooting without noising up normal operation.
59
+ debug(`snapshotSave: failed to remove temp file ${tmp}: ${cleanupErr}`);
60
+ }
61
+ }
62
+
28
63
  export function snapshotSave(
29
64
  name: string,
30
65
  options: SnapshotSaveOptions = {},
@@ -73,31 +108,7 @@ export function snapshotSave(
73
108
  }
74
109
 
75
110
  try {
76
- if (options.force) {
77
- // renameSync overwrites atomically — the correct semantics for --force.
78
- fs.renameSync(tmp, dest);
79
- } else {
80
- // Non-force path: linkSync fails atomically with EEXIST if dest exists,
81
- // closing the TOCTOU window between existsSync above and the final
82
- // placement. We then unlink the temp file; on POSIX and NTFS, link
83
- // creates a second reference so tmp can safely be removed.
84
- try {
85
- fs.linkSync(tmp, dest);
86
- } catch (err) {
87
- if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
88
- throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
89
- }
90
- throw err;
91
- }
92
- try {
93
- fs.unlinkSync(tmp);
94
- } catch (cleanupErr) {
95
- // Best-effort — dest is already in place, so a leftover tmp file is
96
- // harmless. Log at debug so repeated failures surface during
97
- // troubleshooting without noising up normal operation.
98
- debug(`snapshotSave: failed to remove temp file ${tmp}: ${cleanupErr}`);
99
- }
100
- }
111
+ atomicPlaceFile(tmp, dest, name, options.force);
101
112
  } catch (err) {
102
113
  try {
103
114
  fs.unlinkSync(tmp);
@@ -90,6 +90,44 @@ interface SqliteStatement {
90
90
  run(...params: unknown[]): unknown;
91
91
  }
92
92
 
93
+ /** Insert file→parent-directory contains edges (incremental-aware). */
94
+ function insertFileToParentEdges(
95
+ insertEdge: SqliteStatement,
96
+ getNodeIdStmt: NodeIdStmt,
97
+ fileSymbols: Map<string, FileSymbolData>,
98
+ affectedDirs: Set<string> | null,
99
+ ): void {
100
+ for (const relPath of fileSymbols.keys()) {
101
+ const dir = normalizePath(path.dirname(relPath));
102
+ if (!dir || dir === '.') continue;
103
+ if (affectedDirs && !affectedDirs.has(dir)) continue;
104
+ const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
105
+ const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
106
+ if (dirRow && fileRow) {
107
+ insertEdge.run(dirRow.id, fileRow.id, 'contains', 1.0, 0);
108
+ }
109
+ }
110
+ }
111
+
112
+ /** Insert child-directory→parent-directory contains edges (incremental-aware). */
113
+ function insertDirToParentEdges(
114
+ insertEdge: SqliteStatement,
115
+ getNodeIdStmt: NodeIdStmt,
116
+ allDirs: Set<string>,
117
+ affectedDirs: Set<string> | null,
118
+ ): void {
119
+ for (const dir of allDirs) {
120
+ const parent = normalizePath(path.dirname(dir));
121
+ if (!parent || parent === '.' || parent === dir) continue;
122
+ if (affectedDirs && !affectedDirs.has(parent)) continue;
123
+ const parentRow = getNodeIdStmt.get(parent, 'directory', parent, 0);
124
+ const childRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
125
+ if (parentRow && childRow) {
126
+ insertEdge.run(parentRow.id, childRow.id, 'contains', 1.0, 0);
127
+ }
128
+ }
129
+ }
130
+
93
131
  function insertContainsEdges(
94
132
  db: BetterSqlite3Database,
95
133
  insertEdge: SqliteStatement,
@@ -102,26 +140,8 @@ function insertContainsEdges(
102
140
  const affectedDirs = isIncremental ? getAncestorDirs(changedFiles ?? []) : null;
103
141
 
104
142
  db.transaction(() => {
105
- for (const relPath of fileSymbols.keys()) {
106
- const dir = normalizePath(path.dirname(relPath));
107
- if (!dir || dir === '.') continue;
108
- if (affectedDirs && !affectedDirs.has(dir)) continue;
109
- const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
110
- const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
111
- if (dirRow && fileRow) {
112
- insertEdge.run(dirRow.id, fileRow.id, 'contains', 1.0, 0);
113
- }
114
- }
115
- for (const dir of allDirs) {
116
- const parent = normalizePath(path.dirname(dir));
117
- if (!parent || parent === '.' || parent === dir) continue;
118
- if (affectedDirs && !affectedDirs.has(parent)) continue;
119
- const parentRow = getNodeIdStmt.get(parent, 'directory', parent, 0);
120
- const childRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
121
- if (parentRow && childRow) {
122
- insertEdge.run(parentRow.id, childRow.id, 'contains', 1.0, 0);
123
- }
124
- }
143
+ insertFileToParentEdges(insertEdge, getNodeIdStmt, fileSymbols, affectedDirs);
144
+ insertDirToParentEdges(insertEdge, getNodeIdStmt, allDirs, affectedDirs);
125
145
  })();
126
146
  }
127
147
 
@@ -249,40 +269,59 @@ function buildFileToAncestorDirs(dirFiles: Map<string, string[]>): Map<string, S
249
269
  return fileToAncestorDirs;
250
270
  }
251
271
 
272
+ /** Initialise a zero-count map for all known directories. */
273
+ function initDirEdgeCounts(
274
+ allDirs: Set<string>,
275
+ ): Map<string, { intra: number; fanIn: number; fanOut: number }> {
276
+ const m = new Map<string, { intra: number; fanIn: number; fanOut: number }>();
277
+ for (const dir of allDirs) m.set(dir, { intra: 0, fanIn: 0, fanOut: 0 });
278
+ return m;
279
+ }
280
+
281
+ /** Accumulate source-side (intra / fanOut) counts for one import edge. */
282
+ function accumulateSrcDirCounts(
283
+ srcDirs: Set<string>,
284
+ tgtDirs: Set<string> | undefined,
285
+ dirEdgeCounts: Map<string, { intra: number; fanIn: number; fanOut: number }>,
286
+ ): void {
287
+ for (const dir of srcDirs) {
288
+ const counts = dirEdgeCounts.get(dir);
289
+ if (!counts) continue;
290
+ if (tgtDirs?.has(dir)) {
291
+ counts.intra++;
292
+ } else {
293
+ counts.fanOut++;
294
+ }
295
+ }
296
+ }
297
+
298
+ /** Accumulate target-side (fanIn) counts for one import edge. */
299
+ function accumulateTgtDirCounts(
300
+ tgtDirs: Set<string>,
301
+ srcDirs: Set<string> | undefined,
302
+ dirEdgeCounts: Map<string, { intra: number; fanIn: number; fanOut: number }>,
303
+ ): void {
304
+ for (const dir of tgtDirs) {
305
+ if (srcDirs?.has(dir)) continue;
306
+ const counts = dirEdgeCounts.get(dir);
307
+ if (!counts) continue;
308
+ counts.fanIn++;
309
+ }
310
+ }
311
+
252
312
  /** Count intra-directory, fan-in, and fan-out edges per directory. */
253
313
  function countDirectoryEdges(
254
314
  allDirs: Set<string>,
255
315
  importEdges: ImportEdge[],
256
316
  fileToAncestorDirs: Map<string, Set<string>>,
257
317
  ): Map<string, { intra: number; fanIn: number; fanOut: number }> {
258
- const dirEdgeCounts = new Map<string, { intra: number; fanIn: number; fanOut: number }>();
259
- for (const dir of allDirs) {
260
- dirEdgeCounts.set(dir, { intra: 0, fanIn: 0, fanOut: 0 });
261
- }
318
+ const dirEdgeCounts = initDirEdgeCounts(allDirs);
262
319
  for (const { source_file, target_file } of importEdges) {
263
320
  const srcDirs = fileToAncestorDirs.get(source_file);
264
321
  const tgtDirs = fileToAncestorDirs.get(target_file);
265
322
  if (!srcDirs && !tgtDirs) continue;
266
-
267
- if (srcDirs) {
268
- for (const dir of srcDirs) {
269
- const counts = dirEdgeCounts.get(dir);
270
- if (!counts) continue;
271
- if (tgtDirs?.has(dir)) {
272
- counts.intra++;
273
- } else {
274
- counts.fanOut++;
275
- }
276
- }
277
- }
278
- if (tgtDirs) {
279
- for (const dir of tgtDirs) {
280
- if (srcDirs?.has(dir)) continue;
281
- const counts = dirEdgeCounts.get(dir);
282
- if (!counts) continue;
283
- counts.fanIn++;
284
- }
285
- }
323
+ if (srcDirs) accumulateSrcDirCounts(srcDirs, tgtDirs, dirEdgeCounts);
324
+ if (tgtDirs) accumulateTgtDirCounts(tgtDirs, srcDirs, dirEdgeCounts);
286
325
  }
287
326
  return dirEdgeCounts;
288
327
  }
@@ -541,15 +580,50 @@ interface CallableNodeRow {
541
580
  fan_out: number;
542
581
  }
543
582
 
544
- /** Build the activeFiles set: files with at least one callable connected to the graph. */
545
- function buildActiveFilesSet(rows: CallableNodeRow[]): Set<string> {
583
+ /**
584
+ * Kinds that are consumed via annotations/references rather than calls.
585
+ * These do not count as "active callables" for the hasActiveFileSiblings heuristic.
586
+ */
587
+ const ANNOTATION_ONLY_KINDS = new Set([
588
+ 'constant',
589
+ 'struct',
590
+ 'enum',
591
+ 'trait',
592
+ 'type',
593
+ 'interface',
594
+ 'record',
595
+ ]);
596
+
597
+ /**
598
+ * Build two active-files sets from callable rows:
599
+ *
600
+ * - `activeFiles`: files with at least one non-annotation-only callable with
601
+ * `fan_in > 0 || fan_out > 0`. Used for annotation-only kinds (constants,
602
+ * type defs) which have no callers by design.
603
+ *
604
+ * - `calledActiveFiles`: files with at least one non-annotation-only callable
605
+ * with `fan_in > 0` (strictly called). Used for method/function kinds to
606
+ * prevent a self-sibling loop: a function with `fanIn=0, fanOut>0` as the
607
+ * only callable in its file must NOT count itself as an "active sibling" and
608
+ * thus promote itself to `leaf`.
609
+ */
610
+ function buildActiveFilesSet(rows: CallableNodeRow[]): {
611
+ activeFiles: Set<string>;
612
+ calledActiveFiles: Set<string>;
613
+ } {
546
614
  const activeFiles = new Set<string>();
615
+ const calledActiveFiles = new Set<string>();
547
616
  for (const r of rows) {
548
- if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
549
- activeFiles.add(r.file);
617
+ if (!ANNOTATION_ONLY_KINDS.has(r.kind)) {
618
+ if (r.fan_in > 0 || r.fan_out > 0) {
619
+ activeFiles.add(r.file);
620
+ }
621
+ if (r.fan_in > 0) {
622
+ calledActiveFiles.add(r.file);
623
+ }
550
624
  }
551
625
  }
552
- return activeFiles;
626
+ return { activeFiles, calledActiveFiles };
553
627
  }
554
628
 
555
629
  /** Map callable rows to classifier input objects, attaching exported/prod-fan-in/active-file metadata. */
@@ -558,6 +632,7 @@ function buildClassifierInput(
558
632
  exportedIds: Set<number>,
559
633
  prodFanInMap: Map<number, number>,
560
634
  activeFiles: Set<string>,
635
+ calledActiveFiles: Set<string>,
561
636
  ): Array<{
562
637
  id: string;
563
638
  name: string;
@@ -578,7 +653,20 @@ function buildClassifierInput(
578
653
  fanOut: r.fan_out,
579
654
  isExported: exportedIds.has(r.id),
580
655
  productionFanIn: prodFanInMap.get(r.id) || 0,
581
- hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
656
+ // Set hasActiveFileSiblings for annotation-only kinds (constants, type defs)
657
+ // AND for method/function — the latter two can have fanIn === 0 due to
658
+ // untraced call-site patterns (interface dispatch, logical-or defaults).
659
+ // The classifier interprets this field differently per kind (see classifyUnreferencedNode).
660
+ //
661
+ // IMPORTANT: method/function use calledActiveFiles (fan_in > 0 only) to
662
+ // prevent a self-sibling false negative: a function with fanIn=0, fanOut>0
663
+ // as the sole callable in its file must NOT see its own file as "active"
664
+ // and promote itself to leaf.
665
+ hasActiveFileSiblings: ANNOTATION_ONLY_KINDS.has(r.kind)
666
+ ? activeFiles.has(r.file)
667
+ : r.kind === 'method' || r.kind === 'function'
668
+ ? calledActiveFiles.has(r.file)
669
+ : undefined,
582
670
  }));
583
671
  }
584
672
 
@@ -660,7 +748,8 @@ function readCachedMedians(db: BetterSqlite3Database): { fanIn: number; fanOut:
660
748
  )
661
749
  return null;
662
750
  return { fanIn: cached.fanIn, fanOut: cached.fanOut };
663
- } catch {
751
+ } catch (e) {
752
+ debug(`readCachedMedians: failed to parse cached medians — ${e}`);
664
753
  return null;
665
754
  }
666
755
  }
@@ -761,6 +850,20 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
761
850
  .all() as { id: number }[];
762
851
  for (const r of reexportExported) exportedIds.add(r.id);
763
852
 
853
+ // Mark symbols with exported=1 as exported — the extractor sets this flag when the
854
+ // author writes `export interface Foo { }` / `export type Bar = ...` / `export function`.
855
+ // Cross-file edge inference misses these when the symbol is only used as a type annotation
856
+ // within the same file (no calls/imports-type edge is produced for same-file type usage).
857
+ // This fixes false dead-unresolved classification for exported interfaces with no external callers (#1583).
858
+ const explicitlyExported = db
859
+ .prepare(
860
+ `SELECT id FROM nodes
861
+ WHERE exported = 1
862
+ AND kind NOT IN ('file', 'directory', 'parameter', 'property')`,
863
+ )
864
+ .all() as { id: number }[];
865
+ for (const r of explicitlyExported) exportedIds.add(r.id);
866
+
764
867
  // Compute production fan-in (excluding callers in test files)
765
868
  const prodFanInMap = new Map<number, number>();
766
869
  const prodRows = db
@@ -781,8 +884,14 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
781
884
  // Compute medians from the already-loaded rows (no extra DB round-trip),
782
885
  // pass them as overrides to avoid recomputing inside classifyRoles,
783
886
  // and cache them for subsequent incremental builds.
784
- const activeFiles = buildActiveFilesSet(rows);
785
- const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
887
+ const { activeFiles, calledActiveFiles } = buildActiveFilesSet(rows);
888
+ const classifierInput = buildClassifierInput(
889
+ rows,
890
+ exportedIds,
891
+ prodFanInMap,
892
+ activeFiles,
893
+ calledActiveFiles,
894
+ );
786
895
  const nonZeroFanIn = classifierInput
787
896
  .filter((n) => n.fanIn > 0)
788
897
  .map((n) => n.fanIn)
@@ -928,6 +1037,21 @@ function classifyNodeRolesIncremental(
928
1037
  .all(...allAffectedFiles) as { id: number }[];
929
1038
  for (const r of reexportExported) exportedIds.add(r.id);
930
1039
 
1040
+ // 3c. Mark symbols with exported=1 as exported — the extractor sets this flag when the
1041
+ // author writes `export interface Foo { }` / `export type Bar = ...` / `export function`.
1042
+ // Cross-file edge inference misses these when the symbol is only used as a type annotation
1043
+ // within the same file (no calls/imports-type edge is produced for same-file type usage).
1044
+ // Scoped to affected files only for the incremental path (#1583).
1045
+ const explicitlyExported = db
1046
+ .prepare(
1047
+ `SELECT id FROM nodes
1048
+ WHERE exported = 1
1049
+ AND kind NOT IN ('file', 'directory', 'parameter', 'property')
1050
+ AND file IN (${placeholders})`,
1051
+ )
1052
+ .all(...allAffectedFiles) as { id: number }[];
1053
+ for (const r of explicitlyExported) exportedIds.add(r.id);
1054
+
931
1055
  // 4. Production fan-in for affected nodes only
932
1056
  const prodFanInMap = new Map<number, number>();
933
1057
  const prodRows = db
@@ -947,8 +1071,14 @@ function classifyNodeRolesIncremental(
947
1071
  }
948
1072
 
949
1073
  // 5. Classify affected nodes using global medians
950
- const activeFiles = buildActiveFilesSet(rows);
951
- const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
1074
+ const { activeFiles, calledActiveFiles } = buildActiveFilesSet(rows);
1075
+ const classifierInput = buildClassifierInput(
1076
+ rows,
1077
+ exportedIds,
1078
+ prodFanInMap,
1079
+ activeFiles,
1080
+ calledActiveFiles,
1081
+ );
952
1082
  const roleMap = classifyRoles(classifierInput, globalMedians);
953
1083
 
954
1084
  // 6. Build summary (only for affected nodes) and update only those nodes