@ghostlygawd/codeweb 0.9.0

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.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +30 -0
  2. package/CHANGELOG.md +480 -0
  3. package/LICENSE +21 -0
  4. package/README.md +573 -0
  5. package/agents/codeweb-dissector.md +56 -0
  6. package/agents/codeweb-domain-mapper.md +63 -0
  7. package/commands/codeweb.md +57 -0
  8. package/hooks/hooks.json +47 -0
  9. package/hooks/post-edit-diff.mjs +83 -0
  10. package/hooks/pre-edit-impact.mjs +94 -0
  11. package/hooks/session-brief.mjs +53 -0
  12. package/package.json +64 -0
  13. package/scripts/annotate.mjs +46 -0
  14. package/scripts/bench-ts-engine.mjs +59 -0
  15. package/scripts/bench.mjs +133 -0
  16. package/scripts/break-cycles.mjs +0 -0
  17. package/scripts/brief.mjs +28 -0
  18. package/scripts/build-report.mjs +182 -0
  19. package/scripts/campaign.mjs +61 -0
  20. package/scripts/check-consistency.mjs +45 -0
  21. package/scripts/ci-gate.mjs +86 -0
  22. package/scripts/cluster3.mjs +112 -0
  23. package/scripts/codemod.mjs +130 -0
  24. package/scripts/context-pack.mjs +118 -0
  25. package/scripts/deadcode.mjs +96 -0
  26. package/scripts/diff.mjs +0 -0
  27. package/scripts/explain.mjs +87 -0
  28. package/scripts/extract-symbols.mjs +1307 -0
  29. package/scripts/find-similar.mjs +86 -0
  30. package/scripts/find.mjs +50 -0
  31. package/scripts/fitness.mjs +90 -0
  32. package/scripts/grammars/PROVENANCE.md +36 -0
  33. package/scripts/grammars/tree-sitter-c-sharp.wasm +0 -0
  34. package/scripts/grammars/tree-sitter-go.wasm +0 -0
  35. package/scripts/grammars/tree-sitter-java.wasm +0 -0
  36. package/scripts/grammars/tree-sitter-python.wasm +0 -0
  37. package/scripts/grammars/tree-sitter-rust.wasm +0 -0
  38. package/scripts/grammars/tree-sitter-typescript.wasm +0 -0
  39. package/scripts/hotspots.mjs +53 -0
  40. package/scripts/lib/annotations.mjs +51 -0
  41. package/scripts/lib/bench-core.mjs +178 -0
  42. package/scripts/lib/brief-core.mjs +92 -0
  43. package/scripts/lib/campaign.mjs +73 -0
  44. package/scripts/lib/claims-check.mjs +44 -0
  45. package/scripts/lib/cli.mjs +140 -0
  46. package/scripts/lib/complexity.mjs +68 -0
  47. package/scripts/lib/dup-check.mjs +51 -0
  48. package/scripts/lib/find-core.mjs +85 -0
  49. package/scripts/lib/gate-md.mjs +50 -0
  50. package/scripts/lib/graph-ops.mjs +319 -0
  51. package/scripts/lib/hotspots.mjs +34 -0
  52. package/scripts/lib/query-core.mjs +85 -0
  53. package/scripts/lib/reading-order.mjs +53 -0
  54. package/scripts/lib/reliance.mjs +143 -0
  55. package/scripts/lib/risk.mjs +18 -0
  56. package/scripts/lib/shingles.mjs +39 -0
  57. package/scripts/lib/skeleton.mjs +41 -0
  58. package/scripts/lib/stats.mjs +92 -0
  59. package/scripts/lib/ts-engine.mjs +605 -0
  60. package/scripts/mcp-server.mjs +370 -0
  61. package/scripts/optimize.mjs +152 -0
  62. package/scripts/overlap.mjs +380 -0
  63. package/scripts/placement.mjs +93 -0
  64. package/scripts/query.mjs +94 -0
  65. package/scripts/reading-order.mjs +36 -0
  66. package/scripts/refresh.mjs +73 -0
  67. package/scripts/release-utils.mjs +158 -0
  68. package/scripts/release.mjs +62 -0
  69. package/scripts/report-template.html +730 -0
  70. package/scripts/review.mjs +112 -0
  71. package/scripts/risk.mjs +77 -0
  72. package/scripts/run.mjs +96 -0
  73. package/scripts/screenshot.mjs +104 -0
  74. package/scripts/simulate-edit.mjs +70 -0
  75. package/scripts/stats.mjs +31 -0
  76. package/scripts/trend.mjs +147 -0
  77. package/scripts/version-sync.mjs +19 -0
  78. package/skills/codebase-anatomy/SKILL.md +174 -0
  79. package/skills/codebase-anatomy/references/engine-detection.md +49 -0
  80. package/skills/codebase-anatomy/references/graph-schema.md +120 -0
  81. package/skills/codebase-anatomy/references/overlap-heuristics.md +52 -0
