@ophan/core 0.0.2 → 0.0.3

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 (76) hide show
  1. package/dist/community-detectors/index.d.ts +20 -0
  2. package/dist/community-detectors/index.d.ts.map +1 -0
  3. package/dist/community-detectors/index.js +45 -0
  4. package/dist/community-detectors/label-prop.d.ts +20 -0
  5. package/dist/community-detectors/label-prop.d.ts.map +1 -0
  6. package/dist/community-detectors/label-prop.js +77 -0
  7. package/dist/community-detectors/leiden.d.ts +22 -0
  8. package/dist/community-detectors/leiden.d.ts.map +1 -0
  9. package/dist/community-detectors/leiden.js +312 -0
  10. package/dist/community-detectors/louvain.d.ts +13 -0
  11. package/dist/community-detectors/louvain.d.ts.map +1 -0
  12. package/dist/community-detectors/louvain.js +29 -0
  13. package/dist/community-detectors/types.d.ts +36 -0
  14. package/dist/community-detectors/types.d.ts.map +1 -0
  15. package/dist/{parsers/__fixtures__/no-functions.js → community-detectors/types.js} +0 -2
  16. package/dist/edge-resolvers/call.d.ts +13 -0
  17. package/dist/edge-resolvers/call.d.ts.map +1 -0
  18. package/dist/edge-resolvers/call.js +40 -0
  19. package/dist/edge-resolvers/co-location.d.ts +16 -0
  20. package/dist/edge-resolvers/co-location.d.ts.map +1 -0
  21. package/dist/edge-resolvers/co-location.js +129 -0
  22. package/dist/edge-resolvers/import.d.ts +16 -0
  23. package/dist/edge-resolvers/import.d.ts.map +1 -0
  24. package/dist/edge-resolvers/import.js +118 -0
  25. package/dist/edge-resolvers/index.d.ts +9 -0
  26. package/dist/edge-resolvers/index.d.ts.map +1 -0
  27. package/dist/edge-resolvers/index.js +29 -0
  28. package/dist/edge-resolvers/jsx-ref.d.ts +13 -0
  29. package/dist/edge-resolvers/jsx-ref.d.ts.map +1 -0
  30. package/dist/edge-resolvers/jsx-ref.js +40 -0
  31. package/dist/edge-resolvers/types.d.ts +40 -0
  32. package/dist/edge-resolvers/types.d.ts.map +1 -0
  33. package/dist/edge-resolvers/types.js +2 -0
  34. package/dist/graph.d.ts +293 -0
  35. package/dist/graph.d.ts.map +1 -0
  36. package/dist/graph.js +1295 -0
  37. package/dist/index.d.ts +37 -8
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +385 -183
  40. package/dist/migrations.d.ts +25 -0
  41. package/dist/migrations.d.ts.map +1 -0
  42. package/dist/migrations.js +323 -0
  43. package/dist/module-resolvers/index.d.ts +11 -0
  44. package/dist/module-resolvers/index.d.ts.map +1 -0
  45. package/dist/module-resolvers/index.js +67 -0
  46. package/dist/module-resolvers/javascript.d.ts +18 -0
  47. package/dist/module-resolvers/javascript.d.ts.map +1 -0
  48. package/dist/module-resolvers/javascript.js +130 -0
  49. package/dist/module-resolvers/types.d.ts +18 -0
  50. package/dist/module-resolvers/types.d.ts.map +1 -0
  51. package/dist/module-resolvers/types.js +2 -0
  52. package/dist/parsers/python.d.ts.map +1 -1
  53. package/dist/parsers/python.js +38 -4
  54. package/dist/parsers/typescript.d.ts.map +1 -1
  55. package/dist/parsers/typescript.js +133 -0
  56. package/dist/practices.d.ts +28 -0
  57. package/dist/practices.d.ts.map +1 -0
  58. package/dist/practices.js +95 -0
  59. package/dist/schemas.d.ts +251 -3
  60. package/dist/schemas.d.ts.map +1 -1
  61. package/dist/schemas.js +121 -6
  62. package/dist/shared.d.ts +8 -0
  63. package/dist/shared.d.ts.map +1 -1
  64. package/dist/summarize.d.ts +165 -0
  65. package/dist/summarize.d.ts.map +1 -0
  66. package/dist/summarize.js +1067 -0
  67. package/ophan_logo.png +0 -0
  68. package/package.json +9 -2
  69. package/dist/parsers/__fixtures__/arrow-functions.d.ts +0 -5
  70. package/dist/parsers/__fixtures__/arrow-functions.d.ts.map +0 -1
  71. package/dist/parsers/__fixtures__/arrow-functions.js +0 -16
  72. package/dist/parsers/__fixtures__/class-methods.d.ts +0 -6
  73. package/dist/parsers/__fixtures__/class-methods.d.ts.map +0 -1
  74. package/dist/parsers/__fixtures__/class-methods.js +0 -12
  75. package/dist/parsers/__fixtures__/no-functions.d.ts +0 -9
  76. package/dist/parsers/__fixtures__/no-functions.d.ts.map +0 -1
