@optave/codegraph 3.11.0 → 3.11.2
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/call-resolver.d.ts +71 -0
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
- package/dist/domain/graph/builder/call-resolver.js +130 -0
- package/dist/domain/graph/builder/call-resolver.js.map +1 -0
- 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 +214 -127
- 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 +151 -192
- 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 +10 -4
- 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.d.ts.map +1 -1
- package/dist/features/structure.js +149 -52
- 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/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +9 -9
- 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/call-resolver.ts +181 -0
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +321 -152
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +229 -275
- 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 +12 -4
- 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 +199 -79
- 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
|
@@ -31,44 +31,36 @@ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
|
|
|
31
31
|
|
|
32
32
|
// ─── Halstead Metrics Computation ─────────────────────────────────────────
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Skip type annotation subtrees
|
|
48
|
-
if (rules?.skipTypes.has(node.type)) return;
|
|
34
|
+
/** Classify a tree-sitter node as a Halstead operator or operand,
|
|
35
|
+
* updating the running counts. Pure helper extracted from computeHalsteadMetrics
|
|
36
|
+
* to keep the dispatcher thin. */
|
|
37
|
+
function classifyHalsteadToken(
|
|
38
|
+
node: TreeSitterNode,
|
|
39
|
+
rules: HalsteadRules,
|
|
40
|
+
operators: Map<string, number>,
|
|
41
|
+
operands: Map<string, number>,
|
|
42
|
+
): void {
|
|
43
|
+
// Compound operators (non-leaf): count the node type as an operator
|
|
44
|
+
if (rules.compoundOperators.has(node.type)) {
|
|
45
|
+
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Leaf nodes: classify as operator or operand
|
|
49
|
+
if (node.childCount === 0) {
|
|
50
|
+
if (rules.operatorLeafTypes.has(node.type)) {
|
|
52
51
|
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (node.childCount === 0) {
|
|
57
|
-
if (rules?.operatorLeafTypes.has(node.type)) {
|
|
58
|
-
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
59
|
-
} else if (rules?.operandLeafTypes.has(node.type)) {
|
|
60
|
-
const text = node.text;
|
|
61
|
-
operands.set(text, (operands.get(text) || 0) + 1);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
66
|
-
walk(node.child(i));
|
|
52
|
+
} else if (rules.operandLeafTypes.has(node.type)) {
|
|
53
|
+
const text = node.text;
|
|
54
|
+
operands.set(text, (operands.get(text) || 0) + 1);
|
|
67
55
|
}
|
|
68
56
|
}
|
|
57
|
+
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
/** Build a HalsteadDerivedMetrics summary from the raw operator/operand counts. */
|
|
60
|
+
function summarizeHalsteadCounts(
|
|
61
|
+
operators: Map<string, number>,
|
|
62
|
+
operands: Map<string, number>,
|
|
63
|
+
): HalsteadDerivedMetrics {
|
|
72
64
|
const n1 = operators.size; // distinct operators
|
|
73
65
|
const n2 = operands.size; // distinct operands
|
|
74
66
|
let bigN1 = 0; // total operators
|
|
@@ -79,7 +71,6 @@ export function computeHalsteadMetrics(
|
|
|
79
71
|
const vocabulary = n1 + n2;
|
|
80
72
|
const length = bigN1 + bigN2;
|
|
81
73
|
|
|
82
|
-
// Guard against zero
|
|
83
74
|
const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
|
|
84
75
|
const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
|
|
85
76
|
const effort = difficulty * volume;
|
|
@@ -99,6 +90,31 @@ export function computeHalsteadMetrics(
|
|
|
99
90
|
};
|
|
100
91
|
}
|
|
101
92
|
|
|
93
|
+
export function computeHalsteadMetrics(
|
|
94
|
+
functionNode: TreeSitterNode,
|
|
95
|
+
language: string,
|
|
96
|
+
): HalsteadDerivedMetrics | null {
|
|
97
|
+
const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
|
|
98
|
+
if (!rules) return null;
|
|
99
|
+
|
|
100
|
+
const operators = new Map<string, number>(); // type -> count
|
|
101
|
+
const operands = new Map<string, number>(); // text -> count
|
|
102
|
+
|
|
103
|
+
function walk(node: TreeSitterNode | null): void {
|
|
104
|
+
if (!node) return;
|
|
105
|
+
// Skip type annotation subtrees
|
|
106
|
+
if (rules?.skipTypes.has(node.type)) return;
|
|
107
|
+
classifyHalsteadToken(node, rules as HalsteadRules, operators, operands);
|
|
108
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
109
|
+
walk(node.child(i));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
walk(functionNode);
|
|
114
|
+
|
|
115
|
+
return summarizeHalsteadCounts(operators, operands);
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
// ─── LOC Metrics Computation ──────────────────────────────────────────────
|
|
103
119
|
// Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
|
|
104
120
|
export const computeLOCMetrics = _computeLOCMetrics;
|
|
@@ -535,6 +551,89 @@ function upsertAstComplexity(
|
|
|
535
551
|
return 1;
|
|
536
552
|
}
|
|
537
553
|
|
|
554
|
+
/** Decision outcome for a single definition during native bulk-row collection.
|
|
555
|
+
* - 'skip': the definition is legitimately ignorable (non-function, missing line,
|
|
556
|
+
* interface stub, unsupported language).
|
|
557
|
+
* - 'fallback': a genuine function body is missing precomputed complexity —
|
|
558
|
+
* the whole native fast path must abort to JS.
|
|
559
|
+
* - 'emit': the definition has complexity data and a row was (or will be) appended. */
|
|
560
|
+
type NativeRowDecision = 'skip' | 'fallback' | 'emit';
|
|
561
|
+
|
|
562
|
+
/** Classify a definition relative to the native bulk path. Returns
|
|
563
|
+
* 'skip' to ignore it, 'fallback' to bail out, or 'emit' if the row should be added. */
|
|
564
|
+
function classifyDefinitionForNativeBulk(
|
|
565
|
+
def: FileSymbols['definitions'][0],
|
|
566
|
+
langSupported: boolean,
|
|
567
|
+
): NativeRowDecision {
|
|
568
|
+
if (def.kind !== 'function' && def.kind !== 'method') return 'skip';
|
|
569
|
+
if (!def.line) return 'skip';
|
|
570
|
+
if (!def.complexity) {
|
|
571
|
+
// Interface/type property signatures and single-line stubs are extracted
|
|
572
|
+
// as methods but the native engine correctly never assigns complexity.
|
|
573
|
+
// Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
|
|
574
|
+
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
575
|
+
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) return 'skip';
|
|
576
|
+
// Languages without complexity rules will never have data — skip them
|
|
577
|
+
// rather than bailing out of the entire native bulk path.
|
|
578
|
+
if (!langSupported) return 'skip';
|
|
579
|
+
return 'fallback'; // genuine function body missing complexity — needs JS fallback
|
|
580
|
+
}
|
|
581
|
+
return 'emit';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Build a single native-bulk row from a definition with complexity data. */
|
|
585
|
+
function buildNativeBulkRow(
|
|
586
|
+
nodeId: number,
|
|
587
|
+
def: FileSymbols['definitions'][0],
|
|
588
|
+
): Record<string, unknown> {
|
|
589
|
+
const ch = def.complexity?.halstead;
|
|
590
|
+
const cl = def.complexity?.loc;
|
|
591
|
+
return {
|
|
592
|
+
nodeId,
|
|
593
|
+
cognitive: def.complexity?.cognitive ?? 0,
|
|
594
|
+
cyclomatic: def.complexity?.cyclomatic ?? 0,
|
|
595
|
+
maxNesting: def.complexity?.maxNesting ?? 0,
|
|
596
|
+
loc: cl ? cl.loc : 0,
|
|
597
|
+
sloc: cl ? cl.sloc : 0,
|
|
598
|
+
commentLines: cl ? cl.commentLines : 0,
|
|
599
|
+
halsteadN1: ch ? ch.n1 : 0,
|
|
600
|
+
halsteadN2: ch ? ch.n2 : 0,
|
|
601
|
+
halsteadBigN1: ch ? ch.bigN1 : 0,
|
|
602
|
+
halsteadBigN2: ch ? ch.bigN2 : 0,
|
|
603
|
+
halsteadVocabulary: ch ? ch.vocabulary : 0,
|
|
604
|
+
halsteadLength: ch ? ch.length : 0,
|
|
605
|
+
halsteadVolume: ch ? ch.volume : 0,
|
|
606
|
+
halsteadDifficulty: ch ? ch.difficulty : 0,
|
|
607
|
+
halsteadEffort: ch ? ch.effort : 0,
|
|
608
|
+
halsteadBugs: ch ? ch.bugs : 0,
|
|
609
|
+
maintainabilityIndex: def.complexity?.maintainabilityIndex ?? 0,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Try to collect a single file's definitions into native-bulk rows.
|
|
614
|
+
* Returns 'fallback' if any definition forces a JS fallback. */
|
|
615
|
+
function collectFileBulkRows(
|
|
616
|
+
db: BetterSqlite3Database,
|
|
617
|
+
relPath: string,
|
|
618
|
+
symbols: FileSymbols,
|
|
619
|
+
rows: Array<Record<string, unknown>>,
|
|
620
|
+
): NativeRowDecision {
|
|
621
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
622
|
+
const langId = symbols._langId || '';
|
|
623
|
+
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
624
|
+
|
|
625
|
+
for (const def of symbols.definitions) {
|
|
626
|
+
const decision = classifyDefinitionForNativeBulk(def, langSupported);
|
|
627
|
+
if (decision === 'skip') continue;
|
|
628
|
+
if (decision === 'fallback') return 'fallback';
|
|
629
|
+
|
|
630
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
631
|
+
if (!nodeId) continue;
|
|
632
|
+
rows.push(buildNativeBulkRow(nodeId, def));
|
|
633
|
+
}
|
|
634
|
+
return 'emit';
|
|
635
|
+
}
|
|
636
|
+
|
|
538
637
|
/** Collect native bulk-insert rows from precomputed complexity data.
|
|
539
638
|
* Returns the rows array, or null if any definition is missing complexity
|
|
540
639
|
* (signalling that JS fallback is needed). */
|
|
@@ -543,53 +642,9 @@ function collectNativeBulkRows(
|
|
|
543
642
|
fileSymbols: Map<string, FileSymbols>,
|
|
544
643
|
): Array<Record<string, unknown>> | null {
|
|
545
644
|
const rows: Array<Record<string, unknown>> = [];
|
|
546
|
-
|
|
547
645
|
for (const [relPath, symbols] of fileSymbols) {
|
|
548
|
-
|
|
549
|
-
const langId = symbols._langId || '';
|
|
550
|
-
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
551
|
-
|
|
552
|
-
for (const def of symbols.definitions) {
|
|
553
|
-
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
554
|
-
if (!def.line) continue;
|
|
555
|
-
// Interface/type property signatures and single-line stubs are extracted
|
|
556
|
-
// as methods but the native engine correctly never assigns complexity.
|
|
557
|
-
// Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
|
|
558
|
-
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
559
|
-
if (!def.complexity) {
|
|
560
|
-
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
|
|
561
|
-
// Languages without complexity rules will never have data — skip them
|
|
562
|
-
// rather than bailing out of the entire native bulk path.
|
|
563
|
-
if (!langSupported) continue;
|
|
564
|
-
return null; // genuine function body missing complexity — needs JS fallback
|
|
565
|
-
}
|
|
566
|
-
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
567
|
-
if (!nodeId) continue;
|
|
568
|
-
const ch = def.complexity.halstead;
|
|
569
|
-
const cl = def.complexity.loc;
|
|
570
|
-
rows.push({
|
|
571
|
-
nodeId,
|
|
572
|
-
cognitive: def.complexity.cognitive ?? 0,
|
|
573
|
-
cyclomatic: def.complexity.cyclomatic ?? 0,
|
|
574
|
-
maxNesting: def.complexity.maxNesting ?? 0,
|
|
575
|
-
loc: cl ? cl.loc : 0,
|
|
576
|
-
sloc: cl ? cl.sloc : 0,
|
|
577
|
-
commentLines: cl ? cl.commentLines : 0,
|
|
578
|
-
halsteadN1: ch ? ch.n1 : 0,
|
|
579
|
-
halsteadN2: ch ? ch.n2 : 0,
|
|
580
|
-
halsteadBigN1: ch ? ch.bigN1 : 0,
|
|
581
|
-
halsteadBigN2: ch ? ch.bigN2 : 0,
|
|
582
|
-
halsteadVocabulary: ch ? ch.vocabulary : 0,
|
|
583
|
-
halsteadLength: ch ? ch.length : 0,
|
|
584
|
-
halsteadVolume: ch ? ch.volume : 0,
|
|
585
|
-
halsteadDifficulty: ch ? ch.difficulty : 0,
|
|
586
|
-
halsteadEffort: ch ? ch.effort : 0,
|
|
587
|
-
halsteadBugs: ch ? ch.bugs : 0,
|
|
588
|
-
maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
646
|
+
if (collectFileBulkRows(db, relPath, symbols, rows) === 'fallback') return null;
|
|
591
647
|
}
|
|
592
|
-
|
|
593
648
|
return rows;
|
|
594
649
|
}
|
|
595
650
|
|
package/src/features/dataflow.ts
CHANGED
|
@@ -675,6 +675,51 @@ interface BfsParentEntry {
|
|
|
675
675
|
expression: string;
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
type DataflowNeighbor = {
|
|
679
|
+
id: number;
|
|
680
|
+
file: string;
|
|
681
|
+
edge_kind: string;
|
|
682
|
+
expression: string;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
interface DataflowBfsState {
|
|
686
|
+
visited: Set<number>;
|
|
687
|
+
parent: Map<number, BfsParentEntry>;
|
|
688
|
+
nextQueue: number[];
|
|
689
|
+
found: boolean;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Process a single neighbor in the dataflow BFS. Returns true once the target
|
|
694
|
+
* has been reached so the caller can stop expanding.
|
|
695
|
+
*/
|
|
696
|
+
function processDataflowNeighbor(
|
|
697
|
+
n: DataflowNeighbor,
|
|
698
|
+
currentId: number,
|
|
699
|
+
targetId: number,
|
|
700
|
+
noTests: boolean,
|
|
701
|
+
state: DataflowBfsState,
|
|
702
|
+
): boolean {
|
|
703
|
+
if (noTests && isTestFile(n.file)) return false;
|
|
704
|
+
const entry: BfsParentEntry = {
|
|
705
|
+
parentId: currentId,
|
|
706
|
+
edgeKind: n.edge_kind,
|
|
707
|
+
expression: n.expression,
|
|
708
|
+
};
|
|
709
|
+
if (n.id === targetId) {
|
|
710
|
+
if (!state.found) {
|
|
711
|
+
state.found = true;
|
|
712
|
+
state.parent.set(n.id, entry);
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
if (state.visited.has(n.id)) return false;
|
|
717
|
+
state.visited.add(n.id);
|
|
718
|
+
state.parent.set(n.id, entry);
|
|
719
|
+
state.nextQueue.push(n.id);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
678
723
|
/** BFS through dataflow edges to find a path from source to target. */
|
|
679
724
|
function bfsDataflowPath(
|
|
680
725
|
db: BetterSqlite3Database,
|
|
@@ -689,50 +734,28 @@ function bfsDataflowPath(
|
|
|
689
734
|
WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
|
|
690
735
|
);
|
|
691
736
|
|
|
692
|
-
const
|
|
693
|
-
|
|
737
|
+
const state: DataflowBfsState = {
|
|
738
|
+
visited: new Set<number>([sourceId]),
|
|
739
|
+
parent: new Map<number, BfsParentEntry>(),
|
|
740
|
+
nextQueue: [],
|
|
741
|
+
found: false,
|
|
742
|
+
};
|
|
694
743
|
let queue = [sourceId];
|
|
695
|
-
let found = false;
|
|
696
744
|
|
|
697
745
|
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
698
|
-
|
|
746
|
+
state.nextQueue = [];
|
|
699
747
|
for (const currentId of queue) {
|
|
700
|
-
const neighbors = neighborStmt.all(currentId) as
|
|
701
|
-
id: number;
|
|
702
|
-
file: string;
|
|
703
|
-
edge_kind: string;
|
|
704
|
-
expression: string;
|
|
705
|
-
}>;
|
|
748
|
+
const neighbors = neighborStmt.all(currentId) as DataflowNeighbor[];
|
|
706
749
|
for (const n of neighbors) {
|
|
707
|
-
|
|
708
|
-
if (n.id === targetId) {
|
|
709
|
-
if (!found) {
|
|
710
|
-
found = true;
|
|
711
|
-
parent.set(n.id, {
|
|
712
|
-
parentId: currentId,
|
|
713
|
-
edgeKind: n.edge_kind,
|
|
714
|
-
expression: n.expression,
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (!visited.has(n.id)) {
|
|
720
|
-
visited.add(n.id);
|
|
721
|
-
parent.set(n.id, {
|
|
722
|
-
parentId: currentId,
|
|
723
|
-
edgeKind: n.edge_kind,
|
|
724
|
-
expression: n.expression,
|
|
725
|
-
});
|
|
726
|
-
nextQueue.push(n.id);
|
|
727
|
-
}
|
|
750
|
+
processDataflowNeighbor(n, currentId, targetId, noTests, state);
|
|
728
751
|
}
|
|
729
752
|
}
|
|
730
|
-
if (found) break;
|
|
731
|
-
queue = nextQueue;
|
|
753
|
+
if (state.found) break;
|
|
754
|
+
queue = state.nextQueue;
|
|
732
755
|
if (queue.length === 0) break;
|
|
733
756
|
}
|
|
734
757
|
|
|
735
|
-
return found ? parent : null;
|
|
758
|
+
return state.found ? state.parent : null;
|
|
736
759
|
}
|
|
737
760
|
|
|
738
761
|
/** Reconstruct a path from BFS parent map. */
|
package/src/features/flow.ts
CHANGED
|
@@ -133,6 +133,41 @@ interface BfsState {
|
|
|
133
133
|
truncated: boolean;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
interface FlowBfsFrame {
|
|
137
|
+
visited: Set<number>;
|
|
138
|
+
cycles: Array<{ from: string; to: string; depth: number }>;
|
|
139
|
+
nodeDepths: Map<number, number>;
|
|
140
|
+
idToNode: Map<number, NodeInfo>;
|
|
141
|
+
nextFrontier: number[];
|
|
142
|
+
levelNodes: NodeInfo[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Process one callee row, recording cycle hits or expanding frontier. */
|
|
146
|
+
function processFlowCallee(
|
|
147
|
+
c: CalleeRow,
|
|
148
|
+
fid: number,
|
|
149
|
+
depth: number,
|
|
150
|
+
noTests: boolean,
|
|
151
|
+
frame: FlowBfsFrame,
|
|
152
|
+
): void {
|
|
153
|
+
if (noTests && isTestFile(c.file)) return;
|
|
154
|
+
|
|
155
|
+
if (frame.visited.has(c.id)) {
|
|
156
|
+
const fromNode = frame.idToNode.get(fid);
|
|
157
|
+
if (fromNode) {
|
|
158
|
+
frame.cycles.push({ from: fromNode.name, to: c.name, depth });
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
frame.visited.add(c.id);
|
|
164
|
+
frame.nextFrontier.push(c.id);
|
|
165
|
+
const nodeInfo: NodeInfo = toSymbolRef(c);
|
|
166
|
+
frame.levelNodes.push(nodeInfo);
|
|
167
|
+
frame.nodeDepths.set(c.id, depth);
|
|
168
|
+
frame.idToNode.set(c.id, nodeInfo);
|
|
169
|
+
}
|
|
170
|
+
|
|
136
171
|
/** Forward BFS through callees, collecting steps, cycles, and node depth info. */
|
|
137
172
|
function bfsCallees(
|
|
138
173
|
db: ReturnType<typeof openReadonlyOrFail>,
|
|
@@ -157,37 +192,26 @@ function bfsCallees(
|
|
|
157
192
|
);
|
|
158
193
|
|
|
159
194
|
for (let d = 1; d <= maxDepth; d++) {
|
|
160
|
-
const
|
|
161
|
-
|
|
195
|
+
const frame: FlowBfsFrame = {
|
|
196
|
+
visited,
|
|
197
|
+
cycles,
|
|
198
|
+
nodeDepths,
|
|
199
|
+
idToNode,
|
|
200
|
+
nextFrontier: [],
|
|
201
|
+
levelNodes: [],
|
|
202
|
+
};
|
|
162
203
|
|
|
163
204
|
for (const fid of frontier) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
for (const c of callees) {
|
|
167
|
-
if (noTests && isTestFile(c.file)) continue;
|
|
168
|
-
|
|
169
|
-
if (visited.has(c.id)) {
|
|
170
|
-
const fromNode = idToNode.get(fid);
|
|
171
|
-
if (fromNode) {
|
|
172
|
-
cycles.push({ from: fromNode.name, to: c.name, depth: d });
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
visited.add(c.id);
|
|
178
|
-
nextFrontier.push(c.id);
|
|
179
|
-
const nodeInfo: NodeInfo = toSymbolRef(c);
|
|
180
|
-
levelNodes.push(nodeInfo);
|
|
181
|
-
nodeDepths.set(c.id, d);
|
|
182
|
-
idToNode.set(c.id, nodeInfo);
|
|
205
|
+
for (const c of calleesStmt.all(fid)) {
|
|
206
|
+
processFlowCallee(c, fid, d, noTests, frame);
|
|
183
207
|
}
|
|
184
208
|
}
|
|
185
209
|
|
|
186
|
-
if (levelNodes.length > 0) {
|
|
187
|
-
steps.push({ depth: d, nodes: levelNodes });
|
|
210
|
+
if (frame.levelNodes.length > 0) {
|
|
211
|
+
steps.push({ depth: d, nodes: frame.levelNodes });
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
frontier = nextFrontier;
|
|
214
|
+
frontier = frame.nextFrontier;
|
|
191
215
|
if (frontier.length === 0) break;
|
|
192
216
|
if (d === maxDepth && frontier.length > 0) truncated = true;
|
|
193
217
|
}
|
|
@@ -336,13 +336,13 @@ interface FileLevelEdge {
|
|
|
336
336
|
target: string;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
/** Load file-level import/call edges from the DB and optionally exclude test files. */
|
|
340
|
+
function loadFileLevelEdges(
|
|
340
341
|
db: BetterSqlite3Database,
|
|
341
342
|
noTests: boolean,
|
|
342
343
|
minConf: number,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
let edges = db
|
|
344
|
+
): FileLevelEdge[] {
|
|
345
|
+
const edges = db
|
|
346
346
|
.prepare<FileLevelEdge>(
|
|
347
347
|
`
|
|
348
348
|
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
@@ -354,73 +354,118 @@ function prepareFileLevelData(
|
|
|
354
354
|
`,
|
|
355
355
|
)
|
|
356
356
|
.all(minConf);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const files = new Set<string>();
|
|
360
|
-
for (const { source, target } of edges) {
|
|
361
|
-
files.add(source);
|
|
362
|
-
files.add(target);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const fileIds = new Map<string, number>();
|
|
366
|
-
let idx = 0;
|
|
367
|
-
for (const f of files) fileIds.set(f, idx++);
|
|
357
|
+
return noTests ? edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target)) : edges;
|
|
358
|
+
}
|
|
368
359
|
|
|
369
|
-
|
|
360
|
+
/** Compute fan-in and fan-out for each file from a list of edges. */
|
|
361
|
+
function computeFileFanCounts(edges: FileLevelEdge[]): {
|
|
362
|
+
fanInCount: Map<string, number>;
|
|
363
|
+
fanOutCount: Map<string, number>;
|
|
364
|
+
} {
|
|
370
365
|
const fanInCount = new Map<string, number>();
|
|
371
366
|
const fanOutCount = new Map<string, number>();
|
|
372
367
|
for (const { source, target } of edges) {
|
|
373
368
|
fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
|
|
374
369
|
fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
|
|
375
370
|
}
|
|
371
|
+
return { fanInCount, fanOutCount };
|
|
372
|
+
}
|
|
376
373
|
|
|
377
|
-
|
|
374
|
+
/** Run Louvain community detection on the file-level graph. Returns empty map on failure. */
|
|
375
|
+
function detectFileCommunities(files: Set<string>, edges: FileLevelEdge[]): Map<string, number> {
|
|
378
376
|
const communityMap = new Map<string, number>();
|
|
379
|
-
if (files.size
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
const { assignments } = louvainCommunities(fileGraph);
|
|
388
|
-
for (const [file, cid] of assignments) communityMap.set(file, cid);
|
|
389
|
-
} catch {
|
|
390
|
-
// ignore
|
|
377
|
+
if (files.size === 0) return communityMap;
|
|
378
|
+
try {
|
|
379
|
+
const fileGraph = new CodeGraph();
|
|
380
|
+
for (const f of files) fileGraph.addNode(f);
|
|
381
|
+
for (const { source, target } of edges) {
|
|
382
|
+
if (source !== target && !fileGraph.hasEdge(source, target))
|
|
383
|
+
fileGraph.addEdge(source, target);
|
|
391
384
|
}
|
|
385
|
+
const { assignments } = louvainCommunities(fileGraph);
|
|
386
|
+
for (const [file, cid] of assignments) communityMap.set(file, cid);
|
|
387
|
+
} catch {
|
|
388
|
+
// louvain can fail on disconnected graphs
|
|
392
389
|
}
|
|
390
|
+
return communityMap;
|
|
391
|
+
}
|
|
393
392
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
393
|
+
/** Build a VisNode for a single file, applying color based on cfg.colorBy. */
|
|
394
|
+
function buildFileVisNode(
|
|
395
|
+
file: string,
|
|
396
|
+
id: number,
|
|
397
|
+
community: number | null,
|
|
398
|
+
fanIn: number,
|
|
399
|
+
fanOut: number,
|
|
400
|
+
cfg: PlotConfig,
|
|
401
|
+
): VisNode {
|
|
402
|
+
const color: string =
|
|
403
|
+
cfg.colorBy === 'community' && community !== null
|
|
404
|
+
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
|
|
405
|
+
: cfg.nodeColors?.file || (DEFAULT_NODE_COLORS as Record<string, string>).file || '#ccc';
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
id,
|
|
409
|
+
label: path.basename(file),
|
|
410
|
+
title: file,
|
|
411
|
+
color,
|
|
412
|
+
kind: 'file',
|
|
413
|
+
role: '',
|
|
414
|
+
file,
|
|
415
|
+
line: 0,
|
|
416
|
+
community,
|
|
417
|
+
cognitive: null,
|
|
418
|
+
cyclomatic: null,
|
|
419
|
+
maintainabilityIndex: null,
|
|
420
|
+
fanIn,
|
|
421
|
+
fanOut,
|
|
422
|
+
directory: path.dirname(file),
|
|
423
|
+
risk: [],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Select seed node IDs for the file-level graph based on configured strategy. */
|
|
428
|
+
function selectFileSeedNodes(visNodes: VisNode[], cfg: PlotConfig): (number | string)[] {
|
|
429
|
+
if (cfg.seedStrategy === 'top-fanin') {
|
|
430
|
+
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
431
|
+
return sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
432
|
+
}
|
|
433
|
+
// Both 'entry' and the default fallback include every node — file-level graphs
|
|
434
|
+
// don't track per-file roles, so 'entry' has no meaningful filter.
|
|
435
|
+
return visNodes.map((n) => n.id);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function prepareFileLevelData(
|
|
439
|
+
db: BetterSqlite3Database,
|
|
440
|
+
noTests: boolean,
|
|
441
|
+
minConf: number,
|
|
442
|
+
cfg: PlotConfig,
|
|
443
|
+
): GraphData {
|
|
444
|
+
const edges = loadFileLevelEdges(db, noTests, minConf);
|
|
445
|
+
|
|
446
|
+
const files = new Set<string>();
|
|
447
|
+
for (const { source, target } of edges) {
|
|
448
|
+
files.add(source);
|
|
449
|
+
files.add(target);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const fileIds = new Map<string, number>();
|
|
453
|
+
let idx = 0;
|
|
454
|
+
for (const f of files) fileIds.set(f, idx++);
|
|
455
|
+
|
|
456
|
+
const { fanInCount, fanOutCount } = computeFileFanCounts(edges);
|
|
457
|
+
const communityMap = detectFileCommunities(files, edges);
|
|
458
|
+
|
|
459
|
+
const visNodes: VisNode[] = [...files].map((f) =>
|
|
460
|
+
buildFileVisNode(
|
|
461
|
+
f,
|
|
462
|
+
fileIds.get(f)!,
|
|
463
|
+
communityMap.get(f) ?? null,
|
|
464
|
+
fanInCount.get(f) || 0,
|
|
465
|
+
fanOutCount.get(f) || 0,
|
|
466
|
+
cfg,
|
|
467
|
+
),
|
|
468
|
+
);
|
|
424
469
|
|
|
425
470
|
const visEdges: VisEdge[] = edges.map(({ source, target }, i) => ({
|
|
426
471
|
id: `e${i}`,
|
|
@@ -428,17 +473,7 @@ function prepareFileLevelData(
|
|
|
428
473
|
to: fileIds.get(target)!,
|
|
429
474
|
}));
|
|
430
475
|
|
|
431
|
-
|
|
432
|
-
if (cfg.seedStrategy === 'top-fanin') {
|
|
433
|
-
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
434
|
-
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
435
|
-
} else if (cfg.seedStrategy === 'entry') {
|
|
436
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
437
|
-
} else {
|
|
438
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return { nodes: visNodes, edges: visEdges, seedNodeIds };
|
|
476
|
+
return { nodes: visNodes, edges: visEdges, seedNodeIds: selectFileSeedNodes(visNodes, cfg) };
|
|
442
477
|
}
|
|
443
478
|
|
|
444
479
|
// ─── HTML Generation (thin wrapper) ──────────────────────────────────
|