@ophan/core 0.0.1 → 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.
- package/README.md +56 -93
- package/dist/community-detectors/index.d.ts +20 -0
- package/dist/community-detectors/index.d.ts.map +1 -0
- package/dist/community-detectors/index.js +45 -0
- package/dist/community-detectors/label-prop.d.ts +20 -0
- package/dist/community-detectors/label-prop.d.ts.map +1 -0
- package/dist/community-detectors/label-prop.js +77 -0
- package/dist/community-detectors/leiden.d.ts +22 -0
- package/dist/community-detectors/leiden.d.ts.map +1 -0
- package/dist/community-detectors/leiden.js +312 -0
- package/dist/community-detectors/louvain.d.ts +13 -0
- package/dist/community-detectors/louvain.d.ts.map +1 -0
- package/dist/community-detectors/louvain.js +29 -0
- package/dist/community-detectors/types.d.ts +36 -0
- package/dist/community-detectors/types.d.ts.map +1 -0
- package/dist/{parsers/__fixtures__/no-functions.js → community-detectors/types.js} +0 -2
- package/dist/edge-resolvers/call.d.ts +13 -0
- package/dist/edge-resolvers/call.d.ts.map +1 -0
- package/dist/edge-resolvers/call.js +40 -0
- package/dist/edge-resolvers/co-location.d.ts +16 -0
- package/dist/edge-resolvers/co-location.d.ts.map +1 -0
- package/dist/edge-resolvers/co-location.js +129 -0
- package/dist/edge-resolvers/import.d.ts +16 -0
- package/dist/edge-resolvers/import.d.ts.map +1 -0
- package/dist/edge-resolvers/import.js +118 -0
- package/dist/edge-resolvers/index.d.ts +9 -0
- package/dist/edge-resolvers/index.d.ts.map +1 -0
- package/dist/edge-resolvers/index.js +29 -0
- package/dist/edge-resolvers/jsx-ref.d.ts +13 -0
- package/dist/edge-resolvers/jsx-ref.d.ts.map +1 -0
- package/dist/edge-resolvers/jsx-ref.js +40 -0
- package/dist/edge-resolvers/types.d.ts +40 -0
- package/dist/edge-resolvers/types.d.ts.map +1 -0
- package/dist/edge-resolvers/types.js +2 -0
- package/dist/graph.d.ts +293 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +1295 -0
- package/dist/index.d.ts +37 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +385 -183
- package/dist/migrations.d.ts +25 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +323 -0
- package/dist/module-resolvers/index.d.ts +11 -0
- package/dist/module-resolvers/index.d.ts.map +1 -0
- package/dist/module-resolvers/index.js +67 -0
- package/dist/module-resolvers/javascript.d.ts +18 -0
- package/dist/module-resolvers/javascript.d.ts.map +1 -0
- package/dist/module-resolvers/javascript.js +130 -0
- package/dist/module-resolvers/types.d.ts +18 -0
- package/dist/module-resolvers/types.d.ts.map +1 -0
- package/dist/module-resolvers/types.js +2 -0
- package/dist/parsers/python.d.ts.map +1 -1
- package/dist/parsers/python.js +38 -4
- package/dist/parsers/typescript.d.ts.map +1 -1
- package/dist/parsers/typescript.js +133 -0
- package/dist/practices.d.ts +28 -0
- package/dist/practices.d.ts.map +1 -0
- package/dist/practices.js +95 -0
- package/dist/schemas.d.ts +251 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +121 -6
- package/dist/shared.d.ts +8 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/summarize.d.ts +165 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +1067 -0
- package/ophan_logo.png +0 -0
- package/package.json +11 -2
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -492
- package/dist/parsers/__fixtures__/arrow-functions.d.ts +0 -5
- package/dist/parsers/__fixtures__/arrow-functions.d.ts.map +0 -1
- package/dist/parsers/__fixtures__/arrow-functions.js +0 -16
- package/dist/parsers/__fixtures__/class-methods.d.ts +0 -6
- package/dist/parsers/__fixtures__/class-methods.d.ts.map +0 -1
- package/dist/parsers/__fixtures__/class-methods.js +0 -12
- package/dist/parsers/__fixtures__/no-functions.d.ts +0 -9
- package/dist/parsers/__fixtures__/no-functions.d.ts.map +0 -1
- package/dist/parsers/python.test.d.ts +0 -2
- package/dist/parsers/python.test.d.ts.map +0 -1
- package/dist/parsers/python.test.js +0 -96
- package/dist/parsers/typescript.test.d.ts +0 -2
- package/dist/parsers/typescript.test.d.ts.map +0 -1
- package/dist/parsers/typescript.test.js +0 -106
- package/dist/test-utils.d.ts +0 -46
- package/dist/test-utils.d.ts.map +0 -1
- package/dist/test-utils.js +0 -141
|
@@ -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"}
|
|
@@ -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;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EdgeResolver, EdgeResolverContext } from "./types";
|
|
2
|
+
import type { FunctionEdge } from "../graph";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves co-location edges — proximity-based connections between functions
|
|
5
|
+
* in the same file or same directory.
|
|
6
|
+
*
|
|
7
|
+
* Same-file: all-pairs with weight decaying by positional distance (1/offset).
|
|
8
|
+
* Directory (cross-file): half the same-file weight, also decaying by offset.
|
|
9
|
+
*
|
|
10
|
+
* Owns a shared dedup Set to prevent duplicate edges across both phases.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CoLocationEdgeResolver implements EdgeResolver {
|
|
13
|
+
readonly edgeTypes: readonly ["co_location"];
|
|
14
|
+
resolve(ctx: EdgeResolverContext): FunctionEdge[];
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=co-location.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-location.d.ts","sourceRoot":"","sources":["../../src/edge-resolvers/co-location.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,YAAW,YAAY;IACzD,QAAQ,CAAC,SAAS,2BAA4B;IAE9C,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,YAAY,EAAE;CAsFlD"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CoLocationEdgeResolver = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
/**
|
|
39
|
+
* Resolves co-location edges — proximity-based connections between functions
|
|
40
|
+
* in the same file or same directory.
|
|
41
|
+
*
|
|
42
|
+
* Same-file: all-pairs with weight decaying by positional distance (1/offset).
|
|
43
|
+
* Directory (cross-file): half the same-file weight, also decaying by offset.
|
|
44
|
+
*
|
|
45
|
+
* Owns a shared dedup Set to prevent duplicate edges across both phases.
|
|
46
|
+
*/
|
|
47
|
+
class CoLocationEdgeResolver {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.edgeTypes = ["co_location"];
|
|
50
|
+
}
|
|
51
|
+
resolve(ctx) {
|
|
52
|
+
const edges = [];
|
|
53
|
+
const colocAdded = new Set();
|
|
54
|
+
// Phase 1: Same-file co-location
|
|
55
|
+
for (const fn of ctx.functions) {
|
|
56
|
+
if (ctx.affectedHashes && !ctx.affectedHashes.has(fn.contentHash))
|
|
57
|
+
continue;
|
|
58
|
+
const sortedSiblings = ctx.fileToSortedFns.get(fn.filePath);
|
|
59
|
+
if (!sortedSiblings || sortedSiblings.length <= 1)
|
|
60
|
+
continue;
|
|
61
|
+
const idx = sortedSiblings.indexOf(fn);
|
|
62
|
+
if (idx < 0)
|
|
63
|
+
continue;
|
|
64
|
+
for (let offset = 1; offset < sortedSiblings.length - idx; offset++) {
|
|
65
|
+
const neighbor = sortedSiblings[idx + offset];
|
|
66
|
+
if (!neighbor)
|
|
67
|
+
break;
|
|
68
|
+
const a = fn.contentHash < neighbor.contentHash
|
|
69
|
+
? fn.contentHash
|
|
70
|
+
: neighbor.contentHash;
|
|
71
|
+
const b = fn.contentHash < neighbor.contentHash
|
|
72
|
+
? neighbor.contentHash
|
|
73
|
+
: fn.contentHash;
|
|
74
|
+
const key = `${a}|${b}`;
|
|
75
|
+
if (!colocAdded.has(key)) {
|
|
76
|
+
colocAdded.add(key);
|
|
77
|
+
edges.push({
|
|
78
|
+
sourceHash: a,
|
|
79
|
+
targetHash: b,
|
|
80
|
+
edgeType: "co_location",
|
|
81
|
+
weight: ctx.config.edgeWeights.co_location / offset,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Phase 2: Directory co-location (cross-file, same directory)
|
|
87
|
+
const dirToFns = new Map();
|
|
88
|
+
for (const fn of ctx.functions) {
|
|
89
|
+
const dir = path.dirname(fn.filePath);
|
|
90
|
+
const list = dirToFns.get(dir) || [];
|
|
91
|
+
list.push(fn);
|
|
92
|
+
dirToFns.set(dir, list);
|
|
93
|
+
}
|
|
94
|
+
const dirWeight = ctx.config.edgeWeights.co_location * 0.5;
|
|
95
|
+
for (const fns of dirToFns.values()) {
|
|
96
|
+
const files = new Set(fns.map((f) => f.filePath));
|
|
97
|
+
if (files.size < 2)
|
|
98
|
+
continue;
|
|
99
|
+
fns.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
100
|
+
for (let i = 0; i < fns.length; i++) {
|
|
101
|
+
if (ctx.affectedHashes && !ctx.affectedHashes.has(fns[i].contentHash))
|
|
102
|
+
continue;
|
|
103
|
+
for (let j = i + 1; j < fns.length; j++) {
|
|
104
|
+
if (fns[i].filePath === fns[j].filePath)
|
|
105
|
+
continue;
|
|
106
|
+
const a = fns[i].contentHash < fns[j].contentHash
|
|
107
|
+
? fns[i].contentHash
|
|
108
|
+
: fns[j].contentHash;
|
|
109
|
+
const b = fns[i].contentHash < fns[j].contentHash
|
|
110
|
+
? fns[j].contentHash
|
|
111
|
+
: fns[i].contentHash;
|
|
112
|
+
const key = `${a}|${b}`;
|
|
113
|
+
if (!colocAdded.has(key)) {
|
|
114
|
+
colocAdded.add(key);
|
|
115
|
+
const offset = j - i;
|
|
116
|
+
edges.push({
|
|
117
|
+
sourceHash: a,
|
|
118
|
+
targetHash: b,
|
|
119
|
+
edgeType: "co_location",
|
|
120
|
+
weight: dirWeight / offset,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return edges;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.CoLocationEdgeResolver = CoLocationEdgeResolver;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EdgeResolver, EdgeResolverContext } from "./types";
|
|
2
|
+
import type { FunctionEdge } from "../graph";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves import edges from import statements.
|
|
5
|
+
* Three resolution paths:
|
|
6
|
+
* 1. Direct: imported name found in the resolved target file
|
|
7
|
+
* 2. Barrel fallback: imported name found via package-scoped search (re-exports)
|
|
8
|
+
* 3. Namespace fallback: intersect fn.calls with target file's functions
|
|
9
|
+
*
|
|
10
|
+
* Owns importAdded Set for deduplication (import edges are undirected).
|
|
11
|
+
*/
|
|
12
|
+
export declare class ImportEdgeResolver implements EdgeResolver {
|
|
13
|
+
readonly edgeTypes: readonly ["import"];
|
|
14
|
+
resolve(ctx: EdgeResolverContext): FunctionEdge[];
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/edge-resolvers/import.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,QAAQ,CAAC,SAAS,sBAAuB;IAEzC,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,YAAY,EAAE;CAqHlD"}
|