@polycode-projects/seonix 0.9.1 → 0.10.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.
- package/README.md +33 -12
- package/bin/cli.mjs +92 -11
- package/package.json +4 -9
- package/scripts/build-roslyn.mjs +11 -0
- package/src/ask-browser.bundle.js +136 -18
- package/src/browser.mjs +9 -3
- package/src/codegraph.mjs +126 -18
- package/src/cs_roslyn.mjs +13 -7
- package/src/cs_treesitter.mjs +58 -3
- package/src/extract.mjs +98 -23
- package/src/extract_lang.mjs +4 -2
- package/src/interfaces.mjs +110 -16
- package/src/jsts_tsc.mjs +51 -0
- package/src/roslyn_build.mjs +59 -0
- package/src/schema-docs.mjs +33 -3
- package/src/server.mjs +51 -7
- package/src/summary.mjs +14 -3
- package/src/telemetry.mjs +31 -0
package/src/cs_treesitter.mjs
CHANGED
|
@@ -54,6 +54,59 @@ function collectCalls(node) {
|
|
|
54
54
|
return [...out].sort();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// ---- HTTP-client call capture (un-typed interfaces: the callsHttp client→route half). ----
|
|
58
|
+
// `<recv>.GetAsync("api/x")`-style HttpClient invocations whose FIRST argument is a string
|
|
59
|
+
// literal. The verb comes from the method name (Get|Post|Put|Delete|Patch, with the common
|
|
60
|
+
// String/Stream/ByteArray/FromJson/AsJson suffixes, always ...Async); interpolated strings keep
|
|
61
|
+
// their `{expr}` holes verbatim — the matcher in interfaces.mjs collapses them to wildcard
|
|
62
|
+
// segments. The URL must contain "/". Emitted as an optional `http_calls` field on the method
|
|
63
|
+
// define — absent when empty, and the DEFAULT graph ignores the field (gating contract).
|
|
64
|
+
// NOTE: the tree-sitter backend only; the Roslyn backend does not capture http_calls yet.
|
|
65
|
+
const HTTP_METHOD = /^(get|post|put|delete|patch)(string|stream|bytearray|fromjson|asjson)?async$/;
|
|
66
|
+
const URL_LITERALS = new Set(["string_literal", "verbatim_string_literal", "interpolated_string_expression", "raw_string_literal"]);
|
|
67
|
+
function collectHttpCalls(node) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const visit = (n) => {
|
|
71
|
+
if (n.type === "invocation_expression") {
|
|
72
|
+
const fn = n.childForFieldName("function");
|
|
73
|
+
const name = fn && fn.type === "member_access_expression" ? (fn.childForFieldName("name")?.text || "") : "";
|
|
74
|
+
const m = HTTP_METHOD.exec(name.toLowerCase());
|
|
75
|
+
if (m) {
|
|
76
|
+
const arg = n.childForFieldName("arguments")?.namedChild(0); // first `argument` node
|
|
77
|
+
const expr = arg?.namedChild(0) || arg;
|
|
78
|
+
if (expr && URL_LITERALS.has(expr.type)) {
|
|
79
|
+
const url = expr.text.replace(/\s+/g, " ").replace(/^[$@]+/, "").replace(/^"+|"+$/g, "").slice(0, 160);
|
|
80
|
+
if (url.includes("/")) {
|
|
81
|
+
const key = `${m[1]} ${url}`;
|
|
82
|
+
if (!seen.has(key)) { seen.add(key); out.push({ verb: m[1], url }); }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < n.namedChildCount; i++) visit(n.namedChild(i)); // document order
|
|
88
|
+
};
|
|
89
|
+
visit(node);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** C# attributes on a declaration node → decorator strings, matching the Roslyn
|
|
94
|
+
* backend's shape (`m.AttributeLists.SelectMany(a => a.Attributes)` + OneLine):
|
|
95
|
+
* each `attribute` child of an `attribute_list`, whitespace-collapsed to one line.
|
|
96
|
+
* So `[Route("api/[controller]")]` → `Route("api/[controller]")` on both backends. */
|
|
97
|
+
function attrsOf(node) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
100
|
+
const list = node.namedChild(i);
|
|
101
|
+
if (list.type !== "attribute_list") continue;
|
|
102
|
+
for (let j = 0; j < list.namedChildCount; j++) {
|
|
103
|
+
const a = list.namedChild(j);
|
|
104
|
+
if (a.type === "attribute") out.push(a.text.replace(/\s+/g, " ").trim());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
57
110
|
function modifiersText(node) {
|
|
58
111
|
// tree-sitter-c-sharp surfaces modifiers as anonymous children before the name
|
|
59
112
|
const mods = [];
|
|
@@ -76,7 +129,7 @@ function emitType(n, prefix, defines, exports) {
|
|
|
76
129
|
const bases = [];
|
|
77
130
|
const baseList = n.childForFieldName("bases") || n.namedChildren.find((c) => c.type === "base_list");
|
|
78
131
|
if (baseList) for (let i = 0; i < baseList.namedChildCount; i++) bases.push(baseList.namedChild(i).text.slice(0, 80));
|
|
79
|
-
defines.push({ name: cname, kind: "class", lineno: line(n), end_lineno: endLine(n), bases, decorators:
|
|
132
|
+
defines.push({ name: cname, kind: "class", lineno: line(n), end_lineno: endLine(n), bases, decorators: attrsOf(n),
|
|
80
133
|
...(SUBKIND[n.type] ? { subkind: SUBKIND[n.type] } : {}),
|
|
81
134
|
...(visFrom(mods) ? { visibility: visFrom(mods) } : {}) });
|
|
82
135
|
if (mods.includes("public")) exports.add(cname);
|
|
@@ -94,10 +147,12 @@ function emitType(n, prefix, defines, exports) {
|
|
|
94
147
|
const mmods = modifiersText(mem);
|
|
95
148
|
const params = (mem.childForFieldName("parameters")?.text || "").replace(/\s+/g, " ").replace(/^\(|\)$/g, "").slice(0, 160);
|
|
96
149
|
const returns = (mem.childForFieldName("returns") || mem.childForFieldName("type"))?.text?.slice(0, 80) || "";
|
|
150
|
+
const http = collectHttpCalls(mem);
|
|
97
151
|
defines.push({ name: `${cname}.${mn}`, kind: "method", lineno: line(mem), end_lineno: endLine(mem),
|
|
98
|
-
decorators:
|
|
152
|
+
decorators: attrsOf(mem), params, returns, calls: collectCalls(mem),
|
|
99
153
|
...(mmods.includes("static") ? { is_static: true } : {}),
|
|
100
|
-
...(visFrom(mmods) ? { visibility: visFrom(mmods) } : {})
|
|
154
|
+
...(visFrom(mmods) ? { visibility: visFrom(mmods) } : {}),
|
|
155
|
+
...(http.length ? { http_calls: http } : {}) });
|
|
101
156
|
} else if (mem.type === "property_declaration") {
|
|
102
157
|
const mn = fieldText(mem, "name");
|
|
103
158
|
if (mn) defines.push({ name: `${cname}.${mn}`, kind: "attribute", lineno: line(mem), end_lineno: endLine(mem), decorators: [] });
|
package/src/extract.mjs
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// mgx:testsCoverage Module → Module (a test module → the internal modules it imports)
|
|
14
14
|
// seon:history Commit → Module (from git log --name-only)
|
|
15
15
|
// mgx:touchesSymbol Commit → CodeEntity (commit changed-line-range ∩ symbol span)
|
|
16
|
-
// mgx:callsSymbol Function/Method → Function/Class (symbol-granular,
|
|
16
|
+
// mgx:callsSymbol Function/Method → Function/Class/Method (symbol-granular,
|
|
17
|
+
// unambiguous; method targets resolve through a
|
|
18
|
+
// tiered simple-name fallback — see buildEntities)
|
|
17
19
|
// seon:containsCodeEntity Class → Method/Attribute (class membership)
|
|
18
20
|
// mgx:subclassOf Class → Class (inheritance; internal base resolved, else ext:)
|
|
19
21
|
|
|
@@ -233,9 +235,38 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
233
235
|
// Registry of top-level functions/classes by simple name — used to resolve coarse
|
|
234
236
|
// call targets AND inheritance bases to a single internal module (else dropped).
|
|
235
237
|
const classToPaths = new Map(); // class name -> Set<path> (for base resolution)
|
|
238
|
+
// Method-level call-target registries. Receiver-typed call texts (`_svc.ProcessOrder(...)`,
|
|
239
|
+
// `self.render()`) never match a top-level function/class name, and on C#/Java estates there
|
|
240
|
+
// are NO top-level functions at all — so without these every method stays caller-less.
|
|
241
|
+
const methodToSymbolIds = new Map(); // simple method name (last ident of the define) -> Set<fnId>
|
|
242
|
+
const dottedMethodToSymbolIds = new Map(); // "Owner.Method" (last two idents of the define) -> Set<fnId>
|
|
243
|
+
const interfaceOwnedIds = new Set(); // method ids owned by a class defined with subkind "interface"
|
|
236
244
|
for (const m of modules) {
|
|
245
|
+
// Interface class names in THIS module first (defines are document-ordered, but a full
|
|
246
|
+
// pre-pass keeps membership independent of class/member ordering).
|
|
247
|
+
const interfaceNames = new Set();
|
|
237
248
|
for (const d of m.defines || []) {
|
|
238
|
-
if (d.kind === "
|
|
249
|
+
if (d.kind === "class" && d.subkind === "interface") interfaceNames.add(d.name);
|
|
250
|
+
}
|
|
251
|
+
for (const d of m.defines || []) {
|
|
252
|
+
if (d.kind === "method") {
|
|
253
|
+
const id = fnId(m.path, d.name);
|
|
254
|
+
const simple = lastIdent(d.name);
|
|
255
|
+
if (simple) {
|
|
256
|
+
if (!methodToSymbolIds.has(simple)) methodToSymbolIds.set(simple, new Set());
|
|
257
|
+
methodToSymbolIds.get(simple).add(id);
|
|
258
|
+
}
|
|
259
|
+
const parts = d.name.split(".");
|
|
260
|
+
if (parts.length >= 2) {
|
|
261
|
+
const dotted = parts.slice(-2).join(".");
|
|
262
|
+
if (!dottedMethodToSymbolIds.has(dotted)) dottedMethodToSymbolIds.set(dotted, new Set());
|
|
263
|
+
dottedMethodToSymbolIds.get(dotted).add(id);
|
|
264
|
+
}
|
|
265
|
+
const owner = d.name.includes(".") ? d.name.slice(0, d.name.lastIndexOf(".")) : "";
|
|
266
|
+
if (owner && interfaceNames.has(owner)) interfaceOwnedIds.add(id);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (d.kind === "attribute" || d.kind === "global") continue; // not standalone call targets
|
|
239
270
|
const register = (name) => {
|
|
240
271
|
if (!nameToPaths.has(name)) nameToPaths.set(name, new Set());
|
|
241
272
|
nameToPaths.get(name).add(m.path);
|
|
@@ -267,6 +298,32 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
267
298
|
return ids[0];
|
|
268
299
|
};
|
|
269
300
|
|
|
301
|
+
// Resolve a receiver-typed call text (`_svc.ProcessOrder`, `self.render`) to ONE method
|
|
302
|
+
// define, tiered and deterministic — same honest Group-A discipline as the base registry,
|
|
303
|
+
// mirroring its same-module-wins rule:
|
|
304
|
+
// 0. precision: the call's last TWO identifiers hit the dotted "Owner.Method" registry
|
|
305
|
+
// uniquely (qualified/static call texts) → it;
|
|
306
|
+
// a. exactly one candidate defined in the calling module → local win;
|
|
307
|
+
// b. else exactly one after excluding interface-owned members → the implementation
|
|
308
|
+
// wins over the interface declaration it satisfies;
|
|
309
|
+
// c. else exactly one overall → it;
|
|
310
|
+
// d. else drop (ambiguous → no edge, never guessed).
|
|
311
|
+
const resolveMethodCallee = (callName, ident, path) => {
|
|
312
|
+
const dm = String(callName).match(/([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
|
|
313
|
+
if (dm) {
|
|
314
|
+
const dotted = dottedMethodToSymbolIds.get(`${dm[1]}.${dm[2]}`);
|
|
315
|
+
if (dotted?.size === 1) return [...dotted][0];
|
|
316
|
+
}
|
|
317
|
+
const cands = methodToSymbolIds.get(ident);
|
|
318
|
+
if (!cands || !cands.size) return null;
|
|
319
|
+
const pre = `fn:${path}#`;
|
|
320
|
+
const local = [...cands].filter((i) => i.startsWith(pre));
|
|
321
|
+
if (local.length === 1) return local[0];
|
|
322
|
+
const impls = [...cands].filter((i) => !interfaceOwnedIds.has(i));
|
|
323
|
+
if (impls.length === 1) return impls[0];
|
|
324
|
+
return cands.size === 1 ? [...cands][0] : null;
|
|
325
|
+
};
|
|
326
|
+
|
|
270
327
|
// resolve a module's import candidates to internal module paths
|
|
271
328
|
const internalImports = (m) => {
|
|
272
329
|
const set = new Set();
|
|
@@ -380,17 +437,22 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
380
437
|
}
|
|
381
438
|
}
|
|
382
439
|
|
|
383
|
-
// symbol-granular calls: caller fn/method → callee fn/class
|
|
384
|
-
//
|
|
385
|
-
// as the module-coarse calls).
|
|
386
|
-
//
|
|
440
|
+
// symbol-granular calls: caller fn/method → callee fn/class/method. The top-level
|
|
441
|
+
// function/class registry resolves first, unchanged (exactly ONE in-repo definition —
|
|
442
|
+
// same unique-name discipline as the module-coarse calls). On a complete miss there,
|
|
443
|
+
// receiver-typed call texts fall through to the tiered METHOD registry above, so
|
|
444
|
+
// `_svc.ProcessOrder(...)` reaches `B.ProcessOrder` even though no top-level symbol
|
|
445
|
+
// carries that name (C#/Java estates have none). Ambiguous / external names are
|
|
446
|
+
// dropped (honest Group-A). Reuses the per-function call names the extractors parsed.
|
|
387
447
|
if ((d.kind === "function" || d.kind === "method") && d.calls?.length) {
|
|
388
448
|
for (const callName of d.calls) {
|
|
389
449
|
const ident = lastIdent(callName);
|
|
390
450
|
if (!ident) continue;
|
|
391
451
|
const ids = nameToSymbolIds.get(ident);
|
|
392
|
-
|
|
393
|
-
|
|
452
|
+
let callee = null;
|
|
453
|
+
if (ids?.size === 1) callee = [...ids][0];
|
|
454
|
+
else if (!ids) callee = resolveMethodCallee(callName, ident, m.path); // method fallback on a clean miss only
|
|
455
|
+
if (!callee) continue; // ambiguous or external → drop
|
|
394
456
|
if (callee === oid) continue; // self-recursion not an edge
|
|
395
457
|
const ckey = `${oid}>${callee}`;
|
|
396
458
|
if (seenCallSymbol.has(ckey)) continue;
|
|
@@ -544,7 +606,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
544
606
|
// a no-op → byte-identical graph.json/summary.json (the measurement contract).
|
|
545
607
|
const serves = interfaces?.enabled
|
|
546
608
|
? extractServes(modules, { fnId, opts: interfaceOpts(interfaces) })
|
|
547
|
-
: { edges: [], routes: [], vocab: [] };
|
|
609
|
+
: { edges: [], routes: [], vocab: [], callsHttp: [] };
|
|
548
610
|
|
|
549
611
|
// Second pass (PLAN_PROSE_INDEX.md) — see the returned `proseIndex` field's comment below.
|
|
550
612
|
const allIndividuals = attachProseTokens(
|
|
@@ -564,7 +626,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
564
626
|
vocabulary: [
|
|
565
627
|
{ prop: "mgx:importsNamespace", predicate: "imports", note: "module→module; SEON usesComplexType is type→type, so owned (cf. main:dependsOn)" },
|
|
566
628
|
{ prop: "mgx:callsCoarse", predicate: "calls", note: "module→module, import-backed; NOT SEON's method→method invokesMethod" },
|
|
567
|
-
{ prop: "mgx:callsSymbol", predicate: "callsSymbol", note: "caller fn/method→callee fn/class; symbol-granular, unique-name-resolved (Group A); cf. seon:invokesMethod" },
|
|
629
|
+
{ prop: "mgx:callsSymbol", predicate: "callsSymbol", note: "caller fn/method→callee fn/class/method; symbol-granular, unique-name-resolved (Group A; method targets via a tiered simple-name fallback — same-module wins, interface members excluded when one implementation remains); cf. seon:invokesMethod" },
|
|
568
630
|
{ prop: "seon:declaresMethod", predicate: "defines" },
|
|
569
631
|
{ prop: "seon:containsCodeEntity", predicate: "contains" },
|
|
570
632
|
{ prop: "mgx:touchedByCommit", predicate: "touches", note: "owned; seon:history is not a real SEON property (cf. history:isCommittedIn)" },
|
|
@@ -617,8 +679,10 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
617
679
|
rel("cochange", "mgx:changeCoupledWith", cochangeEdges),
|
|
618
680
|
rel("reexports", "mgx:reExports", reExportEdges),
|
|
619
681
|
// GATED (PLAN_UNTYPED_INTERFACES.md): emitted ONLY when the interfaces feature produced
|
|
620
|
-
// edges, so the default graph never carries a zero-count `serves` group →
|
|
682
|
+
// edges, so the default graph never carries a zero-count `serves`/`callsHttp` group →
|
|
683
|
+
// byte-identical.
|
|
621
684
|
...(serves.edges.length ? [rel("serves", "mgx:serves", serves.edges)] : []),
|
|
685
|
+
...(serves.callsHttp.length ? [rel("callsHttp", "mgx:callsHttp", serves.callsHttp)] : []),
|
|
622
686
|
],
|
|
623
687
|
individuals: allIndividuals,
|
|
624
688
|
// Second pass (PLAN_PROSE_INDEX.md): word -> [individual ids], inverted from the
|
|
@@ -828,6 +892,10 @@ async function assembleAndWrite(rootDir, { modules, commits, symbolHistory, gene
|
|
|
828
892
|
// the same in-memory payload, stamped with the JSON's stat for freshness. Mirrors the
|
|
829
893
|
// SEONIX_PROSE_INDEX env-gate precedent; unset → no import of node:sqlite, no graph.db,
|
|
830
894
|
// byte-identical default path. Best-effort: graph.json stays authoritative on any failure.
|
|
895
|
+
// storeMode feeds the A1 summary's `store:` line — "sqlite" ONLY when graph.db was actually
|
|
896
|
+
// (re)built, so an inactive gate (e.g. SEONIX_STORE set in the shell but never EXPORTED) is
|
|
897
|
+
// visible at a glance instead of silently running the JSON single-string path.
|
|
898
|
+
let storeMode = "json";
|
|
831
899
|
if (process.env.SEONIX_STORE === "sqlite") {
|
|
832
900
|
try {
|
|
833
901
|
const { writeStore, storeFileFor } = await import("./store.mjs");
|
|
@@ -837,6 +905,7 @@ async function assembleAndWrite(rootDir, { modules, commits, symbolHistory, gene
|
|
|
837
905
|
// file, so verifyStore/readPayload-freshness would fail on every fresh v2 store. Mirrors
|
|
838
906
|
// bin/cli.mjs store_rebuild (which passes sourceText: text).
|
|
839
907
|
await writeStore(storeFileFor(graphFile), entities, { sourceStat, sourceText: payload });
|
|
908
|
+
storeMode = "sqlite";
|
|
840
909
|
} catch (e) {
|
|
841
910
|
process.stderr.write(`seonix: sqlite store build failed (${e?.message || e}) — graph.json is authoritative\n`);
|
|
842
911
|
}
|
|
@@ -848,7 +917,7 @@ async function assembleAndWrite(rootDir, { modules, commits, symbolHistory, gene
|
|
|
848
917
|
? codegraph.renderToolsCatalog(CLI_SCRIPT)
|
|
849
918
|
: localToolsCatalog(CLI_SCRIPT);
|
|
850
919
|
await writeFile(join(rootDir, ".seonix", "TOOLS.md"), toolsMd);
|
|
851
|
-
return { entities, graphFile, graphBytes };
|
|
920
|
+
return { entities, graphFile, graphBytes, storeMode };
|
|
852
921
|
}
|
|
853
922
|
|
|
854
923
|
function buildCounts(entities, { modules, languages, commits, proseEnabled, baseMs, historyMs }) {
|
|
@@ -886,7 +955,7 @@ export async function indexRepository(repoPath, { python = DEFAULT_PYTHON(), gen
|
|
|
886
955
|
const { modules, perLang, commits, symbolHistory, baseMs, historyMs, gitErrors } = r;
|
|
887
956
|
warnGitErrors(gitErrors, repoPath); // surface any history-pass failure immediately (never fatal)
|
|
888
957
|
const sessions = await readSessionRecords(repoPath); // recorded chat sessions (.seonix/sessions/*.jsonl), [] when none
|
|
889
|
-
const { entities, graphFile, graphBytes } = await assembleAndWrite(repoPath, { modules, commits, symbolHistory, generatedAt, proseEnabled, sessions, interfaces });
|
|
958
|
+
const { entities, graphFile, graphBytes, storeMode } = await assembleAndWrite(repoPath, { modules, commits, symbolHistory, generatedAt, proseEnabled, sessions, interfaces });
|
|
890
959
|
// A1 post-index summary: SUMMARY.md + summary.json next to graph.json (NEW files; the
|
|
891
960
|
// graph.json bytes are untouched). The rendered MD is also printed to stdout by cli.mjs.
|
|
892
961
|
const summary = buildSummary(entities, {
|
|
@@ -901,6 +970,7 @@ export async function indexRepository(repoPath, { python = DEFAULT_PYTHON(), gen
|
|
|
901
970
|
skipped: [],
|
|
902
971
|
languages: perLang,
|
|
903
972
|
graphBytes,
|
|
973
|
+
store: storeMode,
|
|
904
974
|
});
|
|
905
975
|
await writeSummaryArtifacts(dirname(graphFile), summary);
|
|
906
976
|
return {
|
|
@@ -961,7 +1031,8 @@ export function applyRepoPrefix({ modules, commits, symbolHistory }, prefix) {
|
|
|
961
1031
|
export function defaultOutRoot(repoPaths, cwd = process.cwd()) {
|
|
962
1032
|
const segs = repoPaths.map((p) => resolve(p).split(sep));
|
|
963
1033
|
const first = segs[0];
|
|
964
|
-
let depth = Math.min(...
|
|
1034
|
+
let depth = first.length; // running min, not Math.min(...spread) — one arg per repo path
|
|
1035
|
+
for (const s of segs) if (s.length < depth) depth = s.length;
|
|
965
1036
|
let i = 0;
|
|
966
1037
|
while (i < depth && segs.every((s) => s[i] === first[i])) i += 1;
|
|
967
1038
|
const ancestor = first.slice(0, i).join(sep) || sep;
|
|
@@ -1018,9 +1089,11 @@ export async function indexRepositories(repoPaths, { python = DEFAULT_PYTHON(),
|
|
|
1018
1089
|
const r = await extractRepo(rp, { python, ignores, historyDepth, extraIgnores, languages: languageAllow });
|
|
1019
1090
|
warnGitErrors(r.gitErrors, prefix); // surface any history-pass failure immediately (never fatal)
|
|
1020
1091
|
applyRepoPrefix(r, prefix);
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1092
|
+
// NB: append iteratively — spreading into push() passes one argument per element and
|
|
1093
|
+
// module/commit lists scale with repo size, past V8's argument-count limit on estates.
|
|
1094
|
+
for (const m of r.modules) allModules.push(m);
|
|
1095
|
+
for (const c of r.commits) allCommits.push(c);
|
|
1096
|
+
for (const h of r.symbolHistory) allSymbolHistory.push(h);
|
|
1024
1097
|
baseMs += r.baseMs;
|
|
1025
1098
|
historyMs += r.historyMs;
|
|
1026
1099
|
for (const [lang, s] of Object.entries(r.perLang)) {
|
|
@@ -1036,7 +1109,7 @@ export async function indexRepositories(repoPaths, { python = DEFAULT_PYTHON(),
|
|
|
1036
1109
|
// TODO(sessions): multi-repo merges don't fold in the member repos' .seonix/sessions
|
|
1037
1110
|
// yet — their recorded ids would need the same repo-name prefixing as modules to
|
|
1038
1111
|
// re-resolve against the merged graph. Deferred; single-path fold-in is the contract.
|
|
1039
|
-
const { entities, graphFile, graphBytes } = await assembleAndWrite(root, {
|
|
1112
|
+
const { entities, graphFile, graphBytes, storeMode } = await assembleAndWrite(root, {
|
|
1040
1113
|
modules: allModules, commits: allCommits, symbolHistory: allSymbolHistory, generatedAt, proseEnabled, interfaces,
|
|
1041
1114
|
});
|
|
1042
1115
|
// A1 post-index summary (merged): per-repo rows (name = prefix) + any discovery
|
|
@@ -1053,6 +1126,7 @@ export async function indexRepositories(repoPaths, { python = DEFAULT_PYTHON(),
|
|
|
1053
1126
|
skipped,
|
|
1054
1127
|
languages,
|
|
1055
1128
|
graphBytes,
|
|
1129
|
+
store: storeMode,
|
|
1056
1130
|
});
|
|
1057
1131
|
await writeSummaryArtifacts(dirname(graphFile), summary);
|
|
1058
1132
|
return {
|
|
@@ -1188,9 +1262,10 @@ export async function syncRepositories(multiRoot, { python = DEFAULT_PYTHON(), g
|
|
|
1188
1262
|
} else {
|
|
1189
1263
|
ext = await readCache(artifactDir, name); // reused post-prefix extraction
|
|
1190
1264
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1265
|
+
// NB: append iteratively — same repo-scaled-spread hazard as indexRepositories above.
|
|
1266
|
+
for (const m of ext.modules) allModules.push(m);
|
|
1267
|
+
for (const c of ext.commits) allCommits.push(c);
|
|
1268
|
+
for (const h of ext.symbolHistory) allSymbolHistory.push(h);
|
|
1194
1269
|
for (const [lang, s] of Object.entries(ext.perLang || {})) {
|
|
1195
1270
|
const agg = languages[lang] || (languages[lang] = { lib: s.lib, files: 0, modules: 0, symbols: 0, failures: 0, ms: 0 });
|
|
1196
1271
|
agg.files += s.files; agg.modules += s.modules; agg.symbols += s.symbols; agg.failures += s.failures; agg.ms += s.ms;
|
|
@@ -1208,10 +1283,10 @@ export async function syncRepositories(multiRoot, { python = DEFAULT_PYTHON(), g
|
|
|
1208
1283
|
for (const r of diff.removed) await rm(cachePath(artifactDir, r.name), { force: true }).catch(() => {});
|
|
1209
1284
|
|
|
1210
1285
|
// ONE buildEntities over the union (cached + fresh) → the exact full-re-index graph.
|
|
1211
|
-
const { entities, graphFile, graphBytes } = await assembleAndWrite(root, {
|
|
1286
|
+
const { entities, graphFile, graphBytes, storeMode } = await assembleAndWrite(root, {
|
|
1212
1287
|
modules: allModules, commits: allCommits, symbolHistory: allSymbolHistory, generatedAt, proseEnabled, interfaces,
|
|
1213
1288
|
});
|
|
1214
|
-
const summary = buildSummary(entities, { mode: "multi", repos: repoRows, skipped: [], languages, graphBytes });
|
|
1289
|
+
const summary = buildSummary(entities, { mode: "multi", repos: repoRows, skipped: [], languages, graphBytes, store: storeMode });
|
|
1215
1290
|
await writeSummaryArtifacts(dirname(graphFile), summary);
|
|
1216
1291
|
await writeManifest(artifactDir, { format: 1, generated_at: generatedAt, repos: manifestRows });
|
|
1217
1292
|
|
package/src/extract_lang.mjs
CHANGED
|
@@ -66,9 +66,11 @@ export async function ingestRepo(root, { cs, java, ignore = null } = {}) {
|
|
|
66
66
|
const { modules, failures, fileCount } = await extractor.ingest(root, { ignore });
|
|
67
67
|
const ms = Date.now() - t0;
|
|
68
68
|
if (fileCount === 0) continue; // language not present
|
|
69
|
-
|
|
69
|
+
// NB: append iteratively — spreading into push() passes one argument per module and
|
|
70
|
+
// per-language module counts hit ~20k+ on estates, past V8's argument-count limit.
|
|
71
|
+
for (const m of modules) allModules.push(m);
|
|
70
72
|
totalFiles += fileCount;
|
|
71
|
-
allFailures.push(
|
|
73
|
+
for (const f of failures) allFailures.push(f);
|
|
72
74
|
const symbols = modules.reduce((n, m) => n + (m.defines?.length || 0), 0);
|
|
73
75
|
perLang[lang] = { lib: extractor.meta.lib, files: fileCount, modules: modules.length, symbols, failures: failures.length, ms };
|
|
74
76
|
}
|
package/src/interfaces.mjs
CHANGED
|
@@ -18,7 +18,9 @@ export function interfaceOpts(cfg = {}) {
|
|
|
18
18
|
verbs: cfg.verbs && cfg.verbs.length ? cfg.verbs.map((s) => String(s).toLowerCase()) : VERBS,
|
|
19
19
|
combineControllerPrefix: cfg.combineControllerPrefix !== false, // default true
|
|
20
20
|
prefixStrip: (cfg.prefixStrip || []).map((s) => normPath(String(s))),
|
|
21
|
-
//
|
|
21
|
+
// Minimum matchScore (see below) for a callsHttp client↔route link. 1.0 = exact-length
|
|
22
|
+
// matches only; the 0.5 default also admits exact-prefix overlaps (e.g. a client URL with
|
|
23
|
+
// extra trailing segments over a shorter route template).
|
|
22
24
|
matchThreshold: typeof cfg.matchThreshold === "number" ? cfg.matchThreshold : 0.5,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
@@ -64,21 +66,28 @@ function parseCsAttr(attr) {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/** C# controller routes from a module's defines[] (Roslyn emits `[Http*]`/`[Route]` in decorators).
|
|
67
|
-
* Methods are named "Class.Method"
|
|
69
|
+
* Methods are named "Class.Method". ASP.NET semantics honoured here:
|
|
70
|
+
* - EVERY class-level `[Route("…")]` is a prefix (a controller can declare several — one route per
|
|
71
|
+
* prefix × method mapping, document order, deterministic).
|
|
72
|
+
* - A method-level verb attr with its own template (`[HttpGet("{id}")]`) uses that template.
|
|
73
|
+
* - A verb attr with NO template (`[HttpPost]`) takes the method-level verb-less `[Route("…")]`
|
|
74
|
+
* template(s) as its suffix — the common `[HttpPost] + [Route("process")]` pattern; with no
|
|
75
|
+
* method [Route] either, the mapping is just the prefix. */
|
|
68
76
|
function csRoutes(mod, opts) {
|
|
69
77
|
const out = [];
|
|
70
78
|
const defines = mod.defines || [];
|
|
71
|
-
// class name → {
|
|
79
|
+
// class name → { prefixes, isController } from class-level [Route]/[ApiController]
|
|
72
80
|
const classInfo = new Map();
|
|
73
81
|
for (const d of defines) {
|
|
74
82
|
if (d.kind !== "class") continue;
|
|
75
|
-
|
|
83
|
+
const prefixes = [];
|
|
84
|
+
let isController = /controller$/i.test(d.name);
|
|
76
85
|
for (const a of d.decorators || []) {
|
|
77
86
|
const p = parseCsAttr(a);
|
|
78
|
-
if (p && p.verb === null && p.path)
|
|
87
|
+
if (p && p.verb === null && p.path) prefixes.push(p.path);
|
|
79
88
|
if (/^apicontroller/i.test(String(a).trim())) isController = true;
|
|
80
89
|
}
|
|
81
|
-
classInfo.set(d.name, {
|
|
90
|
+
classInfo.set(d.name, { prefixes, isController });
|
|
82
91
|
}
|
|
83
92
|
for (const d of defines) {
|
|
84
93
|
if (d.kind !== "method") continue;
|
|
@@ -87,14 +96,27 @@ function csRoutes(mod, opts) {
|
|
|
87
96
|
const methodName = dot >= 0 ? d.name.slice(dot + 1) : d.name;
|
|
88
97
|
const info = classInfo.get(className);
|
|
89
98
|
const controller = className.replace(/Controller$/i, "");
|
|
99
|
+
// First pass over the method's attributes: verb mappings + verb-less [Route] templates.
|
|
100
|
+
const verbAttrs = [];
|
|
101
|
+
const routeTemplates = [];
|
|
90
102
|
for (const a of d.decorators || []) {
|
|
91
103
|
const p = parseCsAttr(a);
|
|
92
|
-
if (!p
|
|
104
|
+
if (!p) continue;
|
|
105
|
+
if (p.verb === null) { if (p.path) routeTemplates.push(p.path); continue; }
|
|
93
106
|
if (!opts.verbs.includes(p.verb)) continue;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
verbAttrs.push(p);
|
|
108
|
+
}
|
|
109
|
+
if (!verbAttrs.length) continue; // only [Http*] maps a method to a verb
|
|
110
|
+
const prefixes = opts.combineControllerPrefix && info?.prefixes.length ? info.prefixes : [""];
|
|
111
|
+
for (const p of verbAttrs) {
|
|
112
|
+
const suffixes = p.path ? [p.path] : (routeTemplates.length ? routeTemplates : [""]);
|
|
113
|
+
for (const prefix of prefixes) {
|
|
114
|
+
for (const suffix of suffixes) {
|
|
115
|
+
let path = subTokens(joinRoute(prefix, suffix), controller, methodName);
|
|
116
|
+
path = stripPrefixes(path, opts.prefixStrip);
|
|
117
|
+
out.push({ verb: p.verb.toUpperCase(), path, handler: d.name });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
98
120
|
}
|
|
99
121
|
}
|
|
100
122
|
return out;
|
|
@@ -124,16 +146,46 @@ function stripPrefixes(path, prefixes) {
|
|
|
124
146
|
const isCs = (p) => /\.cs$/i.test(p || "");
|
|
125
147
|
const isJsTs = (p) => /\.(m?[jt]sx?)$/i.test(p || "");
|
|
126
148
|
|
|
149
|
+
/** Normalise a URL / route template into match segments (the deterministic half of callsHttp):
|
|
150
|
+
* scheme+host stripped, query/hash dropped, duplicate slashes collapsed, and any parameter-ish
|
|
151
|
+
* segment — `{id}` / `{id:int}` (ASP.NET, also a C# interpolation hole after quote-stripping),
|
|
152
|
+
* `${expr}` / `{*}` (JS interpolation holes), `:id` (Express) — collapsed to a `*` wildcard.
|
|
153
|
+
* Literal segments compare case-insensitively (ASP.NET routing is case-insensitive).
|
|
154
|
+
* Exported for the matcher tests; pure. */
|
|
155
|
+
export function normalizeHttpTemplate(url) {
|
|
156
|
+
let u = String(url || "").trim();
|
|
157
|
+
u = u.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, ""); // scheme://host[:port]
|
|
158
|
+
u = u.split(/[?#]/)[0];
|
|
159
|
+
u = normPath(u);
|
|
160
|
+
if (!u) return [];
|
|
161
|
+
return u.split("/").map((seg) => (/[{}]|^:/.test(seg) ? "*" : seg.toLowerCase()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Deterministic template match: segment-by-segment prefix discipline (`*` matches any single
|
|
165
|
+
* segment; the first literal mismatch is fatal), scored 2·shared/(|url|+|route|). 1.0 is an
|
|
166
|
+
* exact-length match; an exact-prefix overlap scores below 1 and is admitted (or not) by the
|
|
167
|
+
* match_threshold knob. No embeddings, no fuzzy scoring beyond this normalisation. */
|
|
168
|
+
function matchScore(a, b) {
|
|
169
|
+
if (!a.length || !b.length) return 0;
|
|
170
|
+
const n = Math.min(a.length, b.length);
|
|
171
|
+
for (let i = 0; i < n; i += 1) {
|
|
172
|
+
if (a[i] !== b[i] && a[i] !== "*" && b[i] !== "*") return 0;
|
|
173
|
+
}
|
|
174
|
+
return (2 * n) / (a.length + b.length);
|
|
175
|
+
}
|
|
176
|
+
|
|
127
177
|
/**
|
|
128
178
|
* Build the `serves` edges + `Route` individuals for a set of modules.
|
|
129
179
|
* @param modules the same module records buildEntities consumes (path, defines[], routes[]).
|
|
130
180
|
* @param fnId the id-builder buildEntities uses: (path, symbolName) → "fn:path#name".
|
|
131
181
|
* @param opts from interfaceOpts().
|
|
132
|
-
* @returns { edges, routes } — edges are {subject,object,subjectLabel,
|
|
133
|
-
* Route individuals.
|
|
182
|
+
* @returns { edges, routes, callsHttp } — edges/callsHttp are {subject,object,subjectLabel,
|
|
183
|
+
* objectLabel}; routes are Route individuals. All deterministically ordered; empty when
|
|
184
|
+
* nothing matched.
|
|
134
185
|
*/
|
|
135
186
|
export function extractServes(modules, { fnId, opts }) {
|
|
136
187
|
const routeById = new Map(); // route id → individual (deduped across modules/methods)
|
|
188
|
+
const routeMeta = []; // route id → {verb, segs} for the callsHttp matcher below
|
|
137
189
|
const edgeSet = new Set(); // dedupe subject|object
|
|
138
190
|
const edges = [];
|
|
139
191
|
for (const mod of modules || []) {
|
|
@@ -143,7 +195,7 @@ export function extractServes(modules, { fnId, opts }) {
|
|
|
143
195
|
for (const r of routes) {
|
|
144
196
|
const label = `${r.verb} ${r.path}`;
|
|
145
197
|
const rid = `route:${label}`;
|
|
146
|
-
if (!routeById.has(rid))
|
|
198
|
+
if (!routeById.has(rid)) {
|
|
147
199
|
routeById.set(rid, {
|
|
148
200
|
id: rid, label, class: "Route", derived_from: [], mentions: [],
|
|
149
201
|
attributes: [
|
|
@@ -151,6 +203,8 @@ export function extractServes(modules, { fnId, opts }) {
|
|
|
151
203
|
{ prop: "mgx:routePath", key: "path", value: r.path },
|
|
152
204
|
],
|
|
153
205
|
});
|
|
206
|
+
routeMeta.push({ id: rid, label, verb: r.verb, segs: normalizeHttpTemplate(r.path) });
|
|
207
|
+
}
|
|
154
208
|
const object = fnId(mod.path, r.handler);
|
|
155
209
|
const key = `${rid}|${object}`;
|
|
156
210
|
if (edgeSet.has(key)) continue;
|
|
@@ -158,8 +212,47 @@ export function extractServes(modules, { fnId, opts }) {
|
|
|
158
212
|
edges.push({ subject: rid, object, subjectLabel: label, objectLabel: r.handler });
|
|
159
213
|
}
|
|
160
214
|
}
|
|
161
|
-
|
|
215
|
+
const byEdge = (a, b) => (a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : a.object < b.object ? -1 : a.object > b.object ? 1 : 0);
|
|
216
|
+
edges.sort(byEdge);
|
|
162
217
|
const routes = [...routeById.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
218
|
+
|
|
219
|
+
// callsHttp (client→route) — the other half of the cross-service chain, PLAN_UNTYPED_INTERFACES
|
|
220
|
+
// follow-up #1. Client call sites come pre-captured from the extractors as an optional per-define
|
|
221
|
+
// `http_calls: [{verb, url}]` (fetch/axios/HttpClient first-argument URL literals). Each call is
|
|
222
|
+
// matched against the Route table just built: verbs must agree, templates match per matchScore
|
|
223
|
+
// above, and only the BEST-scoring route(s) at or above opts.matchThreshold link (ties all link —
|
|
224
|
+
// deterministic). A call matching nothing emits nothing; no orphan Route individuals are created,
|
|
225
|
+
// so callsHttp can only point at Routes that `serves` already justified.
|
|
226
|
+
const callsHttp = [];
|
|
227
|
+
const chSeen = new Set();
|
|
228
|
+
for (const mod of modules || []) {
|
|
229
|
+
for (const d of mod.defines || []) {
|
|
230
|
+
for (const c of d.http_calls || []) {
|
|
231
|
+
const verb = String(c.verb || "").toUpperCase();
|
|
232
|
+
const segs = normalizeHttpTemplate(c.url);
|
|
233
|
+
if (!segs.length) continue;
|
|
234
|
+
// No anchor, no edge: an all-wildcard template (`{base}{uri}/{id}`) would match ANY
|
|
235
|
+
// same-length route at 1.0 — at least one literal segment is required (house "no wrong edge").
|
|
236
|
+
if (!segs.some((s) => s !== "*")) continue;
|
|
237
|
+
let best = 0;
|
|
238
|
+
let hits = [];
|
|
239
|
+
for (const r of routeMeta) {
|
|
240
|
+
if (r.verb !== verb) continue;
|
|
241
|
+
const s = matchScore(segs, r.segs);
|
|
242
|
+
if (s < opts.matchThreshold || s < best) continue;
|
|
243
|
+
if (s > best) { best = s; hits = [r]; } else hits.push(r);
|
|
244
|
+
}
|
|
245
|
+
const subject = fnId(mod.path, d.name);
|
|
246
|
+
for (const r of hits) {
|
|
247
|
+
const key = `${subject}|${r.id}`;
|
|
248
|
+
if (chSeen.has(key)) continue;
|
|
249
|
+
chSeen.add(key);
|
|
250
|
+
callsHttp.push({ subject, object: r.id, subjectLabel: d.name, objectLabel: r.label });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
callsHttp.sort(byEdge);
|
|
163
256
|
// Vocabulary notes for the emitted tokens, built HERE (not in extract.mjs) so the un-typed-interface
|
|
164
257
|
// props stay scoped to this gated feature module — the core schema drift guard documents the always
|
|
165
258
|
// -on extract.mjs props; these opt-in props are self-documented inline until they earn a
|
|
@@ -167,5 +260,6 @@ export function extractServes(modules, { fnId, opts }) {
|
|
|
167
260
|
const vocab = [];
|
|
168
261
|
if (edges.length) vocab.push({ prop: "mgx:serves", predicate: "serves", note: "route-declaration→handler-symbol; the HTTP boundary a syntax-only AST misses (un-typed interface)" });
|
|
169
262
|
if (routes.length) vocab.push({ prop: "mgx:httpVerb", note: "HTTP verb of a Route individual" }, { prop: "mgx:routePath", note: "normalised path template of a Route individual" });
|
|
170
|
-
|
|
263
|
+
if (callsHttp.length) vocab.push({ prop: "mgx:callsHttp", predicate: "callsHttp", note: "client call-site→Route; HTTP URL literal matched to a route template (deterministic normalisation, match_threshold-gated)" });
|
|
264
|
+
return { edges, routes, vocab, callsHttp };
|
|
171
265
|
}
|
package/src/jsts_tsc.mjs
CHANGED
|
@@ -66,6 +66,55 @@ function decoratorsOf(sf, node) {
|
|
|
66
66
|
return (ds || []).map((d) => d.expression.getText(sf).replace(/\s+/g, " ").slice(0, 80));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// ---- HTTP-client call capture (un-typed interfaces: the callsHttp client→route half). ----
|
|
70
|
+
// The URL literal at a client call site, per define — the base calls[] strips arguments, so the
|
|
71
|
+
// route-matchable string must be captured here. Deterministic name gates, no resolution:
|
|
72
|
+
// fetch('/api/x' [, {method:'POST'}]) → verb from a literal `method:` property, else get
|
|
73
|
+
// <recv>.get|post|…('/api/x', …) where recv's text looks like an HTTP client (axios / http /
|
|
74
|
+
// client / api / request) — so Express-style registrations (router./app. receivers) and
|
|
75
|
+
// Map.get('key') never match.
|
|
76
|
+
// The URL must be a string/template literal containing "/"; template interpolation holes become
|
|
77
|
+
// `{*}` (collapsed to a wildcard segment by the matcher in interfaces.mjs). Emitted as an optional
|
|
78
|
+
// `http_calls` field on the define — absent when empty, so modules without client calls are
|
|
79
|
+
// byte-identical to before, and the DEFAULT graph ignores the field entirely (gating contract).
|
|
80
|
+
const CLIENTISH = /axios|http|client|api|request/i;
|
|
81
|
+
function urlLiteralText(sf, a) {
|
|
82
|
+
if (!a) return "";
|
|
83
|
+
if (ts.isStringLiteralLike(a)) return a.text; // 'x' / "x" / `x` (no holes)
|
|
84
|
+
if (ts.isTemplateExpression(a)) return a.head.text + a.templateSpans.map((s) => `{*}${s.literal.text}`).join("");
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
function httpCallsIn(sf, node) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const visit = (n) => {
|
|
91
|
+
if (ts.isCallExpression(n)) {
|
|
92
|
+
const t = n.expression;
|
|
93
|
+
let verb = "";
|
|
94
|
+
if ((ts.isIdentifier(t) && t.text === "fetch") || (ts.isPropertyAccessExpression(t) && t.name.text === "fetch")) {
|
|
95
|
+
verb = "get"; // fetch defaults to GET; a literal {method:'POST'} option overrides
|
|
96
|
+
const opt = n.arguments[1];
|
|
97
|
+
if (opt && ts.isObjectLiteralExpression(opt)) {
|
|
98
|
+
for (const p of opt.properties) {
|
|
99
|
+
if (ts.isPropertyAssignment(p) && p.name && p.name.getText(sf) === "method" && ts.isStringLiteralLike(p.initializer))
|
|
100
|
+
verb = p.initializer.text.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else if (ts.isPropertyAccessExpression(t) && ROUTE_VERBS.has(t.name.text.toLowerCase()) && CLIENTISH.test(t.expression.getText(sf))) {
|
|
104
|
+
verb = t.name.text.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
const url = verb ? urlLiteralText(sf, n.arguments[0]) : "";
|
|
107
|
+
if (ROUTE_VERBS.has(verb) && url.includes("/")) {
|
|
108
|
+
const key = `${verb} ${url}`;
|
|
109
|
+
if (!seen.has(key)) { seen.add(key); out.push({ verb, url: url.slice(0, 160) }); }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
ts.forEachChild(n, visit);
|
|
113
|
+
};
|
|
114
|
+
ts.forEachChild(node, visit);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
69
118
|
/** Collect callee names within a node body (coarse; unparsed call target). */
|
|
70
119
|
function callsIn(sf, node) {
|
|
71
120
|
const out = new Set();
|
|
@@ -129,12 +178,14 @@ export async function extractFile(absPath, root) {
|
|
|
129
178
|
const isExportsRef = (e) => isModuleExports(e) || (ts.isIdentifier(e) && e.text === "exports");
|
|
130
179
|
|
|
131
180
|
const addFn = (name, node, kind, extra = {}) => {
|
|
181
|
+
const http = httpCallsIn(sf, node);
|
|
132
182
|
defines.push({
|
|
133
183
|
name, kind, lineno: lineOf(sf, node), end_lineno: endLineOf(sf, node),
|
|
134
184
|
decorators: decoratorsOf(sf, node),
|
|
135
185
|
params: paramsText(sf, node), returns: returnText(sf, node),
|
|
136
186
|
calls: callsIn(sf, node), doc: firstDocLine(sf, node),
|
|
137
187
|
...(visibilityOf(node) ? { visibility: visibilityOf(node) } : {}),
|
|
188
|
+
...(http.length ? { http_calls: http } : {}),
|
|
138
189
|
...extra,
|
|
139
190
|
});
|
|
140
191
|
};
|