@structured-vibe-coding/core 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/defaults/prices.yaml +9 -0
  2. package/defaults/search.yaml +18 -0
  3. package/dist/{agents-RWYBLZQE.js → agents-AKSUXQHN.js} +18 -7
  4. package/dist/{change-context-W4HDO2IK.js → change-context-REE7UKU7.js} +2 -4
  5. package/dist/chunk-44LQ5BKZ.js +2628 -0
  6. package/dist/{chunk-KYWETNFU.js → chunk-4BRO7BOJ.js} +2 -4
  7. package/dist/chunk-4PIRWYRA.js +1125 -0
  8. package/dist/{chunk-7JLDL2OK.js → chunk-LHSR7JG2.js} +569 -39
  9. package/dist/{chunk-EPFURATC.js → chunk-O5SP6X5R.js} +89 -0
  10. package/dist/{chunk-74OGSVJZ.js → chunk-QGP43XCN.js} +2 -2
  11. package/dist/{chunk-A752UVPF.js → chunk-QHV5KZKX.js} +193 -28
  12. package/dist/{gather-BZDW4EN4.js → gather-ZNV2NAUN.js} +3 -4
  13. package/dist/{greenfield-Z425JOMP.js → greenfield-I5S5OS2D.js} +5 -6
  14. package/dist/index.d.ts +1034 -40
  15. package/dist/index.js +2927 -988
  16. package/dist/{sources-PF45EUH2.js → sources-3TXUPU6V.js} +2 -3
  17. package/package.json +11 -3
  18. package/vendor/tree-sitter/NOTICE +29 -0
  19. package/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  20. package/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  21. package/vendor/tree-sitter/tree-sitter-rust.wasm +0 -0
  22. package/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  23. package/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  24. package/dist/chunk-67AUAGNV.js +0 -150
  25. package/dist/chunk-7MJYPJMR.js +0 -51
  26. package/dist/chunk-P3N67UEZ.js +0 -538
  27. package/dist/chunk-R5WQN4HA.js +0 -139
  28. package/dist/chunk-WHDG6OTG.js +0 -46
  29. package/dist/config-X5VDBVR4.js +0 -16
  30. package/dist/fs-J6DIV6CX.js +0 -28
  31. package/dist/paths-64GA32IZ.js +0 -28
