@wrongstack/tools 0.286.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/README.md +2 -2
- package/dist/_edit-match.d.ts.map +1 -1
- 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/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +26 -8
- package/dist/batch-tool-use.js.map +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/types.d.ts +1 -1
- package/dist/browser/types.d.ts.map +1 -1
- package/dist/builtin.d.ts +12 -10
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +2107 -864
- package/dist/builtin.js.map +4 -4
- 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/go-parser.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 +934 -283
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.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 +793 -241
- package/dist/codebase-index/worker.js.map +4 -4
- 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/edit.d.ts.map +1 -1
- package/dist/edit.js +36 -12
- package/dist/edit.js.map +3 -3
- 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 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2535 -865
- package/dist/index.js.map +4 -4
- package/dist/install.js +61 -14
- package/dist/install.js.map +3 -3
- package/dist/kanban.d.ts +16 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +795 -532
- package/dist/kanban.js.map +4 -4
- 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/next-steps.d.ts +6 -2
- package/dist/next-steps.d.ts.map +1 -1
- package/dist/next-steps.js +3 -3
- package/dist/next-steps.js.map +2 -2
- package/dist/outdated.js +61 -14
- package/dist/outdated.js.map +3 -3
- package/dist/pack.js +2104 -861
- package/dist/pack.js.map +4 -4
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +158 -4
- package/dist/plan.js.map +4 -4
- 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 +47 -0
- package/dist/session-kanban.d.ts.map +1 -0
- package/dist/session-kanban.js +626 -0
- package/dist/session-kanban.js.map +7 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +167 -4
- package/dist/task.js.map +4 -4
- package/dist/test.js +61 -14
- package/dist/test.js.map +3 -3
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +159 -7
- package/dist/todo.js.map +4 -4
- package/dist/tool-icon-map.d.ts.map +1 -1
- package/dist/tool-icons.d.ts.map +1 -1
- package/dist/tool-icons.js +7 -1
- package/dist/tool-icons.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 +11 -3
|
@@ -3,8 +3,8 @@ import { parentPort } from "node:worker_threads";
|
|
|
3
3
|
|
|
4
4
|
// src/codebase-index/indexer.ts
|
|
5
5
|
import { expectDefined as expectDefined5 } from "@wrongstack/core";
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
6
|
+
import * as fs7 from "node:fs/promises";
|
|
7
|
+
import * as path8 from "node:path";
|
|
8
8
|
import { compileGlob as compileGlob2 } from "@wrongstack/core";
|
|
9
9
|
|
|
10
10
|
// src/codebase-index/writer.ts
|
|
@@ -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;
|
|
@@ -1116,8 +1537,34 @@ function detectLang(file) {
|
|
|
1116
1537
|
// src/codebase-index/go-parser.ts
|
|
1117
1538
|
import { spawn } from "node:child_process";
|
|
1118
1539
|
import * as os from "node:os";
|
|
1540
|
+
import * as path3 from "node:path";
|
|
1541
|
+
import * as fs3 from "node:fs/promises";
|
|
1542
|
+
|
|
1543
|
+
// src/_win32-resolve.ts
|
|
1544
|
+
import * as fs2 from "node:fs";
|
|
1119
1545
|
import * as path2 from "node:path";
|
|
1120
|
-
|
|
1546
|
+
function resolveWin32Command(cmd) {
|
|
1547
|
+
if (process.platform !== "win32") return cmd;
|
|
1548
|
+
if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
|
|
1549
|
+
return cmd;
|
|
1550
|
+
}
|
|
1551
|
+
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
1552
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
|
|
1553
|
+
for (const dir of pathDirs) {
|
|
1554
|
+
const base = path2.join(dir, cmd);
|
|
1555
|
+
for (const ext of pathext) {
|
|
1556
|
+
const full = `${base}${ext}`;
|
|
1557
|
+
try {
|
|
1558
|
+
fs2.accessSync(full, fs2.constants.X_OK);
|
|
1559
|
+
return full;
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return cmd;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/codebase-index/go-parser.ts
|
|
1121
1568
|
async function parseSymbols2(opts) {
|
|
1122
1569
|
const { file, content, lang } = opts;
|
|
1123
1570
|
try {
|
|
@@ -1418,31 +1865,46 @@ func formatType(t ast.Expr) string {
|
|
|
1418
1865
|
}
|
|
1419
1866
|
`;
|
|
1420
1867
|
async function syncGoParse(filePath, content, lang) {
|
|
1421
|
-
const tmpDir = await
|
|
1868
|
+
const tmpDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "ws-go-parse-"));
|
|
1422
1869
|
try {
|
|
1423
|
-
const scriptPath =
|
|
1424
|
-
await
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1870
|
+
const scriptPath = path3.join(tmpDir, "parse.go");
|
|
1871
|
+
await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
1872
|
+
const goBinary = resolveWin32Command("go");
|
|
1873
|
+
const goResult = await new Promise(
|
|
1874
|
+
(resolve2, reject) => {
|
|
1875
|
+
let settled = false;
|
|
1876
|
+
const proc = spawn(goBinary, ["run", scriptPath], {
|
|
1877
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1878
|
+
windowsHide: true
|
|
1879
|
+
});
|
|
1880
|
+
proc.on("error", (err) => {
|
|
1881
|
+
if (settled) return;
|
|
1882
|
+
settled = true;
|
|
1883
|
+
reject(err);
|
|
1884
|
+
});
|
|
1885
|
+
let stdout2 = "";
|
|
1886
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1887
|
+
stdout2 += chunk.toString();
|
|
1888
|
+
});
|
|
1889
|
+
proc.stderr?.resume();
|
|
1890
|
+
proc.stdin?.write(content);
|
|
1891
|
+
proc.stdin?.end();
|
|
1892
|
+
const timer = setTimeout(() => {
|
|
1893
|
+
if (settled) return;
|
|
1894
|
+
settled = true;
|
|
1441
1895
|
proc.kill("SIGKILL");
|
|
1442
1896
|
reject(new Error("timeout"));
|
|
1443
|
-
}, 15e3)
|
|
1444
|
-
|
|
1445
|
-
|
|
1897
|
+
}, 15e3);
|
|
1898
|
+
timer.unref?.();
|
|
1899
|
+
proc.on("close", (code2) => {
|
|
1900
|
+
if (settled) return;
|
|
1901
|
+
settled = true;
|
|
1902
|
+
clearTimeout(timer);
|
|
1903
|
+
resolve2({ code: code2, stdout: stdout2 });
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
);
|
|
1907
|
+
const { code, stdout } = goResult;
|
|
1446
1908
|
if (code !== 0 || !stdout.trim()) {
|
|
1447
1909
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1448
1910
|
}
|
|
@@ -1464,15 +1926,15 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
1464
1926
|
} catch {
|
|
1465
1927
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1466
1928
|
} finally {
|
|
1467
|
-
await
|
|
1929
|
+
await fs3.rm(tmpDir, { recursive: true, force: true });
|
|
1468
1930
|
}
|
|
1469
1931
|
}
|
|
1470
1932
|
|
|
1471
1933
|
// src/codebase-index/py-parser.ts
|
|
1472
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
1473
|
-
import * as
|
|
1934
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
1935
|
+
import * as fs4 from "node:fs/promises";
|
|
1474
1936
|
import * as os2 from "node:os";
|
|
1475
|
-
import * as
|
|
1937
|
+
import * as path4 from "node:path";
|
|
1476
1938
|
async function parseSymbols3(opts) {
|
|
1477
1939
|
const { file, content, lang } = opts;
|
|
1478
1940
|
try {
|
|
@@ -1684,36 +2146,74 @@ visitor.visit(tree)
|
|
|
1684
2146
|
|
|
1685
2147
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
1686
2148
|
`;
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2149
|
+
function resolvePython() {
|
|
2150
|
+
const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
|
|
2151
|
+
for (const name of candidates) {
|
|
2152
|
+
const resolved = resolveWin32Command(name);
|
|
2153
|
+
const result = spawnSync(resolved, ["--version"], {
|
|
2154
|
+
stdio: "pipe",
|
|
2155
|
+
timeout: 5e3
|
|
2156
|
+
});
|
|
2157
|
+
if (result.error) continue;
|
|
2158
|
+
if (result.status !== 0) continue;
|
|
2159
|
+
return resolved;
|
|
2160
|
+
}
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
2164
|
+
return new Promise((resolve2, reject) => {
|
|
2165
|
+
let settled = false;
|
|
2166
|
+
const proc = spawn2(pyBinary, [scriptPath, filePath], {
|
|
1697
2167
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1698
2168
|
windowsHide: true
|
|
1699
2169
|
});
|
|
2170
|
+
proc.on("error", (err) => {
|
|
2171
|
+
if (settled) return;
|
|
2172
|
+
settled = true;
|
|
2173
|
+
reject(err);
|
|
2174
|
+
});
|
|
1700
2175
|
proc.stdin?.write(content);
|
|
1701
2176
|
proc.stdin?.end();
|
|
1702
2177
|
let stdout = "";
|
|
1703
2178
|
proc.stdout?.on("data", (chunk) => {
|
|
1704
2179
|
stdout += chunk.toString();
|
|
1705
2180
|
});
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
)
|
|
1716
|
-
|
|
2181
|
+
proc.stderr?.resume();
|
|
2182
|
+
const timer = setTimeout(() => {
|
|
2183
|
+
if (settled) return;
|
|
2184
|
+
settled = true;
|
|
2185
|
+
proc.kill("SIGKILL");
|
|
2186
|
+
reject(new Error("timeout"));
|
|
2187
|
+
}, 15e3);
|
|
2188
|
+
timer.unref?.();
|
|
2189
|
+
proc.on("close", (code) => {
|
|
2190
|
+
if (settled) return;
|
|
2191
|
+
settled = true;
|
|
2192
|
+
clearTimeout(timer);
|
|
2193
|
+
resolve2({ code, stdout });
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
var _cachedScriptPath = null;
|
|
2198
|
+
var _cachedPyBinary = null;
|
|
2199
|
+
async function syncPyParse(filePath, content, lang) {
|
|
2200
|
+
try {
|
|
2201
|
+
if (!_cachedScriptPath) {
|
|
2202
|
+
const tmpDir = path4.join(os2.tmpdir(), "ws-py-parse");
|
|
2203
|
+
await fs4.mkdir(tmpDir, { recursive: true });
|
|
2204
|
+
_cachedScriptPath = path4.join(tmpDir, "parse.py");
|
|
2205
|
+
await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
2206
|
+
}
|
|
2207
|
+
if (!_cachedPyBinary) {
|
|
2208
|
+
_cachedPyBinary = resolvePython();
|
|
2209
|
+
if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
2210
|
+
}
|
|
2211
|
+
const { code, stdout } = await spawnPyParser(
|
|
2212
|
+
_cachedPyBinary,
|
|
2213
|
+
_cachedScriptPath,
|
|
2214
|
+
filePath,
|
|
2215
|
+
content
|
|
2216
|
+
);
|
|
1717
2217
|
if (code !== 0 || !stdout.trim()) {
|
|
1718
2218
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1719
2219
|
}
|
|
@@ -1740,8 +2240,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1740
2240
|
// src/codebase-index/rs-parser.ts
|
|
1741
2241
|
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
1742
2242
|
import { execFileSync, spawn as spawn3 } from "node:child_process";
|
|
1743
|
-
import * as
|
|
1744
|
-
import * as
|
|
2243
|
+
import * as fs5 from "node:fs/promises";
|
|
2244
|
+
import * as path5 from "node:path";
|
|
1745
2245
|
async function parseSymbols4(opts) {
|
|
1746
2246
|
const { file, content, lang } = opts;
|
|
1747
2247
|
const nativeAvailable = checkNativeParser();
|
|
@@ -1754,7 +2254,7 @@ async function parseSymbols4(opts) {
|
|
|
1754
2254
|
function checkNativeParser() {
|
|
1755
2255
|
try {
|
|
1756
2256
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
1757
|
-
const toolsDir =
|
|
2257
|
+
const toolsDir = path5.join(process.cwd(), "tools");
|
|
1758
2258
|
try {
|
|
1759
2259
|
execFileSync(
|
|
1760
2260
|
"cargo",
|
|
@@ -1764,7 +2264,7 @@ function checkNativeParser() {
|
|
|
1764
2264
|
"--format-version",
|
|
1765
2265
|
"1",
|
|
1766
2266
|
"--manifest-path",
|
|
1767
|
-
|
|
2267
|
+
path5.join(toolsDir, "Cargo.toml")
|
|
1768
2268
|
],
|
|
1769
2269
|
{ stdio: "pipe", windowsHide: true }
|
|
1770
2270
|
);
|
|
@@ -1778,34 +2278,49 @@ function checkNativeParser() {
|
|
|
1778
2278
|
}
|
|
1779
2279
|
async function tryNativeParse(file, content) {
|
|
1780
2280
|
try {
|
|
1781
|
-
const toolsDir =
|
|
1782
|
-
const crateDir =
|
|
1783
|
-
const tmpFile =
|
|
1784
|
-
await
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2281
|
+
const toolsDir = path5.join(process.cwd(), "tools");
|
|
2282
|
+
const crateDir = path5.join(toolsDir, "syn-parser");
|
|
2283
|
+
const tmpFile = path5.join(crateDir, "src", "input.rs");
|
|
2284
|
+
await fs5.writeFile(tmpFile, content, "utf8");
|
|
2285
|
+
const cargoBinary = resolveWin32Command("cargo");
|
|
2286
|
+
const result = await new Promise(
|
|
2287
|
+
(resolve2, reject) => {
|
|
2288
|
+
let settled = false;
|
|
2289
|
+
const proc = spawn3(
|
|
2290
|
+
cargoBinary,
|
|
2291
|
+
["run", "--manifest-path", path5.join(toolsDir, "Cargo.toml")],
|
|
2292
|
+
{
|
|
2293
|
+
cwd: process.cwd(),
|
|
2294
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2295
|
+
windowsHide: true
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
proc.on("error", (err) => {
|
|
2299
|
+
if (settled) return;
|
|
2300
|
+
settled = true;
|
|
2301
|
+
reject(err);
|
|
2302
|
+
});
|
|
2303
|
+
let stdout2 = "";
|
|
2304
|
+
proc.stdout?.on("data", (chunk) => {
|
|
2305
|
+
stdout2 += chunk.toString();
|
|
2306
|
+
});
|
|
2307
|
+
proc.stderr?.resume();
|
|
2308
|
+
const timer = setTimeout(() => {
|
|
2309
|
+
if (settled) return;
|
|
2310
|
+
settled = true;
|
|
1804
2311
|
proc.kill("SIGKILL");
|
|
1805
2312
|
reject(new Error("timeout"));
|
|
1806
|
-
}, 15e3)
|
|
1807
|
-
|
|
1808
|
-
|
|
2313
|
+
}, 15e3);
|
|
2314
|
+
timer.unref?.();
|
|
2315
|
+
proc.on("close", (c) => {
|
|
2316
|
+
if (settled) return;
|
|
2317
|
+
settled = true;
|
|
2318
|
+
clearTimeout(timer);
|
|
2319
|
+
resolve2({ code: c, stdout: stdout2 });
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
);
|
|
2323
|
+
const { code, stdout } = result;
|
|
1809
2324
|
if (code === 0 && stdout.trim()) {
|
|
1810
2325
|
const symbols = JSON.parse(stdout.trim());
|
|
1811
2326
|
return {
|
|
@@ -1888,7 +2403,7 @@ function regexParse(opts) {
|
|
|
1888
2403
|
|
|
1889
2404
|
// src/codebase-index/json-parser.ts
|
|
1890
2405
|
import { expectDefined as expectDefined3 } from "@wrongstack/core";
|
|
1891
|
-
import * as
|
|
2406
|
+
import * as path6 from "node:path";
|
|
1892
2407
|
function parseSymbols5(opts) {
|
|
1893
2408
|
const { file, content, lang } = opts;
|
|
1894
2409
|
try {
|
|
@@ -1900,9 +2415,9 @@ function parseSymbols5(opts) {
|
|
|
1900
2415
|
function regexParse2(opts) {
|
|
1901
2416
|
const { file, content, lang } = opts;
|
|
1902
2417
|
const symbols = [];
|
|
1903
|
-
const
|
|
1904
|
-
const isPackageJson =
|
|
1905
|
-
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";
|
|
1906
2421
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
1907
2422
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
1908
2423
|
const lines = content.split("\n");
|
|
@@ -1926,11 +2441,11 @@ function regexParse2(opts) {
|
|
|
1926
2441
|
const line = lineFromOffset(offset);
|
|
1927
2442
|
symbols.push(
|
|
1928
2443
|
makeSymbol({
|
|
1929
|
-
name:
|
|
2444
|
+
name: path6.basename(file),
|
|
1930
2445
|
kind: "object",
|
|
1931
2446
|
line,
|
|
1932
2447
|
col: 0,
|
|
1933
|
-
signature: `"${
|
|
2448
|
+
signature: `"${path6.basename(file)}" = { ... }`,
|
|
1934
2449
|
file,
|
|
1935
2450
|
lang
|
|
1936
2451
|
})
|
|
@@ -2241,8 +2756,8 @@ function makeSymbol2(opts) {
|
|
|
2241
2756
|
}
|
|
2242
2757
|
|
|
2243
2758
|
// src/codebase-index/gitignore.ts
|
|
2244
|
-
import * as
|
|
2245
|
-
import * as
|
|
2759
|
+
import * as fs6 from "node:fs/promises";
|
|
2760
|
+
import * as path7 from "node:path";
|
|
2246
2761
|
import { compileGlob } from "@wrongstack/core";
|
|
2247
2762
|
function globBody(glob) {
|
|
2248
2763
|
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -2288,7 +2803,7 @@ function compileGitignore(lines) {
|
|
|
2288
2803
|
async function loadGitignoreMatcher(projectRoot) {
|
|
2289
2804
|
let lines = [];
|
|
2290
2805
|
try {
|
|
2291
|
-
const raw = await
|
|
2806
|
+
const raw = await fs6.readFile(path7.join(projectRoot, ".gitignore"), "utf8");
|
|
2292
2807
|
lines = raw.split("\n");
|
|
2293
2808
|
} catch {
|
|
2294
2809
|
}
|
|
@@ -2304,9 +2819,7 @@ function yieldEventLoop() {
|
|
|
2304
2819
|
function throwIfAborted(signal) {
|
|
2305
2820
|
if (!signal?.aborted) return;
|
|
2306
2821
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
2307
|
-
throw new Error(
|
|
2308
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
2309
|
-
);
|
|
2822
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
|
|
2310
2823
|
}
|
|
2311
2824
|
function isAbortError(err) {
|
|
2312
2825
|
return err instanceof DOMException && err.name === "AbortError";
|
|
@@ -2322,8 +2835,20 @@ var DEFAULT_IGNORE = [
|
|
|
2322
2835
|
"__snapshots__",
|
|
2323
2836
|
".nyc_output"
|
|
2324
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
|
+
}
|
|
2325
2848
|
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
2326
2849
|
const results = [];
|
|
2850
|
+
const errors = [];
|
|
2851
|
+
let complete = true;
|
|
2327
2852
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
2328
2853
|
const globs = [
|
|
2329
2854
|
{ ext: ".ts", pat: compileGlob2("**/*.ts") },
|
|
@@ -2346,21 +2871,23 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2346
2871
|
}
|
|
2347
2872
|
let entries;
|
|
2348
2873
|
try {
|
|
2349
|
-
entries = await
|
|
2350
|
-
} catch {
|
|
2874
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
complete = false;
|
|
2877
|
+
errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2351
2878
|
return;
|
|
2352
2879
|
}
|
|
2353
2880
|
dirCount++;
|
|
2354
2881
|
for (const e of entries) {
|
|
2355
2882
|
if (ignoreSet.has(e.name)) continue;
|
|
2356
|
-
const full =
|
|
2357
|
-
const rel =
|
|
2883
|
+
const full = path8.join(dir, e.name);
|
|
2884
|
+
const rel = path8.relative(projectRoot, full).replace(/\\/g, "/");
|
|
2358
2885
|
if (e.isDirectory()) {
|
|
2359
2886
|
if (isGitIgnored(rel, true)) continue;
|
|
2360
2887
|
await walk(full);
|
|
2361
2888
|
} else if (e.isFile()) {
|
|
2362
|
-
if (isGitIgnored(rel, false)) continue;
|
|
2363
|
-
const ext =
|
|
2889
|
+
if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
|
|
2890
|
+
const ext = path8.extname(e.name).toLowerCase();
|
|
2364
2891
|
for (const { ext: extName, pat } of globs) {
|
|
2365
2892
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
2366
2893
|
results.push(full);
|
|
@@ -2371,7 +2898,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2371
2898
|
}
|
|
2372
2899
|
};
|
|
2373
2900
|
await walk(projectRoot);
|
|
2374
|
-
return results;
|
|
2901
|
+
return { files: results, complete, errors };
|
|
2375
2902
|
}
|
|
2376
2903
|
async function parseFile(file, content, lang) {
|
|
2377
2904
|
switch (lang) {
|
|
@@ -2394,6 +2921,26 @@ async function parseFile(file, content, lang) {
|
|
|
2394
2921
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2395
2922
|
}
|
|
2396
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
|
+
}
|
|
2397
2944
|
async function runIndexer(_ctx, opts) {
|
|
2398
2945
|
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
2399
2946
|
try {
|
|
@@ -2406,7 +2953,9 @@ async function runIndexer(_ctx, opts) {
|
|
|
2406
2953
|
}
|
|
2407
2954
|
}
|
|
2408
2955
|
async function runIndexerWithStore(store, opts) {
|
|
2409
|
-
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;
|
|
2410
2959
|
const startMs = Date.now();
|
|
2411
2960
|
const errors = [];
|
|
2412
2961
|
const langStats = {};
|
|
@@ -2415,10 +2964,18 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2415
2964
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2416
2965
|
let files;
|
|
2417
2966
|
let discoveredFiles = null;
|
|
2967
|
+
let discoveryComplete = true;
|
|
2418
2968
|
if (opts.files && opts.files.length > 0) {
|
|
2419
|
-
files = opts.files.map((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
|
+
});
|
|
2420
2974
|
} else {
|
|
2421
|
-
|
|
2975
|
+
const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
2976
|
+
files = discovery.files;
|
|
2977
|
+
errors.push(...discovery.errors);
|
|
2978
|
+
discoveryComplete = discovery.complete;
|
|
2422
2979
|
discoveredFiles = new Set(files);
|
|
2423
2980
|
}
|
|
2424
2981
|
if (langs && langs.length > 0) {
|
|
@@ -2443,36 +3000,66 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2443
3000
|
}
|
|
2444
3001
|
const statOpts = signal ? { signal } : {};
|
|
2445
3002
|
const statReadParse = await Promise.allSettled(
|
|
2446
|
-
batchFiles.map(
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
if (
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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 };
|
|
2473
3061
|
}
|
|
2474
|
-
|
|
2475
|
-
})
|
|
3062
|
+
)
|
|
2476
3063
|
);
|
|
2477
3064
|
const batchEntries = [];
|
|
2478
3065
|
const deleteForFiles = [];
|
|
@@ -2487,8 +3074,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2487
3074
|
}
|
|
2488
3075
|
const result = settled.value;
|
|
2489
3076
|
if (result.error) {
|
|
2490
|
-
if (result.
|
|
2491
|
-
|
|
3077
|
+
if (result.missing) store.deleteFile(file);
|
|
3078
|
+
errors.push(`${file}: ${result.error}`);
|
|
2492
3079
|
continue;
|
|
2493
3080
|
}
|
|
2494
3081
|
const { stat: stat2, lang, parsed } = result;
|
|
@@ -2512,6 +3099,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2512
3099
|
continue;
|
|
2513
3100
|
}
|
|
2514
3101
|
if (parsed.symbols.length === 0) {
|
|
3102
|
+
store.deleteRefsForFile(file);
|
|
3103
|
+
store.deleteSymbolsForFile(file);
|
|
2515
3104
|
store.upsertFile({
|
|
2516
3105
|
file,
|
|
2517
3106
|
lang,
|
|
@@ -2522,15 +3111,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2522
3111
|
filesIndexed++;
|
|
2523
3112
|
continue;
|
|
2524
3113
|
}
|
|
2525
|
-
const refs = [];
|
|
2526
|
-
if (parsed.refs && parsed.refs.length > 0) {
|
|
2527
|
-
for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
|
|
2528
|
-
}
|
|
2529
3114
|
batchEntries.push({
|
|
2530
3115
|
file,
|
|
2531
3116
|
lang,
|
|
2532
3117
|
symbols: parsed.symbols,
|
|
2533
|
-
refs,
|
|
3118
|
+
refs: parsed.refs ?? [],
|
|
2534
3119
|
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
2535
3120
|
symbolCount: parsed.symbols.length
|
|
2536
3121
|
});
|
|
@@ -2538,34 +3123,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2538
3123
|
}
|
|
2539
3124
|
if (batchEntries.length > 0) {
|
|
2540
3125
|
try {
|
|
2541
|
-
|
|
2542
|
-
let cursor = 0;
|
|
3126
|
+
store.commitBatch(batchEntries, { deleteForFiles });
|
|
2543
3127
|
for (const entry of batchEntries) {
|
|
2544
3128
|
const count = entry.symbols.length;
|
|
2545
|
-
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
2546
|
-
cursor += count;
|
|
2547
3129
|
symbolsIndexed += count;
|
|
2548
3130
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
2549
3131
|
filesIndexed++;
|
|
2550
|
-
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2551
|
-
const refsByLine = /* @__PURE__ */ new Map();
|
|
2552
|
-
for (let i = 0; i < symbolsWithIds.length; i++) {
|
|
2553
|
-
const sym = symbolsWithIds[i];
|
|
2554
|
-
let arr = refsByLine.get(sym.line);
|
|
2555
|
-
if (!arr) {
|
|
2556
|
-
arr = [];
|
|
2557
|
-
refsByLine.set(sym.line, arr);
|
|
2558
|
-
}
|
|
2559
|
-
arr.push(i);
|
|
2560
|
-
}
|
|
2561
|
-
for (const ref of entry.refs) {
|
|
2562
|
-
const indices = refsByLine.get(ref.line);
|
|
2563
|
-
if (indices && indices.length > 0) {
|
|
2564
|
-
const idx = indices.shift();
|
|
2565
|
-
ref.fromId = symbolsWithIds[idx].id;
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
3132
|
}
|
|
2570
3133
|
} catch (err) {
|
|
2571
3134
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2579,23 +3142,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2579
3142
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
2580
3143
|
filesIndexed++;
|
|
2581
3144
|
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
2582
|
-
const
|
|
2583
|
-
for (const sym of symbolsWithIds) {
|
|
2584
|
-
let arr = refsByLine.get(sym.line);
|
|
2585
|
-
if (!arr) {
|
|
2586
|
-
arr = [];
|
|
2587
|
-
refsByLine.set(sym.line, arr);
|
|
2588
|
-
}
|
|
2589
|
-
arr.push(sym);
|
|
2590
|
-
}
|
|
2591
|
-
const fallbackBatch = [];
|
|
2592
|
-
for (const ref of entry.refs) {
|
|
2593
|
-
const syms = refsByLine.get(ref.line);
|
|
2594
|
-
if (syms && syms.length > 0) {
|
|
2595
|
-
const sym = syms.shift();
|
|
2596
|
-
fallbackBatch.push({ ...ref, fromId: sym.id });
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
3145
|
+
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
2599
3146
|
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
2600
3147
|
}
|
|
2601
3148
|
store.upsertFile({
|
|
@@ -2606,19 +3153,23 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2606
3153
|
lastIndexed: Date.now()
|
|
2607
3154
|
});
|
|
2608
3155
|
} catch (innerErr) {
|
|
2609
|
-
errors.push(
|
|
3156
|
+
errors.push(
|
|
3157
|
+
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
3158
|
+
);
|
|
2610
3159
|
}
|
|
2611
3160
|
}
|
|
2612
3161
|
}
|
|
2613
3162
|
}
|
|
2614
3163
|
}
|
|
2615
|
-
if (discoveredFiles) {
|
|
3164
|
+
if (discoveredFiles && discoveryComplete) {
|
|
2616
3165
|
for (const [file_] of existingMeta) {
|
|
2617
3166
|
if (!discoveredFiles.has(file_)) {
|
|
2618
3167
|
store.deleteFile(file_);
|
|
2619
3168
|
}
|
|
2620
3169
|
}
|
|
2621
3170
|
}
|
|
3171
|
+
store.resolveRefs();
|
|
3172
|
+
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
2622
3173
|
const durationMs = Date.now() - startMs;
|
|
2623
3174
|
store.setLastIndexed(Date.now());
|
|
2624
3175
|
return {
|
|
@@ -2721,7 +3272,8 @@ port.on("message", (msg) => {
|
|
|
2721
3272
|
type: "response",
|
|
2722
3273
|
id: msg.id,
|
|
2723
3274
|
ok: false,
|
|
2724
|
-
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
|
|
2725
3277
|
});
|
|
2726
3278
|
} catch {
|
|
2727
3279
|
}
|