@maxgfr/codeindex 2.11.0 → 2.11.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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # codeindex
2
2
 
3
+ [![Site](https://img.shields.io/badge/site-maxgfr.github.io%2Fcodeindex-2a78d6)](https://maxgfr.github.io/codeindex/)
4
+
3
5
  Self-contained, deterministic **repo-indexing engine**: file walking, language
4
6
  detection, symbol/import extraction (tree-sitter AST with a regex fallback),
5
7
  import resolution, a typed cross-file link-graph, and graph analytics — shipped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxgfr/codeindex",
3
- "version": "2.11.0",
3
+ "version": "2.11.1",
4
4
  "description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -1,6 +1,6 @@
1
- declare const ENGINE_VERSION = "2.11.0";
1
+ declare const ENGINE_VERSION = "2.11.1";
2
2
  declare const SCHEMA_VERSION = 4;
3
- declare const EXTRACTOR_VERSION = 7;
3
+ declare const EXTRACTOR_VERSION = 8;
4
4
  type FileKind = "code" | "doc" | "config" | "asset" | "other";
5
5
  type EdgeKind = "contains" | "doc-link" | "import" | "call" | "use" | "mention";
6
6
  type Tier = 0 | 1 | 2;
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.11.0";
17
+ ENGINE_VERSION = "2.11.1";
18
18
  SCHEMA_VERSION = 4;
19
- EXTRACTOR_VERSION = 7;
19
+ EXTRACTOR_VERSION = 8;
20
20
  }
21
21
  });
22
22
 
