@wrongstack/tools 0.287.0 → 0.289.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/_fetch-guard.d.ts.map +1 -1
- package/dist/_redact-command.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/audit.js +61 -14
- package/dist/audit.js.map +3 -3
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +97 -19
- package/dist/bash.js.map +3 -3
- package/dist/builtin.d.ts +12 -10
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1620 -803
- package/dist/builtin.js.map +3 -3
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +1 -2
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/index-service.d.ts +11 -1
- package/dist/codebase-index/index-service.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -1
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +749 -192
- package/dist/codebase-index/index.js.map +3 -3
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +43 -1
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker-protocol.d.ts +1 -0
- package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +610 -152
- package/dist/codebase-index/worker.js.map +3 -3
- package/dist/codebase-index/writer.d.ts +27 -1
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/e2e.js +477 -0
- package/dist/e2e.js.map +7 -0
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -5
- package/dist/exec.js.map +3 -3
- package/dist/fetch.js +2 -2
- package/dist/fetch.js.map +2 -2
- package/dist/format.js +61 -14
- package/dist/format.js.map +3 -3
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1786 -809
- package/dist/index.js.map +3 -3
- package/dist/install.js +61 -14
- package/dist/install.js.map +3 -3
- package/dist/kanban.d.ts +11 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +695 -571
- package/dist/kanban.js.map +3 -3
- package/dist/languages/detect.d.ts.map +1 -1
- package/dist/languages/index.js +61 -14
- package/dist/languages/index.js.map +3 -3
- package/dist/languages/types.d.ts +7 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/lint.js +61 -14
- package/dist/lint.js.map +3 -3
- package/dist/outdated.js +61 -14
- package/dist/outdated.js.map +3 -3
- package/dist/pack.js +1617 -800
- package/dist/pack.js.map +3 -3
- package/dist/plan.js +4 -0
- package/dist/plan.js.map +2 -2
- package/dist/process-registry.js +12 -5
- package/dist/process-registry.js.map +2 -2
- package/dist/search.js +2 -2
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +9 -0
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +137 -0
- package/dist/session-kanban.js.map +2 -2
- package/dist/task.js +4 -0
- package/dist/task.js.map +2 -2
- package/dist/test.js +61 -14
- package/dist/test.js.map +3 -3
- package/dist/todo.js +4 -0
- package/dist/todo.js.map +2 -2
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +61 -14
- package/dist/typecheck.js.map +3 -3
- package/package.json +7 -3
|
@@ -92,7 +92,7 @@ import * as path from "node:path";
|
|
|
92
92
|
import { resolveWstackPaths } from "@wrongstack/core";
|
|
93
93
|
|
|
94
94
|
// src/codebase-index/schema.ts
|
|
95
|
-
var SCHEMA_VERSION =
|
|
95
|
+
var SCHEMA_VERSION = 3;
|
|
96
96
|
|
|
97
97
|
// src/codebase-index/lsp-kind.ts
|
|
98
98
|
function lspKindToInternalKind(k) {
|
|
@@ -164,7 +164,7 @@ function tokenise(text) {
|
|
|
164
164
|
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
165
165
|
}
|
|
166
166
|
function splitName(name) {
|
|
167
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
167
|
+
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();
|
|
168
168
|
}
|
|
169
169
|
function buildIndexableText(name, signature, docComment) {
|
|
170
170
|
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
@@ -190,14 +190,12 @@ function buildBm25Index(docs) {
|
|
|
190
190
|
return new Bm25Index(documents, df, N, avgLen);
|
|
191
191
|
}
|
|
192
192
|
var Bm25Index = class {
|
|
193
|
-
constructor(documents,
|
|
193
|
+
constructor(documents, _df, N, avgLen) {
|
|
194
194
|
this.documents = documents;
|
|
195
|
-
this.df = df;
|
|
196
195
|
this.N = N;
|
|
197
196
|
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
198
197
|
}
|
|
199
198
|
documents;
|
|
200
|
-
df;
|
|
201
199
|
N;
|
|
202
200
|
safeAvgLen;
|
|
203
201
|
score(query, filter) {
|
|
@@ -210,10 +208,13 @@ var Bm25Index = class {
|
|
|
210
208
|
for (const qTerm of qTokens) {
|
|
211
209
|
let tf = 0;
|
|
212
210
|
for (const t of doc.tokens) {
|
|
213
|
-
if (t
|
|
211
|
+
if (t.startsWith(qTerm)) tf++;
|
|
214
212
|
}
|
|
215
213
|
if (tf === 0) continue;
|
|
216
|
-
|
|
214
|
+
let dfVal = 0;
|
|
215
|
+
for (const candidate of this.documents) {
|
|
216
|
+
if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
|
|
217
|
+
}
|
|
217
218
|
if (dfVal === 0) continue;
|
|
218
219
|
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
219
220
|
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
@@ -246,6 +247,29 @@ var Bm25Index = class {
|
|
|
246
247
|
|
|
247
248
|
// src/codebase-index/writer.ts
|
|
248
249
|
var DB_FILE = "index.db";
|
|
250
|
+
function escapeLike(value) {
|
|
251
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
252
|
+
}
|
|
253
|
+
function assignRefsToSymbols(refs, symbols) {
|
|
254
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
255
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
256
|
+
const seen = /* @__PURE__ */ new Set();
|
|
257
|
+
const assigned = [];
|
|
258
|
+
for (const ref of refs) {
|
|
259
|
+
let owner;
|
|
260
|
+
for (const symbol of ordered) {
|
|
261
|
+
if (symbol.line > ref.line) break;
|
|
262
|
+
owner = symbol;
|
|
263
|
+
}
|
|
264
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
265
|
+
if (!owner || owner.id <= 0) continue;
|
|
266
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
267
|
+
if (seen.has(key)) continue;
|
|
268
|
+
seen.add(key);
|
|
269
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
270
|
+
}
|
|
271
|
+
return assigned;
|
|
272
|
+
}
|
|
249
273
|
function resolveIndexDir(projectRoot, override) {
|
|
250
274
|
return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
|
|
251
275
|
}
|
|
@@ -299,7 +323,7 @@ function sleepSync(ms) {
|
|
|
299
323
|
} catch {
|
|
300
324
|
}
|
|
301
325
|
}
|
|
302
|
-
var IndexStore = class {
|
|
326
|
+
var IndexStore = class _IndexStore {
|
|
303
327
|
db;
|
|
304
328
|
/** Absolute path to this project's index directory. */
|
|
305
329
|
indexDir;
|
|
@@ -351,10 +375,7 @@ var IndexStore = class {
|
|
|
351
375
|
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
352
376
|
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
353
377
|
}
|
|
354
|
-
const delay = Math.min(
|
|
355
|
-
LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
|
|
356
|
-
LOCK_RETRY_MAX_DELAY_MS
|
|
357
|
-
);
|
|
378
|
+
const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
|
|
358
379
|
sleepSync(delay);
|
|
359
380
|
}
|
|
360
381
|
}
|
|
@@ -436,8 +457,24 @@ var IndexStore = class {
|
|
|
436
457
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
437
458
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
438
459
|
try {
|
|
439
|
-
this.db.exec(
|
|
460
|
+
this.db.exec(
|
|
461
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
|
|
462
|
+
);
|
|
440
463
|
this.ftsAvailable = true;
|
|
464
|
+
const symbolCount = Number(
|
|
465
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
|
|
466
|
+
);
|
|
467
|
+
const ftsCount = Number(
|
|
468
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
|
|
469
|
+
);
|
|
470
|
+
if (symbolCount !== ftsCount) {
|
|
471
|
+
this.db.exec("DELETE FROM symbols_fts");
|
|
472
|
+
const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
|
|
473
|
+
const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
|
|
474
|
+
for (const row of rows) {
|
|
475
|
+
insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
441
478
|
} catch {
|
|
442
479
|
this.ftsAvailable = false;
|
|
443
480
|
}
|
|
@@ -495,7 +532,9 @@ var IndexStore = class {
|
|
|
495
532
|
deleteSymbolsForFile(file) {
|
|
496
533
|
this.runWithRetry(() => {
|
|
497
534
|
if (this.ftsAvailable) {
|
|
498
|
-
this.stmt(
|
|
535
|
+
this.stmt(
|
|
536
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
537
|
+
).run(file);
|
|
499
538
|
}
|
|
500
539
|
this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
501
540
|
});
|
|
@@ -507,9 +546,21 @@ var IndexStore = class {
|
|
|
507
546
|
*/
|
|
508
547
|
deleteFile(file) {
|
|
509
548
|
this.runWithRetry(() => {
|
|
510
|
-
this.
|
|
511
|
-
|
|
512
|
-
|
|
549
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
550
|
+
try {
|
|
551
|
+
if (this.ftsAvailable) {
|
|
552
|
+
this.db.prepare(
|
|
553
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
554
|
+
).run(file);
|
|
555
|
+
}
|
|
556
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
557
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
558
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
559
|
+
this.db.exec("COMMIT");
|
|
560
|
+
} catch (err) {
|
|
561
|
+
this.db.exec("ROLLBACK");
|
|
562
|
+
throw err;
|
|
563
|
+
}
|
|
513
564
|
});
|
|
514
565
|
}
|
|
515
566
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
@@ -532,14 +583,22 @@ var IndexStore = class {
|
|
|
532
583
|
).all(file);
|
|
533
584
|
if (!rows.length) return null;
|
|
534
585
|
const r = expectDefined(rows[0]);
|
|
535
|
-
return {
|
|
586
|
+
return {
|
|
587
|
+
file: r.file,
|
|
588
|
+
lang: r.lang,
|
|
589
|
+
mtimeMs: r.mtime_ms,
|
|
590
|
+
symbolCount: r.symbol_count,
|
|
591
|
+
lastIndexed: r.last_indexed
|
|
592
|
+
};
|
|
536
593
|
}
|
|
537
594
|
getAllFileMetas() {
|
|
538
|
-
return this.db.prepare(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
595
|
+
return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
|
|
596
|
+
file: r.file,
|
|
597
|
+
lang: r.lang,
|
|
598
|
+
mtimeMs: r.mtime_ms,
|
|
599
|
+
symbolCount: r.symbol_count,
|
|
600
|
+
lastIndexed: r.last_indexed
|
|
601
|
+
}));
|
|
543
602
|
}
|
|
544
603
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
545
604
|
search(query, filter) {
|
|
@@ -563,8 +622,8 @@ var IndexStore = class {
|
|
|
563
622
|
values.push(filter.lang);
|
|
564
623
|
}
|
|
565
624
|
if (filter?.file) {
|
|
566
|
-
conditions.push("file LIKE ?");
|
|
567
|
-
values.push(`%${filter.file}%`);
|
|
625
|
+
conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
626
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
568
627
|
}
|
|
569
628
|
if (query.trim()) {
|
|
570
629
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -602,9 +661,11 @@ var IndexStore = class {
|
|
|
602
661
|
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
603
662
|
*/
|
|
604
663
|
searchRanked(query, filter, limit) {
|
|
664
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
665
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
605
666
|
const tokens = tokenise(query);
|
|
606
667
|
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
607
|
-
return this.searchRankedFallback(query, filter,
|
|
668
|
+
return this.searchRankedFallback(query, filter, safeLimit);
|
|
608
669
|
}
|
|
609
670
|
let effectiveKind = filter?.kind;
|
|
610
671
|
if (filter?.lspKind !== void 0) {
|
|
@@ -624,11 +685,13 @@ var IndexStore = class {
|
|
|
624
685
|
values.push(filter.lang);
|
|
625
686
|
}
|
|
626
687
|
if (filter?.file) {
|
|
627
|
-
conditions.push("s.file LIKE ?");
|
|
628
|
-
values.push(`%${filter.file}%`);
|
|
688
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
689
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
629
690
|
}
|
|
630
691
|
const where = conditions.join(" AND ");
|
|
631
|
-
const countRows = this.db.prepare(
|
|
692
|
+
const countRows = this.db.prepare(
|
|
693
|
+
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
694
|
+
).all(...values);
|
|
632
695
|
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
633
696
|
if (total === 0) return { results: [], total: 0 };
|
|
634
697
|
const rows = this.db.prepare(
|
|
@@ -637,9 +700,13 @@ var IndexStore = class {
|
|
|
637
700
|
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
638
701
|
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
639
702
|
WHERE ${where}
|
|
640
|
-
ORDER BY
|
|
703
|
+
ORDER BY
|
|
704
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
705
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
706
|
+
ELSE 2 END,
|
|
707
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
641
708
|
LIMIT ?`
|
|
642
|
-
).all(...values,
|
|
709
|
+
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
643
710
|
return {
|
|
644
711
|
results: rows.map((r) => ({
|
|
645
712
|
id: r.id,
|
|
@@ -669,10 +736,28 @@ var IndexStore = class {
|
|
|
669
736
|
}
|
|
670
737
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
671
738
|
const bm25 = buildBm25Index(
|
|
672
|
-
candidates.map((c) => ({
|
|
739
|
+
candidates.map((c) => ({
|
|
740
|
+
id: c.id,
|
|
741
|
+
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
742
|
+
}))
|
|
673
743
|
);
|
|
674
744
|
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
675
|
-
|
|
745
|
+
const q = query.trim().toLowerCase();
|
|
746
|
+
const rank = (id) => {
|
|
747
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
748
|
+
if (name === q) return 0;
|
|
749
|
+
if (name.startsWith(q)) return 1;
|
|
750
|
+
return 2;
|
|
751
|
+
};
|
|
752
|
+
scored.sort((a, b) => {
|
|
753
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
754
|
+
if (rankDiff !== 0) return rankDiff;
|
|
755
|
+
const scoreDiff = b.score - a.score;
|
|
756
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
757
|
+
const left = expectDefined(candidateById.get(a.id));
|
|
758
|
+
const right = expectDefined(candidateById.get(b.id));
|
|
759
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
760
|
+
});
|
|
676
761
|
const qTokens = tokenise(query);
|
|
677
762
|
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
678
763
|
const c = expectDefined(candidateById.get(id));
|
|
@@ -681,9 +766,7 @@ var IndexStore = class {
|
|
|
681
766
|
return { results, total: candidates.length };
|
|
682
767
|
}
|
|
683
768
|
getAllIndexable() {
|
|
684
|
-
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
685
|
-
({ id, text }) => ({ id, text })
|
|
686
|
-
);
|
|
769
|
+
return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
687
770
|
}
|
|
688
771
|
/**
|
|
689
772
|
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
@@ -699,22 +782,16 @@ var IndexStore = class {
|
|
|
699
782
|
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
700
783
|
getStats() {
|
|
701
784
|
const sizeBytes = this.sizeBytes();
|
|
702
|
-
const lastRows = this.db.prepare(
|
|
703
|
-
"SELECT value FROM metadata WHERE key = 'last_indexed'"
|
|
704
|
-
).all();
|
|
785
|
+
const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
|
|
705
786
|
const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
|
|
706
787
|
const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
|
|
707
788
|
const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
|
|
708
789
|
const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
|
|
709
790
|
const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
|
|
710
|
-
const langRows = this.db.prepare(
|
|
711
|
-
"SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
|
|
712
|
-
).all();
|
|
791
|
+
const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
|
|
713
792
|
const byLang = {};
|
|
714
793
|
for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
|
|
715
|
-
const kindRows = this.db.prepare(
|
|
716
|
-
"SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
|
|
717
|
-
).all();
|
|
794
|
+
const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
|
|
718
795
|
const byKind = {};
|
|
719
796
|
for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
|
|
720
797
|
return {
|
|
@@ -730,17 +807,31 @@ var IndexStore = class {
|
|
|
730
807
|
}
|
|
731
808
|
setLastIndexed(ts2) {
|
|
732
809
|
this.runWithRetry(() => {
|
|
733
|
-
this.db.prepare(
|
|
734
|
-
|
|
735
|
-
|
|
810
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
getMetadata(key) {
|
|
814
|
+
const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
|
|
815
|
+
return rows[0]?.value;
|
|
816
|
+
}
|
|
817
|
+
setMetadata(key, value) {
|
|
818
|
+
this.runWithRetry(() => {
|
|
819
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
|
|
736
820
|
});
|
|
737
821
|
}
|
|
738
822
|
clearAll() {
|
|
739
823
|
this.runWithRetry(() => {
|
|
740
|
-
this.db.exec("
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
824
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
825
|
+
try {
|
|
826
|
+
this.db.exec("DELETE FROM refs");
|
|
827
|
+
this.db.exec("DELETE FROM symbols");
|
|
828
|
+
this.db.exec("DELETE FROM files");
|
|
829
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
830
|
+
this.db.exec("COMMIT");
|
|
831
|
+
} catch (err) {
|
|
832
|
+
this.db.exec("ROLLBACK");
|
|
833
|
+
throw err;
|
|
834
|
+
}
|
|
744
835
|
});
|
|
745
836
|
}
|
|
746
837
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -831,6 +922,7 @@ var IndexStore = class {
|
|
|
831
922
|
const allInserted = [];
|
|
832
923
|
const refsToInsert = [];
|
|
833
924
|
for (const entry of entries) {
|
|
925
|
+
const insertedForEntry = [];
|
|
834
926
|
for (const s of entry.symbols) {
|
|
835
927
|
const id = nextId++;
|
|
836
928
|
symStmt.run(
|
|
@@ -848,9 +940,11 @@ var IndexStore = class {
|
|
|
848
940
|
s.file
|
|
849
941
|
);
|
|
850
942
|
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
851
|
-
|
|
943
|
+
const inserted = { ...s, id };
|
|
944
|
+
allInserted.push(inserted);
|
|
945
|
+
insertedForEntry.push(inserted);
|
|
852
946
|
}
|
|
853
|
-
|
|
947
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
854
948
|
}
|
|
855
949
|
if (refsToInsert.length > 0) {
|
|
856
950
|
const refStmt = this.db.prepare(
|
|
@@ -888,9 +982,7 @@ var IndexStore = class {
|
|
|
888
982
|
*/
|
|
889
983
|
deleteRefsForFile(file) {
|
|
890
984
|
this.runWithRetry(() => {
|
|
891
|
-
this.db.prepare(
|
|
892
|
-
"DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
893
|
-
).run(file);
|
|
985
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
|
|
894
986
|
});
|
|
895
987
|
}
|
|
896
988
|
/**
|
|
@@ -931,9 +1023,7 @@ var IndexStore = class {
|
|
|
931
1023
|
* Find all references FROM a given symbol (what does this symbol call/use?).
|
|
932
1024
|
*/
|
|
933
1025
|
findRefsFrom(symbolId) {
|
|
934
|
-
return this.db.prepare(
|
|
935
|
-
"SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
|
|
936
|
-
).all(symbolId).map((r) => ({
|
|
1026
|
+
return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
|
|
937
1027
|
id: r.id,
|
|
938
1028
|
fromId: r.from_id,
|
|
939
1029
|
toName: r.to_name,
|
|
@@ -950,6 +1040,325 @@ var IndexStore = class {
|
|
|
950
1040
|
return 0;
|
|
951
1041
|
}
|
|
952
1042
|
}
|
|
1043
|
+
// ─── CodeMap graph aggregation ──────────────────────────────────────────────
|
|
1044
|
+
/**
|
|
1045
|
+
* Derive a monorepo package name from an absolute file path.
|
|
1046
|
+
* Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
|
|
1047
|
+
* Returns `undefined` for files outside any recognisable package structure.
|
|
1048
|
+
*/
|
|
1049
|
+
static derivePackage(filePath) {
|
|
1050
|
+
const f = filePath.replace(/\\/g, "/");
|
|
1051
|
+
const pkgsIdx = f.indexOf("/packages/");
|
|
1052
|
+
if (pkgsIdx !== -1) {
|
|
1053
|
+
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
1054
|
+
const seg = rest.split("/")[0];
|
|
1055
|
+
return seg ? `@wrongstack/${seg}` : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
const appsIdx = f.indexOf("/apps/");
|
|
1058
|
+
if (appsIdx !== -1) {
|
|
1059
|
+
const rest = f.slice(appsIdx + "/apps/".length);
|
|
1060
|
+
const seg = rest.split("/")[0];
|
|
1061
|
+
return seg ? `app:${seg}` : void 0;
|
|
1062
|
+
}
|
|
1063
|
+
return void 0;
|
|
1064
|
+
}
|
|
1065
|
+
static packageFromImport(moduleName) {
|
|
1066
|
+
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
1067
|
+
const parts = moduleName.split("/");
|
|
1068
|
+
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
1069
|
+
}
|
|
1070
|
+
static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
1071
|
+
if (!moduleName.startsWith(".")) return void 0;
|
|
1072
|
+
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
1073
|
+
const absolute = path.posix.normalize(
|
|
1074
|
+
path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
|
|
1075
|
+
);
|
|
1076
|
+
const extension = path.posix.extname(absolute);
|
|
1077
|
+
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
1078
|
+
const candidates = [
|
|
1079
|
+
absolute,
|
|
1080
|
+
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
1081
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
|
|
1082
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
|
|
1083
|
+
];
|
|
1084
|
+
const indexedByPortablePath = new Map(
|
|
1085
|
+
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
1086
|
+
);
|
|
1087
|
+
for (const candidate of candidates) {
|
|
1088
|
+
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
1089
|
+
if (indexed) return indexed;
|
|
1090
|
+
}
|
|
1091
|
+
return void 0;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Package-level graph: each workspace package is a node; edges are derived
|
|
1095
|
+
* from cross-package symbol references (a symbol in package A references a
|
|
1096
|
+
* symbol resolved in package B). Node metadata includes symbol/file counts.
|
|
1097
|
+
*/
|
|
1098
|
+
getPackageGraph() {
|
|
1099
|
+
const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
|
|
1100
|
+
const pkgNodes = /* @__PURE__ */ new Map();
|
|
1101
|
+
const fileToPkg = /* @__PURE__ */ new Map();
|
|
1102
|
+
const symbolToPkg = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const s of symbols) {
|
|
1104
|
+
const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
|
|
1105
|
+
fileToPkg.set(s.file, pkg);
|
|
1106
|
+
symbolToPkg.set(s.id, pkg);
|
|
1107
|
+
const node = pkgNodes.get(pkg);
|
|
1108
|
+
if (node) {
|
|
1109
|
+
node.symbolCount = (node.symbolCount ?? 0) + 1;
|
|
1110
|
+
} else {
|
|
1111
|
+
pkgNodes.set(pkg, {
|
|
1112
|
+
id: `pkg:${pkg}`,
|
|
1113
|
+
label: pkg,
|
|
1114
|
+
kind: "package",
|
|
1115
|
+
package: pkg,
|
|
1116
|
+
symbolCount: 1,
|
|
1117
|
+
fileCount: 0
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
|
|
1122
|
+
for (const { file } of files) {
|
|
1123
|
+
const pkg = _IndexStore.derivePackage(file) ?? "(root)";
|
|
1124
|
+
const node = pkgNodes.get(pkg);
|
|
1125
|
+
if (node) node.fileCount = (node.fileCount ?? 0) + 1;
|
|
1126
|
+
}
|
|
1127
|
+
const refRows = this.db.prepare(
|
|
1128
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1129
|
+
FROM refs r
|
|
1130
|
+
WHERE r.to_id IS NOT NULL`
|
|
1131
|
+
).all();
|
|
1132
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1133
|
+
for (const r of refRows) {
|
|
1134
|
+
if (r.call_type === "import") continue;
|
|
1135
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1136
|
+
const toPkg = symbolToPkg.get(r.to_id);
|
|
1137
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
|
|
1138
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1139
|
+
let e = edgeMap.get(key);
|
|
1140
|
+
if (!e) {
|
|
1141
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1142
|
+
edgeMap.set(key, e);
|
|
1143
|
+
}
|
|
1144
|
+
e.weight++;
|
|
1145
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1146
|
+
}
|
|
1147
|
+
const importRows = this.db.prepare(
|
|
1148
|
+
`SELECT r.from_id, r.to_name
|
|
1149
|
+
FROM refs r
|
|
1150
|
+
WHERE r.call_type = 'import'`
|
|
1151
|
+
).all();
|
|
1152
|
+
for (const r of importRows) {
|
|
1153
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1154
|
+
const toPkg = _IndexStore.packageFromImport(r.to_name);
|
|
1155
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
1156
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1157
|
+
let edge = edgeMap.get(key);
|
|
1158
|
+
if (!edge) {
|
|
1159
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1160
|
+
edgeMap.set(key, edge);
|
|
1161
|
+
}
|
|
1162
|
+
edge.weight++;
|
|
1163
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1164
|
+
}
|
|
1165
|
+
const edges = [];
|
|
1166
|
+
for (const [key, e] of edgeMap) {
|
|
1167
|
+
const [source, target] = key.split("\0");
|
|
1168
|
+
let bestType = "call";
|
|
1169
|
+
let bestCount = 0;
|
|
1170
|
+
for (const [t, c] of e.types) {
|
|
1171
|
+
if (c > bestCount) {
|
|
1172
|
+
bestType = t;
|
|
1173
|
+
bestCount = c;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
edges.push({
|
|
1177
|
+
source: `pkg:${source}`,
|
|
1178
|
+
target: `pkg:${target}`,
|
|
1179
|
+
weight: e.weight,
|
|
1180
|
+
refType: bestType
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return { nodes: [...pkgNodes.values()], edges };
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* File-level graph for a single package: each file is a node; edges are
|
|
1187
|
+
* derived from cross-file symbol references within the package.
|
|
1188
|
+
*/
|
|
1189
|
+
getFileGraph(packageFilter) {
|
|
1190
|
+
const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
|
|
1191
|
+
const pkgSyms = allSymbols.filter(
|
|
1192
|
+
(s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
|
|
1193
|
+
);
|
|
1194
|
+
if (pkgSyms.length === 0) return { nodes: [], edges: [] };
|
|
1195
|
+
const fileNodes = /* @__PURE__ */ new Map();
|
|
1196
|
+
const symToFile = /* @__PURE__ */ new Map();
|
|
1197
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const s of allSymbols) {
|
|
1199
|
+
symToFile.set(s.id, s.file);
|
|
1200
|
+
const current = fileStats.get(s.file);
|
|
1201
|
+
fileStats.set(s.file, {
|
|
1202
|
+
count: (current?.count ?? 0) + 1,
|
|
1203
|
+
lang: current?.lang ?? s.lang
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
const localFiles = new Set(pkgSyms.map((s) => s.file));
|
|
1207
|
+
const indexedFiles = new Set(allSymbols.map((s) => s.file));
|
|
1208
|
+
const ensureFileNode = (file) => {
|
|
1209
|
+
if (fileNodes.has(file)) return;
|
|
1210
|
+
const stats = fileStats.get(file);
|
|
1211
|
+
fileNodes.set(file, {
|
|
1212
|
+
id: `file:${file}`,
|
|
1213
|
+
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
1214
|
+
kind: "file",
|
|
1215
|
+
package: _IndexStore.derivePackage(file) ?? "(root)",
|
|
1216
|
+
file,
|
|
1217
|
+
symbolCount: stats?.count ?? 0,
|
|
1218
|
+
lang: stats?.lang,
|
|
1219
|
+
external: !localFiles.has(file)
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1222
|
+
for (const file of localFiles) {
|
|
1223
|
+
ensureFileNode(file);
|
|
1224
|
+
}
|
|
1225
|
+
const refRows = this.db.prepare(
|
|
1226
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1227
|
+
FROM refs r
|
|
1228
|
+
WHERE r.to_id IS NOT NULL`
|
|
1229
|
+
).all();
|
|
1230
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1231
|
+
for (const r of refRows) {
|
|
1232
|
+
if (r.call_type === "import") continue;
|
|
1233
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1234
|
+
const toFile = symToFile.get(r.to_id);
|
|
1235
|
+
if (!fromFile || !toFile || fromFile === toFile) continue;
|
|
1236
|
+
if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
|
|
1237
|
+
ensureFileNode(fromFile);
|
|
1238
|
+
ensureFileNode(toFile);
|
|
1239
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1240
|
+
let e = edgeMap.get(key);
|
|
1241
|
+
if (!e) {
|
|
1242
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1243
|
+
edgeMap.set(key, e);
|
|
1244
|
+
}
|
|
1245
|
+
e.weight++;
|
|
1246
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1247
|
+
}
|
|
1248
|
+
const importRows = this.db.prepare(
|
|
1249
|
+
`SELECT r.from_id, r.to_name
|
|
1250
|
+
FROM refs r
|
|
1251
|
+
WHERE r.call_type = 'import'`
|
|
1252
|
+
).all();
|
|
1253
|
+
for (const r of importRows) {
|
|
1254
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1255
|
+
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
1256
|
+
const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
|
|
1257
|
+
if (!toFile || fromFile === toFile) continue;
|
|
1258
|
+
ensureFileNode(fromFile);
|
|
1259
|
+
ensureFileNode(toFile);
|
|
1260
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1261
|
+
let edge = edgeMap.get(key);
|
|
1262
|
+
if (!edge) {
|
|
1263
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1264
|
+
edgeMap.set(key, edge);
|
|
1265
|
+
}
|
|
1266
|
+
edge.weight++;
|
|
1267
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1268
|
+
}
|
|
1269
|
+
const edges = [];
|
|
1270
|
+
for (const [key, e] of edgeMap) {
|
|
1271
|
+
const [source, target] = key.split("\0");
|
|
1272
|
+
let bestType = "call";
|
|
1273
|
+
let bestCount = 0;
|
|
1274
|
+
for (const [t, c] of e.types) {
|
|
1275
|
+
if (c > bestCount) {
|
|
1276
|
+
bestType = t;
|
|
1277
|
+
bestCount = c;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
edges.push({
|
|
1281
|
+
source: `file:${source}`,
|
|
1282
|
+
target: `file:${target}`,
|
|
1283
|
+
weight: e.weight,
|
|
1284
|
+
refType: bestType
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
return { nodes: [...fileNodes.values()], edges };
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Symbol-level graph for a single file: each symbol is a node; edges are
|
|
1291
|
+
* derived from intra-file and cross-file symbol references (who calls whom).
|
|
1292
|
+
*/
|
|
1293
|
+
getSymbolGraph(fileFilter) {
|
|
1294
|
+
const allSymbols = this.db.prepare(
|
|
1295
|
+
"SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
|
|
1296
|
+
).all();
|
|
1297
|
+
const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
|
|
1298
|
+
if (syms.length === 0) return { nodes: [], edges: [] };
|
|
1299
|
+
const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
|
|
1300
|
+
const relatedIds = new Set(syms.map((symbol) => symbol.id));
|
|
1301
|
+
const toGraphNode = (s) => ({
|
|
1302
|
+
id: `sym:${s.id}`,
|
|
1303
|
+
label: s.name,
|
|
1304
|
+
kind: "symbol",
|
|
1305
|
+
symbolId: s.id,
|
|
1306
|
+
symbolKind: s.kind,
|
|
1307
|
+
file: s.file,
|
|
1308
|
+
package: _IndexStore.derivePackage(s.file) ?? "(root)",
|
|
1309
|
+
lang: s.lang,
|
|
1310
|
+
line: s.line,
|
|
1311
|
+
signature: s.signature,
|
|
1312
|
+
scope: s.scope,
|
|
1313
|
+
external: s.file !== fileFilter
|
|
1314
|
+
});
|
|
1315
|
+
const symIds = new Set(syms.map((s) => s.id));
|
|
1316
|
+
const refRows = this.db.prepare(
|
|
1317
|
+
`SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
1318
|
+
FROM refs r
|
|
1319
|
+
WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
|
|
1320
|
+
OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
|
|
1321
|
+
).all(fileFilter, fileFilter);
|
|
1322
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1323
|
+
for (const r of refRows) {
|
|
1324
|
+
if (r.to_id === null) continue;
|
|
1325
|
+
if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
|
|
1326
|
+
relatedIds.add(r.from_id);
|
|
1327
|
+
relatedIds.add(r.to_id);
|
|
1328
|
+
const key = `${r.from_id}\0${r.to_id}`;
|
|
1329
|
+
let e = edgeMap.get(key);
|
|
1330
|
+
if (!e) {
|
|
1331
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1332
|
+
edgeMap.set(key, e);
|
|
1333
|
+
}
|
|
1334
|
+
e.weight++;
|
|
1335
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1336
|
+
}
|
|
1337
|
+
const edges = [];
|
|
1338
|
+
for (const [key, e] of edgeMap) {
|
|
1339
|
+
const [fromId, toId] = key.split("\0").map(Number);
|
|
1340
|
+
let bestType = "call";
|
|
1341
|
+
let bestCount = 0;
|
|
1342
|
+
for (const [t, c] of e.types) {
|
|
1343
|
+
if (c > bestCount) {
|
|
1344
|
+
bestType = t;
|
|
1345
|
+
bestCount = c;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
edges.push({
|
|
1349
|
+
source: `sym:${fromId}`,
|
|
1350
|
+
target: `sym:${toId}`,
|
|
1351
|
+
weight: e.weight,
|
|
1352
|
+
refType: bestType
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
1356
|
+
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
1357
|
+
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
1358
|
+
return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
|
|
1359
|
+
}).map(toGraphNode);
|
|
1360
|
+
return { nodes, edges };
|
|
1361
|
+
}
|
|
953
1362
|
close() {
|
|
954
1363
|
this.stmtCache.clear();
|
|
955
1364
|
try {
|
|
@@ -1044,6 +1453,14 @@ function getJsDoc(node, sourceFile) {
|
|
|
1044
1453
|
}
|
|
1045
1454
|
return "";
|
|
1046
1455
|
}
|
|
1456
|
+
function hasFunctionLikeAncestor(node) {
|
|
1457
|
+
let current = node.parent;
|
|
1458
|
+
while (current) {
|
|
1459
|
+
if (ts.isFunctionLike(current)) return true;
|
|
1460
|
+
current = current.parent;
|
|
1461
|
+
}
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1047
1464
|
function buildScope(node) {
|
|
1048
1465
|
const parts = [];
|
|
1049
1466
|
let current = node.parent;
|
|
@@ -1071,6 +1488,10 @@ function parseSymbols(opts) {
|
|
|
1071
1488
|
function visit(node) {
|
|
1072
1489
|
const kind = kindOf(node);
|
|
1073
1490
|
if (kind) {
|
|
1491
|
+
if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
|
|
1492
|
+
ts.forEachChild(node, visit);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1074
1495
|
const nameNode = node.name;
|
|
1075
1496
|
if (!nameNode || !ts.isIdentifier(nameNode)) return;
|
|
1076
1497
|
const name = nameNode.text;
|
|
@@ -2038,9 +2459,9 @@ function parseSymbols5(opts) {
|
|
|
2038
2459
|
function regexParse2(opts) {
|
|
2039
2460
|
const { file, content, lang } = opts;
|
|
2040
2461
|
const symbols = [];
|
|
2041
|
-
const
|
|
2042
|
-
const isPackageJson =
|
|
2043
|
-
const isTsconfig =
|
|
2462
|
+
const basename3 = path6.basename(file).toLowerCase();
|
|
2463
|
+
const isPackageJson = basename3 === "package.json";
|
|
2464
|
+
const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
|
|
2044
2465
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
2045
2466
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
2046
2467
|
const lines = content.split("\n");
|
|
@@ -2442,9 +2863,7 @@ function yieldEventLoop() {
|
|
|
2442
2863
|
function throwIfAborted(signal) {
|
|
2443
2864
|
if (!signal?.aborted) return;
|
|
2444
2865
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
2445
|
-
throw new Error(
|
|
2446
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
2447
|
-
);
|
|
2866
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
|
|
2448
2867
|
}
|
|
2449
2868
|
function isAbortError(err) {
|
|
2450
2869
|
return err instanceof DOMException && err.name === "AbortError";
|
|
@@ -2460,8 +2879,20 @@ var DEFAULT_IGNORE = [
|
|
|
2460
2879
|
"__snapshots__",
|
|
2461
2880
|
".nyc_output"
|
|
2462
2881
|
];
|
|
2882
|
+
var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
|
|
2883
|
+
var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
|
|
2884
|
+
function isWithinProject(projectRoot, file) {
|
|
2885
|
+
const rel = path8.relative(projectRoot, file);
|
|
2886
|
+
return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
|
|
2887
|
+
}
|
|
2888
|
+
function isMissingPathError(err) {
|
|
2889
|
+
const code = err?.code;
|
|
2890
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2891
|
+
}
|
|
2463
2892
|
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
2464
2893
|
const results = [];
|
|
2894
|
+
const errors = [];
|
|
2895
|
+
let complete = true;
|
|
2465
2896
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
2466
2897
|
const globs = [
|
|
2467
2898
|
{ ext: ".ts", pat: compileGlob2("**/*.ts") },
|
|
@@ -2485,7 +2916,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2485
2916
|
let entries;
|
|
2486
2917
|
try {
|
|
2487
2918
|
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2488
|
-
} catch {
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
complete = false;
|
|
2921
|
+
errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2489
2922
|
return;
|
|
2490
2923
|
}
|
|
2491
2924
|
dirCount++;
|
|
@@ -2497,8 +2930,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2497
2930
|
if (isGitIgnored(rel, true)) continue;
|
|
2498
2931
|
await walk(full);
|
|
2499
2932
|
} else if (e.isFile()) {
|
|
2500
|
-
if (isGitIgnored(rel, false)) continue;
|
|
2501
|
-
const ext = path8.extname(e.name);
|
|
2933
|
+
if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
|
|
2934
|
+
const ext = path8.extname(e.name).toLowerCase();
|
|
2502
2935
|
for (const { ext: extName, pat } of globs) {
|
|
2503
2936
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
2504
2937
|
results.push(full);
|
|
@@ -2509,7 +2942,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2509
2942
|
}
|
|
2510
2943
|
};
|
|
2511
2944
|
await walk(projectRoot);
|
|
2512
|
-
return results;
|
|
2945
|
+
return { files: results, complete, errors };
|
|
2513
2946
|
}
|
|
2514
2947
|
async function parseFile(file, content, lang) {
|
|
2515
2948
|
switch (lang) {
|
|
@@ -2532,6 +2965,26 @@ async function parseFile(file, content, lang) {
|
|
|
2532
2965
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2533
2966
|
}
|
|
2534
2967
|
}
|
|
2968
|
+
function assignRefsToSymbols2(refs, symbols) {
|
|
2969
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
2970
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
2971
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2972
|
+
const assigned = [];
|
|
2973
|
+
for (const ref of refs) {
|
|
2974
|
+
let owner;
|
|
2975
|
+
for (const symbol of ordered) {
|
|
2976
|
+
if (symbol.line > ref.line) break;
|
|
2977
|
+
owner = symbol;
|
|
2978
|
+
}
|
|
2979
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2980
|
+
if (!owner || owner.id <= 0) continue;
|
|
2981
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
2982
|
+
if (seen.has(key)) continue;
|
|
2983
|
+
seen.add(key);
|
|
2984
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
2985
|
+
}
|
|
2986
|
+
return assigned;
|
|
2987
|
+
}
|
|
2535
2988
|
async function runIndexer(_ctx, opts) {
|
|
2536
2989
|
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
2537
2990
|
try {
|
|
@@ -2544,7 +2997,9 @@ async function runIndexer(_ctx, opts) {
|
|
|
2544
2997
|
}
|
|
2545
2998
|
}
|
|
2546
2999
|
async function runIndexerWithStore(store, opts) {
|
|
2547
|
-
const { projectRoot,
|
|
3000
|
+
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
3001
|
+
const relationGraphVersion = "2";
|
|
3002
|
+
const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
|
|
2548
3003
|
const startMs = Date.now();
|
|
2549
3004
|
const errors = [];
|
|
2550
3005
|
const langStats = {};
|
|
@@ -2553,10 +3008,18 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2553
3008
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2554
3009
|
let files;
|
|
2555
3010
|
let discoveredFiles = null;
|
|
3011
|
+
let discoveryComplete = true;
|
|
2556
3012
|
if (opts.files && opts.files.length > 0) {
|
|
2557
|
-
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) =>
|
|
3013
|
+
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
|
|
3014
|
+
if (!isWithinProject(projectRoot, f)) return false;
|
|
3015
|
+
const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
|
|
3016
|
+
return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
|
|
3017
|
+
});
|
|
2558
3018
|
} else {
|
|
2559
|
-
|
|
3019
|
+
const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
3020
|
+
files = discovery.files;
|
|
3021
|
+
errors.push(...discovery.errors);
|
|
3022
|
+
discoveryComplete = discovery.complete;
|
|
2560
3023
|
discoveredFiles = new Set(files);
|
|
2561
3024
|
}
|
|
2562
3025
|
if (langs && langs.length > 0) {
|
|
@@ -2581,36 +3044,66 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2581
3044
|
}
|
|
2582
3045
|
const statOpts = signal ? { signal } : {};
|
|
2583
3046
|
const statReadParse = await Promise.allSettled(
|
|
2584
|
-
batchFiles.map(
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
if (
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
3047
|
+
batchFiles.map(
|
|
3048
|
+
async (file) => {
|
|
3049
|
+
let stat2;
|
|
3050
|
+
try {
|
|
3051
|
+
stat2 = await fs7.stat(file, statOpts);
|
|
3052
|
+
} catch (e) {
|
|
3053
|
+
if (isAbortError(e)) throw e;
|
|
3054
|
+
return {
|
|
3055
|
+
file,
|
|
3056
|
+
stat: null,
|
|
3057
|
+
lang: "",
|
|
3058
|
+
parsed: null,
|
|
3059
|
+
error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
|
|
3060
|
+
missing: isMissingPathError(e)
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
|
|
3064
|
+
const lang = detectLang(file);
|
|
3065
|
+
if (!lang) return { file, stat: stat2, lang: "", parsed: null };
|
|
3066
|
+
if (stat2.size > MAX_INDEX_FILE_BYTES) {
|
|
3067
|
+
return {
|
|
3068
|
+
file,
|
|
3069
|
+
stat: stat2,
|
|
3070
|
+
lang,
|
|
3071
|
+
parsed: null,
|
|
3072
|
+
error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
const meta = existingMeta.get(file);
|
|
3076
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
|
|
3077
|
+
return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
|
|
3078
|
+
}
|
|
3079
|
+
let content;
|
|
3080
|
+
try {
|
|
3081
|
+
content = await fs7.readFile(file, { encoding: "utf8", signal });
|
|
3082
|
+
} catch (e) {
|
|
3083
|
+
if (isAbortError(e)) throw e;
|
|
3084
|
+
return {
|
|
3085
|
+
file,
|
|
3086
|
+
stat: stat2,
|
|
3087
|
+
lang,
|
|
3088
|
+
parsed: null,
|
|
3089
|
+
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
let parsed;
|
|
3093
|
+
try {
|
|
3094
|
+
parsed = await parseFile(file, content, lang);
|
|
3095
|
+
} catch (e) {
|
|
3096
|
+
return {
|
|
3097
|
+
file,
|
|
3098
|
+
stat: stat2,
|
|
3099
|
+
lang,
|
|
3100
|
+
parsed: null,
|
|
3101
|
+
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
return { file, stat: stat2, lang, parsed, content };
|
|
2611
3105
|
}
|
|
2612
|
-
|
|
2613
|
-
})
|
|
3106
|
+
)
|
|
2614
3107
|
);
|
|
2615
3108
|
const batchEntries = [];
|
|
2616
3109
|
const deleteForFiles = [];
|
|
@@ -2625,8 +3118,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2625
3118
|
}
|
|
2626
3119
|
const result = settled.value;
|
|
2627
3120
|
if (result.error) {
|
|
2628
|
-
if (result.
|
|
2629
|
-
|
|
3121
|
+
if (result.missing) store.deleteFile(file);
|
|
3122
|
+
errors.push(`${file}: ${result.error}`);
|
|
2630
3123
|
continue;
|
|
2631
3124
|
}
|
|
2632
3125
|
const { stat: stat2, lang, parsed } = result;
|
|
@@ -2650,6 +3143,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2650
3143
|
continue;
|
|
2651
3144
|
}
|
|
2652
3145
|
if (parsed.symbols.length === 0) {
|
|
3146
|
+
store.deleteRefsForFile(file);
|
|
3147
|
+
store.deleteSymbolsForFile(file);
|
|
2653
3148
|
store.upsertFile({
|
|
2654
3149
|
file,
|
|
2655
3150
|
lang,
|
|
@@ -2660,15 +3155,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2660
3155
|
filesIndexed++;
|
|
2661
3156
|
continue;
|
|
2662
3157
|
}
|
|
2663
|
-
const refs = [];
|
|
2664
|
-
if (parsed.refs && parsed.refs.length > 0) {
|
|
2665
|
-
for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
|
|
2666
|
-
}
|
|
2667
3158
|
batchEntries.push({
|
|
2668
3159
|
file,
|
|
2669
3160
|
lang,
|
|
2670
3161
|
symbols: parsed.symbols,
|
|
2671
|
-
refs,
|
|
3162
|
+
refs: parsed.refs ?? [],
|
|
2672
3163
|
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
2673
3164
|
symbolCount: parsed.symbols.length
|
|
2674
3165
|
});
|
|
@@ -2676,34 +3167,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2676
3167
|
}
|
|
2677
3168
|
if (batchEntries.length > 0) {
|
|
2678
3169
|
try {
|
|
2679
|
-
|
|
2680
|
-
let cursor = 0;
|
|
3170
|
+
store.commitBatch(batchEntries, { deleteForFiles });
|
|
2681
3171
|
for (const entry of batchEntries) {
|
|
2682
3172
|
const count = entry.symbols.length;
|
|
2683
|
-
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
2684
|
-
cursor += count;
|
|
2685
3173
|
symbolsIndexed += count;
|
|
2686
3174
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
2687
3175
|
filesIndexed++;
|
|
2688
|
-
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2689
|
-
const refsByLine = /* @__PURE__ */ new Map();
|
|
2690
|
-
for (let i = 0; i < symbolsWithIds.length; i++) {
|
|
2691
|
-
const sym = symbolsWithIds[i];
|
|
2692
|
-
let arr = refsByLine.get(sym.line);
|
|
2693
|
-
if (!arr) {
|
|
2694
|
-
arr = [];
|
|
2695
|
-
refsByLine.set(sym.line, arr);
|
|
2696
|
-
}
|
|
2697
|
-
arr.push(i);
|
|
2698
|
-
}
|
|
2699
|
-
for (const ref of entry.refs) {
|
|
2700
|
-
const indices = refsByLine.get(ref.line);
|
|
2701
|
-
if (indices && indices.length > 0) {
|
|
2702
|
-
const idx = indices.shift();
|
|
2703
|
-
ref.fromId = symbolsWithIds[idx].id;
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
3176
|
}
|
|
2708
3177
|
} catch (err) {
|
|
2709
3178
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2717,23 +3186,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2717
3186
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
2718
3187
|
filesIndexed++;
|
|
2719
3188
|
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2720
|
-
const
|
|
2721
|
-
for (const sym of symbolsWithIds) {
|
|
2722
|
-
let arr = refsByLine.get(sym.line);
|
|
2723
|
-
if (!arr) {
|
|
2724
|
-
arr = [];
|
|
2725
|
-
refsByLine.set(sym.line, arr);
|
|
2726
|
-
}
|
|
2727
|
-
arr.push(sym);
|
|
2728
|
-
}
|
|
2729
|
-
const fallbackBatch = [];
|
|
2730
|
-
for (const ref of entry.refs) {
|
|
2731
|
-
const syms = refsByLine.get(ref.line);
|
|
2732
|
-
if (syms && syms.length > 0) {
|
|
2733
|
-
const sym = syms.shift();
|
|
2734
|
-
fallbackBatch.push({ ...ref, fromId: sym.id });
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
3189
|
+
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
2737
3190
|
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
2738
3191
|
}
|
|
2739
3192
|
store.upsertFile({
|
|
@@ -2744,19 +3197,23 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2744
3197
|
lastIndexed: Date.now()
|
|
2745
3198
|
});
|
|
2746
3199
|
} catch (innerErr) {
|
|
2747
|
-
errors.push(
|
|
3200
|
+
errors.push(
|
|
3201
|
+
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
3202
|
+
);
|
|
2748
3203
|
}
|
|
2749
3204
|
}
|
|
2750
3205
|
}
|
|
2751
3206
|
}
|
|
2752
3207
|
}
|
|
2753
|
-
if (discoveredFiles) {
|
|
3208
|
+
if (discoveredFiles && discoveryComplete) {
|
|
2754
3209
|
for (const [file_] of existingMeta) {
|
|
2755
3210
|
if (!discoveredFiles.has(file_)) {
|
|
2756
3211
|
store.deleteFile(file_);
|
|
2757
3212
|
}
|
|
2758
3213
|
}
|
|
2759
3214
|
}
|
|
3215
|
+
store.resolveRefs();
|
|
3216
|
+
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
2760
3217
|
const durationMs = Date.now() - startMs;
|
|
2761
3218
|
store.setLastIndexed(Date.now());
|
|
2762
3219
|
return {
|
|
@@ -2816,11 +3273,35 @@ function statsService(args) {
|
|
|
2816
3273
|
store.close();
|
|
2817
3274
|
}
|
|
2818
3275
|
}
|
|
3276
|
+
function packageGraphService(args) {
|
|
3277
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3278
|
+
try {
|
|
3279
|
+
return store.getPackageGraph();
|
|
3280
|
+
} finally {
|
|
3281
|
+
store.close();
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
function fileGraphService(args) {
|
|
3285
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3286
|
+
try {
|
|
3287
|
+
return store.getFileGraph(args.packageFilter);
|
|
3288
|
+
} finally {
|
|
3289
|
+
store.close();
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
function symbolGraphService(args) {
|
|
3293
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3294
|
+
try {
|
|
3295
|
+
return store.getSymbolGraph(args.fileFilter);
|
|
3296
|
+
} finally {
|
|
3297
|
+
store.close();
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2819
3300
|
|
|
2820
3301
|
// src/codebase-index/background-indexer.ts
|
|
2821
3302
|
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
|
|
2822
3303
|
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
|
|
2823
|
-
var DEFAULT_QUERY_TIMEOUT_MS =
|
|
3304
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
2824
3305
|
var _ready = false;
|
|
2825
3306
|
var _indexing = false;
|
|
2826
3307
|
var _currentFile = 0;
|
|
@@ -2898,14 +3379,22 @@ function ensureWorker() {
|
|
|
2898
3379
|
if (!entry) return;
|
|
2899
3380
|
pending.delete(msg.id);
|
|
2900
3381
|
if (msg.ok) entry.resolve(msg.result);
|
|
2901
|
-
else
|
|
3382
|
+
else {
|
|
3383
|
+
const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
|
|
3384
|
+
if (msg.errorName && msg.errorName !== "Error") {
|
|
3385
|
+
error.name = msg.errorName;
|
|
3386
|
+
}
|
|
3387
|
+
entry.reject(error);
|
|
3388
|
+
}
|
|
2902
3389
|
});
|
|
2903
3390
|
w.on("error", (err) => {
|
|
3391
|
+
if (worker !== w) return;
|
|
2904
3392
|
worker = null;
|
|
2905
3393
|
failAllPending(err);
|
|
2906
3394
|
});
|
|
2907
3395
|
w.on("exit", () => {
|
|
2908
|
-
if (worker
|
|
3396
|
+
if (worker !== w) return;
|
|
3397
|
+
worker = null;
|
|
2909
3398
|
failAllPending(new Error("codebase-index worker exited"));
|
|
2910
3399
|
});
|
|
2911
3400
|
worker = w;
|
|
@@ -2930,6 +3419,11 @@ function shutdownCodebaseIndexHost() {
|
|
|
2930
3419
|
function callIndexOp(op, args, opts) {
|
|
2931
3420
|
const w = ensureWorker();
|
|
2932
3421
|
if (!w) return callInline(op, args, opts);
|
|
3422
|
+
if (opts.signal?.aborted) {
|
|
3423
|
+
return Promise.reject(
|
|
3424
|
+
opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
2933
3427
|
return new Promise((resolve2, reject) => {
|
|
2934
3428
|
const id = nextRpcId++;
|
|
2935
3429
|
const timer = setTimeout(() => {
|
|
@@ -2944,8 +3438,7 @@ function callIndexOp(op, args, opts) {
|
|
|
2944
3438
|
const onAbort = () => {
|
|
2945
3439
|
w.postMessage({ type: "cancel", id });
|
|
2946
3440
|
};
|
|
2947
|
-
|
|
2948
|
-
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3441
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2949
3442
|
const cleanup = () => {
|
|
2950
3443
|
clearTimeout(timer);
|
|
2951
3444
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
@@ -3133,7 +3626,7 @@ var codebaseIndexTool = {
|
|
|
3133
3626
|
category: "Project",
|
|
3134
3627
|
icon: "index",
|
|
3135
3628
|
description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
|
|
3136
|
-
usageHint: "
|
|
3629
|
+
usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
|
|
3137
3630
|
permission: "confirm",
|
|
3138
3631
|
mutating: true,
|
|
3139
3632
|
capabilities: ["fs.write.outside-project"],
|
|
@@ -3189,12 +3682,14 @@ var codebaseSearchTool = {
|
|
|
3189
3682
|
name: "codebase-search",
|
|
3190
3683
|
category: "Project",
|
|
3191
3684
|
icon: "index",
|
|
3192
|
-
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback.
|
|
3193
|
-
usageHint: "
|
|
3685
|
+
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
|
|
3686
|
+
usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
|
|
3194
3687
|
permission: "auto",
|
|
3195
3688
|
mutating: false,
|
|
3196
3689
|
capabilities: ["fs.read"],
|
|
3197
|
-
|
|
3690
|
+
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
3691
|
+
// worker teardown and structured timeout reporting.
|
|
3692
|
+
timeoutMs: 35e3,
|
|
3198
3693
|
inputSchema: {
|
|
3199
3694
|
type: "object",
|
|
3200
3695
|
properties: {
|
|
@@ -3204,11 +3699,34 @@ var codebaseSearchTool = {
|
|
|
3204
3699
|
},
|
|
3205
3700
|
kind: {
|
|
3206
3701
|
type: "string",
|
|
3207
|
-
|
|
3702
|
+
enum: [
|
|
3703
|
+
"class",
|
|
3704
|
+
"interface",
|
|
3705
|
+
"enum",
|
|
3706
|
+
"type",
|
|
3707
|
+
"function",
|
|
3708
|
+
"method",
|
|
3709
|
+
"var",
|
|
3710
|
+
"const",
|
|
3711
|
+
"let",
|
|
3712
|
+
"property",
|
|
3713
|
+
"parameter",
|
|
3714
|
+
"namespace",
|
|
3715
|
+
"object",
|
|
3716
|
+
"literal",
|
|
3717
|
+
"schema",
|
|
3718
|
+
"struct",
|
|
3719
|
+
"trait",
|
|
3720
|
+
"impl",
|
|
3721
|
+
"static",
|
|
3722
|
+
"mod"
|
|
3723
|
+
],
|
|
3724
|
+
description: "Filter by indexed symbol kind"
|
|
3208
3725
|
},
|
|
3209
3726
|
lang: {
|
|
3210
3727
|
type: "string",
|
|
3211
|
-
|
|
3728
|
+
enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
|
|
3729
|
+
description: "Filter by indexed language"
|
|
3212
3730
|
},
|
|
3213
3731
|
lspKind: {
|
|
3214
3732
|
type: "integer",
|
|
@@ -3233,20 +3751,12 @@ var codebaseSearchTool = {
|
|
|
3233
3751
|
},
|
|
3234
3752
|
async execute(input, ctx, execOpts) {
|
|
3235
3753
|
const state = getIndexState();
|
|
3236
|
-
if (!state.ready) {
|
|
3237
|
-
return {
|
|
3238
|
-
results: [],
|
|
3239
|
-
total: 0,
|
|
3240
|
-
query: input.query,
|
|
3241
|
-
indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
|
|
3242
|
-
};
|
|
3243
|
-
}
|
|
3244
|
-
if (state.indexing) {
|
|
3754
|
+
if (state.indexing && !state.ready) {
|
|
3245
3755
|
return {
|
|
3246
3756
|
results: [],
|
|
3247
3757
|
total: 0,
|
|
3248
3758
|
query: input.query,
|
|
3249
|
-
indexStatus: `
|
|
3759
|
+
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
3250
3760
|
};
|
|
3251
3761
|
}
|
|
3252
3762
|
if (state.lastError) {
|
|
@@ -3259,21 +3769,37 @@ var codebaseSearchTool = {
|
|
|
3259
3769
|
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
3260
3770
|
};
|
|
3261
3771
|
}
|
|
3262
|
-
const limit = Math.min(input.limit ?? 20, 100);
|
|
3772
|
+
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
3263
3773
|
const { results, total } = await searchCodebaseIndex(
|
|
3264
3774
|
{
|
|
3265
3775
|
projectRoot: ctx.projectRoot,
|
|
3266
3776
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
3267
3777
|
query: input.query,
|
|
3268
|
-
kind: input.kind,
|
|
3269
|
-
lang: input.lang,
|
|
3778
|
+
kind: input.kind?.toLowerCase(),
|
|
3779
|
+
lang: input.lang?.toLowerCase(),
|
|
3270
3780
|
file: input.file,
|
|
3271
3781
|
lspKind: input.lspKind,
|
|
3272
3782
|
limit
|
|
3273
3783
|
},
|
|
3274
3784
|
{ signal: execOpts?.signal }
|
|
3275
3785
|
);
|
|
3276
|
-
|
|
3786
|
+
let hasPersistedIndex = state.ready || total > 0;
|
|
3787
|
+
if (!hasPersistedIndex) {
|
|
3788
|
+
try {
|
|
3789
|
+
const stats = await codebaseIndexStats(
|
|
3790
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
3791
|
+
{ signal: execOpts?.signal }
|
|
3792
|
+
);
|
|
3793
|
+
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return {
|
|
3798
|
+
results,
|
|
3799
|
+
total,
|
|
3800
|
+
query: input.query,
|
|
3801
|
+
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
3802
|
+
};
|
|
3277
3803
|
}
|
|
3278
3804
|
};
|
|
3279
3805
|
|
|
@@ -3282,12 +3808,15 @@ var codebaseStatsTool = {
|
|
|
3282
3808
|
name: "codebase-stats",
|
|
3283
3809
|
category: "Project",
|
|
3284
3810
|
icon: "index",
|
|
3285
|
-
description: "
|
|
3286
|
-
usageHint: "
|
|
3811
|
+
description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
|
|
3812
|
+
usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
|
|
3287
3813
|
permission: "auto",
|
|
3288
3814
|
mutating: false,
|
|
3289
3815
|
capabilities: ["fs.read"],
|
|
3290
|
-
|
|
3816
|
+
// The index host has its own 30s read watchdog. Keep the outer tool budget
|
|
3817
|
+
// longer so the host can return a structured timeout instead of being
|
|
3818
|
+
// pre-empted by the generic tool executor.
|
|
3819
|
+
timeoutMs: 35e3,
|
|
3291
3820
|
inputSchema: {
|
|
3292
3821
|
type: "object",
|
|
3293
3822
|
properties: {},
|
|
@@ -3295,7 +3824,8 @@ var codebaseStatsTool = {
|
|
|
3295
3824
|
},
|
|
3296
3825
|
async execute(_input, ctx, execOpts) {
|
|
3297
3826
|
const idxState = getIndexState();
|
|
3298
|
-
|
|
3827
|
+
const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
|
|
3828
|
+
if (idxState.indexing) {
|
|
3299
3829
|
return {
|
|
3300
3830
|
totalSymbols: 0,
|
|
3301
3831
|
totalFiles: 0,
|
|
@@ -3303,22 +3833,47 @@ var codebaseStatsTool = {
|
|
|
3303
3833
|
byKind: {},
|
|
3304
3834
|
lastIndexed: null,
|
|
3305
3835
|
sizeBytes: 0,
|
|
3306
|
-
indexPath
|
|
3836
|
+
indexPath,
|
|
3307
3837
|
version: SCHEMA_VERSION,
|
|
3308
|
-
|
|
3838
|
+
statsAvailable: false,
|
|
3839
|
+
indexing: {
|
|
3840
|
+
currentFile: idxState.currentFile,
|
|
3841
|
+
totalFiles: idxState.totalFiles
|
|
3842
|
+
},
|
|
3843
|
+
indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
|
|
3309
3844
|
};
|
|
3310
3845
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3846
|
+
let stats;
|
|
3847
|
+
try {
|
|
3848
|
+
stats = await codebaseIndexStats(
|
|
3849
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
3850
|
+
{ signal: execOpts?.signal }
|
|
3851
|
+
);
|
|
3852
|
+
} catch (err) {
|
|
3853
|
+
if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
|
|
3854
|
+
throw err;
|
|
3855
|
+
}
|
|
3316
3856
|
return {
|
|
3317
|
-
|
|
3318
|
-
|
|
3857
|
+
totalSymbols: 0,
|
|
3858
|
+
totalFiles: 0,
|
|
3859
|
+
byLang: {},
|
|
3860
|
+
byKind: {},
|
|
3861
|
+
lastIndexed: null,
|
|
3862
|
+
sizeBytes: 0,
|
|
3863
|
+
indexPath,
|
|
3864
|
+
version: SCHEMA_VERSION,
|
|
3865
|
+
statsAvailable: false,
|
|
3866
|
+
indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
|
|
3319
3867
|
};
|
|
3320
3868
|
}
|
|
3321
3869
|
const circuit = idxState.circuit;
|
|
3870
|
+
const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
3871
|
+
let indexStatus;
|
|
3872
|
+
if (circuit.state === "open") {
|
|
3873
|
+
indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
|
|
3874
|
+
} else if (!hasPersistedIndex) {
|
|
3875
|
+
indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
|
|
3876
|
+
}
|
|
3322
3877
|
return {
|
|
3323
3878
|
totalSymbols: stats.totalSymbols,
|
|
3324
3879
|
totalFiles: stats.totalFiles,
|
|
@@ -3328,9 +3883,8 @@ var codebaseStatsTool = {
|
|
|
3328
3883
|
sizeBytes: stats.sizeBytes,
|
|
3329
3884
|
indexPath: stats.indexPath,
|
|
3330
3885
|
version: stats.version,
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
} : {}
|
|
3886
|
+
statsAvailable: true,
|
|
3887
|
+
...indexStatus ? { indexStatus } : {}
|
|
3334
3888
|
};
|
|
3335
3889
|
}
|
|
3336
3890
|
};
|
|
@@ -3349,6 +3903,7 @@ export {
|
|
|
3349
3903
|
codebaseSearchTool,
|
|
3350
3904
|
codebaseStatsTool,
|
|
3351
3905
|
enqueueReindex,
|
|
3906
|
+
fileGraphService,
|
|
3352
3907
|
getIndexState,
|
|
3353
3908
|
indexCircuitBreaker,
|
|
3354
3909
|
internalKindToLspKind,
|
|
@@ -3357,12 +3912,14 @@ export {
|
|
|
3357
3912
|
isIndexing,
|
|
3358
3913
|
lspKindToInternalKind,
|
|
3359
3914
|
onIndexStateChange,
|
|
3915
|
+
packageGraphService,
|
|
3360
3916
|
resetIndexCircuitBreaker,
|
|
3361
3917
|
resolveIndexDir,
|
|
3362
3918
|
runIndexer,
|
|
3363
3919
|
runStartupIndex,
|
|
3364
3920
|
searchCodebaseIndex,
|
|
3365
3921
|
shutdownCodebaseIndexHost,
|
|
3922
|
+
symbolGraphService,
|
|
3366
3923
|
tokenise
|
|
3367
3924
|
};
|
|
3368
3925
|
//# sourceMappingURL=index.js.map
|