@@ -0,0 +1,2628 @@
1
+ import {
2
+ ConfigSchema,
3
+ getStage,
4
+ isStageSkipped,
5
+ shouldAutoSkip
6
+ } from "./chunk-QHV5KZKX.js";
7
+ import {
8
+ SddError
9
+ } from "./chunk-MHODNFIL.js";
10
+ import {
11
+ activePointerPath,
12
+ changesPathRel,
13
+ configPath,
14
+ displayChangePath,
15
+ ensureDir,
16
+ guidancePaths,
17
+ indexDbPath,
18
+ memoryPathRel,
19
+ pathExists,
20
+ readYaml,
21
+ sddRoot,
22
+ searchConfigPath,
23
+ writeYaml
24
+ } from "./chunk-O5SP6X5R.js";
25
+
26
+ // src/config.ts
27
+ async function isInitialized(projectRoot) {
28
+ return pathExists(configPath(projectRoot));
29
+ }
30
+ async function loadConfig(projectRoot) {
31
+ const path = configPath(projectRoot);
32
+ if (!await pathExists(path)) {
33
+ throw new SddError(`SDD is not initialized in ${projectRoot}. Run \`sdd init\` first.`, {
34
+ code: "NOT_INITIALIZED",
35
+ hint: "cd into your app and run: sdd init --here --ai copilot"
36
+ });
37
+ }
38
+ const raw = await readYaml(path);
39
+ return ConfigSchema.parse(raw);
40
+ }
41
+ async function saveConfig(projectRoot, config) {
42
+ await ensureDir(sddRoot(projectRoot));
43
+ await writeYaml(configPath(projectRoot), ConfigSchema.parse(config));
44
+ }
45
+ function defaultConfig() {
46
+ return ConfigSchema.parse({});
47
+ }
48
+ function isRecord(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+ function mergeDefaults(defaults, overrides) {
52
+ if (!isRecord(defaults) || !isRecord(overrides)) return overrides ?? defaults;
53
+ const merged = /* @__PURE__ */ Object.create(null);
54
+ for (const [key, value] of Object.entries(defaults)) {
55
+ merged[key] = Object.hasOwn(overrides, key) ? mergeDefaults(value, overrides[key]) : value;
56
+ }
57
+ for (const [key, value] of Object.entries(overrides)) {
58
+ if (!Object.hasOwn(defaults, key)) merged[key] = value;
59
+ }
60
+ return merged;
61
+ }
62
+ function mergeConfigDefaults(existing) {
63
+ return ConfigSchema.parse(mergeDefaults(defaultConfig(), existing));
64
+ }
65
+
66
+ // src/git-head.ts
67
+ import { spawn } from "child_process";
68
+ import { join } from "pathe";
69
+ function runGitHead(projectRoot) {
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn("git", ["rev-parse", "--verify", "HEAD"], {
72
+ cwd: projectRoot,
73
+ stdio: ["ignore", "pipe", "pipe"]
74
+ });
75
+ let stdout = "";
76
+ let stderr = "";
77
+ child.stdout?.on("data", (chunk) => {
78
+ stdout += chunk.toString();
79
+ });
80
+ child.stderr?.on("data", (chunk) => {
81
+ stderr += chunk.toString();
82
+ });
83
+ child.on("error", reject);
84
+ child.on("close", (code) => {
85
+ if (code === 0) resolve(stdout.trim());
86
+ else reject(new Error(stderr.trim() || `git rev-parse exited with code ${code}`));
87
+ });
88
+ });
89
+ }
90
+ async function getHeadSha(projectRoot, runGit = runGitHead) {
91
+ if (!await pathExists(join(projectRoot, ".git"))) return null;
92
+ let sha;
93
+ try {
94
+ sha = await runGit(projectRoot);
95
+ } catch {
96
+ return null;
97
+ }
98
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(sha)) {
99
+ throw new Error("git rev-parse returned an invalid HEAD SHA.");
100
+ }
101
+ return sha.toLowerCase();
102
+ }
103
+
104
+ // src/search/tokenize.ts
105
+ import { encode } from "gpt-tokenizer/encoding/cl100k_base";
106
+ function countTokens(text) {
107
+ return encode(text).length;
108
+ }
109
+ function ftsTokens(query) {
110
+ const tokens = [];
111
+ let current = "";
112
+ for (const ch of query) {
113
+ const code = ch.codePointAt(0) ?? 0;
114
+ const ok = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code > 127;
115
+ if (ok) {
116
+ current += ch;
117
+ } else if (current) {
118
+ tokens.push(current);
119
+ current = "";
120
+ }
121
+ }
122
+ if (current) tokens.push(current);
123
+ return tokens;
124
+ }
125
+
126
+ // src/search/config.ts
127
+ import { z } from "zod";
128
+ var SearchConfigError = class extends Error {
129
+ constructor() {
130
+ super("search.yaml is invalid");
131
+ this.name = "SearchConfigError";
132
+ }
133
+ };
134
+ var BudgetSchema = z.object({
135
+ default: z.number().int().min(1).default(12e3)
136
+ }).catchall(z.number().int().min(1));
137
+ var SearchConfigSchema = z.object({
138
+ version: z.literal(1).default(1),
139
+ provider: z.string().min(1).default("builtin"),
140
+ k: z.number().int().min(1).max(100).default(20),
141
+ embeddings: z.boolean().default(false),
142
+ budgets: BudgetSchema.default({ default: 12e3 }),
143
+ exclude: z.array(z.string().min(1)).default([])
144
+ });
145
+ function budgetForStage(config, stage) {
146
+ if (!stage) return config.budgets.default;
147
+ const value = config.budgets[stage];
148
+ return typeof value === "number" ? value : config.budgets.default;
149
+ }
150
+ async function loadSearchConfig(projectRoot) {
151
+ const file = searchConfigPath(projectRoot);
152
+ if (!await pathExists(file)) {
153
+ return SearchConfigSchema.parse({});
154
+ }
155
+ let raw;
156
+ try {
157
+ raw = await readYaml(file);
158
+ } catch {
159
+ throw new SearchConfigError();
160
+ }
161
+ const parsed = SearchConfigSchema.safeParse(raw ?? {});
162
+ if (!parsed.success) throw new SearchConfigError();
163
+ return parsed.data;
164
+ }
165
+
166
+ // src/search/pack.ts
167
+ var EN_DASH = "\u2013";
168
+ function searchPreamble(query, scope, budget, hybrid = false) {
169
+ if (hybrid) {
170
+ return `# Search
171
+ Query: ${query}
172
+ Scope: ${scope}
173
+ Provider: builtin
174
+ Mode: hybrid
175
+ Budget: ${budget}
176
+
177
+ `;
178
+ }
179
+ return `# Search
180
+ Query: ${query}
181
+ Scope: ${scope}
182
+ Provider: builtin
183
+ Budget: ${budget}
184
+
185
+ `;
186
+ }
187
+ function renderBlock(hit, form) {
188
+ const fence = hit.language ? `\`\`\`${hit.language}` : "```";
189
+ const body = form === "full" ? hit.body : form === "summary" ? hit.summary ?? "" : hit.signature;
190
+ return `### \`${hit.path}\` \xB7 \`${hit.symbol}\` (lines ${hit.startLine}${EN_DASH}${hit.endLine}) \xB7 ${form}
191
+ ${fence}
192
+ ${body}
193
+ \`\`\`
194
+ `;
195
+ }
196
+ function contains(outer, inner) {
197
+ return outer.path === inner.path && inner.startLine >= outer.startLine && inner.endLine <= outer.endLine;
198
+ }
199
+ function packHits(input) {
200
+ const { preamble, hits, budget } = input;
201
+ if (countTokens(preamble) > budget) {
202
+ return { text: "", usedTokens: 0, hits: [] };
203
+ }
204
+ let text = preamble;
205
+ const packed = [];
206
+ const full = [];
207
+ const seen = /* @__PURE__ */ new Set();
208
+ for (const hit of hits) {
209
+ const id = `${hit.path}\0${hit.symbol}\0${hit.startLine}`;
210
+ if (seen.has(id)) continue;
211
+ seen.add(id);
212
+ if (full.some((outer) => contains(outer, hit))) continue;
213
+ const summary = hit.summary;
214
+ if (summary && summary !== hit.body) {
215
+ const summaryBlock = renderBlock(hit, "summary");
216
+ const summaryText = text + summaryBlock;
217
+ if (countTokens(summaryText) <= budget) {
218
+ text = summaryText;
219
+ packed.push({ ...hit, form: "summary", text: summaryBlock });
220
+ continue;
221
+ }
222
+ }
223
+ const fullBlock = renderBlock(hit, "full");
224
+ const fullText = text + fullBlock;
225
+ if (countTokens(fullText) <= budget) {
226
+ text = fullText;
227
+ full.push(hit);
228
+ packed.push({ ...hit, form: "full", text: fullBlock });
229
+ continue;
230
+ }
231
+ const signatureBlock = renderBlock(hit, "signature");
232
+ const signatureText = text + signatureBlock;
233
+ if (countTokens(signatureText) <= budget) {
234
+ text = signatureText;
235
+ packed.push({ ...hit, form: "signature", text: signatureBlock });
236
+ }
237
+ }
238
+ if (!text) return { text: "", usedTokens: 0, hits: packed };
239
+ return { text, usedTokens: countTokens(text), hits: packed };
240
+ }
241
+
242
+ // src/code-context/secrets.ts
243
+ import { basename, normalize } from "pathe";
244
+ var SECRET_BASENAME_PATTERNS = [
245
+ /^\.env$/i,
246
+ /^\.env\..+/i,
247
+ /\.pem$/i,
248
+ /\.key$/i,
249
+ /id_rsa/i,
250
+ /credentials/i,
251
+ /secret/i
252
+ ];
253
+ function isSecretPath(filePath) {
254
+ const norm = normalize(filePath).replace(/\\/g, "/");
255
+ const parts = norm.split("/").filter(Boolean);
256
+ if (parts.includes(".git")) return true;
257
+ if (norm.includes("/.git/") || norm.startsWith(".git/") || norm === ".git") {
258
+ return true;
259
+ }
260
+ const base = basename(norm);
261
+ return SECRET_BASENAME_PATTERNS.some((re) => re.test(base));
262
+ }
263
+
264
+ // src/code-context/ignore.ts
265
+ import { basename as basename2, extname, normalize as normalize2 } from "pathe";
266
+ var IGNORED_DIR_SEGMENTS = /* @__PURE__ */ new Set([
267
+ "node_modules",
268
+ "dist",
269
+ "coverage",
270
+ ".turbo",
271
+ ".next",
272
+ "build",
273
+ ".cache"
274
+ ]);
275
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
276
+ ".png",
277
+ ".jpg",
278
+ ".jpeg",
279
+ ".gif",
280
+ ".webp",
281
+ ".ico",
282
+ ".svg",
283
+ ".woff",
284
+ ".woff2",
285
+ ".ttf",
286
+ ".eot",
287
+ ".pdf",
288
+ ".zip",
289
+ ".tar",
290
+ ".gz",
291
+ ".tgz",
292
+ ".7z",
293
+ ".rar",
294
+ ".exe",
295
+ ".dll",
296
+ ".so",
297
+ ".dylib",
298
+ ".bin",
299
+ ".wasm",
300
+ ".mp3",
301
+ ".mp4",
302
+ ".mov",
303
+ ".lock"
304
+ ]);
305
+ function isIgnoredPath(filePath) {
306
+ const norm = normalize2(filePath).replace(/\\/g, "/");
307
+ const parts = norm.split("/").filter(Boolean);
308
+ for (const p of parts) {
309
+ if (IGNORED_DIR_SEGMENTS.has(p)) return true;
310
+ }
311
+ const ext = extname(basename2(norm)).toLowerCase();
312
+ if (BINARY_EXTENSIONS.has(ext)) return true;
313
+ const base = basename2(norm);
314
+ if (base === "package-lock.json" || base === "pnpm-lock.yaml" || base === "yarn.lock" || base === "bun.lockb") {
315
+ return true;
316
+ }
317
+ return false;
318
+ }
319
+ function classifyPath(filePath) {
320
+ if (isSecretPath(filePath)) return "secret";
321
+ if (isIgnoredPath(filePath)) return "ignored";
322
+ return "allowed";
323
+ }
324
+
325
+ // src/search/exclude.ts
326
+ import { readFile, readdir } from "fs/promises";
327
+ import ignore from "ignore";
328
+ import { join as join2 } from "pathe";
329
+ function isHardDenied(rel) {
330
+ if (isSecretPath(rel) || isIgnoredPath(rel)) return true;
331
+ const slash = rel.indexOf("/");
332
+ const first = slash === -1 ? rel : rel.slice(0, slash);
333
+ return first === ".sdd" || first === ".git";
334
+ }
335
+ function isPrunedDir(rel) {
336
+ if (isIgnoredPath(rel)) return true;
337
+ const parts = rel.split("/");
338
+ for (const part of parts) {
339
+ if (part === ".git" || part === ".sdd") return true;
340
+ }
341
+ return false;
342
+ }
343
+ function rewriteNestedLine(dir, line) {
344
+ if (line.length === 0) return [line];
345
+ let i = 0;
346
+ while (i < line.length && (line.charCodeAt(i) === 32 || line.charCodeAt(i) === 9)) i += 1;
347
+ if (i >= line.length) return [line];
348
+ if (line.charCodeAt(i) === 35) return [line];
349
+ let negated = false;
350
+ if (line.charCodeAt(i) === 33) {
351
+ negated = true;
352
+ i += 1;
353
+ }
354
+ let pattern = line.slice(i);
355
+ let end = pattern.length;
356
+ while (end > 0 && (pattern.charCodeAt(end - 1) === 32 || pattern.charCodeAt(end - 1) === 9)) {
357
+ end -= 1;
358
+ }
359
+ pattern = pattern.slice(0, end);
360
+ if (!pattern) return [line];
361
+ const bang = negated ? "!" : "";
362
+ if (!pattern.includes("/")) {
363
+ return [`${bang}${dir}/${pattern}`, `${bang}${dir}/**/${pattern}`];
364
+ }
365
+ const body = pattern.startsWith("/") ? pattern.slice(1) : pattern;
366
+ return [`${bang}${dir}/${body}`];
367
+ }
368
+ function rewriteNested(dir, content) {
369
+ const lines = content.split("\n");
370
+ const out = [];
371
+ for (const line of lines) {
372
+ const logical = line.endsWith("\r") ? line.slice(0, -1) : line;
373
+ out.push(...rewriteNestedLine(dir, logical));
374
+ }
375
+ return out.join("\n");
376
+ }
377
+ async function readIfPresent(file) {
378
+ if (!await pathExists(file)) return "";
379
+ return readFile(file, "utf8");
380
+ }
381
+ async function nestedGitignoreFiles(projectRoot) {
382
+ const found = [];
383
+ async function walk(dir, rel) {
384
+ let entries;
385
+ try {
386
+ entries = await readdir(dir, { withFileTypes: true });
387
+ } catch {
388
+ return;
389
+ }
390
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
391
+ for (const entry of entries) {
392
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
393
+ if (isPrunedDir(child)) continue;
394
+ if (entry.isDirectory()) {
395
+ await walk(join2(dir, entry.name), child);
396
+ } else if (entry.isFile() && entry.name === ".gitignore" && rel) {
397
+ found.push(child);
398
+ }
399
+ }
400
+ }
401
+ await walk(projectRoot, "");
402
+ found.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
403
+ return found;
404
+ }
405
+ async function loadExcluder(projectRoot, exclude) {
406
+ const git = ignore();
407
+ const rootIgnore = await readIfPresent(join2(projectRoot, ".gitignore"));
408
+ if (rootIgnore) git.add(rootIgnore);
409
+ for (const rel of await nestedGitignoreFiles(projectRoot)) {
410
+ if (git.ignores(rel)) continue;
411
+ const dir = rel.slice(0, rel.lastIndexOf("/"));
412
+ const body = await readIfPresent(join2(projectRoot, rel));
413
+ if (body) git.add(rewriteNested(dir, body));
414
+ }
415
+ const sdd = ignore();
416
+ const sddIgnore = await readIfPresent(join2(projectRoot, ".sddignore"));
417
+ if (sddIgnore) sdd.add(sddIgnore);
418
+ const extra2 = ignore();
419
+ if (exclude.length) extra2.add(exclude.join("\n"));
420
+ return (rel) => {
421
+ const path = rel.replaceAll("\\", "/");
422
+ if (isHardDenied(path)) return true;
423
+ if (git.ignores(path)) return true;
424
+ if (sdd.ignores(path)) return true;
425
+ return extra2.ignores(path);
426
+ };
427
+ }
428
+
429
+ // src/search/chunk-markdown.ts
430
+ function toLines(text) {
431
+ let normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
432
+ if (normalized.endsWith("\n")) normalized = normalized.slice(0, -1);
433
+ if (!normalized) return [];
434
+ return normalized.split("\n");
435
+ }
436
+ function leadingSpaces(line) {
437
+ let i = 0;
438
+ while (i < line.length && line.charCodeAt(i) === 32) i += 1;
439
+ return i;
440
+ }
441
+ function fenceOpen(line) {
442
+ const spaces = leadingSpaces(line);
443
+ if (spaces > 3) return null;
444
+ let i = spaces;
445
+ if (i >= line.length) return null;
446
+ const ch = line[i] ?? "";
447
+ if (ch !== "`" && ch !== "~") return null;
448
+ let len = 0;
449
+ while (i < line.length && line[i] === ch) {
450
+ len += 1;
451
+ i += 1;
452
+ }
453
+ if (len < 3) return null;
454
+ return { ch, len };
455
+ }
456
+ function fenceClose(line, open) {
457
+ const spaces = leadingSpaces(line);
458
+ if (spaces > 3) return false;
459
+ let i = spaces;
460
+ let len = 0;
461
+ while (i < line.length && line[i] === open.ch) {
462
+ len += 1;
463
+ i += 1;
464
+ }
465
+ if (len < open.len) return false;
466
+ while (i < line.length) {
467
+ if (line.charCodeAt(i) !== 32) return false;
468
+ i += 1;
469
+ }
470
+ return true;
471
+ }
472
+ function headingSymbol(line) {
473
+ let i = 0;
474
+ while (i < line.length && (line.charCodeAt(i) === 32 || line.charCodeAt(i) === 9)) i += 1;
475
+ let hashes = 0;
476
+ while (i < line.length && line[i] === "#" && hashes < 7) {
477
+ hashes += 1;
478
+ i += 1;
479
+ }
480
+ if (hashes < 1 || hashes > 6) return null;
481
+ if (line[i] !== " ") return null;
482
+ i += 1;
483
+ let text = line.slice(i);
484
+ let end = text.length;
485
+ while (end > 0 && text.charCodeAt(end - 1) === 32) end -= 1;
486
+ text = text.slice(0, end);
487
+ return text;
488
+ }
489
+ function hasNonWhitespace(lines) {
490
+ for (const line of lines) {
491
+ for (const ch of line) {
492
+ const code = ch.codePointAt(0) ?? 0;
493
+ if (code !== 32 && code !== 9 && code !== 10 && code !== 13) return true;
494
+ }
495
+ }
496
+ return false;
497
+ }
498
+ function chunkMarkdown(text) {
499
+ const lines = toLines(text);
500
+ if (!lines.length) return [];
501
+ let open = null;
502
+ const headings = [];
503
+ for (let n = 0; n < lines.length; n += 1) {
504
+ const line = lines[n] ?? "";
505
+ if (open) {
506
+ if (fenceClose(line, open)) open = null;
507
+ continue;
508
+ }
509
+ const fence = fenceOpen(line);
510
+ if (fence) {
511
+ open = fence;
512
+ continue;
513
+ }
514
+ const symbol = headingSymbol(line);
515
+ if (symbol != null) headings.push({ line: n + 1, symbol, raw: line });
516
+ }
517
+ const chunks = [];
518
+ const first = headings[0]?.line ?? lines.length + 1;
519
+ if (first > 1) {
520
+ const pre = lines.slice(0, first - 1);
521
+ if (hasNonWhitespace(pre)) {
522
+ chunks.push({
523
+ symbol: "(preamble)",
524
+ startLine: 1,
525
+ endLine: first - 1,
526
+ signature: "(preamble)",
527
+ body: pre.join("\n")
528
+ });
529
+ }
530
+ }
531
+ for (let i = 0; i < headings.length; i += 1) {
532
+ const heading = headings[i];
533
+ const next = headings[i + 1]?.line ?? lines.length + 1;
534
+ const start2 = heading.line;
535
+ const end = next - 1;
536
+ chunks.push({
537
+ symbol: heading.symbol,
538
+ startLine: start2,
539
+ endLine: end,
540
+ signature: heading.raw,
541
+ body: lines.slice(start2 - 1, end).join("\n")
542
+ });
543
+ }
544
+ return chunks;
545
+ }
546
+
547
+ // src/search/summarize.ts
548
+ import { decode, encode as encode2 } from "gpt-tokenizer/encoding/cl100k_base";
549
+ var SUMMARY_TOKEN_CAP = 80;
550
+ function clipToTokens(text, cap) {
551
+ const ids = encode2(text);
552
+ let n = Math.min(cap, ids.length);
553
+ let clipped = n === 0 ? "" : decode(ids.slice(0, n));
554
+ if (clipped.endsWith("\n")) clipped = clipped.slice(0, -1);
555
+ while (n > 0 && countTokens(clipped) > cap) {
556
+ n -= 1;
557
+ clipped = n === 0 ? "" : decode(ids.slice(0, n));
558
+ if (clipped.endsWith("\n")) clipped = clipped.slice(0, -1);
559
+ }
560
+ return clipped;
561
+ }
562
+ function extractiveSummary(signature, body) {
563
+ if (countTokens(body) <= SUMMARY_TOKEN_CAP) return body;
564
+ const keepHeading = signature.length > 0 && signature !== "(preamble)" && body.startsWith(signature) && (body.length === signature.length || body.charCodeAt(signature.length) === 10) && countTokens(signature) <= SUMMARY_TOKEN_CAP;
565
+ if (!keepHeading) return clipToTokens(body, SUMMARY_TOKEN_CAP);
566
+ const rest = body.slice(signature.length);
567
+ const restIds = encode2(rest);
568
+ let lo = 0;
569
+ let hi = restIds.length;
570
+ let best = signature;
571
+ while (lo <= hi) {
572
+ const mid = Math.floor((lo + hi) / 2);
573
+ const tail = mid === 0 ? "" : decode(restIds.slice(0, mid));
574
+ const candidate = signature + tail;
575
+ if (countTokens(candidate) <= SUMMARY_TOKEN_CAP) {
576
+ best = candidate;
577
+ lo = mid + 1;
578
+ } else {
579
+ hi = mid - 1;
580
+ }
581
+ }
582
+ if (best.endsWith("\n")) {
583
+ const stripped = best.slice(0, -1);
584
+ if (countTokens(stripped) <= SUMMARY_TOKEN_CAP && stripped.startsWith(signature)) {
585
+ best = stripped;
586
+ }
587
+ }
588
+ return best;
589
+ }
590
+
591
+ // src/search/embed.ts
592
+ import { rm, stat } from "fs/promises";
593
+ import { join as join3 } from "pathe";
594
+ var EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5";
595
+ var EMBEDDING_DIM = 384;
596
+ var EMBEDDINGS_UNAVAILABLE = "Embeddings are unavailable. Using keyword search.";
597
+ var MODEL_DIR = "Qdrant_bge-small-en-v1.5-onnx-Q";
598
+ var CACHE_FILES = [
599
+ "model_optimized.onnx",
600
+ "tokenizer.json",
601
+ "tokenizer_config.json",
602
+ "config.json",
603
+ "special_tokens_map.json"
604
+ ];
605
+ var fastembedImportCount = 0;
606
+ var ready = /* @__PURE__ */ new Map();
607
+ var projectSessions = /* @__PURE__ */ new Map();
608
+ var runtimeSessions = /* @__PURE__ */ new WeakMap();
609
+ async function cacheComplete(dir) {
610
+ for (const name of CACHE_FILES) {
611
+ try {
612
+ const info = await stat(join3(dir, name));
613
+ if (!info.isFile() || info.size <= 0) return false;
614
+ } catch {
615
+ return false;
616
+ }
617
+ }
618
+ return true;
619
+ }
620
+ function asFloat32(value) {
621
+ if (value instanceof Float32Array) return value;
622
+ return Float32Array.from(value);
623
+ }
624
+ function wrapModel(model) {
625
+ return {
626
+ async embedPassages(texts) {
627
+ const out = [];
628
+ for await (const batch of model.passageEmbed(texts, 16)) {
629
+ for (const vector of batch) out.push(asFloat32(vector));
630
+ }
631
+ if (out.length !== texts.length) throw new Error("Embedding batch length does not match.");
632
+ for (const vector of out) {
633
+ if (vector.length !== EMBEDDING_DIM) throw new Error("Embedding length is not 384.");
634
+ }
635
+ return out;
636
+ },
637
+ async embedQuery(text) {
638
+ const vector = asFloat32(await model.queryEmbed(text));
639
+ if (vector.length !== EMBEDDING_DIM) throw new Error("Embedding length is not 384.");
640
+ return vector;
641
+ }
642
+ };
643
+ }
644
+ async function importFastembed() {
645
+ fastembedImportCount += 1;
646
+ return import("fastembed");
647
+ }
648
+ async function initReal(cacheDir) {
649
+ const mod = await importFastembed();
650
+ return mod.FlagEmbedding.init({
651
+ model: mod.EmbeddingModel.BGESmallENV15,
652
+ executionProviders: [mod.ExecutionProvider.CPU],
653
+ maxLength: 512,
654
+ showDownloadProgress: false,
655
+ cacheDir
656
+ });
657
+ }
658
+ async function start(projectRoot, hooks) {
659
+ const cacheDir = join3(sddRoot(projectRoot), "models", "fastembed");
660
+ const modelDir = join3(cacheDir, MODEL_DIR);
661
+ const completeBefore = await cacheComplete(modelDir);
662
+ try {
663
+ const model = hooks?.init ? await hooks.init({
664
+ model: "BGESmallENV15",
665
+ executionProviders: ["cpu"],
666
+ maxLength: 512,
667
+ showDownloadProgress: false,
668
+ cacheDir
669
+ }) : await initReal(cacheDir);
670
+ return wrapModel(model);
671
+ } catch (err) {
672
+ if (!completeBefore) await rm(modelDir, { recursive: true, force: true });
673
+ throw err;
674
+ }
675
+ }
676
+ function createFastembedEmbedder(projectRoot, hooks) {
677
+ if (hooks?.init) return start(projectRoot, hooks);
678
+ const cacheDir = join3(sddRoot(projectRoot), "models", "fastembed");
679
+ const existing = ready.get(cacheDir);
680
+ if (existing) return existing;
681
+ const pending = start(projectRoot).catch((err) => {
682
+ ready.delete(cacheDir);
683
+ throw err;
684
+ });
685
+ ready.set(cacheDir, pending);
686
+ return pending;
687
+ }
688
+ function resolveEmbedder(projectRoot, embeddings, runtime) {
689
+ if (!embeddings) return Promise.resolve(null);
690
+ if (runtime?.embedder) return Promise.resolve(runtime.embedder);
691
+ if (runtime?.embedderFactory) {
692
+ const existing2 = runtimeSessions.get(runtime);
693
+ if (existing2) return existing2;
694
+ const pending2 = runtime.embedderFactory().then((embedder) => embedder).catch(() => null);
695
+ runtimeSessions.set(runtime, pending2);
696
+ return pending2;
697
+ }
698
+ const existing = projectSessions.get(projectRoot);
699
+ if (existing) return existing;
700
+ const pending = createFastembedEmbedder(projectRoot).then(
701
+ (embedder) => embedder,
702
+ () => null
703
+ );
704
+ projectSessions.set(projectRoot, pending);
705
+ return pending;
706
+ }
707
+ async function embedSearchQuery(embedder, embeddings, query) {
708
+ if (!embeddings) return { queryVec: null, failure: "none" };
709
+ if (!embedder) return { queryVec: null, failure: "load" };
710
+ try {
711
+ const queryVec = await embedder.embedQuery(query);
712
+ if (queryVec.length !== EMBEDDING_DIM) return { queryVec: null, failure: "query" };
713
+ return { queryVec, failure: "none" };
714
+ } catch {
715
+ return { queryVec: null, failure: "query" };
716
+ }
717
+ }
718
+
719
+ // src/search/result-cache.ts
720
+ import { createHash } from "crypto";
721
+ import { readFile as readFile2 } from "fs/promises";
722
+ import Database from "better-sqlite3";
723
+ import { isAbsolute } from "pathe";
724
+
725
+ // src/search/schema.ts
726
+ var SEARCH_SCHEMA_SQL = `
727
+ PRAGMA journal_mode = DELETE;
728
+ PRAGMA foreign_keys = ON;
729
+
730
+ CREATE TABLE IF NOT EXISTS files (
731
+ path TEXT PRIMARY KEY,
732
+ content_hash TEXT NOT NULL,
733
+ scope TEXT NOT NULL CHECK (scope IN ('code', 'specs', 'memory')),
734
+ language TEXT NOT NULL
735
+ );
736
+
737
+ CREATE TABLE IF NOT EXISTS chunks (
738
+ id INTEGER PRIMARY KEY,
739
+ path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
740
+ scope TEXT NOT NULL,
741
+ symbol TEXT NOT NULL,
742
+ start_line INTEGER NOT NULL,
743
+ end_line INTEGER NOT NULL,
744
+ signature TEXT NOT NULL,
745
+ body TEXT NOT NULL,
746
+ chunk_hash TEXT NOT NULL
747
+ );
748
+
749
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
750
+ symbol,
751
+ body,
752
+ tokenize = 'unicode61'
753
+ );
754
+
755
+ CREATE TABLE IF NOT EXISTS chunks_fts_map (
756
+ chunk_id INTEGER PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
757
+ fts_rowid INTEGER NOT NULL
758
+ );
759
+
760
+ CREATE TABLE IF NOT EXISTS meta (
761
+ key TEXT PRIMARY KEY,
762
+ value TEXT NOT NULL
763
+ );
764
+
765
+ CREATE TABLE IF NOT EXISTS vec_meta (
766
+ chunk_id INTEGER PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
767
+ chunk_hash TEXT NOT NULL,
768
+ scope TEXT NOT NULL CHECK (scope IN ('code', 'specs', 'memory'))
769
+ );
770
+
771
+ CREATE TABLE IF NOT EXISTS spec_summaries (
772
+ path TEXT NOT NULL,
773
+ content_hash TEXT NOT NULL,
774
+ symbol TEXT NOT NULL,
775
+ start_line INTEGER NOT NULL,
776
+ summary TEXT NOT NULL,
777
+ PRIMARY KEY (path, content_hash, symbol, start_line)
778
+ );
779
+
780
+ CREATE TABLE IF NOT EXISTS result_cache (
781
+ cache_key TEXT PRIMARY KEY,
782
+ query TEXT NOT NULL,
783
+ channel TEXT NOT NULL,
784
+ scope TEXT NOT NULL,
785
+ k INTEGER NOT NULL,
786
+ budget INTEGER NOT NULL,
787
+ embeddings INTEGER NOT NULL,
788
+ active_change TEXT NOT NULL,
789
+ filter TEXT NOT NULL,
790
+ index_version TEXT NOT NULL,
791
+ mode TEXT NOT NULL,
792
+ hybrid_scopes TEXT NOT NULL,
793
+ payload TEXT NOT NULL
794
+ );
795
+
796
+ CREATE TABLE IF NOT EXISTS result_cache_files (
797
+ cache_key TEXT NOT NULL,
798
+ path TEXT NOT NULL,
799
+ content_hash TEXT NOT NULL,
800
+ PRIMARY KEY (cache_key, path)
801
+ );
802
+ `;
803
+ var ADD_CHUNK_HASH_SQL = "ALTER TABLE chunks ADD COLUMN chunk_hash TEXT NOT NULL DEFAULT ''";
804
+
805
+ // src/search/result-cache.ts
806
+ var PRIVATE_MARKERS = ["FAKE_BIRTH_RECORD_S1", "fake-secret-s1-do-not-index"];
807
+ function containsPrivate(text) {
808
+ return PRIVATE_MARKERS.some((marker) => text.includes(marker));
809
+ }
810
+ function field(value) {
811
+ return `${Buffer.byteLength(value, "utf8")}:${value}`;
812
+ }
813
+ function comparePath(a, b) {
814
+ if (a < b) return -1;
815
+ if (a > b) return 1;
816
+ return 0;
817
+ }
818
+ function filterText(paths, symbols) {
819
+ const pathText = [...paths].sort(comparePath).join("\n");
820
+ const symbolText = [...symbols].sort(comparePath).join("\n");
821
+ return `${pathText}
822
+ /
823
+ ${symbolText}`;
824
+ }
825
+ function contributorText(rows) {
826
+ const sorted = [...rows].sort((a, b) => comparePath(a.path, b.path));
827
+ return sorted.map((row) => `${row.path} ${row.content_hash}
828
+ `).join("");
829
+ }
830
+ function sha256(text) {
831
+ return createHash("sha256").update(text, "utf8").digest("hex");
832
+ }
833
+ function cacheMaterial(request, version, filter) {
834
+ return [
835
+ field(request.query),
836
+ field(request.channel),
837
+ field(request.scope),
838
+ field(String(request.k)),
839
+ field(String(request.budget)),
840
+ field(request.embeddings ? "1" : "0"),
841
+ field(request.activeChange),
842
+ field(version),
843
+ field(filter)
844
+ ].join("\n");
845
+ }
846
+ function under(rel, root) {
847
+ let end = root.length;
848
+ while (end > 0 && root.charCodeAt(end - 1) === 47) end -= 1;
849
+ const prefix = root.slice(0, end);
850
+ if (!prefix) return false;
851
+ return rel === prefix || rel.startsWith(`${prefix}/`);
852
+ }
853
+ function openCacheDb(projectRoot) {
854
+ const db = new Database(indexDbPath(projectRoot));
855
+ db.exec(SEARCH_SCHEMA_SQL);
856
+ return db;
857
+ }
858
+ function scopeState(db, scope) {
859
+ const counts = db.prepare(
860
+ `SELECT
861
+ (SELECT COUNT(*) FROM chunks WHERE scope = ?) AS chunks,
862
+ (SELECT COUNT(*) FROM chunks AS c
863
+ JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
864
+ WHERE c.scope = ?) AS ready`
865
+ ).get(scope, scope);
866
+ if (!counts || counts.chunks === 0) return "empty";
867
+ const model = db.prepare("SELECT value FROM meta WHERE key = ?").get("embedding_model");
868
+ const modelOk = !model || model.value === EMBEDDING_MODEL;
869
+ if (counts.ready === counts.chunks && modelOk) return "ready";
870
+ return "partial";
871
+ }
872
+ function readyList(db, scopes, embeddings) {
873
+ if (!embeddings) return "";
874
+ return scopes.filter((scope) => scopeState(db, scope) === "ready").sort(comparePath).join(",");
875
+ }
876
+ function normalizeHybrid(hybridScopes) {
877
+ return hybridScopes.split(",").filter(Boolean).sort(comparePath).join(",");
878
+ }
879
+ async function resolveActiveDirectory(projectRoot, config) {
880
+ let text;
881
+ try {
882
+ text = await readFile2(activePointerPath(projectRoot, config), "utf8");
883
+ } catch {
884
+ return "";
885
+ }
886
+ const id = text.trim();
887
+ if (!id || isAbsolute(id) || id.includes("\\")) return "";
888
+ const parts = id.split("/");
889
+ if (parts.some((part) => part === "" || part === "." || part === "..")) return "";
890
+ return displayChangePath(config, id);
891
+ }
892
+ function forgetPath(db, rel) {
893
+ const keys = db.prepare("SELECT cache_key FROM result_cache_files WHERE path = ?").all(rel);
894
+ const dropCache = db.prepare("DELETE FROM result_cache WHERE cache_key = ?");
895
+ const dropFiles = db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?");
896
+ for (const row of keys) {
897
+ dropCache.run(row.cache_key);
898
+ dropFiles.run(row.cache_key);
899
+ }
900
+ db.prepare("DELETE FROM spec_summaries WHERE path = ?").run(rel);
901
+ }
902
+ function dropCacheKey(db, cacheKey) {
903
+ db.prepare("DELETE FROM result_cache WHERE cache_key = ?").run(cacheKey);
904
+ db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?").run(cacheKey);
905
+ }
906
+ function hitFromPayload(value) {
907
+ if (!value || typeof value !== "object") return null;
908
+ const hit = value;
909
+ const formOk = hit.form === "full" || hit.form === "signature" || hit.form === "summary";
910
+ if (!formOk) return null;
911
+ if (typeof hit.path !== "string" || typeof hit.symbol !== "string") return null;
912
+ if (typeof hit.text !== "string" || typeof hit.signature !== "string") return null;
913
+ if (typeof hit.body !== "string" || typeof hit.language !== "string") return null;
914
+ if (typeof hit.startLine !== "number" || typeof hit.endLine !== "number") return null;
915
+ if (typeof hit.score !== "number" || typeof hit.scope !== "string") return null;
916
+ return {
917
+ scope: hit.scope,
918
+ path: hit.path,
919
+ startLine: hit.startLine,
920
+ endLine: hit.endLine,
921
+ symbol: hit.symbol,
922
+ score: hit.score,
923
+ form: hit.form,
924
+ text: hit.text,
925
+ signature: hit.signature,
926
+ body: hit.body,
927
+ language: hit.language
928
+ };
929
+ }
930
+ function payloadHits(raw) {
931
+ let parsed;
932
+ try {
933
+ parsed = JSON.parse(raw);
934
+ } catch {
935
+ return null;
936
+ }
937
+ if (!parsed || typeof parsed !== "object") return null;
938
+ const body = parsed;
939
+ if (typeof body.text !== "string" || !Array.isArray(body.hits)) return null;
940
+ const hits = [];
941
+ for (const value of body.hits) {
942
+ const hit = hitFromPayload(value);
943
+ if (!hit) return null;
944
+ hits.push(hit);
945
+ }
946
+ return { text: body.text, hits };
947
+ }
948
+ function cachedWarning(db, request) {
949
+ if (!request.embeddings) return void 0;
950
+ const partial = request.scopes.some((scope) => scopeState(db, scope) === "partial");
951
+ return partial ? EMBEDDINGS_UNAVAILABLE : void 0;
952
+ }
953
+ function readPackedCache(request) {
954
+ if (containsPrivate(request.query)) return null;
955
+ const db = openCacheDb(request.projectRoot);
956
+ try {
957
+ const filter = filterText(request.filterPaths ?? [], request.filterSymbols ?? []);
958
+ const rows = db.prepare(
959
+ `SELECT cache_key, mode, hybrid_scopes, index_version, payload, query
960
+ FROM result_cache
961
+ WHERE query = ? AND channel = ? AND scope = ? AND k = ? AND budget = ?
962
+ AND embeddings = ? AND active_change = ? AND filter = ?`
963
+ ).all(
964
+ request.query,
965
+ request.channel,
966
+ request.scope,
967
+ request.k,
968
+ request.budget,
969
+ request.embeddings ? 1 : 0,
970
+ request.activeChange,
971
+ filter
972
+ );
973
+ const expected = readyList(db, request.scopes, request.embeddings);
974
+ for (const row of rows) {
975
+ const parsed = payloadHits(row.payload);
976
+ if (!parsed || containsPrivate(row.query) || containsPrivate(row.payload) || containsPrivate(parsed.text)) {
977
+ dropCacheKey(db, row.cache_key);
978
+ continue;
979
+ }
980
+ const files = db.prepare("SELECT path, content_hash FROM result_cache_files WHERE cache_key = ?").all(row.cache_key);
981
+ let fresh = true;
982
+ for (const file of files) {
983
+ const current = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(file.path);
984
+ if (!current || current.content_hash !== file.content_hash) {
985
+ fresh = false;
986
+ break;
987
+ }
988
+ }
989
+ if (!fresh || sha256(contributorText(files)) !== row.index_version) {
990
+ dropCacheKey(db, row.cache_key);
991
+ continue;
992
+ }
993
+ const hybrid = normalizeHybrid(row.hybrid_scopes);
994
+ if (!request.embeddings) {
995
+ if (row.mode !== "bm25" || hybrid !== "") {
996
+ dropCacheKey(db, row.cache_key);
997
+ continue;
998
+ }
999
+ } else if (hybrid !== expected) {
1000
+ dropCacheKey(db, row.cache_key);
1001
+ continue;
1002
+ }
1003
+ return { text: parsed.text, hits: parsed.hits, warning: cachedWarning(db, request) };
1004
+ }
1005
+ return null;
1006
+ } finally {
1007
+ db.close();
1008
+ }
1009
+ }
1010
+ function serializeHit(hit) {
1011
+ return {
1012
+ scope: hit.scope,
1013
+ path: hit.path,
1014
+ startLine: hit.startLine,
1015
+ endLine: hit.endLine,
1016
+ symbol: hit.symbol,
1017
+ score: hit.score,
1018
+ form: hit.form,
1019
+ text: hit.text,
1020
+ signature: hit.signature,
1021
+ body: hit.body,
1022
+ language: hit.language,
1023
+ rankedBy: hit.rankedBy ?? null
1024
+ };
1025
+ }
1026
+ function writePackedCache(request, packed, contributors, mode, hybridScopes) {
1027
+ if (containsPrivate(request.query) || containsPrivate(packed.text)) return;
1028
+ if (packed.text.startsWith("Retrieved context skipped:")) return;
1029
+ const db = openCacheDb(request.projectRoot);
1030
+ try {
1031
+ const byPath2 = /* @__PURE__ */ new Map();
1032
+ for (const hit of contributors) {
1033
+ if (byPath2.has(hit.path)) continue;
1034
+ const row = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(hit.path);
1035
+ if (row) byPath2.set(hit.path, row.content_hash);
1036
+ }
1037
+ const files = [...byPath2.entries()].map(([path, content_hash]) => ({
1038
+ path,
1039
+ content_hash
1040
+ }));
1041
+ const version = sha256(contributorText(files));
1042
+ const filter = filterText(request.filterPaths ?? [], request.filterSymbols ?? []);
1043
+ const cacheKey = sha256(cacheMaterial(request, version, filter));
1044
+ const payload = JSON.stringify({
1045
+ text: packed.text,
1046
+ hits: packed.hits.map((hit) => serializeHit(hit))
1047
+ });
1048
+ if (containsPrivate(payload)) return;
1049
+ const hybrid = normalizeHybrid(hybridScopes);
1050
+ const write = db.transaction(() => {
1051
+ db.prepare(
1052
+ `INSERT INTO result_cache (
1053
+ cache_key, query, channel, scope, k, budget, embeddings, active_change, filter,
1054
+ index_version, mode, hybrid_scopes, payload
1055
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1056
+ ON CONFLICT(cache_key) DO UPDATE SET
1057
+ query = excluded.query,
1058
+ channel = excluded.channel,
1059
+ scope = excluded.scope,
1060
+ k = excluded.k,
1061
+ budget = excluded.budget,
1062
+ embeddings = excluded.embeddings,
1063
+ active_change = excluded.active_change,
1064
+ filter = excluded.filter,
1065
+ index_version = excluded.index_version,
1066
+ mode = excluded.mode,
1067
+ hybrid_scopes = excluded.hybrid_scopes,
1068
+ payload = excluded.payload`
1069
+ ).run(
1070
+ cacheKey,
1071
+ request.query,
1072
+ request.channel,
1073
+ request.scope,
1074
+ request.k,
1075
+ request.budget,
1076
+ request.embeddings ? 1 : 0,
1077
+ request.activeChange,
1078
+ filter,
1079
+ version,
1080
+ mode,
1081
+ hybrid,
1082
+ payload
1083
+ );
1084
+ db.prepare("DELETE FROM result_cache_files WHERE cache_key = ?").run(cacheKey);
1085
+ const insertFile = db.prepare(
1086
+ "INSERT INTO result_cache_files (cache_key, path, content_hash) VALUES (?, ?, ?)"
1087
+ );
1088
+ for (const file of files) insertFile.run(cacheKey, file.path, file.content_hash);
1089
+ });
1090
+ write();
1091
+ } finally {
1092
+ db.close();
1093
+ }
1094
+ }
1095
+ function attachSummaries(projectRoot, hits, activeChange) {
1096
+ const db = openCacheDb(projectRoot);
1097
+ try {
1098
+ return hits.map((hit) => {
1099
+ if (hit.scope === "code") return hit;
1100
+ if (hit.scope === "specs" && under(hit.path, activeChange)) return hit;
1101
+ if (hit.scope !== "specs" && hit.scope !== "memory") return hit;
1102
+ try {
1103
+ const file = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(hit.path);
1104
+ if (!file) return hit;
1105
+ const stored = db.prepare(
1106
+ `SELECT summary FROM spec_summaries
1107
+ WHERE path = ? AND content_hash = ? AND symbol = ? AND start_line = ?`
1108
+ ).get(hit.path, file.content_hash, hit.symbol, hit.startLine);
1109
+ if (stored) return { ...hit, summary: stored.summary };
1110
+ if (containsPrivate(hit.body) || containsPrivate(hit.signature)) return hit;
1111
+ const summary = extractiveSummary(hit.signature, hit.body);
1112
+ if (containsPrivate(summary)) return hit;
1113
+ db.prepare("DELETE FROM spec_summaries WHERE path = ? AND content_hash != ?").run(
1114
+ hit.path,
1115
+ file.content_hash
1116
+ );
1117
+ db.prepare(
1118
+ `INSERT INTO spec_summaries (path, content_hash, symbol, start_line, summary)
1119
+ VALUES (?, ?, ?, ?, ?)`
1120
+ ).run(hit.path, file.content_hash, hit.symbol, hit.startLine, summary);
1121
+ return { ...hit, summary };
1122
+ } catch {
1123
+ return hit;
1124
+ }
1125
+ });
1126
+ } catch {
1127
+ return hits.slice();
1128
+ } finally {
1129
+ db.close();
1130
+ }
1131
+ }
1132
+
1133
+ // src/search/chunk-code.ts
1134
+ import { existsSync } from "fs";
1135
+ import { createRequire } from "module";
1136
+ import { fileURLToPath } from "url";
1137
+ import { dirname, join as join4 } from "pathe";
1138
+ import { Language, Parser } from "web-tree-sitter";
1139
+ var EMIT = /* @__PURE__ */ new Set([
1140
+ "function_declaration",
1141
+ "generator_function_declaration",
1142
+ "class_declaration",
1143
+ "method_definition",
1144
+ "interface_declaration",
1145
+ "type_alias_declaration",
1146
+ "enum_declaration",
1147
+ "function_definition",
1148
+ "class_definition",
1149
+ "function_item",
1150
+ "struct_item",
1151
+ "enum_item",
1152
+ "trait_item",
1153
+ "impl_item",
1154
+ "mod_item"
1155
+ ]);
1156
+ var INNER_FUNCTION = /* @__PURE__ */ new Set(["arrow_function", "function", "function_expression"]);
1157
+ var NAME_TYPES = /* @__PURE__ */ new Set(["identifier", "property_identifier", "type_identifier"]);
1158
+ var WASM_BY_EXT = {
1159
+ ".js": "tree-sitter-javascript.wasm",
1160
+ ".mjs": "tree-sitter-javascript.wasm",
1161
+ ".cjs": "tree-sitter-javascript.wasm",
1162
+ ".jsx": "tree-sitter-javascript.wasm",
1163
+ ".ts": "tree-sitter-typescript.wasm",
1164
+ ".mts": "tree-sitter-typescript.wasm",
1165
+ ".cts": "tree-sitter-typescript.wasm",
1166
+ ".tsx": "tree-sitter-tsx.wasm",
1167
+ ".py": "tree-sitter-python.wasm",
1168
+ ".rs": "tree-sitter-rust.wasm"
1169
+ };
1170
+ var LANG_BY_EXT = {
1171
+ ".js": "js",
1172
+ ".mjs": "js",
1173
+ ".cjs": "js",
1174
+ ".jsx": "js",
1175
+ ".ts": "ts",
1176
+ ".mts": "ts",
1177
+ ".cts": "ts",
1178
+ ".tsx": "tsx",
1179
+ ".py": "py",
1180
+ ".rs": "rs"
1181
+ };
1182
+ function languageForExt(ext) {
1183
+ return LANG_BY_EXT[ext] ?? null;
1184
+ }
1185
+ function grammarForExt(ext) {
1186
+ return WASM_BY_EXT[ext] ?? null;
1187
+ }
1188
+ function vendorDir() {
1189
+ const here = dirname(fileURLToPath(import.meta.url));
1190
+ const candidates = [
1191
+ join4(here, "..", "vendor", "tree-sitter"),
1192
+ join4(here, "vendor", "tree-sitter"),
1193
+ join4(here, "..", "..", "vendor", "tree-sitter")
1194
+ ];
1195
+ for (const candidate of candidates) {
1196
+ if (existsSync(candidate)) return candidate;
1197
+ }
1198
+ return candidates[0];
1199
+ }
1200
+ var ready2 = null;
1201
+ var languages = /* @__PURE__ */ new Map();
1202
+ async function initParser() {
1203
+ if (!ready2) {
1204
+ const require2 = createRequire(import.meta.url);
1205
+ const wasm = require2.resolve("web-tree-sitter/web-tree-sitter.wasm");
1206
+ ready2 = Parser.init({ locateFile: () => wasm });
1207
+ }
1208
+ await ready2;
1209
+ }
1210
+ async function loadLanguage(ext) {
1211
+ const file = grammarForExt(ext);
1212
+ if (!file) return null;
1213
+ const cached = languages.get(file);
1214
+ if (cached) return cached;
1215
+ await initParser();
1216
+ const lang = await Language.load(join4(vendorDir(), file));
1217
+ languages.set(file, lang);
1218
+ return lang;
1219
+ }
1220
+ function sourceLines(text) {
1221
+ let normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
1222
+ if (normalized.endsWith("\n")) normalized = normalized.slice(0, -1);
1223
+ if (!normalized) return [];
1224
+ return normalized.split("\n");
1225
+ }
1226
+ function startLine(node) {
1227
+ return node.startPosition.row + 1;
1228
+ }
1229
+ function endLine(node) {
1230
+ if (node.endPosition.column === 0 && node.endPosition.row > node.startPosition.row) {
1231
+ return node.endPosition.row;
1232
+ }
1233
+ return node.endPosition.row + 1;
1234
+ }
1235
+ function ownName(node) {
1236
+ for (let i = 0; i < node.childCount; i += 1) {
1237
+ const child = node.child(i);
1238
+ if (child && NAME_TYPES.has(child.type)) return child.text;
1239
+ }
1240
+ return null;
1241
+ }
1242
+ function hasChildType(node, type) {
1243
+ for (let i = 0; i < node.childCount; i += 1) {
1244
+ if (node.child(i)?.type === type) return true;
1245
+ }
1246
+ return false;
1247
+ }
1248
+ function hasDescendantType(node, types) {
1249
+ for (let i = 0; i < node.childCount; i += 1) {
1250
+ const child = node.child(i);
1251
+ if (!child) continue;
1252
+ if (types.has(child.type) || hasDescendantType(child, types)) return true;
1253
+ }
1254
+ return false;
1255
+ }
1256
+ function declaresFunction(node) {
1257
+ return (node.type === "lexical_declaration" || node.type === "variable_declaration") && hasDescendantType(node, INNER_FUNCTION);
1258
+ }
1259
+ function insideCapturedDeclaration(node) {
1260
+ let parent = node.parent;
1261
+ while (parent) {
1262
+ if (declaresFunction(parent)) return true;
1263
+ parent = parent.parent;
1264
+ }
1265
+ return false;
1266
+ }
1267
+ function classPrefix(node) {
1268
+ if (node.type === "class_declaration" || node.type === "class_definition") return null;
1269
+ let parent = node.parent;
1270
+ while (parent) {
1271
+ if (parent.type === "class_declaration" || parent.type === "class_definition" || parent.type === "class") {
1272
+ return ownName(parent);
1273
+ }
1274
+ parent = parent.parent;
1275
+ }
1276
+ return null;
1277
+ }
1278
+ function spanNode(node) {
1279
+ const parent = node.parent;
1280
+ if (!parent) return node;
1281
+ if (parent.type === "export_statement" || parent.type === "decorated_definition") return parent;
1282
+ return node;
1283
+ }
1284
+ function shouldEmit(node) {
1285
+ if (insideCapturedDeclaration(node)) return false;
1286
+ if (declaresFunction(node)) return true;
1287
+ if (!EMIT.has(node.type)) return false;
1288
+ if (node.type === "mod_item" && !hasChildType(node, "declaration_list")) return false;
1289
+ return true;
1290
+ }
1291
+ function symbolFor(node) {
1292
+ const name = ownName(node);
1293
+ if (node.type === "impl_item" && !name) return "(impl)";
1294
+ const prefix = classPrefix(node);
1295
+ if (prefix && name) return `${prefix}.${name}`;
1296
+ if (name) return name;
1297
+ return "(anonymous)";
1298
+ }
1299
+ function fileChunk(text, language) {
1300
+ const lines = sourceLines(text);
1301
+ if (!lines.length) return [];
1302
+ let signature = "(file)";
1303
+ for (const line of lines) {
1304
+ let blank = true;
1305
+ for (const ch of line) {
1306
+ const code = ch.codePointAt(0) ?? 0;
1307
+ if (code !== 32 && code !== 9) {
1308
+ blank = false;
1309
+ break;
1310
+ }
1311
+ }
1312
+ if (!blank) {
1313
+ signature = line;
1314
+ break;
1315
+ }
1316
+ }
1317
+ return [
1318
+ {
1319
+ symbol: "(file)",
1320
+ startLine: 1,
1321
+ endLine: lines.length,
1322
+ signature,
1323
+ body: lines.join("\n"),
1324
+ language
1325
+ }
1326
+ ];
1327
+ }
1328
+ var parser = null;
1329
+ var parseQueue = Promise.resolve();
1330
+ function parseSerialized(lang, text) {
1331
+ let release = () => void 0;
1332
+ const gate = new Promise((resolve) => {
1333
+ release = resolve;
1334
+ });
1335
+ const previous = parseQueue;
1336
+ parseQueue = gate;
1337
+ return previous.then(() => {
1338
+ try {
1339
+ if (!parser) parser = new Parser();
1340
+ parser.setLanguage(lang);
1341
+ return parser.parse(text);
1342
+ } catch {
1343
+ return null;
1344
+ } finally {
1345
+ release();
1346
+ }
1347
+ });
1348
+ }
1349
+ async function chunkCode(text, ext) {
1350
+ if (text.includes("\0")) return null;
1351
+ const language = languageForExt(ext);
1352
+ if (!language) return null;
1353
+ const lang = await loadLanguage(ext);
1354
+ if (!lang) return null;
1355
+ const tree = await parseSerialized(lang, text);
1356
+ if (!tree) return null;
1357
+ const lines = sourceLines(text);
1358
+ const chunks = [];
1359
+ const visit = (node) => {
1360
+ if (shouldEmit(node)) {
1361
+ const span = spanNode(node);
1362
+ const start2 = startLine(span);
1363
+ const end = endLine(span);
1364
+ if (start2 >= 1 && end >= start2 && end <= lines.length) {
1365
+ const bodyLines = lines.slice(start2 - 1, end);
1366
+ const signature = bodyLines[0] ?? "(anonymous)";
1367
+ chunks.push({
1368
+ symbol: symbolFor(node),
1369
+ startLine: start2,
1370
+ endLine: end,
1371
+ signature,
1372
+ body: bodyLines.join("\n"),
1373
+ language
1374
+ });
1375
+ }
1376
+ }
1377
+ for (let i = 0; i < node.childCount; i += 1) {
1378
+ const child = node.child(i);
1379
+ if (child) visit(child);
1380
+ }
1381
+ };
1382
+ visit(tree.rootNode);
1383
+ if (!chunks.length) return fileChunk(text, language);
1384
+ return chunks;
1385
+ }
1386
+
1387
+ // src/search/rrf.ts
1388
+ var RRF_K = 60;
1389
+ function rrf(rank) {
1390
+ if (rank === void 0) return 0;
1391
+ return 1 / (RRF_K + rank);
1392
+ }
1393
+ function byPath(a, b) {
1394
+ if (a.path !== b.path) return a.path < b.path ? -1 : 1;
1395
+ if (a.startLine !== b.startLine) return a.startLine - b.startLine;
1396
+ if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
1397
+ return 0;
1398
+ }
1399
+ function fuseByRank(entries) {
1400
+ const sorted = [...entries].sort((a, b) => {
1401
+ const delta = rrf(b.keywordRank) + rrf(b.vectorRank) - (rrf(a.keywordRank) + rrf(a.vectorRank));
1402
+ if (delta !== 0) return delta;
1403
+ return byPath(a, b);
1404
+ });
1405
+ return sorted.map((entry) => ({
1406
+ id: entry.id,
1407
+ score: rrf(entry.keywordRank) + rrf(entry.vectorRank)
1408
+ }));
1409
+ }
1410
+ function hitKey(hit) {
1411
+ return `${hit.path}\0${hit.startLine}\0${hit.symbol}`;
1412
+ }
1413
+ function comparePath2(a, b) {
1414
+ if (a.path !== b.path) return a.path < b.path ? -1 : 1;
1415
+ if (a.startLine !== b.startLine) return a.startLine - b.startLine;
1416
+ if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
1417
+ return 0;
1418
+ }
1419
+ function fuseHits(keyword, vector) {
1420
+ const map = /* @__PURE__ */ new Map();
1421
+ for (let i = 0; i < keyword.length; i++) {
1422
+ const hit = keyword[i];
1423
+ if (!hit) continue;
1424
+ map.set(hitKey(hit), { hit, keywordRank: i + 1 });
1425
+ }
1426
+ for (let i = 0; i < vector.length; i++) {
1427
+ const hit = vector[i];
1428
+ if (!hit) continue;
1429
+ const key = hitKey(hit);
1430
+ const existing = map.get(key);
1431
+ if (existing) existing.vectorRank = i + 1;
1432
+ else map.set(key, { hit, vectorRank: i + 1 });
1433
+ }
1434
+ const fused = [];
1435
+ for (const entry of map.values()) {
1436
+ fused.push({
1437
+ ...entry.hit,
1438
+ score: rrf(entry.keywordRank) + rrf(entry.vectorRank),
1439
+ rankedBy: "rrf"
1440
+ });
1441
+ }
1442
+ fused.sort((a, b) => {
1443
+ if (a.score !== b.score) return b.score - a.score;
1444
+ return comparePath2(a, b);
1445
+ });
1446
+ return fused;
1447
+ }
1448
+ var SCOPE_ORDER = { specs: 0, memory: 1, code: 2 };
1449
+ function compareBm25(a, b, scopeOrder) {
1450
+ if (a.score !== b.score) return a.score - b.score;
1451
+ if (scopeOrder) {
1452
+ const scopeDelta = SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
1453
+ if (scopeDelta !== 0) return scopeDelta;
1454
+ }
1455
+ return comparePath2(a, b);
1456
+ }
1457
+ function mergeRankedHits(hits) {
1458
+ if (hits.every((hit) => hit.rankedBy !== "rrf")) {
1459
+ return [...hits].sort((a, b) => compareBm25(a, b, true));
1460
+ }
1461
+ const rrfHits = hits.filter((hit) => hit.rankedBy === "rrf");
1462
+ const bm25Hits = hits.filter((hit) => hit.rankedBy !== "rrf");
1463
+ rrfHits.sort((a, b) => {
1464
+ if (a.score !== b.score) return b.score - a.score;
1465
+ return comparePath2(a, b);
1466
+ });
1467
+ bm25Hits.sort((a, b) => compareBm25(a, b, false));
1468
+ return [...rrfHits, ...bm25Hits];
1469
+ }
1470
+
1471
+ // src/search/builtin.ts
1472
+ import { spawn as spawn2 } from "child_process";
1473
+ import { createHash as createHash2 } from "crypto";
1474
+ import { readFile as readFile3, readdir as readdir2 } from "fs/promises";
1475
+ import Database2 from "better-sqlite3";
1476
+ import { isAbsolute as isAbsolute2, join as join5 } from "pathe";
1477
+
1478
+ // src/search/vec.ts
1479
+ var TABLES = {
1480
+ code: "chunk_vec_code",
1481
+ specs: "chunk_vec_specs",
1482
+ memory: "chunk_vec_memory"
1483
+ };
1484
+ var CREATE_SQL = `
1485
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_code USING vec0(
1486
+ embedding float[384] distance_metric=cosine
1487
+ );
1488
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_specs USING vec0(
1489
+ embedding float[384] distance_metric=cosine
1490
+ );
1491
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec_memory USING vec0(
1492
+ embedding float[384] distance_metric=cosine
1493
+ );
1494
+ `;
1495
+ var state = /* @__PURE__ */ new WeakMap();
1496
+ function vecTable(scope) {
1497
+ return TABLES[scope];
1498
+ }
1499
+ function vecTablesPresent(db) {
1500
+ const row = db.prepare(
1501
+ `SELECT 1 AS n FROM sqlite_master
1502
+ WHERE name IN ('chunk_vec_code', 'chunk_vec_specs', 'chunk_vec_memory')
1503
+ LIMIT 1`
1504
+ ).get();
1505
+ return Boolean(row);
1506
+ }
1507
+ function pickLoad(mod) {
1508
+ if (!mod || typeof mod !== "object") return null;
1509
+ const record = mod;
1510
+ if (typeof record.load === "function") return record.load;
1511
+ if (record.default && typeof record.default.load === "function") {
1512
+ return record.default.load;
1513
+ }
1514
+ return null;
1515
+ }
1516
+ async function ensureVecLoaded(db) {
1517
+ const known = state.get(db);
1518
+ if (known === "ready") return true;
1519
+ if (known === "failed") return false;
1520
+ try {
1521
+ const mod = await import("sqlite-vec");
1522
+ const load = pickLoad(mod);
1523
+ if (!load) {
1524
+ state.set(db, "failed");
1525
+ return false;
1526
+ }
1527
+ load(db);
1528
+ state.set(db, "ready");
1529
+ return true;
1530
+ } catch {
1531
+ state.set(db, "failed");
1532
+ return false;
1533
+ }
1534
+ }
1535
+ function vecReady(db) {
1536
+ return state.get(db) === "ready";
1537
+ }
1538
+ function createVecTables(db) {
1539
+ db.exec(CREATE_SQL);
1540
+ }
1541
+ function dropVecTables(db) {
1542
+ db.exec(`
1543
+ DROP TABLE IF EXISTS chunk_vec_code;
1544
+ DROP TABLE IF EXISTS chunk_vec_specs;
1545
+ DROP TABLE IF EXISTS chunk_vec_memory;
1546
+ `);
1547
+ }
1548
+ function insertVector(db, scope, id, value, hash) {
1549
+ const table = TABLES[scope];
1550
+ const rowid = BigInt(id);
1551
+ db.prepare(`DELETE FROM ${table} WHERE rowid = ?`).run(rowid);
1552
+ db.prepare(`INSERT INTO ${table} (rowid, embedding) VALUES (?, ?)`).run(rowid, value);
1553
+ db.prepare(
1554
+ `INSERT INTO vec_meta (chunk_id, chunk_hash, scope) VALUES (?, ?, ?)
1555
+ ON CONFLICT(chunk_id) DO UPDATE SET chunk_hash = excluded.chunk_hash, scope = excluded.scope`
1556
+ ).run(id, hash, scope);
1557
+ }
1558
+ function asBlob(value) {
1559
+ if (value instanceof Float32Array) return value;
1560
+ if (Buffer.isBuffer(value)) return value;
1561
+ if (value instanceof Uint8Array) return Buffer.from(value);
1562
+ return null;
1563
+ }
1564
+ function readVectorBlob(db, hash) {
1565
+ if (!vecReady(db)) return null;
1566
+ const row = db.prepare("SELECT chunk_id, scope FROM vec_meta WHERE chunk_hash = ? LIMIT 1").get(hash);
1567
+ if (!row || !TABLES[row.scope]) return null;
1568
+ try {
1569
+ const stored = db.prepare(`SELECT embedding FROM ${TABLES[row.scope]} WHERE rowid = ?`).get(BigInt(row.chunk_id));
1570
+ return stored ? asBlob(stored.embedding) : null;
1571
+ } catch {
1572
+ return null;
1573
+ }
1574
+ }
1575
+ function queryVectors(db, scope, queryVec, k) {
1576
+ const table = TABLES[scope];
1577
+ return db.prepare(
1578
+ `SELECT c.path, c.scope, c.symbol, c.start_line, c.end_line, c.signature, c.body,
1579
+ f.language AS language, v.distance AS distance
1580
+ FROM ${table} AS v
1581
+ JOIN chunks AS c ON c.id = v.rowid
1582
+ JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
1583
+ JOIN files AS f ON f.path = c.path
1584
+ WHERE v.embedding MATCH ?
1585
+ AND k = ?
1586
+ AND c.scope = ?
1587
+ ORDER BY v.distance ASC, c.path ASC, c.start_line ASC, c.symbol ASC`
1588
+ ).all(queryVec, k, scope);
1589
+ }
1590
+
1591
+ // src/search/builtin.ts
1592
+ var CODE_EXT = /* @__PURE__ */ new Set([
1593
+ ".ts",
1594
+ ".tsx",
1595
+ ".mts",
1596
+ ".cts",
1597
+ ".js",
1598
+ ".jsx",
1599
+ ".mjs",
1600
+ ".cjs",
1601
+ ".py",
1602
+ ".rs"
1603
+ ]);
1604
+ var EMPTY_STATS = {
1605
+ provider: "builtin",
1606
+ files: 0,
1607
+ chunks: 0,
1608
+ lastIndexFilesConsidered: 0,
1609
+ lastIndexFilesReindexed: 0,
1610
+ lastIndexChunksReindexed: 0,
1611
+ lastIndexChunksEmbedded: 0,
1612
+ indexedCommit: null
1613
+ };
1614
+ function extensionOf(rel) {
1615
+ const slash = rel.lastIndexOf("/");
1616
+ const base = slash === -1 ? rel : rel.slice(slash + 1);
1617
+ const dot = base.lastIndexOf(".");
1618
+ if (dot <= 0) return "";
1619
+ return base.slice(dot).toLowerCase();
1620
+ }
1621
+ function stripTrailingSlashes(value) {
1622
+ let end = value.length;
1623
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
1624
+ return value.slice(0, end);
1625
+ }
1626
+ function under2(rel, root) {
1627
+ const prefix = stripTrailingSlashes(root);
1628
+ if (!prefix) return false;
1629
+ return rel === prefix || rel.startsWith(`${prefix}/`);
1630
+ }
1631
+ function scopeForPath(rel, config) {
1632
+ const ext = extensionOf(rel);
1633
+ if (CODE_EXT.has(ext)) return "code";
1634
+ if (ext !== ".md") return null;
1635
+ if (under2(rel, memoryPathRel(config))) return "memory";
1636
+ const archive = stripTrailingSlashes(config.archive_path) || "archive";
1637
+ if (under2(rel, changesPathRel(config)) || under2(rel, archive) || under2(rel, "docs")) {
1638
+ return "specs";
1639
+ }
1640
+ return null;
1641
+ }
1642
+ function assertRepoRelative(input) {
1643
+ if (isAbsolute2(input)) throw new Error("Index path must be project-relative.");
1644
+ let rel = input.replaceAll("\\", "/");
1645
+ while (rel.startsWith("./")) rel = rel.slice(2);
1646
+ if (!rel || rel.split("/").includes("..")) {
1647
+ throw new Error("Index path must be project-relative.");
1648
+ }
1649
+ return rel;
1650
+ }
1651
+ function chunkHash(body) {
1652
+ return createHash2("sha256").update(body, "utf8").digest("hex");
1653
+ }
1654
+ function migrateChunkHash(db) {
1655
+ const columns = db.prepare("PRAGMA table_info(chunks)").all();
1656
+ if (!columns.some((column) => column.name === "chunk_hash")) {
1657
+ db.exec(ADD_CHUNK_HASH_SQL);
1658
+ }
1659
+ const rows = db.prepare("SELECT id, body FROM chunks WHERE chunk_hash = ''").all();
1660
+ const update = db.prepare("UPDATE chunks SET chunk_hash = ? WHERE id = ?");
1661
+ for (const row of rows) update.run(chunkHash(row.body), row.id);
1662
+ }
1663
+ function openDb(projectRoot) {
1664
+ const db = new Database2(indexDbPath(projectRoot));
1665
+ db.exec(SEARCH_SCHEMA_SQL);
1666
+ migrateChunkHash(db);
1667
+ return db;
1668
+ }
1669
+ async function ensureDb(projectRoot) {
1670
+ await ensureDir(sddRoot(projectRoot));
1671
+ return openDb(projectRoot);
1672
+ }
1673
+ function deletePath(db, rel) {
1674
+ forgetPath(db, rel);
1675
+ const had = db.prepare("SELECT 1 AS n FROM files WHERE path = ?").get(rel);
1676
+ if (vecReady(db)) {
1677
+ const ids = db.prepare("SELECT id, scope FROM chunks WHERE path = ?").all(rel);
1678
+ for (const row of ids) {
1679
+ try {
1680
+ db.prepare(`DELETE FROM ${vecTable(row.scope)} WHERE rowid = ?`).run(BigInt(row.id));
1681
+ } catch {
1682
+ }
1683
+ }
1684
+ }
1685
+ db.prepare("DELETE FROM vec_meta WHERE chunk_id IN (SELECT id FROM chunks WHERE path = ?)").run(
1686
+ rel
1687
+ );
1688
+ db.prepare(
1689
+ `DELETE FROM chunks_fts WHERE rowid IN (
1690
+ SELECT m.fts_rowid FROM chunks_fts_map AS m
1691
+ JOIN chunks AS c ON c.id = m.chunk_id
1692
+ WHERE c.path = ?
1693
+ )`
1694
+ ).run(rel);
1695
+ db.prepare("DELETE FROM chunks WHERE path = ?").run(rel);
1696
+ db.prepare("DELETE FROM files WHERE path = ?").run(rel);
1697
+ return Boolean(had);
1698
+ }
1699
+ function writeChunks(db, rel, hash, scope, language, chunks) {
1700
+ deletePath(db, rel);
1701
+ if (!chunks.length) return [];
1702
+ db.prepare("INSERT INTO files (path, content_hash, scope, language) VALUES (?, ?, ?, ?)").run(
1703
+ rel,
1704
+ hash,
1705
+ scope,
1706
+ language
1707
+ );
1708
+ const insertChunk = db.prepare(
1709
+ `INSERT INTO chunks (path, scope, symbol, start_line, end_line, signature, body, chunk_hash)
1710
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1711
+ );
1712
+ const insertFts = db.prepare("INSERT INTO chunks_fts (symbol, body) VALUES (?, ?)");
1713
+ const insertMap = db.prepare("INSERT INTO chunks_fts_map (chunk_id, fts_rowid) VALUES (?, ?)");
1714
+ const inserted = [];
1715
+ for (const chunk of chunks) {
1716
+ const hashOfBody = chunkHash(chunk.body);
1717
+ const info = insertChunk.run(
1718
+ rel,
1719
+ scope,
1720
+ chunk.symbol,
1721
+ chunk.startLine,
1722
+ chunk.endLine,
1723
+ chunk.signature,
1724
+ chunk.body,
1725
+ hashOfBody
1726
+ );
1727
+ const fts = insertFts.run(chunk.symbol, chunk.body);
1728
+ const id = Number(info.lastInsertRowid);
1729
+ insertMap.run(id, fts.lastInsertRowid);
1730
+ inserted.push({ id, hash: hashOfBody, body: chunk.body, scope });
1731
+ }
1732
+ return inserted;
1733
+ }
1734
+ function mapHit(row) {
1735
+ return {
1736
+ scope: row.scope,
1737
+ path: row.path,
1738
+ startLine: row.start_line,
1739
+ endLine: row.end_line,
1740
+ symbol: row.symbol,
1741
+ score: row.score,
1742
+ signature: row.signature,
1743
+ body: row.body,
1744
+ language: row.language,
1745
+ rankedBy: "bm25"
1746
+ };
1747
+ }
1748
+ var HIT_SQL = `c.path, c.scope, c.symbol, c.start_line, c.end_line, c.signature, c.body,
1749
+ f.language AS language`;
1750
+ function queryRanked(db, query, scope, k) {
1751
+ const tokens = ftsTokens(query);
1752
+ if (!tokens.length || k < 1) return [];
1753
+ const rows = db.prepare(
1754
+ `SELECT ${HIT_SQL}, bm25(chunks_fts) AS score
1755
+ FROM chunks_fts
1756
+ JOIN chunks_fts_map AS m ON m.fts_rowid = chunks_fts.rowid
1757
+ JOIN chunks AS c ON c.id = m.chunk_id
1758
+ JOIN files AS f ON f.path = c.path
1759
+ WHERE chunks_fts MATCH ? AND c.scope = ?
1760
+ ORDER BY score ASC, c.path ASC, c.start_line ASC, c.symbol ASC
1761
+ LIMIT ?`
1762
+ ).all(tokens.join(" OR "), scope, k);
1763
+ return rows.map(mapHit);
1764
+ }
1765
+ function queryScope(db, scope) {
1766
+ const rows = db.prepare(
1767
+ `SELECT ${HIT_SQL}, 0 AS score
1768
+ FROM chunks AS c
1769
+ JOIN files AS f ON f.path = c.path
1770
+ WHERE c.scope = ?
1771
+ ORDER BY c.path ASC, c.start_line ASC, c.symbol ASC`
1772
+ ).all(scope);
1773
+ return rows.map(mapHit);
1774
+ }
1775
+ async function walkFiles(projectRoot) {
1776
+ const out = [];
1777
+ async function walk(dir, rel) {
1778
+ let entries;
1779
+ try {
1780
+ entries = await readdir2(dir, { withFileTypes: true });
1781
+ } catch {
1782
+ return;
1783
+ }
1784
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1785
+ for (const entry of entries) {
1786
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
1787
+ if (entry.isDirectory()) {
1788
+ if (isPrunedDir(child)) continue;
1789
+ await walk(join5(dir, entry.name), child);
1790
+ } else if (entry.isFile()) {
1791
+ out.push(child);
1792
+ }
1793
+ }
1794
+ }
1795
+ await walk(projectRoot, "");
1796
+ return out;
1797
+ }
1798
+ async function chunksFor(bytes, rel, scope) {
1799
+ const text = bytes.toString("utf8");
1800
+ if (scope !== "code") {
1801
+ return chunkMarkdown(text).map((chunk) => ({ ...chunk, language: "md" }));
1802
+ }
1803
+ const ext = extensionOf(rel);
1804
+ const language = languageForExt(ext) ?? "js";
1805
+ const parsed = await chunkCode(text, ext);
1806
+ if (!parsed) return null;
1807
+ return parsed.map((chunk) => ({ ...chunk, language: chunk.language || language }));
1808
+ }
1809
+ function metaNumber(db, key) {
1810
+ const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
1811
+ const n = Number(row?.value ?? "0");
1812
+ return Number.isFinite(n) ? n : 0;
1813
+ }
1814
+ function metaString(db, key) {
1815
+ const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
1816
+ return row?.value ?? null;
1817
+ }
1818
+ function writeMetaValue(db, key, value) {
1819
+ db.prepare(
1820
+ `INSERT INTO meta (key, value) VALUES (?, ?)
1821
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
1822
+ ).run(key, value);
1823
+ }
1824
+ function readStats(db) {
1825
+ const files = db.prepare("SELECT COUNT(*) AS n FROM files").get();
1826
+ const chunks = db.prepare("SELECT COUNT(*) AS n FROM chunks").get();
1827
+ return {
1828
+ provider: "builtin",
1829
+ files: files.n,
1830
+ chunks: chunks.n,
1831
+ lastIndexFilesConsidered: metaNumber(db, "last_files_considered"),
1832
+ lastIndexFilesReindexed: metaNumber(db, "last_files_reindexed"),
1833
+ lastIndexChunksReindexed: metaNumber(db, "last_chunks_reindexed"),
1834
+ lastIndexChunksEmbedded: metaNumber(db, "last_chunks_embedded"),
1835
+ indexedCommit: metaString(db, "indexed_commit")
1836
+ };
1837
+ }
1838
+ function writeCounts(db, considered, reindexed, chunks, embedded) {
1839
+ writeMetaValue(db, "last_files_considered", String(considered));
1840
+ writeMetaValue(db, "last_files_reindexed", String(reindexed));
1841
+ writeMetaValue(db, "last_chunks_reindexed", String(chunks));
1842
+ writeMetaValue(db, "last_chunks_embedded", String(embedded));
1843
+ }
1844
+ function isCommitSha(value) {
1845
+ if (!value || value.length !== 40 && value.length !== 64) return false;
1846
+ for (let i = 0; i < value.length; i++) {
1847
+ const code = value.charCodeAt(i);
1848
+ const hex = code >= 48 && code <= 57 || code >= 97 && code <= 102 || code >= 65 && code <= 70;
1849
+ if (!hex) return false;
1850
+ }
1851
+ return true;
1852
+ }
1853
+ async function safeHead(projectRoot) {
1854
+ try {
1855
+ return await getHeadSha(projectRoot);
1856
+ } catch {
1857
+ return null;
1858
+ }
1859
+ }
1860
+ function gitOutput(cwd, args) {
1861
+ return new Promise((resolve) => {
1862
+ let settled = false;
1863
+ const finish = (value) => {
1864
+ if (settled) return;
1865
+ settled = true;
1866
+ resolve(value);
1867
+ };
1868
+ let child;
1869
+ try {
1870
+ child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
1871
+ } catch {
1872
+ finish(null);
1873
+ return;
1874
+ }
1875
+ const parts = [];
1876
+ child.stdout?.on("data", (chunk) => {
1877
+ parts.push(chunk);
1878
+ });
1879
+ child.on("error", () => finish(null));
1880
+ child.on("close", (code) => finish(code === 0 ? Buffer.concat(parts) : null));
1881
+ });
1882
+ }
1883
+ function splitNul(buf) {
1884
+ const parts = [];
1885
+ let start2 = 0;
1886
+ for (let i = 0; i <= buf.length; i++) {
1887
+ if (i === buf.length || buf[i] === 0) {
1888
+ if (i > start2) parts.push(buf.subarray(start2, i).toString("utf8"));
1889
+ start2 = i + 1;
1890
+ }
1891
+ }
1892
+ return parts;
1893
+ }
1894
+ function normalizeGitPath(input) {
1895
+ let rel = input.replaceAll("\\", "/");
1896
+ while (rel.startsWith("./")) rel = rel.slice(2);
1897
+ if (!rel || isAbsolute2(rel)) return null;
1898
+ return rel;
1899
+ }
1900
+ function parseNameStatus(buf) {
1901
+ const parts = splitNul(buf);
1902
+ const candidates = [];
1903
+ const deletions = [];
1904
+ for (let i = 0; i + 1 < parts.length; i += 2) {
1905
+ const status = parts[i] ?? "";
1906
+ const path = normalizeGitPath(parts[i + 1] ?? "");
1907
+ if (!status || !path) continue;
1908
+ if (status.charCodeAt(0) === 68) deletions.push(path);
1909
+ else candidates.push(path);
1910
+ }
1911
+ return { candidates, deletions };
1912
+ }
1913
+ async function readGitDelta(projectRoot, cursor) {
1914
+ const diff = await gitOutput(projectRoot, [
1915
+ "diff",
1916
+ "--name-status",
1917
+ "-z",
1918
+ "--no-renames",
1919
+ cursor
1920
+ ]);
1921
+ const untracked = await gitOutput(projectRoot, [
1922
+ "ls-files",
1923
+ "--others",
1924
+ "--exclude-standard",
1925
+ "-z"
1926
+ ]);
1927
+ if (!diff || !untracked) return null;
1928
+ const parsed = parseNameStatus(diff);
1929
+ for (const path of splitNul(untracked)) {
1930
+ const rel = normalizeGitPath(path);
1931
+ if (rel) parsed.candidates.push(rel);
1932
+ }
1933
+ return parsed;
1934
+ }
1935
+ async function prepareVectors(db, create) {
1936
+ const present = vecTablesPresent(db);
1937
+ if (!present && !create) return false;
1938
+ const loaded = await ensureVecLoaded(db);
1939
+ if (!loaded) return false;
1940
+ if (create) createVecTables(db);
1941
+ const model = metaString(db, "embedding_model");
1942
+ if (create && model && model !== EMBEDDING_MODEL) {
1943
+ dropVecTables(db);
1944
+ db.prepare("DELETE FROM vec_meta").run();
1945
+ db.prepare("DELETE FROM meta WHERE key = ?").run("embedding_model");
1946
+ createVecTables(db);
1947
+ }
1948
+ return true;
1949
+ }
1950
+ async function embedBodies(db, embedder, pending, tally) {
1951
+ for (let i = 0; i < pending.length; i += 16) {
1952
+ const batch = pending.slice(i, i + 16);
1953
+ tally.embedded += batch.length;
1954
+ let vectors;
1955
+ try {
1956
+ vectors = await embedder.embedPassages(batch.map((item) => item.body));
1957
+ } catch {
1958
+ return false;
1959
+ }
1960
+ if (vectors.length !== batch.length || vectors.some((vector) => vector.length !== EMBEDDING_DIM)) {
1961
+ return false;
1962
+ }
1963
+ const tx = db.transaction(() => {
1964
+ for (let j = 0; j < batch.length; j++) {
1965
+ const item = batch[j];
1966
+ const vector = vectors[j];
1967
+ if (!item || !vector) continue;
1968
+ for (const id of item.ids) insertVector(db, item.scope, id, vector, item.hash);
1969
+ }
1970
+ });
1971
+ tx();
1972
+ tally.storedVector = true;
1973
+ }
1974
+ return true;
1975
+ }
1976
+ function queueBody(pending, row) {
1977
+ const found = pending.find((item) => item.hash === row.hash);
1978
+ if (found) found.ids.push(row.id);
1979
+ else pending.push({ hash: row.hash, body: row.body, ids: [row.id], scope: row.scope });
1980
+ }
1981
+ async function embedInserted(db, embedder, inserted, copies, tally) {
1982
+ const pending = [];
1983
+ const tx = db.transaction(() => {
1984
+ for (const row of inserted) {
1985
+ const blob = copies.get(row.hash);
1986
+ if (blob) {
1987
+ insertVector(db, row.scope, row.id, blob, row.hash);
1988
+ tally.storedVector = true;
1989
+ continue;
1990
+ }
1991
+ queueBody(pending, row);
1992
+ }
1993
+ });
1994
+ tx();
1995
+ await embedBodies(db, embedder, pending, tally);
1996
+ }
1997
+ async function backfill(db, embedder, tally) {
1998
+ const rows = db.prepare(
1999
+ `SELECT c.id AS id, c.path AS path, c.body AS body, c.chunk_hash AS hash, c.scope AS scope
2000
+ FROM chunks AS c
2001
+ WHERE NOT EXISTS (
2002
+ SELECT 1 FROM vec_meta AS m
2003
+ WHERE m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
2004
+ )
2005
+ ORDER BY c.path ASC, c.start_line ASC, c.symbol ASC`
2006
+ ).all();
2007
+ let current = "";
2008
+ let group = [];
2009
+ const flush = async () => {
2010
+ if (!group.length) return;
2011
+ const pending = group;
2012
+ group = [];
2013
+ await embedBodies(db, embedder, pending, tally);
2014
+ };
2015
+ for (const row of rows) {
2016
+ if (row.path !== current) {
2017
+ await flush();
2018
+ current = row.path;
2019
+ }
2020
+ const blob = readVectorBlob(db, row.hash);
2021
+ if (blob) {
2022
+ insertVector(db, row.scope, row.id, blob, row.hash);
2023
+ tally.storedVector = true;
2024
+ continue;
2025
+ }
2026
+ queueBody(group, row);
2027
+ }
2028
+ await flush();
2029
+ }
2030
+ function scopeVectorState(db, scope) {
2031
+ const counts = db.prepare(
2032
+ `SELECT
2033
+ (SELECT COUNT(*) FROM chunks WHERE scope = ?) AS chunks,
2034
+ (SELECT COUNT(*) FROM chunks AS c
2035
+ JOIN vec_meta AS m ON m.chunk_id = c.id AND m.chunk_hash = c.chunk_hash
2036
+ WHERE c.scope = ?) AS ready`
2037
+ ).get(scope, scope);
2038
+ if (counts.chunks === 0) return "empty";
2039
+ const model = metaString(db, "embedding_model");
2040
+ const modelOk = model === null || model === EMBEDDING_MODEL;
2041
+ if (counts.ready === counts.chunks && modelOk) return "ready";
2042
+ return "partial";
2043
+ }
2044
+ function createBuiltinProvider(projectRoot, config, runtime) {
2045
+ const syncOne = async (db, rel, excluded, embedder, canVec, tally) => {
2046
+ const abs = join5(projectRoot, rel);
2047
+ const previous = db.prepare("SELECT content_hash FROM files WHERE path = ?").get(rel);
2048
+ let bytes = null;
2049
+ if (await pathExists(abs)) {
2050
+ try {
2051
+ bytes = await readFile3(abs);
2052
+ } catch {
2053
+ bytes = null;
2054
+ }
2055
+ }
2056
+ const drop = () => {
2057
+ if (!previous) return "unchanged";
2058
+ deletePath(db, rel);
2059
+ return "deleted";
2060
+ };
2061
+ if (!bytes || excluded(rel)) return drop();
2062
+ const scope = scopeForPath(rel, config);
2063
+ if (!scope) return drop();
2064
+ const hash = createHash2("sha256").update(bytes).digest("hex");
2065
+ if (previous?.content_hash === hash) return "unchanged";
2066
+ const chunks = await chunksFor(bytes, rel, scope);
2067
+ if (!chunks) return drop();
2068
+ if (!chunks.length && !previous) return "unchanged";
2069
+ const language = chunks[0]?.language ?? (scope === "code" ? "js" : "md");
2070
+ const copies = /* @__PURE__ */ new Map();
2071
+ if (canVec && embedder) {
2072
+ const hashes = new Set(chunks.map((chunk) => chunkHash(chunk.body)));
2073
+ for (const bodyHash of hashes) {
2074
+ const blob = readVectorBlob(db, bodyHash);
2075
+ if (blob) copies.set(bodyHash, blob);
2076
+ }
2077
+ }
2078
+ const inserted = db.transaction(() => writeChunks(db, rel, hash, scope, language, chunks))();
2079
+ tally.chunks += inserted.length;
2080
+ if (canVec && embedder && inserted.length) {
2081
+ await embedInserted(db, embedder, inserted, copies, tally);
2082
+ }
2083
+ return "rewritten";
2084
+ };
2085
+ return {
2086
+ id: "builtin",
2087
+ async index(changed) {
2088
+ const rels = changed?.map((entry) => assertRepoRelative(entry));
2089
+ const search = await loadSearchConfig(projectRoot);
2090
+ const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
2091
+ const db = await ensureDb(projectRoot);
2092
+ try {
2093
+ const filter = await loadExcluder(projectRoot, search.exclude);
2094
+ const canVec = await prepareVectors(db, search.embeddings && embedder !== null);
2095
+ const tally = { chunks: 0, embedded: 0, storedVector: false };
2096
+ let considered = 0;
2097
+ let reindexed = 0;
2098
+ const note = (action) => {
2099
+ considered += 1;
2100
+ if (action !== "unchanged") reindexed += 1;
2101
+ };
2102
+ const runSync = (rel) => syncOne(db, rel, filter, embedder, canVec, tally);
2103
+ if (rels) {
2104
+ for (const rel of rels) note(await runSync(rel));
2105
+ } else {
2106
+ const head = await safeHead(projectRoot);
2107
+ const cursor = metaString(db, "indexed_commit");
2108
+ const delta = head && isCommitSha(cursor) ? await readGitDelta(projectRoot, cursor.toLowerCase()) : null;
2109
+ if (delta) {
2110
+ const deletions = new Set(delta.deletions);
2111
+ const seen = /* @__PURE__ */ new Set();
2112
+ for (const rel of delta.candidates) {
2113
+ if (deletions.has(rel) || seen.has(rel)) continue;
2114
+ seen.add(rel);
2115
+ note(await runSync(rel));
2116
+ }
2117
+ for (const rel of deletions) {
2118
+ if (deletePath(db, rel)) reindexed += 1;
2119
+ }
2120
+ const stored = db.prepare("SELECT path FROM files").all();
2121
+ for (const row of stored) {
2122
+ const exists = await pathExists(join5(projectRoot, row.path));
2123
+ if (exists && !filter(row.path) && scopeForPath(row.path, config)) continue;
2124
+ deletePath(db, row.path);
2125
+ considered += 1;
2126
+ reindexed += 1;
2127
+ }
2128
+ } else {
2129
+ const desired = /* @__PURE__ */ new Set();
2130
+ for (const rel of await walkFiles(projectRoot)) {
2131
+ if (filter(rel) || !scopeForPath(rel, config)) continue;
2132
+ desired.add(rel);
2133
+ note(await runSync(rel));
2134
+ }
2135
+ const stored = db.prepare("SELECT path FROM files").all();
2136
+ const tx = db.transaction(() => {
2137
+ for (const row of stored) {
2138
+ if (desired.has(row.path)) continue;
2139
+ deletePath(db, row.path);
2140
+ reindexed += 1;
2141
+ }
2142
+ });
2143
+ tx();
2144
+ }
2145
+ if (search.embeddings && embedder && canVec) await backfill(db, embedder, tally);
2146
+ if (head) writeMetaValue(db, "indexed_commit", head);
2147
+ }
2148
+ writeCounts(db, considered, reindexed, tally.chunks, tally.embedded);
2149
+ if (tally.storedVector) writeMetaValue(db, "embedding_model", EMBEDDING_MODEL);
2150
+ return readStats(db);
2151
+ } finally {
2152
+ db.close();
2153
+ }
2154
+ },
2155
+ async search(query, options) {
2156
+ return searchProject(projectRoot, query, options, runtime);
2157
+ },
2158
+ async stats() {
2159
+ if (!await pathExists(indexDbPath(projectRoot))) return { ...EMPTY_STATS };
2160
+ const db = openDb(projectRoot);
2161
+ try {
2162
+ return readStats(db);
2163
+ } finally {
2164
+ db.close();
2165
+ }
2166
+ }
2167
+ };
2168
+ }
2169
+ async function keywordHits(projectRoot, query, scope, k) {
2170
+ if (!await pathExists(indexDbPath(projectRoot))) return [];
2171
+ const db = openDb(projectRoot);
2172
+ try {
2173
+ return queryRanked(db, query, scope, k);
2174
+ } finally {
2175
+ db.close();
2176
+ }
2177
+ }
2178
+ async function listScopeHits(projectRoot, scope) {
2179
+ if (!await pathExists(indexDbPath(projectRoot))) return [];
2180
+ const db = openDb(projectRoot);
2181
+ try {
2182
+ return queryScope(db, scope);
2183
+ } finally {
2184
+ db.close();
2185
+ }
2186
+ }
2187
+ async function rankScope(projectRoot, query, scope, k, session) {
2188
+ const keyword = async () => keywordHits(projectRoot, query, scope, k);
2189
+ if (!session.embeddings) return { hits: await keyword(), warning: false, hybrid: false };
2190
+ if (!await pathExists(indexDbPath(projectRoot))) {
2191
+ return { hits: [], warning: session.failure === "query", hybrid: false };
2192
+ }
2193
+ const db = openDb(projectRoot);
2194
+ try {
2195
+ const state2 = scopeVectorState(db, scope);
2196
+ const warn = session.failure === "query" || state2 !== "empty" && (session.failure === "load" || state2 === "partial" || !session.queryVec);
2197
+ if (session.failure !== "none" || state2 !== "ready" || !session.queryVec) {
2198
+ return { hits: queryRanked(db, query, scope, k), warning: warn, hybrid: false };
2199
+ }
2200
+ const loaded = await ensureVecLoaded(db);
2201
+ if (!loaded) return { hits: queryRanked(db, query, scope, k), warning: true, hybrid: false };
2202
+ try {
2203
+ const keywordRows = queryRanked(db, query, scope, k);
2204
+ const vectorRows = queryVectors(db, scope, session.queryVec, k).map(
2205
+ (row) => mapHit({ ...row, score: row.distance })
2206
+ );
2207
+ return { hits: fuseHits(keywordRows, vectorRows), warning: false, hybrid: true };
2208
+ } catch {
2209
+ return { hits: queryRanked(db, query, scope, k), warning: true, hybrid: false };
2210
+ }
2211
+ } finally {
2212
+ db.close();
2213
+ }
2214
+ }
2215
+ async function searchProject(projectRoot, query, options, runtime) {
2216
+ const search = await loadSearchConfig(projectRoot);
2217
+ const config = await loadConfig(projectRoot);
2218
+ const activeChange = await resolveActiveDirectory(projectRoot, config);
2219
+ const request = {
2220
+ projectRoot,
2221
+ query,
2222
+ channel: "search",
2223
+ scope: options.scope,
2224
+ k: options.k,
2225
+ budget: options.budget,
2226
+ embeddings: search.embeddings,
2227
+ activeChange,
2228
+ scopes: [options.scope]
2229
+ };
2230
+ try {
2231
+ const cached = readPackedCache(request);
2232
+ if (cached) {
2233
+ return {
2234
+ provider: "builtin",
2235
+ warning: cached.warning,
2236
+ query,
2237
+ scope: options.scope,
2238
+ budget: options.budget,
2239
+ usedTokens: countTokens(cached.text),
2240
+ hits: cached.hits,
2241
+ text: cached.text
2242
+ };
2243
+ }
2244
+ } catch {
2245
+ }
2246
+ const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
2247
+ const embedded = await embedSearchQuery(embedder, search.embeddings, query);
2248
+ const ranked = await rankScope(projectRoot, query, options.scope, options.k, {
2249
+ embeddings: search.embeddings,
2250
+ queryVec: embedded.queryVec,
2251
+ failure: embedded.failure
2252
+ });
2253
+ const preamble = options.preamble ?? searchPreamble(query, options.scope, options.budget, ranked.hybrid);
2254
+ let hits = ranked.hits;
2255
+ try {
2256
+ hits = attachSummaries(projectRoot, ranked.hits, activeChange);
2257
+ } catch {
2258
+ hits = ranked.hits;
2259
+ }
2260
+ const packed = packHits({ preamble, hits, budget: options.budget });
2261
+ const mode = ranked.hits.some((hit) => hit.rankedBy === "rrf") ? "rrf" : "bm25";
2262
+ try {
2263
+ writePackedCache(request, packed, ranked.hits, mode, ranked.hybrid ? options.scope : "");
2264
+ } catch {
2265
+ }
2266
+ return {
2267
+ provider: "builtin",
2268
+ warning: ranked.warning ? EMBEDDINGS_UNAVAILABLE : void 0,
2269
+ query,
2270
+ scope: options.scope,
2271
+ budget: options.budget,
2272
+ usedTokens: packed.usedTokens,
2273
+ hits: packed.hits,
2274
+ text: packed.text
2275
+ };
2276
+ }
2277
+
2278
+ // src/search/registry.ts
2279
+ var extra = /* @__PURE__ */ new Map();
2280
+ function registerSearchProvider(provider) {
2281
+ extra.set(provider.id, provider);
2282
+ }
2283
+ function clearRegisteredSearchProviders() {
2284
+ extra.clear();
2285
+ }
2286
+ function fallbackWarning(name) {
2287
+ return `Search provider "${name}" is unavailable. Using built-in keyword search.`;
2288
+ }
2289
+ function joinWarning(left, right) {
2290
+ if (left && right) return `${left}
2291
+ ${right}`;
2292
+ return left || right;
2293
+ }
2294
+ function resolveSearchProvider(projectRoot, config, search, runtime) {
2295
+ const builtin = createBuiltinProvider(projectRoot, config, runtime);
2296
+ const name = search.provider || "builtin";
2297
+ if (name === "builtin") return builtin;
2298
+ const registered = extra.get(name);
2299
+ let active = registered ?? builtin;
2300
+ let warning = registered ? void 0 : fallbackWarning(name);
2301
+ const mark = () => {
2302
+ warning = fallbackWarning(name);
2303
+ active = builtin;
2304
+ };
2305
+ return {
2306
+ id: active.id,
2307
+ async index(changed) {
2308
+ try {
2309
+ return await active.index(changed);
2310
+ } catch (err) {
2311
+ if (active.id === "builtin") throw err;
2312
+ mark();
2313
+ return builtin.index(changed);
2314
+ }
2315
+ },
2316
+ async search(query, options) {
2317
+ try {
2318
+ const result = await active.search(query, options);
2319
+ result.warning = joinWarning(warning, result.warning);
2320
+ result.provider = "builtin";
2321
+ return result;
2322
+ } catch (err) {
2323
+ if (active.id === "builtin") throw err;
2324
+ mark();
2325
+ await builtin.index();
2326
+ const result = await builtin.search(query, options);
2327
+ result.warning = joinWarning(warning, result.warning);
2328
+ result.provider = "builtin";
2329
+ return result;
2330
+ }
2331
+ },
2332
+ stats() {
2333
+ return active.stats();
2334
+ }
2335
+ };
2336
+ }
2337
+ function safeMessage(err, projectRoot) {
2338
+ const raw = err instanceof Error ? err.message : "Search failed.";
2339
+ if (!raw) return "Search failed.";
2340
+ if (!projectRoot || !raw.includes(projectRoot)) return raw;
2341
+ return raw.split(projectRoot).join("") || "Search failed.";
2342
+ }
2343
+ async function buildRetrievedInner(projectRoot, config, title, stage, budget, runtime, activeChange = "") {
2344
+ let search;
2345
+ try {
2346
+ search = await loadSearchConfig(projectRoot);
2347
+ } catch (err) {
2348
+ return `Retrieved context skipped: ${safeMessage(err, projectRoot)}.`;
2349
+ }
2350
+ const limit = budget ?? budgetForStage(search, stage);
2351
+ const provider = resolveSearchProvider(projectRoot, config, search, runtime);
2352
+ try {
2353
+ await provider.index();
2354
+ } catch (err) {
2355
+ const message = provider.id === "builtin" ? safeMessage(err, projectRoot) : fallbackWarning(search.provider);
2356
+ return `Retrieved context skipped: ${message}.`;
2357
+ }
2358
+ const request = {
2359
+ projectRoot,
2360
+ query: title,
2361
+ channel: "handoff",
2362
+ scope: "merged",
2363
+ k: search.k,
2364
+ budget: limit,
2365
+ embeddings: search.embeddings,
2366
+ activeChange,
2367
+ scopes: ["specs", "memory", "code"]
2368
+ };
2369
+ try {
2370
+ const cached = readPackedCache(request);
2371
+ if (cached) return cached.text;
2372
+ } catch {
2373
+ }
2374
+ try {
2375
+ const embedder = await resolveEmbedder(projectRoot, search.embeddings, runtime);
2376
+ const embedded = await embedSearchQuery(embedder, search.embeddings, title);
2377
+ const hits = [];
2378
+ const hybridScopes = [];
2379
+ for (const scope of ["specs", "memory", "code"]) {
2380
+ const ranked = await rankScope(projectRoot, title, scope, search.k, {
2381
+ embeddings: search.embeddings,
2382
+ queryVec: embedded.queryVec,
2383
+ failure: embedded.failure
2384
+ });
2385
+ if (ranked.hybrid) hybridScopes.push(scope);
2386
+ hits.push(...ranked.hits);
2387
+ }
2388
+ const merged = mergeRankedHits(hits);
2389
+ let prepared = merged;
2390
+ try {
2391
+ prepared = attachSummaries(projectRoot, merged, activeChange);
2392
+ } catch {
2393
+ prepared = merged;
2394
+ }
2395
+ const packed = packHits({ preamble: "", hits: prepared, budget: limit });
2396
+ const mode = merged.some((hit) => hit.rankedBy === "rrf") ? "rrf" : "bm25";
2397
+ try {
2398
+ writePackedCache(request, packed, merged, mode, hybridScopes.join(","));
2399
+ } catch {
2400
+ }
2401
+ return packed.text;
2402
+ } catch (err) {
2403
+ const message = search.provider === "builtin" ? safeMessage(err, projectRoot) : fallbackWarning(search.provider);
2404
+ return `Retrieved context skipped: ${message}.`;
2405
+ }
2406
+ }
2407
+ async function executeSearch(projectRoot, config, query, scope, budget) {
2408
+ const search = await loadSearchConfig(projectRoot);
2409
+ const provider = resolveSearchProvider(projectRoot, config, search);
2410
+ await provider.index();
2411
+ const options = {
2412
+ scope,
2413
+ k: search.k,
2414
+ budget: budget ?? search.budgets.default
2415
+ };
2416
+ return provider.search(query, options);
2417
+ }
2418
+
2419
+ // src/agent-handoff.ts
2420
+ function formatStatus(ctx) {
2421
+ const lines = [];
2422
+ lines.push(`Change: ${ctx.meta.title}`);
2423
+ lines.push(`ID: ${ctx.id}`);
2424
+ lines.push(`Status: ${ctx.meta.status}`);
2425
+ lines.push(`Workflow: ${ctx.meta.workflow}`);
2426
+ lines.push(`Path: ${ctx.displayPath}`);
2427
+ if (ctx.meta.epic) lines.push(`Epic: ${ctx.meta.epic}`);
2428
+ if (ctx.meta.story) {
2429
+ const depends = ctx.meta.depends?.length ? ctx.meta.depends.join(", ") : "none";
2430
+ lines.push(`Story: ${ctx.meta.story}`);
2431
+ lines.push(`Depends: ${depends}`);
2432
+ }
2433
+ lines.push("");
2434
+ lines.push("Stages:");
2435
+ for (const stage of ctx.stages) {
2436
+ const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
2437
+ let mark = " ";
2438
+ if (skipped) mark = "\xB7";
2439
+ else if (stage.id === ctx.meta.stage) mark = "\u25CF";
2440
+ else {
2441
+ const order = ctx.active.map((s) => s.id);
2442
+ const cur = order.indexOf(ctx.meta.stage);
2443
+ const idx = order.indexOf(stage.id);
2444
+ if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
2445
+ }
2446
+ const gate = ctx.meta.gates?.[stage.id];
2447
+ const gateInfo = gate ? ` [${gate.status}]` : "";
2448
+ const skipInfo = skipped ? " (skipped)" : "";
2449
+ lines.push(
2450
+ ` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`
2451
+ );
2452
+ }
2453
+ if (ctx.meta.overrides?.skip_stages?.length) {
2454
+ lines.push("");
2455
+ lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
2456
+ }
2457
+ return lines.join("\n");
2458
+ }
2459
+ async function buildAgentPrompt(ctx, config, projectRoot, budget, runtime) {
2460
+ const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
2461
+ const parts = [];
2462
+ parts.push(`# SDD Agent Handoff`);
2463
+ parts.push("");
2464
+ parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
2465
+ parts.push("");
2466
+ parts.push(`## Change`);
2467
+ parts.push(`- Title: ${ctx.meta.title}`);
2468
+ parts.push(`- ID: ${ctx.id}`);
2469
+ if (ctx.meta.epic && ctx.meta.story) {
2470
+ const depends = ctx.meta.depends?.length ? ctx.meta.depends.join(", ") : "none";
2471
+ parts.push(`Epic ${ctx.meta.epic} story ${ctx.meta.story}. Depends: ${depends}.`);
2472
+ }
2473
+ parts.push(`- Workflow: ${ctx.meta.workflow}`);
2474
+ parts.push(`- Change path: ${ctx.displayPath}`);
2475
+ parts.push("");
2476
+ const paths = guidancePaths(config);
2477
+ parts.push(`## Constraints`);
2478
+ parts.push(`- Follow artifacts in the change directory as source of truth.`);
2479
+ parts.push(`- Honor \`${paths.constitution}\` when present (non-negotiables).`);
2480
+ parts.push(`- Do not skip ARB/decision constraints if present.`);
2481
+ parts.push(`- Local development only \u2014 verify on this machine.`);
2482
+ parts.push(`- Prefer small, reviewable commits.`);
2483
+ parts.push("");
2484
+ parts.push(`## Artifacts to read`);
2485
+ parts.push(`- Documentation map (if present): \`${paths.memoryIndex}\``);
2486
+ parts.push(`- Memory: \`${paths.memory}/\``);
2487
+ for (const s of ctx.stages) {
2488
+ for (const a of s.artifacts) {
2489
+ parts.push(`- ${ctx.displayPath}/${a.path}`);
2490
+ }
2491
+ }
2492
+ parts.push("");
2493
+ parts.push(`## Stage`);
2494
+ parts.push(`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`);
2495
+ parts.push("");
2496
+ if (stage?.summary) {
2497
+ parts.push(`## Stage goal`);
2498
+ parts.push(stage.summary);
2499
+ parts.push("");
2500
+ }
2501
+ if (stage?.agent_context?.instructions) {
2502
+ parts.push(`## Instructions`);
2503
+ parts.push(stage.agent_context.instructions);
2504
+ parts.push("");
2505
+ }
2506
+ parts.push(`## Current stage artifacts`);
2507
+ if (stage) {
2508
+ for (const a of stage.artifacts) {
2509
+ parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
2510
+ }
2511
+ }
2512
+ parts.push("");
2513
+ const stageId = ctx.meta.stage;
2514
+ if (stageId === "implement" || stageId === "local_verify") {
2515
+ parts.push(`## Code context (product code)`);
2516
+ parts.push("");
2517
+ parts.push(`Process context above is not enough for product code structure.`);
2518
+ parts.push(`When you need symbols, callers, or package-boundary context:`);
2519
+ parts.push(
2520
+ `1. Run: \`sdd context\` (optional: \`--path \u2026\`, \`--symbol \u2026\`, \`--out change\`)`
2521
+ );
2522
+ parts.push(
2523
+ `2. If present, read: \`${changesPathRel(config)}/${ctx.id}/code-context.md\` (regenerate if stale)`
2524
+ );
2525
+ parts.push(
2526
+ `3. Org libraries / remote AST: \`.sdd/mcp.yaml\` sources via \`sdd mcp sources\` / \`sdd mcp fetch\``
2527
+ );
2528
+ parts.push(`Do not dump unbounded source into the prompt; prefer sliced / MCP output.`);
2529
+ parts.push("");
2530
+ }
2531
+ try {
2532
+ const { loadMcpConfig } = await import("./sources-3TXUPU6V.js");
2533
+ const { gatherMcpContextForChange, formatMcpContextForHandoff } = await import("./gather-ZNV2NAUN.js");
2534
+ const mcpCfg = await loadMcpConfig(projectRoot);
2535
+ if (mcpCfg.auto_fetch_on_handoff && mcpCfg.sources.length) {
2536
+ const fetched = await gatherMcpContextForChange(projectRoot, ctx);
2537
+ const block = formatMcpContextForHandoff(fetched.filter((r) => r.ok || r.error));
2538
+ if (block) {
2539
+ parts.push(block);
2540
+ }
2541
+ } else if (mcpCfg.sources.length === 0) {
2542
+ parts.push(`## External MCP sources`);
2543
+ parts.push("");
2544
+ parts.push(`No sources in \`.sdd/mcp.yaml\` yet. To attach org libraries / AST engines:`);
2545
+ parts.push(`\`sdd mcp sources add --help\` \xB7 docs: Configure MCP sources`);
2546
+ parts.push("");
2547
+ }
2548
+ } catch (err) {
2549
+ const msg = err instanceof Error ? err.message : String(err);
2550
+ parts.push(`## External MCP sources`);
2551
+ parts.push("");
2552
+ parts.push(`_MCP auto-fetch skipped (non-fatal): ${msg}_`);
2553
+ parts.push("");
2554
+ }
2555
+ const inner = await buildRetrievedInner(
2556
+ projectRoot,
2557
+ config,
2558
+ ctx.meta.title,
2559
+ ctx.meta.stage,
2560
+ budget,
2561
+ runtime,
2562
+ ctx.displayPath
2563
+ );
2564
+ parts.push("## Retrieved context");
2565
+ parts.push("");
2566
+ parts.push("<!-- retrieved-context:start -->");
2567
+ parts.push(inner);
2568
+ parts.push("<!-- retrieved-context:end -->");
2569
+ parts.push("");
2570
+ parts.push(
2571
+ `When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
2572
+ );
2573
+ return parts.join("\n");
2574
+ }
2575
+
2576
+ export {
2577
+ isInitialized,
2578
+ loadConfig,
2579
+ saveConfig,
2580
+ defaultConfig,
2581
+ mergeConfigDefaults,
2582
+ getHeadSha,
2583
+ countTokens,
2584
+ ftsTokens,
2585
+ SearchConfigError,
2586
+ budgetForStage,
2587
+ loadSearchConfig,
2588
+ searchPreamble,
2589
+ renderBlock,
2590
+ packHits,
2591
+ isSecretPath,
2592
+ isIgnoredPath,
2593
+ classifyPath,
2594
+ isHardDenied,
2595
+ isPrunedDir,
2596
+ loadExcluder,
2597
+ chunkMarkdown,
2598
+ SUMMARY_TOKEN_CAP,
2599
+ extractiveSummary,
2600
+ EMBEDDING_MODEL,
2601
+ EMBEDDINGS_UNAVAILABLE,
2602
+ fastembedImportCount,
2603
+ createFastembedEmbedder,
2604
+ resolveEmbedder,
2605
+ embedSearchQuery,
2606
+ resolveActiveDirectory,
2607
+ readPackedCache,
2608
+ writePackedCache,
2609
+ chunkCode,
2610
+ RRF_K,
2611
+ rrf,
2612
+ fuseByRank,
2613
+ fuseHits,
2614
+ mergeRankedHits,
2615
+ scopeForPath,
2616
+ createBuiltinProvider,
2617
+ keywordHits,
2618
+ listScopeHits,
2619
+ rankScope,
2620
+ registerSearchProvider,
2621
+ clearRegisteredSearchProviders,
2622
+ fallbackWarning,
2623
+ resolveSearchProvider,
2624
+ buildRetrievedInner,
2625
+ executeSearch,
2626
+ formatStatus,
2627
+ buildAgentPrompt
2628
+ };