@@ -0,0 +1,605 @@
1
+ // codeweb optional tree-sitter engine (TypeScript/JavaScript) — exact cyclomatic complexity.
2
+ //
3
+ // This is the ADDITIVE, opt-in tier from docs/backlog-ast-tree-sitter.md (spike: spike/tree-sitter/,
4
+ // PR #17). The regex engine (scripts/lib/complexity.mjs) remains the default and the fallback; this
5
+ // module is loaded only when `--engine tree-sitter` is requested. web-tree-sitter is an
6
+ // OPTIONAL dependency — if it (or the vendored grammar) is unavailable, loadTsEngine() returns null
7
+ // and the caller falls back to regex per-file. It NEVER executes the target (static parse) and is
8
+ // deterministic given the pinned, vendored grammar.
9
+ //
10
+ // cyclomaticExact(src) counts McCabe decisions on a function/method BODY SLICE — the SAME text the
11
+ // regex extractor feeds cyclomatic() — using a decision set held byte-identical to complexity.mjs, so
12
+ // swapping engines changes precision, not definition. tree-sitter's error recovery means a bare body
13
+ // snippet (not a valid top-level program) still yields the correct decision nodes.
14
+
15
+ import { readFileSync, existsSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { dirname, join } from 'node:path';
18
+ import { createRequire } from 'node:module';
19
+
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+ const GRAMMAR = join(HERE, '..', 'grammars', 'tree-sitter-typescript.wasm');
22
+
23
+ // Decision set — MUST stay identical to scripts/lib/complexity.mjs (non-py branch):
24
+ // if/for/while/case/catch + && || ?? + ternary. `switch_default` carries no `case` keyword → excluded.
25
+ // `do_statement` maps to the one `while` token the regex counts in do…while.
26
+ const DECISION_TYPES = new Set([
27
+ 'if_statement', 'for_statement', 'for_in_statement', 'while_statement', 'do_statement',
28
+ 'switch_case', 'catch_clause', 'ternary_expression',
29
+ ]);
30
+ const DECISION_OPS = new Set(['&&', '||', '??']);
31
+
32
+ // A control-flow decision node (McCabe). Same set as scripts/lib/complexity.mjs (non-py branch).
33
+ const isDecision = (n) => {
34
+ if (DECISION_TYPES.has(n.type)) return true;
35
+ if (n.type === 'binary_expression') {
36
+ const op = n.childForFieldName('operator');
37
+ return !!op && DECISION_OPS.has(op.text);
38
+ }
39
+ return false;
40
+ };
41
+
42
+ // Count decisions strictly INSIDE `root` (root itself never counts — a program/body is not a
43
+ // decision). Exact cyclomatic = 1 + this. Iterative DFS so a deep tree can't blow the stack.
44
+ const countDecisions = (root) => {
45
+ let d = 0;
46
+ const stack = [root];
47
+ while (stack.length) {
48
+ const n = stack.pop();
49
+ if (n !== root && isDecision(n)) d++;
50
+ for (let i = 0; i < n.childCount; i++) stack.push(n.child(i));
51
+ }
52
+ return d;
53
+ };
54
+ const complexityOf = (bodyNode) => (bodyNode ? 1 + countDecisions(bodyNode) : 1);
55
+
56
+ // Visit every node once (iterative DFS). Named walkTree (not `walk`) so codeweb's own regex extractor
57
+ // can't confuse it with extract-symbols.mjs's directory `walk` — a generic name collides into a
58
+ // spurious duplication finding / dep cycle when the engine dogfoods itself.
59
+ const walkTree = (root, visit) => {
60
+ const stack = [root];
61
+ while (stack.length) {
62
+ const n = stack.pop();
63
+ visit(n);
64
+ for (let i = 0; i < n.childCount; i++) stack.push(n.child(i));
65
+ }
66
+ };
67
+
68
+ // Map a function/method's typed parameters: identifier -> declared class/type name (`p: Pipeline`
69
+ // -> {p:'Pipeline'}). Only simple `type_identifier` annotations (a named class) — array/generic/union
70
+ // types have no single receiver class to dispatch on, so they're left out (precision over recall).
71
+ const paramTypes = (fnNode) => {
72
+ const map = new Map();
73
+ const params = fnNode?.childForFieldName('parameters');
74
+ if (!params) return map;
75
+ for (let i = 0; i < params.childCount; i++) {
76
+ const p = params.child(i);
77
+ if (p.type !== 'required_parameter' && p.type !== 'optional_parameter') continue;
78
+ const pat = p.childForFieldName('pattern');
79
+ const typeAnn = p.childForFieldName('type'); // type_annotation: ':' then the type node
80
+ const typeId = typeAnn && typeAnn.child(1);
81
+ if (pat?.type === 'identifier' && typeId?.type === 'type_identifier') map.set(pat.text, typeId.text);
82
+ }
83
+ return map;
84
+ };
85
+
86
+ // Nearest enclosing NAMED symbol of a node: a method_definition (with its class) or a
87
+ // function_declaration. Returns {name, classNode|null, fnNode} or null (top level / anonymous arrow).
88
+ const enclosingSymbol = (node) => {
89
+ let cur = node.parent;
90
+ while (cur) {
91
+ if (cur.type === 'method_definition') {
92
+ let c = cur.parent;
93
+ while (c && c.type !== 'class_declaration') c = c.parent;
94
+ return { name: cur.childForFieldName('name')?.text, classNode: c || null, fnNode: cur };
95
+ }
96
+ if (cur.type === 'function_declaration') {
97
+ return { name: cur.childForFieldName('name')?.text, classNode: null, fnNode: cur };
98
+ }
99
+ cur = cur.parent;
100
+ }
101
+ return null;
102
+ };
103
+
104
+ let _engine; // undefined = not tried, null = unavailable, object = ready (memoized)
105
+
106
+ // Runtime + version discovery WITHOUT WASM instantiation — shared by the probe and the loader so
107
+ // the version string stamped into meta from a probe can never diverge from a loaded engine's. The
108
+ // package's exports map blocks the package.json subpath, so resolve the entry and walk up to it.
109
+ function runtimeInfo() {
110
+ try {
111
+ let dir = dirname(createRequire(import.meta.url).resolve('web-tree-sitter'));
112
+ for (let i = 0; i < 5; i++) {
113
+ const pj = join(dir, 'package.json');
114
+ if (existsSync(pj)) { const p = JSON.parse(readFileSync(pj, 'utf8')); if (p.name === 'web-tree-sitter') return { present: true, version: p.version }; }
115
+ dir = dirname(dir);
116
+ }
117
+ return { present: true, version: 'unknown' }; // resolvable but package.json not found
118
+ } catch { return { present: false, version: null }; }
119
+ }
120
+ const tsVersionString = (rt) => `tree-sitter(web-tree-sitter@${rt}, typescript@vscode-tree-sitter-wasm@0.3.1/abi14)`;
121
+
122
+ /**
123
+ * Cheap availability probe (Spec A) — file existence + module resolution only, no Parser.init, no
124
+ * Language.load. Lets extraction decide cache namespaces, meta stamps, and banner text up front
125
+ * while the real (expensive) engine loads lazily on first need. Never throws.
126
+ */
127
+ export function probeAst() {
128
+ const rt = runtimeInfo();
129
+ const ts = rt.present && existsSync(GRAMMAR);
130
+ return {
131
+ ts,
132
+ java: rt.present && existsSync(LANG_GRAMMARS.java),
133
+ csharp: rt.present && existsSync(LANG_GRAMMARS.csharp),
134
+ python: rt.present && existsSync(LANG_GRAMMARS.python),
135
+ go: rt.present && existsSync(LANG_GRAMMARS.go),
136
+ rust: rt.present && existsSync(LANG_GRAMMARS.rust),
137
+ tsVersion: ts ? tsVersionString(rt.version) : null,
138
+ };
139
+ }
140
+
141
+ // Lazily build the engine. Returns { cyclomaticExact, version } or null. Never throws.
142
+ export async function loadTsEngine() {
143
+ if (_engine !== undefined) return _engine;
144
+ try {
145
+ if (!existsSync(GRAMMAR)) { _engine = null; return _engine; }
146
+ const ts = await import('web-tree-sitter');
147
+ const { Parser, Language } = ts;
148
+ await Parser.init();
149
+ const parser = new Parser();
150
+ parser.setLanguage(await Language.load(readFileSync(GRAMMAR)));
151
+
152
+ const version = tsVersionString(runtimeInfo().version);
153
+
154
+ const cyclomaticExact = (src) => 1 + countDecisions(parser.parse(String(src || '')).rootNode);
155
+
156
+ // Whole-file JS/TS extractor (Increment 2): the source of truth for METHOD nodes (class-qualified
157
+ // ids `<rel>:Class.method`, BARE labels) + the dynamic-dispatch call edges the regex engine drops
158
+ // (`this.m()` and typed-receiver `x.m()`). One parse per file; ported from the proven precision
159
+ // contract in spike/tree-sitter/extract-ts.mjs. Returns null on any failure so the caller falls
160
+ // back to the regex scanner per-file. Classes/functions keep bare ids (regex still owns them).
161
+ // Spec H helpers: statement normalization + FNV-1a hash for Type-3 fingerprints.
162
+ const FN_LIKE = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'arrow_function', 'method_definition']);
163
+ const JS_KW = new Set(['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'return', 'break', 'continue', 'throw', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'instanceof', 'in', 'of', 'void', 'yield', 'await', 'async', 'function', 'class', 'extends', 'super', 'this', 'const', 'let', 'var', 'null', 'undefined', 'true', 'false']);
164
+ // Built via RegExp so no quote character appears inside a regex LITERAL — the extractor's
165
+ // maskJs doesn't mask regex literals, and a bare quote there flips its string state and
166
+ // swallows the following lines (codeweb's own gate caught exactly that on this file).
167
+ const Q = String.fromCharCode(39), DQ = String.fromCharCode(34), BT = String.fromCharCode(96);
168
+ const strRe = (q) => new RegExp(q + '(?:[^' + q + '\\\\]|\\\\[^])*' + q, 'g');
169
+ const TPL_STR_RE = strRe(BT), SQ_STR_RE = strRe(Q), DQ_STR_RE = strRe(DQ);
170
+ const stmtHash = (text) => {
171
+ // one tokenizing pass: keywords keep identity (uppercased), identifiers -> I, numbers -> N,
172
+ // string/template contents -> S, whitespace dropped. Statement STRUCTURE survives; naming
173
+ // and literals do not — Type-2 normalization per statement, Type-3 via the multiset.
174
+ const t = String(text)
175
+ .replace(TPL_STR_RE, 'S').replace(SQ_STR_RE, 'S').replace(DQ_STR_RE, 'S')
176
+ .replace(/\b[A-Za-z_$][\w$]*\b/g, (m) => (JS_KW.has(m) ? m.toUpperCase() : 'I'))
177
+ .replace(/\b\d[\w.]*\b/g, 'N')
178
+ .replace(/\s+/g, '');
179
+ if (t.length < 3) return null; // bare punctuation carries no signal
180
+ let h = 0x811c9dc5;
181
+ for (let i = 0; i < t.length; i++) { h ^= t.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
182
+ return h.toString(16).padStart(8, '0');
183
+ };
184
+ const extractJsTs = (text, relPath) => {
185
+ try {
186
+ const tree = parser.parse(String(text || ''));
187
+ const r = String(relPath).replace(/\\/g, '/');
188
+ const mkId = (name) => `${r}:${name}`;
189
+ const methods = [];
190
+ const methodIds = new Set();
191
+ const methodsByClass = new Map(); // className -> Set(methodName)
192
+
193
+ walkTree(tree.rootNode, (n) => {
194
+ if (n.type !== 'class_declaration') return;
195
+ const cname = n.childForFieldName('name')?.text;
196
+ if (!cname) return;
197
+ const set = methodsByClass.get(cname) || new Set();
198
+ const body = n.childForFieldName('body');
199
+ if (body) {
200
+ for (let i = 0; i < body.childCount; i++) {
201
+ const m = body.child(i);
202
+ if (m.type !== 'method_definition') continue;
203
+ const mname = m.childForFieldName('name')?.text;
204
+ if (!mname) continue;
205
+ set.add(mname);
206
+ const mid = mkId(`${cname}.${mname}`);
207
+ if (methodIds.has(mid)) continue; // overloads collapse to one node
208
+ methodIds.add(mid);
209
+ methods.push({
210
+ id: mid, label: mname,
211
+ line: m.startPosition.row + 1,
212
+ endLine: m.endPosition.row + 1,
213
+ complexity: complexityOf(m.childForFieldName('body')),
214
+ });
215
+ }
216
+ }
217
+ methodsByClass.set(cname, set);
218
+ });
219
+
220
+ // Spec H: statement fingerprints for Type-3 (near-miss) clone detection. Each statement's
221
+ // text is identifier/literal-normalized (keywords preserved) and FNV-hashed; a function's
222
+ // multiset of statement hashes survives reordering and small insertions — exactly what the
223
+ // shingle and skeleton passes cannot see. Attributed to the NEAREST enclosing function-like
224
+ // node, keyed by its start line (the extractor joins them onto nodes by line).
225
+ const t3ByLine = {};
226
+ walkTree(tree.rootNode, (n) => {
227
+ if (n.type !== 'statement_block') return;
228
+ let owner = n.parent; // nearest enclosing function-like owns this block's statements
229
+ while (owner && !FN_LIKE.has(owner.type)) owner = owner.parent;
230
+ if (!owner) return;
231
+ const line = owner.startPosition.row + 1;
232
+ const bucket = t3ByLine[line] || (t3ByLine[line] = []);
233
+ for (let i = 0; i < n.childCount; i++) {
234
+ const st = n.child(i);
235
+ if (!st.isNamed || FN_LIKE.has(st.type) || st.type === 'comment') continue;
236
+ const h = stmtHash(st.text);
237
+ if (h) bucket.push(h);
238
+ }
239
+ });
240
+ for (const k of Object.keys(t3ByLine)) { t3ByLine[k].sort(); if (t3ByLine[k].length < 6) delete t3ByLine[k]; }
241
+
242
+ const dispatch = [];
243
+ const seen = new Set();
244
+ const addDispatch = (from, to) => {
245
+ if (from === to || !methodIds.has(to)) return; // only wire to an emitted method (precision)
246
+ const k = from + '\t' + to;
247
+ if (seen.has(k)) return;
248
+ seen.add(k);
249
+ dispatch.push({ from, to });
250
+ };
251
+ walkTree(tree.rootNode, (n) => {
252
+ if (n.type !== 'call_expression') return;
253
+ const fn = n.childForFieldName('function');
254
+ if (!fn || fn.type !== 'member_expression') return; // plain identifier calls = regex's job
255
+ const obj = fn.childForFieldName('object');
256
+ const prop = fn.childForFieldName('property')?.text;
257
+ if (!obj || !prop) return;
258
+ const owner = enclosingSymbol(n);
259
+ if (!owner?.name) return;
260
+ const ownerClass = owner.classNode?.childForFieldName('name')?.text;
261
+ const from = mkId(ownerClass ? `${ownerClass}.${owner.name}` : owner.name);
262
+ if (obj.type === 'this' && ownerClass) {
263
+ if (methodsByClass.get(ownerClass)?.has(prop)) addDispatch(from, mkId(`${ownerClass}.${prop}`));
264
+ return;
265
+ }
266
+ if (obj.type === 'identifier') {
267
+ const t = paramTypes(owner.fnNode).get(obj.text);
268
+ if (t && methodsByClass.get(t)?.has(prop)) addDispatch(from, mkId(`${t}.${prop}`));
269
+ }
270
+ });
271
+
272
+ // Canonical order so the merged graph is deterministic regardless of DFS direction.
273
+ methods.sort((a, b) => a.line - b.line || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
274
+ dispatch.sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : a.to < b.to ? -1 : a.to > b.to ? 1 : 0));
275
+ return { methods, dispatch, t3ByLine };
276
+ } catch {
277
+ return null; // any parse/traversal failure -> regex fallback for this file
278
+ }
279
+ };
280
+
281
+ _engine = { cyclomaticExact, extractJsTs, version };
282
+ } catch {
283
+ _engine = null; // any failure (missing dep, init error) → graceful fallback signal
284
+ }
285
+ return _engine;
286
+ }
287
+
288
+ // ---- Java / C# dispatch tier (docs/specs/java-cs-tree-sitter.md) -------------------------------
289
+ // The regex tier owns Java/C# NODES (proven in the v8 expansion); this tier contributes only the
290
+ // DISPATCH information regex deliberately drops: same-file `this.m()` calls resolved immediately,
291
+ // and typed-receiver `helper.compute()` calls emitted as INTENTS {from, recvType, method} for the
292
+ // extractor's global pass to resolve against the whole graph (unique class name -> edge; anything
293
+ // ambiguous is dropped and counted — the same precision contract as the JS tier). Grammar absent
294
+ // or web-tree-sitter missing -> null, and regex output stays byte-identical.
295
+
296
+ const LANG_GRAMMARS = {
297
+ java: join(HERE, '..', 'grammars', 'tree-sitter-java.wasm'),
298
+ csharp: join(HERE, '..', 'grammars', 'tree-sitter-c-sharp.wasm'),
299
+ python: join(HERE, '..', 'grammars', 'tree-sitter-python.wasm'),
300
+ go: join(HERE, '..', 'grammars', 'tree-sitter-go.wasm'),
301
+ rust: join(HERE, '..', 'grammars', 'tree-sitter-rust.wasm'),
302
+ };
303
+ const LANG_SHAPES = {
304
+ java: { classes: new Set(['class_declaration']), method: 'method_declaration', invoke: 'method_invocation', param: 'formal_parameter', typeNode: 'type_identifier', thisNode: 'this' },
305
+ csharp: { classes: new Set(['class_declaration']), method: 'method_declaration', invoke: 'invocation_expression', param: 'parameter', typeNode: 'identifier', thisNode: 'this_expression' },
306
+ };
307
+
308
+ // Spec F: Python/Go/Rust dispatch walkers — the shape table fits the Java/C# family; these three
309
+ // need dedicated tree shapes (probed empirically, pinned by tests). Same contract as the shape
310
+ // path: regex owns nodes; the walker returns ONLY {thisCalls, typedIntents}, self/receiver calls
311
+ // resolved in-file, typed intents resolved globally by extract-symbols under the one-owner rule.
312
+ // Receivers/types accepted only as bare identifiers (precision over recall, as everywhere).
313
+ const BARE_TYPE = /^[A-Za-z_][\w]*$/;
314
+ const stripRustRef = (t) => t.replace(/^&\s*(?:mut\s+)?/, '').trim();
315
+ const LANG_WALKERS = {
316
+ python: (parser) => (text, relPath) => {
317
+ try {
318
+ const tree = parser.parse(String(text || ''));
319
+ const r = String(relPath).replace(/\\/g, '/');
320
+ const name = (n) => n?.childForFieldName('name')?.text || null;
321
+ const up = (n, type) => { let c = n.parent; while (c && c.type !== type) c = c.parent; return c; };
322
+ const methodsByClass = new Map();
323
+ walkTree(tree.rootNode, (n) => {
324
+ if (n.type !== 'class_definition') return;
325
+ const cn = name(n); if (!cn) return;
326
+ const set = methodsByClass.get(cn) || new Set();
327
+ const body = n.childForFieldName('body');
328
+ if (body) for (let i = 0; i < body.childCount; i++) { const m = body.child(i); if (m.type === 'function_definition') { const mn = name(m); if (mn) set.add(mn); } }
329
+ methodsByClass.set(cn, set);
330
+ });
331
+ const typedParamsOf = (fn) => {
332
+ const map = new Map();
333
+ const params = fn?.childForFieldName('parameters');
334
+ if (!params) return map;
335
+ for (let i = 0; i < params.childCount; i++) {
336
+ const p = params.child(i);
337
+ if (p.type !== 'typed_parameter') continue;
338
+ const id = p.child(0), ty = p.childForFieldName('type');
339
+ if (id?.type === 'identifier' && ty && BARE_TYPE.test(ty.text)) map.set(id.text, ty.text);
340
+ }
341
+ return map;
342
+ };
343
+ const thisCalls = [], typedIntents = [], seen = new Set();
344
+ walkTree(tree.rootNode, (n) => {
345
+ if (n.type !== 'call') return;
346
+ const fn = n.childForFieldName('function');
347
+ if (!fn || fn.type !== 'attribute') return;
348
+ const obj = fn.childForFieldName('object'), prop = fn.childForFieldName('attribute')?.text;
349
+ if (!obj || obj.type !== 'identifier' || !prop) return;
350
+ const encl = up(n, 'function_definition'); if (!encl) return;
351
+ const enclCls = up(encl, 'class_definition');
352
+ const from = `${r}:${enclCls ? name(enclCls) + '.' : ''}${name(encl)}`;
353
+ if (obj.text === 'self' || obj.text === 'cls') {
354
+ const cls = enclCls && name(enclCls);
355
+ if (cls && methodsByClass.get(cls)?.has(prop)) {
356
+ const to = `${r}:${cls}.${prop}`;
357
+ const k = from + '\t' + to;
358
+ if (from !== to && !seen.has(k)) { seen.add(k); thisCalls.push({ from, to }); }
359
+ }
360
+ return;
361
+ }
362
+ const t = typedParamsOf(encl).get(obj.text);
363
+ if (t) { const k = from + '\t' + t + '\t' + prop; if (!seen.has(k)) { seen.add(k); typedIntents.push({ from, recvType: t, method: prop }); } }
364
+ });
365
+ thisCalls.sort((a, b) => (a.from + a.to < b.from + b.to ? -1 : 1));
366
+ typedIntents.sort((a, b) => ((a.from + a.recvType + a.method) < (b.from + b.recvType + b.method) ? -1 : 1));
367
+ return { thisCalls, typedIntents };
368
+ } catch { return null; }
369
+ },
370
+ go: (parser) => (text, relPath) => {
371
+ try {
372
+ const tree = parser.parse(String(text || ''));
373
+ const r = String(relPath).replace(/\\/g, '/');
374
+ const up = (n, types) => { let c = n.parent; while (c && !types.has(c.type)) c = c.parent; return c; };
375
+ const FN_TYPES = new Set(['method_declaration', 'function_declaration']);
376
+ const recvOf = (m) => { // -> {varName, typeName} | null, pointer stripped
377
+ const recv = m.childForFieldName('receiver');
378
+ if (!recv) return null;
379
+ for (let i = 0; i < recv.childCount; i++) {
380
+ const p = recv.child(i);
381
+ if (p.type !== 'parameter_declaration') continue;
382
+ const nm = p.childForFieldName('name')?.text;
383
+ let ty = p.childForFieldName('type');
384
+ if (ty?.type === 'pointer_type') ty = ty.child(1) || ty;
385
+ const tn = ty?.text?.replace(/^\*/, '');
386
+ if (nm && tn && BARE_TYPE.test(tn)) return { varName: nm, typeName: tn };
387
+ }
388
+ return null;
389
+ };
390
+ const methodsByType = new Map();
391
+ walkTree(tree.rootNode, (n) => {
392
+ if (n.type !== 'method_declaration') return;
393
+ const rec = recvOf(n), mn = n.childForFieldName('name')?.text;
394
+ if (!rec || !mn) return;
395
+ const set = methodsByType.get(rec.typeName) || new Set();
396
+ set.add(mn);
397
+ methodsByType.set(rec.typeName, set);
398
+ });
399
+ const typedParamsOf = (fn) => {
400
+ const map = new Map();
401
+ const params = fn?.childForFieldName('parameters');
402
+ if (!params) return map;
403
+ for (let i = 0; i < params.childCount; i++) {
404
+ const p = params.child(i);
405
+ if (p.type !== 'parameter_declaration') continue;
406
+ const nm = p.childForFieldName('name')?.text;
407
+ let ty = p.childForFieldName('type');
408
+ if (ty?.type === 'pointer_type') ty = ty.child(1) || ty;
409
+ const tn = ty?.text?.replace(/^\*/, '');
410
+ if (nm && tn && BARE_TYPE.test(tn)) map.set(nm, tn);
411
+ }
412
+ return map;
413
+ };
414
+ const thisCalls = [], typedIntents = [], seen = new Set();
415
+ walkTree(tree.rootNode, (n) => {
416
+ if (n.type !== 'call_expression') return;
417
+ const fn = n.childForFieldName('function');
418
+ if (!fn || fn.type !== 'selector_expression') return;
419
+ const obj = fn.childForFieldName('operand'), prop = fn.childForFieldName('field')?.text;
420
+ if (!obj || obj.type !== 'identifier' || !prop) return;
421
+ const encl = up(n, FN_TYPES); if (!encl) return;
422
+ const enclName = encl.childForFieldName('name')?.text; if (!enclName) return;
423
+ const enclRecv = encl.type === 'method_declaration' ? recvOf(encl) : null;
424
+ const from = `${r}:${enclRecv ? enclRecv.typeName + '.' : ''}${enclName}`;
425
+ if (enclRecv && obj.text === enclRecv.varName) {
426
+ if (methodsByType.get(enclRecv.typeName)?.has(prop)) {
427
+ const to = `${r}:${enclRecv.typeName}.${prop}`;
428
+ const k = from + '\t' + to;
429
+ if (from !== to && !seen.has(k)) { seen.add(k); thisCalls.push({ from, to }); }
430
+ }
431
+ return;
432
+ }
433
+ const t = typedParamsOf(encl).get(obj.text);
434
+ if (t) { const k = from + '\t' + t + '\t' + prop; if (!seen.has(k)) { seen.add(k); typedIntents.push({ from, recvType: t, method: prop }); } }
435
+ });
436
+ thisCalls.sort((a, b) => (a.from + a.to < b.from + b.to ? -1 : 1));
437
+ typedIntents.sort((a, b) => ((a.from + a.recvType + a.method) < (b.from + b.recvType + b.method) ? -1 : 1));
438
+ return { thisCalls, typedIntents };
439
+ } catch { return null; }
440
+ },
441
+ rust: (parser) => (text, relPath) => {
442
+ try {
443
+ const tree = parser.parse(String(text || ''));
444
+ const r = String(relPath).replace(/\\/g, '/');
445
+ const up = (n, type) => { let c = n.parent; while (c && c.type !== type) c = c.parent; return c; };
446
+ const implType = (imp) => { const t = imp.childForFieldName('type'); return t && BARE_TYPE.test(t.text) ? t.text : null; };
447
+ const methodsByType = new Map();
448
+ walkTree(tree.rootNode, (n) => {
449
+ if (n.type !== 'impl_item') return;
450
+ const tn = implType(n); if (!tn) return;
451
+ const set = methodsByType.get(tn) || new Set();
452
+ const body = n.childForFieldName('body');
453
+ if (body) for (let i = 0; i < body.childCount; i++) { const f = body.child(i); if (f.type === 'function_item') { const fn = f.childForFieldName('name')?.text; if (fn) set.add(fn); } }
454
+ methodsByType.set(tn, set);
455
+ });
456
+ const typedParamsOf = (fn) => {
457
+ const map = new Map();
458
+ const params = fn?.childForFieldName('parameters');
459
+ if (!params) return map;
460
+ for (let i = 0; i < params.childCount; i++) {
461
+ const p = params.child(i);
462
+ if (p.type !== 'parameter') continue;
463
+ const nm = p.childForFieldName('pattern')?.text;
464
+ const tn = p.childForFieldName('type') ? stripRustRef(p.childForFieldName('type').text) : null;
465
+ if (nm && tn && BARE_TYPE.test(nm) && BARE_TYPE.test(tn)) map.set(nm, tn);
466
+ }
467
+ return map;
468
+ };
469
+ const thisCalls = [], typedIntents = [], seen = new Set();
470
+ walkTree(tree.rootNode, (n) => {
471
+ if (n.type !== 'call_expression') return;
472
+ const fn = n.childForFieldName('function');
473
+ if (!fn || fn.type !== 'field_expression') return;
474
+ const obj = fn.childForFieldName('value'), prop = fn.childForFieldName('field')?.text;
475
+ if (!obj || !prop) return;
476
+ const encl = up(n, 'function_item'); if (!encl) return;
477
+ const enclImpl = up(encl, 'impl_item');
478
+ const implName = enclImpl && implType(enclImpl);
479
+ const from = `${r}:${implName ? implName + '.' : ''}${encl.childForFieldName('name')?.text}`;
480
+ if (obj.type === 'self' || obj.text === 'self') {
481
+ if (implName && methodsByType.get(implName)?.has(prop)) {
482
+ const to = `${r}:${implName}.${prop}`;
483
+ const k = from + '\t' + to;
484
+ if (from !== to && !seen.has(k)) { seen.add(k); thisCalls.push({ from, to }); }
485
+ }
486
+ return;
487
+ }
488
+ if (obj.type !== 'identifier') return;
489
+ const t = typedParamsOf(encl).get(obj.text);
490
+ if (t) { const k = from + '\t' + t + '\t' + prop; if (!seen.has(k)) { seen.add(k); typedIntents.push({ from, recvType: t, method: prop }); } }
491
+ });
492
+ thisCalls.sort((a, b) => (a.from + a.to < b.from + b.to ? -1 : 1));
493
+ typedIntents.sort((a, b) => ((a.from + a.recvType + a.method) < (b.from + b.recvType + b.method) ? -1 : 1));
494
+ return { thisCalls, typedIntents };
495
+ } catch { return null; }
496
+ },
497
+ };
498
+
499
+ const _langEngines = {}; // key -> undefined(not tried)/null(unavailable)/engine
500
+
501
+ /** Lazily load the dispatch engine for 'java'|'csharp'|'python'|'go'|'rust'. Returns { extractDispatch } or null. */
502
+ export async function loadLangEngine(key) {
503
+ if (_langEngines[key] !== undefined) return _langEngines[key];
504
+ try {
505
+ const grammarPath = LANG_GRAMMARS[key];
506
+ const shape = LANG_SHAPES[key];
507
+ if (!grammarPath || (!shape && !LANG_WALKERS[key]) || !existsSync(grammarPath)) { _langEngines[key] = null; return null; }
508
+ const ts = await import('web-tree-sitter');
509
+ const { Parser, Language } = ts;
510
+ await Parser.init();
511
+ const parser = new Parser();
512
+ parser.setLanguage(await Language.load(readFileSync(grammarPath)));
513
+
514
+ if (LANG_WALKERS[key]) { // Spec F: dedicated tree-shape walker (python/go/rust)
515
+ _langEngines[key] = { extractDispatch: LANG_WALKERS[key](parser) };
516
+ return _langEngines[key];
517
+ }
518
+
519
+ const className = (n) => n.childForFieldName('name')?.text || null;
520
+ const enclosing = (node, types) => { let c = node.parent; while (c && !types.has(c.type)) c = c.parent; return c; };
521
+ const METHOD_SET = new Set([shape.method]);
522
+
523
+ const extractDispatch = (text, relPath) => {
524
+ try {
525
+ const tree = parser.parse(String(text || ''));
526
+ const r = String(relPath).replace(/\\/g, '/');
527
+ const qualId = (cls, m) => `${r}:${cls}.${m}`;
528
+ const methodsByClass = new Map(); // class -> Set(method names) in THIS file
529
+ walkTree(tree.rootNode, (n) => {
530
+ if (!shape.classes.has(n.type)) return;
531
+ const cname = className(n);
532
+ if (!cname) return;
533
+ const set = methodsByClass.get(cname) || new Set();
534
+ const body = n.childForFieldName('body');
535
+ if (body) for (let i = 0; i < body.childCount; i++) {
536
+ const m = body.child(i);
537
+ if (m.type === shape.method) { const mn = className(m); if (mn) set.add(mn); }
538
+ }
539
+ methodsByClass.set(cname, set);
540
+ });
541
+
542
+ // declared param types of the enclosing method: identifier -> class/type name
543
+ const paramTypesOf = (methodNode) => {
544
+ const map = new Map();
545
+ const params = methodNode?.childForFieldName('parameters');
546
+ if (!params) return map;
547
+ for (let i = 0; i < params.childCount; i++) {
548
+ const p = params.child(i);
549
+ if (p.type !== shape.param) continue;
550
+ const tNode = p.childForFieldName('type');
551
+ const nNode = p.childForFieldName('name');
552
+ if (tNode?.type === shape.typeNode && nNode) map.set(nNode.text, tNode.text);
553
+ }
554
+ return map;
555
+ };
556
+
557
+ const thisCalls = [], typedIntents = [];
558
+ const seen = new Set();
559
+ walkTree(tree.rootNode, (n) => {
560
+ if (n.type !== shape.invoke) return;
561
+ let obj, prop;
562
+ if (key === 'java') {
563
+ obj = n.childForFieldName('object');
564
+ prop = n.childForFieldName('name')?.text;
565
+ } else {
566
+ const fn = n.childForFieldName('function');
567
+ if (!fn || fn.type !== 'member_access_expression') return;
568
+ obj = fn.childForFieldName('expression');
569
+ prop = fn.childForFieldName('name')?.text;
570
+ }
571
+ if (!obj || !prop) return;
572
+ const mNode = enclosing(n, METHOD_SET);
573
+ const cNode = mNode && enclosing(mNode, shape.classes);
574
+ const mName = mNode && className(mNode), cName = cNode && className(cNode);
575
+ if (!mName || !cName) return;
576
+ const from = qualId(cName, mName);
577
+ if (obj.type === shape.thisNode) {
578
+ if (methodsByClass.get(cName)?.has(prop)) {
579
+ const to = qualId(cName, prop);
580
+ const k = from + '\t' + to;
581
+ if (from !== to && !seen.has(k)) { seen.add(k); thisCalls.push({ from, to }); }
582
+ }
583
+ return;
584
+ }
585
+ if (obj.type === 'identifier') {
586
+ const t = paramTypesOf(mNode).get(obj.text);
587
+ if (t) {
588
+ const k = from + '\t' + t + '\t' + prop;
589
+ if (!seen.has(k)) { seen.add(k); typedIntents.push({ from, recvType: t, method: prop }); }
590
+ }
591
+ }
592
+ });
593
+ thisCalls.sort((a, b) => (a.from + a.to < b.from + b.to ? -1 : 1));
594
+ typedIntents.sort((a, b) => ((a.from + a.recvType + a.method) < (b.from + b.recvType + b.method) ? -1 : 1));
595
+ return { thisCalls, typedIntents };
596
+ } catch { return null; } // per-file fallback: regex output stands alone
597
+ };
598
+
599
+ _langEngines[key] = { extractDispatch };
600
+ } catch { _langEngines[key] = null; }
601
+ return _langEngines[key];
602
+ }
603
+
604
+ // Test-only: reset the memoized engine so a test can re-exercise the load path.
605
+ export function _resetForTest() { _engine = undefined; for (const k of Object.keys(_langEngines)) delete _langEngines[k]; }