@@ -0,0 +1,20 @@
1
+ export type { CommunityDetector, RawDetectionResult, DetectorOptions } from "./types";
2
+ export { LouvainDetector } from "./louvain";
3
+ export { LabelPropDetector } from "./label-prop";
4
+ export { LeidenDetector } from "./leiden";
5
+ import type { CommunityDetector } from "./types";
6
+ /**
7
+ * Register a community detection algorithm.
8
+ * Call before detectCommunities() to make the algorithm available via GraphConfig.algorithm.
9
+ */
10
+ export declare function registerDetector(detector: CommunityDetector): void;
11
+ /**
12
+ * Get a registered detector by name.
13
+ * Throws if the algorithm is not registered.
14
+ */
15
+ export declare function getDetector(name: string): CommunityDetector;
16
+ /**
17
+ * List all registered detector names.
18
+ */
19
+ export declare function listDetectors(): string[];
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/community-detectors/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAOjD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAElE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAO3D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC"}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LeidenDetector = exports.LabelPropDetector = exports.LouvainDetector = void 0;
4
+ exports.registerDetector = registerDetector;
5
+ exports.getDetector = getDetector;
6
+ exports.listDetectors = listDetectors;
7
+ var louvain_1 = require("./louvain");
8
+ Object.defineProperty(exports, "LouvainDetector", { enumerable: true, get: function () { return louvain_1.LouvainDetector; } });
9
+ var label_prop_1 = require("./label-prop");
10
+ Object.defineProperty(exports, "LabelPropDetector", { enumerable: true, get: function () { return label_prop_1.LabelPropDetector; } });
11
+ var leiden_1 = require("./leiden");
12
+ Object.defineProperty(exports, "LeidenDetector", { enumerable: true, get: function () { return leiden_1.LeidenDetector; } });
13
+ const louvain_2 = require("./louvain");
14
+ const label_prop_2 = require("./label-prop");
15
+ const leiden_2 = require("./leiden");
16
+ const registry = new Map();
17
+ /**
18
+ * Register a community detection algorithm.
19
+ * Call before detectCommunities() to make the algorithm available via GraphConfig.algorithm.
20
+ */
21
+ function registerDetector(detector) {
22
+ registry.set(detector.name, detector);
23
+ }
24
+ /**
25
+ * Get a registered detector by name.
26
+ * Throws if the algorithm is not registered.
27
+ */
28
+ function getDetector(name) {
29
+ const detector = registry.get(name);
30
+ if (!detector) {
31
+ const available = [...registry.keys()].join(", ");
32
+ throw new Error(`Unknown community detection algorithm: "${name}". Available: ${available}`);
33
+ }
34
+ return detector;
35
+ }
36
+ /**
37
+ * List all registered detector names.
38
+ */
39
+ function listDetectors() {
40
+ return [...registry.keys()];
41
+ }
42
+ // Register built-in detectors
43
+ registerDetector(new louvain_2.LouvainDetector());
44
+ registerDetector(new label_prop_2.LabelPropDetector());
45
+ registerDetector(new leiden_2.LeidenDetector());
@@ -0,0 +1,20 @@
1
+ import type Graph from "graphology";
2
+ import type { CommunityDetector, DetectorOptions, RawDetectionResult } from "./types";
3
+ /**
4
+ * Label Propagation Algorithm (LPA) for community detection.
5
+ *
6
+ * Each node starts with a unique label. On each iteration, every node adopts
7
+ * the label with the highest total edge weight among its neighbors. Ties are
8
+ * broken randomly. Converges when no node changes its label.
9
+ *
10
+ * Pros: fast, no resolution parameter needed, finds natural cluster boundaries
11
+ * Cons: non-deterministic, may produce different results on each run
12
+ */
13
+ export declare class LabelPropDetector implements CommunityDetector {
14
+ readonly name = "label-propagation";
15
+ readonly supportsResolution = false;
16
+ private maxIterations;
17
+ constructor(maxIterations?: number);
18
+ detect(graph: Graph, options: DetectorOptions): RawDetectionResult;
19
+ }
20
+ //# sourceMappingURL=label-prop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-prop.d.ts","sourceRoot":"","sources":["../../src/community-detectors/label-prop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtF;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IACzD,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,kBAAkB,SAAS;IAEpC,OAAO,CAAC,aAAa,CAAS;gBAElB,aAAa,SAAM;IAI/B,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,kBAAkB;CA8DnE"}
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LabelPropDetector = void 0;
4
+ /**
5
+ * Label Propagation Algorithm (LPA) for community detection.
6
+ *
7
+ * Each node starts with a unique label. On each iteration, every node adopts
8
+ * the label with the highest total edge weight among its neighbors. Ties are
9
+ * broken randomly. Converges when no node changes its label.
10
+ *
11
+ * Pros: fast, no resolution parameter needed, finds natural cluster boundaries
12
+ * Cons: non-deterministic, may produce different results on each run
13
+ */
14
+ class LabelPropDetector {
15
+ constructor(maxIterations = 100) {
16
+ this.name = "label-propagation";
17
+ this.supportsResolution = false;
18
+ this.maxIterations = maxIterations;
19
+ }
20
+ detect(graph, options) {
21
+ // Initialize: each node gets a unique label
22
+ const labels = new Map();
23
+ let nextLabel = 0;
24
+ graph.forEachNode((node) => {
25
+ labels.set(node, nextLabel++);
26
+ });
27
+ const nodes = graph.nodes();
28
+ for (let iter = 0; iter < this.maxIterations; iter++) {
29
+ let changed = false;
30
+ // Shuffle nodes for random processing order (reduces bias)
31
+ for (let i = nodes.length - 1; i > 0; i--) {
32
+ const j = Math.floor(Math.random() * (i + 1));
33
+ [nodes[i], nodes[j]] = [nodes[j], nodes[i]];
34
+ }
35
+ for (const node of nodes) {
36
+ // Sum weights per neighboring label
37
+ const labelWeights = new Map();
38
+ graph.forEachEdge(node, (_edge, attrs, source, target) => {
39
+ const neighbor = source === node ? target : source;
40
+ const neighborLabel = labels.get(neighbor);
41
+ const weight = attrs[options.weightAttribute] || 1.0;
42
+ labelWeights.set(neighborLabel, (labelWeights.get(neighborLabel) || 0) + weight);
43
+ });
44
+ if (labelWeights.size === 0)
45
+ continue;
46
+ // Find label(s) with maximum weight
47
+ let maxWeight = -Infinity;
48
+ const candidates = [];
49
+ for (const [label, weight] of labelWeights) {
50
+ if (weight > maxWeight) {
51
+ maxWeight = weight;
52
+ candidates.length = 0;
53
+ candidates.push(label);
54
+ }
55
+ else if (weight === maxWeight) {
56
+ candidates.push(label);
57
+ }
58
+ }
59
+ // Break ties randomly
60
+ const chosen = candidates[Math.floor(Math.random() * candidates.length)];
61
+ if (chosen !== labels.get(node)) {
62
+ labels.set(node, chosen);
63
+ changed = true;
64
+ }
65
+ }
66
+ if (!changed)
67
+ break;
68
+ }
69
+ // Convert to Record<string, number>
70
+ const communities = {};
71
+ for (const [node, label] of labels) {
72
+ communities[node] = label;
73
+ }
74
+ return { communities, modularity: null };
75
+ }
76
+ }
77
+ exports.LabelPropDetector = LabelPropDetector;
@@ -0,0 +1,22 @@
1
+ import type Graph from "graphology";
2
+ import type { CommunityDetector, DetectorOptions, RawDetectionResult } from "./types";
3
+ /**
4
+ * Leiden community detection algorithm (Traag, van Eck & Waltman, 2019).
5
+ *
6
+ * Improvement over Louvain with three phases per iteration:
7
+ * 1. Local moving: nodes move to neighboring community maximizing modularity gain
8
+ * 2. Refinement: ensures each community is internally connected by re-examining
9
+ * sub-structure within communities (the key Leiden innovation)
10
+ * 3. Aggregation: create super-graph where communities become nodes
11
+ *
12
+ * Guarantees communities are well-connected (no disconnected subgroups within a community).
13
+ * Uses modularity quality function: Q = Σ_c [e_c/m - γ(n_c/2m)²]
14
+ *
15
+ * Reference: https://arxiv.org/abs/1810.08473
16
+ */
17
+ export declare class LeidenDetector implements CommunityDetector {
18
+ readonly name = "leiden";
19
+ readonly supportsResolution = true;
20
+ detect(graph: Graph, options: DetectorOptions): RawDetectionResult;
21
+ }
22
+ //# sourceMappingURL=leiden.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leiden.d.ts","sourceRoot":"","sources":["../../src/community-detectors/leiden.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtF;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAe,YAAW,iBAAiB;IACtD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,kBAAkB,QAAQ;IAEnC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,kBAAkB;CAqEnE"}
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LeidenDetector = void 0;
4
+ /**
5
+ * Leiden community detection algorithm (Traag, van Eck & Waltman, 2019).
6
+ *
7
+ * Improvement over Louvain with three phases per iteration:
8
+ * 1. Local moving: nodes move to neighboring community maximizing modularity gain
9
+ * 2. Refinement: ensures each community is internally connected by re-examining
10
+ * sub-structure within communities (the key Leiden innovation)
11
+ * 3. Aggregation: create super-graph where communities become nodes
12
+ *
13
+ * Guarantees communities are well-connected (no disconnected subgroups within a community).
14
+ * Uses modularity quality function: Q = Σ_c [e_c/m - γ(n_c/2m)²]
15
+ *
16
+ * Reference: https://arxiv.org/abs/1810.08473
17
+ */
18
+ class LeidenDetector {
19
+ constructor() {
20
+ this.name = "leiden";
21
+ this.supportsResolution = true;
22
+ }
23
+ detect(graph, options) {
24
+ if (graph.order === 0) {
25
+ return { communities: {}, modularity: null };
26
+ }
27
+ const nodes = graph.nodes();
28
+ const weightAttr = options.weightAttribute;
29
+ const resolution = options.resolution;
30
+ // Total edge weight in graph (sum of all edge weights)
31
+ let totalWeight = 0;
32
+ graph.forEachEdge((_edge, attrs) => {
33
+ totalWeight += attrs[weightAttr] || 1.0;
34
+ });
35
+ if (totalWeight === 0) {
36
+ // No edges — each node is its own community
37
+ const communities = {};
38
+ nodes.forEach((n, i) => { communities[n] = i; });
39
+ return { communities, modularity: 0 };
40
+ }
41
+ // Initialize: each node in its own community
42
+ const nodeToComm = new Map();
43
+ nodes.forEach((n, i) => nodeToComm.set(n, i));
44
+ let nextCommId = nodes.length;
45
+ // Precompute node strengths (weighted degree)
46
+ const nodeStrength = new Map();
47
+ for (const node of nodes) {
48
+ let strength = 0;
49
+ graph.forEachEdge(node, (_edge, attrs) => {
50
+ strength += attrs[weightAttr] || 1.0;
51
+ });
52
+ nodeStrength.set(node, strength);
53
+ }
54
+ // Run Leiden iterations
55
+ const maxIterations = 10;
56
+ for (let iter = 0; iter < maxIterations; iter++) {
57
+ const moved = localMovingPhase(graph, nodeToComm, nodeStrength, totalWeight, resolution, weightAttr);
58
+ refinementPhase(graph, nodeToComm, nodeStrength, totalWeight, resolution, weightAttr, nextCommId);
59
+ // Update nextCommId to be above any community ID used
60
+ let maxComm = 0;
61
+ for (const c of nodeToComm.values()) {
62
+ if (c > maxComm)
63
+ maxComm = c;
64
+ }
65
+ nextCommId = maxComm + 1;
66
+ if (!moved)
67
+ break;
68
+ }
69
+ // Renumber communities to contiguous integers
70
+ const communities = {};
71
+ const commRemap = new Map();
72
+ let nextId = 0;
73
+ for (const node of nodes) {
74
+ const comm = nodeToComm.get(node);
75
+ if (!commRemap.has(comm)) {
76
+ commRemap.set(comm, nextId++);
77
+ }
78
+ communities[node] = commRemap.get(comm);
79
+ }
80
+ // Compute modularity
81
+ const modularity = computeModularity(graph, communities, totalWeight, resolution, weightAttr);
82
+ return { communities, modularity };
83
+ }
84
+ }
85
+ exports.LeidenDetector = LeidenDetector;
86
+ /**
87
+ * Phase 1: Local moving — nodes greedily move to neighboring community maximizing modularity gain.
88
+ * Returns true if any node moved.
89
+ */
90
+ function localMovingPhase(graph, nodeToComm, nodeStrength, totalWeight, resolution, weightAttr) {
91
+ const nodes = graph.nodes();
92
+ let moved = false;
93
+ // Shuffle nodes for random processing order
94
+ for (let i = nodes.length - 1; i > 0; i--) {
95
+ const j = Math.floor(Math.random() * (i + 1));
96
+ [nodes[i], nodes[j]] = [nodes[j], nodes[i]];
97
+ }
98
+ // Precompute community aggregate strengths
99
+ const commStrength = new Map();
100
+ for (const [node, comm] of nodeToComm) {
101
+ commStrength.set(comm, (commStrength.get(comm) || 0) + (nodeStrength.get(node) || 0));
102
+ }
103
+ for (const node of nodes) {
104
+ const currentComm = nodeToComm.get(node);
105
+ const ki = nodeStrength.get(node) || 0;
106
+ // Sum edge weights to each neighboring community
107
+ const neighborCommWeights = new Map();
108
+ graph.forEachEdge(node, (_edge, attrs, source, target) => {
109
+ const neighbor = source === node ? target : source;
110
+ const neighborComm = nodeToComm.get(neighbor);
111
+ const weight = attrs[weightAttr] || 1.0;
112
+ neighborCommWeights.set(neighborComm, (neighborCommWeights.get(neighborComm) || 0) + weight);
113
+ });
114
+ // Temporarily remove node from its community for evaluation
115
+ const currentCommStrengthWithout = (commStrength.get(currentComm) || 0) - ki;
116
+ const edgesToCurrent = neighborCommWeights.get(currentComm) || 0;
117
+ // Modularity loss from removing node from current community
118
+ const removeLoss = edgesToCurrent / totalWeight - resolution * ki * currentCommStrengthWithout / (2 * totalWeight * totalWeight);
119
+ // Find best community to move to
120
+ let bestComm = currentComm;
121
+ let bestGain = 0;
122
+ for (const [comm, edgesToComm] of neighborCommWeights) {
123
+ if (comm === currentComm)
124
+ continue;
125
+ const commStr = commStrength.get(comm) || 0;
126
+ // Modularity gain from adding node to this community
127
+ const addGain = edgesToComm / totalWeight - resolution * ki * commStr / (2 * totalWeight * totalWeight);
128
+ const netGain = addGain - removeLoss;
129
+ if (netGain > bestGain) {
130
+ bestGain = netGain;
131
+ bestComm = comm;
132
+ }
133
+ }
134
+ if (bestComm !== currentComm) {
135
+ // Move node
136
+ commStrength.set(currentComm, (commStrength.get(currentComm) || 0) - ki);
137
+ commStrength.set(bestComm, (commStrength.get(bestComm) || 0) + ki);
138
+ nodeToComm.set(node, bestComm);
139
+ moved = true;
140
+ }
141
+ }
142
+ return moved;
143
+ }
144
+ /**
145
+ * Phase 2: Refinement — ensures communities are internally connected.
146
+ * For each community, check if it can be split into better-connected sub-communities.
147
+ * Nodes that are weakly connected within their community are moved to improve quality.
148
+ */
149
+ function refinementPhase(graph, nodeToComm, nodeStrength, totalWeight, resolution, weightAttr, nextCommId) {
150
+ // Group nodes by community
151
+ const commMembers = new Map();
152
+ for (const [node, comm] of nodeToComm) {
153
+ const members = commMembers.get(comm) || [];
154
+ members.push(node);
155
+ commMembers.set(comm, members);
156
+ }
157
+ for (const [commId, members] of commMembers) {
158
+ if (members.length <= 2)
159
+ continue;
160
+ // Check connectivity within this community using BFS
161
+ // If community is already a single connected component, skip refinement
162
+ const memberSet = new Set(members);
163
+ const components = findConnectedComponents(graph, memberSet, weightAttr);
164
+ if (components.length <= 1) {
165
+ // Community is connected — try sub-community refinement
166
+ // Each node starts in its own sub-community within this community
167
+ const subComm = new Map();
168
+ let subId = nextCommId;
169
+ for (const member of members) {
170
+ subComm.set(member, subId++);
171
+ }
172
+ // Precompute sub-community strengths
173
+ const subCommStrength = new Map();
174
+ for (const member of members) {
175
+ const sc = subComm.get(member);
176
+ subCommStrength.set(sc, nodeStrength.get(member) || 0);
177
+ }
178
+ // Local moving within this community only — iterate until convergence
179
+ for (let refIter = 0; refIter < 10; refIter++) {
180
+ let refMoved = false;
181
+ // Shuffle members
182
+ const shuffled = [...members];
183
+ for (let i = shuffled.length - 1; i > 0; i--) {
184
+ const j = Math.floor(Math.random() * (i + 1));
185
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
186
+ }
187
+ for (const node of shuffled) {
188
+ const currentSub = subComm.get(node);
189
+ const ki = nodeStrength.get(node) || 0;
190
+ const neighborSubWeights = new Map();
191
+ graph.forEachEdge(node, (_edge, attrs, source, target) => {
192
+ const neighbor = source === node ? target : source;
193
+ if (!memberSet.has(neighbor))
194
+ return; // Only within this community
195
+ const nSub = subComm.get(neighbor);
196
+ const weight = attrs[weightAttr] || 1.0;
197
+ neighborSubWeights.set(nSub, (neighborSubWeights.get(nSub) || 0) + weight);
198
+ });
199
+ const edgesToCurrent = neighborSubWeights.get(currentSub) || 0;
200
+ const currentSubStr = (subCommStrength.get(currentSub) || 0) - ki;
201
+ const removeLoss = edgesToCurrent / totalWeight - resolution * ki * currentSubStr / (2 * totalWeight * totalWeight);
202
+ let bestSub = currentSub;
203
+ let bestGain = 0;
204
+ for (const [sc, edgesToSub] of neighborSubWeights) {
205
+ if (sc === currentSub)
206
+ continue;
207
+ const scStr = subCommStrength.get(sc) || 0;
208
+ const addGain = edgesToSub / totalWeight - resolution * ki * scStr / (2 * totalWeight * totalWeight);
209
+ const netGain = addGain - removeLoss;
210
+ if (netGain > bestGain) {
211
+ bestGain = netGain;
212
+ bestSub = sc;
213
+ }
214
+ }
215
+ if (bestSub !== currentSub) {
216
+ subCommStrength.set(currentSub, (subCommStrength.get(currentSub) || 0) - ki);
217
+ subCommStrength.set(bestSub, (subCommStrength.get(bestSub) || 0) + ki);
218
+ subComm.set(node, bestSub);
219
+ refMoved = true;
220
+ }
221
+ }
222
+ if (!refMoved)
223
+ break;
224
+ }
225
+ // Check if refinement produced multiple sub-communities
226
+ const subGroups = new Map();
227
+ for (const member of members) {
228
+ const sc = subComm.get(member);
229
+ const group = subGroups.get(sc) || [];
230
+ group.push(member);
231
+ subGroups.set(sc, group);
232
+ }
233
+ if (subGroups.size > 1) {
234
+ // Apply the refined partition — each sub-community gets its own ID
235
+ for (const [subCommId, subMembers] of subGroups) {
236
+ for (const member of subMembers) {
237
+ nodeToComm.set(member, subCommId);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ else {
243
+ // Community is disconnected — split into connected components
244
+ for (let i = 1; i < components.length; i++) {
245
+ const newId = nextCommId++;
246
+ for (const node of components[i]) {
247
+ nodeToComm.set(node, newId);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ /**
254
+ * Find connected components within a subset of nodes on the graph.
255
+ * Only traverses edges where both endpoints are in the subset.
256
+ */
257
+ function findConnectedComponents(graph, memberSet, weightAttr) {
258
+ const visited = new Set();
259
+ const components = [];
260
+ for (const node of memberSet) {
261
+ if (visited.has(node))
262
+ continue;
263
+ const component = [];
264
+ const queue = [node];
265
+ visited.add(node);
266
+ while (queue.length > 0) {
267
+ const current = queue.shift();
268
+ component.push(current);
269
+ graph.forEachEdge(current, (_edge, attrs, source, target) => {
270
+ const neighbor = source === current ? target : source;
271
+ if (memberSet.has(neighbor) && !visited.has(neighbor)) {
272
+ const weight = attrs[weightAttr] || 1.0;
273
+ if (weight > 0) {
274
+ visited.add(neighbor);
275
+ queue.push(neighbor);
276
+ }
277
+ }
278
+ });
279
+ }
280
+ components.push(component);
281
+ }
282
+ return components;
283
+ }
284
+ /**
285
+ * Compute modularity of a partition.
286
+ * Q = (1/2m) Σ_ij [A_ij - γ * ki*kj / 2m] * δ(ci, cj)
287
+ */
288
+ function computeModularity(graph, communities, totalWeight, resolution, weightAttr) {
289
+ if (totalWeight === 0)
290
+ return 0;
291
+ // Sum of edge weights within communities
292
+ let internalWeight = 0;
293
+ graph.forEachEdge((_edge, attrs, source, target) => {
294
+ if (communities[source] === communities[target]) {
295
+ internalWeight += attrs[weightAttr] || 1.0;
296
+ }
297
+ });
298
+ // Sum of strengths per community
299
+ const commStrength = new Map();
300
+ for (const [node, comm] of Object.entries(communities)) {
301
+ let strength = 0;
302
+ graph.forEachEdge(node, (_edge, attrs) => {
303
+ strength += attrs[weightAttr] || 1.0;
304
+ });
305
+ commStrength.set(comm, (commStrength.get(comm) || 0) + strength);
306
+ }
307
+ let expectedWeight = 0;
308
+ for (const strength of commStrength.values()) {
309
+ expectedWeight += strength * strength;
310
+ }
311
+ return internalWeight / (2 * totalWeight) - resolution * expectedWeight / (4 * totalWeight * totalWeight);
312
+ }
@@ -0,0 +1,13 @@
1
+ import type Graph from "graphology";
2
+ import type { CommunityDetector, DetectorOptions, RawDetectionResult } from "./types";
3
+ /**
4
+ * Louvain community detection via graphology-communities-louvain.
5
+ * Deterministic, fast, returns modularity score. Resolution parameter
6
+ * controls granularity (higher = more, smaller communities).
7
+ */
8
+ export declare class LouvainDetector implements CommunityDetector {
9
+ readonly name = "louvain";
10
+ readonly supportsResolution = true;
11
+ detect(graph: Graph, options: DetectorOptions): RawDetectionResult;
12
+ }
13
+ //# sourceMappingURL=louvain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"louvain.d.ts","sourceRoot":"","sources":["../../src/community-detectors/louvain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtF;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,kBAAkB,QAAQ;IAEnC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,kBAAkB;CAUnE"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LouvainDetector = void 0;
7
+ const graphology_communities_louvain_1 = __importDefault(require("graphology-communities-louvain"));
8
+ /**
9
+ * Louvain community detection via graphology-communities-louvain.
10
+ * Deterministic, fast, returns modularity score. Resolution parameter
11
+ * controls granularity (higher = more, smaller communities).
12
+ */
13
+ class LouvainDetector {
14
+ constructor() {
15
+ this.name = "louvain";
16
+ this.supportsResolution = true;
17
+ }
18
+ detect(graph, options) {
19
+ const result = graphology_communities_louvain_1.default.detailed(graph, {
20
+ resolution: options.resolution,
21
+ getEdgeWeight: options.weightAttribute,
22
+ });
23
+ return {
24
+ communities: result.communities,
25
+ modularity: result.modularity,
26
+ };
27
+ }
28
+ }
29
+ exports.LouvainDetector = LouvainDetector;
@@ -0,0 +1,36 @@
1
+ import type Graph from "graphology";
2
+ /**
3
+ * Raw output from a community detection algorithm.
4
+ * Maps node IDs to integer community labels. Post-processing (dissolution,
5
+ * splitting, rescue, stable matching) is handled by detectCommunities().
6
+ */
7
+ export interface RawDetectionResult {
8
+ /** Node ID → community label */
9
+ communities: Record<string, number>;
10
+ /** Modularity score (null if the algorithm doesn't compute it) */
11
+ modularity: number | null;
12
+ }
13
+ /**
14
+ * Options passed to a detector's detect() method.
15
+ */
16
+ export interface DetectorOptions {
17
+ /** Louvain resolution parameter (ignored by algorithms that don't support it) */
18
+ resolution: number;
19
+ /** Edge weight attribute name on the graphology graph */
20
+ weightAttribute: string;
21
+ }
22
+ /**
23
+ * Strategy interface for community detection algorithms.
24
+ * Implementations wrap a specific algorithm (Louvain, label propagation, etc.)
25
+ * and produce raw community assignments. All post-processing is algorithm-agnostic
26
+ * and stays in detectCommunities().
27
+ */
28
+ export interface CommunityDetector {
29
+ /** Algorithm identifier (e.g., "louvain", "label-propagation") */
30
+ readonly name: string;
31
+ /** Whether this algorithm uses the resolution parameter */
32
+ readonly supportsResolution: boolean;
33
+ /** Run community detection on the given graph */
34
+ detect(graph: Graph, options: DetectorOptions): RawDetectionResult;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/community-detectors/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,kEAAkE;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,iDAAiD;IACjD,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,kBAAkB,CAAC;CACpE"}
@@ -1,4 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_TIMEOUT = void 0;
4
- exports.DEFAULT_TIMEOUT = 5000;
@@ -0,0 +1,13 @@
1
+ import type { EdgeResolver, EdgeResolverContext } from "./types";
2
+ import type { FunctionEdge } from "../graph";
3
+ /**
4
+ * Resolves direct function call edges.
5
+ * For each function's calls[] array, looks up callee names in the name→hash index.
6
+ * Creates edges from caller to all matching callees (handles name collisions).
7
+ * Self-calls are skipped.
8
+ */
9
+ export declare class CallEdgeResolver implements EdgeResolver {
10
+ readonly edgeTypes: readonly ["call"];
11
+ resolve(ctx: EdgeResolverContext): FunctionEdge[];
12
+ }
13
+ //# sourceMappingURL=call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/edge-resolvers/call.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IACnD,QAAQ,CAAC,SAAS,oBAAqB;IAEvC,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,YAAY,EAAE;CAqBlD"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallEdgeResolver = void 0;
4
+ /**
5
+ * Resolves direct function call edges.
6
+ * For each function's calls[] array, looks up callee names in the name→hash index.
7
+ * Creates edges from caller to all matching callees (handles name collisions).
8
+ * Self-calls are skipped.
9
+ */
10
+ class CallEdgeResolver {
11
+ constructor() {
12
+ this.edgeTypes = ["call"];
13
+ }
14
+ resolve(ctx) {
15
+ const edges = [];
16
+ for (const fn of ctx.functions) {
17
+ if (ctx.affectedHashes && !ctx.affectedHashes.has(fn.contentHash))
18
+ continue;
19
+ if (!fn.calls)
20
+ continue;
21
+ for (const calledName of fn.calls) {
22
+ const targetHashes = ctx.nameToHashes.get(calledName);
23
+ if (!targetHashes)
24
+ continue;
25
+ for (const targetHash of targetHashes) {
26
+ if (targetHash === fn.contentHash)
27
+ continue;
28
+ edges.push({
29
+ sourceHash: fn.contentHash,
30
+ targetHash,
31
+ edgeType: "call",
32
+ weight: ctx.config.edgeWeights.call,
33
+ });
34
+ }
35
+ }
36
+ }
37
+ return edges;
38
+ }
39
+ }
40
+ exports.CallEdgeResolver = CallEdgeResolver;