@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
|
@@ -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,
|
|
@@ -78,6 +78,68 @@ export interface RoleClassificationNode {
|
|
|
78
78
|
hasActiveFileSiblings?: boolean;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Compute median fan-in and fan-out across nodes with non-zero values.
|
|
83
|
+
* Used as thresholds for "high" fan-in/out classification.
|
|
84
|
+
*/
|
|
85
|
+
function computeFanMedians(nodes: RoleClassificationNode[]): { fanIn: number; fanOut: number } {
|
|
86
|
+
const nonZeroFanIn = nodes
|
|
87
|
+
.filter((n) => n.fanIn > 0)
|
|
88
|
+
.map((n) => n.fanIn)
|
|
89
|
+
.sort((a, b) => a - b);
|
|
90
|
+
const nonZeroFanOut = nodes
|
|
91
|
+
.filter((n) => n.fanOut > 0)
|
|
92
|
+
.map((n) => n.fanOut)
|
|
93
|
+
.sort((a, b) => a - b);
|
|
94
|
+
return { fanIn: median(nonZeroFanIn), fanOut: median(nonZeroFanOut) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Classify a node with `fanIn === 0` that is not exported.
|
|
99
|
+
* Covers framework-active constants, test-only callables, and the dead-* family.
|
|
100
|
+
*/
|
|
101
|
+
function classifyUnreferencedNode(node: RoleClassificationNode): Role {
|
|
102
|
+
if (node.kind === 'constant' && node.hasActiveFileSiblings) {
|
|
103
|
+
// Constants consumed via identifier reference (not calls) have no
|
|
104
|
+
// inbound call edges. If the same file has active callables, the
|
|
105
|
+
// constant is almost certainly used locally — classify as leaf.
|
|
106
|
+
return 'leaf';
|
|
107
|
+
}
|
|
108
|
+
if (node.testOnlyFanIn != null && node.testOnlyFanIn > 0) return 'test-only';
|
|
109
|
+
return classifyDeadSubRole(node);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pick a role from fan-in/fan-out shape: core/utility/adapter/leaf.
|
|
114
|
+
* Called after entry/test-only/dead cases have been ruled out.
|
|
115
|
+
*/
|
|
116
|
+
function classifyByFanShape(highIn: boolean, highOut: boolean): Role {
|
|
117
|
+
if (highIn && !highOut) return 'core';
|
|
118
|
+
if (highIn && highOut) return 'utility';
|
|
119
|
+
if (!highIn && highOut) return 'adapter';
|
|
120
|
+
return 'leaf';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Apply role-classification rules to a single node.
|
|
125
|
+
* Order matters — framework entries are tagged first, then dead/test cases,
|
|
126
|
+
* then the fan-in/fan-out shape decides among the structural roles.
|
|
127
|
+
*/
|
|
128
|
+
function classifyNodeRole(node: RoleClassificationNode, medFanIn: number, medFanOut: number): Role {
|
|
129
|
+
if (FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p))) return 'entry';
|
|
130
|
+
|
|
131
|
+
if (node.fanIn === 0) {
|
|
132
|
+
return node.isExported ? 'entry' : classifyUnreferencedNode(node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hasProdFanIn = typeof node.productionFanIn === 'number';
|
|
136
|
+
if (hasProdFanIn && node.productionFanIn === 0 && !node.isExported) return 'test-only';
|
|
137
|
+
|
|
138
|
+
const highIn = node.fanIn >= medFanIn;
|
|
139
|
+
const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
|
|
140
|
+
return classifyByFanShape(highIn, highOut);
|
|
141
|
+
}
|
|
142
|
+
|
|
81
143
|
/**
|
|
82
144
|
* Classify nodes into architectural roles based on fan-in/fan-out metrics.
|
|
83
145
|
*/
|
|
@@ -87,63 +149,11 @@ export function classifyRoles(
|
|
|
87
149
|
): Map<string, Role> {
|
|
88
150
|
if (nodes.length === 0) return new Map();
|
|
89
151
|
|
|
90
|
-
|
|
91
|
-
let medFanOut: number;
|
|
92
|
-
if (medianOverrides) {
|
|
93
|
-
medFanIn = medianOverrides.fanIn;
|
|
94
|
-
medFanOut = medianOverrides.fanOut;
|
|
95
|
-
} else {
|
|
96
|
-
const nonZeroFanIn = nodes
|
|
97
|
-
.filter((n) => n.fanIn > 0)
|
|
98
|
-
.map((n) => n.fanIn)
|
|
99
|
-
.sort((a, b) => a - b);
|
|
100
|
-
const nonZeroFanOut = nodes
|
|
101
|
-
.filter((n) => n.fanOut > 0)
|
|
102
|
-
.map((n) => n.fanOut)
|
|
103
|
-
.sort((a, b) => a - b);
|
|
104
|
-
medFanIn = median(nonZeroFanIn);
|
|
105
|
-
medFanOut = median(nonZeroFanOut);
|
|
106
|
-
}
|
|
152
|
+
const { fanIn: medFanIn, fanOut: medFanOut } = medianOverrides ?? computeFanMedians(nodes);
|
|
107
153
|
|
|
108
154
|
const result = new Map<string, Role>();
|
|
109
|
-
|
|
110
155
|
for (const node of nodes) {
|
|
111
|
-
|
|
112
|
-
const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
|
|
113
|
-
const hasProdFanIn = typeof node.productionFanIn === 'number';
|
|
114
|
-
|
|
115
|
-
let role: Role;
|
|
116
|
-
const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p));
|
|
117
|
-
if (isFrameworkEntry) {
|
|
118
|
-
role = 'entry';
|
|
119
|
-
} else if (node.fanIn === 0 && !node.isExported) {
|
|
120
|
-
if (node.kind === 'constant' && node.hasActiveFileSiblings) {
|
|
121
|
-
// Constants consumed via identifier reference (not calls) have no
|
|
122
|
-
// inbound call edges. If the same file has active callables, the
|
|
123
|
-
// constant is almost certainly used locally — classify as leaf.
|
|
124
|
-
role = 'leaf';
|
|
125
|
-
} else {
|
|
126
|
-
role =
|
|
127
|
-
node.testOnlyFanIn != null && node.testOnlyFanIn > 0
|
|
128
|
-
? 'test-only'
|
|
129
|
-
: classifyDeadSubRole(node);
|
|
130
|
-
}
|
|
131
|
-
} else if (node.fanIn === 0 && node.isExported) {
|
|
132
|
-
role = 'entry';
|
|
133
|
-
} else if (hasProdFanIn && node.fanIn > 0 && node.productionFanIn === 0 && !node.isExported) {
|
|
134
|
-
role = 'test-only';
|
|
135
|
-
} else if (highIn && !highOut) {
|
|
136
|
-
role = 'core';
|
|
137
|
-
} else if (highIn && highOut) {
|
|
138
|
-
role = 'utility';
|
|
139
|
-
} else if (!highIn && highOut) {
|
|
140
|
-
role = 'adapter';
|
|
141
|
-
} else {
|
|
142
|
-
role = 'leaf';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
result.set(node.id, role);
|
|
156
|
+
result.set(node.id, classifyNodeRole(node, medFanIn, medFanOut));
|
|
146
157
|
}
|
|
147
|
-
|
|
148
158
|
return result;
|
|
149
159
|
}
|
|
@@ -30,7 +30,7 @@ export const DEFAULTS = {
|
|
|
30
30
|
defaultLimit: 20,
|
|
31
31
|
excludeTests: false,
|
|
32
32
|
},
|
|
33
|
-
embeddings: { model:
|
|
33
|
+
embeddings: { model: null as string | null, llmProvider: null as string | null },
|
|
34
34
|
llm: {
|
|
35
35
|
provider: null as string | null,
|
|
36
36
|
model: null as string | null,
|
package/src/presentation/cfg.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { cfgData, cfgToDOT, cfgToMermaid } from '../features/cfg.js';
|
|
2
2
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
3
|
|
|
4
|
+
type CfgData = ReturnType<typeof cfgData>;
|
|
5
|
+
|
|
4
6
|
interface CfgCliOpts {
|
|
5
7
|
json?: boolean;
|
|
6
8
|
ndjson?: boolean;
|
|
@@ -36,13 +38,56 @@ interface CfgResultEntry {
|
|
|
36
38
|
edges: CfgEdge[];
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function renderBlockLocation(b: CfgBlock): string {
|
|
42
|
+
if (!b.startLine) return '';
|
|
43
|
+
const endSuffix = b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : '';
|
|
44
|
+
return ` L${b.startLine}${endSuffix}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printCfgBlocks(blocks: CfgBlock[]): void {
|
|
48
|
+
if (blocks.length === 0) return;
|
|
49
|
+
console.log('\n Blocks:');
|
|
50
|
+
for (const b of blocks) {
|
|
51
|
+
const label = b.label ? ` (${b.label})` : '';
|
|
52
|
+
console.log(` [${b.index}] ${b.type}${label}${renderBlockLocation(b)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printCfgEdges(edges: CfgEdge[]): void {
|
|
57
|
+
if (edges.length === 0) return;
|
|
58
|
+
console.log('\n Edges:');
|
|
59
|
+
for (const e of edges) {
|
|
60
|
+
console.log(` B${e.source} → B${e.target} [${e.kind}]`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printCfgEntry(r: CfgResultEntry): void {
|
|
65
|
+
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
|
|
66
|
+
console.log('─'.repeat(60));
|
|
67
|
+
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
|
|
68
|
+
printCfgBlocks(r.blocks);
|
|
69
|
+
printCfgEdges(r.edges);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tryRenderGraphFormat(format: string, data: CfgData): boolean {
|
|
73
|
+
if (format === 'dot') {
|
|
74
|
+
console.log(cfgToDOT(data));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (format === 'mermaid') {
|
|
78
|
+
console.log(cfgToMermaid(data));
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
39
84
|
export function cfg(name: string, customDbPath: string | undefined, opts: CfgCliOpts = {}): void {
|
|
40
85
|
const data = cfgData(name, customDbPath, opts);
|
|
41
86
|
|
|
42
87
|
if (outputResult(data, 'results', opts)) return;
|
|
43
88
|
|
|
44
89
|
if (data.warning) {
|
|
45
|
-
console.log(
|
|
90
|
+
console.log(`⚠ ${data.warning}`);
|
|
46
91
|
return;
|
|
47
92
|
}
|
|
48
93
|
if (data.results.length === 0) {
|
|
@@ -50,38 +95,9 @@ export function cfg(name: string, customDbPath: string | undefined, opts: CfgCli
|
|
|
50
95
|
return;
|
|
51
96
|
}
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
if (format === 'dot') {
|
|
55
|
-
console.log(cfgToDOT(data));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (format === 'mermaid') {
|
|
59
|
-
console.log(cfgToMermaid(data));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
98
|
+
if (tryRenderGraphFormat(opts.format || 'text', data)) return;
|
|
62
99
|
|
|
63
|
-
// Text format
|
|
64
100
|
for (const r of data.results as CfgResultEntry[]) {
|
|
65
|
-
|
|
66
|
-
console.log('\u2500'.repeat(60));
|
|
67
|
-
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
|
|
68
|
-
|
|
69
|
-
if (r.blocks.length > 0) {
|
|
70
|
-
console.log('\n Blocks:');
|
|
71
|
-
for (const b of r.blocks) {
|
|
72
|
-
const loc = b.startLine
|
|
73
|
-
? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
|
|
74
|
-
: '';
|
|
75
|
-
const label = b.label ? ` (${b.label})` : '';
|
|
76
|
-
console.log(` [${b.index}] ${b.type}${label}${loc}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (r.edges.length > 0) {
|
|
81
|
-
console.log('\n Edges:');
|
|
82
|
-
for (const e of r.edges) {
|
|
83
|
-
console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
101
|
+
printCfgEntry(r);
|
|
86
102
|
}
|
|
87
103
|
}
|
package/src/presentation/flow.ts
CHANGED
|
@@ -16,54 +16,65 @@ interface FlowOpts {
|
|
|
16
16
|
csv?: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const data = listEntryPointsData(dbPath, {
|
|
26
|
-
noTests: opts.noTests,
|
|
27
|
-
limit: opts.limit,
|
|
28
|
-
offset: opts.offset,
|
|
29
|
-
}) as any;
|
|
30
|
-
if (outputResult(data, 'entries', opts)) return;
|
|
31
|
-
if (data.count === 0) {
|
|
32
|
-
console.log('No entry points found. Run "codegraph build" first.');
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
console.log(`\nEntry points (${data.count} total):\n`);
|
|
36
|
-
for (const [type, entries] of Object.entries(
|
|
37
|
-
data.byType as Record<
|
|
38
|
-
string,
|
|
39
|
-
Array<{ kind: string; name: string; file: string; line: number }>
|
|
40
|
-
>,
|
|
41
|
-
)) {
|
|
42
|
-
console.log(` ${type} (${entries.length}):`);
|
|
43
|
-
for (const e of entries) {
|
|
44
|
-
console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
45
|
-
}
|
|
46
|
-
console.log();
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
19
|
+
interface EntryPoint {
|
|
20
|
+
kind: string;
|
|
21
|
+
name: string;
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
}
|
|
50
25
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
26
|
+
interface FlowNode {
|
|
27
|
+
kind: string;
|
|
28
|
+
name: string;
|
|
29
|
+
file: string;
|
|
30
|
+
line: number;
|
|
31
|
+
}
|
|
57
32
|
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
interface FlowStep {
|
|
34
|
+
depth: number;
|
|
35
|
+
nodes: FlowNode[];
|
|
36
|
+
}
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
interface FlowCycle {
|
|
39
|
+
from: string;
|
|
40
|
+
to: string;
|
|
41
|
+
depth: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface FlowResult {
|
|
45
|
+
entry?: { kind: string; name: string; type: string; file: string; line: number };
|
|
46
|
+
depth: number;
|
|
47
|
+
totalReached: number;
|
|
48
|
+
leaves: Array<{ name: string; file: string }>;
|
|
49
|
+
steps: FlowStep[];
|
|
50
|
+
cycles: FlowCycle[];
|
|
51
|
+
truncated?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runListEntryPoints(dbPath: string | undefined, opts: FlowOpts): void {
|
|
55
|
+
const data = listEntryPointsData(dbPath, {
|
|
56
|
+
noTests: opts.noTests,
|
|
57
|
+
limit: opts.limit,
|
|
58
|
+
offset: opts.offset,
|
|
59
|
+
}) as { count: number; byType: Record<string, EntryPoint[]> };
|
|
60
|
+
if (outputResult(data, 'entries', opts)) return;
|
|
61
|
+
if (data.count === 0) {
|
|
62
|
+
console.log('No entry points found. Run "codegraph build" first.');
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
+
console.log(`\nEntry points (${data.count} total):\n`);
|
|
66
|
+
for (const [type, entries] of Object.entries(data.byType)) {
|
|
67
|
+
console.log(` ${type} (${entries.length}):`);
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
70
|
+
}
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
65
74
|
|
|
75
|
+
function printFlowHeader(data: FlowResult): void {
|
|
66
76
|
const e = data.entry;
|
|
77
|
+
if (!e) return;
|
|
67
78
|
const typeTag = e.type !== 'exported' ? ` (${e.type})` : '';
|
|
68
79
|
console.log(`\nFlow from: [${kindIcon(e.kind)}] ${e.name}${typeTag} ${e.file}:${e.line}`);
|
|
69
80
|
console.log(
|
|
@@ -73,27 +84,64 @@ export function flow(
|
|
|
73
84
|
console.log(` (truncated at depth ${data.depth})`);
|
|
74
85
|
}
|
|
75
86
|
console.log();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isLeafNode(n: FlowNode, leaves: Array<{ name: string; file: string }>): boolean {
|
|
90
|
+
return leaves.some((l) => l.name === n.name && l.file === n.file);
|
|
91
|
+
}
|
|
76
92
|
|
|
93
|
+
/** Returns true when the node is a leaf (no steps); caller should skip cycle output. */
|
|
94
|
+
function printFlowSteps(data: FlowResult): boolean {
|
|
77
95
|
if (data.steps.length === 0) {
|
|
78
96
|
console.log(' (leaf node — no callees)');
|
|
79
|
-
return;
|
|
97
|
+
return true;
|
|
80
98
|
}
|
|
81
|
-
|
|
82
99
|
for (const step of data.steps) {
|
|
83
100
|
console.log(` depth ${step.depth}:`);
|
|
84
101
|
for (const n of step.nodes) {
|
|
85
|
-
const
|
|
86
|
-
(l: { name: string; file: string }) => l.name === n.name && l.file === n.file,
|
|
87
|
-
);
|
|
88
|
-
const leafTag = isLeaf ? ' [leaf]' : '';
|
|
102
|
+
const leafTag = isLeafNode(n, data.leaves) ? ' [leaf]' : '';
|
|
89
103
|
console.log(` [${kindIcon(n.kind)}] ${n.name} ${n.file}:${n.line}${leafTag}`);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printFlowCycles(cycles: FlowCycle[]): void {
|
|
110
|
+
if (cycles.length === 0) return;
|
|
111
|
+
console.log('\n Cycles detected:');
|
|
112
|
+
for (const c of cycles) {
|
|
113
|
+
console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
export function flow(
|
|
118
|
+
name: string | undefined,
|
|
119
|
+
dbPath: string | undefined,
|
|
120
|
+
opts: FlowOpts = {},
|
|
121
|
+
): void {
|
|
122
|
+
if (opts.list) {
|
|
123
|
+
runListEntryPoints(dbPath, opts);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!name) {
|
|
128
|
+
console.log(
|
|
129
|
+
'Please provide a function or entry-point name. Use --list to see available entry points.',
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = flowData(name, dbPath, opts) as unknown as FlowResult;
|
|
135
|
+
if (outputResult(data, 'steps', opts)) return;
|
|
136
|
+
|
|
137
|
+
if (!data.entry) {
|
|
138
|
+
console.log(`No matching entry point or function found for "${name}".`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
printFlowHeader(data);
|
|
143
|
+
const isLeaf = printFlowSteps(data);
|
|
144
|
+
if (!isLeaf) {
|
|
145
|
+
printFlowCycles(data.cycles);
|
|
98
146
|
}
|
|
99
147
|
}
|