@wrongstack/tools 0.299.0 → 0.301.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/dist/audit.js +6 -2
- package/dist/bash.js +6 -2
- package/dist/batch-tool-use.js +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/builtin.d.ts +3 -2
- package/dist/builtin.js +1781 -376
- package/dist/codebase-index/bm25.d.ts +7 -1
- package/dist/codebase-index/import-extractor.d.ts +39 -0
- package/dist/codebase-index/index.js +1418 -267
- package/dist/codebase-index/languages.d.ts +24 -0
- package/dist/codebase-index/module-resolver.d.ts +78 -0
- package/dist/codebase-index/module-roots.d.ts +81 -0
- package/dist/codebase-index/parser-output.d.ts +29 -0
- package/dist/codebase-index/project-server.js +1402 -249
- package/dist/codebase-index/rs-parser.d.ts +22 -0
- package/dist/codebase-index/schema.d.ts +24 -1
- package/dist/codebase-index/worker.js +1401 -250
- package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
- package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
- package/dist/codebase-index/writer-schema.d.ts +15 -3
- package/dist/codebase-index/writer.d.ts +76 -3
- package/dist/exec.js +35 -2
- package/dist/format.js +6 -2
- package/dist/git.js +2 -5
- package/dist/glob.js +2 -2
- package/dist/grep.js +118 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1925 -415
- package/dist/install.js +6 -2
- package/dist/json.js +132 -2
- package/dist/languages/index.js +6 -2
- package/dist/lint.js +6 -2
- package/dist/logs.js +81 -0
- package/dist/next-steps-tool.d.ts +26 -0
- package/dist/outdated.js +6 -2
- package/dist/pack.js +1781 -376
- package/dist/patch.js +206 -45
- package/dist/process-registry.d.ts +6 -0
- package/dist/process-registry.js +6 -2
- package/dist/ps-slash.js +6 -2
- package/dist/read.js +1410 -257
- package/dist/replace.js +81 -0
- package/dist/skill.js +51 -2
- package/dist/test.js +6 -2
- package/dist/tool-help.js +2 -2
- package/dist/tool-search.js +1 -1
- package/dist/tool-tier.d.ts +1 -1
- package/dist/tool-tier.js +1786 -397
- package/dist/tool-use.js +1 -1
- package/dist/tree.js +13 -3
- package/dist/typecheck.js +6 -2
- package/package.json +3 -3
- package/dist/codebase-index/refs-extractor.d.ts +0 -11
|
@@ -27,7 +27,10 @@ function detectLang(file) {
|
|
|
27
27
|
if (!ext) return null;
|
|
28
28
|
return EXT_TO_LANG[ext] ?? null;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
function languageFamily(lang) {
|
|
31
|
+
return LANG_FAMILY[lang] ?? "other";
|
|
32
|
+
}
|
|
33
|
+
var EXT_TO_LANG, INDEXABLE_EXTENSIONS, SPECIAL_FILENAMES, LANG_FAMILY, LANG_FAMILY_ENTRIES;
|
|
31
34
|
var init_languages = __esm({
|
|
32
35
|
"src/codebase-index/languages.ts"() {
|
|
33
36
|
"use strict";
|
|
@@ -118,6 +121,52 @@ var init_languages = __esm({
|
|
|
118
121
|
procfile: "other",
|
|
119
122
|
justfile: "other"
|
|
120
123
|
};
|
|
124
|
+
LANG_FAMILY = {
|
|
125
|
+
// Single-compilation-unit family: a .vue/.svelte script block is JS/TS and
|
|
126
|
+
// imports from — and is imported by — plain .ts files.
|
|
127
|
+
ts: "js",
|
|
128
|
+
tsx: "js",
|
|
129
|
+
js: "js",
|
|
130
|
+
jsx: "js",
|
|
131
|
+
vue: "js",
|
|
132
|
+
svelte: "js",
|
|
133
|
+
go: "go",
|
|
134
|
+
py: "py",
|
|
135
|
+
rs: "rs",
|
|
136
|
+
// The JVM resolves across languages: Kotlin and Scala call Java directly.
|
|
137
|
+
java: "jvm",
|
|
138
|
+
kotlin: "jvm",
|
|
139
|
+
scala: "jvm",
|
|
140
|
+
csharp: "dotnet",
|
|
141
|
+
// A .h header is consumed by both C and C++ translation units.
|
|
142
|
+
c: "c",
|
|
143
|
+
cpp: "c",
|
|
144
|
+
ruby: "ruby",
|
|
145
|
+
php: "php",
|
|
146
|
+
swift: "swift",
|
|
147
|
+
dart: "dart",
|
|
148
|
+
elixir: "elixir",
|
|
149
|
+
haskell: "haskell",
|
|
150
|
+
zig: "zig",
|
|
151
|
+
lua: "lua",
|
|
152
|
+
r: "r",
|
|
153
|
+
shell: "shell",
|
|
154
|
+
sql: "sql",
|
|
155
|
+
json: "data",
|
|
156
|
+
yaml: "data",
|
|
157
|
+
toml: "data",
|
|
158
|
+
html: "web",
|
|
159
|
+
css: "web",
|
|
160
|
+
proto: "proto",
|
|
161
|
+
graphql: "graphql",
|
|
162
|
+
md: "other",
|
|
163
|
+
other: "other"
|
|
164
|
+
};
|
|
165
|
+
LANG_FAMILY_ENTRIES = Object.freeze(
|
|
166
|
+
Object.entries(LANG_FAMILY).map(
|
|
167
|
+
([lang, family]) => Object.freeze([lang, family])
|
|
168
|
+
)
|
|
169
|
+
);
|
|
121
170
|
}
|
|
122
171
|
});
|
|
123
172
|
|
|
@@ -282,7 +331,7 @@ function getTypeName(name) {
|
|
|
282
331
|
function deduplicateRefs(refs) {
|
|
283
332
|
const seen = /* @__PURE__ */ new Set();
|
|
284
333
|
return refs.filter((r) => {
|
|
285
|
-
const key = `${r.toName}:${r.callType}:${r.line}`;
|
|
334
|
+
const key = `${r.toName}:${r.callType}:${r.line}:${r.module ?? ""}`;
|
|
286
335
|
if (seen.has(key)) return false;
|
|
287
336
|
seen.add(key);
|
|
288
337
|
return true;
|
|
@@ -292,10 +341,16 @@ function getImportSpecifierName(spec) {
|
|
|
292
341
|
return spec.propertyName?.text ?? spec.name.text;
|
|
293
342
|
}
|
|
294
343
|
function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
344
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
295
345
|
const clause = node.importClause;
|
|
296
|
-
if (!clause)
|
|
346
|
+
if (!clause) {
|
|
347
|
+
if (module) {
|
|
348
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
297
352
|
if (clause.name) {
|
|
298
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
353
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
299
354
|
}
|
|
300
355
|
const bindings = clause.namedBindings;
|
|
301
356
|
if (!bindings) return;
|
|
@@ -305,26 +360,40 @@ function emitImportSpecifierRefs(node, refs, lineNum) {
|
|
|
305
360
|
fromId: 0,
|
|
306
361
|
toName: getImportSpecifierName(element),
|
|
307
362
|
callType: "import",
|
|
308
|
-
line: lineNum
|
|
363
|
+
line: lineNum,
|
|
364
|
+
module
|
|
309
365
|
});
|
|
310
366
|
}
|
|
311
367
|
} else if (ts.isNamespaceImport(bindings)) {
|
|
312
|
-
refs.push({
|
|
368
|
+
refs.push({
|
|
369
|
+
fromId: 0,
|
|
370
|
+
toName: bindings.name.text,
|
|
371
|
+
callType: "import",
|
|
372
|
+
line: lineNum,
|
|
373
|
+
module
|
|
374
|
+
});
|
|
313
375
|
}
|
|
314
376
|
}
|
|
377
|
+
function moduleSpecifierOf(node) {
|
|
378
|
+
return node && ts.isStringLiteral(node) ? node.text : void 0;
|
|
379
|
+
}
|
|
315
380
|
function emitExportSpecifierRefs(node, refs, lineNum) {
|
|
381
|
+
const module = moduleSpecifierOf(node.moduleSpecifier);
|
|
316
382
|
const clause = node.exportClause;
|
|
317
383
|
if (clause && ts.isNamespaceExport(clause)) {
|
|
318
|
-
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum });
|
|
384
|
+
refs.push({ fromId: 0, toName: clause.name.text, callType: "import", line: lineNum, module });
|
|
319
385
|
return;
|
|
320
386
|
}
|
|
321
387
|
if (clause && ts.isNamedExports(clause)) {
|
|
322
388
|
for (const element of clause.elements) {
|
|
323
389
|
const originalName = element.propertyName?.text ?? element.name.text;
|
|
324
|
-
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum });
|
|
390
|
+
refs.push({ fromId: 0, toName: originalName, callType: "import", line: lineNum, module });
|
|
325
391
|
}
|
|
326
392
|
return;
|
|
327
393
|
}
|
|
394
|
+
if (module) {
|
|
395
|
+
refs.push({ fromId: 0, toName: module, callType: "import", line: lineNum, module });
|
|
396
|
+
}
|
|
328
397
|
}
|
|
329
398
|
var ts, tsLoad, kindMapCache;
|
|
330
399
|
var init_ts_parser = __esm({
|
|
@@ -337,21 +406,21 @@ var init_ts_parser = __esm({
|
|
|
337
406
|
});
|
|
338
407
|
|
|
339
408
|
// src/_win32-resolve.ts
|
|
340
|
-
import * as
|
|
341
|
-
import * as
|
|
409
|
+
import * as fs3 from "node:fs";
|
|
410
|
+
import * as path5 from "node:path";
|
|
342
411
|
function resolveWin32Command(cmd) {
|
|
343
412
|
if (process.platform !== "win32") return cmd;
|
|
344
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
413
|
+
if (cmd.includes("/") || cmd.includes("\\") || path5.extname(cmd.replace(/\//g, "\\"))) {
|
|
345
414
|
return cmd;
|
|
346
415
|
}
|
|
347
416
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
348
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
417
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path5.delimiter);
|
|
349
418
|
for (const dir of pathDirs) {
|
|
350
|
-
const base =
|
|
419
|
+
const base = path5.join(dir, cmd);
|
|
351
420
|
for (const ext of pathext) {
|
|
352
421
|
const full = `${base}${ext}`;
|
|
353
422
|
try {
|
|
354
|
-
|
|
423
|
+
fs3.accessSync(full, fs3.constants.X_OK);
|
|
355
424
|
return full;
|
|
356
425
|
} catch {
|
|
357
426
|
}
|
|
@@ -365,6 +434,82 @@ var init_win32_resolve = __esm({
|
|
|
365
434
|
}
|
|
366
435
|
});
|
|
367
436
|
|
|
437
|
+
// src/codebase-index/parser-output.ts
|
|
438
|
+
function coerceSymbols(value) {
|
|
439
|
+
if (!Array.isArray(value)) return [];
|
|
440
|
+
return value.flatMap((entry) => {
|
|
441
|
+
const candidate = entry;
|
|
442
|
+
if (typeof candidate.name !== "string" || typeof candidate.kind !== "string") return [];
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
name: candidate.name,
|
|
446
|
+
kind: candidate.kind,
|
|
447
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
448
|
+
col: typeof candidate.col === "number" ? candidate.col : 0,
|
|
449
|
+
signature: typeof candidate.signature === "string" ? candidate.signature : "",
|
|
450
|
+
scope: typeof candidate.scope === "string" ? candidate.scope : ""
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function coerceRefs(value, lang) {
|
|
456
|
+
if (!Array.isArray(value)) return [];
|
|
457
|
+
return value.flatMap((entry) => {
|
|
458
|
+
const candidate = entry;
|
|
459
|
+
if (typeof candidate.toName !== "string" || !candidate.toName) return [];
|
|
460
|
+
if (typeof candidate.callType !== "string" || !CALL_TYPES.has(candidate.callType)) return [];
|
|
461
|
+
const module = typeof candidate.module === "string" && candidate.module ? candidate.module : void 0;
|
|
462
|
+
return [
|
|
463
|
+
{
|
|
464
|
+
fromId: 0,
|
|
465
|
+
toName: candidate.toName,
|
|
466
|
+
callType: candidate.callType,
|
|
467
|
+
line: typeof candidate.line === "number" ? candidate.line : 1,
|
|
468
|
+
lang,
|
|
469
|
+
module
|
|
470
|
+
}
|
|
471
|
+
];
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function parseParserOutput(stdout, lang) {
|
|
475
|
+
const trimmed = stdout.trim();
|
|
476
|
+
if (!trimmed) return { symbols: [], refs: [] };
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(trimmed);
|
|
480
|
+
} catch {
|
|
481
|
+
return { symbols: [], refs: [] };
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(parsed)) return { symbols: coerceSymbols(parsed), refs: [] };
|
|
484
|
+
const record = parsed;
|
|
485
|
+
return {
|
|
486
|
+
symbols: coerceSymbols(record.symbols),
|
|
487
|
+
refs: dedupeRefs(coerceRefs(record.refs, lang))
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function dedupeRefs(refs) {
|
|
491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
492
|
+
return refs.filter((ref) => {
|
|
493
|
+
const key = `${ref.toName}:${ref.callType}:${ref.line}:${ref.module ?? ""}`;
|
|
494
|
+
if (seen.has(key)) return false;
|
|
495
|
+
seen.add(key);
|
|
496
|
+
return true;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
var CALL_TYPES;
|
|
500
|
+
var init_parser_output = __esm({
|
|
501
|
+
"src/codebase-index/parser-output.ts"() {
|
|
502
|
+
"use strict";
|
|
503
|
+
CALL_TYPES = /* @__PURE__ */ new Set([
|
|
504
|
+
"call",
|
|
505
|
+
"type_ref",
|
|
506
|
+
"inherit",
|
|
507
|
+
"implement",
|
|
508
|
+
"import"
|
|
509
|
+
]);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
368
513
|
// src/codebase-index/spawn-gate.ts
|
|
369
514
|
function withSpawnGate(fn) {
|
|
370
515
|
const run = chain.then(fn, fn);
|
|
@@ -390,8 +535,8 @@ __export(go_parser_exports, {
|
|
|
390
535
|
});
|
|
391
536
|
import { spawn } from "node:child_process";
|
|
392
537
|
import * as os from "node:os";
|
|
393
|
-
import * as
|
|
394
|
-
import * as
|
|
538
|
+
import * as path6 from "node:path";
|
|
539
|
+
import * as fs4 from "node:fs/promises";
|
|
395
540
|
async function parseSymbols2(opts) {
|
|
396
541
|
const { file, content, lang } = opts;
|
|
397
542
|
try {
|
|
@@ -399,7 +544,8 @@ async function parseSymbols2(opts) {
|
|
|
399
544
|
if (parsed.symbols.length > 0) {
|
|
400
545
|
return parsed;
|
|
401
546
|
}
|
|
402
|
-
|
|
547
|
+
const fallback = fallbackParse(file, content, lang);
|
|
548
|
+
return parsed.refs?.length ? { ...fallback, refs: parsed.refs } : fallback;
|
|
403
549
|
} catch {
|
|
404
550
|
return fallbackParse(file, content, lang);
|
|
405
551
|
}
|
|
@@ -463,9 +609,9 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
463
609
|
try {
|
|
464
610
|
let scriptPath = _cachedGoScriptPath;
|
|
465
611
|
if (!scriptPath) {
|
|
466
|
-
const tmpDir = await
|
|
467
|
-
scriptPath =
|
|
468
|
-
await
|
|
612
|
+
const tmpDir = await fs4.mkdtemp(path6.join(os.tmpdir(), "ws-go-parse-"));
|
|
613
|
+
scriptPath = path6.join(tmpDir, "parse.go");
|
|
614
|
+
await fs4.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
469
615
|
_cachedGoScriptPath = scriptPath;
|
|
470
616
|
}
|
|
471
617
|
const goBinary = resolveWin32Command("go");
|
|
@@ -507,8 +653,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
507
653
|
if (code !== 0 || !stdout.trim()) {
|
|
508
654
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
509
655
|
}
|
|
510
|
-
const
|
|
511
|
-
const symbols =
|
|
656
|
+
const { symbols: rawSymbols, refs } = parseParserOutput(stdout, lang);
|
|
657
|
+
const symbols = rawSymbols.map((s) => ({
|
|
512
658
|
id: 0,
|
|
513
659
|
lang,
|
|
514
660
|
kind: s.kind,
|
|
@@ -521,7 +667,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
521
667
|
scope: s.scope ?? "",
|
|
522
668
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
523
669
|
}));
|
|
524
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
670
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
525
671
|
} catch {
|
|
526
672
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
527
673
|
}
|
|
@@ -531,6 +677,7 @@ var init_go_parser = __esm({
|
|
|
531
677
|
"src/codebase-index/go-parser.ts"() {
|
|
532
678
|
"use strict";
|
|
533
679
|
init_win32_resolve();
|
|
680
|
+
init_parser_output();
|
|
534
681
|
init_spawn_gate();
|
|
535
682
|
init_languages();
|
|
536
683
|
GO_PARSE_SCRIPT = `
|
|
@@ -544,6 +691,7 @@ import (
|
|
|
544
691
|
"go/token"
|
|
545
692
|
"io"
|
|
546
693
|
"os"
|
|
694
|
+
"strconv"
|
|
547
695
|
"strings"
|
|
548
696
|
)
|
|
549
697
|
|
|
@@ -556,16 +704,34 @@ type Sym struct {
|
|
|
556
704
|
Scope string \`json:"scope"\`
|
|
557
705
|
}
|
|
558
706
|
|
|
707
|
+
// Ref is a cross-reference emitted alongside the symbols, so one \`go run\`
|
|
708
|
+
// yields both. Module is the import path for CallType "import", else empty.
|
|
709
|
+
type Ref struct {
|
|
710
|
+
ToName string \`json:"toName"\`
|
|
711
|
+
CallType string \`json:"callType"\`
|
|
712
|
+
Line int \`json:"line"\`
|
|
713
|
+
Module string \`json:"module"\`
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
type Result struct {
|
|
717
|
+
Symbols []Sym \`json:"symbols"\`
|
|
718
|
+
Refs []Ref \`json:"refs"\`
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
func emptyResult() string {
|
|
722
|
+
return "{\\"symbols\\":[],\\"refs\\":[]}"
|
|
723
|
+
}
|
|
724
|
+
|
|
559
725
|
func main() {
|
|
560
726
|
src, err := io.ReadAll(os.Stdin)
|
|
561
727
|
if err != nil {
|
|
562
|
-
fmt.Print(
|
|
728
|
+
fmt.Print(emptyResult())
|
|
563
729
|
return
|
|
564
730
|
}
|
|
565
731
|
fset := token.NewFileSet()
|
|
566
732
|
node, err := parser.ParseFile(fset, "src.go", src, 0)
|
|
567
733
|
if err != nil {
|
|
568
|
-
fmt.Print(
|
|
734
|
+
fmt.Print(emptyResult())
|
|
569
735
|
return
|
|
570
736
|
}
|
|
571
737
|
|
|
@@ -629,9 +795,43 @@ func main() {
|
|
|
629
795
|
}
|
|
630
796
|
}
|
|
631
797
|
|
|
632
|
-
|
|
798
|
+
refs := []Ref{}
|
|
799
|
+
ast.Inspect(node, func(n ast.Node) bool {
|
|
800
|
+
switch expr := n.(type) {
|
|
801
|
+
case *ast.CallExpr:
|
|
802
|
+
line := fset.Position(expr.Pos()).Line
|
|
803
|
+
switch fun := expr.Fun.(type) {
|
|
804
|
+
case *ast.Ident:
|
|
805
|
+
refs = append(refs, Ref{ToName: fun.Name, CallType: "call", Line: line})
|
|
806
|
+
case *ast.SelectorExpr:
|
|
807
|
+
// Record the selected name (\`Join\` of \`filepath.Join\`): it is the
|
|
808
|
+
// declared symbol name, so it resolves the same way the TypeScript
|
|
809
|
+
// and Python extractors' call refs do.
|
|
810
|
+
refs = append(refs, Ref{ToName: fun.Sel.Name, CallType: "call", Line: line})
|
|
811
|
+
}
|
|
812
|
+
case *ast.ImportSpec:
|
|
813
|
+
if expr.Path != nil {
|
|
814
|
+
if importPath, uerr := strconv.Unquote(expr.Path.Value); uerr == nil {
|
|
815
|
+
line := fset.Position(expr.Pos()).Line
|
|
816
|
+
// A Go import names a package, not a symbol; the package's
|
|
817
|
+
// last path segment is the name it is referenced by.
|
|
818
|
+
name := importPath
|
|
819
|
+
if idx := strings.LastIndex(importPath, "/"); idx >= 0 {
|
|
820
|
+
name = importPath[idx+1:]
|
|
821
|
+
}
|
|
822
|
+
refs = append(refs, Ref{ToName: name, CallType: "import", Line: line, Module: importPath})
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return true
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
if syms == nil {
|
|
830
|
+
syms = []Sym{}
|
|
831
|
+
}
|
|
832
|
+
data, err := json.Marshal(Result{Symbols: syms, Refs: refs})
|
|
633
833
|
if err != nil {
|
|
634
|
-
fmt.Print(
|
|
834
|
+
fmt.Print(emptyResult())
|
|
635
835
|
return
|
|
636
836
|
}
|
|
637
837
|
fmt.Print(string(data))
|
|
@@ -792,16 +992,23 @@ function looksBinary(content) {
|
|
|
792
992
|
}
|
|
793
993
|
return bad / sample.length > 0.1;
|
|
794
994
|
}
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
let
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
995
|
+
function newlineOffsets2(content) {
|
|
996
|
+
const offsets = [];
|
|
997
|
+
for (let i = 0; i < content.length; i++) {
|
|
998
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
999
|
+
}
|
|
1000
|
+
return offsets;
|
|
1001
|
+
}
|
|
1002
|
+
function lineColAt(offsets, index) {
|
|
1003
|
+
let low = 0;
|
|
1004
|
+
let high = offsets.length;
|
|
1005
|
+
while (low < high) {
|
|
1006
|
+
const mid = low + high >>> 1;
|
|
1007
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
1008
|
+
else high = mid;
|
|
803
1009
|
}
|
|
804
|
-
|
|
1010
|
+
const lastNl = low > 0 ? offsets[low - 1] : -1;
|
|
1011
|
+
return { line: low + 1, col: index - lastNl };
|
|
805
1012
|
}
|
|
806
1013
|
function parseGeneric(opts) {
|
|
807
1014
|
const { file, lang } = opts;
|
|
@@ -814,6 +1021,7 @@ function parseGeneric(opts) {
|
|
|
814
1021
|
const patterns = patternsFor(lang);
|
|
815
1022
|
const symbols = [];
|
|
816
1023
|
const seen = /* @__PURE__ */ new Set();
|
|
1024
|
+
const nlOffsets = newlineOffsets2(content);
|
|
817
1025
|
for (const pattern of patterns) {
|
|
818
1026
|
const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
|
|
819
1027
|
re.lastIndex = 0;
|
|
@@ -827,7 +1035,7 @@ function parseGeneric(opts) {
|
|
|
827
1035
|
if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
|
|
828
1036
|
continue;
|
|
829
1037
|
}
|
|
830
|
-
const { line, col } = lineColAt(
|
|
1038
|
+
const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
|
|
831
1039
|
const key = `${name}\0${line}\0${pattern.kind}`;
|
|
832
1040
|
if (seen.has(key)) continue;
|
|
833
1041
|
seen.add(key);
|
|
@@ -983,9 +1191,13 @@ var init_generic_parser = __esm({
|
|
|
983
1191
|
],
|
|
984
1192
|
elixir: [
|
|
985
1193
|
{ re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
|
|
986
|
-
|
|
1194
|
+
// Dotted module names must be captured whole: `alias Foo.Bar` resolves
|
|
1195
|
+
// against this symbol, and a `Foo`-only capture never matches it.
|
|
1196
|
+
{ re: /\bdefmodule\s+([A-Z][\w.]*)/g, kind: "namespace" }
|
|
987
1197
|
],
|
|
988
1198
|
haskell: [
|
|
1199
|
+
// Target of `import Data.List`.
|
|
1200
|
+
{ re: /^module\s+([A-Z][\w.]*)/gm, kind: "namespace" },
|
|
989
1201
|
{ re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
|
|
990
1202
|
{ re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
|
|
991
1203
|
{ re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
|
|
@@ -1076,9 +1288,9 @@ __export(py_parser_exports, {
|
|
|
1076
1288
|
parseSymbols: () => parseSymbols4
|
|
1077
1289
|
});
|
|
1078
1290
|
import { spawn as spawn2 } from "node:child_process";
|
|
1079
|
-
import * as
|
|
1291
|
+
import * as fs5 from "node:fs/promises";
|
|
1080
1292
|
import * as os2 from "node:os";
|
|
1081
|
-
import * as
|
|
1293
|
+
import * as path7 from "node:path";
|
|
1082
1294
|
async function parseSymbols4(opts) {
|
|
1083
1295
|
const { file, content, lang } = opts;
|
|
1084
1296
|
try {
|
|
@@ -1156,10 +1368,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
1156
1368
|
async function syncPyParse(filePath, content, lang) {
|
|
1157
1369
|
try {
|
|
1158
1370
|
if (!_cachedScriptPath) {
|
|
1159
|
-
const tmpDir =
|
|
1160
|
-
await
|
|
1161
|
-
_cachedScriptPath =
|
|
1162
|
-
await
|
|
1371
|
+
const tmpDir = path7.join(os2.tmpdir(), "ws-py-parse");
|
|
1372
|
+
await fs5.mkdir(tmpDir, { recursive: true });
|
|
1373
|
+
_cachedScriptPath = path7.join(tmpDir, "parse.py");
|
|
1374
|
+
await fs5.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
1163
1375
|
}
|
|
1164
1376
|
cachedPyBinary ??= resolvePython();
|
|
1165
1377
|
const pyBinary = await cachedPyBinary;
|
|
@@ -1173,7 +1385,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1173
1385
|
if (code !== 0 || !stdout.trim()) {
|
|
1174
1386
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1175
1387
|
}
|
|
1176
|
-
const raw =
|
|
1388
|
+
const { symbols: raw, refs } = parseParserOutput(stdout, lang);
|
|
1177
1389
|
const symbols = raw.map((s) => ({
|
|
1178
1390
|
id: 0,
|
|
1179
1391
|
lang,
|
|
@@ -1187,7 +1399,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1187
1399
|
scope: s.scope ?? "",
|
|
1188
1400
|
text: `${s.name} ${s.signature ?? ""}`.trim()
|
|
1189
1401
|
}));
|
|
1190
|
-
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
1402
|
+
return { file: filePath, lang, symbols, refs, mtimeMs: Date.now() };
|
|
1191
1403
|
} catch {
|
|
1192
1404
|
return null;
|
|
1193
1405
|
}
|
|
@@ -1198,6 +1410,7 @@ var init_py_parser = __esm({
|
|
|
1198
1410
|
"use strict";
|
|
1199
1411
|
init_win32_resolve();
|
|
1200
1412
|
init_generic_parser();
|
|
1413
|
+
init_parser_output();
|
|
1201
1414
|
init_spawn_gate();
|
|
1202
1415
|
init_languages();
|
|
1203
1416
|
PY_PARSE_SCRIPT = `import ast, json, sys, os
|
|
@@ -1259,7 +1472,18 @@ class Sym:
|
|
|
1259
1472
|
def is_private(name):
|
|
1260
1473
|
return name.startswith("__") and not name.endswith("__")
|
|
1261
1474
|
|
|
1475
|
+
def leaf_name(node):
|
|
1476
|
+
# Declared name of the callee: \`join\` of \`os.path.join\`. Matches how the
|
|
1477
|
+
# TypeScript and Go extractors record call refs, so resolution behaves the
|
|
1478
|
+
# same across languages.
|
|
1479
|
+
if isinstance(node, ast.Attribute):
|
|
1480
|
+
return node.attr
|
|
1481
|
+
if isinstance(node, ast.Name):
|
|
1482
|
+
return node.id
|
|
1483
|
+
return get_name(node).split(".")[-1]
|
|
1484
|
+
|
|
1262
1485
|
syms = []
|
|
1486
|
+
refs = []
|
|
1263
1487
|
errors = []
|
|
1264
1488
|
|
|
1265
1489
|
try:
|
|
@@ -1267,7 +1491,7 @@ try:
|
|
|
1267
1491
|
tree = ast.parse(source, filename=sys.argv[1])
|
|
1268
1492
|
except Exception as e:
|
|
1269
1493
|
errors.append(str(e))
|
|
1270
|
-
print("[]")
|
|
1494
|
+
print(json.dumps({"symbols": [], "refs": []}))
|
|
1271
1495
|
sys.exit(0)
|
|
1272
1496
|
|
|
1273
1497
|
# Module-level scope
|
|
@@ -1401,7 +1625,42 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
1401
1625
|
visitor = ModuleVisitor()
|
|
1402
1626
|
visitor.visit(tree)
|
|
1403
1627
|
|
|
1404
|
-
|
|
1628
|
+
# Refs need a separate full walk: ModuleVisitor deliberately does not descend
|
|
1629
|
+
# into function bodies (it would index locals as symbols), but that is exactly
|
|
1630
|
+
# where the calls are.
|
|
1631
|
+
for node in ast.walk(tree):
|
|
1632
|
+
if isinstance(node, ast.Call):
|
|
1633
|
+
name = leaf_name(node.func)
|
|
1634
|
+
if name:
|
|
1635
|
+
refs.append({"toName": name, "callType": "call", "line": node.lineno})
|
|
1636
|
+
elif isinstance(node, ast.Import):
|
|
1637
|
+
for alias in node.names:
|
|
1638
|
+
refs.append({
|
|
1639
|
+
"toName": alias.name.split(".")[-1],
|
|
1640
|
+
"callType": "import",
|
|
1641
|
+
"line": node.lineno,
|
|
1642
|
+
"module": alias.name,
|
|
1643
|
+
})
|
|
1644
|
+
elif isinstance(node, ast.ImportFrom):
|
|
1645
|
+
# PEP 328: node.level is the number of leading dots. Preserving them is
|
|
1646
|
+
# what lets the resolver walk up from the importing file's package \u2014
|
|
1647
|
+
# dropping them made \`from .foo import X\` indistinguishable from an
|
|
1648
|
+
# absolute \`foo\`.
|
|
1649
|
+
module = ("." * (node.level or 0)) + (node.module or "")
|
|
1650
|
+
for alias in node.names:
|
|
1651
|
+
refs.append({
|
|
1652
|
+
"toName": alias.name,
|
|
1653
|
+
"callType": "import",
|
|
1654
|
+
"line": node.lineno,
|
|
1655
|
+
"module": module,
|
|
1656
|
+
})
|
|
1657
|
+
elif isinstance(node, ast.ClassDef):
|
|
1658
|
+
for base in node.bases:
|
|
1659
|
+
name = leaf_name(base)
|
|
1660
|
+
if name:
|
|
1661
|
+
refs.append({"toName": name, "callType": "inherit", "line": node.lineno})
|
|
1662
|
+
|
|
1663
|
+
print(json.dumps({"symbols": [s.to_dict() for s in syms], "refs": refs}))
|
|
1405
1664
|
`;
|
|
1406
1665
|
_cachedScriptPath = null;
|
|
1407
1666
|
}
|
|
@@ -1414,107 +1673,10 @@ __export(rs_parser_exports, {
|
|
|
1414
1673
|
parseSymbols: () => parseSymbols5
|
|
1415
1674
|
});
|
|
1416
1675
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
1417
|
-
import { execFile, spawn as spawn3 } from "node:child_process";
|
|
1418
|
-
import * as fs5 from "node:fs/promises";
|
|
1419
|
-
import * as path6 from "node:path";
|
|
1420
1676
|
async function parseSymbols5(opts) {
|
|
1421
1677
|
const { file, content, lang } = opts;
|
|
1422
|
-
const nativeAvailable = await checkNativeParser();
|
|
1423
|
-
if (nativeAvailable) {
|
|
1424
|
-
const result = await withSpawnGate(() => tryNativeParse(file, content));
|
|
1425
|
-
if (result) return result;
|
|
1426
|
-
}
|
|
1427
1678
|
return regexParse({ file, content, lang });
|
|
1428
1679
|
}
|
|
1429
|
-
function probe(command, args) {
|
|
1430
|
-
return new Promise((resolve2, reject) => {
|
|
1431
|
-
execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
|
|
1432
|
-
if (error) reject(error);
|
|
1433
|
-
else resolve2();
|
|
1434
|
-
});
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
function checkNativeParser() {
|
|
1438
|
-
nativeParserAvailability ??= (async () => {
|
|
1439
|
-
try {
|
|
1440
|
-
await probe("rustc", ["--version"]);
|
|
1441
|
-
const toolsDir = path6.join(process.cwd(), "tools");
|
|
1442
|
-
await probe(
|
|
1443
|
-
"cargo",
|
|
1444
|
-
[
|
|
1445
|
-
"metadata",
|
|
1446
|
-
"--no-deps",
|
|
1447
|
-
"--format-version",
|
|
1448
|
-
"1",
|
|
1449
|
-
"--manifest-path",
|
|
1450
|
-
path6.join(toolsDir, "Cargo.toml")
|
|
1451
|
-
]
|
|
1452
|
-
);
|
|
1453
|
-
return true;
|
|
1454
|
-
} catch {
|
|
1455
|
-
return false;
|
|
1456
|
-
}
|
|
1457
|
-
})();
|
|
1458
|
-
return nativeParserAvailability;
|
|
1459
|
-
}
|
|
1460
|
-
async function tryNativeParse(file, content) {
|
|
1461
|
-
try {
|
|
1462
|
-
const toolsDir = path6.join(process.cwd(), "tools");
|
|
1463
|
-
const crateDir = path6.join(toolsDir, "syn-parser");
|
|
1464
|
-
const tmpFile = path6.join(crateDir, "src", "input.rs");
|
|
1465
|
-
await fs5.writeFile(tmpFile, content, "utf8");
|
|
1466
|
-
const cargoBinary = resolveWin32Command("cargo");
|
|
1467
|
-
const result = await new Promise(
|
|
1468
|
-
(resolve2, reject) => {
|
|
1469
|
-
let settled = false;
|
|
1470
|
-
const proc = spawn3(
|
|
1471
|
-
cargoBinary,
|
|
1472
|
-
["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
|
|
1473
|
-
{
|
|
1474
|
-
cwd: process.cwd(),
|
|
1475
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1476
|
-
windowsHide: true
|
|
1477
|
-
}
|
|
1478
|
-
);
|
|
1479
|
-
proc.on("error", (err) => {
|
|
1480
|
-
if (settled) return;
|
|
1481
|
-
settled = true;
|
|
1482
|
-
reject(err);
|
|
1483
|
-
});
|
|
1484
|
-
let stdout2 = "";
|
|
1485
|
-
proc.stdout?.on("data", (chunk) => {
|
|
1486
|
-
stdout2 += chunk.toString();
|
|
1487
|
-
});
|
|
1488
|
-
proc.stderr?.resume();
|
|
1489
|
-
const timer = setTimeout(() => {
|
|
1490
|
-
if (settled) return;
|
|
1491
|
-
settled = true;
|
|
1492
|
-
proc.kill("SIGKILL");
|
|
1493
|
-
reject(new Error("timeout"));
|
|
1494
|
-
}, 15e3);
|
|
1495
|
-
timer.unref?.();
|
|
1496
|
-
proc.on("close", (c) => {
|
|
1497
|
-
if (settled) return;
|
|
1498
|
-
settled = true;
|
|
1499
|
-
clearTimeout(timer);
|
|
1500
|
-
resolve2({ code: c, stdout: stdout2 });
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
);
|
|
1504
|
-
const { code, stdout } = result;
|
|
1505
|
-
if (code === 0 && stdout.trim()) {
|
|
1506
|
-
const symbols = JSON.parse(stdout.trim());
|
|
1507
|
-
return {
|
|
1508
|
-
file,
|
|
1509
|
-
lang: "rs",
|
|
1510
|
-
symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
|
|
1511
|
-
mtimeMs: Date.now()
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
} catch {
|
|
1515
|
-
}
|
|
1516
|
-
return null;
|
|
1517
|
-
}
|
|
1518
1680
|
function regexParse(opts) {
|
|
1519
1681
|
const { file, content, lang } = opts;
|
|
1520
1682
|
const symbols = [];
|
|
@@ -1570,12 +1732,10 @@ function regexParse(opts) {
|
|
|
1570
1732
|
});
|
|
1571
1733
|
return { file, lang, symbols: deduped, mtimeMs: Date.now() };
|
|
1572
1734
|
}
|
|
1573
|
-
var
|
|
1735
|
+
var RS_PATTERNS;
|
|
1574
1736
|
var init_rs_parser = __esm({
|
|
1575
1737
|
"src/codebase-index/rs-parser.ts"() {
|
|
1576
1738
|
"use strict";
|
|
1577
|
-
init_win32_resolve();
|
|
1578
|
-
init_spawn_gate();
|
|
1579
1739
|
init_languages();
|
|
1580
1740
|
RS_PATTERNS = [
|
|
1581
1741
|
{ regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
|
|
@@ -1598,7 +1758,7 @@ __export(json_parser_exports, {
|
|
|
1598
1758
|
parseSymbols: () => parseSymbols6
|
|
1599
1759
|
});
|
|
1600
1760
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
1601
|
-
import * as
|
|
1761
|
+
import * as path8 from "node:path";
|
|
1602
1762
|
function parseSymbols6(opts) {
|
|
1603
1763
|
const { file, content, lang } = opts;
|
|
1604
1764
|
try {
|
|
@@ -1610,7 +1770,7 @@ function parseSymbols6(opts) {
|
|
|
1610
1770
|
function regexParse2(opts) {
|
|
1611
1771
|
const { file, content, lang } = opts;
|
|
1612
1772
|
const symbols = [];
|
|
1613
|
-
const basename4 =
|
|
1773
|
+
const basename4 = path8.basename(file).toLowerCase();
|
|
1614
1774
|
const isPackageJson = basename4 === "package.json";
|
|
1615
1775
|
const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
|
|
1616
1776
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
@@ -1636,11 +1796,11 @@ function regexParse2(opts) {
|
|
|
1636
1796
|
const line = lineFromOffset(offset);
|
|
1637
1797
|
symbols.push(
|
|
1638
1798
|
makeSymbol({
|
|
1639
|
-
name:
|
|
1799
|
+
name: path8.basename(file),
|
|
1640
1800
|
kind: "object",
|
|
1641
1801
|
line,
|
|
1642
1802
|
col: 0,
|
|
1643
|
-
signature: `"${
|
|
1803
|
+
signature: `"${path8.basename(file)}" = { ... }`,
|
|
1644
1804
|
file,
|
|
1645
1805
|
lang
|
|
1646
1806
|
})
|
|
@@ -1972,7 +2132,7 @@ import { parentPort } from "node:worker_threads";
|
|
|
1972
2132
|
|
|
1973
2133
|
// src/codebase-index/indexer.ts
|
|
1974
2134
|
import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
|
|
1975
|
-
import { execFile
|
|
2135
|
+
import { execFile } from "node:child_process";
|
|
1976
2136
|
import * as fs8 from "node:fs/promises";
|
|
1977
2137
|
import { availableParallelism } from "node:os";
|
|
1978
2138
|
import * as path11 from "node:path";
|
|
@@ -2040,8 +2200,738 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
2040
2200
|
// src/codebase-index/indexer.ts
|
|
2041
2201
|
init_languages();
|
|
2042
2202
|
|
|
2203
|
+
// src/codebase-index/module-resolver.ts
|
|
2204
|
+
init_languages();
|
|
2205
|
+
import * as path4 from "node:path";
|
|
2206
|
+
|
|
2207
|
+
// src/codebase-index/module-roots.ts
|
|
2208
|
+
init_languages();
|
|
2209
|
+
import * as fs2 from "node:fs/promises";
|
|
2210
|
+
import * as path3 from "node:path";
|
|
2211
|
+
function toPortablePath(file) {
|
|
2212
|
+
return file.replace(/\\/g, "/");
|
|
2213
|
+
}
|
|
2214
|
+
async function readTextIfPresent(file) {
|
|
2215
|
+
try {
|
|
2216
|
+
return await fs2.readFile(file, "utf8");
|
|
2217
|
+
} catch {
|
|
2218
|
+
return void 0;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
function parsePackageJsonName(source) {
|
|
2222
|
+
try {
|
|
2223
|
+
const parsed = JSON.parse(source);
|
|
2224
|
+
return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
|
|
2225
|
+
} catch {
|
|
2226
|
+
return void 0;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function parseGoModulePath(source) {
|
|
2230
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2231
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
2232
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
2233
|
+
if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
|
|
2234
|
+
}
|
|
2235
|
+
return void 0;
|
|
2236
|
+
}
|
|
2237
|
+
function parseTomlTableName(source, tables) {
|
|
2238
|
+
let current = "";
|
|
2239
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
2240
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
2241
|
+
if (line.startsWith("[[")) {
|
|
2242
|
+
current = "\0";
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
const table = /^\[([^\]]+)\]$/.exec(line);
|
|
2246
|
+
if (table?.[1]) {
|
|
2247
|
+
current = table[1].trim();
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
if (!tables.includes(current)) continue;
|
|
2251
|
+
const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
|
|
2252
|
+
if (match?.[1]) return match[1];
|
|
2253
|
+
}
|
|
2254
|
+
return void 0;
|
|
2255
|
+
}
|
|
2256
|
+
function parsePomArtifactId(source) {
|
|
2257
|
+
const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
2258
|
+
return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
|
|
2259
|
+
}
|
|
2260
|
+
var LANGS_BY_KIND = {
|
|
2261
|
+
npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
|
|
2262
|
+
cargo: ["rs"],
|
|
2263
|
+
go: ["go"],
|
|
2264
|
+
python: ["py"],
|
|
2265
|
+
maven: ["java", "kotlin", "scala"],
|
|
2266
|
+
gradle: ["java", "kotlin", "scala"],
|
|
2267
|
+
dotnet: ["csharp"]
|
|
2268
|
+
};
|
|
2269
|
+
function ancestorsOf(dir, stopAt) {
|
|
2270
|
+
const out = [];
|
|
2271
|
+
let current = dir;
|
|
2272
|
+
for (; ; ) {
|
|
2273
|
+
out.push(current);
|
|
2274
|
+
if (current === stopAt || current.length <= stopAt.length) break;
|
|
2275
|
+
const parent = path3.posix.dirname(current);
|
|
2276
|
+
if (parent === current) break;
|
|
2277
|
+
current = parent;
|
|
2278
|
+
}
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
var MARKER_PROBES = [
|
|
2282
|
+
{
|
|
2283
|
+
kind: "npm",
|
|
2284
|
+
file: "package.json",
|
|
2285
|
+
build: (dir, source) => {
|
|
2286
|
+
const name = parsePackageJsonName(source) ?? path3.posix.basename(dir);
|
|
2287
|
+
return { name, importPath: name, sourceRoots: [dir] };
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
kind: "cargo",
|
|
2292
|
+
file: "Cargo.toml",
|
|
2293
|
+
build: (dir, source) => {
|
|
2294
|
+
const name = parseTomlTableName(source, ["package"]);
|
|
2295
|
+
if (!name) return void 0;
|
|
2296
|
+
return {
|
|
2297
|
+
name: `crate:${name}`,
|
|
2298
|
+
// Rust paths use underscores where crate names often use dashes.
|
|
2299
|
+
importPath: name.replace(/-/g, "_"),
|
|
2300
|
+
sourceRoots: [path3.posix.join(dir, "src")]
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
{
|
|
2305
|
+
kind: "go",
|
|
2306
|
+
file: "go.mod",
|
|
2307
|
+
build: (dir, source) => {
|
|
2308
|
+
const modulePath = parseGoModulePath(source);
|
|
2309
|
+
if (!modulePath) return void 0;
|
|
2310
|
+
return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
kind: "python",
|
|
2315
|
+
file: "pyproject.toml",
|
|
2316
|
+
build: (dir, source) => {
|
|
2317
|
+
const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path3.posix.basename(dir);
|
|
2318
|
+
return {
|
|
2319
|
+
name: `py:${name}`,
|
|
2320
|
+
importPath: void 0,
|
|
2321
|
+
// `src/` layout is the packaging-guide default; the root itself covers
|
|
2322
|
+
// the flat layout. Both are probed, missing ones simply never match.
|
|
2323
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
kind: "python",
|
|
2329
|
+
file: "setup.py",
|
|
2330
|
+
build: (dir) => ({
|
|
2331
|
+
name: `py:${path3.posix.basename(dir)}`,
|
|
2332
|
+
importPath: void 0,
|
|
2333
|
+
sourceRoots: [path3.posix.join(dir, "src"), dir]
|
|
2334
|
+
})
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
kind: "maven",
|
|
2338
|
+
file: "pom.xml",
|
|
2339
|
+
build: (dir, source) => {
|
|
2340
|
+
const artifactId = parsePomArtifactId(source) ?? path3.posix.basename(dir);
|
|
2341
|
+
return {
|
|
2342
|
+
name: `mvn:${artifactId}`,
|
|
2343
|
+
importPath: void 0,
|
|
2344
|
+
sourceRoots: [
|
|
2345
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2346
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2347
|
+
path3.posix.join(dir, "src/main/scala"),
|
|
2348
|
+
path3.posix.join(dir, "src/test/java")
|
|
2349
|
+
]
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
kind: "gradle",
|
|
2355
|
+
file: "build.gradle",
|
|
2356
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
kind: "gradle",
|
|
2360
|
+
file: "build.gradle.kts",
|
|
2361
|
+
build: (dir) => buildGradleRoot(dir)
|
|
2362
|
+
}
|
|
2363
|
+
];
|
|
2364
|
+
function buildGradleRoot(dir) {
|
|
2365
|
+
return {
|
|
2366
|
+
name: `gradle:${path3.posix.basename(dir)}`,
|
|
2367
|
+
importPath: void 0,
|
|
2368
|
+
sourceRoots: [
|
|
2369
|
+
path3.posix.join(dir, "src/main/java"),
|
|
2370
|
+
path3.posix.join(dir, "src/main/kotlin"),
|
|
2371
|
+
path3.posix.join(dir, "src/main/scala")
|
|
2372
|
+
]
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
async function probeDotnetRoot(dir) {
|
|
2376
|
+
let entries;
|
|
2377
|
+
try {
|
|
2378
|
+
entries = await fs2.readdir(dir);
|
|
2379
|
+
} catch {
|
|
2380
|
+
return void 0;
|
|
2381
|
+
}
|
|
2382
|
+
const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
|
|
2383
|
+
if (!project) return void 0;
|
|
2384
|
+
const name = project.slice(0, -".csproj".length);
|
|
2385
|
+
return {
|
|
2386
|
+
dir,
|
|
2387
|
+
kind: "dotnet",
|
|
2388
|
+
name: `csproj:${name}`,
|
|
2389
|
+
importPath: void 0,
|
|
2390
|
+
sourceRoots: [dir]
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
async function detectModuleRoots(projectRoot, files) {
|
|
2394
|
+
const root = toPortablePath(projectRoot).replace(/\/+$/, "");
|
|
2395
|
+
const langsByDir = /* @__PURE__ */ new Map();
|
|
2396
|
+
for (const file of files) {
|
|
2397
|
+
const portable = toPortablePath(file);
|
|
2398
|
+
const lang = detectLang(portable);
|
|
2399
|
+
if (!lang) continue;
|
|
2400
|
+
const dir = path3.posix.dirname(portable);
|
|
2401
|
+
let langs = langsByDir.get(dir);
|
|
2402
|
+
if (!langs) {
|
|
2403
|
+
langs = /* @__PURE__ */ new Set();
|
|
2404
|
+
langsByDir.set(dir, langs);
|
|
2405
|
+
}
|
|
2406
|
+
langs.add(lang);
|
|
2407
|
+
}
|
|
2408
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const [dir, langs] of langsByDir) {
|
|
2410
|
+
for (const ancestor of ancestorsOf(dir, root)) {
|
|
2411
|
+
let merged = candidates.get(ancestor);
|
|
2412
|
+
if (!merged) {
|
|
2413
|
+
merged = /* @__PURE__ */ new Set();
|
|
2414
|
+
candidates.set(ancestor, merged);
|
|
2415
|
+
}
|
|
2416
|
+
for (const lang of langs) merged.add(lang);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
const roots = [];
|
|
2420
|
+
await Promise.all(
|
|
2421
|
+
[...candidates].map(async ([dir, langs]) => {
|
|
2422
|
+
for (const probe of MARKER_PROBES) {
|
|
2423
|
+
if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
|
|
2424
|
+
const source = await readTextIfPresent(path3.posix.join(dir, probe.file));
|
|
2425
|
+
if (source === void 0) continue;
|
|
2426
|
+
const built = probe.build(dir, source);
|
|
2427
|
+
if (built) roots.push({ dir, kind: probe.kind, ...built });
|
|
2428
|
+
}
|
|
2429
|
+
if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
|
|
2430
|
+
const dotnet = await probeDotnetRoot(dir);
|
|
2431
|
+
if (dotnet) roots.push(dotnet);
|
|
2432
|
+
}
|
|
2433
|
+
})
|
|
2434
|
+
);
|
|
2435
|
+
roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
2436
|
+
return { projectRoot: root, roots };
|
|
2437
|
+
}
|
|
2438
|
+
function findOwningRoot(structure, file, kinds) {
|
|
2439
|
+
const portable = toPortablePath(file);
|
|
2440
|
+
for (const root of structure.roots) {
|
|
2441
|
+
if (kinds && !kinds.includes(root.kind)) continue;
|
|
2442
|
+
if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
|
|
2443
|
+
}
|
|
2444
|
+
return void 0;
|
|
2445
|
+
}
|
|
2446
|
+
function derivePackageFromLayout(filePath) {
|
|
2447
|
+
const portable = toPortablePath(filePath);
|
|
2448
|
+
const packagesIdx = portable.indexOf("/packages/");
|
|
2449
|
+
if (packagesIdx !== -1) {
|
|
2450
|
+
const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
|
|
2451
|
+
if (segment) return `@wrongstack/${segment}`;
|
|
2452
|
+
}
|
|
2453
|
+
const appsIdx = portable.indexOf("/apps/");
|
|
2454
|
+
if (appsIdx !== -1) {
|
|
2455
|
+
const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
|
|
2456
|
+
if (segment) return `app:${segment}`;
|
|
2457
|
+
}
|
|
2458
|
+
return void 0;
|
|
2459
|
+
}
|
|
2460
|
+
function pythonPackageLabel(structure, file, initDirs) {
|
|
2461
|
+
const portable = toPortablePath(file);
|
|
2462
|
+
const dir = path3.posix.dirname(portable);
|
|
2463
|
+
if (!initDirs.has(dir)) return void 0;
|
|
2464
|
+
const segments = [];
|
|
2465
|
+
let current = dir;
|
|
2466
|
+
while (initDirs.has(current) && current.length > structure.projectRoot.length) {
|
|
2467
|
+
segments.unshift(path3.posix.basename(current));
|
|
2468
|
+
current = path3.posix.dirname(current);
|
|
2469
|
+
}
|
|
2470
|
+
return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
|
|
2471
|
+
}
|
|
2472
|
+
function assignPackageLabels(structure, files) {
|
|
2473
|
+
const initDirs = /* @__PURE__ */ new Set();
|
|
2474
|
+
for (const file of files) {
|
|
2475
|
+
const portable = toPortablePath(file);
|
|
2476
|
+
if (path3.posix.basename(portable) === "__init__.py") {
|
|
2477
|
+
initDirs.add(path3.posix.dirname(portable));
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2481
|
+
for (const file of files) {
|
|
2482
|
+
const portable = toPortablePath(file);
|
|
2483
|
+
const lang = detectLang(portable);
|
|
2484
|
+
if (lang === "go") {
|
|
2485
|
+
const owner2 = findOwningRoot(structure, portable, ["go"]);
|
|
2486
|
+
const dir = path3.posix.dirname(portable);
|
|
2487
|
+
if (owner2?.importPath) {
|
|
2488
|
+
const relative2 = path3.posix.relative(owner2.dir, dir);
|
|
2489
|
+
labels.set(file, relative2 ? `${owner2.importPath}/${relative2}` : owner2.importPath);
|
|
2490
|
+
} else {
|
|
2491
|
+
labels.set(file, `go:${path3.posix.relative(structure.projectRoot, dir) || "."}`);
|
|
2492
|
+
}
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
if (lang === "py") {
|
|
2496
|
+
const dotted = pythonPackageLabel(structure, portable, initDirs);
|
|
2497
|
+
if (dotted) {
|
|
2498
|
+
labels.set(file, dotted);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const owner = findOwningRoot(structure, portable);
|
|
2503
|
+
const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
|
|
2504
|
+
labels.set(file, label);
|
|
2505
|
+
}
|
|
2506
|
+
return labels;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/codebase-index/module-resolver.ts
|
|
2510
|
+
var EXTENSIONS = {
|
|
2511
|
+
js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
|
|
2512
|
+
py: [".py", ".pyi"],
|
|
2513
|
+
rs: [".rs"],
|
|
2514
|
+
jvm: [".java", ".kt", ".scala"],
|
|
2515
|
+
c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
|
|
2516
|
+
ruby: [".rb"],
|
|
2517
|
+
go: [".go"]
|
|
2518
|
+
};
|
|
2519
|
+
var DIRECTORY_ENTRIES = {
|
|
2520
|
+
js: ["index"],
|
|
2521
|
+
py: ["__init__"],
|
|
2522
|
+
rs: ["mod"],
|
|
2523
|
+
ruby: ["index"]
|
|
2524
|
+
};
|
|
2525
|
+
function normalizeNamespace(value) {
|
|
2526
|
+
return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
|
|
2527
|
+
}
|
|
2528
|
+
var ModuleResolver = class {
|
|
2529
|
+
structure;
|
|
2530
|
+
/** Lowercased portable path → the path as indexed (case is preserved). */
|
|
2531
|
+
byPath;
|
|
2532
|
+
/** Lowercased portable directory → files directly inside it, as indexed. */
|
|
2533
|
+
byDir;
|
|
2534
|
+
/** Normalized namespace → the file declaring it (first by path, stable). */
|
|
2535
|
+
byNamespace;
|
|
2536
|
+
constructor(structure, files, namespaces = []) {
|
|
2537
|
+
this.structure = structure;
|
|
2538
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
2539
|
+
this.byDir = /* @__PURE__ */ new Map();
|
|
2540
|
+
this.byNamespace = /* @__PURE__ */ new Map();
|
|
2541
|
+
const dirsByKey = /* @__PURE__ */ new Map();
|
|
2542
|
+
for (const file of files) {
|
|
2543
|
+
const portable = toPortablePath(file);
|
|
2544
|
+
const pathKey = portable.toLowerCase();
|
|
2545
|
+
const priorPath = this.byPath.get(pathKey);
|
|
2546
|
+
if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
|
|
2547
|
+
else this.byPath.set(pathKey, file);
|
|
2548
|
+
const dir = path4.posix.dirname(portable);
|
|
2549
|
+
const dirKey = dir.toLowerCase();
|
|
2550
|
+
const knownDir = dirsByKey.get(dirKey);
|
|
2551
|
+
if (knownDir === void 0) {
|
|
2552
|
+
dirsByKey.set(dirKey, dir);
|
|
2553
|
+
this.byDir.set(dirKey, [file]);
|
|
2554
|
+
} else if (knownDir === dir) {
|
|
2555
|
+
this.byDir.get(dirKey)?.push(file);
|
|
2556
|
+
} else {
|
|
2557
|
+
dirsByKey.delete(dirKey);
|
|
2558
|
+
this.byDir.delete(dirKey);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
for (const { name, file } of namespaces) {
|
|
2562
|
+
const lang = detectLang(file);
|
|
2563
|
+
if (!lang) continue;
|
|
2564
|
+
const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
|
|
2565
|
+
if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
|
|
2566
|
+
this.byNamespace.set(key, file);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Resolve `specifier` as written in `fromFile`.
|
|
2572
|
+
* Returns the indexed target path, or `undefined` when it is external or
|
|
2573
|
+
* cannot be located.
|
|
2574
|
+
*/
|
|
2575
|
+
resolve(fromFile, lang, specifier) {
|
|
2576
|
+
const spec = specifier.trim().replace(/\\/g, "/");
|
|
2577
|
+
if (!spec) return void 0;
|
|
2578
|
+
const from = toPortablePath(fromFile);
|
|
2579
|
+
switch (languageFamily(lang)) {
|
|
2580
|
+
case "js":
|
|
2581
|
+
return this.resolveJs(from, spec);
|
|
2582
|
+
case "go":
|
|
2583
|
+
return this.resolveGo(spec);
|
|
2584
|
+
case "py":
|
|
2585
|
+
return this.resolvePython(from, spec);
|
|
2586
|
+
case "rs":
|
|
2587
|
+
return this.resolveRust(from, spec);
|
|
2588
|
+
case "jvm":
|
|
2589
|
+
return this.resolveJvm(spec);
|
|
2590
|
+
case "c":
|
|
2591
|
+
return this.resolveInclude(from, spec);
|
|
2592
|
+
case "ruby":
|
|
2593
|
+
return this.resolveRuby(from, spec);
|
|
2594
|
+
case "dotnet":
|
|
2595
|
+
case "php":
|
|
2596
|
+
case "elixir":
|
|
2597
|
+
case "haskell":
|
|
2598
|
+
return this.resolveNamespace(lang, spec);
|
|
2599
|
+
default:
|
|
2600
|
+
return void 0;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Resolve a namespace specifier to the file declaring it.
|
|
2605
|
+
*
|
|
2606
|
+
* Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
|
|
2607
|
+
* names a namespace outright, while PHP's `use App\Models\User` names a
|
|
2608
|
+
* *class* inside `App\Models`, so the prefix is what was declared.
|
|
2609
|
+
*/
|
|
2610
|
+
resolveNamespace(lang, spec) {
|
|
2611
|
+
const family = languageFamily(lang);
|
|
2612
|
+
const normalized = normalizeNamespace(spec);
|
|
2613
|
+
const exact = this.byNamespace.get(`${family}:${normalized}`);
|
|
2614
|
+
if (exact) return exact;
|
|
2615
|
+
const segments = normalized.split(".").filter(Boolean);
|
|
2616
|
+
if (segments.length < 2) return void 0;
|
|
2617
|
+
return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
|
|
2618
|
+
}
|
|
2619
|
+
// ─── Lookup primitives ──────────────────────────────────────────────────────
|
|
2620
|
+
lookup(candidate) {
|
|
2621
|
+
return this.byPath.get(path4.posix.normalize(candidate).toLowerCase());
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Try `base` verbatim, then `base` + each extension, then each directory
|
|
2625
|
+
* entry point inside `base`.
|
|
2626
|
+
*/
|
|
2627
|
+
lookupWithExtensions(base, family) {
|
|
2628
|
+
const direct = this.lookup(base);
|
|
2629
|
+
if (direct) return direct;
|
|
2630
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2631
|
+
const suffix = path4.posix.extname(base);
|
|
2632
|
+
const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
|
|
2633
|
+
for (const ext of extensions) {
|
|
2634
|
+
const hit = this.lookup(`${stem}${ext}`);
|
|
2635
|
+
if (hit) return hit;
|
|
2636
|
+
}
|
|
2637
|
+
for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
|
|
2638
|
+
for (const ext of extensions) {
|
|
2639
|
+
const hit = this.lookup(path4.posix.join(base, `${entry}${ext}`));
|
|
2640
|
+
if (hit) return hit;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return void 0;
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* A representative indexed file inside `dir`, for ecosystems whose import
|
|
2647
|
+
* unit is a directory rather than a file (Go packages, JVM wildcard imports).
|
|
2648
|
+
*
|
|
2649
|
+
* The choice is deterministic — a file named after the directory, else the
|
|
2650
|
+
* first by name — so the same import always produces the same edge. Package
|
|
2651
|
+
* grouping is unaffected either way: every file in the directory carries the
|
|
2652
|
+
* same package label, so the package-level edge is exact regardless of which
|
|
2653
|
+
* member represents it.
|
|
2654
|
+
*/
|
|
2655
|
+
representativeIn(dir, family) {
|
|
2656
|
+
const members = this.byDir.get(path4.posix.normalize(dir).toLowerCase());
|
|
2657
|
+
if (!members?.length) return void 0;
|
|
2658
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
2659
|
+
const eligible = members.filter((file) => extensions.includes(path4.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
|
|
2660
|
+
if (eligible.length === 0) return void 0;
|
|
2661
|
+
const base = path4.posix.basename(path4.posix.normalize(dir)).toLowerCase();
|
|
2662
|
+
const named = eligible.find(
|
|
2663
|
+
(file) => path4.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
|
|
2664
|
+
);
|
|
2665
|
+
return named ?? eligible[0];
|
|
2666
|
+
}
|
|
2667
|
+
// ─── Per-family resolution ──────────────────────────────────────────────────
|
|
2668
|
+
/** Relative specifiers, then workspace package names and their subpaths. */
|
|
2669
|
+
resolveJs(fromFile, spec) {
|
|
2670
|
+
if (spec.startsWith(".")) {
|
|
2671
|
+
const absolute = path4.posix.join(path4.posix.dirname(fromFile), spec);
|
|
2672
|
+
return this.lookupWithExtensions(absolute, "js");
|
|
2673
|
+
}
|
|
2674
|
+
const owner = this.structure.roots.find(
|
|
2675
|
+
(root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2676
|
+
);
|
|
2677
|
+
if (!owner?.importPath) return void 0;
|
|
2678
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2679
|
+
if (!subpath) {
|
|
2680
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "index"), "js");
|
|
2681
|
+
}
|
|
2682
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "src", subpath), "js");
|
|
2683
|
+
}
|
|
2684
|
+
/** Go import paths are absolute module paths; a package is a directory. */
|
|
2685
|
+
resolveGo(spec) {
|
|
2686
|
+
const owner = this.structure.roots.find(
|
|
2687
|
+
(root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
2688
|
+
);
|
|
2689
|
+
if (!owner?.importPath) return void 0;
|
|
2690
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
2691
|
+
return this.representativeIn(path4.posix.join(owner.dir, subpath), "go");
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
|
|
2695
|
+
* leading dots the extractor preserves (`.sibling`, `..parent.mod`).
|
|
2696
|
+
*/
|
|
2697
|
+
resolvePython(fromFile, spec) {
|
|
2698
|
+
const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
|
|
2699
|
+
if (leadingDots > 0) {
|
|
2700
|
+
let base = path4.posix.dirname(fromFile);
|
|
2701
|
+
for (let i = 1; i < leadingDots; i++) base = path4.posix.dirname(base);
|
|
2702
|
+
const rest = spec.slice(leadingDots).split(".").filter(Boolean);
|
|
2703
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest), "py");
|
|
2704
|
+
}
|
|
2705
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2706
|
+
if (segments.length === 0) return void 0;
|
|
2707
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
|
|
2708
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2709
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, ...segments), "py");
|
|
2710
|
+
if (hit) return hit;
|
|
2711
|
+
if (segments.length > 1) {
|
|
2712
|
+
const parent = this.lookupWithExtensions(
|
|
2713
|
+
path4.posix.join(base, ...segments.slice(0, -1)),
|
|
2714
|
+
"py"
|
|
2715
|
+
);
|
|
2716
|
+
if (parent) return parent;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return void 0;
|
|
2720
|
+
}
|
|
2721
|
+
/** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
|
|
2722
|
+
resolveRust(fromFile, spec) {
|
|
2723
|
+
const segments = spec.split("::").filter(Boolean);
|
|
2724
|
+
if (segments.length === 0) return void 0;
|
|
2725
|
+
const head = segments[0];
|
|
2726
|
+
if (head === "self" || head === "super") {
|
|
2727
|
+
let base = path4.posix.dirname(fromFile);
|
|
2728
|
+
for (const segment of segments) {
|
|
2729
|
+
if (segment === "super") base = path4.posix.dirname(base);
|
|
2730
|
+
else if (segment !== "self") break;
|
|
2731
|
+
}
|
|
2732
|
+
const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
|
|
2733
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest2), "rs");
|
|
2734
|
+
}
|
|
2735
|
+
const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
|
|
2736
|
+
const crate = head === "crate" ? owningCrate : this.structure.roots.find(
|
|
2737
|
+
(root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
|
|
2738
|
+
);
|
|
2739
|
+
if (!crate) {
|
|
2740
|
+
return this.lookupWithExtensions(
|
|
2741
|
+
path4.posix.join(path4.posix.dirname(fromFile), ...segments),
|
|
2742
|
+
"rs"
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
const rest = segments.slice(1);
|
|
2746
|
+
for (const base of crate.sourceRoots) {
|
|
2747
|
+
const parent = rest.length > 1 ? this.lookupWithExtensions(path4.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
|
|
2748
|
+
const exact = this.lookupWithExtensions(path4.posix.join(base, ...rest), "rs");
|
|
2749
|
+
const hit = exact ?? parent ?? this.lookupWithExtensions(path4.posix.join(base, "lib"), "rs");
|
|
2750
|
+
if (hit) return hit;
|
|
2751
|
+
}
|
|
2752
|
+
return void 0;
|
|
2753
|
+
}
|
|
2754
|
+
/** `com.example.Thing` and `com.example.*` against JVM source roots. */
|
|
2755
|
+
resolveJvm(spec) {
|
|
2756
|
+
const segments = spec.split(".").filter(Boolean);
|
|
2757
|
+
if (segments.length === 0) return void 0;
|
|
2758
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
|
|
2759
|
+
const wildcard = segments[segments.length - 1] === "*";
|
|
2760
|
+
const parts = wildcard ? segments.slice(0, -1) : segments;
|
|
2761
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
2762
|
+
const target = path4.posix.join(base, ...parts);
|
|
2763
|
+
const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
|
|
2764
|
+
if (hit) return hit;
|
|
2765
|
+
}
|
|
2766
|
+
return void 0;
|
|
2767
|
+
}
|
|
2768
|
+
/** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
|
|
2769
|
+
resolveInclude(fromFile, spec) {
|
|
2770
|
+
const relative2 = this.lookupWithExtensions(
|
|
2771
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
2772
|
+
"c"
|
|
2773
|
+
);
|
|
2774
|
+
if (relative2) return relative2;
|
|
2775
|
+
for (const base of [
|
|
2776
|
+
path4.posix.join(this.structure.projectRoot, "include"),
|
|
2777
|
+
this.structure.projectRoot
|
|
2778
|
+
]) {
|
|
2779
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "c");
|
|
2780
|
+
if (hit) return hit;
|
|
2781
|
+
}
|
|
2782
|
+
return void 0;
|
|
2783
|
+
}
|
|
2784
|
+
/** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
|
|
2785
|
+
resolveRuby(fromFile, spec) {
|
|
2786
|
+
const relative2 = this.lookupWithExtensions(
|
|
2787
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
2788
|
+
"ruby"
|
|
2789
|
+
);
|
|
2790
|
+
if (relative2) return relative2;
|
|
2791
|
+
for (const base of [
|
|
2792
|
+
path4.posix.join(this.structure.projectRoot, "lib"),
|
|
2793
|
+
this.structure.projectRoot
|
|
2794
|
+
]) {
|
|
2795
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "ruby");
|
|
2796
|
+
if (hit) return hit;
|
|
2797
|
+
}
|
|
2798
|
+
return void 0;
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
// src/codebase-index/import-extractor.ts
|
|
2803
|
+
var IMPORT_MAX_FILE_CHARS = 512 * 1024;
|
|
2804
|
+
var IMPORT_MAX_PER_FILE = 400;
|
|
2805
|
+
var DOTTED_IMPORT = [
|
|
2806
|
+
{ re: /^[ \t]*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;?\s*$/gm }
|
|
2807
|
+
];
|
|
2808
|
+
var LANG_IMPORTS = {
|
|
2809
|
+
// Go and Python have real AST extractors; these patterns are the fallback for
|
|
2810
|
+
// machines with no Go toolchain or Python interpreter installed, where the
|
|
2811
|
+
// parser degrades to regex symbols and would otherwise contribute no edges.
|
|
2812
|
+
go: [
|
|
2813
|
+
{ re: /^[ \t]*import\s+(?:[\w.]+\s+)?"([^"]+)"/gm },
|
|
2814
|
+
// Grouped form: inside `import ( … )` each line is an optional alias plus a
|
|
2815
|
+
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
2816
|
+
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
2817
|
+
],
|
|
2818
|
+
py: [
|
|
2819
|
+
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
2820
|
+
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
2821
|
+
],
|
|
2822
|
+
rs: [
|
|
2823
|
+
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
2824
|
+
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
2825
|
+
// mod foo; (a declaration *and* a dependency on foo.rs / foo/mod.rs)
|
|
2826
|
+
{ re: /^[ \t]*(?:pub\s+)?mod\s+(\w+)\s*;/gm }
|
|
2827
|
+
],
|
|
2828
|
+
java: DOTTED_IMPORT,
|
|
2829
|
+
kotlin: DOTTED_IMPORT,
|
|
2830
|
+
scala: [{ re: /^[ \t]*import\s+([\w.]+(?:\.[_*])?)\s*$/gm }],
|
|
2831
|
+
csharp: [
|
|
2832
|
+
// using Foo.Bar; | using static Foo.Bar; | using Alias = Foo.Bar;
|
|
2833
|
+
{ re: /^[ \t]*global\s+using\s+(?:static\s+)?([\w.]+)\s*;/gm, name: "full" },
|
|
2834
|
+
{ re: /^[ \t]*using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, name: "full" }
|
|
2835
|
+
],
|
|
2836
|
+
// Quoted includes only: <stdio.h> is a system header with no indexed file.
|
|
2837
|
+
c: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2838
|
+
cpp: [{ re: /^[ \t]*#\s*include\s+"([^"]+)"/gm }],
|
|
2839
|
+
ruby: [{ re: /^[ \t]*(?:require|require_relative|load)\s*\(?\s*['"]([^'"]+)['"]/gm }],
|
|
2840
|
+
php: [
|
|
2841
|
+
// `use A\B\C` imports the class C, which is what the index has a symbol
|
|
2842
|
+
// for — the namespace symbol only covers the `A\B` prefix.
|
|
2843
|
+
{ re: /^[ \t]*use\s+(?:function\s+|const\s+)?([\w\\]+)/gm },
|
|
2844
|
+
{ re: /^[ \t]*(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]/gm }
|
|
2845
|
+
],
|
|
2846
|
+
swift: [{ re: /^[ \t]*import\s+(?:struct\s+|class\s+|func\s+)?([\w.]+)\s*$/gm }],
|
|
2847
|
+
dart: [{ re: /^[ \t]*(?:import|export|part)\s+['"]([^'"]+)['"]/gm }],
|
|
2848
|
+
lua: [{ re: /\brequire\s*\(?\s*['"]([^'"]+)['"]/g }],
|
|
2849
|
+
elixir: [{ re: /^[ \t]*(?:alias|import|require|use)\s+([A-Z][\w.]*)/gm, name: "full" }],
|
|
2850
|
+
haskell: [{ re: /^[ \t]*import\s+(?:qualified\s+)?([\w.]+)/gm, name: "full" }],
|
|
2851
|
+
zig: [{ re: /@import\s*\(\s*"([^"]+)"\s*\)/g }],
|
|
2852
|
+
proto: [{ re: /^[ \t]*import\s+(?:public\s+|weak\s+)?"([^"]+)"\s*;/gm }],
|
|
2853
|
+
// `@import "x"`, `@use "x"`, `@forward "x"` — Sass and plain CSS alike.
|
|
2854
|
+
css: [{ re: /^[ \t]*@(?:import|use|forward)\s+(?:url\()?\s*['"]([^'"]+)['"]/gm }],
|
|
2855
|
+
// A .vue/.svelte file's <script> block is JS; the same ESM syntax applies.
|
|
2856
|
+
vue: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
2857
|
+
svelte: [{ re: /^[ \t]*import\s[^'"]*from\s*['"]([^'"]+)['"]/gm }],
|
|
2858
|
+
html: [
|
|
2859
|
+
{ re: /<script[^>]+src\s*=\s*['"]([^'"]+)['"]/g },
|
|
2860
|
+
{ re: /<link[^>]+href\s*=\s*['"]([^'"]+)['"]/g }
|
|
2861
|
+
],
|
|
2862
|
+
shell: [{ re: /^[ \t]*(?:source|\.)\s+(\S+)/gm }],
|
|
2863
|
+
r: [{ re: /\b(?:source|library|require)\s*\(\s*['"]?([\w./-]+)['"]?\s*\)/g }]
|
|
2864
|
+
};
|
|
2865
|
+
function lastSegment(specifier) {
|
|
2866
|
+
const pathLike = /[/\\]|::/.test(specifier);
|
|
2867
|
+
const segments = specifier.split(/[/\\]|::/).filter(Boolean);
|
|
2868
|
+
let last = segments[segments.length - 1] ?? specifier;
|
|
2869
|
+
if (last === "*" || last === "_") {
|
|
2870
|
+
last = segments[segments.length - 2] ?? specifier;
|
|
2871
|
+
}
|
|
2872
|
+
if (pathLike) return last.replace(/\.[A-Za-z0-9]{1,8}$/, "") || last;
|
|
2873
|
+
const dotted = last.split(".").filter((part) => part && part !== "*" && part !== "_");
|
|
2874
|
+
return dotted[dotted.length - 1] ?? last;
|
|
2875
|
+
}
|
|
2876
|
+
function newlineOffsets(content) {
|
|
2877
|
+
const offsets = [];
|
|
2878
|
+
for (let i = 0; i < content.length; i++) {
|
|
2879
|
+
if (content.charCodeAt(i) === 10) offsets.push(i);
|
|
2880
|
+
}
|
|
2881
|
+
return offsets;
|
|
2882
|
+
}
|
|
2883
|
+
function lineAt(offsets, index) {
|
|
2884
|
+
let low = 0;
|
|
2885
|
+
let high = offsets.length;
|
|
2886
|
+
while (low < high) {
|
|
2887
|
+
const mid = low + high >>> 1;
|
|
2888
|
+
if ((offsets[mid] ?? 0) < index) low = mid + 1;
|
|
2889
|
+
else high = mid;
|
|
2890
|
+
}
|
|
2891
|
+
return low + 1;
|
|
2892
|
+
}
|
|
2893
|
+
function hasImportPatterns(lang) {
|
|
2894
|
+
return LANG_IMPORTS[lang] !== void 0;
|
|
2895
|
+
}
|
|
2896
|
+
function extractImports(opts) {
|
|
2897
|
+
const patterns = LANG_IMPORTS[opts.lang];
|
|
2898
|
+
if (!patterns || !opts.content) return [];
|
|
2899
|
+
const limit = opts.maxImports ?? IMPORT_MAX_PER_FILE;
|
|
2900
|
+
const content = opts.content.length > IMPORT_MAX_FILE_CHARS ? opts.content.slice(0, IMPORT_MAX_FILE_CHARS) : opts.content;
|
|
2901
|
+
const refs = [];
|
|
2902
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2903
|
+
const offsets = newlineOffsets(content);
|
|
2904
|
+
for (const pattern of patterns) {
|
|
2905
|
+
const re = new RegExp(pattern.re.source, pattern.re.flags);
|
|
2906
|
+
for (const match of content.matchAll(re)) {
|
|
2907
|
+
if (refs.length >= limit) return refs;
|
|
2908
|
+
const specifier = match[1]?.trim();
|
|
2909
|
+
if (!specifier) continue;
|
|
2910
|
+
const module = specifier;
|
|
2911
|
+
const toName = pattern.name === "full" ? module : lastSegment(module);
|
|
2912
|
+
if (!toName) continue;
|
|
2913
|
+
const key = `${module}\0${toName}`;
|
|
2914
|
+
if (seen.has(key)) continue;
|
|
2915
|
+
seen.add(key);
|
|
2916
|
+
refs.push({
|
|
2917
|
+
fromId: 0,
|
|
2918
|
+
toName,
|
|
2919
|
+
callType: "import",
|
|
2920
|
+
line: lineAt(offsets, match.index ?? 0),
|
|
2921
|
+
lang: opts.lang,
|
|
2922
|
+
module
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
return refs;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2043
2929
|
// src/codebase-index/parser-dispatch.ts
|
|
2044
2930
|
async function parseFileContent(file, content, lang) {
|
|
2931
|
+
const parsed = await dispatch(file, content, lang);
|
|
2932
|
+
return withRelations(parsed, content, lang);
|
|
2933
|
+
}
|
|
2934
|
+
async function dispatch(file, content, lang) {
|
|
2045
2935
|
switch (lang) {
|
|
2046
2936
|
case "ts":
|
|
2047
2937
|
case "tsx":
|
|
@@ -2076,6 +2966,13 @@ async function parseFileContent(file, content, lang) {
|
|
|
2076
2966
|
}
|
|
2077
2967
|
}
|
|
2078
2968
|
}
|
|
2969
|
+
function withRelations(parsed, content, lang) {
|
|
2970
|
+
let refs = parsed.refs ?? [];
|
|
2971
|
+
if (refs.length === 0 && hasImportPatterns(lang)) {
|
|
2972
|
+
refs = extractImports({ content, lang });
|
|
2973
|
+
}
|
|
2974
|
+
return { ...parsed, refs: refs.map((ref) => ref.lang ? ref : { ...ref, lang }) };
|
|
2975
|
+
}
|
|
2079
2976
|
|
|
2080
2977
|
// src/codebase-index/writer.ts
|
|
2081
2978
|
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
@@ -2085,9 +2982,9 @@ import * as path10 from "node:path";
|
|
|
2085
2982
|
// src/codebase-index/bm25.ts
|
|
2086
2983
|
var K1 = 1.5;
|
|
2087
2984
|
var B = 0.75;
|
|
2985
|
+
var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
|
|
2088
2986
|
function tokenise(text) {
|
|
2089
|
-
|
|
2090
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
2987
|
+
return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
2091
2988
|
}
|
|
2092
2989
|
function splitName(name) {
|
|
2093
2990
|
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
@@ -2171,6 +3068,9 @@ var Bm25Index = class {
|
|
|
2171
3068
|
}
|
|
2172
3069
|
};
|
|
2173
3070
|
|
|
3071
|
+
// src/codebase-index/writer.ts
|
|
3072
|
+
init_languages();
|
|
3073
|
+
|
|
2174
3074
|
// src/codebase-index/lsp-kind.ts
|
|
2175
3075
|
function lspKindToInternalKind(k) {
|
|
2176
3076
|
switch (k) {
|
|
@@ -2205,7 +3105,7 @@ function lspKindToInternalKind(k) {
|
|
|
2205
3105
|
}
|
|
2206
3106
|
|
|
2207
3107
|
// src/codebase-index/schema.ts
|
|
2208
|
-
var SCHEMA_VERSION =
|
|
3108
|
+
var SCHEMA_VERSION = 4;
|
|
2209
3109
|
|
|
2210
3110
|
// src/codebase-index/sqlite-runtime.ts
|
|
2211
3111
|
import { createRequire } from "node:module";
|
|
@@ -2352,7 +3252,7 @@ function runSqliteWithRetry(fn) {
|
|
|
2352
3252
|
|
|
2353
3253
|
// src/codebase-index/writer-admin.ts
|
|
2354
3254
|
import * as fs6 from "node:fs";
|
|
2355
|
-
import * as
|
|
3255
|
+
import * as path9 from "node:path";
|
|
2356
3256
|
var DB_FILE = "index.db";
|
|
2357
3257
|
function getAllIndexableWithStatement(stmt) {
|
|
2358
3258
|
return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
@@ -2411,7 +3311,7 @@ function getAllFileMetasWithStatement(stmt) {
|
|
|
2411
3311
|
}
|
|
2412
3312
|
function getIndexDbSizeBytes(indexDir) {
|
|
2413
3313
|
try {
|
|
2414
|
-
return fs6.statSync(
|
|
3314
|
+
return fs6.statSync(path9.join(indexDir, DB_FILE)).size;
|
|
2415
3315
|
} catch {
|
|
2416
3316
|
return 0;
|
|
2417
3317
|
}
|
|
@@ -2462,49 +3362,43 @@ function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
|
|
|
2462
3362
|
}
|
|
2463
3363
|
function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
|
|
2464
3364
|
if (refs.length === 0) return;
|
|
2465
|
-
const chunkSize = Math.max(1, Math.floor(maxSqlVars /
|
|
3365
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 8));
|
|
2466
3366
|
for (let i = 0; i < refs.length; i += chunkSize) {
|
|
2467
3367
|
const chunk = refs.slice(i, i + chunkSize);
|
|
2468
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
|
|
3368
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
2469
3369
|
const insert = stmt(
|
|
2470
|
-
`INSERT INTO refs(from_id, to_name, to_id, call_type, line
|
|
3370
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
|
|
3371
|
+
VALUES ${placeholders}`
|
|
2471
3372
|
);
|
|
2472
3373
|
const binds = [];
|
|
2473
3374
|
for (const ref of chunk) {
|
|
2474
|
-
binds.push(
|
|
3375
|
+
binds.push(
|
|
3376
|
+
ref.fromId,
|
|
3377
|
+
ref.toName,
|
|
3378
|
+
ref.toId ?? null,
|
|
3379
|
+
ref.callType,
|
|
3380
|
+
ref.line,
|
|
3381
|
+
ref.lang ?? "",
|
|
3382
|
+
ref.module ?? null,
|
|
3383
|
+
ref.toFile ?? null
|
|
3384
|
+
);
|
|
2475
3385
|
}
|
|
2476
3386
|
insert.run(...binds);
|
|
2477
3387
|
}
|
|
2478
3388
|
}
|
|
2479
3389
|
|
|
3390
|
+
// src/codebase-index/writer-graph-reader.ts
|
|
3391
|
+
init_languages();
|
|
3392
|
+
|
|
2480
3393
|
// src/codebase-index/writer-graph-helpers.ts
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
const f = filePath.replace(/\\/g, "/");
|
|
2484
|
-
const pkgsIdx = f.indexOf("/packages/");
|
|
2485
|
-
if (pkgsIdx !== -1) {
|
|
2486
|
-
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
2487
|
-
const seg = rest.split("/")[0];
|
|
2488
|
-
return seg ? `@wrongstack/${seg}` : void 0;
|
|
2489
|
-
}
|
|
2490
|
-
const appsIdx = f.indexOf("/apps/");
|
|
2491
|
-
if (appsIdx !== -1) {
|
|
2492
|
-
const rest = f.slice(appsIdx + "/apps/".length);
|
|
2493
|
-
const seg = rest.split("/")[0];
|
|
2494
|
-
return seg ? `app:${seg}` : void 0;
|
|
2495
|
-
}
|
|
2496
|
-
return void 0;
|
|
2497
|
-
}
|
|
2498
|
-
function packageFromImport(moduleName) {
|
|
2499
|
-
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
2500
|
-
const parts = moduleName.split("/");
|
|
2501
|
-
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
3394
|
+
function createPackageLabeller(stored) {
|
|
3395
|
+
return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
|
|
2502
3396
|
}
|
|
2503
|
-
function buildPackageGraphNodes(fileCounts, files) {
|
|
3397
|
+
function buildPackageGraphNodes(fileCounts, files, packageOf) {
|
|
2504
3398
|
const pkgNodes = /* @__PURE__ */ new Map();
|
|
2505
3399
|
const fileToPkg = /* @__PURE__ */ new Map();
|
|
2506
3400
|
for (const { file, n } of fileCounts) {
|
|
2507
|
-
const pkg =
|
|
3401
|
+
const pkg = packageOf(file);
|
|
2508
3402
|
fileToPkg.set(file, pkg);
|
|
2509
3403
|
const node = pkgNodes.get(pkg);
|
|
2510
3404
|
if (node) {
|
|
@@ -2521,7 +3415,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2521
3415
|
}
|
|
2522
3416
|
}
|
|
2523
3417
|
for (const { file } of files) {
|
|
2524
|
-
const pkg =
|
|
3418
|
+
const pkg = packageOf(file);
|
|
2525
3419
|
fileToPkg.set(file, pkg);
|
|
2526
3420
|
const node = pkgNodes.get(pkg);
|
|
2527
3421
|
if (node) {
|
|
@@ -2539,7 +3433,7 @@ function buildPackageGraphNodes(fileCounts, files) {
|
|
|
2539
3433
|
}
|
|
2540
3434
|
return { pkgNodes, fileToPkg };
|
|
2541
3435
|
}
|
|
2542
|
-
function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
3436
|
+
function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
2543
3437
|
const fileNodes = /* @__PURE__ */ new Map();
|
|
2544
3438
|
const symToFile = /* @__PURE__ */ new Map();
|
|
2545
3439
|
const fileStats = /* @__PURE__ */ new Map();
|
|
@@ -2558,7 +3452,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2558
3452
|
id: `file:${file}`,
|
|
2559
3453
|
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
2560
3454
|
kind: "file",
|
|
2561
|
-
package:
|
|
3455
|
+
package: packageOf(file),
|
|
2562
3456
|
file,
|
|
2563
3457
|
symbolCount: stats?.count ?? 0,
|
|
2564
3458
|
lang: stats?.lang,
|
|
@@ -2570,7 +3464,7 @@ function buildFileGraphNodeState(pkgSyms, localFiles) {
|
|
|
2570
3464
|
}
|
|
2571
3465
|
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
2572
3466
|
}
|
|
2573
|
-
function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
3467
|
+
function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
|
|
2574
3468
|
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
2575
3469
|
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
2576
3470
|
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
@@ -2582,7 +3476,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2582
3476
|
symbolId: s.id,
|
|
2583
3477
|
symbolKind: s.kind,
|
|
2584
3478
|
file: s.file,
|
|
2585
|
-
package:
|
|
3479
|
+
package: packageOf(s.file),
|
|
2586
3480
|
lang: s.lang,
|
|
2587
3481
|
line: s.line,
|
|
2588
3482
|
signature: s.signature,
|
|
@@ -2590,29 +3484,6 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
|
|
|
2590
3484
|
external: s.file !== fileFilter
|
|
2591
3485
|
}));
|
|
2592
3486
|
}
|
|
2593
|
-
function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
2594
|
-
if (!moduleName.startsWith(".")) return void 0;
|
|
2595
|
-
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
2596
|
-
const absolute = path9.posix.normalize(
|
|
2597
|
-
path9.posix.join(path9.posix.dirname(normalizedFrom), moduleName)
|
|
2598
|
-
);
|
|
2599
|
-
const extension = path9.posix.extname(absolute);
|
|
2600
|
-
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
2601
|
-
const candidates = [
|
|
2602
|
-
absolute,
|
|
2603
|
-
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
2604
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(absolute, `index${ext}`)),
|
|
2605
|
-
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(base, `index${ext}`))
|
|
2606
|
-
];
|
|
2607
|
-
const indexedByPortablePath = new Map(
|
|
2608
|
-
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
2609
|
-
);
|
|
2610
|
-
for (const candidate of candidates) {
|
|
2611
|
-
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
2612
|
-
if (indexed) return indexed;
|
|
2613
|
-
}
|
|
2614
|
-
return void 0;
|
|
2615
|
-
}
|
|
2616
3487
|
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
2617
3488
|
const key = `${source}\0${target}`;
|
|
2618
3489
|
let edge = edgeMap.get(key);
|
|
@@ -2653,7 +3524,12 @@ function mapWriterRefRow(row) {
|
|
|
2653
3524
|
toName: row.to_name,
|
|
2654
3525
|
toId: row.to_id ?? void 0,
|
|
2655
3526
|
callType: row.call_type,
|
|
2656
|
-
line: row.line
|
|
3527
|
+
line: row.line,
|
|
3528
|
+
// `lang`/`module`/`to_file` are absent from the narrower column lists some
|
|
3529
|
+
// queries select; `undefined` keeps those rows valid Refs.
|
|
3530
|
+
lang: row.lang || void 0,
|
|
3531
|
+
module: row.module ?? void 0,
|
|
3532
|
+
toFile: row.to_file ?? void 0
|
|
2657
3533
|
};
|
|
2658
3534
|
}
|
|
2659
3535
|
|
|
@@ -2801,7 +3677,8 @@ function findRefsFromWithStatement(stmt, symbolId) {
|
|
|
2801
3677
|
function getPackageGraphWithStatement(stmt) {
|
|
2802
3678
|
const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
|
|
2803
3679
|
const files = stmt("SELECT DISTINCT file FROM files").all();
|
|
2804
|
-
const
|
|
3680
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3681
|
+
const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
|
|
2805
3682
|
const refRows = stmt(
|
|
2806
3683
|
`SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
|
|
2807
3684
|
FROM refs r
|
|
@@ -2812,32 +3689,42 @@ function getPackageGraphWithStatement(stmt) {
|
|
|
2812
3689
|
).all();
|
|
2813
3690
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
2814
3691
|
for (const r of refRows) {
|
|
2815
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2816
|
-
const toPkg = fileToPkg.get(r.to_file) ??
|
|
3692
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3693
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
2817
3694
|
if (fromPkg === toPkg) continue;
|
|
2818
3695
|
const n = Number(r.n) || 0;
|
|
2819
3696
|
addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
|
|
2820
3697
|
}
|
|
2821
3698
|
const importRows = stmt(
|
|
2822
|
-
`SELECT
|
|
3699
|
+
`SELECT s.file AS from_file,
|
|
3700
|
+
COALESCE(r.to_file, st.file) AS to_file,
|
|
3701
|
+
COUNT(*) AS n
|
|
2823
3702
|
FROM refs r
|
|
2824
3703
|
JOIN symbols s ON s.id = r.from_id
|
|
3704
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2825
3705
|
WHERE r.call_type = 'import'
|
|
2826
|
-
|
|
3706
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
3707
|
+
GROUP BY s.file, COALESCE(r.to_file, st.file)`
|
|
2827
3708
|
).all();
|
|
2828
3709
|
for (const r of importRows) {
|
|
2829
|
-
const fromPkg = fileToPkg.get(r.from_file) ??
|
|
2830
|
-
const toPkg =
|
|
2831
|
-
if (
|
|
3710
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
3711
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
3712
|
+
if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
2832
3713
|
const n = Number(r.n) || 0;
|
|
2833
3714
|
addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
|
|
2834
3715
|
}
|
|
2835
3716
|
const edges = materializeWeightedEdges(edgeMap, "pkg");
|
|
2836
3717
|
return { nodes: [...pkgNodes.values()], edges };
|
|
2837
3718
|
}
|
|
3719
|
+
function readPackageLabeller(stmt) {
|
|
3720
|
+
const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
3721
|
+
return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
|
|
3722
|
+
}
|
|
2838
3723
|
function getFileGraphWithStatement(stmt, packageFilter) {
|
|
2839
3724
|
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
2840
|
-
const
|
|
3725
|
+
const packageOf = readPackageLabeller(stmt);
|
|
3726
|
+
const langOf = (file) => detectLang(file) ?? "other";
|
|
3727
|
+
const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
|
|
2841
3728
|
const localFiles = new Set(pkgFilePaths);
|
|
2842
3729
|
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
2843
3730
|
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
@@ -2846,9 +3733,9 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2846
3733
|
).all(...pkgFilePaths);
|
|
2847
3734
|
const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
|
|
2848
3735
|
pkgSyms,
|
|
2849
|
-
localFiles
|
|
3736
|
+
localFiles,
|
|
3737
|
+
packageOf
|
|
2850
3738
|
);
|
|
2851
|
-
const indexedFiles = new Set(allFiles.map((f) => f.file));
|
|
2852
3739
|
const refRows = stmt(
|
|
2853
3740
|
`SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
|
|
2854
3741
|
FROM refs r
|
|
@@ -2871,7 +3758,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2871
3758
|
for (const x of extras) {
|
|
2872
3759
|
symToFile.set(x.id, x.file);
|
|
2873
3760
|
if (!fileStats.has(x.file)) {
|
|
2874
|
-
fileStats.set(x.file, { count: 0, lang:
|
|
3761
|
+
fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
|
|
2875
3762
|
}
|
|
2876
3763
|
}
|
|
2877
3764
|
}
|
|
@@ -2888,17 +3775,22 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
2888
3775
|
addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
|
|
2889
3776
|
}
|
|
2890
3777
|
const importRows = stmt(
|
|
2891
|
-
`SELECT r.from_id, r.
|
|
3778
|
+
`SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
|
|
2892
3779
|
FROM refs r
|
|
3780
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
2893
3781
|
WHERE r.call_type = 'import'
|
|
3782
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
2894
3783
|
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
2895
|
-
GROUP BY r.from_id, r.
|
|
3784
|
+
GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
|
|
2896
3785
|
).all(...pkgFilePaths);
|
|
2897
3786
|
for (const r of importRows) {
|
|
2898
3787
|
const fromFile = symToFile.get(r.from_id);
|
|
2899
3788
|
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
2900
|
-
const toFile =
|
|
3789
|
+
const toFile = r.to_file;
|
|
2901
3790
|
if (!toFile || fromFile === toFile) continue;
|
|
3791
|
+
if (!fileStats.has(toFile)) {
|
|
3792
|
+
fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
|
|
3793
|
+
}
|
|
2902
3794
|
ensureFileNode(fromFile);
|
|
2903
3795
|
ensureFileNode(toFile);
|
|
2904
3796
|
const n = Number(r.n) || 0;
|
|
@@ -2948,7 +3840,7 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
2948
3840
|
).all(...missingIds);
|
|
2949
3841
|
for (const s of extras) symById.set(s.id, s);
|
|
2950
3842
|
}
|
|
2951
|
-
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter);
|
|
3843
|
+
const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
|
|
2952
3844
|
return { nodes, edges };
|
|
2953
3845
|
}
|
|
2954
3846
|
|
|
@@ -2970,7 +3862,7 @@ function assignRefsToSymbols(refs, symbols) {
|
|
|
2970
3862
|
}
|
|
2971
3863
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2972
3864
|
if (!owner || owner.id <= 0) continue;
|
|
2973
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
3865
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
2974
3866
|
if (seen.has(key)) continue;
|
|
2975
3867
|
seen.add(key);
|
|
2976
3868
|
assigned.push({ ...ref, fromId: owner.id });
|
|
@@ -3012,7 +3904,11 @@ var CORE_TABLES_SQL = `
|
|
|
3012
3904
|
lang TEXT NOT NULL,
|
|
3013
3905
|
mtime_ms INTEGER NOT NULL,
|
|
3014
3906
|
symbol_count INTEGER NOT NULL DEFAULT 0,
|
|
3015
|
-
last_indexed INTEGER NOT NULL
|
|
3907
|
+
last_indexed INTEGER NOT NULL,
|
|
3908
|
+
-- Code Atlas grouping label, computed at index time from the ecosystem's
|
|
3909
|
+
-- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
|
|
3910
|
+
-- re-derived per query because the evidence lives on disk, not in the DB.
|
|
3911
|
+
package TEXT NOT NULL DEFAULT ''
|
|
3016
3912
|
);
|
|
3017
3913
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
3018
3914
|
id INTEGER PRIMARY KEY,
|
|
@@ -3029,6 +3925,9 @@ var CORE_TABLES_SQL = `
|
|
|
3029
3925
|
file_fk TEXT NOT NULL
|
|
3030
3926
|
);
|
|
3031
3927
|
`;
|
|
3928
|
+
var FILE_INDEX_SQL = [
|
|
3929
|
+
"CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
|
|
3930
|
+
];
|
|
3032
3931
|
var SYMBOL_INDEX_SQL = [
|
|
3033
3932
|
"CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
|
|
3034
3933
|
"CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
|
|
@@ -3045,15 +3944,32 @@ var REFS_TABLE_SQL = `
|
|
|
3045
3944
|
to_name TEXT NOT NULL,
|
|
3046
3945
|
to_id INTEGER,
|
|
3047
3946
|
call_type TEXT NOT NULL,
|
|
3048
|
-
line INTEGER NOT NULL
|
|
3947
|
+
line INTEGER NOT NULL,
|
|
3948
|
+
lang TEXT NOT NULL DEFAULT '',
|
|
3949
|
+
module TEXT,
|
|
3950
|
+
to_file TEXT
|
|
3049
3951
|
);
|
|
3050
3952
|
`;
|
|
3051
3953
|
var REFS_INDEX_SQL = [
|
|
3052
3954
|
"CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
|
|
3053
3955
|
"CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
|
|
3054
3956
|
"CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
|
|
3055
|
-
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)"
|
|
3957
|
+
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
|
|
3958
|
+
// Name resolution matches (to_name, lang) pairs; the composite keeps the
|
|
3959
|
+
// language-scoped UPDATE from degrading into a scan of every same-named row.
|
|
3960
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
|
|
3961
|
+
// The post-index module resolution pass groups unresolved import refs by
|
|
3962
|
+
// (module, lang); graph readers then read to_file back.
|
|
3963
|
+
"CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
|
|
3964
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
|
|
3056
3965
|
];
|
|
3966
|
+
var LANG_FAMILY_TABLE_SQL = `
|
|
3967
|
+
CREATE TABLE IF NOT EXISTS lang_family (
|
|
3968
|
+
lang TEXT PRIMARY KEY,
|
|
3969
|
+
family TEXT NOT NULL
|
|
3970
|
+
);
|
|
3971
|
+
`;
|
|
3972
|
+
var LANG_FAMILY_WILDCARD = "*";
|
|
3057
3973
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
3058
3974
|
|
|
3059
3975
|
// src/codebase-index/writer-search-helpers.ts
|
|
@@ -3274,6 +4190,60 @@ var IndexStore = class _IndexStore {
|
|
|
3274
4190
|
runWithRetry(fn) {
|
|
3275
4191
|
return runSqliteWithRetry(fn);
|
|
3276
4192
|
}
|
|
4193
|
+
/**
|
|
4194
|
+
* Mirror the in-process language→family map into SQLite.
|
|
4195
|
+
*
|
|
4196
|
+
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
4197
|
+
* static lookup data, so a code-side change (a new language, a language
|
|
4198
|
+
* moving families) must take effect without forcing a full reindex.
|
|
4199
|
+
*/
|
|
4200
|
+
seedLangFamilies() {
|
|
4201
|
+
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
4202
|
+
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
4203
|
+
insert.run("", LANG_FAMILY_WILDCARD);
|
|
4204
|
+
}
|
|
4205
|
+
/**
|
|
4206
|
+
* Add any column the current schema expects but the on-disk table lacks.
|
|
4207
|
+
*
|
|
4208
|
+
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
4209
|
+
* and the version check above only rebuilds on a version *mismatch*. That
|
|
4210
|
+
* leaves a real gap: several wstack processes share this database, and while
|
|
4211
|
+
* a version upgrade is rolling out one of them may still be running the
|
|
4212
|
+
* previous build. That older process sees the newer version number, drops the
|
|
4213
|
+
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
4214
|
+
* while the metadata row still reads the new version. Every later query for
|
|
4215
|
+
* one of those columns then fails with `no such column`, and no amount of
|
|
4216
|
+
* reindexing fixes it, because the version numbers already agree.
|
|
4217
|
+
*
|
|
4218
|
+
* Repairing column-by-column makes the schema self-healing from any of those
|
|
4219
|
+
* states. Table and column names are compile-time literals from this module,
|
|
4220
|
+
* never user input.
|
|
4221
|
+
*/
|
|
4222
|
+
repairMissingColumns() {
|
|
4223
|
+
const expected = [
|
|
4224
|
+
{ table: "files", columns: [["package", "TEXT NOT NULL DEFAULT ''"]] },
|
|
4225
|
+
{
|
|
4226
|
+
table: "refs",
|
|
4227
|
+
columns: [
|
|
4228
|
+
["lang", "TEXT NOT NULL DEFAULT ''"],
|
|
4229
|
+
["module", "TEXT"],
|
|
4230
|
+
["to_file", "TEXT"]
|
|
4231
|
+
]
|
|
4232
|
+
}
|
|
4233
|
+
];
|
|
4234
|
+
for (const { table, columns } of expected) {
|
|
4235
|
+
const present = new Set(
|
|
4236
|
+
this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
|
|
4237
|
+
(row) => typeof row.name === "string" ? [row.name] : []
|
|
4238
|
+
)
|
|
4239
|
+
);
|
|
4240
|
+
if (present.size === 0) continue;
|
|
4241
|
+
for (const [name, type] of columns) {
|
|
4242
|
+
if (present.has(name)) continue;
|
|
4243
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
3277
4247
|
initSchema() {
|
|
3278
4248
|
this.db.exec(METADATA_TABLE_SQL);
|
|
3279
4249
|
const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
@@ -3296,9 +4266,13 @@ var IndexStore = class _IndexStore {
|
|
|
3296
4266
|
);
|
|
3297
4267
|
}
|
|
3298
4268
|
this.db.exec(CORE_TABLES_SQL);
|
|
3299
|
-
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3300
4269
|
this.db.exec(REFS_TABLE_SQL);
|
|
4270
|
+
this.repairMissingColumns();
|
|
4271
|
+
for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
|
|
4272
|
+
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
3301
4273
|
for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
|
|
4274
|
+
this.db.exec(LANG_FAMILY_TABLE_SQL);
|
|
4275
|
+
this.seedLangFamilies();
|
|
3302
4276
|
try {
|
|
3303
4277
|
this.db.exec(SYMBOLS_FTS_SQL);
|
|
3304
4278
|
this.ftsAvailable = true;
|
|
@@ -3333,6 +4307,18 @@ var IndexStore = class _IndexStore {
|
|
|
3333
4307
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
3334
4308
|
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
3335
4309
|
static MAX_SQL_VARS = 900;
|
|
4310
|
+
/**
|
|
4311
|
+
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
4312
|
+
* `sym` belong to the same language family — or the ref carries no language,
|
|
4313
|
+
* in which case the wildcard bind matches everything.
|
|
4314
|
+
*
|
|
4315
|
+
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
4316
|
+
*/
|
|
4317
|
+
static FAMILY_MATCH_SQL = `(
|
|
4318
|
+
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
4319
|
+
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
4320
|
+
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
4321
|
+
)`;
|
|
3336
4322
|
/**
|
|
3337
4323
|
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
3338
4324
|
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
@@ -3396,9 +4382,12 @@ var IndexStore = class _IndexStore {
|
|
|
3396
4382
|
const placeholders = chunk.map(() => "?").join(",");
|
|
3397
4383
|
const result = this.stmt(
|
|
3398
4384
|
`UPDATE refs
|
|
3399
|
-
SET to_id = (
|
|
4385
|
+
SET to_id = (
|
|
4386
|
+
SELECT MIN(sym.id) FROM symbols sym
|
|
4387
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
4388
|
+
)
|
|
3400
4389
|
WHERE to_name IN (${placeholders})`
|
|
3401
|
-
).run(...chunk);
|
|
4390
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
3402
4391
|
changes += result.changes ?? 0;
|
|
3403
4392
|
}
|
|
3404
4393
|
return changes;
|
|
@@ -3525,6 +4514,115 @@ var IndexStore = class _IndexStore {
|
|
|
3525
4514
|
getAllFileMetas() {
|
|
3526
4515
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
3527
4516
|
}
|
|
4517
|
+
// ─── Project structure & module resolution ──────────────────────────────────
|
|
4518
|
+
/** Store the Code Atlas grouping label for each indexed file. */
|
|
4519
|
+
setFilePackages(entries) {
|
|
4520
|
+
if (entries.size === 0) return;
|
|
4521
|
+
this.runWithRetry(() => {
|
|
4522
|
+
const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
|
|
4523
|
+
for (const [file, label] of entries) update.run(label, file);
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4526
|
+
/**
|
|
4527
|
+
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
4528
|
+
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
4529
|
+
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
4530
|
+
*/
|
|
4531
|
+
getNamespaceDeclarations() {
|
|
4532
|
+
return this.stmt(
|
|
4533
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
4534
|
+
).all();
|
|
4535
|
+
}
|
|
4536
|
+
/** `file → package` for every indexed file that has a label. */
|
|
4537
|
+
getFilePackages() {
|
|
4538
|
+
const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
4539
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
4543
|
+
*
|
|
4544
|
+
* Distinct rather than per-ref because resolution depends only on these three
|
|
4545
|
+
* values: a file importing the same module twenty times resolves it once.
|
|
4546
|
+
*/
|
|
4547
|
+
getUnresolvedImports(onlyFiles) {
|
|
4548
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
4549
|
+
FROM refs r
|
|
4550
|
+
JOIN symbols s ON s.id = r.from_id
|
|
4551
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
4552
|
+
if (!onlyFiles?.length) {
|
|
4553
|
+
return this.stmt(base).all();
|
|
4554
|
+
}
|
|
4555
|
+
const out = [];
|
|
4556
|
+
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
4557
|
+
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
4558
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
4559
|
+
out.push(
|
|
4560
|
+
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
4561
|
+
);
|
|
4562
|
+
}
|
|
4563
|
+
return out;
|
|
4564
|
+
}
|
|
4565
|
+
/**
|
|
4566
|
+
* Write resolved import targets back onto `refs.to_file`.
|
|
4567
|
+
*
|
|
4568
|
+
* Applied through a temp table and a single UPDATE: one statement per
|
|
4569
|
+
* resolution would mean thousands of round-trips on a first index.
|
|
4570
|
+
*/
|
|
4571
|
+
applyImportResolutions(resolutions) {
|
|
4572
|
+
if (resolutions.length === 0) return 0;
|
|
4573
|
+
return this.runWithRetry(() => {
|
|
4574
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4575
|
+
this.db.exec(
|
|
4576
|
+
`CREATE TEMP TABLE import_resolution (
|
|
4577
|
+
from_file TEXT NOT NULL,
|
|
4578
|
+
lang TEXT NOT NULL,
|
|
4579
|
+
module TEXT NOT NULL,
|
|
4580
|
+
to_file TEXT NOT NULL
|
|
4581
|
+
)`
|
|
4582
|
+
);
|
|
4583
|
+
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
4584
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
4585
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
4586
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
4587
|
+
const binds = [];
|
|
4588
|
+
for (const entry of chunk) {
|
|
4589
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
4590
|
+
}
|
|
4591
|
+
this.stmt(
|
|
4592
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
4593
|
+
VALUES ${placeholders}`
|
|
4594
|
+
).run(...binds);
|
|
4595
|
+
}
|
|
4596
|
+
this.db.exec(
|
|
4597
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
4598
|
+
ON import_resolution(module, lang, from_file)`
|
|
4599
|
+
);
|
|
4600
|
+
const result = this.stmt(
|
|
4601
|
+
`UPDATE refs
|
|
4602
|
+
SET to_file = (
|
|
4603
|
+
SELECT ir.to_file
|
|
4604
|
+
FROM temp.import_resolution ir
|
|
4605
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4606
|
+
WHERE ir.module = refs.module
|
|
4607
|
+
AND ir.lang = refs.lang
|
|
4608
|
+
AND ir.from_file = s.file
|
|
4609
|
+
LIMIT 1
|
|
4610
|
+
)
|
|
4611
|
+
WHERE refs.call_type = 'import'
|
|
4612
|
+
AND refs.module IS NOT NULL
|
|
4613
|
+
AND EXISTS (
|
|
4614
|
+
SELECT 1
|
|
4615
|
+
FROM temp.import_resolution ir
|
|
4616
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
4617
|
+
WHERE ir.module = refs.module
|
|
4618
|
+
AND ir.lang = refs.lang
|
|
4619
|
+
AND ir.from_file = s.file
|
|
4620
|
+
)`
|
|
4621
|
+
).run();
|
|
4622
|
+
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
4623
|
+
return result.changes ?? 0;
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
3528
4626
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
3529
4627
|
search(query, filter, opts) {
|
|
3530
4628
|
const built = this.buildSearchWhere(query, filter);
|
|
@@ -3911,9 +5009,12 @@ var IndexStore = class _IndexStore {
|
|
|
3911
5009
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
3912
5010
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
3913
5011
|
*
|
|
3914
|
-
*
|
|
3915
|
-
* the
|
|
3916
|
-
*
|
|
5012
|
+
* A match additionally requires the referencing ref and the target symbol to
|
|
5013
|
+
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
5014
|
+
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
5015
|
+
* `Config` are declared in most languages at once, and each collision draws a
|
|
5016
|
+
* Code Atlas edge between files that never reference each other. Refs stored
|
|
5017
|
+
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
3917
5018
|
*/
|
|
3918
5019
|
resolveRefs() {
|
|
3919
5020
|
return this.runWithRetry(() => {
|
|
@@ -3922,20 +5023,35 @@ var IndexStore = class _IndexStore {
|
|
|
3922
5023
|
`UPDATE refs
|
|
3923
5024
|
SET to_id = s.id
|
|
3924
5025
|
FROM (
|
|
3925
|
-
|
|
3926
|
-
|
|
5026
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5027
|
+
FROM symbols sym
|
|
5028
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5029
|
+
GROUP BY sym.name, lf.family
|
|
5030
|
+
UNION ALL
|
|
5031
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5032
|
+
FROM symbols sym
|
|
5033
|
+
GROUP BY sym.name
|
|
5034
|
+
) AS s,
|
|
5035
|
+
lang_family AS rf
|
|
3927
5036
|
WHERE refs.to_id IS NULL
|
|
3928
5037
|
AND refs.to_name IS NOT NULL
|
|
3929
|
-
AND
|
|
5038
|
+
AND rf.lang = refs.lang
|
|
5039
|
+
AND s.name = refs.to_name
|
|
5040
|
+
AND s.family = rf.family`
|
|
3930
5041
|
).run();
|
|
3931
5042
|
return result.changes ?? 0;
|
|
3932
5043
|
} catch {
|
|
3933
5044
|
const result = this.stmt(
|
|
3934
5045
|
`UPDATE refs SET to_id = (
|
|
3935
|
-
SELECT id FROM symbols
|
|
5046
|
+
SELECT sym.id FROM symbols sym
|
|
5047
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5048
|
+
ORDER BY sym.id LIMIT 1
|
|
3936
5049
|
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
3937
|
-
AND
|
|
3938
|
-
|
|
5050
|
+
AND EXISTS (
|
|
5051
|
+
SELECT 1 FROM symbols sym
|
|
5052
|
+
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5053
|
+
)`
|
|
5054
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
3939
5055
|
return result.changes ?? 0;
|
|
3940
5056
|
}
|
|
3941
5057
|
});
|
|
@@ -4153,7 +5269,7 @@ function normalizeComparablePath(value) {
|
|
|
4153
5269
|
}
|
|
4154
5270
|
function gitOutput(projectRoot, args) {
|
|
4155
5271
|
return new Promise((resolve2, reject) => {
|
|
4156
|
-
|
|
5272
|
+
execFile(
|
|
4157
5273
|
"git",
|
|
4158
5274
|
["-C", projectRoot, ...args],
|
|
4159
5275
|
{
|
|
@@ -4284,13 +5400,40 @@ function assignRefsToSymbols2(refs, symbols) {
|
|
|
4284
5400
|
}
|
|
4285
5401
|
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
4286
5402
|
if (!owner || owner.id <= 0) continue;
|
|
4287
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
5403
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
4288
5404
|
if (seen.has(key)) continue;
|
|
4289
5405
|
seen.add(key);
|
|
4290
5406
|
assigned.push({ ...ref, fromId: owner.id });
|
|
4291
5407
|
}
|
|
4292
5408
|
return assigned;
|
|
4293
5409
|
}
|
|
5410
|
+
async function resolveProjectRelations(store, projectRoot, opts) {
|
|
5411
|
+
if (opts.signal?.aborted) return;
|
|
5412
|
+
try {
|
|
5413
|
+
const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
|
|
5414
|
+
if (indexedFiles.length === 0) return;
|
|
5415
|
+
const structure = await detectModuleRoots(projectRoot, indexedFiles);
|
|
5416
|
+
if (opts.signal?.aborted) return;
|
|
5417
|
+
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
5418
|
+
const resolver = new ModuleResolver(
|
|
5419
|
+
structure,
|
|
5420
|
+
indexedFiles,
|
|
5421
|
+
store.getNamespaceDeclarations()
|
|
5422
|
+
);
|
|
5423
|
+
const pending = store.getUnresolvedImports(opts.onlyFiles);
|
|
5424
|
+
const resolutions = [];
|
|
5425
|
+
for (const entry of pending) {
|
|
5426
|
+
const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
|
|
5427
|
+
if (toFile && toFile !== entry.fromFile) {
|
|
5428
|
+
resolutions.push({ ...entry, toFile });
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
if (opts.signal?.aborted) return;
|
|
5432
|
+
store.applyImportResolutions(resolutions);
|
|
5433
|
+
} catch (err) {
|
|
5434
|
+
opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
4294
5437
|
async function runIndexerWithStore(store, opts) {
|
|
4295
5438
|
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
4296
5439
|
const relationGraphVersion = "2";
|
|
@@ -4535,6 +5678,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4535
5678
|
}
|
|
4536
5679
|
}
|
|
4537
5680
|
if (needsFullRefResolution) store.resolveRefs();
|
|
5681
|
+
await resolveProjectRelations(store, projectRoot, {
|
|
5682
|
+
// A watcher run re-resolves only what it touched; a full run (or a contract
|
|
5683
|
+
// bump) re-resolves everything, because a newly indexed file can be the
|
|
5684
|
+
// target of imports written long before it.
|
|
5685
|
+
onlyFiles: needsFullRefResolution ? void 0 : opts.files,
|
|
5686
|
+
errors,
|
|
5687
|
+
signal
|
|
5688
|
+
});
|
|
4538
5689
|
store.setMetadata("ref_resolution_version", refResolutionVersion);
|
|
4539
5690
|
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
4540
5691
|
if (!opts.files || filesIndexed >= 50) store.optimize();
|
|
@@ -4641,7 +5792,7 @@ var inFlight = /* @__PURE__ */ new Map();
|
|
|
4641
5792
|
function post(msg) {
|
|
4642
5793
|
port.postMessage(msg);
|
|
4643
5794
|
}
|
|
4644
|
-
async function
|
|
5795
|
+
async function dispatch2(msg) {
|
|
4645
5796
|
switch (msg.op) {
|
|
4646
5797
|
case "index": {
|
|
4647
5798
|
const ac = new AbortController();
|
|
@@ -4678,7 +5829,7 @@ port.on("message", (msg) => {
|
|
|
4678
5829
|
inFlight.get(msg.id)?.abort(new Error("Indexing cancelled"));
|
|
4679
5830
|
return;
|
|
4680
5831
|
}
|
|
4681
|
-
void
|
|
5832
|
+
void dispatch2(msg).then(
|
|
4682
5833
|
(result) => post({ type: "response", id: msg.id, ok: true, result }),
|
|
4683
5834
|
(err) => {
|
|
4684
5835
|
try {
|