@azure-id/orc 1.7.1 → 1.8.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +3869 -3381
  2. package/README-id.md +942 -844
  3. package/README.md +882 -788
  4. package/bin/build-agents.js +43 -27
  5. package/bin/cli.js +45460 -44169
  6. package/bin/graph-extract.js +3216 -0
  7. package/bin/graph-gain.js +404 -0
  8. package/bin/graph-map.js +232 -0
  9. package/bin/graph-notes.js +229 -0
  10. package/bin/graph-query.js +1770 -0
  11. package/bin/graph-resolve.js +255 -0
  12. package/bin/graph-shard.js +325 -0
  13. package/bin/graph-signals.js +277 -0
  14. package/bin/graph.js +658 -0
  15. package/bin/verify-contracts.js +4809 -4553
  16. package/bin/verify-package.js +640 -616
  17. package/bin/webui/api.js +1425 -1414
  18. package/bin/webui/fixtures/index.js +584 -576
  19. package/bin/webui/fixtures/knowledge.js +356 -291
  20. package/bin/webui/fixtures/stats.js +107 -104
  21. package/bin/webui/i18n/en/knowledge.json +182 -151
  22. package/bin/webui/i18n/en/overview.json +101 -100
  23. package/bin/webui/i18n/id/knowledge.json +182 -151
  24. package/bin/webui/i18n/id/overview.json +101 -100
  25. package/bin/webui/js/panels/knowledge.js +1130 -1006
  26. package/bin/webui/js/panels/overview.js +492 -488
  27. package/package.json +39 -39
  28. package/templates/agents/MODEL-MAPPING.md +163 -158
  29. package/templates/agents/orc-executor-haiku-4-5.md +128 -121
  30. package/templates/agents/orc-executor-opus-4-7-high.md +129 -122
  31. package/templates/agents/orc-executor-opus-4-7-med.md +129 -122
  32. package/templates/agents/orc-executor-opus-4-8-high.md +129 -122
  33. package/templates/agents/orc-executor-opus-5-high.md +129 -122
  34. package/templates/agents/orc-executor-opus-5-low.md +129 -122
  35. package/templates/agents/orc-executor-opus-5-med.md +129 -122
  36. package/templates/agents/orc-executor-sonnet-4-6-high.md +129 -122
  37. package/templates/agents/orc-executor-sonnet-4-6-med.md +129 -122
  38. package/templates/agents/orc-executor-sonnet-5-high.md +129 -122
  39. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +89 -0
  40. package/templates/hooks/README.md +454 -396
  41. package/templates/hooks/orc-graph-hook.js +471 -0
  42. package/templates/hooks/orc-statusline-render.js +922 -921
  43. package/templates/hooks/orc-statusline.js +1596 -1545
  44. package/templates/skills/_shared/README.md +4 -0
  45. package/templates/skills/_shared/code-graph.md +348 -0
  46. package/templates/skills/_shared/opus5-only.md +4 -0
  47. package/templates/skills/_shared/phases/execution.md +168 -147
  48. package/templates/skills/_shared/phases/planning.md +149 -135
  49. package/templates/skills/_shared/phases/preflight.md +132 -118
  50. package/templates/skills/_shared/phases/review.md +63 -53
  51. package/templates/skills/_shared/phases/ship.md +100 -88
  52. package/templates/skills/_shared/phases/trace.md +8 -0
  53. package/templates/skills/_shared/phases/wiki-consult.md +194 -189
  54. package/templates/skills/_shared/read-ladder.md +132 -102
  55. package/templates/skills/_shared/return-validation.md +259 -250
  56. package/templates/skills/orc/SKILL.md +255 -254
  57. package/templates/skills/orc-analyze/SKILL.md +13 -13
  58. package/templates/skills/orc-diy/references/flow-schema.md +101 -100
  59. package/templates/skills/orc-fast/SKILL.md +236 -229
  60. package/templates/skills/orc-mini/SKILL.md +267 -259
  61. package/templates/skills/orc-quick/SKILL.md +378 -361
  62. package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
  63. package/templates/skills/orc-wiki/references/staleness.md +294 -288
