@azure-id/orc 1.7.0 → 1.8.1

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 +3649 -3339
  2. package/README-id.md +923 -844
  3. package/README.md +836 -860
  4. package/bin/build-agents.js +43 -27
  5. package/bin/cli.js +701 -3
  6. package/bin/graph-extract.js +927 -0
  7. package/bin/graph-notes.js +188 -0
  8. package/bin/graph-query.js +808 -0
  9. package/bin/graph-resolve.js +178 -0
  10. package/bin/graph-signals.js +277 -0
  11. package/bin/graph.js +605 -0
  12. package/bin/verify-contracts.js +4669 -4527
  13. package/bin/verify-package.js +626 -616
  14. package/bin/webui/api.js +1419 -1414
  15. package/bin/webui/fixtures/index.js +579 -576
  16. package/bin/webui/fixtures/knowledge.js +316 -291
  17. package/bin/webui/i18n/en/knowledge.json +167 -151
  18. package/bin/webui/i18n/en/overview.json +101 -100
  19. package/bin/webui/i18n/id/knowledge.json +167 -151
  20. package/bin/webui/i18n/id/overview.json +101 -100
  21. package/bin/webui/js/panels/knowledge.js +1065 -1006
  22. package/bin/webui/js/panels/overview.js +492 -488
  23. package/package.json +39 -39
  24. package/templates/agents/MODEL-MAPPING.md +163 -158
  25. package/templates/agents/orc-executor-haiku-4-5.md +133 -113
  26. package/templates/agents/orc-executor-opus-4-7-high.md +134 -114
  27. package/templates/agents/orc-executor-opus-4-7-med.md +134 -114
  28. package/templates/agents/orc-executor-opus-4-8-high.md +134 -114
  29. package/templates/agents/orc-executor-opus-5-high.md +134 -114
  30. package/templates/agents/orc-executor-opus-5-low.md +134 -114
  31. package/templates/agents/orc-executor-opus-5-med.md +134 -114
  32. package/templates/agents/orc-executor-sonnet-4-6-high.md +134 -114
  33. package/templates/agents/orc-executor-sonnet-4-6-med.md +134 -114
  34. package/templates/agents/orc-executor-sonnet-5-high.md +134 -114
  35. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +86 -0
  36. package/templates/hooks/README.md +444 -396
  37. package/templates/hooks/orc-graph-hook.js +336 -0
  38. package/templates/hooks/orc-statusline-render.js +922 -921
  39. package/templates/hooks/orc-statusline.js +1596 -1545
  40. package/templates/skills/_shared/README.md +4 -0
  41. package/templates/skills/_shared/code-graph.md +220 -0
  42. package/templates/skills/_shared/extra-dispatch.md +1 -1
  43. package/templates/skills/_shared/opus5-only.md +4 -0
  44. package/templates/skills/_shared/phases/execution.md +166 -145
  45. package/templates/skills/_shared/phases/planning.md +142 -135
  46. package/templates/skills/_shared/phases/preflight.md +132 -118
  47. package/templates/skills/_shared/phases/review.md +63 -53
  48. package/templates/skills/_shared/phases/rules.md +5 -0
  49. package/templates/skills/_shared/phases/ship.md +96 -88
  50. package/templates/skills/_shared/phases/trace.md +6 -0
  51. package/templates/skills/_shared/phases/wiki-consult.md +194 -189
  52. package/templates/skills/_shared/read-ladder.md +124 -102
  53. package/templates/skills/_shared/return-validation.md +259 -250
  54. package/templates/skills/orc/SKILL.md +255 -254
  55. package/templates/skills/orc/references/preflight-report.md +6 -0
  56. package/templates/skills/orc/subskills/orc-execution/SKILL.md +2 -1
  57. package/templates/skills/orc/subskills/orc-execution/core.md +9 -1
  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 -360
  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,927 @@
