@optave/codegraph 3.1.3 → 3.1.4

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 (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
@@ -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';
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Unified in-memory graph model.
3
+ *
4
+ * Stores directed (default) or undirected graphs with node/edge attributes.
5
+ * Node IDs are always strings. DB integer IDs should be stringified before use.
6
+ */
7
+
8
+ import Graph from 'graphology';
9
+
10
+ export class CodeGraph {
11
+ /**
12
+ * @param {{ directed?: boolean }} [opts]
13
+ */
14
+ constructor(opts = {}) {
15
+ this._directed = opts.directed !== false;
16
+ /** @type {Map<string, object>} */
17
+ this._nodes = new Map();
18
+ /** @type {Map<string, Map<string, object>>} node → (target → edgeAttrs) */
19
+ this._successors = new Map();
20
+ /** @type {Map<string, Map<string, object>>} node → (source → edgeAttrs) */
21
+ this._predecessors = new Map();
22
+ }
23
+
24
+ get directed() {
25
+ return this._directed;
26
+ }
27
+
28
+ get nodeCount() {
29
+ return this._nodes.size;
30
+ }
31
+
32
+ get edgeCount() {
33
+ let count = 0;
34
+ for (const targets of this._successors.values()) count += targets.size;
35
+ // Undirected graphs store each edge twice (a→b and b→a)
36
+ return this._directed ? count : count / 2;
37
+ }
38
+
39
+ // ─── Node operations ────────────────────────────────────────────
40
+
41
+ addNode(id, attrs = {}) {
42
+ const key = String(id);
43
+ this._nodes.set(key, attrs);
44
+ if (!this._successors.has(key)) this._successors.set(key, new Map());
45
+ if (!this._predecessors.has(key)) this._predecessors.set(key, new Map());
46
+ return this;
47
+ }
48
+
49
+ hasNode(id) {
50
+ return this._nodes.has(String(id));
51
+ }
52
+
53
+ getNodeAttrs(id) {
54
+ return this._nodes.get(String(id));
55
+ }
56
+
57
+ /** @returns {IterableIterator<[string, object]>} */
58
+ nodes() {
59
+ return this._nodes.entries();
60
+ }
61
+
62
+ /** @returns {string[]} */
63
+ nodeIds() {
64
+ return [...this._nodes.keys()];
65
+ }
66
+
67
+ // ─── Edge operations ────────────────────────────────────────────
68
+
69
+ addEdge(source, target, attrs = {}) {
70
+ const src = String(source);
71
+ const tgt = String(target);
72
+ // Auto-add nodes if missing
73
+ if (!this._nodes.has(src)) this.addNode(src);
74
+ if (!this._nodes.has(tgt)) this.addNode(tgt);
75
+
76
+ this._successors.get(src).set(tgt, attrs);
77
+ this._predecessors.get(tgt).set(src, attrs);
78
+
79
+ if (!this._directed) {
80
+ this._successors.get(tgt).set(src, attrs);
81
+ this._predecessors.get(src).set(tgt, attrs);
82
+ }
83
+ return this;
84
+ }
85
+
86
+ hasEdge(source, target) {
87
+ const src = String(source);
88
+ const tgt = String(target);
89
+ return this._successors.has(src) && this._successors.get(src).has(tgt);
90
+ }
91
+
92
+ getEdgeAttrs(source, target) {
93
+ const src = String(source);
94
+ const tgt = String(target);
95
+ return this._successors.get(src)?.get(tgt);
96
+ }
97
+
98
+ /** @yields {[string, string, object]} source, target, attrs */
99
+ *edges() {
100
+ const seen = this._directed ? null : new Set();
101
+ for (const [src, targets] of this._successors) {
102
+ for (const [tgt, attrs] of targets) {
103
+ if (!this._directed) {
104
+ // \0 is safe as separator — node IDs are file paths/symbols, never contain null bytes
105
+ const key = src < tgt ? `${src}\0${tgt}` : `${tgt}\0${src}`;
106
+ if (seen.has(key)) continue;
107
+ seen.add(key);
108
+ }
109
+ yield [src, tgt, attrs];
110
+ }
111
+ }
112
+ }
113
+
114
+ // ─── Adjacency ──────────────────────────────────────────────────
115
+
116
+ /** Direct successors of a node (outgoing edges). */
117
+ successors(id) {
118
+ const key = String(id);
119
+ const map = this._successors.get(key);
120
+ return map ? [...map.keys()] : [];
121
+ }
122
+
123
+ /** Direct predecessors of a node (incoming edges). */
124
+ predecessors(id) {
125
+ const key = String(id);
126
+ const map = this._predecessors.get(key);
127
+ return map ? [...map.keys()] : [];
128
+ }
129
+
130
+ /** All neighbors (union of successors + predecessors). */
131
+ neighbors(id) {
132
+ const key = String(id);
133
+ const set = new Set();
134
+ const succ = this._successors.get(key);
135
+ if (succ) for (const k of succ.keys()) set.add(k);
136
+ const pred = this._predecessors.get(key);
137
+ if (pred) for (const k of pred.keys()) set.add(k);
138
+ return [...set];
139
+ }
140
+
141
+ outDegree(id) {
142
+ const map = this._successors.get(String(id));
143
+ return map ? map.size : 0;
144
+ }
145
+
146
+ inDegree(id) {
147
+ const map = this._predecessors.get(String(id));
148
+ return map ? map.size : 0;
149
+ }
150
+
151
+ // ─── Filtering ──────────────────────────────────────────────────
152
+
153
+ /** Return a new graph containing only nodes matching the predicate. */
154
+ subgraph(predicate) {
155
+ const g = new CodeGraph({ directed: this._directed });
156
+ for (const [id, attrs] of this._nodes) {
157
+ if (predicate(id, attrs)) g.addNode(id, { ...attrs });
158
+ }
159
+ for (const [src, tgt, attrs] of this.edges()) {
160
+ if (g.hasNode(src) && g.hasNode(tgt)) {
161
+ g.addEdge(src, tgt, { ...attrs });
162
+ }
163
+ }
164
+ return g;
165
+ }
166
+
167
+ /** Return a new graph containing only edges matching the predicate. */
168
+ filterEdges(predicate) {
169
+ const g = new CodeGraph({ directed: this._directed });
170
+ for (const [id, attrs] of this._nodes) {
171
+ g.addNode(id, { ...attrs });
172
+ }
173
+ for (const [src, tgt, attrs] of this.edges()) {
174
+ if (predicate(src, tgt, attrs)) {
175
+ g.addEdge(src, tgt, { ...attrs });
176
+ }
177
+ }
178
+ return g;
179
+ }
180
+
181
+ // ─── Conversion ─────────────────────────────────────────────────
182
+
183
+ /** Convert to flat edge array for native Rust interop. */
184
+ toEdgeArray() {
185
+ const result = [];
186
+ for (const [source, target] of this.edges()) {
187
+ result.push({ source, target });
188
+ }
189
+ return result;
190
+ }
191
+
192
+ /** Convert to graphology instance (for Louvain etc). */
193
+ toGraphology(opts = {}) {
194
+ const type = opts.type || (this._directed ? 'directed' : 'undirected');
195
+ const g = new Graph({ type });
196
+ for (const [id] of this._nodes) {
197
+ g.addNode(id);
198
+ }
199
+
200
+ for (const [src, tgt] of this.edges()) {
201
+ if (src === tgt) continue;
202
+ if (!g.hasEdge(src, tgt)) g.addEdge(src, tgt);
203
+ }
204
+ return g;
205
+ }
206
+
207
+ // ─── Utilities ──────────────────────────────────────────────────
208
+
209
+ clone() {
210
+ const g = new CodeGraph({ directed: this._directed });
211
+ for (const [id, attrs] of this._nodes) {
212
+ g.addNode(id, { ...attrs });
213
+ }
214
+ for (const [src, tgt, attrs] of this.edges()) {
215
+ g.addEdge(src, tgt, { ...attrs });
216
+ }
217
+ return g;
218
+ }
219
+
220
+ /** Merge another graph into this one. Nodes/edges from other override on conflict. */
221
+ merge(other) {
222
+ for (const [id, attrs] of other.nodes()) {
223
+ this.addNode(id, attrs);
224
+ }
225
+ for (const [src, tgt, attrs] of other.edges()) {
226
+ this.addEdge(src, tgt, attrs);
227
+ }
228
+ return this;
229
+ }
230
+ }