@kb-labs/mind-core 2.94.0 → 2.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,579 +1,1404 @@
1
- export * from '@kb-labs/mind-types';
2
- import { createHash } from 'crypto';
3
- import path, { dirname } from 'path';
4
- import { existsSync, promises } from 'fs';
1
+ import { AgentResponseSchema, resolveScope, effectiveIndexConfig, MIND_NAMESPACE_PREFIX } from '@kb-labs/mind-contracts';
5
2
  import { readFile } from 'fs/promises';
3
+ import path, { join } from 'path';
4
+ import globby from 'globby';
5
+ import { statSync, openSync, readSync, closeSync } from 'fs';
6
6
 
7
- // src/index.ts
7
+ // src/mind.ts
8
8
 
9
- // src/error/mind-error.ts
10
- var MindError = class extends Error {
11
- constructor(code, message, hint, meta) {
12
- super(message);
13
- this.code = code;
14
- this.hint = hint;
15
- this.meta = meta;
16
- this.name = "MindError";
9
+ // src/types.ts
10
+ function chunkId(path2, startLine, endLine) {
11
+ return `${path2}#${startLine}-${endLine}`;
12
+ }
13
+ function hashContent(text) {
14
+ let h = 2166136261;
15
+ for (let i = 0; i < text.length; i++) {
16
+ h ^= text.charCodeAt(i);
17
+ h = Math.imul(h, 16777619);
17
18
  }
18
- code;
19
- hint;
20
- meta;
21
- };
22
- function getExitCode(err) {
23
- if (err.code === "MIND_FORBIDDEN") {
24
- return 3;
19
+ return (h >>> 0).toString(16);
20
+ }
21
+ function kindFromPath(path2) {
22
+ const lower = path2.toLowerCase();
23
+ if (lower.includes("/adr/") || /adr-\d+/.test(lower)) {
24
+ return "adr";
25
25
  }
26
- if (err.code === "MIND_NO_GIT") {
27
- return 2;
26
+ if (lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".txt")) {
27
+ return "doc";
28
28
  }
29
- if (err.code === "MIND_FS_TIMEOUT") {
30
- return 2;
29
+ if (lower.endsWith(".json") || lower.endsWith(".yaml") || lower.endsWith(".yml") || lower.endsWith(".toml")) {
30
+ return "config";
31
31
  }
32
- if (err.code === "MIND_PARSE_ERROR") {
33
- return 1;
32
+ return "code";
33
+ }
34
+ var IGNORE = [
35
+ "**/node_modules/**",
36
+ "**/dist/**",
37
+ "**/build/**",
38
+ "**/out/**",
39
+ "**/coverage/**",
40
+ "**/.git/**",
41
+ "**/.kb/**",
42
+ "**/bin/**",
43
+ "**/obj/**",
44
+ "**/.next/**",
45
+ "**/.nuxt/**",
46
+ "**/vendor/**",
47
+ "**/__pycache__/**"
48
+ ];
49
+ var DENY_EXT = /* @__PURE__ */ new Set([
50
+ "png",
51
+ "jpg",
52
+ "jpeg",
53
+ "gif",
54
+ "svg",
55
+ "ico",
56
+ "webp",
57
+ "bmp",
58
+ "tif",
59
+ "tiff",
60
+ "avif",
61
+ "mp4",
62
+ "mov",
63
+ "avi",
64
+ "webm",
65
+ "mp3",
66
+ "wav",
67
+ "ogg",
68
+ "flac",
69
+ "pdf",
70
+ "woff",
71
+ "woff2",
72
+ "ttf",
73
+ "eot",
74
+ "otf",
75
+ "zip",
76
+ "gz",
77
+ "tgz",
78
+ "bz2",
79
+ "xz",
80
+ "tar",
81
+ "rar",
82
+ "7z",
83
+ "exe",
84
+ "dll",
85
+ "so",
86
+ "dylib",
87
+ "bin",
88
+ "wasm",
89
+ "class",
90
+ "node",
91
+ "pdb",
92
+ "xlsx",
93
+ "xls",
94
+ "docx",
95
+ "doc",
96
+ "pptx",
97
+ "ppt",
98
+ "parquet",
99
+ "db",
100
+ "sqlite",
101
+ "map",
102
+ "min.js",
103
+ "min.css",
104
+ "snap",
105
+ // Bulk data / logs / dumps — text, but no semantic value and they bloat the
106
+ // index + embedding cost (the old extension allowlist excluded these by omission).
107
+ "csv",
108
+ "tsv",
109
+ "ndjson",
110
+ "jsonl",
111
+ "log",
112
+ "sql",
113
+ "dump",
114
+ "out"
115
+ ]);
116
+ var DENY_FILE = /* @__PURE__ */ new Set([
117
+ "package-lock.json",
118
+ "pnpm-lock.yaml",
119
+ "yarn.lock",
120
+ "go.sum",
121
+ "Cargo.lock",
122
+ "composer.lock"
123
+ ]);
124
+ var MAX_BYTES = 512 * 1024;
125
+ function extOf(file) {
126
+ const name = file.slice(file.lastIndexOf("/") + 1).toLowerCase();
127
+ if (name.endsWith(".min.js")) {
128
+ return "min.js";
34
129
  }
35
- if (err.code === "MIND_PACK_BUDGET_EXCEEDED") {
36
- return 1;
130
+ if (name.endsWith(".min.css")) {
131
+ return "min.css";
37
132
  }
38
- if (err.code.startsWith("MIND_")) {
39
- return 1;
133
+ const dot = name.lastIndexOf(".");
134
+ return dot === -1 ? "" : name.slice(dot + 1);
135
+ }
136
+ function isBinary(abs) {
137
+ let fd;
138
+ try {
139
+ fd = openSync(abs, "r");
140
+ const buf = Buffer.alloc(4096);
141
+ const n = readSync(fd, buf, 0, buf.length, 0);
142
+ for (let i = 0; i < n; i++) {
143
+ if (buf[i] === 0) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ } catch {
149
+ return true;
150
+ } finally {
151
+ if (fd !== void 0) {
152
+ closeSync(fd);
153
+ }
40
154
  }
41
- return 1;
42
- }
43
- var ERROR_HINTS = {
44
- MIND_NO_GIT: "Initialize git repository or run from a git repository",
45
- MIND_FS_TIMEOUT: "File system operation timed out - try increasing time budget",
46
- MIND_PARSE_ERROR: "Failed to parse file - check syntax and try again",
47
- MIND_PACK_BUDGET_EXCEEDED: "Context pack exceeds token budget - reduce content or increase budget",
48
- MIND_FORBIDDEN: "Operation not permitted - check file permissions",
49
- MIND_TIME_BUDGET: "Time budget exceeded - operation completed partially",
50
- MIND_BAD_FLAGS: "Invalid command line flags - check values and try again",
51
- MIND_INVALID_FLAG: "Invalid flag value - check format and try again",
52
- MIND_BUNDLE_TIMEOUT: "Bundle operation timed out - skipped bundle information",
53
- MIND_FEED_ERROR: "Mind feed operation failed - check logs for details",
54
- MIND_INIT_ERROR: "Mind initialization failed - check permissions and try again",
55
- MIND_UPDATE_ERROR: "Mind update operation failed - check logs for details",
56
- MIND_PACK_ERROR: "Mind pack operation failed - check logs for details",
57
- MIND_GIT_ERROR: "Git operation failed - check git repository status",
58
- MIND_INDEX_NOT_FOUND: 'Mind indexes not found - run "kb mind init" first',
59
- MIND_INVALID_PATH: "Invalid file or directory path - check path exists and is accessible",
60
- MIND_DEPENDENCY_ERROR: "Dependency resolution failed - check package configuration",
61
- MIND_BUILD_ERROR: "Build operation failed - check configuration and try again"
62
- };
63
- function createMindError(code, message, meta) {
64
- return new MindError(code, message, ERROR_HINTS[code], meta);
65
- }
66
- function wrapError(error, code = "MIND_FEED_ERROR") {
67
- if (error instanceof MindError) {
68
- return error;
69
- }
70
- const message = error instanceof Error ? error.message : String(error);
71
- return createMindError(code, message, { originalError: error });
72
- }
73
- function isMindError(error) {
74
- return error instanceof MindError;
75
- }
76
-
77
- // src/utils/token.ts
78
- var DefaultTokenEstimator = class {
79
- charsPerToken = 4;
80
- codeBonus = 0.1;
81
- // 10% bonus for code-like content
82
- punctuationWeight = 0.8;
83
- estimate(text) {
84
- if (!text || text.length === 0) {
85
- return 0;
86
- }
87
- const words = text.match(/\b\w+\b/g) || [];
88
- const punctuation = text.match(/[^\w\s]/g) || [];
89
- const whitespace = text.match(/\s/g) || [];
90
- let tokens = words.length;
91
- tokens += punctuation.length * this.punctuationWeight;
92
- tokens += whitespace.length * 0.3;
93
- const codeIndicators = text.match(/[{}();=<>]/g) || [];
94
- if (codeIndicators.length > words.length * 0.1) {
95
- tokens *= 1 + this.codeBonus;
96
- }
97
- const charBasedEstimate = text.length / this.charsPerToken;
98
- return Math.ceil(Math.max(tokens, charBasedEstimate));
99
- }
100
- truncate(text, maxTokens, mode) {
101
- if (this.estimate(text) <= maxTokens) {
102
- return text;
103
- }
104
- const lines = text.split("\n");
105
- const estimatedTokens = this.estimate(text);
106
- const ratio = maxTokens / estimatedTokens;
107
- const targetLines = Math.max(1, Math.floor(lines.length * ratio));
108
- if (targetLines >= lines.length) {
109
- return text;
110
- }
111
- switch (mode) {
112
- case "start":
113
- return lines.slice(0, targetLines).join("\n");
114
- case "end":
115
- return lines.slice(-targetLines).join("\n");
116
- case "middle":
117
- default: {
118
- const startLines = Math.max(1, Math.floor(targetLines / 2));
119
- const endLines = Math.max(1, targetLines - startLines);
120
- const start = lines.slice(0, startLines);
121
- const end = lines.slice(-endLines);
122
- return [...start, "...", ...end].join("\n");
155
+ }
156
+ function trimTrailingSlashes(s) {
157
+ let end = s.length;
158
+ while (end > 0 && s.charCodeAt(end - 1) === 47) {
159
+ end--;
160
+ }
161
+ return s.slice(0, end);
162
+ }
163
+ async function discover(cwd, scope) {
164
+ const include = scope?.include && scope.include.length > 0 ? scope.include : ["."];
165
+ const exclude = scope?.exclude ?? [];
166
+ const patterns = include.map((p) => {
167
+ const base = p && p !== "." ? trimTrailingSlashes(p) : "";
168
+ return base ? `${base}/**/*` : "**/*";
169
+ });
170
+ const candidates = await globby(patterns, {
171
+ cwd,
172
+ ignore: [...IGNORE, ...exclude],
173
+ dot: false,
174
+ followSymbolicLinks: false,
175
+ gitignore: false,
176
+ onlyFiles: true
177
+ });
178
+ return candidates.filter((rel) => {
179
+ if (DENY_FILE.has(rel.slice(rel.lastIndexOf("/") + 1))) {
180
+ return false;
181
+ }
182
+ if (DENY_EXT.has(extOf(rel))) {
183
+ return false;
184
+ }
185
+ const abs = path.join(cwd, rel);
186
+ try {
187
+ const st = statSync(abs);
188
+ if (st.size === 0 || st.size > MAX_BYTES) {
189
+ return false;
123
190
  }
191
+ } catch {
192
+ return false;
124
193
  }
194
+ return !isBinary(abs);
195
+ });
196
+ }
197
+
198
+ // src/ingest/chunk.ts
199
+ function approxTokens(line) {
200
+ const t = line.trim();
201
+ if (t === "") {
202
+ return 0;
125
203
  }
126
- };
127
- var defaultTokenEstimator = new DefaultTokenEstimator();
128
- function estimateTokens(text) {
129
- return defaultTokenEstimator.estimate(text);
130
- }
131
- function truncateToTokens(text, maxTokens, mode = "middle") {
132
- return defaultTokenEstimator.truncate(text, maxTokens, mode);
133
- }
134
- function sha256(content) {
135
- return createHash("sha256").update(content, "utf8").digest("hex");
136
- }
137
- function sha256Buffer(buffer) {
138
- return createHash("sha256").update(buffer).digest("hex");
139
- }
140
- async function sha256File(filePath) {
141
- const { readFile: readFile2 } = await import('fs/promises');
142
- const content = await readFile2(filePath);
143
- return sha256Buffer(content);
144
- }
145
- function toPosix(filePath) {
146
- return filePath.replace(/\\/g, "/");
147
- }
148
- function fromPosix(posixPath) {
149
- return posixPath.split("/").join(path.sep);
150
- }
151
- async function findWorkspaceRoot(cwd) {
152
- let current = path.resolve(cwd);
153
- const root = path.parse(current).root;
154
- while (current !== root) {
155
- if (existsSync(path.join(current, ".git"))) {
156
- return toPosix(current);
157
- }
158
- const packageJsonPath = path.join(current, "package.json");
159
- if (existsSync(packageJsonPath)) {
160
- try {
161
- const packageJsonContent = await readFile(packageJsonPath, "utf8");
162
- const packageJson = JSON.parse(packageJsonContent);
163
- if (packageJson.workspaces || packageJson.pnpm?.workspace) {
164
- return toPosix(current);
165
- }
166
- } catch {
204
+ return t.split(/\s+/).length;
205
+ }
206
+ function slidingWindowChunks(path2, content, opts) {
207
+ const lines = content.split("\n");
208
+ const kind = kindFromPath(path2);
209
+ const chunks = [];
210
+ const maxTokens = Math.max(1, opts.maxTokens);
211
+ const overlapTokens = Math.max(0, Math.min(opts.overlapTokens, maxTokens - 1));
212
+ let startIdx = 0;
213
+ while (startIdx < lines.length) {
214
+ let tokens = 0;
215
+ let endIdx = startIdx;
216
+ while (endIdx < lines.length && tokens < maxTokens) {
217
+ tokens += approxTokens(lines[endIdx] ?? "");
218
+ endIdx++;
219
+ }
220
+ const startLine = startIdx + 1;
221
+ const endLine = endIdx;
222
+ const text = lines.slice(startIdx, endIdx).join("\n");
223
+ if (text.trim() !== "") {
224
+ chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text, kind });
225
+ }
226
+ if (endIdx >= lines.length) {
227
+ break;
228
+ }
229
+ let overlap = 0;
230
+ let stepBack = 0;
231
+ let i = endIdx - 1;
232
+ while (i > startIdx && overlap < overlapTokens) {
233
+ overlap += approxTokens(lines[i] ?? "");
234
+ stepBack++;
235
+ i--;
236
+ }
237
+ startIdx = Math.max(startIdx + 1, endIdx - stepBack);
238
+ }
239
+ return chunks;
240
+ }
241
+
242
+ // src/ingest/structural.ts
243
+ var DECL_KEYWORDS = /* @__PURE__ */ new Set([
244
+ "function",
245
+ "class",
246
+ "interface",
247
+ "type",
248
+ "enum",
249
+ "const",
250
+ "let",
251
+ "var",
252
+ "namespace",
253
+ "module",
254
+ "def",
255
+ "func",
256
+ "fn",
257
+ "impl",
258
+ "struct",
259
+ "trait",
260
+ "public",
261
+ "private"
262
+ ]);
263
+ var MODIFIERS = /* @__PURE__ */ new Set([
264
+ "export",
265
+ "default",
266
+ "declare",
267
+ "public",
268
+ "private",
269
+ "protected",
270
+ "static",
271
+ "abstract",
272
+ "async"
273
+ ]);
274
+ function isBoundaryLine(line) {
275
+ if (line.length === 0 || line.charCodeAt(0) === 32 || line.charCodeAt(0) === 9) {
276
+ return false;
277
+ }
278
+ for (const word of line.split(/\s+/)) {
279
+ if (DECL_KEYWORDS.has(word)) {
280
+ return true;
281
+ }
282
+ if (!MODIFIERS.has(word)) {
283
+ return false;
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+ function approxTokens2(text) {
289
+ const t = text.trim();
290
+ return t === "" ? 0 : t.split(/\s+/).length;
291
+ }
292
+ function structuralChunks(path2, content, opts) {
293
+ const lines = content.split("\n");
294
+ const kind = kindFromPath(path2);
295
+ const boundaries = [];
296
+ lines.forEach((line, i) => {
297
+ if (isBoundaryLine(line)) {
298
+ boundaries.push(i);
299
+ }
300
+ });
301
+ if (boundaries.length === 0) {
302
+ return slidingWindowChunks(path2, content, opts);
303
+ }
304
+ const starts = [.../* @__PURE__ */ new Set([0, ...boundaries])].sort((a, b) => a - b);
305
+ const chunks = [];
306
+ for (let b = 0; b < starts.length; b++) {
307
+ const from = starts[b];
308
+ const to = b + 1 < starts.length ? starts[b + 1] : lines.length;
309
+ const blockLines = lines.slice(from, to);
310
+ const blockText = blockLines.join("\n");
311
+ if (blockText.trim() === "") {
312
+ continue;
313
+ }
314
+ if (approxTokens2(blockText) > opts.maxTokens) {
315
+ for (const sub of slidingWindowChunks(path2, blockText, opts)) {
316
+ const startLine = from + sub.startLine;
317
+ const endLine = from + sub.endLine;
318
+ chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text: sub.text, kind });
167
319
  }
320
+ } else {
321
+ const startLine = from + 1;
322
+ const endLine = to;
323
+ chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text: blockText, kind });
168
324
  }
169
- if (existsSync(path.join(current, "pnpm-workspace.yaml"))) {
170
- return toPosix(current);
171
- }
172
- current = path.dirname(current);
173
- }
174
- return toPosix(cwd);
175
- }
176
- function makeRelativeToRoot(absolutePath, root) {
177
- const relative = path.relative(root, absolutePath);
178
- return toPosix(relative);
179
- }
180
- function shouldIgnorePath(filePath) {
181
- const posixPath = toPosix(filePath);
182
- const ignorePatterns = [
183
- "node_modules/**",
184
- ".git/**",
185
- ".kb/**",
186
- // except .kb/mind/**
187
- "dist/**",
188
- "coverage/**",
189
- ".turbo/**",
190
- ".vite/**",
191
- "**/*.log",
192
- "**/*.tmp",
193
- "**/*.temp"
194
- ];
195
- const extensionPatterns = [".log", ".tmp", ".temp"];
196
- if (posixPath.startsWith(".kb/") && !posixPath.startsWith(".kb/mind/")) {
197
- return true;
198
325
  }
199
- if (posixPath.startsWith(".kb/mind/")) {
326
+ return chunks;
327
+ }
328
+ function chunkFile(path2, content, opts, ast) {
329
+ if (ast && kindFromPath(path2) === "code") {
330
+ return structuralChunks(path2, content, opts);
331
+ }
332
+ return slidingWindowChunks(path2, content, opts);
333
+ }
334
+
335
+ // src/ingest/embed.ts
336
+ var MAX_BATCH_CHUNKS = 96;
337
+ var MAX_BATCH_TOKENS = 2e5;
338
+ var MAX_INPUT_CHARS = 12e3;
339
+ var CHARS_PER_TOKEN = 4;
340
+ var approxTokens3 = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
341
+ function toRecord(chunk, vector, text) {
342
+ const meta = {
343
+ path: chunk.path,
344
+ startLine: chunk.startLine,
345
+ endLine: chunk.endLine,
346
+ text,
347
+ kind: chunk.kind
348
+ };
349
+ return { id: chunk.id, vector, metadata: meta };
350
+ }
351
+ function planBatches(chunks) {
352
+ const items = chunks.map((c) => ({
353
+ chunk: c,
354
+ text: c.text.length > MAX_INPUT_CHARS ? c.text.slice(0, MAX_INPUT_CHARS) : c.text
355
+ }));
356
+ const batches = [];
357
+ let cur = [];
358
+ let curTokens = 0;
359
+ for (const item of items) {
360
+ const t = approxTokens3(item.text);
361
+ if (cur.length > 0 && (cur.length >= MAX_BATCH_CHUNKS || curTokens + t > MAX_BATCH_TOKENS)) {
362
+ batches.push(cur);
363
+ cur = [];
364
+ curTokens = 0;
365
+ }
366
+ cur.push(item);
367
+ curTokens += t;
368
+ }
369
+ if (cur.length > 0) {
370
+ batches.push(cur);
371
+ }
372
+ return batches;
373
+ }
374
+ async function embedChunks(chunks, embeddings, onProgress) {
375
+ if (chunks.length === 0) {
376
+ return [];
377
+ }
378
+ const records = [];
379
+ for (const batch of planBatches(chunks)) {
380
+ const vectors = await embeddings.embedBatch(batch.map((b) => b.text));
381
+ batch.forEach((b, j) => records.push(toRecord(b.chunk, vectors[j] ?? [], b.chunk.text)));
382
+ onProgress?.(records.length, chunks.length);
383
+ }
384
+ return records;
385
+ }
386
+ function manifestPath(indexId) {
387
+ return `mind/${indexId}/manifest.json`;
388
+ }
389
+ async function isStale(manifest, file, cwd) {
390
+ const entry = manifest.files[file];
391
+ if (!entry) {
200
392
  return false;
201
393
  }
202
- if (extensionPatterns.some((ext) => posixPath.endsWith(ext))) {
394
+ try {
395
+ const content = await readFile(join(cwd, file), "utf8");
396
+ return hashContent(content) !== entry.hash;
397
+ } catch {
203
398
  return true;
204
399
  }
205
- return ignorePatterns.some((pattern) => {
206
- if (pattern.endsWith("/**")) {
207
- const prefix = pattern.slice(0, -3);
208
- return posixPath.startsWith(prefix + "/") || posixPath === prefix;
400
+ }
401
+ async function staleMap(manifest, files, cwd) {
402
+ const out = /* @__PURE__ */ new Map();
403
+ await Promise.all(
404
+ [...new Set(files)].map(async (f) => {
405
+ out.set(f, await isStale(manifest, f, cwd));
406
+ })
407
+ );
408
+ return out;
409
+ }
410
+ async function staleCount(manifest, cwd) {
411
+ const flags = await staleMap(manifest, Object.keys(manifest.files), cwd);
412
+ return [...flags.values()].filter(Boolean).length;
413
+ }
414
+ async function manifestExists(storage, indexId) {
415
+ return Boolean(await storage.read(manifestPath(indexId)));
416
+ }
417
+ async function loadManifest(storage, indexId) {
418
+ const buf = await storage.read(manifestPath(indexId));
419
+ if (!buf) {
420
+ return { indexId, chunks: [], files: {}, updatedAt: null };
421
+ }
422
+ try {
423
+ const parsed = JSON.parse(buf.toString("utf8"));
424
+ return { indexId, chunks: parsed.chunks ?? [], files: parsed.files ?? {}, updatedAt: parsed.updatedAt ?? null };
425
+ } catch {
426
+ return { indexId, chunks: [], files: {}, updatedAt: null };
427
+ }
428
+ }
429
+ async function saveManifest(storage, manifest) {
430
+ const buf = Buffer.from(JSON.stringify(manifest), "utf8");
431
+ await storage.write(manifestPath(manifest.indexId), buf);
432
+ }
433
+ async function deleteManifest(storage, indexId) {
434
+ await storage.delete(manifestPath(indexId));
435
+ }
436
+
437
+ // src/ingest/ingest.ts
438
+ async function readAndHash(cwd, paths, logger) {
439
+ const contentByPath = /* @__PURE__ */ new Map();
440
+ const hashByPath = /* @__PURE__ */ new Map();
441
+ for (const path2 of paths) {
442
+ let content;
443
+ try {
444
+ content = await readFile(join(cwd, path2), "utf8");
445
+ } catch (err) {
446
+ logger?.warn("mind: skipped unreadable file", { path: path2, reason: err instanceof Error ? err.message : String(err) });
447
+ continue;
209
448
  }
210
- if (pattern.endsWith("**")) {
211
- const prefix = pattern.slice(0, -2);
212
- return posixPath.startsWith(prefix);
449
+ contentByPath.set(path2, content);
450
+ hashByPath.set(path2, hashContent(content));
451
+ }
452
+ return { contentByPath, hashByPath };
453
+ }
454
+ function classify(contentByPath, hashByPath, prevFiles) {
455
+ const toIndex = [];
456
+ const unchanged = [];
457
+ let added = 0;
458
+ let updated = 0;
459
+ for (const path2 of contentByPath.keys()) {
460
+ const prevEntry = prevFiles[path2];
461
+ if (prevEntry && prevEntry.hash === hashByPath.get(path2)) {
462
+ unchanged.push(path2);
463
+ } else {
464
+ toIndex.push(path2);
465
+ if (prevEntry) {
466
+ updated++;
467
+ } else {
468
+ added++;
469
+ }
213
470
  }
214
- if (pattern.startsWith("**/")) {
215
- const suffix = pattern.slice(3);
216
- return posixPath.endsWith(suffix);
471
+ }
472
+ const removedPaths = Object.keys(prevFiles).filter((p) => !contentByPath.has(p));
473
+ return { toIndex, unchanged, removedPaths, added, updated };
474
+ }
475
+ async function ingest(input, services) {
476
+ const { storage, embeddings, vectorStore } = services;
477
+ const prev = await loadManifest(storage, input.indexId);
478
+ const prevFiles = input.full ? {} : prev.files;
479
+ const emit = input.onProgress ?? (() => {
480
+ });
481
+ const paths = await discover(input.cwd, input.scope);
482
+ emit({ stage: "discover", files: paths.length });
483
+ const { contentByPath, hashByPath } = await readAndHash(input.cwd, paths, services.logger);
484
+ const { toIndex, unchanged, removedPaths, added, updated } = classify(contentByPath, hashByPath, prevFiles);
485
+ emit({ stage: "delta", toIndex: toIndex.length, unchanged: unchanged.length, removed: removedPaths.length });
486
+ const stalePaths = new Set(input.full ? Object.keys(prev.files) : [...toIndex, ...removedPaths]);
487
+ const staleIds = (input.full ? prev.chunks : prev.chunks.filter((c) => stalePaths.has(c.path))).map((c) => c.id);
488
+ if (staleIds.length > 0) {
489
+ await vectorStore.delete(staleIds, input.indexId);
490
+ }
491
+ const unchangedSet = new Set(unchanged);
492
+ const keptChunks = input.full ? [] : prev.chunks.filter((c) => unchangedSet.has(c.path));
493
+ const files = {};
494
+ for (const path2 of unchanged) {
495
+ files[path2] = prevFiles[path2];
496
+ }
497
+ const newChunks = [];
498
+ for (const path2 of toIndex) {
499
+ const chunks = chunkFile(path2, contentByPath.get(path2), input.chunk, input.ast);
500
+ if (chunks.length === 0) {
501
+ continue;
502
+ }
503
+ newChunks.push(...chunks);
504
+ files[path2] = { chunks: chunks.length, indexedAt: input.now, hash: hashByPath.get(path2) };
505
+ }
506
+ emit({ stage: "chunk", chunks: newChunks.length });
507
+ const records = await embedChunks(
508
+ newChunks,
509
+ embeddings,
510
+ (done, total) => emit({ stage: "embed", done, total })
511
+ );
512
+ if (records.length > 0) {
513
+ emit({ stage: "upsert", count: records.length });
514
+ await vectorStore.upsert(records, input.indexId);
515
+ }
516
+ emit({ stage: "save" });
517
+ const manifest = {
518
+ indexId: input.indexId,
519
+ chunks: [...keptChunks, ...newChunks],
520
+ files,
521
+ updatedAt: input.now
522
+ };
523
+ await saveManifest(storage, manifest);
524
+ return {
525
+ filesIndexed: Object.keys(files).length,
526
+ chunks: manifest.chunks.length,
527
+ added,
528
+ updated,
529
+ removed: removedPaths.length,
530
+ unchanged: unchanged.length
531
+ };
532
+ }
533
+ async function removePaths(manifest, paths, services, indexId) {
534
+ const removedIds = manifest.chunks.filter((c) => paths.has(c.path)).map((c) => c.id);
535
+ if (removedIds.length > 0) {
536
+ await services.vectorStore.delete(removedIds, indexId);
537
+ }
538
+ manifest.chunks = manifest.chunks.filter((c) => !paths.has(c.path));
539
+ for (const p of paths) {
540
+ delete manifest.files[p];
541
+ }
542
+ return removedIds.length;
543
+ }
544
+ async function addPaths(manifest, paths, services, opts) {
545
+ const newChunks = [];
546
+ for (const path2 of paths) {
547
+ let content;
548
+ try {
549
+ content = await readFile(join(opts.cwd, path2), "utf8");
550
+ } catch {
551
+ continue;
217
552
  }
218
- if (pattern.includes("*")) {
219
- let regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
220
- if (pattern.startsWith("**/")) {
221
- regexPattern = ".*" + regexPattern.slice(3);
553
+ const chunks = chunkFile(path2, content, opts.chunk, opts.ast);
554
+ if (chunks.length === 0) {
555
+ continue;
556
+ }
557
+ newChunks.push(...chunks);
558
+ manifest.files[path2] = { chunks: chunks.length, indexedAt: opts.now, hash: hashContent(content) };
559
+ }
560
+ if (newChunks.length > 0) {
561
+ const records = await embedChunks(newChunks, services.embeddings);
562
+ await services.vectorStore.upsert(records, opts.indexId);
563
+ manifest.chunks.push(...newChunks);
564
+ }
565
+ return newChunks.length;
566
+ }
567
+ async function syncAdd(paths, services, opts) {
568
+ const manifest = await loadManifest(services.storage, opts.indexId);
569
+ const existing = new Set(paths.filter((p) => manifest.files[p]));
570
+ await removePaths(manifest, existing, services, opts.indexId);
571
+ await addPaths(manifest, paths, services, opts);
572
+ manifest.updatedAt = opts.now;
573
+ await saveManifest(services.storage, manifest);
574
+ return { added: paths.length - existing.size, updated: existing.size, deleted: 0 };
575
+ }
576
+ async function syncUpdate(paths, services, opts) {
577
+ const manifest = await loadManifest(services.storage, opts.indexId);
578
+ await removePaths(manifest, new Set(paths), services, opts.indexId);
579
+ await addPaths(manifest, paths, services, opts);
580
+ manifest.updatedAt = opts.now;
581
+ await saveManifest(services.storage, manifest);
582
+ return { added: 0, updated: paths.length, deleted: 0 };
583
+ }
584
+ async function syncDelete(paths, services, opts) {
585
+ const manifest = await loadManifest(services.storage, opts.indexId);
586
+ const deleted = await removePaths(manifest, new Set(paths), services, opts.indexId);
587
+ manifest.updatedAt = opts.now;
588
+ await saveManifest(services.storage, manifest);
589
+ return { added: 0, updated: 0, deleted };
590
+ }
591
+
592
+ // src/retrieval/bm25.ts
593
+ var K1 = 1.5;
594
+ var B = 0.75;
595
+ function tokenize(text) {
596
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
597
+ }
598
+ function bm25Search(chunks, query, limit) {
599
+ if (chunks.length === 0) {
600
+ return [];
601
+ }
602
+ const docs = chunks.map((c) => tokenize(c.text));
603
+ const docLens = docs.map((d) => d.length);
604
+ const avgDocLen = docLens.reduce((a, b) => a + b, 0) / docs.length || 1;
605
+ const df = /* @__PURE__ */ new Map();
606
+ for (const doc of docs) {
607
+ for (const term of new Set(doc)) {
608
+ df.set(term, (df.get(term) ?? 0) + 1);
609
+ }
610
+ }
611
+ const queryTerms = [...new Set(tokenize(query))];
612
+ const N = docs.length;
613
+ const scored = chunks.map((chunk, i) => {
614
+ const doc = docs[i] ?? [];
615
+ const len = docLens[i] ?? 0;
616
+ const tf = /* @__PURE__ */ new Map();
617
+ for (const term of doc) {
618
+ tf.set(term, (tf.get(term) ?? 0) + 1);
619
+ }
620
+ let score = 0;
621
+ for (const term of queryTerms) {
622
+ const termTf = tf.get(term);
623
+ if (!termTf) {
624
+ continue;
222
625
  }
223
- const regex = new RegExp("^" + regexPattern + "$");
224
- return regex.test(posixPath);
626
+ const docFreq = df.get(term) ?? 0;
627
+ const idf = Math.log(1 + (N - docFreq + 0.5) / (docFreq + 0.5));
628
+ const denom = termTf + K1 * (1 - B + B * len / avgDocLen);
629
+ score += idf * (termTf * (K1 + 1) / denom);
225
630
  }
226
- return posixPath.includes(pattern);
631
+ return { id: chunk.id, score };
227
632
  });
633
+ return scored.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
228
634
  }
229
635
 
230
- // src/utils/math.ts
231
- function cosineSimilarity(a, b) {
232
- if (a.length !== b.length) {
233
- return 0;
636
+ // src/retrieval/vector.ts
637
+ async function vectorSearch(queryText, vectorStore, embed, indexId, limit) {
638
+ let vector;
639
+ try {
640
+ vector = await embed(queryText);
641
+ } catch (err) {
642
+ throw new Error(`mind: query embedding failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
234
643
  }
235
- let dotProduct2 = 0;
236
- let normA = 0;
237
- let normB = 0;
238
- for (let i = 0; i < a.length; i++) {
239
- const av = a[i] ?? 0;
240
- const bv = b[i] ?? 0;
241
- dotProduct2 += av * bv;
242
- normA += av * av;
243
- normB += bv * bv;
244
- }
245
- if (normA === 0 || normB === 0) {
246
- return 0;
644
+ try {
645
+ const hits = await vectorStore.search(vector, limit, void 0, indexId);
646
+ return hits.map((h) => ({ id: h.id, score: h.score }));
647
+ } catch (err) {
648
+ throw new Error(
649
+ `mind: vector search failed (indexId="${indexId}", dims=${vector.length}) \u2014 ${err instanceof Error ? err.message : String(err)}`
650
+ );
651
+ }
652
+ }
653
+
654
+ // src/retrieval/fuse.ts
655
+ function intentWeights(intent) {
656
+ switch (intent) {
657
+ case "lookup":
658
+ return { vector: 0.4, bm25: 0.6 };
659
+ // exact symbol lookups lean keyword
660
+ case "architecture":
661
+ return { vector: 0.6, bm25: 0.4 };
662
+ case "concept":
663
+ default:
664
+ return { vector: 0.7, bm25: 0.3 };
665
+ }
666
+ }
667
+ function rrfFuse(lists, k) {
668
+ const scores = /* @__PURE__ */ new Map();
669
+ const labels = /* @__PURE__ */ new Map();
670
+ for (const { ranked, weight, label } of lists) {
671
+ ranked.forEach((item, idx) => {
672
+ const rank = idx + 1;
673
+ scores.set(item.id, (scores.get(item.id) ?? 0) + weight * (1 / (k + rank)));
674
+ if (label) {
675
+ const set = labels.get(item.id) ?? /* @__PURE__ */ new Set();
676
+ set.add(label);
677
+ labels.set(item.id, set);
678
+ }
679
+ });
680
+ }
681
+ return [...scores.entries()].map(([id, score]) => ({ id, score, matchedBy: resolveMatchedBy(labels.get(id)) })).sort((a, b) => b.score - a.score);
682
+ }
683
+ function resolveMatchedBy(set) {
684
+ if (!set || set.size === 0 || set.size === 2) {
685
+ return "both";
686
+ }
687
+ return set.has("semantic") ? "semantic" : "lexical";
688
+ }
689
+
690
+ // src/retrieval/hyde.ts
691
+ var HYDE_PROMPT = (query) => `Write a short, realistic code or documentation snippet that would directly answer the question below. Output only the snippet \u2014 no preamble, no explanation, no fences.
692
+
693
+ Question: ${query}`;
694
+ async function hypotheticalDocument(query, llm) {
695
+ try {
696
+ const { content } = await llm.complete(HYDE_PROMPT(query), { temperature: 0, maxTokens: 256 });
697
+ const text = (content ?? "").trim();
698
+ return text === "" ? query : `${text}
699
+
700
+ ${query}`;
701
+ } catch {
702
+ return query;
703
+ }
704
+ }
705
+
706
+ // src/retrieval/expand.ts
707
+ var EXPAND_PROMPT = (query) => `List code identifiers, function/type names, and close synonyms that would likely appear in source code or documentation relevant to the query below. Output a single space-separated line of terms only \u2014 no explanation, no punctuation, no fences.
708
+
709
+ Query: ${query}`;
710
+ async function expandQuery(query, llm) {
711
+ try {
712
+ const { content } = await llm.complete(EXPAND_PROMPT(query), { temperature: 0, maxTokens: 64 });
713
+ const extra = (content ?? "").trim();
714
+ return extra === "" ? query : `${query} ${extra}`;
715
+ } catch {
716
+ return query;
247
717
  }
248
- return dotProduct2 / Math.sqrt(normA * normB);
249
718
  }
250
- function dotProduct(a, b) {
251
- if (a.length !== b.length) {
719
+
720
+ // src/retrieval/retrieve.ts
721
+ async function retrieve(input, services) {
722
+ const manifest = await loadManifest(services.storage, input.indexId);
723
+ const byId = new Map(manifest.chunks.map((c) => [c.id, c]));
724
+ const candidateLimit = Math.max(input.limit * 3, 30);
725
+ const vectorText = input.hyde ? await hypotheticalDocument(input.text, services.llm) : input.text;
726
+ const lexicalText = input.expand ? await expandQuery(input.text, services.llm) : input.text;
727
+ const bm25 = bm25Search(manifest.chunks, lexicalText, candidateLimit);
728
+ const vector = await vectorSearch(
729
+ vectorText,
730
+ services.vectorStore,
731
+ (t) => services.embeddings.embed(t),
732
+ input.indexId,
733
+ candidateLimit
734
+ );
735
+ const weights = intentWeights(input.intent);
736
+ const fused = rrfFuse(
737
+ [
738
+ { ranked: vector, weight: weights.vector, label: "semantic" },
739
+ { ranked: bm25, weight: weights.bm25, label: "lexical" }
740
+ ],
741
+ input.rrfK
742
+ );
743
+ const ranked = fused.map((r) => {
744
+ const chunk = byId.get(r.id);
745
+ return chunk ? { chunk, score: r.score, matchedBy: r.matchedBy } : void 0;
746
+ }).filter((r) => r !== void 0).slice(0, input.limit);
747
+ const semanticWinRate = ranked.length === 0 ? 0 : ranked.filter((r) => r.matchedBy === "semantic").length / ranked.length;
748
+ return { ranked, confidence: confidenceFrom(vector), semanticWinRate };
749
+ }
750
+ function confidenceFrom(vector) {
751
+ if (vector.length === 0) {
252
752
  return 0;
253
753
  }
254
- let result = 0;
255
- for (let i = 0; i < a.length; i++) {
256
- result += (a[i] ?? 0) * (b[i] ?? 0);
257
- }
258
- return result;
259
- }
260
- function magnitude(vec) {
261
- let sum = 0;
262
- for (let i = 0; i < vec.length; i++) {
263
- const v = vec[i] ?? 0;
264
- sum += v * v;
265
- }
266
- return Math.sqrt(sum);
267
- }
268
- function normalize(vec) {
269
- const mag = magnitude(vec);
270
- if (mag === 0) {
271
- return vec;
272
- }
273
- return vec.map((v) => (v ?? 0) / mag);
274
- }
275
- var FileRotationStore = class {
276
- constructor(storage, options = {}) {
277
- this.storage = storage;
278
- this.basePath = options.basePath ? this.ensureTrailingSlash(options.basePath) : ".kb/mind/store/";
279
- this.filePrefix = options.filePrefix ?? "store-";
280
- this.maxRecordsPerFile = options.maxRecordsPerFile ?? 1e3;
281
- this.maxFiles = options.maxFiles ?? 30;
282
- }
283
- storage;
284
- basePath;
285
- filePrefix;
286
- maxRecordsPerFile;
287
- maxFiles;
288
- /**
289
- * Append a record to the current writable file
290
- *
291
- * Automatically handles:
292
- * - File rotation when maxRecordsPerFile exceeded
293
- * - Cleanup when maxFiles exceeded
294
- * - JSONL formatting
295
- *
296
- * @param record - Record to append
297
- */
298
- async appendRecord(record) {
299
- const target = await this.getWritableFile();
300
- const line = JSON.stringify({ v: 1, record }) + "\n";
301
- const existing = await this.storage.read(target);
302
- const buffer = existing ? Buffer.concat([existing, Buffer.from(line, "utf8")]) : Buffer.from(line, "utf8");
303
- await this.storage.write(target, buffer);
304
- await this.enforceRotation();
305
- }
306
- /**
307
- * Read records from all files, optionally filtering
308
- *
309
- * @param filter - Optional filter function
310
- * @param limit - Maximum number of records to return
311
- * @returns Array of records matching filter
312
- */
313
- async readRecords(filter, limit) {
314
- const files = await this.getFilesSorted();
315
- const results = [];
316
- for (const file of files) {
317
- if (limit && results.length >= limit) {
318
- break;
754
+ const top = vector.slice(0, 3);
755
+ const avg = top.reduce((a, r) => a + r.score, 0) / top.length;
756
+ return Math.max(0, Math.min(1, avg));
757
+ }
758
+
759
+ // src/retrieval/rerank.ts
760
+ function rerank(ranked, query) {
761
+ const qTokens = new Set(tokenize(query));
762
+ if (qTokens.size === 0) {
763
+ return ranked;
764
+ }
765
+ const boosted = ranked.map((item) => {
766
+ const docTokens = tokenize(item.chunk.text);
767
+ const docSet = new Set(docTokens);
768
+ let covered = 0;
769
+ for (const q of qTokens) {
770
+ if (docSet.has(q)) {
771
+ covered++;
319
772
  }
320
- const buf = await this.storage.read(file);
321
- if (!buf) {
322
- continue;
773
+ }
774
+ const coverage = covered / qTokens.size;
775
+ const lowerText = item.chunk.text.toLowerCase();
776
+ const verbatim = [...qTokens].some((q) => q.length >= 3 && lowerText.includes(q)) ? 1 : 0;
777
+ const boost = 1 + 0.5 * coverage + 0.25 * verbatim;
778
+ return { ...item, score: item.score * boost };
779
+ });
780
+ return boosted.sort((a, b) => b.score - a.score);
781
+ }
782
+
783
+ // src/retrieval/dedup.ts
784
+ function jaccard(a, b) {
785
+ if (a.size === 0 && b.size === 0) {
786
+ return 1;
787
+ }
788
+ let inter = 0;
789
+ for (const t of a) {
790
+ if (b.has(t)) {
791
+ inter++;
792
+ }
793
+ }
794
+ const union = a.size + b.size - inter;
795
+ return union === 0 ? 0 : inter / union;
796
+ }
797
+ function dedupRanked(ranked, threshold = 0.85) {
798
+ const kept = [];
799
+ const keptTokens = [];
800
+ for (const item of ranked) {
801
+ const tokens = new Set(tokenize(item.chunk.text));
802
+ const isDup = keptTokens.some((k) => jaccard(tokens, k) >= threshold);
803
+ if (!isDup) {
804
+ kept.push(item);
805
+ keptTokens.push(tokens);
806
+ }
807
+ }
808
+ return kept;
809
+ }
810
+
811
+ // src/answer/verify.ts
812
+ var FILE_WEIGHT = 0.7;
813
+ var SNIPPET_WEIGHT = 0.3;
814
+ async function verifySources(ranked, storage) {
815
+ if (ranked.length === 0) {
816
+ return { rate: 1, perChunk: [] };
817
+ }
818
+ const perChunk = [];
819
+ for (const { chunk } of ranked) {
820
+ const exists = await storage.exists(chunk.path);
821
+ let score = exists ? FILE_WEIGHT : 0;
822
+ if (exists) {
823
+ const buf = await storage.read(chunk.path);
824
+ const content = buf?.toString("utf8") ?? "";
825
+ const probe = chunk.text.split("\n").find((l) => l.trim().length > 8)?.trim();
826
+ if (probe && content.includes(probe)) {
827
+ score += SNIPPET_WEIGHT;
323
828
  }
324
- const lines = buf.toString("utf8").split("\n").filter(Boolean);
325
- for (const line of lines) {
326
- if (limit && results.length >= limit) {
327
- break;
328
- }
329
- try {
330
- const parsed = JSON.parse(line);
331
- const rec = parsed.record;
332
- if (!filter || filter(rec)) {
333
- results.push(rec);
334
- }
335
- } catch {
336
- continue;
337
- }
829
+ }
830
+ perChunk.push(score);
831
+ }
832
+ const rate = perChunk.reduce((a, b) => a + b, 0) / perChunk.length;
833
+ return { rate, perChunk };
834
+ }
835
+ function computeConfidence(retrievalConfidence, verificationRate, floor) {
836
+ const confidence = Math.max(0, Math.min(1, retrievalConfidence * verificationRate));
837
+ const warnings = [];
838
+ if (confidence < floor) {
839
+ warnings.push({
840
+ code: "LOW_CONFIDENCE",
841
+ message: `Confidence ${confidence.toFixed(2)} is below the floor ${floor.toFixed(2)}; the answer may be unreliable.`
842
+ });
843
+ }
844
+ return { confidence, warnings };
845
+ }
846
+
847
+ // src/answer/field-check.ts
848
+ function isCodeSymbol(t) {
849
+ if (t.length < 3 || t.length > 80) {
850
+ return false;
851
+ }
852
+ if (/\.(ts|tsx|js|jsx|json|md|go|py|rs)$/.test(t)) {
853
+ return true;
854
+ }
855
+ if (t.includes(".") && /[a-zA-Z]/.test(t)) {
856
+ return true;
857
+ }
858
+ if (/_/.test(t) && /[a-zA-Z]/.test(t)) {
859
+ return true;
860
+ }
861
+ if (/[a-z][A-Z]/.test(t)) {
862
+ return true;
863
+ }
864
+ return false;
865
+ }
866
+ function extractSymbols(answer) {
867
+ const out = /* @__PURE__ */ new Set();
868
+ for (const m of answer.matchAll(/`([^`\n]{2,80})`/g)) {
869
+ for (const tok of m[1].trim().split(/[\s(),;:[\]{}'"]+/)) {
870
+ if (isCodeSymbol(tok)) {
871
+ out.add(tok);
338
872
  }
339
873
  }
340
- return limit ? results.slice(0, limit) : results;
341
- }
342
- /**
343
- * Get the current writable file path
344
- *
345
- * Creates a new segment if:
346
- * - No files exist
347
- * - Latest file has >= maxRecordsPerFile records
348
- *
349
- * @returns Path to writable file
350
- */
351
- async getWritableFile() {
352
- const files = await this.getFilesSorted();
353
- if (files.length === 0) {
354
- return this.segmentPath(Date.now());
355
- }
356
- const latest = files[files.length - 1];
357
- const buf = await this.storage.read(latest);
358
- if (!buf) {
359
- return latest;
360
- }
361
- const count = buf.toString("utf8").split("\n").filter(Boolean).length;
362
- if (count >= this.maxRecordsPerFile) {
363
- return this.segmentPath(Date.now());
364
- }
365
- return latest;
366
- }
367
- /**
368
- * Get all store files sorted by timestamp (oldest to newest)
369
- *
370
- * @returns Sorted array of file paths
371
- */
372
- async getFilesSorted() {
373
- const files = await this.storage.list(this.basePath);
374
- return files.filter((f) => f.startsWith(this.basePath + this.filePrefix) && f.endsWith(".jsonl")).sort();
375
- }
376
- /**
377
- * Generate a segment file path from timestamp
378
- *
379
- * Format: {filePrefix}YYYYMMDD-{timestamp}.jsonl
380
- * Example: history-20251209-1733769000123.jsonl
381
- *
382
- * @param ts - Unix timestamp in milliseconds
383
- * @returns Full file path
384
- */
385
- segmentPath(ts) {
386
- const date = new Date(ts);
387
- const day = String(date.getDate()).padStart(2, "0");
388
- const month = String(date.getMonth() + 1).padStart(2, "0");
389
- const year = date.getFullYear();
390
- const filename = `${this.filePrefix}${year}${month}${day}-${ts}.jsonl`;
391
- return path.posix.join(this.basePath, filename);
392
- }
393
- /**
394
- * Enforce file rotation by deleting oldest files if maxFiles exceeded
395
- */
396
- async enforceRotation() {
397
- const files = await this.getFilesSorted();
398
- if (files.length <= this.maxFiles) {
399
- return;
400
- }
401
- const excess = files.length - this.maxFiles;
402
- const toDelete = files.slice(0, excess);
403
- await Promise.all(toDelete.map((f) => this.storage.delete(f)));
404
- }
405
- /**
406
- * Ensure path ends with trailing slash
407
- */
408
- ensureTrailingSlash(p) {
409
- return p.endsWith("/") ? p : `${p}/`;
410
874
  }
411
- };
412
- function sortKeysRecursively(obj) {
413
- if (obj === null || typeof obj !== "object") {
414
- return obj;
875
+ for (const m of answer.matchAll(/[A-Za-z_$][A-Za-z0-9_$-]*(?:[./][A-Za-z0-9_$.-]+)*/g)) {
876
+ if (isCodeSymbol(m[0])) {
877
+ out.add(m[0]);
878
+ }
879
+ }
880
+ return [...out];
881
+ }
882
+ function checkFields(answer, ranked) {
883
+ const symbols = extractSymbols(answer);
884
+ if (symbols.length === 0) {
885
+ return { rate: 1, ungrounded: [], checked: 0 };
415
886
  }
416
- if (Array.isArray(obj)) {
417
- return obj.map(sortKeysRecursively);
887
+ const corpus = ranked.map((r) => `${r.chunk.path}
888
+ ${r.chunk.text}`).join("\n").toLowerCase();
889
+ const grounded = (sym) => {
890
+ const s = sym.toLowerCase();
891
+ if (corpus.includes(s)) {
892
+ return true;
893
+ }
894
+ const head = s.split(".")[0];
895
+ return head.length >= 3 && corpus.includes(head);
896
+ };
897
+ const ungrounded = symbols.filter((s) => !grounded(s));
898
+ return { rate: (symbols.length - ungrounded.length) / symbols.length, ungrounded, checked: symbols.length };
899
+ }
900
+ function applyFieldCheck(answer, ranked, confidence, warnings, floor) {
901
+ const fc = checkFields(answer, ranked);
902
+ if (fc.checked === 0 || fc.rate === 1) {
903
+ return { confidence, warnings };
904
+ }
905
+ const adjusted = Math.max(0, confidence * (0.5 + 0.5 * fc.rate));
906
+ const next = [...warnings];
907
+ if (fc.ungrounded.length > 0) {
908
+ next.push({
909
+ code: "UNGROUNDED_TERMS",
910
+ message: `Answer mentions ${fc.ungrounded.length} term(s) not found in sources: ${fc.ungrounded.slice(0, 5).join(", ")}`,
911
+ details: { ungrounded: fc.ungrounded, rate: Math.round(fc.rate * 1e3) / 1e3 }
912
+ });
418
913
  }
419
- const sorted = {};
420
- const record = obj;
421
- const keys = Object.keys(record).sort();
422
- for (const key of keys) {
423
- sorted[key] = sortKeysRecursively(record[key]);
914
+ if (adjusted < floor && !next.some((w) => w.code === "LOW_CONFIDENCE")) {
915
+ next.push({
916
+ code: "LOW_CONFIDENCE",
917
+ message: `Confidence ${adjusted.toFixed(2)} is below the floor ${floor.toFixed(2)}; the answer may be unreliable.`
918
+ });
424
919
  }
425
- return sorted;
920
+ return { confidence: adjusted, warnings: next };
426
921
  }
427
- async function readJson(filePath) {
922
+
923
+ // src/answer/decompose.ts
924
+ async function decompose(query, llm, maxSubqueries) {
925
+ if (maxSubqueries <= 0) {
926
+ return [query];
927
+ }
928
+ const prompt = `Break the following question into at most ${maxSubqueries} focused, self-contained sub-questions that together cover it. Return one sub-question per line, no numbering.
929
+
930
+ Question: ${query}`;
428
931
  try {
429
- const content = await promises.readFile(filePath, "utf8");
430
- return JSON.parse(content);
431
- } catch (error) {
432
- if (error.code === "ENOENT") {
433
- return null;
434
- }
435
- throw error;
436
- }
437
- }
438
- async function writeJson(filePath, data) {
439
- const tmp = `${filePath}.tmp`;
440
- const sorted = sortKeysRecursively(data);
441
- const content = JSON.stringify(sorted, null, 2) + "\n";
442
- await promises.mkdir(dirname(filePath), { recursive: true });
443
- await promises.writeFile(tmp, content, "utf8");
444
- if (process.platform === "win32") {
445
- try {
446
- await promises.unlink(filePath);
447
- } catch (err) {
448
- if (err.code !== "ENOENT") {
449
- throw err;
450
- }
932
+ const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 256 });
933
+ const subs = (content ?? "").split("\n").map((l) => l.replace(/^[-*\d.)\s]+/, "").trim()).filter((l) => l.length > 0).slice(0, maxSubqueries);
934
+ return subs.length > 0 ? [query, ...subs] : [query];
935
+ } catch {
936
+ return [query];
937
+ }
938
+ }
939
+
940
+ // src/answer/synthesize.ts
941
+ var MAX_SNIPPET_CHARS = 600;
942
+ function snippetFrom(text, mode) {
943
+ if (mode === "none") {
944
+ return void 0;
945
+ }
946
+ if (mode === "full") {
947
+ return text.length > MAX_SNIPPET_CHARS ? `${text.slice(0, MAX_SNIPPET_CHARS)}\u2026` : text;
948
+ }
949
+ const line = text.split("\n").find((l) => l.trim().length > 8)?.trim() ?? text.trim().slice(0, 120);
950
+ return line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
951
+ }
952
+ function toSearchResults(ranked, opts) {
953
+ return ranked.map(({ chunk, matchedBy }) => ({
954
+ file: chunk.path,
955
+ lines: [chunk.startLine, chunk.endLine],
956
+ kind: chunk.kind,
957
+ matchedBy,
958
+ stale: opts.staleByFile.get(chunk.path) ?? false,
959
+ snippet: snippetFrom(chunk.text, opts.snippet)
960
+ }));
961
+ }
962
+
963
+ // src/answer/answer.ts
964
+ var MAX_SNIPPET_CHARS2 = 600;
965
+ function truncate(text) {
966
+ return text.length > MAX_SNIPPET_CHARS2 ? `${text.slice(0, MAX_SNIPPET_CHARS2)}\u2026` : text;
967
+ }
968
+ function extractiveAnswer(query, ranked) {
969
+ if (ranked.length === 0) {
970
+ return `No indexed content matched the query: "${query}".`;
971
+ }
972
+ const top = ranked[0].chunk;
973
+ return `Most relevant source: ${top.path} (lines ${top.startLine}-${top.endLine}).
974
+
975
+ ${truncate(top.text)}`;
976
+ }
977
+ async function synthesizeAnswer(query, ranked, llm, useLLM) {
978
+ if (!useLLM || ranked.length === 0) {
979
+ return extractiveAnswer(query, ranked);
980
+ }
981
+ const context = ranked.map((r, i) => `[${i + 1}] ${r.chunk.path}:${r.chunk.startLine}-${r.chunk.endLine}
982
+ ${r.chunk.text}`).join("\n\n");
983
+ const prompt = `Answer the question using ONLY the provided context. Be concise and cite the relevant files by path. If the context does not contain the answer, say so.
984
+
985
+ Question: ${query}
986
+
987
+ Context:
988
+ ${context}`;
989
+ try {
990
+ const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 800 });
991
+ const trimmed = (content ?? "").trim();
992
+ return trimmed.length > 0 ? trimmed : extractiveAnswer(query, ranked);
993
+ } catch {
994
+ return extractiveAnswer(query, ranked);
995
+ }
996
+ }
997
+ function toSources(ranked, opts) {
998
+ return ranked.map(({ chunk, matchedBy }) => ({
999
+ file: chunk.path,
1000
+ lines: [chunk.startLine, chunk.endLine],
1001
+ kind: chunk.kind,
1002
+ matchedBy,
1003
+ stale: opts.staleByFile.get(chunk.path) ?? false,
1004
+ snippet: snippetFrom(chunk.text, opts.snippet)
1005
+ }));
1006
+ }
1007
+ function buildAgentResponse(input) {
1008
+ const sources = toSources(input.ranked, { snippet: input.snippet, staleByFile: input.staleByFile });
1009
+ const abstained = input.confidence < input.floor || sources.length === 0;
1010
+ const response = {
1011
+ answer: input.answer,
1012
+ confidence: input.confidence,
1013
+ abstained,
1014
+ sources,
1015
+ warnings: input.warnings && input.warnings.length > 0 ? input.warnings : void 0,
1016
+ meta: {
1017
+ requestId: input.requestId,
1018
+ mode: input.mode,
1019
+ timingMs: input.timingMs,
1020
+ indexId: input.indexId
1021
+ }
1022
+ };
1023
+ return AgentResponseSchema.parse(response);
1024
+ }
1025
+
1026
+ // src/answer/explore.ts
1027
+ function toExploreEntries(ranked, staleByFile) {
1028
+ const seen = /* @__PURE__ */ new Set();
1029
+ const out = [];
1030
+ for (const r of ranked) {
1031
+ if (seen.has(r.chunk.path)) {
1032
+ continue;
451
1033
  }
1034
+ seen.add(r.chunk.path);
1035
+ const line = snippetFrom(r.chunk.text, "line");
1036
+ out.push({
1037
+ file: r.chunk.path,
1038
+ lines: [r.chunk.startLine, r.chunk.endLine],
1039
+ why: line && line.length > 0 ? line : `${r.chunk.kind} file`,
1040
+ matchedBy: r.matchedBy,
1041
+ stale: staleByFile.get(r.chunk.path) ?? false
1042
+ });
452
1043
  }
453
- await promises.rename(tmp, filePath);
1044
+ return out;
454
1045
  }
455
- function computeJsonHash(data) {
456
- const content = JSON.stringify(sortKeysRecursively(data));
457
- return sha256(content);
1046
+ function spreadOf(files) {
1047
+ return new Set(
1048
+ files.map((f) => {
1049
+ const i = f.lastIndexOf("/");
1050
+ return i < 0 ? "." : f.slice(0, i);
1051
+ })
1052
+ ).size;
458
1053
  }
459
- async function verifyIndexes(cwd) {
460
- const inconsistencies = [];
1054
+ async function orientationSummary(task, entries, llm, useLLM) {
1055
+ if (!useLLM || entries.length === 0) {
1056
+ return "";
1057
+ }
1058
+ const list = entries.map((e, i) => `[${i + 1}] ${e.file}:${e.lines[0]}-${e.lines[1]} \u2014 ${e.why}`).join("\n");
1059
+ const prompt = `A developer must approach the task below in an unfamiliar codebase. Using ONLY the relevant files listed, briefly explain WHERE to start, the KEY files and their role, and HOW involved the task looks. Be concise; cite files by path.
1060
+
1061
+ Task: ${task}
1062
+
1063
+ Relevant files:
1064
+ ${list}`;
461
1065
  try {
462
- const index = await readJson(`${cwd}/.kb/mind/index.json`);
463
- if (!index) {
1066
+ const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 400 });
1067
+ return (content ?? "").trim();
1068
+ } catch {
1069
+ return "";
1070
+ }
1071
+ }
1072
+ function historyKey(indexId) {
1073
+ return `${MIND_NAMESPACE_PREFIX}history:${indexId}`;
1074
+ }
1075
+ async function recordQuery(cache, indexId, query, at) {
1076
+ try {
1077
+ await cache.zadd(historyKey(indexId), at, query);
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ async function recentQueries(cache, indexId, sinceMs = 0) {
1082
+ try {
1083
+ return await cache.zrangebyscore(historyKey(indexId), sinceMs, Number.MAX_SAFE_INTEGER);
1084
+ } catch {
1085
+ return [];
1086
+ }
1087
+ }
1088
+
1089
+ // src/mind.ts
1090
+ function createMind(services, config, options = {}) {
1091
+ const now = options.now ?? (() => Date.now());
1092
+ const isoNow = options.isoNow ?? (() => new Date(now()).toISOString());
1093
+ const cwd = options.cwd ?? process.cwd();
1094
+ function resolveIndexId(indexId) {
1095
+ return indexId && indexId.trim() !== "" ? indexId : config.defaultIndex;
1096
+ }
1097
+ function syncOpts(indexId) {
1098
+ const eff = effectiveIndexConfig(config, indexId);
1099
+ return {
1100
+ indexId,
1101
+ cwd,
1102
+ chunk: { maxTokens: eff.chunk.maxTokens, overlapTokens: eff.chunk.overlapTokens },
1103
+ ast: eff.chunk.ast,
1104
+ now: isoNow()
1105
+ };
1106
+ }
1107
+ return {
1108
+ async index(req, onProgress) {
1109
+ const indexId = resolveIndexId(req.indexId);
1110
+ const eff = effectiveIndexConfig(config, indexId);
1111
+ const start = now();
1112
+ const result = await ingest(
1113
+ {
1114
+ indexId,
1115
+ cwd,
1116
+ // CLI `--scope` overrides the include set (keeping configured excludes);
1117
+ // otherwise use the index's configured include/exclude scope.
1118
+ scope: req.scope ? { include: [req.scope], exclude: eff.scope.exclude } : eff.scope,
1119
+ full: req.full,
1120
+ chunk: { maxTokens: eff.chunk.maxTokens, overlapTokens: eff.chunk.overlapTokens },
1121
+ ast: eff.chunk.ast,
1122
+ now: isoNow(),
1123
+ onProgress
1124
+ },
1125
+ services
1126
+ );
1127
+ const durationMs = now() - start;
1128
+ services.logger?.info("mind: index", {
1129
+ indexId,
1130
+ filesIndexed: result.filesIndexed,
1131
+ chunks: result.chunks,
1132
+ added: result.added,
1133
+ updated: result.updated,
1134
+ removed: result.removed,
1135
+ unchanged: result.unchanged,
1136
+ durationMs
1137
+ });
1138
+ return { indexId, filesIndexed: result.filesIndexed, chunks: result.chunks, durationMs };
1139
+ },
1140
+ async search(req) {
1141
+ const indexId = resolveIndexId(req.indexId);
1142
+ const retrieval = effectiveIndexConfig(config, indexId).retrieval;
1143
+ const limit = req.limit ?? retrieval.limit;
1144
+ const snippet = req.snippet ?? "line";
1145
+ const t0 = now();
1146
+ const retrieved = await retrieve(
1147
+ { text: req.text, indexId, limit: limit * 3, intent: req.intent, rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
1148
+ services
1149
+ );
1150
+ let ranked = retrieval.rerank ? rerank(retrieved.ranked, req.text) : retrieved.ranked;
1151
+ if (retrieval.dedup) {
1152
+ ranked = dedupRanked(ranked);
1153
+ }
1154
+ ranked = ranked.slice(0, limit);
1155
+ const manifest = await loadManifest(services.storage, indexId);
1156
+ const stales = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
1157
+ const results = toSearchResults(ranked, { snippet, staleByFile: stales });
1158
+ const staleCount2 = [...stales.values()].filter(Boolean).length;
1159
+ const timingMs = now() - t0;
1160
+ services.logger?.info("mind: search", {
1161
+ indexId,
1162
+ results: results.length,
1163
+ semanticWinRate: retrieved.semanticWinRate,
1164
+ staleCount: staleCount2,
1165
+ timingMs
1166
+ });
464
1167
  return {
465
- ok: false,
466
- code: "MIND_NO_INDEX",
467
- inconsistencies: ["Main index file not found"],
468
- hint: 'Run "kb mind rag-index" to initialize indexes'
1168
+ results,
1169
+ confidence: retrieved.confidence,
1170
+ indexId,
1171
+ meta: { requestId: `mind:${t0}`, timingMs, semanticWinRate: retrieved.semanticWinRate, staleCount: staleCount2 }
469
1172
  };
470
- }
471
- const [apiIndex, depsGraph, recentDiff, meta, docs] = await Promise.all([
472
- readJson(`${cwd}/.kb/mind/api-index.json`),
473
- readJson(`${cwd}/.kb/mind/deps.json`),
474
- readJson(`${cwd}/.kb/mind/recent-diff.json`),
475
- readJson(`${cwd}/.kb/mind/meta.json`),
476
- readJson(`${cwd}/.kb/mind/docs.json`)
477
- ]);
478
- if (apiIndex) {
479
- const computedHash = computeJsonHash(apiIndex);
480
- if (computedHash !== index.apiIndexHash) {
481
- inconsistencies.push(
482
- `API index hash mismatch: expected ${index.apiIndexHash}, got ${computedHash}`
483
- );
484
- }
485
- } else if (index.apiIndexHash) {
486
- inconsistencies.push("API index file missing but hash is present");
487
- }
488
- if (depsGraph) {
489
- const computedHash = computeJsonHash(depsGraph);
490
- if (computedHash !== index.depsHash) {
491
- inconsistencies.push(
492
- `Dependencies hash mismatch: expected ${index.depsHash}, got ${computedHash}`
1173
+ },
1174
+ async ask(req) {
1175
+ const indexId = resolveIndexId(req.indexId);
1176
+ const retrieval = effectiveIndexConfig(config, indexId).retrieval;
1177
+ const mode = req.mode ?? "auto";
1178
+ const budget = config.modes[mode];
1179
+ const t0 = now();
1180
+ await recordQuery(services.cache, indexId, req.text, t0);
1181
+ const queries = budget.useLLM ? await decompose(req.text, services.llm, budget.maxSubqueries) : [req.text];
1182
+ const merged = /* @__PURE__ */ new Map();
1183
+ let retrievalConfidence = 0;
1184
+ for (const q of queries) {
1185
+ const r = await retrieve(
1186
+ { text: q, indexId, limit: budget.maxChunks, intent: void 0, rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
1187
+ services
493
1188
  );
1189
+ retrievalConfidence = Math.max(retrievalConfidence, r.confidence);
1190
+ for (const rc of r.ranked) {
1191
+ const prev = merged.get(rc.chunk.id);
1192
+ if (!prev || rc.score > prev.score) {
1193
+ merged.set(rc.chunk.id, rc);
1194
+ }
1195
+ }
494
1196
  }
495
- } else if (index.depsHash) {
496
- inconsistencies.push("Dependencies file missing but hash is present");
497
- }
498
- if (recentDiff) {
499
- const computedHash = computeJsonHash(recentDiff);
500
- if (computedHash !== index.recentDiffHash) {
501
- inconsistencies.push(
502
- `Recent diff hash mismatch: expected ${index.recentDiffHash}, got ${computedHash}`
503
- );
1197
+ let ranked = retrieval.rerank ? rerank([...merged.values()], req.text) : [...merged.values()].sort((a, b) => b.score - a.score);
1198
+ if (retrieval.dedup) {
1199
+ ranked = dedupRanked(ranked);
504
1200
  }
505
- } else if (index.recentDiffHash) {
506
- inconsistencies.push("Recent diff file missing but hash is present");
507
- }
508
- const combinedContent = JSON.stringify({
509
- apiIndex: apiIndex || {},
510
- deps: depsGraph || {},
511
- recentDiff: recentDiff || {},
512
- meta: meta || {},
513
- docs: docs || {}
514
- });
515
- const computedChecksum = sha256(combinedContent);
516
- if (computedChecksum !== index.indexChecksum) {
517
- inconsistencies.push(
518
- `Index checksum mismatch: expected ${index.indexChecksum}, got ${computedChecksum}`
1201
+ ranked = ranked.slice(0, budget.maxChunks);
1202
+ const verification = await verifySources(ranked, services.storage);
1203
+ const base = computeConfidence(retrievalConfidence, verification.rate, config.confidence.floor);
1204
+ const answer = await synthesizeAnswer(req.text, ranked, services.llm, budget.useLLM);
1205
+ const { confidence, warnings } = budget.useLLM ? applyFieldCheck(answer, ranked, base.confidence, base.warnings, config.confidence.floor) : base;
1206
+ const manifest = await loadManifest(services.storage, indexId);
1207
+ const staleByFile = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
1208
+ const timingMs = now() - t0;
1209
+ services.logger?.info("mind: ask", {
1210
+ indexId,
1211
+ mode,
1212
+ subqueries: queries.length,
1213
+ chunks: ranked.length,
1214
+ confidence: Math.round(confidence * 1e3) / 1e3,
1215
+ timingMs
1216
+ });
1217
+ return buildAgentResponse({
1218
+ answer,
1219
+ ranked,
1220
+ confidence,
1221
+ mode,
1222
+ requestId: `mind:${t0}`,
1223
+ timingMs,
1224
+ indexId,
1225
+ floor: config.confidence.floor,
1226
+ snippet: req.snippet ?? "line",
1227
+ staleByFile,
1228
+ warnings
1229
+ });
1230
+ },
1231
+ async explore(req) {
1232
+ const indexId = resolveIndexId(req.indexId);
1233
+ const retrieval = effectiveIndexConfig(config, indexId).retrieval;
1234
+ const budget = config.modes.auto;
1235
+ const limit = req.limit ?? retrieval.limit;
1236
+ const t0 = now();
1237
+ const retrieved = await retrieve(
1238
+ { text: req.task, indexId, limit: limit * 3, intent: "architecture", rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
1239
+ services
519
1240
  );
520
- }
521
- const expectedFiles = ["api-index.json", "deps.json", "recent-diff.json"];
522
- for (const file of expectedFiles) {
523
- try {
524
- await promises.access(`${cwd}/.kb/mind/${file}`);
525
- } catch {
526
- inconsistencies.push(`Required index file missing: ${file}`);
1241
+ let ranked = retrieval.rerank ? rerank(retrieved.ranked, req.task) : retrieved.ranked;
1242
+ if (retrieval.dedup) {
1243
+ ranked = dedupRanked(ranked);
1244
+ }
1245
+ ranked = ranked.slice(0, limit);
1246
+ const manifest = await loadManifest(services.storage, indexId);
1247
+ const stales = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
1248
+ const files = toExploreEntries(ranked, stales);
1249
+ const spread = spreadOf(files.map((f) => f.file));
1250
+ const summary = await orientationSummary(req.task, files, services.llm, budget.useLLM);
1251
+ const timingMs = now() - t0;
1252
+ services.logger?.info("mind: explore", {
1253
+ indexId,
1254
+ filesTouched: files.length,
1255
+ spread,
1256
+ timingMs
1257
+ });
1258
+ return {
1259
+ task: req.task,
1260
+ indexId,
1261
+ confidence: retrieved.confidence,
1262
+ summary,
1263
+ files,
1264
+ meta: { requestId: `mind:${t0}`, timingMs, filesTouched: files.length, spread }
1265
+ };
1266
+ },
1267
+ async reindex(req) {
1268
+ return this.index({ indexId: req.indexId, full: true });
1269
+ },
1270
+ async drop(req) {
1271
+ const indexId = resolveIndexId(req.indexId);
1272
+ if (!await manifestExists(services.storage, indexId)) {
1273
+ throw new Error(`No such index "${indexId}" \u2014 nothing to drop (run \`kb mind status\` to list indexes).`);
1274
+ }
1275
+ const manifest = await loadManifest(services.storage, indexId);
1276
+ const ids = manifest.chunks.map((c) => c.id);
1277
+ const droppedFiles = Object.keys(manifest.files).length;
1278
+ const BATCH = 500;
1279
+ for (let i = 0; i < ids.length; i += BATCH) {
1280
+ await services.vectorStore.delete(ids.slice(i, i + BATCH), indexId);
1281
+ }
1282
+ await deleteManifest(services.storage, indexId);
1283
+ services.logger?.info("mind: drop", { indexId, droppedChunks: ids.length, droppedFiles });
1284
+ return { indexId, droppedChunks: ids.length, droppedFiles };
1285
+ },
1286
+ async syncAdd(paths, indexId) {
1287
+ const id = resolveIndexId(indexId);
1288
+ const counts = await syncAdd(paths, services, syncOpts(id));
1289
+ return { indexId: id, ...counts };
1290
+ },
1291
+ async syncUpdate(paths, indexId) {
1292
+ const id = resolveIndexId(indexId);
1293
+ const counts = await syncUpdate(paths, services, syncOpts(id));
1294
+ return { indexId: id, ...counts };
1295
+ },
1296
+ async syncDelete(paths, indexId) {
1297
+ const id = resolveIndexId(indexId);
1298
+ const counts = await syncDelete(paths, services, syncOpts(id));
1299
+ return { indexId: id, ...counts };
1300
+ },
1301
+ async syncList(indexId) {
1302
+ const id = resolveIndexId(indexId);
1303
+ const manifest = await loadManifest(services.storage, id);
1304
+ return {
1305
+ indexId: id,
1306
+ documents: Object.entries(manifest.files).map(([path2, info]) => ({
1307
+ path: path2,
1308
+ chunks: info.chunks,
1309
+ indexedAt: info.indexedAt
1310
+ }))
1311
+ };
1312
+ },
1313
+ async syncStatus(indexId) {
1314
+ const id = resolveIndexId(indexId);
1315
+ const manifest = await loadManifest(services.storage, id);
1316
+ return {
1317
+ indexId: id,
1318
+ documents: Object.keys(manifest.files).length,
1319
+ chunks: manifest.chunks.length,
1320
+ lastIndexedAt: manifest.updatedAt,
1321
+ stale: false
1322
+ };
1323
+ },
1324
+ async status(indexId) {
1325
+ const ids = indexId ? [indexId] : [.../* @__PURE__ */ new Set([...Object.keys(config.indexes), ...await listIndexIds(services)])];
1326
+ const indexes = [];
1327
+ for (const id of ids) {
1328
+ const manifest = await loadManifest(services.storage, id);
1329
+ const declared = config.indexes[id];
1330
+ const sc = resolveScope(declared?.scope);
1331
+ const coverage = sc.include.join(", ") + (sc.exclude.length ? ` \xB7 excl ${sc.exclude.join(", ")}` : "");
1332
+ indexes.push({
1333
+ indexId: id,
1334
+ label: declared?.label,
1335
+ coverage,
1336
+ documents: Object.keys(manifest.files).length,
1337
+ chunks: manifest.chunks.length,
1338
+ lastIndexedAt: manifest.updatedAt,
1339
+ staleCount: await staleCount(manifest, cwd)
1340
+ });
527
1341
  }
1342
+ return { indexes, healthy: true };
1343
+ },
1344
+ async health() {
1345
+ return {
1346
+ ok: true,
1347
+ vectorStore: Boolean(services.vectorStore),
1348
+ embeddings: Boolean(services.embeddings),
1349
+ llm: Boolean(services.llm)
1350
+ };
1351
+ }
1352
+ };
1353
+ }
1354
+ async function listIndexIds(services) {
1355
+ const paths = await services.storage.list("mind/");
1356
+ const ids = /* @__PURE__ */ new Set();
1357
+ for (const p of paths) {
1358
+ const match = /^mind\/([^/]+)\/manifest\.json$/.exec(p);
1359
+ if (match) {
1360
+ ids.add(match[1]);
528
1361
  }
529
- const ok = inconsistencies.length === 0;
530
- const code = ok ? null : "MIND_INDEX_INCONSISTENT";
531
- const hint = ok ? "All indexes are consistent and up to date" : 'Run "kb mind rag-index" to rebuild indexes';
532
- return { ok, code, inconsistencies, hint };
533
- } catch (error) {
534
- return {
535
- ok: false,
536
- code: "MIND_VERIFY_ERROR",
537
- inconsistencies: [`Verification failed: ${error instanceof Error ? error.message : String(error)}`],
538
- hint: "Check file permissions and workspace structure"
539
- };
540
1362
  }
1363
+ return [...ids];
541
1364
  }
542
1365
 
543
- // src/defaults.ts
544
- var DEFAULT_BUDGET = {
545
- totalTokens: 9e3,
546
- caps: {
547
- intent_summary: 300,
548
- product_overview: 600,
549
- project_meta: 500,
550
- api_signatures: 2200,
551
- recent_diffs: 1200,
552
- docs_overview: 600,
553
- impl_snippets: 3e3,
554
- configs_profiles: 700
555
- },
556
- truncation: "middle"
557
- };
558
- var DEFAULT_PRESET = {
559
- name: "balanced",
560
- weight: {
561
- overview: 1,
562
- api: 1.2,
563
- diffs: 1,
564
- snippets: 1.4,
565
- configs: 0.6,
566
- meta: 0.8,
567
- docs: 0.9
1366
+ // src/pipeline.ts
1367
+ var defaultClock = () => Date.now();
1368
+ var Tracer = class {
1369
+ constructor(requestId, mode, clock = defaultClock) {
1370
+ this.requestId = requestId;
1371
+ this.mode = mode;
1372
+ this.clock = clock;
1373
+ }
1374
+ requestId;
1375
+ mode;
1376
+ clock;
1377
+ stages = [];
1378
+ async run(stage, input, services, fn) {
1379
+ const start = this.clock();
1380
+ const output = await fn(input, services);
1381
+ this.stages.push({
1382
+ stage,
1383
+ durationMs: this.clock() - start,
1384
+ outputCount: Array.isArray(output) ? output.length : void 0
1385
+ });
1386
+ return output;
1387
+ }
1388
+ /** Record a stage trace manually (for stages not wrapped by `run`). */
1389
+ record(trace) {
1390
+ this.stages.push(trace);
1391
+ }
1392
+ build(totalMs) {
1393
+ return {
1394
+ requestId: this.requestId,
1395
+ mode: this.mode,
1396
+ totalMs,
1397
+ stages: [...this.stages]
1398
+ };
568
1399
  }
569
1400
  };
570
- var DEFAULT_TIME_BUDGET_MS = 800;
571
- var MAX_FILE_SIZE_BYTES = 1.5 * 1024 * 1024;
572
- var MAX_SNIPPET_LINES = 60;
573
- function getGenerator() {
574
- return "kb-labs-mind@0.1.0";
575
- }
576
1401
 
577
- export { DEFAULT_BUDGET, DEFAULT_PRESET, DEFAULT_TIME_BUDGET_MS, DefaultTokenEstimator, ERROR_HINTS, FileRotationStore, MAX_FILE_SIZE_BYTES, MAX_SNIPPET_LINES, MindError, computeJsonHash, cosineSimilarity, createMindError, defaultTokenEstimator, dotProduct, estimateTokens, findWorkspaceRoot, fromPosix, getExitCode, getGenerator, isMindError, magnitude, makeRelativeToRoot, normalize, readJson, sha256, sha256Buffer, sha256File, shouldIgnorePath, toPosix, truncateToTokens, verifyIndexes, wrapError, writeJson };
1402
+ export { Tracer, bm25Search, buildAgentResponse, checkFields, chunkFile, chunkId, computeConfidence, createMind, decompose, dedupRanked, extractSymbols, ingest, intentWeights, kindFromPath, loadManifest, recentQueries, recordQuery, rerank, retrieve, rrfFuse, saveManifest, slidingWindowChunks, structuralChunks, syncAdd, syncDelete, syncUpdate, synthesizeAnswer, toSearchResults, toSources, tokenize, verifySources };
578
1403
  //# sourceMappingURL=index.js.map
579
1404
  //# sourceMappingURL=index.js.map