@optave/codegraph 3.1.3 → 3.1.5

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 (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -1,39 +1,8 @@
1
- import { findNodesForTriage, openReadonlyOrFail } from './db.js';
2
- import { isTestFile } from './infrastructure/test-filter.js';
3
- import { warn } from './logger.js';
4
- import { paginateResult } from './paginate.js';
5
-
6
- // ─── Constants ────────────────────────────────────────────────────────
7
-
8
- const DEFAULT_WEIGHTS = {
9
- fanIn: 0.25,
10
- complexity: 0.3,
11
- churn: 0.2,
12
- role: 0.15,
13
- mi: 0.1,
14
- };
15
-
16
- const ROLE_WEIGHTS = {
17
- core: 1.0,
18
- utility: 0.9,
19
- entry: 0.8,
20
- adapter: 0.5,
21
- leaf: 0.2,
22
- dead: 0.1,
23
- };
24
-
25
- const DEFAULT_ROLE_WEIGHT = 0.5;
26
-
27
- // ─── Helpers ──────────────────────────────────────────────────────────
28
-
29
- /** Min-max normalize an array of numbers. All-equal → all zeros. */
30
- function minMaxNormalize(values) {
31
- const min = Math.min(...values);
32
- const max = Math.max(...values);
33
- if (max === min) return values.map(() => 0);
34
- const range = max - min;
35
- return values.map((v) => (v - min) / range);
36
- }
1
+ import { openRepo } from '../db/index.js';
2
+ import { DEFAULT_WEIGHTS, scoreRisk } from '../graph/classifiers/risk.js';
3
+ import { warn } from '../infrastructure/logger.js';
4
+ import { isTestFile } from '../infrastructure/test-filter.js';
5
+ import { paginateResult } from '../shared/paginate.js';
37
6
 
38
7
  // ─── Data Function ────────────────────────────────────────────────────
39
8
 
@@ -45,7 +14,7 @@ function minMaxNormalize(values) {
45
14
  * @returns {{ items: object[], summary: object, _pagination?: object }}
46
15
  */
47
16
  export function triageData(customDbPath, opts = {}) {
48
- const db = openReadonlyOrFail(customDbPath);
17
+ const { repo, close } = openRepo(customDbPath, opts);
49
18
  try {
50
19
  const noTests = opts.noTests || false;
51
20
  const fileFilter = opts.file || null;
@@ -57,7 +26,7 @@ export function triageData(customDbPath, opts = {}) {
57
26
 
58
27
  let rows;
59
28
  try {
60
- rows = findNodesForTriage(db, {
29
+ rows = repo.findNodesForTriage({
61
30
  noTests,
62
31
  file: fileFilter,
63
32
  kind: kindFilter,
@@ -81,48 +50,27 @@ export function triageData(customDbPath, opts = {}) {
81
50
  };
82
51
  }
83
52
 
84
- // Extract raw signal arrays
85
- const fanIns = filtered.map((r) => r.fan_in);
86
- const cognitives = filtered.map((r) => r.cognitive);
87
- const churns = filtered.map((r) => r.churn);
88
- const mis = filtered.map((r) => r.mi);
89
-
90
- // Min-max normalize
91
- const normFanIns = minMaxNormalize(fanIns);
92
- const normCognitives = minMaxNormalize(cognitives);
93
- const normChurns = minMaxNormalize(churns);
94
- // MI: higher is better, so invert: 1 - norm(mi)
95
- const normMIsRaw = minMaxNormalize(mis);
96
- const normMIs = normMIsRaw.map((v) => round4(1 - v));
53
+ // Delegate scoring to classifier
54
+ const riskMetrics = scoreRisk(filtered, weights);
97
55
 
98
56
  // Compute risk scores
99
- const items = filtered.map((r, i) => {
100
- const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
101
- const riskScore =
102
- weights.fanIn * normFanIns[i] +
103
- weights.complexity * normCognitives[i] +
104
- weights.churn * normChurns[i] +
105
- weights.role * roleWeight +
106
- weights.mi * normMIs[i];
107
-
108
- return {
109
- name: r.name,
110
- kind: r.kind,
111
- file: r.file,
112
- line: r.line,
113
- role: r.role || null,
114
- fanIn: r.fan_in,
115
- cognitive: r.cognitive,
116
- churn: r.churn,
117
- maintainabilityIndex: r.mi,
118
- normFanIn: round4(normFanIns[i]),
119
- normComplexity: round4(normCognitives[i]),
120
- normChurn: round4(normChurns[i]),
121
- normMI: round4(normMIs[i]),
122
- roleWeight,
123
- riskScore: round4(riskScore),
124
- };
125
- });
57
+ const items = filtered.map((r, i) => ({
58
+ name: r.name,
59
+ kind: r.kind,
60
+ file: r.file,
61
+ line: r.line,
62
+ role: r.role || null,
63
+ fanIn: r.fan_in,
64
+ cognitive: r.cognitive,
65
+ churn: r.churn,
66
+ maintainabilityIndex: r.mi,
67
+ normFanIn: riskMetrics[i].normFanIn,
68
+ normComplexity: riskMetrics[i].normComplexity,
69
+ normChurn: riskMetrics[i].normChurn,
70
+ normMI: riskMetrics[i].normMI,
71
+ roleWeight: riskMetrics[i].roleWeight,
72
+ riskScore: riskMetrics[i].riskScore,
73
+ }));
126
74
 
127
75
  // Apply minScore filter
128
76
  const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
@@ -167,7 +115,7 @@ export function triageData(customDbPath, opts = {}) {
167
115
  offset: opts.offset,
168
116
  });
169
117
  } finally {
170
- db.close();
118
+ close();
171
119
  }
172
120
  }
173
121
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Breadth-first traversal on a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {string|string[]} startIds - One or more starting node IDs
6
+ * @param {{ maxDepth?: number, direction?: 'forward'|'backward'|'both' }} [opts]
7
+ * @returns {Map<string, number>} nodeId → depth from nearest start node
8
+ */
9
+ export function bfs(graph, startIds, opts = {}) {
10
+ const maxDepth = opts.maxDepth ?? Infinity;
11
+ const direction = opts.direction ?? 'forward';
12
+ const starts = Array.isArray(startIds) ? startIds : [startIds];
13
+
14
+ const depths = new Map();
15
+ const queue = [];
16
+
17
+ for (const id of starts) {
18
+ const key = String(id);
19
+ if (graph.hasNode(key)) {
20
+ depths.set(key, 0);
21
+ queue.push(key);
22
+ }
23
+ }
24
+
25
+ let head = 0;
26
+ while (head < queue.length) {
27
+ const current = queue[head++];
28
+ const depth = depths.get(current);
29
+ if (depth >= maxDepth) continue;
30
+
31
+ let neighbors;
32
+ if (direction === 'forward') {
33
+ neighbors = graph.successors(current);
34
+ } else if (direction === 'backward') {
35
+ neighbors = graph.predecessors(current);
36
+ } else {
37
+ neighbors = graph.neighbors(current);
38
+ }
39
+
40
+ for (const n of neighbors) {
41
+ if (!depths.has(n)) {
42
+ depths.set(n, depth + 1);
43
+ queue.push(n);
44
+ }
45
+ }
46
+ }
47
+
48
+ return depths;
49
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Fan-in / fan-out centrality for all nodes in a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @returns {Map<string, { fanIn: number, fanOut: number }>}
6
+ */
7
+ export function fanInOut(graph) {
8
+ const result = new Map();
9
+ for (const id of graph.nodeIds()) {
10
+ result.set(id, {
11
+ fanIn: graph.inDegree(id),
12
+ fanOut: graph.outDegree(id),
13
+ });
14
+ }
15
+ return result;
16
+ }
@@ -0,0 +1,5 @@
1
+ export { bfs } from './bfs.js';
2
+ export { fanInOut } from './centrality.js';
3
+ export { louvainCommunities } from './louvain.js';
4
+ export { shortestPath } from './shortest-path.js';
5
+ export { tarjan } from './tarjan.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Louvain community detection via graphology.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {{ resolution?: number }} [opts]
6
+ * @returns {{ assignments: Map<string, number>, modularity: number }}
7
+ */
8
+ import graphologyLouvain from 'graphology-communities-louvain';
9
+
10
+ export function louvainCommunities(graph, opts = {}) {
11
+ const gy = graph.toGraphology({ type: 'undirected' });
12
+
13
+ if (gy.order === 0 || gy.size === 0) {
14
+ return { assignments: new Map(), modularity: 0 };
15
+ }
16
+
17
+ const resolution = opts.resolution ?? 1.0;
18
+ const details = graphologyLouvain.detailed(gy, { resolution });
19
+
20
+ const assignments = new Map();
21
+ for (const [nodeId, communityId] of Object.entries(details.communities)) {
22
+ assignments.set(nodeId, communityId);
23
+ }
24
+
25
+ return { assignments, modularity: details.modularity };
26
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * BFS-based shortest path on a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {string} fromId
6
+ * @param {string} toId
7
+ * @returns {string[]|null} Path from fromId to toId (inclusive), or null if unreachable
8
+ */
9
+ export function shortestPath(graph, fromId, toId) {
10
+ const from = String(fromId);
11
+ const to = String(toId);
12
+
13
+ if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
14
+ if (from === to) return [from];
15
+
16
+ const parent = new Map();
17
+ parent.set(from, null);
18
+ const queue = [from];
19
+ let head = 0;
20
+
21
+ while (head < queue.length) {
22
+ const current = queue[head++];
23
+ for (const neighbor of graph.successors(current)) {
24
+ if (parent.has(neighbor)) continue;
25
+ parent.set(neighbor, current);
26
+ if (neighbor === to) {
27
+ // Reconstruct path
28
+ const path = [];
29
+ let node = to;
30
+ while (node !== null) {
31
+ path.push(node);
32
+ node = parent.get(node);
33
+ }
34
+ return path.reverse();
35
+ }
36
+ queue.push(neighbor);
37
+ }
38
+ }
39
+
40
+ return null;
41
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tarjan's strongly connected components algorithm.
3
+ * Operates on a CodeGraph instance.
4
+ *
5
+ * @param {import('../model.js').CodeGraph} graph
6
+ * @returns {string[][]} SCCs with length > 1 (cycles)
7
+ */
8
+ export function tarjan(graph) {
9
+ let index = 0;
10
+ const stack = [];
11
+ const onStack = new Set();
12
+ const indices = new Map();
13
+ const lowlinks = new Map();
14
+ const sccs = [];
15
+
16
+ function strongconnect(v) {
17
+ indices.set(v, index);
18
+ lowlinks.set(v, index);
19
+ index++;
20
+ stack.push(v);
21
+ onStack.add(v);
22
+
23
+ for (const w of graph.successors(v)) {
24
+ if (!indices.has(w)) {
25
+ strongconnect(w);
26
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
27
+ } else if (onStack.has(w)) {
28
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
29
+ }
30
+ }
31
+
32
+ if (lowlinks.get(v) === indices.get(v)) {
33
+ const scc = [];
34
+ let w;
35
+ do {
36
+ w = stack.pop();
37
+ onStack.delete(w);
38
+ scc.push(w);
39
+ } while (w !== v);
40
+ if (scc.length > 1) sccs.push(scc);
41
+ }
42
+ }
43
+
44
+ for (const id of graph.nodeIds()) {
45
+ if (!indices.has(id)) strongconnect(id);
46
+ }
47
+
48
+ return sccs;
49
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Build a CodeGraph from the SQLite database.
3
+ * Replaces inline graph construction in cycles.js, communities.js, viewer.js, export.js.
4
+ */
5
+
6
+ import {
7
+ getCallableNodes,
8
+ getCallEdges,
9
+ getFileNodesAll,
10
+ getImportEdges,
11
+ Repository,
12
+ } from '../../db/index.js';
13
+ import { isTestFile } from '../../infrastructure/test-filter.js';
14
+ import { CodeGraph } from '../model.js';
15
+
16
+ /**
17
+ * @param {object} dbOrRepo - Open better-sqlite3 database (readonly) or a Repository instance
18
+ * @param {object} [opts]
19
+ * @param {boolean} [opts.fileLevel=true] - File-level (imports) or function-level (calls)
20
+ * @param {boolean} [opts.noTests=false] - Exclude test files
21
+ * @param {number} [opts.minConfidence] - Minimum edge confidence (function-level only)
22
+ * @returns {CodeGraph}
23
+ */
24
+ export function buildDependencyGraph(dbOrRepo, opts = {}) {
25
+ const fileLevel = opts.fileLevel !== false;
26
+ const noTests = opts.noTests || false;
27
+
28
+ if (fileLevel) {
29
+ return buildFileLevelGraph(dbOrRepo, noTests);
30
+ }
31
+ return buildFunctionLevelGraph(dbOrRepo, noTests, opts.minConfidence);
32
+ }
33
+
34
+ function buildFileLevelGraph(dbOrRepo, noTests) {
35
+ const graph = new CodeGraph();
36
+ const isRepo = dbOrRepo instanceof Repository;
37
+
38
+ let nodes = isRepo ? dbOrRepo.getFileNodesAll() : getFileNodesAll(dbOrRepo);
39
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
40
+
41
+ const nodeIds = new Set();
42
+ for (const n of nodes) {
43
+ graph.addNode(String(n.id), { label: n.file, file: n.file, dbId: n.id });
44
+ nodeIds.add(n.id);
45
+ }
46
+
47
+ const edges = isRepo ? dbOrRepo.getImportEdges() : getImportEdges(dbOrRepo);
48
+ for (const e of edges) {
49
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
50
+ const src = String(e.source_id);
51
+ const tgt = String(e.target_id);
52
+ if (src === tgt) continue;
53
+ if (!graph.hasEdge(src, tgt)) {
54
+ graph.addEdge(src, tgt, { kind: 'imports' });
55
+ }
56
+ }
57
+
58
+ return graph;
59
+ }
60
+
61
+ function buildFunctionLevelGraph(dbOrRepo, noTests, minConfidence) {
62
+ const graph = new CodeGraph();
63
+ const isRepo = dbOrRepo instanceof Repository;
64
+
65
+ let nodes = isRepo ? dbOrRepo.getCallableNodes() : getCallableNodes(dbOrRepo);
66
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
67
+
68
+ const nodeIds = new Set();
69
+ for (const n of nodes) {
70
+ graph.addNode(String(n.id), {
71
+ label: n.name,
72
+ file: n.file,
73
+ kind: n.kind,
74
+ dbId: n.id,
75
+ });
76
+ nodeIds.add(n.id);
77
+ }
78
+
79
+ let edges;
80
+ if (minConfidence != null) {
81
+ if (isRepo) {
82
+ // Trade-off: Repository.getCallEdges() returns all call edges, so we
83
+ // filter in JS. This is O(all call edges) rather than the SQL path's
84
+ // indexed WHERE clause. Acceptable for current data sizes; a dedicated
85
+ // getCallEdgesByMinConfidence(threshold) method on the Repository
86
+ // interface would be the proper fix if this becomes a bottleneck.
87
+ edges = dbOrRepo
88
+ .getCallEdges()
89
+ .filter((e) => e.confidence != null && e.confidence >= minConfidence);
90
+ } else {
91
+ edges = dbOrRepo
92
+ .prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND confidence >= ?")
93
+ .all(minConfidence);
94
+ }
95
+ } else {
96
+ edges = isRepo ? dbOrRepo.getCallEdges() : getCallEdges(dbOrRepo);
97
+ }
98
+
99
+ for (const e of edges) {
100
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
101
+ const src = String(e.source_id);
102
+ const tgt = String(e.target_id);
103
+ if (src === tgt) continue;
104
+ if (!graph.hasEdge(src, tgt)) {
105
+ graph.addEdge(src, tgt, { kind: 'calls' });
106
+ }
107
+ }
108
+
109
+ return graph;
110
+ }
@@ -0,0 +1,3 @@
1
+ export { buildDependencyGraph } from './dependency.js';
2
+ export { buildStructureGraph } from './structure.js';
3
+ export { buildTemporalGraph } from './temporal.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Build a containment graph (directory → file) from the SQLite database.
3
+ */
4
+
5
+ import { CodeGraph } from '../model.js';
6
+
7
+ /**
8
+ * @param {object} db - Open better-sqlite3 database (readonly)
9
+ * @returns {CodeGraph} Directed graph with directory→file containment edges
10
+ */
11
+ export function buildStructureGraph(db) {
12
+ const graph = new CodeGraph();
13
+
14
+ const dirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
15
+
16
+ for (const d of dirs) {
17
+ graph.addNode(String(d.id), { label: d.name, kind: 'directory' });
18
+ }
19
+
20
+ const files = db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
21
+
22
+ for (const f of files) {
23
+ graph.addNode(String(f.id), { label: f.name, kind: 'file', file: f.file });
24
+ }
25
+
26
+ const containsEdges = db
27
+ .prepare(`
28
+ SELECT e.source_id, e.target_id
29
+ FROM edges e
30
+ JOIN nodes n ON e.source_id = n.id
31
+ WHERE e.kind = 'contains' AND n.kind = 'directory'
32
+ `)
33
+ .all();
34
+
35
+ for (const e of containsEdges) {
36
+ graph.addEdge(String(e.source_id), String(e.target_id), { kind: 'contains' });
37
+ }
38
+
39
+ return graph;
40
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Build a co-change (temporal) graph weighted by Jaccard similarity.
3
+ */
4
+
5
+ import { CodeGraph } from '../model.js';
6
+
7
+ /**
8
+ * @param {object} db - Open better-sqlite3 database (readonly)
9
+ * @param {{ minJaccard?: number }} [opts]
10
+ * @returns {CodeGraph} Undirected graph weighted by Jaccard similarity
11
+ */
12
+ export function buildTemporalGraph(db, opts = {}) {
13
+ const minJaccard = opts.minJaccard ?? 0.0;
14
+ const graph = new CodeGraph({ directed: false });
15
+
16
+ // Check if co_changes table exists
17
+ const tableCheck = db
18
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='co_changes'")
19
+ .get();
20
+ if (!tableCheck) return graph;
21
+
22
+ const rows = db
23
+ .prepare('SELECT file_a, file_b, jaccard FROM co_changes WHERE jaccard >= ?')
24
+ .all(minJaccard);
25
+
26
+ for (const r of rows) {
27
+ if (!graph.hasNode(r.file_a)) graph.addNode(r.file_a, { label: r.file_a });
28
+ if (!graph.hasNode(r.file_b)) graph.addNode(r.file_b, { label: r.file_b });
29
+ graph.addEdge(r.file_a, r.file_b, { jaccard: r.jaccard });
30
+ }
31
+
32
+ return graph;
33
+ }
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_WEIGHTS, minMaxNormalize, ROLE_WEIGHTS, scoreRisk } from './risk.js';
2
+ export { classifyRoles, FRAMEWORK_ENTRY_PREFIXES } from './roles.js';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Risk scoring — pure logic, no DB.
3
+ */
4
+
5
+ // Weights sum to 1.0. Complexity gets the highest weight because cognitive load
6
+ // is the strongest predictor of defect density. Fan-in and churn are next as
7
+ // they reflect coupling and volatility. Role adds architectural context, and MI
8
+ // (maintainability index) is a weaker composite signal, so it gets the least.
9
+ export const DEFAULT_WEIGHTS = {
10
+ fanIn: 0.25,
11
+ complexity: 0.3,
12
+ churn: 0.2,
13
+ role: 0.15,
14
+ mi: 0.1,
15
+ };
16
+
17
+ // Role weights reflect structural importance: core modules are central to the
18
+ // dependency graph, utilities are widely imported, entry points are API
19
+ // surfaces. Adapters bridge subsystems but are replaceable. Leaves and dead
20
+ // code have minimal downstream impact.
21
+ export const ROLE_WEIGHTS = {
22
+ core: 1.0,
23
+ utility: 0.9,
24
+ entry: 0.8,
25
+ adapter: 0.5,
26
+ leaf: 0.2,
27
+ dead: 0.1,
28
+ };
29
+
30
+ const DEFAULT_ROLE_WEIGHT = 0.5;
31
+
32
+ /** Min-max normalize an array of numbers. All-equal → all zeros. */
33
+ export function minMaxNormalize(values) {
34
+ const min = Math.min(...values);
35
+ const max = Math.max(...values);
36
+ if (max === min) return values.map(() => 0);
37
+ const range = max - min;
38
+ return values.map((v) => (v - min) / range);
39
+ }
40
+
41
+ function round4(n) {
42
+ return Math.round(n * 10000) / 10000;
43
+ }
44
+
45
+ /**
46
+ * Score risk for a list of items.
47
+ *
48
+ * @param {{ fan_in: number, cognitive: number, churn: number, mi: number, role: string|null }[]} items
49
+ * @param {object} [weights] - Override DEFAULT_WEIGHTS
50
+ * @returns {{ normFanIn: number, normComplexity: number, normChurn: number, normMI: number, roleWeight: number, riskScore: number }[]}
51
+ * Parallel array with risk metrics for each input item.
52
+ */
53
+ export function scoreRisk(items, weights = {}) {
54
+ const w = { ...DEFAULT_WEIGHTS, ...weights };
55
+
56
+ const fanIns = items.map((r) => r.fan_in);
57
+ const cognitives = items.map((r) => r.cognitive);
58
+ const churns = items.map((r) => r.churn);
59
+ const mis = items.map((r) => r.mi);
60
+
61
+ const normFanIns = minMaxNormalize(fanIns);
62
+ const normCognitives = minMaxNormalize(cognitives);
63
+ const normChurns = minMaxNormalize(churns);
64
+ const normMIsRaw = minMaxNormalize(mis);
65
+ const normMIs = normMIsRaw.map((v) => round4(1 - v));
66
+
67
+ return items.map((r, i) => {
68
+ const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
69
+ const riskScore =
70
+ w.fanIn * normFanIns[i] +
71
+ w.complexity * normCognitives[i] +
72
+ w.churn * normChurns[i] +
73
+ w.role * roleWeight +
74
+ w.mi * normMIs[i];
75
+
76
+ return {
77
+ normFanIn: round4(normFanIns[i]),
78
+ normComplexity: round4(normCognitives[i]),
79
+ normChurn: round4(normChurns[i]),
80
+ normMI: round4(normMIs[i]),
81
+ roleWeight,
82
+ riskScore: round4(riskScore),
83
+ };
84
+ });
85
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Node role classification — pure logic, no DB.
3
+ *
4
+ * Roles: entry, core, utility, adapter, leaf, dead
5
+ */
6
+
7
+ export const FRAMEWORK_ENTRY_PREFIXES = ['route:', 'event:', 'command:'];
8
+
9
+ function median(sorted) {
10
+ if (sorted.length === 0) return 0;
11
+ const mid = Math.floor(sorted.length / 2);
12
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
13
+ }
14
+
15
+ /**
16
+ * Classify nodes into architectural roles based on fan-in/fan-out metrics.
17
+ *
18
+ * @param {{ id: string, name: string, fanIn: number, fanOut: number, isExported: boolean }[]} nodes
19
+ * @returns {Map<string, string>} nodeId → role
20
+ */
21
+ export function classifyRoles(nodes) {
22
+ if (nodes.length === 0) return new Map();
23
+
24
+ const nonZeroFanIn = nodes
25
+ .filter((n) => n.fanIn > 0)
26
+ .map((n) => n.fanIn)
27
+ .sort((a, b) => a - b);
28
+ const nonZeroFanOut = nodes
29
+ .filter((n) => n.fanOut > 0)
30
+ .map((n) => n.fanOut)
31
+ .sort((a, b) => a - b);
32
+
33
+ const medFanIn = median(nonZeroFanIn);
34
+ const medFanOut = median(nonZeroFanOut);
35
+
36
+ const result = new Map();
37
+
38
+ for (const node of nodes) {
39
+ const highIn = node.fanIn >= medFanIn && node.fanIn > 0;
40
+ const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
41
+
42
+ let role;
43
+ const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p));
44
+ if (isFrameworkEntry) {
45
+ role = 'entry';
46
+ } else if (node.fanIn === 0 && !node.isExported) {
47
+ role = 'dead';
48
+ } else if (node.fanIn === 0 && node.isExported) {
49
+ role = 'entry';
50
+ } else if (highIn && !highOut) {
51
+ role = 'core';
52
+ } else if (highIn && highOut) {
53
+ role = 'utility';
54
+ } else if (!highIn && highOut) {
55
+ role = 'adapter';
56
+ } else {
57
+ role = 'leaf';
58
+ }
59
+
60
+ result.set(node.id, role);
61
+ }
62
+
63
+ return result;
64
+ }
@@ -0,0 +1,13 @@
1
+ // Graph subsystem barrel export
2
+
3
+ export { bfs, fanInOut, louvainCommunities, shortestPath, tarjan } from './algorithms/index.js';
4
+ export { buildDependencyGraph, buildStructureGraph, buildTemporalGraph } from './builders/index.js';
5
+ export {
6
+ classifyRoles,
7
+ DEFAULT_WEIGHTS,
8
+ FRAMEWORK_ENTRY_PREFIXES,
9
+ minMaxNormalize,
10
+ ROLE_WEIGHTS,
11
+ scoreRisk,
12
+ } from './classifiers/index.js';
13
+ export { CodeGraph } from './model.js';