@optave/codegraph 3.11.0 → 3.11.1
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 +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +142 -76
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +133 -96
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +5 -2
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.js +28 -36
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +217 -90
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +198 -140
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +8 -2
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +56 -56
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +1 -1
|
@@ -227,6 +227,96 @@ interface HotspotsDataOpts {
|
|
|
227
227
|
noTests?: boolean;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
type HotspotEntry = {
|
|
231
|
+
name: string;
|
|
232
|
+
kind: string;
|
|
233
|
+
lineCount: number | null;
|
|
234
|
+
symbolCount: number | null;
|
|
235
|
+
importCount: number | null;
|
|
236
|
+
exportCount: number | null;
|
|
237
|
+
fanIn: number | null;
|
|
238
|
+
fanOut: number | null;
|
|
239
|
+
cohesion: number | null;
|
|
240
|
+
fileCount: number | null;
|
|
241
|
+
density: number;
|
|
242
|
+
coupling: number;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Compute density from either fileCount/symbolCount or lineCount/symbolCount. */
|
|
246
|
+
function computeHotspotDensity(
|
|
247
|
+
symbolCount: number | null,
|
|
248
|
+
fileCount: number | null,
|
|
249
|
+
lineCount: number | null,
|
|
250
|
+
): number {
|
|
251
|
+
if ((fileCount ?? 0) > 0) return (symbolCount || 0) / (fileCount ?? 1);
|
|
252
|
+
if ((lineCount ?? 0) > 0) return (symbolCount || 0) / (lineCount ?? 1);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Map a native-engine hotspot row (camelCase keys) to the public HotspotEntry shape. */
|
|
257
|
+
function mapNativeHotspotRow(r: {
|
|
258
|
+
name: string;
|
|
259
|
+
kind: string;
|
|
260
|
+
lineCount: number | null;
|
|
261
|
+
symbolCount: number | null;
|
|
262
|
+
importCount: number | null;
|
|
263
|
+
exportCount: number | null;
|
|
264
|
+
fanIn: number | null;
|
|
265
|
+
fanOut: number | null;
|
|
266
|
+
cohesion: number | null;
|
|
267
|
+
fileCount: number | null;
|
|
268
|
+
}): HotspotEntry {
|
|
269
|
+
return {
|
|
270
|
+
name: r.name,
|
|
271
|
+
kind: r.kind,
|
|
272
|
+
lineCount: r.lineCount,
|
|
273
|
+
symbolCount: r.symbolCount,
|
|
274
|
+
importCount: r.importCount,
|
|
275
|
+
exportCount: r.exportCount,
|
|
276
|
+
fanIn: r.fanIn,
|
|
277
|
+
fanOut: r.fanOut,
|
|
278
|
+
cohesion: r.cohesion,
|
|
279
|
+
fileCount: r.fileCount,
|
|
280
|
+
density: computeHotspotDensity(r.symbolCount, r.fileCount, r.lineCount),
|
|
281
|
+
coupling: (r.fanIn || 0) + (r.fanOut || 0),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Map a JS-path hotspot row (snake_case keys from SQLite) to the public HotspotEntry shape. */
|
|
286
|
+
function mapJsHotspotRow(r: HotspotRow): HotspotEntry {
|
|
287
|
+
return {
|
|
288
|
+
name: r.name,
|
|
289
|
+
kind: r.kind,
|
|
290
|
+
lineCount: r.line_count,
|
|
291
|
+
symbolCount: r.symbol_count,
|
|
292
|
+
importCount: r.import_count,
|
|
293
|
+
exportCount: r.export_count,
|
|
294
|
+
fanIn: r.fan_in,
|
|
295
|
+
fanOut: r.fan_out,
|
|
296
|
+
cohesion: r.cohesion,
|
|
297
|
+
fileCount: r.file_count,
|
|
298
|
+
density: computeHotspotDensity(r.symbol_count, r.file_count, r.line_count),
|
|
299
|
+
coupling: (r.fan_in || 0) + (r.fan_out || 0),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** ORDER BY clause for each ranking dimension (strategy pattern). */
|
|
304
|
+
const HOTSPOT_ORDER_BY: Record<string, string> = {
|
|
305
|
+
'fan-in': 'nm.fan_in DESC NULLS LAST',
|
|
306
|
+
'fan-out': 'nm.fan_out DESC NULLS LAST',
|
|
307
|
+
density: 'nm.symbol_count DESC NULLS LAST',
|
|
308
|
+
coupling: '(COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Build the JS-path SQL query for a given metric and test filter. */
|
|
312
|
+
function buildHotspotQuery(metric: string, testFilter: string): string {
|
|
313
|
+
const orderBy = HOTSPOT_ORDER_BY[metric] ?? HOTSPOT_ORDER_BY['fan-in'];
|
|
314
|
+
return `SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
315
|
+
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
316
|
+
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
317
|
+
WHERE n.kind = ? ${testFilter} ORDER BY ${orderBy} LIMIT ?`;
|
|
318
|
+
}
|
|
319
|
+
|
|
230
320
|
export function hotspotsData(
|
|
231
321
|
customDbPath?: string,
|
|
232
322
|
opts: HotspotsDataOpts = {},
|
|
@@ -242,96 +332,21 @@ export function hotspotsData(
|
|
|
242
332
|
const level = opts.level || 'file';
|
|
243
333
|
const limit = opts.limit || 10;
|
|
244
334
|
const noTests = opts.noTests || false;
|
|
245
|
-
|
|
246
335
|
const kind = level === 'directory' ? 'directory' : 'file';
|
|
247
336
|
|
|
248
|
-
const mapRow = (r: {
|
|
249
|
-
name: string;
|
|
250
|
-
kind: string;
|
|
251
|
-
lineCount: number | null;
|
|
252
|
-
symbolCount: number | null;
|
|
253
|
-
importCount: number | null;
|
|
254
|
-
exportCount: number | null;
|
|
255
|
-
fanIn: number | null;
|
|
256
|
-
fanOut: number | null;
|
|
257
|
-
cohesion: number | null;
|
|
258
|
-
fileCount: number | null;
|
|
259
|
-
}) => ({
|
|
260
|
-
name: r.name,
|
|
261
|
-
kind: r.kind,
|
|
262
|
-
lineCount: r.lineCount,
|
|
263
|
-
symbolCount: r.symbolCount,
|
|
264
|
-
importCount: r.importCount,
|
|
265
|
-
exportCount: r.exportCount,
|
|
266
|
-
fanIn: r.fanIn,
|
|
267
|
-
fanOut: r.fanOut,
|
|
268
|
-
cohesion: r.cohesion,
|
|
269
|
-
fileCount: r.fileCount,
|
|
270
|
-
density:
|
|
271
|
-
(r.fileCount ?? 0) > 0
|
|
272
|
-
? (r.symbolCount || 0) / (r.fileCount ?? 1)
|
|
273
|
-
: (r.lineCount ?? 0) > 0
|
|
274
|
-
? (r.symbolCount || 0) / (r.lineCount ?? 1)
|
|
275
|
-
: 0,
|
|
276
|
-
coupling: (r.fanIn || 0) + (r.fanOut || 0),
|
|
277
|
-
});
|
|
278
|
-
|
|
279
337
|
// ── Native fast path: single query instead of 4 eagerly prepared ──
|
|
280
338
|
if (nativeDb?.getHotspots) {
|
|
281
339
|
const rows = nativeDb.getHotspots(kind, metric, noTests, limit);
|
|
282
|
-
const hotspots = rows.map(
|
|
340
|
+
const hotspots = rows.map(mapNativeHotspotRow);
|
|
283
341
|
const base = { metric, level, limit, hotspots };
|
|
284
342
|
return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
|
|
285
343
|
}
|
|
286
344
|
|
|
287
345
|
// ── JS fallback ───────────────────────────────────────────────────
|
|
288
346
|
const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
293
|
-
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
294
|
-
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
295
|
-
WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
|
|
296
|
-
'fan-out': db.prepare(`
|
|
297
|
-
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
298
|
-
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
299
|
-
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
300
|
-
WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
|
|
301
|
-
density: db.prepare(`
|
|
302
|
-
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
303
|
-
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
304
|
-
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
305
|
-
WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
|
|
306
|
-
coupling: db.prepare(`
|
|
307
|
-
SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
|
|
308
|
-
nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
|
|
309
|
-
FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
|
|
310
|
-
WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const stmt = HOTSPOT_QUERIES[metric] ?? HOTSPOT_QUERIES['fan-in'];
|
|
314
|
-
const rows = stmt!.all(kind, limit);
|
|
315
|
-
|
|
316
|
-
const hotspots = rows.map((r) => ({
|
|
317
|
-
name: r.name,
|
|
318
|
-
kind: r.kind,
|
|
319
|
-
lineCount: r.line_count,
|
|
320
|
-
symbolCount: r.symbol_count,
|
|
321
|
-
importCount: r.import_count,
|
|
322
|
-
exportCount: r.export_count,
|
|
323
|
-
fanIn: r.fan_in,
|
|
324
|
-
fanOut: r.fan_out,
|
|
325
|
-
cohesion: r.cohesion,
|
|
326
|
-
fileCount: r.file_count,
|
|
327
|
-
density:
|
|
328
|
-
(r.file_count ?? 0) > 0
|
|
329
|
-
? (r.symbol_count || 0) / (r.file_count ?? 1)
|
|
330
|
-
: (r.line_count ?? 0) > 0
|
|
331
|
-
? (r.symbol_count || 0) / (r.line_count ?? 1)
|
|
332
|
-
: 0,
|
|
333
|
-
coupling: (r.fan_in || 0) + (r.fan_out || 0),
|
|
334
|
-
}));
|
|
347
|
+
const stmt = db.prepare(buildHotspotQuery(metric, testFilter));
|
|
348
|
+
const rows = stmt.all(kind, limit) as HotspotRow[];
|
|
349
|
+
const hotspots = rows.map(mapJsHotspotRow);
|
|
335
350
|
|
|
336
351
|
const base = { metric, level, limit, hotspots };
|
|
337
352
|
return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
|
|
@@ -532,6 +532,56 @@ function batchUpdateRoles(
|
|
|
532
532
|
})();
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
interface CallableNodeRow {
|
|
536
|
+
id: number;
|
|
537
|
+
name: string;
|
|
538
|
+
kind: string;
|
|
539
|
+
file: string;
|
|
540
|
+
fan_in: number;
|
|
541
|
+
fan_out: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Build the activeFiles set: files with at least one callable connected to the graph. */
|
|
545
|
+
function buildActiveFilesSet(rows: CallableNodeRow[]): Set<string> {
|
|
546
|
+
const activeFiles = new Set<string>();
|
|
547
|
+
for (const r of rows) {
|
|
548
|
+
if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
|
|
549
|
+
activeFiles.add(r.file);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return activeFiles;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Map callable rows to classifier input objects, attaching exported/prod-fan-in/active-file metadata. */
|
|
556
|
+
function buildClassifierInput(
|
|
557
|
+
rows: CallableNodeRow[],
|
|
558
|
+
exportedIds: Set<number>,
|
|
559
|
+
prodFanInMap: Map<number, number>,
|
|
560
|
+
activeFiles: Set<string>,
|
|
561
|
+
): Array<{
|
|
562
|
+
id: string;
|
|
563
|
+
name: string;
|
|
564
|
+
kind: string;
|
|
565
|
+
file: string;
|
|
566
|
+
fanIn: number;
|
|
567
|
+
fanOut: number;
|
|
568
|
+
isExported: boolean;
|
|
569
|
+
productionFanIn: number;
|
|
570
|
+
hasActiveFileSiblings: boolean | undefined;
|
|
571
|
+
}> {
|
|
572
|
+
return rows.map((r) => ({
|
|
573
|
+
id: String(r.id),
|
|
574
|
+
name: r.name,
|
|
575
|
+
kind: r.kind,
|
|
576
|
+
file: r.file,
|
|
577
|
+
fanIn: r.fan_in,
|
|
578
|
+
fanOut: r.fan_out,
|
|
579
|
+
isExported: exportedIds.has(r.id),
|
|
580
|
+
productionFanIn: prodFanInMap.get(r.id) || 0,
|
|
581
|
+
hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
|
|
535
585
|
function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSummary): RoleSummary {
|
|
536
586
|
// Leaf kinds (parameter, property) can never have callers/callees.
|
|
537
587
|
// Classify them directly as dead-leaf without the expensive fan-in/fan-out JOINs.
|
|
@@ -558,14 +608,7 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
|
|
|
558
608
|
) fo ON n.id = fo.source_id
|
|
559
609
|
WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')`,
|
|
560
610
|
)
|
|
561
|
-
.all() as
|
|
562
|
-
id: number;
|
|
563
|
-
name: string;
|
|
564
|
-
kind: string;
|
|
565
|
-
file: string;
|
|
566
|
-
fan_in: number;
|
|
567
|
-
fan_out: number;
|
|
568
|
-
}[];
|
|
611
|
+
.all() as CallableNodeRow[];
|
|
569
612
|
|
|
570
613
|
if (rows.length === 0 && leafRows.length === 0) return emptySummary;
|
|
571
614
|
|
|
@@ -629,28 +672,9 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
|
|
|
629
672
|
prodFanInMap.set(r.target_id, r.cnt);
|
|
630
673
|
}
|
|
631
674
|
|
|
632
|
-
// Files with at least one callable (non-constant) connected to the graph.
|
|
633
|
-
// Constants in these files are likely consumed locally via identifier reference.
|
|
634
|
-
const activeFiles = new Set<string>();
|
|
635
|
-
for (const r of rows) {
|
|
636
|
-
if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
|
|
637
|
-
activeFiles.add(r.file);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
675
|
// Delegate classification to the pure-logic classifier
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
name: r.name,
|
|
645
|
-
kind: r.kind,
|
|
646
|
-
file: r.file,
|
|
647
|
-
fanIn: r.fan_in,
|
|
648
|
-
fanOut: r.fan_out,
|
|
649
|
-
isExported: exportedIds.has(r.id),
|
|
650
|
-
productionFanIn: prodFanInMap.get(r.id) || 0,
|
|
651
|
-
hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
|
|
652
|
-
}));
|
|
653
|
-
|
|
676
|
+
const activeFiles = buildActiveFilesSet(rows);
|
|
677
|
+
const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
|
|
654
678
|
const roleMap = classifyRoles(classifierInput);
|
|
655
679
|
|
|
656
680
|
const { summary, idsByRole } = buildRoleSummary(rows, leafRows, roleMap, emptySummary);
|
|
@@ -733,14 +757,7 @@ function classifyNodeRolesIncremental(
|
|
|
733
757
|
WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')
|
|
734
758
|
AND n.file IN (${placeholders})`,
|
|
735
759
|
)
|
|
736
|
-
.all(...allAffectedFiles) as
|
|
737
|
-
id: number;
|
|
738
|
-
name: string;
|
|
739
|
-
kind: string;
|
|
740
|
-
file: string;
|
|
741
|
-
fan_in: number;
|
|
742
|
-
fan_out: number;
|
|
743
|
-
}[];
|
|
760
|
+
.all(...allAffectedFiles) as CallableNodeRow[];
|
|
744
761
|
|
|
745
762
|
if (rows.length === 0 && leafRows.length === 0) return emptySummary;
|
|
746
763
|
|
|
@@ -810,25 +827,8 @@ function classifyNodeRolesIncremental(
|
|
|
810
827
|
}
|
|
811
828
|
|
|
812
829
|
// 5. Classify affected nodes using global medians
|
|
813
|
-
const activeFiles =
|
|
814
|
-
|
|
815
|
-
if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
|
|
816
|
-
activeFiles.add(r.file);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const classifierInput = rows.map((r) => ({
|
|
821
|
-
id: String(r.id),
|
|
822
|
-
name: r.name,
|
|
823
|
-
kind: r.kind,
|
|
824
|
-
file: r.file,
|
|
825
|
-
fanIn: r.fan_in,
|
|
826
|
-
fanOut: r.fan_out,
|
|
827
|
-
isExported: exportedIds.has(r.id),
|
|
828
|
-
productionFanIn: prodFanInMap.get(r.id) || 0,
|
|
829
|
-
hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
|
|
830
|
-
}));
|
|
831
|
-
|
|
830
|
+
const activeFiles = buildActiveFilesSet(rows);
|
|
831
|
+
const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
|
|
832
832
|
const roleMap = classifyRoles(classifierInput, globalMedians);
|
|
833
833
|
|
|
834
834
|
// 6. Build summary (only for affected nodes) and update only those nodes
|
|
@@ -88,12 +88,10 @@ export function runLouvainUndirectedModularity(
|
|
|
88
88
|
optionsInput: LeidenOptions = {},
|
|
89
89
|
): LouvainResult {
|
|
90
90
|
const options: NormalizedOptions = normalizeOptions(optionsInput);
|
|
91
|
-
let currentGraph: CodeGraph = graph;
|
|
92
|
-
const levels: LevelEntry[] = [];
|
|
93
91
|
const rngSource = createRng(options.randomSeed);
|
|
94
92
|
const random: () => number = () => rngSource.nextDouble();
|
|
95
93
|
|
|
96
|
-
const baseGraphAdapter: GraphAdapter = makeGraphAdapter(
|
|
94
|
+
const baseGraphAdapter: GraphAdapter = makeGraphAdapter(graph, {
|
|
97
95
|
directed: options.directed,
|
|
98
96
|
...optionsInput,
|
|
99
97
|
});
|
|
@@ -101,98 +99,27 @@ export function runLouvainUndirectedModularity(
|
|
|
101
99
|
const originalToCurrent = new Int32Array(origN);
|
|
102
100
|
for (let i = 0; i < origN; i++) originalToCurrent[i] = i;
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
if (options.fixedNodes) {
|
|
106
|
-
const fixed = new Uint8Array(origN);
|
|
107
|
-
const asSet: Set<string> =
|
|
108
|
-
options.fixedNodes instanceof Set ? options.fixedNodes : new Set(options.fixedNodes);
|
|
109
|
-
for (const id of asSet) {
|
|
110
|
-
const idx = baseGraphAdapter.idToIndex.get(String(id));
|
|
111
|
-
if (idx != null) fixed[idx] = 1;
|
|
112
|
-
}
|
|
113
|
-
fixedNodeMask = fixed;
|
|
114
|
-
}
|
|
102
|
+
const fixedNodeMask: Uint8Array | null = buildFixedNodeMask(baseGraphAdapter, options.fixedNodes);
|
|
115
103
|
|
|
104
|
+
const levels: LevelEntry[] = [];
|
|
105
|
+
let currentGraph: CodeGraph = graph;
|
|
116
106
|
for (let level = 0; level < options.maxLevels; level++) {
|
|
117
107
|
const graphAdapter: GraphAdapter =
|
|
118
108
|
level === 0
|
|
119
109
|
? baseGraphAdapter
|
|
120
110
|
: makeGraphAdapter(currentGraph, { directed: options.directed, ...optionsInput });
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let improved: boolean = true;
|
|
129
|
-
let localPasses: number = 0;
|
|
130
|
-
const strategyCode: CandidateStrategyCode = options.candidateStrategyCode;
|
|
131
|
-
while (improved) {
|
|
132
|
-
improved = false;
|
|
133
|
-
localPasses++;
|
|
134
|
-
shuffleArrayInPlace(order, random);
|
|
135
|
-
for (let idx = 0; idx < order.length; idx++) {
|
|
136
|
-
const nodeIndex: number = order[idx]!;
|
|
137
|
-
if (level === 0 && fixedNodeMask && fixedNodeMask[nodeIndex]) continue;
|
|
138
|
-
const candidateCount: number = partition.accumulateNeighborCommunityEdgeWeights(nodeIndex);
|
|
139
|
-
const { bestCommunityId, bestGain } = findBestCommunityMove(
|
|
140
|
-
partition,
|
|
141
|
-
graphAdapter,
|
|
142
|
-
nodeIndex,
|
|
143
|
-
candidateCount,
|
|
144
|
-
strategyCode,
|
|
145
|
-
options,
|
|
146
|
-
random,
|
|
147
|
-
);
|
|
148
|
-
if (bestCommunityId !== partition.nodeCommunity[nodeIndex]! && bestGain > GAIN_EPSILON) {
|
|
149
|
-
partition.moveNodeToCommunity(nodeIndex, bestCommunityId);
|
|
150
|
-
improved = true;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (localPasses >= options.maxLocalPasses) break;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
renumberCommunities(partition, options.preserveLabels);
|
|
157
|
-
|
|
158
|
-
let effectivePartition: Partition = partition;
|
|
159
|
-
if (options.refine) {
|
|
160
|
-
const refined: Partition = refineWithinCoarseCommunities(
|
|
161
|
-
graphAdapter,
|
|
162
|
-
partition,
|
|
163
|
-
random,
|
|
164
|
-
options,
|
|
165
|
-
level === 0 ? fixedNodeMask : null,
|
|
166
|
-
);
|
|
167
|
-
// Post-refinement: split any disconnected communities into their
|
|
168
|
-
// connected components. This is the cheap O(V+E) alternative to
|
|
169
|
-
// checking gamma-connectedness on every candidate during refinement.
|
|
170
|
-
// A disconnected community violates even basic connectivity, so
|
|
171
|
-
// splitting is always correct.
|
|
172
|
-
splitDisconnectedCommunities(graphAdapter, refined);
|
|
173
|
-
renumberCommunities(refined, options.preserveLabels);
|
|
174
|
-
effectivePartition = refined;
|
|
175
|
-
}
|
|
111
|
+
const levelOutcome = runLevel(
|
|
112
|
+
graphAdapter,
|
|
113
|
+
options,
|
|
114
|
+
random,
|
|
115
|
+
level === 0 ? fixedNodeMask : null,
|
|
116
|
+
);
|
|
176
117
|
|
|
177
|
-
levels.push({ graph: graphAdapter, partition: effectivePartition });
|
|
178
|
-
|
|
179
|
-
for (let i = 0; i < originalToCurrent.length; i++) {
|
|
180
|
-
originalToCurrent[i] = fineToCoarse[originalToCurrent[i]!]!;
|
|
181
|
-
}
|
|
118
|
+
levels.push({ graph: graphAdapter, partition: levelOutcome.effectivePartition });
|
|
119
|
+
applyFineToCoarseMapping(originalToCurrent, levelOutcome.effectivePartition.nodeCommunity);
|
|
182
120
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// effective partition that feeds buildCoarseGraph (would coarsening
|
|
186
|
-
// actually reduce the graph?). When refine is enabled the refined
|
|
187
|
-
// partition starts from singletons and may have more communities than
|
|
188
|
-
// the move phase found, so checking only effectivePartition would
|
|
189
|
-
// cause premature termination.
|
|
190
|
-
if (
|
|
191
|
-
partition.communityCount === graphAdapter.n &&
|
|
192
|
-
effectivePartition.communityCount === graphAdapter.n
|
|
193
|
-
)
|
|
194
|
-
break;
|
|
195
|
-
currentGraph = buildCoarseGraph(graphAdapter, effectivePartition);
|
|
121
|
+
if (levelOutcome.terminate) break;
|
|
122
|
+
currentGraph = buildCoarseGraph(graphAdapter, levelOutcome.effectivePartition);
|
|
196
123
|
}
|
|
197
124
|
|
|
198
125
|
const last: LevelEntry = levels[levels.length - 1]!;
|
|
@@ -206,6 +133,134 @@ export function runLouvainUndirectedModularity(
|
|
|
206
133
|
};
|
|
207
134
|
}
|
|
208
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Build a fixed-node mask aligned with the base graph adapter's node indices.
|
|
138
|
+
* Returns null when no fixed nodes are configured.
|
|
139
|
+
*/
|
|
140
|
+
function buildFixedNodeMask(
|
|
141
|
+
baseGraphAdapter: GraphAdapter,
|
|
142
|
+
fixedNodes: Set<string> | string[] | undefined,
|
|
143
|
+
): Uint8Array | null {
|
|
144
|
+
if (!fixedNodes) return null;
|
|
145
|
+
const mask = new Uint8Array(baseGraphAdapter.n);
|
|
146
|
+
const asSet: Set<string> = fixedNodes instanceof Set ? fixedNodes : new Set(fixedNodes);
|
|
147
|
+
for (const id of asSet) {
|
|
148
|
+
const idx = baseGraphAdapter.idToIndex.get(String(id));
|
|
149
|
+
if (idx != null) mask[idx] = 1;
|
|
150
|
+
}
|
|
151
|
+
return mask;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface LevelOutcome {
|
|
155
|
+
effectivePartition: Partition;
|
|
156
|
+
terminate: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run one level of the Louvain/Leiden pipeline: greedy local-move phase,
|
|
161
|
+
* optional Leiden refinement, and a termination check. Returns the
|
|
162
|
+
* partition that feeds the next coarse graph plus a `terminate` flag set
|
|
163
|
+
* when no further coarsening is possible.
|
|
164
|
+
*/
|
|
165
|
+
function runLevel(
|
|
166
|
+
graphAdapter: GraphAdapter,
|
|
167
|
+
options: NormalizedOptions,
|
|
168
|
+
random: () => number,
|
|
169
|
+
fixedNodeMask: Uint8Array | null,
|
|
170
|
+
): LevelOutcome {
|
|
171
|
+
const partition: Partition = makePartition(graphAdapter);
|
|
172
|
+
partition.graph = graphAdapter;
|
|
173
|
+
partition.initializeAggregates();
|
|
174
|
+
|
|
175
|
+
runLocalMovePhase(graphAdapter, partition, options, random, fixedNodeMask);
|
|
176
|
+
renumberCommunities(partition, options.preserveLabels);
|
|
177
|
+
|
|
178
|
+
let effectivePartition: Partition = partition;
|
|
179
|
+
if (options.refine) {
|
|
180
|
+
const refined: Partition = refineWithinCoarseCommunities(
|
|
181
|
+
graphAdapter,
|
|
182
|
+
partition,
|
|
183
|
+
random,
|
|
184
|
+
options,
|
|
185
|
+
fixedNodeMask,
|
|
186
|
+
);
|
|
187
|
+
// Post-refinement: split any disconnected communities into their
|
|
188
|
+
// connected components. This is the cheap O(V+E) alternative to
|
|
189
|
+
// checking gamma-connectedness on every candidate during refinement.
|
|
190
|
+
// A disconnected community violates even basic connectivity, so
|
|
191
|
+
// splitting is always correct.
|
|
192
|
+
splitDisconnectedCommunities(graphAdapter, refined);
|
|
193
|
+
renumberCommunities(refined, options.preserveLabels);
|
|
194
|
+
effectivePartition = refined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Terminate when no further coarsening is possible. Check both the
|
|
198
|
+
// move-phase partition (did the greedy phase find merges?) and the
|
|
199
|
+
// effective partition that feeds buildCoarseGraph (would coarsening
|
|
200
|
+
// actually reduce the graph?). When refine is enabled the refined
|
|
201
|
+
// partition starts from singletons and may have more communities than
|
|
202
|
+
// the move phase found, so checking only effectivePartition would
|
|
203
|
+
// cause premature termination.
|
|
204
|
+
const terminate =
|
|
205
|
+
partition.communityCount === graphAdapter.n &&
|
|
206
|
+
effectivePartition.communityCount === graphAdapter.n;
|
|
207
|
+
return { effectivePartition, terminate };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Greedy local-move phase: iterate randomly over nodes, moving each to the
|
|
212
|
+
* best community among the candidate set. Loops until no improvement or
|
|
213
|
+
* `maxLocalPasses` is reached.
|
|
214
|
+
*/
|
|
215
|
+
function runLocalMovePhase(
|
|
216
|
+
graphAdapter: GraphAdapter,
|
|
217
|
+
partition: Partition,
|
|
218
|
+
options: NormalizedOptions,
|
|
219
|
+
random: () => number,
|
|
220
|
+
fixedNodeMask: Uint8Array | null,
|
|
221
|
+
): void {
|
|
222
|
+
const order = new Int32Array(graphAdapter.n);
|
|
223
|
+
for (let i = 0; i < graphAdapter.n; i++) order[i] = i;
|
|
224
|
+
|
|
225
|
+
const strategyCode: CandidateStrategyCode = options.candidateStrategyCode;
|
|
226
|
+
let improved: boolean = true;
|
|
227
|
+
let localPasses: number = 0;
|
|
228
|
+
while (improved) {
|
|
229
|
+
improved = false;
|
|
230
|
+
localPasses++;
|
|
231
|
+
shuffleArrayInPlace(order, random);
|
|
232
|
+
for (let idx = 0; idx < order.length; idx++) {
|
|
233
|
+
const nodeIndex: number = order[idx]!;
|
|
234
|
+
if (fixedNodeMask?.[nodeIndex]) continue;
|
|
235
|
+
const candidateCount: number = partition.accumulateNeighborCommunityEdgeWeights(nodeIndex);
|
|
236
|
+
const { bestCommunityId, bestGain } = findBestCommunityMove(
|
|
237
|
+
partition,
|
|
238
|
+
graphAdapter,
|
|
239
|
+
nodeIndex,
|
|
240
|
+
candidateCount,
|
|
241
|
+
strategyCode,
|
|
242
|
+
options,
|
|
243
|
+
random,
|
|
244
|
+
);
|
|
245
|
+
if (bestCommunityId !== partition.nodeCommunity[nodeIndex]! && bestGain > GAIN_EPSILON) {
|
|
246
|
+
partition.moveNodeToCommunity(nodeIndex, bestCommunityId);
|
|
247
|
+
improved = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (localPasses >= options.maxLocalPasses) break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Compose the running `originalToCurrent` mapping with this level's
|
|
256
|
+
* fine→coarse community labels, in place.
|
|
257
|
+
*/
|
|
258
|
+
function applyFineToCoarseMapping(originalToCurrent: Int32Array, fineToCoarse: Int32Array): void {
|
|
259
|
+
for (let i = 0; i < originalToCurrent.length; i++) {
|
|
260
|
+
originalToCurrent[i] = fineToCoarse[originalToCurrent[i]!]!;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
209
264
|
/**
|
|
210
265
|
* Evaluate all candidate communities for a node and return the best move.
|
|
211
266
|
* Encapsulates the four candidate-selection strategies (All, RandomAny,
|