@@ -731,7 +731,57 @@ function scan(rel, content, lang, rules) {
731
731
  function extToLang(ext) {
732
732
  return EXT_LANG[ext] ?? "other";
733
733
  }
734
- var EXT_LANG;
734
+ function extractReexports(rel, content, localSymbols) {
735
+ if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
736
+ const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
737
+ const out2 = [];
738
+ const seen = /* @__PURE__ */ new Set();
739
+ const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
740
+ const localKindOf = /* @__PURE__ */ new Map();
741
+ for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
742
+ const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
743
+ let m;
744
+ while ((m = named.exec(content)) && out2.length < 60) {
745
+ const from = m[2];
746
+ for (const part of m[1].split(",")) {
747
+ const p = part.trim().replace(/^type\s+/, "");
748
+ const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
749
+ const orig = as ? as[1] : p;
750
+ const name2 = as ? as[2] : p;
751
+ if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
752
+ seen.add(name2);
753
+ const mirroredKind = !from ? localKindOf.get(orig) : void 0;
754
+ out2.push({
755
+ name: name2,
756
+ kind: mirroredKind ?? "reexport",
757
+ file: rel,
758
+ line: lineAt(m.index),
759
+ signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
760
+ exported: true,
761
+ lang
762
+ });
763
+ }
764
+ }
765
+ const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
766
+ while ((m = star.exec(content)) && out2.length < 60) {
767
+ const ns = m[1];
768
+ const from = m[2];
769
+ const key = "*" + (ns ?? from);
770
+ if (seen.has(key)) continue;
771
+ seen.add(key);
772
+ out2.push({
773
+ name: ns ?? `* (${from})`,
774
+ kind: ns ? "reexport" : "reexport-all",
775
+ file: rel,
776
+ line: lineAt(m.index),
777
+ signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
778
+ exported: true,
779
+ lang
780
+ });
781
+ }
782
+ return out2;
783
+ }
784
+ var EXT_LANG, REEXPORT_EXTS;
735
785
  var init_common = __esm({
736
786
  "src/lang/common.ts"() {
737
787
  "use strict";
@@ -799,6 +849,7 @@ var init_common = __esm({
799
849
  ".svelte": "svelte",
800
850
  ".astro": "astro"
801
851
  };
852
+ REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
802
853
  }
803
854
  });
804
855
 
@@ -1231,12 +1282,18 @@ var init_scala = __esm({
1231
1282
  // src/lang/registry.ts
1232
1283
  function extractSymbols(rel, ext, content) {
1233
1284
  const extractor = BY_EXT.get(ext);
1234
- if (!extractor) return [];
1235
- try {
1236
- return extractor.extract(rel, content);
1237
- } catch {
1238
- return [];
1285
+ let symbols;
1286
+ if (!extractor) symbols = [];
1287
+ else {
1288
+ try {
1289
+ symbols = extractor.extract(rel, content);
1290
+ } catch {
1291
+ symbols = [];
1292
+ }
1239
1293
  }
1294
+ const known = new Set(symbols.map((s) => s.name));
1295
+ const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
1296
+ return reexports.length ? [...symbols, ...reexports] : symbols;
1240
1297
  }
1241
1298
  function languageOf(ext) {
1242
1299
  return BY_EXT.get(ext)?.lang ?? extToLang(ext);
@@ -6340,58 +6397,9 @@ function extractImports(ext, content) {
6340
6397
  }
6341
6398
  return [...specs].map((spec) => ({ kind: "import", spec }));
6342
6399
  }
6343
- function extractReexports(rel, content, localSymbols) {
6344
- if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
6345
- const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
6346
- const out2 = [];
6347
- const seen = /* @__PURE__ */ new Set();
6348
- const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
6349
- const localKindOf = /* @__PURE__ */ new Map();
6350
- for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
6351
- const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
6352
- let m;
6353
- while ((m = named.exec(content)) && out2.length < 60) {
6354
- const from = m[2];
6355
- for (const part of m[1].split(",")) {
6356
- const p = part.trim().replace(/^type\s+/, "");
6357
- const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
6358
- const orig = as ? as[1] : p;
6359
- const name2 = as ? as[2] : p;
6360
- if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
6361
- seen.add(name2);
6362
- const mirroredKind = !from ? localKindOf.get(orig) : void 0;
6363
- out2.push({
6364
- name: name2,
6365
- kind: mirroredKind ?? "reexport",
6366
- file: rel,
6367
- line: lineAt(m.index),
6368
- signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
6369
- exported: true,
6370
- lang
6371
- });
6372
- }
6373
- }
6374
- const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
6375
- while ((m = star.exec(content)) && out2.length < 60) {
6376
- const ns = m[1];
6377
- const from = m[2];
6378
- const key = "*" + (ns ?? from);
6379
- if (seen.has(key)) continue;
6380
- seen.add(key);
6381
- out2.push({
6382
- name: ns ?? `* (${from})`,
6383
- kind: ns ? "reexport" : "reexport-all",
6384
- file: rel,
6385
- line: lineAt(m.index),
6386
- signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
6387
- exported: true,
6388
- lang
6389
- });
6390
- }
6391
- return out2;
6392
- }
6393
- function collectCallsRegex(content) {
6400
+ function collectCallsRegex(content, symbols = []) {
6394
6401
  const out2 = /* @__PURE__ */ new Map();
6402
+ const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
6395
6403
  const lines = content.split("\n");
6396
6404
  const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
6397
6405
  for (let i2 = 0; i2 < lines.length && out2.size < 512; i2++) {
@@ -6399,13 +6407,28 @@ function collectCallsRegex(content) {
6399
6407
  const trimmed = line.trimStart();
6400
6408
  if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
6401
6409
  CALL_RE.lastIndex = 0;
6410
+ let probe;
6411
+ const introducerCaught = /* @__PURE__ */ new Set();
6412
+ while ((probe = CALL_RE.exec(line)) !== null) {
6413
+ const name2 = probe[2];
6414
+ const key = `${name2} ${i2 + 1}`;
6415
+ if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
6416
+ }
6417
+ CALL_RE.lastIndex = 0;
6402
6418
  let m;
6419
+ const fallbackExcluded = /* @__PURE__ */ new Set();
6403
6420
  while ((m = CALL_RE.exec(line)) !== null && out2.size < 512) {
6404
6421
  const receiver = m[1];
6405
6422
  const name2 = m[2];
6406
6423
  if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
6407
6424
  if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
6408
6425
  const key = `${name2} ${i2 + 1}`;
6426
+ if (ownDefLines.has(key) && !introducerCaught.has(key)) {
6427
+ if (!fallbackExcluded.has(key)) {
6428
+ fallbackExcluded.add(key);
6429
+ continue;
6430
+ }
6431
+ }
6409
6432
  if (!out2.has(key)) out2.set(key, receiver ? { name: name2, line: i2 + 1, receiver } : { name: name2, line: i2 + 1 });
6410
6433
  }
6411
6434
  }
@@ -6426,7 +6449,9 @@ function extractCode(rel, ext, content) {
6426
6449
  idents: ast?.idents,
6427
6450
  // AST call sites when a grammar parsed the file; the conservative regex
6428
6451
  // collector otherwise, so caller indexes exist without the wasm sidecar.
6429
- calls: ast ? ast.calls : collectCallsRegex(content),
6452
+ // `symbols` (this file's own regex-extracted defs) lets the collector
6453
+ // exclude a definition's own name+line from its call candidates.
6454
+ calls: ast ? ast.calls : collectCallsRegex(content, symbols),
6430
6455
  importedNames: ast?.importedNames
6431
6456
  };
6432
6457
  }
@@ -6436,6 +6461,7 @@ var init_code = __esm({
6436
6461
  "use strict";
6437
6462
  init_registry();
6438
6463
  init_extract();
6464
+ init_common();
6439
6465
  JS_TS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
6440
6466
  PY = /* @__PURE__ */ new Set([".py", ".pyi"]);
6441
6467
  C_CPP = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"]);
@@ -1,6 +1,7 @@
1
1
  import type { CodeSymbol, RawRef } from "../types.js";
2
2
  import { extractSymbols } from "../lang/registry.js";
3
3
  import { extractAst } from "../ast/extract.js";
4
+ import { extractReexports } from "../lang/common.js";
4
5
 
5
6
  export interface CodeInfo {
6
7
  symbols: CodeSymbol[];
@@ -253,62 +254,6 @@ function extractImports(ext: string, content: string): RawRef[] {
253
254
  return [...specs].map((spec) => ({ kind: "import" as const, spec }));
254
255
  }
255
256
 
256
- // Barrel re-exports (`export { A, B as C } from './x'`, `export * from './y'`).
257
- // The line-based lang extractor can't capture multi-name lists, but these ARE
258
- // the public facade of a module — so list them as exported symbols here.
259
- //
260
- // An ALIAS with no `from` clause (`export { b as c }`) renames an in-file
261
- // declaration — `localSymbols` (already extracted by the AST or regex tier)
262
- // lets us resolve `b` and mirror ITS kind onto `c` (e.g. "function"), so the
263
- // alias reads as the real symbol it is rather than the generic "reexport".
264
- // A true cross-module re-export (`export { b as c } from "./mod"`) has no
265
- // local `b` to resolve — and an alias the local pass genuinely can't see
266
- // (destructured/ambient/etc.) falls back the same way — both keep "reexport".
267
- function extractReexports(rel: string, content: string, localSymbols: CodeSymbol[]): CodeSymbol[] {
268
- if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
269
- const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
270
- const out: CodeSymbol[] = [];
271
- const seen = new Set<string>();
272
- const lineAt = (idx: number): number => content.slice(0, idx).split(/\r?\n/).length;
273
- const localKindOf = new Map<string, string>();
274
- for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
275
-
276
- const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
277
- let m: RegExpExecArray | null;
278
- while ((m = named.exec(content)) && out.length < 60) {
279
- const from = m[2];
280
- for (const part of m[1]!.split(",")) {
281
- const p = part.trim().replace(/^type\s+/, "");
282
- const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
283
- const orig = as ? as[1]! : p;
284
- const name = as ? as[2]! : p;
285
- if (!/^[A-Za-z_$][\w$]*$/.test(name) || name === "default" || seen.has(name)) continue;
286
- seen.add(name);
287
- const mirroredKind = !from ? localKindOf.get(orig) : undefined;
288
- out.push({
289
- name, kind: mirroredKind ?? "reexport", file: rel, line: lineAt(m.index),
290
- signature: from ? `export { ${name} } from "${from}"` : `export { ${name} }`,
291
- exported: true, lang,
292
- });
293
- }
294
- }
295
-
296
- const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
297
- while ((m = star.exec(content)) && out.length < 60) {
298
- const ns = m[1];
299
- const from = m[2]!;
300
- const key = "*" + (ns ?? from);
301
- if (seen.has(key)) continue;
302
- seen.add(key);
303
- out.push({
304
- name: ns ?? `* (${from})`, kind: ns ? "reexport" : "reexport-all", file: rel,
305
- line: lineAt(m.index), signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
306
- exported: true, lang,
307
- });
308
- }
309
- return out;
310
- }
311
-
312
257
  // Control-flow and declaration keywords that syntactically precede `(` but are
313
258
  // never call targets — the union across supported languages. Deliberately does
314
259
  // NOT list real builtins (python's `print`, go's `make`…): a false call to a
@@ -331,8 +276,35 @@ const DEF_INTRODUCERS = /(?:\bfunction|\bdef|\bfunc|\bfun|\bfn|\bclass|\bsub|\bm
331
276
  // immediate `receiver.` prefix is captured too (`axios.get(` → receiver
332
277
  // "axios"; `a.b.c(` → receiver "b" — the group anchors to the segment right
333
278
  // before the called name); bare calls carry no receiver.
334
- function collectCallsRegex(content: string): { name: string; line: number; receiver?: string }[] {
279
+ //
280
+ // `symbols` is the file's OWN regex-extracted symbols (name + definition
281
+ // line). DEF_INTRODUCERS already excludes definitions that read `function
282
+ // foo(`/`def foo(`/etc. (per OCCURRENCE, wherever on the line it sits), but
283
+ // C/C++ function definitions have no such introducer (`void load(void) {`) —
284
+ // the bare name reads exactly like a call to itself on its own definition
285
+ // line. For those, a call candidate whose (name, line) exactly matches one of
286
+ // `symbols` is excluded too — but ONLY its first (leftmost) occurrence on that
287
+ // line, never every same-named occurrence: dense/minified one-liners can pack
288
+ // a genuine call to the same name on the same physical line as its own
289
+ // definition (`function aa(){}function bb(){return aa()+cc()}function cc(){}`
290
+ // — bb's calls to aa() and cc() must survive), and even a bodyless
291
+ // single-line recursive definition (`function foo(){foo();}`) has a real self
292
+ // -call to keep. Two-tier: if ANY occurrence of a def'd name on this line is
293
+ // already caught by DEF_INTRODUCERS (JS/Python/…), that occurrence alone is
294
+ // excluded (existing per-occurrence check below) and no further exclusion is
295
+ // applied — every OTHER occurrence is a genuine call. Only when NO occurrence
296
+ // carries an introducer (C/C++) does this fall back to excluding just the
297
+ // first occurrence: C/C++'s own definition regex requires column 0, so on a
298
+ // line where it matches at all, the first occurrence IS that definition.
299
+ // Exported for direct testing (extraction-v8.test.ts): once a wasm sidecar/
300
+ // grammar is loaded, extractCode never reaches this path for C/C++, so tests
301
+ // exercise it directly rather than through extractCode.
302
+ export function collectCallsRegex(
303
+ content: string,
304
+ symbols: Pick<CodeSymbol, "name" | "line">[] = [],
305
+ ): { name: string; line: number; receiver?: string }[] {
335
306
  const out = new Map<string, { name: string; line: number; receiver?: string }>();
307
+ const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
336
308
  const lines = content.split("\n");
337
309
  const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
338
310
  for (let i = 0; i < lines.length && out.size < 512; i++) {
@@ -342,14 +314,36 @@ function collectCallsRegex(content: string): { name: string; line: number; recei
342
314
  // regexes — noise resolves to nothing in the global call pass).
343
315
  const trimmed = line.trimStart();
344
316
  if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
317
+
318
+ // Pass 1: which own-def keys on this line have at least one occurrence
319
+ // DEF_INTRODUCERS already catches? Those are fully handled per-occurrence
320
+ // below — no fallback exclusion needed (or wanted) for them.
321
+ CALL_RE.lastIndex = 0;
322
+ let probe: RegExpExecArray | null;
323
+ const introducerCaught = new Set<string>();
324
+ while ((probe = CALL_RE.exec(line)) !== null) {
325
+ const name = probe[2]!;
326
+ const key = `${name} ${i + 1}`;
327
+ if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
328
+ }
329
+
330
+ // Pass 2: the real collection. Own-def keys with no introducer occurrence
331
+ // fall back to excluding just their first occurrence on the line.
345
332
  CALL_RE.lastIndex = 0;
346
333
  let m: RegExpExecArray | null;
334
+ const fallbackExcluded = new Set<string>();
347
335
  while ((m = CALL_RE.exec(line)) !== null && out.size < 512) {
348
336
  const receiver = m[1];
349
337
  const name = m[2]!;
350
338
  if (name.length < 2 || CALL_KEYWORDS.has(name)) continue;
351
339
  if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
352
340
  const key = `${name} ${i + 1}`;
341
+ if (ownDefLines.has(key) && !introducerCaught.has(key)) {
342
+ if (!fallbackExcluded.has(key)) {
343
+ fallbackExcluded.add(key);
344
+ continue;
345
+ }
346
+ }
353
347
  if (!out.has(key)) out.set(key, receiver ? { name, line: i + 1, receiver } : { name, line: i + 1 });
354
348
  }
355
349
  }
@@ -382,7 +376,9 @@ export function extractCode(rel: string, ext: string, content: string): CodeInfo
382
376
  idents: ast?.idents,
383
377
  // AST call sites when a grammar parsed the file; the conservative regex
384
378
  // collector otherwise, so caller indexes exist without the wasm sidecar.
385
- calls: ast ? ast.calls : collectCallsRegex(content),
379
+ // `symbols` (this file's own regex-extracted defs) lets the collector
380
+ // exclude a definition's own name+line from its call candidates.
381
+ calls: ast ? ast.calls : collectCallsRegex(content, symbols),
386
382
  importedNames: ast?.importedNames,
387
383
  };
388
384
  }
@@ -66,3 +66,67 @@ const EXT_LANG: Record<string, string> = {
66
66
  export function extToLang(ext: string): string {
67
67
  return EXT_LANG[ext] ?? "other";
68
68
  }
69
+
70
+ const REEXPORT_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
71
+
72
+ // Barrel re-exports (`export { A, B as C } from './x'`, `export * from './y'`).
73
+ // The line-based lang extractor can't capture multi-name lists, but these ARE
74
+ // the public facade of a module — so list them as exported symbols here.
75
+ //
76
+ // An ALIAS with no `from` clause (`export { b as c }`) renames an in-file
77
+ // declaration — `localSymbols` (already extracted by the AST or regex tier)
78
+ // lets us resolve `b` and mirror ITS kind onto `c` (e.g. "function"), so the
79
+ // alias reads as the real symbol it is rather than the generic "reexport".
80
+ // A true cross-module re-export (`export { b as c } from "./mod"`) has no
81
+ // local `b` to resolve — and an alias the local pass genuinely can't see
82
+ // (destructured/ambient/etc.) falls back the same way — both keep "reexport".
83
+ //
84
+ // Shared by extractCode (extract/code.ts) AND the standalone extractSymbols
85
+ // (lang/registry.ts) — ultradoc and other direct extractSymbols consumers hit
86
+ // the same barrels a repo scan does, so both entry points must agree; this is
87
+ // the one place the alias-mirroring logic lives, reused rather than
88
+ // reimplemented on either side.
89
+ export function extractReexports(rel: string, content: string, localSymbols: CodeSymbol[]): CodeSymbol[] {
90
+ if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
91
+ const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
92
+ const out: CodeSymbol[] = [];
93
+ const seen = new Set<string>();
94
+ const lineAt = (idx: number): number => content.slice(0, idx).split(/\r?\n/).length;
95
+ const localKindOf = new Map<string, string>();
96
+ for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
97
+
98
+ const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
99
+ let m: RegExpExecArray | null;
100
+ while ((m = named.exec(content)) && out.length < 60) {
101
+ const from = m[2];
102
+ for (const part of m[1]!.split(",")) {
103
+ const p = part.trim().replace(/^type\s+/, "");
104
+ const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
105
+ const orig = as ? as[1]! : p;
106
+ const name = as ? as[2]! : p;
107
+ if (!/^[A-Za-z_$][\w$]*$/.test(name) || name === "default" || seen.has(name)) continue;
108
+ seen.add(name);
109
+ const mirroredKind = !from ? localKindOf.get(orig) : undefined;
110
+ out.push({
111
+ name, kind: mirroredKind ?? "reexport", file: rel, line: lineAt(m.index),
112
+ signature: from ? `export { ${name} } from "${from}"` : `export { ${name} }`,
113
+ exported: true, lang,
114
+ });
115
+ }
116
+ }
117
+
118
+ const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
119
+ while ((m = star.exec(content)) && out.length < 60) {
120
+ const ns = m[1];
121
+ const from = m[2]!;
122
+ const key = "*" + (ns ?? from);
123
+ if (seen.has(key)) continue;
124
+ seen.add(key);
125
+ out.push({
126
+ name: ns ?? `* (${from})`, kind: ns ? "reexport" : "reexport-all", file: rel,
127
+ line: lineAt(m.index), signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
128
+ exported: true, lang,
129
+ });
130
+ }
131
+ return out;
132
+ }
@@ -1,5 +1,5 @@
1
1
  import type { CodeSymbol } from "../types.js";
2
- import { extToLang } from "./common.js";
2
+ import { extToLang, extractReexports } from "./common.js";
3
3
  import { jsTs } from "./js-ts.js";
4
4
  import { python } from "./python.js";
5
5
  import { go } from "./go.js";
@@ -35,14 +35,29 @@ for (const e of EXTRACTORS) for (const ext of e.exts) BY_EXT.set(ext, e);
35
35
 
36
36
  // Extract declared symbols from one file. Returns [] for languages without a
37
37
  // dedicated extractor (their content is still fully searchable via ripgrep).
38
+ //
39
+ // Also mirrors extractCode's barrel re-export/alias pass (extractReexports):
40
+ // a direct extractSymbols consumer (ultradoc, or any tool that doesn't go
41
+ // through extractCode) hits the same `export { a, b as c }` barrels a repo
42
+ // scan does, and should see the same alias-mirrors-b's-own-kind symbols —
43
+ // not the bare "reexport" kind extractReexports falls back to when there's
44
+ // no local declaration to mirror. extractCode already runs extractReexports
45
+ // itself and filters by name, so this can't double-add: whichever entry point
46
+ // adds "c" first wins, the other's re-run is a no-op duplicate filtered out.
38
47
  export function extractSymbols(rel: string, ext: string, content: string): CodeSymbol[] {
39
48
  const extractor = BY_EXT.get(ext);
40
- if (!extractor) return [];
41
- try {
42
- return extractor.extract(rel, content);
43
- } catch {
44
- return [];
49
+ let symbols: CodeSymbol[];
50
+ if (!extractor) symbols = [];
51
+ else {
52
+ try {
53
+ symbols = extractor.extract(rel, content);
54
+ } catch {
55
+ symbols = [];
56
+ }
45
57
  }
58
+ const known = new Set(symbols.map((s) => s.name));
59
+ const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
60
+ return reexports.length ? [...symbols, ...reexports] : symbols;
46
61
  }
47
62
 
48
63
  // Human-readable language label for an extension (used for the language
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Single source of truth for the engine version the bundle reports. Kept in
2
2
  // lockstep with package.json by the release pipeline. Do not edit by hand
3
3
  // outside a release.
4
- export const ENGINE_VERSION = "2.11.0";
4
+ export const ENGINE_VERSION = "2.11.1";
5
5
 
6
6
  // Bumped whenever the on-disk artifact shape changes, so a consumer can reject
7
7
  // an index written by an incompatible engine instead of misreading it. The
@@ -27,8 +27,12 @@ export const SCHEMA_VERSION = 4;
27
27
  // named after the file stem, `export default Foo` marking the declaration);
28
28
  // v7 makes an export-alias symbol (`export { b as c }`) mirror the aliased
29
29
  // local declaration's own kind (e.g. "function") instead of the generic
30
- // "reexport" when it resolves in-file.
31
- export const EXTRACTOR_VERSION = 7;
30
+ // "reexport" when it resolves in-file. v8 fixes the C/C++ regex tier
31
+ // reporting a function DEFINITION as a call to itself (`void load(void) {`
32
+ // yielding a spurious call `load@<defline>`, found during the ultrasec
33
+ // consumer migration) by excluding call candidates whose name+line match one
34
+ // of the file's own extracted symbols.
35
+ export const EXTRACTOR_VERSION = 8;
32
36
 
33
37
  // How a file is classified. `code` gets symbol/import extraction; `doc` gets
34
38
  // link/heading extraction; the rest are catalogued but not deeply parsed.