@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) {
|
|
@@ -135,7 +135,7 @@ function tokenise(text) {
|
|
|
135
135
|
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
136
136
|
}
|
|
137
137
|
function splitName(name) {
|
|
138
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
138
|
+
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();
|
|
139
139
|
}
|
|
140
140
|
function buildIndexableText(name, signature, docComment) {
|
|
141
141
|
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
@@ -161,14 +161,12 @@ function buildBm25Index(docs) {
|
|
|
161
161
|
return new Bm25Index(documents, df, N, avgLen);
|
|
162
162
|
}
|
|
163
163
|
var Bm25Index = class {
|
|
164
|
-
constructor(documents,
|
|
164
|
+
constructor(documents, _df, N, avgLen) {
|
|
165
165
|
this.documents = documents;
|
|
166
|
-
this.df = df;
|
|
167
166
|
this.N = N;
|
|
168
167
|
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
169
168
|
}
|
|
170
169
|
documents;
|
|
171
|
-
df;
|
|
172
170
|
N;
|
|
173
171
|
safeAvgLen;
|
|
174
172
|
score(query, filter) {
|
|
@@ -181,10 +179,13 @@ var Bm25Index = class {
|
|
|
181
179
|
for (const qTerm of qTokens) {
|
|
182
180
|
let tf = 0;
|
|
183
181
|
for (const t of doc.tokens) {
|
|
184
|
-
if (t
|
|
182
|
+
if (t.startsWith(qTerm)) tf++;
|
|
185
183
|
}
|
|
186
184
|
if (tf === 0) continue;
|
|
187
|
-
|
|
185
|
+
let dfVal = 0;
|
|
186
|
+
for (const candidate of this.documents) {
|
|
187
|
+
if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
|
|
188
|
+
}
|
|
188
189
|
if (dfVal === 0) continue;
|
|
189
190
|
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
190
191
|
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
@@ -217,6 +218,29 @@ var Bm25Index = class {
|
|
|
217
218
|
|
|
218
219
|
// src/codebase-index/writer.ts
|
|
219
220
|
var DB_FILE = "index.db";
|
|
221
|
+
function escapeLike(value) {
|
|
222
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
223
|
+
}
|
|
224
|
+
function assignRefsToSymbols(refs, symbols) {
|
|
225
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
226
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
228
|
+
const assigned = [];
|
|
229
|
+
for (const ref of refs) {
|
|
230
|
+
let owner;
|
|
231
|
+
for (const symbol of ordered) {
|
|
232
|
+
if (symbol.line > ref.line) break;
|
|
233
|
+
owner = symbol;
|
|
234
|
+
}
|
|
235
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
236
|
+
if (!owner || owner.id <= 0) continue;
|
|
237
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
238
|
+
if (seen.has(key)) continue;
|
|
239
|
+
seen.add(key);
|
|
240
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
241
|
+
}
|
|
242
|
+
return assigned;
|
|
243
|
+
}
|
|
220
244
|
function resolveIndexDir(projectRoot, override) {
|
|
221
245
|
return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
|
|
222
246
|
}
|
|
@@ -266,7 +290,7 @@ function sleepSync(ms) {
|
|
|
266
290
|
} catch {
|
|
267
291
|
}
|
|
268
292
|
}
|
|
269
|
-
var IndexStore = class {
|
|
293
|
+
var IndexStore = class _IndexStore {
|
|
270
294
|
db;
|
|
271
295
|
/** Absolute path to this project's index directory. */
|
|
272
296
|
indexDir;
|
|
@@ -318,10 +342,7 @@ var IndexStore = class {
|
|
|
318
342
|
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
319
343
|
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
320
344
|
}
|
|
321
|
-
const delay = Math.min(
|
|
322
|
-
LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
|
|
323
|
-
LOCK_RETRY_MAX_DELAY_MS
|
|
324
|
-
);
|
|
345
|
+
const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
|
|
325
346
|
sleepSync(delay);
|
|
326
347
|
}
|
|
327
348
|
}
|
|
@@ -403,8 +424,24 @@ var IndexStore = class {
|
|
|
403
424
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
404
425
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
405
426
|
try {
|
|
406
|
-
this.db.exec(
|
|
427
|
+
this.db.exec(
|
|
428
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
|
|
429
|
+
);
|
|
407
430
|
this.ftsAvailable = true;
|
|
431
|
+
const symbolCount = Number(
|
|
432
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
|
|
433
|
+
);
|
|
434
|
+
const ftsCount = Number(
|
|
435
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
|
|
436
|
+
);
|
|
437
|
+
if (symbolCount !== ftsCount) {
|
|
438
|
+
this.db.exec("DELETE FROM symbols_fts");
|
|
439
|
+
const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
|
|
440
|
+
const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
|
|
441
|
+
for (const row of rows) {
|
|
442
|
+
insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
408
445
|
} catch {
|
|
409
446
|
this.ftsAvailable = false;
|
|
410
447
|
}
|
|
@@ -462,7 +499,9 @@ var IndexStore = class {
|
|
|
462
499
|
deleteSymbolsForFile(file) {
|
|
463
500
|
this.runWithRetry(() => {
|
|
464
501
|
if (this.ftsAvailable) {
|
|
465
|
-
this.stmt(
|
|
502
|
+
this.stmt(
|
|
503
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
504
|
+
).run(file);
|
|
466
505
|
}
|
|
467
506
|
this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
468
507
|
});
|
|
@@ -474,9 +513,21 @@ var IndexStore = class {
|
|
|
474
513
|
*/
|
|
475
514
|
deleteFile(file) {
|
|
476
515
|
this.runWithRetry(() => {
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
516
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
517
|
+
try {
|
|
518
|
+
if (this.ftsAvailable) {
|
|
519
|
+
this.db.prepare(
|
|
520
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
521
|
+
).run(file);
|
|
522
|
+
}
|
|
523
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
524
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
525
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
526
|
+
this.db.exec("COMMIT");
|
|
527
|
+
} catch (err) {
|
|
528
|
+
this.db.exec("ROLLBACK");
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
480
531
|
});
|
|
481
532
|
}
|
|
482
533
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
@@ -499,14 +550,22 @@ var IndexStore = class {
|
|
|
499
550
|
).all(file);
|
|
500
551
|
if (!rows.length) return null;
|
|
501
552
|
const r = expectDefined(rows[0]);
|
|
502
|
-
return {
|
|
553
|
+
return {
|
|
554
|
+
file: r.file,
|
|
555
|
+
lang: r.lang,
|
|
556
|
+
mtimeMs: r.mtime_ms,
|
|
557
|
+
symbolCount: r.symbol_count,
|
|
558
|
+
lastIndexed: r.last_indexed
|
|
559
|
+
};
|
|
503
560
|
}
|
|
504
561
|
getAllFileMetas() {
|
|
505
|
-
return this.db.prepare(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
562
|
+
return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
|
|
563
|
+
file: r.file,
|
|
564
|
+
lang: r.lang,
|
|
565
|
+
mtimeMs: r.mtime_ms,
|
|
566
|
+
symbolCount: r.symbol_count,
|
|
567
|
+
lastIndexed: r.last_indexed
|
|
568
|
+
}));
|
|
510
569
|
}
|
|
511
570
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
512
571
|
search(query, filter) {
|
|
@@ -530,8 +589,8 @@ var IndexStore = class {
|
|
|
530
589
|
values.push(filter.lang);
|
|
531
590
|
}
|
|
532
591
|
if (filter?.file) {
|
|
533
|
-
conditions.push("file LIKE ?");
|
|
534
|
-
values.push(`%${filter.file}%`);
|
|
592
|
+
conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
593
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
535
594
|
}
|
|
536
595
|
if (query.trim()) {
|
|
537
596
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -569,9 +628,11 @@ var IndexStore = class {
|
|
|
569
628
|
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
570
629
|
*/
|
|
571
630
|
searchRanked(query, filter, limit) {
|
|
631
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
632
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
572
633
|
const tokens = tokenise(query);
|
|
573
634
|
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
574
|
-
return this.searchRankedFallback(query, filter,
|
|
635
|
+
return this.searchRankedFallback(query, filter, safeLimit);
|
|
575
636
|
}
|
|
576
637
|
let effectiveKind = filter?.kind;
|
|
577
638
|
if (filter?.lspKind !== void 0) {
|
|
@@ -591,11 +652,13 @@ var IndexStore = class {
|
|
|
591
652
|
values.push(filter.lang);
|
|
592
653
|
}
|
|
593
654
|
if (filter?.file) {
|
|
594
|
-
conditions.push("s.file LIKE ?");
|
|
595
|
-
values.push(`%${filter.file}%`);
|
|
655
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
656
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
596
657
|
}
|
|
597
658
|
const where = conditions.join(" AND ");
|
|
598
|
-
const countRows = this.db.prepare(
|
|
659
|
+
const countRows = this.db.prepare(
|
|
660
|
+
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
661
|
+
).all(...values);
|
|
599
662
|
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
600
663
|
if (total === 0) return { results: [], total: 0 };
|
|
601
664
|
const rows = this.db.prepare(
|
|
@@ -604,9 +667,13 @@ var IndexStore = class {
|
|
|
604
667
|
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
605
668
|
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
606
669
|
WHERE ${where}
|
|
607
|
-
ORDER BY
|
|
670
|
+
ORDER BY
|
|
671
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
672
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
673
|
+
ELSE 2 END,
|
|
674
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
608
675
|
LIMIT ?`
|
|
609
|
-
).all(...values,
|
|
676
|
+
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
610
677
|
return {
|
|
611
678
|
results: rows.map((r) => ({
|
|
612
679
|
id: r.id,
|
|
@@ -636,10 +703,28 @@ var IndexStore = class {
|
|
|
636
703
|
}
|
|
637
704
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
638
705
|
const bm25 = buildBm25Index(
|
|
639
|
-
candidates.map((c) => ({
|
|
706
|
+
candidates.map((c) => ({
|
|
707
|
+
id: c.id,
|
|
708
|
+
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
709
|
+
}))
|
|
640
710
|
);
|
|
641
711
|
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
642
|
-
|
|
712
|
+
const q = query.trim().toLowerCase();
|
|
713
|
+
const rank = (id) => {
|
|
714
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
715
|
+
if (name === q) return 0;
|
|
716
|
+
if (name.startsWith(q)) return 1;
|
|
717
|
+
return 2;
|
|
718
|
+
};
|
|
719
|
+
scored.sort((a, b) => {
|
|
720
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
721
|
+
if (rankDiff !== 0) return rankDiff;
|
|
722
|
+
const scoreDiff = b.score - a.score;
|
|
723
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
724
|
+
const left = expectDefined(candidateById.get(a.id));
|
|
725
|
+
const right = expectDefined(candidateById.get(b.id));
|
|
726
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
727
|
+
});
|
|
643
728
|
const qTokens = tokenise(query);
|
|
644
729
|
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
645
730
|
const c = expectDefined(candidateById.get(id));
|
|
@@ -648,9 +733,7 @@ var IndexStore = class {
|
|
|
648
733
|
return { results, total: candidates.length };
|
|
649
734
|
}
|
|
650
735
|
getAllIndexable() {
|
|
651
|
-
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
652
|
-
({ id, text }) => ({ id, text })
|
|
653
|
-
);
|
|
736
|
+
return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
654
737
|
}
|
|
655
738
|
/**
|
|
656
739
|
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
@@ -666,22 +749,16 @@ var IndexStore = class {
|
|
|
666
749
|
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
667
750
|
getStats() {
|
|
668
751
|
const sizeBytes = this.sizeBytes();
|
|
669
|
-
const lastRows = this.db.prepare(
|
|
670
|
-
"SELECT value FROM metadata WHERE key = 'last_indexed'"
|
|
671
|
-
).all();
|
|
752
|
+
const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
|
|
672
753
|
const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
|
|
673
754
|
const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
|
|
674
755
|
const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
|
|
675
756
|
const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
|
|
676
757
|
const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
|
|
677
|
-
const langRows = this.db.prepare(
|
|
678
|
-
"SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
|
|
679
|
-
).all();
|
|
758
|
+
const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
|
|
680
759
|
const byLang = {};
|
|
681
760
|
for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
|
|
682
|
-
const kindRows = this.db.prepare(
|
|
683
|
-
"SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
|
|
684
|
-
).all();
|
|
761
|
+
const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
|
|
685
762
|
const byKind = {};
|
|
686
763
|
for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
|
|
687
764
|
return {
|
|
@@ -697,17 +774,31 @@ var IndexStore = class {
|
|
|
697
774
|
}
|
|
698
775
|
setLastIndexed(ts2) {
|
|
699
776
|
this.runWithRetry(() => {
|
|
700
|
-
this.db.prepare(
|
|
701
|
-
|
|
702
|
-
|
|
777
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
getMetadata(key) {
|
|
781
|
+
const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
|
|
782
|
+
return rows[0]?.value;
|
|
783
|
+
}
|
|
784
|
+
setMetadata(key, value) {
|
|
785
|
+
this.runWithRetry(() => {
|
|
786
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
|
|
703
787
|
});
|
|
704
788
|
}
|
|
705
789
|
clearAll() {
|
|
706
790
|
this.runWithRetry(() => {
|
|
707
|
-
this.db.exec("
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
791
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
792
|
+
try {
|
|
793
|
+
this.db.exec("DELETE FROM refs");
|
|
794
|
+
this.db.exec("DELETE FROM symbols");
|
|
795
|
+
this.db.exec("DELETE FROM files");
|
|
796
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
797
|
+
this.db.exec("COMMIT");
|
|
798
|
+
} catch (err) {
|
|
799
|
+
this.db.exec("ROLLBACK");
|
|
800
|
+
throw err;
|
|
801
|
+
}
|
|
711
802
|
});
|
|
712
803
|
}
|
|
713
804
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -798,6 +889,7 @@ var IndexStore = class {
|
|
|
798
889
|
const allInserted = [];
|
|
799
890
|
const refsToInsert = [];
|
|
800
891
|
for (const entry of entries) {
|
|
892
|
+
const insertedForEntry = [];
|
|
801
893
|
for (const s of entry.symbols) {
|
|
802
894
|
const id = nextId++;
|
|
803
895
|
symStmt.run(
|
|
@@ -815,9 +907,11 @@ var IndexStore = class {
|
|
|
815
907
|
s.file
|
|
816
908
|
);
|
|
817
909
|
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
818
|
-
|
|
910
|
+
const inserted = { ...s, id };
|
|
911
|
+
allInserted.push(inserted);
|
|
912
|
+
insertedForEntry.push(inserted);
|
|
819
913
|
}
|
|
820
|
-
|
|
914
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
821
915
|
}
|
|
822
916
|
if (refsToInsert.length > 0) {
|
|
823
917
|
const refStmt = this.db.prepare(
|
|
@@ -855,9 +949,7 @@ var IndexStore = class {
|
|
|
855
949
|
*/
|
|
856
950
|
deleteRefsForFile(file) {
|
|
857
951
|
this.runWithRetry(() => {
|
|
858
|
-
this.db.prepare(
|
|
859
|
-
"DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
860
|
-
).run(file);
|
|
952
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
|
|
861
953
|
});
|
|
862
954
|
}
|
|
863
955
|
/**
|
|
@@ -898,9 +990,7 @@ var IndexStore = class {
|
|
|
898
990
|
* Find all references FROM a given symbol (what does this symbol call/use?).
|
|
899
991
|
*/
|
|
900
992
|
findRefsFrom(symbolId) {
|
|
901
|
-
return this.db.prepare(
|
|
902
|
-
"SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
|
|
903
|
-
).all(symbolId).map((r) => ({
|
|
993
|
+
return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
|
|
904
994
|
id: r.id,
|
|
905
995
|
fromId: r.from_id,
|
|
906
996
|
toName: r.to_name,
|
|
@@ -917,6 +1007,325 @@ var IndexStore = class {
|
|
|
917
1007
|
return 0;
|
|
918
1008
|
}
|
|
919
1009
|
}
|
|
1010
|
+
// ─── CodeMap graph aggregation ──────────────────────────────────────────────
|
|
1011
|
+
/**
|
|
1012
|
+
* Derive a monorepo package name from an absolute file path.
|
|
1013
|
+
* Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
|
|
1014
|
+
* Returns `undefined` for files outside any recognisable package structure.
|
|
1015
|
+
*/
|
|
1016
|
+
static derivePackage(filePath) {
|
|
1017
|
+
const f = filePath.replace(/\\/g, "/");
|
|
1018
|
+
const pkgsIdx = f.indexOf("/packages/");
|
|
1019
|
+
if (pkgsIdx !== -1) {
|
|
1020
|
+
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
1021
|
+
const seg = rest.split("/")[0];
|
|
1022
|
+
return seg ? `@wrongstack/${seg}` : void 0;
|
|
1023
|
+
}
|
|
1024
|
+
const appsIdx = f.indexOf("/apps/");
|
|
1025
|
+
if (appsIdx !== -1) {
|
|
1026
|
+
const rest = f.slice(appsIdx + "/apps/".length);
|
|
1027
|
+
const seg = rest.split("/")[0];
|
|
1028
|
+
return seg ? `app:${seg}` : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
return void 0;
|
|
1031
|
+
}
|
|
1032
|
+
static packageFromImport(moduleName) {
|
|
1033
|
+
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
1034
|
+
const parts = moduleName.split("/");
|
|
1035
|
+
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
1036
|
+
}
|
|
1037
|
+
static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
1038
|
+
if (!moduleName.startsWith(".")) return void 0;
|
|
1039
|
+
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
1040
|
+
const absolute = path.posix.normalize(
|
|
1041
|
+
path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
|
|
1042
|
+
);
|
|
1043
|
+
const extension = path.posix.extname(absolute);
|
|
1044
|
+
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
1045
|
+
const candidates = [
|
|
1046
|
+
absolute,
|
|
1047
|
+
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
1048
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
|
|
1049
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
|
|
1050
|
+
];
|
|
1051
|
+
const indexedByPortablePath = new Map(
|
|
1052
|
+
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
1053
|
+
);
|
|
1054
|
+
for (const candidate of candidates) {
|
|
1055
|
+
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
1056
|
+
if (indexed) return indexed;
|
|
1057
|
+
}
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Package-level graph: each workspace package is a node; edges are derived
|
|
1062
|
+
* from cross-package symbol references (a symbol in package A references a
|
|
1063
|
+
* symbol resolved in package B). Node metadata includes symbol/file counts.
|
|
1064
|
+
*/
|
|
1065
|
+
getPackageGraph() {
|
|
1066
|
+
const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
|
|
1067
|
+
const pkgNodes = /* @__PURE__ */ new Map();
|
|
1068
|
+
const fileToPkg = /* @__PURE__ */ new Map();
|
|
1069
|
+
const symbolToPkg = /* @__PURE__ */ new Map();
|
|
1070
|
+
for (const s of symbols) {
|
|
1071
|
+
const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
|
|
1072
|
+
fileToPkg.set(s.file, pkg);
|
|
1073
|
+
symbolToPkg.set(s.id, pkg);
|
|
1074
|
+
const node = pkgNodes.get(pkg);
|
|
1075
|
+
if (node) {
|
|
1076
|
+
node.symbolCount = (node.symbolCount ?? 0) + 1;
|
|
1077
|
+
} else {
|
|
1078
|
+
pkgNodes.set(pkg, {
|
|
1079
|
+
id: `pkg:${pkg}`,
|
|
1080
|
+
label: pkg,
|
|
1081
|
+
kind: "package",
|
|
1082
|
+
package: pkg,
|
|
1083
|
+
symbolCount: 1,
|
|
1084
|
+
fileCount: 0
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
|
|
1089
|
+
for (const { file } of files) {
|
|
1090
|
+
const pkg = _IndexStore.derivePackage(file) ?? "(root)";
|
|
1091
|
+
const node = pkgNodes.get(pkg);
|
|
1092
|
+
if (node) node.fileCount = (node.fileCount ?? 0) + 1;
|
|
1093
|
+
}
|
|
1094
|
+
const refRows = this.db.prepare(
|
|
1095
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1096
|
+
FROM refs r
|
|
1097
|
+
WHERE r.to_id IS NOT NULL`
|
|
1098
|
+
).all();
|
|
1099
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1100
|
+
for (const r of refRows) {
|
|
1101
|
+
if (r.call_type === "import") continue;
|
|
1102
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1103
|
+
const toPkg = symbolToPkg.get(r.to_id);
|
|
1104
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
|
|
1105
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1106
|
+
let e = edgeMap.get(key);
|
|
1107
|
+
if (!e) {
|
|
1108
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1109
|
+
edgeMap.set(key, e);
|
|
1110
|
+
}
|
|
1111
|
+
e.weight++;
|
|
1112
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1113
|
+
}
|
|
1114
|
+
const importRows = this.db.prepare(
|
|
1115
|
+
`SELECT r.from_id, r.to_name
|
|
1116
|
+
FROM refs r
|
|
1117
|
+
WHERE r.call_type = 'import'`
|
|
1118
|
+
).all();
|
|
1119
|
+
for (const r of importRows) {
|
|
1120
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1121
|
+
const toPkg = _IndexStore.packageFromImport(r.to_name);
|
|
1122
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
1123
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1124
|
+
let edge = edgeMap.get(key);
|
|
1125
|
+
if (!edge) {
|
|
1126
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1127
|
+
edgeMap.set(key, edge);
|
|
1128
|
+
}
|
|
1129
|
+
edge.weight++;
|
|
1130
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1131
|
+
}
|
|
1132
|
+
const edges = [];
|
|
1133
|
+
for (const [key, e] of edgeMap) {
|
|
1134
|
+
const [source, target] = key.split("\0");
|
|
1135
|
+
let bestType = "call";
|
|
1136
|
+
let bestCount = 0;
|
|
1137
|
+
for (const [t, c] of e.types) {
|
|
1138
|
+
if (c > bestCount) {
|
|
1139
|
+
bestType = t;
|
|
1140
|
+
bestCount = c;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
edges.push({
|
|
1144
|
+
source: `pkg:${source}`,
|
|
1145
|
+
target: `pkg:${target}`,
|
|
1146
|
+
weight: e.weight,
|
|
1147
|
+
refType: bestType
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
return { nodes: [...pkgNodes.values()], edges };
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* File-level graph for a single package: each file is a node; edges are
|
|
1154
|
+
* derived from cross-file symbol references within the package.
|
|
1155
|
+
*/
|
|
1156
|
+
getFileGraph(packageFilter) {
|
|
1157
|
+
const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
|
|
1158
|
+
const pkgSyms = allSymbols.filter(
|
|
1159
|
+
(s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
|
|
1160
|
+
);
|
|
1161
|
+
if (pkgSyms.length === 0) return { nodes: [], edges: [] };
|
|
1162
|
+
const fileNodes = /* @__PURE__ */ new Map();
|
|
1163
|
+
const symToFile = /* @__PURE__ */ new Map();
|
|
1164
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
1165
|
+
for (const s of allSymbols) {
|
|
1166
|
+
symToFile.set(s.id, s.file);
|
|
1167
|
+
const current = fileStats.get(s.file);
|
|
1168
|
+
fileStats.set(s.file, {
|
|
1169
|
+
count: (current?.count ?? 0) + 1,
|
|
1170
|
+
lang: current?.lang ?? s.lang
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const localFiles = new Set(pkgSyms.map((s) => s.file));
|
|
1174
|
+
const indexedFiles = new Set(allSymbols.map((s) => s.file));
|
|
1175
|
+
const ensureFileNode = (file) => {
|
|
1176
|
+
if (fileNodes.has(file)) return;
|
|
1177
|
+
const stats = fileStats.get(file);
|
|
1178
|
+
fileNodes.set(file, {
|
|
1179
|
+
id: `file:${file}`,
|
|
1180
|
+
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
1181
|
+
kind: "file",
|
|
1182
|
+
package: _IndexStore.derivePackage(file) ?? "(root)",
|
|
1183
|
+
file,
|
|
1184
|
+
symbolCount: stats?.count ?? 0,
|
|
1185
|
+
lang: stats?.lang,
|
|
1186
|
+
external: !localFiles.has(file)
|
|
1187
|
+
});
|
|
1188
|
+
};
|
|
1189
|
+
for (const file of localFiles) {
|
|
1190
|
+
ensureFileNode(file);
|
|
1191
|
+
}
|
|
1192
|
+
const refRows = this.db.prepare(
|
|
1193
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1194
|
+
FROM refs r
|
|
1195
|
+
WHERE r.to_id IS NOT NULL`
|
|
1196
|
+
).all();
|
|
1197
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const r of refRows) {
|
|
1199
|
+
if (r.call_type === "import") continue;
|
|
1200
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1201
|
+
const toFile = symToFile.get(r.to_id);
|
|
1202
|
+
if (!fromFile || !toFile || fromFile === toFile) continue;
|
|
1203
|
+
if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
|
|
1204
|
+
ensureFileNode(fromFile);
|
|
1205
|
+
ensureFileNode(toFile);
|
|
1206
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1207
|
+
let e = edgeMap.get(key);
|
|
1208
|
+
if (!e) {
|
|
1209
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1210
|
+
edgeMap.set(key, e);
|
|
1211
|
+
}
|
|
1212
|
+
e.weight++;
|
|
1213
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1214
|
+
}
|
|
1215
|
+
const importRows = this.db.prepare(
|
|
1216
|
+
`SELECT r.from_id, r.to_name
|
|
1217
|
+
FROM refs r
|
|
1218
|
+
WHERE r.call_type = 'import'`
|
|
1219
|
+
).all();
|
|
1220
|
+
for (const r of importRows) {
|
|
1221
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1222
|
+
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
1223
|
+
const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
|
|
1224
|
+
if (!toFile || fromFile === toFile) continue;
|
|
1225
|
+
ensureFileNode(fromFile);
|
|
1226
|
+
ensureFileNode(toFile);
|
|
1227
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1228
|
+
let edge = edgeMap.get(key);
|
|
1229
|
+
if (!edge) {
|
|
1230
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1231
|
+
edgeMap.set(key, edge);
|
|
1232
|
+
}
|
|
1233
|
+
edge.weight++;
|
|
1234
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1235
|
+
}
|
|
1236
|
+
const edges = [];
|
|
1237
|
+
for (const [key, e] of edgeMap) {
|
|
1238
|
+
const [source, target] = key.split("\0");
|
|
1239
|
+
let bestType = "call";
|
|
1240
|
+
let bestCount = 0;
|
|
1241
|
+
for (const [t, c] of e.types) {
|
|
1242
|
+
if (c > bestCount) {
|
|
1243
|
+
bestType = t;
|
|
1244
|
+
bestCount = c;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
edges.push({
|
|
1248
|
+
source: `file:${source}`,
|
|
1249
|
+
target: `file:${target}`,
|
|
1250
|
+
weight: e.weight,
|
|
1251
|
+
refType: bestType
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
return { nodes: [...fileNodes.values()], edges };
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Symbol-level graph for a single file: each symbol is a node; edges are
|
|
1258
|
+
* derived from intra-file and cross-file symbol references (who calls whom).
|
|
1259
|
+
*/
|
|
1260
|
+
getSymbolGraph(fileFilter) {
|
|
1261
|
+
const allSymbols = this.db.prepare(
|
|
1262
|
+
"SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
|
|
1263
|
+
).all();
|
|
1264
|
+
const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
|
|
1265
|
+
if (syms.length === 0) return { nodes: [], edges: [] };
|
|
1266
|
+
const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
|
|
1267
|
+
const relatedIds = new Set(syms.map((symbol) => symbol.id));
|
|
1268
|
+
const toGraphNode = (s) => ({
|
|
1269
|
+
id: `sym:${s.id}`,
|
|
1270
|
+
label: s.name,
|
|
1271
|
+
kind: "symbol",
|
|
1272
|
+
symbolId: s.id,
|
|
1273
|
+
symbolKind: s.kind,
|
|
1274
|
+
file: s.file,
|
|
1275
|
+
package: _IndexStore.derivePackage(s.file) ?? "(root)",
|
|
1276
|
+
lang: s.lang,
|
|
1277
|
+
line: s.line,
|
|
1278
|
+
signature: s.signature,
|
|
1279
|
+
scope: s.scope,
|
|
1280
|
+
external: s.file !== fileFilter
|
|
1281
|
+
});
|
|
1282
|
+
const symIds = new Set(syms.map((s) => s.id));
|
|
1283
|
+
const refRows = this.db.prepare(
|
|
1284
|
+
`SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
1285
|
+
FROM refs r
|
|
1286
|
+
WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
|
|
1287
|
+
OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
|
|
1288
|
+
).all(fileFilter, fileFilter);
|
|
1289
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1290
|
+
for (const r of refRows) {
|
|
1291
|
+
if (r.to_id === null) continue;
|
|
1292
|
+
if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
|
|
1293
|
+
relatedIds.add(r.from_id);
|
|
1294
|
+
relatedIds.add(r.to_id);
|
|
1295
|
+
const key = `${r.from_id}\0${r.to_id}`;
|
|
1296
|
+
let e = edgeMap.get(key);
|
|
1297
|
+
if (!e) {
|
|
1298
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1299
|
+
edgeMap.set(key, e);
|
|
1300
|
+
}
|
|
1301
|
+
e.weight++;
|
|
1302
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1303
|
+
}
|
|
1304
|
+
const edges = [];
|
|
1305
|
+
for (const [key, e] of edgeMap) {
|
|
1306
|
+
const [fromId, toId] = key.split("\0").map(Number);
|
|
1307
|
+
let bestType = "call";
|
|
1308
|
+
let bestCount = 0;
|
|
1309
|
+
for (const [t, c] of e.types) {
|
|
1310
|
+
if (c > bestCount) {
|
|
1311
|
+
bestType = t;
|
|
1312
|
+
bestCount = c;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
edges.push({
|
|
1316
|
+
source: `sym:${fromId}`,
|
|
1317
|
+
target: `sym:${toId}`,
|
|
1318
|
+
weight: e.weight,
|
|
1319
|
+
refType: bestType
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
1323
|
+
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
1324
|
+
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
1325
|
+
return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
|
|
1326
|
+
}).map(toGraphNode);
|
|
1327
|
+
return { nodes, edges };
|
|
1328
|
+
}
|
|
920
1329
|
close() {
|
|
921
1330
|
this.stmtCache.clear();
|
|
922
1331
|
try {
|
|
@@ -1000,6 +1409,14 @@ function getJsDoc(node, sourceFile) {
|
|
|
1000
1409
|
}
|
|
1001
1410
|
return "";
|
|
1002
1411
|
}
|
|
1412
|
+
function hasFunctionLikeAncestor(node) {
|
|
1413
|
+
let current = node.parent;
|
|
1414
|
+
while (current) {
|
|
1415
|
+
if (ts.isFunctionLike(current)) return true;
|
|
1416
|
+
current = current.parent;
|
|
1417
|
+
}
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1003
1420
|
function buildScope(node) {
|
|
1004
1421
|
const parts = [];
|
|
1005
1422
|
let current = node.parent;
|
|
@@ -1027,6 +1444,10 @@ function parseSymbols(opts) {
|
|
|
1027
1444
|
function visit(node) {
|
|
1028
1445
|
const kind = kindOf(node);
|
|
1029
1446
|
if (kind) {
|
|
1447
|
+
if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
|
|
1448
|
+
ts.forEachChild(node, visit);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1030
1451
|
const nameNode = node.name;
|
|
1031
1452
|
if (!nameNode || !ts.isIdentifier(nameNode)) return;
|
|
1032
1453
|
const name = nameNode.text;
|
|
@@ -1994,9 +2415,9 @@ function parseSymbols5(opts) {
|
|
|
1994
2415
|
function regexParse2(opts) {
|
|
1995
2416
|
const { file, content, lang } = opts;
|
|
1996
2417
|
const symbols = [];
|
|
1997
|
-
const
|
|
1998
|
-
const isPackageJson =
|
|
1999
|
-
const isTsconfig =
|
|
2418
|
+
const basename3 = path6.basename(file).toLowerCase();
|
|
2419
|
+
const isPackageJson = basename3 === "package.json";
|
|
2420
|
+
const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
|
|
2000
2421
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
2001
2422
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
2002
2423
|
const lines = content.split("\n");
|
|
@@ -2398,9 +2819,7 @@ function yieldEventLoop() {
|
|
|
2398
2819
|
function throwIfAborted(signal) {
|
|
2399
2820
|
if (!signal?.aborted) return;
|
|
2400
2821
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
2401
|
-
throw new Error(
|
|
2402
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
2403
|
-
);
|
|
2822
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
|
|
2404
2823
|
}
|
|
2405
2824
|
function isAbortError(err) {
|
|
2406
2825
|
return err instanceof DOMException && err.name === "AbortError";
|
|
@@ -2416,8 +2835,20 @@ var DEFAULT_IGNORE = [
|
|
|
2416
2835
|
"__snapshots__",
|
|
2417
2836
|
".nyc_output"
|
|
2418
2837
|
];
|
|
2838
|
+
var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
|
|
2839
|
+
var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
|
|
2840
|
+
function isWithinProject(projectRoot, file) {
|
|
2841
|
+
const rel = path8.relative(projectRoot, file);
|
|
2842
|
+
return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
|
|
2843
|
+
}
|
|
2844
|
+
function isMissingPathError(err) {
|
|
2845
|
+
const code = err?.code;
|
|
2846
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2847
|
+
}
|
|
2419
2848
|
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
2420
2849
|
const results = [];
|
|
2850
|
+
const errors = [];
|
|
2851
|
+
let complete = true;
|
|
2421
2852
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
2422
2853
|
const globs = [
|
|
2423
2854
|
{ ext: ".ts", pat: compileGlob2("**/*.ts") },
|
|
@@ -2441,7 +2872,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2441
2872
|
let entries;
|
|
2442
2873
|
try {
|
|
2443
2874
|
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2444
|
-
} catch {
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
complete = false;
|
|
2877
|
+
errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2445
2878
|
return;
|
|
2446
2879
|
}
|
|
2447
2880
|
dirCount++;
|
|
@@ -2453,8 +2886,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2453
2886
|
if (isGitIgnored(rel, true)) continue;
|
|
2454
2887
|
await walk(full);
|
|
2455
2888
|
} else if (e.isFile()) {
|
|
2456
|
-
if (isGitIgnored(rel, false)) continue;
|
|
2457
|
-
const ext = path8.extname(e.name);
|
|
2889
|
+
if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
|
|
2890
|
+
const ext = path8.extname(e.name).toLowerCase();
|
|
2458
2891
|
for (const { ext: extName, pat } of globs) {
|
|
2459
2892
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
2460
2893
|
results.push(full);
|
|
@@ -2465,7 +2898,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2465
2898
|
}
|
|
2466
2899
|
};
|
|
2467
2900
|
await walk(projectRoot);
|
|
2468
|
-
return results;
|
|
2901
|
+
return { files: results, complete, errors };
|
|
2469
2902
|
}
|
|
2470
2903
|
async function parseFile(file, content, lang) {
|
|
2471
2904
|
switch (lang) {
|
|
@@ -2488,6 +2921,26 @@ async function parseFile(file, content, lang) {
|
|
|
2488
2921
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2489
2922
|
}
|
|
2490
2923
|
}
|
|
2924
|
+
function assignRefsToSymbols2(refs, symbols) {
|
|
2925
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
2926
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
2927
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2928
|
+
const assigned = [];
|
|
2929
|
+
for (const ref of refs) {
|
|
2930
|
+
let owner;
|
|
2931
|
+
for (const symbol of ordered) {
|
|
2932
|
+
if (symbol.line > ref.line) break;
|
|
2933
|
+
owner = symbol;
|
|
2934
|
+
}
|
|
2935
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
2936
|
+
if (!owner || owner.id <= 0) continue;
|
|
2937
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
2938
|
+
if (seen.has(key)) continue;
|
|
2939
|
+
seen.add(key);
|
|
2940
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
2941
|
+
}
|
|
2942
|
+
return assigned;
|
|
2943
|
+
}
|
|
2491
2944
|
async function runIndexer(_ctx, opts) {
|
|
2492
2945
|
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
2493
2946
|
try {
|
|
@@ -2500,7 +2953,9 @@ async function runIndexer(_ctx, opts) {
|
|
|
2500
2953
|
}
|
|
2501
2954
|
}
|
|
2502
2955
|
async function runIndexerWithStore(store, opts) {
|
|
2503
|
-
const { projectRoot,
|
|
2956
|
+
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
2957
|
+
const relationGraphVersion = "2";
|
|
2958
|
+
const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
|
|
2504
2959
|
const startMs = Date.now();
|
|
2505
2960
|
const errors = [];
|
|
2506
2961
|
const langStats = {};
|
|
@@ -2509,10 +2964,18 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2509
2964
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2510
2965
|
let files;
|
|
2511
2966
|
let discoveredFiles = null;
|
|
2967
|
+
let discoveryComplete = true;
|
|
2512
2968
|
if (opts.files && opts.files.length > 0) {
|
|
2513
|
-
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) =>
|
|
2969
|
+
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
|
|
2970
|
+
if (!isWithinProject(projectRoot, f)) return false;
|
|
2971
|
+
const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
|
|
2972
|
+
return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
|
|
2973
|
+
});
|
|
2514
2974
|
} else {
|
|
2515
|
-
|
|
2975
|
+
const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
2976
|
+
files = discovery.files;
|
|
2977
|
+
errors.push(...discovery.errors);
|
|
2978
|
+
discoveryComplete = discovery.complete;
|
|
2516
2979
|
discoveredFiles = new Set(files);
|
|
2517
2980
|
}
|
|
2518
2981
|
if (langs && langs.length > 0) {
|
|
@@ -2537,36 +3000,66 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2537
3000
|
}
|
|
2538
3001
|
const statOpts = signal ? { signal } : {};
|
|
2539
3002
|
const statReadParse = await Promise.allSettled(
|
|
2540
|
-
batchFiles.map(
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
if (
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
3003
|
+
batchFiles.map(
|
|
3004
|
+
async (file) => {
|
|
3005
|
+
let stat2;
|
|
3006
|
+
try {
|
|
3007
|
+
stat2 = await fs7.stat(file, statOpts);
|
|
3008
|
+
} catch (e) {
|
|
3009
|
+
if (isAbortError(e)) throw e;
|
|
3010
|
+
return {
|
|
3011
|
+
file,
|
|
3012
|
+
stat: null,
|
|
3013
|
+
lang: "",
|
|
3014
|
+
parsed: null,
|
|
3015
|
+
error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
|
|
3016
|
+
missing: isMissingPathError(e)
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
|
|
3020
|
+
const lang = detectLang(file);
|
|
3021
|
+
if (!lang) return { file, stat: stat2, lang: "", parsed: null };
|
|
3022
|
+
if (stat2.size > MAX_INDEX_FILE_BYTES) {
|
|
3023
|
+
return {
|
|
3024
|
+
file,
|
|
3025
|
+
stat: stat2,
|
|
3026
|
+
lang,
|
|
3027
|
+
parsed: null,
|
|
3028
|
+
error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
const meta = existingMeta.get(file);
|
|
3032
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
|
|
3033
|
+
return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
|
|
3034
|
+
}
|
|
3035
|
+
let content;
|
|
3036
|
+
try {
|
|
3037
|
+
content = await fs7.readFile(file, { encoding: "utf8", signal });
|
|
3038
|
+
} catch (e) {
|
|
3039
|
+
if (isAbortError(e)) throw e;
|
|
3040
|
+
return {
|
|
3041
|
+
file,
|
|
3042
|
+
stat: stat2,
|
|
3043
|
+
lang,
|
|
3044
|
+
parsed: null,
|
|
3045
|
+
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
let parsed;
|
|
3049
|
+
try {
|
|
3050
|
+
parsed = await parseFile(file, content, lang);
|
|
3051
|
+
} catch (e) {
|
|
3052
|
+
return {
|
|
3053
|
+
file,
|
|
3054
|
+
stat: stat2,
|
|
3055
|
+
lang,
|
|
3056
|
+
parsed: null,
|
|
3057
|
+
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
return { file, stat: stat2, lang, parsed, content };
|
|
2567
3061
|
}
|
|
2568
|
-
|
|
2569
|
-
})
|
|
3062
|
+
)
|
|
2570
3063
|
);
|
|
2571
3064
|
const batchEntries = [];
|
|
2572
3065
|
const deleteForFiles = [];
|
|
@@ -2581,8 +3074,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2581
3074
|
}
|
|
2582
3075
|
const result = settled.value;
|
|
2583
3076
|
if (result.error) {
|
|
2584
|
-
if (result.
|
|
2585
|
-
|
|
3077
|
+
if (result.missing) store.deleteFile(file);
|
|
3078
|
+
errors.push(`${file}: ${result.error}`);
|
|
2586
3079
|
continue;
|
|
2587
3080
|
}
|
|
2588
3081
|
const { stat: stat2, lang, parsed } = result;
|
|
@@ -2606,6 +3099,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2606
3099
|
continue;
|
|
2607
3100
|
}
|
|
2608
3101
|
if (parsed.symbols.length === 0) {
|
|
3102
|
+
store.deleteRefsForFile(file);
|
|
3103
|
+
store.deleteSymbolsForFile(file);
|
|
2609
3104
|
store.upsertFile({
|
|
2610
3105
|
file,
|
|
2611
3106
|
lang,
|
|
@@ -2616,15 +3111,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2616
3111
|
filesIndexed++;
|
|
2617
3112
|
continue;
|
|
2618
3113
|
}
|
|
2619
|
-
const refs = [];
|
|
2620
|
-
if (parsed.refs && parsed.refs.length > 0) {
|
|
2621
|
-
for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
|
|
2622
|
-
}
|
|
2623
3114
|
batchEntries.push({
|
|
2624
3115
|
file,
|
|
2625
3116
|
lang,
|
|
2626
3117
|
symbols: parsed.symbols,
|
|
2627
|
-
refs,
|
|
3118
|
+
refs: parsed.refs ?? [],
|
|
2628
3119
|
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
2629
3120
|
symbolCount: parsed.symbols.length
|
|
2630
3121
|
});
|
|
@@ -2632,34 +3123,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2632
3123
|
}
|
|
2633
3124
|
if (batchEntries.length > 0) {
|
|
2634
3125
|
try {
|
|
2635
|
-
|
|
2636
|
-
let cursor = 0;
|
|
3126
|
+
store.commitBatch(batchEntries, { deleteForFiles });
|
|
2637
3127
|
for (const entry of batchEntries) {
|
|
2638
3128
|
const count = entry.symbols.length;
|
|
2639
|
-
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
2640
|
-
cursor += count;
|
|
2641
3129
|
symbolsIndexed += count;
|
|
2642
3130
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
2643
3131
|
filesIndexed++;
|
|
2644
|
-
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2645
|
-
const refsByLine = /* @__PURE__ */ new Map();
|
|
2646
|
-
for (let i = 0; i < symbolsWithIds.length; i++) {
|
|
2647
|
-
const sym = symbolsWithIds[i];
|
|
2648
|
-
let arr = refsByLine.get(sym.line);
|
|
2649
|
-
if (!arr) {
|
|
2650
|
-
arr = [];
|
|
2651
|
-
refsByLine.set(sym.line, arr);
|
|
2652
|
-
}
|
|
2653
|
-
arr.push(i);
|
|
2654
|
-
}
|
|
2655
|
-
for (const ref of entry.refs) {
|
|
2656
|
-
const indices = refsByLine.get(ref.line);
|
|
2657
|
-
if (indices && indices.length > 0) {
|
|
2658
|
-
const idx = indices.shift();
|
|
2659
|
-
ref.fromId = symbolsWithIds[idx].id;
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
3132
|
}
|
|
2664
3133
|
} catch (err) {
|
|
2665
3134
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2673,23 +3142,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2673
3142
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
2674
3143
|
filesIndexed++;
|
|
2675
3144
|
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2676
|
-
const
|
|
2677
|
-
for (const sym of symbolsWithIds) {
|
|
2678
|
-
let arr = refsByLine.get(sym.line);
|
|
2679
|
-
if (!arr) {
|
|
2680
|
-
arr = [];
|
|
2681
|
-
refsByLine.set(sym.line, arr);
|
|
2682
|
-
}
|
|
2683
|
-
arr.push(sym);
|
|
2684
|
-
}
|
|
2685
|
-
const fallbackBatch = [];
|
|
2686
|
-
for (const ref of entry.refs) {
|
|
2687
|
-
const syms = refsByLine.get(ref.line);
|
|
2688
|
-
if (syms && syms.length > 0) {
|
|
2689
|
-
const sym = syms.shift();
|
|
2690
|
-
fallbackBatch.push({ ...ref, fromId: sym.id });
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
3145
|
+
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
2693
3146
|
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
2694
3147
|
}
|
|
2695
3148
|
store.upsertFile({
|
|
@@ -2700,19 +3153,23 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2700
3153
|
lastIndexed: Date.now()
|
|
2701
3154
|
});
|
|
2702
3155
|
} catch (innerErr) {
|
|
2703
|
-
errors.push(
|
|
3156
|
+
errors.push(
|
|
3157
|
+
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
3158
|
+
);
|
|
2704
3159
|
}
|
|
2705
3160
|
}
|
|
2706
3161
|
}
|
|
2707
3162
|
}
|
|
2708
3163
|
}
|
|
2709
|
-
if (discoveredFiles) {
|
|
3164
|
+
if (discoveredFiles && discoveryComplete) {
|
|
2710
3165
|
for (const [file_] of existingMeta) {
|
|
2711
3166
|
if (!discoveredFiles.has(file_)) {
|
|
2712
3167
|
store.deleteFile(file_);
|
|
2713
3168
|
}
|
|
2714
3169
|
}
|
|
2715
3170
|
}
|
|
3171
|
+
store.resolveRefs();
|
|
3172
|
+
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
2716
3173
|
const durationMs = Date.now() - startMs;
|
|
2717
3174
|
store.setLastIndexed(Date.now());
|
|
2718
3175
|
return {
|
|
@@ -2815,7 +3272,8 @@ port.on("message", (msg) => {
|
|
|
2815
3272
|
type: "response",
|
|
2816
3273
|
id: msg.id,
|
|
2817
3274
|
ok: false,
|
|
2818
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3275
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3276
|
+
errorName: err instanceof Error ? err.name : void 0
|
|
2819
3277
|
});
|
|
2820
3278
|
} catch {
|
|
2821
3279
|
}
|