@@ -0,0 +1,3216 @@
1
+ "use strict";
2
+ // ── orc graph — EXTRACTION (v1.8.0 W2 · v1.8.2 W1) ──────────────────────────
3
+ //
4
+ // One source file in, one record out: imports, symbols (with line ranges and a
5
+ // body hash), the calls each symbol makes, and the side effects it has (SQL,
6
+ // db, HTTP, env, fs). No model, no dependency.
7
+ //
8
+ // THE LADDER (D3)
9
+ //
10
+ // 1. BORROWED TOOLCHAIN — Python's own `ast` module, when a Python ≥3.8 runs
11
+ // on this machine. Exact symbols, ranges, calls and imports. One
12
+ // subprocess for the whole batch.
13
+ // 2. HEURISTIC — a comment/string MASK first (so a call inside a comment or a
14
+ // string is never a call), then per-language declaration patterns, brace
15
+ // or indentation matching for the body, and a call scan.
16
+ //
17
+ // TypeScript's own parser is NOT borrowed in this release: no TypeScript was
18
+ // available to test it against, and an untested extractor is worse than an
19
+ // honest heuristic. `ORC_GRAPH_NO_BORROW=1` forces rung 2 (tests use it).
20
+ //
21
+ // What the record promises, and what it does not:
22
+ // - `body_hash` changes when THIS symbol's text changes, and not when another
23
+ // symbol in the same file changes. Notes (W4) key on it.
24
+ // - calls are NAMES, not targets. Resolution is `graph-query.js`'s job, and it
25
+ // is computed on read.
26
+ // - dynamic dispatch, reflection, macros and eval are invisible. The graph is
27
+ // a LOCATOR; the code is still the truth.
28
+ //
29
+ // v1.8.2 W1 — ROUTES ARE EDGES (R3-C). A caller that reaches a symbol through a
30
+ // URL had no edge, and the card was silent about eleven route-level tests. Four
31
+ // additions, all NAMES again, resolved on read by `graph-query.js`:
32
+ // - `urls[]` on a symbol: every string literal that is the URL of an
33
+ // HTTP-shaped call (`request(app).get("/p")`, `client.post("/p")`,
34
+ // `fetch("/p", {method})`, `httptest.NewRequest("GET", "/p")`), with its
35
+ // method. A `BASE + "/p"` is recorded as a SUFFIX (prefix unknown).
36
+ // - `mounts[]` on a module symbol: `app.use("/orders", ordersRouter)`,
37
+ // `include_router(r, prefix=)`, `register_blueprint(bp, url_prefix=)`,
38
+ // Django `path("api/", include("x"))` — a prefix and the NAME it mounts.
39
+ // - decorator routes: `@Get(":id")`, `@router.get("/p")`, `@GetMapping`,
40
+ // `[HttpGet]`, `#[Route]` — the decorated function KEEPS its symbol and gains
41
+ // `route`; an alias ROUTE symbol named `<METHOD> <path>` points at it
42
+ // (`handler`) and CALLS it, so every walk reaches the handler with no
43
+ // special case. A same-file or same-class prefix (`@Controller("orders")`,
44
+ // `APIRouter(prefix=)`, `Blueprint(url_prefix=)`) is folded into the name
45
+ // here; a prefix from ANOTHER file is a mount, applied on read.
46
+ // - registration routes with a handler passed BY NAME: `mux.HandleFunc("/p",
47
+ // h)`, `r.GET("/p", h)`, `Route::get('/p', [C::class, 'm'])`, Django
48
+ // `path("p/", views.x)` and Express `router.get("/p", handler)`.
49
+
50
+ const crypto = require("crypto");
51
+ const fs = require("fs");
52
+ const os = require("os");
53
+ const path = require("path");
54
+ const { spawnSync } = require("child_process");
55
+
56
+ // @3 (W9): route handlers are symbols, and a function passed BY NAME is a `ref`.
57
+ // @4 (EW1): every record says how much of the file the extractor actually saw.
58
+ // @5 (v1.8.2 W1): urls, mounts, decorator routes, `handler` on a route alias.
59
+ const HEURISTIC = "heuristic@5";
60
+ const MAX_CALLS_PER_SYMBOL = 200;
61
+ const MAX_EFFECTS_PER_SYMBOL = 12;
62
+ const MAX_URLS_PER_SYMBOL = 200;
63
+
64
+ const KW = new Set(
65
+ (
66
+ "if for while switch catch function return typeof sizeof def class elif with not and or in " +
67
+ "await yield case new throw else do try foreach using lock fixed func go defer select range " +
68
+ "match fn when elseif isset empty unset array list print echo import export from lambda assert " +
69
+ "del pass raise except finally is this self super base var let const delete instanceof " +
70
+ "require_once include_once require include die exit"
71
+ ).split(" ")
72
+ );
73
+
74
+ // A declaration NAME is checked against its own language's keywords only. The
75
+ // shared `KW` set above is for CALL heads, where `list(` in Python is a
76
+ // builtin — but `list()` is an ordinary method on a TypeScript controller,
77
+ // and W1's Nest fixture lost it to the PHP `list` construct.
78
+ const LANG_KW = {
79
+ js: new Set("if for while switch catch function return typeof await yield case new throw else do try instanceof void this super with class export import default extends".split(" ")),
80
+ go: new Set("func if for switch case return range go defer select type var const map chan struct interface".split(" ")),
81
+ java: new Set("if for while switch catch return new throw try do synchronized this super".split(" ")),
82
+ cs: new Set("if for while switch catch return new throw try do using lock fixed foreach this base".split(" ")),
83
+ php: new Set("if for while switch catch return new throw foreach isset empty unset array list print echo require include function".split(" ")),
84
+ // W5 (G6). Each list is that language's control words only — a name a real
85
+ // declaration can never carry. `get` is a Ktor route and a Ruby method, so
86
+ // it is never in one of these sets.
87
+ rb: new Set("if elsif unless while until for case when begin rescue ensure end def class module do then return yield super self nil true false and or not".split(" ")),
88
+ rs: new Set("if else match loop while for return break continue let mut fn impl struct enum trait mod use pub crate self super where as move ref unsafe".split(" ")),
89
+ kt: new Set("if else when while for return break continue val var fun class object interface is in as by try catch finally throw this super companion".split(" ")),
90
+ c: new Set("if else switch case for while do return break continue goto sizeof typedef struct union enum static extern const inline register volatile new delete throw try catch template typename namespace using operator".split(" ")),
91
+ };
92
+ LANG_KW.ts = LANG_KW.js;
93
+ LANG_KW.vue = LANG_KW.js;
94
+ LANG_KW.svelte = LANG_KW.js;
95
+ const isKw = (lang, name) => (LANG_KW[lang] || KW).has(name);
96
+ // A class MEMBER can be called `delete`, `get`, `of` or `list`; only a control
97
+ // word that a failed body skip could expose is refused there.
98
+ const MEMBER_KW = new Set("if for while switch catch return function typeof throw else do try".split(" "));
99
+
100
+ const HTTP_VERBS = new Set(["get", "post", "put", "patch", "delete", "del", "head", "options"]);
101
+ const VERB_OF = (v) => (v === "del" ? "DELETE" : String(v).toUpperCase());
102
+
103
+ // ── text utilities ──────────────────────────────────────────────────────────
104
+ function lineIndex(src) {
105
+ const starts = [0];
106
+ for (let i = 0; i < src.length; i++) if (src.charCodeAt(i) === 10) starts.push(i + 1);
107
+ return starts;
108
+ }
109
+
110
+ function lineOf(starts, pos) {
111
+ let lo = 0;
112
+ let hi = starts.length - 1;
113
+ while (lo < hi) {
114
+ const mid = (lo + hi + 1) >> 1;
115
+ if (starts[mid] <= pos) lo = mid;
116
+ else hi = mid - 1;
117
+ }
118
+ return lo + 1;
119
+ }
120
+
121
+ const clip = (s, n = 80) => {
122
+ const t = String(s).replace(/\s+/g, " ").trim();
123
+ return t.length > n ? t.slice(0, n - 1) + "…" : t;
124
+ };
125
+
126
+ const hash = (s) => crypto.createHash("sha1").update(String(s).replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
127
+
128
+ // ── N1 (v1.8.2 W4): doc notes at 0 model tokens ─────────────────────────────
129
+ // Layer 2 pays a subagent for one sentence about a function. For a documented
130
+ // function that sentence is ALREADY THERE — a docstring, a JSDoc block, a
131
+ // `///` run, a `#` run — and the extractor is reading the bytes anyway.
132
+ //
133
+ // It is a NOTE, not a fact. A comment can lie and a comment can rot, which is
134
+ // why the precedence line puts `doc` beside graph notes, below a stale wiki,
135
+ // and why a MODEL note outranks the doc note for the same symbol. The body
136
+ // hash rule is the same: a doc is shown only while the body hashes the same,
137
+ // and since it is re-extracted with the body it can never be stale on its own.
138
+ const DOC_MAX = 240;
139
+ // The first line that is a TAG ends the sentence: `@param`, `:returns:`,
140
+ // `Args:`. Nobody wants half a parameter table as the summary of a function.
141
+ const DOC_TAG = /^\s*(?:[@\\][A-Za-z]|:(?:param|type|returns?|raises?|rtype|arg)\b|(?:Args|Arguments|Returns?|Raises?|Yields?|Params?|Parameters|Example|Examples|Note|Notes|Attributes|See Also|Usage|Todo|TODO)\s*:|[-=*_]{3,}\s*$)/;
142
+
143
+ function docText(lines) {
144
+ const buf = [];
145
+ for (const raw of lines) {
146
+ if (DOC_TAG.test(raw)) break;
147
+ buf.push(raw);
148
+ }
149
+ let t = buf.join(" ").replace(/\s+/g, " ").trim();
150
+ if (!t) return "";
151
+ // The FIRST sentence. `Math.max` guards an abbreviation at the very start.
152
+ const m = /^([\s\S]*?[.!?])(?:\s|$)/.exec(t);
153
+ if (m && m[1].length >= 12) t = m[1];
154
+ t = t.replace(/^[*/#\s]+/, "").trim();
155
+ return t.length > DOC_MAX ? t.slice(0, DOC_MAX - 1) + "…" : t;
156
+ }
157
+
158
+ // Python: the docstring is the first thing INSIDE the body.
159
+ function pyDoc(lines, d) {
160
+ let i = d.s - 1; // 0-based index of the `def` line
161
+ const lim = Math.min(lines.length, d.s + 8);
162
+ while (i < lim && !/:\s*(?:#.*)?$/.test(lines[i] || "")) i++;
163
+ i++;
164
+ while (i < lim && !(lines[i] || "").trim()) i++;
165
+ const first = (lines[i] || "").trim();
166
+ const q = /^[rRbBuUfF]{0,2}("""|''')/.exec(first);
167
+ if (!q) return "";
168
+ const open = first.indexOf(q[1]) + 3;
169
+ const rest = first.slice(open);
170
+ const close = rest.indexOf(q[1]);
171
+ if (close >= 0) return docText([rest.slice(0, close)]);
172
+ const buf = [rest];
173
+ for (let j = i + 1; j < Math.min(lines.length, i + 16); j++) {
174
+ const ln = lines[j] || "";
175
+ const e = ln.indexOf(q[1]);
176
+ if (e >= 0) {
177
+ buf.push(ln.slice(0, e));
178
+ break;
179
+ }
180
+ buf.push(ln);
181
+ }
182
+ return docText(buf);
183
+ }
184
+
185
+ // Everything else: the comment block immediately ABOVE, past any decorators.
186
+ function aboveDoc(lines, d) {
187
+ let i = d.s - 2; // 0-based index of the line above the definition
188
+ const skip = (t) => !t || /^@/.test(t) || /^\[[A-Za-z]/.test(t) || /^#\[/.test(t);
189
+ while (i >= 0 && skip((lines[i] || "").trim())) i--;
190
+ if (i < 0) return "";
191
+ const t = (lines[i] || "").trim();
192
+ if (/\*\/$/.test(t)) {
193
+ const buf = [];
194
+ let j = i;
195
+ for (; j >= 0 && j > i - 40; j--) {
196
+ const s = (lines[j] || "").trim();
197
+ buf.unshift(s.replace(/\*\/\s*$/, "").replace(/^\/\*+!?/, "").replace(/^\*+\/?\s?/, ""));
198
+ if (/^\/\*/.test(s)) break;
199
+ }
200
+ return j < 0 ? "" : docText(buf);
201
+ }
202
+ const head = /^\/\//.test(t) ? "//" : /^#/.test(t) ? "#" : /^--/.test(t) ? "--" : null;
203
+ if (!head) return "";
204
+ const strip = head === "//" ? /^\/\/+[!/]?\s?/ : head === "#" ? /^#+!?\s?/ : /^-{2,}!?\s?/;
205
+ const buf = [];
206
+ for (let j = i; j >= 0 && j > i - 20 && (lines[j] || "").trim().startsWith(head); j--) buf.unshift((lines[j] || "").trim().replace(strip, ""));
207
+ return docText(buf);
208
+ }
209
+
210
+ const DOC_KINDS = new Set(["function", "method", "route"]);
211
+
212
+ function docFor(lines, lang, d) {
213
+ if (!DOC_KINDS.has(d.kind)) return "";
214
+ try {
215
+ // Python first looks INSIDE (the docstring), then above (a `#` block) —
216
+ // measured on django, the `#` block above is the only other place a Python
217
+ // author writes the sentence.
218
+ if (lang === "py") return pyDoc(lines, d) || aboveDoc(lines, d) || "";
219
+ return aboveDoc(lines, d) || "";
220
+ } catch (_) {
221
+ return ""; // a doc is a nicety; it never fails an extraction
222
+ }
223
+ }
224
+
225
+ // Replace comment and string CONTENT with spaces, keeping every newline and
226
+ // every offset. Declarations and calls are found in the mask; string text (for
227
+ // SQL and URLs) is read back from the original at the recorded spans.
228
+ function mask(src, lang) {
229
+ const slash = lang !== "py" && lang !== "rb";
230
+ const hashC = lang === "py" || lang === "php" || lang === "rb";
231
+ const block = lang !== "py" && lang !== "rb";
232
+ // A Ruby backtick runs a shell command, so its content is a string. A Kotlin
233
+ // backtick quotes an IDENTIFIER (`fun \`adds an order\`()`), so it is code.
234
+ const backtick = lang === "js" || lang === "ts" || lang === "go" || lang === "rb";
235
+ const m = src.split("");
236
+ const n = src.length;
237
+ const strings = [];
238
+ const blank = (a, b) => {
239
+ for (let k = a; k < b && k < n; k++) if (m[k] !== "\n" && m[k] !== "\r") m[k] = " ";
240
+ };
241
+ let i = 0;
242
+ while (i < n) {
243
+ const c = src[i];
244
+ const c2 = src[i + 1];
245
+ if ((slash && c === "/" && c2 === "/") || (hashC && c === "#" && !(lang === "php" && c2 === "["))) {
246
+ const e = src.indexOf("\n", i);
247
+ const end = e < 0 ? n : e;
248
+ blank(i, end);
249
+ i = end;
250
+ continue;
251
+ }
252
+ if (block && c === "/" && c2 === "*") {
253
+ const e = src.indexOf("*/", i + 2);
254
+ const end = e < 0 ? n : e + 2;
255
+ blank(i, end);
256
+ i = end;
257
+ continue;
258
+ }
259
+ // W5 (G6). Ruby `=begin` … `=end` is a block comment, at column 0 only.
260
+ if (lang === "rb" && src.startsWith("=begin", i) && (i === 0 || src[i - 1] === "\n")) {
261
+ const e = src.indexOf("\n=end", i);
262
+ const end = e < 0 ? n : src.indexOf("\n", e + 1) < 0 ? n : src.indexOf("\n", e + 1);
263
+ blank(i, end);
264
+ i = end;
265
+ continue;
266
+ }
267
+ // W5 (G6). A Rust raw string `r"…"` / `r#"…"#`, and a Rust LIFETIME or
268
+ // char literal. `&'a str` opens a quote that never closes, and without
269
+ // this rule the mask blanks the rest of the line — the same class of loss
270
+ // G5 fixed for regex literals.
271
+ if (lang === "rs" && c === "r" && (c2 === '"' || c2 === "#")) {
272
+ let h = 0;
273
+ while (src[i + 1 + h] === "#") h++;
274
+ if (src[i + 1 + h] === '"') {
275
+ const close = '"' + "#".repeat(h);
276
+ const e = src.indexOf(close, i + 2 + h);
277
+ const end = e < 0 ? n : e;
278
+ strings.push({ start: i + 2 + h, end });
279
+ blank(i + 2 + h, end);
280
+ i = e < 0 ? n : e + close.length;
281
+ continue;
282
+ }
283
+ }
284
+ if (lang === "rs" && c === "'" && /[A-Za-z_]/.test(c2 || "") && src[i + 2] !== "'") {
285
+ i++;
286
+ continue;
287
+ }
288
+ // W5 (G6). A Kotlin raw string is `"""…"""`.
289
+ if (lang === "kt" && src.startsWith('"""', i)) {
290
+ const e = src.indexOf('"""', i + 3);
291
+ const end = e < 0 ? n : e;
292
+ strings.push({ start: i + 3, end });
293
+ blank(i + 3, end);
294
+ i = e < 0 ? n : e + 3;
295
+ continue;
296
+ }
297
+ if (lang === "py" && (src.startsWith('"""', i) || src.startsWith("'''", i))) {
298
+ const q = src.substr(i, 3);
299
+ const e = src.indexOf(q, i + 3);
300
+ const end = e < 0 ? n : e;
301
+ strings.push({ start: i + 3, end });
302
+ blank(i + 3, end);
303
+ i = e < 0 ? n : e + 3;
304
+ continue;
305
+ }
306
+ // W2 (G5): a regex literal is masked like a string, so the `/` inside
307
+ // `/\/\*/` cannot open a bogus comment and `str.match(/x/)` is one call.
308
+ // Only where a regex can start: after an operator, a bracket, a comma, a
309
+ // line start, or a keyword — never after a name, a number or `)` (division).
310
+ if ((lang === "js" || lang === "ts") && c === "/" && c2 !== "/" && c2 !== "*") {
311
+ let k = i - 1;
312
+ while (k >= 0 && (m[k] === " " || m[k] === "\t")) k--;
313
+ const prev = k >= 0 ? m[k] : "\n";
314
+ let ok = k < 0 || "(,=:[!&|?{};\n+-*%<>~^".includes(prev);
315
+ if (!ok && /[A-Za-z_$]/.test(prev)) {
316
+ let w = k;
317
+ while (w >= 0 && /[A-Za-z_$]/.test(m[w])) w--;
318
+ ok = /^(return|typeof|instanceof|in|of|delete|void|throw|case|do|else|yield|await)$/.test(m.slice(w + 1, k + 1).join(""));
319
+ }
320
+ if (ok) {
321
+ let j = i + 1;
322
+ let cls = false;
323
+ while (j < n && src[j] !== "\n") {
324
+ const d = src[j];
325
+ if (d === "\\") {
326
+ j += 2;
327
+ continue;
328
+ }
329
+ if (cls) {
330
+ if (d === "]") cls = false;
331
+ } else if (d === "[") cls = true;
332
+ else if (d === "/") break;
333
+ j++;
334
+ }
335
+ if (j < n && src[j] === "/") {
336
+ blank(i + 1, j);
337
+ i = j + 1;
338
+ continue;
339
+ }
340
+ }
341
+ }
342
+ if (c === '"' || c === "'" || (backtick && c === "`")) {
343
+ const verbatim = lang === "cs" && src[i - 1] === "@";
344
+ const raw = c === "`" && lang === "go";
345
+ // W2 (G5): a template literal's `${…}` and an f-string's `{…}` are CODE:
346
+ // `${fn()}` is a call, and `f"{base}/p"` keeps its expression. The
347
+ // string is recorded as segments around each expression, and the
348
+ // expression is masked on its own (a string inside it is still a string).
349
+ const tpl = (c === "`" && !raw) || (lang === "rb" && c !== "'");
350
+ const fstr = lang === "py" && c !== "`" && /^[fF]$|^[rRbB][fF]$|^[fF][rRbB]$/.test(((src[i - 2] || "") + (src[i - 1] || "")).replace(/^[^A-Za-z]/, ""));
351
+ let j = i + 1;
352
+ let seg = i + 1;
353
+ while (j < n) {
354
+ const d = src[j];
355
+ if (d === "\\" && !verbatim && !raw) {
356
+ j += 2;
357
+ continue;
358
+ }
359
+ if (d === c) {
360
+ if (verbatim && src[j + 1] === '"') {
361
+ j += 2;
362
+ continue;
363
+ }
364
+ break;
365
+ }
366
+ // A quote that never closes on its line is not a string (a regex
367
+ // literal, an apostrophe in a template): stop at the line end so one
368
+ // bad guess costs one line, not the rest of the file.
369
+ if (d === "\n" && c !== "`" && !verbatim) break;
370
+ // A Ruby interpolation opens with `#{`, a JS template with `${`.
371
+ if ((tpl && d === (lang === "rb" ? "#" : "$") && src[j + 1] === "{") || (fstr && d === "{" && src[j + 1] !== "{")) {
372
+ const open = tpl ? j + 1 : j;
373
+ const close = closeBrace(src, open);
374
+ if (close < 0) {
375
+ j++;
376
+ continue;
377
+ }
378
+ strings.push({ start: seg, end: j });
379
+ blank(seg, j);
380
+ const inner = mask(src.slice(open + 1, close), lang);
381
+ for (let q = 0; q < inner.masked.length; q++) m[open + 1 + q] = inner.masked[q];
382
+ for (const s of inner.strings) strings.push({ start: open + 1 + s.start, end: open + 1 + s.end });
383
+ j = close + 1;
384
+ seg = j;
385
+ continue;
386
+ }
387
+ if (fstr && d === "{" && src[j + 1] === "{") {
388
+ j += 2;
389
+ continue;
390
+ }
391
+ j++;
392
+ }
393
+ strings.push({ start: seg, end: Math.min(j, n) });
394
+ blank(seg, j);
395
+ i = j + 1;
396
+ continue;
397
+ }
398
+ i++;
399
+ }
400
+ return { masked: m.join(""), strings };
401
+ }
402
+
403
+ // The `}` that closes the brace at `open`, skipping quoted text on the way.
404
+ function closeBrace(src, open) {
405
+ let d = 0;
406
+ for (let k = open; k < src.length; k++) {
407
+ const ch = src[k];
408
+ if (ch === '"' || ch === "'" || ch === "`") {
409
+ let q = k + 1;
410
+ while (q < src.length && src[q] !== ch && src[q] !== "\n") q += src[q] === "\\" ? 2 : 1;
411
+ k = q;
412
+ continue;
413
+ }
414
+ if (ch === "{") d++;
415
+ else if (ch === "}") {
416
+ d--;
417
+ if (d === 0) return k;
418
+ }
419
+ }
420
+ return -1;
421
+ }
422
+
423
+ // COVERAGE (EW1). A heuristic extractor can lose the thread: an opening brace
424
+ // with no partner makes a declaration run to the end of the file, and a symbol
425
+ // that hits a cap keeps only part of what it found. Neither is an error — the
426
+ // record is still useful — but the reader must be told which lines are only
427
+ // partly seen, or a card that shows nothing reads as "there is nothing".
428
+ // One extraction runs at a time, so a module-level note is enough.
429
+ let UNBALANCED = -1;
430
+ function resetCoverage() {
431
+ UNBALANCED = -1;
432
+ }
433
+
434
+ function matchPair(s, open, a, b) {
435
+ let d = 0;
436
+ for (let k = open; k < s.length; k++) {
437
+ const ch = s[k];
438
+ if (ch === a) d++;
439
+ else if (ch === b) {
440
+ d--;
441
+ if (d === 0) return k;
442
+ }
443
+ }
444
+ // Only a BLOCK that really never closes is a coverage gap. Two other callers
445
+ // land here and neither is one: a parameter list scanned speculatively, and a
446
+ // probe whose start is not the opener at all (`s[open] !== a`), which can
447
+ // never balance. Noting those marked 40% of this repo's own files partial —
448
+ // a signal that fires everywhere says nothing.
449
+ if (a === "{" && s[open] === a && (UNBALANCED < 0 || open < UNBALANCED)) UNBALANCED = open;
450
+ return s.length - 1;
451
+ }
452
+
453
+ // After a parameter list: find the body. `requireArrow` = this is only a
454
+ // function if an `=>` follows (a `const x = (a + b)` is not one).
455
+ function bodyAfter(m, from, requireArrow) {
456
+ const limit = Math.min(m.length, from + 400);
457
+ for (let k = from; k < limit; k++) {
458
+ const ch = m[k];
459
+ if (ch === "=" && m[k + 1] === ">") {
460
+ let j = k + 2;
461
+ while (j < m.length && /\s/.test(m[j])) j++;
462
+ if (m[j] === "{") return { open: j, end: matchPair(m, j, "{", "}") };
463
+ const nl = m.indexOf("\n", j);
464
+ return { open: j, end: nl < 0 ? m.length - 1 : nl - 1 };
465
+ }
466
+ if (ch === "{") return requireArrow ? null : { open: k, end: matchPair(m, k, "{", "}") };
467
+ if (ch === ";") return requireArrow ? null : { open: k, end: k };
468
+ if (ch === "\n" && requireArrow) {
469
+ let j = k + 1;
470
+ while (j < limit && (m[j] === " " || m[j] === "\t" || m[j] === "\r")) j++;
471
+ if (m[j] !== "=" && m[j] !== ":") return null;
472
+ }
473
+ }
474
+ return null;
475
+ }
476
+
477
+ // ── call arguments (v1.8.2 W1) ──────────────────────────────────────────────
478
+ // The mask blanks string CONTENT but keeps the quotes, so an argument that is
479
+ // one string reads as `" "` in the mask and its text is read back from the
480
+ // original at the recorded span. Each top-level argument becomes one piece:
481
+ // s the string text, when the piece IS one string literal
482
+ // id a bare or dotted identifier (`ordersRouter`, `views.list`, `C::class`)
483
+ // req the specifier of an inline `require("…")`
484
+ // kw the keyword when the piece is `name=value` / `name: value`
485
+ // list the string items of a `[...]` value (`methods=["GET","POST"]`)
486
+ // obj `{ method: "POST", … }` — the string-valued keys of an object literal
487
+ // inc Django: the specifier inside an `include("…")` piece
488
+ // W5 (G6). Kotlin gives a function either a block body or an EXPRESSION body
489
+ // (`fun total() = items.sumOf { it.price }`), and both are real declarations.
490
+ // A member of an interface, or an `abstract fun`, has neither and is not one.
491
+ function ktBody(m, from) {
492
+ const limit = Math.min(m.length, from + 400);
493
+ for (let k = from; k < limit; k++) {
494
+ const ch = m[k];
495
+ if (ch === "{") return { open: k, end: matchPair(m, k, "{", "}") };
496
+ if (ch === "=" && m[k + 1] !== "=") {
497
+ let j = k + 1;
498
+ while (j < m.length && (m[j] === " " || m[j] === "\t" || m[j] === "\r" || m[j] === "\n")) j++;
499
+ if (m[j] === "{") return { open: j, end: matchPair(m, j, "{", "}") };
500
+ const nl = m.indexOf("\n", j);
501
+ return { open: j, end: nl < 0 ? m.length - 1 : nl - 1 };
502
+ }
503
+ if (ch === "\n") {
504
+ let j = k + 1;
505
+ while (j < limit && (m[j] === " " || m[j] === "\t" || m[j] === "\r")) j++;
506
+ // A `:` return type or a `where` clause continues the header; anything
507
+ // else on the next line means this declaration had no body.
508
+ if (m[j] !== ":" && m[j] !== "{" && m[j] !== "=" && m.slice(j, j + 5) !== "where") return null;
509
+ }
510
+ }
511
+ return null;
512
+ }
513
+
514
+ function firstStringIn(src, strAt, a, b) {
515
+ for (let k = a; k < b; k++) {
516
+ const ch = src[k];
517
+ if (ch !== '"' && ch !== "'" && ch !== "`") continue;
518
+ const sp = strAt.get(k + 1);
519
+ if (sp) return src.slice(sp.start, sp.end);
520
+ }
521
+ return null;
522
+ }
523
+
524
+ function stringsIn(src, strAt, a, b) {
525
+ const out = [];
526
+ for (let k = a; k < b; k++) {
527
+ const ch = src[k];
528
+ if (ch !== '"' && ch !== "'" && ch !== "`") continue;
529
+ const sp = strAt.get(k + 1);
530
+ if (sp) {
531
+ out.push(src.slice(sp.start, sp.end));
532
+ k = sp.end;
533
+ }
534
+ }
535
+ return out;
536
+ }
537
+
538
+ const KW_PIECE = /^\s*([A-Za-z_]\w*)\s*(?:=(?![=>])|:(?!:))\s*([\s\S]*)$/;
539
+ const IDENT_PIECE = /^\s*&?\$?((?:[A-Za-z_$][\w$]*\s*(?:\.|->|::)\s*)*[A-Za-z_$][\w$]*)\s*$/;
540
+
541
+ function argPieces(c, open, close) {
542
+ const { src, m, strAt } = c;
543
+ const pieces = [];
544
+ let depth = 0;
545
+ let a = open + 1;
546
+ for (let k = open + 1; k <= close && k < m.length; k++) {
547
+ const ch = m[k];
548
+ const end = k === close;
549
+ if (!end && (ch === "(" || ch === "[" || ch === "{")) depth++;
550
+ else if (!end && (ch === ")" || ch === "]" || ch === "}")) depth--;
551
+ if (!end && !(ch === "," && depth === 0)) continue;
552
+ const text = m.slice(a, k);
553
+ if (text.trim()) pieces.push(piece(c, a, k, text));
554
+ a = k + 1;
555
+ if (pieces.length >= 8) break;
556
+ }
557
+ return pieces;
558
+
559
+ function piece(c, start, end, text) {
560
+ const p = {};
561
+ let body = text;
562
+ let bodyStart = start;
563
+ const kw = KW_PIECE.exec(text);
564
+ if (kw && !/^\s*(new|return|await)\b/.test(kw[2])) {
565
+ p.kw = kw[1];
566
+ body = kw[2];
567
+ bodyStart = start + text.length - kw[2].length;
568
+ }
569
+ const t = body.trim();
570
+ const q = /^\s*(?:[A-Za-z]{1,2}|@)?(["'`])([\s\S]*)\1\s*$/.exec(body);
571
+ if (q && !q[2].includes(q[1])) {
572
+ // ONE string literal. A template literal / f-string with expressions
573
+ // (G5 keeps them as code, so the mask is not blank between the quotes)
574
+ // is rebuilt from its recorded segments, `{}` per expression: `${base}/p`
575
+ // is a suffix, `/orders/${id}` matches a route's parameter segment.
576
+ if (/^\s*$/.test(q[2])) p.s = firstStringIn(src, strAt, bodyStart, end);
577
+ else {
578
+ const segs = c.strings.filter((s) => s.start >= bodyStart && s.end <= end).map((s) => src.slice(s.start, s.end));
579
+ if (segs.length) p.s = segs.join("{}");
580
+ }
581
+ } else if (/^\[[\s\S]*\]$/.test(t)) p.list = stringsIn(src, strAt, bodyStart, end);
582
+ else if (/^\{[\s\S]*\}$/.test(t)) {
583
+ const obj = {};
584
+ const re = /([A-Za-z_]\w*)\s*:\s*(["'`])/g;
585
+ let x;
586
+ while ((x = re.exec(body))) {
587
+ const at = bodyStart + x.index + x[0].length;
588
+ const sp = strAt.get(at);
589
+ if (sp) obj[x[1]] = src.slice(sp.start, sp.end);
590
+ }
591
+ if (Object.keys(obj).length) p.obj = obj;
592
+ } else if (/\+\s*(["'`])[^"'`]*\1\s*$/.test(t)) {
593
+ // `BASE + "/p"` — the TAIL is known, the prefix is not.
594
+ const all = stringsIn(src, strAt, bodyStart, end);
595
+ if (all.length) p.cat = all[all.length - 1];
596
+ } else {
597
+ const req = /^\s*require\s*\(\s*(["'])[^"']*\1\s*\)\s*$/.exec(body);
598
+ if (req) p.req = firstStringIn(src, strAt, bodyStart, end);
599
+ const inc = /^\s*include\s*\(/.exec(body);
600
+ if (inc) p.inc = firstStringIn(src, strAt, bodyStart, end);
601
+ const cls = /\[\s*([A-Za-z_\\][\w\\]*)\s*::\s*class\s*,\s*(["'])[^"']*\2\s*\]/.exec(body);
602
+ if (cls) p.id = cls[1].split("\\").pop() + "." + (firstStringIn(src, strAt, bodyStart, end) || "");
603
+ const id = !p.req && !p.inc && !p.id ? IDENT_PIECE.exec(body) : null;
604
+ if (id && !KW.has(id[1]) && !/^\d/.test(id[1])) p.id = id[1].replace(/\s+/g, "").replace(/->|::/g, ".");
605
+ }
606
+ return p;
607
+ }
608
+ }
609
+
610
+ // What a call's arguments say the RIGHT-HAND assignment target is, when the
611
+ // call is `x = f(...)`. Used for router prefixes (`router = APIRouter(prefix=)`)
612
+ // here, and for instance aliases in W2.
613
+ const ASSIGN_BEFORE = /(?:^|[;{}\n(,:])\s*(?:(?:const|let|var|val|final|public|private|protected|readonly|static|export)\s+)*(?:[A-Za-z_][\w<>\[\],?]*\s+)?((?:\$?[A-Za-z_][\w$]*\s*(?:\.|->)\s*)*\$?[A-Za-z_$][\w$]*)\s*(?::\s*[\w.<>\[\]|?]+\s*)?(?::=|=)\s*(?:new\s+)?$/;
614
+ function assignBefore(m, at) {
615
+ const before = m.slice(Math.max(0, at - 120), at);
616
+ const x = ASSIGN_BEFORE.exec(before);
617
+ if (!x) return null;
618
+ let n = x[1].replace(/\s+/g, "").replace(/->/g, ".").replace(/\$/g, "");
619
+ if (/^(this|self)\./.test(n)) n = "self." + n.replace(/^(this|self)\./, "");
620
+ return KW.has(n) ? null : n;
621
+ }
622
+
623
+ // ── declarations: brace languages ───────────────────────────────────────────
624
+ function braceDefs(c, lang) {
625
+ const { src, m, starts } = c;
626
+ const defs = [];
627
+ const defParens = new Set();
628
+ // The parens of a route REGISTRATION (`router.get("/p", fn)`): the call
629
+ // stays a call, but its URL is the route's own path, never a URL it reaches.
630
+ const regParens = new Set();
631
+ const classes = [];
632
+ const add = (d) => {
633
+ if (!d.name || isKw(lang, d.name)) return;
634
+ d.s = lineOf(starts, d.from);
635
+ d.e = lineOf(starts, Math.max(d.from, d.to));
636
+ defs.push(d);
637
+ };
638
+ const innermostClass = (pos) => {
639
+ let best = null;
640
+ for (const cl of classes) if (cl.open < pos && pos < cl.to && (!best || cl.to - cl.open < best.to - best.open)) best = cl;
641
+ return best;
642
+ };
643
+ let x;
644
+
645
+ if (lang === "js" || lang === "ts") {
646
+ const CLASS = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:abstract[ \t]+)?class[ \t]+([A-Za-z_$][\w$]*)([^{;]*)\{/gm;
647
+ while ((x = CLASS.exec(m))) {
648
+ const open = x.index + x[0].length - 1;
649
+ const to = matchPair(m, open, "{", "}");
650
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: !!x[1], bases: basesOf(x[3]) };
651
+ classes.push(cl);
652
+ add(cl);
653
+ }
654
+ const FN = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:async[ \t]+)?function[ \t]*\*?[ \t]*([A-Za-z_$][\w$]*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
655
+ while ((x = FN.exec(m))) {
656
+ const paren = x.index + x[0].length - 1;
657
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
658
+ defParens.add(paren);
659
+ // `function f(a: A): B;` is an overload SIGNATURE — no body, not a symbol.
660
+ if (!body || m[body.open] === ";") continue;
661
+ add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
662
+ }
663
+ const ARROW = /^[ \t]*(export[ \t]+)?(?:const|let|var)[ \t]+([A-Za-z_$][\w$]*)[ \t]*(?::[^=\n]+)?=[ \t]*(?:async[ \t]+)?(function\b[^(\n]*\(|\(|[A-Za-z_$][\w$]*[ \t]*=>)/gm;
664
+ while ((x = ARROW.exec(m))) {
665
+ const end = x.index + x[0].length;
666
+ let body;
667
+ if (x[3].endsWith("(")) {
668
+ const paren = end - 1;
669
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, !x[3].startsWith("function"));
670
+ if (body) defParens.add(paren);
671
+ } else {
672
+ body = bodyAfter(m, m.lastIndexOf("=>", end), true);
673
+ }
674
+ if (!body) continue;
675
+ add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
676
+ }
677
+ // Route handlers (Express, Koa, Fastify, Hono): `router.get("/path", mw,
678
+ // (req, res) => { … })`. The handler is anonymous, so without this a route
679
+ // file has NO symbol and its card is an import list — W9 measured 6 files,
680
+ // 1 symbol on an Express app. Named `<METHOD> <path>`; the body is the whole
681
+ // registration call, so the middleware passed to it and every call inside
682
+ // the handler belong to the route. W1: a handler passed BY NAME
683
+ // (`app.get("/p", handler)`) is a route too, when the receiver is a router.
684
+ const ROUTE = /\b([A-Za-z_$][\w$]*)[ \t]*\.[ \t]*(get|post|put|patch|delete|del|all|options|head)[ \t]*\(/g;
685
+ const ROUTER_HEAD = /^(router|app|server|api|routes?|r|fastify|koa|hono|express|v\d+)$/i;
686
+ while ((x = ROUTE.exec(m))) {
687
+ const paren = x.index + x[0].length - 1;
688
+ const close = matchPair(m, paren, "(", ")");
689
+ const pieces = argPieces(c, paren, close);
690
+ const p0 = pieces[0];
691
+ if (!p0 || p0.s == null || p0.kw) continue;
692
+ const routePath = p0.s;
693
+ if (!/^(\/|\*$)/.test(routePath) || /[\r\n]/.test(routePath)) continue;
694
+ // `cache.get("/k")` and `request(app).get("/orders")` pass no handler.
695
+ const fnArg = /=>|\bfunction\b/.test(m.slice(p0 ? paren + 1 : paren, close));
696
+ const namedHandler = !fnArg && pieces.length >= 2 && pieces[pieces.length - 1].id && ROUTER_HEAD.test(x[1]);
697
+ if (!fnArg && !namedHandler) continue;
698
+ regParens.add(paren);
699
+ add({ kind: "route", method: VERB_OF(x[2]), path: routePath, head: x[1], name: "route", from: x.index, to: close, exported: false });
700
+ }
701
+ // Members: scanned inside each class body, skipping every member body it
702
+ // finds, so a statement inside a method is never read as a member.
703
+ // A member starts at a line start, or right after the `{` / `}` / `;`
704
+ // before it — so `class A extends B { go() { … } }` on one line is a
705
+ // class WITH a member (W2: two fixtures and one real repository had it).
706
+ const MEMBER = /(?<=^|[{};])[ \t]*((?:(?:public|private|protected|static|async|readonly|override|abstract|get|set|declare|accessor)[ \t]+)*)\*?[ \t]*(#?[A-Za-z_$][\w$]*)[ \t]*\??[ \t]*(?:<[^>\n]*>)?[ \t]*(\(|(?::[^=\n;]+)?=[ \t]*(?:async[ \t]+)?(?:\(|[A-Za-z_$][\w$]*[ \t]*=>))/gm;
707
+ for (const cl of classes) {
708
+ MEMBER.lastIndex = cl.open + 1;
709
+ while ((x = MEMBER.exec(m)) && x.index < cl.to) {
710
+ const end = x.index + x[0].length;
711
+ let body = null;
712
+ let paren = null;
713
+ if (x[3] === "(") {
714
+ paren = end - 1;
715
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
716
+ } else if (x[3].endsWith("(")) {
717
+ paren = end - 1;
718
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, true);
719
+ } else {
720
+ body = bodyAfter(m, m.lastIndexOf("=>", end), true);
721
+ }
722
+ if (paren != null) defParens.add(paren);
723
+ // A member with no body is an overload signature or an abstract
724
+ // declaration. Recording it made `NestFactoryStatic.create` three
725
+ // symbols on nestjs/nest and every `ctx` on it an ambiguity.
726
+ if (!body || body.end >= cl.to || m[body.open] === ";") {
727
+ if (body && body.end < cl.to) MEMBER.lastIndex = Math.max(body.end + 1, end);
728
+ continue;
729
+ }
730
+ const name = x[2].replace(/^#/, "");
731
+ if (!MEMBER_KW.has(name)) {
732
+ const priv = /private|protected/.test(x[1]) || x[2].startsWith("#");
733
+ add({ name, qname: `${cl.name}.${name}`, kind: "method", from: x.index, to: body.end, exported: cl.exported && !priv, member: true, owner: cl });
734
+ }
735
+ MEMBER.lastIndex = Math.max(body.end + 1, end);
736
+ }
737
+ }
738
+ } else if (lang === "go") {
739
+ const TYPE = /^type[ \t]+([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]+(?:struct|interface)[ \t]*\{/gm;
740
+ while ((x = TYPE.exec(m))) {
741
+ const open = x.index + x[0].length - 1;
742
+ add({ name: x[1], qname: x[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), exported: /^[A-Z]/.test(x[1]) });
743
+ }
744
+ const FUNC = /^func[ \t]*(?:\([ \t]*\w*[ \t]*\*?[ \t]*([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]*\)[ \t]*)?([A-Za-z_]\w*)[ \t]*(?:\[[^\]]*\])?[ \t]*\(/gm;
745
+ while ((x = FUNC.exec(m))) {
746
+ const paren = x.index + x[0].length - 1;
747
+ const close = matchPair(m, paren, "(", ")");
748
+ const nextFunc = m.indexOf("\nfunc", close);
749
+ const brace = m.indexOf("{", close);
750
+ if (brace < 0 || (nextFunc >= 0 && brace > nextFunc)) continue;
751
+ defParens.add(paren);
752
+ const recv = x[1];
753
+ add({
754
+ name: x[2],
755
+ qname: recv ? `${recv}.${x[2]}` : x[2],
756
+ kind: recv ? "method" : "function",
757
+ from: x.index,
758
+ to: matchPair(m, brace, "{", "}"),
759
+ exported: /^[A-Z]/.test(x[2]),
760
+ });
761
+ }
762
+ // W1: net/http, gin, echo, chi registrations with a handler BY NAME. A Go
763
+ // 1.22 pattern carries its method in the string (`"GET /orders"`).
764
+ const GOROUTE = /\b([A-Za-z_]\w*)[ \t]*\.[ \t]*(HandleFunc|Handle|GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|Any|Get|Post|Put|Patch|Delete|Head|Options)[ \t]*\(/g;
765
+ while ((x = GOROUTE.exec(m))) {
766
+ const paren = x.index + x[0].length - 1;
767
+ const close = matchPair(m, paren, "(", ")");
768
+ const pieces = argPieces(c, paren, close);
769
+ if (pieces.length < 2 || pieces[0].s == null) continue;
770
+ let method = /^(HandleFunc|Handle|Any)$/.test(x[2]) ? "ANY" : x[2].toUpperCase();
771
+ let routePath = pieces[0].s;
772
+ const pat = /^([A-Z]+)\s+(\/\S*)$/.exec(routePath);
773
+ if (pat) {
774
+ method = pat[1];
775
+ routePath = pat[2];
776
+ }
777
+ if (!routePath.startsWith("/")) continue;
778
+ regParens.add(paren);
779
+ add({ kind: "route", method, path: routePath, head: x[1], name: "route", from: x.index, to: close, exported: false });
780
+ }
781
+ } else if (lang === "java" || lang === "cs") {
782
+ const CLASS = /^[ \t]*((?:(?:public|private|protected|internal|static|abstract|final|sealed|partial|readonly|unsafe)[ \t]+)*)(?:class|interface|enum|record|struct)[ \t]+([A-Za-z_]\w*)([^{;]*)\{/gm;
783
+ while ((x = CLASS.exec(m))) {
784
+ const open = x.index + x[0].length - 1;
785
+ const to = matchPair(m, open, "{", "}");
786
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: /public/.test(x[1]), bases: basesOf(x[3]) };
787
+ classes.push(cl);
788
+ add(cl);
789
+ }
790
+ const METHOD = /^[ \t]*((?:(?:public|private|protected|internal|static|final|abstract|synchronized|async|virtual|override|sealed|extern|unsafe|new|default|native|strictfp)[ \t]+)*)(?:<[^>\n]+>[ \t]+)?([\w<>\[\],.?]+(?:[ \t]*<[^>\n]*>)?)[ \t]+([A-Za-z_]\w*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
791
+ while ((x = METHOD.exec(m))) {
792
+ const typeWord = x[2].split(/[<.\[]/)[0];
793
+ if (KW.has(typeWord) || isKw(lang, x[3])) continue;
794
+ const paren = x.index + x[0].length - 1;
795
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
796
+ if (!body || (m[body.open] === ";" && !/abstract|extern|native/.test(x[1]))) continue;
797
+ defParens.add(paren);
798
+ const owner = innermostClass(x.index);
799
+ add({
800
+ name: x[3],
801
+ qname: owner ? `${owner.name}.${x[3]}` : x[3],
802
+ kind: owner ? "method" : "function",
803
+ from: x.index,
804
+ to: body.end,
805
+ exported: /public/.test(x[1]),
806
+ owner,
807
+ });
808
+ }
809
+ const CTOR = /^[ \t]*(?:(?:public|private|protected|internal)[ \t]+)([A-Z]\w*)[ \t]*\(/gm;
810
+ while ((x = CTOR.exec(m))) {
811
+ const owner = innermostClass(x.index);
812
+ if (!owner || owner.name !== x[1]) continue;
813
+ const paren = x.index + x[0].length - 1;
814
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
815
+ if (!body) continue;
816
+ defParens.add(paren);
817
+ add({ name: x[1], qname: `${owner.name}.${x[1]}`, kind: "method", from: x.index, to: body.end, exported: /public/.test(x[0]), owner });
818
+ }
819
+ } else if (lang === "php") {
820
+ const CLASS = /^[ \t]*(?:(?:abstract|final|readonly)[ \t]+)*(?:class|interface|trait|enum)[ \t]+([A-Za-z_]\w*)([^{;]*)\{/gm;
821
+ while ((x = CLASS.exec(m))) {
822
+ const open = x.index + x[0].length - 1;
823
+ const to = matchPair(m, open, "{", "}");
824
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to, open, exported: true, bases: basesOf(x[2]) };
825
+ // `use Trait;` inside the body is a base too.
826
+ for (const u of m.slice(open, to).matchAll(/^[ \t]*use[ \t]+([A-Za-z_\\][\w\\, \t]*);/gm))
827
+ for (const t of u[1].split(",")) if (t.trim()) cl.bases.push(t.trim().split("\\").pop());
828
+ classes.push(cl);
829
+ add(cl);
830
+ }
831
+ const FUNC = /^[ \t]*((?:(?:public|private|protected|static|abstract|final)[ \t]+)*)function[ \t]+&?([A-Za-z_]\w*)[ \t]*\(/gm;
832
+ while ((x = FUNC.exec(m))) {
833
+ const paren = x.index + x[0].length - 1;
834
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
835
+ if (!body) continue;
836
+ defParens.add(paren);
837
+ const owner = innermostClass(x.index);
838
+ add({
839
+ name: x[2],
840
+ qname: owner ? `${owner.name}.${x[2]}` : x[2],
841
+ kind: owner ? "method" : "function",
842
+ from: x.index,
843
+ to: body.end,
844
+ exported: !/private|protected/.test(x[1]),
845
+ owner,
846
+ });
847
+ }
848
+ // Laravel: `Route::get('/p', [OrdersController::class, 'index'])`.
849
+ const LARAVEL = /\bRoute[ \t]*::[ \t]*(get|post|put|patch|delete|any|options)[ \t]*\(/g;
850
+ while ((x = LARAVEL.exec(m))) {
851
+ const paren = x.index + x[0].length - 1;
852
+ const close = matchPair(m, paren, "(", ")");
853
+ const pieces = argPieces(c, paren, close);
854
+ if (!pieces[0] || pieces[0].s == null) continue;
855
+ regParens.add(paren);
856
+ add({ kind: "route", method: x[1] === "any" ? "ANY" : VERB_OF(x[1]), path: pieces[0].s, head: "Route", name: "route", from: x.index, to: close, exported: false });
857
+ }
858
+ } else if (lang === "rs") {
859
+ // W5 (G6). A Rust type is a `struct`, `enum`, `trait` or `union`; its
860
+ // methods live in an `impl` block somewhere else in the file, so the impl
861
+ // block — not the type — is what owns a method.
862
+ const TYPE = /^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?(?:struct|enum|trait|union)[ \t]+([A-Za-z_]\w*)/gm;
863
+ while ((x = TYPE.exec(m))) {
864
+ const brace = m.indexOf("{", x.index + x[0].length);
865
+ const semi = m.indexOf(";", x.index + x[0].length);
866
+ const unit = semi >= 0 && (brace < 0 || semi < brace);
867
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to: unit ? semi : brace < 0 ? x.index : matchPair(m, brace, "{", "}"), exported: /pub/.test(x[0]), bases: [] };
868
+ add(cl);
869
+ // A `trait` body owns its DEFAULT methods, which is what makes
870
+ // `self.ok(…)` on an implementing struct an inherited call.
871
+ if (!unit && brace >= 0) classes.push({ ...cl, open: brace });
872
+ }
873
+ // `impl Trait for Type {` and `impl<T> Type<T> {` — the owner is the TYPE,
874
+ // which is the last word of the tail.
875
+ const IMPL = /^[ \t]*(?:unsafe[ \t]+)?impl(?:[ \t]*<[^>\n]*>)?[ \t]+([^{\n]+?)[ \t]*\{/gm;
876
+ while ((x = IMPL.exec(m))) {
877
+ const tail = x[1].split(/\bfor\b/).pop();
878
+ const mm = /([A-Za-z_]\w*)/.exec(tail.replace(/<[^>]*>/g, "").replace(/'\w+/g, ""));
879
+ if (!mm) continue;
880
+ const open = x.index + x[0].length - 1;
881
+ classes.push({ name: mm[1], qname: mm[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: false, bases: [] });
882
+ // `impl Trait for Type` is Rust's inheritance: the type gains the
883
+ // trait's default methods, so the trait is a BASE of the type.
884
+ const tr = /\bfor\b/.test(x[1]) ? /([A-Za-z_]\w*)/.exec(x[1].split(/\bfor\b/)[0].replace(/<[^>]*>/g, "").replace(/'\w+/g, "")) : null;
885
+ if (tr) for (const d of defs) if (d.kind === "class" && d.name === mm[1] && d.bases) d.bases.push(tr[1]);
886
+ }
887
+ const FN = /(?:^|[{};])[ \t]*(?:#\[[^\]]*\][ \t\r\n]*)*((?:(?:pub(?:\([^)]*\))?|async|unsafe|const|extern[ \t]+"[^"\n]*")[ \t]+)*)fn[ \t]+([A-Za-z_]\w*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
888
+ while ((x = FN.exec(m))) {
889
+ const paren = x.index + x[0].length - 1;
890
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
891
+ defParens.add(paren);
892
+ // A trait method with no body is a signature, not a symbol.
893
+ if (!body || m[body.open] === ";") continue;
894
+ const owner = innermostClass(x.index);
895
+ add({
896
+ name: x[2],
897
+ qname: owner ? `${owner.name}.${x[2]}` : x[2],
898
+ kind: owner ? "method" : "function",
899
+ from: x.index,
900
+ to: body.end,
901
+ exported: /\bpub\b/.test(x[1]),
902
+ owner,
903
+ });
904
+ }
905
+ // axum / actix builder registration: `.route("/orders", get(list))`.
906
+ const RSROUTE = /\.[ \t]*route[ \t]*\(/g;
907
+ while ((x = RSROUTE.exec(m))) {
908
+ const paren = x.index + x[0].length - 1;
909
+ const close = matchPair(m, paren, "(", ")");
910
+ const pieces = argPieces(c, paren, close);
911
+ if (!pieces[0] || pieces[0].s == null || !pieces[0].s.startsWith("/")) continue;
912
+ const verbs = [...m.slice(paren, close).matchAll(/\b(get|post|put|patch|delete|head|options)[ \t]*\(/g)].map((v) => v[1]);
913
+ if (!verbs.length) continue;
914
+ regParens.add(paren);
915
+ for (const v of verbs) add({ kind: "route", method: VERB_OF(v), path: pieces[0].s, head: "route", name: "route", from: x.index, to: close, exported: false });
916
+ }
917
+ } else if (lang === "kt") {
918
+ const CLASS = /^[ \t]*((?:(?:public|private|protected|internal|open|abstract|sealed|data|enum|inner|annotation|value)[ \t]+)*)(?:class|object|interface)[ \t]+([A-Za-z_]\w*)([^{\n]*)\{/gm;
919
+ while ((x = CLASS.exec(m))) {
920
+ const open = x.index + x[0].length - 1;
921
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: !/private|internal/.test(x[1]), bases: basesOf(x[3].replace(/\([^)]*\)/g, "")) };
922
+ classes.push(cl);
923
+ add(cl);
924
+ }
925
+ // `fun list(): List<Order>` · `fun Order.label()` (an extension) · an
926
+ // expression body (`fun n() = x`) is as real a declaration as a block one.
927
+ const FUN = /^[ \t]*((?:(?:public|private|protected|internal|open|override|suspend|inline|operator|abstract|external|tailrec|infix)[ \t]+)*)fun[ \t]*(?:<[^>\n]*>[ \t]*)?(?:([A-Za-z_]\w*)[ \t]*\.[ \t]*)?([A-Za-z_]\w*|`[^`\n]+`)[ \t]*\(/gm;
928
+ while ((x = FUN.exec(m))) {
929
+ const paren = x.index + x[0].length - 1;
930
+ const body = ktBody(m, matchPair(m, paren, "(", ")") + 1);
931
+ defParens.add(paren);
932
+ if (!body) continue;
933
+ const name = x[3].replace(/^`|`$/g, "");
934
+ const owner = x[2] ? { name: x[2] } : innermostClass(x.index);
935
+ add({
936
+ name,
937
+ qname: owner ? `${owner.name}.${name}` : name,
938
+ kind: owner ? "method" : "function",
939
+ from: x.index,
940
+ to: body.end,
941
+ exported: !/private|internal/.test(x[1]),
942
+ owner: x[2] ? null : owner,
943
+ });
944
+ }
945
+ // Ktor: `get("/orders") { … }`, nested inside `route("/api") { … }`.
946
+ const GROUPS = [];
947
+ const KROUTE = /(?:^|[^\w.])route[ \t]*\(/g;
948
+ while ((x = KROUTE.exec(m))) {
949
+ const paren = x.index + x[0].length - 1;
950
+ const close = matchPair(m, paren, "(", ")");
951
+ const pieces = argPieces(c, paren, close);
952
+ const brace = m.indexOf("{", close);
953
+ if (!pieces[0] || pieces[0].s == null || brace < 0 || brace > close + 4) continue;
954
+ GROUPS.push({ path: pieces[0].s, open: brace, to: matchPair(m, brace, "{", "}") });
955
+ regParens.add(paren);
956
+ }
957
+ const KVERB = /(?:^|[^\w.])(get|post|put|patch|delete|head|options)[ \t]*\(/g;
958
+ while ((x = KVERB.exec(m))) {
959
+ const paren = x.index + x[0].length - 1;
960
+ const close = matchPair(m, paren, "(", ")");
961
+ const pieces = argPieces(c, paren, close);
962
+ const brace = m.indexOf("{", close);
963
+ if (!pieces[0] || pieces[0].s == null || !pieces[0].s.startsWith("/") || brace < 0 || brace > close + 4) continue;
964
+ let pre = "";
965
+ for (const g of GROUPS) if (g.open < x.index && x.index < g.to) pre = joinPath(pre, g.path);
966
+ regParens.add(paren);
967
+ add({ kind: "route", method: VERB_OF(x[1]), path: joinPath(pre, pieces[0].s), head: "route", name: "route", from: x.index, to: matchPair(m, brace, "{", "}"), exported: false });
968
+ }
969
+ } else if (lang === "c") {
970
+ const CLASS = /^[ \t]*(?:template[ \t]*<[^>\n]*>[ \t\r\n]*)?(?:class|struct)[ \t]+([A-Za-z_]\w*)([^{;\n]*)\{/gm;
971
+ while ((x = CLASS.exec(m))) {
972
+ const open = x.index + x[0].length - 1;
973
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: true, bases: basesOf(x[2].replace(/\b(public|private|protected|virtual)\b/g, "")) };
974
+ classes.push(cl);
975
+ add(cl);
976
+ }
977
+ // A MACRO at column 0 with a block body is a definition the preprocessor
978
+ // writes: `TEST_F(NonMutatingTest, Equal) { … }`. Without this rung every
979
+ // such body belongs to the module symbol, which then passes the call cap
980
+ // and reports the whole file `partial` — measured on abseil-cpp, that was
981
+ // most of the gap, and every one of those files was a test file.
982
+ const MACRODEF = /^([A-Z][A-Z0-9_]{2,})[ \t]*\(/gm;
983
+ while ((x = MACRODEF.exec(m))) {
984
+ const paren = x.index + x[0].length - 1;
985
+ const close = matchPair(m, paren, "(", ")");
986
+ if (close < 0) continue;
987
+ const body = bodyAfter(m, close + 1, false);
988
+ if (!body || m[body.open] !== "{") continue;
989
+ const ids = argPieces(c, paren, close)
990
+ .filter((a) => !a.kw && a.id)
991
+ .map((a) => a.id)
992
+ .filter((sid) => /^[A-Za-z_]\w*$/.test(sid));
993
+ const name = ids.length >= 2 ? ids[1] : ids.length === 1 ? ids[0] : x[1];
994
+ defParens.add(paren);
995
+ add({ name, qname: ids.length >= 2 ? `${ids[0]}.${name}` : name, kind: ids.length >= 2 ? "method" : "function", from: x.index, to: body.end, exported: true });
996
+ }
997
+ // A definition is `<type> name(args) {`, or `Type::name(args) {` for a
998
+ // member defined outside its class. A DECLARATION ends in `;` and is not
999
+ // a symbol — the header file would otherwise duplicate every function.
1000
+ const FUNC = /(?:^|[{};])[ \t]*((?:(?:static|inline|extern|virtual|explicit|constexpr|friend|const|unsigned|signed)[ \t]+)*)([A-Za-z_][\w:<>,*&\[\] \t]*?[ \t*&])?(?:([A-Za-z_]\w*)[ \t]*::[ \t]*)?(~?[A-Za-z_]\w*)[ \t]*\(/gm;
1001
+ while ((x = FUNC.exec(m))) {
1002
+ const name = x[4];
1003
+ if (isKw(lang, name) || MEMBER_KW.has(name)) continue;
1004
+ if (!x[2] && !x[3] && !/^~/.test(name)) continue; // a plain call, not a definition
1005
+ // `return ok(body);` looks like `<type> <name>(` — the "type" is a
1006
+ // keyword, so it is a CALL, and it must stay one. The paren is claimed
1007
+ // for a declaration only once the body proves it is a definition.
1008
+ const typeWord = (x[2] || "").trim().split(/[<:*&[\s]/)[0];
1009
+ if (typeWord && (KW.has(typeWord) || isKw(lang, typeWord))) continue;
1010
+ const paren = x.index + x[0].length - 1;
1011
+ const close = matchPair(m, paren, "(", ")");
1012
+ if (close < 0) continue;
1013
+ const body = bodyAfter(m, close + 1, false);
1014
+ if (!body || m[body.open] === ";") continue;
1015
+ defParens.add(paren);
1016
+ const owner = x[3] ? { name: x[3] } : innermostClass(x.index);
1017
+ add({
1018
+ name,
1019
+ qname: owner ? `${owner.name}.${name}` : name,
1020
+ kind: owner ? "method" : "function",
1021
+ from: x.index,
1022
+ to: body.end,
1023
+ exported: !/\bstatic\b/.test(x[1]),
1024
+ owner: x[3] ? null : owner,
1025
+ });
1026
+ }
1027
+ }
1028
+
1029
+ let exportsSet = null;
1030
+ if (lang === "js" || lang === "ts") {
1031
+ exportsSet = new Set();
1032
+ for (const e of m.matchAll(/^[ \t]*export[ \t]*\{([^}]*)\}/gm))
1033
+ for (const p of e[1].split(",")) {
1034
+ const mm = /^\s*([\w$]+)/.exec(p);
1035
+ if (mm) exportsSet.add(mm[1]);
1036
+ }
1037
+ for (const e of m.matchAll(/export[ \t]+default[ \t]+([A-Za-z_$][\w$]*)/g)) exportsSet.add(e[1]);
1038
+ for (const e of m.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=/g)) exportsSet.add(e[1]);
1039
+ const me = /module\.exports[ \t]*=[ \t]*(\{[^}]*\}|[A-Za-z_$][\w$]*)/.exec(m);
1040
+ if (me) {
1041
+ if (me[1][0] === "{") {
1042
+ for (const p of me[1].slice(1, -1).split(",")) {
1043
+ const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?/.exec(p);
1044
+ if (mm) exportsSet.add(mm[2] || mm[1]);
1045
+ }
1046
+ } else exportsSet.add(me[1]);
1047
+ }
1048
+ }
1049
+ return { defs, defParens, regParens, exportsSet };
1050
+ }
1051
+
1052
+ // `class X extends A implements B, C` / `class X : A, IB` → ["A", "B", "C"].
1053
+ // Recorded for W2 (inherited members); harmless before it.
1054
+ function basesOf(tail) {
1055
+ const out = [];
1056
+ if (!tail) return out;
1057
+ const t = tail.replace(/<[^>]*>/g, "");
1058
+ for (const mm of t.matchAll(/(?:\bextends\b|\bimplements\b|:)\s*((?:[A-Za-z_$][\w$.\\]*\s*,\s*)*[A-Za-z_$][\w$.\\]*)/g))
1059
+ for (const b of mm[1].split(","))
1060
+ if (/^[A-Za-z_$][\w$.\\]*$/.test(b.trim()) && !KW.has(b.trim())) out.push(b.trim().split(/[.\\]/).pop());
1061
+ return [...new Set(out)].slice(0, 8);
1062
+ }
1063
+
1064
+ // ── declarations: Python (indentation) ──────────────────────────────────────
1065
+ function pyDefs(c) {
1066
+ const { src, m, starts, strAt } = c;
1067
+ const lines = m.split("\n");
1068
+ const defs = [];
1069
+ const defParens = new Set();
1070
+ const regParens = new Set();
1071
+ const stack = [];
1072
+ const width = (s) => s.replace(/\t/g, " ").length;
1073
+ for (let i = 0; i < lines.length; i++) {
1074
+ const L = lines[i];
1075
+ const d = /^([ \t]*)(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)[ \t]*\(/.exec(L);
1076
+ const cl = d ? null : /^([ \t]*)class[ \t]+([A-Za-z_]\w*)[ \t]*(?:\(([^)]*)\))?/.exec(L);
1077
+ const hit = d || cl;
1078
+ if (!hit) continue;
1079
+ const indent = width(hit[1]);
1080
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
1081
+ let header = i;
1082
+ if (d) {
1083
+ const paren = starts[i] + hit[0].length - 1;
1084
+ defParens.add(paren);
1085
+ header = lineOf(starts, matchPair(m, paren, "(", ")")) - 1;
1086
+ } else if (cl[3] !== undefined) {
1087
+ // `class X(Base):` is a declaration, not a call to X.
1088
+ defParens.add(starts[i] + L.indexOf("("));
1089
+ }
1090
+ let end = header;
1091
+ for (let k = header + 1; k < lines.length; k++) {
1092
+ const t = lines[k];
1093
+ if (!t.trim()) continue;
1094
+ if (width(/^[ \t]*/.exec(t)[0]) <= indent) break;
1095
+ end = k;
1096
+ }
1097
+ const chain = [];
1098
+ for (let s = stack.length - 1; s >= 0 && stack[s].kind === "class"; s--) chain.unshift(stack[s].name);
1099
+ const name = hit[2];
1100
+ // Decorators are the lines directly above, at the same indent, starting `@`.
1101
+ const decos = [];
1102
+ for (let k = i - 1; k >= 0; k--) {
1103
+ const dm = /^([ \t]*)@([A-Za-z_][\w.]*)[ \t]*(\()?/.exec(lines[k]);
1104
+ if (!dm || width(dm[1]) !== indent) break;
1105
+ const deco = { name: dm[2] };
1106
+ if (dm[3]) {
1107
+ const open = starts[k] + dm[0].length - 1;
1108
+ deco.args = argPieces(c, open, matchPair(m, open, "(", ")"));
1109
+ // `@router.get("/p")` is a route, never a URL the module reaches.
1110
+ regParens.add(open);
1111
+ }
1112
+ decos.unshift(deco);
1113
+ }
1114
+ defs.push({
1115
+ name,
1116
+ qname: [...chain, name].join("."),
1117
+ kind: d ? (chain.length ? "method" : "function") : "class",
1118
+ s: i + 1,
1119
+ e: end + 1,
1120
+ from: starts[i],
1121
+ exported: !name.startsWith("_"),
1122
+ ...(decos.length ? { decos } : {}),
1123
+ ...(cl && cl[3] ? { bases: cl[3].split(",").map((b) => b.trim().split(".").pop()).filter((b) => /^[A-Za-z_]\w*$/.test(b)) } : {}),
1124
+ });
1125
+ stack.push({ indent, name, kind: d ? "function" : "class" });
1126
+ }
1127
+ // Django `urlpatterns`: `path("p/", views.x)` / `re_path` / `url(...)` — a
1128
+ // route with its handler by name, or a mount when the target is `include()`.
1129
+ let x;
1130
+ const DJ = /\b(path|re_path|url)[ \t]*\(/g;
1131
+ while ((x = DJ.exec(m))) {
1132
+ const paren = x.index + x[0].length - 1;
1133
+ if (defParens.has(paren)) continue;
1134
+ const close = matchPair(m, paren, "(", ")");
1135
+ const pieces = argPieces(c, paren, close);
1136
+ if (!pieces[0] || pieces[0].s == null || !pieces[1] || pieces[1].inc) continue;
1137
+ if (!pieces[1].id) continue;
1138
+ const p = pieces[0].s.replace(/^\^/, "").replace(/\$$/, "");
1139
+ regParens.add(paren);
1140
+ defs.push({ kind: "route", method: "ANY", path: p.startsWith("/") ? p : "/" + p, head: x[1], name: "route", from: x.index, to: close, s: lineOf(starts, x.index), e: lineOf(starts, close), exported: false });
1141
+ }
1142
+ void src;
1143
+ void strAt;
1144
+ return { defs, defParens, regParens };
1145
+ }
1146
+
1147
+ // ── declarations: Ruby (keyword depth) ──────────────────────────────────────
1148
+ // W5 (G6). Ruby has no braces around a body: every `class`, `module`, `def`,
1149
+ // `if`, `while`, `case`, `begin` and every trailing `do` is closed by one
1150
+ // `end`. So the scanner counts keyword depth, one line at a time, and a
1151
+ // declaration's body runs from its own line to the `end` that pops it.
1152
+ //
1153
+ // The MODIFIER form (`save if valid?`) must never open a block, so `if`,
1154
+ // `unless`, `while` and `until` count only as the FIRST word of a statement —
1155
+ // which is the one place a real block can start.
1156
+ const RB_OPEN_FIRST = /^(class|module|def|if|unless|while|until|for|begin|case)\b/;
1157
+ const RB_DO_TAIL = /(?:^|[)\s])do(?:[ \t]*\|[^|\n]*\|)?[ \t]*$/;
1158
+ const RB_ROUTE = /^(get|post|put|patch|delete|head|options|match)[ \t]+(['"])([^'"\n]+)\2/;
1159
+
1160
+ function rbDefs(c) {
1161
+ const { m, src, starts } = c;
1162
+ const lines = m.split("\n");
1163
+ // The mask blanks string CONTENT, so a route path is read back from the
1164
+ // original bytes — the same rule `argPieces` follows everywhere else.
1165
+ const rawLines = src.split("\n");
1166
+ const defs = [];
1167
+ const defParens = new Set();
1168
+ const regParens = new Set();
1169
+ const stack = [];
1170
+ const classOf = () => {
1171
+ for (let k = stack.length - 1; k >= 0; k--) if (stack[k].kind === "class" || stack[k].kind === "module") return stack[k];
1172
+ return null;
1173
+ };
1174
+ for (let i = 0; i < lines.length; i++) {
1175
+ const t = lines[i].trim();
1176
+ if (!t) continue;
1177
+ const ln = i + 1;
1178
+ // `private` with no argument hides every method below it in this body.
1179
+ if (/^(private|protected)[ \t]*$/.test(t)) {
1180
+ const owner = classOf();
1181
+ if (owner) owner.private = true;
1182
+ continue;
1183
+ }
1184
+ let decl = null;
1185
+ let mm;
1186
+ if ((mm = /^class[ \t]+([A-Z]\w*(?:::\w+)*)(?:[ \t]*<[ \t]*([\w:]+))?/.exec(t))) {
1187
+ decl = { kind: "class", name: mm[1].split("::").pop(), bases: mm[2] ? [mm[2].split("::").pop()] : [] };
1188
+ } else if ((mm = /^module[ \t]+([A-Z]\w*(?:::\w+)*)/.exec(t))) {
1189
+ decl = { kind: "module", name: mm[1].split("::").pop(), bases: [] };
1190
+ } else if ((mm = /^def[ \t]+(self\.)?([A-Za-z_]\w*[?!=]?)/.exec(t))) {
1191
+ decl = { kind: "def", name: mm[2] };
1192
+ const p = lines[i].indexOf("(", lines[i].indexOf(mm[0]));
1193
+ if (p >= 0) defParens.add(starts[i] + p);
1194
+ } else if (RB_ROUTE.test(t) && (mm = RB_ROUTE.exec(rawLines[i].trim()))) {
1195
+ // Rails `get "/orders/:id", to: "orders#show"` in `config/routes.rb`.
1196
+ // The path is what a request URL reaches; the `to:` string names a
1197
+ // controller action in ANOTHER file, so it is not a local edge.
1198
+ const p = mm[3].startsWith("/") ? mm[3] : "/" + mm[3];
1199
+ const open = lines[i].indexOf("(");
1200
+ if (open >= 0 && open < lines[i].indexOf(mm[3])) regParens.add(starts[i] + open);
1201
+ defs.push({
1202
+ kind: "route",
1203
+ method: mm[1] === "match" ? "ANY" : VERB_OF(mm[1]),
1204
+ path: p,
1205
+ head: "routes",
1206
+ name: "route",
1207
+ from: starts[i],
1208
+ to: starts[i] + lines[i].length,
1209
+ s: ln,
1210
+ e: ln,
1211
+ exported: false,
1212
+ });
1213
+ }
1214
+ const firstKw = RB_OPEN_FIRST.exec(t);
1215
+ let opens = firstKw ? 1 : 0;
1216
+ // `while x do` and `for a in b do` are ONE block, not two: the trailing
1217
+ // `do` belongs to the loop keyword that already opened it.
1218
+ if (RB_DO_TAIL.test(t) && !(firstKw && /^(while|until|for)$/.test(firstKw[1]))) opens++;
1219
+ const ends = (t.match(/\bend\b/g) || []).length;
1220
+ for (let k = 0; k < opens; k++) {
1221
+ if (k === 0 && decl) stack.push({ ...decl, line: ln, private: false });
1222
+ else stack.push({ kind: "_" });
1223
+ }
1224
+ for (let k = 0; k < ends; k++) {
1225
+ const fr = stack.pop();
1226
+ if (!fr || fr.kind === "_") continue;
1227
+ const owner = classOf();
1228
+ const name = fr.name;
1229
+ if (fr.kind === "def") {
1230
+ defs.push({
1231
+ name,
1232
+ qname: owner ? `${owner.name}.${name}` : name,
1233
+ kind: owner ? "method" : "function",
1234
+ s: fr.line,
1235
+ e: ln,
1236
+ from: starts[fr.line - 1] || 0,
1237
+ exported: !(owner && owner.private) && !name.startsWith("_"),
1238
+ });
1239
+ } else {
1240
+ defs.push({
1241
+ name,
1242
+ qname: name,
1243
+ kind: "class",
1244
+ s: fr.line,
1245
+ e: ln,
1246
+ from: starts[fr.line - 1] || 0,
1247
+ exported: true,
1248
+ ...(fr.bases && fr.bases.length ? { bases: fr.bases } : {}),
1249
+ });
1250
+ }
1251
+ }
1252
+ }
1253
+ defs.sort((a, b) => a.from - b.from);
1254
+ return { defs, defParens, regParens };
1255
+ }
1256
+
1257
+ // ── decorators (brace languages) ────────────────────────────────────────────
1258
+ // `@Get(":id")` / `@GetMapping("/p")` / `[HttpGet("{id}")]` / `#[Route('/p')]`
1259
+ // belong to the NEXT declaration that starts after them. A class-level
1260
+ // `@Controller("orders")` / `@RequestMapping("/orders")` / `[Route("api/x")]`
1261
+ // is the prefix every route in that class carries.
1262
+ function braceDecorators(c, lang, defs) {
1263
+ const { m } = c;
1264
+ const marks = [];
1265
+ let x;
1266
+ if (lang === "js" || lang === "ts" || lang === "java" || lang === "kt") {
1267
+ const RE = /(?:^|[\s;}])@([A-Za-z_$][\w$.]*)[ \t]*(\()?/g;
1268
+ while ((x = RE.exec(m))) marks.push({ pos: x.index + x[0].indexOf("@"), name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1269
+ } else if (lang === "rs") {
1270
+ // actix and rocket put the route on the function: `#[get("/orders")]`.
1271
+ const RE = /#\[[ \t]*(get|post|put|patch|delete|head|options|route)[ \t]*(\()?/g;
1272
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1273
+ } else if (lang === "cs") {
1274
+ const RE = /^[ \t]*\[[ \t]*(HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete|HttpHead|HttpOptions|Route|ApiController)[ \t]*(\()?/gm;
1275
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1276
+ } else if (lang === "php") {
1277
+ const RE = /#\[[ \t]*(Route|Get|Post|Put|Patch|Delete)[ \t]*(\()?/g;
1278
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1279
+ }
1280
+ if (!marks.length) return;
1281
+ const targets = defs.filter((d) => d.kind !== "route").sort((a, b) => a.from - b.from);
1282
+ for (const mk of marks) {
1283
+ const deco = { name: mk.name };
1284
+ if (mk.paren != null) deco.args = argPieces(c, mk.paren, matchPair(m, mk.paren, "(", ")"));
1285
+ // The nearest declaration that starts after the mark, and not further than
1286
+ // a screen away — a stray `@` in a JSDoc that the mask kept is not a
1287
+ // decorator of the next function.
1288
+ let best = null;
1289
+ for (const d of targets) if (d.from > mk.pos && (!best || d.from < best.from)) best = d;
1290
+ if (!best || best.from - mk.pos > 600) continue;
1291
+ (best.decos || (best.decos = [])).push(deco);
1292
+ }
1293
+ }
1294
+
1295
+ // One decorator → `{ prefix }` (class level) or `{ routes: [{method, path}] }`.
1296
+ function decoRoute(deco, isClass) {
1297
+ const last = String(deco.name).split(".").pop();
1298
+ const args = deco.args || [];
1299
+ const pos = args.filter((a) => !a.kw);
1300
+ const kw = {};
1301
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1302
+ const str = (a) => (a && a.s != null ? a.s : null);
1303
+ const first = str(pos[0]) != null ? str(pos[0]) : str(kw.path) != null ? str(kw.path) : str(kw.value) != null ? str(kw.value) : str(kw.template) != null ? str(kw.template) : null;
1304
+ const norm = (p) => {
1305
+ let s = String(p == null ? "" : p).trim();
1306
+ if (s && !s.startsWith("/")) s = "/" + s;
1307
+ return s;
1308
+ };
1309
+ if (isClass) {
1310
+ if (/^(Controller|RequestMapping|Route|Blueprint)$/.test(last)) return { prefix: norm(first != null ? first : pos[0] && pos[0].obj && pos[0].obj.path) };
1311
+ return null;
1312
+ }
1313
+ if (first == null && pos[0] && pos[0].obj && pos[0].obj.path) return isClass ? { prefix: norm(pos[0].obj.path) } : null;
1314
+ const NEST = /^(Get|Post|Put|Patch|Delete|Head|Options|All)$/;
1315
+ const PY = /^(get|post|put|patch|delete|head|options)$/;
1316
+ const SPRING = /^(Get|Post|Put|Patch|Delete)Mapping$/;
1317
+ const CS = /^Http(Get|Post|Put|Patch|Delete|Head|Options)$/;
1318
+ let methods = null;
1319
+ if (NEST.test(last)) methods = [last === "All" ? "ANY" : last.toUpperCase()];
1320
+ else if (PY.test(last)) methods = [last.toUpperCase()];
1321
+ else if (SPRING.test(last)) methods = [SPRING.exec(last)[1].toUpperCase()];
1322
+ else if (CS.test(last)) methods = [CS.exec(last)[1].toUpperCase()];
1323
+ else if (/^(route|api_route|Route|RequestMapping)$/.test(last)) {
1324
+ const list = (kw.methods && kw.methods.list) || (kw.method && kw.method.list) || null;
1325
+ if (list && list.length) methods = list.map((v) => v.toUpperCase());
1326
+ else if (kw.method && kw.method.id) methods = [kw.method.id.split(".").pop().toUpperCase()];
1327
+ else methods = ["ANY"];
1328
+ }
1329
+ if (!methods) return null;
1330
+ return { routes: methods.map((method) => ({ method, path: norm(first) })) };
1331
+ }
1332
+
1333
+ // ── calls ───────────────────────────────────────────────────────────────────
1334
+ const CALL = /(?:\bnew[ \t]+)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)[ \t]*(?:<[\w$., \t\[\]]*>)?[ \t]*\(/g;
1335
+
1336
+ function normCall(name) {
1337
+ let n = String(name).replace(/[ \t]*(?:\?\.|->|::)[ \t]*/g, ".").replace(/[ \t]+/g, "");
1338
+ let self = false;
1339
+ let sup = false;
1340
+ const m = /^(?:\$this|this|self|static|cls)\.(.+)$/.exec(n);
1341
+ if (m) {
1342
+ n = m[1];
1343
+ self = true;
1344
+ } else {
1345
+ // W2 (G3): `super.m()` / `parent::m()` / `base.m()` is a self call that
1346
+ // starts ONE level up the base chain.
1347
+ const sm = /^(?:super|parent|base)\.(.+)$/.exec(n);
1348
+ if (sm) {
1349
+ n = sm[1];
1350
+ self = true;
1351
+ sup = true;
1352
+ }
1353
+ }
1354
+ n = n.replace(/^\$/, "").replace(/\.\$/g, ".");
1355
+ const last = n.split(".").pop();
1356
+ // A keyword after a dot is a METHOD name — `router.delete(`, `promise.catch(` —
1357
+ // and the arguments inside it (a middleware passed by name) are still scanned.
1358
+ if (!last || (KW.has(last) && !n.includes(".")) || /^\d/.test(last)) return null;
1359
+ return { name: n, self, ...(sup ? { sup: true } : {}) };
1360
+ }
1361
+
1362
+ // Which calls carry arguments worth keeping: an HTTP-shaped call (its URL), a
1363
+ // mount, a router constructor (its prefix), or an assignment target. Every
1364
+ // other call keeps only its name and line, as before.
1365
+ const ARGS_WORTH = /(^|\.)(get|post|put|patch|delete|del|head|options|fetch|axios|request|use|register|include_router|register_blueprint|mount|path|re_path|url|include|NewRequest|Get|Post|Head|APIRouter|Blueprint|Router|Route|json|perform|GetAsync|PostAsync|PutAsync|DeleteAsync|PatchAsync|SendAsync|open|route|add_url_rule|Group|Mount|Handle|HandleFunc)$/;
1366
+
1367
+ function scanCalls(c, defParens, regParens) {
1368
+ const { m, starts } = c;
1369
+ const out = [];
1370
+ let x;
1371
+ CALL.lastIndex = 0;
1372
+ while ((x = CALL.exec(m))) {
1373
+ const paren = x.index + x[0].length - 1;
1374
+ if (defParens.has(paren)) continue;
1375
+ // `function (` / `def (` style anonymous forms, and the word before a
1376
+ // declaration name, are not calls.
1377
+ const before = m.slice(Math.max(0, x.index - 12), x.index);
1378
+ if (/\b(function|def|func|fn)[ \t]*\*?[ \t]*$/.test(before)) continue;
1379
+ const cc = normCall(x[1]);
1380
+ if (!cc) continue;
1381
+ const close = matchPair(m, paren, "(", ")");
1382
+ // `async (req, res) => …` is a parameter list, not a call to `async`.
1383
+ if (cc.name === "async" && /^\s*=>/.test(m.slice(close + 1, close + 8))) continue;
1384
+ // W1: `new OrderService().create(…)` / Python `OrderService().create(…)` —
1385
+ // the receiver is a constructor call, so the head is the class. Without it
1386
+ // the call is a bare `create`, and a bare name is never a class method.
1387
+ if (!cc.name.includes(".") && !cc.self) {
1388
+ const chain = m.slice(Math.max(0, x.index - 160), x.index);
1389
+ const nw = c.lang === "py" ? /\b([A-Z][\w]*)[ \t]*\([^()]*\)[ \t]*\.[ \t]*$/.exec(chain) : /\bnew[ \t]+([A-Za-z_$][\w$]*)[ \t]*\([^()]*\)[ \t]*\.[ \t]*$/.exec(chain);
1390
+ if (nw) cc.name = `${nw[1]}.${cc.name}`;
1391
+ else if (/\bsuper[ \t]*\([ \t]*\)[ \t]*\.[ \t]*$/.test(chain)) {
1392
+ // Python `super().m()` — a self call one level up (W2, G3).
1393
+ cc.self = true;
1394
+ cc.sup = true;
1395
+ }
1396
+ }
1397
+ const rec = { name: cc.name, line: lineOf(starts, paren), ...(cc.self ? { self: true } : {}), ...(cc.sup ? { sup: true } : {}), _p: paren };
1398
+ if (regParens && regParens.has(paren)) rec.reg = true;
1399
+ if (ARGS_WORTH.test(cc.name)) {
1400
+ rec.args = argPieces(c, paren, close);
1401
+ const assign = assignBefore(m, x.index);
1402
+ if (assign) rec.assign = assign;
1403
+ }
1404
+ out.push(rec);
1405
+ refsIn(m, starts, paren, close, out, c);
1406
+ }
1407
+ // W5 (G6). Ruby calls a method with NO parentheses — `@store.list`,
1408
+ // `handler.index`, `OrderStore.new` — so without this rung a Ruby file looks
1409
+ // as though it calls almost nothing. Only a RECEIVER form is taken: a bare
1410
+ // word with no parentheses is far more often a local variable, and guessing
1411
+ // there would invent edges, which is the one thing this engine must not do.
1412
+ if (c.lang === "rb") {
1413
+ const RBDOT = /@{0,2}[A-Za-z_]\w*(?:[ \t]*\.[ \t]*[A-Za-z_]\w*[?!]?)+(?![\w?!]|[ \t]*[(.])/g;
1414
+ while ((x = RBDOT.exec(m))) {
1415
+ const before = m.slice(Math.max(0, x.index - 6), x.index);
1416
+ if (/def[ ]*$/.test(before)) continue;
1417
+ const cc = normCall(x[0].replace(/^@+/, ""));
1418
+ if (!cc || !cc.name.includes(".")) continue;
1419
+ out.push({ name: cc.name, line: lineOf(starts, x.index), ...(cc.self ? { self: true } : {}), ...(cc.sup ? { sup: true } : {}), _p: x.index });
1420
+ }
1421
+ }
1422
+ return out;
1423
+ }
1424
+
1425
+ // A function passed BY NAME — `router.post("/", requireAuth, handler)`,
1426
+ // `items.map(normalize)` — is a use no call scan sees, so `requireAuth` had
1427
+ // fan-in 0 on every route that used it. Each bare or dotted identifier argument
1428
+ // becomes a REF. `finalize` keeps a ref only when its head is defined in this
1429
+ // file or bound by an import, and resolution accepts only LOCAL or IMPORT for
1430
+ // it, so a local variable passed along never becomes an edge.
1431
+ const REF_WORDS = new Set(["true", "false", "null", "undefined", "nil", "None", "True", "False", "NaN", "Infinity"]);
1432
+ const REF_ARG = /^\s*(?:\.\.\.|&)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)\s*$/;
1433
+ const LARAVEL_REF = /^\s*\[\s*([A-Za-z_\\][\w\\]*)\s*::\s*class\s*,\s*(["'])\s*\2\s*\]\s*$/;
1434
+
1435
+ function refsIn(m, starts, open, close, out, c) {
1436
+ let depth = 0;
1437
+ let a = open + 1;
1438
+ for (let k = open + 1; k <= close && k < m.length; k++) {
1439
+ const ch = m[k];
1440
+ const end = k === close;
1441
+ if (!end && (ch === "(" || ch === "[" || ch === "{")) depth++;
1442
+ else if (!end && (ch === ")" || ch === "]" || ch === "}")) depth--;
1443
+ if (!end && !(ch === "," && depth === 0)) continue;
1444
+ const piece = m.slice(a, k);
1445
+ const t = REF_ARG.exec(piece);
1446
+ if (t && !REF_WORDS.has(t[1])) {
1447
+ const cc = normCall(t[1]);
1448
+ if (cc && !cc.sup) out.push({ name: cc.name, line: lineOf(starts, a + piece.indexOf(t[1])), ref: true, ...(cc.self ? { self: true } : {}) });
1449
+ } else if (c) {
1450
+ const lr = LARAVEL_REF.exec(piece);
1451
+ if (lr) {
1452
+ const method = firstStringIn(c.src, c.strAt, a, k);
1453
+ if (method) out.push({ name: `${lr[1].split("\\").pop()}.${method}`, line: lineOf(starts, a), ref: true });
1454
+ }
1455
+ }
1456
+ a = k + 1;
1457
+ }
1458
+ }
1459
+
1460
+ // ── instance aliases (v1.8.2 W2, G2) ────────────────────────────────────────
1461
+ // `const store = new OrderStore()` makes `store.list()` a call to
1462
+ // `OrderStore.list`. nestjs/nest measured 118 `maybe` callers on ONE symbol
1463
+ // from this shape alone. Each declaration below yields { line, local, cls,
1464
+ // self }: `self` is a class field (`this.x = new S()`, a constructor
1465
+ // param-property, a typed field), scoped to the class; anything else is scoped
1466
+ // to the symbol that contains the line (a function, a method, or the module).
1467
+ // A local re-bound to something that is not a class (`cls: null`) SHADOWS an
1468
+ // outer alias for that scope.
1469
+ const UP = "[A-Z][A-Za-z0-9_$]*";
1470
+ const ID = "[A-Za-z_$][A-Za-z0-9_$]*";
1471
+
1472
+ function aliasesOf(c, lang) {
1473
+ const { m, starts } = c;
1474
+ const out = [];
1475
+ const push = (pos, local, cls, self) => {
1476
+ if (!local || (cls && !/^[A-Z]/.test(cls))) return;
1477
+ out.push({ line: lineOf(starts, pos), local: local.replace(/^[$@]+/, ""), cls: cls || null, ...(self ? { self: true } : {}) });
1478
+ };
1479
+ let x;
1480
+ if (lang === "js" || lang === "ts") {
1481
+ // const x = new S() · const x: S = … · let x: S · const x = S() (a factory)
1482
+ const DECL = new RegExp(`\\b(?:const|let|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=;\\n]*)?=\\s*(?:new\\s+(${UP})|(${UP})\\s*\\(|([^\\n;]*))`, "g");
1483
+ while ((x = DECL.exec(m))) push(x.index, x[1], x[3] || x[4] || x[2] || null, false);
1484
+ const TYPED = new RegExp(`\\b(?:let|var)\\s+(${ID})\\s*:\\s*(${UP})\\s*[;\\n]`, "g");
1485
+ while ((x = TYPED.exec(m))) push(x.index, x[1], x[2], false);
1486
+ // this.x = new S() · constructor(private readonly x: S) · x: S; · x = new S();
1487
+ const THIS = new RegExp(`\\bthis\\.(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1488
+ while ((x = THIS.exec(m))) push(x.index, x[1], x[2], true);
1489
+ const PARAM = new RegExp(`\\b(?:public|private|protected|readonly)\\s+(?:readonly\\s+)?(${ID})\\s*[?!]?\\s*:\\s*(${UP})`, "g");
1490
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], true);
1491
+ const FIELD = new RegExp(`^[ \\t]+(?:(?:public|private|protected|static|readonly|declare|override|accessor)\\s+)*(${ID})\\s*[?!]?\\s*(?::\\s*(${UP})\\s*(?:=[^\\n;]*)?|=\\s*new\\s+(${UP}))\\s*(?:\\(|;)`, "gm");
1492
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[2] || x[3], true);
1493
+ } else if (lang === "py") {
1494
+ // x = S() · x: S = … · x: S · self.x = S() · def f(x: S)
1495
+ const ASSIGN = new RegExp(`^[ \\t]*(${ID})\\s*(?::\\s*(${UP})\\s*)?=\\s*(?:(${UP})\\s*\\(|([^\\n]*))`, "gm");
1496
+ while ((x = ASSIGN.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1497
+ const SELF = new RegExp(`\\bself\\.(${ID})\\s*(?::\\s*(${UP})\\s*)?=\\s*(?:(${UP})\\s*\\(|[^\\n]*)`, "g");
1498
+ while ((x = SELF.exec(m))) if (x[3] || x[2]) push(x.index, x[1], x[3] || x[2], true);
1499
+ const ANN = new RegExp(`[(,]\\s*(${ID})\\s*:\\s*(${UP})\\b`, "g");
1500
+ while ((x = ANN.exec(m))) push(x.index, x[1], x[2], false);
1501
+ } else if (lang === "go") {
1502
+ // x := S{} · x := &S{} · var x *S · x := NewS() · func (s *S) · f(s *S)
1503
+ const LIT = new RegExp(`\\b(${ID})\\s*:=\\s*&?(${UP})\\s*\\{`, "g");
1504
+ while ((x = LIT.exec(m))) push(x.index, x[1], x[2], false);
1505
+ const VAR = new RegExp(`\\bvar\\s+(${ID})\\s+\\*?(${UP})\\b`, "g");
1506
+ while ((x = VAR.exec(m))) push(x.index, x[1], x[2], false);
1507
+ const NEW = new RegExp(`\\b(${ID})\\s*:?=\\s*(?:[a-z][\\w]*\\.)?New(${UP})\\s*\\(`, "g");
1508
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2], false);
1509
+ const RECV = new RegExp(`^func\\s*\\(\\s*(${ID})\\s+\\*?(${UP})\\s*\\)`, "gm");
1510
+ while ((x = RECV.exec(m))) push(x.index, x[1], x[2], false);
1511
+ const PARAM = new RegExp(`[(,]\\s*(${ID})\\s+\\*?(${UP})\\s*[,)]`, "g");
1512
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], false);
1513
+ } else if (lang === "java" || lang === "cs") {
1514
+ // S x = … · var x = new S() · S x; (fields, locals, parameters)
1515
+ const TYPED = new RegExp(`\\b(${UP})(?:<[^>\\n]*>)?\\s+(${ID})\\s*(?:=|;|,|\\))`, "g");
1516
+ while ((x = TYPED.exec(m))) if (!KW.has(x[1]) && !/^(String|Integer|Long|Boolean|Double|Float|Object|List|Map|Set|Task|Void)$/.test(x[1])) push(x.index, x[2], x[1], false);
1517
+ const VAR = new RegExp(`\\bvar\\s+(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1518
+ while ((x = VAR.exec(m))) push(x.index, x[1], x[2], false);
1519
+ } else if (lang === "php") {
1520
+ // $x = new S() · private S $x · $this->x = new S()
1521
+ const NEW = new RegExp(`\\$(${ID})\\s*=\\s*new\\s+\\\\?(${UP})`, "g");
1522
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2].split("\\").pop(), false);
1523
+ const PROP = new RegExp(`\\b(?:public|private|protected)\\s+(?:readonly\\s+)?\\??\\\\?(${UP})\\s+\\$(${ID})`, "g");
1524
+ while ((x = PROP.exec(m))) push(x.index, x[2], x[1], true);
1525
+ const THIS = new RegExp(`\\$this->(${ID})\\s*=\\s*new\\s+\\\\?(${UP})`, "g");
1526
+ while ((x = THIS.exec(m))) push(x.index, x[1], x[2], true);
1527
+ } else if (lang === "rb") {
1528
+ // `store = OrderStore.new` · `@store = OrderStore.new` (an instance
1529
+ // variable IS the field form here, so it is a `self` alias).
1530
+ const LOCAL = new RegExp(`^[ \\t]*(${ID})\\s*=\\s*(${UP})(?:::\\w+)*\\.new\\b`, "gm");
1531
+ while ((x = LOCAL.exec(m))) push(x.index, x[1], x[2], false);
1532
+ const IVAR = new RegExp(`@@?(${ID})\\s*=\\s*(${UP})(?:::\\w+)*\\.new\\b`, "g");
1533
+ while ((x = IVAR.exec(m))) push(x.index, x[1], x[2], true);
1534
+ } else if (lang === "rs") {
1535
+ // `let s = Service::new()` · `let s: Service = …` · `s: Service` (a struct
1536
+ // field, so a `self.s` call) · `fn f(s: &Service)`.
1537
+ const LET = new RegExp(`\\blet\\s+(?:mut\\s+)?(${ID})\\s*(?::\\s*&?(?:mut\\s+)?(${UP}))?\\s*=\\s*(?:&?(${UP})(?:::\\w+)*\\s*(?:\\{|\\()|[^\\n;]*)`, "g");
1538
+ while ((x = LET.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1539
+ const FIELD = new RegExp(`^[ \\t]+(?:pub(?:\\([^)]*\\))?\\s+)?(${ID})\\s*:\\s*&?(?:mut\\s+)?(?:Arc<|Box<|Rc<|RwLock<|Mutex<)*(${UP})`, "gm");
1540
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[2], true);
1541
+ const PARAM = new RegExp(`[(,]\\s*(${ID})\\s*:\\s*&?(?:mut\\s+)?(?:Arc<|Box<|Rc<|State<)*(${UP})`, "g");
1542
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], false);
1543
+ } else if (lang === "kt") {
1544
+ // `val s = Service()` · `val s: Service` · `class X(private val s: Service)`
1545
+ const DECL = new RegExp(`\\b(?:val|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=\\n]*)?\\s*=\\s*(${UP})\\s*\\(`, "g");
1546
+ while ((x = DECL.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1547
+ const TYPED = new RegExp(`\\b(?:val|var)\\s+(${ID})\\s*:\\s*(${UP})`, "g");
1548
+ while ((x = TYPED.exec(m))) push(x.index, x[1], x[2], false);
1549
+ const PARAM = new RegExp(`[(,]\\s*(?:(?:private|protected|internal|public|val|var)\\s+)*(${ID})\\s*:\\s*(${UP})`, "g");
1550
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], true);
1551
+ // A `val` at class level is a FIELD: it is reached from a method with no
1552
+ // receiver, so it has to be recorded as a `self` alias, not a local.
1553
+ const FIELD = new RegExp(`^[ \\t]+(?:(?:private|protected|internal|public|open|override|lateinit|const)\\s+)*(?:val|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=\\n]*)?\\s*=\\s*(${UP})\\s*\\(`, "gm");
1554
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[3] || x[2], true);
1555
+ } else if (lang === "c") {
1556
+ // `Service s;` · `Service* s = new Service();` · a member declared in the
1557
+ // class body is reached without `this->`, like Java and C#.
1558
+ const NEW = new RegExp(`\\b(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1559
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2], false);
1560
+ const TYPED = new RegExp(`\\b(${UP})(?:<[^>\\n]*>)?\\s*[*&]?\\s+(${ID})\\s*(?:=|;|,|\\))`, "g");
1561
+ while ((x = TYPED.exec(m))) if (!isKw(lang, x[1])) push(x.index, x[2], x[1], false);
1562
+ const FIELD = new RegExp(`^[ \\t]+(${UP})(?:<[^>\\n]*>)?\\s*[*&]?\\s+(${ID})\\s*;`, "gm");
1563
+ while ((x = FIELD.exec(m))) if (!isKw(lang, x[1])) push(x.index, x[2], x[1], true);
1564
+ }
1565
+ return out;
1566
+ }
1567
+
1568
+ // ── re-exports (v1.8.2 W2, G4) ──────────────────────────────────────────────
1569
+ // A barrel defines nothing and re-exports: `export * from "./x"`,
1570
+ // `export { a as b } from "./x"`, `module.exports = require("./x")`,
1571
+ // `exports.a = require("./x").a`, and every import in a Python `__init__.py`.
1572
+ // The IMPORT rung on read follows these to the defining file.
1573
+ function reexportsFor(lang, src, m, rel, imports) {
1574
+ const out = [];
1575
+ let x;
1576
+ if (lang === "js" || lang === "ts") {
1577
+ const STAR = /^[ \t]*export[ \t]+\*[ \t]+from[ \t]*['"]([^'"\n]+)['"]/gm;
1578
+ while ((x = STAR.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[1], names: "*" });
1579
+ const NAMED = /^[ \t]*export[ \t]+(?:type[ \t]+)?\{([^}]*)\}[ \t]*from[ \t]*['"]([^'"\n]+)['"]/gm;
1580
+ while ((x = NAMED.exec(src))) {
1581
+ if (!codeAt(src, m, x.index)) continue;
1582
+ const names = [];
1583
+ for (const p of x[1].split(",")) {
1584
+ const mm = /^\s*(?:type\s+)?([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(p);
1585
+ if (mm) names.push({ local: mm[2] || mm[1], orig: mm[1] });
1586
+ }
1587
+ if (names.length) out.push({ at: x.index, from: x[2], names });
1588
+ }
1589
+ const WHOLE = /module\.exports[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)[ \t]*;?[ \t]*$/gm;
1590
+ while ((x = WHOLE.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[1], names: "*" });
1591
+ const PROP = /(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)\.([A-Za-z_$][\w$]*)/g;
1592
+ while ((x = PROP.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[2], names: [{ local: x[1], orig: x[3] }] });
1593
+ // `export { a, b }` of names that were IMPORTED (not defined here) is a
1594
+ // re-export too: `import { A } from "./a"; export { A };`.
1595
+ const LOCAL = /^[ \t]*export[ \t]*\{([^}]*)\}[ \t]*;?[ \t]*$/gm;
1596
+ while ((x = LOCAL.exec(src))) {
1597
+ if (!codeAt(src, m, x.index)) continue;
1598
+ for (const p of x[1].split(",")) {
1599
+ const mm = /^\s*([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(p);
1600
+ if (!mm) continue;
1601
+ for (const imp of imports || [])
1602
+ for (const b of imp.bindings || []) if (b.local === mm[1] && b.kind === "named") out.push({ at: x.index, from: imp.from, names: [{ local: mm[2] || mm[1], orig: b.orig }] });
1603
+ }
1604
+ }
1605
+ // Source order, whatever shape found them.
1606
+ out.sort((a, b) => a.at - b.at);
1607
+ for (const r of out) delete r.at;
1608
+ } else if (lang === "py" && /(^|\/)__init__\.py$/.test(rel)) {
1609
+ for (const imp of imports || []) {
1610
+ if (!imp.from) continue;
1611
+ if (!(imp.bindings || []).length) continue;
1612
+ out.push({ from: imp.from, names: imp.bindings.map((b) => ({ local: b.local, orig: b.orig })) });
1613
+ }
1614
+ if (/^[ \t]*from[ \t]+[.\w]+[ \t]+import[ \t]+\*/m.test(src)) {
1615
+ for (const mm of src.matchAll(/^[ \t]*from[ \t]+([.\w]+)[ \t]+import[ \t]+\*/gm)) if (codeAt(src, m, mm.index)) out.push({ from: mm[1], names: "*" });
1616
+ }
1617
+ }
1618
+ return out.slice(0, 200);
1619
+ }
1620
+
1621
+ // ── imports ─────────────────────────────────────────────────────────────────
1622
+ // A statement is read from the ORIGINAL text (its path is a string), but only
1623
+ // where the mask agrees the keyword is code — never inside a comment.
1624
+ function codeAt(src, m, idx) {
1625
+ let k = idx;
1626
+ while (k < src.length && (src[k] === " " || src[k] === "\t")) k++;
1627
+ return m[k] === src[k] && m[k] !== " ";
1628
+ }
1629
+
1630
+ function jsClause(cl) {
1631
+ const b = [];
1632
+ const brace = /\{([^}]*)\}/.exec(cl);
1633
+ if (brace)
1634
+ for (const part of brace[1].split(",")) {
1635
+ const p = part.trim().replace(/^type\s+/, "");
1636
+ const mm = /^([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(p);
1637
+ if (mm) b.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
1638
+ }
1639
+ for (const r of cl.replace(/\{[^}]*\}/, "").split(",")) {
1640
+ const t = r.trim();
1641
+ if (!t) continue;
1642
+ const ns = /^\*\s+as\s+([\w$]+)$/.exec(t);
1643
+ if (ns) b.push({ local: ns[1], orig: "*", kind: "namespace" });
1644
+ else if (/^[\w$]+$/.test(t)) b.push({ local: t, orig: "default", kind: "default" });
1645
+ }
1646
+ return b;
1647
+ }
1648
+
1649
+ function importsFor(lang, src, m, starts) {
1650
+ const out = [];
1651
+ const line = (i) => lineOf(starts, i);
1652
+ let x;
1653
+ if (lang === "js" || lang === "ts") {
1654
+ const IMP = /^[ \t]*import[ \t]+(?:type[ \t]+)?([\w$*{}\s,]+?)[ \t]*from[ \t]*['"]([^'"\n]+)['"]/gm;
1655
+ while ((x = IMP.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: jsClause(x[1]) });
1656
+ const REQ = /(?:const|let|var)[ \t]+(\{[^}]*\}|[A-Za-z_$][\w$]*)[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)(?:\.([A-Za-z_$][\w$]*))?/g;
1657
+ while ((x = REQ.exec(src))) {
1658
+ if (!codeAt(src, m, x.index)) continue;
1659
+ const bindings = [];
1660
+ if (x[1][0] === "{") {
1661
+ for (const p of x[1].slice(1, -1).split(",")) {
1662
+ const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?\s*$/.exec(p);
1663
+ if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
1664
+ }
1665
+ } else if (x[3]) bindings.push({ local: x[1], orig: x[3], kind: "named" });
1666
+ else bindings.push({ local: x[1], orig: "default", kind: "namespace" });
1667
+ out.push({ from: x[2], line: line(x.index), bindings });
1668
+ }
1669
+ } else if (lang === "py") {
1670
+ const FROM = /^[ \t]*from[ \t]+([.\w]+)[ \t]+import[ \t]+(\([^)]*\)|[^\n#]+)/gm;
1671
+ while ((x = FROM.exec(src))) {
1672
+ if (!codeAt(src, m, x.index)) continue;
1673
+ const bindings = [];
1674
+ for (const p of x[2].replace(/[()]/g, "").split(",")) {
1675
+ const mm = /^\s*(\w+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
1676
+ if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
1677
+ }
1678
+ out.push({ from: x[1], line: line(x.index), bindings });
1679
+ }
1680
+ const IMP = /^[ \t]*import[ \t]+([^\n#]+)/gm;
1681
+ while ((x = IMP.exec(src))) {
1682
+ if (!codeAt(src, m, x.index)) continue;
1683
+ for (const p of x[1].split(",")) {
1684
+ const mm = /^\s*([\w.]+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
1685
+ if (!mm) continue;
1686
+ const mod = mm[2] ? mm[1] : mm[1].split(".")[0];
1687
+ out.push({ from: mod, line: line(x.index), bindings: [{ local: mm[2] || mod, orig: mod, kind: "namespace" }] });
1688
+ }
1689
+ }
1690
+ } else if (lang === "go") {
1691
+ const addGo = (alias, p, at) => {
1692
+ const local = alias || p.split("/").pop();
1693
+ out.push({ from: p, line: line(at), bindings: alias === "_" || alias === "." ? [] : [{ local, orig: local, kind: "namespace" }] });
1694
+ };
1695
+ const ONE = /^import[ \t]+(?:([\w.]+)[ \t]+)?"([^"]+)"/gm;
1696
+ while ((x = ONE.exec(src))) if (codeAt(src, m, x.index)) addGo(x[1], x[2], x.index);
1697
+ const BLOCK = /^import[ \t]*\(([\s\S]*?)\)/gm;
1698
+ while ((x = BLOCK.exec(src))) {
1699
+ if (!codeAt(src, m, x.index)) continue;
1700
+ let off = x.index + x[0].indexOf("(") + 1;
1701
+ for (const l of x[1].split("\n")) {
1702
+ const mm = /^\s*(?:([\w.]+)\s+)?"([^"]+)"/.exec(l);
1703
+ if (mm) addGo(mm[1], mm[2], off);
1704
+ off += l.length + 1;
1705
+ }
1706
+ }
1707
+ } else if (lang === "java") {
1708
+ const IMP = /^[ \t]*import[ \t]+(static[ \t]+)?([\w.]+?)(\.\*)?[ \t]*;/gm;
1709
+ while ((x = IMP.exec(src))) {
1710
+ if (!codeAt(src, m, x.index)) continue;
1711
+ const parts = x[2].split(".");
1712
+ if (x[3]) out.push({ from: x[2], line: line(x.index), bindings: [], star: true });
1713
+ else if (x[1]) out.push({ from: parts.slice(0, -1).join("."), line: line(x.index), bindings: [{ local: parts[parts.length - 1], orig: parts[parts.length - 1], kind: "named" }] });
1714
+ else out.push({ from: x[2], line: line(x.index), bindings: [{ local: parts[parts.length - 1], orig: parts[parts.length - 1], kind: "named" }] });
1715
+ }
1716
+ } else if (lang === "cs") {
1717
+ const USING = /^[ \t]*using[ \t]+(?:static[ \t]+)?(?:(\w+)[ \t]*=[ \t]*)?([\w.]+)[ \t]*;/gm;
1718
+ while ((x = USING.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: [] });
1719
+ } else if (lang === "php") {
1720
+ const USE = /^[ \t]*use[ \t]+(?:function[ \t]+)?([\w\\]+)(?:[ \t]+as[ \t]+(\w+))?[ \t]*;/gm;
1721
+ while ((x = USE.exec(src))) {
1722
+ if (!codeAt(src, m, x.index)) continue;
1723
+ const last = x[1].split("\\").pop();
1724
+ out.push({ from: x[1], line: line(x.index), bindings: [{ local: x[2] || last, orig: last, kind: "named" }] });
1725
+ }
1726
+ const INC = /\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*['"]([^'"\n]+\.php)['"]/g;
1727
+ while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
1728
+ } else if (lang === "rb") {
1729
+ // `require_relative "../store"` is a path; `require "store"` is a load
1730
+ // path entry, matched by suffix. Neither names the constants it brings in,
1731
+ // so the file is the binding and the UNIQUE rung names the symbol.
1732
+ const REQ = /^[ \t]*require(_relative)?[ \t]*\(?[ \t]*['"]([^'"\n]+)['"]/gm;
1733
+ while ((x = REQ.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: [], ...(x[1] ? { include: true } : {}) });
1734
+ } else if (lang === "rs") {
1735
+ // `mod store;` names a sibling file; `use crate::store::OrderStore` names
1736
+ // a symbol in one. A `{a, b}` group brings in several names at once.
1737
+ const MOD = /^[ \t]*(?:pub[ \t]+)?mod[ \t]+([A-Za-z_]\w*)[ \t]*;/gm;
1738
+ while ((x = MOD.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], mod: true });
1739
+ const USE = /^[ \t]*(?:pub[ \t]+)?use[ \t]+([^;\n]+);/gm;
1740
+ while ((x = USE.exec(src))) {
1741
+ if (!codeAt(src, m, x.index)) continue;
1742
+ const raw = x[1].replace(/\s+/g, "");
1743
+ const g = /^(.*?)::\{(.+)\}$/.exec(raw);
1744
+ const bindings = [];
1745
+ const take = (p) => {
1746
+ const mm = /^(?:.*::)?(\w+)(?:as(\w+))?$/.exec(p);
1747
+ if (mm && mm[1] !== "self" && mm[1] !== "*") bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
1748
+ };
1749
+ if (g) {
1750
+ for (const p of g[2].split(",")) if (p) take(p);
1751
+ out.push({ from: g[1], line: line(x.index), bindings });
1752
+ } else {
1753
+ take(raw);
1754
+ out.push({ from: raw.replace(/::\w+$/, ""), line: line(x.index), bindings });
1755
+ }
1756
+ }
1757
+ } else if (lang === "kt") {
1758
+ const IMP = /^[ \t]*import[ \t]+([\w.]+?)(\.\*)?(?:[ \t]+as[ \t]+(\w+))?[ \t]*$/gm;
1759
+ while ((x = IMP.exec(src))) {
1760
+ if (!codeAt(src, m, x.index)) continue;
1761
+ const last = x[1].split(".").pop();
1762
+ if (x[2]) out.push({ from: x[1], line: line(x.index), bindings: [], star: true });
1763
+ else out.push({ from: x[1], line: line(x.index), bindings: [{ local: x[3] || last, orig: last, kind: "named" }] });
1764
+ }
1765
+ } else if (lang === "c") {
1766
+ // Only a QUOTED include is a file in this repository; `<vector>` is not.
1767
+ const INC = /^[ \t]*#[ \t]*include[ \t]*"([^"\n]+)"/gm;
1768
+ while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
1769
+ }
1770
+ return out;
1771
+ }
1772
+
1773
+ // ── effects ─────────────────────────────────────────────────────────────────
1774
+ const SQL_RE = /^\s*(SELECT\s|INSERT\s+INTO\s|UPDATE\s+[\w."`\[\]]+\s+SET\s|DELETE\s+FROM\s|CREATE\s+(TABLE|INDEX|VIEW)\s|ALTER\s+TABLE\s|DROP\s+TABLE\s|WITH\s+\w+\s+AS\s*\(|MERGE\s+INTO\s)/i;
1775
+ const HTTP_RE = /^(fetch|axios|axios\.(get|post|put|patch|delete|head|request)|https?\.(request|get)|requests\.(get|post|put|patch|delete|head|request)|httpx\.(get|post|put|patch|delete|request)|http\.(Get|Post|Head|NewRequest|NewRequestWithContext)|curl_exec|Http\.(get|post|put|patch|delete)|restTemplate\.\w+|webClient\.\w+|httpClient\.(get|post|put|patch|delete|send|GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)|urllib\.request\.urlopen|urlopen)$/;
1776
+ const DB_RE = /(^[\w$]+(\.[\w$]+)*\.(query|execute|executemany|executescript|raw|\$queryRaw|\$executeRaw|\$queryRawUnsafe|\$executeRawUnsafe)$)|(^prisma\.[\w$]+\.[\w$]+$)|(^knex(\.|$))|(\.objects\.\w+$)|(^(db|conn|connection|pool|cursor|session|sequelize|em|entityManager|dataSource|queryRunner|mongoose|collection)\.[\w$]+$)/;
1777
+ const FS_RE = {
1778
+ js: /^(fs|fsp|fse|fs\.promises)\.(writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream|mkdir|mkdirSync|rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync|rename|renameSync|copyFile|copyFileSync|outputFile|remove)$/,
1779
+ go: /^(os\.(WriteFile|Create|Remove|RemoveAll|Mkdir|MkdirAll|Rename)|ioutil\.WriteFile)$/,
1780
+ py: /^(os\.(remove|unlink|rename|makedirs|mkdir|rmdir)|shutil\.(rmtree|move|copy|copyfile)|.+\.(write_text|write_bytes))$/,
1781
+ php: /^(file_put_contents|unlink|mkdir|rmdir|rename|fwrite)$/,
1782
+ cs: /^(File\.(Write\w*|Delete|Move|Copy|AppendAll\w*|Create)|Directory\.(CreateDirectory|Delete))$/,
1783
+ java: /^(Files\.(write|writeString|delete|deleteIfExists|move|copy|createDirectories))$/,
1784
+ };
1785
+ FS_RE.ts = FS_RE.js;
1786
+ const ENV_RE = /process\.env(?:\.([A-Za-z_]\w*)|\[\s*['"`]([^'"`]+)['"`]\s*\])|os\.environ(?:\.get\(\s*|\[\s*)['"]([^'"]+)['"]|os\.getenv\(\s*['"]([^'"]+)['"]|os\.Getenv\(\s*"([^"]+)"|System\.getenv\(\s*"([^"]+)"|Environment\.GetEnvironmentVariable\(\s*"([^"]+)"|\bgetenv\(\s*['"]([^'"]+)['"]|\$_ENV\[\s*['"]([^'"]+)['"]/g;
1787
+
1788
+ function stringAt(src, strAt, pos) {
1789
+ let k = pos;
1790
+ while (k < src.length && /\s/.test(src[k])) k++;
1791
+ if (!/['"`]/.test(src[k] || "")) return null;
1792
+ const s = strAt.get(k + 1);
1793
+ return s ? src.slice(s.start, s.end) : null;
1794
+ }
1795
+
1796
+ function parenFor(src, starts, call) {
1797
+ if (call._p != null) return call._p;
1798
+ const ls = starts[call.line - 1] || 0;
1799
+ const le = starts[call.line] || src.length;
1800
+ const last = call.name.split(".").pop();
1801
+ const i = src.indexOf(last, ls);
1802
+ if (i < 0 || i >= le) return null;
1803
+ const p = src.indexOf("(", i + last.length);
1804
+ return p >= 0 && p < le ? p : null;
1805
+ }
1806
+
1807
+ function effectsOf(lang, src, m, starts, strings, strAt, calls) {
1808
+ const out = [];
1809
+ for (const s of strings) {
1810
+ const text = src.slice(s.start, s.end);
1811
+ if (SQL_RE.test(text)) out.push({ type: "sql", text: clip(text), line: lineOf(starts, s.start) });
1812
+ }
1813
+ let x;
1814
+ ENV_RE.lastIndex = 0;
1815
+ while ((x = ENV_RE.exec(src))) {
1816
+ if (!codeAt(src, m, x.index)) continue;
1817
+ const name = x.slice(1).find(Boolean);
1818
+ if (name) out.push({ type: "env", text: name, line: lineOf(starts, x.index) });
1819
+ }
1820
+ const fsRe = FS_RE[lang];
1821
+ for (const cc of calls) {
1822
+ if (HTTP_RE.test(cc.name)) {
1823
+ const p = parenFor(src, starts, cc);
1824
+ const url = p != null ? stringAt(src, strAt, p + 1) : null;
1825
+ out.push({ type: "http", text: url ? `${cc.name} ${clip(url, 60)}` : cc.name, line: cc.line });
1826
+ } else if (DB_RE.test(cc.name)) {
1827
+ out.push({ type: "db", text: cc.name, line: cc.line });
1828
+ } else if (fsRe && fsRe.test(cc.name)) {
1829
+ out.push({ type: "fs", text: cc.name, line: cc.line });
1830
+ } else if (lang === "py" && cc.name === "open") {
1831
+ const p = parenFor(src, starts, cc);
1832
+ if (p != null && /^[^)]*?,\s*(?:mode\s*=\s*)?['"][wax]/.test(src.slice(p + 1, p + 120))) out.push({ type: "fs", text: "open (write)", line: cc.line });
1833
+ }
1834
+ }
1835
+ return out;
1836
+ }
1837
+
1838
+ // ── URLs, mounts and prefixes (v1.8.2 W1) ───────────────────────────────────
1839
+ // A URL is a string that starts with `/`. A template or an f-string whose
1840
+ // leading expression is unknown (`${base}/p`, `f"{base}/p"`) and a `BASE + "/p"`
1841
+ // concatenation are SUFFIXES: the path is known, the prefix is not, and the
1842
+ // resolver matches them by their tail only.
1843
+ function urlText(s) {
1844
+ if (s == null) return null;
1845
+ let t = String(s).trim();
1846
+ let suffix = false;
1847
+ const lead = /^(\$\{[^}]*\}|\{[^}]*\})+/.exec(t);
1848
+ if (lead) {
1849
+ t = t.slice(lead[0].length);
1850
+ suffix = true;
1851
+ }
1852
+ const abs = /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(\/.*)?$/i.exec(t);
1853
+ if (abs) t = abs[2] || "/";
1854
+ else if (/^[a-z]+:\/\//i.test(t)) return null; // another host — not a route here
1855
+ if (!t.startsWith("/") || /[\r\n]/.test(t) || t.length > 200) return null;
1856
+ return { url: t, suffix };
1857
+ }
1858
+
1859
+ const GO_METHOD = /^http\.Method(Get|Post|Put|Patch|Delete|Head|Options)$/;
1860
+
1861
+ // The URL an HTTP-shaped call names, with its method — or null.
1862
+ function urlOf(lang, call) {
1863
+ const args = call.args;
1864
+ if (!args || !args.length) return null;
1865
+ const parts = call.name.split(".");
1866
+ const last = parts[parts.length - 1];
1867
+ const head = parts.length > 1 ? parts[parts.length - 2] : null;
1868
+ const pos = args.filter((a) => !a.kw);
1869
+ const kw = {};
1870
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1871
+ // `BASE + "/p"` is one piece whose tail is known: it becomes `{}/p`, which
1872
+ // `urlText` reads as a SUFFIX.
1873
+ const strArg = (i) => (pos[i] && pos[i].s != null ? pos[i].s : pos[i] && pos[i].cat != null ? "{}" + pos[i].cat : null);
1874
+ let method = null;
1875
+ let raw = null;
1876
+ if (last === "open" && !(kw.method && kw.method.s)) return null;
1877
+ if (HTTP_VERBS.has(last) && head !== "Route" && !/^(cache|map|store|redis|client\.hgetall|localStorage|params|headers|searchParams|Map|WeakMap|process|obj|opts|options|config|env|form|url)$/i.test(head || "")) {
1878
+ method = VERB_OF(last);
1879
+ raw = strArg(0) != null ? strArg(0) : kw.url && kw.url.s != null ? kw.url.s : null;
1880
+ // `$this->json('POST', '/p')`, `client.open("/p", method="post")`
1881
+ } else if (last === "json" && lang === "php" && strArg(0) && /^[A-Z]+$/.test(strArg(0))) {
1882
+ method = strArg(0);
1883
+ raw = strArg(1);
1884
+ } else if (last === "fetch" || last === "axios" || last === "request" || last === "open" || last === "perform") {
1885
+ raw = strArg(0) != null ? strArg(0) : kw.url && kw.url.s != null ? kw.url.s : null;
1886
+ const o = (pos[1] && pos[1].obj) || (pos[0] && pos[0].obj) || null;
1887
+ method = (o && o.method && o.method.toUpperCase()) || (kw.method && kw.method.s && kw.method.s.toUpperCase()) || "ANY";
1888
+ if (last === "perform" && !raw) return null;
1889
+ } else if (/^(NewRequest|NewRequestWithContext)$/.test(last) && (head === "http" || head === "httptest")) {
1890
+ const i = last === "NewRequestWithContext" ? 1 : 0;
1891
+ const mm = strArg(i);
1892
+ method = mm ? mm.toUpperCase() : pos[i] && pos[i].id && GO_METHOD.test(pos[i].id) ? GO_METHOD.exec(pos[i].id)[1].toUpperCase() : "ANY";
1893
+ raw = strArg(i + 1);
1894
+ } else if (/^(Get|Post|Head)$/.test(last) && head === "http") {
1895
+ method = last.toUpperCase();
1896
+ raw = strArg(0);
1897
+ } else if (/^(GetAsync|PostAsync|PutAsync|DeleteAsync|PatchAsync)$/.test(last)) {
1898
+ method = last.replace(/Async$/, "").toUpperCase();
1899
+ raw = strArg(0);
1900
+ } else return null;
1901
+ const u = urlText(raw);
1902
+ if (!u) return null;
1903
+ return { method: method || "ANY", url: u.url, ...(u.suffix ? { suffix: true } : {}), line: call.line };
1904
+ }
1905
+
1906
+ // `app.use("/orders", ordersRouter)` and its cousins → { prefix, target }.
1907
+ // `target` is a NAME (an import binding, a dotted name), an inline require's
1908
+ // specifier, or a Django `include()` module — resolved on read.
1909
+ function mountOf(lang, call) {
1910
+ const args = call.args;
1911
+ if (!args || !args.length) return null;
1912
+ const last = call.name.split(".").pop();
1913
+ const pos = args.filter((a) => !a.kw);
1914
+ const kw = {};
1915
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1916
+ const tgt = (a) => (a ? (a.req != null ? a.req : a.inc != null ? a.inc : a.id != null ? a.id : null) : null);
1917
+ const norm = (p) => (p == null ? null : String(p).startsWith("/") ? String(p) : "/" + String(p));
1918
+ if ((lang === "js" || lang === "ts") && last === "use" && pos[0] && pos[0].s != null && pos[0].s.startsWith("/")) {
1919
+ const t = pos.slice(1).map(tgt).find(Boolean);
1920
+ return t ? { prefix: pos[0].s, target: t } : null;
1921
+ }
1922
+ if ((lang === "js" || lang === "ts") && last === "register" && pos[0] && tgt(pos[0])) {
1923
+ const o = pos[1] && pos[1].obj;
1924
+ return { prefix: (o && o.prefix) || "", target: tgt(pos[0]) };
1925
+ }
1926
+ if (lang === "py" && (last === "include_router" || last === "register_blueprint" || last === "mount")) {
1927
+ if (last === "mount") return pos[0] && pos[0].s != null && tgt(pos[1]) ? { prefix: pos[0].s, target: tgt(pos[1]) } : null;
1928
+ const t = tgt(pos[0]);
1929
+ if (!t) return null;
1930
+ const p = (kw.prefix && kw.prefix.s) || (kw.url_prefix && kw.url_prefix.s) || (pos[1] && pos[1].s) || "";
1931
+ return { prefix: p, target: t };
1932
+ }
1933
+ if (lang === "py" && /^(path|re_path|url)$/.test(last) && pos[0] && pos[0].s != null && pos[1] && pos[1].inc != null) {
1934
+ return { prefix: norm(pos[0].s.replace(/^\^/, "")), target: pos[1].inc, module: true };
1935
+ }
1936
+ if (lang === "go" && (last === "Mount" || last === "Group") && pos[0] && pos[0].s != null && pos[1] && tgt(pos[1])) {
1937
+ return { prefix: pos[0].s, target: tgt(pos[1]) };
1938
+ }
1939
+ return null;
1940
+ }
1941
+
1942
+ // `router = APIRouter(prefix="/orders")` / `bp = Blueprint("x", __name__,
1943
+ // url_prefix="/x")` / `new Router({ prefix: "/x" })` — the prefix a VARIABLE
1944
+ // carries in this file. Decorator routes on it and `router.get` registrations
1945
+ // on it fold it into their name.
1946
+ function prefixOf(call) {
1947
+ const args = call.args;
1948
+ if (!args || !call.assign) return null;
1949
+ const last = call.name.split(".").pop();
1950
+ if (!/^(APIRouter|Blueprint|Router)$/.test(last)) return null;
1951
+ for (const a of args) {
1952
+ if (a.kw === "prefix" || a.kw === "url_prefix") return a.s != null ? a.s : null;
1953
+ if (a.obj && a.obj.prefix) return a.obj.prefix;
1954
+ }
1955
+ return "";
1956
+ }
1957
+
1958
+ const joinPath = (prefix, p) => {
1959
+ const a = String(prefix || "");
1960
+ let b = String(p || "");
1961
+ if (b && !b.startsWith("/")) b = "/" + b;
1962
+ const out = (a + b).replace(/\/{2,}/g, "/");
1963
+ return out || "/";
1964
+ };
1965
+
1966
+ // ── assembly ────────────────────────────────────────────────────────────────
1967
+ function finalize(c, lang, defs, calls, imports, exportsSet, extractor) {
1968
+ const { src, m, starts, strings, strAt } = c;
1969
+ const rel = c.rel;
1970
+ const nLines = starts.length;
1971
+
1972
+ // W1: the prefix each router VARIABLE carries in this file, and the prefix of
1973
+ // each class (a decorator on the class).
1974
+ const varPrefix = new Map();
1975
+ for (const cc of calls) {
1976
+ if (cc.ref) continue;
1977
+ const p = prefixOf(cc);
1978
+ if (p != null) varPrefix.set(cc.assign, p);
1979
+ }
1980
+ const classPrefix = new Map();
1981
+ for (const d of defs) {
1982
+ if (d.kind !== "class" || !d.decos) continue;
1983
+ for (const deco of d.decos) {
1984
+ const r = decoRoute(deco, true);
1985
+ if (r && r.prefix != null) classPrefix.set(d.name, r.prefix);
1986
+ }
1987
+ }
1988
+ // Route names. A registration (`router.get("/p", …)`) carries its head's
1989
+ // prefix; a decorated handler carries its class's, or its decorator head's.
1990
+ const aliases = [];
1991
+ for (const d of defs) {
1992
+ if (d.kind === "route") {
1993
+ const pre = varPrefix.get(d.head) || "";
1994
+ d.name = `${d.method} ${clip(joinPath(pre, d.path), 60)}`;
1995
+ d.qname = d.name;
1996
+ continue;
1997
+ }
1998
+ if (!d.decos) continue;
1999
+ const routes = [];
2000
+ for (const deco of d.decos) {
2001
+ const r = decoRoute(deco, false);
2002
+ if (!r) continue;
2003
+ const head = String(deco.name).split(".").slice(0, -1).join(".");
2004
+ const cls = d.qname.includes(".") ? d.qname.split(".").slice(0, -1).pop() : null;
2005
+ let pre = (cls && classPrefix.get(cls)) || varPrefix.get(head) || "";
2006
+ // ASP.NET: `[Route("api/[controller]")]` names the class without its suffix.
2007
+ if (cls && /\[controller\]/i.test(pre)) pre = pre.replace(/\[controller\]/gi, cls.replace(/Controller$/, "").toLowerCase());
2008
+ for (const rt of r.routes) routes.push({ method: rt.method, path: joinPath(pre, rt.path) });
2009
+ }
2010
+ if (routes.length) {
2011
+ d.route = `${routes[0].method} ${clip(routes[0].path, 60)}`;
2012
+ for (const rt of routes) aliases.push({ def: d, name: `${rt.method} ${clip(rt.path, 60)}` });
2013
+ }
2014
+ }
2015
+
2016
+ const owner = new Int32Array(nLines + 2).fill(-1);
2017
+ const order = defs.map((_, i) => i).sort((a, b) => defs[b].e - defs[b].s - (defs[a].e - defs[a].s));
2018
+ for (const i of order) for (let L = defs[i].s; L <= defs[i].e && L <= nLines; L++) owner[L] = i;
2019
+
2020
+ // W2 (G2): instance aliases, scoped. `classAt(line)` is the innermost class
2021
+ // holding a line; `ownerOf(line)` the innermost symbol (−1 = the module).
2022
+ const classDefs = defs.map((d, i) => ({ d, i })).filter((x) => x.d.kind === "class");
2023
+ const classAt = (line) => {
2024
+ let best = null;
2025
+ for (const x of classDefs) if (x.d.s <= line && line <= x.d.e && (!best || x.d.e - x.d.s < best.d.e - best.d.s)) best = x;
2026
+ return best ? best.d : null;
2027
+ };
2028
+ const ownerOf = (line) => (line >= 0 && line <= nLines ? owner[line] : -1);
2029
+ const localAl = new Map(); // owner index → Map<local, cls|null>
2030
+ const classAl = new Map(); // class name → Map<field, cls>
2031
+ for (let al of aliasesOf(c, lang)) {
2032
+ // W5 (G6): a `self` alias with no enclosing class is not a field — it is
2033
+ // an indented local (a Kotlin `val` inside a top-level function). Reading
2034
+ // it as a local is what it is; dropping it loses the edge.
2035
+ if (al.self && !classAt(al.line)) al = { ...al, self: false };
2036
+ if (al.self) {
2037
+ const cl = classAt(al.line);
2038
+ if (!cl) continue;
2039
+ const mm = classAl.get(cl.name) || new Map();
2040
+ mm.set(al.local, al.cls);
2041
+ classAl.set(cl.name, mm);
2042
+ } else {
2043
+ const idx = ownerOf(al.line);
2044
+ const mm = localAl.get(idx) || new Map();
2045
+ // The first declaration in a scope wins; a later re-binding of the same
2046
+ // name in the same scope is rare and a guess either way.
2047
+ if (!mm.has(al.local)) mm.set(al.local, al.cls);
2048
+ localAl.set(idx, mm);
2049
+ }
2050
+ }
2051
+ const classOfDef = (d) => (d && d.kind === "method" && d.qname.includes(".") ? d.qname.split(".").slice(0, -1).pop() : d && d.kind === "class" ? d.name : null);
2052
+ const aliasFor = (cc) => {
2053
+ if (cc.ref || cc.sup || !cc.name.includes(".")) return null;
2054
+ const parts = cc.name.split(".");
2055
+ if (parts.length !== 2) return null;
2056
+ const head = parts[0];
2057
+ const idx = ownerOf(cc.line);
2058
+ const d = idx >= 0 ? defs[idx] : null;
2059
+ if (cc.self) {
2060
+ const cn = classOfDef(d) || (classAt(cc.line) || {}).name;
2061
+ const mm = cn ? classAl.get(cn) : null;
2062
+ const cls = mm ? mm.get(head) : undefined;
2063
+ return cls ? { cls, head } : null;
2064
+ }
2065
+ const scoped = localAl.get(idx);
2066
+ if (scoped && scoped.has(head)) {
2067
+ const cls = scoped.get(head);
2068
+ return cls ? { cls, head } : null; // null = shadowed here
2069
+ }
2070
+ const modAl = localAl.get(-1);
2071
+ if (idx !== -1 && modAl && modAl.has(head)) {
2072
+ const cls = modAl.get(head);
2073
+ return cls ? { cls, head } : null;
2074
+ }
2075
+ // Java, C#, Kotlin, C++ and Ruby all reach a field with no `this.`
2076
+ // (`@store` in Ruby is the field itself, written without `self.`).
2077
+ if (lang === "java" || lang === "cs" || lang === "kt" || lang === "c" || lang === "rb") {
2078
+ const cn = classOfDef(d);
2079
+ const mm = cn ? classAl.get(cn) : null;
2080
+ const cls = mm ? mm.get(head) : undefined;
2081
+ return cls ? { cls, head } : null;
2082
+ }
2083
+ return null;
2084
+ };
2085
+ // W5 (G6). In Ruby and Kotlin a bare call inside a method is a call on the
2086
+ // object — `ok(body)` means `self.ok(body)` — and that is what lets it reach
2087
+ // the base class. It is marked self only when THIS FILE has no top-level
2088
+ // function of the same name, so a real module function still wins where the
2089
+ // language would give it the call.
2090
+ if (lang === "rb" || lang === "kt") {
2091
+ const topLevel = new Set(defs.filter((d) => d.kind === "function").map((d) => d.name));
2092
+ for (const imp of imports || []) for (const b of imp.bindings || []) topLevel.add(b.local);
2093
+ for (const cc of calls) {
2094
+ if (cc.ref || cc.self || cc.name.includes(".") || topLevel.has(cc.name)) continue;
2095
+ const d = defs[ownerOf(cc.line)];
2096
+ if (d && d.kind === "method") cc.self = true;
2097
+ }
2098
+ }
2099
+ for (const cc of calls) {
2100
+ const al = aliasFor(cc);
2101
+ if (!al) continue;
2102
+ cc.name = `${al.cls}.${cc.name.split(".")[1]}`;
2103
+ cc.alias = al.head;
2104
+ delete cc.self;
2105
+ }
2106
+
2107
+ // N1: the sentence the author already wrote, read out of the raw source (the
2108
+ // mask blanks comments, so it cannot come from `m`).
2109
+ const rawLines = src.split(/\r?\n/);
2110
+ const syms = defs.map((d) => {
2111
+ const doc = docFor(rawLines, lang, d);
2112
+ return {
2113
+ id: "",
2114
+ name: d.name,
2115
+ qname: d.qname || d.name,
2116
+ kind: d.kind,
2117
+ lines: [d.s, d.e],
2118
+ exported: !!(d.exported || (exportsSet && exportsSet.has(d.name))),
2119
+ body_hash: "",
2120
+ calls: [],
2121
+ effects: [],
2122
+ ...(doc ? { doc } : {}),
2123
+ ...(d.route ? { route: d.route } : {}),
2124
+ ...(d.bases && d.bases.length ? { bases: d.bases } : {}),
2125
+ };
2126
+ });
2127
+ const mod = { id: "", name: "<module>", qname: "<module>", kind: "module", lines: [1, nLines], exported: false, body_hash: "", calls: [], effects: [] };
2128
+ const at = (line) => (owner[line] >= 0 ? syms[owner[line]] : mod);
2129
+
2130
+ const known = new Set(defs.map((d) => d.name));
2131
+ for (const imp of imports || []) for (const b of imp.bindings || []) known.add(b.local);
2132
+ // A cap is a coverage gap, and the gap starts at the FIRST thing that was
2133
+ // dropped — not at the top of the symbol. A file whose `<module>` holds 400
2134
+ // calls is fully seen down to call 200; saying the whole file is partial
2135
+ // would hide where the record actually stops.
2136
+ const cutAt = new Map();
2137
+ const cut = (t, line) => {
2138
+ const was = cutAt.get(t);
2139
+ if (was === undefined || line < was) cutAt.set(t, line);
2140
+ };
2141
+ for (const cc of calls) {
2142
+ // A ref is kept when its head is defined here or imported. Go's package
2143
+ // scope is the DIRECTORY, so a bare Go identifier is kept too and the
2144
+ // package rung on read decides (`mux.HandleFunc("/p", listOrders)`).
2145
+ const refHead = cc.self ? cc.name.split(".").pop() : cc.name.split(".")[0];
2146
+ if (cc.ref && !known.has(refHead) && !(lang === "go" && !cc.name.includes("."))) continue;
2147
+ const t = at(cc.line);
2148
+ if (t.calls.length < MAX_CALLS_PER_SYMBOL)
2149
+ t.calls.push({
2150
+ name: cc.name,
2151
+ line: cc.line,
2152
+ ...(cc.self ? { self: true } : {}),
2153
+ ...(cc.sup ? { sup: true } : {}),
2154
+ ...(cc.ref ? { ref: true } : {}),
2155
+ ...(cc.alias ? { alias: cc.alias } : {}),
2156
+ });
2157
+ else cut(t, cc.line);
2158
+ if (cc.ref || !cc.args) continue;
2159
+ const u = cc.reg ? null : urlOf(lang, cc);
2160
+ if (u) {
2161
+ if (!t.urls) t.urls = [];
2162
+ if (t.urls.length < MAX_URLS_PER_SYMBOL) t.urls.push(u);
2163
+ else cut(t, cc.line);
2164
+ }
2165
+ const mt = mountOf(lang, cc);
2166
+ if (mt) (t.mounts || (t.mounts = [])).push({ ...mt, line: cc.line });
2167
+ }
2168
+ // A function passed as an argument runs no HTTP, SQL or fs effect by being passed.
2169
+ for (const ef of effectsOf(lang, src, m, starts, strings, strAt, calls.filter((cc) => !cc.ref))) {
2170
+ const t = at(ef.line);
2171
+ if (t.effects.length >= MAX_EFFECTS_PER_SYMBOL) {
2172
+ cut(t, ef.line);
2173
+ continue;
2174
+ }
2175
+ if (!t.effects.some((e) => e.type === ef.type && e.text === ef.text)) t.effects.push(ef);
2176
+ }
2177
+
2178
+ // The partial RANGES, merged, in line order. A cap gives the tail from the
2179
+ // first dropped line; an unbalanced block gives everything from it to the end
2180
+ // of the file.
2181
+ const ranges = [];
2182
+ for (const [t, line] of cutAt) ranges.push([line, t.lines[1]]);
2183
+ if (UNBALANCED >= 0) ranges.push([lineOf(starts, UNBALANCED), nLines]);
2184
+ ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
2185
+ const partial = [];
2186
+ for (const r of ranges) {
2187
+ const last = partial[partial.length - 1];
2188
+ if (last && r[0] <= last[1] + 1) last[1] = Math.max(last[1], r[1]);
2189
+ else partial.push([r[0], r[1]]);
2190
+ }
2191
+
2192
+ const seen = new Map();
2193
+ const assignId = (s) => {
2194
+ let id = `${rel}#${s.qname}`;
2195
+ const k = seen.get(id) || 0;
2196
+ seen.set(id, k + 1);
2197
+ if (k) id += `~${k + 1}`;
2198
+ s.id = id;
2199
+ };
2200
+ for (const s of syms) {
2201
+ const a = starts[s.lines[0] - 1] || 0;
2202
+ const b = s.lines[1] < nLines ? starts[s.lines[1]] : src.length;
2203
+ s.body_hash = hash(src.slice(a, b));
2204
+ assignId(s);
2205
+ }
2206
+ // W1: the alias ROUTE symbol for a decorated handler. Same lines and the same
2207
+ // body hash as the handler; it CALLS the handler (one LOCAL edge on read), so
2208
+ // `impact` and `ctx` walk test → route → handler with no special case.
2209
+ for (const al of aliases) {
2210
+ const h = syms[defs.indexOf(al.def)];
2211
+ if (!h) continue;
2212
+ const s = {
2213
+ id: "",
2214
+ name: al.name,
2215
+ qname: al.name,
2216
+ kind: "route",
2217
+ lines: h.lines.slice(),
2218
+ exported: false,
2219
+ body_hash: h.body_hash,
2220
+ calls: [{ name: h.qname, line: h.lines[0], ...(h.kind === "method" ? {} : {}) }],
2221
+ effects: [],
2222
+ ...(h.doc ? { doc: h.doc } : {}),
2223
+ handler: h.id,
2224
+ };
2225
+ assignId(s);
2226
+ syms.push(s);
2227
+ }
2228
+ if (mod.calls.length || mod.effects.length || mod.urls || mod.mounts) {
2229
+ mod.id = `${rel}#<module>`;
2230
+ syms.push(mod);
2231
+ }
2232
+ const reexports = reexportsFor(lang, src, m, rel, imports);
2233
+ return {
2234
+ extractor,
2235
+ imports,
2236
+ symbols: syms,
2237
+ // K1 (v1.8.2 W4b): the line count. The gain meter's counterfactual needs
2238
+ // the AVERAGE line length of this file to price "an 80-line range read",
2239
+ // and `bytes / lines` is that, for free, from the parse that just ran.
2240
+ lines: nLines,
2241
+ coverage: partial.length ? "partial" : "full",
2242
+ ...(partial.length ? { partial } : {}),
2243
+ ...(reexports.length ? { reexports } : {}),
2244
+ };
2245
+ }
2246
+
2247
+ function context(item) {
2248
+ const { rel, lang, src } = item;
2249
+ const { masked: m, strings } = mask(src, lang);
2250
+ const strAt = new Map();
2251
+ for (const s of strings) strAt.set(s.start, s);
2252
+ return { rel, lang, src, m, strings, strAt, starts: lineIndex(src) };
2253
+ }
2254
+
2255
+ // W5 (G6). A single-file component is a `<script>` block inside markup. Blank
2256
+ // everything outside it, keeping every newline, so the js/ts rung reads the
2257
+ // script and every line number is still the FILE's own line number.
2258
+ function sfcSource(src) {
2259
+ const out = src.split("");
2260
+ let lang = "js";
2261
+ const keep = [];
2262
+ const lower = src.toLowerCase();
2263
+ const RE = /<script\b([^>]*)>/gi;
2264
+ let x;
2265
+ while ((x = RE.exec(src))) {
2266
+ const open = x.index + x[0].length;
2267
+ const close = lower.indexOf("</script>", open);
2268
+ const end = close < 0 ? src.length : close;
2269
+ if (/lang[ \t]*=[ \t]*["']?(ts|typescript)/i.test(x[1])) lang = "ts";
2270
+ keep.push([open, end]);
2271
+ RE.lastIndex = end;
2272
+ }
2273
+ let at = 0;
2274
+ const blank = (a, b) => {
2275
+ for (let k = a; k < b; k++) if (out[k] !== "\n" && out[k] !== "\r") out[k] = " ";
2276
+ };
2277
+ for (const [a, b] of keep) {
2278
+ blank(at, a);
2279
+ at = b;
2280
+ }
2281
+ blank(at, src.length);
2282
+ return { src: out.join(""), lang };
2283
+ }
2284
+
2285
+ function extractOne(item, borrowed) {
2286
+ let { lang, src } = item;
2287
+ resetCoverage();
2288
+ if (lang === "vue" || lang === "svelte") {
2289
+ const s = sfcSource(src);
2290
+ src = s.src;
2291
+ lang = s.lang;
2292
+ item = { ...item, src, lang };
2293
+ }
2294
+ const c = context(item);
2295
+ if (borrowed) {
2296
+ const calls = [];
2297
+ for (const cc of borrowed.calls) {
2298
+ const n = normCall(cc.name);
2299
+ if (!n) continue;
2300
+ if (cc.ref && n.sup) continue;
2301
+ const rec = { name: n.name, line: cc.line, ...(n.self ? { self: true } : {}), ...(n.sup ? { sup: true } : {}), ...(cc.ref ? { ref: true } : {}) };
2302
+ if (cc.args) rec.args = cc.args;
2303
+ if (cc.assign) rec.assign = cc.assign;
2304
+ calls.push(rec);
2305
+ }
2306
+ const defs = borrowed.defs.map((d) => ({ ...d, from: c.starts[d.s - 1] || 0 }));
2307
+ if (lang !== "py") {
2308
+ // A borrowed rung that is not Python still needs the ROUTE pass that
2309
+ // reads registrations out of the mask (`router.get("/p", …)`), because a
2310
+ // route handler is anonymous and the AST has no name to give it.
2311
+ const reg = new Set();
2312
+ const { defs: rdefs, regParens, exportsSet: hexports } = braceDefs(c, lang);
2313
+ resetCoverage();
2314
+ for (const d of rdefs) if (d.kind === "route") defs.push({ ...d, s: lineOf(c.starts, d.from), e: lineOf(c.starts, Math.max(d.from, d.to)) });
2315
+ for (const p of regParens) reg.add(lineOf(c.starts, p));
2316
+ for (const cc of calls) if (reg.has(cc.line) && ARGS_WORTH.test(cc.name)) cc.reg = true;
2317
+ defs.sort((a, b) => a.from - b.from);
2318
+ // `module.exports = { … }` is not in the AST walk; the heuristic half
2319
+ // that just ran read it out of the source, so the two sets are merged.
2320
+ const exp = new Set([...(borrowed.exportsSet || []), ...(hexports || [])]);
2321
+ return finalize(c, lang, defs, calls, borrowed.imports, exp.size ? exp : null, borrowed.extractor);
2322
+ }
2323
+ // Django routes are module-level calls the ast visitor reports with their
2324
+ // argument pieces; the heuristic finds them by regex. Same shape either way.
2325
+ for (const cc of calls) {
2326
+ if (cc.ref || !cc.args || !/^(path|re_path|url)$/.test(cc.name)) continue;
2327
+ const pos = cc.args.filter((a) => !a.kw);
2328
+ if (!pos[0] || pos[0].s == null || !pos[1] || pos[1].inc != null || !pos[1].id) continue;
2329
+ const p = pos[0].s.replace(/^\^/, "").replace(/\$$/, "");
2330
+ const line = cc.line;
2331
+ // The ast visitor already reported `views.x` as a ref on this line.
2332
+ defs.push({ kind: "route", method: "ANY", path: p.startsWith("/") ? p : "/" + p, head: cc.name, name: "route", s: line, e: cc.end || line, from: c.starts[line - 1] || 0, exported: false });
2333
+ }
2334
+ return finalize(c, lang, defs, calls, borrowed.imports, null, borrowed.extractor);
2335
+ }
2336
+ if (lang === "py" || lang === "rb") {
2337
+ const { defs, defParens, regParens } = lang === "py" ? pyDefs(c) : rbDefs(c);
2338
+ return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), null, HEURISTIC);
2339
+ }
2340
+ const { defs, defParens, regParens, exportsSet } = braceDefs(c, lang);
2341
+ braceDecorators(c, lang, defs);
2342
+ return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), exportsSet, HEURISTIC);
2343
+ }
2344
+
2345
+ // ── the borrowed Python parser ──────────────────────────────────────────────
2346
+ // Emits the SAME rich call shape the heuristic builds (`args` pieces with
2347
+ // `s` / `id` / `kw` / `list` / `inc`, and `assign`), so `finalize` is one code
2348
+ // path for both rungs.
2349
+ const PY_AST = String.raw`
2350
+ import ast, sys, json
2351
+ def dotted(n):
2352
+ parts = []
2353
+ while isinstance(n, ast.Attribute):
2354
+ parts.append(n.attr)
2355
+ n = n.value
2356
+ if isinstance(n, ast.Name):
2357
+ parts.append(n.id)
2358
+ return ".".join(reversed(parts))
2359
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "super":
2360
+ parts.append("super")
2361
+ return ".".join(reversed(parts))
2362
+ # OrderService().create() -- a constructor call is the receiver (W1).
2363
+ if parts and isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id[:1].isupper():
2364
+ parts.append(n.func.id)
2365
+ return ".".join(reversed(parts))
2366
+ return None
2367
+ def sconst(n):
2368
+ if isinstance(n, ast.Constant) and isinstance(n.value, str):
2369
+ return n.value
2370
+ if isinstance(n, ast.JoinedStr):
2371
+ out = []
2372
+ for v in n.values:
2373
+ if isinstance(v, ast.Constant) and isinstance(v.value, str):
2374
+ out.append(v.value)
2375
+ else:
2376
+ out.append("{}")
2377
+ return "".join(out)
2378
+ return None
2379
+ def piece(n, kw=None):
2380
+ p = {}
2381
+ if kw:
2382
+ p["kw"] = kw
2383
+ s = sconst(n)
2384
+ if s is not None:
2385
+ p["s"] = s
2386
+ return p
2387
+ if isinstance(n, (ast.List, ast.Tuple)):
2388
+ items = [sconst(e) for e in n.elts]
2389
+ p["list"] = [i for i in items if i is not None]
2390
+ return p
2391
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "include" and n.args:
2392
+ inc = sconst(n.args[0])
2393
+ if inc is not None:
2394
+ p["inc"] = inc
2395
+ return p
2396
+ d = dotted(n)
2397
+ if d:
2398
+ p["id"] = d
2399
+ return p
2400
+ WORTH = set("get post put patch delete head options fetch request open include_router register_blueprint mount path re_path url APIRouter Blueprint Router route add_url_rule json".split())
2401
+ class V(ast.NodeVisitor):
2402
+ def __init__(s):
2403
+ s.defs = []; s.calls = []; s.imports = []; s.stack = []; s.assign_for = {}; s.skip = set()
2404
+ def qual(s, name):
2405
+ chain = []
2406
+ for kind, n in reversed(s.stack):
2407
+ if kind != "class":
2408
+ break
2409
+ chain.append(n)
2410
+ chain.reverse()
2411
+ return ".".join(chain + [name]), bool(chain)
2412
+ def decos(s, node):
2413
+ out = []
2414
+ for d in node.decorator_list:
2415
+ if isinstance(d, ast.Call):
2416
+ s.skip.add(id(d))
2417
+ name = dotted(d.func)
2418
+ if not name:
2419
+ continue
2420
+ args = [piece(a) for a in d.args] + [piece(k.value, k.arg) for k in d.keywords if k.arg]
2421
+ out.append({"name": name, "args": args})
2422
+ else:
2423
+ name = dotted(d)
2424
+ if name:
2425
+ out.append({"name": name})
2426
+ return out
2427
+ def visit_ClassDef(s, node):
2428
+ q, _ = s.qual(node.name)
2429
+ bases = [b for b in (dotted(x) for x in node.bases) if b]
2430
+ d = {"name": node.name, "qname": q, "kind": "class", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")}
2431
+ if bases:
2432
+ d["bases"] = [b.split(".")[-1] for b in bases][:8]
2433
+ dl = s.decos(node)
2434
+ if dl:
2435
+ d["decos"] = dl
2436
+ s.defs.append(d)
2437
+ s.stack.append(("class", node.name)); s.generic_visit(node); s.stack.pop()
2438
+ def visit_FunctionDef(s, node):
2439
+ q, inclass = s.qual(node.name)
2440
+ d = {"name": node.name, "qname": q, "kind": "method" if inclass else "function", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")}
2441
+ dl = s.decos(node)
2442
+ if dl:
2443
+ d["decos"] = dl
2444
+ s.defs.append(d)
2445
+ s.stack.append(("function", node.name)); s.generic_visit(node); s.stack.pop()
2446
+ visit_AsyncFunctionDef = visit_FunctionDef
2447
+ def visit_Assign(s, node):
2448
+ if isinstance(node.value, ast.Call) and len(node.targets) == 1:
2449
+ t = dotted(node.targets[0])
2450
+ if t:
2451
+ s.assign_for[id(node.value)] = t
2452
+ s.generic_visit(node)
2453
+ def visit_AnnAssign(s, node):
2454
+ if isinstance(node.value, ast.Call):
2455
+ t = dotted(node.target)
2456
+ if t:
2457
+ s.assign_for[id(node.value)] = t
2458
+ s.generic_visit(node)
2459
+ def visit_Call(s, node):
2460
+ name = dotted(node.func)
2461
+ if name and id(node) not in s.skip:
2462
+ rec = {"name": name, "line": node.lineno}
2463
+ last = name.split(".")[-1]
2464
+ if last in WORTH:
2465
+ rec["args"] = [piece(a) for a in node.args] + [piece(k.value, k.arg) for k in node.keywords if k.arg]
2466
+ rec["end"] = node.end_lineno or node.lineno
2467
+ t = s.assign_for.get(id(node))
2468
+ if t:
2469
+ rec["assign"] = t
2470
+ s.calls.append(rec)
2471
+ for a in list(node.args) + [k.value for k in node.keywords]:
2472
+ r = dotted(a)
2473
+ if r:
2474
+ s.calls.append({"name": r, "line": getattr(a, "lineno", node.lineno), "ref": True})
2475
+ s.generic_visit(node)
2476
+ def visit_Import(s, node):
2477
+ for a in node.names:
2478
+ mod = a.name if a.asname else a.name.split(".")[0]
2479
+ s.imports.append({"from": mod, "line": node.lineno, "bindings": [{"local": a.asname or mod, "orig": mod, "kind": "namespace"}]})
2480
+ def visit_ImportFrom(s, node):
2481
+ mod = "." * (node.level or 0) + (node.module or "")
2482
+ b = [{"local": a.asname or a.name, "orig": a.name, "kind": "named"} for a in node.names if a.name != "*"]
2483
+ s.imports.append({"from": mod, "line": node.lineno, "bindings": b})
2484
+ items = json.loads(sys.stdin.buffer.read().decode("utf-8"))
2485
+ out = {}
2486
+ for it in items:
2487
+ try:
2488
+ with open(it["abs"], "r", encoding="utf-8", errors="replace") as f:
2489
+ tree = ast.parse(f.read())
2490
+ v = V(); v.visit(tree)
2491
+ out[it["rel"]] = {"defs": v.defs, "calls": v.calls, "imports": v.imports}
2492
+ except Exception:
2493
+ out[it["rel"]] = None
2494
+ sys.stdout.buffer.write(json.dumps(out).encode("utf-8"))
2495
+ `;
2496
+
2497
+ let pythonProbe;
2498
+ // ── the borrowed TypeScript parser (v1.8.2 W6, G7) ──────────────────────────
2499
+ // The Python `ast` rung proved the pattern: if the PROJECT already has the
2500
+ // tool, borrow it — one batch, exact ranges, and no dependency for ORC.
2501
+ //
2502
+ // A project with `node_modules/typescript` gets an EXACT parse of its `.ts`,
2503
+ // `.tsx`, `.js` and `.jsx` files. It is loaded IN PROCESS (`require`) and only
2504
+ // `createSourceFile` is used — no `Program`, no type checker, because the graph
2505
+ // is a locator and a type checker would make an update cost minutes.
2506
+ //
2507
+ // Everything else is unchanged: `finalize` gets the same shapes the heuristic
2508
+ // builds, the mask still runs (aliases and effects are read from it), and
2509
+ // `ORC_GRAPH_NO_BORROW=1` forces the heuristic back.
2510
+ let tsProbe;
2511
+ function findTypeScript(root) {
2512
+ if (tsProbe !== undefined && tsProbe !== null && tsProbe.root === root) return tsProbe;
2513
+ if (tsProbe === null) return null;
2514
+ tsProbe = null;
2515
+ if (!root) return null;
2516
+ try {
2517
+ const lib = path.join(root, "node_modules", "typescript", "lib", "typescript.js");
2518
+ if (!fs.existsSync(lib)) return null;
2519
+ const ts = require(lib);
2520
+ if (!ts || typeof ts.createSourceFile !== "function") return null;
2521
+ tsProbe = { ts, ver: String(ts.version || "?"), root };
2522
+ } catch (_) {
2523
+ tsProbe = null;
2524
+ }
2525
+ return tsProbe;
2526
+ }
2527
+
2528
+ const TS_SCRIPT_KIND = (ts, rel) => {
2529
+ if (/\.tsx$/i.test(rel)) return ts.ScriptKind.TSX;
2530
+ if (/\.[cm]?ts$/i.test(rel)) return ts.ScriptKind.TS;
2531
+ if (/\.jsx$/i.test(rel)) return ts.ScriptKind.JSX;
2532
+ return ts.ScriptKind.JS;
2533
+ };
2534
+
2535
+ function tsWalk(ts, it) {
2536
+ const sf = ts.createSourceFile(it.rel, it.src, ts.ScriptTarget.Latest, true, TS_SCRIPT_KIND(ts, it.rel));
2537
+ const lineAt = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
2538
+ const defs = [];
2539
+ const calls = [];
2540
+ const imports = [];
2541
+ const exportsSet = new Set();
2542
+ const chain = []; // the enclosing CLASS names, innermost last
2543
+
2544
+ const text = (n) => {
2545
+ try {
2546
+ return n.getText(sf);
2547
+ } catch (_) {
2548
+ return "";
2549
+ }
2550
+ };
2551
+ const unwrap = (n) => {
2552
+ while (n && (ts.isParenthesizedExpression(n) || ts.isNonNullExpression(n) || ts.isAsExpression(n) || (ts.isTypeAssertionExpression && ts.isTypeAssertionExpression(n)))) n = n.expression;
2553
+ return n;
2554
+ };
2555
+ // A dotted NAME the resolver understands: `a.b.c`, `this.x`, `super.m`. A
2556
+ // constructor call as the receiver carries the CLASS as the head, which is
2557
+ // the W1-chained-new rule (`new S().m()` is a call on `S`).
2558
+ const dotted = (node) => {
2559
+ const n = unwrap(node);
2560
+ if (!n) return null;
2561
+ if (ts.isIdentifier(n) || ts.isPrivateIdentifier(n)) return n.text.replace(/^#/, "");
2562
+ if (n.kind === ts.SyntaxKind.ThisKeyword) return "this";
2563
+ if (n.kind === ts.SyntaxKind.SuperKeyword) return "super";
2564
+ if (ts.isPropertyAccessExpression(n)) {
2565
+ const head = dotted(n.expression);
2566
+ const tail = n.name && n.name.text;
2567
+ if (!tail) return null;
2568
+ // A receiver the parser cannot name (`request(app).get(…)`, `rows[0].id`)
2569
+ // leaves the BARE member name, exactly as the heuristic's chain regex
2570
+ // does — and W1's rule keeps a bare name from ever becoming a method
2571
+ // edge. Dropping the call instead would lose the URL it carries.
2572
+ return head ? `${head}.${tail}` : tail;
2573
+ }
2574
+ if (ts.isNewExpression(n)) {
2575
+ const c = dotted(n.expression);
2576
+ return c && /^[A-Z]/.test(c.split(".").pop()) ? c : null;
2577
+ }
2578
+ if (ts.isCallExpression(n)) {
2579
+ const c = dotted(n.expression);
2580
+ if (c === "super") return "super";
2581
+ return c && /^[A-Z]/.test(c.split(".").pop()) ? c : null;
2582
+ }
2583
+ return null;
2584
+ };
2585
+ // The piece shapes `argPieces` builds, so `urlOf`, `mountOf`, `prefixOf` and
2586
+ // `decoRoute` are ONE code path for the borrowed rung and the heuristic.
2587
+ const strOf = (n) => {
2588
+ const e = unwrap(n);
2589
+ if (!e) return null;
2590
+ if (ts.isStringLiteralLike(e)) return e.text;
2591
+ if (ts.isTemplateExpression(e)) return [e.head.text, ...e.templateSpans.map((s) => s.literal.text)].join("{}");
2592
+ return null;
2593
+ };
2594
+ const piece = (n, kw) => {
2595
+ const p = {};
2596
+ if (kw) p.kw = kw;
2597
+ const e = unwrap(n);
2598
+ const s = strOf(e);
2599
+ if (s != null) {
2600
+ p.s = s;
2601
+ return p;
2602
+ }
2603
+ if (ts.isArrayLiteralExpression(e)) {
2604
+ p.list = e.elements.map(strOf).filter((x) => x != null);
2605
+ return p;
2606
+ }
2607
+ if (ts.isObjectLiteralExpression(e)) {
2608
+ const obj = {};
2609
+ for (const pr of e.properties) {
2610
+ if (!ts.isPropertyAssignment(pr) || !pr.name) continue;
2611
+ const key = ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name) ? pr.name.text : null;
2612
+ const val = strOf(pr.initializer);
2613
+ if (key && val != null) obj[key] = val;
2614
+ }
2615
+ if (Object.keys(obj).length) p.obj = obj;
2616
+ return p;
2617
+ }
2618
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.PlusToken) {
2619
+ // `BASE + "/p"` — the TAIL is known, the prefix is not.
2620
+ const tail = strOf(e.right);
2621
+ if (tail != null) p.cat = tail;
2622
+ return p;
2623
+ }
2624
+ if (ts.isCallExpression(e)) {
2625
+ const head = dotted(e.expression);
2626
+ const first = e.arguments && e.arguments.length ? strOf(e.arguments[0]) : null;
2627
+ if (head === "require" && first != null) {
2628
+ p.req = first;
2629
+ return p;
2630
+ }
2631
+ }
2632
+ const id = dotted(e);
2633
+ if (id) p.id = id;
2634
+ return p;
2635
+ };
2636
+ const skip = new Set(); // decorator call expressions — never a call of the declaration
2637
+ const decosOf = (n) => {
2638
+ const list = (ts.getDecorators ? ts.getDecorators(n) : null) || (n.decorators || []);
2639
+ const out = [];
2640
+ for (const d of list) {
2641
+ const e = unwrap(d.expression);
2642
+ if (ts.isCallExpression(e)) {
2643
+ skip.add(e);
2644
+ const name = dotted(e.expression);
2645
+ if (!name) continue;
2646
+ out.push({ name, args: e.arguments.map((a) => piece(a)) });
2647
+ } else {
2648
+ const name = dotted(e);
2649
+ if (name) out.push({ name });
2650
+ }
2651
+ }
2652
+ return out;
2653
+ };
2654
+ const modifiers = (n) => (ts.getModifiers ? ts.getModifiers(n) : n.modifiers) || [];
2655
+ const has = (n, k) => modifiers(n).some((m2) => m2.kind === k);
2656
+ const isExported = (n) => has(n, ts.SyntaxKind.ExportKeyword);
2657
+ const isPrivate = (n) => has(n, ts.SyntaxKind.PrivateKeyword) || has(n, ts.SyntaxKind.ProtectedKeyword);
2658
+
2659
+ const push = (node, name, kind, extra) => {
2660
+ if (!name) return;
2661
+ defs.push({
2662
+ name,
2663
+ qname: chain.length && kind === "method" ? `${chain[chain.length - 1]}.${name}` : name,
2664
+ kind,
2665
+ s: lineAt(node.getStart(sf)),
2666
+ e: lineAt(node.getEnd()),
2667
+ ...extra,
2668
+ });
2669
+ };
2670
+
2671
+ const visit = (node) => {
2672
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
2673
+ const bindings = [];
2674
+ const cl = node.importClause;
2675
+ if (cl) {
2676
+ if (cl.name) bindings.push({ local: cl.name.text, orig: "default", kind: "default" });
2677
+ const nb = cl.namedBindings;
2678
+ if (nb && ts.isNamespaceImport(nb)) bindings.push({ local: nb.name.text, orig: "*", kind: "namespace" });
2679
+ else if (nb && ts.isNamedImports(nb)) for (const el of nb.elements) bindings.push({ local: el.name.text, orig: (el.propertyName || el.name).text, kind: "named" });
2680
+ }
2681
+ imports.push({ from: node.moduleSpecifier.text, line: lineAt(node.getStart(sf)), bindings });
2682
+ }
2683
+ if (ts.isExportDeclaration(node) && !node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
2684
+ for (const el of node.exportClause.elements) exportsSet.add(el.name.text);
2685
+ }
2686
+ if (ts.isExportAssignment(node)) {
2687
+ const d = dotted(node.expression);
2688
+ if (d) exportsSet.add(d);
2689
+ }
2690
+ if (ts.isClassDeclaration(node) && node.name) {
2691
+ const bases = [];
2692
+ for (const h of node.heritageClauses || []) for (const t of h.types) {
2693
+ const d = dotted(t.expression);
2694
+ if (d) bases.push(d.split(".").pop());
2695
+ }
2696
+ const decos = decosOf(node);
2697
+ push(node, node.name.text, "class", {
2698
+ exported: isExported(node),
2699
+ ...(bases.length ? { bases: bases.slice(0, 8) } : {}),
2700
+ ...(decos.length ? { decos } : {}),
2701
+ });
2702
+ chain.push(node.name.text);
2703
+ ts.forEachChild(node, visit);
2704
+ chain.pop();
2705
+ return;
2706
+ }
2707
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) {
2708
+ push(node, node.name.text, "function", { exported: isExported(node) });
2709
+ }
2710
+ if (ts.isConstructorDeclaration(node) && node.body && chain.length) {
2711
+ push(node, "constructor", "method", { exported: true });
2712
+ }
2713
+ if ((ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) && node.body && node.name && chain.length) {
2714
+ const name = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : ts.isPrivateIdentifier(node.name) ? node.name.text.replace(/^#/, "") : null;
2715
+ const decos = decosOf(node);
2716
+ push(node, name, "method", { exported: !isPrivate(node), ...(decos.length ? { decos } : {}) });
2717
+ }
2718
+ // `handle = (req, res) => {}` on a class is a method; at module level it is
2719
+ // a function. An OVERLOAD signature and an `abstract` member have no body
2720
+ // and are not symbols — recording them made every `ctx` on them ambiguous.
2721
+ if (ts.isPropertyDeclaration(node) && node.initializer && node.name && chain.length) {
2722
+ const init = unwrap(node.initializer);
2723
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
2724
+ const name = ts.isIdentifier(node.name) ? node.name.text : null;
2725
+ const decos = decosOf(node);
2726
+ push(node, name, "method", { exported: !isPrivate(node), ...(decos.length ? { decos } : {}) });
2727
+ }
2728
+ }
2729
+ if (ts.isVariableStatement(node)) {
2730
+ for (const d of node.declarationList.declarations) {
2731
+ const init = d.initializer && unwrap(d.initializer);
2732
+ if (!init) continue;
2733
+ if (!chain.length && ts.isIdentifier(d.name) && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) push(d, d.name.text, "function", { exported: isExported(node) });
2734
+ // `const x = require("m")` · `const { a } = require("m")` ·
2735
+ // `const x = require("m").Y`
2736
+ let call = init;
2737
+ let prop = null;
2738
+ if (ts.isPropertyAccessExpression(call)) {
2739
+ prop = call.name.text;
2740
+ call = unwrap(call.expression);
2741
+ }
2742
+ if (!ts.isCallExpression(call)) continue;
2743
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") continue;
2744
+ const spec = call.arguments[0] && ts.isStringLiteralLike(call.arguments[0]) ? call.arguments[0].text : null;
2745
+ if (!spec) continue;
2746
+ const bindings = [];
2747
+ if (ts.isObjectBindingPattern(d.name)) {
2748
+ for (const el of d.name.elements) if (ts.isIdentifier(el.name)) bindings.push({ local: el.name.text, orig: (el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text), kind: "named" });
2749
+ } else if (ts.isIdentifier(d.name)) {
2750
+ if (prop) bindings.push({ local: d.name.text, orig: prop, kind: "named" });
2751
+ else bindings.push({ local: d.name.text, orig: "default", kind: "namespace" });
2752
+ }
2753
+ if (bindings.length) imports.push({ from: spec, line: lineAt(d.getStart(sf)), bindings });
2754
+ }
2755
+ }
2756
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && !skip.has(node)) {
2757
+ const name = dotted(node.expression);
2758
+ // The line of the CALLEE's own name, not of the whole expression: in a
2759
+ // chain broken over lines (`request(app)\n .post("/orders")`) the call
2760
+ // a reader points at is `.post`, and that is the line the heuristic
2761
+ // reports and the line a card has to print.
2762
+ const callee = unwrap(node.expression);
2763
+ const at = callee && ts.isPropertyAccessExpression(callee) && callee.name ? callee.name : callee || node;
2764
+ const line = lineAt(at.getStart(sf));
2765
+ if (name) {
2766
+ // The RAW dotted name: `extractOne` normalises every borrowed rung's
2767
+ // calls in ONE place, and normalising twice loses `self`
2768
+ // (`this.ok` would already be `ok` by the time it got there).
2769
+ const rec = { name, line };
2770
+ if (ARGS_WORTH.test(name)) {
2771
+ rec.args = (node.arguments || []).map((a) => piece(a));
2772
+ rec.end = lineAt(node.getEnd());
2773
+ const par = node.parent;
2774
+ if (par && ts.isVariableDeclaration(par) && ts.isIdentifier(par.name)) rec.assign = par.name.text;
2775
+ else if (par && ts.isBinaryExpression(par) && par.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
2776
+ const t = dotted(par.left);
2777
+ if (t) rec.assign = t;
2778
+ }
2779
+ }
2780
+ calls.push(rec);
2781
+ }
2782
+ // A name passed AS an argument (`app.get("/p", handler)`) is a ref: an
2783
+ // edge only when this file defines or imports it (`finalize` decides).
2784
+ for (const a of node.arguments || []) {
2785
+ const r = dotted(a);
2786
+ if (r) calls.push({ name: r, line: lineAt(a.getStart(sf)), ref: true });
2787
+ }
2788
+ }
2789
+ ts.forEachChild(node, visit);
2790
+ };
2791
+ ts.forEachChild(sf, visit);
2792
+ // `module.exports = …` and `exports.x = …` are CommonJS, and the regex half
2793
+ // of the heuristic already reads them out of the source; the walker adds the
2794
+ // ES forms above. Both end up in the same set.
2795
+ for (const e of it.src.matchAll(/(?:^|[\s;])(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=/g)) exportsSet.add(e[1]);
2796
+ return { defs, calls, imports, exportsSet: [...exportsSet] };
2797
+ }
2798
+
2799
+ function runTsWalk(items, root) {
2800
+ const probe = findTypeScript(root);
2801
+ if (!probe) return null;
2802
+ const map = {};
2803
+ let ok = 0;
2804
+ for (const it of items) {
2805
+ try {
2806
+ map[it.rel] = tsWalk(probe.ts, it);
2807
+ ok++;
2808
+ } catch (_) {
2809
+ // Per FILE, exactly like the Python rung: one file the parser cannot
2810
+ // read falls back to the heuristic, and the rest of the batch is exact.
2811
+ map[it.rel] = null;
2812
+ }
2813
+ }
2814
+ return ok ? { map, extractor: `typescript@${probe.ver}` } : null;
2815
+ }
2816
+
2817
+ // ── the borrowed Go parser (v1.8.2 W6, G7) ──────────────────────────────────
2818
+ // Go ships its own parser in its standard library, so a repository that has a
2819
+ // Go toolchain can have an EXACT parse for free. The program below is written
2820
+ // once per CLI process to the OS temp directory and run with `go run` over the
2821
+ // whole batch — JSON in on stdin, JSON out on stdout, the same contract the
2822
+ // Python rung uses. No module, no network: it imports only `go/parser`,
2823
+ // `go/ast`, `go/token`, `encoding/json` and `os`.
2824
+ const GO_AST = String.raw`package main
2825
+
2826
+ import (
2827
+ "encoding/json"
2828
+ "go/ast"
2829
+ "go/parser"
2830
+ "go/token"
2831
+ "os"
2832
+ "strconv"
2833
+ "strings"
2834
+ )
2835
+
2836
+ type Piece struct {
2837
+ S *string ` + "`json:\"s,omitempty\"`" + `
2838
+ ID string ` + "`json:\"id,omitempty\"`" + `
2839
+ List []string ` + "`json:\"list,omitempty\"`" + `
2840
+ Cat string ` + "`json:\"cat,omitempty\"`" + `
2841
+ }
2842
+
2843
+ type Call struct {
2844
+ Name string ` + "`json:\"name\"`" + `
2845
+ Line int ` + "`json:\"line\"`" + `
2846
+ End int ` + "`json:\"end,omitempty\"`" + `
2847
+ Ref bool ` + "`json:\"ref,omitempty\"`" + `
2848
+ Assign string ` + "`json:\"assign,omitempty\"`" + `
2849
+ Args []Piece ` + "`json:\"args,omitempty\"`" + `
2850
+ }
2851
+
2852
+ type Def struct {
2853
+ Name string ` + "`json:\"name\"`" + `
2854
+ QName string ` + "`json:\"qname\"`" + `
2855
+ Kind string ` + "`json:\"kind\"`" + `
2856
+ S int ` + "`json:\"s\"`" + `
2857
+ E int ` + "`json:\"e\"`" + `
2858
+ Exported bool ` + "`json:\"exported\"`" + `
2859
+ Bases []string ` + "`json:\"bases,omitempty\"`" + `
2860
+ }
2861
+
2862
+ type Imp struct {
2863
+ From string ` + "`json:\"from\"`" + `
2864
+ Line int ` + "`json:\"line\"`" + `
2865
+ Bindings []Binding ` + "`json:\"bindings\"`" + `
2866
+ }
2867
+
2868
+ type Binding struct {
2869
+ Local string ` + "`json:\"local\"`" + `
2870
+ Orig string ` + "`json:\"orig\"`" + `
2871
+ Kind string ` + "`json:\"kind\"`" + `
2872
+ }
2873
+
2874
+ type Out struct {
2875
+ Defs []Def ` + "`json:\"defs\"`" + `
2876
+ Calls []Call ` + "`json:\"calls\"`" + `
2877
+ Imports []Imp ` + "`json:\"imports\"`" + `
2878
+ }
2879
+
2880
+ type Item struct {
2881
+ Rel string ` + "`json:\"rel\"`" + `
2882
+ Abs string ` + "`json:\"abs\"`" + `
2883
+ }
2884
+
2885
+ var worth = map[string]bool{
2886
+ "Get": true, "Post": true, "Put": true, "Patch": true, "Delete": true, "Head": true, "Options": true,
2887
+ "HandleFunc": true, "Handle": true, "GET": true, "POST": true, "PUT": true, "PATCH": true, "DELETE": true,
2888
+ "HEAD": true, "OPTIONS": true, "Any": true, "Mount": true, "Group": true, "NewRequest": true,
2889
+ "NewRequestWithContext": true, "Query": true, "Exec": true, "ExecContext": true, "QueryContext": true,
2890
+ "QueryRow": true, "QueryRowContext": true, "Getenv": true, "LookupEnv": true, "ReadFile": true,
2891
+ "WriteFile": true, "Open": true, "Create": true,
2892
+ }
2893
+
2894
+ func dotted(e ast.Expr) string {
2895
+ switch n := e.(type) {
2896
+ case *ast.Ident:
2897
+ return n.Name
2898
+ case *ast.SelectorExpr:
2899
+ h := dotted(n.X)
2900
+ if h == "" {
2901
+ return ""
2902
+ }
2903
+ return h + "." + n.Sel.Name
2904
+ case *ast.ParenExpr:
2905
+ return dotted(n.X)
2906
+ case *ast.StarExpr:
2907
+ return dotted(n.X)
2908
+ case *ast.IndexExpr:
2909
+ return dotted(n.X)
2910
+ case *ast.CallExpr:
2911
+ h := dotted(n.Fun)
2912
+ if h == "" {
2913
+ return ""
2914
+ }
2915
+ last := h[strings.LastIndex(h, ".")+1:]
2916
+ if last != "" && strings.ToUpper(last[:1]) == last[:1] {
2917
+ return h
2918
+ }
2919
+ return ""
2920
+ }
2921
+ return ""
2922
+ }
2923
+
2924
+ func strOf(e ast.Expr) (string, bool) {
2925
+ switch n := e.(type) {
2926
+ case *ast.BasicLit:
2927
+ if n.Kind == token.STRING {
2928
+ v, err := strconv.Unquote(n.Value)
2929
+ if err == nil {
2930
+ return v, true
2931
+ }
2932
+ }
2933
+ case *ast.BinaryExpr:
2934
+ if n.Op == token.ADD {
2935
+ if r, ok := strOf(n.Y); ok {
2936
+ return r, false
2937
+ }
2938
+ }
2939
+ }
2940
+ return "", false
2941
+ }
2942
+
2943
+ func pieceOf(e ast.Expr) Piece {
2944
+ p := Piece{}
2945
+ if s, whole := strOf(e); s != "" {
2946
+ if whole {
2947
+ v := s
2948
+ p.S = &v
2949
+ } else {
2950
+ p.Cat = s
2951
+ }
2952
+ return p
2953
+ }
2954
+ if cl, ok := e.(*ast.CompositeLit); ok {
2955
+ for _, el := range cl.Elts {
2956
+ if s, whole := strOf(el); whole {
2957
+ p.List = append(p.List, s)
2958
+ }
2959
+ }
2960
+ if len(p.List) > 0 {
2961
+ return p
2962
+ }
2963
+ }
2964
+ if id := dotted(e); id != "" {
2965
+ p.ID = id
2966
+ }
2967
+ return p
2968
+ }
2969
+
2970
+ func main() {
2971
+ var items []Item
2972
+ dec := json.NewDecoder(os.Stdin)
2973
+ if err := dec.Decode(&items); err != nil {
2974
+ os.Exit(1)
2975
+ }
2976
+ out := map[string]*Out{}
2977
+ for _, it := range items {
2978
+ fset := token.NewFileSet()
2979
+ f, err := parser.ParseFile(fset, it.Abs, nil, parser.SkipObjectResolution)
2980
+ if err != nil || f == nil {
2981
+ out[it.Rel] = nil
2982
+ continue
2983
+ }
2984
+ line := func(p token.Pos) int { return fset.Position(p).Line }
2985
+ o := &Out{Defs: []Def{}, Calls: []Call{}, Imports: []Imp{}}
2986
+ for _, im := range f.Imports {
2987
+ pth, err := strconv.Unquote(im.Path.Value)
2988
+ if err != nil {
2989
+ continue
2990
+ }
2991
+ local := pth[strings.LastIndex(pth, "/")+1:]
2992
+ alias := ""
2993
+ if im.Name != nil {
2994
+ alias = im.Name.Name
2995
+ }
2996
+ b := []Binding{}
2997
+ if alias != "_" && alias != "." {
2998
+ if alias != "" {
2999
+ local = alias
3000
+ }
3001
+ b = append(b, Binding{Local: local, Orig: local, Kind: "namespace"})
3002
+ }
3003
+ o.Imports = append(o.Imports, Imp{From: pth, Line: line(im.Pos()), Bindings: b})
3004
+ }
3005
+ for _, d := range f.Decls {
3006
+ switch n := d.(type) {
3007
+ case *ast.GenDecl:
3008
+ if n.Tok != token.TYPE {
3009
+ continue
3010
+ }
3011
+ for _, sp := range n.Specs {
3012
+ ts, ok := sp.(*ast.TypeSpec)
3013
+ if !ok {
3014
+ continue
3015
+ }
3016
+ def := Def{Name: ts.Name.Name, QName: ts.Name.Name, Kind: "class", S: line(ts.Pos()), E: line(ts.End()), Exported: ts.Name.IsExported()}
3017
+ // An EMBEDDED struct field is Go's inheritance: the outer
3018
+ // type answers the embedded type's methods.
3019
+ if st, ok := ts.Type.(*ast.StructType); ok && st.Fields != nil {
3020
+ for _, fl := range st.Fields.List {
3021
+ if len(fl.Names) == 0 {
3022
+ if b := dotted(fl.Type); b != "" {
3023
+ def.Bases = append(def.Bases, b[strings.LastIndex(b, ".")+1:])
3024
+ }
3025
+ }
3026
+ }
3027
+ }
3028
+ if it, ok := ts.Type.(*ast.InterfaceType); ok && it.Methods != nil {
3029
+ for _, fl := range it.Methods.List {
3030
+ if len(fl.Names) == 0 {
3031
+ if b := dotted(fl.Type); b != "" {
3032
+ def.Bases = append(def.Bases, b[strings.LastIndex(b, ".")+1:])
3033
+ }
3034
+ }
3035
+ }
3036
+ }
3037
+ o.Defs = append(o.Defs, def)
3038
+ }
3039
+ case *ast.FuncDecl:
3040
+ if n.Body == nil {
3041
+ continue
3042
+ }
3043
+ recv := ""
3044
+ if n.Recv != nil && len(n.Recv.List) > 0 {
3045
+ recv = dotted(n.Recv.List[0].Type)
3046
+ recv = recv[strings.LastIndex(recv, ".")+1:]
3047
+ }
3048
+ kind := "function"
3049
+ q := n.Name.Name
3050
+ if recv != "" {
3051
+ kind = "method"
3052
+ q = recv + "." + n.Name.Name
3053
+ }
3054
+ o.Defs = append(o.Defs, Def{Name: n.Name.Name, QName: q, Kind: kind, S: line(n.Pos()), E: line(n.End()), Exported: n.Name.IsExported()})
3055
+ }
3056
+ }
3057
+ assign := map[ast.Expr]string{}
3058
+ ast.Inspect(f, func(nd ast.Node) bool {
3059
+ if a, ok := nd.(*ast.AssignStmt); ok && len(a.Lhs) == 1 && len(a.Rhs) == 1 {
3060
+ if t := dotted(a.Lhs[0]); t != "" {
3061
+ assign[a.Rhs[0]] = t
3062
+ }
3063
+ }
3064
+ return true
3065
+ })
3066
+ ast.Inspect(f, func(nd ast.Node) bool {
3067
+ ce, ok := nd.(*ast.CallExpr)
3068
+ if !ok {
3069
+ return true
3070
+ }
3071
+ name := dotted(ce.Fun)
3072
+ if name != "" {
3073
+ c := Call{Name: name, Line: line(ce.Pos())}
3074
+ last := name[strings.LastIndex(name, ".")+1:]
3075
+ if worth[last] {
3076
+ c.End = line(ce.End())
3077
+ for _, a := range ce.Args {
3078
+ c.Args = append(c.Args, pieceOf(a))
3079
+ }
3080
+ if t, ok := assign[ce]; ok {
3081
+ c.Assign = t
3082
+ }
3083
+ }
3084
+ o.Calls = append(o.Calls, c)
3085
+ }
3086
+ for _, a := range ce.Args {
3087
+ if r := dotted(a); r != "" {
3088
+ o.Calls = append(o.Calls, Call{Name: r, Line: line(a.Pos()), Ref: true})
3089
+ }
3090
+ }
3091
+ return true
3092
+ })
3093
+ out[it.Rel] = o
3094
+ }
3095
+ enc := json.NewEncoder(os.Stdout)
3096
+ if err := enc.Encode(out); err != nil {
3097
+ os.Exit(1)
3098
+ }
3099
+ }
3100
+ `;
3101
+
3102
+ let goProbe;
3103
+ function findGo() {
3104
+ if (goProbe !== undefined) return goProbe;
3105
+ goProbe = null;
3106
+ const cmd = process.env.ORC_GRAPH_GO || "go";
3107
+ try {
3108
+ const r = spawnSync(cmd, ["version"], { encoding: "utf8", timeout: 15000, windowsHide: true });
3109
+ if (r.status !== 0 || !/go\d|go version/.test(r.stdout || "")) return null;
3110
+ const ver = /go(\d+\.\d+(?:\.\d+)?)/.exec(r.stdout || "");
3111
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "orc-graph-go-"));
3112
+ fs.writeFileSync(path.join(dir, "main.go"), GO_AST);
3113
+ fs.writeFileSync(path.join(dir, "go.mod"), "module orcgraph\n\ngo 1.20\n");
3114
+ goProbe = { cmd, ver: ver ? ver[1] : "?", dir };
3115
+ } catch (_) {
3116
+ goProbe = null;
3117
+ }
3118
+ return goProbe;
3119
+ }
3120
+
3121
+ function runGoAst(items, root) {
3122
+ void root;
3123
+ const go = findGo();
3124
+ if (!go) return null;
3125
+ const r = spawnSync(go.cmd, ["run", "."], {
3126
+ cwd: go.dir,
3127
+ input: JSON.stringify(items.map((i) => ({ rel: i.rel, abs: i.abs }))),
3128
+ encoding: "utf8",
3129
+ maxBuffer: 512 * 1024 * 1024,
3130
+ timeout: 600000,
3131
+ windowsHide: true,
3132
+ env: { ...process.env, GOFLAGS: "-mod=mod", GO111MODULE: "on" },
3133
+ });
3134
+ if (r.status !== 0 || !r.stdout) return null;
3135
+ try {
3136
+ return { map: JSON.parse(r.stdout), extractor: `go-ast@${go.ver}` };
3137
+ } catch (_) {
3138
+ return null;
3139
+ }
3140
+ }
3141
+
3142
+ function findPython() {
3143
+ if (pythonProbe !== undefined) return pythonProbe;
3144
+ pythonProbe = null;
3145
+ const cands = [];
3146
+ if (process.env.ORC_GRAPH_PYTHON) cands.push([process.env.ORC_GRAPH_PYTHON]);
3147
+ cands.push(["python3"], ["python"], ["py", "-3"]);
3148
+ for (const [cmd, ...pre] of cands) {
3149
+ const r = spawnSync(cmd, [...pre, "-c", "import sys;print('%d.%d' % sys.version_info[:2]);print(sys.version_info >= (3, 8))"], {
3150
+ encoding: "utf8",
3151
+ timeout: 10000,
3152
+ windowsHide: true,
3153
+ });
3154
+ if (r.status === 0 && /True/.test(r.stdout || "")) {
3155
+ pythonProbe = { cmd, pre, ver: (r.stdout || "").split(/\r?\n/)[0].trim() };
3156
+ break;
3157
+ }
3158
+ }
3159
+ return pythonProbe;
3160
+ }
3161
+
3162
+ function runPythonAst(items) {
3163
+ const py = findPython();
3164
+ if (!py) return null;
3165
+ const r = spawnSync(py.cmd, [...py.pre, "-c", PY_AST], {
3166
+ input: JSON.stringify(items.map((i) => ({ rel: i.rel, abs: i.abs }))),
3167
+ encoding: "utf8",
3168
+ maxBuffer: 512 * 1024 * 1024,
3169
+ timeout: 300000,
3170
+ windowsHide: true,
3171
+ });
3172
+ if (r.status !== 0 || !r.stdout) return null;
3173
+ try {
3174
+ return { map: JSON.parse(r.stdout), extractor: `python-ast@${py.ver}` };
3175
+ } catch (_) {
3176
+ return null;
3177
+ }
3178
+ }
3179
+
3180
+ // items: [{ rel, abs, lang, src }] → Map<rel, { extractor, imports, symbols }>
3181
+ function extractBatch(items, opts) {
3182
+ const out = new Map();
3183
+ const root = (opts && opts.root) || null;
3184
+ const borrow = !process.env.ORC_GRAPH_NO_BORROW;
3185
+ const pys = items.filter((i) => i.lang === "py");
3186
+ const ast = pys.length && borrow ? runPythonAst(pys) : null;
3187
+ // W6 (G7): TypeScript and JavaScript, when the PROJECT has the compiler.
3188
+ // The `<script>` block of an SFC is handled by the heuristic rung — the
3189
+ // walker would need the blanked source, and the gain is in the .ts tree.
3190
+ const jts = items.filter((i) => i.lang === "ts" || i.lang === "js");
3191
+ const tsw = jts.length && borrow ? runTsWalk(jts, root) : null;
3192
+ const gos = items.filter((i) => i.lang === "go");
3193
+ const goa = gos.length && borrow ? runGoAst(gos, root) : null;
3194
+ // Which rung answered for this file, so the record's `extractor` names the
3195
+ // tool that actually read it — a claim of `typescript@5.4.2` on a file the
3196
+ // walker choked on would be a lie the card repeats.
3197
+ const rungOf = (rel) => {
3198
+ if (ast && ast.map[rel]) return [ast.map[rel], ast.extractor];
3199
+ if (tsw && tsw.map[rel]) return [tsw.map[rel], tsw.extractor];
3200
+ if (goa && goa.map[rel]) return [goa.map[rel], goa.extractor];
3201
+ return [null, null];
3202
+ };
3203
+ for (const it of items) {
3204
+ const [b, tag] = rungOf(it.rel);
3205
+ try {
3206
+ out.set(it.rel, extractOne(it, b ? { ...b, extractor: tag } : null));
3207
+ } catch (_) {
3208
+ // A file the extractor cannot read is an empty record, never a failed
3209
+ // update: one pathological file must not take the whole map down.
3210
+ out.set(it.rel, { extractor: HEURISTIC, error: "extract-failed", imports: [], symbols: [] });
3211
+ }
3212
+ }
3213
+ return out;
3214
+ }
3215
+
3216
+ module.exports = { HEURISTIC, MAX_CALLS_PER_SYMBOL, MAX_EFFECTS_PER_SYMBOL, extractBatch, extractOne, mask, normCall, urlText, joinPath, _findPython: findPython, _findTypeScript: findTypeScript, _findGo: findGo };