@polycode-projects/seonix 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/bin/cli.mjs +92 -11
- package/package.json +48 -7
- package/src/ask-browser.bundle.js +776 -49
- package/src/browser.mjs +9 -3
- package/src/codegraph.mjs +126 -18
- 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/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/browser.mjs
CHANGED
|
@@ -46,6 +46,7 @@ const PROP_KIND = {
|
|
|
46
46
|
"mgx:touchessymbol": "touchesSymbol",
|
|
47
47
|
"mgx:callssymbol": "callsSymbol",
|
|
48
48
|
"mgx:serves": "serves", // un-typed interfaces (gated); inert on a default graph
|
|
49
|
+
"mgx:callshttp": "callsHttp", // un-typed interfaces (gated); client call-site → Route
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
const pathOfId = (id) => {
|
|
@@ -284,7 +285,10 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
284
285
|
}
|
|
285
286
|
const directBorn = (id) => {
|
|
286
287
|
const s = touchesByNode.get(id);
|
|
287
|
-
|
|
288
|
+
if (!s || !s.size) return null;
|
|
289
|
+
let min = Infinity; // running min, not Math.min(...spread) — touch sets scale with history
|
|
290
|
+
for (const o of s) if (o < min) min = o;
|
|
291
|
+
return min;
|
|
288
292
|
};
|
|
289
293
|
const fallbackBorn = (id) => {
|
|
290
294
|
let cur = parentOf.get(id);
|
|
@@ -604,7 +608,8 @@ function render(){
|
|
|
604
608
|
const hs=(S.heat&&!cmp)?heatScale(S.tg,idx):null;
|
|
605
609
|
const grav=S.grav?gravityAt(S.tg,hi):null;
|
|
606
610
|
const gW=grav?new Map(grav.map(e=>[e.src+'|'+e.dst,e.w])):null;
|
|
607
|
-
|
|
611
|
+
let gMax=1; // running max, not Math.max(...spread) — gravity edge lists scale with the graph
|
|
612
|
+
if(grav&&grav.length){gMax=grav[0].w;for(const e of grav)if(e.w>gMax)gMax=e.w;}
|
|
608
613
|
cy.batch(()=>{
|
|
609
614
|
cy.nodes().forEach(cn=>{
|
|
610
615
|
const n=S.byId.get(cn.id());
|
|
@@ -667,7 +672,8 @@ function runGravityLayout(){
|
|
|
667
672
|
const hi=S.cursorB!=null?Math.max(S.cursor,S.cursorB):S.cursor;
|
|
668
673
|
const g=gravityAt(S.tg,hi);
|
|
669
674
|
const wByKey=new Map(g.map(e=>[e.src+'|'+e.dst,e.w]));
|
|
670
|
-
|
|
675
|
+
let maxW=1; // running max, not Math.max(...spread) — gravity edge lists scale with the graph
|
|
676
|
+
if(g.length){maxW=g[0].w;for(const e of g)if(e.w>maxW)maxW=e.w;}
|
|
671
677
|
const wOf=e=>{const a=e.data('source'),b=e.data('target');return wByKey.get(a+'|'+b)??wByKey.get(b+'|'+a)??0;};
|
|
672
678
|
const vis=S.cy.elements().filter(el=>el.style('display')==='element');
|
|
673
679
|
vis.layout({name:'cose',animate:false,nodeRepulsion:6000,
|
package/src/codegraph.mjs
CHANGED
|
@@ -89,9 +89,10 @@ const PROP_KIND = {
|
|
|
89
89
|
// closure (module-coarse) is unchanged.
|
|
90
90
|
"mgx:touchessymbol": "touchesSymbol",
|
|
91
91
|
"mgx:callssymbol": "callsSymbol",
|
|
92
|
-
// un-typed interfaces (PLAN_UNTYPED_INTERFACES.md) — route→handler; only present
|
|
93
|
-
// gated [interfaces] feature emitted
|
|
92
|
+
// un-typed interfaces (PLAN_UNTYPED_INTERFACES.md) — route→handler + client→route; only present
|
|
93
|
+
// when the gated [interfaces] feature emitted edges, so these keys are inert on a default graph.
|
|
94
94
|
"mgx:serves": "serves",
|
|
95
|
+
"mgx:callshttp": "callsHttp",
|
|
95
96
|
// legacy tokens (pre-realign graphs) — kept so a stale artifact still classifies
|
|
96
97
|
"seon:usescomplextype": "imports",
|
|
97
98
|
"seon:invokesmethod": "calls",
|
|
@@ -189,6 +190,10 @@ export function resolveSymbol(graph, symbol) {
|
|
|
189
190
|
return {
|
|
190
191
|
match: scored[0]?.ind || null,
|
|
191
192
|
candidates: scored.slice(1, 5).map((x) => x.ind),
|
|
193
|
+
// `exact` distinguishes a literal label/id hit from a path-normalised or substring
|
|
194
|
+
// (fuzzy) hit, so a caller can NAME a substitution instead of silently answering
|
|
195
|
+
// about a different symbol than the one asked for (wh dogfood T14).
|
|
196
|
+
exact: (scored[0]?.score ?? 0) === 100,
|
|
192
197
|
};
|
|
193
198
|
}
|
|
194
199
|
|
|
@@ -320,6 +325,36 @@ export function impactClosure(graph, ind, { maxDepth = 8 } = {}) {
|
|
|
320
325
|
const subjLabel = graph.byId.get(subjModId)?.label || subjModId;
|
|
321
326
|
addDependent(objModId, subjModId, subjLabel, g.predicate);
|
|
322
327
|
}
|
|
328
|
+
} else if (kind === "serves") {
|
|
329
|
+
// Un-typed interfaces (interfaces.mjs, gated — these kinds never occur in a default
|
|
330
|
+
// graph, so the default closure is untouched). Edge direction as W4 emits it:
|
|
331
|
+
// subject = Route individual, object = handler symbol (fn:<path>#name). The Route
|
|
332
|
+
// and its handler are one interface point seen from two sides, so impact crosses
|
|
333
|
+
// the edge BOTH ways:
|
|
334
|
+
// - reverse (like imports/calls): a handler change impacts its Route — keyed on
|
|
335
|
+
// the handler symbol AND its module (coarsened on read, the callsSymbol
|
|
336
|
+
// technique), so the closure crosses the edge whether it entered at symbol or
|
|
337
|
+
// module level;
|
|
338
|
+
// - forward: a Route change impacts its handler's module — so the closure from a
|
|
339
|
+
// Route continues into the handler's own dependents at the next depth.
|
|
340
|
+
for (const e of g.edges) {
|
|
341
|
+
const handlerModId = moduleIdOfId(graph, e.object);
|
|
342
|
+
addDependent(e.object, e.subject, e.subjectLabel || e.subject, g.predicate);
|
|
343
|
+
if (handlerModId) {
|
|
344
|
+
addDependent(handlerModId, e.subject, e.subjectLabel || e.subject, g.predicate);
|
|
345
|
+
addDependent(e.subject, handlerModId, graph.byId.get(handlerModId)?.label || handlerModId, g.predicate);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (kind === "callsHttp") {
|
|
349
|
+
// Gated, like serves. subject = client call-site symbol, object = Route individual.
|
|
350
|
+
// Reverse-only (a Route change impacts its HTTP clients; a client change never
|
|
351
|
+
// impacts the Route), caller coarsened to its module — completing the chain
|
|
352
|
+
// handler → Route (serves) → client modules (callsHttp).
|
|
353
|
+
for (const e of g.edges) {
|
|
354
|
+
const subjModId = moduleIdOfId(graph, e.subject);
|
|
355
|
+
if (!subjModId) continue;
|
|
356
|
+
addDependent(e.object, subjModId, graph.byId.get(subjModId)?.label || subjModId, g.predicate);
|
|
357
|
+
}
|
|
323
358
|
} else if (kind === "tests") {
|
|
324
359
|
for (const e of g.edges) {
|
|
325
360
|
if (!coveredBy.has(e.object)) coveredBy.set(e.object, []);
|
|
@@ -689,9 +724,9 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
689
724
|
}
|
|
690
725
|
const [survivors, kindOverflow] = pruneToBeam(candidates);
|
|
691
726
|
for (const [id, score] of survivors) merged.set(id, Math.max(merged.get(id) || 0, score));
|
|
692
|
-
plyOverflow.push(...kindOverflow);
|
|
727
|
+
plyOverflow.push(...kindOverflow); // spread OK: bounded by BEAM_OVERFLOW_CAP (≤4 args)
|
|
693
728
|
}
|
|
694
|
-
overflow.push(...plyOverflow);
|
|
729
|
+
overflow.push(...plyOverflow); // spread OK: ≤ cap × edge groups = 16 args, never repo-scaled
|
|
695
730
|
// Apply the bounded nudge once per module (first ply it's reached), same shape as the other
|
|
696
731
|
// proximity families — a nudge, never a replacement.
|
|
697
732
|
for (const [id, propagated] of merged) {
|
|
@@ -1292,21 +1327,20 @@ export function renderSignature(graph, ind) {
|
|
|
1292
1327
|
return lines.join("\n");
|
|
1293
1328
|
}
|
|
1294
1329
|
|
|
1295
|
-
/**
|
|
1296
|
-
|
|
1297
|
-
export function renderSubclasses(graph, ind) {
|
|
1298
|
-
const inherits = edgesOfKind(graph, "inherits");
|
|
1299
|
-
const bases = inherits.filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object);
|
|
1330
|
+
/** child map over the `inherits` edges: base id → [{id,label}] of direct subclasses. */
|
|
1331
|
+
function inheritsChildrenOf(inherits) {
|
|
1300
1332
|
const childrenOf = new Map();
|
|
1301
1333
|
for (const e of inherits) {
|
|
1302
1334
|
if (!childrenOf.has(e.object)) childrenOf.set(e.object, []);
|
|
1303
1335
|
childrenOf.get(e.object).push({ id: e.subject, label: e.subjectLabel || e.subject });
|
|
1304
1336
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1337
|
+
return childrenOf;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** BFS the reverse-inheritance closure below `frontier` (depth-capped at 8); returns
|
|
1341
|
+
* sorted label levels. `visited` guards cycles and is mutated. */
|
|
1342
|
+
function descendSubclassLevels(childrenOf, frontier, visited) {
|
|
1308
1343
|
const levels = [];
|
|
1309
|
-
let frontier = [ind.id];
|
|
1310
1344
|
for (let depth = 1; depth <= 8 && frontier.length; depth += 1) {
|
|
1311
1345
|
const next = [];
|
|
1312
1346
|
const level = [];
|
|
@@ -1324,6 +1358,18 @@ export function renderSubclasses(graph, ind) {
|
|
|
1324
1358
|
}
|
|
1325
1359
|
frontier = next;
|
|
1326
1360
|
}
|
|
1361
|
+
return levels;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Forward bases + the transitive reverse inheritance closure (who extends this) —
|
|
1365
|
+
* replaces grepping `class X(Base)` across the tree. Uses `inherits` (mgx:subclassOf). */
|
|
1366
|
+
export function renderSubclasses(graph, ind) {
|
|
1367
|
+
const inherits = edgesOfKind(graph, "inherits");
|
|
1368
|
+
const bases = inherits.filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object);
|
|
1369
|
+
const childrenOf = inheritsChildrenOf(inherits);
|
|
1370
|
+
const lines = [`${ind.label} — ${ind.class || "Entity"} (id: ${ind.id})`];
|
|
1371
|
+
lines.push(bases.length ? `extends: ${capJoin(bases, SUBCLASS_CAP)}` : "extends: (no internal/recorded base classes)");
|
|
1372
|
+
const levels = descendSubclassLevels(childrenOf, [ind.id], new Set([ind.id]));
|
|
1327
1373
|
const total = levels.reduce((n, l) => n + l.length, 0);
|
|
1328
1374
|
if (!total) {
|
|
1329
1375
|
lines.push("subclasses: none recorded — nothing extends it in the extracted graph.");
|
|
@@ -1334,6 +1380,44 @@ export function renderSubclasses(graph, ind) {
|
|
|
1334
1380
|
return lines.join("\n");
|
|
1335
1381
|
}
|
|
1336
1382
|
|
|
1383
|
+
/** Honest subclasses answer for a base-class NAME with no individual of its own — an
|
|
1384
|
+
* external/framework base (e.g. ASP.NET's ControllerBase) that appears only as a
|
|
1385
|
+
* supertype label on `inherits` edges. Lists the classes whose recorded supertype
|
|
1386
|
+
* label matches the name, plus their transitive subclasses. Returns null when no
|
|
1387
|
+
* inherits edge carries the label, so the caller can fall through. (wh dogfood T14:
|
|
1388
|
+
* the silent fuzzy substitution answered "none recorded" while 176 classes inherited
|
|
1389
|
+
* from the asked-for external base.) */
|
|
1390
|
+
export function renderExternalSubclasses(graph, name) {
|
|
1391
|
+
const want = String(name || "").trim().toLowerCase();
|
|
1392
|
+
if (!want) return null;
|
|
1393
|
+
const inherits = edgesOfKind(graph, "inherits");
|
|
1394
|
+
const matched = inherits.filter((e) => {
|
|
1395
|
+
const label = String(e.objectLabel || "").trim().toLowerCase();
|
|
1396
|
+
if (label) return label === want;
|
|
1397
|
+
const id = String(e.object || "").toLowerCase();
|
|
1398
|
+
return id === want || id.endsWith(`#${want}`); // label-less edge: match the id tail
|
|
1399
|
+
});
|
|
1400
|
+
if (!matched.length) return null;
|
|
1401
|
+
const visited = new Set();
|
|
1402
|
+
const direct = [];
|
|
1403
|
+
for (const e of matched) {
|
|
1404
|
+
if (visited.has(e.subject)) continue;
|
|
1405
|
+
visited.add(e.subject);
|
|
1406
|
+
direct.push({ id: e.subject, label: e.subjectLabel || e.subject });
|
|
1407
|
+
}
|
|
1408
|
+
const directLabels = direct.map((d) => d.label).sort((a, b) => String(a).localeCompare(String(b)));
|
|
1409
|
+
const deeper = descendSubclassLevels(inheritsChildrenOf(inherits), direct.map((d) => d.id), visited);
|
|
1410
|
+
const levels = [directLabels, ...deeper];
|
|
1411
|
+
const total = levels.reduce((n, l) => n + l.length, 0);
|
|
1412
|
+
const lines = [
|
|
1413
|
+
`${name} — external/framework base class: not an individual in the graph, but ` +
|
|
1414
|
+
`${matched.length} inherits edge(s) name it as a supertype.`,
|
|
1415
|
+
`subclasses: ${total} total across ${levels.length} level(s).`,
|
|
1416
|
+
];
|
|
1417
|
+
levels.forEach((l, i) => lines.push(` depth ${i + 1} (${l.length}): ${capJoin(l, SUBCLASS_CAP)}`));
|
|
1418
|
+
return lines.join("\n");
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1337
1421
|
const ARCH_PKG_CAP = 25;
|
|
1338
1422
|
const ARCH_HUB_CAP = 15;
|
|
1339
1423
|
|
|
@@ -1520,13 +1604,24 @@ export function renderClassHistory(graph, ind) {
|
|
|
1520
1604
|
return renderSymbolHistory(graph, ind);
|
|
1521
1605
|
}
|
|
1522
1606
|
|
|
1523
|
-
// ---- symbol search (kind=function/class/method/attribute, with name/decorator filters)
|
|
1607
|
+
// ---- symbol search (kind=function/class/method/attribute/route, with name/decorator filters)
|
|
1524
1608
|
|
|
1525
|
-
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute" };
|
|
1609
|
+
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute", route: "Route" };
|
|
1610
|
+
|
|
1611
|
+
/** handler labels (with file:line where sited) a Route individual's `serves` edges point at —
|
|
1612
|
+
* a Route has no source span of its own, so the handler is what makes the line useful. */
|
|
1613
|
+
function routeHandlers(graph, servesEdges, routeId) {
|
|
1614
|
+
return servesEdges
|
|
1615
|
+
.filter((e) => e.subject === routeId)
|
|
1616
|
+
.map((e) => {
|
|
1617
|
+
const h = graph.byId.get(e.object);
|
|
1618
|
+
return `${e.objectLabel || h?.label || e.object}${spanTag(h ? siteOf(h) : null)}`;
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1526
1621
|
|
|
1527
1622
|
function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, nameRe }) {
|
|
1528
1623
|
const targetClass = SYMBOL_CLASSES[kind];
|
|
1529
|
-
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, or module).`;
|
|
1624
|
+
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, route, or module).`;
|
|
1530
1625
|
const hits = [];
|
|
1531
1626
|
for (const ind of graph.individuals) {
|
|
1532
1627
|
if ((ind.class || "") !== targetClass) continue;
|
|
@@ -1542,8 +1637,21 @@ function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, n
|
|
|
1542
1637
|
hits.sort((a, b) => b.score - a.score || String(a.ind.label).length - String(b.ind.label).length);
|
|
1543
1638
|
const top = hits.slice(0, limit);
|
|
1544
1639
|
const lines = [`${hits.length} ${kind}(s) match (top ${top.length}):`];
|
|
1545
|
-
|
|
1546
|
-
|
|
1640
|
+
// Routes were dead data (wh dogfood C9/C18/T3/T11): 22 Route individuals + serves edges in the
|
|
1641
|
+
// graph, reachable from no surface. A Route line shows verb + path template (its label) → the
|
|
1642
|
+
// handler symbol(s) its serves edges point at, so the hit is actionable without another call.
|
|
1643
|
+
const servesEdges = targetClass === "Route" ? edgesOfKind(graph, "serves") : null;
|
|
1644
|
+
for (const { ind } of top) {
|
|
1645
|
+
if (servesEdges) {
|
|
1646
|
+
const handlers = routeHandlers(graph, servesEdges, ind.id);
|
|
1647
|
+
lines.push(`- ${ind.label}${handlers.length ? ` → ${capJoin(handlers, 4)}` : " (no handler edge recorded)"}`);
|
|
1648
|
+
} else {
|
|
1649
|
+
lines.push(`- ${ind.label}${spanTag(siteOf(ind))}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
lines.push(servesEdges
|
|
1653
|
+
? "Then seonix_snippet <Class.method> for a handler body, or seonix_describe for a route's edges."
|
|
1654
|
+
: "Then seonix_snippet <name> for the exact body, or seonix_describe for its edges.");
|
|
1547
1655
|
return lines.join("\n");
|
|
1548
1656
|
}
|
|
1549
1657
|
|
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
|
}
|