@opengeni/jev 0.1.0-canary.36199476632001

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/index.js ADDED
@@ -0,0 +1,3110 @@
1
+ // src/client.ts
2
+ var JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
3
+ var JEV_DEFAULT_MODEL = "jev-latest";
4
+ var JEV_PRICE_PER_MILLION_INPUT_TOKENS_USD = 0.042;
5
+ function noul(instructions, criteria) {
6
+ return criteria ? { type: "noul", instructions, criteria } : { type: "noul", instructions };
7
+ }
8
+ function jevCostUsd(inputTokens) {
9
+ return inputTokens * JEV_PRICE_PER_MILLION_INPUT_TOKENS_USD / 1e6;
10
+ }
11
+ var JevError = class extends Error {
12
+ /** HTTP status of the failed response, when there was one. */
13
+ status;
14
+ /** Machine-readable reason when known (for example "max_tokens_exceeded" or "state_too_large"). */
15
+ code;
16
+ constructor(message, options = {}) {
17
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
18
+ this.name = new.target.name;
19
+ this.status = options.status;
20
+ this.code = options.code;
21
+ }
22
+ };
23
+ var JevUnavailableError = class extends JevError {
24
+ };
25
+ var JevRequestError = class extends JevError {
26
+ };
27
+ var JEV_DEFAULT_LIMITS = {
28
+ stateLongestMax: 32e3,
29
+ stateAllMax: 64e3,
30
+ safety: 0.9,
31
+ perRequestOverhead: 300,
32
+ perQuestionOverhead: 12,
33
+ maxQuestionsPerRequest: 256
34
+ };
35
+ var JEV_CHARS_PER_TOKEN = 3;
36
+ function estimateJevTokens(value, charsPerToken = JEV_CHARS_PER_TOKEN) {
37
+ const s = typeof value === "string" ? value : JSON.stringify(value ?? "");
38
+ return Math.ceil(s.length / charsPerToken);
39
+ }
40
+ function planChunks(stateTokens, questionTokens, limits = JEV_DEFAULT_LIMITS) {
41
+ const longestCap = limits.stateLongestMax * limits.safety;
42
+ const allCap = limits.stateAllMax * limits.safety;
43
+ const base = stateTokens + limits.perRequestOverhead;
44
+ const chunks = [];
45
+ let cur = [];
46
+ let curSum = 0;
47
+ for (const [id, qt] of questionTokens) {
48
+ const cost = qt + limits.perQuestionOverhead;
49
+ if (base + cost > longestCap) {
50
+ throw new JevRequestError(
51
+ `Jev state (~${stateTokens} tokens) plus question "${id}" (~${qt} tokens) exceeds ~${Math.floor(longestCap)} tokens`,
52
+ { code: "state_too_large" }
53
+ );
54
+ }
55
+ if (cur.length > 0 && (base + curSum + cost > allCap || cur.length >= limits.maxQuestionsPerRequest)) {
56
+ chunks.push(cur);
57
+ cur = [];
58
+ curSum = 0;
59
+ }
60
+ cur.push(id);
61
+ curSum += cost;
62
+ }
63
+ if (cur.length) chunks.push(cur);
64
+ return chunks;
65
+ }
66
+ var JevLimiter = class {
67
+ constructor(max) {
68
+ this.max = max;
69
+ }
70
+ active = 0;
71
+ queue = [];
72
+ async run(fn, signal) {
73
+ await this.acquire(signal);
74
+ try {
75
+ return await fn();
76
+ } finally {
77
+ this.release();
78
+ }
79
+ }
80
+ acquire(signal) {
81
+ signal?.throwIfAborted();
82
+ if (this.active < this.max) {
83
+ this.active += 1;
84
+ return Promise.resolve();
85
+ }
86
+ return new Promise((resolve, reject) => {
87
+ const onAbort = () => {
88
+ const i = this.queue.indexOf(entry);
89
+ if (i >= 0) this.queue.splice(i, 1);
90
+ reject(signal?.reason);
91
+ };
92
+ const entry = {
93
+ grant: () => {
94
+ signal?.removeEventListener("abort", onAbort);
95
+ resolve();
96
+ }
97
+ };
98
+ this.queue.push(entry);
99
+ signal?.addEventListener("abort", onAbort, { once: true });
100
+ });
101
+ }
102
+ /** A released slot passes straight to the next waiter, so `active` never overshoots `max`. */
103
+ release() {
104
+ const next = this.queue.shift();
105
+ if (next) next.grant();
106
+ else this.active -= 1;
107
+ }
108
+ };
109
+ var RETRYABLE_STATUS = (status) => status === 429 || status >= 500 && status <= 599;
110
+ var AUTH_STATUS = (status) => status === 401 || status === 402 || status === 403;
111
+ var WARM_WINDOW_MS = 4e3;
112
+ var WARM_TIMEOUT_MS = 3e3;
113
+ var MAX_DETAIL_CHARS = 200;
114
+ var JevClient = class {
115
+ baseUrl;
116
+ model;
117
+ apiKey;
118
+ timeoutMs;
119
+ maxRetries;
120
+ maxRetryAfterMs;
121
+ retryBaseDelayMs;
122
+ fetchImpl;
123
+ limiter;
124
+ lastActivityAt = -Infinity;
125
+ constructor(options) {
126
+ if (!options.apiKey) throw new JevUnavailableError("Jev API key is not configured");
127
+ this.apiKey = options.apiKey;
128
+ this.baseUrl = (options.baseUrl ?? JEV_DEFAULT_BASE_URL).replace(/\/+$/, "");
129
+ this.model = options.model ?? JEV_DEFAULT_MODEL;
130
+ this.timeoutMs = options.timeoutMs ?? 1e4;
131
+ this.maxRetries = Math.max(0, options.maxRetries ?? 2);
132
+ this.maxRetryAfterMs = Math.max(0, options.maxRetryAfterMs ?? 2e3);
133
+ this.retryBaseDelayMs = Math.max(0, options.retryBaseDelayMs ?? 250);
134
+ this.limiter = new JevLimiter(Math.max(1, options.concurrency ?? 16));
135
+ this.fetchImpl = options.fetch ?? ((url, init) => fetch(url, init));
136
+ }
137
+ /**
138
+ * Ask many questions about one state. Splits the questions into several requests when they exceed the
139
+ * limits, runs them concurrently (bounded by `concurrency`) and merges the answers.
140
+ */
141
+ async ask(state, questions, options = {}) {
142
+ const { signal } = options;
143
+ signal?.throwIfAborted();
144
+ const ids = Object.keys(questions);
145
+ if (!ids.length) throw new JevRequestError("Jev ask() needs at least one question");
146
+ const stateTokens = estimateJevTokens(state);
147
+ const chunks = planChunks(
148
+ stateTokens,
149
+ ids.map((id) => [id, estimateJevTokens(questions[id])])
150
+ );
151
+ const label = options.tag ? `${options.tag}: ` : "";
152
+ const results = await Promise.all(
153
+ chunks.map(
154
+ (chunk2) => this.limiter.run(
155
+ () => this.post(
156
+ JSON.stringify({
157
+ state,
158
+ model: this.model,
159
+ questions: Object.fromEntries(chunk2.map((id) => [id, questions[id]]))
160
+ }),
161
+ label,
162
+ signal
163
+ ),
164
+ signal
165
+ )
166
+ )
167
+ );
168
+ const answers = {};
169
+ let inputTokens = 0;
170
+ let model = "";
171
+ results.forEach((json, i) => {
172
+ const got = isRecord(json.answers) ? json.answers : {};
173
+ for (const id of chunks[i]) {
174
+ const answer = normalizeAnswer(got[id]);
175
+ if (!answer)
176
+ throw new JevUnavailableError(`${label}Jev response is missing the answer "${id}"`);
177
+ answers[id] = answer;
178
+ }
179
+ const usage = isRecord(json.usage) ? json.usage : {};
180
+ inputTokens += Number(usage.input_tokens ?? 0) || 0;
181
+ if (typeof json.model === "string" && json.model) model = json.model;
182
+ });
183
+ return {
184
+ answers,
185
+ model,
186
+ usage: { inputTokens },
187
+ requests: results.length,
188
+ costUsd: jevCostUsd(inputTokens)
189
+ };
190
+ }
191
+ /**
192
+ * Open keep-alive connections before a burst of parallel requests (a cold TLS connection costs ~0.7 s).
193
+ * GET /healthz is free. Skipped when the client was active within the last few seconds; never throws.
194
+ */
195
+ warmUp(connections) {
196
+ const now = Date.now();
197
+ if (connections <= 0 || now - this.lastActivityAt < WARM_WINDOW_MS) return;
198
+ this.lastActivityAt = now;
199
+ for (let i = 0; i < connections; i++) {
200
+ const timeout = AbortSignal.timeout(WARM_TIMEOUT_MS);
201
+ this.fetchImpl(`${this.baseUrl}/healthz`, { method: "GET", signal: timeout }).then((res) => res.arrayBuffer()).catch(() => void 0);
202
+ }
203
+ }
204
+ /** One HTTP request with retries. Returns the parsed JSON body of a 2xx response. */
205
+ async post(body, label, signal) {
206
+ const url = `${this.baseUrl}/v1/systemone`;
207
+ const headers = { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` };
208
+ let lastError = "unknown error";
209
+ let lastStatus;
210
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
211
+ signal?.throwIfAborted();
212
+ const attemptSignal = withTimeout(signal, this.timeoutMs);
213
+ let delay = this.backoff(attempt);
214
+ let response = null;
215
+ try {
216
+ const res = await this.fetchImpl(url, {
217
+ method: "POST",
218
+ headers,
219
+ body,
220
+ signal: attemptSignal.signal
221
+ });
222
+ response = { res, text: await res.text() };
223
+ } catch (error) {
224
+ if (signal?.aborted) throw signal.reason;
225
+ lastStatus = void 0;
226
+ lastError = attemptSignal.timedOut() ? `timed out after ${this.timeoutMs} ms` : describeError(error);
227
+ } finally {
228
+ attemptSignal.dispose();
229
+ this.lastActivityAt = Date.now();
230
+ }
231
+ if (response) {
232
+ const { res, text } = response;
233
+ if (res.ok) {
234
+ const json = parseJson(text);
235
+ if (!isRecord(json))
236
+ throw new JevUnavailableError(`${label}Jev returned an invalid response body`);
237
+ return json;
238
+ }
239
+ const detail = errorDetail(text);
240
+ const suffix = detail.message ? `: ${detail.message}` : "";
241
+ if (AUTH_STATUS(res.status)) {
242
+ const what = res.status === 402 ? "billing or credits" : "authentication";
243
+ throw new JevUnavailableError(
244
+ `${label}Jev ${what} failed (HTTP ${res.status}${suffix})`,
245
+ errorOptions(res.status, detail.code)
246
+ );
247
+ }
248
+ if (!RETRYABLE_STATUS(res.status)) {
249
+ throw new JevRequestError(
250
+ `${label}Jev rejected the request (HTTP ${res.status}${suffix})`,
251
+ errorOptions(res.status, detail.code)
252
+ );
253
+ }
254
+ lastStatus = res.status;
255
+ lastError = `HTTP ${res.status}${suffix}`;
256
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
257
+ if (retryAfter !== null) delay = Math.min(retryAfter, this.maxRetryAfterMs);
258
+ }
259
+ if (attempt < this.maxRetries) await sleep(delay, signal);
260
+ }
261
+ throw new JevUnavailableError(
262
+ `${label}Jev unavailable after ${this.maxRetries + 1} attempts (${lastError})`,
263
+ lastStatus === void 0 ? {} : { status: lastStatus }
264
+ );
265
+ }
266
+ backoff(attempt) {
267
+ const base = Math.min(this.maxRetryAfterMs, this.retryBaseDelayMs * 2 ** attempt);
268
+ return Math.round(base * (1 - Math.random() * 0.25));
269
+ }
270
+ };
271
+ function isRecord(value) {
272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
273
+ }
274
+ function parseJson(text) {
275
+ try {
276
+ return text ? JSON.parse(text) : null;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ function normalizeAnswer(raw) {
282
+ if (!isRecord(raw)) return null;
283
+ if (raw.type === "noul") {
284
+ return { type: "noul", probability: typeof raw.noul === "number" ? raw.noul : Number.NaN };
285
+ }
286
+ if (raw.type === "choice") {
287
+ const answer = {
288
+ type: "choice",
289
+ option: String(raw.choice ?? raw.option ?? "")
290
+ };
291
+ if (isRecord(raw.probabilities))
292
+ answer.probabilities = raw.probabilities;
293
+ if (typeof raw.confidence === "number") answer.confidence = raw.confidence;
294
+ return answer;
295
+ }
296
+ if (raw.type === "score")
297
+ return { ...raw, type: "score", score: Number(raw.score) };
298
+ return null;
299
+ }
300
+ function errorOptions(status, code) {
301
+ return code === void 0 ? { status } : { status, code };
302
+ }
303
+ function errorDetail(text) {
304
+ const json = parseJson(text);
305
+ const codes = [];
306
+ let message = "";
307
+ const visit = (value) => {
308
+ if (typeof value === "string") {
309
+ if (!message) message = value;
310
+ return;
311
+ }
312
+ if (Array.isArray(value)) {
313
+ if (!message) message = JSON.stringify(value);
314
+ return;
315
+ }
316
+ if (!isRecord(value)) return;
317
+ for (const key of ["code", "type"]) {
318
+ const v = value[key];
319
+ if (typeof v === "string" && v !== "error") codes.push(v);
320
+ }
321
+ for (const key of ["detail", "error", "message", "msg"]) if (key in value) visit(value[key]);
322
+ };
323
+ if (json === null) message = text.trim();
324
+ else visit(json);
325
+ let code = codes[0];
326
+ if (!code && /^[a-z][a-z0-9_]{2,60}$/.test(message)) code = message;
327
+ return { code, message: message.replace(/\s+/g, " ").slice(0, MAX_DETAIL_CHARS) };
328
+ }
329
+ function parseRetryAfter(header) {
330
+ if (!header) return null;
331
+ const secs = Number(header);
332
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
333
+ const at = Date.parse(header);
334
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
335
+ }
336
+ function describeError(error) {
337
+ const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
338
+ return text.slice(0, MAX_DETAIL_CHARS);
339
+ }
340
+ function withTimeout(parent, timeoutMs) {
341
+ const controller = new AbortController();
342
+ let timedOut = false;
343
+ const onAbort = () => controller.abort(parent?.reason);
344
+ const timer = setTimeout(() => {
345
+ timedOut = true;
346
+ controller.abort(new JevUnavailableError(`Jev request timed out after ${timeoutMs} ms`));
347
+ }, timeoutMs);
348
+ if (parent?.aborted) controller.abort(parent.reason);
349
+ else parent?.addEventListener("abort", onAbort, { once: true });
350
+ return {
351
+ signal: controller.signal,
352
+ timedOut: () => timedOut,
353
+ dispose: () => {
354
+ clearTimeout(timer);
355
+ parent?.removeEventListener("abort", onAbort);
356
+ }
357
+ };
358
+ }
359
+ function sleep(ms, signal) {
360
+ if (ms <= 0) return Promise.resolve();
361
+ return new Promise((resolve, reject) => {
362
+ const onAbort = () => {
363
+ clearTimeout(timer);
364
+ reject(signal?.reason);
365
+ };
366
+ const timer = setTimeout(() => {
367
+ signal?.removeEventListener("abort", onAbort);
368
+ resolve();
369
+ }, ms);
370
+ if (signal?.aborted) onAbort();
371
+ else signal?.addEventListener("abort", onAbort, { once: true });
372
+ });
373
+ }
374
+
375
+ // src/circuit-breaker.ts
376
+ var JevCircuitBreaker = class {
377
+ failureThreshold;
378
+ cooldownMs;
379
+ authCooldownMs;
380
+ trialTimeoutMs;
381
+ consecutiveFailures = 0;
382
+ openUntil = null;
383
+ trial = null;
384
+ nextLeaseId = 1;
385
+ lastFailure = null;
386
+ constructor(options = {}) {
387
+ this.failureThreshold = Math.max(1, options.failureThreshold ?? 3);
388
+ this.cooldownMs = options.cooldownMs ?? 5 * 6e4;
389
+ this.authCooldownMs = options.authCooldownMs ?? 30 * 6e4;
390
+ this.trialTimeoutMs = options.trialTimeoutMs ?? 10 * 6e4;
391
+ }
392
+ /** True while calls should be refused without contacting Jev (the cooldown is running). */
393
+ isOpen(now = Date.now()) {
394
+ return this.openUntil !== null && now < this.openUntil;
395
+ }
396
+ /**
397
+ * Admit one call and return its lease, or null when refused. Closed: always. Open: never. Half-open:
398
+ * only the first caller, as the trial, until its lease is settled or it has run for trialTimeoutMs, so
399
+ * a trial that never ends cannot block the tool for good.
400
+ */
401
+ tryAcquire(now = Date.now()) {
402
+ if (this.openUntil === null) return this.issue(false);
403
+ if (now < this.openUntil) return null;
404
+ if (this.trial !== null && now - this.trial.startedAt < this.trialTimeoutMs) return null;
405
+ const lease = this.issue(true);
406
+ this.trial = { lease, startedAt: now };
407
+ return lease;
408
+ }
409
+ /** End an admitted call that did not contact Jev, so its outcome says nothing about Jev. */
410
+ release(lease) {
411
+ if (this.trial !== null && this.trial.lease === lease) this.trial = null;
412
+ }
413
+ /** Any call's success proves Jev works, so the lease does not change the outcome. */
414
+ recordSuccess(_lease) {
415
+ this.consecutiveFailures = 0;
416
+ this.openUntil = null;
417
+ this.trial = null;
418
+ }
419
+ /** Settle a failed call. A failure without a lease counts the same but never ends a trial. */
420
+ recordFailure(error, now = Date.now(), lease) {
421
+ if (!(error instanceof JevUnavailableError)) {
422
+ if (lease) this.release(lease);
423
+ return;
424
+ }
425
+ const halfOpen = this.openUntil !== null && now >= this.openUntil;
426
+ this.consecutiveFailures += 1;
427
+ this.lastFailure = { message: error.message, status: error.status ?? null, at: now };
428
+ if (halfOpen || this.consecutiveFailures >= this.failureThreshold) {
429
+ const auth = error.status === 401 || error.status === 402 || error.status === 403;
430
+ this.openUntil = now + (auth ? this.authCooldownMs : this.cooldownMs);
431
+ this.trial = null;
432
+ }
433
+ }
434
+ status(now = Date.now()) {
435
+ const state = this.openUntil === null ? "closed" : now < this.openUntil ? "open" : "half_open";
436
+ return {
437
+ state,
438
+ consecutiveFailures: this.consecutiveFailures,
439
+ openUntil: state === "open" ? this.openUntil : null,
440
+ trialInFlight: state === "half_open" && this.trial !== null && now - this.trial.startedAt < this.trialTimeoutMs,
441
+ lastFailure: this.lastFailure
442
+ };
443
+ }
444
+ issue(trial) {
445
+ return Object.freeze({ id: this.nextLeaseId++, trial });
446
+ }
447
+ };
448
+
449
+ // src/code-search/config.ts
450
+ var DEFAULT_CODE_SEARCH_CONFIG = deepFreeze({
451
+ recall: {
452
+ maxCandidates: 240,
453
+ maxMatchesPerFile: 25,
454
+ maxFileBytes: 4e6,
455
+ maxLineColumns: 8e3,
456
+ testWeight: 0.6,
457
+ pathBonus: 0.5,
458
+ hitCountWeight: 0.1,
459
+ shortWordMaxLen: 5,
460
+ fragmentFallback: true,
461
+ minCandidatesBeforeWiden: 5,
462
+ extraExcludes: [],
463
+ searchHidden: true,
464
+ changelogWeight: 0.6,
465
+ ripgrepTimeoutMs: 3e4
466
+ },
467
+ wave1: {
468
+ filesPerRequest: 60,
469
+ hitLinesPerFile: 3,
470
+ hitLineChars: 160,
471
+ maxFiles: 16,
472
+ minFiles: 8,
473
+ lexicalGuard: 5
474
+ },
475
+ wave2: {
476
+ windowsPerFile: 5,
477
+ seedHitsPerFile: 24,
478
+ maxUp: 40,
479
+ maxDown: 120,
480
+ maxWindowLines: 150,
481
+ fallbackBefore: 10,
482
+ fallbackAfter: 30,
483
+ mergeGap: 2,
484
+ minWindowLines: 12,
485
+ passagesPerRequest: 4,
486
+ maxRequestChars: 24e3,
487
+ maxPassages: 80,
488
+ maxLineChars: 400,
489
+ maxProseLineChars: 1600,
490
+ maxWindowChars: 8e3,
491
+ maxProseWindowChars: 4e3
492
+ },
493
+ wave3: {
494
+ enabled: true,
495
+ maxLeadCandidates: 60,
496
+ maxLeadsFollowed: 6,
497
+ seedPassages: 12,
498
+ defsPerLead: 1
499
+ },
500
+ status: { enabled: true, maxEvidenceChars: 6e4, hi: 0.7, lo: 0.4 },
501
+ pack: {
502
+ charsPerToken: 3.2,
503
+ minPassages: 4,
504
+ minRelevance: 0.1,
505
+ fillBudget: false,
506
+ moreCandidates: 10,
507
+ leadsNotFollowed: 8,
508
+ minTrimLines: 15,
509
+ filePenalty: 0.1,
510
+ maxPassageChars: 3200,
511
+ changelogPrior: 0.5,
512
+ docPrior: 0.85,
513
+ testPrior: 1,
514
+ lexWeight: 0
515
+ },
516
+ jev: {
517
+ inlineQuestionMaxChars: 600,
518
+ fileCriteriaPerQuestion: false,
519
+ warmConnections: 12
520
+ },
521
+ // calibrated on the DEV split from the observed score distributions (relevant files mostly 0.6-0.9;
522
+ // minFiles and the lexical guard keep recall when few files pass)
523
+ thresholds: { T1: 0.6, T2: 0.5, T3: 0.5 }
524
+ });
525
+ function codeSearchConfig(override = {}) {
526
+ const base = structuredClone(DEFAULT_CODE_SEARCH_CONFIG);
527
+ const out = base;
528
+ for (const [section, values] of Object.entries(override)) {
529
+ const target = out[section];
530
+ if (!target) throw new Error(`unknown code_search config section: ${section}`);
531
+ if (!isPlainObject(values)) throw new Error(`code_search config ${section}: expected object`);
532
+ for (const [key, value] of Object.entries(values)) {
533
+ if (!(key in target)) throw new Error(`unknown code_search config key: ${section}.${key}`);
534
+ const current = target[key];
535
+ if (Array.isArray(current) ? !Array.isArray(value) : typeof current !== typeof value) {
536
+ throw new Error(
537
+ `code_search config ${section}.${key}: expected ${Array.isArray(current) ? "array" : typeof current}`
538
+ );
539
+ }
540
+ target[key] = value;
541
+ }
542
+ }
543
+ validateCodeSearchConfig(base);
544
+ return base;
545
+ }
546
+ function validateCodeSearchConfig(c) {
547
+ const probs = [];
548
+ for (const [k, v] of Object.entries(c.thresholds))
549
+ if (!(v >= 0 && v <= 1)) probs.push(`thresholds.${k} must be in [0,1]`);
550
+ if (c.wave1.maxFiles < 1) probs.push("wave1.maxFiles must be >= 1");
551
+ if (c.wave1.minFiles > c.wave1.maxFiles) probs.push("wave1.minFiles must be <= wave1.maxFiles");
552
+ if (c.wave1.filesPerRequest < 1 || c.wave1.filesPerRequest > 250)
553
+ probs.push("wave1.filesPerRequest must be 1..250");
554
+ if (c.wave2.passagesPerRequest < 1) probs.push("wave2.passagesPerRequest must be >= 1");
555
+ if (c.wave2.maxWindowLines < 10) probs.push("wave2.maxWindowLines must be >= 10");
556
+ if (c.wave2.maxWindowChars < 4 * c.wave2.maxLineChars)
557
+ probs.push("wave2.maxWindowChars must be >= 4 x wave2.maxLineChars");
558
+ if (c.wave2.maxProseWindowChars < 2 * c.wave2.maxProseLineChars + 200) {
559
+ probs.push("wave2.maxProseWindowChars must be >= 2 x wave2.maxProseLineChars + 200");
560
+ }
561
+ if (c.wave3.maxLeadCandidates > 250) probs.push("wave3.maxLeadCandidates must be <= 250");
562
+ if (!(c.status.lo <= c.status.hi)) probs.push("status.lo must be <= status.hi");
563
+ if (c.pack.charsPerToken <= 0) probs.push("pack.charsPerToken must be > 0");
564
+ if (c.pack.filePenalty < 0) probs.push("pack.filePenalty must be >= 0");
565
+ if (c.pack.maxPassageChars !== 0 && c.pack.maxPassageChars < 600)
566
+ probs.push("pack.maxPassageChars must be 0 or >= 600");
567
+ for (const k of ["changelogPrior", "docPrior", "testPrior"]) {
568
+ if (!(c.pack[k] > 0 && c.pack[k] <= 1)) probs.push(`pack.${k} must be in (0,1]`);
569
+ }
570
+ if (!(c.pack.lexWeight >= 0)) probs.push("pack.lexWeight must be >= 0");
571
+ if (!(c.recall.changelogWeight > 0 && c.recall.changelogWeight <= 1))
572
+ probs.push("recall.changelogWeight must be in (0,1]");
573
+ if (!(c.recall.ripgrepTimeoutMs > 0)) probs.push("recall.ripgrepTimeoutMs must be > 0");
574
+ if (probs.length) throw new Error(`invalid code_search config: ${probs.join("; ")}`);
575
+ }
576
+ function isPlainObject(x) {
577
+ return typeof x === "object" && x !== null && !Array.isArray(x);
578
+ }
579
+ function deepFreeze(value) {
580
+ if (typeof value === "object" && value !== null) {
581
+ for (const v of Object.values(value)) deepFreeze(v);
582
+ Object.freeze(value);
583
+ }
584
+ return value;
585
+ }
586
+
587
+ // src/code-search/judge.ts
588
+ var PROMPTS = {
589
+ fileTask: "Code search triage for a software repository. A developer asked `question`. `files` lists candidate files found by keyword search; each entry is the file path followed by up to 3 matching lines as `line: text`. Judge each file independently by its path and matching lines.",
590
+ fileCriteria: {
591
+ yes: "Reading this file would likely help answer the question: it probably implements, decides, computes, configures, enforces or documents the asked behavior. A doc or design note that explains the behavior counts.",
592
+ no: "The file probably only mentions the same words, imports or passes values through, belongs to an unrelated feature that shares keywords, or is a test that is not about the asked behavior."
593
+ },
594
+ fileQuestion: (id, path, q) => `Would reading file \`files.${id}\` (${path}) help answer the question${q}? Apply \`criteria\`.`,
595
+ fileQuestionPerQ: (id, path, q) => `Would reading file \`files.${id}\` (${path}) help answer the question${q}?`,
596
+ passageTask: "Evidence check for a question about a software repository. Each entry in `passages` is a verbatim excerpt of one repository file with its original line numbers (`N| text`). `in` names the enclosing declaration when the excerpt starts inside one. Judge each passage on its own content only.",
597
+ passageQuestion: (id, path, a, b, q) => `Does \`passages.${id}\` (${path} lines ${a}-${b}) contain code or text needed to answer the question${q}?`,
598
+ passageCriteria: {
599
+ true: "The passage implements, decides, computes, configures, enforces or documents part of the asked behavior. A helper that performs one asked step counts, and so does a doc passage that states the behavior.",
600
+ false: "The passage only mentions, imports, calls or passes through names related to the question, is a test or type declaration that does not show the behavior, or is unrelated code that shares keywords."
601
+ },
602
+ coverageQuestion: (id, path, a, b, sub) => `Does \`passages.${id}\` (${path} lines ${a}-${b}) show the answer to this part of the question: "${sub}"?`,
603
+ coverageCriteria: {
604
+ true: "The passage itself states or implements the answer to this part.",
605
+ false: "The passage does not address this part, or only mentions it without showing the answer."
606
+ },
607
+ leadTask: "Follow-up selection for a question about a software repository. Evidence passages already found reference the identifiers in `leads`, whose definitions have not been read yet. Each lead shows the identifier, where it was seen, and the line it appears on.",
608
+ leadCriteria: {
609
+ yes: "The definition likely computes, decides, configures or enforces part of the asked behavior, or holds a constant, setting or rule that the answer depends on.",
610
+ no: "It is a generic helper (logging, formatting, errors, type plumbing, database or HTTP plumbing) or it is unrelated to the question."
611
+ },
612
+ leadQuestion: (id, name, q) => `Would reading where \`${name}\` (\`leads.${id}\`) is defined help answer the question${q}? Apply \`criteria\`.`,
613
+ statusTask: "Sufficiency check. `evidence` is a set of verbatim excerpts of repository files (with original line numbers) collected to answer `question`.",
614
+ statusQuestion: (q) => `Do the \`evidence\` passages show the answer to the question${q}?`,
615
+ statusSubQuestion: (sub) => `Do the \`evidence\` passages show the answer to this part of the question: "${sub}"?`,
616
+ statusCriteria: {
617
+ true: "Together the passages state or implement the answer concretely enough to cite file and line.",
618
+ false: "Some part of the answer is missing: the passages only mention the topic, or the deciding code is elsewhere."
619
+ }
620
+ };
621
+ function inlineQ(question, maxChars) {
622
+ return question.length <= maxChars ? `: "${question}"` : " in `question`";
623
+ }
624
+ function withSubs(ctx) {
625
+ return ctx.subQuestions.length ? { sub_questions: Object.fromEntries(ctx.subQuestions.map((s, i) => [`s${i + 1}`, s])) } : {};
626
+ }
627
+ function buildFileRequest(items, ctx, cfg) {
628
+ const q = inlineQ(ctx.question, cfg.jev.inlineQuestionMaxChars);
629
+ const perQ = cfg.jev.fileCriteriaPerQuestion;
630
+ const state = {
631
+ task: PROMPTS.fileTask,
632
+ question: ctx.question,
633
+ ...withSubs(ctx),
634
+ ...perQ ? {} : { criteria: PROMPTS.fileCriteria },
635
+ files: Object.fromEntries(items.map((f) => [f.id, f.descriptor]))
636
+ };
637
+ const questions = Object.fromEntries(
638
+ items.map((f) => [
639
+ f.id,
640
+ perQ ? noul(PROMPTS.fileQuestionPerQ(f.id, f.path, q), {
641
+ true: PROMPTS.fileCriteria.yes,
642
+ false: PROMPTS.fileCriteria.no
643
+ }) : noul(PROMPTS.fileQuestion(f.id, f.path, q))
644
+ ])
645
+ );
646
+ return { state, questions };
647
+ }
648
+ function buildPassageRequest(items, ctx, cfg) {
649
+ const q = inlineQ(ctx.question, cfg.jev.inlineQuestionMaxChars);
650
+ const state = {
651
+ task: PROMPTS.passageTask,
652
+ question: ctx.question,
653
+ ...withSubs(ctx),
654
+ passages: Object.fromEntries(
655
+ items.map((p) => [
656
+ p.id,
657
+ {
658
+ file: p.path,
659
+ lines: `${p.start}-${p.end}`,
660
+ ...p.label ? { in: p.label } : {},
661
+ text: p.text
662
+ }
663
+ ])
664
+ )
665
+ };
666
+ const questions = {};
667
+ for (const p of items) {
668
+ questions[`rel::${p.id}`] = noul(
669
+ PROMPTS.passageQuestion(p.id, p.path, p.start, p.end, q),
670
+ PROMPTS.passageCriteria
671
+ );
672
+ ctx.subQuestions.forEach((sub, j) => {
673
+ questions[`cov::${p.id}::${j}`] = noul(
674
+ PROMPTS.coverageQuestion(p.id, p.path, p.start, p.end, sub),
675
+ PROMPTS.coverageCriteria
676
+ );
677
+ });
678
+ }
679
+ return { state, questions };
680
+ }
681
+ function buildLeadRequest(items, ctx, cfg) {
682
+ const q = inlineQ(ctx.question, cfg.jev.inlineQuestionMaxChars);
683
+ const state = {
684
+ task: PROMPTS.leadTask,
685
+ question: ctx.question,
686
+ ...withSubs(ctx),
687
+ criteria: PROMPTS.leadCriteria,
688
+ leads: Object.fromEntries(
689
+ items.map((l) => [l.id, `${l.name} (seen at ${l.seenAt}) ${l.context}`])
690
+ )
691
+ };
692
+ const questions = Object.fromEntries(
693
+ items.map((l) => [l.id, noul(PROMPTS.leadQuestion(l.id, l.name, q))])
694
+ );
695
+ return { state, questions };
696
+ }
697
+ function buildStatusRequest(evidence, ctx, cfg) {
698
+ const q = inlineQ(ctx.question, cfg.jev.inlineQuestionMaxChars);
699
+ const state = { task: PROMPTS.statusTask, question: ctx.question, ...withSubs(ctx), evidence };
700
+ const questions = {
701
+ overall: noul(PROMPTS.statusQuestion(q), PROMPTS.statusCriteria)
702
+ };
703
+ ctx.subQuestions.forEach((sub, j) => {
704
+ questions[`sub::${j}`] = noul(PROMPTS.statusSubQuestion(sub), PROMPTS.statusCriteria);
705
+ });
706
+ return { state, questions };
707
+ }
708
+ function chunkPassages(items, perRequest, maxChars) {
709
+ const out = [];
710
+ let cur = [];
711
+ let chars = 0;
712
+ for (const p of items) {
713
+ const c = p.text.length;
714
+ if (cur.length && (cur.length >= perRequest || chars + c > maxChars)) {
715
+ out.push(cur);
716
+ cur = [];
717
+ chars = 0;
718
+ }
719
+ cur.push(p);
720
+ chars += c;
721
+ }
722
+ if (cur.length) out.push(cur);
723
+ return out;
724
+ }
725
+ function chunkEven(xs, n) {
726
+ if (!xs.length) return [];
727
+ const groups = Math.ceil(xs.length / n);
728
+ const size = Math.ceil(xs.length / groups);
729
+ return chunk(xs, size);
730
+ }
731
+ function chunk(xs, n) {
732
+ const out = [];
733
+ for (let i = 0; i < xs.length; i += n) out.push(xs.slice(i, i + n));
734
+ return out;
735
+ }
736
+ var noulOf = (a) => a && a.type === "noul" && Number.isFinite(a.probability) ? a.probability : Number.NaN;
737
+ function orLex(p, lex) {
738
+ return Number.isFinite(p) ? p : lex;
739
+ }
740
+ var JevJudge = class {
741
+ constructor(o) {
742
+ this.o = o;
743
+ }
744
+ stageStats = {};
745
+ jevModel = null;
746
+ stats() {
747
+ return this.stageStats;
748
+ }
749
+ model() {
750
+ return this.jevModel;
751
+ }
752
+ stat(stage) {
753
+ return this.stageStats[stage] ??= { requests: 0, inputTokens: 0, costUsd: 0, ms: 0 };
754
+ }
755
+ /** One logical request (the client may split its questions into several HTTP requests). */
756
+ async ask(stage, state, questions) {
757
+ const t0 = performance.now();
758
+ const r = await this.o.client.ask(state, questions, {
759
+ tag: stage,
760
+ signal: this.o.signal
761
+ });
762
+ const st = this.stat(stage);
763
+ st.requests += r.requests;
764
+ st.inputTokens += r.usage.inputTokens;
765
+ st.costUsd += r.costUsd;
766
+ this.jevModel = r.model || this.jevModel;
767
+ this.o.onEvent?.("jev", {
768
+ jevStage: stage,
769
+ ms: Math.round(performance.now() - t0),
770
+ requests: r.requests,
771
+ model: r.model,
772
+ inputTokens: r.usage.inputTokens,
773
+ costUsd: r.costUsd,
774
+ state,
775
+ questions,
776
+ answers: r.answers
777
+ });
778
+ return r.answers;
779
+ }
780
+ /** Run a stage's batches in parallel; the first failure rejects the stage. */
781
+ async stage(stage, batches, build, read) {
782
+ const t0 = performance.now();
783
+ try {
784
+ const results = await Promise.all(
785
+ batches.map(async (b) => {
786
+ const req = build(b);
787
+ return read(b, await this.ask(stage, req.state, req.questions));
788
+ })
789
+ );
790
+ return new Map(results.flat());
791
+ } finally {
792
+ this.stat(stage).ms += Math.round(performance.now() - t0);
793
+ }
794
+ }
795
+ async scoreFiles(items, ctx) {
796
+ if (!items.length) return /* @__PURE__ */ new Map();
797
+ return this.stage(
798
+ "wave1",
799
+ chunkEven(items, this.o.config.wave1.filesPerRequest),
800
+ (b) => buildFileRequest(b, ctx, this.o.config),
801
+ (b, a) => b.map((f) => [f.id, orLex(noulOf(a[f.id]), f.lex)])
802
+ );
803
+ }
804
+ async scorePassages(items, ctx, stage = "wave2") {
805
+ if (!items.length) return /* @__PURE__ */ new Map();
806
+ const w = this.o.config.wave2;
807
+ return this.stage(
808
+ stage,
809
+ chunkPassages(items, w.passagesPerRequest, w.maxRequestChars),
810
+ (b) => buildPassageRequest(b, ctx, this.o.config),
811
+ (b, a) => b.map((p) => [
812
+ p.id,
813
+ {
814
+ rel: orLex(noulOf(a[`rel::${p.id}`]), p.lex),
815
+ cov: ctx.subQuestions.map(
816
+ (_, j) => orLex(noulOf(a[`cov::${p.id}::${j}`]), p.lexCov[j] ?? 0)
817
+ )
818
+ }
819
+ ])
820
+ );
821
+ }
822
+ async scoreLeads(items, ctx) {
823
+ if (!items.length) return /* @__PURE__ */ new Map();
824
+ return this.stage(
825
+ "leads",
826
+ chunk(items, 250),
827
+ (b) => buildLeadRequest(b, ctx, this.o.config),
828
+ (b, a) => b.map((l) => [l.id, orLex(noulOf(a[l.id]), l.lex)])
829
+ );
830
+ }
831
+ /** null = nothing to check (empty evidence). */
832
+ async status(evidence, ctx) {
833
+ if (!evidence.trim()) return null;
834
+ const req = buildStatusRequest(evidence, ctx, this.o.config);
835
+ const t0 = performance.now();
836
+ try {
837
+ const a = await this.ask("status", req.state, req.questions);
838
+ return {
839
+ overall: noulOf(a.overall),
840
+ subs: ctx.subQuestions.map((_, j) => noulOf(a[`sub::${j}`]))
841
+ };
842
+ } finally {
843
+ this.stat("status").ms += Math.round(performance.now() - t0);
844
+ }
845
+ }
846
+ };
847
+
848
+ // src/code-search/workspace.ts
849
+ var CODE_SEARCH_MAX_PATTERN_CHARS = 16e3;
850
+ var CodeSearchWorkspaceError = class extends Error {
851
+ };
852
+ var CodeSearchRipgrepMissingError = class extends CodeSearchWorkspaceError {
853
+ constructor(message = "ripgrep (rg) is not installed in this workspace") {
854
+ super(message);
855
+ this.name = "CodeSearchRipgrepMissingError";
856
+ }
857
+ };
858
+
859
+ // src/code-search/session.ts
860
+ var READ_CONCURRENCY = 8;
861
+ var RIPGREP_SPLIT_CONCURRENCY = 4;
862
+ var WorkspaceSession = class {
863
+ constructor(workspace, signal, ripgrepTimeoutMs) {
864
+ this.workspace = workspace;
865
+ this.signal = signal;
866
+ this.ripgrepTimeoutMs = ripgrepTimeoutMs;
867
+ }
868
+ calls = 0;
869
+ /** A ripgrep call returned output cut at the adapter's byte cap. */
870
+ truncated = false;
871
+ /** A ripgrep call hit its time limit (its output is partial). */
872
+ timedOut = false;
873
+ get partial() {
874
+ return this.truncated || this.timedOut;
875
+ }
876
+ /**
877
+ * ripgrep stdout. Exit code 2 (error) with some output keeps the output (for example unreadable files);
878
+ * with no output it throws unless `allowFailure`, which then yields "".
879
+ */
880
+ async ripgrep(args, options = {}) {
881
+ this.signal.throwIfAborted();
882
+ this.calls++;
883
+ const r = await this.workspace.ripgrep(args, {
884
+ signal: this.signal,
885
+ timeoutMs: this.ripgrepTimeoutMs
886
+ });
887
+ this.signal.throwIfAborted();
888
+ if (r.truncated) this.truncated = true;
889
+ if (r.timedOut) this.timedOut = true;
890
+ if (r.exitCode === 2 && !r.stdout && !r.truncated && !r.timedOut && !options.allowFailure) {
891
+ throw new CodeSearchWorkspaceError("ripgrep failed (exit code 2) without output");
892
+ }
893
+ return r.stdout;
894
+ }
895
+ /** File text, or null when missing or binary (NUL bytes). */
896
+ async readText(path, maxBytes) {
897
+ this.signal.throwIfAborted();
898
+ this.calls++;
899
+ const r = await this.workspace.readText(path, { signal: this.signal, maxBytes });
900
+ this.signal.throwIfAborted();
901
+ if (!r || r.binary || r.text.includes("\0")) return null;
902
+ return r.text;
903
+ }
904
+ async pathKinds(paths) {
905
+ this.signal.throwIfAborted();
906
+ this.calls++;
907
+ const r = await this.workspace.pathKinds(paths, { signal: this.signal });
908
+ this.signal.throwIfAborted();
909
+ return r;
910
+ }
911
+ };
912
+ async function mapLimit(items, limit, fn) {
913
+ const out = new Array(items.length);
914
+ let next = 0;
915
+ const worker = async () => {
916
+ while (next < items.length) {
917
+ const i = next++;
918
+ out[i] = await fn(items[i]);
919
+ }
920
+ };
921
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
922
+ return out;
923
+ }
924
+
925
+ // src/code-search/text.ts
926
+ function splitWords(s) {
927
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.toLowerCase());
928
+ }
929
+ var cap = (w) => w ? w[0].toUpperCase() + w.slice(1) : w;
930
+ function keywordVariants(kw) {
931
+ const raw = kw.trim();
932
+ if (!raw) return [];
933
+ const words = splitWords(raw);
934
+ const out = [raw];
935
+ if (words.length >= 2) {
936
+ out.push(words[0] + words.slice(1).map(cap).join(""));
937
+ out.push(words.join("_"));
938
+ out.push(words.join("-"));
939
+ out.push(words.join(" "));
940
+ }
941
+ const seen = /* @__PURE__ */ new Set();
942
+ return out.filter((v) => {
943
+ const k = v.toLowerCase();
944
+ if (seen.has(k) || k.length < 2) return false;
945
+ seen.add(k);
946
+ return true;
947
+ });
948
+ }
949
+ function compoundFragments(kw) {
950
+ const words = splitWords(kw);
951
+ if (words.length < 3) return [];
952
+ const out = [];
953
+ for (let i = 0; i + 1 < words.length; i++) {
954
+ const a = words[i];
955
+ const b = words[i + 1];
956
+ if (a.length + b.length < 8) continue;
957
+ out.push(a + cap(b));
958
+ }
959
+ return [...new Set(out)];
960
+ }
961
+ function isShortPlainWord(kw, maxLen) {
962
+ return /^[A-Za-z]+$/.test(kw) && kw.length <= maxLen && splitWords(kw).length === 1;
963
+ }
964
+ function escapeRegex(s) {
965
+ return s.replace(/[\\.+*?()|[\]{}^$#&\-~]/g, (m) => `\\${m}`);
966
+ }
967
+ var STOPWORDS = new Set(
968
+ "a an and are as at be been but by can could did do does doing done for from had has have how i if in into is it its just me my no not of on or our should so some such than that the their them then there these they this those to too was we were what when where which while who why will with would you your also any each else ever every much more most other same very about above after again against all am before being below between both during few further here him his her hers himself itself let may might must nor now off once only own over under until up use used using want way get got make makes happen happens happening still actually really even whether instead rather something someone thing things one two three kind already mean means".split(" ")
969
+ );
970
+ function stem(w) {
971
+ let s = w.toLowerCase();
972
+ if (s.length > 5 && s.endsWith("ies")) s = s.slice(0, -3) + "y";
973
+ else if (s.length > 5 && (s.endsWith("ing") || s.endsWith("ers"))) s = s.slice(0, -3);
974
+ else if (s.length > 4 && (s.endsWith("ed") || s.endsWith("es") || s.endsWith("er")))
975
+ s = s.slice(0, -2);
976
+ else if (s.length > 3 && s.endsWith("s") && !s.endsWith("ss")) s = s.slice(0, -1);
977
+ return s;
978
+ }
979
+ function contentTerms(text, minLen = 3) {
980
+ const out = /* @__PURE__ */ new Set();
981
+ for (const tok of text.match(/[A-Za-z][A-Za-z0-9_]*/g) ?? []) {
982
+ for (const w of splitWords(tok)) {
983
+ if (w.length < minLen || STOPWORDS.has(w)) continue;
984
+ out.add(stem(w));
985
+ }
986
+ }
987
+ return [...out];
988
+ }
989
+ function overlap(terms, textTerms) {
990
+ if (!terms.length) return 0;
991
+ let n = 0;
992
+ for (const t of terms) if (textTerms.has(t)) n++;
993
+ return n / terms.length;
994
+ }
995
+ function isTestPath(p) {
996
+ return /(^|\/)(test|tests|__tests__|spec|e2e|fixtures?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(p);
997
+ }
998
+ function isChangelogPath(p) {
999
+ return /(^|\/)(CHANGELOG|HISTORY|RELEASE[-_]?NOTES)[^/]*$/i.test(p) || /(^|\/)\.changeset\//.test(p);
1000
+ }
1001
+ function questionMentionsHistory(q) {
1002
+ return /\b(changelog|release notes?|released|history|historical|when (?:was|did)|which version|since version|changed in)\b/i.test(
1003
+ q
1004
+ );
1005
+ }
1006
+ function isDocPath(p) {
1007
+ return /\.(md|mdx|txt|rst)$/i.test(p);
1008
+ }
1009
+ function questionMentionsTests(q) {
1010
+ return /\b(test|tests|testing|spec|specs|unit test|e2e|fixture)\b/i.test(q);
1011
+ }
1012
+ function trimAround(line, needles, maxChars) {
1013
+ const t = line.replace(/\t/g, " ").trim();
1014
+ if (t.length <= maxChars) return t;
1015
+ const lower = t.toLowerCase();
1016
+ let at = -1;
1017
+ for (const n of needles) {
1018
+ const i = lower.indexOf(n.toLowerCase());
1019
+ if (i >= 0 && (at < 0 || i < at)) at = i;
1020
+ }
1021
+ if (at < 0) return t.slice(0, maxChars - 3) + "...";
1022
+ const start = Math.max(0, Math.min(at - Math.floor(maxChars / 3), t.length - maxChars));
1023
+ const s = t.slice(start, start + maxChars - 6);
1024
+ return (start > 0 ? "..." : "") + s + (start + maxChars - 6 < t.length ? "..." : "");
1025
+ }
1026
+ function estTokens(chars, charsPerToken) {
1027
+ return Math.ceil(chars / charsPerToken);
1028
+ }
1029
+ function fmtK(n) {
1030
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
1031
+ }
1032
+
1033
+ // src/code-search/leads.ts
1034
+ var LEAD_STOPLIST = new Set(
1035
+ // JS/TS builtins, globals, utility types
1036
+ "Promise Array Object String Number Boolean Map Set WeakMap WeakSet Symbol Error TypeError RangeError SyntaxError JSON Math Date RegExp BigInt Buffer URL URLSearchParams console require parseInt parseFloat setTimeout clearTimeout setInterval clearInterval setImmediate queueMicrotask isNaN isFinite encodeURIComponent decodeURIComponent structuredClone fetch Response Request Headers FormData Blob File AbortController AbortSignal TextEncoder TextDecoder Uint8Array Int32Array Float64Array ArrayBuffer DataView Record Partial Readonly ReadonlyArray Pick Omit Exclude Extract ReturnType Parameters Awaited NonNullable Required InstanceType Iterable AsyncIterable Generator AsyncGenerator PromiseLike Function Uppercase Lowercase keyof typeof instanceof undefined null true false this super process Bun Deno globalThis window document performance crypto expect describe test beforeEach afterEach beforeAll afterAll mock spyOn toBe toEqual toStrictEqual toMatch toContain toThrow toHaveBeenCalled toHaveBeenCalledWith toHaveLength toBeDefined toBeUndefined toBeNull toBeTruthy toBeFalsy toMatchObject toBeGreaterThan toBeLessThan resolves rejects push pop shift unshift map filter reduce forEach find findIndex some every includes join split slice splice concat indexOf lastIndexOf keys values entries from assign freeze stringify parse toString valueOf trim trimStart trimEnd replace replaceAll match matchAll exec startsWith endsWith toLowerCase toUpperCase padStart padEnd localeCompare then catch finally resolve reject allSettled race floor ceil round abs sqrt random sort reverse flat flatMap fill length size delete clear warn error info debug trace assert emit once listen close write read send next done toISOString getTime toFixed charAt charCodeAt codePointAt fromEntries isArray hasOwnProperty defineProperty getOwnPropertyNames create apply call bind select insert update values returning where from innerJoin leftJoin orderBy groupBy limit offset execute inArray isNull isNotNull desc asc sql eq ne gt gte lt lte and or not like ilike between exists object string number boolean array optional nullable nullish literal union enum infer default describe safeParse parseAsync strict passthrough extend merge partial refine superRefine transform coerce positive nonnegative int min max email uuid regex coalesce count now greatest least jsonb_build_object jsonb_set jsonb_agg array_agg format lower upper nullif current_setting set_config gen_random_uuid clock_timestamp Ok Err Some None Box Vec Arc Mutex RwLock Option Result clone unwrap expect into iter collect map_err ok_or as_ref to_string to_owned println eprintln format vec".split(/\s+/)
1037
+ );
1038
+ var CONTROL = /* @__PURE__ */ new Set([
1039
+ "if",
1040
+ "for",
1041
+ "while",
1042
+ "switch",
1043
+ "catch",
1044
+ "return",
1045
+ "function",
1046
+ "typeof",
1047
+ "await",
1048
+ "async",
1049
+ "new",
1050
+ "else",
1051
+ "case",
1052
+ "void",
1053
+ "delete",
1054
+ "yield",
1055
+ "import",
1056
+ "export",
1057
+ "constructor",
1058
+ "super",
1059
+ "this",
1060
+ "static",
1061
+ "private",
1062
+ "public",
1063
+ "protected"
1064
+ ]);
1065
+ function definedNames(text) {
1066
+ const out = /* @__PURE__ */ new Set();
1067
+ const res = [
1068
+ /\bfunction\*?\s+([A-Za-z_$][\w$]*)/g,
1069
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g,
1070
+ /\b(?:class|interface|type|enum|namespace|struct|trait|fn|def|func|mod)\s+([A-Za-z_$][\w$]*)/g,
1071
+ /(?:FUNCTION|function)\s+(?:[\w"]+\.)?"?([A-Za-z_][\w]*)"?\s*\(/g,
1072
+ /^\s*(?:(?:public|private|protected|static|async|readonly|override|get|set)\s+)*([A-Za-z_$][\w$]*)\s*(?:<[^>()]*>)?\s*\([^;]*\)\s*(?::[^={;]+)?\{\s*$/gm
1073
+ ];
1074
+ for (const re of res) for (const m of text.matchAll(re)) if (!CONTROL.has(m[1])) out.add(m[1]);
1075
+ for (const m of text.matchAll(/\b(?:const|let|var)\s*\{([^}]*)\}/g)) {
1076
+ for (const part of m[1].split(",")) {
1077
+ const name = part.split(":").pop().split("=")[0].trim();
1078
+ if (/^[A-Za-z_$][\w$]*$/.test(name)) out.add(name);
1079
+ }
1080
+ }
1081
+ return out;
1082
+ }
1083
+ function identifiersInLine(line) {
1084
+ const out = [];
1085
+ for (const m of line.matchAll(/\b([A-Za-z_$][\w$]*)\s*(?:<[^<>()]*>)?\s*\(/g))
1086
+ out.push({ name: m[1], kind: "call" });
1087
+ const imp = /import\s+(?:type\s+)?\{([^}]*)\}/.exec(line);
1088
+ if (imp) {
1089
+ for (const part of imp[1].split(",")) {
1090
+ const name = part.replace(/^\s*type\s+/, "").split(/\s+as\s+/)[0].trim();
1091
+ if (/^[A-Za-z_$][\w$]*$/.test(name)) out.push({ name, kind: "import" });
1092
+ }
1093
+ }
1094
+ for (const m of line.matchAll(/\b([A-Z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+)\b/g))
1095
+ out.push({ name: m[1], kind: "type" });
1096
+ for (const m of line.matchAll(/\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g))
1097
+ out.push({ name: m[1], kind: "constant" });
1098
+ for (const m of line.matchAll(/\.([a-z][a-z0-9]*[A-Z][A-Za-z0-9]*)\b/g))
1099
+ out.push({ name: m[1], kind: "member" });
1100
+ return out;
1101
+ }
1102
+ function trimContext(line, name, max = 160) {
1103
+ const t = line.trim().replace(/\s+/g, " ");
1104
+ if (t.length <= max) return t;
1105
+ const i = Math.max(0, t.indexOf(name) - 50);
1106
+ return (i > 0 ? "..." : "") + t.slice(i, i + max - 6) + "...";
1107
+ }
1108
+ function extractLeads(seeds, searched, questionWords, max) {
1109
+ const defined = /* @__PURE__ */ new Set();
1110
+ for (const s of seeds) for (const n of definedNames(s.lines.join("\n"))) defined.add(n);
1111
+ const byName = /* @__PURE__ */ new Map();
1112
+ for (const s of seeds) {
1113
+ const inThis = /* @__PURE__ */ new Set();
1114
+ const prose = /\.(md|mdx|txt|rst)$/i.test(s.path);
1115
+ const sql = /\.sql$/i.test(s.path);
1116
+ s.lines.forEach((line, i) => {
1117
+ if (!prose && /^\s*(\/\/|\*|\/\*|#(?!\[)|--)/.test(line)) return;
1118
+ const scan = prose ? (line.match(/`[^`]+`/g) ?? []).join(" ") : line;
1119
+ if (!scan) return;
1120
+ for (const { name, kind } of identifiersInLine(scan)) {
1121
+ if (name.length < 4 || CONTROL.has(name) || LEAD_STOPLIST.has(name) || defined.has(name))
1122
+ continue;
1123
+ if (sql && /^[A-Z]+$/.test(name)) continue;
1124
+ if (searched.has(name.toLowerCase()) || searched.has(splitWords(name).join(" "))) continue;
1125
+ let c = byName.get(name);
1126
+ if (!c) {
1127
+ c = {
1128
+ name,
1129
+ weight: 0,
1130
+ occurrences: 0,
1131
+ called: false,
1132
+ seenAt: { path: s.path, line: s.start + i },
1133
+ context: trimContext(line, name),
1134
+ kinds: []
1135
+ };
1136
+ byName.set(name, c);
1137
+ }
1138
+ c.occurrences++;
1139
+ if (kind === "call") c.called = true;
1140
+ if (!c.kinds.includes(kind)) c.kinds.push(kind);
1141
+ if (!inThis.has(name)) {
1142
+ c.weight += s.rel;
1143
+ inThis.add(name);
1144
+ }
1145
+ }
1146
+ });
1147
+ }
1148
+ const score = (c) => {
1149
+ const words = splitWords(c.name);
1150
+ const ov = words.filter((w) => questionWords.has(w)).length / Math.max(1, words.length);
1151
+ return c.weight + 0.1 * Math.min(5, c.occurrences - 1) + (c.called ? 0.2 : 0) + ov;
1152
+ };
1153
+ return [...byName.values()].sort((a, b) => score(b) - score(a) || (a.name < b.name ? -1 : 1)).slice(0, max).map((c) => ({ ...c, weight: Math.round(score(c) * 1e3) / 1e3 }));
1154
+ }
1155
+ function definitionKinds(name) {
1156
+ const n = escapeRegex(name);
1157
+ return [
1158
+ {
1159
+ kind: "decl",
1160
+ re: new RegExp(
1161
+ `\\b(?:function\\*?|const|let|var|class|interface|type|enum|namespace|struct|trait|fn|def|func|mod)\\s+${n}\\b`
1162
+ )
1163
+ },
1164
+ { kind: "sqlfn", re: new RegExp(`\\bfunction\\s+(?:[\\w"]+\\.)?"?${n}"?\\s*\\(`, "i") },
1165
+ {
1166
+ kind: "method",
1167
+ re: new RegExp(
1168
+ `^\\s*(?:(?:public|private|protected|static|async|readonly|override|get|set)\\s+)*${n}\\s*(?:<[^>()]*>)?\\s*\\([^;]*(?:\\{|\\(|,)\\s*$`
1169
+ )
1170
+ },
1171
+ { kind: "key", re: new RegExp(`^\\s*["']?${n}["']?\\??\\s*[:=]`) }
1172
+ ];
1173
+ }
1174
+ function definitionPattern(names) {
1175
+ const alt = names.map(escapeRegex).join("|");
1176
+ return "(?-u:" + [
1177
+ `\\b(?:function\\*?|const|let|var|class|interface|type|enum|namespace|struct|trait|fn|def|func|mod)\\s+(?:${alt})\\b`,
1178
+ `(?i:function)\\s+(?:[\\w"]+\\.)?"?(?:${alt})"?\\s*\\(`,
1179
+ `^\\s*(?:(?:public|private|protected|static|async|readonly|override|get|set)\\s+)*(?:${alt})\\s*(?:<[^>()]*>)?\\s*\\(`,
1180
+ `^\\s*["']?(?:${alt})["']?\\??\\s*[:=]`
1181
+ ].join("|") + ")";
1182
+ }
1183
+ function definitionPatterns(names, maxChars = CODE_SEARCH_MAX_PATTERN_CHARS) {
1184
+ const out = [];
1185
+ let group = [];
1186
+ for (const name of names) {
1187
+ if (definitionPattern([name]).length > maxChars) continue;
1188
+ if (group.length && definitionPattern([...group, name]).length > maxChars) {
1189
+ out.push(definitionPattern(group));
1190
+ group = [];
1191
+ }
1192
+ group.push(name);
1193
+ }
1194
+ if (group.length) out.push(definitionPattern(group));
1195
+ return out;
1196
+ }
1197
+ var KIND_RANK = { decl: 3, sqlfn: 3, method: 2, key: 1 };
1198
+ function packageOf(path) {
1199
+ const parts = path.split("/");
1200
+ return /^(apps|packages|services|libs|crates)$/.test(parts[0] ?? "") && parts.length > 2 ? parts.slice(0, 2).join("/") : parts[0] ?? "";
1201
+ }
1202
+ function chooseDefinitions(hits, preferPaths, perName, allowTests = false, seenAt = /* @__PURE__ */ new Map()) {
1203
+ const by = /* @__PURE__ */ new Map();
1204
+ for (const h of hits) {
1205
+ if (!allowTests && isTestPath(h.path)) continue;
1206
+ const arr = by.get(h.name) ?? [];
1207
+ arr.push(h);
1208
+ by.set(h.name, arr);
1209
+ }
1210
+ const rank = (h) => (seenAt.get(h.name) === h.path ? 100 : 0) + (seenAt.has(h.name) && packageOf(seenAt.get(h.name)) === packageOf(h.path) ? 50 : 0) + KIND_RANK[h.kind] * 10 + (isTestPath(h.path) ? 0 : 4) + (preferPaths.has(h.path) ? 2 : 0) + (h.kind === "key" && /config|setting|schema|env/i.test(h.path) ? 1 : 0);
1211
+ const out = /* @__PURE__ */ new Map();
1212
+ for (const [name, arr] of by) {
1213
+ arr.sort(
1214
+ (a, b) => rank(b) - rank(a) || (a.path < b.path ? -1 : a.path > b.path ? 1 : a.line - b.line)
1215
+ );
1216
+ out.set(name, arr.slice(0, perName));
1217
+ }
1218
+ return out;
1219
+ }
1220
+ var DEF_SCAN_EXCLUDES = ["!*.{md,mdx,txt,rst,json,jsonc,html,csv,svg,snap,xml}"];
1221
+ var TEST_EXCLUDES = [
1222
+ "!**/test/**",
1223
+ "!**/tests/**",
1224
+ "!**/__tests__/**",
1225
+ "!**/e2e/**",
1226
+ "!**/fixtures/**",
1227
+ "!*.test.*",
1228
+ "!*.spec.*"
1229
+ ];
1230
+ async function locateDefinitions(session, names, cfg, excludeArgs2, maxKeyOnlyDefs = 12, allowTests = false) {
1231
+ const t0 = performance.now();
1232
+ if (!names.length) return { hits: [], fileCounts: /* @__PURE__ */ new Map(), ms: 0 };
1233
+ const extra = [...DEF_SCAN_EXCLUDES, ...allowTests ? [] : TEST_EXCLUDES].flatMap((g) => [
1234
+ "-g",
1235
+ g
1236
+ ]);
1237
+ const defOuts = await mapLimit(
1238
+ definitionPatterns(names),
1239
+ RIPGREP_SPLIT_CONCURRENCY,
1240
+ (pattern) => session.ripgrep(
1241
+ [
1242
+ "--null",
1243
+ "--line-number",
1244
+ "--with-filename",
1245
+ "--no-heading",
1246
+ "--color",
1247
+ "never",
1248
+ "--no-require-git",
1249
+ "--max-columns",
1250
+ String(cfg.recall.maxLineColumns),
1251
+ "--max-filesize",
1252
+ String(cfg.recall.maxFileBytes),
1253
+ ...excludeArgs2,
1254
+ ...extra,
1255
+ "-e",
1256
+ pattern,
1257
+ "--",
1258
+ "."
1259
+ ],
1260
+ { allowFailure: true }
1261
+ )
1262
+ );
1263
+ const kinds = new Map(names.map((n) => [n, definitionKinds(n)]));
1264
+ const nameSet = new Set(names);
1265
+ const files = /* @__PURE__ */ new Map();
1266
+ const strong = [];
1267
+ const keys = [];
1268
+ const seenRows = /* @__PURE__ */ new Set();
1269
+ for (const row of defOuts.flatMap((out) => out.split("\n"))) {
1270
+ const z = row.indexOf("\0");
1271
+ if (z <= 0) continue;
1272
+ const colon = row.indexOf(":", z + 1);
1273
+ if (colon < 0) continue;
1274
+ const at = row.slice(0, colon);
1275
+ if (seenRows.has(at)) continue;
1276
+ seenRows.add(at);
1277
+ const text = row.slice(colon + 1);
1278
+ if (text.length > 400 || text.startsWith("[Omitted long line")) continue;
1279
+ const path = row.slice(0, z).replace(/^\.\//, "");
1280
+ const line = Number(row.slice(z + 1, colon));
1281
+ const present = new Set((text.match(/[A-Za-z_$][\w$]*/g) ?? []).filter((t) => nameSet.has(t)));
1282
+ for (const name of present) {
1283
+ for (const k of kinds.get(name)) {
1284
+ if (!k.re.test(text)) continue;
1285
+ (k.kind === "key" ? keys : strong).push({
1286
+ name,
1287
+ path,
1288
+ line,
1289
+ kind: k.kind,
1290
+ text: text.trim().slice(0, 200)
1291
+ });
1292
+ let fs = files.get(name);
1293
+ if (!fs) files.set(name, fs = /* @__PURE__ */ new Set());
1294
+ fs.add(path);
1295
+ break;
1296
+ }
1297
+ }
1298
+ }
1299
+ const hasStrong = new Set(strong.map((h) => h.name));
1300
+ const keyHits = keys.filter((h) => !hasStrong.has(h.name));
1301
+ const keyCount = /* @__PURE__ */ new Map();
1302
+ for (const h of keyHits) keyCount.set(h.name, (keyCount.get(h.name) ?? 0) + 1);
1303
+ const hits = [...strong, ...keyHits.filter((h) => (keyCount.get(h.name) ?? 0) <= maxKeyOnlyDefs)];
1304
+ hits.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.line - b.line);
1305
+ const fileCounts = new Map(names.map((n) => [n, files.get(n)?.size ?? 0]));
1306
+ return { hits, fileCounts, ms: Math.round(performance.now() - t0) };
1307
+ }
1308
+
1309
+ // src/code-search/windows.ts
1310
+ function langOf(path) {
1311
+ const ext = path.toLowerCase().split(".").pop() ?? "";
1312
+ if ([
1313
+ "ts",
1314
+ "tsx",
1315
+ "js",
1316
+ "jsx",
1317
+ "mjs",
1318
+ "cjs",
1319
+ "mts",
1320
+ "cts",
1321
+ "rs",
1322
+ "go",
1323
+ "java",
1324
+ "c",
1325
+ "h",
1326
+ "cc",
1327
+ "cpp",
1328
+ "hpp",
1329
+ "cs",
1330
+ "swift",
1331
+ "kt",
1332
+ "scala",
1333
+ "css",
1334
+ "scss",
1335
+ "json",
1336
+ "jsonc",
1337
+ "proto",
1338
+ "tf",
1339
+ "hcl"
1340
+ ].includes(ext)) {
1341
+ return "brace";
1342
+ }
1343
+ if (["py", "yaml", "yml"].includes(ext)) return "indent";
1344
+ if (["md", "mdx"].includes(ext)) return "markdown";
1345
+ if (ext === "sql") return "sql";
1346
+ return "other";
1347
+ }
1348
+ function indentOf(line) {
1349
+ let n = 0;
1350
+ for (const ch of line) {
1351
+ if (ch === " ") n += 1;
1352
+ else if (ch === " ") n += 4;
1353
+ else break;
1354
+ }
1355
+ return n;
1356
+ }
1357
+ var CONTROL2 = /* @__PURE__ */ new Set([
1358
+ "if",
1359
+ "for",
1360
+ "while",
1361
+ "switch",
1362
+ "catch",
1363
+ "return",
1364
+ "else",
1365
+ "do",
1366
+ "try",
1367
+ "with",
1368
+ "await",
1369
+ "new",
1370
+ "typeof",
1371
+ "function",
1372
+ "throw",
1373
+ "yield",
1374
+ "delete",
1375
+ "void",
1376
+ "in",
1377
+ "of",
1378
+ "case",
1379
+ "super",
1380
+ "this",
1381
+ "import",
1382
+ "export"
1383
+ ]);
1384
+ var TS_DECL = /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\b|class\s|interface\s|type\s+[A-Za-z_$][\w$]*\s*(?:<[^>]*>)?\s*=|enum\s|const\s+[A-Za-z_$[{]|let\s+[A-Za-z_$[{]|var\s+[A-Za-z_$]|namespace\s|module\s)/;
1385
+ var EXPORT_DEFAULT = /^\s*export\s+default\b/;
1386
+ var RUST_DECL = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?(?:fn|struct|enum|trait|impl|mod|macro_rules!)[\s<]/;
1387
+ var GO_DECL = /^\s*func\s/;
1388
+ var TEST_DECL = /^\s*(?:describe|it|test)(?:\.\w+)?\s*\(/;
1389
+ var METHOD = /^\s*(?:(?:public|private|protected|static|readonly|async|override|get|set)\s+)*\*?\s*([A-Za-z_$][\w$]*)\s*(?:<[^>()]*>)?\s*\([^;]*$/;
1390
+ var PROP_FN = /^\s*(?:(?:public|private|protected|static|readonly)\s+)*[A-Za-z_$][\w$]*\s*[:=]\s*(?:async\s*)?(?:function\b|\([^)]*\)?[^;]*=>|[A-Za-z_$][\w$]*\s*=>)/;
1391
+ var OBJ_KEY_OPEN = /^\s*["']?[A-Za-z_$][\w$-]*["']?\s*:\s*(?:[\w$.]+\()?[{[]\s*$/;
1392
+ var CALLBACK_OPEN = /^\s*(?:await\s+)?[\w$]+(?:\.[\w$]+)+\s*\(.*(?:=>|function\b).*\{\s*$/;
1393
+ var MD_HEADING = /^(#{1,6})\s/;
1394
+ var SQL_STMT = /^(?:CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|WITH|SELECT|DO|COMMENT|GRANT|REVOKE|BEGIN|SET|LOCK|TRUNCATE)\b/i;
1395
+ var PY_DECL = /^\s*(?:async\s+)?(?:def|class)\s/;
1396
+ var YAML_KEY = /^\s*(?:- )?["']?[A-Za-z_$][\w$ .-]*["']?\s*:(?:\s|$)/;
1397
+ function isDeclLine(line, lang) {
1398
+ if (!line.trim()) return false;
1399
+ switch (lang) {
1400
+ case "markdown":
1401
+ return MD_HEADING.test(line);
1402
+ case "sql":
1403
+ return indentOf(line) === 0 && SQL_STMT.test(line);
1404
+ case "indent":
1405
+ return PY_DECL.test(line) || YAML_KEY.test(line);
1406
+ case "brace": {
1407
+ if (TS_DECL.test(line) || EXPORT_DEFAULT.test(line) || RUST_DECL.test(line) || GO_DECL.test(line) || TEST_DECL.test(line))
1408
+ return true;
1409
+ if (PROP_FN.test(line) || OBJ_KEY_OPEN.test(line) || CALLBACK_OPEN.test(line)) return true;
1410
+ const m = METHOD.exec(line);
1411
+ if (m && !CONTROL2.has(m[1]) && /(\{|\(|,)\s*$/.test(line.trimEnd()) && !/^\s*[\w$]+\s*\(.*\)\s*;?\s*$/.test(line)) {
1412
+ return true;
1413
+ }
1414
+ return false;
1415
+ }
1416
+ default:
1417
+ return false;
1418
+ }
1419
+ }
1420
+ function stripForBrackets(line) {
1421
+ const t = line.trim();
1422
+ if (t.startsWith("*") || t.startsWith("/*") || t.startsWith("//")) return "";
1423
+ return line.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''").replace(/`(?:[^`\\]|\\.)*`/g, "``").replace(/\/\*.*?\*\//g, "").replace(/\/\/.*$/, "");
1424
+ }
1425
+ var blockEndCache = /* @__PURE__ */ new WeakMap();
1426
+ function blockEnd(lines, d, lang, maxScan = 3e3) {
1427
+ let cache = blockEndCache.get(lines);
1428
+ if (!cache) {
1429
+ cache = /* @__PURE__ */ new Map();
1430
+ blockEndCache.set(lines, cache);
1431
+ }
1432
+ const key = d * 8 + ["brace", "indent", "markdown", "sql", "other"].indexOf(lang);
1433
+ if (cache.has(key)) return cache.get(key);
1434
+ const r = computeBlockEnd(lines, d, lang, maxScan);
1435
+ cache.set(key, r);
1436
+ return r;
1437
+ }
1438
+ function computeBlockEnd(lines, d, lang, maxScan) {
1439
+ const n = lines.length;
1440
+ if (d < 0 || d >= n) return null;
1441
+ const last = Math.min(n - 1, d + maxScan);
1442
+ if (lang === "brace") {
1443
+ let depth = 0;
1444
+ let maxDepth = 0;
1445
+ for (let i = d; i <= last; i++) {
1446
+ const s = stripForBrackets(lines[i]);
1447
+ for (const ch of s) {
1448
+ if (ch === "{" || ch === "(" || ch === "[") {
1449
+ depth++;
1450
+ if (depth > maxDepth) maxDepth = depth;
1451
+ } else if (ch === "}" || ch === ")" || ch === "]") depth--;
1452
+ }
1453
+ if (maxDepth > 0 && depth <= 0) return i;
1454
+ if (maxDepth === 0 && /;\s*$/.test(s)) return i;
1455
+ }
1456
+ return null;
1457
+ }
1458
+ if (lang === "indent") {
1459
+ const ind = indentOf(lines[d]);
1460
+ let lastNonEmpty = d;
1461
+ for (let i = d + 1; i <= last; i++) {
1462
+ const l = lines[i];
1463
+ if (!l.trim()) continue;
1464
+ if (indentOf(l) <= ind) return lastNonEmpty;
1465
+ lastNonEmpty = i;
1466
+ }
1467
+ return last === n - 1 ? lastNonEmpty : null;
1468
+ }
1469
+ if (lang === "markdown") {
1470
+ const m = MD_HEADING.exec(lines[d]);
1471
+ if (!m) return null;
1472
+ const level = m[1].length;
1473
+ let inFence = false;
1474
+ for (let i = d + 1; i <= last; i++) {
1475
+ const l = lines[i];
1476
+ if (/^\s*(```|~~~)/.test(l)) inFence = !inFence;
1477
+ if (inFence) continue;
1478
+ const h = MD_HEADING.exec(l);
1479
+ if (h && h[1].length <= level) return i - 1;
1480
+ }
1481
+ return last === n - 1 ? n - 1 : null;
1482
+ }
1483
+ if (lang === "sql") {
1484
+ let inDollar = false;
1485
+ for (let i = d; i <= last; i++) {
1486
+ const l = lines[i].replace(/--.*$/, "");
1487
+ const toggles = (l.match(/\$[A-Za-z_]*\$/g) ?? []).length;
1488
+ if (toggles % 2 === 1) inDollar = !inDollar;
1489
+ if (!inDollar && /;\s*$/.test(l)) return i;
1490
+ }
1491
+ return null;
1492
+ }
1493
+ return null;
1494
+ }
1495
+ function isCommentOrDecorator(line) {
1496
+ return /^(\/\/|\/\*\*?|\*|#\[|@[A-Za-z]|--)/.test(line.trim());
1497
+ }
1498
+ function leadingComments(lines, d, lang, maxLines = 6) {
1499
+ if (lang === "markdown" || lang === "other") return d;
1500
+ let s = d;
1501
+ while (s > 0 && d - s < maxLines && isCommentOrDecorator(lines[s - 1] ?? "")) s--;
1502
+ return s;
1503
+ }
1504
+ function nearestEnclosingLabel(lines, idx, lang, maxScan = 2e3) {
1505
+ const ind = indentOf(lines[idx] ?? "");
1506
+ for (let i = idx; i >= Math.max(0, idx - maxScan); i--) {
1507
+ const l = lines[i] ?? "";
1508
+ if (!isDeclLine(l, lang)) continue;
1509
+ if (lang === "markdown" || lang === "sql" || i === idx || indentOf(l) < ind || ind === 0 && indentOf(l) === 0) {
1510
+ return { line: i + 1, text: l.trim().slice(0, 140) };
1511
+ }
1512
+ }
1513
+ return void 0;
1514
+ }
1515
+ function enclosingWindow(lines, hitLine, lang, cfg) {
1516
+ const w = cfg.wave2;
1517
+ const hi = hitLine - 1;
1518
+ const n = lines.length;
1519
+ const hitIndent = indentOf(lines[hi] ?? "");
1520
+ if (lang !== "other") {
1521
+ for (let i = hi; i >= Math.max(0, hi - w.maxUp); i--) {
1522
+ const l = lines[i] ?? "";
1523
+ if (!isDeclLine(l, lang)) continue;
1524
+ if ((lang === "brace" || lang === "indent") && i !== hi && indentOf(l) > hitIndent) continue;
1525
+ const e = blockEnd(lines, i, lang);
1526
+ if (e === null || e < hi) continue;
1527
+ if (i === hi && e - hi < 2) continue;
1528
+ const start2 = leadingComments(lines, i, lang);
1529
+ const end2 = Math.min(e, hi + w.maxDown, n - 1);
1530
+ return padWindow(
1531
+ {
1532
+ start: start2 + 1,
1533
+ end: end2 + 1,
1534
+ hits: [hitLine],
1535
+ label: { line: i + 1, text: l.trim().slice(0, 140) },
1536
+ kind: "hit",
1537
+ score: 0
1538
+ },
1539
+ n,
1540
+ cfg
1541
+ );
1542
+ }
1543
+ }
1544
+ const start = Math.max(0, hi - w.fallbackBefore);
1545
+ const end = Math.min(n - 1, hi + w.fallbackAfter);
1546
+ const label = lang === "other" ? void 0 : nearestEnclosingLabel(lines, hi, lang);
1547
+ return { start: start + 1, end: end + 1, hits: [hitLine], label, kind: "hit", score: 0 };
1548
+ }
1549
+ function padWindow(win, nLines, cfg) {
1550
+ const min = cfg.wave2.minWindowLines;
1551
+ const len = win.end - win.start + 1;
1552
+ if (len >= min) return win;
1553
+ const need = min - len;
1554
+ const up = Math.floor(need / 3);
1555
+ let start = Math.max(1, win.start - up);
1556
+ let end = Math.min(nLines, win.end + (need - (win.start - start)));
1557
+ if (end - start + 1 < min) start = Math.max(1, end - min + 1);
1558
+ return { ...win, start, end };
1559
+ }
1560
+ function mergeWindows(ws, gap) {
1561
+ const sorted = [...ws].sort((a, b) => a.start - b.start || a.end - b.end);
1562
+ const out = [];
1563
+ for (const w of sorted) {
1564
+ const prev = out[out.length - 1];
1565
+ if (prev && w.start <= prev.end + gap + 1) {
1566
+ prev.end = Math.max(prev.end, w.end);
1567
+ prev.hits = [.../* @__PURE__ */ new Set([...prev.hits, ...w.hits])].sort((a, b) => a - b);
1568
+ if (w.label && (!prev.label || w.label.line < prev.label.line)) prev.label = w.label;
1569
+ if (prev.kind !== w.kind && w.kind === "def") prev.kind = "def";
1570
+ } else {
1571
+ out.push({ ...w, hits: [...w.hits] });
1572
+ }
1573
+ }
1574
+ return out;
1575
+ }
1576
+ function splitWindow(w, maxLines, before = 20) {
1577
+ if (w.end - w.start + 1 <= maxLines) return [w];
1578
+ const hits = [...w.hits].sort((a, b) => a - b);
1579
+ if (!hits.length) return [{ ...w, end: w.start + maxLines - 1 }];
1580
+ const out = [];
1581
+ let i = 0;
1582
+ while (i < hits.length) {
1583
+ let start = Math.max(w.start, hits[i] - before);
1584
+ if (w.start >= hits[i] - 2 * before) start = w.start;
1585
+ const end = Math.min(w.end, start + maxLines - 1);
1586
+ const chunkHits = [];
1587
+ while (i < hits.length && hits[i] <= end) chunkHits.push(hits[i++]);
1588
+ if (!chunkHits.length) {
1589
+ i++;
1590
+ continue;
1591
+ }
1592
+ out.push({
1593
+ ...w,
1594
+ start,
1595
+ end,
1596
+ hits: chunkHits,
1597
+ label: w.label && w.label.line < start ? w.label : w.label
1598
+ });
1599
+ }
1600
+ return out;
1601
+ }
1602
+ var kwRegexCache = /* @__PURE__ */ new WeakMap();
1603
+ function keywordRegex(k) {
1604
+ if (!kwRegexCache.has(k)) {
1605
+ let re = null;
1606
+ try {
1607
+ re = new RegExp(k.pattern, "i");
1608
+ } catch {
1609
+ re = null;
1610
+ }
1611
+ kwRegexCache.set(k, re);
1612
+ }
1613
+ return kwRegexCache.get(k);
1614
+ }
1615
+ function textHasKeyword(text, k) {
1616
+ const re = keywordRegex(k);
1617
+ if (re) return re.test(text);
1618
+ const lower = text.toLowerCase();
1619
+ return k.variants.some((v) => lower.includes(v.toLowerCase()));
1620
+ }
1621
+ function keywordsInRange(lines, start, end, keywords) {
1622
+ const text = lines.slice(start - 1, end).join("\n");
1623
+ return keywords.filter((k) => k.idf > 0 && textHasKeyword(text, k)).map((k) => k.index);
1624
+ }
1625
+ function scoreWindow(lines, w, keywords, hitWeight = 0.1) {
1626
+ const kws = keywordsInRange(lines, w.start, w.end, keywords);
1627
+ return kws.reduce((s, k) => s + keywords[k].idf, 0) + hitWeight * Math.log(1 + w.hits.length);
1628
+ }
1629
+ function buildFileWindows(lines, hitLines, keywords, lang, cfg, render) {
1630
+ const w = cfg.wave2;
1631
+ const ro = render ?? { maxLineChars: w.maxLineChars };
1632
+ const maxChars = lang === "markdown" ? w.maxProseWindowChars : w.maxWindowChars;
1633
+ if (!lines.length) return [];
1634
+ if (!hitLines.length) {
1635
+ const e = lang === "markdown" ? Math.min(lines.length, 80) : Math.min(lines.length, 60);
1636
+ return splitByChars(
1637
+ lines,
1638
+ { start: 1, end: e, hits: [], kind: "header", score: 0 },
1639
+ maxChars,
1640
+ ro
1641
+ ).slice(0, 1);
1642
+ }
1643
+ const inRange = hitLines.filter((h) => h.line >= 1 && h.line <= lines.length);
1644
+ const weight = (h) => h.kws.reduce((s, k) => s + (keywords[k]?.idf ?? 0), 0);
1645
+ const ranked = [...inRange].sort((a, b) => weight(b) - weight(a) || a.line - b.line);
1646
+ const budgetHits = w.seedHitsPerFile;
1647
+ const raw = [];
1648
+ let used = 0;
1649
+ for (const h of ranked) {
1650
+ if (used >= budgetHits) break;
1651
+ const inside = raw.find((r) => h.line >= r.start && h.line <= r.end);
1652
+ if (inside) {
1653
+ inside.hits.push(h.line);
1654
+ continue;
1655
+ }
1656
+ raw.push(enclosingWindow(lines, h.line, lang, cfg));
1657
+ used++;
1658
+ }
1659
+ for (const h of inRange) {
1660
+ for (const r of raw)
1661
+ if (h.line >= r.start && h.line <= r.end && !r.hits.includes(h.line)) r.hits.push(h.line);
1662
+ }
1663
+ const merged = mergeWindows(raw, w.mergeGap).flatMap((x) => splitWindow(x, w.maxWindowLines)).flatMap((x) => splitByChars(lines, x, maxChars, ro));
1664
+ for (const x of merged) x.score = scoreWindow(lines, x, keywords, cfg.recall.hitCountWeight);
1665
+ return merged.sort((a, b) => b.score - a.score || a.start - b.start).slice(0, w.windowsPerFile).sort((a, b) => a.start - b.start);
1666
+ }
1667
+ function definitionWindow(lines, defLine, lang, cfg, render) {
1668
+ const w = cfg.wave2;
1669
+ const d = defLine - 1;
1670
+ const start = leadingComments(lines, d, lang);
1671
+ const e = lang === "other" ? null : blockEnd(lines, d, lang);
1672
+ let end = e === null ? Math.min(lines.length - 1, d + w.fallbackAfter) : Math.min(e, d + w.maxDown);
1673
+ end = Math.min(end, start + w.maxWindowLines - 1, lines.length - 1);
1674
+ const win = {
1675
+ start: start + 1,
1676
+ end: end + 1,
1677
+ hits: [defLine],
1678
+ label: { line: defLine, text: (lines[d] ?? "").trim().slice(0, 140) },
1679
+ kind: "def",
1680
+ score: 0
1681
+ };
1682
+ const maxChars = lang === "markdown" ? w.maxProseWindowChars : w.maxWindowChars;
1683
+ return splitByChars(
1684
+ lines,
1685
+ win,
1686
+ maxChars,
1687
+ render ?? { maxLineChars: w.maxLineChars },
1688
+ defLine - win.start
1689
+ )[0];
1690
+ }
1691
+ function cutLine(t, maxChars, needles = []) {
1692
+ if (t.length <= maxChars) return t;
1693
+ let at = -1;
1694
+ for (const re of needles) {
1695
+ const m = re.exec(t);
1696
+ if (m && (at < 0 || m.index < at)) at = m.index;
1697
+ }
1698
+ if (at < 0 || at < maxChars * 0.6)
1699
+ return `${t.slice(0, maxChars)} ...[line cut, ${t.length} chars]`;
1700
+ const head = Math.floor(maxChars * 0.25);
1701
+ const before = Math.floor(maxChars * 0.3);
1702
+ const s = Math.max(head, at - before);
1703
+ const e = Math.min(t.length, s + (maxChars - head));
1704
+ return `${t.slice(0, head)} ...[cut]... ${t.slice(s, e)}${e < t.length ? ` ...[line cut, ${t.length} chars]` : ""}`;
1705
+ }
1706
+ function renderLines(lines, start, end, opts) {
1707
+ const o = typeof opts === "number" ? { maxLineChars: opts } : opts;
1708
+ const out = [];
1709
+ for (let i = start; i <= end && i <= lines.length; i++) {
1710
+ out.push(`${i}| ${cutLine(lines[i - 1].replace(/\t/g, " "), o.maxLineChars, o.needles)}`);
1711
+ }
1712
+ return out.join("\n");
1713
+ }
1714
+ function renderedLineLength(lines, i, o) {
1715
+ return renderLines(lines, i, i, o).length + 1;
1716
+ }
1717
+ function splitByChars(lines, w, maxChars, o, ctxUp = 3) {
1718
+ const len = (i) => renderedLineLength(lines, i, o);
1719
+ let total = 0;
1720
+ for (let i = w.start; i <= w.end; i++) total += len(i);
1721
+ if (total <= maxChars) return [w];
1722
+ const hits = [...w.hits].filter((h) => h >= w.start && h <= w.end).sort((a, b) => a - b);
1723
+ const anchors = hits.length ? hits : [w.start];
1724
+ const out = [];
1725
+ let covered = w.start - 1;
1726
+ for (const h of anchors) {
1727
+ if (h <= covered) continue;
1728
+ let s = h;
1729
+ let used = len(h);
1730
+ while (s - 1 >= w.start && s - 1 > covered && h - (s - 1) <= ctxUp && used + len(s - 1) <= maxChars / 4)
1731
+ used += len(--s);
1732
+ let e = h;
1733
+ while (e + 1 <= w.end && used + len(e + 1) <= maxChars) used += len(++e);
1734
+ out.push({ ...w, start: s, end: e, hits: hits.filter((x) => x >= s && x <= e) });
1735
+ covered = e;
1736
+ }
1737
+ return out;
1738
+ }
1739
+ function splitLines(text) {
1740
+ const lines = text.split(/\r?\n/);
1741
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
1742
+ return lines;
1743
+ }
1744
+
1745
+ // src/code-search/pack.ts
1746
+ var r2 = (x) => Number.isFinite(x) ? x.toFixed(2) : "?";
1747
+ function diverseOrder(xs, eff, penalty, taken) {
1748
+ const tie = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.start - b.start;
1749
+ if (penalty <= 0) return [...xs].sort((a, b) => eff(b) - eff(a) || tie(a, b));
1750
+ const left = [...xs];
1751
+ const out = [];
1752
+ while (left.length) {
1753
+ let bi = 0;
1754
+ let bv = -Infinity;
1755
+ left.forEach((x2, i) => {
1756
+ const v = eff(x2) - penalty * (taken.get(x2.path) ?? 0);
1757
+ if (v > bv || v === bv && tie(x2, left[bi]) < 0) {
1758
+ bv = v;
1759
+ bi = i;
1760
+ }
1761
+ });
1762
+ const x = left.splice(bi, 1)[0];
1763
+ taken.set(x.path, (taken.get(x.path) ?? 0) + 1);
1764
+ out.push(x);
1765
+ }
1766
+ return out;
1767
+ }
1768
+ function pathPrior(path, o) {
1769
+ const p = o.cfg.pack;
1770
+ if (isChangelogPath(path)) return o.downweightChangelogs !== false ? p.changelogPrior : 1;
1771
+ if (isTestPath(path)) return o.downweightTests !== false ? p.testPrior : 1;
1772
+ if (isDocPath(path)) return p.docPrior;
1773
+ return 1;
1774
+ }
1775
+ function packScore(x, o) {
1776
+ return (x.rel + o.cfg.pack.lexWeight * (x.lex ?? 0)) * pathPrior(x.path, o);
1777
+ }
1778
+ function priorityOrder(passages, o) {
1779
+ const p = o.cfg.pack;
1780
+ const effCache = /* @__PURE__ */ new Map();
1781
+ const eff = (x) => {
1782
+ let v = effCache.get(x);
1783
+ if (v === void 0) effCache.set(x, v = packScore(x, o));
1784
+ return v;
1785
+ };
1786
+ const byRel = [...passages].sort(
1787
+ (a, b) => eff(b) - eff(a) || (a.path < b.path ? -1 : a.path > b.path ? 1 : a.start - b.start)
1788
+ );
1789
+ const out = [];
1790
+ const taken = /* @__PURE__ */ new Map();
1791
+ const add = (x) => {
1792
+ if (!out.includes(x)) {
1793
+ out.push(x);
1794
+ taken.set(x.path, (taken.get(x.path) ?? 0) + 1);
1795
+ }
1796
+ };
1797
+ o.subQuestions.forEach((_, j) => {
1798
+ const best = [...passages].filter((x) => (x.cov[j] ?? 0) >= o.T2 && x.rel >= p.minRelevance).sort((a, b) => (b.cov[j] ?? 0) - (a.cov[j] ?? 0) || b.rel - a.rel)[0];
1799
+ if (best) add(best);
1800
+ });
1801
+ for (const x of diverseOrder(
1802
+ byRel.filter((y) => y.rel >= o.T2 && !out.includes(y)),
1803
+ eff,
1804
+ p.filePenalty,
1805
+ new Map(taken)
1806
+ ))
1807
+ add(x);
1808
+ for (const x of byRel) {
1809
+ if (out.length >= p.minPassages) break;
1810
+ if (x.rel >= p.minRelevance) add(x);
1811
+ }
1812
+ if (p.fillBudget)
1813
+ for (const x of diverseOrder(
1814
+ byRel.filter((y) => y.rel >= p.minRelevance && !out.includes(y)),
1815
+ eff,
1816
+ p.filePenalty,
1817
+ new Map(taken)
1818
+ ))
1819
+ add(x);
1820
+ return out;
1821
+ }
1822
+ function blockHeader(x, start, end, o) {
1823
+ const covTags = o.subQuestions.map((_, j) => (x.cov[j] ?? 0) >= o.T2 ? `s${j + 1}` : "").filter(Boolean);
1824
+ const parts = [`== ${x.path}:${start}-${end} rel ${r2(x.rel)}`];
1825
+ if (covTags.length) parts.push(`[${covTags.join(" ")}]`);
1826
+ if (x.kind === "def" && x.lead) parts.push(`(definition of ${x.lead})`);
1827
+ if (start !== x.start || end !== x.end) parts.push(`(trimmed from ${x.start}-${x.end})`);
1828
+ let h = parts.join(" ");
1829
+ if (x.label && x.label.line < start) h += `
1830
+ in L${x.label.line}: ${x.label.text}`;
1831
+ return h;
1832
+ }
1833
+ function renderBlock(x, start, end, o) {
1834
+ return `${blockHeader(x, start, end, o)}
1835
+ ${renderLines(x.fileLines, start, end, x.render ?? o.cfg.wave2.maxLineChars)}`;
1836
+ }
1837
+ function trimToFit(x, maxChars, o) {
1838
+ const lineLen = (i) => renderLines(x.fileLines, i, i, x.render ?? o.cfg.wave2.maxLineChars).length + 1;
1839
+ const hdr = blockHeader(x, x.start, x.end, o).length + 40;
1840
+ const inside = x.hits.filter((h) => h >= x.start && h <= x.end).sort((a, b) => a - b);
1841
+ const center = inside.length ? inside[Math.floor((inside.length - 1) / 2)] : x.start;
1842
+ let s = center;
1843
+ let e = center;
1844
+ let used = hdr + lineLen(center);
1845
+ if (used > maxChars) return null;
1846
+ let stuckUp = false;
1847
+ let stuckDown = false;
1848
+ let turn = 0;
1849
+ while (!(stuckUp && stuckDown)) {
1850
+ const goDown = !stuckDown && (stuckUp || turn % 3 !== 2);
1851
+ turn++;
1852
+ if (goDown) {
1853
+ if (e + 1 > x.end) {
1854
+ stuckDown = true;
1855
+ continue;
1856
+ }
1857
+ const c = lineLen(e + 1);
1858
+ if (used + c > maxChars) stuckDown = true;
1859
+ else {
1860
+ e++;
1861
+ used += c;
1862
+ }
1863
+ } else {
1864
+ if (s - 1 < x.start) {
1865
+ stuckUp = true;
1866
+ continue;
1867
+ }
1868
+ const c = lineLen(s - 1);
1869
+ if (used + c > maxChars) stuckUp = true;
1870
+ else {
1871
+ s--;
1872
+ used += c;
1873
+ }
1874
+ }
1875
+ }
1876
+ if (e - s + 1 < o.cfg.pack.minTrimLines && e - s + 1 < x.end - x.start + 1) return null;
1877
+ return { start: s, end: e };
1878
+ }
1879
+ function packBody(passages, o) {
1880
+ const order = priorityOrder(passages, o);
1881
+ let remaining = o.bodyChars;
1882
+ const included = [];
1883
+ for (const x of order) {
1884
+ if (remaining < 200) break;
1885
+ let s = x.start;
1886
+ let e = x.end;
1887
+ let block = renderBlock(x, s, e, o);
1888
+ const cap2 = o.cfg.pack.maxPassageChars;
1889
+ if (cap2 > 0 && block.length > cap2) {
1890
+ const t = trimToFit(x, cap2, o);
1891
+ if (t) {
1892
+ s = t.start;
1893
+ e = t.end;
1894
+ block = renderBlock(x, s, e, o);
1895
+ }
1896
+ }
1897
+ if (block.length + 2 > remaining) {
1898
+ const t = trimToFit({ ...x, start: s, end: e }, remaining - 2, o);
1899
+ if (!t) continue;
1900
+ s = t.start;
1901
+ e = t.end;
1902
+ block = renderBlock(x, s, e, o);
1903
+ if (block.length + 2 > remaining) continue;
1904
+ }
1905
+ remaining -= block.length + 2;
1906
+ included.push({
1907
+ id: x.id,
1908
+ path: x.path,
1909
+ start: s,
1910
+ end: e,
1911
+ origStart: x.start,
1912
+ origEnd: x.end,
1913
+ trimmed: s !== x.start || e !== x.end,
1914
+ rel: x.rel,
1915
+ cov: x.cov,
1916
+ kind: x.kind,
1917
+ lead: x.lead,
1918
+ label: x.label,
1919
+ block
1920
+ });
1921
+ }
1922
+ const inc = new Set(included.map((p) => p.id));
1923
+ const excluded = passages.filter((x) => !inc.has(x.id)).sort((a, b) => b.rel - a.rel);
1924
+ const fileBest = /* @__PURE__ */ new Map();
1925
+ for (const p of included) fileBest.set(p.path, Math.max(fileBest.get(p.path) ?? 0, p.rel));
1926
+ const grouped = [...included].sort(
1927
+ (a, b) => fileBest.get(b.path) - fileBest.get(a.path) || (a.path < b.path ? -1 : a.path > b.path ? 1 : a.start - b.start)
1928
+ );
1929
+ return {
1930
+ included: grouped,
1931
+ excluded,
1932
+ body: grouped.map((p) => p.block).join("\n\n"),
1933
+ priority: order.map((x) => x.id)
1934
+ };
1935
+ }
1936
+ function renderFooter(f) {
1937
+ const p = f.cfg.pack;
1938
+ const more = f.excluded.slice(0, p.moreCandidates).map((x) => `${x.path}:${x.start}-${x.end} (${r2(x.rel)})`);
1939
+ const moreFiles = f.otherFiles.slice(0, Math.max(0, p.moreCandidates - more.length)).map((x) => `${x.path} (${r2(x.score)})`);
1940
+ const leads = f.leadsNotFollowed.slice(0, p.leadsNotFollowed).map((l2) => `${l2.name} (${r2(l2.score)}${l2.note ? `, ${l2.note}` : ""}) @${l2.seenAt}`);
1941
+ const zero = f.zeroHitKeywords.map(
1942
+ (k) => k.fragments.length ? `${k.raw} (matched fragments: ${k.fragments.join(", ")})` : k.raw
1943
+ );
1944
+ const build = (m2, mf2, l2) => {
1945
+ const lines = [];
1946
+ if (m2.length || mf2.length) {
1947
+ lines.push("More candidates (not included; read if needed):");
1948
+ if (m2.length) lines.push(` ${m2.join(", ")}`);
1949
+ if (mf2.length) lines.push(` files: ${mf2.join(", ")}`);
1950
+ }
1951
+ if (l2.length) lines.push(`Leads not followed: ${l2.join(", ")}`);
1952
+ if (zero.length) lines.push(`Keywords with zero hits: ${zero.join(", ")}`);
1953
+ if (f.widenedNote) lines.push(f.widenedNote);
1954
+ return lines.join("\n");
1955
+ };
1956
+ let m = more;
1957
+ let mf = moreFiles;
1958
+ let l = leads;
1959
+ let out = build(m, mf, l);
1960
+ while (out.length > f.maxChars && (m.length || mf.length || l.length)) {
1961
+ if (mf.length) mf = mf.slice(0, -1);
1962
+ else if (l.length > 2) l = l.slice(0, -1);
1963
+ else if (m.length) m = m.slice(0, -1);
1964
+ else l = l.slice(0, -1);
1965
+ out = build(m, mf, l);
1966
+ }
1967
+ return out;
1968
+ }
1969
+
1970
+ // src/code-search/recall.ts
1971
+ var BUILTIN_EXCLUDES = [
1972
+ "!**/node_modules/**",
1973
+ "!**/dist/**",
1974
+ "!**/build/**",
1975
+ "!**/.next/**",
1976
+ "!**/coverage/**",
1977
+ "!**/target/**",
1978
+ "!**/vendor/**",
1979
+ "!**/.git/**",
1980
+ // a linked worktree has a `.git` FILE (gitdir pointer), which --hidden would otherwise search
1981
+ "!**/.git",
1982
+ "!**/.turbo/**",
1983
+ "!*.lock",
1984
+ "!**/package-lock.json",
1985
+ "!**/pnpm-lock.yaml",
1986
+ "!**/yarn.lock",
1987
+ "!**/bun.lockb",
1988
+ "!*.min.js",
1989
+ "!*.min.css",
1990
+ "!*.map",
1991
+ "!*.snap",
1992
+ "!**/*.gen.*",
1993
+ "!**/*.generated.*",
1994
+ "!**/gen/**",
1995
+ "!**/*_pb.*",
1996
+ "!**/*.pb.go",
1997
+ "!*.{svg,png,jpg,jpeg,gif,ico,webp,woff,woff2,ttf,otf,eot,wasm,pdf,zip,gz,tgz,mp4,mp3,mov}"
1998
+ ];
1999
+ function excludeArgs(cfg) {
2000
+ return [
2001
+ ...BUILTIN_EXCLUDES,
2002
+ ...cfg.recall.extraExcludes.map((g) => g.startsWith("!") ? g : `!${g}`)
2003
+ ].flatMap((g) => ["-g", g]);
2004
+ }
2005
+ function hiddenArgs(cfg) {
2006
+ return cfg.recall.searchHidden ? ["--hidden"] : [];
2007
+ }
2008
+ async function listFiles(session, paths, cfg) {
2009
+ const out = await session.ripgrep(
2010
+ [
2011
+ "--files",
2012
+ "--no-require-git",
2013
+ ...hiddenArgs(cfg),
2014
+ "--max-filesize",
2015
+ String(cfg.recall.maxFileBytes),
2016
+ ...excludeArgs(cfg),
2017
+ "--",
2018
+ ...paths
2019
+ ],
2020
+ { allowFailure: true }
2021
+ );
2022
+ return out.split("\n").filter(Boolean).map((p) => p.replace(/^\.\//, "")).sort();
2023
+ }
2024
+ function buildPattern(kw, cfg) {
2025
+ const variants = keywordVariants(kw);
2026
+ if (variants.length === 1 && isShortPlainWord(variants[0], cfg.recall.shortWordMaxLen)) {
2027
+ const lit = escapeRegex(variants[0]);
2028
+ return { pattern: `\\b${lit}`, rgPattern: `(?-u:\\b)${lit}`, mode: "word-prefix", variants };
2029
+ }
2030
+ const alts = variants.map((v) => escapeRegex(v).replace(/ /g, "\\s+"));
2031
+ return { pattern: alts.join("|"), rgPattern: alts.join("|"), mode: "phrase", variants };
2032
+ }
2033
+ function fragmentPattern(frags) {
2034
+ return frags.flatMap((f) => keywordVariants(f)).map((v) => escapeRegex(v).replace(/ /g, "\\s+")).join("|");
2035
+ }
2036
+ function parseRgOutput(out) {
2037
+ const matches = [];
2038
+ let pos = 0;
2039
+ while (pos < out.length) {
2040
+ let nl = out.indexOf("\n", pos);
2041
+ if (nl < 0) nl = out.length;
2042
+ const z = out.indexOf("\0", pos);
2043
+ if (z > pos && z < nl) {
2044
+ const colon = out.indexOf(":", z + 1);
2045
+ if (colon > z && colon < nl) {
2046
+ const line = Number(out.slice(z + 1, colon));
2047
+ const text = out.slice(colon + 1, nl).replace(/\r$/, "");
2048
+ if (Number.isFinite(line) && !text.startsWith("[Omitted long line")) {
2049
+ matches.push({ path: out.slice(pos, z).replace(/^\.\//, ""), line, text });
2050
+ }
2051
+ }
2052
+ }
2053
+ pos = nl + 1;
2054
+ }
2055
+ return matches;
2056
+ }
2057
+ var byPathLine = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.line - b.line;
2058
+ function ripgrepPatternCompiles(pattern) {
2059
+ return jsRegex(pattern.replace(/\(\?[A-Za-z-]+:/g, "(?:")) !== null;
2060
+ }
2061
+ async function searchPattern(session, pattern, paths, cfg, opts = {}) {
2062
+ const args = [
2063
+ "--null",
2064
+ "--line-number",
2065
+ "--with-filename",
2066
+ "--no-heading",
2067
+ "--color",
2068
+ "never",
2069
+ ...opts.caseInsensitive === false ? [] : ["-i"],
2070
+ ...opts.word ? ["-w"] : [],
2071
+ "--no-require-git",
2072
+ ...hiddenArgs(cfg),
2073
+ ...opts.maxPerFile ? ["-m", String(opts.maxPerFile)] : [],
2074
+ // very long lines (minified / generated / data) are omitted by rg and skipped by the parser
2075
+ "--max-columns",
2076
+ String(cfg.recall.maxLineColumns),
2077
+ "--max-filesize",
2078
+ String(cfg.recall.maxFileBytes),
2079
+ ...excludeArgs(cfg),
2080
+ "-e",
2081
+ pattern,
2082
+ "--",
2083
+ ...paths
2084
+ ];
2085
+ const out = await session.ripgrep(args, { allowFailure: ripgrepPatternCompiles(pattern) });
2086
+ const matches = parseRgOutput(out);
2087
+ matches.sort(byPathLine);
2088
+ return matches;
2089
+ }
2090
+ function splitAlternation(pattern) {
2091
+ const out = [];
2092
+ let depth = 0;
2093
+ let inClass = false;
2094
+ let start = 0;
2095
+ for (let i = 0; i < pattern.length; i++) {
2096
+ const ch = pattern[i];
2097
+ if (ch === "\\") i++;
2098
+ else if (inClass) inClass = ch !== "]";
2099
+ else if (ch === "[") inClass = true;
2100
+ else if (ch === "(") depth++;
2101
+ else if (ch === ")") depth--;
2102
+ else if (ch === "|" && depth === 0) {
2103
+ out.push(pattern.slice(start, i));
2104
+ start = i + 1;
2105
+ }
2106
+ }
2107
+ out.push(pattern.slice(start));
2108
+ return out;
2109
+ }
2110
+ function packAlternatives(alternatives, maxChars) {
2111
+ const out = [];
2112
+ let cur = null;
2113
+ for (const alt of alternatives) {
2114
+ if (cur !== null && cur.length + 1 + alt.length <= maxChars) {
2115
+ cur += `|${alt}`;
2116
+ } else {
2117
+ if (cur !== null) out.push(cur);
2118
+ cur = alt;
2119
+ }
2120
+ }
2121
+ if (cur !== null) out.push(cur);
2122
+ return out;
2123
+ }
2124
+ function mergeMatches(lists) {
2125
+ const all = lists.flat().sort(byPathLine);
2126
+ return all.filter(
2127
+ (m, i) => i === 0 || m.path !== all[i - 1].path || m.line !== all[i - 1].line
2128
+ );
2129
+ }
2130
+ async function searchAlternatives(session, alternatives, paths, cfg, maxPatternChars = CODE_SEARCH_MAX_PATTERN_CHARS) {
2131
+ const groups = alternatives.flatMap(
2132
+ (p) => p.length + 4 <= maxPatternChars ? [`(?:${p})`] : splitAlternation(p).map((a) => `(?:${a})`)
2133
+ ).filter((g) => g.length <= maxPatternChars);
2134
+ const patterns = packAlternatives(groups, maxPatternChars);
2135
+ if (patterns.length <= 1) {
2136
+ return patterns.length ? searchPattern(session, patterns[0], paths, cfg) : [];
2137
+ }
2138
+ const found = await mapLimit(
2139
+ patterns,
2140
+ RIPGREP_SPLIT_CONCURRENCY,
2141
+ (p) => searchPattern(session, p, paths, cfg)
2142
+ );
2143
+ return mergeMatches(found);
2144
+ }
2145
+ function idfOf(df, n) {
2146
+ return Math.log(1 + n / (1 + df));
2147
+ }
2148
+ function pathMatches(path, variants) {
2149
+ const flatPath = path.toLowerCase().replace(/[-_ .]/g, "");
2150
+ return variants.some((v) => {
2151
+ const flat = v.toLowerCase().replace(/[-_ ]/g, "");
2152
+ return flat.length >= 4 && flatPath.includes(flat);
2153
+ });
2154
+ }
2155
+ function jsRegex(pattern) {
2156
+ try {
2157
+ return new RegExp(pattern, "i");
2158
+ } catch {
2159
+ return null;
2160
+ }
2161
+ }
2162
+ function attribute(matches, slots) {
2163
+ const out = slots.map(() => []);
2164
+ for (const m of matches) {
2165
+ const lower = m.text.toLowerCase();
2166
+ slots.forEach((s, i) => {
2167
+ const hit = s.re ? s.re.test(m.text) : s.literals.some((l) => lower.includes(l));
2168
+ if (hit) out[i].push(m);
2169
+ });
2170
+ }
2171
+ return out;
2172
+ }
2173
+ async function normalizePrefixes(session, prefixes) {
2174
+ const cleaned = prefixes.map((raw) => ({ raw, rel: cleanPrefix(raw) }));
2175
+ const lookup = [
2176
+ ...new Set(cleaned.map((c) => c.rel).filter((r) => r !== null && r !== "."))
2177
+ ];
2178
+ const kinds = lookup.length ? await session.pathKinds(lookup) : {};
2179
+ const ok = [];
2180
+ const missing = [];
2181
+ for (const { raw, rel } of cleaned) {
2182
+ if (rel === ".") ok.push(".");
2183
+ else if (rel !== null && (kinds[rel] === "file" || kinds[rel] === "directory")) ok.push(rel);
2184
+ else missing.push(raw);
2185
+ }
2186
+ return { ok: [...new Set(ok)], missing };
2187
+ }
2188
+ function cleanPrefix(p) {
2189
+ let s = p.trim().replace(/\\/g, "/");
2190
+ if (!s || s.startsWith("/") || s.startsWith("~") || /^[A-Za-z]:\//.test(s)) return null;
2191
+ while (s.startsWith("./")) s = s.slice(2);
2192
+ s = s.replace(/\/+$/, "").replace(/\/{2,}/g, "/");
2193
+ if (s === "" || s === ".") return ".";
2194
+ if (s.startsWith("-") || s.split("/").some((seg) => seg === "..")) return null;
2195
+ return s;
2196
+ }
2197
+ async function looksBinary(session, path, bytes = 8192) {
2198
+ return await session.readText(path, bytes) === null;
2199
+ }
2200
+ async function recall(input) {
2201
+ const t0 = performance.now();
2202
+ const cfg = input.config;
2203
+ const session = input.session;
2204
+ const kwsRaw = [
2205
+ ...new Set(
2206
+ input.keywords.map((k) => k.replace(/\s+/g, " ").trim()).filter((k) => k.length >= 2)
2207
+ )
2208
+ ];
2209
+ const prefixes = await normalizePrefixes(session, input.pathPrefixes);
2210
+ let searchPaths = prefixes.ok.length ? prefixes.ok : ["."];
2211
+ let widened = input.pathPrefixes.length > 0 && prefixes.ok.length === 0;
2212
+ const keywordsFresh = () => kwsRaw.map((raw, index) => {
2213
+ const { pattern, rgPattern, mode, variants } = buildPattern(raw, cfg);
2214
+ return {
2215
+ index,
2216
+ raw,
2217
+ variants,
2218
+ mode,
2219
+ pattern,
2220
+ rgPattern,
2221
+ df: 0,
2222
+ pathDf: 0,
2223
+ hitLines: 0,
2224
+ idf: 0,
2225
+ fragments: []
2226
+ };
2227
+ });
2228
+ const attempt = async (paths) => {
2229
+ const keywords2 = keywordsFresh();
2230
+ const slots = [];
2231
+ const fragsOf = /* @__PURE__ */ new Map();
2232
+ for (const k of keywords2) {
2233
+ slots.push({
2234
+ ki: k.index,
2235
+ fragment: false,
2236
+ re: jsRegex(k.pattern),
2237
+ literals: k.variants.map((v) => v.toLowerCase())
2238
+ });
2239
+ const frags = cfg.recall.fragmentFallback ? compoundFragments(k.raw) : [];
2240
+ if (frags.length) {
2241
+ const pattern = fragmentPattern(frags);
2242
+ fragsOf.set(k.index, { frags, pattern });
2243
+ slots.push({
2244
+ ki: k.index,
2245
+ fragment: true,
2246
+ re: jsRegex(pattern),
2247
+ literals: frags.flatMap((f) => keywordVariants(f)).map((v) => v.toLowerCase())
2248
+ });
2249
+ }
2250
+ }
2251
+ const alternatives = [
2252
+ ...keywords2.map((k) => k.rgPattern),
2253
+ ...[...fragsOf.values()].map((f) => f.pattern)
2254
+ ];
2255
+ const [files2, matches] = await Promise.all([
2256
+ listFiles(session, paths, cfg),
2257
+ searchAlternatives(session, alternatives, paths, cfg)
2258
+ ]);
2259
+ const bySlot = attribute(matches, slots);
2260
+ const results2 = keywords2.map(() => []);
2261
+ slots.forEach((s, i) => {
2262
+ if (!s.fragment) results2[s.ki] = bySlot[i];
2263
+ });
2264
+ slots.forEach((s, i) => {
2265
+ if (s.fragment && results2[s.ki].length === 0 && bySlot[i].length > 0) {
2266
+ results2[s.ki] = bySlot[i];
2267
+ keywords2[s.ki].fragments = fragsOf.get(s.ki).frags;
2268
+ }
2269
+ });
2270
+ return { files: files2, keywords: keywords2, results: results2 };
2271
+ };
2272
+ let { files, keywords, results } = await attempt(searchPaths);
2273
+ const countFiles = (rs) => new Set(rs.flat().map((m) => m.path)).size;
2274
+ if (prefixes.ok.length && countFiles(results) < cfg.recall.minCandidatesBeforeWiden) {
2275
+ searchPaths = ["."];
2276
+ widened = true;
2277
+ ({ files, keywords, results } = await attempt(searchPaths));
2278
+ }
2279
+ const n = Math.max(files.length, 1);
2280
+ const byPath = /* @__PURE__ */ new Map();
2281
+ const get = (path) => {
2282
+ let c = byPath.get(path);
2283
+ if (!c) {
2284
+ c = {
2285
+ path,
2286
+ lexScore: 0,
2287
+ kwHits: {},
2288
+ pathKws: [],
2289
+ hitLines: /* @__PURE__ */ new Map(),
2290
+ isTest: isTestPath(path),
2291
+ isDoc: isDocPath(path)
2292
+ };
2293
+ byPath.set(path, c);
2294
+ }
2295
+ return c;
2296
+ };
2297
+ const kwFiles = keywords.map(() => /* @__PURE__ */ new Set());
2298
+ const maxStored = cfg.recall.maxMatchesPerFile;
2299
+ results.forEach((matches, ki) => {
2300
+ const kw = keywords[ki];
2301
+ const seen = kwFiles[ki];
2302
+ for (const m of matches) {
2303
+ seen.add(m.path);
2304
+ const c = get(m.path);
2305
+ const cnt = c.kwHits[ki] = (c.kwHits[ki] ?? 0) + 1;
2306
+ if (cnt > maxStored) continue;
2307
+ let h = c.hitLines.get(m.line);
2308
+ if (!h) {
2309
+ h = { line: m.line, text: m.text, kws: [] };
2310
+ c.hitLines.set(m.line, h);
2311
+ }
2312
+ if (!h.kws.includes(ki)) h.kws.push(ki);
2313
+ }
2314
+ kw.df = seen.size;
2315
+ kw.hitLines = matches.length;
2316
+ });
2317
+ for (const kw of keywords) {
2318
+ const variants = kw.fragments.length ? kw.fragments.flatMap((f) => keywordVariants(f)) : kw.variants;
2319
+ for (const f of files) {
2320
+ if (pathMatches(f, variants)) {
2321
+ kw.pathDf++;
2322
+ kwFiles[kw.index].add(f);
2323
+ const c = get(f);
2324
+ if (!c.pathKws.includes(kw.index)) c.pathKws.push(kw.index);
2325
+ }
2326
+ }
2327
+ }
2328
+ for (const kw of keywords)
2329
+ kw.idf = kwFiles[kw.index].size > 0 ? idfOf(kwFiles[kw.index].size, n) : 0;
2330
+ const testsOk = questionMentionsTests(input.question);
2331
+ const historyOk = questionMentionsHistory(input.question);
2332
+ for (const c of byPath.values()) {
2333
+ let s = 0;
2334
+ let lines = 0;
2335
+ for (const [ki, cnt] of Object.entries(c.kwHits)) {
2336
+ s += keywords[Number(ki)].idf;
2337
+ lines += cnt;
2338
+ }
2339
+ for (const ki of c.pathKws) s += cfg.recall.pathBonus * keywords[ki].idf;
2340
+ s += cfg.recall.hitCountWeight * Math.log(1 + lines);
2341
+ if (c.isTest && !testsOk) s *= cfg.recall.testWeight;
2342
+ if (!historyOk && isChangelogPath(c.path)) s *= cfg.recall.changelogWeight;
2343
+ c.lexScore = s;
2344
+ }
2345
+ const scored = [...byPath.values()].filter((c) => c.lexScore > 0);
2346
+ scored.sort((a, b) => b.lexScore - a.lexScore || (a.path < b.path ? -1 : 1));
2347
+ const candidates = [];
2348
+ let binaryDropped = 0;
2349
+ let next = 0;
2350
+ while (next < scored.length && candidates.length < cfg.recall.maxCandidates) {
2351
+ const batch = scored.slice(next, next + cfg.recall.maxCandidates - candidates.length);
2352
+ next += batch.length;
2353
+ const pathOnly = batch.filter((c) => c.hitLines.size === 0);
2354
+ const binary = /* @__PURE__ */ new Set();
2355
+ const flags = await mapLimit(pathOnly, READ_CONCURRENCY, (c) => looksBinary(session, c.path));
2356
+ pathOnly.forEach((c, i) => {
2357
+ if (flags[i]) binary.add(c);
2358
+ });
2359
+ for (const c of batch) {
2360
+ if (binary.has(c)) binaryDropped++;
2361
+ else candidates.push(c);
2362
+ }
2363
+ }
2364
+ return {
2365
+ keywords,
2366
+ totalFiles: files.length,
2367
+ candidates,
2368
+ scoredFiles: scored.length,
2369
+ searchPaths,
2370
+ widened,
2371
+ missingPrefixes: prefixes.missing,
2372
+ validPrefixes: prefixes.ok,
2373
+ binaryDropped,
2374
+ ms: Math.round(performance.now() - t0)
2375
+ };
2376
+ }
2377
+ function lineWeight(h, keywords) {
2378
+ return h.kws.reduce((s, k) => s + (keywords[k]?.idf ?? 0), 0);
2379
+ }
2380
+ function bestHitLines(c, keywords, n) {
2381
+ return [...c.hitLines.values()].sort((a, b) => lineWeight(b, keywords) - lineWeight(a, keywords) || a.line - b.line).slice(0, n).sort((a, b) => a.line - b.line);
2382
+ }
2383
+
2384
+ // src/code-search/search.ts
2385
+ var CODE_SEARCH_ENGINE_VERSION = "scout-0.3.1";
2386
+ var CODE_SEARCH_DEFAULT_BUDGET_TOKENS = 12e3;
2387
+ var r22 = (x) => x === null || x === void 0 || !Number.isFinite(x) ? "?" : x.toFixed(2);
2388
+ function lexicalPassageScore(text, kwPresent, keywords, qTerms, fileNorm) {
2389
+ const total = keywords.reduce((s, k) => s + k.idf, 0) || 1;
2390
+ const mass = kwPresent.reduce((s, k) => s + keywords[k].idf, 0) / total;
2391
+ const qov = overlap(qTerms, new Set(contentTerms(text)));
2392
+ return Math.max(0, Math.min(1, 0.55 * mass + 0.3 * qov + 0.15 * fileNorm));
2393
+ }
2394
+ function selectFiles(cands, scores, ids, T1, cfg) {
2395
+ const ranked = cands.map((_, i) => i).sort(
2396
+ (a, b) => (scores.get(ids[b]) ?? 0) - (scores.get(ids[a]) ?? 0) || cands[b].lexScore - cands[a].lexScore || a - b
2397
+ );
2398
+ const selected = [];
2399
+ for (let i = 0; i < Math.min(cfg.wave1.lexicalGuard, cands.length); i++) selected.push(i);
2400
+ ranked.forEach((i, rank) => {
2401
+ if (selected.length >= cfg.wave1.maxFiles || selected.includes(i)) return;
2402
+ if ((scores.get(ids[i]) ?? 0) >= T1 || rank < cfg.wave1.minFiles) selected.push(i);
2403
+ });
2404
+ selected.sort((a, b) => ranked.indexOf(a) - ranked.indexOf(b));
2405
+ return { selected, ranked };
2406
+ }
2407
+ function capPassages(perFile, max) {
2408
+ const sorted = perFile.map((ws) => [...ws].sort((a, b) => b.score - a.score));
2409
+ const out = perFile.map(() => []);
2410
+ let taken = 0;
2411
+ for (let pass = 0; taken < max; pass++) {
2412
+ let any = false;
2413
+ for (let f = 0; f < sorted.length && taken < max; f++) {
2414
+ const w = sorted[f][pass];
2415
+ if (w) {
2416
+ out[f].push(w);
2417
+ taken++;
2418
+ any = true;
2419
+ }
2420
+ }
2421
+ if (!any) break;
2422
+ }
2423
+ return out;
2424
+ }
2425
+ function statusEvidence(included, maxChars) {
2426
+ const out = [];
2427
+ let used = 0;
2428
+ for (const p of [...included].sort((a, b) => b.rel - a.rel)) {
2429
+ if (used + p.block.length + 2 > maxChars) continue;
2430
+ out.push(p.block);
2431
+ used += p.block.length + 2;
2432
+ }
2433
+ return out.join("\n\n");
2434
+ }
2435
+ function statusLabel(s, cfg) {
2436
+ if (!s || !Number.isFinite(s.overall)) return { label: "unknown", overall: null, subs: [] };
2437
+ const { hi, lo } = cfg.status;
2438
+ const subsOk = s.subs.every((x) => x >= hi);
2439
+ const anySub = s.subs.some((x) => x >= hi);
2440
+ const label = s.overall >= hi && subsOk ? "sufficient" : s.overall >= lo || anySub ? "partial" : "insufficient";
2441
+ return { label, overall: s.overall, subs: s.subs };
2442
+ }
2443
+ function prefixNote(rec) {
2444
+ const notes = [];
2445
+ if (rec.missingPrefixes.length) {
2446
+ notes.push(
2447
+ `Note: paths not found in the workspace (ignored): ${rec.missingPrefixes.join(", ")}.`
2448
+ );
2449
+ }
2450
+ if (rec.widened) {
2451
+ notes.push(
2452
+ !rec.validPrefixes.length ? "Note: the whole workspace was searched." : "Note: nothing matched under the given paths; the whole workspace was searched."
2453
+ );
2454
+ }
2455
+ return notes.length ? notes.join(" ") : void 0;
2456
+ }
2457
+ function partialLabel(session) {
2458
+ if (!session.partial) return "";
2459
+ const why = session.timedOut ? "a ripgrep search hit its time limit" : "ripgrep output was cut at its size limit";
2460
+ return `(partial search: ${why}; some matches may be missing)`;
2461
+ }
2462
+ async function runCodeSearch(input) {
2463
+ const outer = input.signal;
2464
+ outer?.throwIfAborted();
2465
+ const controller = new AbortController();
2466
+ const onOuterAbort = () => controller.abort(outer?.reason);
2467
+ outer?.addEventListener("abort", onOuterAbort, { once: true });
2468
+ try {
2469
+ return await pipeline(input, controller.signal);
2470
+ } catch (error) {
2471
+ controller.abort(error);
2472
+ if (outer?.aborted) throw outer.reason;
2473
+ throw error;
2474
+ } finally {
2475
+ outer?.removeEventListener("abort", onOuterAbort);
2476
+ }
2477
+ }
2478
+ async function pipeline(o, signal) {
2479
+ const cfg = o.config ?? DEFAULT_CODE_SEARCH_CONFIG;
2480
+ const t0 = performance.now();
2481
+ const stageMs = {};
2482
+ const mark = (stage, since) => {
2483
+ stageMs[stage] = Math.round(performance.now() - since);
2484
+ };
2485
+ const emit = (stage, data) => {
2486
+ try {
2487
+ o.onStage?.(stage, data);
2488
+ } catch {
2489
+ }
2490
+ };
2491
+ const session = new WorkspaceSession(o.workspace, signal, cfg.recall.ripgrepTimeoutMs);
2492
+ const judge = new JevJudge({ client: o.jev, config: cfg, signal, onEvent: emit });
2493
+ const subQuestions = (o.subQuestions ?? []).map((s) => s.trim()).filter(Boolean).slice(0, 4);
2494
+ const ctx = { question: o.question.trim(), subQuestions };
2495
+ const budgetTokens = o.budgetTokens ?? CODE_SEARCH_DEFAULT_BUDGET_TOKENS;
2496
+ const thr = cfg.thresholds;
2497
+ emit("start", {
2498
+ version: CODE_SEARCH_ENGINE_VERSION,
2499
+ question: ctx.question,
2500
+ subQuestions,
2501
+ keywords: o.keywords,
2502
+ paths: o.paths ?? [],
2503
+ budgetTokens
2504
+ });
2505
+ o.jev.warmUp(cfg.jev.warmConnections);
2506
+ let ts = performance.now();
2507
+ const rec = await recall({
2508
+ session,
2509
+ question: ctx.question + " " + subQuestions.join(" "),
2510
+ keywords: o.keywords,
2511
+ pathPrefixes: o.paths ?? [],
2512
+ config: cfg
2513
+ });
2514
+ mark("recall", ts);
2515
+ const kws = rec.keywords;
2516
+ emit("recall", {
2517
+ ms: rec.ms,
2518
+ totalFiles: rec.totalFiles,
2519
+ scoredFiles: rec.scoredFiles,
2520
+ searchPaths: rec.searchPaths,
2521
+ widened: rec.widened,
2522
+ missingPrefixes: rec.missingPrefixes,
2523
+ binaryDropped: rec.binaryDropped,
2524
+ partial: session.partial,
2525
+ keywords: kws.map((k) => ({
2526
+ raw: k.raw,
2527
+ mode: k.mode,
2528
+ pattern: k.pattern,
2529
+ df: k.df,
2530
+ pathDf: k.pathDf,
2531
+ hitLines: k.hitLines,
2532
+ idf: round(k.idf),
2533
+ fragments: k.fragments
2534
+ })),
2535
+ candidates: rec.candidates.map((c) => ({
2536
+ path: c.path,
2537
+ lex: round(c.lexScore),
2538
+ kws: Object.keys(c.kwHits).map(Number),
2539
+ pathKws: c.pathKws,
2540
+ lines: c.hitLines.size,
2541
+ test: c.isTest
2542
+ }))
2543
+ });
2544
+ ts = performance.now();
2545
+ const maxLex = rec.candidates[0]?.lexScore || 1;
2546
+ const fileIds = rec.candidates.map((_, i) => `f${String(i).padStart(3, "0")}`);
2547
+ const fileItems = rec.candidates.map((c, i) => {
2548
+ const hits = bestHitLines(c, kws, cfg.wave1.hitLinesPerFile);
2549
+ const needles = Object.keys(c.kwHits).flatMap((k) => kws[Number(k)].variants);
2550
+ const lines = hits.map(
2551
+ (h) => ` ${h.line}: ${trimAround(h.text, needles, cfg.wave1.hitLineChars)}`
2552
+ );
2553
+ return {
2554
+ id: fileIds[i],
2555
+ path: c.path,
2556
+ descriptor: [c.path, ...lines].join("\n"),
2557
+ lex: c.lexScore / maxLex
2558
+ };
2559
+ });
2560
+ const fileScores = await judge.scoreFiles(fileItems, ctx);
2561
+ const { selected, ranked } = selectFiles(rec.candidates, fileScores, fileIds, thr.T1, cfg);
2562
+ mark("wave1", ts);
2563
+ emit("wave1", {
2564
+ T1: thr.T1,
2565
+ scores: rec.candidates.map((c, i) => ({
2566
+ path: c.path,
2567
+ p: round(fileScores.get(fileIds[i]) ?? Number.NaN),
2568
+ lex: round(fileItems[i].lex)
2569
+ })),
2570
+ selected: selected.map((i) => rec.candidates[i].path)
2571
+ });
2572
+ ts = performance.now();
2573
+ const fileLines = /* @__PURE__ */ new Map();
2574
+ const loadLines = async (paths) => {
2575
+ const missing = [...new Set(paths)].filter((p) => !fileLines.has(p));
2576
+ const texts = await mapLimit(
2577
+ missing,
2578
+ READ_CONCURRENCY,
2579
+ (p) => session.readText(p, cfg.recall.maxFileBytes)
2580
+ );
2581
+ missing.forEach((p, i) => {
2582
+ const text2 = texts[i];
2583
+ fileLines.set(p, text2 === null || text2 === void 0 ? [] : splitLines(text2));
2584
+ });
2585
+ };
2586
+ const readLines = (path) => fileLines.get(path) ?? [];
2587
+ await loadLines(selected.map((i) => rec.candidates[i].path));
2588
+ const kwNeedles = kws.filter((k) => k.idf > 0).map((k) => keywordRegex(k)).filter((re) => re !== null);
2589
+ const renderFor = (path, needles = kwNeedles) => ({
2590
+ maxLineChars: isDocPath(path) ? cfg.wave2.maxProseLineChars : cfg.wave2.maxLineChars,
2591
+ needles
2592
+ });
2593
+ const perFileWindows = selected.map((i) => {
2594
+ const c = rec.candidates[i];
2595
+ const lines = readLines(c.path);
2596
+ const hits = [...c.hitLines.values()];
2597
+ return buildFileWindows(lines, hits, kws, langOf(c.path), cfg, renderFor(c.path));
2598
+ });
2599
+ const capped = capPassages(perFileWindows, cfg.wave2.maxPassages);
2600
+ const qTerms = contentTerms(ctx.question);
2601
+ const subTerms = subQuestions.map((s) => contentTerms(s));
2602
+ const evidence = [];
2603
+ const passageItems = [];
2604
+ let pid = 0;
2605
+ const makePassage = (path, w, fileNorm, lead) => {
2606
+ const lines = readLines(path);
2607
+ const render = renderFor(path, lead ? [new RegExp(`\\b${escapeRegex(lead)}\\b`)] : kwNeedles);
2608
+ const text2 = renderLines(lines, w.start, w.end, render);
2609
+ const raw = lines.slice(w.start - 1, w.end).join("\n");
2610
+ const present = keywordsInRange(lines, w.start, w.end, kws);
2611
+ const rawTerms = new Set(contentTerms(raw));
2612
+ const id = `p${String(pid++).padStart(3, "0")}`;
2613
+ const lex = lexicalPassageScore(raw, present, kws, qTerms, fileNorm);
2614
+ const lexCov = subTerms.map((t) => overlap(t, rawTerms));
2615
+ const label2 = w.label && w.label.line < w.start ? `L${w.label.line}: ${w.label.text}` : void 0;
2616
+ passageItems.push({ id, path, start: w.start, end: w.end, text: text2, label: label2, lex, lexCov });
2617
+ evidence.push({
2618
+ id,
2619
+ path,
2620
+ start: w.start,
2621
+ end: w.end,
2622
+ fileLines: lines,
2623
+ hits: w.hits,
2624
+ label: w.label,
2625
+ kind: w.kind,
2626
+ rel: lex,
2627
+ cov: lexCov,
2628
+ lex,
2629
+ lead,
2630
+ render
2631
+ });
2632
+ };
2633
+ selected.forEach((ci, f) => {
2634
+ const c = rec.candidates[ci];
2635
+ for (const w of [...capped[f]].sort((a, b) => a.start - b.start))
2636
+ makePassage(c.path, w, c.lexScore / maxLex);
2637
+ });
2638
+ const wave2Scores = await judge.scorePassages(passageItems, ctx, "wave2");
2639
+ for (const e of evidence) {
2640
+ const s = wave2Scores.get(e.id);
2641
+ if (s) {
2642
+ e.rel = s.rel;
2643
+ e.cov = s.cov;
2644
+ }
2645
+ }
2646
+ mark("wave2", ts);
2647
+ emit("wave2", {
2648
+ T2: thr.T2,
2649
+ passages: evidence.map((e) => ({
2650
+ id: e.id,
2651
+ path: e.path,
2652
+ start: e.start,
2653
+ end: e.end,
2654
+ kind: e.kind,
2655
+ hits: e.hits.length,
2656
+ rel: round(e.rel),
2657
+ cov: e.cov.map(round),
2658
+ lex: round(e.lex ?? Number.NaN)
2659
+ }))
2660
+ });
2661
+ ts = performance.now();
2662
+ let leadCands = [];
2663
+ let leadScores = /* @__PURE__ */ new Map();
2664
+ const chosenNames = /* @__PURE__ */ new Set();
2665
+ if (cfg.wave3.enabled) {
2666
+ const leadsFollowed = [];
2667
+ const seeds = evidence.filter((e) => e.rel >= thr.T2).sort((a, b) => b.rel - a.rel).slice(0, cfg.wave3.seedPassages).map((e) => ({
2668
+ path: e.path,
2669
+ start: e.start,
2670
+ lines: e.fileLines.slice(e.start - 1, e.end),
2671
+ rel: e.rel
2672
+ }));
2673
+ const searched = /* @__PURE__ */ new Set();
2674
+ for (const k of kws) {
2675
+ for (const v of [...k.variants, ...k.fragments.flatMap((f) => keywordVariants(f))]) {
2676
+ searched.add(v.toLowerCase());
2677
+ searched.add(splitWords(v).join(" "));
2678
+ }
2679
+ }
2680
+ const qWords = new Set(
2681
+ [
2682
+ ...splitWords(ctx.question + " " + subQuestions.join(" ")),
2683
+ ...kws.flatMap((k) => splitWords(k.raw))
2684
+ ].filter((w) => w.length >= 3 && !STOPWORDS.has(w))
2685
+ );
2686
+ const extracted = extractLeads(
2687
+ seeds,
2688
+ searched,
2689
+ qWords,
2690
+ Math.ceil(cfg.wave3.maxLeadCandidates * 1.5)
2691
+ );
2692
+ const selectedPaths = new Set(selected.map((i) => rec.candidates[i].path));
2693
+ const allowTests = questionMentionsTests(ctx.question);
2694
+ const defSearch = await locateDefinitions(
2695
+ session,
2696
+ extracted.map((l) => l.name),
2697
+ cfg,
2698
+ excludeArgs(cfg),
2699
+ 12,
2700
+ allowTests
2701
+ );
2702
+ const seenAt = new Map(extracted.map((l) => [l.name, l.seenAt.path]));
2703
+ const defs = chooseDefinitions(
2704
+ defSearch.hits,
2705
+ selectedPaths,
2706
+ cfg.wave3.defsPerLead,
2707
+ allowTests,
2708
+ seenAt
2709
+ );
2710
+ const inEvidence = (path, line) => evidence.some((e) => e.path === path && line >= e.start && line <= e.end);
2711
+ const leadDrops = {};
2712
+ const nFiles = Math.max(rec.totalFiles, 1);
2713
+ const genericity = (name) => idfOf(defSearch.fileCounts.get(name) ?? 0, nFiles) / idfOf(0, nFiles);
2714
+ leadCands = extracted.filter((l) => {
2715
+ const ds = defs.get(l.name) ?? [];
2716
+ if (!ds.length) leadDrops[l.name] = "no definition found";
2717
+ else if (ds.every((d) => inEvidence(d.path, d.line)))
2718
+ leadDrops[l.name] = "definition already in evidence";
2719
+ return ds.length > 0 && !ds.every((d) => inEvidence(d.path, d.line));
2720
+ }).map((l) => ({ ...l, weight: Math.round(l.weight * genericity(l.name) * 1e3) / 1e3 })).sort((a, b) => b.weight - a.weight || (a.name < b.name ? -1 : 1)).slice(0, cfg.wave3.maxLeadCandidates);
2721
+ const maxW = Math.max(...leadCands.map((l) => l.weight), 1e-9);
2722
+ const leadItems = leadCands.map((l, i) => ({
2723
+ id: `l${String(i).padStart(3, "0")}`,
2724
+ name: l.name,
2725
+ seenAt: `${l.seenAt.path}:${l.seenAt.line}`,
2726
+ context: l.context,
2727
+ lex: l.weight / maxW
2728
+ }));
2729
+ leadScores = await judge.scoreLeads(leadItems, ctx);
2730
+ const chosen = leadItems.map((l) => ({ l, p: leadScores.get(l.id) ?? 0 })).filter((x) => x.p >= thr.T3).sort((a, b) => b.p - a.p || (a.l.id < b.l.id ? -1 : 1)).slice(0, cfg.wave3.maxLeadsFollowed);
2731
+ for (const x of chosen) chosenNames.add(x.l.name);
2732
+ await loadLines(chosen.flatMap((x) => (defs.get(x.l.name) ?? []).map((d) => d.path)));
2733
+ const defItemsStart = passageItems.length;
2734
+ for (const x of chosen) {
2735
+ for (const d of defs.get(x.l.name) ?? []) {
2736
+ const lines = readLines(d.path);
2737
+ if (d.line > lines.length) {
2738
+ leadsFollowed.push({
2739
+ name: x.l.name,
2740
+ score: x.p,
2741
+ def: `${d.path}:${d.line} (not in the file as read)`
2742
+ });
2743
+ continue;
2744
+ }
2745
+ const w = definitionWindow(
2746
+ lines,
2747
+ d.line,
2748
+ langOf(d.path),
2749
+ cfg,
2750
+ renderFor(d.path, [new RegExp(`\\b${escapeRegex(x.l.name)}\\b`)])
2751
+ );
2752
+ const overlapsExisting = evidence.some(
2753
+ (e) => e.path === d.path && !(w.end < e.start || w.start > e.end)
2754
+ );
2755
+ leadsFollowed.push({
2756
+ name: x.l.name,
2757
+ score: x.p,
2758
+ def: `${d.path}:${d.line}${overlapsExisting ? " (overlaps evidence)" : ""}`
2759
+ });
2760
+ if (overlapsExisting) continue;
2761
+ makePassage(d.path, w, 0, x.l.name);
2762
+ }
2763
+ }
2764
+ const defItems = passageItems.slice(defItemsStart);
2765
+ if (defItems.length) {
2766
+ const defScores = await judge.scorePassages(defItems, ctx, "lead_defs");
2767
+ for (const e of evidence) {
2768
+ const s = defScores.get(e.id);
2769
+ if (s) {
2770
+ e.rel = s.rel;
2771
+ e.cov = s.cov;
2772
+ }
2773
+ }
2774
+ }
2775
+ emit("leads", {
2776
+ T3: thr.T3,
2777
+ seeds: seeds.map((s) => `${s.path}:${s.start}`),
2778
+ candidates: leadItems.map((l) => ({
2779
+ id: l.id,
2780
+ name: l.name,
2781
+ seenAt: l.seenAt,
2782
+ lex: round(l.lex),
2783
+ p: round(leadScores.get(l.id) ?? Number.NaN)
2784
+ })),
2785
+ followed: leadsFollowed,
2786
+ defsFound: defSearch.hits.length,
2787
+ defSearchMs: defSearch.ms,
2788
+ extracted: extracted.length,
2789
+ dropped: leadDrops,
2790
+ defPassages: evidence.filter((e) => e.kind === "def").map((e) => ({
2791
+ id: e.id,
2792
+ path: e.path,
2793
+ start: e.start,
2794
+ end: e.end,
2795
+ lead: e.lead,
2796
+ rel: round(e.rel)
2797
+ }))
2798
+ });
2799
+ }
2800
+ mark("wave3", ts);
2801
+ ts = performance.now();
2802
+ const cpt = cfg.pack.charsPerToken;
2803
+ const totalChars = Math.floor(budgetTokens * cpt);
2804
+ const headerReserve = 420 + subQuestions.reduce((s, q) => s + q.length + 8, 0);
2805
+ const footerReserve = Math.min(1800, Math.floor(totalChars * 0.12));
2806
+ const body = packBody(evidence, {
2807
+ subQuestions,
2808
+ T2: thr.T2,
2809
+ bodyChars: Math.max(0, totalChars - headerReserve - footerReserve),
2810
+ cfg,
2811
+ downweightChangelogs: !questionMentionsHistory(ctx.question + " " + subQuestions.join(" ")),
2812
+ downweightTests: !questionMentionsTests(ctx.question)
2813
+ });
2814
+ const includedFiles = new Set(body.included.map((p) => p.path));
2815
+ const windowedFiles = new Set(evidence.map((e) => e.path));
2816
+ const otherFiles = ranked.filter((i) => !windowedFiles.has(rec.candidates[i].path)).map((i) => ({ path: rec.candidates[i].path, score: fileScores.get(fileIds[i]) ?? 0 })).filter((x) => x.score > 0);
2817
+ const leadIdByName = new Map(leadCands.map((l, i) => [l.name, `l${String(i).padStart(3, "0")}`]));
2818
+ const notFollowed = leadCands.filter((l) => !chosenNames.has(l.name)).map((l) => ({
2819
+ name: l.name,
2820
+ score: leadScores.get(leadIdByName.get(l.name)) ?? 0,
2821
+ seenAt: `${l.seenAt.path}:${l.seenAt.line}`
2822
+ })).sort((a, b) => b.score - a.score);
2823
+ const zero = kws.filter((k) => k.df === 0 && k.pathDf === 0);
2824
+ const fragmentOnly = kws.filter((k) => k.fragments.length > 0 && (k.df > 0 || k.pathDf > 0));
2825
+ const zeroHitKeywords = [
2826
+ ...zero.map((k) => ({ raw: k.raw, fragments: [] })),
2827
+ ...fragmentOnly.map((k) => ({ raw: k.raw, fragments: k.fragments }))
2828
+ ];
2829
+ const widenedNote = prefixNote(rec);
2830
+ const footer = renderFooter({
2831
+ excluded: body.excluded,
2832
+ otherFiles,
2833
+ leadsNotFollowed: notFollowed,
2834
+ zeroHitKeywords,
2835
+ ...widenedNote ? { widenedNote } : {},
2836
+ cfg,
2837
+ maxChars: footerReserve
2838
+ });
2839
+ mark("pack", ts);
2840
+ ts = performance.now();
2841
+ let status = { label: "unknown", overall: null, subs: [] };
2842
+ let statusCheckError;
2843
+ let statusEvidenceChars = 0;
2844
+ if (cfg.status.enabled && body.included.length) {
2845
+ const ev = statusEvidence(body.included, cfg.status.maxEvidenceChars);
2846
+ statusEvidenceChars = ev.length;
2847
+ try {
2848
+ status = statusLabel(await judge.status(ev, ctx), cfg);
2849
+ } catch (error) {
2850
+ if (signal.aborted || !(error instanceof JevUnavailableError || error instanceof JevRequestError))
2851
+ throw error;
2852
+ statusCheckError = error;
2853
+ status = { label: "unknown", overall: null, subs: [], error: error.message.slice(0, 200) };
2854
+ }
2855
+ } else if (!body.included.length) {
2856
+ status = { label: "insufficient", overall: null, subs: [] };
2857
+ }
2858
+ mark("status", ts);
2859
+ emit("pack", {
2860
+ T2: thr.T2,
2861
+ budgetTokens,
2862
+ totalChars,
2863
+ priority: body.priority,
2864
+ included: body.included.map((p) => ({
2865
+ id: p.id,
2866
+ path: p.path,
2867
+ start: p.start,
2868
+ end: p.end,
2869
+ trimmed: p.trimmed,
2870
+ rel: round(p.rel),
2871
+ cov: p.cov.map(round),
2872
+ chars: p.block.length
2873
+ })),
2874
+ excluded: body.excluded.map((e) => ({
2875
+ id: e.id,
2876
+ path: e.path,
2877
+ start: e.start,
2878
+ end: e.end,
2879
+ rel: round(e.rel)
2880
+ })),
2881
+ status,
2882
+ statusEvidenceChars
2883
+ });
2884
+ const jevTotals = Object.values(judge.stats()).reduce(
2885
+ (a, s) => ({
2886
+ requests: a.requests + s.requests,
2887
+ inputTokens: a.inputTokens + s.inputTokens,
2888
+ costUsd: a.costUsd + s.costUsd
2889
+ }),
2890
+ { requests: 0, inputTokens: 0, costUsd: 0 }
2891
+ );
2892
+ const wallMs = Math.round(performance.now() - t0);
2893
+ const label = partialLabel(session);
2894
+ const statusText = status.label === "unknown" || status.overall === null ? status.error ? "evidence rating unknown (check failed)" : "evidence rating unknown (no check)" : `evidence rating ${r22(status.overall)}` + (status.subs.length ? ` (${status.subs.map((x, j) => `s${j + 1} ${r22(x)}`).join(", ")})` : "");
2895
+ const buildText = (packTok) => {
2896
+ const head = [
2897
+ `code_search${label ? " " + label : ""}: ${statusText} | ${body.included.length} passages from ${includedFiles.size} files, ~${fmtK(packTok)} tokens | ${(wallMs / 1e3).toFixed(1)}s`
2898
+ ];
2899
+ if (subQuestions.length) head.push(subQuestions.map((s, j) => `s${j + 1}: ${s}`).join("\n"));
2900
+ head.push(
2901
+ body.included.length ? "Passages are verbatim with original line numbers (N| text), grouped by file, best first; rel = relevance, [sN] = covers sub-question N. The rating covers only these passages; it cannot see other entry points, defaults, flags or exceptions the search did not return." : "No passage passed verification. Try other keywords (exact identifiers, config keys, error strings) or read the candidates below."
2902
+ );
2903
+ return [head.join("\n"), body.body, footer].filter(Boolean).join("\n\n") + "\n";
2904
+ };
2905
+ let text = buildText(0);
2906
+ text = buildText(estTokens(text.length, cpt));
2907
+ const stats = {
2908
+ wallMs,
2909
+ stageMs,
2910
+ candidates: rec.candidates.length,
2911
+ filesSelected: selected.length,
2912
+ passagesVerified: evidence.length,
2913
+ passagesIncluded: body.included.length,
2914
+ packChars: text.length,
2915
+ packTokensEst: estTokens(text.length, cpt),
2916
+ workspaceCalls: session.calls,
2917
+ ripgrepTruncated: session.partial,
2918
+ jev: { ...jevTotals, model: judge.model() }
2919
+ };
2920
+ emit("summary", { wallMs, stageMs, stats, status, jevByStage: judge.stats() });
2921
+ const result = { version: CODE_SEARCH_ENGINE_VERSION, text, status, stats };
2922
+ if (statusCheckError) result.statusCheckError = statusCheckError;
2923
+ return result;
2924
+ }
2925
+ function round(x) {
2926
+ return Number.isFinite(x) ? Math.round(x * 1e3) / 1e3 : x;
2927
+ }
2928
+
2929
+ // src/code-search/tool.ts
2930
+ var CODE_SEARCH_TOOL_NAME = "code_search";
2931
+ var CODE_SEARCH_TOOL_DESCRIPTION = "Find where something is implemented, configured or decided in the code under the working directory, in one call instead of many separate searches and file reads. Give one precise question and 6-15 keywords: likely identifiers, file-name fragments, config keys, error strings and synonyms. Optional subQuestions split distinct parts (for 'is X required?', add one for what could skip or override X); optional paths limit the search. It ranks files and passages with a fast relevance model, follows definitions one level, and returns the best passages verbatim with file paths and line numbers, plus an evidence rating for those passages. The rating cannot see what the search missed: use the passages instead of re-reading them, then check what they do not cover (other entry points, defaults, flags, exceptions) before concluding.";
2932
+ var CODE_SEARCH_LIMITS = {
2933
+ questionMinChars: 3,
2934
+ questionMaxChars: 2e3,
2935
+ keywordsMax: 20,
2936
+ keywordMaxChars: 120,
2937
+ subQuestionsMax: 3,
2938
+ subQuestionMaxChars: 1e3,
2939
+ pathsMax: 8,
2940
+ pathMaxChars: 1e3
2941
+ };
2942
+ var L = CODE_SEARCH_LIMITS;
2943
+ var codeSearchInputSchema = {
2944
+ type: "object",
2945
+ properties: {
2946
+ question: {
2947
+ type: "string",
2948
+ minLength: L.questionMinChars,
2949
+ maxLength: L.questionMaxChars,
2950
+ description: "One precise question about the code, for example where a behavior is implemented or how a value is computed."
2951
+ },
2952
+ keywords: {
2953
+ type: "array",
2954
+ minItems: 1,
2955
+ maxItems: L.keywordsMax,
2956
+ items: { type: "string", minLength: 1, maxLength: L.keywordMaxChars },
2957
+ description: "6-15 search keywords: likely identifiers (camelCase, snake_case, UPPER_CASE), file-name fragments, config or env keys, error strings, synonyms. Case and camel/snake/kebab variants are searched automatically."
2958
+ },
2959
+ subQuestions: {
2960
+ type: "array",
2961
+ maxItems: L.subQuestionsMax,
2962
+ items: { type: "string", minLength: 1, maxLength: L.subQuestionMaxChars },
2963
+ description: "Optional: the distinct parts of a multi-part question, one per entry."
2964
+ },
2965
+ paths: {
2966
+ type: "array",
2967
+ maxItems: L.pathsMax,
2968
+ items: { type: "string", minLength: 1, maxLength: L.pathMaxChars },
2969
+ description: "Optional: workspace-relative directories or files to limit the search to. Default: the whole workspace."
2970
+ }
2971
+ },
2972
+ required: ["question", "keywords"],
2973
+ additionalProperties: false
2974
+ };
2975
+ var CodeSearchArgumentError = class extends Error {
2976
+ constructor(message) {
2977
+ super(message);
2978
+ this.name = "CodeSearchArgumentError";
2979
+ }
2980
+ };
2981
+ var KNOWN_ARGUMENTS = /* @__PURE__ */ new Set(["question", "keywords", "subQuestions", "paths"]);
2982
+ function parseCodeSearchArguments(args) {
2983
+ const unknown = Object.keys(args).filter((k) => !KNOWN_ARGUMENTS.has(k));
2984
+ if (unknown.length) {
2985
+ throw new CodeSearchArgumentError(
2986
+ `unknown argument ${unknown.map((k) => `"${k}"`).join(", ")}; allowed: question, keywords, subQuestions, paths`
2987
+ );
2988
+ }
2989
+ if (typeof args.question !== "string")
2990
+ throw new CodeSearchArgumentError("question is required and must be a string");
2991
+ const question = args.question.trim();
2992
+ if (question.length < L.questionMinChars)
2993
+ throw new CodeSearchArgumentError(`question must be at least ${L.questionMinChars} characters`);
2994
+ if (question.length > L.questionMaxChars)
2995
+ throw new CodeSearchArgumentError(`question must be at most ${L.questionMaxChars} characters`);
2996
+ const keywords = stringList(args.keywords, "keywords", true);
2997
+ if (!keywords.length)
2998
+ throw new CodeSearchArgumentError("keywords must contain at least one non-empty keyword");
2999
+ if (keywords.length > L.keywordsMax)
3000
+ throw new CodeSearchArgumentError(
3001
+ `keywords accepts at most ${L.keywordsMax} entries; keep the ${L.keywordsMax} most specific`
3002
+ );
3003
+ const longKeyword = keywords.find((k) => k.length > L.keywordMaxChars);
3004
+ if (longKeyword)
3005
+ throw new CodeSearchArgumentError(
3006
+ `each keyword must be at most ${L.keywordMaxChars} characters; use short identifiers or phrases`
3007
+ );
3008
+ const subQuestions = stringList(args.subQuestions, "subQuestions", false);
3009
+ if (subQuestions.length > L.subQuestionsMax)
3010
+ throw new CodeSearchArgumentError(`subQuestions accepts at most ${L.subQuestionsMax} entries`);
3011
+ if (subQuestions.some((s) => s.length > L.subQuestionMaxChars)) {
3012
+ throw new CodeSearchArgumentError(
3013
+ `each sub-question must be at most ${L.subQuestionMaxChars} characters`
3014
+ );
3015
+ }
3016
+ const paths = [...new Set(stringList(args.paths, "paths", false).map(normalizePath))];
3017
+ if (paths.length > L.pathsMax)
3018
+ throw new CodeSearchArgumentError(`paths accepts at most ${L.pathsMax} entries`);
3019
+ return { question, keywords, subQuestions, paths };
3020
+ }
3021
+ function stringList(value, name, required) {
3022
+ if (value === void 0 || value === null) {
3023
+ if (required)
3024
+ throw new CodeSearchArgumentError(`${name} is required and must be an array of strings`);
3025
+ return [];
3026
+ }
3027
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
3028
+ throw new CodeSearchArgumentError(`${name} must be an array of strings`);
3029
+ }
3030
+ return [...new Set(value.map((v) => v.trim()).filter(Boolean))];
3031
+ }
3032
+ function normalizePath(raw) {
3033
+ if (raw.length > L.pathMaxChars)
3034
+ throw new CodeSearchArgumentError(`each path must be at most ${L.pathMaxChars} characters`);
3035
+ if (raw.startsWith("/") || raw.startsWith("~") || /^[A-Za-z]:[\\/]/.test(raw)) {
3036
+ throw new CodeSearchArgumentError(
3037
+ `path "${raw}" is absolute; give paths relative to the working directory`
3038
+ );
3039
+ }
3040
+ let p = raw;
3041
+ while (p.startsWith("./")) p = p.slice(2);
3042
+ p = p.replace(/\/+$/, "");
3043
+ if (!p) p = ".";
3044
+ if (p.split("/").some((seg) => seg === "..")) {
3045
+ throw new CodeSearchArgumentError(
3046
+ `path "${raw}" leaves the working directory; ".." is not allowed`
3047
+ );
3048
+ }
3049
+ if (p.startsWith("-")) throw new CodeSearchArgumentError(`path "${raw}" must not start with "-"`);
3050
+ return p;
3051
+ }
3052
+ var FALLBACK = "Search with exec_command (rg, sed) instead.";
3053
+ function renderCodeSearchError(error) {
3054
+ const detail = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim().slice(0, 240);
3055
+ if (error instanceof CodeSearchArgumentError)
3056
+ return `code_search: invalid arguments: ${error.message}.`;
3057
+ if (error instanceof JevUnavailableError)
3058
+ return `code_search is unavailable right now (${detail(error)}). ${FALLBACK}`;
3059
+ if (error instanceof JevRequestError)
3060
+ return `code_search failed: the relevance model rejected the request (${detail(error)}). ${FALLBACK}`;
3061
+ if (isRipgrepMissing(error)) {
3062
+ return "code_search cannot run here: ripgrep (rg) is not installed in this workspace. Search with exec_command (grep, find, sed) instead.";
3063
+ }
3064
+ if (error instanceof CodeSearchWorkspaceError)
3065
+ return `code_search could not search this workspace (${detail(error)}). ${FALLBACK}`;
3066
+ if (isAbort(error)) return "code_search was cancelled.";
3067
+ return `code_search failed unexpectedly (${detail(error)}). ${FALLBACK}`;
3068
+ }
3069
+ function isRipgrepMissing(error) {
3070
+ if (error instanceof CodeSearchRipgrepMissingError) return true;
3071
+ if (!(error instanceof CodeSearchWorkspaceError)) return false;
3072
+ const m = error.message;
3073
+ return /ripgrep|\brg\b/i.test(m) && /not (installed|found)|missing|ENOENT|no such file/i.test(m);
3074
+ }
3075
+ function isAbort(error) {
3076
+ return typeof error === "object" && error !== null && error.name === "AbortError";
3077
+ }
3078
+ export {
3079
+ CODE_SEARCH_DEFAULT_BUDGET_TOKENS,
3080
+ CODE_SEARCH_ENGINE_VERSION,
3081
+ CODE_SEARCH_LIMITS,
3082
+ CODE_SEARCH_MAX_PATTERN_CHARS,
3083
+ CODE_SEARCH_TOOL_DESCRIPTION,
3084
+ CODE_SEARCH_TOOL_NAME,
3085
+ CodeSearchArgumentError,
3086
+ CodeSearchRipgrepMissingError,
3087
+ CodeSearchWorkspaceError,
3088
+ DEFAULT_CODE_SEARCH_CONFIG,
3089
+ JEV_CHARS_PER_TOKEN,
3090
+ JEV_DEFAULT_BASE_URL,
3091
+ JEV_DEFAULT_LIMITS,
3092
+ JEV_DEFAULT_MODEL,
3093
+ JEV_PRICE_PER_MILLION_INPUT_TOKENS_USD,
3094
+ JevCircuitBreaker,
3095
+ JevClient,
3096
+ JevError,
3097
+ JevLimiter,
3098
+ JevRequestError,
3099
+ JevUnavailableError,
3100
+ codeSearchConfig,
3101
+ codeSearchInputSchema,
3102
+ estimateJevTokens,
3103
+ jevCostUsd,
3104
+ noul,
3105
+ parseCodeSearchArguments,
3106
+ planChunks,
3107
+ renderCodeSearchError,
3108
+ runCodeSearch
3109
+ };
3110
+ //# sourceMappingURL=index.js.map