@wrongstack/tools 0.313.0 → 0.313.1
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 +10 -4055
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33949
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,4412 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFilesContent
|
|
3
|
+
} from "./chunk-QHVW4AAM.js";
|
|
4
|
+
import {
|
|
5
|
+
INDEXABLE_EXTENSIONS,
|
|
6
|
+
LANG_FAMILY_ENTRIES,
|
|
7
|
+
detectLang,
|
|
8
|
+
languageFamily
|
|
9
|
+
} from "./chunk-ILESNGNM.js";
|
|
10
|
+
import {
|
|
11
|
+
loadGitignoreMatcher
|
|
12
|
+
} from "./chunk-LIJQMYYX.js";
|
|
13
|
+
|
|
14
|
+
// src/codebase-index/circuit-breaker.ts
|
|
15
|
+
var CircuitOpenError = class extends Error {
|
|
16
|
+
name = "CircuitOpenError";
|
|
17
|
+
};
|
|
18
|
+
var IndexTimeoutError = class extends Error {
|
|
19
|
+
name = "IndexTimeoutError";
|
|
20
|
+
};
|
|
21
|
+
var LockError = class extends Error {
|
|
22
|
+
name = "LockError";
|
|
23
|
+
};
|
|
24
|
+
var IndexCircuitBreaker = class {
|
|
25
|
+
failureThreshold;
|
|
26
|
+
cooldownMs;
|
|
27
|
+
now;
|
|
28
|
+
state = "closed";
|
|
29
|
+
consecutiveFailures = 0;
|
|
30
|
+
openedAt = 0;
|
|
31
|
+
lastFailure = null;
|
|
32
|
+
probeInFlight = false;
|
|
33
|
+
constructor(opts = {}) {
|
|
34
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
35
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
36
|
+
this.now = opts.now ?? Date.now;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
40
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
41
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
42
|
+
*/
|
|
43
|
+
allowRequest() {
|
|
44
|
+
if (this.state === "closed") return true;
|
|
45
|
+
if (this.state === "open") {
|
|
46
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
47
|
+
this.state = "half-open";
|
|
48
|
+
this.probeInFlight = true;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (this.probeInFlight) return false;
|
|
52
|
+
this.probeInFlight = true;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
recordSuccess() {
|
|
56
|
+
this.state = "closed";
|
|
57
|
+
this.consecutiveFailures = 0;
|
|
58
|
+
this.lastFailure = null;
|
|
59
|
+
this.probeInFlight = false;
|
|
60
|
+
}
|
|
61
|
+
recordFailure(err) {
|
|
62
|
+
if (err instanceof LockError) {
|
|
63
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
64
|
+
this.probeInFlight = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
68
|
+
this.probeInFlight = false;
|
|
69
|
+
this.consecutiveFailures++;
|
|
70
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
71
|
+
this.state = "open";
|
|
72
|
+
this.openedAt = this.now();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
76
|
+
reset() {
|
|
77
|
+
this.state = "closed";
|
|
78
|
+
this.consecutiveFailures = 0;
|
|
79
|
+
this.lastFailure = null;
|
|
80
|
+
this.probeInFlight = false;
|
|
81
|
+
this.openedAt = 0;
|
|
82
|
+
}
|
|
83
|
+
snapshot() {
|
|
84
|
+
return {
|
|
85
|
+
state: this.state,
|
|
86
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
87
|
+
lastFailure: this.lastFailure,
|
|
88
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
93
|
+
function resetIndexCircuitBreaker() {
|
|
94
|
+
indexCircuitBreaker.reset();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/codebase-index/bm25.ts
|
|
98
|
+
var K1 = 1.5;
|
|
99
|
+
var B = 0.75;
|
|
100
|
+
var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
|
|
101
|
+
function tokenise(text) {
|
|
102
|
+
return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
function splitName(name) {
|
|
105
|
+
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();
|
|
106
|
+
}
|
|
107
|
+
function buildIndexableText(name, signature, docComment) {
|
|
108
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
109
|
+
}
|
|
110
|
+
function buildBm25Index(docs) {
|
|
111
|
+
const documents = docs.map((d) => {
|
|
112
|
+
const tokens = tokenise(d.text);
|
|
113
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
114
|
+
});
|
|
115
|
+
const N = documents.length;
|
|
116
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
117
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
118
|
+
return new Bm25Index(documents, N, avgLen);
|
|
119
|
+
}
|
|
120
|
+
var Bm25Index = class {
|
|
121
|
+
constructor(documents, N, avgLen) {
|
|
122
|
+
this.documents = documents;
|
|
123
|
+
this.N = N;
|
|
124
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
125
|
+
}
|
|
126
|
+
documents;
|
|
127
|
+
N;
|
|
128
|
+
safeAvgLen;
|
|
129
|
+
/** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
|
|
130
|
+
_byId;
|
|
131
|
+
score(query, filter) {
|
|
132
|
+
const qTokens = tokenise(query);
|
|
133
|
+
if (qTokens.length === 0) return [];
|
|
134
|
+
const dfByTerm = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const qTerm of qTokens) {
|
|
136
|
+
let dfVal = 0;
|
|
137
|
+
for (const candidate of this.documents) {
|
|
138
|
+
if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
|
|
139
|
+
}
|
|
140
|
+
dfByTerm.set(qTerm, dfVal);
|
|
141
|
+
}
|
|
142
|
+
const results = [];
|
|
143
|
+
for (const doc of this.documents) {
|
|
144
|
+
if (filter && !filter(doc.id)) continue;
|
|
145
|
+
let docScore = 0;
|
|
146
|
+
for (const qTerm of qTokens) {
|
|
147
|
+
let tf = 0;
|
|
148
|
+
for (const t of doc.tokens) {
|
|
149
|
+
if (t.startsWith(qTerm)) tf++;
|
|
150
|
+
}
|
|
151
|
+
if (tf === 0) continue;
|
|
152
|
+
const dfVal = dfByTerm.get(qTerm) ?? 0;
|
|
153
|
+
if (dfVal === 0) continue;
|
|
154
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
155
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
156
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
157
|
+
docScore += idf * tfComponent;
|
|
158
|
+
}
|
|
159
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
160
|
+
}
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
getDoc(id) {
|
|
164
|
+
if (!this._byId) {
|
|
165
|
+
this._byId = new Map(this.documents.map((d) => [d.id, d]));
|
|
166
|
+
}
|
|
167
|
+
return this._byId.get(id);
|
|
168
|
+
}
|
|
169
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
170
|
+
const doc = this.getDoc(docId);
|
|
171
|
+
if (!doc) return "";
|
|
172
|
+
for (const tok of queryTokens) {
|
|
173
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
174
|
+
if (idx !== -1) {
|
|
175
|
+
const start = Math.max(0, idx - radius);
|
|
176
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
177
|
+
const excerpt = doc.raw.slice(start, end);
|
|
178
|
+
const ellipsis = "\u2026";
|
|
179
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/codebase-index/schema.ts
|
|
187
|
+
var SCHEMA_VERSION = 5;
|
|
188
|
+
|
|
189
|
+
// src/codebase-index/writer-helpers.ts
|
|
190
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
191
|
+
function escapeLike(value) {
|
|
192
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
193
|
+
}
|
|
194
|
+
function ladderChunkSizes(total, max) {
|
|
195
|
+
if (total <= 0) return [];
|
|
196
|
+
const sizes = [];
|
|
197
|
+
let remaining = total;
|
|
198
|
+
while (remaining > 0) {
|
|
199
|
+
const cap = Math.min(remaining, max);
|
|
200
|
+
const pow = cap >= 1 ? 2 ** Math.floor(Math.log2(cap)) : 1;
|
|
201
|
+
const take = Math.max(1, Math.min(pow, remaining));
|
|
202
|
+
sizes.push(take);
|
|
203
|
+
remaining -= take;
|
|
204
|
+
}
|
|
205
|
+
return sizes;
|
|
206
|
+
}
|
|
207
|
+
function nextPow2(count) {
|
|
208
|
+
return count <= 1 ? 1 : 2 ** Math.ceil(Math.log2(count));
|
|
209
|
+
}
|
|
210
|
+
function padToInBucket(values) {
|
|
211
|
+
if (values.length <= 1) return values.slice();
|
|
212
|
+
const target = nextPow2(values.length);
|
|
213
|
+
const padded = values.slice();
|
|
214
|
+
while (padded.length < target) padded.push(padded[0]);
|
|
215
|
+
return padded;
|
|
216
|
+
}
|
|
217
|
+
function placeholders(count) {
|
|
218
|
+
return Array.from({ length: count }, () => "?").join(",");
|
|
219
|
+
}
|
|
220
|
+
function inListChunks(total, max) {
|
|
221
|
+
if (total <= 0) return [];
|
|
222
|
+
if (nextPow2(total) <= max) return [total];
|
|
223
|
+
const powMax = Math.max(1, 2 ** Math.floor(Math.log2(max)));
|
|
224
|
+
return ladderChunkSizes(total, powMax);
|
|
225
|
+
}
|
|
226
|
+
function posixIndexPath(file) {
|
|
227
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
228
|
+
}
|
|
229
|
+
function indexedFileMatchSql(column = "file") {
|
|
230
|
+
return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
|
|
231
|
+
}
|
|
232
|
+
function indexedFileMatchArgs(file) {
|
|
233
|
+
const posix4 = posixIndexPath(file.trim());
|
|
234
|
+
return [file, posix4, `%/${escapeLike(posix4)}`];
|
|
235
|
+
}
|
|
236
|
+
function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
|
|
237
|
+
if (packageLabel === filter) return true;
|
|
238
|
+
const posixFile = posixIndexPath(storedFile);
|
|
239
|
+
const posixFilter = posixIndexPath(filter.trim());
|
|
240
|
+
if (!posixFilter) return false;
|
|
241
|
+
return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
|
|
242
|
+
}
|
|
243
|
+
function assignRefsToSymbols(refs, symbols) {
|
|
244
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
245
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
const assigned = [];
|
|
248
|
+
for (const ref of refs) {
|
|
249
|
+
let owner;
|
|
250
|
+
for (const symbol of ordered) {
|
|
251
|
+
if (symbol.line > ref.line) break;
|
|
252
|
+
owner = symbol;
|
|
253
|
+
}
|
|
254
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
255
|
+
if (!owner || owner.id <= 0) continue;
|
|
256
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
257
|
+
if (seen.has(key)) continue;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
260
|
+
}
|
|
261
|
+
return assigned;
|
|
262
|
+
}
|
|
263
|
+
function resolveIndexDir(projectRoot, override) {
|
|
264
|
+
return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
|
|
265
|
+
}
|
|
266
|
+
function codebaseIndexDirOverride(ctx) {
|
|
267
|
+
const v = ctx.meta?.["codebaseIndexDir"];
|
|
268
|
+
return typeof v === "string" ? v : void 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/codebase-index/lsp-kind.ts
|
|
272
|
+
function lspKindToInternalKind(k) {
|
|
273
|
+
switch (k) {
|
|
274
|
+
case 5 /* Class */:
|
|
275
|
+
return "class";
|
|
276
|
+
case 6 /* Method */:
|
|
277
|
+
return "method";
|
|
278
|
+
case 7 /* Property */:
|
|
279
|
+
case 8 /* Field */:
|
|
280
|
+
return "property";
|
|
281
|
+
case 9 /* Constructor */:
|
|
282
|
+
return "class";
|
|
283
|
+
case 10 /* Enum */:
|
|
284
|
+
return "enum";
|
|
285
|
+
case 11 /* Interface */:
|
|
286
|
+
return "interface";
|
|
287
|
+
case 12 /* Function */:
|
|
288
|
+
return "function";
|
|
289
|
+
case 13 /* Variable */:
|
|
290
|
+
return "var";
|
|
291
|
+
case 14 /* Constant */:
|
|
292
|
+
return "const";
|
|
293
|
+
case 22 /* EnumMember */:
|
|
294
|
+
return "enum";
|
|
295
|
+
case 26 /* TypeParameter */:
|
|
296
|
+
return "type";
|
|
297
|
+
case 3 /* Namespace */:
|
|
298
|
+
return "namespace";
|
|
299
|
+
default:
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function internalKindToLspKind(k) {
|
|
304
|
+
switch (k) {
|
|
305
|
+
case "class":
|
|
306
|
+
return 5 /* Class */;
|
|
307
|
+
case "method":
|
|
308
|
+
return 6 /* Method */;
|
|
309
|
+
case "property":
|
|
310
|
+
return 7 /* Property */;
|
|
311
|
+
case "function":
|
|
312
|
+
return 12 /* Function */;
|
|
313
|
+
case "var":
|
|
314
|
+
return 13 /* Variable */;
|
|
315
|
+
case "const":
|
|
316
|
+
return 14 /* Constant */;
|
|
317
|
+
case "let":
|
|
318
|
+
return 13 /* Variable */;
|
|
319
|
+
case "enum":
|
|
320
|
+
return 10 /* Enum */;
|
|
321
|
+
case "interface":
|
|
322
|
+
return 11 /* Interface */;
|
|
323
|
+
case "namespace":
|
|
324
|
+
return 3 /* Namespace */;
|
|
325
|
+
case "type":
|
|
326
|
+
return 26 /* TypeParameter */;
|
|
327
|
+
// parameter and other internal-only kinds have no LSP equivalent
|
|
328
|
+
default:
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/codebase-index/writer.ts
|
|
334
|
+
import * as fs3 from "node:fs";
|
|
335
|
+
import * as path3 from "node:path";
|
|
336
|
+
|
|
337
|
+
// src/codebase-index/sqlite-runtime.ts
|
|
338
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
339
|
+
import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
|
|
340
|
+
var warningSilenced = false;
|
|
341
|
+
function silenceSqliteExperimentalWarning() {
|
|
342
|
+
if (warningSilenced) return;
|
|
343
|
+
warningSilenced = true;
|
|
344
|
+
const original = process.emitWarning.bind(process);
|
|
345
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
346
|
+
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
347
|
+
const name = typeof warning === "string" ? String(rest[0] ?? "") : warning?.name ?? "";
|
|
348
|
+
if (/sqlite/i.test(msg) && /experimental/i.test(`${name} ${msg}`)) return;
|
|
349
|
+
original(warning, ...rest);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
var DatabaseSyncCtor;
|
|
353
|
+
function loadDatabaseSync() {
|
|
354
|
+
if (DatabaseSyncCtor) return DatabaseSyncCtor;
|
|
355
|
+
silenceSqliteExperimentalWarning();
|
|
356
|
+
try {
|
|
357
|
+
DatabaseSyncCtor = loadRuntimeDatabaseSync();
|
|
358
|
+
} catch (err) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`The codebase index needs node:sqlite (Node >= 22.5) or bun:sqlite. This runtime doesn't provide it: ${toErrorMessage(err)}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return DatabaseSyncCtor;
|
|
364
|
+
}
|
|
365
|
+
var MAX_LOCK_RETRIES = 3;
|
|
366
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
367
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
368
|
+
function isLockError(err) {
|
|
369
|
+
if (!(err instanceof Error)) return false;
|
|
370
|
+
const e = err;
|
|
371
|
+
const code = e.code ?? e.sqliteCode;
|
|
372
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
373
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
374
|
+
return /SQLITE_(BUSY|LOCKED)/.test(err.message);
|
|
375
|
+
}
|
|
376
|
+
function sleepSync(ms) {
|
|
377
|
+
try {
|
|
378
|
+
const sab = new SharedArrayBuffer(4);
|
|
379
|
+
const view = new Int32Array(sab);
|
|
380
|
+
Atomics.wait(view, 0, 0, ms);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function runSqliteWithRetry(fn) {
|
|
385
|
+
let lastError;
|
|
386
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
387
|
+
try {
|
|
388
|
+
return fn();
|
|
389
|
+
} catch (err) {
|
|
390
|
+
lastError = err;
|
|
391
|
+
if (!isLockError(err)) throw err;
|
|
392
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
393
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
394
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
395
|
+
}
|
|
396
|
+
const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
|
|
397
|
+
sleepSync(delay);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw lastError;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/codebase-index/vector-search.ts
|
|
404
|
+
var RRF_K = 60;
|
|
405
|
+
function vectorEmbeddingEnabled() {
|
|
406
|
+
return process.env["WRONGSTACK_INDEX_VECTORS"] === "1";
|
|
407
|
+
}
|
|
408
|
+
var VECTOR_DIMENSIONS = 384;
|
|
409
|
+
var NGRAM_SIZE = 3;
|
|
410
|
+
function embedText(text) {
|
|
411
|
+
const vec = new Float32Array(VECTOR_DIMENSIONS);
|
|
412
|
+
const normalized = text.toLowerCase().trim();
|
|
413
|
+
if (normalized.length < NGRAM_SIZE) {
|
|
414
|
+
const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
|
|
415
|
+
for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
|
|
416
|
+
const ngram = padded.slice(i, i + NGRAM_SIZE);
|
|
417
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
418
|
+
vec[bucket] += 1;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
|
|
422
|
+
const ngram = normalized.slice(i, i + NGRAM_SIZE);
|
|
423
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
424
|
+
vec[bucket] += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
let norm = 0;
|
|
428
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
429
|
+
norm += vec[i] * vec[i];
|
|
430
|
+
}
|
|
431
|
+
norm = Math.sqrt(norm);
|
|
432
|
+
if (norm > 0) {
|
|
433
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
434
|
+
vec[i] /= norm;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return vec;
|
|
438
|
+
}
|
|
439
|
+
function hashNgram(str) {
|
|
440
|
+
let hash = 2166136261;
|
|
441
|
+
for (let i = 0; i < str.length; i++) {
|
|
442
|
+
hash ^= str.charCodeAt(i);
|
|
443
|
+
hash = Math.imul(hash, 16777619);
|
|
444
|
+
}
|
|
445
|
+
return hash >>> 0;
|
|
446
|
+
}
|
|
447
|
+
function cosineSimilarity(a, b) {
|
|
448
|
+
let dot = 0;
|
|
449
|
+
const len = Math.min(a.length, b.length);
|
|
450
|
+
for (let i = 0; i < len; i++) {
|
|
451
|
+
dot += a[i] * b[i];
|
|
452
|
+
}
|
|
453
|
+
return dot;
|
|
454
|
+
}
|
|
455
|
+
function encodeVector(vec) {
|
|
456
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
457
|
+
}
|
|
458
|
+
function decodeVector(buf) {
|
|
459
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
460
|
+
const copy = new Float32Array(buf.byteLength / 4);
|
|
461
|
+
for (let i = 0; i < copy.length; i++) {
|
|
462
|
+
copy[i] = view.getFloat32(i * 4, true);
|
|
463
|
+
}
|
|
464
|
+
return copy;
|
|
465
|
+
}
|
|
466
|
+
function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
|
|
467
|
+
const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
|
|
468
|
+
const scored = [];
|
|
469
|
+
for (const id of allIds) {
|
|
470
|
+
const bm25Rank = bm25Ranks.get(id);
|
|
471
|
+
const vecRank = vectorRanks.get(id);
|
|
472
|
+
let score = 0;
|
|
473
|
+
if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
|
|
474
|
+
if (vecRank !== void 0) score += 1 / (k + vecRank);
|
|
475
|
+
scored.push([id, score]);
|
|
476
|
+
}
|
|
477
|
+
scored.sort((a, b) => b[1] - a[1]);
|
|
478
|
+
return scored;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/codebase-index/writer-admin.ts
|
|
482
|
+
import * as fs from "node:fs";
|
|
483
|
+
import * as path from "node:path";
|
|
484
|
+
var DB_FILE = "index.db";
|
|
485
|
+
function getAllIndexableWithStatement(stmt) {
|
|
486
|
+
return stmt("SELECT id, text FROM symbols").all().map(
|
|
487
|
+
({ id, text }) => ({ id, text })
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function getMaxSymbolIdWithStatement(stmt) {
|
|
491
|
+
const rows = stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
492
|
+
return rows[0]?.m ?? 0;
|
|
493
|
+
}
|
|
494
|
+
function getStatsWithStatement(stmt, indexDir) {
|
|
495
|
+
const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
|
|
496
|
+
const totalRows = stmt("SELECT COUNT(*) FROM symbols").all();
|
|
497
|
+
const fileRows = stmt("SELECT COUNT(*) FROM files").all();
|
|
498
|
+
const langRows = stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
|
|
499
|
+
const kindRows = stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
|
|
500
|
+
const byLang = {};
|
|
501
|
+
for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
|
|
502
|
+
const byKind = {};
|
|
503
|
+
for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
|
|
504
|
+
return {
|
|
505
|
+
totalSymbols: totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0,
|
|
506
|
+
totalFiles: fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0,
|
|
507
|
+
byLang,
|
|
508
|
+
byKind,
|
|
509
|
+
indexPath: indexDir,
|
|
510
|
+
lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null,
|
|
511
|
+
sizeBytes: getIndexDbSizeBytes(indexDir),
|
|
512
|
+
version: SCHEMA_VERSION
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function getMetadataWithStatement(stmt, key) {
|
|
516
|
+
const rows = stmt("SELECT value FROM metadata WHERE key = ?").all(key);
|
|
517
|
+
return rows[0]?.value;
|
|
518
|
+
}
|
|
519
|
+
function getIndexSummaryWithStatement(stmt) {
|
|
520
|
+
const fileRows = stmt("SELECT COUNT(*) AS n FROM files").all();
|
|
521
|
+
const lastRows = stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
|
|
522
|
+
return {
|
|
523
|
+
totalFiles: fileRows[0] ? Number(fileRows[0].n) : 0,
|
|
524
|
+
lastIndexed: lastRows.length ? Number(lastRows[0]?.value) : null
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function getFileMetaWithStatement(stmt, file) {
|
|
528
|
+
const rows = stmt(
|
|
529
|
+
"SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files WHERE file = ?"
|
|
530
|
+
).all(file);
|
|
531
|
+
const r = rows[0];
|
|
532
|
+
if (!r) return null;
|
|
533
|
+
return {
|
|
534
|
+
file: r.file,
|
|
535
|
+
lang: r.lang,
|
|
536
|
+
mtimeMs: r.mtime_ms,
|
|
537
|
+
symbolCount: r.symbol_count,
|
|
538
|
+
lastIndexed: r.last_indexed,
|
|
539
|
+
contentHash: r.content_hash
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function getAllFileMetasWithStatement(stmt) {
|
|
543
|
+
return stmt(
|
|
544
|
+
"SELECT file, lang, mtime_ms, symbol_count, last_indexed, content_hash FROM files"
|
|
545
|
+
).all().map((r) => ({
|
|
546
|
+
file: r.file,
|
|
547
|
+
lang: r.lang,
|
|
548
|
+
mtimeMs: r.mtime_ms,
|
|
549
|
+
symbolCount: r.symbol_count,
|
|
550
|
+
lastIndexed: r.last_indexed,
|
|
551
|
+
contentHash: r.content_hash
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
function getIndexDbSizeBytes(indexDir) {
|
|
555
|
+
try {
|
|
556
|
+
return fs.statSync(path.join(indexDir, DB_FILE)).size;
|
|
557
|
+
} catch {
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/codebase-index/writer-bulk-insert.ts
|
|
563
|
+
function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
|
|
564
|
+
if (rows.length === 0) return;
|
|
565
|
+
const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 12)));
|
|
566
|
+
let cursor = 0;
|
|
567
|
+
for (const take of ladder) {
|
|
568
|
+
const chunk = rows.slice(cursor, cursor + take);
|
|
569
|
+
cursor += take;
|
|
570
|
+
const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
571
|
+
const insert = stmt(
|
|
572
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text)
|
|
573
|
+
VALUES ${placeholders2}`
|
|
574
|
+
);
|
|
575
|
+
const binds = [];
|
|
576
|
+
for (const r of chunk) {
|
|
577
|
+
binds.push(
|
|
578
|
+
r.id,
|
|
579
|
+
r.lang,
|
|
580
|
+
r.kind,
|
|
581
|
+
r.name,
|
|
582
|
+
r.file,
|
|
583
|
+
r.line,
|
|
584
|
+
r.col,
|
|
585
|
+
r.signature,
|
|
586
|
+
r.docComment,
|
|
587
|
+
r.scope,
|
|
588
|
+
r.text
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
insert.run(...binds);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function bulkInsertFtsWithStatement(stmt, maxSqlVars, ftsAvailable, rows) {
|
|
595
|
+
if (!ftsAvailable || rows.length === 0) return;
|
|
596
|
+
const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
|
|
597
|
+
let cursor = 0;
|
|
598
|
+
for (const take of ladder) {
|
|
599
|
+
const chunk = rows.slice(cursor, cursor + take);
|
|
600
|
+
cursor += take;
|
|
601
|
+
const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
|
|
602
|
+
const insert = stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders2}`);
|
|
603
|
+
const binds = [];
|
|
604
|
+
for (const r of chunk) binds.push(r.id, r.text);
|
|
605
|
+
insert.run(...binds);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function bulkInsertVectorsWithStatement(stmt, maxSqlVars, rows) {
|
|
609
|
+
if (rows.length === 0) return;
|
|
610
|
+
const ladder = ladderChunkSizes(rows.length, Math.max(1, Math.floor(maxSqlVars / 2)));
|
|
611
|
+
let cursor = 0;
|
|
612
|
+
for (const take of ladder) {
|
|
613
|
+
const chunk = rows.slice(cursor, cursor + take);
|
|
614
|
+
cursor += take;
|
|
615
|
+
const placeholders2 = chunk.map(() => "(?, ?)").join(", ");
|
|
616
|
+
const insert = stmt(`INSERT INTO symbol_vectors(symbol_id, vector) VALUES ${placeholders2}`);
|
|
617
|
+
const binds = [];
|
|
618
|
+
for (const r of chunk) binds.push(r.id, r.vector);
|
|
619
|
+
insert.run(...binds);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
|
|
623
|
+
if (refs.length === 0) return;
|
|
624
|
+
const ladder = ladderChunkSizes(refs.length, Math.max(1, Math.floor(maxSqlVars / 8)));
|
|
625
|
+
let cursor = 0;
|
|
626
|
+
for (const take of ladder) {
|
|
627
|
+
const chunk = refs.slice(cursor, cursor + take);
|
|
628
|
+
cursor += take;
|
|
629
|
+
const placeholders2 = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
630
|
+
const insert = stmt(
|
|
631
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
|
|
632
|
+
VALUES ${placeholders2}`
|
|
633
|
+
);
|
|
634
|
+
const binds = [];
|
|
635
|
+
for (const ref of chunk) {
|
|
636
|
+
binds.push(
|
|
637
|
+
ref.fromId,
|
|
638
|
+
ref.toName,
|
|
639
|
+
ref.toId ?? null,
|
|
640
|
+
ref.callType,
|
|
641
|
+
ref.line,
|
|
642
|
+
ref.lang ?? "",
|
|
643
|
+
ref.module ?? null,
|
|
644
|
+
ref.toFile ?? null
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
insert.run(...binds);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/codebase-index/module-roots.ts
|
|
652
|
+
import * as fs2 from "node:fs/promises";
|
|
653
|
+
import * as path2 from "node:path";
|
|
654
|
+
function toPortablePath(file) {
|
|
655
|
+
return file.replace(/\\/g, "/");
|
|
656
|
+
}
|
|
657
|
+
async function readTextIfPresent(file) {
|
|
658
|
+
try {
|
|
659
|
+
return await fs2.readFile(file, "utf8");
|
|
660
|
+
} catch {
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function parsePackageJsonName(source) {
|
|
665
|
+
try {
|
|
666
|
+
const parsed = JSON.parse(source);
|
|
667
|
+
return typeof parsed.name === "string" && parsed.name ? parsed.name : void 0;
|
|
668
|
+
} catch {
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function parseGoModulePath(source) {
|
|
673
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
674
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
675
|
+
const match = /^module\s+(\S+)/.exec(line);
|
|
676
|
+
if (match?.[1]) return match[1].replace(/^["']|["']$/g, "");
|
|
677
|
+
}
|
|
678
|
+
return void 0;
|
|
679
|
+
}
|
|
680
|
+
function parseTomlTableName(source, tables) {
|
|
681
|
+
let current = "";
|
|
682
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
683
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
684
|
+
if (line.startsWith("[[")) {
|
|
685
|
+
current = "\0";
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const table = /^\[([^\]]+)\]$/.exec(line);
|
|
689
|
+
if (table?.[1]) {
|
|
690
|
+
current = table[1].trim();
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (!tables.includes(current)) continue;
|
|
694
|
+
const match = /^name\s*=\s*["']([^"']+)["']/.exec(line);
|
|
695
|
+
if (match?.[1]) return match[1];
|
|
696
|
+
}
|
|
697
|
+
return void 0;
|
|
698
|
+
}
|
|
699
|
+
function parsePomArtifactId(source) {
|
|
700
|
+
const withoutParent = source.replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
701
|
+
return /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(withoutParent)?.[1];
|
|
702
|
+
}
|
|
703
|
+
var LANGS_BY_KIND = {
|
|
704
|
+
npm: ["ts", "tsx", "js", "jsx", "vue", "svelte"],
|
|
705
|
+
cargo: ["rs"],
|
|
706
|
+
go: ["go"],
|
|
707
|
+
python: ["py"],
|
|
708
|
+
maven: ["java", "kotlin", "scala"],
|
|
709
|
+
gradle: ["java", "kotlin", "scala"],
|
|
710
|
+
dotnet: ["csharp"]
|
|
711
|
+
};
|
|
712
|
+
function ancestorsOf(dir, stopAt) {
|
|
713
|
+
const out = [];
|
|
714
|
+
let current = dir;
|
|
715
|
+
for (; ; ) {
|
|
716
|
+
out.push(current);
|
|
717
|
+
if (current === stopAt || current.length <= stopAt.length) break;
|
|
718
|
+
const parent = path2.posix.dirname(current);
|
|
719
|
+
if (parent === current) break;
|
|
720
|
+
current = parent;
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
var MARKER_PROBES = [
|
|
725
|
+
{
|
|
726
|
+
kind: "npm",
|
|
727
|
+
file: "package.json",
|
|
728
|
+
build: (dir, source) => {
|
|
729
|
+
const name = parsePackageJsonName(source) ?? path2.posix.basename(dir);
|
|
730
|
+
return { name, importPath: name, sourceRoots: [dir] };
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
kind: "cargo",
|
|
735
|
+
file: "Cargo.toml",
|
|
736
|
+
build: (dir, source) => {
|
|
737
|
+
const name = parseTomlTableName(source, ["package"]);
|
|
738
|
+
if (!name) return void 0;
|
|
739
|
+
return {
|
|
740
|
+
name: `crate:${name}`,
|
|
741
|
+
// Rust paths use underscores where crate names often use dashes.
|
|
742
|
+
importPath: name.replace(/-/g, "_"),
|
|
743
|
+
sourceRoots: [path2.posix.join(dir, "src")]
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
kind: "go",
|
|
749
|
+
file: "go.mod",
|
|
750
|
+
build: (dir, source) => {
|
|
751
|
+
const modulePath = parseGoModulePath(source);
|
|
752
|
+
if (!modulePath) return void 0;
|
|
753
|
+
return { name: `go:${modulePath}`, importPath: modulePath, sourceRoots: [dir] };
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
kind: "python",
|
|
758
|
+
file: "pyproject.toml",
|
|
759
|
+
build: (dir, source) => {
|
|
760
|
+
const name = parseTomlTableName(source, ["project", "tool.poetry"]) ?? path2.posix.basename(dir);
|
|
761
|
+
return {
|
|
762
|
+
name: `py:${name}`,
|
|
763
|
+
importPath: void 0,
|
|
764
|
+
// `src/` layout is the packaging-guide default; the root itself covers
|
|
765
|
+
// the flat layout. Both are probed, missing ones simply never match.
|
|
766
|
+
sourceRoots: [path2.posix.join(dir, "src"), dir]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
kind: "python",
|
|
772
|
+
file: "setup.py",
|
|
773
|
+
build: (dir) => ({
|
|
774
|
+
name: `py:${path2.posix.basename(dir)}`,
|
|
775
|
+
importPath: void 0,
|
|
776
|
+
sourceRoots: [path2.posix.join(dir, "src"), dir]
|
|
777
|
+
})
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
kind: "maven",
|
|
781
|
+
file: "pom.xml",
|
|
782
|
+
build: (dir, source) => {
|
|
783
|
+
const artifactId = parsePomArtifactId(source) ?? path2.posix.basename(dir);
|
|
784
|
+
return {
|
|
785
|
+
name: `mvn:${artifactId}`,
|
|
786
|
+
importPath: void 0,
|
|
787
|
+
sourceRoots: [
|
|
788
|
+
path2.posix.join(dir, "src/main/java"),
|
|
789
|
+
path2.posix.join(dir, "src/main/kotlin"),
|
|
790
|
+
path2.posix.join(dir, "src/main/scala"),
|
|
791
|
+
path2.posix.join(dir, "src/test/java")
|
|
792
|
+
]
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
kind: "gradle",
|
|
798
|
+
file: "build.gradle",
|
|
799
|
+
build: (dir) => buildGradleRoot(dir)
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
kind: "gradle",
|
|
803
|
+
file: "build.gradle.kts",
|
|
804
|
+
build: (dir) => buildGradleRoot(dir)
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
function buildGradleRoot(dir) {
|
|
808
|
+
return {
|
|
809
|
+
name: `gradle:${path2.posix.basename(dir)}`,
|
|
810
|
+
importPath: void 0,
|
|
811
|
+
sourceRoots: [
|
|
812
|
+
path2.posix.join(dir, "src/main/java"),
|
|
813
|
+
path2.posix.join(dir, "src/main/kotlin"),
|
|
814
|
+
path2.posix.join(dir, "src/main/scala")
|
|
815
|
+
]
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
async function probeDotnetRoot(dir) {
|
|
819
|
+
let entries;
|
|
820
|
+
try {
|
|
821
|
+
entries = await fs2.readdir(dir);
|
|
822
|
+
} catch {
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
825
|
+
const project = entries.find((entry) => entry.toLowerCase().endsWith(".csproj"));
|
|
826
|
+
if (!project) return void 0;
|
|
827
|
+
const name = project.slice(0, -".csproj".length);
|
|
828
|
+
return {
|
|
829
|
+
dir,
|
|
830
|
+
kind: "dotnet",
|
|
831
|
+
name: `csproj:${name}`,
|
|
832
|
+
importPath: void 0,
|
|
833
|
+
sourceRoots: [dir]
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
async function detectModuleRoots(projectRoot, files) {
|
|
837
|
+
const root = toPortablePath(projectRoot).replace(/\/+$/, "");
|
|
838
|
+
const langsByDir = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const file of files) {
|
|
840
|
+
const portable = toPortablePath(file);
|
|
841
|
+
const lang = detectLang(portable);
|
|
842
|
+
if (!lang) continue;
|
|
843
|
+
const dir = path2.posix.dirname(portable);
|
|
844
|
+
let langs = langsByDir.get(dir);
|
|
845
|
+
if (!langs) {
|
|
846
|
+
langs = /* @__PURE__ */ new Set();
|
|
847
|
+
langsByDir.set(dir, langs);
|
|
848
|
+
}
|
|
849
|
+
langs.add(lang);
|
|
850
|
+
}
|
|
851
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
852
|
+
for (const [dir, langs] of langsByDir) {
|
|
853
|
+
for (const ancestor of ancestorsOf(dir, root)) {
|
|
854
|
+
let merged = candidates.get(ancestor);
|
|
855
|
+
if (!merged) {
|
|
856
|
+
merged = /* @__PURE__ */ new Set();
|
|
857
|
+
candidates.set(ancestor, merged);
|
|
858
|
+
}
|
|
859
|
+
for (const lang of langs) merged.add(lang);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const roots = [];
|
|
863
|
+
await Promise.all(
|
|
864
|
+
[...candidates].map(async ([dir, langs]) => {
|
|
865
|
+
for (const probe of MARKER_PROBES) {
|
|
866
|
+
if (!LANGS_BY_KIND[probe.kind].some((lang) => langs.has(lang))) continue;
|
|
867
|
+
const source = await readTextIfPresent(path2.posix.join(dir, probe.file));
|
|
868
|
+
if (source === void 0) continue;
|
|
869
|
+
const built = probe.build(dir, source);
|
|
870
|
+
if (built) roots.push({ dir, kind: probe.kind, ...built });
|
|
871
|
+
}
|
|
872
|
+
if (LANGS_BY_KIND.dotnet.some((lang) => langs.has(lang))) {
|
|
873
|
+
const dotnet = await probeDotnetRoot(dir);
|
|
874
|
+
if (dotnet) roots.push(dotnet);
|
|
875
|
+
}
|
|
876
|
+
})
|
|
877
|
+
);
|
|
878
|
+
roots.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
879
|
+
return { projectRoot: root, roots };
|
|
880
|
+
}
|
|
881
|
+
function findOwningRoot(structure, file, kinds) {
|
|
882
|
+
const portable = toPortablePath(file);
|
|
883
|
+
for (const root of structure.roots) {
|
|
884
|
+
if (kinds && !kinds.includes(root.kind)) continue;
|
|
885
|
+
if (portable === root.dir || portable.startsWith(`${root.dir}/`)) return root;
|
|
886
|
+
}
|
|
887
|
+
return void 0;
|
|
888
|
+
}
|
|
889
|
+
function derivePackageFromLayout(filePath) {
|
|
890
|
+
const portable = toPortablePath(filePath);
|
|
891
|
+
const packagesIdx = portable.indexOf("/packages/");
|
|
892
|
+
if (packagesIdx !== -1) {
|
|
893
|
+
const segment = portable.slice(packagesIdx + "/packages/".length).split("/")[0];
|
|
894
|
+
if (segment) return `@wrongstack/${segment}`;
|
|
895
|
+
}
|
|
896
|
+
const appsIdx = portable.indexOf("/apps/");
|
|
897
|
+
if (appsIdx !== -1) {
|
|
898
|
+
const segment = portable.slice(appsIdx + "/apps/".length).split("/")[0];
|
|
899
|
+
if (segment) return `app:${segment}`;
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
}
|
|
903
|
+
function pythonPackageLabel(structure, file, initDirs) {
|
|
904
|
+
const portable = toPortablePath(file);
|
|
905
|
+
const dir = path2.posix.dirname(portable);
|
|
906
|
+
if (!initDirs.has(dir)) return void 0;
|
|
907
|
+
const segments = [];
|
|
908
|
+
let current = dir;
|
|
909
|
+
while (initDirs.has(current) && current.length > structure.projectRoot.length) {
|
|
910
|
+
segments.unshift(path2.posix.basename(current));
|
|
911
|
+
current = path2.posix.dirname(current);
|
|
912
|
+
}
|
|
913
|
+
return segments.length > 0 ? `py:${segments.join(".")}` : void 0;
|
|
914
|
+
}
|
|
915
|
+
function assignPackageLabels(structure, files) {
|
|
916
|
+
const initDirs = /* @__PURE__ */ new Set();
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
const portable = toPortablePath(file);
|
|
919
|
+
if (path2.posix.basename(portable) === "__init__.py") {
|
|
920
|
+
initDirs.add(path2.posix.dirname(portable));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const labels = /* @__PURE__ */ new Map();
|
|
924
|
+
for (const file of files) {
|
|
925
|
+
const portable = toPortablePath(file);
|
|
926
|
+
const lang = detectLang(portable);
|
|
927
|
+
if (lang === "go") {
|
|
928
|
+
const owner2 = findOwningRoot(structure, portable, ["go"]);
|
|
929
|
+
const dir = path2.posix.dirname(portable);
|
|
930
|
+
if (owner2?.importPath) {
|
|
931
|
+
const relative2 = path2.posix.relative(owner2.dir, dir);
|
|
932
|
+
labels.set(file, relative2 ? `${owner2.importPath}/${relative2}` : owner2.importPath);
|
|
933
|
+
} else {
|
|
934
|
+
labels.set(file, `go:${path2.posix.relative(structure.projectRoot, dir) || "."}`);
|
|
935
|
+
}
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (lang === "py") {
|
|
939
|
+
const dotted = pythonPackageLabel(structure, portable, initDirs);
|
|
940
|
+
if (dotted) {
|
|
941
|
+
labels.set(file, dotted);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const owner = findOwningRoot(structure, portable);
|
|
946
|
+
const label = owner?.name ?? derivePackageFromLayout(portable) ?? "(root)";
|
|
947
|
+
labels.set(file, label);
|
|
948
|
+
}
|
|
949
|
+
return labels;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/codebase-index/writer-graph-helpers.ts
|
|
953
|
+
function createPackageLabeller(stored) {
|
|
954
|
+
return (file) => stored.get(file) ?? derivePackageFromLayout(file) ?? "(root)";
|
|
955
|
+
}
|
|
956
|
+
function buildPackageGraphNodes(fileCounts, files, packageOf) {
|
|
957
|
+
const pkgNodes = /* @__PURE__ */ new Map();
|
|
958
|
+
const fileToPkg = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const { file, n } of fileCounts) {
|
|
960
|
+
const pkg = packageOf(file);
|
|
961
|
+
fileToPkg.set(file, pkg);
|
|
962
|
+
const node = pkgNodes.get(pkg);
|
|
963
|
+
if (node) {
|
|
964
|
+
node.symbolCount = (node.symbolCount ?? 0) + n;
|
|
965
|
+
} else {
|
|
966
|
+
pkgNodes.set(pkg, {
|
|
967
|
+
id: `pkg:${pkg}`,
|
|
968
|
+
label: pkg,
|
|
969
|
+
kind: "package",
|
|
970
|
+
package: pkg,
|
|
971
|
+
symbolCount: n,
|
|
972
|
+
fileCount: 0
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
for (const { file } of files) {
|
|
977
|
+
const pkg = packageOf(file);
|
|
978
|
+
fileToPkg.set(file, pkg);
|
|
979
|
+
const node = pkgNodes.get(pkg);
|
|
980
|
+
if (node) {
|
|
981
|
+
node.fileCount = (node.fileCount ?? 0) + 1;
|
|
982
|
+
} else {
|
|
983
|
+
pkgNodes.set(pkg, {
|
|
984
|
+
id: `pkg:${pkg}`,
|
|
985
|
+
label: pkg,
|
|
986
|
+
kind: "package",
|
|
987
|
+
package: pkg,
|
|
988
|
+
symbolCount: 0,
|
|
989
|
+
fileCount: 1
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return { pkgNodes, fileToPkg };
|
|
994
|
+
}
|
|
995
|
+
function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
996
|
+
const fileNodes = /* @__PURE__ */ new Map();
|
|
997
|
+
const symToFile = /* @__PURE__ */ new Map();
|
|
998
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
999
|
+
for (const s of pkgSyms) {
|
|
1000
|
+
symToFile.set(s.id, s.file);
|
|
1001
|
+
const current = fileStats.get(s.file);
|
|
1002
|
+
fileStats.set(s.file, {
|
|
1003
|
+
count: (current?.count ?? 0) + 1,
|
|
1004
|
+
lang: current?.lang ?? s.lang
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const ensureFileNode = (file) => {
|
|
1008
|
+
if (fileNodes.has(file)) return;
|
|
1009
|
+
const stats = fileStats.get(file);
|
|
1010
|
+
fileNodes.set(file, {
|
|
1011
|
+
id: `file:${file}`,
|
|
1012
|
+
label: file.replace(/\\/g, "/").split("/").pop() ?? file,
|
|
1013
|
+
kind: "file",
|
|
1014
|
+
package: packageOf(file),
|
|
1015
|
+
file,
|
|
1016
|
+
symbolCount: stats?.count ?? 0,
|
|
1017
|
+
lang: stats?.lang,
|
|
1018
|
+
external: !localFiles.has(file)
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
for (const file of localFiles) {
|
|
1022
|
+
ensureFileNode(file);
|
|
1023
|
+
}
|
|
1024
|
+
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
1025
|
+
}
|
|
1026
|
+
function buildSymbolGraphNodes(symById, relatedIds, localFiles, packageOf) {
|
|
1027
|
+
const local = new Set(
|
|
1028
|
+
[...typeof localFiles === "string" ? [localFiles] : localFiles].map(
|
|
1029
|
+
(file) => file.replace(/\\/g, "/")
|
|
1030
|
+
)
|
|
1031
|
+
);
|
|
1032
|
+
const isLocal = (file) => local.has(file.replace(/\\/g, "/"));
|
|
1033
|
+
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
1034
|
+
const aExternal = isLocal(a.file) ? 0 : 1;
|
|
1035
|
+
const bExternal = isLocal(b.file) ? 0 : 1;
|
|
1036
|
+
return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
|
|
1037
|
+
}).map((s) => ({
|
|
1038
|
+
id: `sym:${s.id}`,
|
|
1039
|
+
label: s.name,
|
|
1040
|
+
kind: "symbol",
|
|
1041
|
+
symbolId: s.id,
|
|
1042
|
+
symbolKind: s.kind,
|
|
1043
|
+
file: s.file,
|
|
1044
|
+
package: packageOf(s.file),
|
|
1045
|
+
lang: s.lang,
|
|
1046
|
+
line: s.line,
|
|
1047
|
+
signature: s.signature,
|
|
1048
|
+
scope: s.scope,
|
|
1049
|
+
external: !isLocal(s.file)
|
|
1050
|
+
}));
|
|
1051
|
+
}
|
|
1052
|
+
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
1053
|
+
const key = `${source}\0${target}`;
|
|
1054
|
+
let edge = edgeMap.get(key);
|
|
1055
|
+
if (!edge) {
|
|
1056
|
+
edge = { weight: 0, types: /* @__PURE__ */ new Map() };
|
|
1057
|
+
edgeMap.set(key, edge);
|
|
1058
|
+
}
|
|
1059
|
+
edge.weight += weight;
|
|
1060
|
+
edge.types.set(callType, (edge.types.get(callType) ?? 0) + weight);
|
|
1061
|
+
}
|
|
1062
|
+
function materializeWeightedEdges(edgeMap, idPrefix) {
|
|
1063
|
+
const edges = [];
|
|
1064
|
+
for (const [key, edge] of edgeMap) {
|
|
1065
|
+
const [source, target] = key.split("\0");
|
|
1066
|
+
let bestType = "call";
|
|
1067
|
+
let bestCount = 0;
|
|
1068
|
+
for (const [type, count] of edge.types) {
|
|
1069
|
+
if (count > bestCount) {
|
|
1070
|
+
bestType = type;
|
|
1071
|
+
bestCount = count;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
edges.push({
|
|
1075
|
+
source: `${idPrefix}:${source}`,
|
|
1076
|
+
target: `${idPrefix}:${target}`,
|
|
1077
|
+
weight: edge.weight,
|
|
1078
|
+
refType: bestType
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return edges;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/codebase-index/writer-ref-mapper.ts
|
|
1085
|
+
function mapWriterRefRow(row) {
|
|
1086
|
+
return {
|
|
1087
|
+
id: row.id,
|
|
1088
|
+
fromId: row.from_id,
|
|
1089
|
+
toName: row.to_name,
|
|
1090
|
+
toId: row.to_id ?? void 0,
|
|
1091
|
+
callType: row.call_type,
|
|
1092
|
+
line: row.line,
|
|
1093
|
+
// `lang`/`module`/`to_file` are absent from the narrower column lists some
|
|
1094
|
+
// queries select; `undefined` keeps those rows valid Refs.
|
|
1095
|
+
lang: row.lang || void 0,
|
|
1096
|
+
module: row.module ?? void 0,
|
|
1097
|
+
toFile: row.to_file ?? void 0
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/codebase-index/writer-graph-reader.ts
|
|
1102
|
+
var MAX_SQL_VARS = 900;
|
|
1103
|
+
function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
|
|
1104
|
+
const results = [];
|
|
1105
|
+
for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
|
|
1106
|
+
const chunk = padToInBucket(ids.slice(0, take));
|
|
1107
|
+
ids = ids.slice(take);
|
|
1108
|
+
const sql = buildSql(placeholders(chunk.length));
|
|
1109
|
+
results.push(...stmt(sql).all(...chunk, ...extraArgs));
|
|
1110
|
+
}
|
|
1111
|
+
return results;
|
|
1112
|
+
}
|
|
1113
|
+
function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
|
|
1114
|
+
let total = 0;
|
|
1115
|
+
for (const take of inListChunks(ids.length, MAX_SQL_VARS)) {
|
|
1116
|
+
const chunk = padToInBucket(ids.slice(0, take));
|
|
1117
|
+
ids = ids.slice(take);
|
|
1118
|
+
const sql = buildSql(placeholders(chunk.length));
|
|
1119
|
+
const rows = stmt(sql).all(...chunk, ...extraArgs);
|
|
1120
|
+
total += rows[0]?.n ?? 0;
|
|
1121
|
+
}
|
|
1122
|
+
return total;
|
|
1123
|
+
}
|
|
1124
|
+
function mapCallSiteRow(row) {
|
|
1125
|
+
return {
|
|
1126
|
+
symbol: {
|
|
1127
|
+
id: row.sym_id,
|
|
1128
|
+
name: row.sym_name,
|
|
1129
|
+
kind: row.sym_kind,
|
|
1130
|
+
lang: row.sym_lang,
|
|
1131
|
+
file: row.sym_file,
|
|
1132
|
+
line: row.sym_line,
|
|
1133
|
+
signature: row.sym_signature
|
|
1134
|
+
},
|
|
1135
|
+
callType: row.call_type,
|
|
1136
|
+
line: row.ref_line
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function resolveIndexedFiles(stmt, file) {
|
|
1140
|
+
const rows = stmt(
|
|
1141
|
+
`SELECT DISTINCT file FROM symbols WHERE ${indexedFileMatchSql("file")} ORDER BY length(file), file`
|
|
1142
|
+
).all(...indexedFileMatchArgs(file));
|
|
1143
|
+
return rows.map((row) => row.file);
|
|
1144
|
+
}
|
|
1145
|
+
function resolveSymbolIds(stmt, symbolName, file) {
|
|
1146
|
+
if (!file) {
|
|
1147
|
+
const rows = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(
|
|
1148
|
+
symbolName
|
|
1149
|
+
);
|
|
1150
|
+
return rows.map((r) => r.id);
|
|
1151
|
+
}
|
|
1152
|
+
const indexedFiles = resolveIndexedFiles(stmt, file);
|
|
1153
|
+
if (indexedFiles.length === 0) return [];
|
|
1154
|
+
const ids = [];
|
|
1155
|
+
let cursor = 0;
|
|
1156
|
+
for (const take of inListChunks(indexedFiles.length, MAX_SQL_VARS)) {
|
|
1157
|
+
const files = padToInBucket(indexedFiles.slice(cursor, cursor + take));
|
|
1158
|
+
cursor += take;
|
|
1159
|
+
const rows = stmt(
|
|
1160
|
+
`SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders(files.length)}) ORDER BY id`
|
|
1161
|
+
).all(symbolName, ...files);
|
|
1162
|
+
ids.push(...rows.map((r) => r.id));
|
|
1163
|
+
}
|
|
1164
|
+
return ids;
|
|
1165
|
+
}
|
|
1166
|
+
function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
1167
|
+
const targetIds = resolveSymbolIds(stmt, symbolName, file);
|
|
1168
|
+
if (targetIds.length === 0)
|
|
1169
|
+
return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
|
|
1170
|
+
let matchIds = targetIds;
|
|
1171
|
+
let ambiguous = false;
|
|
1172
|
+
if (file !== void 0) {
|
|
1173
|
+
const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
|
|
1174
|
+
if (allNamedIds.length > targetIds.length) {
|
|
1175
|
+
matchIds = allNamedIds;
|
|
1176
|
+
ambiguous = true;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const useFallback = !file;
|
|
1180
|
+
const rows = chunkedIdQuery(
|
|
1181
|
+
stmt,
|
|
1182
|
+
matchIds,
|
|
1183
|
+
(ph) => `SELECT
|
|
1184
|
+
s.id AS sym_id,
|
|
1185
|
+
s.name AS sym_name,
|
|
1186
|
+
s.kind AS sym_kind,
|
|
1187
|
+
s.lang AS sym_lang,
|
|
1188
|
+
s.file AS sym_file,
|
|
1189
|
+
s.line AS sym_line,
|
|
1190
|
+
s.signature AS sym_signature,
|
|
1191
|
+
r.call_type,
|
|
1192
|
+
r.line AS ref_line
|
|
1193
|
+
FROM refs r
|
|
1194
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1195
|
+
WHERE r.to_id IN (${ph})
|
|
1196
|
+
ORDER BY r.line, r.id`,
|
|
1197
|
+
[]
|
|
1198
|
+
);
|
|
1199
|
+
if (useFallback) {
|
|
1200
|
+
const fallbackRows = stmt(
|
|
1201
|
+
`SELECT
|
|
1202
|
+
s.id AS sym_id,
|
|
1203
|
+
s.name AS sym_name,
|
|
1204
|
+
s.kind AS sym_kind,
|
|
1205
|
+
s.lang AS sym_lang,
|
|
1206
|
+
s.file AS sym_file,
|
|
1207
|
+
s.line AS sym_line,
|
|
1208
|
+
s.signature AS sym_signature,
|
|
1209
|
+
r.call_type,
|
|
1210
|
+
r.line AS ref_line
|
|
1211
|
+
FROM refs r
|
|
1212
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1213
|
+
WHERE r.to_id IS NULL AND r.to_name = ?
|
|
1214
|
+
ORDER BY r.line, r.id`
|
|
1215
|
+
).all(symbolName);
|
|
1216
|
+
rows.push(...fallbackRows);
|
|
1217
|
+
}
|
|
1218
|
+
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
1219
|
+
const allCalls = rows.map(mapCallSiteRow);
|
|
1220
|
+
return {
|
|
1221
|
+
calls: allCalls.slice(0, limit),
|
|
1222
|
+
symbolFound: true,
|
|
1223
|
+
ambiguous,
|
|
1224
|
+
totalMatches: allCalls.length
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function findOutgoingCallsByName(stmt, symbolName, file, limit) {
|
|
1228
|
+
const sourceIds = resolveSymbolIds(stmt, symbolName, file);
|
|
1229
|
+
if (sourceIds.length === 0)
|
|
1230
|
+
return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
|
|
1231
|
+
const unresolvedCount = chunkedIdScalar(
|
|
1232
|
+
stmt,
|
|
1233
|
+
sourceIds,
|
|
1234
|
+
(ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
|
|
1235
|
+
);
|
|
1236
|
+
const rows = chunkedIdQuery(
|
|
1237
|
+
stmt,
|
|
1238
|
+
sourceIds,
|
|
1239
|
+
(ph) => `SELECT
|
|
1240
|
+
s.id AS sym_id,
|
|
1241
|
+
s.name AS sym_name,
|
|
1242
|
+
s.kind AS sym_kind,
|
|
1243
|
+
s.lang AS sym_lang,
|
|
1244
|
+
s.file AS sym_file,
|
|
1245
|
+
s.line AS sym_line,
|
|
1246
|
+
s.signature AS sym_signature,
|
|
1247
|
+
r.call_type,
|
|
1248
|
+
r.line AS ref_line
|
|
1249
|
+
FROM refs r
|
|
1250
|
+
JOIN symbols s ON s.id = r.to_id
|
|
1251
|
+
WHERE r.from_id IN (${ph})
|
|
1252
|
+
AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
|
|
1253
|
+
ORDER BY r.line, r.id`,
|
|
1254
|
+
[]
|
|
1255
|
+
);
|
|
1256
|
+
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
1257
|
+
const calls = rows.map(mapCallSiteRow).slice(0, limit);
|
|
1258
|
+
return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
|
|
1259
|
+
}
|
|
1260
|
+
function runCteWithSeeds(stmt, seedIds, buildSql) {
|
|
1261
|
+
if (seedIds.length <= 900) {
|
|
1262
|
+
const ph = seedIds.map(() => "?").join(",");
|
|
1263
|
+
return stmt(buildSql(ph)).all(...seedIds);
|
|
1264
|
+
}
|
|
1265
|
+
stmt("DROP TABLE IF EXISTS _cte_seeds").run();
|
|
1266
|
+
try {
|
|
1267
|
+
stmt("CREATE TEMP TABLE _cte_seeds (id INTEGER PRIMARY KEY)").run();
|
|
1268
|
+
for (let i = 0; i < seedIds.length; i += 500) {
|
|
1269
|
+
const chunk = seedIds.slice(i, i + 500);
|
|
1270
|
+
const ph = chunk.map(() => "(?)").join(",");
|
|
1271
|
+
stmt(`INSERT OR IGNORE INTO _cte_seeds (id) VALUES ${ph}`).run(...chunk);
|
|
1272
|
+
}
|
|
1273
|
+
return stmt(buildSql("SELECT id FROM _cte_seeds")).all();
|
|
1274
|
+
} finally {
|
|
1275
|
+
stmt("DROP TABLE IF EXISTS _cte_seeds").run();
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function findTransitiveIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
1279
|
+
const targetIds = resolveSymbolIds(stmt, symbolName, file);
|
|
1280
|
+
if (targetIds.length === 0)
|
|
1281
|
+
return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
|
|
1282
|
+
let matchIds = targetIds;
|
|
1283
|
+
let ambiguous = false;
|
|
1284
|
+
if (file !== void 0) {
|
|
1285
|
+
const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
|
|
1286
|
+
if (allNamedIds.length > targetIds.length) {
|
|
1287
|
+
matchIds = allNamedIds;
|
|
1288
|
+
ambiguous = true;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const cteSql = (seedSource) => `WITH RECURSIVE incoming_tree(from_id) AS (
|
|
1292
|
+
SELECT r.from_id
|
|
1293
|
+
FROM refs r
|
|
1294
|
+
WHERE r.to_id IN (${seedSource})
|
|
1295
|
+
|
|
1296
|
+
UNION
|
|
1297
|
+
|
|
1298
|
+
SELECT r.from_id
|
|
1299
|
+
FROM refs r
|
|
1300
|
+
JOIN incoming_tree it ON r.to_id = it.from_id
|
|
1301
|
+
)
|
|
1302
|
+
SELECT
|
|
1303
|
+
s.id AS sym_id,
|
|
1304
|
+
s.name AS sym_name,
|
|
1305
|
+
s.kind AS sym_kind,
|
|
1306
|
+
s.lang AS sym_lang,
|
|
1307
|
+
s.file AS sym_file,
|
|
1308
|
+
s.line AS sym_line,
|
|
1309
|
+
s.signature AS sym_signature,
|
|
1310
|
+
'' AS call_type,
|
|
1311
|
+
0 AS ref_line
|
|
1312
|
+
FROM incoming_tree it
|
|
1313
|
+
JOIN symbols s ON s.id = it.from_id
|
|
1314
|
+
GROUP BY s.id
|
|
1315
|
+
ORDER BY s.file, s.line`;
|
|
1316
|
+
const rows = runCteWithSeeds(stmt, matchIds, cteSql);
|
|
1317
|
+
if (!file) {
|
|
1318
|
+
const fallbackRows = stmt(
|
|
1319
|
+
`SELECT
|
|
1320
|
+
s.id AS sym_id,
|
|
1321
|
+
s.name AS sym_name,
|
|
1322
|
+
s.kind AS sym_kind,
|
|
1323
|
+
s.lang AS sym_lang,
|
|
1324
|
+
s.file AS sym_file,
|
|
1325
|
+
s.line AS sym_line,
|
|
1326
|
+
s.signature AS sym_signature,
|
|
1327
|
+
r.call_type,
|
|
1328
|
+
r.line AS ref_line
|
|
1329
|
+
FROM refs r
|
|
1330
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1331
|
+
WHERE r.to_id IS NULL AND r.to_name = ?
|
|
1332
|
+
ORDER BY r.line, r.id`
|
|
1333
|
+
).all(symbolName);
|
|
1334
|
+
rows.push(...fallbackRows);
|
|
1335
|
+
}
|
|
1336
|
+
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
1337
|
+
const allCalls = rows.map(mapCallSiteRow);
|
|
1338
|
+
return {
|
|
1339
|
+
calls: allCalls.slice(0, limit),
|
|
1340
|
+
symbolFound: true,
|
|
1341
|
+
ambiguous,
|
|
1342
|
+
totalMatches: allCalls.length
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function findTransitiveOutgoingCallsByName(stmt, symbolName, file, limit) {
|
|
1346
|
+
const sourceIds = resolveSymbolIds(stmt, symbolName, file);
|
|
1347
|
+
if (sourceIds.length === 0)
|
|
1348
|
+
return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
|
|
1349
|
+
const unresolvedCount = chunkedIdScalar(
|
|
1350
|
+
stmt,
|
|
1351
|
+
sourceIds,
|
|
1352
|
+
(ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
|
|
1353
|
+
);
|
|
1354
|
+
const cteSql = (seedSource) => `WITH RECURSIVE outgoing_tree(to_id) AS (
|
|
1355
|
+
SELECT r.to_id
|
|
1356
|
+
FROM refs r
|
|
1357
|
+
WHERE r.from_id IN (${seedSource}) AND r.to_id IS NOT NULL
|
|
1358
|
+
|
|
1359
|
+
UNION
|
|
1360
|
+
|
|
1361
|
+
SELECT r.to_id
|
|
1362
|
+
FROM refs r
|
|
1363
|
+
JOIN outgoing_tree ot ON r.from_id = ot.to_id
|
|
1364
|
+
WHERE r.to_id IS NOT NULL
|
|
1365
|
+
)
|
|
1366
|
+
SELECT
|
|
1367
|
+
s.id AS sym_id,
|
|
1368
|
+
s.name AS sym_name,
|
|
1369
|
+
s.kind AS sym_kind,
|
|
1370
|
+
s.lang AS sym_lang,
|
|
1371
|
+
s.file AS sym_file,
|
|
1372
|
+
s.line AS sym_line,
|
|
1373
|
+
s.signature AS sym_signature,
|
|
1374
|
+
'' AS call_type,
|
|
1375
|
+
0 AS ref_line
|
|
1376
|
+
FROM outgoing_tree ot
|
|
1377
|
+
JOIN symbols s ON s.id = ot.to_id
|
|
1378
|
+
GROUP BY s.id
|
|
1379
|
+
ORDER BY s.file, s.line`;
|
|
1380
|
+
const rows = runCteWithSeeds(stmt, sourceIds, cteSql);
|
|
1381
|
+
const calls = rows.map(mapCallSiteRow).slice(0, limit);
|
|
1382
|
+
return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
|
|
1383
|
+
}
|
|
1384
|
+
function findReachableSymbolIds(stmt, seedIds) {
|
|
1385
|
+
if (seedIds.length === 0) return /* @__PURE__ */ new Set();
|
|
1386
|
+
if (seedIds.length > 900) {
|
|
1387
|
+
stmt("DROP TABLE IF EXISTS _seeds").run();
|
|
1388
|
+
try {
|
|
1389
|
+
stmt("CREATE TEMP TABLE _seeds (id INTEGER PRIMARY KEY)").run();
|
|
1390
|
+
for (let i = 0; i < seedIds.length; i += 500) {
|
|
1391
|
+
const chunk = seedIds.slice(i, i + 500);
|
|
1392
|
+
const ph2 = chunk.map(() => "(?)").join(",");
|
|
1393
|
+
stmt(`INSERT OR IGNORE INTO _seeds (id) VALUES ${ph2}`).run(...chunk);
|
|
1394
|
+
}
|
|
1395
|
+
const rows2 = stmt(
|
|
1396
|
+
`WITH RECURSIVE reachable(id) AS (
|
|
1397
|
+
SELECT id FROM _seeds
|
|
1398
|
+
UNION
|
|
1399
|
+
SELECT r.to_id
|
|
1400
|
+
FROM refs r
|
|
1401
|
+
JOIN reachable ON r.from_id = reachable.id
|
|
1402
|
+
WHERE r.to_id IS NOT NULL
|
|
1403
|
+
)
|
|
1404
|
+
SELECT DISTINCT id FROM reachable`
|
|
1405
|
+
).all();
|
|
1406
|
+
return new Set(rows2.map((r) => r.id));
|
|
1407
|
+
} finally {
|
|
1408
|
+
stmt("DROP TABLE IF EXISTS _seeds").run();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const ph = seedIds.map(() => "?").join(",");
|
|
1412
|
+
const rows = stmt(
|
|
1413
|
+
`WITH RECURSIVE reachable(id) AS (
|
|
1414
|
+
SELECT id FROM symbols WHERE id IN (${ph})
|
|
1415
|
+
UNION
|
|
1416
|
+
SELECT r.to_id
|
|
1417
|
+
FROM refs r
|
|
1418
|
+
JOIN reachable ON r.from_id = reachable.id
|
|
1419
|
+
WHERE r.to_id IS NOT NULL
|
|
1420
|
+
)
|
|
1421
|
+
SELECT DISTINCT id FROM reachable`
|
|
1422
|
+
).all(...seedIds);
|
|
1423
|
+
return new Set(rows.map((r) => r.id));
|
|
1424
|
+
}
|
|
1425
|
+
function findRefsToWithStatement(stmt, symbolId) {
|
|
1426
|
+
return stmt(
|
|
1427
|
+
"SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
|
|
1428
|
+
).all(symbolId, symbolId).map(mapWriterRefRow);
|
|
1429
|
+
}
|
|
1430
|
+
function findRefsFromWithStatement(stmt, symbolId) {
|
|
1431
|
+
return stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(
|
|
1432
|
+
symbolId
|
|
1433
|
+
).map(mapWriterRefRow);
|
|
1434
|
+
}
|
|
1435
|
+
function getPackageGraphWithStatement(stmt) {
|
|
1436
|
+
const fileCounts = stmt("SELECT file, COUNT(*) AS n FROM symbols GROUP BY file").all();
|
|
1437
|
+
const files = stmt("SELECT DISTINCT file FROM files").all();
|
|
1438
|
+
const packageOf = readPackageLabeller(stmt);
|
|
1439
|
+
const { pkgNodes, fileToPkg } = buildPackageGraphNodes(fileCounts, files, packageOf);
|
|
1440
|
+
const refRows = stmt(
|
|
1441
|
+
`SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
|
|
1442
|
+
FROM refs r
|
|
1443
|
+
JOIN symbols sf ON sf.id = r.from_id
|
|
1444
|
+
JOIN symbols st ON st.id = r.to_id
|
|
1445
|
+
WHERE r.to_id IS NOT NULL AND r.call_type != 'import'
|
|
1446
|
+
GROUP BY r.call_type, sf.file, st.file`
|
|
1447
|
+
).all();
|
|
1448
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const r of refRows) {
|
|
1450
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
1451
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
1452
|
+
if (fromPkg === toPkg) continue;
|
|
1453
|
+
const n = Number(r.n) || 0;
|
|
1454
|
+
addWeightedEdge(edgeMap, fromPkg, toPkg, r.call_type, n);
|
|
1455
|
+
}
|
|
1456
|
+
const importRows = stmt(
|
|
1457
|
+
`SELECT s.file AS from_file,
|
|
1458
|
+
COALESCE(r.to_file, st.file) AS to_file,
|
|
1459
|
+
COUNT(*) AS n
|
|
1460
|
+
FROM refs r
|
|
1461
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1462
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
1463
|
+
WHERE r.call_type = 'import'
|
|
1464
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
1465
|
+
GROUP BY s.file, COALESCE(r.to_file, st.file)`
|
|
1466
|
+
).all();
|
|
1467
|
+
for (const r of importRows) {
|
|
1468
|
+
const fromPkg = fileToPkg.get(r.from_file) ?? packageOf(r.from_file);
|
|
1469
|
+
const toPkg = fileToPkg.get(r.to_file) ?? packageOf(r.to_file);
|
|
1470
|
+
if (fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
|
|
1471
|
+
const n = Number(r.n) || 0;
|
|
1472
|
+
addWeightedEdge(edgeMap, fromPkg, toPkg, "import", n);
|
|
1473
|
+
}
|
|
1474
|
+
const edges = materializeWeightedEdges(edgeMap, "pkg");
|
|
1475
|
+
return { nodes: [...pkgNodes.values()], edges };
|
|
1476
|
+
}
|
|
1477
|
+
function readPackageLabeller(stmt) {
|
|
1478
|
+
const rows = stmt("SELECT file, package FROM files WHERE package != ''").all();
|
|
1479
|
+
return createPackageLabeller(new Map(rows.map((row) => [row.file, row.package])));
|
|
1480
|
+
}
|
|
1481
|
+
function getFileGraphWithStatement(stmt, packageFilter) {
|
|
1482
|
+
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
1483
|
+
const packageOf = readPackageLabeller(stmt);
|
|
1484
|
+
const langOf = (file) => detectLang(file) ?? "other";
|
|
1485
|
+
const pkgFilePaths = allFiles.filter((f) => matchesIndexedPackageFilter(f.file, packageOf(f.file), packageFilter)).map((f) => f.file);
|
|
1486
|
+
const localFiles = new Set(pkgFilePaths);
|
|
1487
|
+
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
1488
|
+
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
1489
|
+
const pkgSyms = stmt(
|
|
1490
|
+
`SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
|
|
1491
|
+
).all(...pkgFilePaths);
|
|
1492
|
+
const { fileNodes, symToFile, fileStats, ensureFileNode } = buildFileGraphNodeState(
|
|
1493
|
+
pkgSyms,
|
|
1494
|
+
localFiles,
|
|
1495
|
+
packageOf
|
|
1496
|
+
);
|
|
1497
|
+
const refRows = stmt(
|
|
1498
|
+
`SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
|
|
1499
|
+
FROM refs r
|
|
1500
|
+
WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
1501
|
+
OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
|
|
1502
|
+
AND r.to_id IS NOT NULL
|
|
1503
|
+
GROUP BY r.from_id, r.to_id, r.call_type`
|
|
1504
|
+
).all(...pkgFilePaths, ...pkgFilePaths);
|
|
1505
|
+
const knownSymIds = new Set(pkgSyms.map((s) => s.id));
|
|
1506
|
+
const crossRefIds = /* @__PURE__ */ new Set();
|
|
1507
|
+
for (const r of refRows) {
|
|
1508
|
+
if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
|
|
1509
|
+
if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
|
|
1510
|
+
}
|
|
1511
|
+
if (crossRefIds.size > 0) {
|
|
1512
|
+
const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
|
|
1513
|
+
const extras = stmt(`SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`).all(
|
|
1514
|
+
...crossRefIds
|
|
1515
|
+
);
|
|
1516
|
+
for (const x of extras) {
|
|
1517
|
+
symToFile.set(x.id, x.file);
|
|
1518
|
+
if (!fileStats.has(x.file)) {
|
|
1519
|
+
fileStats.set(x.file, { count: 0, lang: langOf(x.file) });
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1524
|
+
for (const r of refRows) {
|
|
1525
|
+
if (r.call_type === "import") continue;
|
|
1526
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1527
|
+
const toFile = symToFile.get(r.to_id);
|
|
1528
|
+
if (!fromFile || !toFile || fromFile === toFile) continue;
|
|
1529
|
+
if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
|
|
1530
|
+
ensureFileNode(fromFile);
|
|
1531
|
+
ensureFileNode(toFile);
|
|
1532
|
+
const n = Number(r.n) || 0;
|
|
1533
|
+
addWeightedEdge(edgeMap, fromFile, toFile, r.call_type, n);
|
|
1534
|
+
}
|
|
1535
|
+
const importRows = stmt(
|
|
1536
|
+
`SELECT r.from_id, COALESCE(r.to_file, st.file) AS to_file, COUNT(*) AS n
|
|
1537
|
+
FROM refs r
|
|
1538
|
+
LEFT JOIN symbols st ON st.id = r.to_id
|
|
1539
|
+
WHERE r.call_type = 'import'
|
|
1540
|
+
AND COALESCE(r.to_file, st.file) IS NOT NULL
|
|
1541
|
+
AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
|
|
1542
|
+
GROUP BY r.from_id, COALESCE(r.to_file, st.file)`
|
|
1543
|
+
).all(...pkgFilePaths);
|
|
1544
|
+
for (const r of importRows) {
|
|
1545
|
+
const fromFile = symToFile.get(r.from_id);
|
|
1546
|
+
if (!fromFile || !localFiles.has(fromFile)) continue;
|
|
1547
|
+
const toFile = r.to_file;
|
|
1548
|
+
if (!toFile || fromFile === toFile) continue;
|
|
1549
|
+
if (!fileStats.has(toFile)) {
|
|
1550
|
+
fileStats.set(toFile, { count: 0, lang: langOf(toFile) });
|
|
1551
|
+
}
|
|
1552
|
+
ensureFileNode(fromFile);
|
|
1553
|
+
ensureFileNode(toFile);
|
|
1554
|
+
const n = Number(r.n) || 0;
|
|
1555
|
+
addWeightedEdge(edgeMap, fromFile, toFile, "import", n);
|
|
1556
|
+
}
|
|
1557
|
+
const edges = materializeWeightedEdges(edgeMap, "file");
|
|
1558
|
+
return { nodes: [...fileNodes.values()], edges };
|
|
1559
|
+
}
|
|
1560
|
+
function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
1561
|
+
const indexedFiles = resolveIndexedFiles(stmt, fileFilter);
|
|
1562
|
+
if (indexedFiles.length === 0) return { nodes: [], edges: [] };
|
|
1563
|
+
const filePlaceholders = indexedFiles.map(() => "?").join(",");
|
|
1564
|
+
const syms = stmt(
|
|
1565
|
+
`SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY line, id`
|
|
1566
|
+
).all(...indexedFiles);
|
|
1567
|
+
if (syms.length === 0) return { nodes: [], edges: [] };
|
|
1568
|
+
const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
|
|
1569
|
+
const relatedIds = new Set(syms.map((symbol) => symbol.id));
|
|
1570
|
+
const refRows = stmt(
|
|
1571
|
+
`SELECT from_id, to_id, call_type, COUNT(*) AS n
|
|
1572
|
+
FROM (
|
|
1573
|
+
SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
1574
|
+
FROM refs r
|
|
1575
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1576
|
+
WHERE s.file IN (${filePlaceholders})
|
|
1577
|
+
UNION
|
|
1578
|
+
SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
1579
|
+
FROM refs r
|
|
1580
|
+
JOIN symbols s ON s.id = r.to_id
|
|
1581
|
+
WHERE s.file IN (${filePlaceholders})
|
|
1582
|
+
)
|
|
1583
|
+
WHERE to_id IS NOT NULL
|
|
1584
|
+
GROUP BY from_id, to_id, call_type`
|
|
1585
|
+
).all(...indexedFiles, ...indexedFiles);
|
|
1586
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const r of refRows) {
|
|
1588
|
+
if (r.to_id == null) continue;
|
|
1589
|
+
relatedIds.add(r.from_id);
|
|
1590
|
+
relatedIds.add(r.to_id);
|
|
1591
|
+
const n = Number(r.n) || 0;
|
|
1592
|
+
addWeightedEdge(edgeMap, r.from_id, r.to_id, r.call_type, n);
|
|
1593
|
+
}
|
|
1594
|
+
const edges = materializeWeightedEdges(edgeMap, "sym");
|
|
1595
|
+
const loadedIds = new Set(syms.map((s) => s.id));
|
|
1596
|
+
const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
|
|
1597
|
+
if (missingIds.length > 0) {
|
|
1598
|
+
const placeholders2 = missingIds.map(() => "?").join(",");
|
|
1599
|
+
const extras = stmt(
|
|
1600
|
+
`SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders2})`
|
|
1601
|
+
).all(...missingIds);
|
|
1602
|
+
for (const s of extras) symById.set(s.id, s);
|
|
1603
|
+
}
|
|
1604
|
+
const nodes = buildSymbolGraphNodes(
|
|
1605
|
+
symById,
|
|
1606
|
+
relatedIds,
|
|
1607
|
+
new Set(syms.map((symbol) => symbol.file)),
|
|
1608
|
+
readPackageLabeller(stmt)
|
|
1609
|
+
);
|
|
1610
|
+
return { nodes, edges };
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/codebase-index/writer-mutations.ts
|
|
1614
|
+
function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
|
|
1615
|
+
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
const affectedNames = /* @__PURE__ */ new Set();
|
|
1619
|
+
for (const entry of entries) {
|
|
1620
|
+
for (const symbol of entry.symbols) affectedNames.add(symbol.name);
|
|
1621
|
+
for (const ref of entry.refs) affectedNames.add(ref.toName);
|
|
1622
|
+
}
|
|
1623
|
+
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
1624
|
+
for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
|
|
1625
|
+
affectedNames.add(name);
|
|
1626
|
+
}
|
|
1627
|
+
let cursor = 0;
|
|
1628
|
+
for (const take of inListChunks(options.deleteForFiles.length, Math.floor(maxSqlVars / 4))) {
|
|
1629
|
+
const bucket = padToInBucket(options.deleteForFiles.slice(cursor, cursor + take));
|
|
1630
|
+
cursor += take;
|
|
1631
|
+
const ph = placeholders(bucket.length);
|
|
1632
|
+
if (ftsAvailable) {
|
|
1633
|
+
stmtFn(
|
|
1634
|
+
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${ph}))`
|
|
1635
|
+
).run(...bucket);
|
|
1636
|
+
}
|
|
1637
|
+
if (vectorsAvailable) {
|
|
1638
|
+
stmtFn(
|
|
1639
|
+
`DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
|
|
1640
|
+
).run(...bucket);
|
|
1641
|
+
}
|
|
1642
|
+
stmtFn(
|
|
1643
|
+
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
|
|
1644
|
+
).run(...bucket);
|
|
1645
|
+
stmtFn(`DELETE FROM symbols WHERE file IN (${ph})`).run(...bucket);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
1649
|
+
let nextId = allocateSymbolIds(totalSymbols);
|
|
1650
|
+
const allInserted = [];
|
|
1651
|
+
const refsToInsert = [];
|
|
1652
|
+
const bulkSyms = [];
|
|
1653
|
+
const ftsRows = [];
|
|
1654
|
+
const vectorRows = [];
|
|
1655
|
+
for (const entry of entries) {
|
|
1656
|
+
const insertedForEntry = [];
|
|
1657
|
+
for (const s of entry.symbols) {
|
|
1658
|
+
const id = nextId++;
|
|
1659
|
+
bulkSyms.push({
|
|
1660
|
+
id,
|
|
1661
|
+
lang: s.lang,
|
|
1662
|
+
kind: s.kind,
|
|
1663
|
+
name: s.name,
|
|
1664
|
+
file: s.file,
|
|
1665
|
+
line: s.line,
|
|
1666
|
+
col: s.col,
|
|
1667
|
+
signature: s.signature,
|
|
1668
|
+
docComment: s.docComment,
|
|
1669
|
+
scope: s.scope,
|
|
1670
|
+
text: s.text
|
|
1671
|
+
});
|
|
1672
|
+
if (ftsAvailable) {
|
|
1673
|
+
ftsRows.push({
|
|
1674
|
+
id,
|
|
1675
|
+
text: buildIndexableText(s.name, s.signature, s.docComment)
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (vectorsAvailable) {
|
|
1679
|
+
vectorRows.push({
|
|
1680
|
+
id,
|
|
1681
|
+
vector: encodeVector(
|
|
1682
|
+
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
1683
|
+
)
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const inserted = { ...s, id };
|
|
1687
|
+
allInserted.push(inserted);
|
|
1688
|
+
insertedForEntry.push(inserted);
|
|
1689
|
+
}
|
|
1690
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
1691
|
+
}
|
|
1692
|
+
bulkInsertSymbolsWithStatement((sql) => stmtFn(sql), maxSqlVars, bulkSyms);
|
|
1693
|
+
bulkInsertFtsWithStatement((sql) => stmtFn(sql), maxSqlVars, ftsAvailable, ftsRows);
|
|
1694
|
+
if (vectorsAvailable) {
|
|
1695
|
+
bulkInsertVectorsWithStatement((sql) => stmtFn(sql), maxSqlVars, vectorRows);
|
|
1696
|
+
}
|
|
1697
|
+
bulkInsertRefsWithStatement((sql) => stmtFn(sql), maxSqlVars, refsToInsert);
|
|
1698
|
+
const upsertStmt = stmtFn(
|
|
1699
|
+
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
1700
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1701
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
1702
|
+
lang = excluded.lang,
|
|
1703
|
+
mtime_ms = excluded.mtime_ms,
|
|
1704
|
+
content_hash = excluded.content_hash,
|
|
1705
|
+
symbol_count = excluded.symbol_count,
|
|
1706
|
+
last_indexed = excluded.last_indexed`
|
|
1707
|
+
);
|
|
1708
|
+
const now = Date.now();
|
|
1709
|
+
for (const entry of entries) {
|
|
1710
|
+
upsertStmt.run(
|
|
1711
|
+
entry.file,
|
|
1712
|
+
entry.lang,
|
|
1713
|
+
entry.mtimeMs,
|
|
1714
|
+
entry.contentHash ?? "",
|
|
1715
|
+
entry.symbolCount,
|
|
1716
|
+
now
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
resolveRefsForNamesUnsafe2(affectedNames);
|
|
1720
|
+
return allInserted;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/codebase-index/writer-pragmas.ts
|
|
1724
|
+
import { sqliteCachePragmas } from "@wrongstack/core/utils";
|
|
1725
|
+
function applyIndexStorePragmas(db) {
|
|
1726
|
+
try {
|
|
1727
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
1728
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
1729
|
+
db.exec("PRAGMA busy_timeout = 15000");
|
|
1730
|
+
db.exec("PRAGMA temp_store = MEMORY");
|
|
1731
|
+
const cache = sqliteCachePragmas();
|
|
1732
|
+
db.exec(`PRAGMA cache_size = -${cache.cacheSizeKiB}`);
|
|
1733
|
+
db.exec(`PRAGMA mmap_size = ${cache.mmapBytes}`);
|
|
1734
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
1735
|
+
db.exec("PRAGMA journal_size_limit = 67108864");
|
|
1736
|
+
db.exec("PRAGMA wal_autocheckpoint = 1000");
|
|
1737
|
+
} catch {
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/codebase-index/writer-schema.ts
|
|
1742
|
+
var METADATA_TABLE_SQL = `
|
|
1743
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
1744
|
+
key TEXT PRIMARY KEY,
|
|
1745
|
+
value TEXT NOT NULL
|
|
1746
|
+
);
|
|
1747
|
+
`;
|
|
1748
|
+
var CORE_TABLES_SQL = `
|
|
1749
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
1750
|
+
file TEXT PRIMARY KEY,
|
|
1751
|
+
lang TEXT NOT NULL,
|
|
1752
|
+
mtime_ms INTEGER NOT NULL,
|
|
1753
|
+
-- Phase 2: xxHash64 of the file's UTF-8 bytes. Empty string when the
|
|
1754
|
+
-- indexer hasn't populated it yet (legacy rows, schema repaired by
|
|
1755
|
+
-- repairMissingColumns). Compared on incremental re-index so that a
|
|
1756
|
+
-- touch or branch-switch that leaves content byte-identical skips the
|
|
1757
|
+
-- expensive parse phase entirely (refactoring proposal Phase 2).
|
|
1758
|
+
content_hash TEXT NOT NULL DEFAULT '',
|
|
1759
|
+
symbol_count INTEGER NOT NULL DEFAULT 0,
|
|
1760
|
+
last_indexed INTEGER NOT NULL,
|
|
1761
|
+
-- Code Atlas grouping label, computed at index time from the ecosystem's
|
|
1762
|
+
-- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than
|
|
1763
|
+
-- re-derived per query because the evidence lives on disk, not in the DB.
|
|
1764
|
+
package TEXT NOT NULL DEFAULT ''
|
|
1765
|
+
);
|
|
1766
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
1767
|
+
id INTEGER PRIMARY KEY,
|
|
1768
|
+
lang TEXT NOT NULL,
|
|
1769
|
+
kind TEXT NOT NULL,
|
|
1770
|
+
name TEXT NOT NULL,
|
|
1771
|
+
file TEXT NOT NULL,
|
|
1772
|
+
line INTEGER NOT NULL,
|
|
1773
|
+
col INTEGER NOT NULL,
|
|
1774
|
+
signature TEXT NOT NULL DEFAULT '',
|
|
1775
|
+
doc_comment TEXT NOT NULL DEFAULT '',
|
|
1776
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
1777
|
+
text TEXT NOT NULL DEFAULT ''
|
|
1778
|
+
);
|
|
1779
|
+
`;
|
|
1780
|
+
var FILE_INDEX_SQL = [
|
|
1781
|
+
"CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)"
|
|
1782
|
+
];
|
|
1783
|
+
var SYMBOL_INDEX_SQL = [
|
|
1784
|
+
"CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)",
|
|
1785
|
+
"CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)",
|
|
1786
|
+
"CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)",
|
|
1787
|
+
"CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)",
|
|
1788
|
+
"CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)",
|
|
1789
|
+
"CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)"
|
|
1790
|
+
];
|
|
1791
|
+
var REFS_TABLE_SQL = `
|
|
1792
|
+
CREATE TABLE IF NOT EXISTS refs (
|
|
1793
|
+
id INTEGER PRIMARY KEY,
|
|
1794
|
+
from_id INTEGER NOT NULL,
|
|
1795
|
+
to_name TEXT NOT NULL,
|
|
1796
|
+
to_id INTEGER,
|
|
1797
|
+
call_type TEXT NOT NULL,
|
|
1798
|
+
line INTEGER NOT NULL,
|
|
1799
|
+
lang TEXT NOT NULL DEFAULT '',
|
|
1800
|
+
module TEXT,
|
|
1801
|
+
to_file TEXT
|
|
1802
|
+
);
|
|
1803
|
+
`;
|
|
1804
|
+
var REFS_INDEX_SQL = [
|
|
1805
|
+
"CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)",
|
|
1806
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)",
|
|
1807
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)",
|
|
1808
|
+
"CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)",
|
|
1809
|
+
// Name resolution matches (to_name, lang) pairs; the composite keeps the
|
|
1810
|
+
// language-scoped UPDATE from degrading into a scan of every same-named row.
|
|
1811
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)",
|
|
1812
|
+
// The post-index module resolution pass groups unresolved import refs by
|
|
1813
|
+
// (module, lang); graph readers then read to_file back.
|
|
1814
|
+
"CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)",
|
|
1815
|
+
"CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)"
|
|
1816
|
+
];
|
|
1817
|
+
var LANG_FAMILY_TABLE_SQL = `
|
|
1818
|
+
CREATE TABLE IF NOT EXISTS lang_family (
|
|
1819
|
+
lang TEXT PRIMARY KEY,
|
|
1820
|
+
family TEXT NOT NULL
|
|
1821
|
+
);
|
|
1822
|
+
`;
|
|
1823
|
+
var LANG_FAMILY_WILDCARD = "*";
|
|
1824
|
+
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'trigram')";
|
|
1825
|
+
var SYMBOL_VECTORS_TABLE_SQL = `
|
|
1826
|
+
CREATE TABLE IF NOT EXISTS symbol_vectors (
|
|
1827
|
+
symbol_id INTEGER PRIMARY KEY,
|
|
1828
|
+
vector BLOB NOT NULL,
|
|
1829
|
+
FOREIGN KEY (symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
|
|
1830
|
+
);
|
|
1831
|
+
`;
|
|
1832
|
+
|
|
1833
|
+
// src/codebase-index/writer-refs.ts
|
|
1834
|
+
var FAMILY_MATCH_SQL = `(
|
|
1835
|
+
sym.lang = refs.lang
|
|
1836
|
+
OR EXISTS (
|
|
1837
|
+
SELECT 1 FROM lang_family lf1
|
|
1838
|
+
JOIN lang_family lf2 ON lf1.family = lf2.family
|
|
1839
|
+
WHERE lf1.lang = sym.lang AND lf2.lang = refs.lang
|
|
1840
|
+
)
|
|
1841
|
+
OR ? IN (
|
|
1842
|
+
SELECT family FROM lang_family WHERE lang = refs.lang
|
|
1843
|
+
)
|
|
1844
|
+
)`;
|
|
1845
|
+
function getNamespaceDeclarationsWithStatement(stmtFn) {
|
|
1846
|
+
return stmtFn(
|
|
1847
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
1848
|
+
).all();
|
|
1849
|
+
}
|
|
1850
|
+
function getFilePackagesWithStatement(stmtFn) {
|
|
1851
|
+
const rows = stmtFn("SELECT file, package FROM files WHERE package != ''").all();
|
|
1852
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
1853
|
+
}
|
|
1854
|
+
function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
|
|
1855
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
1856
|
+
FROM refs r
|
|
1857
|
+
JOIN symbols s ON s.id = r.from_id
|
|
1858
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
1859
|
+
if (!onlyFiles?.length) {
|
|
1860
|
+
return stmtFn(base).all();
|
|
1861
|
+
}
|
|
1862
|
+
const out = [];
|
|
1863
|
+
let cursor = 0;
|
|
1864
|
+
for (const take of inListChunks(onlyFiles.length, maxSqlVars)) {
|
|
1865
|
+
const chunk = padToInBucket(onlyFiles.slice(cursor, cursor + take));
|
|
1866
|
+
cursor += take;
|
|
1867
|
+
out.push(
|
|
1868
|
+
...stmtFn(`${base} AND s.file IN (${placeholders(chunk.length)})`).all(...chunk)
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
function getAllResolvedRefsWithStatement(stmtFn) {
|
|
1874
|
+
return stmtFn(
|
|
1875
|
+
"SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
|
|
1876
|
+
).all();
|
|
1877
|
+
}
|
|
1878
|
+
function getAllImportRefsWithStatement(stmtFn) {
|
|
1879
|
+
return stmtFn(
|
|
1880
|
+
`SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
|
|
1881
|
+
r.call_type AS callType, r.line
|
|
1882
|
+
FROM refs r
|
|
1883
|
+
LEFT JOIN symbols s ON r.from_id = s.id
|
|
1884
|
+
WHERE r.call_type = 'import'
|
|
1885
|
+
ORDER BY r.line`
|
|
1886
|
+
).all();
|
|
1887
|
+
}
|
|
1888
|
+
function resolveRefsWithStatement(stmtFn) {
|
|
1889
|
+
try {
|
|
1890
|
+
const result = stmtFn(
|
|
1891
|
+
`UPDATE refs
|
|
1892
|
+
SET to_id = s.id
|
|
1893
|
+
FROM (
|
|
1894
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
1895
|
+
FROM symbols sym
|
|
1896
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
1897
|
+
GROUP BY sym.name, lf.family
|
|
1898
|
+
UNION ALL
|
|
1899
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
1900
|
+
FROM symbols sym
|
|
1901
|
+
GROUP BY sym.name
|
|
1902
|
+
) AS s,
|
|
1903
|
+
lang_family AS rf
|
|
1904
|
+
WHERE refs.to_id IS NULL
|
|
1905
|
+
AND refs.to_name IS NOT NULL
|
|
1906
|
+
AND rf.lang = refs.lang
|
|
1907
|
+
AND s.name = refs.to_name
|
|
1908
|
+
AND s.family = rf.family`
|
|
1909
|
+
).run();
|
|
1910
|
+
return result.changes ?? 0;
|
|
1911
|
+
} catch {
|
|
1912
|
+
const result = stmtFn(
|
|
1913
|
+
`UPDATE refs SET to_id = (
|
|
1914
|
+
SELECT sym.id FROM symbols sym
|
|
1915
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
1916
|
+
ORDER BY sym.id LIMIT 1
|
|
1917
|
+
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
1918
|
+
AND EXISTS (
|
|
1919
|
+
SELECT 1 FROM symbols sym
|
|
1920
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
1921
|
+
)`
|
|
1922
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
1923
|
+
return result.changes ?? 0;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVars, resolutions) {
|
|
1927
|
+
if (resolutions.length === 0) return 0;
|
|
1928
|
+
return runWithRetry(() => {
|
|
1929
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
1930
|
+
db.exec(
|
|
1931
|
+
`CREATE TEMP TABLE import_resolution (
|
|
1932
|
+
from_file TEXT NOT NULL,
|
|
1933
|
+
lang TEXT NOT NULL,
|
|
1934
|
+
module TEXT NOT NULL,
|
|
1935
|
+
to_file TEXT NOT NULL
|
|
1936
|
+
)`
|
|
1937
|
+
);
|
|
1938
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
|
|
1939
|
+
let cursor = 0;
|
|
1940
|
+
for (const take of ladderChunkSizes(resolutions.length, chunkSize)) {
|
|
1941
|
+
const chunk = resolutions.slice(cursor, cursor + take);
|
|
1942
|
+
cursor += take;
|
|
1943
|
+
const valuesPh = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
1944
|
+
const binds = [];
|
|
1945
|
+
for (const entry of chunk) {
|
|
1946
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
1947
|
+
}
|
|
1948
|
+
stmtFn(
|
|
1949
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
1950
|
+
VALUES ${valuesPh}`
|
|
1951
|
+
).run(...binds);
|
|
1952
|
+
}
|
|
1953
|
+
db.exec(
|
|
1954
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
1955
|
+
ON import_resolution(module, lang, from_file)`
|
|
1956
|
+
);
|
|
1957
|
+
const result = stmtFn(
|
|
1958
|
+
`UPDATE refs
|
|
1959
|
+
SET to_file = (
|
|
1960
|
+
SELECT ir.to_file
|
|
1961
|
+
FROM temp.import_resolution ir
|
|
1962
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
1963
|
+
WHERE ir.module = refs.module
|
|
1964
|
+
AND ir.lang = refs.lang
|
|
1965
|
+
AND ir.from_file = s.file
|
|
1966
|
+
LIMIT 1
|
|
1967
|
+
)
|
|
1968
|
+
WHERE refs.call_type = 'import'
|
|
1969
|
+
AND refs.module IS NOT NULL
|
|
1970
|
+
AND EXISTS (
|
|
1971
|
+
SELECT 1
|
|
1972
|
+
FROM temp.import_resolution ir
|
|
1973
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
1974
|
+
WHERE ir.module = refs.module
|
|
1975
|
+
AND ir.lang = refs.lang
|
|
1976
|
+
AND ir.from_file = s.file
|
|
1977
|
+
)`
|
|
1978
|
+
).run();
|
|
1979
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
1980
|
+
return result.changes ?? 0;
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
|
|
1984
|
+
const list = [...names].filter((name) => name.length > 0);
|
|
1985
|
+
if (list.length === 0) return 0;
|
|
1986
|
+
let total = 0;
|
|
1987
|
+
let cursor = 0;
|
|
1988
|
+
for (const take of inListChunks(list.length, maxSqlVars)) {
|
|
1989
|
+
const chunk = padToInBucket(list.slice(cursor, cursor + take));
|
|
1990
|
+
cursor += take;
|
|
1991
|
+
const ph = placeholders(chunk.length);
|
|
1992
|
+
try {
|
|
1993
|
+
const result = stmtFn(
|
|
1994
|
+
`UPDATE refs
|
|
1995
|
+
SET to_id = s.id
|
|
1996
|
+
FROM (
|
|
1997
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
1998
|
+
FROM symbols sym
|
|
1999
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
2000
|
+
WHERE sym.name IN (${ph})
|
|
2001
|
+
GROUP BY sym.name, lf.family
|
|
2002
|
+
UNION ALL
|
|
2003
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
2004
|
+
FROM symbols sym
|
|
2005
|
+
WHERE sym.name IN (${ph})
|
|
2006
|
+
GROUP BY sym.name
|
|
2007
|
+
) AS s,
|
|
2008
|
+
lang_family AS rf
|
|
2009
|
+
WHERE refs.to_name IN (${ph})
|
|
2010
|
+
AND rf.lang = refs.lang
|
|
2011
|
+
AND s.name = refs.to_name
|
|
2012
|
+
AND s.family = rf.family`
|
|
2013
|
+
).run(...chunk, ...chunk, ...chunk);
|
|
2014
|
+
total += result.changes ?? 0;
|
|
2015
|
+
} catch {
|
|
2016
|
+
const result = stmtFn(
|
|
2017
|
+
`UPDATE refs SET to_id = (
|
|
2018
|
+
SELECT sym.id FROM symbols sym
|
|
2019
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
2020
|
+
ORDER BY sym.id LIMIT 1
|
|
2021
|
+
) WHERE refs.to_name IN (${ph})
|
|
2022
|
+
AND EXISTS (
|
|
2023
|
+
SELECT 1 FROM symbols sym
|
|
2024
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
2025
|
+
)`
|
|
2026
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk, LANG_FAMILY_WILDCARD);
|
|
2027
|
+
total += result.changes ?? 0;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return total;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/codebase-index/writer-search.ts
|
|
2034
|
+
import { expectDefined } from "@wrongstack/core/utils";
|
|
2035
|
+
|
|
2036
|
+
// src/codebase-index/writer-search-helpers.ts
|
|
2037
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
2038
|
+
function normalizeSearchLimit(limit) {
|
|
2039
|
+
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
2040
|
+
}
|
|
2041
|
+
function buildWriterSearchWhere(query, filter) {
|
|
2042
|
+
const conditions = [];
|
|
2043
|
+
const values = [];
|
|
2044
|
+
let effectiveKind = filter?.kind;
|
|
2045
|
+
if (filter?.lspKind != null) {
|
|
2046
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
2047
|
+
if (mapped !== null) {
|
|
2048
|
+
effectiveKind = mapped;
|
|
2049
|
+
} else {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (effectiveKind) {
|
|
2054
|
+
conditions.push("kind = ?");
|
|
2055
|
+
values.push(effectiveKind);
|
|
2056
|
+
}
|
|
2057
|
+
if (filter?.lang) {
|
|
2058
|
+
conditions.push("lang = ?");
|
|
2059
|
+
values.push(filter.lang);
|
|
2060
|
+
}
|
|
2061
|
+
if (filter?.file) {
|
|
2062
|
+
conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
2063
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
2064
|
+
}
|
|
2065
|
+
if (query.trim()) {
|
|
2066
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
2067
|
+
conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
|
|
2068
|
+
for (const token of tokens) values.push(`%${escapeLike(token)}%`);
|
|
2069
|
+
}
|
|
2070
|
+
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
2071
|
+
}
|
|
2072
|
+
function mapWriterSearchRow(row, lspKind, score = 0, snippet = "") {
|
|
2073
|
+
return {
|
|
2074
|
+
id: row.id,
|
|
2075
|
+
lang: row.lang,
|
|
2076
|
+
kind: row.kind,
|
|
2077
|
+
name: row.name,
|
|
2078
|
+
file: row.file,
|
|
2079
|
+
line: row.line,
|
|
2080
|
+
col: row.col,
|
|
2081
|
+
signature: row.signature,
|
|
2082
|
+
docComment: row.doc_comment,
|
|
2083
|
+
score,
|
|
2084
|
+
snippet,
|
|
2085
|
+
lspKind
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/codebase-index/writer-search.ts
|
|
2090
|
+
function searchWithStatement(stmtFn, query, filter, opts) {
|
|
2091
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
2092
|
+
if (built === null) return [];
|
|
2093
|
+
const { where, values } = built;
|
|
2094
|
+
const limit = normalizeSearchLimit(opts?.limit);
|
|
2095
|
+
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
2096
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
2097
|
+
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
2098
|
+
const rows = stmtFn(sql).all(...binds);
|
|
2099
|
+
return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
|
|
2100
|
+
}
|
|
2101
|
+
function countSearchWithStatement(stmtFn, query, filter) {
|
|
2102
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
2103
|
+
if (built === null) return 0;
|
|
2104
|
+
const row = stmtFn(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
|
|
2105
|
+
...built.values
|
|
2106
|
+
);
|
|
2107
|
+
return Number(row?.n ?? 0);
|
|
2108
|
+
}
|
|
2109
|
+
function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvailable, getOrBuildBm25, query, filter, limit) {
|
|
2110
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
2111
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
2112
|
+
const tokens = tokenise(query);
|
|
2113
|
+
if (tokens.length === 0 || !ftsAvailable) {
|
|
2114
|
+
return searchRankedFallbackWithStatement(
|
|
2115
|
+
stmtFn,
|
|
2116
|
+
searchFn,
|
|
2117
|
+
getOrBuildBm25,
|
|
2118
|
+
query,
|
|
2119
|
+
filter,
|
|
2120
|
+
safeLimit
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
let effectiveKind = filter?.kind;
|
|
2124
|
+
if (filter?.lspKind != null) {
|
|
2125
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
2126
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
2127
|
+
effectiveKind = mapped;
|
|
2128
|
+
}
|
|
2129
|
+
const longTokens = tokens.filter((t) => t.length >= 3);
|
|
2130
|
+
const shortTokens = tokens.filter((t) => t.length < 3);
|
|
2131
|
+
if (longTokens.length === 0) {
|
|
2132
|
+
return searchRankedFallbackWithStatement(
|
|
2133
|
+
stmtFn,
|
|
2134
|
+
searchFn,
|
|
2135
|
+
getOrBuildBm25,
|
|
2136
|
+
query,
|
|
2137
|
+
filter,
|
|
2138
|
+
safeLimit
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
2142
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
2143
|
+
const values = [match];
|
|
2144
|
+
for (const shortTok of shortTokens) {
|
|
2145
|
+
conditions.push("s.text LIKE ? ESCAPE '\\'");
|
|
2146
|
+
values.push(`%${escapeLike(shortTok)}%`);
|
|
2147
|
+
}
|
|
2148
|
+
if (effectiveKind) {
|
|
2149
|
+
conditions.push("s.kind = ?");
|
|
2150
|
+
values.push(effectiveKind);
|
|
2151
|
+
}
|
|
2152
|
+
if (filter?.lang) {
|
|
2153
|
+
conditions.push("s.lang = ?");
|
|
2154
|
+
values.push(filter.lang);
|
|
2155
|
+
}
|
|
2156
|
+
if (filter?.file) {
|
|
2157
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
2158
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
2159
|
+
}
|
|
2160
|
+
const where = conditions.join(" AND ");
|
|
2161
|
+
const bm25Rows = stmtFn(
|
|
2162
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
2163
|
+
-bm25(symbols_fts) AS score,
|
|
2164
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet,
|
|
2165
|
+
-- Keep this uncorrelated: referencing outer columns turns it into
|
|
2166
|
+
-- a per-row subquery and defeats the one-count-per-statement win.
|
|
2167
|
+
(SELECT COUNT(*) FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
2168
|
+
WHERE ${where}) AS total_count
|
|
2169
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
2170
|
+
WHERE ${where}
|
|
2171
|
+
ORDER BY
|
|
2172
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
2173
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
2174
|
+
ELSE 2 END,
|
|
2175
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
2176
|
+
LIMIT ?`
|
|
2177
|
+
).all(...values, ...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
2178
|
+
if (bm25Rows.length === 0) return { results: [], total: 0 };
|
|
2179
|
+
const total = Number(bm25Rows[0]?.total_count ?? 0);
|
|
2180
|
+
if (vectorsAvailable) {
|
|
2181
|
+
const queryVec = embedText(query);
|
|
2182
|
+
const candidateIds = bm25Rows.map((r) => r.id);
|
|
2183
|
+
const placeholders2 = candidateIds.map(() => "?").join(",");
|
|
2184
|
+
const vecRows = stmtFn(
|
|
2185
|
+
`SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders2})`
|
|
2186
|
+
).all(...candidateIds);
|
|
2187
|
+
const vecScores = vecRows.map((r) => ({
|
|
2188
|
+
id: r.symbol_id,
|
|
2189
|
+
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
2190
|
+
})).sort((a, b) => b.sim - a.sim);
|
|
2191
|
+
const bm25Rank = /* @__PURE__ */ new Map();
|
|
2192
|
+
bm25Rows.forEach((r, i) => {
|
|
2193
|
+
bm25Rank.set(r.id, i);
|
|
2194
|
+
});
|
|
2195
|
+
const vecRank = /* @__PURE__ */ new Map();
|
|
2196
|
+
vecScores.forEach((r, i) => {
|
|
2197
|
+
vecRank.set(r.id, i);
|
|
2198
|
+
});
|
|
2199
|
+
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
2200
|
+
const fusedScore = new Map(fused);
|
|
2201
|
+
const sorted = [...bm25Rows].sort(
|
|
2202
|
+
(a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
|
|
2203
|
+
);
|
|
2204
|
+
return {
|
|
2205
|
+
results: sorted.map(
|
|
2206
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
2207
|
+
),
|
|
2208
|
+
total
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
return {
|
|
2212
|
+
results: bm25Rows.map(
|
|
2213
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
2214
|
+
),
|
|
2215
|
+
total
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function searchRankedFallbackWithStatement(stmtFn, searchFn, getOrBuildBm25, query, filter, limit) {
|
|
2219
|
+
if (!query.trim()) {
|
|
2220
|
+
const total2 = countSearchWithStatement(stmtFn, query, filter);
|
|
2221
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
2222
|
+
return { results: searchFn(query, filter, { limit }), total: total2 };
|
|
2223
|
+
}
|
|
2224
|
+
const total = countSearchWithStatement(stmtFn, query, filter);
|
|
2225
|
+
if (total === 0) return { results: [], total: 0 };
|
|
2226
|
+
const candidates = searchFn(query, filter, {
|
|
2227
|
+
limit: SEARCH_CANDIDATE_SCAN_CAP
|
|
2228
|
+
});
|
|
2229
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
2230
|
+
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
2231
|
+
const bm25 = getOrBuildBm25();
|
|
2232
|
+
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
2233
|
+
const q = query.trim().toLowerCase();
|
|
2234
|
+
const rank = (id) => {
|
|
2235
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
2236
|
+
if (name === q) return 0;
|
|
2237
|
+
if (name.startsWith(q)) return 1;
|
|
2238
|
+
return 2;
|
|
2239
|
+
};
|
|
2240
|
+
scored.sort((a, b) => {
|
|
2241
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
2242
|
+
if (rankDiff !== 0) return rankDiff;
|
|
2243
|
+
const scoreDiff = b.score - a.score;
|
|
2244
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
2245
|
+
const left = expectDefined(candidateById.get(a.id));
|
|
2246
|
+
const right = expectDefined(candidateById.get(b.id));
|
|
2247
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
2248
|
+
});
|
|
2249
|
+
const qTokens = tokenise(query);
|
|
2250
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
2251
|
+
const c = expectDefined(candidateById.get(id));
|
|
2252
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
2253
|
+
});
|
|
2254
|
+
return { results, total };
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// src/codebase-index/writer-store-pool.ts
|
|
2258
|
+
var DEFAULT_MAX_WARM_STORES = 2;
|
|
2259
|
+
var StorePool = class {
|
|
2260
|
+
constructor(createStore, maxWarmStores = DEFAULT_MAX_WARM_STORES) {
|
|
2261
|
+
this.createStore = createStore;
|
|
2262
|
+
this.maxWarmStores = maxWarmStores;
|
|
2263
|
+
}
|
|
2264
|
+
createStore;
|
|
2265
|
+
maxWarmStores;
|
|
2266
|
+
stores = /* @__PURE__ */ new Map();
|
|
2267
|
+
keyByStore = /* @__PURE__ */ new WeakMap();
|
|
2268
|
+
clock = 0;
|
|
2269
|
+
key(projectRoot, indexDir) {
|
|
2270
|
+
return `${projectRoot}\0${indexDir ?? ""}`;
|
|
2271
|
+
}
|
|
2272
|
+
/** Borrow a store. Creates it on first access for this key. */
|
|
2273
|
+
acquire(projectRoot, opts) {
|
|
2274
|
+
const k = this.key(projectRoot, opts?.indexDir);
|
|
2275
|
+
let entry = this.stores.get(k);
|
|
2276
|
+
if (!entry) {
|
|
2277
|
+
const store = this.createStore(projectRoot, { indexDir: opts?.indexDir });
|
|
2278
|
+
entry = { store, refs: 0, lastUsed: 0 };
|
|
2279
|
+
this.stores.set(k, entry);
|
|
2280
|
+
this.keyByStore.set(store, k);
|
|
2281
|
+
}
|
|
2282
|
+
entry.refs++;
|
|
2283
|
+
entry.lastUsed = ++this.clock;
|
|
2284
|
+
return entry.store;
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Return the store to the pool. The connection stays warm for reuse, but the
|
|
2288
|
+
* pool may now close the least-recently-used *idle* connection to stay within
|
|
2289
|
+
* {@link maxWarmStores}. A store with outstanding refs is never closed.
|
|
2290
|
+
*/
|
|
2291
|
+
release(store) {
|
|
2292
|
+
const k = this.keyByStore.get(store);
|
|
2293
|
+
const entry = k === void 0 ? void 0 : this.stores.get(k);
|
|
2294
|
+
if (entry && entry.refs > 0) entry.refs--;
|
|
2295
|
+
this.trim();
|
|
2296
|
+
}
|
|
2297
|
+
trim() {
|
|
2298
|
+
if (this.stores.size <= this.maxWarmStores) return;
|
|
2299
|
+
const idle = [...this.stores.entries()].filter(([, entry]) => entry.refs === 0).sort((a, b) => a[1].lastUsed - b[1].lastUsed);
|
|
2300
|
+
let overflow = this.stores.size - this.maxWarmStores;
|
|
2301
|
+
for (const [k, entry] of idle) {
|
|
2302
|
+
if (overflow <= 0) break;
|
|
2303
|
+
try {
|
|
2304
|
+
entry.store.close();
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
this.stores.delete(k);
|
|
2308
|
+
overflow--;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
/** Close every pooled connection and drain the pool. Call on shutdown. */
|
|
2312
|
+
closeAll() {
|
|
2313
|
+
for (const entry of this.stores.values()) {
|
|
2314
|
+
try {
|
|
2315
|
+
entry.store.close();
|
|
2316
|
+
} catch {
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
this.stores.clear();
|
|
2320
|
+
}
|
|
2321
|
+
/** Remove one store from the pool. Used by tests that need isolation. */
|
|
2322
|
+
evict(projectRoot, indexDir) {
|
|
2323
|
+
const k = this.key(projectRoot, indexDir);
|
|
2324
|
+
const entry = this.stores.get(k);
|
|
2325
|
+
if (entry) {
|
|
2326
|
+
try {
|
|
2327
|
+
entry.store.close();
|
|
2328
|
+
} catch {
|
|
2329
|
+
}
|
|
2330
|
+
this.stores.delete(k);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
/** True when the pool holds a connection for the given key. */
|
|
2334
|
+
has(projectRoot, indexDir) {
|
|
2335
|
+
return this.stores.has(this.key(projectRoot, indexDir));
|
|
2336
|
+
}
|
|
2337
|
+
/** Number of warm connections currently held. */
|
|
2338
|
+
get size() {
|
|
2339
|
+
return this.stores.size;
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
|
|
2343
|
+
// src/codebase-index/writer.ts
|
|
2344
|
+
var DB_FILE2 = "index.db";
|
|
2345
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
2346
|
+
var IndexStore = class _IndexStore {
|
|
2347
|
+
db;
|
|
2348
|
+
atomicIndexUpdateActive = false;
|
|
2349
|
+
writeSavepointSequence = 0;
|
|
2350
|
+
indexDir;
|
|
2351
|
+
ftsAvailable = false;
|
|
2352
|
+
vectorsAvailable = false;
|
|
2353
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
2354
|
+
bm25Cache = null;
|
|
2355
|
+
bm25Dirty = true;
|
|
2356
|
+
stmt(sql) {
|
|
2357
|
+
const cached = this.stmtCache.get(sql);
|
|
2358
|
+
if (cached !== void 0) {
|
|
2359
|
+
this.stmtCache.delete(sql);
|
|
2360
|
+
this.stmtCache.set(sql, cached);
|
|
2361
|
+
return cached;
|
|
2362
|
+
}
|
|
2363
|
+
const s = this.db.prepare(sql);
|
|
2364
|
+
this.stmtCache.set(sql, s);
|
|
2365
|
+
if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
|
|
2366
|
+
const oldest = this.stmtCache.keys().next();
|
|
2367
|
+
if (!oldest.done) this.stmtCache.delete(oldest.value);
|
|
2368
|
+
}
|
|
2369
|
+
return s;
|
|
2370
|
+
}
|
|
2371
|
+
constructor(projectRoot, opts = {}) {
|
|
2372
|
+
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
2373
|
+
fs3.mkdirSync(this.indexDir, { recursive: true });
|
|
2374
|
+
const Database = loadDatabaseSync();
|
|
2375
|
+
this.db = new Database(path3.join(this.indexDir, DB_FILE2));
|
|
2376
|
+
applyIndexStorePragmas(this.db);
|
|
2377
|
+
this.initSchema();
|
|
2378
|
+
}
|
|
2379
|
+
runWithRetry(fn) {
|
|
2380
|
+
return runSqliteWithRetry(fn);
|
|
2381
|
+
}
|
|
2382
|
+
async runAtomicIndexUpdate(job) {
|
|
2383
|
+
if (this.atomicIndexUpdateActive) return job();
|
|
2384
|
+
this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
|
|
2385
|
+
this.atomicIndexUpdateActive = true;
|
|
2386
|
+
try {
|
|
2387
|
+
const result = await job();
|
|
2388
|
+
this.db.exec("COMMIT");
|
|
2389
|
+
return result;
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
try {
|
|
2392
|
+
this.db.exec("ROLLBACK");
|
|
2393
|
+
} catch {
|
|
2394
|
+
}
|
|
2395
|
+
throw error;
|
|
2396
|
+
} finally {
|
|
2397
|
+
this.atomicIndexUpdateActive = false;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
beginWriteTransaction() {
|
|
2401
|
+
if (this.atomicIndexUpdateActive) {
|
|
2402
|
+
const savepoint = `index_write_${++this.writeSavepointSequence}`;
|
|
2403
|
+
this.db.exec(`SAVEPOINT ${savepoint}`);
|
|
2404
|
+
return savepoint;
|
|
2405
|
+
}
|
|
2406
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
commitWriteTransaction(savepoint) {
|
|
2410
|
+
if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
2411
|
+
else this.db.exec("COMMIT");
|
|
2412
|
+
}
|
|
2413
|
+
rollbackWriteTransaction(savepoint) {
|
|
2414
|
+
if (savepoint) {
|
|
2415
|
+
this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
2416
|
+
this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
2417
|
+
} else {
|
|
2418
|
+
this.db.exec("ROLLBACK");
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
seedLangFamilies() {
|
|
2422
|
+
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
2423
|
+
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
2424
|
+
insert.run("", LANG_FAMILY_WILDCARD);
|
|
2425
|
+
}
|
|
2426
|
+
repairMissingColumns() {
|
|
2427
|
+
const expected = [
|
|
2428
|
+
{
|
|
2429
|
+
table: "files",
|
|
2430
|
+
columns: [
|
|
2431
|
+
["package", "TEXT NOT NULL DEFAULT ''"],
|
|
2432
|
+
["content_hash", "TEXT NOT NULL DEFAULT ''"]
|
|
2433
|
+
]
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
table: "refs",
|
|
2437
|
+
columns: [
|
|
2438
|
+
["lang", "TEXT NOT NULL DEFAULT ''"],
|
|
2439
|
+
["module", "TEXT"],
|
|
2440
|
+
["to_file", "TEXT"]
|
|
2441
|
+
]
|
|
2442
|
+
}
|
|
2443
|
+
];
|
|
2444
|
+
for (const { table, columns } of expected) {
|
|
2445
|
+
const present = new Set(
|
|
2446
|
+
this.db.prepare(`PRAGMA table_info(${table})`).all().flatMap(
|
|
2447
|
+
(row) => typeof row.name === "string" ? [row.name] : []
|
|
2448
|
+
)
|
|
2449
|
+
);
|
|
2450
|
+
if (present.size === 0) continue;
|
|
2451
|
+
for (const [name, type] of columns) {
|
|
2452
|
+
if (present.has(name)) continue;
|
|
2453
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
initSchema() {
|
|
2458
|
+
this.db.exec(METADATA_TABLE_SQL);
|
|
2459
|
+
const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
2460
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
2461
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
2462
|
+
this.db.exec(`
|
|
2463
|
+
DROP TABLE IF EXISTS symbols;
|
|
2464
|
+
DROP TABLE IF EXISTS files;
|
|
2465
|
+
DROP TABLE IF EXISTS refs;
|
|
2466
|
+
DROP TABLE IF EXISTS symbol_vectors;
|
|
2467
|
+
`);
|
|
2468
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
2469
|
+
this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(
|
|
2470
|
+
String(SCHEMA_VERSION),
|
|
2471
|
+
"version"
|
|
2472
|
+
);
|
|
2473
|
+
} else if (storedVersion === null) {
|
|
2474
|
+
this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run(
|
|
2475
|
+
"version",
|
|
2476
|
+
String(SCHEMA_VERSION)
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
this.db.exec(CORE_TABLES_SQL);
|
|
2480
|
+
this.db.exec(REFS_TABLE_SQL);
|
|
2481
|
+
this.repairMissingColumns();
|
|
2482
|
+
for (const sql of FILE_INDEX_SQL) this.db.exec(sql);
|
|
2483
|
+
for (const sql of SYMBOL_INDEX_SQL) this.db.exec(sql);
|
|
2484
|
+
for (const sql of REFS_INDEX_SQL) this.db.exec(sql);
|
|
2485
|
+
this.db.exec(LANG_FAMILY_TABLE_SQL);
|
|
2486
|
+
this.seedLangFamilies();
|
|
2487
|
+
try {
|
|
2488
|
+
const ftsSchema = this.stmt(
|
|
2489
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
|
|
2490
|
+
).get();
|
|
2491
|
+
if (ftsSchema?.sql?.includes("unicode61")) {
|
|
2492
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
2493
|
+
}
|
|
2494
|
+
this.db.exec(SYMBOLS_FTS_SQL);
|
|
2495
|
+
this.ftsAvailable = true;
|
|
2496
|
+
const symbolCount = Number(
|
|
2497
|
+
this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
|
|
2498
|
+
);
|
|
2499
|
+
const ftsCount = Number(
|
|
2500
|
+
this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
|
|
2501
|
+
);
|
|
2502
|
+
if (symbolCount !== ftsCount) {
|
|
2503
|
+
this.db.exec("DELETE FROM symbols_fts");
|
|
2504
|
+
if (vectorEmbeddingEnabled() && this.stmt(
|
|
2505
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='symbol_vectors'"
|
|
2506
|
+
).get() !== void 0) {
|
|
2507
|
+
this.db.exec("DELETE FROM symbol_vectors");
|
|
2508
|
+
}
|
|
2509
|
+
const rows = this.stmt(
|
|
2510
|
+
"SELECT id, name, signature, doc_comment FROM symbols ORDER BY id"
|
|
2511
|
+
).all();
|
|
2512
|
+
bulkInsertFtsWithStatement(
|
|
2513
|
+
(sql) => this.stmt(sql),
|
|
2514
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2515
|
+
this.ftsAvailable,
|
|
2516
|
+
rows.map((row) => ({
|
|
2517
|
+
id: row.id,
|
|
2518
|
+
text: buildIndexableText(row.name, row.signature, row.doc_comment)
|
|
2519
|
+
}))
|
|
2520
|
+
);
|
|
2521
|
+
this.invalidateBm25();
|
|
2522
|
+
}
|
|
2523
|
+
} catch {
|
|
2524
|
+
this.ftsAvailable = false;
|
|
2525
|
+
}
|
|
2526
|
+
try {
|
|
2527
|
+
if (vectorEmbeddingEnabled()) {
|
|
2528
|
+
this.db.exec(SYMBOL_VECTORS_TABLE_SQL);
|
|
2529
|
+
this.vectorsAvailable = true;
|
|
2530
|
+
} else {
|
|
2531
|
+
this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
|
|
2532
|
+
this.vectorsAvailable = false;
|
|
2533
|
+
}
|
|
2534
|
+
} catch {
|
|
2535
|
+
this.vectorsAvailable = false;
|
|
2536
|
+
}
|
|
2537
|
+
this.ensureNextSymbolIdSeeded();
|
|
2538
|
+
}
|
|
2539
|
+
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
2540
|
+
static MAX_SQL_VARS = 900;
|
|
2541
|
+
ensureNextSymbolIdSeeded() {
|
|
2542
|
+
const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
|
|
2543
|
+
_IndexStore.NEXT_SYMBOL_ID_KEY
|
|
2544
|
+
);
|
|
2545
|
+
if (existing?.value !== void 0) return;
|
|
2546
|
+
const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
2547
|
+
const next = (maxRows[0]?.m ?? 0) + 1;
|
|
2548
|
+
this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
|
|
2549
|
+
_IndexStore.NEXT_SYMBOL_ID_KEY,
|
|
2550
|
+
String(next)
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
allocateSymbolIds(count) {
|
|
2554
|
+
if (count <= 0) return this.getMaxSymbolId() + 1;
|
|
2555
|
+
this.ensureNextSymbolIdSeeded();
|
|
2556
|
+
const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
|
|
2557
|
+
_IndexStore.NEXT_SYMBOL_ID_KEY
|
|
2558
|
+
);
|
|
2559
|
+
const start = Math.max(1, Number(row?.value ?? 1) || 1);
|
|
2560
|
+
this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
|
|
2561
|
+
_IndexStore.NEXT_SYMBOL_ID_KEY,
|
|
2562
|
+
String(start + count)
|
|
2563
|
+
);
|
|
2564
|
+
return start;
|
|
2565
|
+
}
|
|
2566
|
+
invalidateIncomingRefsForFiles(files) {
|
|
2567
|
+
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
2568
|
+
const names = [];
|
|
2569
|
+
let cursor = 0;
|
|
2570
|
+
for (const take of inListChunks(files.length, _IndexStore.MAX_SQL_VARS)) {
|
|
2571
|
+
const bucket = padToInBucket(files.slice(cursor, cursor + take));
|
|
2572
|
+
cursor += take;
|
|
2573
|
+
const ph = placeholders(bucket.length);
|
|
2574
|
+
for (const row of this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${ph})`).all(
|
|
2575
|
+
...bucket
|
|
2576
|
+
)) {
|
|
2577
|
+
names.push(row.name);
|
|
2578
|
+
}
|
|
2579
|
+
this.stmt(
|
|
2580
|
+
`UPDATE refs SET to_id = NULL
|
|
2581
|
+
WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${ph}))`
|
|
2582
|
+
).run(...bucket);
|
|
2583
|
+
}
|
|
2584
|
+
return new Set(names);
|
|
2585
|
+
}
|
|
2586
|
+
resolveRefsForNamesUnsafe(names) {
|
|
2587
|
+
return resolveRefsForNamesUnsafe((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, names);
|
|
2588
|
+
}
|
|
2589
|
+
insertSymbols(symbols) {
|
|
2590
|
+
this.invalidateBm25();
|
|
2591
|
+
return this.runWithRetry(() => {
|
|
2592
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2593
|
+
try {
|
|
2594
|
+
let nextId = this.allocateSymbolIds(symbols.length);
|
|
2595
|
+
const result = [];
|
|
2596
|
+
const bulk = [];
|
|
2597
|
+
const ftsRows = [];
|
|
2598
|
+
const vectorRows = [];
|
|
2599
|
+
for (const s of symbols) {
|
|
2600
|
+
const id = nextId++;
|
|
2601
|
+
bulk.push({
|
|
2602
|
+
id,
|
|
2603
|
+
lang: s.lang,
|
|
2604
|
+
kind: s.kind,
|
|
2605
|
+
name: s.name,
|
|
2606
|
+
file: s.file,
|
|
2607
|
+
line: s.line,
|
|
2608
|
+
col: s.col,
|
|
2609
|
+
signature: s.signature,
|
|
2610
|
+
docComment: s.docComment,
|
|
2611
|
+
scope: s.scope,
|
|
2612
|
+
text: s.text
|
|
2613
|
+
});
|
|
2614
|
+
if (this.ftsAvailable) {
|
|
2615
|
+
ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
|
|
2616
|
+
}
|
|
2617
|
+
if (this.vectorsAvailable) {
|
|
2618
|
+
vectorRows.push({
|
|
2619
|
+
id,
|
|
2620
|
+
vector: encodeVector(
|
|
2621
|
+
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
2622
|
+
)
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
result.push({ ...s, id });
|
|
2626
|
+
}
|
|
2627
|
+
bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
|
|
2628
|
+
bulkInsertFtsWithStatement(
|
|
2629
|
+
(sql) => this.stmt(sql),
|
|
2630
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2631
|
+
this.ftsAvailable,
|
|
2632
|
+
ftsRows
|
|
2633
|
+
);
|
|
2634
|
+
if (this.vectorsAvailable) {
|
|
2635
|
+
bulkInsertVectorsWithStatement(
|
|
2636
|
+
(sql) => this.stmt(sql),
|
|
2637
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2638
|
+
vectorRows
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2642
|
+
return result;
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2645
|
+
throw err;
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
deleteSymbolsForFile(file) {
|
|
2650
|
+
this.invalidateBm25();
|
|
2651
|
+
this.runWithRetry(() => {
|
|
2652
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2653
|
+
try {
|
|
2654
|
+
const affectedNames = this.invalidateIncomingRefsForFiles([file]);
|
|
2655
|
+
if (this.ftsAvailable) {
|
|
2656
|
+
this.stmt(
|
|
2657
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2658
|
+
).run(file);
|
|
2659
|
+
}
|
|
2660
|
+
if (this.vectorsAvailable) {
|
|
2661
|
+
this.stmt(
|
|
2662
|
+
"DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2663
|
+
).run(file);
|
|
2664
|
+
}
|
|
2665
|
+
this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
|
|
2666
|
+
this.resolveRefsForNamesUnsafe(affectedNames);
|
|
2667
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2670
|
+
throw error;
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
deleteFile(file) {
|
|
2675
|
+
this.invalidateBm25();
|
|
2676
|
+
this.runWithRetry(() => {
|
|
2677
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2678
|
+
try {
|
|
2679
|
+
const affectedNames = this.invalidateIncomingRefsForFiles([file]);
|
|
2680
|
+
if (this.ftsAvailable) {
|
|
2681
|
+
this.stmt(
|
|
2682
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2683
|
+
).run(file);
|
|
2684
|
+
}
|
|
2685
|
+
if (this.vectorsAvailable) {
|
|
2686
|
+
this.stmt(
|
|
2687
|
+
"DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2688
|
+
).run(file);
|
|
2689
|
+
}
|
|
2690
|
+
this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
|
|
2691
|
+
file
|
|
2692
|
+
);
|
|
2693
|
+
this.stmt("DELETE FROM symbols WHERE file = ?").run(file);
|
|
2694
|
+
this.stmt("DELETE FROM files WHERE file = ?").run(file);
|
|
2695
|
+
this.resolveRefsForNamesUnsafe(affectedNames);
|
|
2696
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2699
|
+
throw err;
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
upsertFile(meta) {
|
|
2704
|
+
this.runWithRetry(() => {
|
|
2705
|
+
this.stmt(
|
|
2706
|
+
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
2707
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2708
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
2709
|
+
lang = excluded.lang,
|
|
2710
|
+
mtime_ms = excluded.mtime_ms,
|
|
2711
|
+
content_hash = excluded.content_hash,
|
|
2712
|
+
symbol_count = excluded.symbol_count,
|
|
2713
|
+
last_indexed = excluded.last_indexed`
|
|
2714
|
+
).run(
|
|
2715
|
+
meta.file,
|
|
2716
|
+
meta.lang,
|
|
2717
|
+
meta.mtimeMs,
|
|
2718
|
+
meta.contentHash ?? "",
|
|
2719
|
+
meta.symbolCount,
|
|
2720
|
+
meta.lastIndexed
|
|
2721
|
+
);
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
getFileMeta(file) {
|
|
2725
|
+
return getFileMetaWithStatement((sql) => this.stmt(sql), file);
|
|
2726
|
+
}
|
|
2727
|
+
getAllFileMetas() {
|
|
2728
|
+
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
2729
|
+
}
|
|
2730
|
+
setFilePackages(entries) {
|
|
2731
|
+
if (entries.size === 0) return;
|
|
2732
|
+
this.runWithRetry(() => {
|
|
2733
|
+
const update = this.stmt("UPDATE files SET package = ? WHERE file = ?");
|
|
2734
|
+
for (const [file, label] of entries) update.run(label, file);
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
getNamespaceDeclarations() {
|
|
2738
|
+
return getNamespaceDeclarationsWithStatement((sql) => this.stmt(sql));
|
|
2739
|
+
}
|
|
2740
|
+
getFilePackages() {
|
|
2741
|
+
return getFilePackagesWithStatement((sql) => this.stmt(sql));
|
|
2742
|
+
}
|
|
2743
|
+
getUnresolvedImports(onlyFiles) {
|
|
2744
|
+
return getUnresolvedImportsWithStatement(
|
|
2745
|
+
(sql) => this.stmt(sql),
|
|
2746
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2747
|
+
onlyFiles
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
applyImportResolutions(resolutions) {
|
|
2751
|
+
return applyImportResolutionsWithStatement(
|
|
2752
|
+
this.db,
|
|
2753
|
+
(sql) => this.stmt(sql),
|
|
2754
|
+
this.runWithRetry.bind(this),
|
|
2755
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2756
|
+
resolutions
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
search(query, filter, opts) {
|
|
2760
|
+
return searchWithStatement((sql) => this.stmt(sql), query, filter, opts);
|
|
2761
|
+
}
|
|
2762
|
+
countSearch(query, filter) {
|
|
2763
|
+
return countSearchWithStatement((sql) => this.stmt(sql), query, filter);
|
|
2764
|
+
}
|
|
2765
|
+
searchRanked(query, filter, limit) {
|
|
2766
|
+
return searchRankedWithStatement(
|
|
2767
|
+
(sql) => this.stmt(sql),
|
|
2768
|
+
this.search.bind(this),
|
|
2769
|
+
this.ftsAvailable,
|
|
2770
|
+
this.vectorsAvailable,
|
|
2771
|
+
this.getOrBuildBm25.bind(this),
|
|
2772
|
+
query,
|
|
2773
|
+
filter,
|
|
2774
|
+
limit
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
invalidateBm25() {
|
|
2778
|
+
this.bm25Dirty = true;
|
|
2779
|
+
this.bm25Cache = null;
|
|
2780
|
+
}
|
|
2781
|
+
getOrBuildBm25() {
|
|
2782
|
+
if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
|
|
2783
|
+
const docs = this.getAllIndexable();
|
|
2784
|
+
this.bm25Cache = buildBm25Index(docs);
|
|
2785
|
+
this.bm25Dirty = false;
|
|
2786
|
+
return this.bm25Cache;
|
|
2787
|
+
}
|
|
2788
|
+
getAllIndexable() {
|
|
2789
|
+
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
2790
|
+
}
|
|
2791
|
+
getMaxSymbolId() {
|
|
2792
|
+
return getMaxSymbolIdWithStatement((sql) => this.stmt(sql));
|
|
2793
|
+
}
|
|
2794
|
+
getStats() {
|
|
2795
|
+
return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
|
|
2796
|
+
}
|
|
2797
|
+
/** P2.5: minimal summary for search-response piggyback (see writer-admin). */
|
|
2798
|
+
getIndexSummary() {
|
|
2799
|
+
return getIndexSummaryWithStatement((sql) => this.stmt(sql));
|
|
2800
|
+
}
|
|
2801
|
+
setLastIndexed(ts) {
|
|
2802
|
+
this.runWithRetry(() => {
|
|
2803
|
+
this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(
|
|
2804
|
+
String(ts)
|
|
2805
|
+
);
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
getMetadata(key) {
|
|
2809
|
+
return getMetadataWithStatement((sql) => this.stmt(sql), key);
|
|
2810
|
+
}
|
|
2811
|
+
setMetadata(key, value) {
|
|
2812
|
+
this.runWithRetry(() => {
|
|
2813
|
+
this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
clearAll() {
|
|
2817
|
+
this.invalidateBm25();
|
|
2818
|
+
this.runWithRetry(() => {
|
|
2819
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2820
|
+
try {
|
|
2821
|
+
this.db.exec("DROP TABLE IF EXISTS refs");
|
|
2822
|
+
this.db.exec("DROP TABLE IF EXISTS symbols");
|
|
2823
|
+
this.db.exec("DROP TABLE IF EXISTS files");
|
|
2824
|
+
this.db.exec("DROP TABLE IF EXISTS metadata");
|
|
2825
|
+
if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
2826
|
+
this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
|
|
2827
|
+
this.stmtCache.clear();
|
|
2828
|
+
this.initSchema();
|
|
2829
|
+
this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
|
|
2830
|
+
_IndexStore.NEXT_SYMBOL_ID_KEY,
|
|
2831
|
+
"1"
|
|
2832
|
+
);
|
|
2833
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2836
|
+
throw err;
|
|
2837
|
+
}
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
insertRefs(fromId, refs) {
|
|
2841
|
+
this.runWithRetry(() => {
|
|
2842
|
+
this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
2843
|
+
if (refs.length === 0) return;
|
|
2844
|
+
bulkInsertRefsWithStatement(
|
|
2845
|
+
(sql) => this.stmt(sql),
|
|
2846
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2847
|
+
refs.map((ref) => ({ ...ref, fromId }))
|
|
2848
|
+
);
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
insertRefsBatch(refs) {
|
|
2852
|
+
if (refs.length === 0) return;
|
|
2853
|
+
this.runWithRetry(() => {
|
|
2854
|
+
bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refs);
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
commitBatch(entries, options = {}) {
|
|
2858
|
+
this.invalidateBm25();
|
|
2859
|
+
return this.runWithRetry(() => {
|
|
2860
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2861
|
+
try {
|
|
2862
|
+
const result = commitBatchWithStatement(
|
|
2863
|
+
(sql) => this.stmt(sql),
|
|
2864
|
+
_IndexStore.MAX_SQL_VARS,
|
|
2865
|
+
this.ftsAvailable,
|
|
2866
|
+
this.vectorsAvailable,
|
|
2867
|
+
this.allocateSymbolIds.bind(this),
|
|
2868
|
+
this.invalidateIncomingRefsForFiles.bind(this),
|
|
2869
|
+
this.resolveRefsForNamesUnsafe.bind(this),
|
|
2870
|
+
entries,
|
|
2871
|
+
options
|
|
2872
|
+
);
|
|
2873
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2874
|
+
return result;
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2877
|
+
throw err;
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
deleteRefsForFile(file) {
|
|
2882
|
+
this.runWithRetry(() => {
|
|
2883
|
+
this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
|
|
2884
|
+
file
|
|
2885
|
+
);
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
resolveRefs() {
|
|
2889
|
+
return this.runWithRetry(() => resolveRefsWithStatement((sql) => this.stmt(sql)));
|
|
2890
|
+
}
|
|
2891
|
+
resolveRefsForNames(names) {
|
|
2892
|
+
return this.runWithRetry(() => this.resolveRefsForNamesUnsafe(names));
|
|
2893
|
+
}
|
|
2894
|
+
replaceEmptyFile(meta) {
|
|
2895
|
+
this.invalidateBm25();
|
|
2896
|
+
this.runWithRetry(() => {
|
|
2897
|
+
const ownsTransaction = this.beginWriteTransaction();
|
|
2898
|
+
try {
|
|
2899
|
+
const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
|
|
2900
|
+
if (this.ftsAvailable) {
|
|
2901
|
+
this.stmt(
|
|
2902
|
+
"DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2903
|
+
).run(meta.file);
|
|
2904
|
+
}
|
|
2905
|
+
if (this.vectorsAvailable) {
|
|
2906
|
+
this.stmt(
|
|
2907
|
+
"DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2908
|
+
).run(meta.file);
|
|
2909
|
+
}
|
|
2910
|
+
this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
|
|
2911
|
+
meta.file
|
|
2912
|
+
);
|
|
2913
|
+
this.stmt("DELETE FROM symbols WHERE file = ?").run(meta.file);
|
|
2914
|
+
this.stmt(
|
|
2915
|
+
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
2916
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2917
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
2918
|
+
lang = excluded.lang,
|
|
2919
|
+
mtime_ms = excluded.mtime_ms,
|
|
2920
|
+
content_hash = excluded.content_hash,
|
|
2921
|
+
symbol_count = excluded.symbol_count,
|
|
2922
|
+
last_indexed = excluded.last_indexed`
|
|
2923
|
+
).run(
|
|
2924
|
+
meta.file,
|
|
2925
|
+
meta.lang,
|
|
2926
|
+
meta.mtimeMs,
|
|
2927
|
+
meta.contentHash ?? "",
|
|
2928
|
+
meta.symbolCount,
|
|
2929
|
+
meta.lastIndexed
|
|
2930
|
+
);
|
|
2931
|
+
this.resolveRefsForNamesUnsafe(affectedNames);
|
|
2932
|
+
this.commitWriteTransaction(ownsTransaction);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
this.rollbackWriteTransaction(ownsTransaction);
|
|
2935
|
+
throw err;
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
optimize() {
|
|
2940
|
+
try {
|
|
2941
|
+
this.db.exec("PRAGMA optimize");
|
|
2942
|
+
} catch {
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* P4.14: best-effort WAL checkpoint for idle-time maintenance.
|
|
2947
|
+
*
|
|
2948
|
+
* `wal_autocheckpoint` is PASSIVE and only attempts work after a COMMIT —
|
|
2949
|
+
* once writes stop, nothing fires again, so the WAL keeps whatever frames
|
|
2950
|
+
* the last burst left. This probes with PASSIVE first (never blocks; busy=1
|
|
2951
|
+
* means readers still hold WAL snapshots) and only issues the TRUNCATE —
|
|
2952
|
+
* which resets index.db-wal to zero bytes — when the checkpointer can
|
|
2953
|
+
* proceed immediately. Callers run this on the daemon's single thread, so
|
|
2954
|
+
* never wait on readers here: busy means "retry at the next idle window".
|
|
2955
|
+
*/
|
|
2956
|
+
checkpointWal() {
|
|
2957
|
+
try {
|
|
2958
|
+
const probe = this.db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get();
|
|
2959
|
+
if (Number(probe?.busy ?? 1) !== 0) return false;
|
|
2960
|
+
const done = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
2961
|
+
return Number(done?.busy ?? 1) === 0;
|
|
2962
|
+
} catch {
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
compactIfNeeded(options = {}) {
|
|
2967
|
+
const minBytes = options.minBytes ?? 256 * 1024 * 1024;
|
|
2968
|
+
const minFreeRatio = options.minFreeRatio ?? 0.35;
|
|
2969
|
+
try {
|
|
2970
|
+
const pageCount = Number(
|
|
2971
|
+
this.stmt("PRAGMA page_count").get()?.page_count ?? 0
|
|
2972
|
+
);
|
|
2973
|
+
const pageSize = Number(
|
|
2974
|
+
this.stmt("PRAGMA page_size").get()?.page_size ?? 0
|
|
2975
|
+
);
|
|
2976
|
+
const freePages = Number(
|
|
2977
|
+
this.stmt("PRAGMA freelist_count").get()?.freelist_count ?? 0
|
|
2978
|
+
);
|
|
2979
|
+
if (pageCount <= 0 || pageSize <= 0 || pageCount * pageSize < minBytes || freePages / pageCount < minFreeRatio) {
|
|
2980
|
+
return false;
|
|
2981
|
+
}
|
|
2982
|
+
this.runWithRetry(() => {
|
|
2983
|
+
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
2984
|
+
this.db.exec("VACUUM");
|
|
2985
|
+
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
2986
|
+
});
|
|
2987
|
+
return true;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return false;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
findIncomingCallsByName(symbolName, file, limit = 100) {
|
|
2993
|
+
return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
2994
|
+
}
|
|
2995
|
+
findOutgoingCallsByName(symbolName, file, limit = 100) {
|
|
2996
|
+
return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
2997
|
+
}
|
|
2998
|
+
findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
|
|
2999
|
+
return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
3000
|
+
}
|
|
3001
|
+
findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
|
|
3002
|
+
return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
3003
|
+
}
|
|
3004
|
+
findReachableSymbolIds(seedIds) {
|
|
3005
|
+
return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
|
|
3006
|
+
}
|
|
3007
|
+
findRefsTo(symbolId) {
|
|
3008
|
+
return findRefsToWithStatement((sql) => this.stmt(sql), symbolId);
|
|
3009
|
+
}
|
|
3010
|
+
findRefsFrom(symbolId) {
|
|
3011
|
+
return findRefsFromWithStatement((sql) => this.stmt(sql), symbolId);
|
|
3012
|
+
}
|
|
3013
|
+
getPackageGraph() {
|
|
3014
|
+
return getPackageGraphWithStatement((sql) => this.stmt(sql));
|
|
3015
|
+
}
|
|
3016
|
+
getFileGraph(packageFilter) {
|
|
3017
|
+
return getFileGraphWithStatement((sql) => this.stmt(sql), packageFilter);
|
|
3018
|
+
}
|
|
3019
|
+
getSymbolGraph(fileFilter) {
|
|
3020
|
+
return getSymbolGraphWithStatement((sql) => this.stmt(sql), fileFilter);
|
|
3021
|
+
}
|
|
3022
|
+
getAllSymbols() {
|
|
3023
|
+
return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
|
|
3024
|
+
}
|
|
3025
|
+
getAllResolvedRefs() {
|
|
3026
|
+
return getAllResolvedRefsWithStatement((sql) => this.stmt(sql));
|
|
3027
|
+
}
|
|
3028
|
+
getAllImportRefs() {
|
|
3029
|
+
return getAllImportRefsWithStatement((sql) => this.stmt(sql));
|
|
3030
|
+
}
|
|
3031
|
+
close() {
|
|
3032
|
+
this.stmtCache.clear();
|
|
3033
|
+
this.bm25Dirty = true;
|
|
3034
|
+
this.bm25Cache = null;
|
|
3035
|
+
try {
|
|
3036
|
+
this.db.close();
|
|
3037
|
+
} catch {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
var indexStorePool = new StorePool(
|
|
3042
|
+
(projectRoot, opts) => new IndexStore(projectRoot, opts)
|
|
3043
|
+
);
|
|
3044
|
+
|
|
3045
|
+
// src/codebase-index/indexer.ts
|
|
3046
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
3047
|
+
import { execFile } from "node:child_process";
|
|
3048
|
+
import { createHash } from "node:crypto";
|
|
3049
|
+
import * as fs5 from "node:fs/promises";
|
|
3050
|
+
import { availableParallelism } from "node:os";
|
|
3051
|
+
import * as path5 from "node:path";
|
|
3052
|
+
import {
|
|
3053
|
+
DEFAULT_WALK_IGNORE_DIRS,
|
|
3054
|
+
indexParallelBatchSize,
|
|
3055
|
+
isFrugalPerf
|
|
3056
|
+
} from "@wrongstack/core/utils";
|
|
3057
|
+
|
|
3058
|
+
// src/codebase-index/content-hash.ts
|
|
3059
|
+
var PRIME64_1 = 0x9e3779b185ebca87n;
|
|
3060
|
+
var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
|
|
3061
|
+
var PRIME64_3 = 0x165667b19e3779f9n;
|
|
3062
|
+
var PRIME64_4 = 0x85ebca77c2b2ae63n;
|
|
3063
|
+
var PRIME64_5 = 0x27d4eb2f165667c5n;
|
|
3064
|
+
var MASK64 = 0xffffffffffffffffn;
|
|
3065
|
+
function mul64(a, b) {
|
|
3066
|
+
return (a & MASK64) * (b & MASK64) & MASK64;
|
|
3067
|
+
}
|
|
3068
|
+
function rotl64(x, n) {
|
|
3069
|
+
const v = x & MASK64;
|
|
3070
|
+
return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
|
|
3071
|
+
}
|
|
3072
|
+
function readU64LE(buf, off) {
|
|
3073
|
+
let v = 0n;
|
|
3074
|
+
for (let i = 7; i >= 0; i--) {
|
|
3075
|
+
v = v << 8n | BigInt(buf[off + i] ?? 0);
|
|
3076
|
+
}
|
|
3077
|
+
return v & MASK64;
|
|
3078
|
+
}
|
|
3079
|
+
function readU32LE(buf, off) {
|
|
3080
|
+
return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
|
|
3081
|
+
}
|
|
3082
|
+
function xxh64Round(acc, lane) {
|
|
3083
|
+
return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
|
|
3084
|
+
}
|
|
3085
|
+
function xxh64MergeRound(acc, val) {
|
|
3086
|
+
return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
|
|
3087
|
+
}
|
|
3088
|
+
function xxhash64Hex(buf, explicitLen) {
|
|
3089
|
+
const length = explicitLen ?? buf.length;
|
|
3090
|
+
let h;
|
|
3091
|
+
let off = 0;
|
|
3092
|
+
if (length >= 32) {
|
|
3093
|
+
let v1 = PRIME64_1 + PRIME64_2 & MASK64;
|
|
3094
|
+
let v2 = PRIME64_2;
|
|
3095
|
+
let v3 = 0n;
|
|
3096
|
+
let v4 = 0n - PRIME64_1 & MASK64;
|
|
3097
|
+
const end32 = length - 32;
|
|
3098
|
+
while (off <= end32) {
|
|
3099
|
+
v1 = xxh64Round(v1, readU64LE(buf, off));
|
|
3100
|
+
v2 = xxh64Round(v2, readU64LE(buf, off + 8));
|
|
3101
|
+
v3 = xxh64Round(v3, readU64LE(buf, off + 16));
|
|
3102
|
+
v4 = xxh64Round(v4, readU64LE(buf, off + 24));
|
|
3103
|
+
off += 32;
|
|
3104
|
+
}
|
|
3105
|
+
h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
|
|
3106
|
+
h = xxh64MergeRound(h, v1);
|
|
3107
|
+
h = xxh64MergeRound(h, v2);
|
|
3108
|
+
h = xxh64MergeRound(h, v3);
|
|
3109
|
+
h = xxh64MergeRound(h, v4);
|
|
3110
|
+
} else {
|
|
3111
|
+
h = PRIME64_5;
|
|
3112
|
+
}
|
|
3113
|
+
h = h + BigInt(length) & MASK64;
|
|
3114
|
+
while (off + 8 <= length) {
|
|
3115
|
+
const k1 = xxh64Round(0n, readU64LE(buf, off));
|
|
3116
|
+
h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
|
|
3117
|
+
off += 8;
|
|
3118
|
+
}
|
|
3119
|
+
if (off + 4 <= length) {
|
|
3120
|
+
h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
|
|
3121
|
+
off += 4;
|
|
3122
|
+
}
|
|
3123
|
+
while (off < length) {
|
|
3124
|
+
h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
|
|
3125
|
+
off += 1;
|
|
3126
|
+
}
|
|
3127
|
+
h = (h ^ h >> 33n) & MASK64;
|
|
3128
|
+
h = mul64(h, PRIME64_2);
|
|
3129
|
+
h = (h ^ h >> 29n) & MASK64;
|
|
3130
|
+
h = mul64(h, PRIME64_3);
|
|
3131
|
+
h = (h ^ h >> 32n) & MASK64;
|
|
3132
|
+
return h.toString(16).padStart(16, "0");
|
|
3133
|
+
}
|
|
3134
|
+
function xxhash64String(content) {
|
|
3135
|
+
return xxhash64Hex(new TextEncoder().encode(content));
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// src/codebase-index/module-resolver.ts
|
|
3139
|
+
import * as path4 from "node:path";
|
|
3140
|
+
var EXTENSIONS = {
|
|
3141
|
+
js: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"],
|
|
3142
|
+
py: [".py", ".pyi"],
|
|
3143
|
+
rs: [".rs"],
|
|
3144
|
+
jvm: [".java", ".kt", ".scala"],
|
|
3145
|
+
c: [".h", ".hpp", ".hh", ".hxx", ".c", ".cpp", ".cc", ".cxx"],
|
|
3146
|
+
ruby: [".rb"],
|
|
3147
|
+
go: [".go"]
|
|
3148
|
+
};
|
|
3149
|
+
var DIRECTORY_ENTRIES = {
|
|
3150
|
+
js: ["index"],
|
|
3151
|
+
py: ["__init__"],
|
|
3152
|
+
rs: ["mod"],
|
|
3153
|
+
ruby: ["index"]
|
|
3154
|
+
};
|
|
3155
|
+
function normalizeNamespace(value) {
|
|
3156
|
+
return value.replace(/::|[\\/]/g, ".").replace(/^\.+|\.+$/g, "").toLowerCase();
|
|
3157
|
+
}
|
|
3158
|
+
var ModuleResolver = class {
|
|
3159
|
+
structure;
|
|
3160
|
+
/** Lowercased portable path → the path as indexed (case is preserved). */
|
|
3161
|
+
byPath;
|
|
3162
|
+
/** Lowercased portable directory → files directly inside it, as indexed. */
|
|
3163
|
+
byDir;
|
|
3164
|
+
/** Normalized namespace → the file declaring it (first by path, stable). */
|
|
3165
|
+
byNamespace;
|
|
3166
|
+
constructor(structure, files, namespaces = []) {
|
|
3167
|
+
this.structure = structure;
|
|
3168
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
3169
|
+
this.byDir = /* @__PURE__ */ new Map();
|
|
3170
|
+
this.byNamespace = /* @__PURE__ */ new Map();
|
|
3171
|
+
const dirsByKey = /* @__PURE__ */ new Map();
|
|
3172
|
+
for (const file of files) {
|
|
3173
|
+
const portable = toPortablePath(file);
|
|
3174
|
+
const pathKey = portable.toLowerCase();
|
|
3175
|
+
const priorPath = this.byPath.get(pathKey);
|
|
3176
|
+
if (priorPath !== void 0 && priorPath !== file) this.byPath.delete(pathKey);
|
|
3177
|
+
else this.byPath.set(pathKey, file);
|
|
3178
|
+
const dir = path4.posix.dirname(portable);
|
|
3179
|
+
const dirKey = dir.toLowerCase();
|
|
3180
|
+
const knownDir = dirsByKey.get(dirKey);
|
|
3181
|
+
if (knownDir === void 0) {
|
|
3182
|
+
dirsByKey.set(dirKey, dir);
|
|
3183
|
+
this.byDir.set(dirKey, [file]);
|
|
3184
|
+
} else if (knownDir === dir) {
|
|
3185
|
+
this.byDir.get(dirKey)?.push(file);
|
|
3186
|
+
} else {
|
|
3187
|
+
dirsByKey.delete(dirKey);
|
|
3188
|
+
this.byDir.delete(dirKey);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
for (const { name, file } of namespaces) {
|
|
3192
|
+
const lang = detectLang(file);
|
|
3193
|
+
if (!lang) continue;
|
|
3194
|
+
const key = `${languageFamily(lang)}:${normalizeNamespace(name)}`;
|
|
3195
|
+
if (normalizeNamespace(name) && !this.byNamespace.has(key)) {
|
|
3196
|
+
this.byNamespace.set(key, file);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Resolve `specifier` as written in `fromFile`.
|
|
3202
|
+
* Returns the indexed target path, or `undefined` when it is external or
|
|
3203
|
+
* cannot be located.
|
|
3204
|
+
*/
|
|
3205
|
+
resolve(fromFile, lang, specifier) {
|
|
3206
|
+
const spec = specifier.trim().replace(/\\/g, "/");
|
|
3207
|
+
if (!spec) return void 0;
|
|
3208
|
+
const from = toPortablePath(fromFile);
|
|
3209
|
+
switch (languageFamily(lang)) {
|
|
3210
|
+
case "js":
|
|
3211
|
+
return this.resolveJs(from, spec);
|
|
3212
|
+
case "go":
|
|
3213
|
+
return this.resolveGo(spec);
|
|
3214
|
+
case "py":
|
|
3215
|
+
return this.resolvePython(from, spec);
|
|
3216
|
+
case "rs":
|
|
3217
|
+
return this.resolveRust(from, spec);
|
|
3218
|
+
case "jvm":
|
|
3219
|
+
return this.resolveJvm(spec);
|
|
3220
|
+
case "c":
|
|
3221
|
+
return this.resolveInclude(from, spec);
|
|
3222
|
+
case "ruby":
|
|
3223
|
+
return this.resolveRuby(from, spec);
|
|
3224
|
+
case "dotnet":
|
|
3225
|
+
case "php":
|
|
3226
|
+
case "elixir":
|
|
3227
|
+
case "haskell":
|
|
3228
|
+
return this.resolveNamespace(lang, spec);
|
|
3229
|
+
default:
|
|
3230
|
+
return void 0;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Resolve a namespace specifier to the file declaring it.
|
|
3235
|
+
*
|
|
3236
|
+
* Tried whole first, then with the trailing segment dropped: `using Foo.Bar`
|
|
3237
|
+
* names a namespace outright, while PHP's `use App\Models\User` names a
|
|
3238
|
+
* *class* inside `App\Models`, so the prefix is what was declared.
|
|
3239
|
+
*/
|
|
3240
|
+
resolveNamespace(lang, spec) {
|
|
3241
|
+
const family = languageFamily(lang);
|
|
3242
|
+
const normalized = normalizeNamespace(spec);
|
|
3243
|
+
const exact = this.byNamespace.get(`${family}:${normalized}`);
|
|
3244
|
+
if (exact) return exact;
|
|
3245
|
+
const segments = normalized.split(".").filter(Boolean);
|
|
3246
|
+
if (segments.length < 2) return void 0;
|
|
3247
|
+
return this.byNamespace.get(`${family}:${segments.slice(0, -1).join(".")}`);
|
|
3248
|
+
}
|
|
3249
|
+
// ─── Lookup primitives ──────────────────────────────────────────────────────
|
|
3250
|
+
lookup(candidate) {
|
|
3251
|
+
return this.byPath.get(path4.posix.normalize(candidate).toLowerCase());
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Try `base` verbatim, then `base` + each extension, then each directory
|
|
3255
|
+
* entry point inside `base`.
|
|
3256
|
+
*/
|
|
3257
|
+
lookupWithExtensions(base, family) {
|
|
3258
|
+
const direct = this.lookup(base);
|
|
3259
|
+
if (direct) return direct;
|
|
3260
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
3261
|
+
const suffix = path4.posix.extname(base);
|
|
3262
|
+
const stem = suffix && extensions.includes(suffix) ? base.slice(0, -suffix.length) : base;
|
|
3263
|
+
for (const ext of extensions) {
|
|
3264
|
+
const hit = this.lookup(`${stem}${ext}`);
|
|
3265
|
+
if (hit) return hit;
|
|
3266
|
+
}
|
|
3267
|
+
for (const entry of DIRECTORY_ENTRIES[family] ?? []) {
|
|
3268
|
+
for (const ext of extensions) {
|
|
3269
|
+
const hit = this.lookup(path4.posix.join(base, `${entry}${ext}`));
|
|
3270
|
+
if (hit) return hit;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return void 0;
|
|
3274
|
+
}
|
|
3275
|
+
/**
|
|
3276
|
+
* A representative indexed file inside `dir`, for ecosystems whose import
|
|
3277
|
+
* unit is a directory rather than a file (Go packages, JVM wildcard imports).
|
|
3278
|
+
*
|
|
3279
|
+
* The choice is deterministic — a file named after the directory, else the
|
|
3280
|
+
* first by name — so the same import always produces the same edge. Package
|
|
3281
|
+
* grouping is unaffected either way: every file in the directory carries the
|
|
3282
|
+
* same package label, so the package-level edge is exact regardless of which
|
|
3283
|
+
* member represents it.
|
|
3284
|
+
*/
|
|
3285
|
+
representativeIn(dir, family) {
|
|
3286
|
+
const members = this.byDir.get(path4.posix.normalize(dir).toLowerCase());
|
|
3287
|
+
if (!members?.length) return void 0;
|
|
3288
|
+
const extensions = EXTENSIONS[family] ?? [];
|
|
3289
|
+
const eligible = members.filter((file) => extensions.includes(path4.posix.extname(toPortablePath(file)).toLowerCase())).sort((a, b) => toPortablePath(a).localeCompare(toPortablePath(b)));
|
|
3290
|
+
if (eligible.length === 0) return void 0;
|
|
3291
|
+
const base = path4.posix.basename(path4.posix.normalize(dir)).toLowerCase();
|
|
3292
|
+
const named = eligible.find(
|
|
3293
|
+
(file) => path4.posix.basename(toPortablePath(file)).split(".")[0]?.toLowerCase() === base
|
|
3294
|
+
);
|
|
3295
|
+
return named ?? eligible[0];
|
|
3296
|
+
}
|
|
3297
|
+
// ─── Per-family resolution ──────────────────────────────────────────────────
|
|
3298
|
+
/** Relative specifiers, then workspace package names and their subpaths. */
|
|
3299
|
+
resolveJs(fromFile, spec) {
|
|
3300
|
+
if (spec.startsWith(".")) {
|
|
3301
|
+
const absolute = path4.posix.join(path4.posix.dirname(fromFile), spec);
|
|
3302
|
+
return this.lookupWithExtensions(absolute, "js");
|
|
3303
|
+
}
|
|
3304
|
+
const owner = this.structure.roots.find(
|
|
3305
|
+
(root) => root.kind === "npm" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
3306
|
+
);
|
|
3307
|
+
if (!owner?.importPath) return void 0;
|
|
3308
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
3309
|
+
if (!subpath) {
|
|
3310
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, "src/index"), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "index"), "js");
|
|
3311
|
+
}
|
|
3312
|
+
return this.lookupWithExtensions(path4.posix.join(owner.dir, subpath), "js") ?? this.lookupWithExtensions(path4.posix.join(owner.dir, "src", subpath), "js");
|
|
3313
|
+
}
|
|
3314
|
+
/** Go import paths are absolute module paths; a package is a directory. */
|
|
3315
|
+
resolveGo(spec) {
|
|
3316
|
+
const owner = this.structure.roots.find(
|
|
3317
|
+
(root) => root.kind === "go" && root.importPath !== void 0 && (spec === root.importPath || spec.startsWith(`${root.importPath}/`))
|
|
3318
|
+
);
|
|
3319
|
+
if (!owner?.importPath) return void 0;
|
|
3320
|
+
const subpath = spec.slice(owner.importPath.length).replace(/^\//, "");
|
|
3321
|
+
return this.representativeIn(path4.posix.join(owner.dir, subpath), "go");
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* `import a.b.c` / `from a.b import c`, plus PEP 328 relative imports whose
|
|
3325
|
+
* leading dots the extractor preserves (`.sibling`, `..parent.mod`).
|
|
3326
|
+
*/
|
|
3327
|
+
resolvePython(fromFile, spec) {
|
|
3328
|
+
const leadingDots = /^\.*/.exec(spec)?.[0].length ?? 0;
|
|
3329
|
+
if (leadingDots > 0) {
|
|
3330
|
+
let base = path4.posix.dirname(fromFile);
|
|
3331
|
+
for (let i = 1; i < leadingDots; i++) base = path4.posix.dirname(base);
|
|
3332
|
+
const rest = spec.slice(leadingDots).split(".").filter(Boolean);
|
|
3333
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest), "py");
|
|
3334
|
+
}
|
|
3335
|
+
const segments = spec.split(".").filter(Boolean);
|
|
3336
|
+
if (segments.length === 0) return void 0;
|
|
3337
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "python").flatMap((root) => root.sourceRoots);
|
|
3338
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
3339
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, ...segments), "py");
|
|
3340
|
+
if (hit) return hit;
|
|
3341
|
+
if (segments.length > 1) {
|
|
3342
|
+
const parent = this.lookupWithExtensions(
|
|
3343
|
+
path4.posix.join(base, ...segments.slice(0, -1)),
|
|
3344
|
+
"py"
|
|
3345
|
+
);
|
|
3346
|
+
if (parent) return parent;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
/** `use crate::a::b`, `use super::x`, `use self::y`, `use other_crate::z`. */
|
|
3352
|
+
resolveRust(fromFile, spec) {
|
|
3353
|
+
const segments = spec.split("::").filter(Boolean);
|
|
3354
|
+
if (segments.length === 0) return void 0;
|
|
3355
|
+
const head = segments[0];
|
|
3356
|
+
if (head === "self" || head === "super") {
|
|
3357
|
+
let base = path4.posix.dirname(fromFile);
|
|
3358
|
+
for (const segment of segments) {
|
|
3359
|
+
if (segment === "super") base = path4.posix.dirname(base);
|
|
3360
|
+
else if (segment !== "self") break;
|
|
3361
|
+
}
|
|
3362
|
+
const rest2 = segments.filter((segment) => segment !== "self" && segment !== "super");
|
|
3363
|
+
return this.lookupWithExtensions(path4.posix.join(base, ...rest2), "rs");
|
|
3364
|
+
}
|
|
3365
|
+
const owningCrate = findOwningRoot(this.structure, fromFile, ["cargo"]);
|
|
3366
|
+
const crate = head === "crate" ? owningCrate : this.structure.roots.find(
|
|
3367
|
+
(root) => root.kind === "cargo" && root.importPath === head?.replace(/-/g, "_")
|
|
3368
|
+
);
|
|
3369
|
+
if (!crate) {
|
|
3370
|
+
return this.lookupWithExtensions(
|
|
3371
|
+
path4.posix.join(path4.posix.dirname(fromFile), ...segments),
|
|
3372
|
+
"rs"
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
const rest = segments.slice(1);
|
|
3376
|
+
for (const base of crate.sourceRoots) {
|
|
3377
|
+
const parent = rest.length > 1 ? this.lookupWithExtensions(path4.posix.join(base, ...rest.slice(0, -1)), "rs") : void 0;
|
|
3378
|
+
const exact = this.lookupWithExtensions(path4.posix.join(base, ...rest), "rs");
|
|
3379
|
+
const hit = exact ?? parent ?? this.lookupWithExtensions(path4.posix.join(base, "lib"), "rs");
|
|
3380
|
+
if (hit) return hit;
|
|
3381
|
+
}
|
|
3382
|
+
return void 0;
|
|
3383
|
+
}
|
|
3384
|
+
/** `com.example.Thing` and `com.example.*` against JVM source roots. */
|
|
3385
|
+
resolveJvm(spec) {
|
|
3386
|
+
const segments = spec.split(".").filter(Boolean);
|
|
3387
|
+
if (segments.length === 0) return void 0;
|
|
3388
|
+
const sourceRoots = this.structure.roots.filter((root) => root.kind === "maven" || root.kind === "gradle").flatMap((root) => root.sourceRoots);
|
|
3389
|
+
const wildcard = segments[segments.length - 1] === "*";
|
|
3390
|
+
const parts = wildcard ? segments.slice(0, -1) : segments;
|
|
3391
|
+
for (const base of [...sourceRoots, this.structure.projectRoot]) {
|
|
3392
|
+
const target = path4.posix.join(base, ...parts);
|
|
3393
|
+
const hit = wildcard ? this.representativeIn(target, "jvm") : this.lookupWithExtensions(target, "jvm");
|
|
3394
|
+
if (hit) return hit;
|
|
3395
|
+
}
|
|
3396
|
+
return void 0;
|
|
3397
|
+
}
|
|
3398
|
+
/** `#include "foo/bar.h"` — quoted form only; `<…>` is a system header. */
|
|
3399
|
+
resolveInclude(fromFile, spec) {
|
|
3400
|
+
const relative2 = this.lookupWithExtensions(
|
|
3401
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
3402
|
+
"c"
|
|
3403
|
+
);
|
|
3404
|
+
if (relative2) return relative2;
|
|
3405
|
+
for (const base of [
|
|
3406
|
+
path4.posix.join(this.structure.projectRoot, "include"),
|
|
3407
|
+
this.structure.projectRoot
|
|
3408
|
+
]) {
|
|
3409
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "c");
|
|
3410
|
+
if (hit) return hit;
|
|
3411
|
+
}
|
|
3412
|
+
return void 0;
|
|
3413
|
+
}
|
|
3414
|
+
/** `require_relative 'x'` is relative; `require 'x'` is looked up under lib/. */
|
|
3415
|
+
resolveRuby(fromFile, spec) {
|
|
3416
|
+
const relative2 = this.lookupWithExtensions(
|
|
3417
|
+
path4.posix.join(path4.posix.dirname(fromFile), spec),
|
|
3418
|
+
"ruby"
|
|
3419
|
+
);
|
|
3420
|
+
if (relative2) return relative2;
|
|
3421
|
+
for (const base of [
|
|
3422
|
+
path4.posix.join(this.structure.projectRoot, "lib"),
|
|
3423
|
+
this.structure.projectRoot
|
|
3424
|
+
]) {
|
|
3425
|
+
const hit = this.lookupWithExtensions(path4.posix.join(base, spec), "ruby");
|
|
3426
|
+
if (hit) return hit;
|
|
3427
|
+
}
|
|
3428
|
+
return void 0;
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
|
|
3432
|
+
// src/codebase-index/parser-worker-pool.ts
|
|
3433
|
+
import * as fs4 from "node:fs";
|
|
3434
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3435
|
+
import { Worker } from "node:worker_threads";
|
|
3436
|
+
var WORKER_POOL_THRESHOLD = 500;
|
|
3437
|
+
function resolveWorkerPoolThreshold() {
|
|
3438
|
+
const raw = process.env["WRONGSTACK_INDEX_WORKER_THRESHOLD"];
|
|
3439
|
+
if (raw === void 0) return WORKER_POOL_THRESHOLD;
|
|
3440
|
+
if (!/^\d+$/.test(raw)) return WORKER_POOL_THRESHOLD;
|
|
3441
|
+
return Number.parseInt(raw, 10);
|
|
3442
|
+
}
|
|
3443
|
+
var ParserWorkerPool = class {
|
|
3444
|
+
constructor(maxWorkers = defaultWorkerCount()) {
|
|
3445
|
+
this.maxWorkers = maxWorkers;
|
|
3446
|
+
}
|
|
3447
|
+
maxWorkers;
|
|
3448
|
+
workers = [];
|
|
3449
|
+
nextBatchId = 1;
|
|
3450
|
+
pending = /* @__PURE__ */ new Map();
|
|
3451
|
+
creating = false;
|
|
3452
|
+
unavailable = false;
|
|
3453
|
+
/**
|
|
3454
|
+
* True if the pool is available for use. Returns false when:
|
|
3455
|
+
* - Worker threads aren't supported (sandbox, exotic runtime)
|
|
3456
|
+
* - The built worker script can't be found
|
|
3457
|
+
* - Pool creation was attempted and failed
|
|
3458
|
+
*/
|
|
3459
|
+
isAvailable() {
|
|
3460
|
+
return !this.unavailable && this.workers.length > 0;
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Lazily create the worker pool. Returns true if the pool is ready, false
|
|
3464
|
+
* if it's unavailable (caller should fall back to inline parsing).
|
|
3465
|
+
*/
|
|
3466
|
+
async ensureReady() {
|
|
3467
|
+
if (this.isAvailable()) return true;
|
|
3468
|
+
if (this.unavailable) return false;
|
|
3469
|
+
if (this.creating) {
|
|
3470
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
3471
|
+
return this.isAvailable();
|
|
3472
|
+
}
|
|
3473
|
+
this.creating = true;
|
|
3474
|
+
try {
|
|
3475
|
+
const url = resolveWorkerScriptUrl();
|
|
3476
|
+
if (!url) {
|
|
3477
|
+
this.unavailable = true;
|
|
3478
|
+
return false;
|
|
3479
|
+
}
|
|
3480
|
+
for (let i = 0; i < this.maxWorkers; i++) {
|
|
3481
|
+
try {
|
|
3482
|
+
const w = new Worker(url, { name: `wstack-parser-${i}` });
|
|
3483
|
+
w.unref();
|
|
3484
|
+
w.on("message", (msg) => this.handleMessage(msg));
|
|
3485
|
+
w.on("error", (err) => this.handleError(err, w));
|
|
3486
|
+
w.on("exit", () => this.retireByReference(w));
|
|
3487
|
+
this.workers.push({ worker: w, workerId: w.threadId, busy: false });
|
|
3488
|
+
} catch {
|
|
3489
|
+
if (this.workers.length === 0) {
|
|
3490
|
+
this.unavailable = true;
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
break;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
return this.workers.length > 0;
|
|
3497
|
+
} finally {
|
|
3498
|
+
this.creating = false;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Parse files in parallel across the worker pool. Returns a flat
|
|
3503
|
+
* `FileSymbols[]` in completion order (caller matches by file path).
|
|
3504
|
+
*
|
|
3505
|
+
* Content is pre-read by the main thread (for the content-hash check)
|
|
3506
|
+
* and passed to workers to avoid a second disk read. Files are
|
|
3507
|
+
* distributed round-robin across workers.
|
|
3508
|
+
*/
|
|
3509
|
+
async parseFiles(files) {
|
|
3510
|
+
if (!this.isAvailable()) {
|
|
3511
|
+
throw new Error("ParserWorkerPool.parseFiles called before ensureReady() succeeded");
|
|
3512
|
+
}
|
|
3513
|
+
if (files.length === 0) return [];
|
|
3514
|
+
const batchId = this.nextBatchId++;
|
|
3515
|
+
const workerCount = Math.min(this.workers.length, files.length);
|
|
3516
|
+
const chunks = Array.from(
|
|
3517
|
+
{ length: workerCount },
|
|
3518
|
+
() => []
|
|
3519
|
+
);
|
|
3520
|
+
for (let i = 0; i < files.length; i++) {
|
|
3521
|
+
chunks[i % workerCount].push(files[i]);
|
|
3522
|
+
}
|
|
3523
|
+
return new Promise((resolve2, reject) => {
|
|
3524
|
+
const pendingChunks = /* @__PURE__ */ new Map();
|
|
3525
|
+
this.pending.set(batchId, {
|
|
3526
|
+
resolve: resolve2,
|
|
3527
|
+
reject,
|
|
3528
|
+
accumulated: [],
|
|
3529
|
+
pendingChunks,
|
|
3530
|
+
settled: false
|
|
3531
|
+
});
|
|
3532
|
+
for (let i = 0; i < workerCount; i++) {
|
|
3533
|
+
const pw = this.workers[i];
|
|
3534
|
+
pw.busy = true;
|
|
3535
|
+
pendingChunks.set(pw.workerId, chunks[i]);
|
|
3536
|
+
pw.worker.postMessage({ type: "parse", id: batchId, files: chunks[i] });
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
/** Shut down all workers. Safe to call multiple times. */
|
|
3541
|
+
async shutdown() {
|
|
3542
|
+
const workers = this.workers.map((w) => w.worker);
|
|
3543
|
+
this.workers = [];
|
|
3544
|
+
this.unavailable = false;
|
|
3545
|
+
for (const [, p] of this.pending) {
|
|
3546
|
+
if (p.settled) continue;
|
|
3547
|
+
p.settled = true;
|
|
3548
|
+
p.reject(new Error("ParserWorkerPool shut down"));
|
|
3549
|
+
}
|
|
3550
|
+
this.pending.clear();
|
|
3551
|
+
for (const w of workers) {
|
|
3552
|
+
try {
|
|
3553
|
+
w.postMessage({ type: "shutdown" });
|
|
3554
|
+
} catch {
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
await Promise.allSettled(
|
|
3558
|
+
workers.map(
|
|
3559
|
+
(w) => Promise.race([
|
|
3560
|
+
new Promise((resolve2) => {
|
|
3561
|
+
w.once("exit", () => resolve2());
|
|
3562
|
+
}),
|
|
3563
|
+
new Promise((resolve2) => setTimeout(() => resolve2(), 2e3))
|
|
3564
|
+
]).then(() => {
|
|
3565
|
+
if (!w.threadId) return;
|
|
3566
|
+
return w.terminate().catch(() => {
|
|
3567
|
+
});
|
|
3568
|
+
})
|
|
3569
|
+
)
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
handleMessage(msg) {
|
|
3573
|
+
const batch = this.pending.get(msg.id);
|
|
3574
|
+
if (!batch) return;
|
|
3575
|
+
const worker = this.workers.find((w) => w.workerId === msg.workerId);
|
|
3576
|
+
if (worker) worker.busy = false;
|
|
3577
|
+
batch.pendingChunks.delete(msg.workerId);
|
|
3578
|
+
batch.accumulated.push(...msg.results);
|
|
3579
|
+
if (batch.pendingChunks.size === 0) {
|
|
3580
|
+
if (batch.settled) return;
|
|
3581
|
+
batch.settled = true;
|
|
3582
|
+
this.pending.delete(msg.id);
|
|
3583
|
+
batch.resolve(batch.accumulated);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Remove a worker from the pool and salvage any chunk it still owed.
|
|
3588
|
+
*
|
|
3589
|
+
* Idempotent by workerId — `error` and `exit` can both fire for one
|
|
3590
|
+
* death, and a worker may die while no batch references it. When the
|
|
3591
|
+
* dead worker owed files to an in-flight batch and other workers remain,
|
|
3592
|
+
* those files are re-parsed inline on this thread (one fewer worker
|
|
3593
|
+
* should cost latency, not correctness). When it was the last worker,
|
|
3594
|
+
* every remaining batch rejects so the indexer's existing inline
|
|
3595
|
+
* fallback takes over the whole batch.
|
|
3596
|
+
*/
|
|
3597
|
+
retireWorker(workerId) {
|
|
3598
|
+
const entry = this.workers.find((w) => w.workerId === workerId);
|
|
3599
|
+
if (!entry) return;
|
|
3600
|
+
this.workers = this.workers.filter((w) => w.workerId !== workerId);
|
|
3601
|
+
for (const [batchId, batch] of [...this.pending]) {
|
|
3602
|
+
const orphaned = batch.pendingChunks.get(workerId);
|
|
3603
|
+
if (!orphaned) continue;
|
|
3604
|
+
if (this.workers.length === 0) {
|
|
3605
|
+
this.pending.delete(batchId);
|
|
3606
|
+
this.unavailable = true;
|
|
3607
|
+
if (!batch.settled) {
|
|
3608
|
+
batch.settled = true;
|
|
3609
|
+
batch.reject(new Error("ParserWorkerPool: all workers died mid-batch"));
|
|
3610
|
+
}
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
void this.reparseInline(batch, orphaned, workerId);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Salvage path: re-parse an orphaned chunk on this thread. Files that
|
|
3618
|
+
* fail here stay absent from the results — same contract as a per-file
|
|
3619
|
+
* error inside a live worker (see handleMessage).
|
|
3620
|
+
*/
|
|
3621
|
+
async reparseInline(batch, orphaned, workerId) {
|
|
3622
|
+
try {
|
|
3623
|
+
const { parseFileContent } = await import("./parser-dispatch-VPOFDO2U.js");
|
|
3624
|
+
for (const item of orphaned) {
|
|
3625
|
+
if (batch.settled) return;
|
|
3626
|
+
try {
|
|
3627
|
+
const parsed = await parseFileContent(item.file, item.content, item.lang);
|
|
3628
|
+
batch.accumulated.push(parsed);
|
|
3629
|
+
} catch {
|
|
3630
|
+
}
|
|
3631
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
3632
|
+
}
|
|
3633
|
+
} finally {
|
|
3634
|
+
this.finishSalvage(batch, workerId);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Terminal tail of a salvage — runs on every exit path. Kept free of
|
|
3639
|
+
* control flow inside a `finally` (noUnsafeFinally): releases the
|
|
3640
|
+
* pending-marker and resolves the batch if this was its last chunk.
|
|
3641
|
+
*/
|
|
3642
|
+
finishSalvage(batch, workerId) {
|
|
3643
|
+
batch.pendingChunks.delete(workerId);
|
|
3644
|
+
if (batch.pendingChunks.size === 0) {
|
|
3645
|
+
for (const [batchId, tracked] of this.pending) {
|
|
3646
|
+
if (tracked === batch) {
|
|
3647
|
+
if (batch.settled) return;
|
|
3648
|
+
batch.settled = true;
|
|
3649
|
+
this.pending.delete(batchId);
|
|
3650
|
+
batch.resolve(batch.accumulated);
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Retire by worker object rather than threadId. `threadId` is -1 before
|
|
3658
|
+
* the worker emits `online`, so a death during script load would make a
|
|
3659
|
+
* threadId-keyed lookup silently no-op and leak the entry (with its
|
|
3660
|
+
* pending chunk) — reference identity is correct in every case.
|
|
3661
|
+
*/
|
|
3662
|
+
retireByReference(source) {
|
|
3663
|
+
const entry = this.workers.find((w) => w.worker === source);
|
|
3664
|
+
if (entry) this.retireWorker(entry.workerId);
|
|
3665
|
+
}
|
|
3666
|
+
handleError(err, source) {
|
|
3667
|
+
void err;
|
|
3668
|
+
this.retireByReference(source);
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
3671
|
+
function defaultWorkerCount() {
|
|
3672
|
+
const cores = globalThis.navigator?.hardwareConcurrency ?? 4;
|
|
3673
|
+
return Math.max(1, Math.min(4, cores - 1));
|
|
3674
|
+
}
|
|
3675
|
+
function resolveWorkerScriptUrl() {
|
|
3676
|
+
const override = process.env["WRONGSTACK_PARSER_WORKER_SCRIPT"];
|
|
3677
|
+
if (override) return pathToFileURL(override);
|
|
3678
|
+
for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
|
|
3679
|
+
try {
|
|
3680
|
+
const url = new URL(rel, import.meta.url);
|
|
3681
|
+
if (url.protocol === "file:" && fs4.existsSync(fileURLToPath(url))) return url;
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
return null;
|
|
3686
|
+
}
|
|
3687
|
+
var _pool = null;
|
|
3688
|
+
function getParserPool() {
|
|
3689
|
+
_pool ??= new ParserWorkerPool();
|
|
3690
|
+
return _pool;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/codebase-index/indexer.ts
|
|
3694
|
+
var YIELD_EVERY_N = 50;
|
|
3695
|
+
function resolveParallelBatch() {
|
|
3696
|
+
return indexParallelBatchSize(availableParallelism());
|
|
3697
|
+
}
|
|
3698
|
+
function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
3699
|
+
const threshold = resolveWorkerPoolThreshold();
|
|
3700
|
+
if (threshold === 0) return false;
|
|
3701
|
+
return !isFrugalPerf() && candidateFileCount >= threshold && parseBatchCount > 1;
|
|
3702
|
+
}
|
|
3703
|
+
function yieldEventLoop() {
|
|
3704
|
+
return new Promise((resolve2) => setImmediate(resolve2));
|
|
3705
|
+
}
|
|
3706
|
+
function throwIfAborted(signal) {
|
|
3707
|
+
if (!signal?.aborted) return;
|
|
3708
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
3709
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
|
|
3710
|
+
}
|
|
3711
|
+
function isAbortError(err) {
|
|
3712
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
3713
|
+
}
|
|
3714
|
+
var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
|
|
3715
|
+
var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
|
|
3716
|
+
var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
|
|
3717
|
+
var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
|
|
3718
|
+
var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
|
|
3719
|
+
var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
|
|
3720
|
+
var IndexSourceChangedError = class extends Error {
|
|
3721
|
+
name = "IndexSourceChangedError";
|
|
3722
|
+
};
|
|
3723
|
+
function isWithinProject(projectRoot, file) {
|
|
3724
|
+
const rel = path5.relative(projectRoot, file);
|
|
3725
|
+
return rel !== "" && !rel.startsWith(`..${path5.sep}`) && rel !== ".." && !path5.isAbsolute(rel);
|
|
3726
|
+
}
|
|
3727
|
+
function isMissingPathError(err) {
|
|
3728
|
+
const code = err?.code;
|
|
3729
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
3730
|
+
}
|
|
3731
|
+
function normalizeComparablePath(value) {
|
|
3732
|
+
const resolved = path5.resolve(value);
|
|
3733
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3734
|
+
}
|
|
3735
|
+
function gitOutput(projectRoot, args) {
|
|
3736
|
+
return new Promise((resolve2, reject) => {
|
|
3737
|
+
execFile(
|
|
3738
|
+
"git",
|
|
3739
|
+
["-C", projectRoot, ...args],
|
|
3740
|
+
{
|
|
3741
|
+
encoding: "buffer",
|
|
3742
|
+
maxBuffer: MAX_GIT_FILE_LIST_BYTES,
|
|
3743
|
+
windowsHide: true
|
|
3744
|
+
},
|
|
3745
|
+
(error, stdout) => {
|
|
3746
|
+
if (error) reject(error);
|
|
3747
|
+
else resolve2(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
3748
|
+
}
|
|
3749
|
+
);
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
async function findGitSourceFiles(projectRoot, ignore, signal) {
|
|
3753
|
+
try {
|
|
3754
|
+
throwIfAborted(signal);
|
|
3755
|
+
const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
|
|
3756
|
+
if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
|
|
3757
|
+
throwIfAborted(signal);
|
|
3758
|
+
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
3759
|
+
const [output, statusOutput, stagedOutput] = await Promise.all([
|
|
3760
|
+
gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
|
|
3761
|
+
gitOutput(projectRoot, [
|
|
3762
|
+
"status",
|
|
3763
|
+
"--porcelain=v1",
|
|
3764
|
+
"-z",
|
|
3765
|
+
"--untracked-files=all",
|
|
3766
|
+
"--ignored=no"
|
|
3767
|
+
]),
|
|
3768
|
+
gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
|
|
3769
|
+
]);
|
|
3770
|
+
throwIfAborted(signal);
|
|
3771
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
3772
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
3773
|
+
const statusRecords = statusOutput.toString("utf8").split("\0");
|
|
3774
|
+
for (let i = 0; i < statusRecords.length; i++) {
|
|
3775
|
+
const record = statusRecords[i];
|
|
3776
|
+
if (!record) continue;
|
|
3777
|
+
const status = record.slice(0, 2);
|
|
3778
|
+
const changedPath = path5.resolve(projectRoot, record.slice(3));
|
|
3779
|
+
dirty.add(changedPath);
|
|
3780
|
+
if (status.includes("D")) deleted.add(changedPath);
|
|
3781
|
+
if (status.includes("R") || status.includes("C")) {
|
|
3782
|
+
const source = statusRecords[++i];
|
|
3783
|
+
if (source) dirty.add(path5.resolve(projectRoot, source));
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
const files = [];
|
|
3787
|
+
for (const relative2 of output.toString("utf8").split("\0")) {
|
|
3788
|
+
if (!relative2) continue;
|
|
3789
|
+
const portable = relative2.replace(/\\/g, "/");
|
|
3790
|
+
if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path5.posix.basename(portable))) {
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
const full = path5.resolve(projectRoot, relative2);
|
|
3794
|
+
if (deleted.has(full)) continue;
|
|
3795
|
+
const ext = path5.extname(relative2).toLowerCase();
|
|
3796
|
+
if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
|
|
3797
|
+
}
|
|
3798
|
+
const snapshot = createHash("sha256").update(stagedOutput).update("\0").update(statusOutput);
|
|
3799
|
+
const indexedFiles = new Set(files);
|
|
3800
|
+
for (const dirtyFile of [...dirty].sort()) {
|
|
3801
|
+
if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
|
|
3802
|
+
snapshot.update("\0").update(dirtyFile).update("\0");
|
|
3803
|
+
snapshot.update(xxhash64String(await fs5.readFile(dirtyFile, "utf8")));
|
|
3804
|
+
}
|
|
3805
|
+
return {
|
|
3806
|
+
files,
|
|
3807
|
+
trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
|
|
3808
|
+
snapshotKey: snapshot.digest("hex")
|
|
3809
|
+
};
|
|
3810
|
+
} catch {
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
3815
|
+
const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
|
|
3816
|
+
if (gitFiles) {
|
|
3817
|
+
return {
|
|
3818
|
+
files: gitFiles.files,
|
|
3819
|
+
complete: true,
|
|
3820
|
+
errors: [],
|
|
3821
|
+
trustedUnchanged: gitFiles.trustedUnchanged,
|
|
3822
|
+
snapshotKey: gitFiles.snapshotKey
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
const results = [];
|
|
3826
|
+
const errors = [];
|
|
3827
|
+
let complete = true;
|
|
3828
|
+
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
3829
|
+
const indexableExts = new Set(INDEXABLE_EXTENSIONS);
|
|
3830
|
+
let dirCount = 0;
|
|
3831
|
+
const walk = async (dir) => {
|
|
3832
|
+
throwIfAborted(signal);
|
|
3833
|
+
if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
|
|
3834
|
+
await yieldEventLoop();
|
|
3835
|
+
throwIfAborted(signal);
|
|
3836
|
+
}
|
|
3837
|
+
let entries;
|
|
3838
|
+
try {
|
|
3839
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
complete = false;
|
|
3842
|
+
errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
dirCount++;
|
|
3846
|
+
for (const e of entries) {
|
|
3847
|
+
if (ignoreSet.has(e.name)) continue;
|
|
3848
|
+
const full = path5.join(dir, e.name);
|
|
3849
|
+
const rel = path5.relative(projectRoot, full).replace(/\\/g, "/");
|
|
3850
|
+
if (e.isDirectory()) {
|
|
3851
|
+
if (isGitIgnored(rel, true)) continue;
|
|
3852
|
+
await walk(full);
|
|
3853
|
+
} else if (e.isFile()) {
|
|
3854
|
+
if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
|
|
3855
|
+
const ext = path5.extname(e.name).toLowerCase();
|
|
3856
|
+
if (indexableExts.has(ext) || detectLang(full) !== null) {
|
|
3857
|
+
results.push(full);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
await walk(projectRoot);
|
|
3863
|
+
return { files: results, complete, errors };
|
|
3864
|
+
}
|
|
3865
|
+
function assignRefsToSymbols2(refs, symbols) {
|
|
3866
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
3867
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
3868
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3869
|
+
const assigned = [];
|
|
3870
|
+
for (const ref of refs) {
|
|
3871
|
+
let owner;
|
|
3872
|
+
for (const symbol of ordered) {
|
|
3873
|
+
if (symbol.line > ref.line) break;
|
|
3874
|
+
owner = symbol;
|
|
3875
|
+
}
|
|
3876
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
3877
|
+
if (!owner || owner.id <= 0) continue;
|
|
3878
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
3879
|
+
if (seen.has(key)) continue;
|
|
3880
|
+
seen.add(key);
|
|
3881
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
3882
|
+
}
|
|
3883
|
+
return assigned;
|
|
3884
|
+
}
|
|
3885
|
+
async function resolveProjectRelations(store, projectRoot, opts) {
|
|
3886
|
+
if (opts.signal?.aborted) return;
|
|
3887
|
+
try {
|
|
3888
|
+
const indexedFiles = store.getAllFileMetas().map((meta) => meta.file);
|
|
3889
|
+
if (indexedFiles.length === 0) return;
|
|
3890
|
+
const structure = await detectModuleRoots(projectRoot, indexedFiles);
|
|
3891
|
+
if (opts.signal?.aborted) return;
|
|
3892
|
+
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
3893
|
+
const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
|
|
3894
|
+
const pending = store.getUnresolvedImports(opts.onlyFiles);
|
|
3895
|
+
const resolutions = [];
|
|
3896
|
+
for (const entry of pending) {
|
|
3897
|
+
const toFile = resolver.resolve(entry.fromFile, entry.lang, entry.module);
|
|
3898
|
+
if (toFile && toFile !== entry.fromFile) {
|
|
3899
|
+
resolutions.push({ ...entry, toFile });
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
if (opts.signal?.aborted) return;
|
|
3903
|
+
store.applyImportResolutions(resolutions);
|
|
3904
|
+
} catch (err) {
|
|
3905
|
+
opts.errors.push(`relation resolution: ${err instanceof Error ? err.message : String(err)}`);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
async function runIndexer(_ctx, opts) {
|
|
3909
|
+
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
3910
|
+
try {
|
|
3911
|
+
return await runIndexerWithStore(store, opts);
|
|
3912
|
+
} finally {
|
|
3913
|
+
try {
|
|
3914
|
+
store.close();
|
|
3915
|
+
} catch {
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
async function runIndexerWithStore(store, opts) {
|
|
3920
|
+
let result;
|
|
3921
|
+
try {
|
|
3922
|
+
result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
if (!(error instanceof IndexSourceChangedError)) throw error;
|
|
3925
|
+
result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
|
|
3926
|
+
}
|
|
3927
|
+
if (!opts.files) store.compactIfNeeded();
|
|
3928
|
+
return result;
|
|
3929
|
+
}
|
|
3930
|
+
async function runIndexerAtomic(store, opts) {
|
|
3931
|
+
const { projectRoot, langs, ignore = [], signal } = opts;
|
|
3932
|
+
const relationGraphVersion = "2";
|
|
3933
|
+
const refResolutionVersion = "2";
|
|
3934
|
+
const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
|
|
3935
|
+
const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
|
|
3936
|
+
const startMs = Date.now();
|
|
3937
|
+
const errors = [];
|
|
3938
|
+
const langStats = {};
|
|
3939
|
+
let filesIndexed = 0;
|
|
3940
|
+
let filesParsed = 0;
|
|
3941
|
+
let filesSkipped = 0;
|
|
3942
|
+
let filesEmpty = 0;
|
|
3943
|
+
let filesFailed = 0;
|
|
3944
|
+
let symbolsIndexed = 0;
|
|
3945
|
+
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
3946
|
+
let files;
|
|
3947
|
+
let discoveredFiles = null;
|
|
3948
|
+
let discoveryComplete = true;
|
|
3949
|
+
let trustedUnchanged;
|
|
3950
|
+
let discoverySnapshotKey;
|
|
3951
|
+
if (opts.files && opts.files.length > 0) {
|
|
3952
|
+
files = opts.files.map((f) => path5.resolve(projectRoot, f)).filter((f) => {
|
|
3953
|
+
if (!isWithinProject(projectRoot, f)) return false;
|
|
3954
|
+
const rel = path5.relative(projectRoot, f).replace(/\\/g, "/");
|
|
3955
|
+
return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path5.basename(f)) && !isGitIgnored(rel, false);
|
|
3956
|
+
});
|
|
3957
|
+
} else {
|
|
3958
|
+
const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
3959
|
+
files = discovery.files;
|
|
3960
|
+
errors.push(...discovery.errors);
|
|
3961
|
+
discoveryComplete = discovery.complete;
|
|
3962
|
+
discoveredFiles = new Set(files);
|
|
3963
|
+
trustedUnchanged = discovery.trustedUnchanged;
|
|
3964
|
+
discoverySnapshotKey = discovery.snapshotKey;
|
|
3965
|
+
}
|
|
3966
|
+
if (langs && langs.length > 0) {
|
|
3967
|
+
const langSet = new Set(langs);
|
|
3968
|
+
files = files.filter((f) => {
|
|
3969
|
+
const lang = detectLang(f);
|
|
3970
|
+
return lang ? langSet.has(lang) : false;
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
if (force) store.clearAll();
|
|
3974
|
+
const existingMeta = /* @__PURE__ */ new Map();
|
|
3975
|
+
if (!force) {
|
|
3976
|
+
for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
|
|
3977
|
+
}
|
|
3978
|
+
const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
|
|
3979
|
+
if (!snapshotTrusted) trustedUnchanged = void 0;
|
|
3980
|
+
const totalFilesForProgress = files.length;
|
|
3981
|
+
let filesPreSkipped = 0;
|
|
3982
|
+
if (!force && trustedUnchanged) {
|
|
3983
|
+
files = files.filter((file) => {
|
|
3984
|
+
const meta = existingMeta.get(file);
|
|
3985
|
+
if (!meta || !trustedUnchanged.has(file)) return true;
|
|
3986
|
+
langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
|
|
3987
|
+
symbolsIndexed += meta.symbolCount;
|
|
3988
|
+
filesSkipped++;
|
|
3989
|
+
filesPreSkipped++;
|
|
3990
|
+
return false;
|
|
3991
|
+
});
|
|
3992
|
+
if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
|
|
3993
|
+
}
|
|
3994
|
+
const parallelBatch = resolveParallelBatch();
|
|
3995
|
+
const parserPoolCandidateCount = files.length;
|
|
3996
|
+
let filesSinceLastYield = 0;
|
|
3997
|
+
for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
|
|
3998
|
+
const batchEnd = Math.min(batchStart + parallelBatch, files.length);
|
|
3999
|
+
const batchFiles = files.slice(batchStart, batchEnd);
|
|
4000
|
+
opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
|
|
4001
|
+
filesSinceLastYield += batchFiles.length;
|
|
4002
|
+
if (filesSinceLastYield >= YIELD_EVERY_N) {
|
|
4003
|
+
filesSinceLastYield = 0;
|
|
4004
|
+
await yieldEventLoop();
|
|
4005
|
+
if (isFrugalPerf()) {
|
|
4006
|
+
await new Promise((r) => setTimeout(r, 8));
|
|
4007
|
+
}
|
|
4008
|
+
throwIfAborted(signal);
|
|
4009
|
+
}
|
|
4010
|
+
const statOpts = signal ? { signal } : {};
|
|
4011
|
+
const statReadParse = await Promise.allSettled(
|
|
4012
|
+
batchFiles.map(
|
|
4013
|
+
async (file) => {
|
|
4014
|
+
let stat2;
|
|
4015
|
+
try {
|
|
4016
|
+
stat2 = await fs5.stat(file, statOpts);
|
|
4017
|
+
} catch (e) {
|
|
4018
|
+
if (isAbortError(e)) throw e;
|
|
4019
|
+
return {
|
|
4020
|
+
file,
|
|
4021
|
+
stat: null,
|
|
4022
|
+
lang: "",
|
|
4023
|
+
parsed: null,
|
|
4024
|
+
error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
|
|
4025
|
+
missing: isMissingPathError(e)
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
|
|
4029
|
+
const lang = detectLang(file);
|
|
4030
|
+
if (!lang) return { file, stat: stat2, lang: "", parsed: null };
|
|
4031
|
+
if (stat2.size > MAX_INDEX_FILE_BYTES) {
|
|
4032
|
+
return {
|
|
4033
|
+
file,
|
|
4034
|
+
stat: stat2,
|
|
4035
|
+
lang,
|
|
4036
|
+
parsed: null,
|
|
4037
|
+
error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
4038
|
+
};
|
|
4039
|
+
}
|
|
4040
|
+
const meta = existingMeta.get(file);
|
|
4041
|
+
let content;
|
|
4042
|
+
try {
|
|
4043
|
+
content = await fs5.readFile(file, { encoding: "utf8", signal });
|
|
4044
|
+
} catch (e) {
|
|
4045
|
+
if (isAbortError(e)) throw e;
|
|
4046
|
+
return {
|
|
4047
|
+
file,
|
|
4048
|
+
stat: stat2,
|
|
4049
|
+
lang,
|
|
4050
|
+
parsed: null,
|
|
4051
|
+
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
const contentHash = xxhash64String(content);
|
|
4055
|
+
if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
|
|
4056
|
+
return {
|
|
4057
|
+
file,
|
|
4058
|
+
stat: stat2,
|
|
4059
|
+
lang,
|
|
4060
|
+
parsed: null,
|
|
4061
|
+
content,
|
|
4062
|
+
contentHash,
|
|
4063
|
+
skippedMeta: { ...meta, mtimeMs: Math.floor(stat2.mtimeMs) }
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
return { file, stat: stat2, lang, parsed: null, content, contentHash };
|
|
4067
|
+
}
|
|
4068
|
+
)
|
|
4069
|
+
);
|
|
4070
|
+
const toParse = [];
|
|
4071
|
+
for (let pi = 0; pi < statReadParse.length; pi++) {
|
|
4072
|
+
const s = statReadParse[pi];
|
|
4073
|
+
if (s.status !== "fulfilled") continue;
|
|
4074
|
+
const r = s.value;
|
|
4075
|
+
if (r.error || r.skippedMeta || !r.lang || r.parsed) continue;
|
|
4076
|
+
if (r.content === void 0) continue;
|
|
4077
|
+
toParse.push({
|
|
4078
|
+
index: pi,
|
|
4079
|
+
file: batchFiles[pi],
|
|
4080
|
+
content: r.content,
|
|
4081
|
+
lang: r.lang
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
if (toParse.length > 0) {
|
|
4085
|
+
let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
|
|
4086
|
+
if (pool) {
|
|
4087
|
+
try {
|
|
4088
|
+
await pool.ensureReady();
|
|
4089
|
+
const parsedResults = await pool.parseFiles(
|
|
4090
|
+
toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
|
|
4091
|
+
);
|
|
4092
|
+
const byFile = new Map(parsedResults.map((r) => [r.file, r]));
|
|
4093
|
+
for (const item of toParse) {
|
|
4094
|
+
const parsed = byFile.get(item.file);
|
|
4095
|
+
const settled = statReadParse[item.index];
|
|
4096
|
+
if (settled.status !== "fulfilled") continue;
|
|
4097
|
+
if (parsed) {
|
|
4098
|
+
settled.value.parsed = parsed;
|
|
4099
|
+
} else {
|
|
4100
|
+
settled.value.error = `parse error: worker returned no result for ${item.file}`;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
} catch {
|
|
4104
|
+
pool = null;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
if (!pool) {
|
|
4108
|
+
const parsedAll = await parseFilesContent(
|
|
4109
|
+
toParse.map((p) => ({ file: p.file, content: p.content, lang: p.lang }))
|
|
4110
|
+
);
|
|
4111
|
+
for (let pi2 = 0; pi2 < parsedAll.length && pi2 < toParse.length; pi2++) {
|
|
4112
|
+
const settled = statReadParse[toParse[pi2].index];
|
|
4113
|
+
if (settled.status !== "fulfilled") continue;
|
|
4114
|
+
const slot = parsedAll[pi2];
|
|
4115
|
+
if (slot.result) {
|
|
4116
|
+
settled.value.parsed = slot.result;
|
|
4117
|
+
} else {
|
|
4118
|
+
settled.value.error = `parse error: ${slot.error ?? `no result for ${toParse[pi2].file}`}`;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
const batchEntries = [];
|
|
4124
|
+
const deleteForFiles = [];
|
|
4125
|
+
for (let fi = 0; fi < statReadParse.length; fi++) {
|
|
4126
|
+
const settled = statReadParse[fi];
|
|
4127
|
+
const file = expectDefined2(batchFiles[fi]);
|
|
4128
|
+
if (settled.status === "rejected") {
|
|
4129
|
+
const err = settled.reason;
|
|
4130
|
+
if (err instanceof Error && isAbortError(err)) throw err;
|
|
4131
|
+
errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4132
|
+
filesFailed++;
|
|
4133
|
+
continue;
|
|
4134
|
+
}
|
|
4135
|
+
const result = settled.value;
|
|
4136
|
+
if (result.error) {
|
|
4137
|
+
if (result.missing) store.deleteFile(file);
|
|
4138
|
+
errors.push(`${file}: ${result.error}`);
|
|
4139
|
+
filesFailed++;
|
|
4140
|
+
continue;
|
|
4141
|
+
}
|
|
4142
|
+
const { stat: stat2, lang, parsed } = result;
|
|
4143
|
+
if (result.skippedMeta) {
|
|
4144
|
+
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
4145
|
+
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
4146
|
+
filesSkipped++;
|
|
4147
|
+
const stored = existingMeta.get(file);
|
|
4148
|
+
if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
|
|
4149
|
+
store.upsertFile({
|
|
4150
|
+
file,
|
|
4151
|
+
lang,
|
|
4152
|
+
mtimeMs: result.skippedMeta.mtimeMs,
|
|
4153
|
+
symbolCount: result.skippedMeta.symbolCount,
|
|
4154
|
+
lastIndexed: Date.now(),
|
|
4155
|
+
contentHash: result.skippedMeta.contentHash
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
if (!lang || !parsed) {
|
|
4161
|
+
if (lang) {
|
|
4162
|
+
store.upsertFile({
|
|
4163
|
+
file,
|
|
4164
|
+
lang,
|
|
4165
|
+
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
4166
|
+
symbolCount: 0,
|
|
4167
|
+
lastIndexed: Date.now(),
|
|
4168
|
+
contentHash: result.contentHash ?? ""
|
|
4169
|
+
});
|
|
4170
|
+
filesEmpty++;
|
|
4171
|
+
}
|
|
4172
|
+
continue;
|
|
4173
|
+
}
|
|
4174
|
+
if (parsed.symbols.length === 0) {
|
|
4175
|
+
store.replaceEmptyFile({
|
|
4176
|
+
file,
|
|
4177
|
+
lang,
|
|
4178
|
+
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
4179
|
+
symbolCount: 0,
|
|
4180
|
+
lastIndexed: Date.now(),
|
|
4181
|
+
contentHash: result.contentHash ?? ""
|
|
4182
|
+
});
|
|
4183
|
+
filesEmpty++;
|
|
4184
|
+
continue;
|
|
4185
|
+
}
|
|
4186
|
+
batchEntries.push({
|
|
4187
|
+
file,
|
|
4188
|
+
lang,
|
|
4189
|
+
symbols: parsed.symbols,
|
|
4190
|
+
refs: parsed.refs ?? [],
|
|
4191
|
+
mtimeMs: Math.floor(stat2.mtimeMs),
|
|
4192
|
+
symbolCount: parsed.symbols.length,
|
|
4193
|
+
contentHash: result.contentHash ?? ""
|
|
4194
|
+
});
|
|
4195
|
+
deleteForFiles.push(file);
|
|
4196
|
+
}
|
|
4197
|
+
if (batchEntries.length > 0) {
|
|
4198
|
+
try {
|
|
4199
|
+
store.commitBatch(batchEntries, { deleteForFiles });
|
|
4200
|
+
for (const entry of batchEntries) {
|
|
4201
|
+
const count = entry.symbols.length;
|
|
4202
|
+
symbolsIndexed += count;
|
|
4203
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
4204
|
+
filesIndexed++;
|
|
4205
|
+
filesParsed++;
|
|
4206
|
+
}
|
|
4207
|
+
} catch (err) {
|
|
4208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4209
|
+
errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
|
|
4210
|
+
for (const entry of batchEntries) {
|
|
4211
|
+
try {
|
|
4212
|
+
store.deleteRefsForFile(entry.file);
|
|
4213
|
+
store.deleteSymbolsForFile(entry.file);
|
|
4214
|
+
const symbolsWithIds = store.insertSymbols(entry.symbols);
|
|
4215
|
+
symbolsIndexed += symbolsWithIds.length;
|
|
4216
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
4217
|
+
filesIndexed++;
|
|
4218
|
+
filesParsed++;
|
|
4219
|
+
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
4220
|
+
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
4221
|
+
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
4222
|
+
}
|
|
4223
|
+
store.resolveRefsForNames([
|
|
4224
|
+
...entry.symbols.map((symbol) => symbol.name),
|
|
4225
|
+
...entry.refs.map((ref) => ref.toName)
|
|
4226
|
+
]);
|
|
4227
|
+
store.upsertFile({
|
|
4228
|
+
file: entry.file,
|
|
4229
|
+
lang: entry.lang,
|
|
4230
|
+
mtimeMs: entry.mtimeMs,
|
|
4231
|
+
symbolCount: entry.symbolCount,
|
|
4232
|
+
lastIndexed: Date.now(),
|
|
4233
|
+
contentHash: entry.contentHash
|
|
4234
|
+
});
|
|
4235
|
+
} catch (innerErr) {
|
|
4236
|
+
filesFailed++;
|
|
4237
|
+
errors.push(
|
|
4238
|
+
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
4239
|
+
);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
if (discoveredFiles && discoveryComplete) {
|
|
4246
|
+
for (const [file_] of existingMeta) {
|
|
4247
|
+
if (!discoveredFiles.has(file_)) {
|
|
4248
|
+
store.deleteFile(file_);
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
if (needsFullRefResolution) store.resolveRefs();
|
|
4253
|
+
await resolveProjectRelations(store, projectRoot, {
|
|
4254
|
+
// A watcher run re-resolves only what it touched; a full run (or a contract
|
|
4255
|
+
// bump) re-resolves everything, because a newly indexed file can be the
|
|
4256
|
+
// target of imports written long before it.
|
|
4257
|
+
onlyFiles: needsFullRefResolution ? void 0 : opts.files,
|
|
4258
|
+
errors,
|
|
4259
|
+
signal
|
|
4260
|
+
});
|
|
4261
|
+
store.setMetadata("ref_resolution_version", refResolutionVersion);
|
|
4262
|
+
store.setMetadata("relation_graph_version", relationGraphVersion);
|
|
4263
|
+
const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
|
|
4264
|
+
if (completeProjectScope && discoverySnapshotKey !== void 0) {
|
|
4265
|
+
const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
|
|
4266
|
+
if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
|
|
4267
|
+
throw new IndexSourceChangedError(
|
|
4268
|
+
"Project files changed during indexing; retrying before publishing the generation."
|
|
4269
|
+
);
|
|
4270
|
+
}
|
|
4271
|
+
store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
|
|
4272
|
+
}
|
|
4273
|
+
if (!opts.files || filesIndexed >= 50) store.optimize();
|
|
4274
|
+
store.setLastIndexed(Date.now());
|
|
4275
|
+
const durationMs = Date.now() - startMs;
|
|
4276
|
+
return {
|
|
4277
|
+
filesIndexed,
|
|
4278
|
+
fileOutcomes: {
|
|
4279
|
+
parsed: filesParsed,
|
|
4280
|
+
skipped: filesSkipped,
|
|
4281
|
+
empty: filesEmpty,
|
|
4282
|
+
failed: filesFailed
|
|
4283
|
+
},
|
|
4284
|
+
symbolsIndexed,
|
|
4285
|
+
langStats,
|
|
4286
|
+
durationMs,
|
|
4287
|
+
errors
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
// src/codebase-index/index-service.ts
|
|
4292
|
+
async function indexService(args, hooks = {}) {
|
|
4293
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4294
|
+
try {
|
|
4295
|
+
return await runIndexerWithStore(store, {
|
|
4296
|
+
projectRoot: args.projectRoot,
|
|
4297
|
+
indexDir: args.indexDir,
|
|
4298
|
+
files: args.files,
|
|
4299
|
+
force: args.force,
|
|
4300
|
+
langs: args.langs,
|
|
4301
|
+
ignore: args.ignore,
|
|
4302
|
+
signal: hooks.signal,
|
|
4303
|
+
onProgress: hooks.onProgress
|
|
4304
|
+
});
|
|
4305
|
+
} finally {
|
|
4306
|
+
indexStorePool.release(store);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
function searchService(args) {
|
|
4310
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4311
|
+
try {
|
|
4312
|
+
const result = store.searchRanked(
|
|
4313
|
+
args.query,
|
|
4314
|
+
{
|
|
4315
|
+
kind: args.kind,
|
|
4316
|
+
lang: args.lang,
|
|
4317
|
+
file: args.file,
|
|
4318
|
+
lspKind: args.lspKind
|
|
4319
|
+
},
|
|
4320
|
+
args.limit
|
|
4321
|
+
);
|
|
4322
|
+
if (result.total === 0) {
|
|
4323
|
+
return { ...result, indexSummary: store.getIndexSummary() };
|
|
4324
|
+
}
|
|
4325
|
+
return result;
|
|
4326
|
+
} finally {
|
|
4327
|
+
indexStorePool.release(store);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
function statsService(args) {
|
|
4331
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4332
|
+
try {
|
|
4333
|
+
return store.getStats();
|
|
4334
|
+
} finally {
|
|
4335
|
+
indexStorePool.release(store);
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
function packageGraphService(args) {
|
|
4339
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4340
|
+
try {
|
|
4341
|
+
return store.getPackageGraph();
|
|
4342
|
+
} finally {
|
|
4343
|
+
indexStorePool.release(store);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
function fileGraphService(args) {
|
|
4347
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4348
|
+
try {
|
|
4349
|
+
return store.getFileGraph(args.packageFilter);
|
|
4350
|
+
} finally {
|
|
4351
|
+
indexStorePool.release(store);
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
function symbolGraphService(args) {
|
|
4355
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4356
|
+
try {
|
|
4357
|
+
return store.getSymbolGraph(args.fileFilter);
|
|
4358
|
+
} finally {
|
|
4359
|
+
indexStorePool.release(store);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function incomingCallsService(args) {
|
|
4363
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4364
|
+
try {
|
|
4365
|
+
if (args.transitive) {
|
|
4366
|
+
return store.findTransitiveIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
4367
|
+
}
|
|
4368
|
+
return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
4369
|
+
} finally {
|
|
4370
|
+
indexStorePool.release(store);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
function outgoingCallsService(args) {
|
|
4374
|
+
const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
|
|
4375
|
+
try {
|
|
4376
|
+
if (args.transitive) {
|
|
4377
|
+
return store.findTransitiveOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
4378
|
+
}
|
|
4379
|
+
return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
|
|
4380
|
+
} finally {
|
|
4381
|
+
indexStorePool.release(store);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
export {
|
|
4386
|
+
CircuitOpenError,
|
|
4387
|
+
IndexTimeoutError,
|
|
4388
|
+
LockError,
|
|
4389
|
+
IndexCircuitBreaker,
|
|
4390
|
+
indexCircuitBreaker,
|
|
4391
|
+
resetIndexCircuitBreaker,
|
|
4392
|
+
tokenise,
|
|
4393
|
+
buildIndexableText,
|
|
4394
|
+
buildBm25Index,
|
|
4395
|
+
SCHEMA_VERSION,
|
|
4396
|
+
resolveIndexDir,
|
|
4397
|
+
codebaseIndexDirOverride,
|
|
4398
|
+
lspKindToInternalKind,
|
|
4399
|
+
internalKindToLspKind,
|
|
4400
|
+
IndexStore,
|
|
4401
|
+
indexStorePool,
|
|
4402
|
+
runIndexer,
|
|
4403
|
+
indexService,
|
|
4404
|
+
searchService,
|
|
4405
|
+
statsService,
|
|
4406
|
+
packageGraphService,
|
|
4407
|
+
fileGraphService,
|
|
4408
|
+
symbolGraphService,
|
|
4409
|
+
incomingCallsService,
|
|
4410
|
+
outgoingCallsService
|
|
4411
|
+
};
|
|
4412
|
+
//# sourceMappingURL=chunk-OA6YJIWL.js.map
|