@ophan/core 0.0.2 → 0.0.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.
- 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 +9 -2
- 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
|
@@ -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"}
|
|
@@ -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;
|