polycodegraph 0.1.0__py3-none-any.whl
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.
- codegraph/__init__.py +10 -0
- codegraph/analysis/__init__.py +30 -0
- codegraph/analysis/_common.py +125 -0
- codegraph/analysis/blast_radius.py +63 -0
- codegraph/analysis/cycles.py +79 -0
- codegraph/analysis/dataflow.py +861 -0
- codegraph/analysis/dead_code.py +165 -0
- codegraph/analysis/hotspots.py +68 -0
- codegraph/analysis/infrastructure.py +439 -0
- codegraph/analysis/metrics.py +52 -0
- codegraph/analysis/report.py +222 -0
- codegraph/analysis/roles.py +323 -0
- codegraph/analysis/untested.py +79 -0
- codegraph/cli.py +1506 -0
- codegraph/config.py +64 -0
- codegraph/embed/__init__.py +35 -0
- codegraph/embed/chunker.py +120 -0
- codegraph/embed/embedder.py +113 -0
- codegraph/embed/query.py +181 -0
- codegraph/embed/store.py +360 -0
- codegraph/graph/__init__.py +0 -0
- codegraph/graph/builder.py +212 -0
- codegraph/graph/schema.py +69 -0
- codegraph/graph/store_networkx.py +55 -0
- codegraph/graph/store_sqlite.py +249 -0
- codegraph/mcp_server/__init__.py +6 -0
- codegraph/mcp_server/server.py +933 -0
- codegraph/parsers/__init__.py +0 -0
- codegraph/parsers/base.py +70 -0
- codegraph/parsers/go.py +570 -0
- codegraph/parsers/python.py +1707 -0
- codegraph/parsers/typescript.py +1397 -0
- codegraph/py.typed +0 -0
- codegraph/resolve/__init__.py +4 -0
- codegraph/resolve/calls.py +480 -0
- codegraph/review/__init__.py +31 -0
- codegraph/review/baseline.py +32 -0
- codegraph/review/differ.py +211 -0
- codegraph/review/hook.py +70 -0
- codegraph/review/risk.py +219 -0
- codegraph/review/rules.py +342 -0
- codegraph/viz/__init__.py +17 -0
- codegraph/viz/_style.py +45 -0
- codegraph/viz/dashboard.py +740 -0
- codegraph/viz/diagrams.py +370 -0
- codegraph/viz/explore.py +453 -0
- codegraph/viz/hld.py +683 -0
- codegraph/viz/html.py +115 -0
- codegraph/viz/mermaid.py +111 -0
- codegraph/viz/svg.py +77 -0
- codegraph/web/__init__.py +4 -0
- codegraph/web/server.py +165 -0
- codegraph/web/static/app.css +664 -0
- codegraph/web/static/app.js +919 -0
- codegraph/web/static/index.html +112 -0
- codegraph/web/static/views/architecture.js +1671 -0
- codegraph/web/static/views/graph3d.css +564 -0
- codegraph/web/static/views/graph3d.js +999 -0
- codegraph/web/static/views/graph3d_transform.js +984 -0
- codegraph/workspace/__init__.py +34 -0
- codegraph/workspace/config.py +110 -0
- codegraph/workspace/operations.py +294 -0
- polycodegraph-0.1.0.dist-info/METADATA +687 -0
- polycodegraph-0.1.0.dist-info/RECORD +67 -0
- polycodegraph-0.1.0.dist-info/WHEEL +4 -0
- polycodegraph-0.1.0.dist-info/entry_points.txt +2 -0
- polycodegraph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
/* graph3d_transform.js — pure data transforms for the 3D Graph view.
|
|
2
|
+
*
|
|
3
|
+
* Loaded as a classic browser <script> AND as a CommonJS module under Node
|
|
4
|
+
* for `node --test`. No DOM, no globals other than the optional
|
|
5
|
+
* `module.exports` guard at the bottom.
|
|
6
|
+
*
|
|
7
|
+
* Two transforms live here:
|
|
8
|
+
*
|
|
9
|
+
* buildGraph3dData(hld, filters)
|
|
10
|
+
* Legacy "show everything matching filters" view. Kept for tests and
|
|
11
|
+
* future side-panel use; the focus-mode controller does NOT call it.
|
|
12
|
+
*
|
|
13
|
+
* buildFocusGraph(hld, rootQn, depth, direction)
|
|
14
|
+
* BFS from rootQn over the per-symbol callers/callees adjacency in the
|
|
15
|
+
* HLD payload. Returns the same {nodes, links} shape, with a `role`
|
|
16
|
+
* field on each node ('root' | 'ancestor' | 'descendant') and edge
|
|
17
|
+
* colors keyed to the descendant role.
|
|
18
|
+
*
|
|
19
|
+
* Node shape: { id, name, qualname, kind, file, language, layer,
|
|
20
|
+
* fan_in, fan_out, role, depth, val, color }
|
|
21
|
+
* Link shape: { source, target, kind, color }
|
|
22
|
+
*/
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
// ---- Color tokens ----------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
var KIND_COLORS = {
|
|
28
|
+
FUNCTION: '#34d399', // emerald
|
|
29
|
+
CLASS: '#a78bfa', // violet
|
|
30
|
+
METHOD: '#22d3ee', // cyan
|
|
31
|
+
MODULE: '#818cf8', // brand
|
|
32
|
+
};
|
|
33
|
+
var KIND_FALLBACK = '#8b9ab8';
|
|
34
|
+
|
|
35
|
+
var EDGE_COLORS = {
|
|
36
|
+
CALLS: 'rgba(129,140,248,0.5)',
|
|
37
|
+
IMPORTS: 'rgba(34,211,238,0.4)',
|
|
38
|
+
INHERITS: 'rgba(167,139,250,0.5)',
|
|
39
|
+
IMPLEMENTS: 'rgba(167,139,250,0.5)',
|
|
40
|
+
};
|
|
41
|
+
var EDGE_FALLBACK = 'rgba(139,154,184,0.35)';
|
|
42
|
+
|
|
43
|
+
// Focus-mode role colors. The root pops violet, ancestors flow in amber
|
|
44
|
+
// and descendants flow out cyan. Edge color follows the descendant role
|
|
45
|
+
// so caller→root edges read amber and root→callee edges read cyan.
|
|
46
|
+
// External (stdlib / third-party) nodes render as gray-outline terminal
|
|
47
|
+
// leaves — visible at the boundary but never traversed.
|
|
48
|
+
var ROLE_COLORS = {
|
|
49
|
+
root: '#a78bfa', // violet
|
|
50
|
+
ancestor: '#fbbf24', // amber
|
|
51
|
+
descendant: '#22d3ee', // cyan
|
|
52
|
+
external: '#8b9ab8', // gray (terminal leaf)
|
|
53
|
+
};
|
|
54
|
+
var ROLE_EDGE_COLORS = {
|
|
55
|
+
ancestor: 'rgba(251,191,36,0.55)',
|
|
56
|
+
descendant: 'rgba(34,211,238,0.55)',
|
|
57
|
+
external: 'rgba(139,154,184,0.35)',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function kindColor(kind) {
|
|
61
|
+
return KIND_COLORS[kind] || KIND_FALLBACK;
|
|
62
|
+
}
|
|
63
|
+
function edgeColor(kind) {
|
|
64
|
+
return EDGE_COLORS[kind] || EDGE_FALLBACK;
|
|
65
|
+
}
|
|
66
|
+
function roleColor(role) {
|
|
67
|
+
return ROLE_COLORS[role] || KIND_FALLBACK;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- Call-arg label (Change 2 / DF0) ---------------------------------------
|
|
71
|
+
//
|
|
72
|
+
// Format a single edge's args+kwargs into a short label.
|
|
73
|
+
// args=[1], kwargs={x:2} -> "1, x=2"
|
|
74
|
+
// args=[], kwargs={} -> ""
|
|
75
|
+
// missing -> ""
|
|
76
|
+
function formatCallArgs(callArg) {
|
|
77
|
+
if (!callArg || typeof callArg !== 'object') return '';
|
|
78
|
+
var parts = [];
|
|
79
|
+
var args = Array.isArray(callArg.args) ? callArg.args : [];
|
|
80
|
+
args.forEach(function (a) {
|
|
81
|
+
if (a === null || a === undefined) return;
|
|
82
|
+
var s = String(a);
|
|
83
|
+
if (s.length) parts.push(s);
|
|
84
|
+
});
|
|
85
|
+
var kwargs = (callArg.kwargs && typeof callArg.kwargs === 'object') ? callArg.kwargs : {};
|
|
86
|
+
Object.keys(kwargs).forEach(function (k) {
|
|
87
|
+
var v = kwargs[k];
|
|
88
|
+
parts.push(String(k) + '=' + String(v == null ? '' : v));
|
|
89
|
+
});
|
|
90
|
+
return parts.join(', ');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build a map: callee qualname -> argLabel from a symbol's parallel
|
|
94
|
+
// callees / callee_args arrays. Older payloads without callee_args
|
|
95
|
+
// yield an empty map.
|
|
96
|
+
function callArgsFromSym(sym) {
|
|
97
|
+
var out = {};
|
|
98
|
+
if (!sym) return out;
|
|
99
|
+
var callees = sym.callees || [];
|
|
100
|
+
var callArgs = sym.callee_args || [];
|
|
101
|
+
for (var i = 0; i < callees.length; i++) {
|
|
102
|
+
var label = formatCallArgs(callArgs[i]);
|
|
103
|
+
if (label) out[callees[i]] = label;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- Signature formatting (Change 4 / DF0) ---------------------------------
|
|
109
|
+
//
|
|
110
|
+
// Render a function's signature as a single line:
|
|
111
|
+
// f(a: int, b: str = "x") -> bool
|
|
112
|
+
// Skip ": type" if type is None/missing; skip "= default" if missing;
|
|
113
|
+
// skip "-> returns" if returns is None/missing. Returns '' when params is
|
|
114
|
+
// empty AND returns is missing — the caller suppresses the entire block.
|
|
115
|
+
function formatSignature(node) {
|
|
116
|
+
if (!node) return '';
|
|
117
|
+
var name = node.name || '';
|
|
118
|
+
var params = Array.isArray(node.params) ? node.params : [];
|
|
119
|
+
var returns = node.returns;
|
|
120
|
+
var hasParams = params.length > 0;
|
|
121
|
+
var hasReturns = returns != null && String(returns).length > 0;
|
|
122
|
+
if (!hasParams && !hasReturns) return '';
|
|
123
|
+
var parts = params.map(function (p) {
|
|
124
|
+
if (!p || !p.name) return '';
|
|
125
|
+
var s = String(p.name);
|
|
126
|
+
if (p.type != null && String(p.type).length) s += ': ' + String(p.type);
|
|
127
|
+
if (p.default != null && String(p.default).length) s += ' = ' + String(p.default);
|
|
128
|
+
return s;
|
|
129
|
+
}).filter(function (s) { return s; });
|
|
130
|
+
var sig = String(name) + '(' + parts.join(', ') + ')';
|
|
131
|
+
if (hasReturns) sig += ' -> ' + String(returns);
|
|
132
|
+
return sig;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clampVal(fanIn) {
|
|
136
|
+
var v = 2 + (Number(fanIn) || 0);
|
|
137
|
+
if (v < 2) v = 2;
|
|
138
|
+
if (v > 12) v = 12;
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- Symbol index ----------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
// Walk hld.modules once and return a map qualname -> { sym, modQn, mod }.
|
|
145
|
+
function indexSymbols(hld) {
|
|
146
|
+
var modules = (hld && hld.modules) || {};
|
|
147
|
+
var index = new Map();
|
|
148
|
+
Object.keys(modules).forEach(function (modQn) {
|
|
149
|
+
var mod = modules[modQn] || {};
|
|
150
|
+
var symbols = mod.symbols || [];
|
|
151
|
+
symbols.forEach(function (sym) {
|
|
152
|
+
if (!sym || !sym.qualname) return;
|
|
153
|
+
index.set(sym.qualname, { sym: sym, modQn: modQn, mod: mod });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return index;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- Legacy "show all" transform ------------------------------------------
|
|
160
|
+
|
|
161
|
+
function buildGraph3dData(hld, filters) {
|
|
162
|
+
var modules = (hld && hld.modules) || {};
|
|
163
|
+
var kinds = (filters && filters.kinds) || new Set();
|
|
164
|
+
var edgeKinds = (filters && filters.edgeKinds) || new Set();
|
|
165
|
+
|
|
166
|
+
var nodeMap = new Map();
|
|
167
|
+
|
|
168
|
+
Object.keys(modules).forEach(function (modQn) {
|
|
169
|
+
var mod = modules[modQn] || {};
|
|
170
|
+
var symbols = mod.symbols || [];
|
|
171
|
+
symbols.forEach(function (sym) {
|
|
172
|
+
if (!sym || !sym.qualname) return;
|
|
173
|
+
if (!kinds.has(sym.kind)) return;
|
|
174
|
+
nodeMap.set(sym.qualname, {
|
|
175
|
+
id: sym.qualname,
|
|
176
|
+
name: sym.name || sym.qualname,
|
|
177
|
+
qualname: sym.qualname,
|
|
178
|
+
kind: sym.kind,
|
|
179
|
+
file: mod.file || '',
|
|
180
|
+
language: mod.language || '',
|
|
181
|
+
layer: mod.layer || '',
|
|
182
|
+
fan_in: Number(sym.fan_in) || 0,
|
|
183
|
+
fan_out: Number(sym.fan_out) || 0,
|
|
184
|
+
val: clampVal(sym.fan_in),
|
|
185
|
+
color: kindColor(sym.kind),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
var links = [];
|
|
191
|
+
if (edgeKinds.has('CALLS')) {
|
|
192
|
+
var seen = new Set();
|
|
193
|
+
Object.keys(modules).forEach(function (modQn) {
|
|
194
|
+
var symbols = (modules[modQn] || {}).symbols || [];
|
|
195
|
+
symbols.forEach(function (sym) {
|
|
196
|
+
var srcId = sym.qualname;
|
|
197
|
+
if (!nodeMap.has(srcId)) return;
|
|
198
|
+
(sym.callees || []).forEach(function (dstId) {
|
|
199
|
+
if (!nodeMap.has(dstId)) return;
|
|
200
|
+
var key = srcId + '' + dstId;
|
|
201
|
+
if (seen.has(key)) return;
|
|
202
|
+
seen.add(key);
|
|
203
|
+
links.push({
|
|
204
|
+
source: srcId,
|
|
205
|
+
target: dstId,
|
|
206
|
+
kind: 'CALLS',
|
|
207
|
+
color: edgeColor('CALLS'),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
(sym.callers || []).forEach(function (callerId) {
|
|
211
|
+
if (!nodeMap.has(callerId)) return;
|
|
212
|
+
var key = callerId + '' + srcId;
|
|
213
|
+
if (seen.has(key)) return;
|
|
214
|
+
seen.add(key);
|
|
215
|
+
links.push({
|
|
216
|
+
source: callerId,
|
|
217
|
+
target: srcId,
|
|
218
|
+
kind: 'CALLS',
|
|
219
|
+
color: edgeColor('CALLS'),
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
nodes: Array.from(nodeMap.values()),
|
|
228
|
+
links: links,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- Focus mode (BFS from a root) -----------------------------------------
|
|
233
|
+
|
|
234
|
+
function makeNode(entry, role, depth) {
|
|
235
|
+
var sym = entry.sym;
|
|
236
|
+
var mod = entry.mod;
|
|
237
|
+
// Root pops larger; descendants/ancestors get a moderate boost.
|
|
238
|
+
var baseVal = clampVal(sym.fan_in);
|
|
239
|
+
if (role === 'root') baseVal = 8;
|
|
240
|
+
return {
|
|
241
|
+
id: sym.qualname,
|
|
242
|
+
name: sym.name || sym.qualname,
|
|
243
|
+
qualname: sym.qualname,
|
|
244
|
+
kind: sym.kind,
|
|
245
|
+
file: mod.file || '',
|
|
246
|
+
language: mod.language || '',
|
|
247
|
+
layer: mod.layer || '',
|
|
248
|
+
fan_in: Number(sym.fan_in) || 0,
|
|
249
|
+
fan_out: Number(sym.fan_out) || 0,
|
|
250
|
+
role: role,
|
|
251
|
+
depth: depth,
|
|
252
|
+
val: baseVal,
|
|
253
|
+
color: roleColor(role),
|
|
254
|
+
external: false,
|
|
255
|
+
// DF0 enrichment — fall back gracefully on older payloads.
|
|
256
|
+
params: Array.isArray(sym.params) ? sym.params : [],
|
|
257
|
+
returns: sym.returns != null ? sym.returns : null,
|
|
258
|
+
symbolRole: sym.role || null,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Build a synthetic node for an external (stdlib / third-party) symbol
|
|
263
|
+
// that the BFS doesn't expand. Pretty-print short name from the qualname.
|
|
264
|
+
function makeExternalNode(qn, depth) {
|
|
265
|
+
var raw = String(qn || '');
|
|
266
|
+
var clean = raw.indexOf('unresolved::') === 0 ? raw.slice('unresolved::'.length) : raw;
|
|
267
|
+
var parts = clean.split('.');
|
|
268
|
+
var short = parts[parts.length - 1] || clean;
|
|
269
|
+
return {
|
|
270
|
+
id: raw,
|
|
271
|
+
name: short,
|
|
272
|
+
qualname: raw,
|
|
273
|
+
kind: 'EXTERNAL',
|
|
274
|
+
file: '',
|
|
275
|
+
language: '',
|
|
276
|
+
layer: '',
|
|
277
|
+
fan_in: 0,
|
|
278
|
+
fan_out: 0,
|
|
279
|
+
role: 'external',
|
|
280
|
+
depth: depth,
|
|
281
|
+
val: 3,
|
|
282
|
+
color: ROLE_COLORS.external,
|
|
283
|
+
external: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A qualname is "external" if it starts with `unresolved::` or it is not
|
|
288
|
+
// present in the symbol index built from hld.modules.
|
|
289
|
+
function isExternalQn(qn, index) {
|
|
290
|
+
if (!qn) return true;
|
|
291
|
+
if (String(qn).indexOf('unresolved::') === 0) return true;
|
|
292
|
+
return !index.has(qn);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A node belongs to test code if its qualname or file matches a common
|
|
296
|
+
// test convention. Drives the "Hide tests" toggle in the 3D view, since
|
|
297
|
+
// most users want to see real-code call chains by default and only opt
|
|
298
|
+
// into test fan-in when debugging coverage.
|
|
299
|
+
//
|
|
300
|
+
// Heuristics (intentionally conservative — false negatives over noise):
|
|
301
|
+
// - qualname starts with `tests.` or `test.`
|
|
302
|
+
// - qualname segment-bounded `tests` (e.g. `pkg.tests.foo`)
|
|
303
|
+
// - file path under a tests/ directory
|
|
304
|
+
// - filename matches Python (test_*.py, *_test.py), JS/TS (*.test.[jt]sx?,
|
|
305
|
+
// *.spec.[jt]sx?), or Go (*_test.go) test conventions
|
|
306
|
+
var TEST_QN_RE = /(^|\.)(tests?)(\.|$)/i;
|
|
307
|
+
var TEST_FILE_RE = new RegExp(
|
|
308
|
+
// tests/ directory anywhere
|
|
309
|
+
'(^|/)tests?/' +
|
|
310
|
+
// OR Python test_* / _test files
|
|
311
|
+
'|(^|/)test_[^/]+\\.py$|_test\\.py$' +
|
|
312
|
+
// OR JS/TS *.test.* / *.spec.*
|
|
313
|
+
'|\\.(test|spec)\\.[jt]sx?$' +
|
|
314
|
+
// OR Go *_test.go
|
|
315
|
+
'|_test\\.go$',
|
|
316
|
+
'i'
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
function isTestNode(qualname, file) {
|
|
320
|
+
if (qualname && TEST_QN_RE.test(String(qualname))) return true;
|
|
321
|
+
if (file && TEST_FILE_RE.test(String(file))) return true;
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function buildFocusGraph(hld, rootQn, depth, direction, opts) {
|
|
326
|
+
if (!rootQn) return { nodes: [], links: [] };
|
|
327
|
+
|
|
328
|
+
var maxDepth = Number(depth);
|
|
329
|
+
if (!isFinite(maxDepth) || maxDepth < 1) maxDepth = 1;
|
|
330
|
+
if (maxDepth > 8) maxDepth = 8;
|
|
331
|
+
|
|
332
|
+
var dir = direction || 'both';
|
|
333
|
+
var hideTests = !!(opts && opts.hideTests);
|
|
334
|
+
|
|
335
|
+
var index = indexSymbols(hld);
|
|
336
|
+
if (!index.has(rootQn)) return { nodes: [], links: [] };
|
|
337
|
+
|
|
338
|
+
// The root itself is never filtered: if the user explicitly searched
|
|
339
|
+
// for a test function, they want to see it.
|
|
340
|
+
function shouldSkipNeighbor(qn) {
|
|
341
|
+
if (!hideTests || qn === rootQn) return false;
|
|
342
|
+
var entry = index.get(qn);
|
|
343
|
+
var file = entry ? (entry.sym && entry.sym.file) || (entry.mod && entry.mod.file) || '' : '';
|
|
344
|
+
return isTestNode(qn, file);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
var nodes = new Map(); // qn -> node
|
|
348
|
+
var links = [];
|
|
349
|
+
var linkKeys = new Set();
|
|
350
|
+
|
|
351
|
+
function addLink(source, target, role, external, argLabel) {
|
|
352
|
+
var key = source + '' + target + '' + role;
|
|
353
|
+
if (linkKeys.has(key)) return;
|
|
354
|
+
linkKeys.add(key);
|
|
355
|
+
var edgeRole = external ? 'external' : role;
|
|
356
|
+
links.push({
|
|
357
|
+
source: source,
|
|
358
|
+
target: target,
|
|
359
|
+
kind: 'CALLS',
|
|
360
|
+
color: ROLE_EDGE_COLORS[edgeRole] || EDGE_FALLBACK,
|
|
361
|
+
external: !!external,
|
|
362
|
+
argLabel: argLabel || '',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Seed with root.
|
|
367
|
+
nodes.set(rootQn, makeNode(index.get(rootQn), 'root', 0));
|
|
368
|
+
|
|
369
|
+
// Generic BFS that walks one direction. `step(qn) -> [neighbor qn]`
|
|
370
|
+
// maps a node to its outgoing/incoming neighbors per direction.
|
|
371
|
+
function bfs(role, step, edgeFromTo) {
|
|
372
|
+
var frontier = [rootQn];
|
|
373
|
+
var visited = new Set([rootQn]);
|
|
374
|
+
for (var d = 1; d <= maxDepth; d++) {
|
|
375
|
+
var next = [];
|
|
376
|
+
for (var i = 0; i < frontier.length; i++) {
|
|
377
|
+
var here = frontier[i];
|
|
378
|
+
var neighbors = step(here);
|
|
379
|
+
// For descendant edges, args live on `here.callees`. For ancestor
|
|
380
|
+
// edges, args live on the neighbor's callees pointing at `here`.
|
|
381
|
+
var hereEntry = index.get(here);
|
|
382
|
+
var hereCallArgs = (role === 'descendant' && hereEntry)
|
|
383
|
+
? callArgsFromSym(hereEntry.sym) : {};
|
|
384
|
+
for (var j = 0; j < neighbors.length; j++) {
|
|
385
|
+
var nb = neighbors[j];
|
|
386
|
+
if (!nb) continue;
|
|
387
|
+
if (shouldSkipNeighbor(nb)) continue;
|
|
388
|
+
var external = isExternalQn(nb, index);
|
|
389
|
+
var argLabel = '';
|
|
390
|
+
if (role === 'descendant') {
|
|
391
|
+
argLabel = hereCallArgs[nb] || '';
|
|
392
|
+
} else if (role === 'ancestor') {
|
|
393
|
+
var nbEntry = index.get(nb);
|
|
394
|
+
if (nbEntry) {
|
|
395
|
+
argLabel = callArgsFromSym(nbEntry.sym)[here] || '';
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (external) {
|
|
399
|
+
// Terminal leaf: render once, never traverse.
|
|
400
|
+
var fromToExt = edgeFromTo(here, nb);
|
|
401
|
+
addLink(fromToExt[0], fromToExt[1], role, true, argLabel);
|
|
402
|
+
if (!nodes.has(nb)) {
|
|
403
|
+
nodes.set(nb, makeExternalNode(nb, d));
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
// Emit the edge (even if neighbor was already visited via
|
|
408
|
+
// another path — but dedup via linkKeys).
|
|
409
|
+
var fromTo = edgeFromTo(here, nb);
|
|
410
|
+
addLink(fromTo[0], fromTo[1], role, false, argLabel);
|
|
411
|
+
if (visited.has(nb)) continue;
|
|
412
|
+
visited.add(nb);
|
|
413
|
+
// Don't downgrade root if it shows up in a cycle.
|
|
414
|
+
if (!nodes.has(nb)) {
|
|
415
|
+
nodes.set(nb, makeNode(index.get(nb), role, d));
|
|
416
|
+
}
|
|
417
|
+
next.push(nb);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (next.length === 0) break;
|
|
421
|
+
frontier = next;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (dir === 'ancestors' || dir === 'both') {
|
|
426
|
+
bfs(
|
|
427
|
+
'ancestor',
|
|
428
|
+
function (qn) { return (index.get(qn).sym.callers || []); },
|
|
429
|
+
function (here, nb) { return [nb, here]; } // caller -> here
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (dir === 'descendants' || dir === 'both') {
|
|
433
|
+
bfs(
|
|
434
|
+
'descendant',
|
|
435
|
+
function (qn) { return (index.get(qn).sym.callees || []); },
|
|
436
|
+
function (here, nb) { return [here, nb]; } // here -> callee
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
nodes: Array.from(nodes.values()),
|
|
442
|
+
links: links,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- Inline expand / collapse ---------------------------------------------
|
|
447
|
+
//
|
|
448
|
+
// expandNode(state, hld, qn) and collapseNode(state, qn) mutate a graph
|
|
449
|
+
// state object in place:
|
|
450
|
+
//
|
|
451
|
+
// state = {
|
|
452
|
+
// nodes: Map<qn, node>,
|
|
453
|
+
// links: Array<{source, target, kind, color, external}>,
|
|
454
|
+
// linkKeys: Set<string>, // dedup key
|
|
455
|
+
// refcount: Map<qn, number>, // node refcount (>= 1 while present)
|
|
456
|
+
// expansions: Map<qn, {addedNodes: Set<qn>, addedLinkKeys: Set<string>}>,
|
|
457
|
+
// }
|
|
458
|
+
//
|
|
459
|
+
// The graph state lives in the controller (graph3d.js); these helpers are
|
|
460
|
+
// pure transforms over it so we can unit-test the expand/collapse logic
|
|
461
|
+
// without touching the DOM.
|
|
462
|
+
|
|
463
|
+
function makeFocusState(hld, rootQn, depth, direction, opts) {
|
|
464
|
+
var graph = buildFocusGraph(hld, rootQn, depth, direction, opts);
|
|
465
|
+
var nodes = new Map();
|
|
466
|
+
var refcount = new Map();
|
|
467
|
+
graph.nodes.forEach(function (n) {
|
|
468
|
+
nodes.set(n.id, n);
|
|
469
|
+
refcount.set(n.id, 1);
|
|
470
|
+
});
|
|
471
|
+
var linkKeys = new Set();
|
|
472
|
+
var links = [];
|
|
473
|
+
graph.links.forEach(function (l) {
|
|
474
|
+
var key = linkKey(l.source, l.target);
|
|
475
|
+
if (linkKeys.has(key)) return;
|
|
476
|
+
linkKeys.add(key);
|
|
477
|
+
links.push(l);
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
rootQn: rootQn,
|
|
481
|
+
nodes: nodes,
|
|
482
|
+
links: links,
|
|
483
|
+
linkKeys: linkKeys,
|
|
484
|
+
refcount: refcount,
|
|
485
|
+
expansions: new Map(),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function linkKey(source, target) {
|
|
490
|
+
var s = (source && source.id) || source;
|
|
491
|
+
var t = (target && target.id) || target;
|
|
492
|
+
return String(s) + '->' + String(t);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function snapshotState(state) {
|
|
496
|
+
return { nodes: Array.from(state.nodes.values()), links: state.links.slice() };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Expand: bring 1-hop neighbors of qn into the graph.
|
|
500
|
+
// External neighbors render as terminal leaves; internals get a role
|
|
501
|
+
// matching the relationship to qn (callers => ancestor, callees => descendant).
|
|
502
|
+
function expandNode(state, hld, qn, opts) {
|
|
503
|
+
if (!state || !qn) return state;
|
|
504
|
+
if (state.expansions.has(qn)) return state; // already expanded
|
|
505
|
+
if (qn === state.rootQn) return state;
|
|
506
|
+
|
|
507
|
+
var index = indexSymbols(hld);
|
|
508
|
+
if (!index.has(qn)) return state;
|
|
509
|
+
|
|
510
|
+
var hideTests = !!(opts && opts.hideTests);
|
|
511
|
+
var entry = index.get(qn);
|
|
512
|
+
var sym = entry.sym;
|
|
513
|
+
var addedNodes = new Set();
|
|
514
|
+
var addedLinkKeys = new Set();
|
|
515
|
+
|
|
516
|
+
function shouldSkipNeighbor(neighborQn) {
|
|
517
|
+
if (!hideTests || neighborQn === state.rootQn) return false;
|
|
518
|
+
var nbEntry = index.get(neighborQn);
|
|
519
|
+
var file = nbEntry ? (nbEntry.sym && nbEntry.sym.file) || (nbEntry.mod && nbEntry.mod.file) || '' : '';
|
|
520
|
+
return isTestNode(neighborQn, file);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function addLeaf(neighborQn, role, edgePair, argLabel) {
|
|
524
|
+
if (!neighborQn) return;
|
|
525
|
+
var external = isExternalQn(neighborQn, index);
|
|
526
|
+
var key = linkKey(edgePair[0], edgePair[1]);
|
|
527
|
+
if (!state.linkKeys.has(key)) {
|
|
528
|
+
state.linkKeys.add(key);
|
|
529
|
+
addedLinkKeys.add(key);
|
|
530
|
+
var edgeRole = external ? 'external' : role;
|
|
531
|
+
state.links.push({
|
|
532
|
+
source: edgePair[0],
|
|
533
|
+
target: edgePair[1],
|
|
534
|
+
kind: 'CALLS',
|
|
535
|
+
color: ROLE_EDGE_COLORS[edgeRole] || EDGE_FALLBACK,
|
|
536
|
+
external: external,
|
|
537
|
+
argLabel: argLabel || '',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (state.nodes.has(neighborQn)) {
|
|
541
|
+
// Bump refcount; another expansion already brought it in.
|
|
542
|
+
state.refcount.set(neighborQn, (state.refcount.get(neighborQn) || 0) + 1);
|
|
543
|
+
addedNodes.add(neighborQn);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
var node;
|
|
547
|
+
if (external) {
|
|
548
|
+
node = makeExternalNode(neighborQn, 1);
|
|
549
|
+
} else {
|
|
550
|
+
node = makeNode(index.get(neighborQn), role, 1);
|
|
551
|
+
}
|
|
552
|
+
state.nodes.set(neighborQn, node);
|
|
553
|
+
state.refcount.set(neighborQn, 1);
|
|
554
|
+
addedNodes.add(neighborQn);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Descendant edges carry args from this symbol's callee_args.
|
|
558
|
+
// Ancestor edges (caller -> qn) carry args from the caller's
|
|
559
|
+
// own callee_args list pointing at qn.
|
|
560
|
+
var ownCallArgs = callArgsFromSym(sym);
|
|
561
|
+
(sym.callers || []).forEach(function (c) {
|
|
562
|
+
if (shouldSkipNeighbor(c)) return;
|
|
563
|
+
var cEntry = index.get(c);
|
|
564
|
+
var argLabel = '';
|
|
565
|
+
if (cEntry) argLabel = callArgsFromSym(cEntry.sym)[qn] || '';
|
|
566
|
+
addLeaf(c, 'ancestor', [c, qn], argLabel);
|
|
567
|
+
});
|
|
568
|
+
(sym.callees || []).forEach(function (c) {
|
|
569
|
+
if (shouldSkipNeighbor(c)) return;
|
|
570
|
+
addLeaf(c, 'descendant', [qn, c], ownCallArgs[c] || '');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
state.expansions.set(qn, { addedNodes: addedNodes, addedLinkKeys: addedLinkKeys });
|
|
574
|
+
return state;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Collapse: undo a previous expand. Decrement refcounts of nodes added
|
|
578
|
+
// by this expansion; nodes whose refcount drops to 0 are removed. Edges
|
|
579
|
+
// added by this expansion are always removed.
|
|
580
|
+
function collapseNode(state, qn) {
|
|
581
|
+
if (!state || !qn) return state;
|
|
582
|
+
var rec = state.expansions.get(qn);
|
|
583
|
+
if (!rec) return state;
|
|
584
|
+
// Drop edges this expansion added.
|
|
585
|
+
state.links = state.links.filter(function (l) {
|
|
586
|
+
var key = linkKey(l.source, l.target);
|
|
587
|
+
if (rec.addedLinkKeys.has(key)) {
|
|
588
|
+
state.linkKeys.delete(key);
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
});
|
|
593
|
+
// Decrement refcounts; remove orphaned nodes.
|
|
594
|
+
rec.addedNodes.forEach(function (id) {
|
|
595
|
+
var rc = (state.refcount.get(id) || 0) - 1;
|
|
596
|
+
if (rc <= 0) {
|
|
597
|
+
state.refcount.delete(id);
|
|
598
|
+
state.nodes.delete(id);
|
|
599
|
+
} else {
|
|
600
|
+
state.refcount.set(id, rc);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
state.expansions.delete(qn);
|
|
604
|
+
return state;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function isExpanded(state, qn) {
|
|
608
|
+
return !!(state && state.expansions && state.expansions.has(qn));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ---- Grouped picker (Item 4) ----------------------------------------------
|
|
612
|
+
//
|
|
613
|
+
// groupSymbols(hld) returns a list of modules. Each module contains:
|
|
614
|
+
// { qualname, file, language, classes: [...], functions: [...] }
|
|
615
|
+
// Each class:
|
|
616
|
+
// { qualname, name, methods: [{qualname, name, kind, fan_in, fan_out}] }
|
|
617
|
+
// Each function (top-level):
|
|
618
|
+
// { qualname, name, kind, fan_in, fan_out }
|
|
619
|
+
//
|
|
620
|
+
// The shape is intentionally flat-but-grouped so the picker can render it
|
|
621
|
+
// as a tree: module -> class -> method, plus module-level functions.
|
|
622
|
+
|
|
623
|
+
function groupSymbols(hld) {
|
|
624
|
+
var modules = (hld && hld.modules) || {};
|
|
625
|
+
var out = [];
|
|
626
|
+
Object.keys(modules).sort().forEach(function (modQn) {
|
|
627
|
+
var mod = modules[modQn] || {};
|
|
628
|
+
var symbols = mod.symbols || [];
|
|
629
|
+
var classes = {}; // qn -> class entry
|
|
630
|
+
var functions = [];
|
|
631
|
+
var methodOwners = {}; // method qn -> class qn
|
|
632
|
+
|
|
633
|
+
// First pass: seed classes (CLASS kind).
|
|
634
|
+
symbols.forEach(function (s) {
|
|
635
|
+
if (!s || !s.qualname) return;
|
|
636
|
+
if (s.kind === 'CLASS') {
|
|
637
|
+
classes[s.qualname] = {
|
|
638
|
+
qualname: s.qualname,
|
|
639
|
+
name: s.name || s.qualname,
|
|
640
|
+
methods: [],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Second pass: assign methods to their class by qualname prefix.
|
|
646
|
+
symbols.forEach(function (s) {
|
|
647
|
+
if (!s || !s.qualname || s.kind === 'CLASS') return;
|
|
648
|
+
var meta = {
|
|
649
|
+
qualname: s.qualname,
|
|
650
|
+
name: s.name || s.qualname,
|
|
651
|
+
kind: s.kind,
|
|
652
|
+
fan_in: Number(s.fan_in) || 0,
|
|
653
|
+
fan_out: Number(s.fan_out) || 0,
|
|
654
|
+
};
|
|
655
|
+
// Find owning class: longest prefix s.qualname.startsWith(classQn + '.')
|
|
656
|
+
var owner = null;
|
|
657
|
+
Object.keys(classes).forEach(function (cqn) {
|
|
658
|
+
if (s.qualname.indexOf(cqn + '.') === 0) {
|
|
659
|
+
if (!owner || cqn.length > owner.length) owner = cqn;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
if (owner) {
|
|
663
|
+
classes[owner].methods.push(meta);
|
|
664
|
+
methodOwners[s.qualname] = owner;
|
|
665
|
+
} else {
|
|
666
|
+
functions.push(meta);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Sort: classes alpha by name, methods alpha, functions alpha.
|
|
671
|
+
var classList = Object.keys(classes).sort().map(function (k) {
|
|
672
|
+
var c = classes[k];
|
|
673
|
+
c.methods.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
674
|
+
return c;
|
|
675
|
+
});
|
|
676
|
+
functions.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
677
|
+
|
|
678
|
+
out.push({
|
|
679
|
+
qualname: modQn,
|
|
680
|
+
file: mod.file || '',
|
|
681
|
+
language: mod.language || '',
|
|
682
|
+
classes: classList,
|
|
683
|
+
functions: functions,
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Filter a grouped tree by query — keeps modules/classes that contain a
|
|
690
|
+
// match, plus the matching leaves themselves.
|
|
691
|
+
function filterGrouped(groups, query) {
|
|
692
|
+
var q = String(query || '').trim().toLowerCase();
|
|
693
|
+
if (!q) return groups;
|
|
694
|
+
|
|
695
|
+
function matches(text) {
|
|
696
|
+
return String(text || '').toLowerCase().indexOf(q) !== -1;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
var out = [];
|
|
700
|
+
groups.forEach(function (g) {
|
|
701
|
+
var keptClasses = [];
|
|
702
|
+
g.classes.forEach(function (c) {
|
|
703
|
+
var keptMethods = c.methods.filter(function (m) {
|
|
704
|
+
return matches(m.name) || matches(m.qualname);
|
|
705
|
+
});
|
|
706
|
+
var classMatches = matches(c.name) || matches(c.qualname);
|
|
707
|
+
if (keptMethods.length || classMatches) {
|
|
708
|
+
keptClasses.push({
|
|
709
|
+
qualname: c.qualname,
|
|
710
|
+
name: c.name,
|
|
711
|
+
methods: classMatches ? c.methods : keptMethods,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
var keptFns = g.functions.filter(function (f) {
|
|
716
|
+
return matches(f.name) || matches(f.qualname);
|
|
717
|
+
});
|
|
718
|
+
var moduleMatches = matches(g.qualname);
|
|
719
|
+
if (keptClasses.length || keptFns.length || moduleMatches) {
|
|
720
|
+
out.push({
|
|
721
|
+
qualname: g.qualname,
|
|
722
|
+
file: g.file,
|
|
723
|
+
language: g.language,
|
|
724
|
+
classes: moduleMatches ? g.classes : keptClasses,
|
|
725
|
+
functions: moduleMatches ? g.functions : keptFns,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ---- Role-grouped picker (Change 3 / DF1.5) -------------------------------
|
|
733
|
+
//
|
|
734
|
+
// groupSymbolsByRole(hld) -> [
|
|
735
|
+
// { role: 'HANDLER', color: '#fbbf24', modules: [<group>] },
|
|
736
|
+
// { role: 'SERVICE', color: '#3b82f6', modules: [...] },
|
|
737
|
+
// { role: 'COMPONENT',color: '#34d399', modules: [...] },
|
|
738
|
+
// { role: 'REPO', color: '#c084fc', modules: [...] },
|
|
739
|
+
// { role: '(no role)',color: '#8b9ab8', modules: [...] },
|
|
740
|
+
// ]
|
|
741
|
+
// Each <group> has the same shape as groupSymbols() emits, but only the
|
|
742
|
+
// symbols matching the bucket role are retained. Methods inherit their
|
|
743
|
+
// class's role; free functions use their own role; symbols without a
|
|
744
|
+
// recognized role land in the "(no role)" bucket. Buckets render in fixed
|
|
745
|
+
// order so the picker UI has stable headers even when a bucket is empty.
|
|
746
|
+
|
|
747
|
+
var ROLE_ORDER = ['HANDLER', 'SERVICE', 'COMPONENT', 'REPO', '(no role)'];
|
|
748
|
+
var ROLE_PICKER_COLORS = {
|
|
749
|
+
'HANDLER': '#fbbf24', // amber
|
|
750
|
+
'SERVICE': '#3b82f6', // blue
|
|
751
|
+
'COMPONENT': '#34d399', // green
|
|
752
|
+
'REPO': '#c084fc', // purple-pink
|
|
753
|
+
'(no role)': '#8b9ab8', // gray
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
function normalizeRole(role) {
|
|
757
|
+
if (!role) return '(no role)';
|
|
758
|
+
var r = String(role).toUpperCase();
|
|
759
|
+
if (ROLE_PICKER_COLORS[r]) return r;
|
|
760
|
+
return '(no role)';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function groupSymbolsByRole(hld) {
|
|
764
|
+
var modules = (hld && hld.modules) || {};
|
|
765
|
+
var buckets = {};
|
|
766
|
+
ROLE_ORDER.forEach(function (r) { buckets[r] = {}; });
|
|
767
|
+
|
|
768
|
+
function getModule(role, modQn, mod) {
|
|
769
|
+
if (!buckets[role][modQn]) {
|
|
770
|
+
buckets[role][modQn] = {
|
|
771
|
+
qualname: modQn,
|
|
772
|
+
file: mod.file || '',
|
|
773
|
+
language: mod.language || '',
|
|
774
|
+
classes: {},
|
|
775
|
+
functions: [],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return buckets[role][modQn];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
Object.keys(modules).forEach(function (modQn) {
|
|
782
|
+
var mod = modules[modQn] || {};
|
|
783
|
+
var symbols = mod.symbols || [];
|
|
784
|
+
var classByQn = {};
|
|
785
|
+
symbols.forEach(function (s) {
|
|
786
|
+
if (s && s.kind === 'CLASS' && s.qualname) classByQn[s.qualname] = s;
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
symbols.forEach(function (s) {
|
|
790
|
+
if (!s || !s.qualname) return;
|
|
791
|
+
// Methods inherit their class's role.
|
|
792
|
+
var ownerClass = null;
|
|
793
|
+
if (s.kind !== 'CLASS') {
|
|
794
|
+
Object.keys(classByQn).forEach(function (cqn) {
|
|
795
|
+
if (s.qualname.indexOf(cqn + '.') === 0) {
|
|
796
|
+
if (!ownerClass || cqn.length > ownerClass.qualname.length) {
|
|
797
|
+
ownerClass = classByQn[cqn];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
var roleSrc = ownerClass ? (ownerClass.role || s.role) : s.role;
|
|
803
|
+
var role = normalizeRole(roleSrc);
|
|
804
|
+
var modEntry = getModule(role, modQn, mod);
|
|
805
|
+
var meta = {
|
|
806
|
+
qualname: s.qualname,
|
|
807
|
+
name: s.name || s.qualname,
|
|
808
|
+
kind: s.kind,
|
|
809
|
+
fan_in: Number(s.fan_in) || 0,
|
|
810
|
+
fan_out: Number(s.fan_out) || 0,
|
|
811
|
+
};
|
|
812
|
+
if (s.kind === 'CLASS') {
|
|
813
|
+
if (!modEntry.classes[s.qualname]) {
|
|
814
|
+
modEntry.classes[s.qualname] = {
|
|
815
|
+
qualname: s.qualname,
|
|
816
|
+
name: s.name || s.qualname,
|
|
817
|
+
methods: [],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
} else if (ownerClass) {
|
|
821
|
+
if (!modEntry.classes[ownerClass.qualname]) {
|
|
822
|
+
modEntry.classes[ownerClass.qualname] = {
|
|
823
|
+
qualname: ownerClass.qualname,
|
|
824
|
+
name: ownerClass.name || ownerClass.qualname,
|
|
825
|
+
methods: [],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
modEntry.classes[ownerClass.qualname].methods.push(meta);
|
|
829
|
+
} else {
|
|
830
|
+
modEntry.functions.push(meta);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
return ROLE_ORDER.map(function (role) {
|
|
836
|
+
var modMap = buckets[role];
|
|
837
|
+
var modList = Object.keys(modMap).sort().map(function (mqn) {
|
|
838
|
+
var m = modMap[mqn];
|
|
839
|
+
var classList = Object.keys(m.classes).sort().map(function (k) {
|
|
840
|
+
var c = m.classes[k];
|
|
841
|
+
c.methods.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
842
|
+
return c;
|
|
843
|
+
});
|
|
844
|
+
m.functions.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
845
|
+
return {
|
|
846
|
+
qualname: m.qualname,
|
|
847
|
+
file: m.file,
|
|
848
|
+
language: m.language,
|
|
849
|
+
classes: classList,
|
|
850
|
+
functions: m.functions,
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
return {
|
|
854
|
+
role: role,
|
|
855
|
+
color: ROLE_PICKER_COLORS[role],
|
|
856
|
+
modules: modList,
|
|
857
|
+
};
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Filter a role-grouped tree by query, keeping role buckets that contain
|
|
862
|
+
// at least one match. Reuses filterGrouped() per bucket.
|
|
863
|
+
function filterGroupedByRole(roleGroups, query) {
|
|
864
|
+
var q = String(query || '').trim();
|
|
865
|
+
if (!q) return roleGroups;
|
|
866
|
+
return roleGroups.map(function (rg) {
|
|
867
|
+
return {
|
|
868
|
+
role: rg.role,
|
|
869
|
+
color: rg.color,
|
|
870
|
+
modules: filterGrouped(rg.modules, q),
|
|
871
|
+
};
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ---- Symbol search (top-N matches) ----------------------------------------
|
|
876
|
+
|
|
877
|
+
function searchSymbols(hld, query, limit) {
|
|
878
|
+
var max = Number(limit) || 20;
|
|
879
|
+
var q = String(query || '').trim().toLowerCase();
|
|
880
|
+
var modules = (hld && hld.modules) || {};
|
|
881
|
+
var results = [];
|
|
882
|
+
|
|
883
|
+
Object.keys(modules).forEach(function (modQn) {
|
|
884
|
+
var mod = modules[modQn] || {};
|
|
885
|
+
var symbols = mod.symbols || [];
|
|
886
|
+
symbols.forEach(function (sym) {
|
|
887
|
+
if (!sym || !sym.qualname) return;
|
|
888
|
+
var qn = String(sym.qualname).toLowerCase();
|
|
889
|
+
var nm = String(sym.name || '').toLowerCase();
|
|
890
|
+
var score = -1;
|
|
891
|
+
if (!q) {
|
|
892
|
+
// No query: surface high-fan-in symbols first.
|
|
893
|
+
score = 1000 - (Number(sym.fan_in) || 0);
|
|
894
|
+
} else if (qn === q || nm === q) {
|
|
895
|
+
score = 0;
|
|
896
|
+
} else if (nm.startsWith(q)) {
|
|
897
|
+
score = 1;
|
|
898
|
+
} else if (qn.indexOf(q) !== -1) {
|
|
899
|
+
score = 2 + qn.indexOf(q);
|
|
900
|
+
} else if (nm.indexOf(q) !== -1) {
|
|
901
|
+
score = 50 + nm.indexOf(q);
|
|
902
|
+
} else {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
results.push({
|
|
906
|
+
qualname: sym.qualname,
|
|
907
|
+
name: sym.name || sym.qualname,
|
|
908
|
+
kind: sym.kind,
|
|
909
|
+
file: mod.file || '',
|
|
910
|
+
fan_in: Number(sym.fan_in) || 0,
|
|
911
|
+
fan_out: Number(sym.fan_out) || 0,
|
|
912
|
+
_score: score,
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
results.sort(function (a, b) {
|
|
918
|
+
if (a._score !== b._score) return a._score - b._score;
|
|
919
|
+
// Tie-break: higher fan_in first, then qualname asc.
|
|
920
|
+
if (a.fan_in !== b.fan_in) return b.fan_in - a.fan_in;
|
|
921
|
+
return a.qualname < b.qualname ? -1 : 1;
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
return results.slice(0, max).map(function (r) {
|
|
925
|
+
delete r._score;
|
|
926
|
+
return r;
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ---- Exports --------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
if (typeof window !== 'undefined') {
|
|
933
|
+
window.CG_Graph3DTransform = {
|
|
934
|
+
buildGraph3dData: buildGraph3dData,
|
|
935
|
+
buildFocusGraph: buildFocusGraph,
|
|
936
|
+
makeFocusState: makeFocusState,
|
|
937
|
+
expandNode: expandNode,
|
|
938
|
+
collapseNode: collapseNode,
|
|
939
|
+
isExpanded: isExpanded,
|
|
940
|
+
snapshotState: snapshotState,
|
|
941
|
+
searchSymbols: searchSymbols,
|
|
942
|
+
groupSymbols: groupSymbols,
|
|
943
|
+
groupSymbolsByRole: groupSymbolsByRole,
|
|
944
|
+
filterGrouped: filterGrouped,
|
|
945
|
+
filterGroupedByRole: filterGroupedByRole,
|
|
946
|
+
formatCallArgs: formatCallArgs,
|
|
947
|
+
formatSignature: formatSignature,
|
|
948
|
+
ROLE_ORDER: ROLE_ORDER,
|
|
949
|
+
ROLE_PICKER_COLORS: ROLE_PICKER_COLORS,
|
|
950
|
+
isExternalQn: isExternalQn,
|
|
951
|
+
isTestNode: isTestNode,
|
|
952
|
+
indexSymbols: indexSymbols,
|
|
953
|
+
kindColor: kindColor,
|
|
954
|
+
edgeColor: edgeColor,
|
|
955
|
+
roleColor: roleColor,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
960
|
+
module.exports = {
|
|
961
|
+
buildGraph3dData: buildGraph3dData,
|
|
962
|
+
buildFocusGraph: buildFocusGraph,
|
|
963
|
+
makeFocusState: makeFocusState,
|
|
964
|
+
expandNode: expandNode,
|
|
965
|
+
collapseNode: collapseNode,
|
|
966
|
+
isExpanded: isExpanded,
|
|
967
|
+
snapshotState: snapshotState,
|
|
968
|
+
searchSymbols: searchSymbols,
|
|
969
|
+
groupSymbols: groupSymbols,
|
|
970
|
+
groupSymbolsByRole: groupSymbolsByRole,
|
|
971
|
+
filterGrouped: filterGrouped,
|
|
972
|
+
filterGroupedByRole: filterGroupedByRole,
|
|
973
|
+
formatCallArgs: formatCallArgs,
|
|
974
|
+
formatSignature: formatSignature,
|
|
975
|
+
ROLE_ORDER: ROLE_ORDER,
|
|
976
|
+
ROLE_PICKER_COLORS: ROLE_PICKER_COLORS,
|
|
977
|
+
isExternalQn: isExternalQn,
|
|
978
|
+
isTestNode: isTestNode,
|
|
979
|
+
indexSymbols: indexSymbols,
|
|
980
|
+
kindColor: kindColor,
|
|
981
|
+
edgeColor: edgeColor,
|
|
982
|
+
roleColor: roleColor,
|
|
983
|
+
};
|
|
984
|
+
}
|