1
+ "use strict";
2
+ // ── orc graph — EXTRACTION (v1.8.0 W2) ─────────────────────────────────────
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
+ const crypto = require("crypto");
30
+ const { spawnSync } = require("child_process");
31
+
32
+ // @3 (W9): route handlers are symbols, and a function passed BY NAME is a `ref`.
33
+ // @4 (EW1): every record says how much of the file the extractor actually saw.
34
+ const HEURISTIC = "heuristic@4";
35
+ const MAX_CALLS_PER_SYMBOL = 200;
36
+ const MAX_EFFECTS_PER_SYMBOL = 12;
37
+
38
+ const KW = new Set(
39
+ (
40
+ "if for while switch catch function return typeof sizeof def class elif with not and or in " +
41
+ "await yield case new throw else do try foreach using lock fixed func go defer select range " +
42
+ "match fn when elseif isset empty unset array list print echo import export from lambda assert " +
43
+ "del pass raise except finally is this self super base var let const delete instanceof " +
44
+ "require_once include_once require include die exit"
45
+ ).split(" ")
46
+ );
47
+
48
+ // ── text utilities ──────────────────────────────────────────────────────────
49
+ function lineIndex(src) {
50
+ const starts = [0];
51
+ for (let i = 0; i < src.length; i++) if (src.charCodeAt(i) === 10) starts.push(i + 1);
52
+ return starts;
53
+ }
54
+
55
+ function lineOf(starts, pos) {
56
+ let lo = 0;
57
+ let hi = starts.length - 1;
58
+ while (lo < hi) {
59
+ const mid = (lo + hi + 1) >> 1;
60
+ if (starts[mid] <= pos) lo = mid;
61
+ else hi = mid - 1;
62
+ }
63
+ return lo + 1;
64
+ }
65
+
66
+ const clip = (s, n = 80) => {
67
+ const t = String(s).replace(/\s+/g, " ").trim();
68
+ return t.length > n ? t.slice(0, n - 1) + "…" : t;
69
+ };
70
+
71
+ const hash = (s) => crypto.createHash("sha1").update(String(s).replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
72
+
73
+ // Replace comment and string CONTENT with spaces, keeping every newline and
74
+ // every offset. Declarations and calls are found in the mask; string text (for
75
+ // SQL and URLs) is read back from the original at the recorded spans.
76
+ function mask(src, lang) {
77
+ const slash = lang !== "py";
78
+ const hashC = lang === "py" || lang === "php";
79
+ const block = lang !== "py";
80
+ const backtick = lang === "js" || lang === "ts" || lang === "go";
81
+ const m = src.split("");
82
+ const n = src.length;
83
+ const strings = [];
84
+ const blank = (a, b) => {
85
+ for (let k = a; k < b && k < n; k++) if (m[k] !== "\n" && m[k] !== "\r") m[k] = " ";
86
+ };
87
+ let i = 0;
88
+ while (i < n) {
89
+ const c = src[i];
90
+ const c2 = src[i + 1];
91
+ if ((slash && c === "/" && c2 === "/") || (hashC && c === "#" && !(lang === "php" && c2 === "["))) {
92
+ const e = src.indexOf("\n", i);
93
+ const end = e < 0 ? n : e;
94
+ blank(i, end);
95
+ i = end;
96
+ continue;
97
+ }
98
+ if (block && c === "/" && c2 === "*") {
99
+ const e = src.indexOf("*/", i + 2);
100
+ const end = e < 0 ? n : e + 2;
101
+ blank(i, end);
102
+ i = end;
103
+ continue;
104
+ }
105
+ if (lang === "py" && (src.startsWith('"""', i) || src.startsWith("'''", i))) {
106
+ const q = src.substr(i, 3);
107
+ const e = src.indexOf(q, i + 3);
108
+ const end = e < 0 ? n : e;
109
+ strings.push({ start: i + 3, end });
110
+ blank(i + 3, end);
111
+ i = e < 0 ? n : e + 3;
112
+ continue;
113
+ }
114
+ if (c === '"' || c === "'" || (backtick && c === "`")) {
115
+ const verbatim = lang === "cs" && src[i - 1] === "@";
116
+ const raw = c === "`" && lang === "go";
117
+ let j = i + 1;
118
+ while (j < n) {
119
+ const d = src[j];
120
+ if (d === "\\" && !verbatim && !raw) {
121
+ j += 2;
122
+ continue;
123
+ }
124
+ if (d === c) {
125
+ if (verbatim && src[j + 1] === '"') {
126
+ j += 2;
127
+ continue;
128
+ }
129
+ break;
130
+ }
131
+ // A quote that never closes on its line is not a string (a regex
132
+ // literal, an apostrophe in a template): stop at the line end so one
133
+ // bad guess costs one line, not the rest of the file.
134
+ if (d === "\n" && c !== "`" && !verbatim) break;
135
+ j++;
136
+ }
137
+ strings.push({ start: i + 1, end: Math.min(j, n) });
138
+ blank(i + 1, j);
139
+ i = j + 1;
140
+ continue;
141
+ }
142
+ i++;
143
+ }
144
+ return { masked: m.join(""), strings };
145
+ }
146
+
147
+ // COVERAGE (EW1). A heuristic extractor can lose the thread: an opening brace
148
+ // with no partner makes a declaration run to the end of the file, and a symbol
149
+ // that hits a cap keeps only part of what it found. Neither is an error — the
150
+ // record is still useful — but the reader must be told which lines are only
151
+ // partly seen, or a card that shows nothing reads as "there is nothing".
152
+ // One extraction runs at a time, so a module-level note is enough.
153
+ let UNBALANCED = -1;
154
+ function resetCoverage() {
155
+ UNBALANCED = -1;
156
+ }
157
+
158
+ function matchPair(s, open, a, b) {
159
+ let d = 0;
160
+ for (let k = open; k < s.length; k++) {
161
+ const ch = s[k];
162
+ if (ch === a) d++;
163
+ else if (ch === b) {
164
+ d--;
165
+ if (d === 0) return k;
166
+ }
167
+ }
168
+ // Only a BLOCK that really never closes is a coverage gap. Two other callers
169
+ // land here and neither is one: a parameter list scanned speculatively, and a
170
+ // probe whose start is not the opener at all (`s[open] !== a`), which can
171
+ // never balance. Noting those marked 40% of this repo's own files partial —
172
+ // a signal that fires everywhere says nothing.
173
+ if (a === "{" && s[open] === a && (UNBALANCED < 0 || open < UNBALANCED)) UNBALANCED = open;
174
+ return s.length - 1;
175
+ }
176
+
177
+ // After a parameter list: find the body. `requireArrow` = this is only a
178
+ // function if an `=>` follows (a `const x = (a + b)` is not one).
179
+ function bodyAfter(m, from, requireArrow) {
180
+ const limit = Math.min(m.length, from + 400);
181
+ for (let k = from; k < limit; k++) {
182
+ const ch = m[k];
183
+ if (ch === "=" && m[k + 1] === ">") {
184
+ let j = k + 2;
185
+ while (j < m.length && /\s/.test(m[j])) j++;
186
+ if (m[j] === "{") return { open: j, end: matchPair(m, j, "{", "}") };
187
+ const nl = m.indexOf("\n", j);
188
+ return { open: j, end: nl < 0 ? m.length - 1 : nl - 1 };
189
+ }
190
+ if (ch === "{") return requireArrow ? null : { open: k, end: matchPair(m, k, "{", "}") };
191
+ if (ch === ";") return requireArrow ? null : { open: k, end: k };
192
+ if (ch === "\n" && requireArrow) {
193
+ let j = k + 1;
194
+ while (j < limit && (m[j] === " " || m[j] === "\t" || m[j] === "\r")) j++;
195
+ if (m[j] !== "=" && m[j] !== ":") return null;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+
201
+ // ── declarations: brace languages ───────────────────────────────────────────
202
+ function braceDefs(src, m, starts, lang, strings) {
203
+ const defs = [];
204
+ const defParens = new Set();
205
+ const classes = [];
206
+ const add = (d) => {
207
+ if (!d.name || KW.has(d.name)) return;
208
+ d.s = lineOf(starts, d.from);
209
+ d.e = lineOf(starts, Math.max(d.from, d.to));
210
+ defs.push(d);
211
+ };
212
+ const innermostClass = (pos) => {
213
+ let best = null;
214
+ for (const c of classes) if (c.open < pos && pos < c.to && (!best || c.to - c.open < best.to - best.open)) best = c;
215
+ return best;
216
+ };
217
+ let x;
218
+
219
+ if (lang === "js" || lang === "ts") {
220
+ const CLASS = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:abstract[ \t]+)?class[ \t]+([A-Za-z_$][\w$]*)[^{;]*\{/gm;
221
+ while ((x = CLASS.exec(m))) {
222
+ const open = x.index + x[0].length - 1;
223
+ const to = matchPair(m, open, "{", "}");
224
+ const c = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: !!x[1] };
225
+ classes.push(c);
226
+ add(c);
227
+ }
228
+ const FN = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:async[ \t]+)?function[ \t]*\*?[ \t]*([A-Za-z_$][\w$]*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
229
+ while ((x = FN.exec(m))) {
230
+ const paren = x.index + x[0].length - 1;
231
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
232
+ defParens.add(paren);
233
+ // `function f(a: A): B;` is an overload SIGNATURE — no body, not a symbol.
234
+ if (!body || m[body.open] === ";") continue;
235
+ add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
236
+ }
237
+ 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;
238
+ while ((x = ARROW.exec(m))) {
239
+ const end = x.index + x[0].length;
240
+ let body;
241
+ if (x[3].endsWith("(")) {
242
+ const paren = end - 1;
243
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, !x[3].startsWith("function"));
244
+ if (body) defParens.add(paren);
245
+ } else {
246
+ body = bodyAfter(m, m.lastIndexOf("=>", end), true);
247
+ }
248
+ if (!body) continue;
249
+ add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
250
+ }
251
+ // Route handlers (Express, Koa, Fastify, Hono): `router.get("/path", mw,
252
+ // (req, res) => { … })`. The handler is anonymous, so without this a route
253
+ // file has NO symbol and its card is an import list — W9 measured 6 files,
254
+ // 1 symbol on an Express app. Named `<METHOD> <path>`; the body is the whole
255
+ // registration call, so the middleware passed to it and every call inside
256
+ // the handler belong to the route.
257
+ const ROUTE = /\b[A-Za-z_$][\w$]*[ \t]*\.[ \t]*(get|post|put|patch|delete|del|all|options|head)[ \t]*\(/g;
258
+ while ((x = ROUTE.exec(m))) {
259
+ const paren = x.index + x[0].length - 1;
260
+ let k = paren + 1;
261
+ while (k < m.length && /\s/.test(m[k])) k++;
262
+ const lit = (strings || []).find((t) => t.start === k + 1);
263
+ if (!lit) continue;
264
+ const routePath = src.slice(lit.start, lit.end);
265
+ if (!/^(\/|\*$)/.test(routePath) || /[\r\n]/.test(routePath)) continue;
266
+ const close = matchPair(m, paren, "(", ")");
267
+ // `cache.get("/k")` and `request(app).get("/orders")` pass no handler.
268
+ if (!/=>|\bfunction\b/.test(m.slice(lit.end + 1, close))) continue;
269
+ const name = `${x[1] === "del" ? "DELETE" : x[1].toUpperCase()} ${clip(routePath, 60)}`;
270
+ add({ name, qname: name, kind: "route", from: x.index, to: close, exported: false });
271
+ }
272
+ // Members: scanned inside each class body, skipping every member body it
273
+ // finds, so a statement inside a method is never read as a member.
274
+ 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;
275
+ for (const c of classes) {
276
+ MEMBER.lastIndex = c.open + 1;
277
+ while ((x = MEMBER.exec(m)) && x.index < c.to) {
278
+ const end = x.index + x[0].length;
279
+ let body = null;
280
+ let paren = null;
281
+ if (x[3] === "(") {
282
+ paren = end - 1;
283
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
284
+ } else if (x[3].endsWith("(")) {
285
+ paren = end - 1;
286
+ body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, true);
287
+ } else {
288
+ body = bodyAfter(m, m.lastIndexOf("=>", end), true);
289
+ }
290
+ if (paren != null) defParens.add(paren);
291
+ // A member with no body is an overload signature or an abstract
292
+ // declaration. Recording it made `NestFactoryStatic.create` three
293
+ // symbols on nestjs/nest and every `ctx` on it an ambiguity.
294
+ if (!body || body.end >= c.to || m[body.open] === ";") {
295
+ if (body && body.end < c.to) MEMBER.lastIndex = Math.max(body.end + 1, end);
296
+ continue;
297
+ }
298
+ const name = x[2].replace(/^#/, "");
299
+ if (!KW.has(name)) {
300
+ const priv = /private|protected/.test(x[1]) || x[2].startsWith("#");
301
+ add({ name, qname: `${c.name}.${name}`, kind: "method", from: x.index, to: body.end, exported: c.exported && !priv, member: true });
302
+ }
303
+ MEMBER.lastIndex = Math.max(body.end + 1, end);
304
+ }
305
+ }
306
+ } else if (lang === "go") {
307
+ const TYPE = /^type[ \t]+([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]+(?:struct|interface)[ \t]*\{/gm;
308
+ while ((x = TYPE.exec(m))) {
309
+ const open = x.index + x[0].length - 1;
310
+ add({ name: x[1], qname: x[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), exported: /^[A-Z]/.test(x[1]) });
311
+ }
312
+ const FUNC = /^func[ \t]*(?:\([ \t]*\w*[ \t]*\*?[ \t]*([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]*\)[ \t]*)?([A-Za-z_]\w*)[ \t]*(?:\[[^\]]*\])?[ \t]*\(/gm;
313
+ while ((x = FUNC.exec(m))) {
314
+ const paren = x.index + x[0].length - 1;
315
+ const close = matchPair(m, paren, "(", ")");
316
+ const nextFunc = m.indexOf("\nfunc", close);
317
+ const brace = m.indexOf("{", close);
318
+ if (brace < 0 || (nextFunc >= 0 && brace > nextFunc)) continue;
319
+ defParens.add(paren);
320
+ const recv = x[1];
321
+ add({
322
+ name: x[2],
323
+ qname: recv ? `${recv}.${x[2]}` : x[2],
324
+ kind: recv ? "method" : "function",
325
+ from: x.index,
326
+ to: matchPair(m, brace, "{", "}"),
327
+ exported: /^[A-Z]/.test(x[2]),
328
+ });
329
+ }
330
+ } else if (lang === "java" || lang === "cs") {
331
+ 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;
332
+ while ((x = CLASS.exec(m))) {
333
+ const open = x.index + x[0].length - 1;
334
+ const to = matchPair(m, open, "{", "}");
335
+ const c = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: /public/.test(x[1]) };
336
+ classes.push(c);
337
+ add(c);
338
+ }
339
+ 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;
340
+ while ((x = METHOD.exec(m))) {
341
+ const typeWord = x[2].split(/[<.\[]/)[0];
342
+ if (KW.has(typeWord) || KW.has(x[3])) continue;
343
+ const paren = x.index + x[0].length - 1;
344
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
345
+ if (!body || (m[body.open] === ";" && !/abstract|extern|native/.test(x[1]))) continue;
346
+ defParens.add(paren);
347
+ const owner = innermostClass(x.index);
348
+ add({
349
+ name: x[3],
350
+ qname: owner ? `${owner.name}.${x[3]}` : x[3],
351
+ kind: owner ? "method" : "function",
352
+ from: x.index,
353
+ to: body.end,
354
+ exported: /public/.test(x[1]),
355
+ });
356
+ }
357
+ const CTOR = /^[ \t]*(?:(?:public|private|protected|internal)[ \t]+)([A-Z]\w*)[ \t]*\(/gm;
358
+ while ((x = CTOR.exec(m))) {
359
+ const owner = innermostClass(x.index);
360
+ if (!owner || owner.name !== x[1]) continue;
361
+ const paren = x.index + x[0].length - 1;
362
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
363
+ if (!body) continue;
364
+ defParens.add(paren);
365
+ add({ name: x[1], qname: `${owner.name}.${x[1]}`, kind: "method", from: x.index, to: body.end, exported: /public/.test(x[0]) });
366
+ }
367
+ } else if (lang === "php") {
368
+ const CLASS = /^[ \t]*(?:(?:abstract|final|readonly)[ \t]+)*(?:class|interface|trait|enum)[ \t]+([A-Za-z_]\w*)[^{;]*\{/gm;
369
+ while ((x = CLASS.exec(m))) {
370
+ const open = x.index + x[0].length - 1;
371
+ const to = matchPair(m, open, "{", "}");
372
+ const c = { name: x[1], qname: x[1], kind: "class", from: x.index, to, open, exported: true };
373
+ classes.push(c);
374
+ add(c);
375
+ }
376
+ const FUNC = /^[ \t]*((?:(?:public|private|protected|static|abstract|final)[ \t]+)*)function[ \t]+&?([A-Za-z_]\w*)[ \t]*\(/gm;
377
+ while ((x = FUNC.exec(m))) {
378
+ const paren = x.index + x[0].length - 1;
379
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
380
+ if (!body) continue;
381
+ defParens.add(paren);
382
+ const owner = innermostClass(x.index);
383
+ add({
384
+ name: x[2],
385
+ qname: owner ? `${owner.name}.${x[2]}` : x[2],
386
+ kind: owner ? "method" : "function",
387
+ from: x.index,
388
+ to: body.end,
389
+ exported: !/private|protected/.test(x[1]),
390
+ });
391
+ }
392
+ }
393
+
394
+ let exportsSet = null;
395
+ if (lang === "js" || lang === "ts") {
396
+ exportsSet = new Set();
397
+ for (const e of m.matchAll(/^[ \t]*export[ \t]*\{([^}]*)\}/gm))
398
+ for (const p of e[1].split(",")) {
399
+ const mm = /^\s*([\w$]+)/.exec(p);
400
+ if (mm) exportsSet.add(mm[1]);
401
+ }
402
+ for (const e of m.matchAll(/export[ \t]+default[ \t]+([A-Za-z_$][\w$]*)/g)) exportsSet.add(e[1]);
403
+ for (const e of m.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=/g)) exportsSet.add(e[1]);
404
+ const me = /module\.exports[ \t]*=[ \t]*(\{[^}]*\}|[A-Za-z_$][\w$]*)/.exec(m);
405
+ if (me) {
406
+ if (me[1][0] === "{") {
407
+ for (const p of me[1].slice(1, -1).split(",")) {
408
+ const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?/.exec(p);
409
+ if (mm) exportsSet.add(mm[2] || mm[1]);
410
+ }
411
+ } else exportsSet.add(me[1]);
412
+ }
413
+ }
414
+ return { defs, defParens, exportsSet };
415
+ }
416
+
417
+ // ── declarations: Python (indentation) ──────────────────────────────────────
418
+ function pyDefs(src, m, starts) {
419
+ const lines = m.split("\n");
420
+ const defs = [];
421
+ const defParens = new Set();
422
+ const stack = [];
423
+ const width = (s) => s.replace(/\t/g, " ").length;
424
+ for (let i = 0; i < lines.length; i++) {
425
+ const L = lines[i];
426
+ const d = /^([ \t]*)(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)[ \t]*\(/.exec(L);
427
+ const c = d ? null : /^([ \t]*)class[ \t]+([A-Za-z_]\w*)/.exec(L);
428
+ const hit = d || c;
429
+ if (!hit) continue;
430
+ const indent = width(hit[1]);
431
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
432
+ let header = i;
433
+ if (d) {
434
+ const paren = starts[i] + hit[0].length - 1;
435
+ defParens.add(paren);
436
+ header = lineOf(starts, matchPair(m, paren, "(", ")")) - 1;
437
+ }
438
+ let end = header;
439
+ for (let k = header + 1; k < lines.length; k++) {
440
+ const t = lines[k];
441
+ if (!t.trim()) continue;
442
+ if (width(/^[ \t]*/.exec(t)[0]) <= indent) break;
443
+ end = k;
444
+ }
445
+ const chain = [];
446
+ for (let s = stack.length - 1; s >= 0 && stack[s].kind === "class"; s--) chain.unshift(stack[s].name);
447
+ const name = hit[2];
448
+ defs.push({
449
+ name,
450
+ qname: [...chain, name].join("."),
451
+ kind: d ? (chain.length ? "method" : "function") : "class",
452
+ s: i + 1,
453
+ e: end + 1,
454
+ exported: !name.startsWith("_"),
455
+ });
456
+ stack.push({ indent, name, kind: d ? "function" : "class" });
457
+ }
458
+ return { defs, defParens };
459
+ }
460
+
461
+ // ── calls ───────────────────────────────────────────────────────────────────
462
+ const CALL = /(?:\bnew[ \t]+)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)[ \t]*(?:<[\w$., \t\[\]]*>)?[ \t]*\(/g;
463
+
464
+ function normCall(name) {
465
+ let n = String(name).replace(/[ \t]*(?:\?\.|->|::)[ \t]*/g, ".").replace(/[ \t]+/g, "");
466
+ let self = false;
467
+ const m = /^(?:\$this|this|self|static|cls)\.(.+)$/.exec(n);
468
+ if (m) {
469
+ n = m[1];
470
+ self = true;
471
+ }
472
+ n = n.replace(/^\$/, "").replace(/\.\$/g, ".");
473
+ const last = n.split(".").pop();
474
+ // A keyword after a dot is a METHOD name — `router.delete(`, `promise.catch(` —
475
+ // and the arguments inside it (a middleware passed by name) are still scanned.
476
+ if (!last || (KW.has(last) && !n.includes(".")) || /^\d/.test(last)) return null;
477
+ return { name: n, self };
478
+ }
479
+
480
+ function scanCalls(m, starts, defParens) {
481
+ const out = [];
482
+ let x;
483
+ CALL.lastIndex = 0;
484
+ while ((x = CALL.exec(m))) {
485
+ const paren = x.index + x[0].length - 1;
486
+ if (defParens.has(paren)) continue;
487
+ // `function (` / `def (` style anonymous forms, and the word before a
488
+ // declaration name, are not calls.
489
+ const before = m.slice(Math.max(0, x.index - 12), x.index);
490
+ if (/\b(function|def|func|fn)[ \t]*\*?[ \t]*$/.test(before)) continue;
491
+ const c = normCall(x[1]);
492
+ if (!c) continue;
493
+ const close = matchPair(m, paren, "(", ")");
494
+ // `async (req, res) => …` is a parameter list, not a call to `async`.
495
+ if (c.name === "async" && /^\s*=>/.test(m.slice(close + 1, close + 8))) continue;
496
+ out.push({ name: c.name, line: lineOf(starts, paren), ...(c.self ? { self: true } : {}), _p: paren });
497
+ refsIn(m, starts, paren, close, out);
498
+ }
499
+ return out;
500
+ }
501
+
502
+ // A function passed BY NAME — `router.post("/", requireAuth, handler)`,
503
+ // `items.map(normalize)` — is a use no call scan sees, so `requireAuth` had
504
+ // fan-in 0 on every route that used it. Each bare or dotted identifier argument
505
+ // becomes a REF. `finalize` keeps a ref only when its head is defined in this
506
+ // file or bound by an import, and resolution accepts only LOCAL or IMPORT for
507
+ // it, so a local variable passed along never becomes an edge.
508
+ const REF_WORDS = new Set(["true", "false", "null", "undefined", "nil", "None", "True", "False", "NaN", "Infinity"]);
509
+ const REF_ARG = /^\s*(?:\.\.\.|&)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)\s*$/;
510
+
511
+ function refsIn(m, starts, open, close, out) {
512
+ let depth = 0;
513
+ let a = open + 1;
514
+ for (let k = open + 1; k <= close && k < m.length; k++) {
515
+ const ch = m[k];
516
+ const end = k === close;
517
+ if (!end && (ch === "(" || ch === "[" || ch === "{")) depth++;
518
+ else if (!end && (ch === ")" || ch === "]" || ch === "}")) depth--;
519
+ if (!end && !(ch === "," && depth === 0)) continue;
520
+ const piece = m.slice(a, k);
521
+ const t = REF_ARG.exec(piece);
522
+ if (t && !REF_WORDS.has(t[1])) {
523
+ const c = normCall(t[1]);
524
+ if (c) out.push({ name: c.name, line: lineOf(starts, a + piece.indexOf(t[1])), ref: true, ...(c.self ? { self: true } : {}) });
525
+ }
526
+ a = k + 1;
527
+ }
528
+ }
529
+
530
+ // ── imports ─────────────────────────────────────────────────────────────────
531
+ // A statement is read from the ORIGINAL text (its path is a string), but only
532
+ // where the mask agrees the keyword is code — never inside a comment.
533
+ function codeAt(src, m, idx) {
534
+ let k = idx;
535
+ while (k < src.length && (src[k] === " " || src[k] === "\t")) k++;
536
+ return m[k] === src[k] && m[k] !== " ";
537
+ }
538
+
539
+ function jsClause(cl) {
540
+ const b = [];
541
+ const brace = /\{([^}]*)\}/.exec(cl);
542
+ if (brace)
543
+ for (const part of brace[1].split(",")) {
544
+ const p = part.trim().replace(/^type\s+/, "");
545
+ const mm = /^([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(p);
546
+ if (mm) b.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
547
+ }
548
+ for (const r of cl.replace(/\{[^}]*\}/, "").split(",")) {
549
+ const t = r.trim();
550
+ if (!t) continue;
551
+ const ns = /^\*\s+as\s+([\w$]+)$/.exec(t);
552
+ if (ns) b.push({ local: ns[1], orig: "*", kind: "namespace" });
553
+ else if (/^[\w$]+$/.test(t)) b.push({ local: t, orig: "default", kind: "default" });
554
+ }
555
+ return b;
556
+ }
557
+
558
+ function importsFor(lang, src, m, starts) {
559
+ const out = [];
560
+ const line = (i) => lineOf(starts, i);
561
+ let x;
562
+ if (lang === "js" || lang === "ts") {
563
+ const IMP = /^[ \t]*import[ \t]+(?:type[ \t]+)?([\w$*{}\s,]+?)[ \t]*from[ \t]*['"]([^'"\n]+)['"]/gm;
564
+ while ((x = IMP.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: jsClause(x[1]) });
565
+ const REQ = /(?:const|let|var)[ \t]+(\{[^}]*\}|[A-Za-z_$][\w$]*)[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)(?:\.([A-Za-z_$][\w$]*))?/g;
566
+ while ((x = REQ.exec(src))) {
567
+ if (!codeAt(src, m, x.index)) continue;
568
+ const bindings = [];
569
+ if (x[1][0] === "{") {
570
+ for (const p of x[1].slice(1, -1).split(",")) {
571
+ const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?\s*$/.exec(p);
572
+ if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
573
+ }
574
+ } else if (x[3]) bindings.push({ local: x[1], orig: x[3], kind: "named" });
575
+ else bindings.push({ local: x[1], orig: "default", kind: "namespace" });
576
+ out.push({ from: x[2], line: line(x.index), bindings });
577
+ }
578
+ } else if (lang === "py") {
579
+ const FROM = /^[ \t]*from[ \t]+([.\w]+)[ \t]+import[ \t]+(\([^)]*\)|[^\n#]+)/gm;
580
+ while ((x = FROM.exec(src))) {
581
+ if (!codeAt(src, m, x.index)) continue;
582
+ const bindings = [];
583
+ for (const p of x[2].replace(/[()]/g, "").split(",")) {
584
+ const mm = /^\s*(\w+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
585
+ if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
586
+ }
587
+ out.push({ from: x[1], line: line(x.index), bindings });
588
+ }
589
+ const IMP = /^[ \t]*import[ \t]+([^\n#]+)/gm;
590
+ while ((x = IMP.exec(src))) {
591
+ if (!codeAt(src, m, x.index)) continue;
592
+ for (const p of x[1].split(",")) {
593
+ const mm = /^\s*([\w.]+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
594
+ if (!mm) continue;
595
+ const mod = mm[2] ? mm[1] : mm[1].split(".")[0];
596
+ out.push({ from: mod, line: line(x.index), bindings: [{ local: mm[2] || mod, orig: mod, kind: "namespace" }] });
597
+ }
598
+ }
599
+ } else if (lang === "go") {
600
+ const addGo = (alias, p, at) => {
601
+ const local = alias || p.split("/").pop();
602
+ out.push({ from: p, line: line(at), bindings: alias === "_" || alias === "." ? [] : [{ local, orig: local, kind: "namespace" }] });
603
+ };
604
+ const ONE = /^import[ \t]+(?:([\w.]+)[ \t]+)?"([^"]+)"/gm;
605
+ while ((x = ONE.exec(src))) if (codeAt(src, m, x.index)) addGo(x[1], x[2], x.index);
606
+ const BLOCK = /^import[ \t]*\(([\s\S]*?)\)/gm;
607
+ while ((x = BLOCK.exec(src))) {
608
+ if (!codeAt(src, m, x.index)) continue;
609
+ let off = x.index + x[0].indexOf("(") + 1;
610
+ for (const l of x[1].split("\n")) {
611
+ const mm = /^\s*(?:([\w.]+)\s+)?"([^"]+)"/.exec(l);
612
+ if (mm) addGo(mm[1], mm[2], off);
613
+ off += l.length + 1;
614
+ }
615
+ }
616
+ } else if (lang === "java") {
617
+ const IMP = /^[ \t]*import[ \t]+(static[ \t]+)?([\w.]+?)(\.\*)?[ \t]*;/gm;
618
+ while ((x = IMP.exec(src))) {
619
+ if (!codeAt(src, m, x.index)) continue;
620
+ const parts = x[2].split(".");
621
+ if (x[3]) out.push({ from: x[2], line: line(x.index), bindings: [], star: true });
622
+ 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" }] });
623
+ else out.push({ from: x[2], line: line(x.index), bindings: [{ local: parts[parts.length - 1], orig: parts[parts.length - 1], kind: "named" }] });
624
+ }
625
+ } else if (lang === "cs") {
626
+ const USING = /^[ \t]*using[ \t]+(?:static[ \t]+)?(?:(\w+)[ \t]*=[ \t]*)?([\w.]+)[ \t]*;/gm;
627
+ while ((x = USING.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: [] });
628
+ } else if (lang === "php") {
629
+ const USE = /^[ \t]*use[ \t]+(?:function[ \t]+)?([\w\\]+)(?:[ \t]+as[ \t]+(\w+))?[ \t]*;/gm;
630
+ while ((x = USE.exec(src))) {
631
+ if (!codeAt(src, m, x.index)) continue;
632
+ const last = x[1].split("\\").pop();
633
+ out.push({ from: x[1], line: line(x.index), bindings: [{ local: x[2] || last, orig: last, kind: "named" }] });
634
+ }
635
+ const INC = /\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*['"]([^'"\n]+\.php)['"]/g;
636
+ while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
637
+ }
638
+ return out;
639
+ }
640
+
641
+ // ── effects ─────────────────────────────────────────────────────────────────
642
+ 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;
643
+ 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)$/;
644
+ 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$]+$)/;
645
+ const FS_RE = {
646
+ 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)$/,
647
+ go: /^(os\.(WriteFile|Create|Remove|RemoveAll|Mkdir|MkdirAll|Rename)|ioutil\.WriteFile)$/,
648
+ py: /^(os\.(remove|unlink|rename|makedirs|mkdir|rmdir)|shutil\.(rmtree|move|copy|copyfile)|.+\.(write_text|write_bytes))$/,
649
+ php: /^(file_put_contents|unlink|mkdir|rmdir|rename|fwrite)$/,
650
+ cs: /^(File\.(Write\w*|Delete|Move|Copy|AppendAll\w*|Create)|Directory\.(CreateDirectory|Delete))$/,
651
+ java: /^(Files\.(write|writeString|delete|deleteIfExists|move|copy|createDirectories))$/,
652
+ };
653
+ FS_RE.ts = FS_RE.js;
654
+ 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;
655
+
656
+ function stringAt(src, strings, pos) {
657
+ let k = pos;
658
+ while (k < src.length && /\s/.test(src[k])) k++;
659
+ if (!/['"`]/.test(src[k] || "")) return null;
660
+ const s = strings.find((t) => t.start === k + 1);
661
+ return s ? src.slice(s.start, s.end) : null;
662
+ }
663
+
664
+ function parenFor(src, starts, call) {
665
+ if (call._p != null) return call._p;
666
+ const ls = starts[call.line - 1] || 0;
667
+ const le = starts[call.line] || src.length;
668
+ const last = call.name.split(".").pop();
669
+ const i = src.indexOf(last, ls);
670
+ if (i < 0 || i >= le) return null;
671
+ const p = src.indexOf("(", i + last.length);
672
+ return p >= 0 && p < le ? p : null;
673
+ }
674
+
675
+ function effectsOf(lang, src, m, starts, strings, calls) {
676
+ const out = [];
677
+ for (const s of strings) {
678
+ const text = src.slice(s.start, s.end);
679
+ if (SQL_RE.test(text)) out.push({ type: "sql", text: clip(text), line: lineOf(starts, s.start) });
680
+ }
681
+ let x;
682
+ ENV_RE.lastIndex = 0;
683
+ while ((x = ENV_RE.exec(src))) {
684
+ if (!codeAt(src, m, x.index)) continue;
685
+ const name = x.slice(1).find(Boolean);
686
+ if (name) out.push({ type: "env", text: name, line: lineOf(starts, x.index) });
687
+ }
688
+ const fsRe = FS_RE[lang];
689
+ for (const c of calls) {
690
+ if (HTTP_RE.test(c.name)) {
691
+ const p = parenFor(src, starts, c);
692
+ const url = p != null ? stringAt(src, strings, p + 1) : null;
693
+ out.push({ type: "http", text: url ? `${c.name} ${clip(url, 60)}` : c.name, line: c.line });
694
+ } else if (DB_RE.test(c.name)) {
695
+ out.push({ type: "db", text: c.name, line: c.line });
696
+ } else if (fsRe && fsRe.test(c.name)) {
697
+ out.push({ type: "fs", text: c.name, line: c.line });
698
+ } else if (lang === "py" && c.name === "open") {
699
+ const p = parenFor(src, starts, c);
700
+ if (p != null && /^[^)]*?,\s*(?:mode\s*=\s*)?['"][wax]/.test(src.slice(p + 1, p + 120))) out.push({ type: "fs", text: "open (write)", line: c.line });
701
+ }
702
+ }
703
+ return out;
704
+ }
705
+
706
+ // ── assembly ────────────────────────────────────────────────────────────────
707
+ function finalize(rel, lang, src, m, starts, strings, defs, calls, imports, exportsSet, extractor) {
708
+ const nLines = starts.length;
709
+ const owner = new Int32Array(nLines + 2).fill(-1);
710
+ const order = defs.map((_, i) => i).sort((a, b) => defs[b].e - defs[b].s - (defs[a].e - defs[a].s));
711
+ for (const i of order) for (let L = defs[i].s; L <= defs[i].e && L <= nLines; L++) owner[L] = i;
712
+
713
+ const syms = defs.map((d) => ({
714
+ id: "",
715
+ name: d.name,
716
+ qname: d.qname || d.name,
717
+ kind: d.kind,
718
+ lines: [d.s, d.e],
719
+ exported: !!(d.exported || (exportsSet && exportsSet.has(d.name))),
720
+ body_hash: "",
721
+ calls: [],
722
+ effects: [],
723
+ }));
724
+ const mod = { id: "", name: "<module>", qname: "<module>", kind: "module", lines: [1, nLines], exported: false, body_hash: "", calls: [], effects: [] };
725
+ const at = (line) => (owner[line] >= 0 ? syms[owner[line]] : mod);
726
+
727
+ const known = new Set(defs.map((d) => d.name));
728
+ for (const imp of imports || []) for (const b of imp.bindings || []) known.add(b.local);
729
+ // A cap is a coverage gap, and the gap starts at the FIRST thing that was
730
+ // dropped — not at the top of the symbol. A file whose `<module>` holds 400
731
+ // calls is fully seen down to call 200; saying the whole file is partial
732
+ // would hide where the record actually stops.
733
+ const cutAt = new Map();
734
+ const cut = (t, line) => {
735
+ const was = cutAt.get(t);
736
+ if (was === undefined || line < was) cutAt.set(t, line);
737
+ };
738
+ for (const c of calls) {
739
+ if (c.ref && !known.has(c.self ? c.name.split(".").pop() : c.name.split(".")[0])) continue;
740
+ const t = at(c.line);
741
+ if (t.calls.length < MAX_CALLS_PER_SYMBOL)
742
+ t.calls.push({ name: c.name, line: c.line, ...(c.self ? { self: true } : {}), ...(c.ref ? { ref: true } : {}) });
743
+ else cut(t, c.line);
744
+ }
745
+ // A function passed as an argument runs no HTTP, SQL or fs effect by being passed.
746
+ for (const ef of effectsOf(lang, src, m, starts, strings, calls.filter((c) => !c.ref))) {
747
+ const t = at(ef.line);
748
+ if (t.effects.length >= MAX_EFFECTS_PER_SYMBOL) {
749
+ cut(t, ef.line);
750
+ continue;
751
+ }
752
+ if (!t.effects.some((e) => e.type === ef.type && e.text === ef.text)) t.effects.push(ef);
753
+ }
754
+
755
+ // The partial RANGES, merged, in line order. A cap gives the tail from the
756
+ // first dropped line; an unbalanced block gives everything from it to the end
757
+ // of the file.
758
+ const ranges = [];
759
+ for (const [t, line] of cutAt) ranges.push([line, t.lines[1]]);
760
+ if (UNBALANCED >= 0) ranges.push([lineOf(starts, UNBALANCED), nLines]);
761
+ ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
762
+ const partial = [];
763
+ for (const r of ranges) {
764
+ const last = partial[partial.length - 1];
765
+ if (last && r[0] <= last[1] + 1) last[1] = Math.max(last[1], r[1]);
766
+ else partial.push([r[0], r[1]]);
767
+ }
768
+
769
+ const seen = new Map();
770
+ for (const s of syms) {
771
+ const a = starts[s.lines[0] - 1] || 0;
772
+ const b = s.lines[1] < nLines ? starts[s.lines[1]] : src.length;
773
+ s.body_hash = hash(src.slice(a, b));
774
+ let id = `${rel}#${s.qname}`;
775
+ const k = seen.get(id) || 0;
776
+ seen.set(id, k + 1);
777
+ if (k) id += `~${k + 1}`;
778
+ s.id = id;
779
+ }
780
+ if (mod.calls.length || mod.effects.length) {
781
+ mod.id = `${rel}#<module>`;
782
+ syms.push(mod);
783
+ }
784
+ return { extractor, imports, symbols: syms, coverage: partial.length ? "partial" : "full", ...(partial.length ? { partial } : {}) };
785
+ }
786
+
787
+ function extractOne(item, borrowed) {
788
+ const { rel, lang, src } = item;
789
+ resetCoverage();
790
+ const { masked: m, strings } = mask(src, lang);
791
+ const starts = lineIndex(src);
792
+ if (borrowed) {
793
+ const calls = [];
794
+ for (const c of borrowed.calls) {
795
+ const n = normCall(c.name);
796
+ if (n) calls.push({ name: n.name, line: c.line, ...(n.self ? { self: true } : {}), ...(c.ref ? { ref: true } : {}) });
797
+ }
798
+ return finalize(rel, lang, src, m, starts, strings, borrowed.defs, calls, borrowed.imports, null, borrowed.extractor);
799
+ }
800
+ if (lang === "py") {
801
+ const { defs, defParens } = pyDefs(src, m, starts);
802
+ return finalize(rel, lang, src, m, starts, strings, defs, scanCalls(m, starts, defParens), importsFor(lang, src, m, starts), null, HEURISTIC);
803
+ }
804
+ const { defs, defParens, exportsSet } = braceDefs(src, m, starts, lang, strings);
805
+ return finalize(rel, lang, src, m, starts, strings, defs, scanCalls(m, starts, defParens), importsFor(lang, src, m, starts), exportsSet, HEURISTIC);
806
+ }
807
+
808
+ // ── the borrowed Python parser ──────────────────────────────────────────────
809
+ const PY_AST = String.raw`
810
+ import ast, sys, json
811
+ def dotted(n):
812
+ parts = []
813
+ while isinstance(n, ast.Attribute):
814
+ parts.append(n.attr)
815
+ n = n.value
816
+ if isinstance(n, ast.Name):
817
+ parts.append(n.id)
818
+ return ".".join(reversed(parts))
819
+ return None
820
+ class V(ast.NodeVisitor):
821
+ def __init__(s):
822
+ s.defs = []; s.calls = []; s.imports = []; s.stack = []
823
+ def qual(s, name):
824
+ chain = []
825
+ for kind, n in reversed(s.stack):
826
+ if kind != "class":
827
+ break
828
+ chain.append(n)
829
+ chain.reverse()
830
+ return ".".join(chain + [name]), bool(chain)
831
+ def visit_ClassDef(s, node):
832
+ q, _ = s.qual(node.name)
833
+ s.defs.append({"name": node.name, "qname": q, "kind": "class", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")})
834
+ s.stack.append(("class", node.name)); s.generic_visit(node); s.stack.pop()
835
+ def visit_FunctionDef(s, node):
836
+ q, inclass = s.qual(node.name)
837
+ s.defs.append({"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("_")})
838
+ s.stack.append(("function", node.name)); s.generic_visit(node); s.stack.pop()
839
+ visit_AsyncFunctionDef = visit_FunctionDef
840
+ def visit_Call(s, node):
841
+ name = dotted(node.func)
842
+ if name:
843
+ s.calls.append({"name": name, "line": node.lineno})
844
+ for a in list(node.args) + [k.value for k in node.keywords]:
845
+ r = dotted(a)
846
+ if r:
847
+ s.calls.append({"name": r, "line": getattr(a, "lineno", node.lineno), "ref": True})
848
+ s.generic_visit(node)
849
+ def visit_Import(s, node):
850
+ for a in node.names:
851
+ mod = a.name if a.asname else a.name.split(".")[0]
852
+ s.imports.append({"from": mod, "line": node.lineno, "bindings": [{"local": a.asname or mod, "orig": mod, "kind": "namespace"}]})
853
+ def visit_ImportFrom(s, node):
854
+ mod = "." * (node.level or 0) + (node.module or "")
855
+ b = [{"local": a.asname or a.name, "orig": a.name, "kind": "named"} for a in node.names if a.name != "*"]
856
+ s.imports.append({"from": mod, "line": node.lineno, "bindings": b})
857
+ items = json.loads(sys.stdin.buffer.read().decode("utf-8"))
858
+ out = {}
859
+ for it in items:
860
+ try:
861
+ with open(it["abs"], "r", encoding="utf-8", errors="replace") as f:
862
+ tree = ast.parse(f.read())
863
+ v = V(); v.visit(tree)
864
+ out[it["rel"]] = {"defs": v.defs, "calls": v.calls, "imports": v.imports}
865
+ except Exception:
866
+ out[it["rel"]] = None
867
+ sys.stdout.buffer.write(json.dumps(out).encode("utf-8"))
868
+ `;
869
+
870
+ let pythonProbe;
871
+ function findPython() {
872
+ if (pythonProbe !== undefined) return pythonProbe;
873
+ pythonProbe = null;
874
+ const cands = [];
875
+ if (process.env.ORC_GRAPH_PYTHON) cands.push([process.env.ORC_GRAPH_PYTHON]);
876
+ cands.push(["python3"], ["python"], ["py", "-3"]);
877
+ for (const [cmd, ...pre] of cands) {
878
+ const r = spawnSync(cmd, [...pre, "-c", "import sys;print('%d.%d' % sys.version_info[:2]);print(sys.version_info >= (3, 8))"], {
879
+ encoding: "utf8",
880
+ timeout: 10000,
881
+ windowsHide: true,
882
+ });
883
+ if (r.status === 0 && /True/.test(r.stdout || "")) {
884
+ pythonProbe = { cmd, pre, ver: (r.stdout || "").split(/\r?\n/)[0].trim() };
885
+ break;
886
+ }
887
+ }
888
+ return pythonProbe;
889
+ }
890
+
891
+ function runPythonAst(items) {
892
+ const py = findPython();
893
+ if (!py) return null;
894
+ const r = spawnSync(py.cmd, [...py.pre, "-c", PY_AST], {
895
+ input: JSON.stringify(items.map((i) => ({ rel: i.rel, abs: i.abs }))),
896
+ encoding: "utf8",
897
+ maxBuffer: 512 * 1024 * 1024,
898
+ timeout: 300000,
899
+ windowsHide: true,
900
+ });
901
+ if (r.status !== 0 || !r.stdout) return null;
902
+ try {
903
+ return { map: JSON.parse(r.stdout), extractor: `python-ast@${py.ver}` };
904
+ } catch (_) {
905
+ return null;
906
+ }
907
+ }
908
+
909
+ // items: [{ rel, abs, lang, src }] → Map<rel, { extractor, imports, symbols }>
910
+ function extractBatch(items) {
911
+ const out = new Map();
912
+ const pys = items.filter((i) => i.lang === "py");
913
+ const ast = pys.length && !process.env.ORC_GRAPH_NO_BORROW ? runPythonAst(pys) : null;
914
+ for (const it of items) {
915
+ const b = ast && ast.map[it.rel];
916
+ try {
917
+ out.set(it.rel, extractOne(it, b ? { ...b, extractor: ast.extractor } : null));
918
+ } catch (_) {
919
+ // A file the extractor cannot read is an empty record, never a failed
920
+ // update: one pathological file must not take the whole map down.
921
+ out.set(it.rel, { extractor: HEURISTIC, error: "extract-failed", imports: [], symbols: [] });
922
+ }
923
+ }
924
+ return out;
925
+ }
926
+
927
+ module.exports = { HEURISTIC, MAX_CALLS_PER_SYMBOL, MAX_EFFECTS_PER_SYMBOL, extractBatch, extractOne, mask, normCall, _findPython: findPython };