@wrongstack/tools 0.236.0 → 0.255.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +591 -48
- package/dist/audit.js.map +1 -1
- package/dist/background-indexer-CJ5JiV5i.d.ts +365 -0
- package/dist/bash.js +135 -20
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +1840 -1109
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +53 -2
- package/dist/codebase-index/index.js +870 -364
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.d.ts +2 -0
- package/dist/codebase-index/worker.js +2326 -0
- package/dist/codebase-index/worker.js.map +1 -0
- package/dist/diff.js +2 -1
- package/dist/diff.js.map +1 -1
- package/dist/exec.js +116 -5
- package/dist/exec.js.map +1 -1
- package/dist/format.js +591 -48
- package/dist/format.js.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1189 -496
- package/dist/index.js.map +1 -1
- package/dist/install.js +591 -48
- package/dist/install.js.map +1 -1
- package/dist/lint.js +590 -47
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js +1 -1
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +1840 -1109
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +1 -1
- package/dist/patch.js.map +1 -1
- package/dist/replace.js +3 -2
- package/dist/replace.js.map +1 -1
- package/dist/test.d.ts +1 -0
- package/dist/test.js +605 -55
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +591 -48
- package/dist/typecheck.js.map +1 -1
- package/package.json +3 -3
- package/dist/background-indexer-CtbgPExj.d.ts +0 -228
|
@@ -1,17 +1,102 @@
|
|
|
1
|
-
import { resolveWstackPaths, expectDefined, compileGlob } from '@wrongstack/core';
|
|
2
|
-
import * as fs3 from 'node:fs/promises';
|
|
3
|
-
import * as path4 from 'node:path';
|
|
1
|
+
import { resolveWstackPaths, expectDefined, compileGlob, truncate } from '@wrongstack/core';
|
|
4
2
|
import { createRequire } from 'node:module';
|
|
5
3
|
import * as fs from 'node:fs';
|
|
6
4
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import * as path4 from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Worker } from 'node:worker_threads';
|
|
8
|
+
import * as fs3 from 'node:fs/promises';
|
|
7
9
|
import * as ts from 'typescript';
|
|
8
10
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
9
11
|
import * as os from 'node:os';
|
|
10
12
|
|
|
11
|
-
// src/codebase-index/
|
|
13
|
+
// src/codebase-index/writer.ts
|
|
14
|
+
|
|
15
|
+
// src/codebase-index/circuit-breaker.ts
|
|
16
|
+
var CircuitOpenError = class extends Error {
|
|
17
|
+
name = "CircuitOpenError";
|
|
18
|
+
};
|
|
19
|
+
var IndexTimeoutError = class extends Error {
|
|
20
|
+
name = "IndexTimeoutError";
|
|
21
|
+
};
|
|
22
|
+
var LockError = class extends Error {
|
|
23
|
+
name = "LockError";
|
|
24
|
+
};
|
|
25
|
+
var IndexCircuitBreaker = class {
|
|
26
|
+
failureThreshold;
|
|
27
|
+
cooldownMs;
|
|
28
|
+
now;
|
|
29
|
+
state = "closed";
|
|
30
|
+
consecutiveFailures = 0;
|
|
31
|
+
openedAt = 0;
|
|
32
|
+
lastFailure = null;
|
|
33
|
+
probeInFlight = false;
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
36
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
37
|
+
this.now = opts.now ?? Date.now;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
41
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
42
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
43
|
+
*/
|
|
44
|
+
allowRequest() {
|
|
45
|
+
if (this.state === "closed") return true;
|
|
46
|
+
if (this.state === "open") {
|
|
47
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
48
|
+
this.state = "half-open";
|
|
49
|
+
this.probeInFlight = true;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (this.probeInFlight) return false;
|
|
53
|
+
this.probeInFlight = true;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
recordSuccess() {
|
|
57
|
+
this.state = "closed";
|
|
58
|
+
this.consecutiveFailures = 0;
|
|
59
|
+
this.lastFailure = null;
|
|
60
|
+
this.probeInFlight = false;
|
|
61
|
+
}
|
|
62
|
+
recordFailure(err) {
|
|
63
|
+
if (err instanceof LockError) {
|
|
64
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
65
|
+
this.probeInFlight = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
69
|
+
this.probeInFlight = false;
|
|
70
|
+
this.consecutiveFailures++;
|
|
71
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
72
|
+
this.state = "open";
|
|
73
|
+
this.openedAt = this.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
77
|
+
reset() {
|
|
78
|
+
this.state = "closed";
|
|
79
|
+
this.consecutiveFailures = 0;
|
|
80
|
+
this.lastFailure = null;
|
|
81
|
+
this.probeInFlight = false;
|
|
82
|
+
this.openedAt = 0;
|
|
83
|
+
}
|
|
84
|
+
snapshot() {
|
|
85
|
+
return {
|
|
86
|
+
state: this.state,
|
|
87
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
88
|
+
lastFailure: this.lastFailure,
|
|
89
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
94
|
+
function resetIndexCircuitBreaker() {
|
|
95
|
+
indexCircuitBreaker.reset();
|
|
96
|
+
}
|
|
12
97
|
|
|
13
98
|
// src/codebase-index/schema.ts
|
|
14
|
-
var SCHEMA_VERSION =
|
|
99
|
+
var SCHEMA_VERSION = 2;
|
|
15
100
|
|
|
16
101
|
// src/codebase-index/lsp-kind.ts
|
|
17
102
|
function lspKindToInternalKind(k) {
|
|
@@ -75,6 +160,94 @@ function internalKindToLspKind(k) {
|
|
|
75
160
|
}
|
|
76
161
|
}
|
|
77
162
|
|
|
163
|
+
// src/codebase-index/bm25.ts
|
|
164
|
+
var K1 = 1.5;
|
|
165
|
+
var B = 0.75;
|
|
166
|
+
function tokenise(text) {
|
|
167
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
168
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
function splitName(name) {
|
|
171
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
172
|
+
}
|
|
173
|
+
function buildIndexableText(name, signature, docComment) {
|
|
174
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
175
|
+
}
|
|
176
|
+
function buildBm25Index(docs) {
|
|
177
|
+
const documents = docs.map((d) => {
|
|
178
|
+
const tokens = tokenise(d.text);
|
|
179
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
180
|
+
});
|
|
181
|
+
const df = {};
|
|
182
|
+
for (const doc of documents) {
|
|
183
|
+
const seen = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const t of doc.tokens) {
|
|
185
|
+
if (!seen.has(t)) {
|
|
186
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
187
|
+
seen.add(t);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const N = documents.length;
|
|
192
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
193
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
194
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
195
|
+
}
|
|
196
|
+
var Bm25Index = class {
|
|
197
|
+
constructor(documents, df, N, avgLen) {
|
|
198
|
+
this.documents = documents;
|
|
199
|
+
this.df = df;
|
|
200
|
+
this.N = N;
|
|
201
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
202
|
+
}
|
|
203
|
+
documents;
|
|
204
|
+
df;
|
|
205
|
+
N;
|
|
206
|
+
safeAvgLen;
|
|
207
|
+
score(query, filter) {
|
|
208
|
+
const qTokens = tokenise(query);
|
|
209
|
+
if (qTokens.length === 0) return [];
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const doc of this.documents) {
|
|
212
|
+
if (filter && !filter(doc.id)) continue;
|
|
213
|
+
let docScore = 0;
|
|
214
|
+
for (const qTerm of qTokens) {
|
|
215
|
+
let tf = 0;
|
|
216
|
+
for (const t of doc.tokens) {
|
|
217
|
+
if (t === qTerm) tf++;
|
|
218
|
+
}
|
|
219
|
+
if (tf === 0) continue;
|
|
220
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
221
|
+
if (dfVal === 0) continue;
|
|
222
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
223
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
224
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
225
|
+
docScore += idf * tfComponent;
|
|
226
|
+
}
|
|
227
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
getDoc(id) {
|
|
232
|
+
return this.documents.find((d) => d.id === id);
|
|
233
|
+
}
|
|
234
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
235
|
+
const doc = this.getDoc(docId);
|
|
236
|
+
if (!doc) return "";
|
|
237
|
+
for (const tok of queryTokens) {
|
|
238
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
239
|
+
if (idx !== -1) {
|
|
240
|
+
const start = Math.max(0, idx - radius);
|
|
241
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
242
|
+
const excerpt = doc.raw.slice(start, end);
|
|
243
|
+
const ellipsis = "\u2026";
|
|
244
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
78
251
|
// src/codebase-index/writer.ts
|
|
79
252
|
var DB_FILE = "index.db";
|
|
80
253
|
function resolveIndexDir(projectRoot, override) {
|
|
@@ -110,15 +283,79 @@ function loadDatabaseSync() {
|
|
|
110
283
|
}
|
|
111
284
|
return DatabaseSyncCtor;
|
|
112
285
|
}
|
|
286
|
+
var MAX_LOCK_RETRIES = 3;
|
|
287
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
288
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
289
|
+
function isLockError(err) {
|
|
290
|
+
if (!(err instanceof Error)) return false;
|
|
291
|
+
const e = err;
|
|
292
|
+
const code = e.code ?? e.sqliteCode;
|
|
293
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
294
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
295
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
function sleepSync(ms) {
|
|
299
|
+
try {
|
|
300
|
+
const sab = new SharedArrayBuffer(4);
|
|
301
|
+
const view = new Int32Array(sab);
|
|
302
|
+
Atomics.wait(view, 0, 0, ms);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
113
306
|
var IndexStore = class {
|
|
114
307
|
db;
|
|
115
308
|
/** Absolute path to this project's index directory. */
|
|
116
309
|
indexDir;
|
|
310
|
+
/**
|
|
311
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
312
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
313
|
+
*/
|
|
314
|
+
ftsAvailable = false;
|
|
315
|
+
/**
|
|
316
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
317
|
+
*
|
|
318
|
+
* When another wstack process is holding the write lock the statement first
|
|
319
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
320
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
321
|
+
* giving the competing writer time to finish and release the lock.
|
|
322
|
+
*
|
|
323
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
324
|
+
* returned to the caller on success.
|
|
325
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
326
|
+
* (non-lock errors always propagate on the first attempt).
|
|
327
|
+
*/
|
|
328
|
+
runWithRetry(fn) {
|
|
329
|
+
let lastError;
|
|
330
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
331
|
+
try {
|
|
332
|
+
return fn();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
lastError = err;
|
|
335
|
+
if (!isLockError(err)) throw err;
|
|
336
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
337
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
338
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
339
|
+
}
|
|
340
|
+
const delay = Math.min(
|
|
341
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
342
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
343
|
+
);
|
|
344
|
+
sleepSync(delay);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw lastError;
|
|
348
|
+
}
|
|
117
349
|
constructor(projectRoot, opts = {}) {
|
|
118
350
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
119
351
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
120
352
|
const Database = loadDatabaseSync();
|
|
121
353
|
this.db = new Database(path4.join(this.indexDir, DB_FILE));
|
|
354
|
+
try {
|
|
355
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
356
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
122
359
|
this.initSchema();
|
|
123
360
|
}
|
|
124
361
|
initSchema() {
|
|
@@ -127,6 +364,21 @@ var IndexStore = class {
|
|
|
127
364
|
key TEXT PRIMARY KEY,
|
|
128
365
|
value TEXT NOT NULL
|
|
129
366
|
);
|
|
367
|
+
`);
|
|
368
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
369
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
370
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
371
|
+
this.db.exec(`
|
|
372
|
+
DROP TABLE IF EXISTS symbols;
|
|
373
|
+
DROP TABLE IF EXISTS files;
|
|
374
|
+
DROP TABLE IF EXISTS refs;
|
|
375
|
+
`);
|
|
376
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
377
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
378
|
+
} else if (storedVersion === null) {
|
|
379
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
380
|
+
}
|
|
381
|
+
this.db.exec(`
|
|
130
382
|
CREATE TABLE IF NOT EXISTS files (
|
|
131
383
|
file TEXT PRIMARY KEY,
|
|
132
384
|
lang TEXT NOT NULL,
|
|
@@ -167,53 +419,76 @@ var IndexStore = class {
|
|
|
167
419
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
168
420
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
169
421
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.
|
|
422
|
+
try {
|
|
423
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
424
|
+
this.ftsAvailable = true;
|
|
425
|
+
} catch {
|
|
426
|
+
this.ftsAvailable = false;
|
|
173
427
|
}
|
|
174
428
|
}
|
|
175
429
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
176
430
|
insertSymbols(symbols, nextId) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let id = nextId;
|
|
182
|
-
for (const s of symbols) {
|
|
183
|
-
stmt.run(
|
|
184
|
-
id++,
|
|
185
|
-
s.lang,
|
|
186
|
-
s.kind,
|
|
187
|
-
s.name,
|
|
188
|
-
s.file,
|
|
189
|
-
s.line,
|
|
190
|
-
s.col,
|
|
191
|
-
s.signature,
|
|
192
|
-
s.docComment,
|
|
193
|
-
s.scope,
|
|
194
|
-
s.text,
|
|
195
|
-
s.file
|
|
431
|
+
return this.runWithRetry(() => {
|
|
432
|
+
const stmt = this.db.prepare(
|
|
433
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
434
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
196
435
|
);
|
|
197
|
-
|
|
198
|
-
|
|
436
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
437
|
+
let id = nextId;
|
|
438
|
+
for (const s of symbols) {
|
|
439
|
+
stmt.run(
|
|
440
|
+
id,
|
|
441
|
+
s.lang,
|
|
442
|
+
s.kind,
|
|
443
|
+
s.name,
|
|
444
|
+
s.file,
|
|
445
|
+
s.line,
|
|
446
|
+
s.col,
|
|
447
|
+
s.signature,
|
|
448
|
+
s.docComment,
|
|
449
|
+
s.scope,
|
|
450
|
+
s.text,
|
|
451
|
+
s.file
|
|
452
|
+
);
|
|
453
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
454
|
+
id++;
|
|
455
|
+
}
|
|
456
|
+
return id;
|
|
457
|
+
});
|
|
199
458
|
}
|
|
200
459
|
deleteSymbolsForFile(file) {
|
|
201
|
-
this.
|
|
460
|
+
this.runWithRetry(() => {
|
|
461
|
+
if (this.ftsAvailable) {
|
|
462
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
463
|
+
}
|
|
464
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
465
|
+
});
|
|
202
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
469
|
+
* when a source file disappears between index runs — previously this only
|
|
470
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
471
|
+
*/
|
|
203
472
|
deleteFile(file) {
|
|
204
|
-
this.
|
|
473
|
+
this.runWithRetry(() => {
|
|
474
|
+
this.deleteRefsForFile(file);
|
|
475
|
+
this.deleteSymbolsForFile(file);
|
|
476
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
477
|
+
});
|
|
205
478
|
}
|
|
206
479
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
207
480
|
upsertFile(meta) {
|
|
208
|
-
this.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
481
|
+
this.runWithRetry(() => {
|
|
482
|
+
this.db.prepare(
|
|
483
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
484
|
+
VALUES (?, ?, ?, ?, ?)
|
|
485
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
486
|
+
lang = excluded.lang,
|
|
487
|
+
mtime_ms = excluded.mtime_ms,
|
|
488
|
+
symbol_count = excluded.symbol_count,
|
|
489
|
+
last_indexed = excluded.last_indexed`
|
|
490
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
491
|
+
});
|
|
217
492
|
}
|
|
218
493
|
getFileMeta(file) {
|
|
219
494
|
const rows = this.db.prepare(
|
|
@@ -280,6 +555,94 @@ var IndexStore = class {
|
|
|
280
555
|
lspKind: filter?.lspKind
|
|
281
556
|
}));
|
|
282
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
560
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
561
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
562
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
563
|
+
*
|
|
564
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
565
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
566
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
567
|
+
*/
|
|
568
|
+
searchRanked(query, filter, limit) {
|
|
569
|
+
const tokens = tokenise(query);
|
|
570
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
571
|
+
return this.searchRankedFallback(query, filter, limit);
|
|
572
|
+
}
|
|
573
|
+
let effectiveKind = filter?.kind;
|
|
574
|
+
if (filter?.lspKind !== void 0) {
|
|
575
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
576
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
577
|
+
effectiveKind = mapped;
|
|
578
|
+
}
|
|
579
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
580
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
581
|
+
const values = [match];
|
|
582
|
+
if (effectiveKind) {
|
|
583
|
+
conditions.push("s.kind = ?");
|
|
584
|
+
values.push(effectiveKind);
|
|
585
|
+
}
|
|
586
|
+
if (filter?.lang) {
|
|
587
|
+
conditions.push("s.lang = ?");
|
|
588
|
+
values.push(filter.lang);
|
|
589
|
+
}
|
|
590
|
+
if (filter?.file) {
|
|
591
|
+
conditions.push("s.file LIKE ?");
|
|
592
|
+
values.push(`%${filter.file}%`);
|
|
593
|
+
}
|
|
594
|
+
const where = conditions.join(" AND ");
|
|
595
|
+
const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
|
|
596
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
597
|
+
if (total === 0) return { results: [], total: 0 };
|
|
598
|
+
const rows = this.db.prepare(
|
|
599
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
600
|
+
-bm25(symbols_fts) AS score,
|
|
601
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
602
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
603
|
+
WHERE ${where}
|
|
604
|
+
ORDER BY bm25(symbols_fts)
|
|
605
|
+
LIMIT ?`
|
|
606
|
+
).all(...values, limit);
|
|
607
|
+
return {
|
|
608
|
+
results: rows.map((r) => ({
|
|
609
|
+
id: r.id,
|
|
610
|
+
lang: r.lang,
|
|
611
|
+
kind: r.kind,
|
|
612
|
+
name: r.name,
|
|
613
|
+
file: r.file,
|
|
614
|
+
line: r.line,
|
|
615
|
+
col: r.col,
|
|
616
|
+
signature: r.signature,
|
|
617
|
+
docComment: r.doc_comment,
|
|
618
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
619
|
+
// better" and clamp so a match never reports a zero score.
|
|
620
|
+
score: Math.max(1e-4, r.score),
|
|
621
|
+
snippet: r.snippet,
|
|
622
|
+
lspKind: filter?.lspKind
|
|
623
|
+
})),
|
|
624
|
+
total
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
628
|
+
searchRankedFallback(query, filter, limit) {
|
|
629
|
+
const candidates = this.search(query, filter);
|
|
630
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
631
|
+
if (!query.trim()) {
|
|
632
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
633
|
+
}
|
|
634
|
+
const bm25 = buildBm25Index(
|
|
635
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
636
|
+
);
|
|
637
|
+
const scored = bm25.score(query, (id) => candidates.some((c) => c.id === id));
|
|
638
|
+
scored.sort((a, b) => b.score - a.score);
|
|
639
|
+
const qTokens = tokenise(query);
|
|
640
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
641
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
642
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
643
|
+
});
|
|
644
|
+
return { results, total: candidates.length };
|
|
645
|
+
}
|
|
283
646
|
getAllIndexable() {
|
|
284
647
|
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
285
648
|
({ id, text }) => ({ id, text })
|
|
@@ -329,14 +692,19 @@ var IndexStore = class {
|
|
|
329
692
|
};
|
|
330
693
|
}
|
|
331
694
|
setLastIndexed(ts2) {
|
|
332
|
-
this.
|
|
333
|
-
|
|
334
|
-
|
|
695
|
+
this.runWithRetry(() => {
|
|
696
|
+
this.db.prepare(
|
|
697
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
698
|
+
).run(String(ts2));
|
|
699
|
+
});
|
|
335
700
|
}
|
|
336
701
|
clearAll() {
|
|
337
|
-
this.
|
|
338
|
-
|
|
339
|
-
|
|
702
|
+
this.runWithRetry(() => {
|
|
703
|
+
this.db.exec("DELETE FROM symbols");
|
|
704
|
+
this.db.exec("DELETE FROM files");
|
|
705
|
+
this.db.exec("DELETE FROM refs");
|
|
706
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
707
|
+
});
|
|
340
708
|
}
|
|
341
709
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
342
710
|
/**
|
|
@@ -344,46 +712,52 @@ var IndexStore = class {
|
|
|
344
712
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
345
713
|
*/
|
|
346
714
|
insertRefs(fromId, refs) {
|
|
347
|
-
this.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
715
|
+
this.runWithRetry(() => {
|
|
716
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
717
|
+
if (refs.length === 0) return;
|
|
718
|
+
const stmt = this.db.prepare(
|
|
719
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
720
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
721
|
+
);
|
|
722
|
+
for (const ref of refs) {
|
|
723
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
356
726
|
}
|
|
357
727
|
/**
|
|
358
728
|
* Delete all refs whose source symbols are in a given file.
|
|
359
729
|
* Used when re-indexing a file to clear stale refs.
|
|
360
730
|
*/
|
|
361
731
|
deleteRefsForFile(file) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
732
|
+
this.runWithRetry(() => {
|
|
733
|
+
const ids = this.db.prepare(
|
|
734
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
735
|
+
).all(file);
|
|
736
|
+
if (!ids.length) return;
|
|
737
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
738
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
739
|
+
});
|
|
368
740
|
}
|
|
369
741
|
/**
|
|
370
742
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
371
743
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
372
744
|
*/
|
|
373
745
|
resolveRefs() {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
746
|
+
return this.runWithRetry(() => {
|
|
747
|
+
const unresolved = this.db.prepare(
|
|
748
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
749
|
+
).all();
|
|
750
|
+
let resolved = 0;
|
|
751
|
+
for (const row of unresolved) {
|
|
752
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
753
|
+
const first = target[0];
|
|
754
|
+
if (first) {
|
|
755
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
756
|
+
resolved++;
|
|
757
|
+
}
|
|
384
758
|
}
|
|
385
|
-
|
|
386
|
-
|
|
759
|
+
return resolved;
|
|
760
|
+
});
|
|
387
761
|
}
|
|
388
762
|
/**
|
|
389
763
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -1144,7 +1518,7 @@ function parseSymbols4(opts) {
|
|
|
1144
1518
|
}
|
|
1145
1519
|
function checkNativeParser() {
|
|
1146
1520
|
try {
|
|
1147
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
1521
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
1148
1522
|
const toolsDir = path4.join(process.cwd(), "tools");
|
|
1149
1523
|
try {
|
|
1150
1524
|
execFileSync(
|
|
@@ -1157,7 +1531,7 @@ function checkNativeParser() {
|
|
|
1157
1531
|
"--manifest-path",
|
|
1158
1532
|
path4.join(toolsDir, "Cargo.toml")
|
|
1159
1533
|
],
|
|
1160
|
-
{ stdio: "pipe" }
|
|
1534
|
+
{ stdio: "pipe", windowsHide: true }
|
|
1161
1535
|
);
|
|
1162
1536
|
return true;
|
|
1163
1537
|
} catch {
|
|
@@ -1180,7 +1554,8 @@ function tryNativeParse(file, content) {
|
|
|
1180
1554
|
cwd: process.cwd(),
|
|
1181
1555
|
encoding: "utf8",
|
|
1182
1556
|
timeout: 15e3,
|
|
1183
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1557
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1558
|
+
windowsHide: true
|
|
1184
1559
|
}
|
|
1185
1560
|
);
|
|
1186
1561
|
if (result.status === 0 && result.stdout) {
|
|
@@ -1594,10 +1969,6 @@ function isScalar(value) {
|
|
|
1594
1969
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
1595
1970
|
return false;
|
|
1596
1971
|
}
|
|
1597
|
-
function truncate(s, max) {
|
|
1598
|
-
if (s.length <= max) return s;
|
|
1599
|
-
return s.slice(0, max) + "...";
|
|
1600
|
-
}
|
|
1601
1972
|
function makeSymbol2(opts) {
|
|
1602
1973
|
return {
|
|
1603
1974
|
id: 0,
|
|
@@ -1664,136 +2035,16 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
1664
2035
|
return compileGitignore(lines);
|
|
1665
2036
|
}
|
|
1666
2037
|
|
|
1667
|
-
// src/codebase-index/
|
|
1668
|
-
var
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
var _totalFiles = 0;
|
|
1672
|
-
var _lastError = null;
|
|
1673
|
-
function isIndexReady() {
|
|
1674
|
-
return _ready;
|
|
1675
|
-
}
|
|
1676
|
-
function setIndexReady() {
|
|
1677
|
-
_ready = true;
|
|
1678
|
-
}
|
|
1679
|
-
function isIndexing() {
|
|
1680
|
-
return _indexing;
|
|
1681
|
-
}
|
|
1682
|
-
function getIndexState() {
|
|
1683
|
-
return {
|
|
1684
|
-
ready: _ready,
|
|
1685
|
-
indexing: _indexing,
|
|
1686
|
-
currentFile: _currentFile,
|
|
1687
|
-
totalFiles: _totalFiles,
|
|
1688
|
-
lastError: _lastError
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
var _listeners = [];
|
|
1692
|
-
function onIndexStateChange(listener) {
|
|
1693
|
-
_listeners.push(listener);
|
|
1694
|
-
return () => {
|
|
1695
|
-
_listeners = _listeners.filter((l) => l !== listener);
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
function emitState() {
|
|
1699
|
-
const state = getIndexState();
|
|
1700
|
-
for (const l of _listeners) l(state);
|
|
1701
|
-
}
|
|
1702
|
-
function _setIndexProgress(current, total) {
|
|
1703
|
-
_currentFile = current;
|
|
1704
|
-
_totalFiles = total;
|
|
1705
|
-
emitState();
|
|
1706
|
-
}
|
|
1707
|
-
function stubCtx(projectRoot) {
|
|
1708
|
-
return {
|
|
1709
|
-
projectRoot,
|
|
1710
|
-
cwd: projectRoot,
|
|
1711
|
-
messages: [],
|
|
1712
|
-
todos: [],
|
|
1713
|
-
readFiles: /* @__PURE__ */ new Set(),
|
|
1714
|
-
fileMtimes: /* @__PURE__ */ new Map()
|
|
1715
|
-
};
|
|
2038
|
+
// src/codebase-index/indexer.ts
|
|
2039
|
+
var YIELD_EVERY_N = 50;
|
|
2040
|
+
function yieldEventLoop() {
|
|
2041
|
+
return new Promise((resolve2) => setImmediate(resolve2));
|
|
1716
2042
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
() => void 0
|
|
1723
|
-
);
|
|
1724
|
-
return run;
|
|
1725
|
-
}
|
|
1726
|
-
var DEFAULT_DEBOUNCE_MS = 400;
|
|
1727
|
-
var debounceTimers = /* @__PURE__ */ new Map();
|
|
1728
|
-
function debounceKey(indexDir, file) {
|
|
1729
|
-
return `${indexDir ?? ""}|${file}`;
|
|
1730
|
-
}
|
|
1731
|
-
function isIndexableFile(filePath) {
|
|
1732
|
-
return detectLang(filePath) !== null;
|
|
1733
|
-
}
|
|
1734
|
-
async function runStartupIndex(opts) {
|
|
1735
|
-
_indexing = true;
|
|
1736
|
-
emitState();
|
|
1737
|
-
try {
|
|
1738
|
-
const result = await withMutex(() => {
|
|
1739
|
-
_currentFile = 0;
|
|
1740
|
-
_totalFiles = 0;
|
|
1741
|
-
_lastError = null;
|
|
1742
|
-
return runIndexer(stubCtx(opts.projectRoot), {
|
|
1743
|
-
projectRoot: opts.projectRoot,
|
|
1744
|
-
indexDir: opts.indexDir,
|
|
1745
|
-
force: opts.force,
|
|
1746
|
-
signal: opts.signal
|
|
1747
|
-
});
|
|
1748
|
-
});
|
|
1749
|
-
_ready = true;
|
|
1750
|
-
return result;
|
|
1751
|
-
} catch (err) {
|
|
1752
|
-
_lastError = err instanceof Error ? err.message : String(err);
|
|
1753
|
-
_ready = true;
|
|
1754
|
-
throw err;
|
|
1755
|
-
} finally {
|
|
1756
|
-
_indexing = false;
|
|
1757
|
-
emitState();
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
function enqueueReindex(opts) {
|
|
1761
|
-
const files = opts.files.filter(isIndexableFile);
|
|
1762
|
-
if (files.length === 0) return;
|
|
1763
|
-
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1764
|
-
for (const file of files) {
|
|
1765
|
-
const key = debounceKey(opts.indexDir, file);
|
|
1766
|
-
const existing = debounceTimers.get(key);
|
|
1767
|
-
if (existing) clearTimeout(existing);
|
|
1768
|
-
const timer = setTimeout(() => {
|
|
1769
|
-
debounceTimers.delete(key);
|
|
1770
|
-
void withMutex(
|
|
1771
|
-
() => runIndexer(stubCtx(opts.projectRoot), {
|
|
1772
|
-
projectRoot: opts.projectRoot,
|
|
1773
|
-
files: [file],
|
|
1774
|
-
indexDir: opts.indexDir
|
|
1775
|
-
})
|
|
1776
|
-
).catch((err) => opts.onError?.(err));
|
|
1777
|
-
}, ms);
|
|
1778
|
-
timer.unref?.();
|
|
1779
|
-
debounceTimers.set(key, timer);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
function cancelPendingReindexes() {
|
|
1783
|
-
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
1784
|
-
debounceTimers.clear();
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// src/codebase-index/indexer.ts
|
|
1788
|
-
var YIELD_EVERY_N = 50;
|
|
1789
|
-
function yieldEventLoop() {
|
|
1790
|
-
return new Promise((resolve2) => setImmediate(resolve2));
|
|
1791
|
-
}
|
|
1792
|
-
function throwIfAborted(signal) {
|
|
1793
|
-
if (!signal?.aborted) return;
|
|
1794
|
-
if (signal.reason instanceof Error) throw signal.reason;
|
|
1795
|
-
throw new Error(
|
|
1796
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
2043
|
+
function throwIfAborted(signal) {
|
|
2044
|
+
if (!signal?.aborted) return;
|
|
2045
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
1797
2048
|
);
|
|
1798
2049
|
}
|
|
1799
2050
|
function isAbortError(err) {
|
|
@@ -1921,7 +2172,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
1921
2172
|
}
|
|
1922
2173
|
for (let fi = 0; fi < files.length; fi++) {
|
|
1923
2174
|
const file = expectDefined(files[fi]);
|
|
1924
|
-
|
|
2175
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
1925
2176
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
1926
2177
|
await yieldEventLoop();
|
|
1927
2178
|
throwIfAborted(signal);
|
|
@@ -2016,6 +2267,365 @@ async function runIndexerWithStore(store, opts) {
|
|
|
2016
2267
|
};
|
|
2017
2268
|
}
|
|
2018
2269
|
|
|
2270
|
+
// src/codebase-index/index-service.ts
|
|
2271
|
+
function stubCtx(projectRoot) {
|
|
2272
|
+
return {
|
|
2273
|
+
projectRoot,
|
|
2274
|
+
cwd: projectRoot,
|
|
2275
|
+
messages: [],
|
|
2276
|
+
todos: [],
|
|
2277
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
2278
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
async function indexService(args, hooks = {}) {
|
|
2282
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
2283
|
+
projectRoot: args.projectRoot,
|
|
2284
|
+
indexDir: args.indexDir,
|
|
2285
|
+
files: args.files,
|
|
2286
|
+
force: args.force,
|
|
2287
|
+
langs: args.langs,
|
|
2288
|
+
ignore: args.ignore,
|
|
2289
|
+
signal: hooks.signal,
|
|
2290
|
+
onProgress: hooks.onProgress
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
function searchService(args) {
|
|
2294
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
2295
|
+
try {
|
|
2296
|
+
return store.searchRanked(
|
|
2297
|
+
args.query,
|
|
2298
|
+
{
|
|
2299
|
+
kind: args.kind,
|
|
2300
|
+
lang: args.lang,
|
|
2301
|
+
file: args.file,
|
|
2302
|
+
lspKind: args.lspKind
|
|
2303
|
+
},
|
|
2304
|
+
args.limit
|
|
2305
|
+
);
|
|
2306
|
+
} finally {
|
|
2307
|
+
store.close();
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function statsService(args) {
|
|
2311
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
2312
|
+
try {
|
|
2313
|
+
return store.getStats();
|
|
2314
|
+
} finally {
|
|
2315
|
+
store.close();
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/codebase-index/background-indexer.ts
|
|
2320
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
2321
|
+
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 3e4;
|
|
2322
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
2323
|
+
var _ready = false;
|
|
2324
|
+
var _indexing = false;
|
|
2325
|
+
var _currentFile = 0;
|
|
2326
|
+
var _totalFiles = 0;
|
|
2327
|
+
var _lastError = null;
|
|
2328
|
+
function isIndexReady() {
|
|
2329
|
+
return _ready;
|
|
2330
|
+
}
|
|
2331
|
+
function isIndexing() {
|
|
2332
|
+
return _indexing;
|
|
2333
|
+
}
|
|
2334
|
+
function getIndexState() {
|
|
2335
|
+
return {
|
|
2336
|
+
ready: _ready,
|
|
2337
|
+
indexing: _indexing,
|
|
2338
|
+
currentFile: _currentFile,
|
|
2339
|
+
totalFiles: _totalFiles,
|
|
2340
|
+
lastError: _lastError,
|
|
2341
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
var _listeners = [];
|
|
2345
|
+
function onIndexStateChange(listener) {
|
|
2346
|
+
_listeners.push(listener);
|
|
2347
|
+
return () => {
|
|
2348
|
+
_listeners = _listeners.filter((l) => l !== listener);
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function emitState() {
|
|
2352
|
+
const state = getIndexState();
|
|
2353
|
+
for (const l of _listeners) l(state);
|
|
2354
|
+
}
|
|
2355
|
+
function setIndexProgress(current, total) {
|
|
2356
|
+
_currentFile = current;
|
|
2357
|
+
_totalFiles = total;
|
|
2358
|
+
emitState();
|
|
2359
|
+
}
|
|
2360
|
+
var worker = null;
|
|
2361
|
+
var workerUnavailable = false;
|
|
2362
|
+
var nextRpcId = 1;
|
|
2363
|
+
var pending = /* @__PURE__ */ new Map();
|
|
2364
|
+
function resolveWorkerUrl() {
|
|
2365
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
2366
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
2367
|
+
try {
|
|
2368
|
+
const url = new URL(rel, import.meta.url);
|
|
2369
|
+
if (url.protocol === "file:" && fs.existsSync(fileURLToPath(url))) return url;
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
function failAllPending(err) {
|
|
2376
|
+
const entries = [...pending.values()];
|
|
2377
|
+
pending.clear();
|
|
2378
|
+
for (const p of entries) p.reject(err);
|
|
2379
|
+
}
|
|
2380
|
+
function ensureWorker() {
|
|
2381
|
+
if (worker) return worker;
|
|
2382
|
+
if (workerUnavailable) return null;
|
|
2383
|
+
const url = resolveWorkerUrl();
|
|
2384
|
+
if (!url) {
|
|
2385
|
+
workerUnavailable = true;
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
try {
|
|
2389
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
2390
|
+
w.unref();
|
|
2391
|
+
w.on("message", (msg) => {
|
|
2392
|
+
if (msg.type === "progress") {
|
|
2393
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const entry = pending.get(msg.id);
|
|
2397
|
+
if (!entry) return;
|
|
2398
|
+
pending.delete(msg.id);
|
|
2399
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
2400
|
+
else entry.reject(new Error(msg.error));
|
|
2401
|
+
});
|
|
2402
|
+
w.on("error", (err) => {
|
|
2403
|
+
worker = null;
|
|
2404
|
+
failAllPending(err);
|
|
2405
|
+
});
|
|
2406
|
+
w.on("exit", () => {
|
|
2407
|
+
if (worker === w) worker = null;
|
|
2408
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
2409
|
+
});
|
|
2410
|
+
worker = w;
|
|
2411
|
+
return w;
|
|
2412
|
+
} catch {
|
|
2413
|
+
workerUnavailable = true;
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function terminateWorker(reason) {
|
|
2418
|
+
const w = worker;
|
|
2419
|
+
worker = null;
|
|
2420
|
+
failAllPending(reason);
|
|
2421
|
+
if (w) void w.terminate().catch(() => {
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
function shutdownCodebaseIndexHost() {
|
|
2425
|
+
cancelPendingReindexes();
|
|
2426
|
+
terminateWorker(new Error("codebase-index host shut down"));
|
|
2427
|
+
workerUnavailable = false;
|
|
2428
|
+
}
|
|
2429
|
+
function callIndexOp(op, args, opts) {
|
|
2430
|
+
const w = ensureWorker();
|
|
2431
|
+
if (!w) return callInline(op, args, opts);
|
|
2432
|
+
return new Promise((resolve2, reject) => {
|
|
2433
|
+
const id = nextRpcId++;
|
|
2434
|
+
const timer = setTimeout(() => {
|
|
2435
|
+
pending.delete(id);
|
|
2436
|
+
const err = new IndexTimeoutError(
|
|
2437
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
2438
|
+
);
|
|
2439
|
+
terminateWorker(err);
|
|
2440
|
+
reject(err);
|
|
2441
|
+
}, opts.timeoutMs);
|
|
2442
|
+
timer.unref?.();
|
|
2443
|
+
const onAbort = () => {
|
|
2444
|
+
w.postMessage({ type: "cancel", id });
|
|
2445
|
+
};
|
|
2446
|
+
if (opts.signal?.aborted) onAbort();
|
|
2447
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2448
|
+
const cleanup = () => {
|
|
2449
|
+
clearTimeout(timer);
|
|
2450
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
2451
|
+
};
|
|
2452
|
+
pending.set(id, {
|
|
2453
|
+
resolve: (v) => {
|
|
2454
|
+
cleanup();
|
|
2455
|
+
resolve2(v);
|
|
2456
|
+
},
|
|
2457
|
+
reject: (e) => {
|
|
2458
|
+
cleanup();
|
|
2459
|
+
reject(e);
|
|
2460
|
+
},
|
|
2461
|
+
onProgress: opts.onProgress
|
|
2462
|
+
});
|
|
2463
|
+
w.postMessage({ type: "request", id, op, args });
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async function callInline(op, args, opts) {
|
|
2467
|
+
const ac = new AbortController();
|
|
2468
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
2469
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
2470
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
2471
|
+
let timer;
|
|
2472
|
+
const watchdog = new Promise((_, reject) => {
|
|
2473
|
+
timer = setTimeout(() => {
|
|
2474
|
+
const err = new IndexTimeoutError(
|
|
2475
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
2476
|
+
);
|
|
2477
|
+
ac.abort(err);
|
|
2478
|
+
reject(err);
|
|
2479
|
+
}, opts.timeoutMs);
|
|
2480
|
+
timer.unref?.();
|
|
2481
|
+
});
|
|
2482
|
+
const job = async () => {
|
|
2483
|
+
switch (op) {
|
|
2484
|
+
case "index":
|
|
2485
|
+
return await indexService(args, {
|
|
2486
|
+
signal: ac.signal,
|
|
2487
|
+
onProgress: opts.onProgress
|
|
2488
|
+
});
|
|
2489
|
+
case "search":
|
|
2490
|
+
return searchService(args);
|
|
2491
|
+
case "stats":
|
|
2492
|
+
return statsService(args);
|
|
2493
|
+
default:
|
|
2494
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
try {
|
|
2498
|
+
return await Promise.race([job(), watchdog]);
|
|
2499
|
+
} finally {
|
|
2500
|
+
if (timer) clearTimeout(timer);
|
|
2501
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
var chain = Promise.resolve();
|
|
2505
|
+
function withMutex(job) {
|
|
2506
|
+
const run = chain.then(job, job);
|
|
2507
|
+
chain = run.then(
|
|
2508
|
+
() => void 0,
|
|
2509
|
+
() => void 0
|
|
2510
|
+
);
|
|
2511
|
+
return run;
|
|
2512
|
+
}
|
|
2513
|
+
function circuitOpenError() {
|
|
2514
|
+
const c = indexCircuitBreaker.snapshot();
|
|
2515
|
+
return new CircuitOpenError(
|
|
2516
|
+
"Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
var DEFAULT_DEBOUNCE_MS = 400;
|
|
2520
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
2521
|
+
function debounceKey(indexDir, file) {
|
|
2522
|
+
return `${indexDir ?? ""}|${file}`;
|
|
2523
|
+
}
|
|
2524
|
+
function isIndexableFile(filePath) {
|
|
2525
|
+
return detectLang(filePath) !== null;
|
|
2526
|
+
}
|
|
2527
|
+
function isUniqueConstraintError(err) {
|
|
2528
|
+
if (err instanceof Error) {
|
|
2529
|
+
const msg = err.message.toLowerCase();
|
|
2530
|
+
return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
|
|
2531
|
+
}
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
async function runStartupIndex(opts) {
|
|
2535
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
2536
|
+
_indexing = true;
|
|
2537
|
+
emitState();
|
|
2538
|
+
try {
|
|
2539
|
+
const result = await withMutex(() => {
|
|
2540
|
+
_currentFile = 0;
|
|
2541
|
+
_totalFiles = 0;
|
|
2542
|
+
_lastError = null;
|
|
2543
|
+
return callIndexOp(
|
|
2544
|
+
"index",
|
|
2545
|
+
{
|
|
2546
|
+
projectRoot: opts.projectRoot,
|
|
2547
|
+
indexDir: opts.indexDir,
|
|
2548
|
+
force: opts.force,
|
|
2549
|
+
langs: opts.langs
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
2553
|
+
signal: opts.signal,
|
|
2554
|
+
onProgress: setIndexProgress
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
});
|
|
2558
|
+
_ready = true;
|
|
2559
|
+
indexCircuitBreaker.recordSuccess();
|
|
2560
|
+
return result;
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
_lastError = err instanceof Error ? err.message : String(err);
|
|
2563
|
+
if (isUniqueConstraintError(err) && !opts.force) {
|
|
2564
|
+
_lastError = null;
|
|
2565
|
+
const rebuildResult = await runStartupIndex({
|
|
2566
|
+
...opts,
|
|
2567
|
+
force: true
|
|
2568
|
+
});
|
|
2569
|
+
_ready = true;
|
|
2570
|
+
return rebuildResult;
|
|
2571
|
+
}
|
|
2572
|
+
_ready = true;
|
|
2573
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
2574
|
+
throw err;
|
|
2575
|
+
} finally {
|
|
2576
|
+
_indexing = false;
|
|
2577
|
+
emitState();
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function enqueueReindex(opts) {
|
|
2581
|
+
const files = opts.files.filter(isIndexableFile);
|
|
2582
|
+
if (files.length === 0) return;
|
|
2583
|
+
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
2584
|
+
for (const file of files) {
|
|
2585
|
+
const key = debounceKey(opts.indexDir, file);
|
|
2586
|
+
const existing = debounceTimers.get(key);
|
|
2587
|
+
if (existing) clearTimeout(existing);
|
|
2588
|
+
const timer = setTimeout(() => {
|
|
2589
|
+
debounceTimers.delete(key);
|
|
2590
|
+
if (!indexCircuitBreaker.allowRequest()) {
|
|
2591
|
+
opts.onError?.(circuitOpenError());
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
void withMutex(
|
|
2595
|
+
() => callIndexOp(
|
|
2596
|
+
"index",
|
|
2597
|
+
{ projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
|
|
2598
|
+
{ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
|
|
2599
|
+
)
|
|
2600
|
+
).then(
|
|
2601
|
+
() => indexCircuitBreaker.recordSuccess(),
|
|
2602
|
+
(err) => {
|
|
2603
|
+
indexCircuitBreaker.recordFailure(err);
|
|
2604
|
+
opts.onError?.(err);
|
|
2605
|
+
}
|
|
2606
|
+
);
|
|
2607
|
+
}, ms);
|
|
2608
|
+
timer.unref?.();
|
|
2609
|
+
debounceTimers.set(key, timer);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
function cancelPendingReindexes() {
|
|
2613
|
+
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
2614
|
+
debounceTimers.clear();
|
|
2615
|
+
}
|
|
2616
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
2617
|
+
return callIndexOp("search", args, {
|
|
2618
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
2619
|
+
signal: opts.signal
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
2623
|
+
return callIndexOp("stats", args, {
|
|
2624
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
2625
|
+
signal: opts.signal
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2019
2629
|
// src/codebase-index/codebase-index-tool.ts
|
|
2020
2630
|
var codebaseIndexTool = {
|
|
2021
2631
|
name: "codebase-index",
|
|
@@ -2051,103 +2661,24 @@ var codebaseIndexTool = {
|
|
|
2051
2661
|
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
2052
2662
|
};
|
|
2053
2663
|
}
|
|
2054
|
-
const
|
|
2664
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
2665
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
2666
|
+
return {
|
|
2667
|
+
filesIndexed: 0,
|
|
2668
|
+
symbolsIndexed: 0,
|
|
2669
|
+
langStats: {},
|
|
2670
|
+
durationMs: 0,
|
|
2671
|
+
errors: [],
|
|
2672
|
+
note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
return await runStartupIndex({
|
|
2055
2676
|
projectRoot: ctx.projectRoot,
|
|
2056
2677
|
force: input.force ?? false,
|
|
2057
2678
|
langs: input.langs,
|
|
2058
2679
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
2059
2680
|
signal: execOpts?.signal
|
|
2060
2681
|
});
|
|
2061
|
-
setIndexReady();
|
|
2062
|
-
return result;
|
|
2063
|
-
}
|
|
2064
|
-
};
|
|
2065
|
-
|
|
2066
|
-
// src/codebase-index/bm25.ts
|
|
2067
|
-
var K1 = 1.5;
|
|
2068
|
-
var B = 0.75;
|
|
2069
|
-
function tokenise(text) {
|
|
2070
|
-
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
2071
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
2072
|
-
}
|
|
2073
|
-
function splitName(name) {
|
|
2074
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
2075
|
-
}
|
|
2076
|
-
function buildIndexableText(name, signature, docComment) {
|
|
2077
|
-
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
2078
|
-
}
|
|
2079
|
-
function buildBm25Index(docs) {
|
|
2080
|
-
const documents = docs.map((d) => {
|
|
2081
|
-
const tokens = tokenise(d.text);
|
|
2082
|
-
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
2083
|
-
});
|
|
2084
|
-
const df = {};
|
|
2085
|
-
for (const doc of documents) {
|
|
2086
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2087
|
-
for (const t of doc.tokens) {
|
|
2088
|
-
if (!seen.has(t)) {
|
|
2089
|
-
df[t] = (df[t] ?? 0) + 1;
|
|
2090
|
-
seen.add(t);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
const N = documents.length;
|
|
2095
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
2096
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
2097
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
2098
|
-
}
|
|
2099
|
-
var Bm25Index = class {
|
|
2100
|
-
constructor(documents, df, N, avgLen) {
|
|
2101
|
-
this.documents = documents;
|
|
2102
|
-
this.df = df;
|
|
2103
|
-
this.N = N;
|
|
2104
|
-
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
2105
|
-
}
|
|
2106
|
-
documents;
|
|
2107
|
-
df;
|
|
2108
|
-
N;
|
|
2109
|
-
safeAvgLen;
|
|
2110
|
-
score(query, filter) {
|
|
2111
|
-
const qTokens = tokenise(query);
|
|
2112
|
-
if (qTokens.length === 0) return [];
|
|
2113
|
-
const results = [];
|
|
2114
|
-
for (const doc of this.documents) {
|
|
2115
|
-
if (filter && !filter(doc.id)) continue;
|
|
2116
|
-
let docScore = 0;
|
|
2117
|
-
for (const qTerm of qTokens) {
|
|
2118
|
-
let tf = 0;
|
|
2119
|
-
for (const t of doc.tokens) {
|
|
2120
|
-
if (t === qTerm) tf++;
|
|
2121
|
-
}
|
|
2122
|
-
if (tf === 0) continue;
|
|
2123
|
-
const dfVal = this.df[qTerm] ?? 0;
|
|
2124
|
-
if (dfVal === 0) continue;
|
|
2125
|
-
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
2126
|
-
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
2127
|
-
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
2128
|
-
docScore += idf * tfComponent;
|
|
2129
|
-
}
|
|
2130
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
2131
|
-
}
|
|
2132
|
-
return results;
|
|
2133
|
-
}
|
|
2134
|
-
getDoc(id) {
|
|
2135
|
-
return this.documents.find((d) => d.id === id);
|
|
2136
|
-
}
|
|
2137
|
-
extractSnippet(docId, queryTokens, radius = 40) {
|
|
2138
|
-
const doc = this.getDoc(docId);
|
|
2139
|
-
if (!doc) return "";
|
|
2140
|
-
for (const tok of queryTokens) {
|
|
2141
|
-
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
2142
|
-
if (idx !== -1) {
|
|
2143
|
-
const start = Math.max(0, idx - radius);
|
|
2144
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
2145
|
-
const excerpt = doc.raw.slice(start, end);
|
|
2146
|
-
const ellipsis = "\u2026";
|
|
2147
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
2151
2682
|
}
|
|
2152
2683
|
};
|
|
2153
2684
|
|
|
@@ -2193,7 +2724,7 @@ var codebaseSearchTool = {
|
|
|
2193
2724
|
},
|
|
2194
2725
|
required: ["query"]
|
|
2195
2726
|
},
|
|
2196
|
-
async execute(input, ctx) {
|
|
2727
|
+
async execute(input, ctx, execOpts) {
|
|
2197
2728
|
const state = getIndexState();
|
|
2198
2729
|
if (!state.ready) {
|
|
2199
2730
|
return {
|
|
@@ -2212,51 +2743,30 @@ var codebaseSearchTool = {
|
|
|
2212
2743
|
};
|
|
2213
2744
|
}
|
|
2214
2745
|
if (state.lastError) {
|
|
2746
|
+
const circuit = state.circuit;
|
|
2747
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
2215
2748
|
return {
|
|
2216
2749
|
results: [],
|
|
2217
2750
|
total: 0,
|
|
2218
2751
|
query: input.query,
|
|
2219
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
2752
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
2220
2753
|
};
|
|
2221
2754
|
}
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2755
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
2756
|
+
const { results, total } = await searchCodebaseIndex(
|
|
2757
|
+
{
|
|
2758
|
+
projectRoot: ctx.projectRoot,
|
|
2759
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
2760
|
+
query: input.query,
|
|
2226
2761
|
kind: input.kind,
|
|
2227
2762
|
lang: input.lang,
|
|
2228
2763
|
file: input.file,
|
|
2229
|
-
lspKind: input.lspKind
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
id: c.id,
|
|
2236
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
2237
|
-
}));
|
|
2238
|
-
const bm25 = buildBm25Index(indexable);
|
|
2239
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
2240
|
-
scored.sort((a, b) => b.score - a.score);
|
|
2241
|
-
const top = scored.slice(0, limit);
|
|
2242
|
-
const qTokens = tokenise(input.query);
|
|
2243
|
-
const results = top.map(({ id, score }) => {
|
|
2244
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
2245
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
2246
|
-
return {
|
|
2247
|
-
...c,
|
|
2248
|
-
score,
|
|
2249
|
-
snippet
|
|
2250
|
-
};
|
|
2251
|
-
});
|
|
2252
|
-
return {
|
|
2253
|
-
results,
|
|
2254
|
-
total: candidates.length,
|
|
2255
|
-
query: input.query
|
|
2256
|
-
};
|
|
2257
|
-
} finally {
|
|
2258
|
-
store.close();
|
|
2259
|
-
}
|
|
2764
|
+
lspKind: input.lspKind,
|
|
2765
|
+
limit
|
|
2766
|
+
},
|
|
2767
|
+
{ signal: execOpts?.signal }
|
|
2768
|
+
);
|
|
2769
|
+
return { results, total, query: input.query };
|
|
2260
2770
|
}
|
|
2261
2771
|
};
|
|
2262
2772
|
|
|
@@ -2275,7 +2785,7 @@ var codebaseStatsTool = {
|
|
|
2275
2785
|
properties: {},
|
|
2276
2786
|
additionalProperties: false
|
|
2277
2787
|
},
|
|
2278
|
-
async execute(_input, ctx) {
|
|
2788
|
+
async execute(_input, ctx, execOpts) {
|
|
2279
2789
|
const idxState = getIndexState();
|
|
2280
2790
|
if (!idxState.ready) {
|
|
2281
2791
|
return {
|
|
@@ -2290,37 +2800,33 @@ var codebaseStatsTool = {
|
|
|
2290
2800
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
2291
2801
|
};
|
|
2292
2802
|
}
|
|
2803
|
+
const stats = await codebaseIndexStats(
|
|
2804
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
2805
|
+
{ signal: execOpts?.signal }
|
|
2806
|
+
);
|
|
2293
2807
|
if (idxState.indexing) {
|
|
2294
|
-
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
2295
|
-
try {
|
|
2296
|
-
const stats = store2.getStats();
|
|
2297
|
-
return {
|
|
2298
|
-
...stats,
|
|
2299
|
-
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
2300
|
-
};
|
|
2301
|
-
} finally {
|
|
2302
|
-
store2.close();
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
2306
|
-
try {
|
|
2307
|
-
const stats = store.getStats();
|
|
2308
2808
|
return {
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
byLang: stats.byLang,
|
|
2312
|
-
byKind: stats.byKind,
|
|
2313
|
-
lastIndexed: stats.lastIndexed,
|
|
2314
|
-
sizeBytes: stats.sizeBytes,
|
|
2315
|
-
indexPath: stats.indexPath,
|
|
2316
|
-
version: stats.version
|
|
2809
|
+
...stats,
|
|
2810
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
2317
2811
|
};
|
|
2318
|
-
} finally {
|
|
2319
|
-
store.close();
|
|
2320
2812
|
}
|
|
2813
|
+
const circuit = idxState.circuit;
|
|
2814
|
+
return {
|
|
2815
|
+
totalSymbols: stats.totalSymbols,
|
|
2816
|
+
totalFiles: stats.totalFiles,
|
|
2817
|
+
byLang: stats.byLang,
|
|
2818
|
+
byKind: stats.byKind,
|
|
2819
|
+
lastIndexed: stats.lastIndexed,
|
|
2820
|
+
sizeBytes: stats.sizeBytes,
|
|
2821
|
+
indexPath: stats.indexPath,
|
|
2822
|
+
version: stats.version,
|
|
2823
|
+
...circuit.state === "open" ? {
|
|
2824
|
+
indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
|
|
2825
|
+
} : {}
|
|
2826
|
+
};
|
|
2321
2827
|
}
|
|
2322
2828
|
};
|
|
2323
2829
|
|
|
2324
|
-
export { IndexStore, SCHEMA_VERSION, buildBm25Index, buildIndexableText, cancelPendingReindexes, codebaseIndexDirOverride, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, enqueueReindex, getIndexState, internalKindToLspKind, isIndexReady, isIndexableFile, isIndexing, lspKindToInternalKind, onIndexStateChange, resolveIndexDir, runIndexer, runStartupIndex, tokenise };
|
|
2830
|
+
export { CircuitOpenError, IndexCircuitBreaker, IndexStore, IndexTimeoutError, SCHEMA_VERSION, buildBm25Index, buildIndexableText, cancelPendingReindexes, codebaseIndexDirOverride, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, enqueueReindex, getIndexState, indexCircuitBreaker, internalKindToLspKind, isIndexReady, isIndexableFile, isIndexing, lspKindToInternalKind, onIndexStateChange, resetIndexCircuitBreaker, resolveIndexDir, runIndexer, runStartupIndex, searchCodebaseIndex, shutdownCodebaseIndexHost, tokenise };
|
|
2325
2831
|
//# sourceMappingURL=index.js.map
|
|
2326
2832
|
//# sourceMappingURL=index.js.map
|