@wrongstack/tools 0.287.0 → 0.291.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/auto-proceed-loop-guard.d.ts +128 -0
- package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
- package/dist/auto-proceed-loop-guard.js +46 -0
- package/dist/auto-proceed-loop-guard.js.map +7 -0
- 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 +1766 -864
- package/dist/builtin.js.map +3 -3
- package/dist/codebase-index/background-indexer.d.ts +1 -1
- 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 +888 -254
- package/dist/codebase-index/index.js.map +3 -3
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/refs-extractor.d.ts +2 -17
- package/dist/codebase-index/refs-extractor.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 +738 -212
- package/dist/codebase-index/worker.js.map +3 -3
- package/dist/codebase-index/writer.d.ts +60 -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 +2001 -873
- 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 +21 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +713 -572
- 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 +1763 -861
- 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/read.d.ts.map +1 -1
- package/dist/read.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 +197 -3
- 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 +13 -5
|
@@ -5,6 +5,7 @@ import { parentPort } from "node:worker_threads";
|
|
|
5
5
|
import { expectDefined as expectDefined5 } from "@wrongstack/core";
|
|
6
6
|
import * as fs7 from "node:fs/promises";
|
|
7
7
|
import * as path8 from "node:path";
|
|
8
|
+
import { availableParallelism } from "node:os";
|
|
8
9
|
import { compileGlob as compileGlob2 } from "@wrongstack/core";
|
|
9
10
|
|
|
10
11
|
// src/codebase-index/writer.ts
|
|
@@ -92,7 +93,7 @@ import * as path from "node:path";
|
|
|
92
93
|
import { resolveWstackPaths } from "@wrongstack/core";
|
|
93
94
|
|
|
94
95
|
// src/codebase-index/schema.ts
|
|
95
|
-
var SCHEMA_VERSION =
|
|
96
|
+
var SCHEMA_VERSION = 3;
|
|
96
97
|
|
|
97
98
|
// src/codebase-index/lsp-kind.ts
|
|
98
99
|
function lspKindToInternalKind(k) {
|
|
@@ -135,7 +136,7 @@ function tokenise(text) {
|
|
|
135
136
|
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
136
137
|
}
|
|
137
138
|
function splitName(name) {
|
|
138
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
139
|
+
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
140
|
}
|
|
140
141
|
function buildIndexableText(name, signature, docComment) {
|
|
141
142
|
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
@@ -161,14 +162,12 @@ function buildBm25Index(docs) {
|
|
|
161
162
|
return new Bm25Index(documents, df, N, avgLen);
|
|
162
163
|
}
|
|
163
164
|
var Bm25Index = class {
|
|
164
|
-
constructor(documents,
|
|
165
|
+
constructor(documents, _df, N, avgLen) {
|
|
165
166
|
this.documents = documents;
|
|
166
|
-
this.df = df;
|
|
167
167
|
this.N = N;
|
|
168
168
|
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
169
169
|
}
|
|
170
170
|
documents;
|
|
171
|
-
df;
|
|
172
171
|
N;
|
|
173
172
|
safeAvgLen;
|
|
174
173
|
score(query, filter) {
|
|
@@ -181,10 +180,13 @@ var Bm25Index = class {
|
|
|
181
180
|
for (const qTerm of qTokens) {
|
|
182
181
|
let tf = 0;
|
|
183
182
|
for (const t of doc.tokens) {
|
|
184
|
-
if (t
|
|
183
|
+
if (t.startsWith(qTerm)) tf++;
|
|
185
184
|
}
|
|
186
185
|
if (tf === 0) continue;
|
|
187
|
-
|
|
186
|
+
let dfVal = 0;
|
|
187
|
+
for (const candidate of this.documents) {
|
|
188
|
+
if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
|
|
189
|
+
}
|
|
188
190
|
if (dfVal === 0) continue;
|
|
189
191
|
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
190
192
|
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
@@ -217,9 +219,79 @@ var Bm25Index = class {
|
|
|
217
219
|
|
|
218
220
|
// src/codebase-index/writer.ts
|
|
219
221
|
var DB_FILE = "index.db";
|
|
222
|
+
function escapeLike(value) {
|
|
223
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
224
|
+
}
|
|
225
|
+
function assignRefsToSymbols(refs, symbols) {
|
|
226
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
227
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
228
|
+
const seen = /* @__PURE__ */ new Set();
|
|
229
|
+
const assigned = [];
|
|
230
|
+
for (const ref of refs) {
|
|
231
|
+
let owner;
|
|
232
|
+
for (const symbol of ordered) {
|
|
233
|
+
if (symbol.line > ref.line) break;
|
|
234
|
+
owner = symbol;
|
|
235
|
+
}
|
|
236
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
237
|
+
if (!owner || owner.id <= 0) continue;
|
|
238
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
239
|
+
if (seen.has(key)) continue;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
242
|
+
}
|
|
243
|
+
return assigned;
|
|
244
|
+
}
|
|
220
245
|
function resolveIndexDir(projectRoot, override) {
|
|
221
246
|
return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
|
|
222
247
|
}
|
|
248
|
+
var StorePool = class {
|
|
249
|
+
stores = /* @__PURE__ */ new Map();
|
|
250
|
+
key(projectRoot, indexDir) {
|
|
251
|
+
return `${projectRoot}\0${indexDir ?? ""}`;
|
|
252
|
+
}
|
|
253
|
+
/** Borrow a store. Creates it on first access for this key. */
|
|
254
|
+
acquire(projectRoot, opts) {
|
|
255
|
+
const k = this.key(projectRoot, opts?.indexDir);
|
|
256
|
+
let store = this.stores.get(k);
|
|
257
|
+
if (!store) {
|
|
258
|
+
store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
|
|
259
|
+
this.stores.set(k, store);
|
|
260
|
+
}
|
|
261
|
+
return store;
|
|
262
|
+
}
|
|
263
|
+
/** Return the store to the pool. The connection stays warm for subsequent
|
|
264
|
+
* operations on the same (projectRoot, indexDir). */
|
|
265
|
+
release(_store) {
|
|
266
|
+
}
|
|
267
|
+
/** Close every pooled connection and drain the pool. Call on shutdown. */
|
|
268
|
+
closeAll() {
|
|
269
|
+
for (const store of this.stores.values()) {
|
|
270
|
+
try {
|
|
271
|
+
store.close();
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.stores.clear();
|
|
276
|
+
}
|
|
277
|
+
/** Remove one store from the pool. Used by tests that need isolation. */
|
|
278
|
+
evict(projectRoot, indexDir) {
|
|
279
|
+
const k = this.key(projectRoot, indexDir);
|
|
280
|
+
const store = this.stores.get(k);
|
|
281
|
+
if (store) {
|
|
282
|
+
try {
|
|
283
|
+
store.close();
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
this.stores.delete(k);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** True when the pool holds a connection for the given key. */
|
|
290
|
+
has(projectRoot, indexDir) {
|
|
291
|
+
return this.stores.has(this.key(projectRoot, indexDir));
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
var indexStorePool = new StorePool();
|
|
223
295
|
var warningSilenced = false;
|
|
224
296
|
function silenceSqliteExperimentalWarning() {
|
|
225
297
|
if (warningSilenced) return;
|
|
@@ -266,7 +338,7 @@ function sleepSync(ms) {
|
|
|
266
338
|
} catch {
|
|
267
339
|
}
|
|
268
340
|
}
|
|
269
|
-
var IndexStore = class {
|
|
341
|
+
var IndexStore = class _IndexStore {
|
|
270
342
|
db;
|
|
271
343
|
/** Absolute path to this project's index directory. */
|
|
272
344
|
indexDir;
|
|
@@ -318,10 +390,7 @@ var IndexStore = class {
|
|
|
318
390
|
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
319
391
|
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
320
392
|
}
|
|
321
|
-
const delay = Math.min(
|
|
322
|
-
LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
|
|
323
|
-
LOCK_RETRY_MAX_DELAY_MS
|
|
324
|
-
);
|
|
393
|
+
const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
|
|
325
394
|
sleepSync(delay);
|
|
326
395
|
}
|
|
327
396
|
}
|
|
@@ -403,8 +472,24 @@ var IndexStore = class {
|
|
|
403
472
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
404
473
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
405
474
|
try {
|
|
406
|
-
this.db.exec(
|
|
475
|
+
this.db.exec(
|
|
476
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
|
|
477
|
+
);
|
|
407
478
|
this.ftsAvailable = true;
|
|
479
|
+
const symbolCount = Number(
|
|
480
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
|
|
481
|
+
);
|
|
482
|
+
const ftsCount = Number(
|
|
483
|
+
this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
|
|
484
|
+
);
|
|
485
|
+
if (symbolCount !== ftsCount) {
|
|
486
|
+
this.db.exec("DELETE FROM symbols_fts");
|
|
487
|
+
const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
|
|
488
|
+
const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
|
|
489
|
+
for (const row of rows) {
|
|
490
|
+
insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
408
493
|
} catch {
|
|
409
494
|
this.ftsAvailable = false;
|
|
410
495
|
}
|
|
@@ -424,7 +509,7 @@ var IndexStore = class {
|
|
|
424
509
|
return this.runWithRetry(() => {
|
|
425
510
|
this.db.exec("BEGIN IMMEDIATE");
|
|
426
511
|
try {
|
|
427
|
-
const maxRows = this.
|
|
512
|
+
const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
428
513
|
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
429
514
|
const stmt = this.db.prepare(
|
|
430
515
|
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
@@ -462,7 +547,9 @@ var IndexStore = class {
|
|
|
462
547
|
deleteSymbolsForFile(file) {
|
|
463
548
|
this.runWithRetry(() => {
|
|
464
549
|
if (this.ftsAvailable) {
|
|
465
|
-
this.stmt(
|
|
550
|
+
this.stmt(
|
|
551
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
552
|
+
).run(file);
|
|
466
553
|
}
|
|
467
554
|
this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
468
555
|
});
|
|
@@ -474,9 +561,21 @@ var IndexStore = class {
|
|
|
474
561
|
*/
|
|
475
562
|
deleteFile(file) {
|
|
476
563
|
this.runWithRetry(() => {
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
564
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
565
|
+
try {
|
|
566
|
+
if (this.ftsAvailable) {
|
|
567
|
+
this.db.prepare(
|
|
568
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
|
|
569
|
+
).run(file);
|
|
570
|
+
}
|
|
571
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
572
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
573
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
574
|
+
this.db.exec("COMMIT");
|
|
575
|
+
} catch (err) {
|
|
576
|
+
this.db.exec("ROLLBACK");
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
480
579
|
});
|
|
481
580
|
}
|
|
482
581
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
@@ -499,14 +598,22 @@ var IndexStore = class {
|
|
|
499
598
|
).all(file);
|
|
500
599
|
if (!rows.length) return null;
|
|
501
600
|
const r = expectDefined(rows[0]);
|
|
502
|
-
return {
|
|
601
|
+
return {
|
|
602
|
+
file: r.file,
|
|
603
|
+
lang: r.lang,
|
|
604
|
+
mtimeMs: r.mtime_ms,
|
|
605
|
+
symbolCount: r.symbol_count,
|
|
606
|
+
lastIndexed: r.last_indexed
|
|
607
|
+
};
|
|
503
608
|
}
|
|
504
609
|
getAllFileMetas() {
|
|
505
|
-
return this.db.prepare(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
610
|
+
return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
|
|
611
|
+
file: r.file,
|
|
612
|
+
lang: r.lang,
|
|
613
|
+
mtimeMs: r.mtime_ms,
|
|
614
|
+
symbolCount: r.symbol_count,
|
|
615
|
+
lastIndexed: r.last_indexed
|
|
616
|
+
}));
|
|
510
617
|
}
|
|
511
618
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
512
619
|
search(query, filter) {
|
|
@@ -530,8 +637,8 @@ var IndexStore = class {
|
|
|
530
637
|
values.push(filter.lang);
|
|
531
638
|
}
|
|
532
639
|
if (filter?.file) {
|
|
533
|
-
conditions.push("file LIKE ?");
|
|
534
|
-
values.push(`%${filter.file}%`);
|
|
640
|
+
conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
641
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
535
642
|
}
|
|
536
643
|
if (query.trim()) {
|
|
537
644
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -569,9 +676,11 @@ var IndexStore = class {
|
|
|
569
676
|
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
570
677
|
*/
|
|
571
678
|
searchRanked(query, filter, limit) {
|
|
679
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
680
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
572
681
|
const tokens = tokenise(query);
|
|
573
682
|
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
574
|
-
return this.searchRankedFallback(query, filter,
|
|
683
|
+
return this.searchRankedFallback(query, filter, safeLimit);
|
|
575
684
|
}
|
|
576
685
|
let effectiveKind = filter?.kind;
|
|
577
686
|
if (filter?.lspKind !== void 0) {
|
|
@@ -591,11 +700,13 @@ var IndexStore = class {
|
|
|
591
700
|
values.push(filter.lang);
|
|
592
701
|
}
|
|
593
702
|
if (filter?.file) {
|
|
594
|
-
conditions.push("s.file LIKE ?");
|
|
595
|
-
values.push(`%${filter.file}%`);
|
|
703
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
704
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
596
705
|
}
|
|
597
706
|
const where = conditions.join(" AND ");
|
|
598
|
-
const countRows = this.db.prepare(
|
|
707
|
+
const countRows = this.db.prepare(
|
|
708
|
+
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
709
|
+
).all(...values);
|
|
599
710
|
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
600
711
|
if (total === 0) return { results: [], total: 0 };
|
|
601
712
|
const rows = this.db.prepare(
|
|
@@ -604,9 +715,13 @@ var IndexStore = class {
|
|
|
604
715
|
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
605
716
|
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
606
717
|
WHERE ${where}
|
|
607
|
-
ORDER BY
|
|
718
|
+
ORDER BY
|
|
719
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
720
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
721
|
+
ELSE 2 END,
|
|
722
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
608
723
|
LIMIT ?`
|
|
609
|
-
).all(...values,
|
|
724
|
+
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
610
725
|
return {
|
|
611
726
|
results: rows.map((r) => ({
|
|
612
727
|
id: r.id,
|
|
@@ -636,10 +751,28 @@ var IndexStore = class {
|
|
|
636
751
|
}
|
|
637
752
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
638
753
|
const bm25 = buildBm25Index(
|
|
639
|
-
candidates.map((c) => ({
|
|
754
|
+
candidates.map((c) => ({
|
|
755
|
+
id: c.id,
|
|
756
|
+
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
757
|
+
}))
|
|
640
758
|
);
|
|
641
759
|
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
642
|
-
|
|
760
|
+
const q = query.trim().toLowerCase();
|
|
761
|
+
const rank = (id) => {
|
|
762
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
763
|
+
if (name === q) return 0;
|
|
764
|
+
if (name.startsWith(q)) return 1;
|
|
765
|
+
return 2;
|
|
766
|
+
};
|
|
767
|
+
scored.sort((a, b) => {
|
|
768
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
769
|
+
if (rankDiff !== 0) return rankDiff;
|
|
770
|
+
const scoreDiff = b.score - a.score;
|
|
771
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
772
|
+
const left = expectDefined(candidateById.get(a.id));
|
|
773
|
+
const right = expectDefined(candidateById.get(b.id));
|
|
774
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
775
|
+
});
|
|
643
776
|
const qTokens = tokenise(query);
|
|
644
777
|
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
645
778
|
const c = expectDefined(candidateById.get(id));
|
|
@@ -648,9 +781,7 @@ var IndexStore = class {
|
|
|
648
781
|
return { results, total: candidates.length };
|
|
649
782
|
}
|
|
650
783
|
getAllIndexable() {
|
|
651
|
-
return this.
|
|
652
|
-
({ id, text }) => ({ id, text })
|
|
653
|
-
);
|
|
784
|
+
return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
|
|
654
785
|
}
|
|
655
786
|
/**
|
|
656
787
|
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
@@ -660,28 +791,22 @@ var IndexStore = class {
|
|
|
660
791
|
* `symbols.id`). Ids may have gaps — that is fine.
|
|
661
792
|
*/
|
|
662
793
|
getMaxSymbolId() {
|
|
663
|
-
const rows = this.
|
|
794
|
+
const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
664
795
|
return rows[0]?.m ?? 0;
|
|
665
796
|
}
|
|
666
797
|
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
667
798
|
getStats() {
|
|
668
799
|
const sizeBytes = this.sizeBytes();
|
|
669
|
-
const lastRows = this.db.prepare(
|
|
670
|
-
"SELECT value FROM metadata WHERE key = 'last_indexed'"
|
|
671
|
-
).all();
|
|
800
|
+
const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
|
|
672
801
|
const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
|
|
673
|
-
const totalRows = this.
|
|
802
|
+
const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
|
|
674
803
|
const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
|
|
675
|
-
const fileRows = this.
|
|
804
|
+
const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
|
|
676
805
|
const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
|
|
677
|
-
const langRows = this.
|
|
678
|
-
"SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
|
|
679
|
-
).all();
|
|
806
|
+
const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
|
|
680
807
|
const byLang = {};
|
|
681
808
|
for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
|
|
682
|
-
const kindRows = this.
|
|
683
|
-
"SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
|
|
684
|
-
).all();
|
|
809
|
+
const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
|
|
685
810
|
const byKind = {};
|
|
686
811
|
for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
|
|
687
812
|
return {
|
|
@@ -697,17 +822,34 @@ var IndexStore = class {
|
|
|
697
822
|
}
|
|
698
823
|
setLastIndexed(ts2) {
|
|
699
824
|
this.runWithRetry(() => {
|
|
700
|
-
this.db.prepare(
|
|
701
|
-
|
|
702
|
-
|
|
825
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
getMetadata(key) {
|
|
829
|
+
const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
|
|
830
|
+
return rows[0]?.value;
|
|
831
|
+
}
|
|
832
|
+
setMetadata(key, value) {
|
|
833
|
+
this.runWithRetry(() => {
|
|
834
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
|
|
703
835
|
});
|
|
704
836
|
}
|
|
705
837
|
clearAll() {
|
|
706
838
|
this.runWithRetry(() => {
|
|
707
|
-
this.db.exec("
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
839
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
840
|
+
try {
|
|
841
|
+
this.db.exec("DROP TABLE IF EXISTS refs");
|
|
842
|
+
this.db.exec("DROP TABLE IF EXISTS symbols");
|
|
843
|
+
this.db.exec("DROP TABLE IF EXISTS files");
|
|
844
|
+
this.db.exec("DROP TABLE IF EXISTS metadata");
|
|
845
|
+
if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
846
|
+
this.db.exec("COMMIT");
|
|
847
|
+
this.stmtCache.clear();
|
|
848
|
+
this.initSchema();
|
|
849
|
+
} catch (err) {
|
|
850
|
+
this.db.exec("ROLLBACK");
|
|
851
|
+
throw err;
|
|
852
|
+
}
|
|
711
853
|
});
|
|
712
854
|
}
|
|
713
855
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -788,7 +930,7 @@ var IndexStore = class {
|
|
|
788
930
|
).run(...options.deleteForFiles);
|
|
789
931
|
this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
|
|
790
932
|
}
|
|
791
|
-
const maxRows = this.
|
|
933
|
+
const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
792
934
|
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
793
935
|
const symStmt = this.db.prepare(
|
|
794
936
|
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
@@ -798,6 +940,7 @@ var IndexStore = class {
|
|
|
798
940
|
const allInserted = [];
|
|
799
941
|
const refsToInsert = [];
|
|
800
942
|
for (const entry of entries) {
|
|
943
|
+
const insertedForEntry = [];
|
|
801
944
|
for (const s of entry.symbols) {
|
|
802
945
|
const id = nextId++;
|
|
803
946
|
symStmt.run(
|
|
@@ -815,9 +958,11 @@ var IndexStore = class {
|
|
|
815
958
|
s.file
|
|
816
959
|
);
|
|
817
960
|
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
818
|
-
|
|
961
|
+
const inserted = { ...s, id };
|
|
962
|
+
allInserted.push(inserted);
|
|
963
|
+
insertedForEntry.push(inserted);
|
|
819
964
|
}
|
|
820
|
-
|
|
965
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
821
966
|
}
|
|
822
967
|
if (refsToInsert.length > 0) {
|
|
823
968
|
const refStmt = this.db.prepare(
|
|
@@ -855,9 +1000,7 @@ var IndexStore = class {
|
|
|
855
1000
|
*/
|
|
856
1001
|
deleteRefsForFile(file) {
|
|
857
1002
|
this.runWithRetry(() => {
|
|
858
|
-
this.db.prepare(
|
|
859
|
-
"DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
860
|
-
).run(file);
|
|
1003
|
+
this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
|
|
861
1004
|
});
|
|
862
1005
|
}
|
|
863
1006
|
/**
|
|
@@ -898,9 +1041,7 @@ var IndexStore = class {
|
|
|
898
1041
|
* Find all references FROM a given symbol (what does this symbol call/use?).
|
|
899
1042
|
*/
|
|
900
1043
|
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) => ({
|
|
1044
|
+
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
1045
|
id: r.id,
|
|
905
1046
|
fromId: r.from_id,
|
|
906
1047
|
toName: r.to_name,
|
|
@@ -917,6 +1058,356 @@ var IndexStore = class {
|
|
|
917
1058
|
return 0;
|
|
918
1059
|
}
|
|
919
1060
|
}
|
|
1061
|
+
// ─── CodeMap graph aggregation ──────────────────────────────────────────────
|
|
1062
|
+
/**
|
|
1063
|
+
* Derive a monorepo package name from an absolute file path.
|
|
1064
|
+
* Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
|
|
1065
|
+
* Returns `undefined` for files outside any recognisable package structure.
|
|
1066
|
+
*/
|
|
1067
|
+
static derivePackage(filePath) {
|
|
1068
|
+
const f = filePath.replace(/\\/g, "/");
|
|
1069
|
+
const pkgsIdx = f.indexOf("/packages/");
|
|
1070
|
+
if (pkgsIdx !== -1) {
|
|
1071
|
+
const rest = f.slice(pkgsIdx + "/packages/".length);
|
|
1072
|
+
const seg = rest.split("/")[0];
|
|
1073
|
+
return seg ? `@wrongstack/${seg}` : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
const appsIdx = f.indexOf("/apps/");
|
|
1076
|
+
if (appsIdx !== -1) {
|
|
1077
|
+
const rest = f.slice(appsIdx + "/apps/".length);
|
|
1078
|
+
const seg = rest.split("/")[0];
|
|
1079
|
+
return seg ? `app:${seg}` : void 0;
|
|
1080
|
+
}
|
|
1081
|
+
return void 0;
|
|
1082
|
+
}
|
|
1083
|
+
static packageFromImport(moduleName) {
|
|
1084
|
+
if (!moduleName.startsWith("@wrongstack/")) return void 0;
|
|
1085
|
+
const parts = moduleName.split("/");
|
|
1086
|
+
return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
|
|
1087
|
+
}
|
|
1088
|
+
static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
|
|
1089
|
+
if (!moduleName.startsWith(".")) return void 0;
|
|
1090
|
+
const normalizedFrom = fromFile.replace(/\\/g, "/");
|
|
1091
|
+
const absolute = path.posix.normalize(
|
|
1092
|
+
path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
|
|
1093
|
+
);
|
|
1094
|
+
const extension = path.posix.extname(absolute);
|
|
1095
|
+
const base = extension ? absolute.slice(0, -extension.length) : absolute;
|
|
1096
|
+
const candidates = [
|
|
1097
|
+
absolute,
|
|
1098
|
+
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
|
|
1099
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
|
|
1100
|
+
...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
|
|
1101
|
+
];
|
|
1102
|
+
const indexedByPortablePath = new Map(
|
|
1103
|
+
[...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
|
|
1104
|
+
);
|
|
1105
|
+
for (const candidate of candidates) {
|
|
1106
|
+
const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
|
|
1107
|
+
if (indexed) return indexed;
|
|
1108
|
+
}
|
|
1109
|
+
return void 0;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Package-level graph: each workspace package is a node; edges are derived
|
|
1113
|
+
* from cross-package symbol references (a symbol in package A references a
|
|
1114
|
+
* symbol resolved in package B). Node metadata includes symbol/file counts.
|
|
1115
|
+
*/
|
|
1116
|
+
getPackageGraph() {
|
|
1117
|
+
const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
|
|
1118
|
+
const pkgNodes = /* @__PURE__ */ new Map();
|
|
1119
|
+
const fileToPkg = /* @__PURE__ */ new Map();
|
|
1120
|
+
const symbolToPkg = /* @__PURE__ */ new Map();
|
|
1121
|
+
for (const s of symbols) {
|
|
1122
|
+
const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
|
|
1123
|
+
fileToPkg.set(s.file, pkg);
|
|
1124
|
+
symbolToPkg.set(s.id, pkg);
|
|
1125
|
+
const node = pkgNodes.get(pkg);
|
|
1126
|
+
if (node) {
|
|
1127
|
+
node.symbolCount = (node.symbolCount ?? 0) + 1;
|
|
1128
|
+
} else {
|
|
1129
|
+
pkgNodes.set(pkg, {
|
|
1130
|
+
id: `pkg:${pkg}`,
|
|
1131
|
+
label: pkg,
|
|
1132
|
+
kind: "package",
|
|
1133
|
+
package: pkg,
|
|
1134
|
+
symbolCount: 1,
|
|
1135
|
+
fileCount: 0
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
|
|
1140
|
+
for (const { file } of files) {
|
|
1141
|
+
const pkg = _IndexStore.derivePackage(file) ?? "(root)";
|
|
1142
|
+
const node = pkgNodes.get(pkg);
|
|
1143
|
+
if (node) node.fileCount = (node.fileCount ?? 0) + 1;
|
|
1144
|
+
}
|
|
1145
|
+
const refRows = this.db.prepare(
|
|
1146
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1147
|
+
FROM refs r
|
|
1148
|
+
WHERE r.to_id IS NOT NULL`
|
|
1149
|
+
).all();
|
|
1150
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1151
|
+
for (const r of refRows) {
|
|
1152
|
+
if (r.call_type === "import") continue;
|
|
1153
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1154
|
+
const toPkg = symbolToPkg.get(r.to_id);
|
|
1155
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
|
|
1156
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1157
|
+
let e = edgeMap.get(key);
|
|
1158
|
+
if (!e) {
|
|
1159
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1160
|
+
edgeMap.set(key, e);
|
|
1161
|
+
}
|
|
1162
|
+
e.weight++;
|
|
1163
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1164
|
+
}
|
|
1165
|
+
const importRows = this.db.prepare(
|
|
1166
|
+
`SELECT r.from_id, r.to_name
|
|
1167
|
+
FROM refs r
|
|
1168
|
+
WHERE r.call_type = 'import'`
|
|
1169
|
+
).all();
|
|
1170
|
+
for (const r of importRows) {
|
|
1171
|
+
const fromPkg = symbolToPkg.get(r.from_id);
|
|
1172
|
+
const toPkg = _IndexStore.packageFromImport(r.to_name);
|
|
1173
|
+
if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
1174
|
+
const key = `${fromPkg}\0${toPkg}`;
|
|
1175
|
+
let edge = edgeMap.get(key);
|
|
1176
|
+
if (!edge) {
|
|
1177
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1178
|
+
edgeMap.set(key, edge);
|
|
1179
|
+
}
|
|
1180
|
+
edge.weight++;
|
|
1181
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1182
|
+
}
|
|
1183
|
+
const edges = [];
|
|
1184
|
+
for (const [key, e] of edgeMap) {
|
|
1185
|
+
const [source, target] = key.split("\0");
|
|
1186
|
+
let bestType = "call";
|
|
1187
|
+
let bestCount = 0;
|
|
1188
|
+
for (const [t, c] of e.types) {
|
|
1189
|
+
if (c > bestCount) {
|
|
1190
|
+
bestType = t;
|
|
1191
|
+
bestCount = c;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
edges.push({
|
|
1195
|
+
source: `pkg:${source}`,
|
|
1196
|
+
target: `pkg:${target}`,
|
|
1197
|
+
weight: e.weight,
|
|
1198
|
+
refType: bestType
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
return { nodes: [...pkgNodes.values()], edges };
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* File-level graph for a single package: each file is a node; edges are
|
|
1205
|
+
* derived from cross-file symbol references within the package.
|
|
1206
|
+
*/
|
|
1207
|
+
getFileGraph(packageFilter) {
|
|
1208
|
+
const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
|
|
1209
|
+
const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
|
|
1210
|
+
const localFiles = new Set(pkgFilePaths);
|
|
1211
|
+
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
1212
|
+
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
1213
|
+
const pkgSyms = this.db.prepare(
|
|
1214
|
+
`SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
|
|
1215
|
+
).all(...pkgFilePaths);
|
|
1216
|
+
const fileNodes = /* @__PURE__ */ new Map();
|
|
1217
|
+
const symToFile = /* @__PURE__ */ new Map();
|
|
1218
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const s of pkgSyms) {
|
|
1220
|
+
symToFile.set(s.id, s.file);
|
|
1221
|
+
const current = fileStats.get(s.file);
|
|
1222
|
+
fileStats.set(s.file, {
|
|
1223
|
+
count: (current?.count ?? 0) + 1,
|
|
1224
|
+
lang: current?.lang ?? s.lang
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const ensureFileNode = (file) => {
|
|
1228
|
+
if (fileNodes.has(file)) return;
|
|
1229
|
+
const stats = fileStats.get(file);
|
|
1230
|
+
fileNodes.set(file, {
|
|
1231
|
+
id: `file:${file}`,
|
|
1232
|
+
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
1233
|
+
kind: "file",
|
|
1234
|
+
package: _IndexStore.derivePackage(file) ?? "(root)",
|
|
1235
|
+
file,
|
|
1236
|
+
symbolCount: stats?.count ?? 0,
|
|
1237
|
+
lang: stats?.lang,
|
|
1238
|
+
external: !localFiles.has(file)
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
for (const file of localFiles) {
|
|
1242
|
+
ensureFileNode(file);
|
|
1243
|
+
}
|
|
1244
|
+
const indexedFiles = new Set(allFiles.map((f) => f.file));
|
|
1245
|
+
const refRows = this.db.prepare(
|
|
1246
|
+
`SELECT r.from_id, r.to_id, r.call_type
|
|
1247
|
+
FROM refs r
|
|
1248
|
+
WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
1249
|
+
OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
|
|
1250
|
+
AND r.to_id IS NOT NULL`
|
|
1251
|
+
).all(...pkgFilePaths, ...pkgFilePaths);
|
|
1252
|
+
const knownSymIds = new Set(pkgSyms.map((s) => s.id));
|
|
1253
|
+
const crossRefIds = /* @__PURE__ */ new Set();
|
|
1254
|
+
for (const r of refRows) {
|
|
1255
|
+
if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
|
|
1256
|
+
if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
|
|
1257
|
+
}
|
|
1258
|
+
if (crossRefIds.size > 0) {
|
|
1259
|
+
const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
|
|
1260
|
+
const extras = this.db.prepare(
|
|
1261
|
+
`SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
|
|
1262
|
+
).all(...crossRefIds);
|
|
1263
|
+
for (const x of extras) {
|
|
1264
|
+
symToFile.set(x.id, x.file);
|
|
1265
|
+
if (!fileStats.has(x.file)) {
|
|
1266
|
+
fileStats.set(x.file, { count: 0, lang: "ts" });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1271
|
+
for (const r of refRows) {
|
|
1272
|
+
if (r.call_type === "import") continue;
|
|
1273
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1274
|
+
const toFile = symToFile.get(r.to_id);
|
|
1275
|
+
if (!fromFile || !toFile || fromFile === toFile) continue;
|
|
1276
|
+
if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
|
|
1277
|
+
ensureFileNode(fromFile);
|
|
1278
|
+
ensureFileNode(toFile);
|
|
1279
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1280
|
+
let e = edgeMap.get(key);
|
|
1281
|
+
if (!e) {
|
|
1282
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1283
|
+
edgeMap.set(key, e);
|
|
1284
|
+
}
|
|
1285
|
+
e.weight++;
|
|
1286
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1287
|
+
}
|
|
1288
|
+
const importRows = this.db.prepare(
|
|
1289
|
+
`SELECT r.from_id, r.to_name
|
|
1290
|
+
FROM refs r
|
|
1291
|
+
WHERE r.call_type = 'import'
|
|
1292
|
+
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
|
|
1293
|
+
).all(...pkgFilePaths);
|
|
1294
|
+
for (const r of importRows) {
|
|
1295
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1296
|
+
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
1297
|
+
const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
|
|
1298
|
+
if (!toFile || fromFile === toFile) continue;
|
|
1299
|
+
ensureFileNode(fromFile);
|
|
1300
|
+
ensureFileNode(toFile);
|
|
1301
|
+
const key = `${fromFile}\0${toFile}`;
|
|
1302
|
+
let edge = edgeMap.get(key);
|
|
1303
|
+
if (!edge) {
|
|
1304
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1305
|
+
edgeMap.set(key, edge);
|
|
1306
|
+
}
|
|
1307
|
+
edge.weight++;
|
|
1308
|
+
edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
|
|
1309
|
+
}
|
|
1310
|
+
const edges = [];
|
|
1311
|
+
for (const [key, e] of edgeMap) {
|
|
1312
|
+
const [source, target] = key.split("\0");
|
|
1313
|
+
let bestType = "call";
|
|
1314
|
+
let bestCount = 0;
|
|
1315
|
+
for (const [t, c] of e.types) {
|
|
1316
|
+
if (c > bestCount) {
|
|
1317
|
+
bestType = t;
|
|
1318
|
+
bestCount = c;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
edges.push({
|
|
1322
|
+
source: `file:${source}`,
|
|
1323
|
+
target: `file:${target}`,
|
|
1324
|
+
weight: e.weight,
|
|
1325
|
+
refType: bestType
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return { nodes: [...fileNodes.values()], edges };
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Symbol-level graph for a single file: each symbol is a node; edges are
|
|
1332
|
+
* derived from intra-file and cross-file symbol references (who calls whom).
|
|
1333
|
+
*/
|
|
1334
|
+
getSymbolGraph(fileFilter) {
|
|
1335
|
+
const syms = this.db.prepare(
|
|
1336
|
+
"SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
|
|
1337
|
+
).all(fileFilter);
|
|
1338
|
+
if (syms.length === 0) return { nodes: [], edges: [] };
|
|
1339
|
+
const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
|
|
1340
|
+
const relatedIds = new Set(syms.map((symbol) => symbol.id));
|
|
1341
|
+
const toGraphNode = (s) => ({
|
|
1342
|
+
id: `sym:${s.id}`,
|
|
1343
|
+
label: s.name,
|
|
1344
|
+
kind: "symbol",
|
|
1345
|
+
symbolId: s.id,
|
|
1346
|
+
symbolKind: s.kind,
|
|
1347
|
+
file: s.file,
|
|
1348
|
+
package: _IndexStore.derivePackage(s.file) ?? "(root)",
|
|
1349
|
+
lang: s.lang,
|
|
1350
|
+
line: s.line,
|
|
1351
|
+
signature: s.signature,
|
|
1352
|
+
scope: s.scope,
|
|
1353
|
+
external: s.file !== fileFilter
|
|
1354
|
+
});
|
|
1355
|
+
const symIds = new Set(syms.map((s) => s.id));
|
|
1356
|
+
const refRows = this.db.prepare(
|
|
1357
|
+
`SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
1358
|
+
FROM refs r
|
|
1359
|
+
WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
|
|
1360
|
+
OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
|
|
1361
|
+
).all(fileFilter, fileFilter);
|
|
1362
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (const r of refRows) {
|
|
1364
|
+
if (r.to_id === null) continue;
|
|
1365
|
+
if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
|
|
1366
|
+
relatedIds.add(r.from_id);
|
|
1367
|
+
relatedIds.add(r.to_id);
|
|
1368
|
+
const key = `${r.from_id}\0${r.to_id}`;
|
|
1369
|
+
let e = edgeMap.get(key);
|
|
1370
|
+
if (!e) {
|
|
1371
|
+
e = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1372
|
+
edgeMap.set(key, e);
|
|
1373
|
+
}
|
|
1374
|
+
e.weight++;
|
|
1375
|
+
e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
|
|
1376
|
+
}
|
|
1377
|
+
const edges = [];
|
|
1378
|
+
for (const [key, e] of edgeMap) {
|
|
1379
|
+
const [fromId, toId] = key.split("\0").map(Number);
|
|
1380
|
+
let bestType = "call";
|
|
1381
|
+
let bestCount = 0;
|
|
1382
|
+
for (const [t, c] of e.types) {
|
|
1383
|
+
if (c > bestCount) {
|
|
1384
|
+
bestType = t;
|
|
1385
|
+
bestCount = c;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
edges.push({
|
|
1389
|
+
source: `sym:${fromId}`,
|
|
1390
|
+
target: `sym:${toId}`,
|
|
1391
|
+
weight: e.weight,
|
|
1392
|
+
refType: bestType
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
const loadedIds = new Set(syms.map((s) => s.id));
|
|
1396
|
+
const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
|
|
1397
|
+
if (missingIds.length > 0) {
|
|
1398
|
+
const placeholders = missingIds.map(() => "?").join(",");
|
|
1399
|
+
const extras = this.db.prepare(
|
|
1400
|
+
`SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
|
|
1401
|
+
).all(...missingIds);
|
|
1402
|
+
for (const s of extras) symById.set(s.id, s);
|
|
1403
|
+
}
|
|
1404
|
+
const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
1405
|
+
const aExternal = a.file === fileFilter ? 0 : 1;
|
|
1406
|
+
const bExternal = b.file === fileFilter ? 0 : 1;
|
|
1407
|
+
return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
|
|
1408
|
+
}).map(toGraphNode);
|
|
1409
|
+
return { nodes, edges };
|
|
1410
|
+
}
|
|
920
1411
|
close() {
|
|
921
1412
|
this.stmtCache.clear();
|
|
922
1413
|
try {
|
|
@@ -980,8 +1471,7 @@ function extToLang(ext) {
|
|
|
980
1471
|
return null;
|
|
981
1472
|
}
|
|
982
1473
|
}
|
|
983
|
-
function getSignature(node, sourceFile) {
|
|
984
|
-
const printer = ts.createPrinter({});
|
|
1474
|
+
function getSignature(printer, node, sourceFile) {
|
|
985
1475
|
const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
|
986
1476
|
return raw.replace(/\s+/g, " ").slice(0, 500);
|
|
987
1477
|
}
|
|
@@ -1000,20 +1490,14 @@ function getJsDoc(node, sourceFile) {
|
|
|
1000
1490
|
}
|
|
1001
1491
|
return "";
|
|
1002
1492
|
}
|
|
1003
|
-
function
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
if (
|
|
1008
|
-
parts.
|
|
1009
|
-
} else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
|
|
1010
|
-
if (current.name && ts.isIdentifier(current.name)) {
|
|
1011
|
-
parts.unshift(current.name.text);
|
|
1012
|
-
}
|
|
1493
|
+
function pushScopeName(node, parts) {
|
|
1494
|
+
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
|
|
1495
|
+
parts.push(node.name?.text ?? "Anon");
|
|
1496
|
+
} else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
|
|
1497
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
1498
|
+
parts.push(node.name.text);
|
|
1013
1499
|
}
|
|
1014
|
-
current = current.parent;
|
|
1015
1500
|
}
|
|
1016
|
-
return parts.join(".");
|
|
1017
1501
|
}
|
|
1018
1502
|
function parseSymbols(opts) {
|
|
1019
1503
|
const { file, content, lang } = opts;
|
|
@@ -1024,41 +1508,39 @@ function parseSymbols(opts) {
|
|
|
1024
1508
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
1025
1509
|
}
|
|
1026
1510
|
const symbols = [];
|
|
1027
|
-
|
|
1511
|
+
const refs = [];
|
|
1512
|
+
const printer = ts.createPrinter({});
|
|
1513
|
+
function visit(node, funcDepth, scopeParts) {
|
|
1028
1514
|
const kind = kindOf(node);
|
|
1029
1515
|
if (kind) {
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1516
|
+
if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
|
|
1517
|
+
} else {
|
|
1518
|
+
const nameNode = node.name;
|
|
1519
|
+
if (!nameNode || !ts.isIdentifier(nameNode)) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const name = nameNode.text;
|
|
1523
|
+
const pos2 = nameNode.getStart(sourceFile);
|
|
1524
|
+
const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
|
|
1525
|
+
const scope = scopeParts.join(".");
|
|
1526
|
+
const signature = getSignature(printer, node, sourceFile);
|
|
1527
|
+
const docComment = getJsDoc(node, sourceFile);
|
|
1528
|
+
const text = [name, signature, docComment].filter(Boolean).join(" | ");
|
|
1529
|
+
symbols.push({
|
|
1530
|
+
id: 0,
|
|
1531
|
+
lang,
|
|
1532
|
+
kind,
|
|
1533
|
+
name,
|
|
1534
|
+
file,
|
|
1535
|
+
line: line2 + 1,
|
|
1536
|
+
col: character,
|
|
1537
|
+
signature,
|
|
1538
|
+
docComment,
|
|
1539
|
+
scope,
|
|
1540
|
+
text
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1052
1543
|
}
|
|
1053
|
-
ts.forEachChild(node, visit);
|
|
1054
|
-
}
|
|
1055
|
-
visit(sourceFile);
|
|
1056
|
-
const refs = extractRefs(sourceFile);
|
|
1057
|
-
return { file, lang, symbols, refs, mtimeMs: Date.now() };
|
|
1058
|
-
}
|
|
1059
|
-
function extractRefs(sourceFile) {
|
|
1060
|
-
const refs = [];
|
|
1061
|
-
function visit(node) {
|
|
1062
1544
|
const pos = node.getStart(sourceFile);
|
|
1063
1545
|
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
1064
1546
|
const lineNum = line + 1;
|
|
@@ -1083,10 +1565,14 @@ function extractRefs(sourceFile) {
|
|
|
1083
1565
|
const moduleName = getModuleName(node);
|
|
1084
1566
|
if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
|
|
1085
1567
|
}
|
|
1086
|
-
|
|
1568
|
+
const scopeIdx = scopeParts.length;
|
|
1569
|
+
pushScopeName(node, scopeParts);
|
|
1570
|
+
const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
|
|
1571
|
+
ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
|
|
1572
|
+
scopeParts.length = scopeIdx;
|
|
1087
1573
|
}
|
|
1088
|
-
visit(sourceFile);
|
|
1089
|
-
return deduplicateRefs(refs);
|
|
1574
|
+
visit(sourceFile, 0, []);
|
|
1575
|
+
return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
|
|
1090
1576
|
}
|
|
1091
1577
|
function getTypeName(name) {
|
|
1092
1578
|
if (ts.isIdentifier(name)) return name.text;
|
|
@@ -1994,9 +2480,9 @@ function parseSymbols5(opts) {
|
|
|
1994
2480
|
function regexParse2(opts) {
|
|
1995
2481
|
const { file, content, lang } = opts;
|
|
1996
2482
|
const symbols = [];
|
|
1997
|
-
const
|
|
1998
|
-
const isPackageJson =
|
|
1999
|
-
const isTsconfig =
|
|
2483
|
+
const basename3 = path6.basename(file).toLowerCase();
|
|
2484
|
+
const isPackageJson = basename3 === "package.json";
|
|
2485
|
+
const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
|
|
2000
2486
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
2001
2487
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
2002
2488
|
const lines = content.split("\n");
|
|
@@ -2391,16 +2877,14 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
2391
2877
|
|
|
2392
2878
|
// src/codebase-index/indexer.ts
|
|
2393
2879
|
var YIELD_EVERY_N = 50;
|
|
2394
|
-
var PARALLEL_BATCH =
|
|
2880
|
+
var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
|
|
2395
2881
|
function yieldEventLoop() {
|
|
2396
2882
|
return new Promise((resolve2) => setImmediate(resolve2));
|
|
2397
2883
|
}
|
|
2398
2884
|
function throwIfAborted(signal) {
|
|
2399
2885
|
if (!signal?.aborted) return;
|
|
2400
2886
|
if (signal.reason instanceof Error) throw signal.reason;
|
|
2401
|
-
throw new Error(
|
|
2402
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
2403
|
-
);
|
|
2887
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
|
|
2404
2888
|
}
|
|
2405
2889
|
function isAbortError(err) {
|
|
2406
2890
|
return err instanceof DOMException && err.name === "AbortError";
|
|
@@ -2416,8 +2900,20 @@ var DEFAULT_IGNORE = [
|
|
|
2416
2900
|
"__snapshots__",
|
|
2417
2901
|
".nyc_output"
|
|
2418
2902
|
];
|
|
2903
|
+
var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
|
|
2904
|
+
var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
|
|
2905
|
+
function isWithinProject(projectRoot, file) {
|
|
2906
|
+
const rel = path8.relative(projectRoot, file);
|
|
2907
|
+
return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
|
|
2908
|
+
}
|
|
2909
|
+
function isMissingPathError(err) {
|
|
2910
|
+
const code = err?.code;
|
|
2911
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2912
|
+
}
|
|
2419
2913
|
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
2420
2914
|
const results = [];
|
|
2915
|
+
const errors = [];
|
|
2916
|
+
let complete = true;
|
|
2421
2917
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
2422
2918
|
const globs = [
|
|
2423
2919
|
{ ext: ".ts", pat: compileGlob2("**/*.ts") },
|
|
@@ -2441,7 +2937,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2441
2937
|
let entries;
|
|
2442
2938
|
try {
|
|
2443
2939
|
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2444
|
-
} catch {
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
complete = false;
|
|
2942
|
+
errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2445
2943
|
return;
|
|
2446
2944
|
}
|
|
2447
2945
|
dirCount++;
|
|
@@ -2453,8 +2951,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2453
2951
|
if (isGitIgnored(rel, true)) continue;
|
|
2454
2952
|
await walk(full);
|
|
2455
2953
|
} else if (e.isFile()) {
|
|
2456
|
-
if (isGitIgnored(rel, false)) continue;
|
|
2457
|
-
const ext = path8.extname(e.name);
|
|
2954
|
+
if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
|
|
2955
|
+
const ext = path8.extname(e.name).toLowerCase();
|
|
2458
2956
|
for (const { ext: extName, pat } of globs) {
|
|
2459
2957
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
2460
2958
|
results.push(full);
|
|
@@ -2465,7 +2963,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2465
2963
|
}
|
|
2466
2964
|
};
|
|
2467
2965
|
await walk(projectRoot);
|
|
2468
|
-
return results;
|
|
2966
|
+
return { files: results, complete, errors };
|
|
2469
2967
|
}
|
|
2470
2968
|
async function parseFile(file, content, lang) {
|
|
2471
2969
|
switch (lang) {
|
|
@@ -2488,6 +2986,26 @@ async function parseFile(file, content, lang) {
|
|
|
2488
2986
|
return { file, lang, symbols: [], mtimeMs: Date.now() };
|
|
2489
2987
|
}
|
|
2490
2988
|
}
|
|
2989
|
+
function assignRefsToSymbols2(refs, symbols) {
|
|
2990
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
2991
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
2992
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2993
|
+
const assigned = [];
|
|
2994
|
+
for (const ref of refs) {
|
|
2995
|
+
let owner;
|
|
2996
|
+
for (const symbol of ordered) {
|
|
2997
|
+
if (symbol.line > ref.line) break;
|
|
2998
|
+
owner = symbol;
|
|
2999
|
+
}
|
|
3000
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
3001
|
+
if (!owner || owner.id <= 0) continue;
|
|
3002
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}`;
|
|
3003
|
+
if (seen.has(key)) continue;
|
|
3004
|
+
seen.add(key);
|
|
3005
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
3006
|
+
}
|
|
3007
|
+
return assigned;
|
|
3008
|
+
}
|
|
2491
3009
|
async function runIndexer(_ctx, opts) {
|
|
2492
3010
|
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
2493
3011
|
try {
|
|
@@ -2500,7 +3018,9 @@ async function runIndexer(_ctx, opts) {
|
|
|
2500
3018
|
}
|
|
2501
3019
|
}
|
|
2502
3020
|
async function runIndexerWithStore(store, opts) {
|
|
2503
|
-
const { projectRoot,
|
|
3021
|
+
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
3022
|
+
const relationGraphVersion = "2";
|
|
3023
|
+
const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
|
|
2504
3024
|
const startMs = Date.now();
|
|
2505
3025
|
const errors = [];
|
|
2506
3026
|
const langStats = {};
|
|
@@ -2509,10 +3029,18 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2509
3029
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
2510
3030
|
let files;
|
|
2511
3031
|
let discoveredFiles = null;
|
|
3032
|
+
let discoveryComplete = true;
|
|
2512
3033
|
if (opts.files && opts.files.length > 0) {
|
|
2513
|
-
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) =>
|
|
3034
|
+
files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
|
|
3035
|
+
if (!isWithinProject(projectRoot, f)) return false;
|
|
3036
|
+
const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
|
|
3037
|
+
return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
|
|
3038
|
+
});
|
|
2514
3039
|
} else {
|
|
2515
|
-
|
|
3040
|
+
const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
3041
|
+
files = discovery.files;
|
|
3042
|
+
errors.push(...discovery.errors);
|
|
3043
|
+
discoveryComplete = discovery.complete;
|
|
2516
3044
|
discoveredFiles = new Set(files);
|
|
2517
3045
|
}
|
|
2518
3046
|
if (langs && langs.length > 0) {
|
|
@@ -2527,46 +3055,79 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2527
3055
|
if (!force) {
|
|
2528
3056
|
for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
|
|
2529
3057
|
}
|
|
3058
|
+
let filesSinceLastYield = 0;
|
|
2530
3059
|
for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
|
|
2531
3060
|
const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
|
|
2532
3061
|
const batchFiles = files.slice(batchStart, batchEnd);
|
|
2533
3062
|
opts.onProgress?.(batchEnd, files.length);
|
|
2534
|
-
|
|
3063
|
+
filesSinceLastYield += batchFiles.length;
|
|
3064
|
+
if (filesSinceLastYield >= YIELD_EVERY_N) {
|
|
3065
|
+
filesSinceLastYield = 0;
|
|
2535
3066
|
await yieldEventLoop();
|
|
2536
3067
|
throwIfAborted(signal);
|
|
2537
3068
|
}
|
|
2538
3069
|
const statOpts = signal ? { signal } : {};
|
|
2539
3070
|
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
|
-
|
|
3071
|
+
batchFiles.map(
|
|
3072
|
+
async (file) => {
|
|
3073
|
+
let stat2;
|
|
3074
|
+
try {
|
|
3075
|
+
stat2 = await fs7.stat(file, statOpts);
|
|
3076
|
+
} catch (e) {
|
|
3077
|
+
if (isAbortError(e)) throw e;
|
|
3078
|
+
return {
|
|
3079
|
+
file,
|
|
3080
|
+
stat: null,
|
|
3081
|
+
lang: "",
|
|
3082
|
+
parsed: null,
|
|
3083
|
+
error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
|
|
3084
|
+
missing: isMissingPathError(e)
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
|
|
3088
|
+
const lang = detectLang(file);
|
|
3089
|
+
if (!lang) return { file, stat: stat2, lang: "", parsed: null };
|
|
3090
|
+
if (stat2.size > MAX_INDEX_FILE_BYTES) {
|
|
3091
|
+
return {
|
|
3092
|
+
file,
|
|
3093
|
+
stat: stat2,
|
|
3094
|
+
lang,
|
|
3095
|
+
parsed: null,
|
|
3096
|
+
error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
const meta = existingMeta.get(file);
|
|
3100
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
|
|
3101
|
+
return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
|
|
3102
|
+
}
|
|
3103
|
+
let content;
|
|
3104
|
+
try {
|
|
3105
|
+
content = await fs7.readFile(file, { encoding: "utf8", signal });
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
if (isAbortError(e)) throw e;
|
|
3108
|
+
return {
|
|
3109
|
+
file,
|
|
3110
|
+
stat: stat2,
|
|
3111
|
+
lang,
|
|
3112
|
+
parsed: null,
|
|
3113
|
+
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
let parsed;
|
|
3117
|
+
try {
|
|
3118
|
+
parsed = await parseFile(file, content, lang);
|
|
3119
|
+
} catch (e) {
|
|
3120
|
+
return {
|
|
3121
|
+
file,
|
|
3122
|
+
stat: stat2,
|
|
3123
|
+
lang,
|
|
3124
|
+
parsed: null,
|
|
3125
|
+
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
return { file, stat: stat2, lang, parsed, content };
|
|
2567
3129
|
}
|
|
2568
|
-
|
|
2569
|
-
})
|
|
3130
|
+
)
|
|
2570
3131
|
);
|
|
2571
3132
|
const batchEntries = [];
|
|
2572
3133
|
const deleteForFiles = [];
|
|
@@ -2581,8 +3142,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2581
3142
|
}
|
|
2582
3143
|
const result = settled.value;
|
|
2583
3144
|
if (result.error) {
|
|
2584
|
-
if (result.
|
|
2585
|
-
|
|
3145
|
+
if (result.missing) store.deleteFile(file);
|
|
3146
|
+
errors.push(`${file}: ${result.error}`);
|
|
2586
3147
|
continue;
|
|
2587
3148
|
}
|
|
2588
3149
|
const { stat: stat2, lang, parsed } = result;
|
|
@@ -2606,6 +3167,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2606
3167
|
continue;
|
|
2607
3168
|
}
|
|
2608
3169
|
if (parsed.symbols.length === 0) {
|
|
3170
|
+
store.deleteRefsForFile(file);
|
|
3171
|
+
store.deleteSymbolsForFile(file);
|
|
2609
3172
|
store.upsertFile({
|
|
2610
3173
|
file,
|
|
2611
3174
|
lang,
|
|
@@ -2616,15 +3179,11 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2616
3179
|
filesIndexed++;
|
|
2617
3180
|
continue;
|
|
2618
3181
|
}
|
|
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
3182
|
batchEntries.push({
|
|
2624
3183
|
file,
|
|
2625
3184
|
lang,
|
|
2626
3185
|
symbols: parsed.symbols,
|
|
2627
|
-
refs,
|
|
3186
|
+
refs: parsed.refs ?? [],
|
|
2628
3187
|
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
2629
3188
|
symbolCount: parsed.symbols.length
|
|
2630
3189
|
});
|
|
@@ -2632,34 +3191,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2632
3191
|
}
|
|
2633
3192
|
if (batchEntries.length > 0) {
|
|
2634
3193
|
try {
|
|
2635
|
-
|
|
2636
|
-
let cursor = 0;
|
|
3194
|
+
store.commitBatch(batchEntries, { deleteForFiles });
|
|
2637
3195
|
for (const entry of batchEntries) {
|
|
2638
3196
|
const count = entry.symbols.length;
|
|
2639
|
-
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
2640
|
-
cursor += count;
|
|
2641
3197
|
symbolsIndexed += count;
|
|
2642
3198
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
2643
3199
|
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
3200
|
}
|
|
2664
3201
|
} catch (err) {
|
|
2665
3202
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2673,23 +3210,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2673
3210
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
2674
3211
|
filesIndexed++;
|
|
2675
3212
|
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
|
-
}
|
|
3213
|
+
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
2693
3214
|
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
2694
3215
|
}
|
|
2695
3216
|
store.upsertFile({
|
|
@@ -2700,19 +3221,23 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2700
3221
|
lastIndexed: Date.now()
|
|
2701
3222
|
});
|
|
2702
3223
|
} catch (innerErr) {
|
|
2703
|
-
errors.push(
|
|
3224
|
+
errors.push(
|
|
3225
|
+
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
3226
|
+
);
|
|
2704
3227
|
}
|
|
2705
3228
|
}
|
|
2706
3229
|
}
|
|
2707
3230
|
}
|
|
2708
3231
|
}
|
|
2709
|
-
if (discoveredFiles) {
|
|
3232
|
+
if (discoveredFiles && discoveryComplete) {
|
|
2710
3233
|
for (const [file_] of existingMeta) {
|
|
2711
3234
|
if (!discoveredFiles.has(file_)) {
|
|
2712
3235
|
store.deleteFile(file_);
|
|
2713
3236
|
}
|
|
2714
3237
|
}
|
|
2715
3238
|
}
|
|
3239
|
+
store.resolveRefs();
|
|
3240
|
+
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
2716
3241
|
const durationMs = Date.now() - startMs;
|
|
2717
3242
|
store.setLastIndexed(Date.now());
|
|
2718
3243
|
return {
|
|
@@ -2748,7 +3273,7 @@ async function indexService(args, hooks = {}) {
|
|
|
2748
3273
|
});
|
|
2749
3274
|
}
|
|
2750
3275
|
function searchService(args) {
|
|
2751
|
-
const store =
|
|
3276
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
2752
3277
|
try {
|
|
2753
3278
|
return store.searchRanked(
|
|
2754
3279
|
args.query,
|
|
@@ -2761,15 +3286,15 @@ function searchService(args) {
|
|
|
2761
3286
|
args.limit
|
|
2762
3287
|
);
|
|
2763
3288
|
} finally {
|
|
2764
|
-
|
|
3289
|
+
indexStorePool.release(store);
|
|
2765
3290
|
}
|
|
2766
3291
|
}
|
|
2767
3292
|
function statsService(args) {
|
|
2768
|
-
const store =
|
|
3293
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
2769
3294
|
try {
|
|
2770
3295
|
return store.getStats();
|
|
2771
3296
|
} finally {
|
|
2772
|
-
|
|
3297
|
+
indexStorePool.release(store);
|
|
2773
3298
|
}
|
|
2774
3299
|
}
|
|
2775
3300
|
|
|
@@ -2815,7 +3340,8 @@ port.on("message", (msg) => {
|
|
|
2815
3340
|
type: "response",
|
|
2816
3341
|
id: msg.id,
|
|
2817
3342
|
ok: false,
|
|
2818
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3343
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3344
|
+
errorName: err instanceof Error ? err.name : void 0
|
|
2819
3345
|
});
|
|
2820
3346
|
} catch {
|
|
2821
3347
|
}
|