opencode-codebase-index 0.7.0 → 0.8.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/cli.cjs CHANGED
@@ -487,7 +487,7 @@ var require_ignore = __commonJS({
487
487
  // path matching.
488
488
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
489
489
  // @returns {TestResult} true if a file is ignored
490
- test(path11, checkUnignored, mode) {
490
+ test(path13, checkUnignored, mode) {
491
491
  let ignored = false;
492
492
  let unignored = false;
493
493
  let matchedRule;
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
496
496
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
497
497
  return;
498
498
  }
499
- const matched = rule[mode].test(path11);
499
+ const matched = rule[mode].test(path13);
500
500
  if (!matched) {
501
501
  return;
502
502
  }
@@ -517,17 +517,17 @@ var require_ignore = __commonJS({
517
517
  var throwError = (message, Ctor) => {
518
518
  throw new Ctor(message);
519
519
  };
520
- var checkPath = (path11, originalPath, doThrow) => {
521
- if (!isString(path11)) {
520
+ var checkPath = (path13, originalPath, doThrow) => {
521
+ if (!isString(path13)) {
522
522
  return doThrow(
523
523
  `path must be a string, but got \`${originalPath}\``,
524
524
  TypeError
525
525
  );
526
526
  }
527
- if (!path11) {
527
+ if (!path13) {
528
528
  return doThrow(`path must not be empty`, TypeError);
529
529
  }
530
- if (checkPath.isNotRelative(path11)) {
530
+ if (checkPath.isNotRelative(path13)) {
531
531
  const r = "`path.relative()`d";
532
532
  return doThrow(
533
533
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -536,7 +536,7 @@ var require_ignore = __commonJS({
536
536
  }
537
537
  return true;
538
538
  };
539
- var isNotRelative = (path11) => REGEX_TEST_INVALID_PATH.test(path11);
539
+ var isNotRelative = (path13) => REGEX_TEST_INVALID_PATH.test(path13);
540
540
  checkPath.isNotRelative = isNotRelative;
541
541
  checkPath.convert = (p) => p;
542
542
  var Ignore2 = class {
@@ -566,19 +566,19 @@ var require_ignore = __commonJS({
566
566
  }
567
567
  // @returns {TestResult}
568
568
  _test(originalPath, cache, checkUnignored, slices) {
569
- const path11 = originalPath && checkPath.convert(originalPath);
569
+ const path13 = originalPath && checkPath.convert(originalPath);
570
570
  checkPath(
571
- path11,
571
+ path13,
572
572
  originalPath,
573
573
  this._strictPathCheck ? throwError : RETURN_FALSE
574
574
  );
575
- return this._t(path11, cache, checkUnignored, slices);
575
+ return this._t(path13, cache, checkUnignored, slices);
576
576
  }
577
- checkIgnore(path11) {
578
- if (!REGEX_TEST_TRAILING_SLASH.test(path11)) {
579
- return this.test(path11);
577
+ checkIgnore(path13) {
578
+ if (!REGEX_TEST_TRAILING_SLASH.test(path13)) {
579
+ return this.test(path13);
580
580
  }
581
- const slices = path11.split(SLASH).filter(Boolean);
581
+ const slices = path13.split(SLASH).filter(Boolean);
582
582
  slices.pop();
583
583
  if (slices.length) {
584
584
  const parent = this._t(
@@ -591,18 +591,18 @@ var require_ignore = __commonJS({
591
591
  return parent;
592
592
  }
593
593
  }
594
- return this._rules.test(path11, false, MODE_CHECK_IGNORE);
594
+ return this._rules.test(path13, false, MODE_CHECK_IGNORE);
595
595
  }
596
- _t(path11, cache, checkUnignored, slices) {
597
- if (path11 in cache) {
598
- return cache[path11];
596
+ _t(path13, cache, checkUnignored, slices) {
597
+ if (path13 in cache) {
598
+ return cache[path13];
599
599
  }
600
600
  if (!slices) {
601
- slices = path11.split(SLASH).filter(Boolean);
601
+ slices = path13.split(SLASH).filter(Boolean);
602
602
  }
603
603
  slices.pop();
604
604
  if (!slices.length) {
605
- return cache[path11] = this._rules.test(path11, checkUnignored, MODE_IGNORE);
605
+ return cache[path13] = this._rules.test(path13, checkUnignored, MODE_IGNORE);
606
606
  }
607
607
  const parent = this._t(
608
608
  slices.join(SLASH) + SLASH,
@@ -610,29 +610,29 @@ var require_ignore = __commonJS({
610
610
  checkUnignored,
611
611
  slices
612
612
  );
613
- return cache[path11] = parent.ignored ? parent : this._rules.test(path11, checkUnignored, MODE_IGNORE);
613
+ return cache[path13] = parent.ignored ? parent : this._rules.test(path13, checkUnignored, MODE_IGNORE);
614
614
  }
615
- ignores(path11) {
616
- return this._test(path11, this._ignoreCache, false).ignored;
615
+ ignores(path13) {
616
+ return this._test(path13, this._ignoreCache, false).ignored;
617
617
  }
618
618
  createFilter() {
619
- return (path11) => !this.ignores(path11);
619
+ return (path13) => !this.ignores(path13);
620
620
  }
621
621
  filter(paths) {
622
622
  return makeArray(paths).filter(this.createFilter());
623
623
  }
624
624
  // @returns {TestResult}
625
- test(path11) {
626
- return this._test(path11, this._testCache, true);
625
+ test(path13) {
626
+ return this._test(path13, this._testCache, true);
627
627
  }
628
628
  };
629
629
  var factory = (options) => new Ignore2(options);
630
- var isPathValid = (path11) => checkPath(path11 && checkPath.convert(path11), path11, RETURN_FALSE);
630
+ var isPathValid = (path13) => checkPath(path13 && checkPath.convert(path13), path13, RETURN_FALSE);
631
631
  var setupWindows = () => {
632
632
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
633
633
  checkPath.convert = makePosix;
634
634
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
635
- checkPath.isNotRelative = (path11) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path11) || isNotRelative(path11);
635
+ checkPath.isNotRelative = (path13) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path13) || isNotRelative(path13);
636
636
  };
637
637
  if (
638
638
  // Detect `process` so that it can run in browsers.
@@ -649,7 +649,7 @@ var require_ignore = __commonJS({
649
649
 
650
650
  // src/cli.ts
651
651
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
652
- var path10 = __toESM(require("path"), 1);
652
+ var path12 = __toESM(require("path"), 1);
653
653
 
654
654
  // src/config/constants.ts
655
655
  var DEFAULT_INCLUDE = [
@@ -658,12 +658,14 @@ var DEFAULT_INCLUDE = [
658
658
  "**/*.{go,rs,java,kt,scala}",
659
659
  "**/*.{c,cpp,cc,h,hpp}",
660
660
  "**/*.{rb,php,inc,swift}",
661
+ "**/*.{cls,trigger}",
661
662
  "**/*.{vue,svelte,astro}",
662
663
  "**/*.{sql,graphql,proto}",
663
664
  "**/*.{yaml,yml,toml}",
664
665
  "**/*.{md,mdx}",
665
666
  "**/*.{sh,bash,zsh}",
666
- "**/*.{txt,html,htm}"
667
+ "**/*.{txt,html,htm}",
668
+ "**/*.zig"
667
669
  ];
668
670
  var DEFAULT_EXCLUDE = [
669
671
  "**/node_modules/**",
@@ -728,7 +730,7 @@ var EMBEDDING_MODELS = {
728
730
  provider: "ollama",
729
731
  model: "nomic-embed-text",
730
732
  dimensions: 768,
731
- maxTokens: 8192,
733
+ maxTokens: 2048,
732
734
  costPer1MTokens: 0
733
735
  },
734
736
  "mxbai-embed-large": {
@@ -752,9 +754,15 @@ var EMBEDDING_MODELS = {
752
754
  var DEFAULT_PROVIDER_MODELS = {
753
755
  "github-copilot": "text-embedding-3-small",
754
756
  "openai": "text-embedding-3-small",
755
- "google": "text-embedding-005",
757
+ "google": "gemini-embedding-001",
756
758
  "ollama": "nomic-embed-text"
757
759
  };
760
+ var AUTO_DETECT_PROVIDER_ORDER = [
761
+ "ollama",
762
+ "github-copilot",
763
+ "openai",
764
+ "google"
765
+ ];
758
766
 
759
767
  // src/config/env-substitution.ts
760
768
  var ENV_REFERENCE_PATTERN = /^\{env:([A-Z_][A-Z0-9_]*)\}$/;
@@ -1013,9 +1021,12 @@ function getDefaultModelForProvider(provider) {
1013
1021
  return models[providerDefault];
1014
1022
  }
1015
1023
  var availableProviders = Object.keys(EMBEDDING_MODELS);
1024
+ var autoDetectProviders = AUTO_DETECT_PROVIDER_ORDER.filter(
1025
+ (provider) => provider in EMBEDDING_MODELS
1026
+ );
1016
1027
 
1017
1028
  // src/eval/cli.ts
1018
- var path8 = __toESM(require("path"), 1);
1029
+ var path10 = __toESM(require("path"), 1);
1019
1030
 
1020
1031
  // src/eval/compare.ts
1021
1032
  function metricDelta(current, baseline) {
@@ -1058,9 +1069,9 @@ function compareSummaries(current, baseline, againstPath) {
1058
1069
  // src/eval/reports.ts
1059
1070
  var import_fs = require("fs");
1060
1071
  var path = __toESM(require("path"), 1);
1061
- function assertFiniteNumber(value, path11) {
1072
+ function assertFiniteNumber(value, path13) {
1062
1073
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1063
- throw new Error(`${path11} must be a finite number`);
1074
+ throw new Error(`${path13} must be a finite number`);
1064
1075
  }
1065
1076
  return value;
1066
1077
  }
@@ -1234,16 +1245,352 @@ function buildPerQueryArtifact(perQuery) {
1234
1245
  }
1235
1246
 
1236
1247
  // src/eval/runner.ts
1237
- var import_fs7 = require("fs");
1238
- var import_fs8 = require("fs");
1239
1248
  var import_fs9 = require("fs");
1240
- var os3 = __toESM(require("os"), 1);
1241
- var path7 = __toESM(require("path"), 1);
1249
+ var import_fs10 = require("fs");
1250
+ var import_fs11 = require("fs");
1251
+ var import_fs12 = require("fs");
1252
+ var import_fs13 = require("fs");
1253
+ var os5 = __toESM(require("os"), 1);
1254
+ var path9 = __toESM(require("path"), 1);
1242
1255
  var import_perf_hooks2 = require("perf_hooks");
1243
1256
 
1257
+ // src/config/merger.ts
1258
+ var import_fs4 = require("fs");
1259
+ var os2 = __toESM(require("os"), 1);
1260
+ var path4 = __toESM(require("path"), 1);
1261
+
1262
+ // src/config/paths.ts
1263
+ var import_fs3 = require("fs");
1264
+ var os = __toESM(require("os"), 1);
1265
+ var path3 = __toESM(require("path"), 1);
1266
+
1267
+ // src/git/index.ts
1268
+ var import_fs2 = require("fs");
1269
+ var path2 = __toESM(require("path"), 1);
1270
+ function readPackedRefs(gitDir) {
1271
+ const packedRefsPath = path2.join(gitDir, "packed-refs");
1272
+ if (!(0, import_fs2.existsSync)(packedRefsPath)) {
1273
+ return [];
1274
+ }
1275
+ try {
1276
+ return (0, import_fs2.readFileSync)(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
1277
+ } catch {
1278
+ return [];
1279
+ }
1280
+ }
1281
+ function resolveCommonGitDir(gitDir) {
1282
+ const commonDirPath = path2.join(gitDir, "commondir");
1283
+ if (!(0, import_fs2.existsSync)(commonDirPath)) {
1284
+ return gitDir;
1285
+ }
1286
+ try {
1287
+ const raw = (0, import_fs2.readFileSync)(commonDirPath, "utf-8").trim();
1288
+ if (!raw) {
1289
+ return gitDir;
1290
+ }
1291
+ const resolved = path2.isAbsolute(raw) ? raw : path2.resolve(gitDir, raw);
1292
+ if ((0, import_fs2.existsSync)(resolved)) {
1293
+ return resolved;
1294
+ }
1295
+ } catch {
1296
+ return gitDir;
1297
+ }
1298
+ return gitDir;
1299
+ }
1300
+ function resolveWorktreeMainRepoRoot(repoRoot) {
1301
+ const gitDir = resolveGitDir(repoRoot);
1302
+ if (!gitDir) {
1303
+ return null;
1304
+ }
1305
+ const commonGitDir = resolveCommonGitDir(gitDir);
1306
+ if (commonGitDir === gitDir || path2.basename(commonGitDir) !== ".git") {
1307
+ return null;
1308
+ }
1309
+ const mainRepoRoot = path2.dirname(commonGitDir);
1310
+ if (!(0, import_fs2.existsSync)(mainRepoRoot)) {
1311
+ return null;
1312
+ }
1313
+ return path2.resolve(mainRepoRoot) === path2.resolve(repoRoot) ? null : mainRepoRoot;
1314
+ }
1315
+ function resolveGitDir(repoRoot) {
1316
+ const gitPath = path2.join(repoRoot, ".git");
1317
+ if (!(0, import_fs2.existsSync)(gitPath)) {
1318
+ return null;
1319
+ }
1320
+ try {
1321
+ const stat = (0, import_fs2.statSync)(gitPath);
1322
+ if (stat.isDirectory()) {
1323
+ return gitPath;
1324
+ }
1325
+ if (stat.isFile()) {
1326
+ const content = (0, import_fs2.readFileSync)(gitPath, "utf-8").trim();
1327
+ const match = content.match(/^gitdir:\s*(.+)$/);
1328
+ if (match) {
1329
+ const gitdir = match[1];
1330
+ const resolvedPath = path2.isAbsolute(gitdir) ? gitdir : path2.resolve(repoRoot, gitdir);
1331
+ if ((0, import_fs2.existsSync)(resolvedPath)) {
1332
+ return resolvedPath;
1333
+ }
1334
+ }
1335
+ }
1336
+ } catch {
1337
+ }
1338
+ return null;
1339
+ }
1340
+ function isGitRepo(dir) {
1341
+ return resolveGitDir(dir) !== null;
1342
+ }
1343
+ function getCurrentBranch(repoRoot) {
1344
+ const gitDir = resolveGitDir(repoRoot);
1345
+ if (!gitDir) {
1346
+ return null;
1347
+ }
1348
+ const headPath = path2.join(gitDir, "HEAD");
1349
+ if (!(0, import_fs2.existsSync)(headPath)) {
1350
+ return null;
1351
+ }
1352
+ try {
1353
+ const headContent = (0, import_fs2.readFileSync)(headPath, "utf-8").trim();
1354
+ const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
1355
+ if (match) {
1356
+ return match[1];
1357
+ }
1358
+ if (/^[0-9a-f]{40}$/i.test(headContent)) {
1359
+ return headContent.slice(0, 7);
1360
+ }
1361
+ return null;
1362
+ } catch {
1363
+ return null;
1364
+ }
1365
+ }
1366
+ function getBaseBranch(repoRoot) {
1367
+ const gitDir = resolveGitDir(repoRoot);
1368
+ const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
1369
+ const candidates = ["main", "master", "develop", "trunk"];
1370
+ if (refStoreDir) {
1371
+ for (const candidate of candidates) {
1372
+ const refPath = path2.join(refStoreDir, "refs", "heads", candidate);
1373
+ if ((0, import_fs2.existsSync)(refPath)) {
1374
+ return candidate;
1375
+ }
1376
+ const packedRefs = readPackedRefs(refStoreDir);
1377
+ if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
1378
+ return candidate;
1379
+ }
1380
+ }
1381
+ }
1382
+ return getCurrentBranch(repoRoot) ?? "main";
1383
+ }
1384
+ function getBranchOrDefault(repoRoot) {
1385
+ if (!isGitRepo(repoRoot)) {
1386
+ return "default";
1387
+ }
1388
+ return getCurrentBranch(repoRoot) ?? "default";
1389
+ }
1390
+
1391
+ // src/config/paths.ts
1392
+ var PROJECT_CONFIG_RELATIVE_PATH = path3.join(".opencode", "codebase-index.json");
1393
+ var PROJECT_INDEX_RELATIVE_PATH = path3.join(".opencode", "index");
1394
+ function resolveWorktreeFallbackPath(projectRoot, relativePath) {
1395
+ const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
1396
+ if (!mainRepoRoot) {
1397
+ return null;
1398
+ }
1399
+ const fallbackPath = path3.join(mainRepoRoot, relativePath);
1400
+ return (0, import_fs3.existsSync)(fallbackPath) ? fallbackPath : null;
1401
+ }
1402
+ function hasProjectConfig(projectRoot) {
1403
+ return (0, import_fs3.existsSync)(path3.join(projectRoot, PROJECT_CONFIG_RELATIVE_PATH));
1404
+ }
1405
+ function getGlobalIndexPath() {
1406
+ return path3.join(os.homedir(), ".opencode", "global-index");
1407
+ }
1408
+ function resolveProjectConfigPath(projectRoot) {
1409
+ const localConfigPath = path3.join(projectRoot, PROJECT_CONFIG_RELATIVE_PATH);
1410
+ if ((0, import_fs3.existsSync)(localConfigPath)) {
1411
+ return localConfigPath;
1412
+ }
1413
+ return resolveWorktreeFallbackPath(projectRoot, PROJECT_CONFIG_RELATIVE_PATH) ?? localConfigPath;
1414
+ }
1415
+ function resolveProjectIndexPath(projectRoot, scope) {
1416
+ if (scope === "global") {
1417
+ return getGlobalIndexPath();
1418
+ }
1419
+ const localIndexPath = path3.join(projectRoot, PROJECT_INDEX_RELATIVE_PATH);
1420
+ if ((0, import_fs3.existsSync)(localIndexPath)) {
1421
+ return localIndexPath;
1422
+ }
1423
+ if (hasProjectConfig(projectRoot)) {
1424
+ return localIndexPath;
1425
+ }
1426
+ return resolveWorktreeFallbackPath(projectRoot, PROJECT_INDEX_RELATIVE_PATH) ?? localIndexPath;
1427
+ }
1428
+
1429
+ // src/config/merger.ts
1430
+ function loadJsonFile(filePath) {
1431
+ try {
1432
+ if ((0, import_fs4.existsSync)(filePath)) {
1433
+ const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
1434
+ return JSON.parse(content);
1435
+ }
1436
+ } catch {
1437
+ }
1438
+ return null;
1439
+ }
1440
+ function normalizeRelativeConfigPath(candidate) {
1441
+ return candidate.replace(/\\/g, "/");
1442
+ }
1443
+ function rebasePathEntries(values, fromDir, toDir) {
1444
+ if (!Array.isArray(values)) {
1445
+ return [];
1446
+ }
1447
+ return values.filter((value) => typeof value === "string").map((value) => {
1448
+ const trimmed = value.trim();
1449
+ if (!trimmed || path4.isAbsolute(trimmed)) {
1450
+ return trimmed;
1451
+ }
1452
+ return normalizeRelativeConfigPath(path4.normalize(path4.relative(toDir, path4.resolve(fromDir, trimmed))));
1453
+ }).filter(Boolean);
1454
+ }
1455
+ function isWithinRoot(rootDir, targetPath) {
1456
+ const relativePath = path4.relative(rootDir, targetPath);
1457
+ return relativePath === "" || !relativePath.startsWith("..") && !path4.isAbsolute(relativePath);
1458
+ }
1459
+ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
1460
+ if (!Array.isArray(values)) {
1461
+ return [];
1462
+ }
1463
+ return values.filter((value) => typeof value === "string").map((value) => {
1464
+ const trimmed = value.trim();
1465
+ if (!trimmed) {
1466
+ return trimmed;
1467
+ }
1468
+ if (path4.isAbsolute(trimmed)) {
1469
+ if (isWithinRoot(sourceRoot, trimmed)) {
1470
+ return normalizeRelativeConfigPath(path4.normalize(path4.relative(sourceRoot, trimmed) || "."));
1471
+ }
1472
+ return path4.normalize(trimmed);
1473
+ }
1474
+ const resolvedFromSource = path4.resolve(sourceRoot, trimmed);
1475
+ if (isWithinRoot(sourceRoot, resolvedFromSource)) {
1476
+ return normalizeRelativeConfigPath(path4.normalize(trimmed));
1477
+ }
1478
+ return normalizeRelativeConfigPath(path4.normalize(path4.relative(targetRoot, resolvedFromSource)));
1479
+ }).filter(Boolean);
1480
+ }
1481
+ function materializeLocalProjectConfig(projectRoot, config) {
1482
+ const localConfigPath = path4.join(projectRoot, ".opencode", "codebase-index.json");
1483
+ (0, import_fs4.mkdirSync)(path4.dirname(localConfigPath), { recursive: true });
1484
+ (0, import_fs4.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
1485
+ return localConfigPath;
1486
+ }
1487
+ function loadProjectConfigLayer(projectRoot) {
1488
+ const projectConfigPath = resolveProjectConfigPath(projectRoot);
1489
+ const projectConfig = loadJsonFile(projectConfigPath);
1490
+ if (!projectConfig) {
1491
+ return {};
1492
+ }
1493
+ const normalizedConfig = { ...projectConfig };
1494
+ const projectConfigBaseDir = path4.dirname(path4.dirname(projectConfigPath));
1495
+ if (Array.isArray(normalizedConfig.knowledgeBases)) {
1496
+ normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
1497
+ normalizedConfig.knowledgeBases,
1498
+ projectConfigBaseDir,
1499
+ projectRoot
1500
+ );
1501
+ }
1502
+ return normalizedConfig;
1503
+ }
1504
+ function loadMergedConfig(projectRoot) {
1505
+ const globalConfigPath = os2.homedir() + "/.config/opencode/codebase-index.json";
1506
+ const globalConfig = loadJsonFile(globalConfigPath);
1507
+ const projectConfigPath = resolveProjectConfigPath(projectRoot);
1508
+ const projectConfig = loadJsonFile(projectConfigPath);
1509
+ const normalizedProjectConfig = loadProjectConfigLayer(projectRoot);
1510
+ if (!globalConfig && !projectConfig) {
1511
+ return {};
1512
+ }
1513
+ if (!projectConfig && globalConfig) {
1514
+ return globalConfig;
1515
+ }
1516
+ if (!globalConfig && projectConfig) {
1517
+ return normalizedProjectConfig;
1518
+ }
1519
+ const merged = { ...globalConfig };
1520
+ if (projectConfig && "embeddingProvider" in normalizedProjectConfig) {
1521
+ merged.embeddingProvider = normalizedProjectConfig.embeddingProvider;
1522
+ } else if (globalConfig && globalConfig.embeddingProvider) {
1523
+ merged.embeddingProvider = globalConfig.embeddingProvider;
1524
+ }
1525
+ if (projectConfig && "customProvider" in normalizedProjectConfig) {
1526
+ merged.customProvider = normalizedProjectConfig.customProvider;
1527
+ } else if (globalConfig && globalConfig.customProvider) {
1528
+ merged.customProvider = globalConfig.customProvider;
1529
+ }
1530
+ if (projectConfig && "embeddingModel" in normalizedProjectConfig) {
1531
+ merged.embeddingModel = normalizedProjectConfig.embeddingModel;
1532
+ } else if (globalConfig && globalConfig.embeddingModel) {
1533
+ merged.embeddingModel = globalConfig.embeddingModel;
1534
+ }
1535
+ if (projectConfig && "reranker" in normalizedProjectConfig) {
1536
+ merged.reranker = normalizedProjectConfig.reranker;
1537
+ } else if (globalConfig && globalConfig.reranker) {
1538
+ merged.reranker = globalConfig.reranker;
1539
+ }
1540
+ if (projectConfig && "include" in normalizedProjectConfig) {
1541
+ merged.include = normalizedProjectConfig.include;
1542
+ } else if (globalConfig && globalConfig.include) {
1543
+ merged.include = globalConfig.include;
1544
+ }
1545
+ if (projectConfig && "exclude" in normalizedProjectConfig) {
1546
+ merged.exclude = normalizedProjectConfig.exclude;
1547
+ } else if (globalConfig && globalConfig.exclude) {
1548
+ merged.exclude = globalConfig.exclude;
1549
+ }
1550
+ if (projectConfig && "indexing" in normalizedProjectConfig) {
1551
+ merged.indexing = normalizedProjectConfig.indexing;
1552
+ } else if (globalConfig && globalConfig.indexing) {
1553
+ merged.indexing = globalConfig.indexing;
1554
+ }
1555
+ if (projectConfig && "search" in normalizedProjectConfig) {
1556
+ merged.search = normalizedProjectConfig.search;
1557
+ } else if (globalConfig && globalConfig.search) {
1558
+ merged.search = globalConfig.search;
1559
+ }
1560
+ if (projectConfig && "debug" in normalizedProjectConfig) {
1561
+ merged.debug = normalizedProjectConfig.debug;
1562
+ } else if (globalConfig && globalConfig.debug) {
1563
+ merged.debug = globalConfig.debug;
1564
+ }
1565
+ if (projectConfig && "scope" in normalizedProjectConfig) {
1566
+ merged.scope = normalizedProjectConfig.scope;
1567
+ } else if (globalConfig && "scope" in globalConfig) {
1568
+ merged.scope = globalConfig.scope;
1569
+ }
1570
+ if (projectConfig) {
1571
+ for (const key of Object.keys(projectConfig)) {
1572
+ if (key === "embeddingProvider" || key === "customProvider" || key === "embeddingModel" || key === "reranker" || key === "include" || key === "exclude" || key === "indexing" || key === "search" || key === "debug" || key === "scope" || key === "knowledgeBases" || key === "additionalInclude") {
1573
+ continue;
1574
+ }
1575
+ merged[key] = normalizedProjectConfig[key];
1576
+ }
1577
+ }
1578
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
1579
+ const projectKbs = projectConfig ? Array.isArray(normalizedProjectConfig.knowledgeBases) ? normalizedProjectConfig.knowledgeBases : [] : [];
1580
+ const allKbs = [...globalKbs, ...projectKbs];
1581
+ const uniqueKbs = [...new Set(allKbs.map((p) => String(p).trim()))];
1582
+ merged.knowledgeBases = uniqueKbs;
1583
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
1584
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
1585
+ const allAdditional = [...globalAdditional, ...projectAdditional];
1586
+ const uniqueAdditional = [...new Set(allAdditional.map((p) => String(p).trim()))];
1587
+ merged.additionalInclude = uniqueAdditional;
1588
+ return merged;
1589
+ }
1590
+
1244
1591
  // src/indexer/index.ts
1245
- var import_fs5 = require("fs");
1246
- var path6 = __toESM(require("path"), 1);
1592
+ var import_fs7 = require("fs");
1593
+ var path8 = __toESM(require("path"), 1);
1247
1594
  var import_perf_hooks = require("perf_hooks");
1248
1595
 
1249
1596
  // node_modules/eventemitter3/index.mjs
@@ -1268,7 +1615,7 @@ function pTimeout(promise, options) {
1268
1615
  } = options;
1269
1616
  let timer;
1270
1617
  let abortHandler;
1271
- const wrappedPromise = new Promise((resolve6, reject) => {
1618
+ const wrappedPromise = new Promise((resolve8, reject) => {
1272
1619
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
1273
1620
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
1274
1621
  }
@@ -1282,7 +1629,7 @@ function pTimeout(promise, options) {
1282
1629
  };
1283
1630
  signal.addEventListener("abort", abortHandler, { once: true });
1284
1631
  }
1285
- promise.then(resolve6, reject);
1632
+ promise.then(resolve8, reject);
1286
1633
  if (milliseconds === Number.POSITIVE_INFINITY) {
1287
1634
  return;
1288
1635
  }
@@ -1290,7 +1637,7 @@ function pTimeout(promise, options) {
1290
1637
  timer = customTimers.setTimeout.call(void 0, () => {
1291
1638
  if (fallback) {
1292
1639
  try {
1293
- resolve6(fallback());
1640
+ resolve8(fallback());
1294
1641
  } catch (error) {
1295
1642
  reject(error);
1296
1643
  }
@@ -1300,7 +1647,7 @@ function pTimeout(promise, options) {
1300
1647
  promise.cancel();
1301
1648
  }
1302
1649
  if (message === false) {
1303
- resolve6();
1650
+ resolve8();
1304
1651
  } else if (message instanceof Error) {
1305
1652
  reject(message);
1306
1653
  } else {
@@ -1702,7 +2049,7 @@ var PQueue = class extends import_index.default {
1702
2049
  // Assign unique ID if not provided
1703
2050
  id: options.id ?? (this.#idAssigner++).toString()
1704
2051
  };
1705
- return new Promise((resolve6, reject) => {
2052
+ return new Promise((resolve8, reject) => {
1706
2053
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1707
2054
  let cleanupQueueAbortHandler = () => void 0;
1708
2055
  const run = async () => {
@@ -1742,7 +2089,7 @@ var PQueue = class extends import_index.default {
1742
2089
  })]);
1743
2090
  }
1744
2091
  const result = await operation;
1745
- resolve6(result);
2092
+ resolve8(result);
1746
2093
  this.emit("completed", result);
1747
2094
  } catch (error) {
1748
2095
  reject(error);
@@ -1930,13 +2277,13 @@ var PQueue = class extends import_index.default {
1930
2277
  });
1931
2278
  }
1932
2279
  async #onEvent(event, filter) {
1933
- return new Promise((resolve6) => {
2280
+ return new Promise((resolve8) => {
1934
2281
  const listener = () => {
1935
2282
  if (filter && !filter()) {
1936
2283
  return;
1937
2284
  }
1938
2285
  this.off(event, listener);
1939
- resolve6();
2286
+ resolve8();
1940
2287
  };
1941
2288
  this.on(event, listener);
1942
2289
  });
@@ -2222,7 +2569,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2222
2569
  const finalDelay = Math.min(delayTime, remainingTime);
2223
2570
  options.signal?.throwIfAborted();
2224
2571
  if (finalDelay > 0) {
2225
- await new Promise((resolve6, reject) => {
2572
+ await new Promise((resolve8, reject) => {
2226
2573
  const onAbort = () => {
2227
2574
  clearTimeout(timeoutToken);
2228
2575
  options.signal?.removeEventListener("abort", onAbort);
@@ -2230,7 +2577,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2230
2577
  };
2231
2578
  const timeoutToken = setTimeout(() => {
2232
2579
  options.signal?.removeEventListener("abort", onAbort);
2233
- resolve6();
2580
+ resolve8();
2234
2581
  }, finalDelay);
2235
2582
  if (options.unref) {
2236
2583
  timeoutToken.unref?.();
@@ -2291,17 +2638,17 @@ async function pRetry(input, options = {}) {
2291
2638
  }
2292
2639
 
2293
2640
  // src/embeddings/detector.ts
2294
- var import_fs2 = require("fs");
2295
- var path2 = __toESM(require("path"), 1);
2296
- var os = __toESM(require("os"), 1);
2641
+ var import_fs5 = require("fs");
2642
+ var path5 = __toESM(require("path"), 1);
2643
+ var os3 = __toESM(require("os"), 1);
2297
2644
  function getOpenCodeAuthPath() {
2298
- return path2.join(os.homedir(), ".local", "share", "opencode", "auth.json");
2645
+ return path5.join(os3.homedir(), ".local", "share", "opencode", "auth.json");
2299
2646
  }
2300
2647
  function loadOpenCodeAuth() {
2301
2648
  const authPath = getOpenCodeAuthPath();
2302
2649
  try {
2303
- if ((0, import_fs2.existsSync)(authPath)) {
2304
- return JSON.parse((0, import_fs2.readFileSync)(authPath, "utf-8"));
2650
+ if ((0, import_fs5.existsSync)(authPath)) {
2651
+ return JSON.parse((0, import_fs5.readFileSync)(authPath, "utf-8"));
2305
2652
  }
2306
2653
  } catch {
2307
2654
  }
@@ -2334,7 +2681,7 @@ async function detectEmbeddingProvider(preferredProvider, model) {
2334
2681
  );
2335
2682
  }
2336
2683
  async function tryDetectProvider() {
2337
- for (const provider of availableProviders) {
2684
+ for (const provider of autoDetectProviders) {
2338
2685
  const credentials = await getProviderCredentials(provider);
2339
2686
  if (credentials) {
2340
2687
  return {
@@ -2345,7 +2692,7 @@ async function tryDetectProvider() {
2345
2692
  }
2346
2693
  }
2347
2694
  throw new Error(
2348
- `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${availableProviders.join(", ")}.`
2695
+ `No embedding-capable provider found. Please authenticate with OpenCode using one of: ${autoDetectProviders.join(", ")}.`
2349
2696
  );
2350
2697
  }
2351
2698
  async function getProviderCredentials(provider) {
@@ -2665,11 +3012,12 @@ var GoogleEmbeddingProvider = class _GoogleEmbeddingProvider {
2665
3012
  return this.modelInfo;
2666
3013
  }
2667
3014
  };
2668
- var OllamaEmbeddingProvider = class {
3015
+ var OllamaEmbeddingProvider = class _OllamaEmbeddingProvider {
2669
3016
  constructor(credentials, modelInfo) {
2670
3017
  this.credentials = credentials;
2671
3018
  this.modelInfo = modelInfo;
2672
3019
  }
3020
+ static MIN_TRUNCATION_CHARS = 512;
2673
3021
  async embedQuery(query) {
2674
3022
  const result = await this.embedBatch([query]);
2675
3023
  return {
@@ -2684,43 +3032,116 @@ var OllamaEmbeddingProvider = class {
2684
3032
  tokensUsed: result.totalTokensUsed
2685
3033
  };
2686
3034
  }
2687
- async embedBatch(texts) {
2688
- const results = await Promise.all(
2689
- texts.map(async (text) => {
2690
- const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
2691
- method: "POST",
2692
- headers: {
2693
- "Content-Type": "application/json"
2694
- },
2695
- body: JSON.stringify({
2696
- model: this.modelInfo.model,
2697
- prompt: text
2698
- })
2699
- });
2700
- if (!response.ok) {
2701
- const error = await response.text();
2702
- throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
2703
- }
2704
- const data = await response.json();
2705
- return {
2706
- embedding: data.embedding,
2707
- tokensUsed: Math.ceil(text.length / 4)
2708
- };
2709
- })
2710
- );
2711
- return {
2712
- embeddings: results.map((r) => r.embedding),
2713
- totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
2714
- };
3035
+ estimateTokens(text) {
3036
+ return Math.ceil(text.length / 4);
2715
3037
  }
2716
- getModelInfo() {
2717
- return this.modelInfo;
3038
+ truncateToCharLimit(text, maxChars) {
3039
+ if (text.length <= maxChars) {
3040
+ return text;
3041
+ }
3042
+ return `${text.slice(0, Math.max(0, maxChars - 17))}
3043
+ ... [truncated]`;
2718
3044
  }
2719
- };
2720
- var CustomEmbeddingProvider = class {
2721
- constructor(credentials, modelInfo) {
2722
- this.credentials = credentials;
2723
- this.modelInfo = modelInfo;
3045
+ isContextLengthError(error) {
3046
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
3047
+ return message.includes("context length") && (message.includes("exceed") || message.includes("exceeded") || message.includes("too long")) || message.includes("input length exceeds the context length") || message.includes("context length exceeded");
3048
+ }
3049
+ buildTruncationCandidates(text) {
3050
+ const baseMaxChars = Math.max(1, this.modelInfo.maxTokens * 4);
3051
+ const candidateLimits = /* @__PURE__ */ new Set();
3052
+ const baselineLimit = text.length > baseMaxChars ? baseMaxChars : Math.max(
3053
+ _OllamaEmbeddingProvider.MIN_TRUNCATION_CHARS,
3054
+ Math.floor(text.length * 0.9)
3055
+ );
3056
+ if (baselineLimit < text.length) {
3057
+ candidateLimits.add(baselineLimit);
3058
+ }
3059
+ for (const factor of [0.75, 0.6, 0.45, 0.35, 0.25]) {
3060
+ const scaledLimit = Math.max(
3061
+ _OllamaEmbeddingProvider.MIN_TRUNCATION_CHARS,
3062
+ Math.floor(baselineLimit * factor)
3063
+ );
3064
+ if (scaledLimit < text.length) {
3065
+ candidateLimits.add(scaledLimit);
3066
+ }
3067
+ }
3068
+ candidateLimits.add(Math.min(text.length - 1, _OllamaEmbeddingProvider.MIN_TRUNCATION_CHARS));
3069
+ const candidates = [];
3070
+ const seen = /* @__PURE__ */ new Set();
3071
+ for (const limit of [...candidateLimits].sort((a, b) => b - a)) {
3072
+ if (limit <= 0 || limit >= text.length) {
3073
+ continue;
3074
+ }
3075
+ const truncated = this.truncateToCharLimit(text, limit);
3076
+ if (truncated === text || seen.has(truncated)) {
3077
+ continue;
3078
+ }
3079
+ seen.add(truncated);
3080
+ candidates.push(truncated);
3081
+ }
3082
+ return candidates;
3083
+ }
3084
+ async embedSingleWithFallback(text) {
3085
+ try {
3086
+ return await this.embedSingle(text);
3087
+ } catch (error) {
3088
+ if (!this.isContextLengthError(error)) {
3089
+ throw error;
3090
+ }
3091
+ let lastError = error;
3092
+ for (const truncated of this.buildTruncationCandidates(text)) {
3093
+ try {
3094
+ return await this.embedSingle(truncated);
3095
+ } catch (retryError) {
3096
+ if (!this.isContextLengthError(retryError)) {
3097
+ throw retryError;
3098
+ }
3099
+ lastError = retryError;
3100
+ }
3101
+ }
3102
+ throw lastError;
3103
+ }
3104
+ }
3105
+ async embedSingle(text) {
3106
+ const response = await fetch(`${this.credentials.baseUrl}/api/embeddings`, {
3107
+ method: "POST",
3108
+ headers: {
3109
+ "Content-Type": "application/json"
3110
+ },
3111
+ body: JSON.stringify({
3112
+ model: this.modelInfo.model,
3113
+ prompt: text,
3114
+ truncate: false
3115
+ })
3116
+ });
3117
+ if (!response.ok) {
3118
+ const error = await response.text();
3119
+ throw new Error(`Ollama embedding API error: ${response.status} - ${error}`);
3120
+ }
3121
+ const data = await response.json();
3122
+ return {
3123
+ embedding: data.embedding,
3124
+ tokensUsed: this.estimateTokens(text)
3125
+ };
3126
+ }
3127
+ async embedBatch(texts) {
3128
+ const results = [];
3129
+ for (const text of texts) {
3130
+ results.push(await this.embedSingleWithFallback(text));
3131
+ }
3132
+ return {
3133
+ embeddings: results.map((r) => r.embedding),
3134
+ totalTokensUsed: results.reduce((sum, r) => sum + r.tokensUsed, 0)
3135
+ };
3136
+ }
3137
+ getModelInfo() {
3138
+ return this.modelInfo;
3139
+ }
3140
+ };
3141
+ var CustomEmbeddingProvider = class {
3142
+ constructor(credentials, modelInfo) {
3143
+ this.credentials = credentials;
3144
+ this.modelInfo = modelInfo;
2724
3145
  }
2725
3146
  splitIntoRequestBatches(texts) {
2726
3147
  const maxBatchSize = this.modelInfo.maxBatchSize;
@@ -2911,8 +3332,8 @@ var SiliconFlowReranker = class {
2911
3332
 
2912
3333
  // src/utils/files.ts
2913
3334
  var import_ignore = __toESM(require_ignore(), 1);
2914
- var import_fs3 = require("fs");
2915
- var path3 = __toESM(require("path"), 1);
3335
+ var import_fs6 = require("fs");
3336
+ var path6 = __toESM(require("path"), 1);
2916
3337
  function createIgnoreFilter(projectRoot) {
2917
3338
  const ig = (0, import_ignore.default)();
2918
3339
  const defaultIgnores = [
@@ -2933,9 +3354,9 @@ function createIgnoreFilter(projectRoot) {
2933
3354
  "**/*build*/**"
2934
3355
  ];
2935
3356
  ig.add(defaultIgnores);
2936
- const gitignorePath = path3.join(projectRoot, ".gitignore");
2937
- if ((0, import_fs3.existsSync)(gitignorePath)) {
2938
- const gitignoreContent = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
3357
+ const gitignorePath = path6.join(projectRoot, ".gitignore");
3358
+ if ((0, import_fs6.existsSync)(gitignorePath)) {
3359
+ const gitignoreContent = (0, import_fs6.readFileSync)(gitignorePath, "utf-8");
2939
3360
  ig.add(gitignoreContent);
2940
3361
  }
2941
3362
  return ig;
@@ -2956,12 +3377,12 @@ function matchGlob(filePath, pattern) {
2956
3377
  return regex.test(filePath);
2957
3378
  }
2958
3379
  async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped, options, currentDepth = 0) {
2959
- const entries = await import_fs3.promises.readdir(dir, { withFileTypes: true });
3380
+ const entries = await import_fs6.promises.readdir(dir, { withFileTypes: true });
2960
3381
  const filesInDir = [];
2961
3382
  const subdirs = [];
2962
3383
  for (const entry of entries) {
2963
- const fullPath = path3.join(dir, entry.name);
2964
- const relativePath = path3.relative(projectRoot, fullPath);
3384
+ const fullPath = path6.join(dir, entry.name);
3385
+ const relativePath = path6.relative(projectRoot, fullPath);
2965
3386
  if (entry.name.startsWith(".") && entry.name !== "." && entry.name !== "..") {
2966
3387
  if (entry.isDirectory()) {
2967
3388
  skipped.push({ path: relativePath, reason: "excluded" });
@@ -2981,7 +3402,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2981
3402
  if (entry.isDirectory()) {
2982
3403
  subdirs.push({ fullPath, relativePath });
2983
3404
  } else if (entry.isFile()) {
2984
- const stat = await import_fs3.promises.stat(fullPath);
3405
+ const stat = await import_fs6.promises.stat(fullPath);
2985
3406
  if (stat.size > maxFileSize) {
2986
3407
  skipped.push({ path: relativePath, reason: "too_large" });
2987
3408
  continue;
@@ -3010,7 +3431,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
3010
3431
  yield f;
3011
3432
  }
3012
3433
  for (let i = options.maxFilesPerDirectory; i < filesInDir.length; i++) {
3013
- skipped.push({ path: path3.relative(projectRoot, filesInDir[i].path), reason: "excluded" });
3434
+ skipped.push({ path: path6.relative(projectRoot, filesInDir[i].path), reason: "excluded" });
3014
3435
  }
3015
3436
  const canRecurse = options.maxDepth === -1 || currentDepth < options.maxDepth;
3016
3437
  if (canRecurse) {
@@ -3050,14 +3471,14 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
3050
3471
  if (additionalRoots && additionalRoots.length > 0) {
3051
3472
  const normalizedRoots = /* @__PURE__ */ new Set();
3052
3473
  for (const kbRoot of additionalRoots) {
3053
- const resolved = path3.normalize(
3054
- path3.isAbsolute(kbRoot) ? kbRoot : path3.resolve(projectRoot, kbRoot)
3474
+ const resolved = path6.normalize(
3475
+ path6.isAbsolute(kbRoot) ? kbRoot : path6.resolve(projectRoot, kbRoot)
3055
3476
  );
3056
3477
  normalizedRoots.add(resolved);
3057
3478
  }
3058
3479
  for (const resolvedKbRoot of normalizedRoots) {
3059
3480
  try {
3060
- const stat = await import_fs3.promises.stat(resolvedKbRoot);
3481
+ const stat = await import_fs6.promises.stat(resolvedKbRoot);
3061
3482
  if (!stat.isDirectory()) {
3062
3483
  skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3063
3484
  continue;
@@ -3441,14 +3862,14 @@ function initializeLogger(config) {
3441
3862
  }
3442
3863
 
3443
3864
  // src/native/index.ts
3444
- var path4 = __toESM(require("path"), 1);
3445
- var os2 = __toESM(require("os"), 1);
3865
+ var path7 = __toESM(require("path"), 1);
3866
+ var os4 = __toESM(require("os"), 1);
3446
3867
  var module2 = __toESM(require("module"), 1);
3447
3868
  var import_url = require("url");
3448
3869
  var import_meta = {};
3449
3870
  function getNativeBinding() {
3450
- const platform2 = os2.platform();
3451
- const arch2 = os2.arch();
3871
+ const platform2 = os4.platform();
3872
+ const arch2 = os4.arch();
3452
3873
  let bindingName;
3453
3874
  if (platform2 === "darwin" && arch2 === "arm64") {
3454
3875
  bindingName = "codebase-index-native.darwin-arm64.node";
@@ -3466,18 +3887,19 @@ function getNativeBinding() {
3466
3887
  let currentDir;
3467
3888
  let requireTarget;
3468
3889
  if (typeof import_meta !== "undefined" && import_meta.url) {
3469
- currentDir = path4.dirname((0, import_url.fileURLToPath)(import_meta.url));
3890
+ currentDir = path7.dirname((0, import_url.fileURLToPath)(import_meta.url));
3470
3891
  requireTarget = import_meta.url;
3471
3892
  } else if (typeof __dirname !== "undefined") {
3472
3893
  currentDir = __dirname;
3473
3894
  requireTarget = __filename;
3474
3895
  } else {
3475
3896
  currentDir = process.cwd();
3476
- requireTarget = path4.join(currentDir, "index.js");
3897
+ requireTarget = path7.join(currentDir, "index.js");
3477
3898
  }
3478
- const isDevMode = currentDir.includes("/src/native");
3479
- const packageRoot = isDevMode ? path4.resolve(currentDir, "../..") : path4.resolve(currentDir, "..");
3480
- const nativePath = path4.join(packageRoot, "native", bindingName);
3899
+ const normalizedDir = currentDir.replace(/\\/g, "/");
3900
+ const isDevMode = normalizedDir.includes("/src/native") || currentDir.includes(path7.join("src", "native"));
3901
+ const packageRoot = isDevMode ? path7.resolve(currentDir, "../..") : path7.resolve(currentDir, "..");
3902
+ const nativePath = path7.join(packageRoot, "native", bindingName);
3481
3903
  const require2 = module2.createRequire(requireTarget);
3482
3904
  return require2(nativePath);
3483
3905
  }
@@ -3508,11 +3930,20 @@ function createMockNativeBinding() {
3508
3930
  constructor() {
3509
3931
  throw error;
3510
3932
  }
3933
+ serialize() {
3934
+ throw error;
3935
+ }
3936
+ deserialize() {
3937
+ throw error;
3938
+ }
3511
3939
  },
3512
3940
  Database: class {
3513
3941
  constructor() {
3514
3942
  throw error;
3515
3943
  }
3944
+ close() {
3945
+ throw error;
3946
+ }
3516
3947
  }
3517
3948
  };
3518
3949
  }
@@ -3645,7 +4076,7 @@ var MAX_SINGLE_CHUNK_TOKENS = 2e3;
3645
4076
  function estimateTokens2(text) {
3646
4077
  return Math.ceil(text.length / CHARS_PER_TOKEN);
3647
4078
  }
3648
- function createEmbeddingText(chunk, filePath) {
4079
+ function getEmbeddingHeaderParts(chunk, filePath) {
3649
4080
  const parts = [];
3650
4081
  const fileName = filePath.split("/").pop() || filePath;
3651
4082
  const dirPath = filePath.split("/").slice(-3, -1).join("/");
@@ -3692,23 +4123,52 @@ function createEmbeddingText(chunk, filePath) {
3692
4123
  if (semanticHints.length > 0) {
3693
4124
  parts.push(`Purpose: ${semanticHints.join(", ")}`);
3694
4125
  }
3695
- parts.push("");
3696
- let content = chunk.content;
3697
- const headerLength = parts.join("\n").length;
3698
- const maxContentChars = MAX_SINGLE_CHUNK_TOKENS * CHARS_PER_TOKEN - headerLength;
3699
- if (content.length > maxContentChars) {
3700
- content = content.slice(0, maxContentChars) + "\n... [truncated]";
4126
+ return parts;
4127
+ }
4128
+ function buildEmbeddingText(headerParts, content, partIndex, partCount) {
4129
+ const parts = [...headerParts];
4130
+ if (partCount && partCount > 1 && partIndex) {
4131
+ parts.push(`Part ${partIndex}/${partCount}`);
3701
4132
  }
4133
+ parts.push("");
3702
4134
  parts.push(content);
3703
4135
  return parts.join("\n");
3704
4136
  }
3705
- function createDynamicBatches(chunks) {
4137
+ function splitOversizedContent(content, maxContentChars) {
4138
+ if (content.length <= maxContentChars) {
4139
+ return [content];
4140
+ }
4141
+ const overlapChars = Math.max(CHARS_PER_TOKEN * 32, Math.min(Math.floor(maxContentChars * 0.15), CHARS_PER_TOKEN * 128));
4142
+ const stepChars = Math.max(1, maxContentChars - overlapChars);
4143
+ const segments = [];
4144
+ for (let start = 0; start < content.length; start += stepChars) {
4145
+ const end = Math.min(content.length, start + maxContentChars);
4146
+ segments.push(content.slice(start, end));
4147
+ if (end >= content.length) {
4148
+ break;
4149
+ }
4150
+ }
4151
+ return segments;
4152
+ }
4153
+ function createEmbeddingTexts(chunk, filePath, maxChunkTokens = MAX_SINGLE_CHUNK_TOKENS) {
4154
+ const headerParts = getEmbeddingHeaderParts(chunk, filePath);
4155
+ const headerLength = buildEmbeddingText(headerParts, "", 1, 9).length;
4156
+ const maxContentChars = Math.max(1, maxChunkTokens * CHARS_PER_TOKEN - headerLength);
4157
+ const segments = splitOversizedContent(chunk.content, maxContentChars);
4158
+ if (segments.length === 1) {
4159
+ return [buildEmbeddingText(headerParts, segments[0])];
4160
+ }
4161
+ return segments.map((segment, index) => buildEmbeddingText(headerParts, segment, index + 1, segments.length));
4162
+ }
4163
+ function createDynamicBatches(chunks, options = {}) {
3706
4164
  const batches = [];
3707
4165
  let currentBatch = [];
3708
4166
  let currentTokens = 0;
4167
+ const maxBatchTokens = Math.max(1, options.maxBatchTokens ?? MAX_BATCH_TOKENS);
4168
+ const maxBatchItems = Math.max(1, options.maxBatchItems ?? Number.MAX_SAFE_INTEGER);
3709
4169
  for (const chunk of chunks) {
3710
- const chunkTokens = estimateTokens2(chunk.text);
3711
- if (currentBatch.length > 0 && currentTokens + chunkTokens > MAX_BATCH_TOKENS) {
4170
+ const chunkTokens = chunk.tokenCount ?? estimateTokens2(chunk.text);
4171
+ if (currentBatch.length > 0 && (currentTokens + chunkTokens > maxBatchTokens || currentBatch.length >= maxBatchItems)) {
3712
4172
  batches.push(currentBatch);
3713
4173
  currentBatch = [];
3714
4174
  currentTokens = 0;
@@ -3827,6 +4287,12 @@ var InvertedIndex = class {
3827
4287
  save() {
3828
4288
  this.inner.save();
3829
4289
  }
4290
+ serialize() {
4291
+ return this.inner.serialize();
4292
+ }
4293
+ deserialize(json) {
4294
+ this.inner.deserialize(json);
4295
+ }
3830
4296
  addChunk(chunkId, content) {
3831
4297
  this.inner.addChunk(chunkId, content);
3832
4298
  }
@@ -3853,267 +4319,263 @@ var InvertedIndex = class {
3853
4319
  };
3854
4320
  var Database = class {
3855
4321
  inner;
4322
+ closed = false;
3856
4323
  constructor(dbPath) {
3857
4324
  this.inner = new native.Database(dbPath);
3858
4325
  }
4326
+ throwIfClosed() {
4327
+ if (this.closed) {
4328
+ throw new Error("Database is closed");
4329
+ }
4330
+ }
4331
+ close() {
4332
+ if (this.closed) {
4333
+ return;
4334
+ }
4335
+ if (typeof this.inner.close === "function") {
4336
+ this.inner.close();
4337
+ }
4338
+ this.closed = true;
4339
+ }
3859
4340
  embeddingExists(contentHash) {
4341
+ this.throwIfClosed();
3860
4342
  return this.inner.embeddingExists(contentHash);
3861
4343
  }
3862
4344
  getEmbedding(contentHash) {
4345
+ this.throwIfClosed();
3863
4346
  return this.inner.getEmbedding(contentHash) ?? null;
3864
4347
  }
3865
4348
  upsertEmbedding(contentHash, embedding, chunkText, model) {
4349
+ this.throwIfClosed();
3866
4350
  this.inner.upsertEmbedding(contentHash, embedding, chunkText, model);
3867
4351
  }
3868
4352
  upsertEmbeddingsBatch(items) {
4353
+ this.throwIfClosed();
3869
4354
  if (items.length === 0) return;
3870
4355
  this.inner.upsertEmbeddingsBatch(items);
3871
4356
  }
3872
4357
  getMissingEmbeddings(contentHashes) {
4358
+ this.throwIfClosed();
3873
4359
  return this.inner.getMissingEmbeddings(contentHashes);
3874
4360
  }
3875
4361
  upsertChunk(chunk) {
4362
+ this.throwIfClosed();
3876
4363
  this.inner.upsertChunk(chunk);
3877
4364
  }
3878
4365
  upsertChunksBatch(chunks) {
4366
+ this.throwIfClosed();
3879
4367
  if (chunks.length === 0) return;
3880
4368
  this.inner.upsertChunksBatch(chunks);
3881
4369
  }
3882
4370
  getChunk(chunkId) {
4371
+ this.throwIfClosed();
3883
4372
  return this.inner.getChunk(chunkId) ?? null;
3884
4373
  }
3885
4374
  getChunksByFile(filePath) {
4375
+ this.throwIfClosed();
3886
4376
  return this.inner.getChunksByFile(filePath);
3887
4377
  }
3888
4378
  getChunksByName(name) {
4379
+ this.throwIfClosed();
3889
4380
  return this.inner.getChunksByName(name);
3890
4381
  }
3891
4382
  getChunksByNameCi(name) {
4383
+ this.throwIfClosed();
3892
4384
  return this.inner.getChunksByNameCi(name);
3893
4385
  }
3894
4386
  deleteChunksByFile(filePath) {
4387
+ this.throwIfClosed();
3895
4388
  return this.inner.deleteChunksByFile(filePath);
3896
4389
  }
4390
+ deleteChunksByIds(chunkIds) {
4391
+ this.throwIfClosed();
4392
+ if (chunkIds.length === 0) return 0;
4393
+ return this.inner.deleteChunksByIds(chunkIds);
4394
+ }
3897
4395
  addChunksToBranch(branch, chunkIds) {
4396
+ this.throwIfClosed();
3898
4397
  this.inner.addChunksToBranch(branch, chunkIds);
3899
4398
  }
3900
4399
  addChunksToBranchBatch(branch, chunkIds) {
4400
+ this.throwIfClosed();
3901
4401
  if (chunkIds.length === 0) return;
3902
4402
  this.inner.addChunksToBranchBatch(branch, chunkIds);
3903
4403
  }
3904
4404
  clearBranch(branch) {
4405
+ this.throwIfClosed();
3905
4406
  return this.inner.clearBranch(branch);
3906
4407
  }
4408
+ deleteBranchChunksByChunkIds(chunkIds) {
4409
+ this.throwIfClosed();
4410
+ if (chunkIds.length === 0) return 0;
4411
+ return this.inner.deleteBranchChunksByChunkIds(chunkIds);
4412
+ }
4413
+ deleteBranchChunksForBranch(branch, chunkIds) {
4414
+ this.throwIfClosed();
4415
+ if (chunkIds.length === 0) return 0;
4416
+ return this.inner.deleteBranchChunksForBranch(branch, chunkIds);
4417
+ }
3907
4418
  getBranchChunkIds(branch) {
4419
+ this.throwIfClosed();
3908
4420
  return this.inner.getBranchChunkIds(branch);
3909
4421
  }
3910
4422
  getBranchDelta(branch, baseBranch) {
4423
+ this.throwIfClosed();
3911
4424
  return this.inner.getBranchDelta(branch, baseBranch);
3912
4425
  }
4426
+ getReferencedChunkIds(chunkIds) {
4427
+ this.throwIfClosed();
4428
+ if (chunkIds.length === 0) return [];
4429
+ return this.inner.getReferencedChunkIds(chunkIds);
4430
+ }
3913
4431
  chunkExistsOnBranch(branch, chunkId) {
4432
+ this.throwIfClosed();
3914
4433
  return this.inner.chunkExistsOnBranch(branch, chunkId);
3915
4434
  }
3916
4435
  getAllBranches() {
4436
+ this.throwIfClosed();
3917
4437
  return this.inner.getAllBranches();
3918
4438
  }
3919
4439
  getMetadata(key) {
4440
+ this.throwIfClosed();
3920
4441
  return this.inner.getMetadata(key) ?? null;
3921
4442
  }
3922
4443
  setMetadata(key, value) {
4444
+ this.throwIfClosed();
3923
4445
  this.inner.setMetadata(key, value);
3924
4446
  }
3925
4447
  deleteMetadata(key) {
4448
+ this.throwIfClosed();
3926
4449
  return this.inner.deleteMetadata(key);
3927
4450
  }
4451
+ clearAllIndexedData() {
4452
+ this.throwIfClosed();
4453
+ this.inner.clearAllIndexedData();
4454
+ }
4455
+ clearCallEdgeTargetsForSymbols(symbolIds) {
4456
+ this.throwIfClosed();
4457
+ if (symbolIds.length === 0) return 0;
4458
+ return this.inner.clearCallEdgeTargetsForSymbols(symbolIds);
4459
+ }
3928
4460
  gcOrphanEmbeddings() {
4461
+ this.throwIfClosed();
3929
4462
  return this.inner.gcOrphanEmbeddings();
3930
4463
  }
3931
4464
  gcOrphanChunks() {
4465
+ this.throwIfClosed();
3932
4466
  return this.inner.gcOrphanChunks();
3933
4467
  }
3934
4468
  getStats() {
4469
+ this.throwIfClosed();
3935
4470
  return this.inner.getStats();
3936
4471
  }
3937
4472
  // ── Symbol methods ──────────────────────────────────────────────
3938
4473
  upsertSymbol(symbol) {
4474
+ this.throwIfClosed();
3939
4475
  this.inner.upsertSymbol(symbol);
3940
4476
  }
3941
4477
  upsertSymbolsBatch(symbols) {
4478
+ this.throwIfClosed();
3942
4479
  if (symbols.length === 0) return;
3943
4480
  this.inner.upsertSymbolsBatch(symbols);
3944
4481
  }
3945
4482
  getSymbolsByFile(filePath) {
4483
+ this.throwIfClosed();
3946
4484
  return this.inner.getSymbolsByFile(filePath);
3947
4485
  }
3948
4486
  getSymbolByName(name, filePath) {
4487
+ this.throwIfClosed();
3949
4488
  return this.inner.getSymbolByName(name, filePath) ?? null;
3950
4489
  }
3951
4490
  getSymbolsByName(name) {
4491
+ this.throwIfClosed();
3952
4492
  return this.inner.getSymbolsByName(name);
3953
4493
  }
3954
4494
  getSymbolsByNameCi(name) {
4495
+ this.throwIfClosed();
3955
4496
  return this.inner.getSymbolsByNameCi(name);
3956
4497
  }
3957
4498
  deleteSymbolsByFile(filePath) {
4499
+ this.throwIfClosed();
3958
4500
  return this.inner.deleteSymbolsByFile(filePath);
3959
4501
  }
3960
4502
  // ── Call Edge methods ────────────────────────────────────────────
3961
4503
  upsertCallEdge(edge) {
4504
+ this.throwIfClosed();
3962
4505
  this.inner.upsertCallEdge(edge);
3963
4506
  }
3964
4507
  upsertCallEdgesBatch(edges) {
4508
+ this.throwIfClosed();
3965
4509
  if (edges.length === 0) return;
3966
4510
  this.inner.upsertCallEdgesBatch(edges);
3967
4511
  }
3968
4512
  getCallers(targetName, branch) {
4513
+ this.throwIfClosed();
3969
4514
  return this.inner.getCallers(targetName, branch);
3970
4515
  }
3971
4516
  getCallersWithContext(targetName, branch) {
4517
+ this.throwIfClosed();
3972
4518
  return this.inner.getCallersWithContext(targetName, branch);
3973
4519
  }
3974
4520
  getCallees(symbolId, branch) {
4521
+ this.throwIfClosed();
3975
4522
  return this.inner.getCallees(symbolId, branch);
3976
4523
  }
3977
4524
  deleteCallEdgesByFile(filePath) {
4525
+ this.throwIfClosed();
3978
4526
  return this.inner.deleteCallEdgesByFile(filePath);
3979
4527
  }
3980
4528
  resolveCallEdge(edgeId, toSymbolId) {
4529
+ this.throwIfClosed();
3981
4530
  this.inner.resolveCallEdge(edgeId, toSymbolId);
3982
4531
  }
3983
4532
  // ── Branch Symbol methods ────────────────────────────────────────
3984
4533
  addSymbolsToBranch(branch, symbolIds) {
4534
+ this.throwIfClosed();
3985
4535
  this.inner.addSymbolsToBranch(branch, symbolIds);
3986
4536
  }
3987
4537
  addSymbolsToBranchBatch(branch, symbolIds) {
4538
+ this.throwIfClosed();
3988
4539
  if (symbolIds.length === 0) return;
3989
4540
  this.inner.addSymbolsToBranchBatch(branch, symbolIds);
3990
4541
  }
3991
4542
  getBranchSymbolIds(branch) {
4543
+ this.throwIfClosed();
3992
4544
  return this.inner.getBranchSymbolIds(branch);
3993
4545
  }
3994
4546
  clearBranchSymbols(branch) {
4547
+ this.throwIfClosed();
3995
4548
  return this.inner.clearBranchSymbols(branch);
3996
4549
  }
4550
+ getReferencedSymbolIds(symbolIds) {
4551
+ this.throwIfClosed();
4552
+ if (symbolIds.length === 0) return [];
4553
+ return this.inner.getReferencedSymbolIds(symbolIds);
4554
+ }
4555
+ deleteBranchSymbolsBySymbolIds(symbolIds) {
4556
+ this.throwIfClosed();
4557
+ if (symbolIds.length === 0) return 0;
4558
+ return this.inner.deleteBranchSymbolsBySymbolIds(symbolIds);
4559
+ }
4560
+ deleteBranchSymbolsForBranch(branch, symbolIds) {
4561
+ this.throwIfClosed();
4562
+ if (symbolIds.length === 0) return 0;
4563
+ return this.inner.deleteBranchSymbolsForBranch(branch, symbolIds);
4564
+ }
3997
4565
  // ── GC methods for symbols/edges ─────────────────────────────────
3998
4566
  gcOrphanSymbols() {
4567
+ this.throwIfClosed();
3999
4568
  return this.inner.gcOrphanSymbols();
4000
4569
  }
4001
4570
  gcOrphanCallEdges() {
4571
+ this.throwIfClosed();
4002
4572
  return this.inner.gcOrphanCallEdges();
4003
4573
  }
4004
4574
  };
4005
4575
 
4006
- // src/git/index.ts
4007
- var import_fs4 = require("fs");
4008
- var path5 = __toESM(require("path"), 1);
4009
- function readPackedRefs(gitDir) {
4010
- const packedRefsPath = path5.join(gitDir, "packed-refs");
4011
- if (!(0, import_fs4.existsSync)(packedRefsPath)) {
4012
- return [];
4013
- }
4014
- try {
4015
- return (0, import_fs4.readFileSync)(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
4016
- } catch {
4017
- return [];
4018
- }
4019
- }
4020
- function resolveCommonGitDir(gitDir) {
4021
- const commonDirPath = path5.join(gitDir, "commondir");
4022
- if (!(0, import_fs4.existsSync)(commonDirPath)) {
4023
- return gitDir;
4024
- }
4025
- try {
4026
- const raw = (0, import_fs4.readFileSync)(commonDirPath, "utf-8").trim();
4027
- if (!raw) {
4028
- return gitDir;
4029
- }
4030
- const resolved = path5.isAbsolute(raw) ? raw : path5.resolve(gitDir, raw);
4031
- if ((0, import_fs4.existsSync)(resolved)) {
4032
- return resolved;
4033
- }
4034
- } catch {
4035
- return gitDir;
4036
- }
4037
- return gitDir;
4038
- }
4039
- function resolveGitDir(repoRoot) {
4040
- const gitPath = path5.join(repoRoot, ".git");
4041
- if (!(0, import_fs4.existsSync)(gitPath)) {
4042
- return null;
4043
- }
4044
- try {
4045
- const stat = (0, import_fs4.statSync)(gitPath);
4046
- if (stat.isDirectory()) {
4047
- return gitPath;
4048
- }
4049
- if (stat.isFile()) {
4050
- const content = (0, import_fs4.readFileSync)(gitPath, "utf-8").trim();
4051
- const match = content.match(/^gitdir:\s*(.+)$/);
4052
- if (match) {
4053
- const gitdir = match[1];
4054
- const resolvedPath = path5.isAbsolute(gitdir) ? gitdir : path5.resolve(repoRoot, gitdir);
4055
- if ((0, import_fs4.existsSync)(resolvedPath)) {
4056
- return resolvedPath;
4057
- }
4058
- }
4059
- }
4060
- } catch {
4061
- }
4062
- return null;
4063
- }
4064
- function isGitRepo(dir) {
4065
- return resolveGitDir(dir) !== null;
4066
- }
4067
- function getCurrentBranch(repoRoot) {
4068
- const gitDir = resolveGitDir(repoRoot);
4069
- if (!gitDir) {
4070
- return null;
4071
- }
4072
- const headPath = path5.join(gitDir, "HEAD");
4073
- if (!(0, import_fs4.existsSync)(headPath)) {
4074
- return null;
4075
- }
4076
- try {
4077
- const headContent = (0, import_fs4.readFileSync)(headPath, "utf-8").trim();
4078
- const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
4079
- if (match) {
4080
- return match[1];
4081
- }
4082
- if (/^[0-9a-f]{40}$/i.test(headContent)) {
4083
- return headContent.slice(0, 7);
4084
- }
4085
- return null;
4086
- } catch {
4087
- return null;
4088
- }
4089
- }
4090
- function getBaseBranch(repoRoot) {
4091
- const gitDir = resolveGitDir(repoRoot);
4092
- const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
4093
- const candidates = ["main", "master", "develop", "trunk"];
4094
- if (refStoreDir) {
4095
- for (const candidate of candidates) {
4096
- const refPath = path5.join(refStoreDir, "refs", "heads", candidate);
4097
- if ((0, import_fs4.existsSync)(refPath)) {
4098
- return candidate;
4099
- }
4100
- const packedRefs = readPackedRefs(refStoreDir);
4101
- if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
4102
- return candidate;
4103
- }
4104
- }
4105
- }
4106
- return getCurrentBranch(repoRoot) ?? "main";
4107
- }
4108
- function getBranchOrDefault(repoRoot) {
4109
- if (!isGitRepo(repoRoot)) {
4110
- return "default";
4111
- }
4112
- return getCurrentBranch(repoRoot) ?? "default";
4113
- }
4114
-
4115
4576
  // src/indexer/index.ts
4116
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php"]);
4577
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig"]);
4578
+ var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
4117
4579
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4118
4580
  "function_declaration",
4119
4581
  "function",
@@ -4135,7 +4597,11 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4135
4597
  "enum_item",
4136
4598
  "trait_item",
4137
4599
  "mod_item",
4138
- "trait_declaration"
4600
+ "trait_declaration",
4601
+ "trigger_declaration",
4602
+ "test_declaration",
4603
+ "struct_declaration",
4604
+ "union_declaration"
4139
4605
  ]);
4140
4606
  function float32ArrayToBuffer(arr) {
4141
4607
  const float32 = new Float32Array(arr);
@@ -4160,12 +4626,192 @@ function isRateLimitError(error) {
4160
4626
  const message = getErrorMessage(error);
4161
4627
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
4162
4628
  }
4629
+ function getSafeEmbeddingChunkTokenLimit(provider) {
4630
+ const providerMaxTokens = provider.modelInfo.maxTokens;
4631
+ const maxChunkTokens = Math.max(256, Math.floor(providerMaxTokens * 0.75));
4632
+ return Math.min(2e3, maxChunkTokens);
4633
+ }
4634
+ function getDynamicBatchOptions(provider) {
4635
+ if (provider.provider === "ollama") {
4636
+ return {
4637
+ maxBatchTokens: provider.modelInfo.maxTokens,
4638
+ maxBatchItems: 1
4639
+ };
4640
+ }
4641
+ return {};
4642
+ }
4643
+ function isSqliteCorruptionError(error) {
4644
+ const message = getErrorMessage(error).toLowerCase();
4645
+ return message.includes("database disk image is malformed") || message.includes("file is not a database") || message.includes("database schema is corrupt") || message.includes("sqlite_corrupt");
4646
+ }
4647
+ var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
4163
4648
  var INDEX_METADATA_VERSION = "1";
4649
+ var EMBEDDING_STRATEGY_VERSION = "2";
4164
4650
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
4651
+ var RANK_HYBRID_CACHE_LIMIT = 256;
4652
+ function createPendingChunkStorageText(texts) {
4653
+ const primaryText = texts[0]?.text ?? "";
4654
+ if (texts.length <= 1) {
4655
+ return primaryText;
4656
+ }
4657
+ return `${primaryText}
4658
+
4659
+ ... [split into ${texts.length} parts for embedding]`;
4660
+ }
4661
+ function normalizePendingChunk(rawChunk, maxChunkTokens) {
4662
+ if (!rawChunk || typeof rawChunk !== "object") {
4663
+ return null;
4664
+ }
4665
+ const chunk = rawChunk;
4666
+ if (typeof chunk.id !== "string" || typeof chunk.contentHash !== "string" || !chunk.metadata || typeof chunk.metadata !== "object") {
4667
+ return null;
4668
+ }
4669
+ const texts = Array.isArray(chunk.texts) ? chunk.texts.map((entry) => {
4670
+ if (!entry || typeof entry.text !== "string") {
4671
+ return null;
4672
+ }
4673
+ return {
4674
+ text: entry.text,
4675
+ tokenCount: typeof entry.tokenCount === "number" && Number.isFinite(entry.tokenCount) ? entry.tokenCount : estimateTokens2(entry.text)
4676
+ };
4677
+ }).filter((entry) => entry !== null) : [];
4678
+ if (texts.length === 0 && typeof chunk.text === "string") {
4679
+ if (typeof chunk.content === "string" && chunk.content.length > 0 && chunk.metadata && typeof chunk.metadata === "object") {
4680
+ const metadata = chunk.metadata;
4681
+ const rebuiltChunk = {
4682
+ content: chunk.content,
4683
+ startLine: typeof metadata.startLine === "number" ? metadata.startLine : 1,
4684
+ endLine: typeof metadata.endLine === "number" ? metadata.endLine : 1,
4685
+ chunkType: typeof metadata.chunkType === "string" ? metadata.chunkType : "other",
4686
+ name: typeof metadata.name === "string" ? metadata.name : void 0,
4687
+ language: typeof metadata.language === "string" ? metadata.language : "text"
4688
+ };
4689
+ const filePath = typeof metadata.filePath === "string" ? metadata.filePath : "unknown";
4690
+ texts.push(
4691
+ ...createEmbeddingTexts(rebuiltChunk, filePath, maxChunkTokens).map((text) => ({
4692
+ text,
4693
+ tokenCount: estimateTokens2(text)
4694
+ }))
4695
+ );
4696
+ } else {
4697
+ texts.push({
4698
+ text: chunk.text,
4699
+ tokenCount: estimateTokens2(chunk.text)
4700
+ });
4701
+ }
4702
+ }
4703
+ if (texts.length === 0) {
4704
+ return null;
4705
+ }
4706
+ return {
4707
+ id: chunk.id,
4708
+ texts,
4709
+ storageText: typeof chunk.storageText === "string" ? chunk.storageText : createPendingChunkStorageText(texts),
4710
+ content: typeof chunk.content === "string" ? chunk.content : "",
4711
+ contentHash: chunk.contentHash,
4712
+ metadata: chunk.metadata
4713
+ };
4714
+ }
4715
+ function getPendingChunkFilePath(rawChunk) {
4716
+ if (!rawChunk || typeof rawChunk !== "object") {
4717
+ return null;
4718
+ }
4719
+ const chunk = rawChunk;
4720
+ if (!chunk.metadata || typeof chunk.metadata !== "object") {
4721
+ return null;
4722
+ }
4723
+ const metadata = chunk.metadata;
4724
+ return typeof metadata.filePath === "string" ? metadata.filePath : null;
4725
+ }
4726
+ function normalizeFailedBatch(batch, maxChunkTokens) {
4727
+ const chunks = batch.chunks.map((chunk) => normalizePendingChunk(chunk, maxChunkTokens)).filter((chunk) => chunk !== null);
4728
+ if (chunks.length === 0) {
4729
+ return null;
4730
+ }
4731
+ return {
4732
+ chunks,
4733
+ error: batch.error,
4734
+ attemptCount: batch.attemptCount,
4735
+ lastAttempt: batch.lastAttempt
4736
+ };
4737
+ }
4738
+ function createPendingEmbeddingRequests(chunks) {
4739
+ return chunks.flatMap(
4740
+ (chunk) => chunk.texts.map((textPart, partIndex) => ({
4741
+ chunk,
4742
+ partIndex,
4743
+ text: textPart.text,
4744
+ tokenCount: textPart.tokenCount
4745
+ }))
4746
+ );
4747
+ }
4748
+ function createPendingEmbeddingRequestBatches(chunks, options = {}) {
4749
+ return createDynamicBatches(createPendingEmbeddingRequests(chunks), options);
4750
+ }
4751
+ function getUniquePendingChunksFromRequests(requests) {
4752
+ const uniqueChunks = /* @__PURE__ */ new Map();
4753
+ for (const request of requests) {
4754
+ uniqueChunks.set(request.chunk.id, request.chunk);
4755
+ }
4756
+ return Array.from(uniqueChunks.values());
4757
+ }
4758
+ function coalesceFailedBatches(batches) {
4759
+ const grouped = /* @__PURE__ */ new Map();
4760
+ for (const batch of batches) {
4761
+ const key = `${batch.attemptCount}:${batch.lastAttempt}:${batch.error}`;
4762
+ const existing = grouped.get(key);
4763
+ if (!existing) {
4764
+ grouped.set(key, {
4765
+ ...batch,
4766
+ chunks: [...batch.chunks]
4767
+ });
4768
+ continue;
4769
+ }
4770
+ existing.chunks.push(...batch.chunks);
4771
+ }
4772
+ return Array.from(grouped.values());
4773
+ }
4774
+ function poolEmbeddingVectors(vectors, weights) {
4775
+ const firstVector = vectors[0];
4776
+ if (!firstVector) {
4777
+ return [];
4778
+ }
4779
+ const pooled = new Array(firstVector.length).fill(0);
4780
+ let totalWeight = 0;
4781
+ for (let index = 0; index < vectors.length; index++) {
4782
+ const vector = vectors[index];
4783
+ const weight = Math.max(1, weights[index] ?? 1);
4784
+ totalWeight += weight;
4785
+ for (let dimension = 0; dimension < vector.length; dimension++) {
4786
+ pooled[dimension] += vector[dimension] * weight;
4787
+ }
4788
+ }
4789
+ if (totalWeight === 0) {
4790
+ return firstVector;
4791
+ }
4792
+ return pooled.map((value) => value / totalWeight);
4793
+ }
4794
+ function hasAllEmbeddingParts(parts, expectedPartCount) {
4795
+ if (parts.length !== expectedPartCount) {
4796
+ return false;
4797
+ }
4798
+ for (let index = 0; index < expectedPartCount; index++) {
4799
+ if (parts[index] === void 0) {
4800
+ return false;
4801
+ }
4802
+ }
4803
+ return true;
4804
+ }
4805
+ function isPathWithinRoot(filePath, rootPath) {
4806
+ const normalizedFilePath = path8.resolve(filePath);
4807
+ const normalizedRoot = path8.resolve(rootPath);
4808
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path8.sep}`);
4809
+ }
4165
4810
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
4166
4811
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
4167
4812
  var rankingPathTokenCache = /* @__PURE__ */ new Map();
4168
4813
  var rankingTextTokenCache = /* @__PURE__ */ new Map();
4814
+ var rankHybridResultsCache = /* @__PURE__ */ new WeakMap();
4169
4815
  var STOPWORDS = /* @__PURE__ */ new Set([
4170
4816
  "the",
4171
4817
  "and",
@@ -4381,7 +5027,16 @@ function classifyExternalRerankBand(candidate, preferSourcePaths, docIntent) {
4381
5027
  function classifyQueryIntent(tokens) {
4382
5028
  const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
4383
5029
  const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
4384
- return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
5030
+ if (sourceIntentHits === 0 && docTestIntentHits === 0) {
5031
+ return "neutral";
5032
+ }
5033
+ if (sourceIntentHits > docTestIntentHits) {
5034
+ return "source";
5035
+ }
5036
+ if (docTestIntentHits > sourceIntentHits) {
5037
+ return "doc_test";
5038
+ }
5039
+ return "neutral";
4385
5040
  }
4386
5041
  function classifyQueryIntentRaw(query) {
4387
5042
  const lowerQuery = query.toLowerCase();
@@ -4403,7 +5058,7 @@ function classifyQueryIntentRaw(query) {
4403
5058
  if (docTestRawHits > sourceRawHits) {
4404
5059
  return "doc_test";
4405
5060
  }
4406
- if (sourceRawHits > docTestRawHits) {
5061
+ if (sourceRawHits > docTestRawHits && sourceRawHits > 0) {
4407
5062
  return "source";
4408
5063
  }
4409
5064
  const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
@@ -4670,9 +5325,8 @@ function rerankResults(query, candidates, rerankTopN, options) {
4670
5325
  return candidates;
4671
5326
  }
4672
5327
  const queryTokenList = Array.from(queryTokens);
4673
- const intent = classifyQueryIntentRaw(query);
4674
5328
  const docIntent = classifyDocIntent(queryTokenList);
4675
- const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
5329
+ const preferSourcePaths = options?.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
4676
5330
  const identifierHints = extractIdentifierHints(query);
4677
5331
  const head = candidates.slice(0, topN).map((candidate, idx) => {
4678
5332
  const pathTokens = splitPathTokens(candidate.metadata.filePath);
@@ -4822,13 +5476,38 @@ function buildDiversityKey(metadata) {
4822
5476
  return normalizedPath;
4823
5477
  }
4824
5478
  function rankHybridResults(query, semanticResults, keywordResults, options) {
5479
+ const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
5480
+ const cacheKey = `${query}${options.fusionStrategy}|${options.rrfK}|${options.hybridWeight}|${options.rerankTopN}|${options.limit}|${prioritizeSourcePaths ? 1 : 0}`;
5481
+ let byKeyword = rankHybridResultsCache.get(semanticResults);
5482
+ if (!byKeyword) {
5483
+ byKeyword = /* @__PURE__ */ new WeakMap();
5484
+ rankHybridResultsCache.set(semanticResults, byKeyword);
5485
+ }
5486
+ let bucket = byKeyword.get(keywordResults);
5487
+ if (!bucket) {
5488
+ bucket = /* @__PURE__ */ new Map();
5489
+ byKeyword.set(keywordResults, bucket);
5490
+ } else {
5491
+ const cached = bucket.get(cacheKey);
5492
+ if (cached) {
5493
+ return cached;
5494
+ }
5495
+ }
4825
5496
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
4826
5497
  const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
4827
5498
  const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
4828
5499
  const rerankPool = fused.slice(0, rerankPoolLimit);
4829
- return rerankResults(query, rerankPool, options.rerankTopN, {
4830
- prioritizeSourcePaths: options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source"
5500
+ const ranked = rerankResults(query, rerankPool, options.rerankTopN, {
5501
+ prioritizeSourcePaths
4831
5502
  });
5503
+ if (bucket.size >= RANK_HYBRID_CACHE_LIMIT) {
5504
+ const oldest = bucket.keys().next().value;
5505
+ if (oldest !== void 0) {
5506
+ bucket.delete(oldest);
5507
+ }
5508
+ }
5509
+ bucket.set(cacheKey, ranked);
5510
+ return ranked;
4832
5511
  }
4833
5512
  function rankSemanticOnlyResults(query, semanticResults, options) {
4834
5513
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
@@ -5125,7 +5804,22 @@ function mergeTieredResults(symbolLane, hybridLane, limit) {
5125
5804
  }
5126
5805
  return out;
5127
5806
  }
5128
- function unionCandidates(semanticCandidates, keywordCandidates) {
5807
+ function matchesSearchFilters(candidate, options, minScore) {
5808
+ if (candidate.score < minScore) return false;
5809
+ if (options?.fileType) {
5810
+ const ext = candidate.metadata.filePath.split(".").pop()?.toLowerCase();
5811
+ if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
5812
+ }
5813
+ if (options?.directory) {
5814
+ const normalizedDir = options.directory.replace(/^\/|\/$/g, "");
5815
+ if (!candidate.metadata.filePath.includes(`/${normalizedDir}/`) && !candidate.metadata.filePath.includes(`${normalizedDir}/`)) return false;
5816
+ }
5817
+ if (options?.chunkType && candidate.metadata.chunkType !== options.chunkType) {
5818
+ return false;
5819
+ }
5820
+ return true;
5821
+ }
5822
+ function unionCandidates(semanticCandidates, keywordCandidates) {
5129
5823
  const byId = /* @__PURE__ */ new Map();
5130
5824
  for (const candidate of semanticCandidates) {
5131
5825
  byId.set(candidate.id, candidate);
@@ -5164,22 +5858,18 @@ var Indexer = class {
5164
5858
  this.projectRoot = projectRoot;
5165
5859
  this.config = config;
5166
5860
  this.indexPath = this.getIndexPath();
5167
- this.fileHashCachePath = path6.join(this.indexPath, "file-hashes.json");
5168
- this.failedBatchesPath = path6.join(this.indexPath, "failed-batches.json");
5169
- this.indexingLockPath = path6.join(this.indexPath, "indexing.lock");
5861
+ this.fileHashCachePath = path8.join(this.indexPath, "file-hashes.json");
5862
+ this.failedBatchesPath = path8.join(this.indexPath, "failed-batches.json");
5863
+ this.indexingLockPath = path8.join(this.indexPath, "indexing.lock");
5170
5864
  this.logger = initializeLogger(config.debug);
5171
5865
  }
5172
5866
  getIndexPath() {
5173
- if (this.config.scope === "global") {
5174
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
5175
- return path6.join(homeDir, ".opencode", "global-index");
5176
- }
5177
- return path6.join(this.projectRoot, ".opencode", "index");
5867
+ return resolveProjectIndexPath(this.projectRoot, this.config.scope);
5178
5868
  }
5179
5869
  loadFileHashCache() {
5180
5870
  try {
5181
- if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
5182
- const data = (0, import_fs5.readFileSync)(this.fileHashCachePath, "utf-8");
5871
+ if ((0, import_fs7.existsSync)(this.fileHashCachePath)) {
5872
+ const data = (0, import_fs7.readFileSync)(this.fileHashCachePath, "utf-8");
5183
5873
  const parsed = JSON.parse(data);
5184
5874
  this.fileHashCache = new Map(Object.entries(parsed));
5185
5875
  }
@@ -5196,63 +5886,422 @@ var Indexer = class {
5196
5886
  }
5197
5887
  atomicWriteSync(targetPath, data) {
5198
5888
  const tempPath = `${targetPath}.tmp`;
5199
- (0, import_fs5.writeFileSync)(tempPath, data);
5200
- (0, import_fs5.renameSync)(tempPath, targetPath);
5889
+ (0, import_fs7.writeFileSync)(tempPath, data);
5890
+ (0, import_fs7.renameSync)(tempPath, targetPath);
5891
+ }
5892
+ getScopedRoots() {
5893
+ const roots = /* @__PURE__ */ new Set([path8.resolve(this.projectRoot)]);
5894
+ for (const kbRoot of this.config.knowledgeBases) {
5895
+ roots.add(path8.resolve(this.projectRoot, kbRoot));
5896
+ }
5897
+ return Array.from(roots);
5898
+ }
5899
+ getBranchCatalogKey() {
5900
+ const branchName = this.currentBranch || "default";
5901
+ if (this.config.scope !== "global") {
5902
+ return branchName;
5903
+ }
5904
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
5905
+ return `${projectHash}:${branchName}`;
5906
+ }
5907
+ getLegacyBranchCatalogKey() {
5908
+ return this.currentBranch || "default";
5909
+ }
5910
+ getLegacyMigrationMetadataKey() {
5911
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
5912
+ return `index.globalBranchMigration.${projectHash}`;
5913
+ }
5914
+ getProjectEmbeddingStrategyMetadataKey() {
5915
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
5916
+ return `index.embeddingStrategyVersion.${projectHash}`;
5917
+ }
5918
+ getProjectForceReembedMetadataKey() {
5919
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
5920
+ return `index.forceReembed.${projectHash}`;
5921
+ }
5922
+ hasProjectForceReembedPending() {
5923
+ return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
5924
+ }
5925
+ hasScopedIndexedData() {
5926
+ if (!this.store || this.config.scope !== "global") {
5927
+ return false;
5928
+ }
5929
+ if (this.hasProjectForceReembedPending()) {
5930
+ return false;
5931
+ }
5932
+ const roots = this.getScopedRoots();
5933
+ if (Array.from(this.fileHashCache.keys()).some((filePath) => this.isFileInCurrentScope(filePath, roots))) {
5934
+ return true;
5935
+ }
5936
+ if (this.loadSerializedFailedBatches().some(
5937
+ (batch) => batch.chunks.some((chunk) => {
5938
+ const filePath = getPendingChunkFilePath(chunk);
5939
+ return filePath !== null && this.isFileInCurrentScope(filePath, roots);
5940
+ })
5941
+ )) {
5942
+ return true;
5943
+ }
5944
+ if (!this.database) {
5945
+ return false;
5946
+ }
5947
+ if (this.getBranchCatalogKeys().some((branchKey) => {
5948
+ const branchChunkIds = this.database.getBranchChunkIds(branchKey);
5949
+ if (branchChunkIds.length > 0) {
5950
+ return true;
5951
+ }
5952
+ return this.database.getBranchSymbolIds(branchKey).length > 0;
5953
+ })) {
5954
+ return true;
5955
+ }
5956
+ const hasAnyBranchRows = this.database.getAllBranches().some((branchKey) => {
5957
+ const branchChunkIds = this.database.getBranchChunkIds(branchKey);
5958
+ if (branchChunkIds.length > 0) {
5959
+ return true;
5960
+ }
5961
+ return this.database.getBranchSymbolIds(branchKey).length > 0;
5962
+ });
5963
+ if (hasAnyBranchRows) {
5964
+ return false;
5965
+ }
5966
+ return this.store.getAllMetadata().some(({ metadata }) => this.isFileInCurrentScope(metadata.filePath, roots));
5967
+ }
5968
+ loadStoredEmbeddingStrategyVersion() {
5969
+ if (!this.database) {
5970
+ return null;
5971
+ }
5972
+ if (this.hasProjectForceReembedPending()) {
5973
+ return null;
5974
+ }
5975
+ if (this.config.scope !== "global") {
5976
+ return this.database.getMetadata("index.embeddingStrategyVersion") ?? "1";
5977
+ }
5978
+ const projectVersion = this.database.getMetadata(this.getProjectEmbeddingStrategyMetadataKey());
5979
+ if (projectVersion) {
5980
+ return projectVersion;
5981
+ }
5982
+ const legacySharedVersion = this.database.getMetadata("index.embeddingStrategyVersion");
5983
+ if (legacySharedVersion && this.hasScopedIndexedData()) {
5984
+ return legacySharedVersion;
5985
+ }
5986
+ return null;
5987
+ }
5988
+ getBranchCatalogKeys() {
5989
+ const primary = this.getBranchCatalogKey();
5990
+ if (this.config.scope !== "global") {
5991
+ return [primary];
5992
+ }
5993
+ if (this.database?.getMetadata(this.getLegacyMigrationMetadataKey()) === "done") {
5994
+ return [primary];
5995
+ }
5996
+ const legacy = this.getLegacyBranchCatalogKey();
5997
+ return primary === legacy ? [primary] : [primary, legacy];
5998
+ }
5999
+ getBranchCatalogCleanupKeys() {
6000
+ const primary = this.getBranchCatalogKey();
6001
+ if (this.config.scope !== "global") {
6002
+ return [primary];
6003
+ }
6004
+ const legacy = this.getLegacyBranchCatalogKey();
6005
+ return primary === legacy ? [primary] : [primary, legacy];
6006
+ }
6007
+ getProjectLocalScopedOwnershipIds(roots) {
6008
+ const chunkIds = /* @__PURE__ */ new Set();
6009
+ const symbolIds = /* @__PURE__ */ new Set();
6010
+ if (!this.database) {
6011
+ return { chunkIds, symbolIds };
6012
+ }
6013
+ const projectRootPath = path8.resolve(this.projectRoot);
6014
+ const projectLocalFilePaths = /* @__PURE__ */ new Set([
6015
+ ...Array.from(this.fileHashCache.keys()).filter(
6016
+ (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
6017
+ ),
6018
+ ...(this.store?.getAllMetadata() ?? []).map(({ metadata }) => metadata.filePath).filter(
6019
+ (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
6020
+ )
6021
+ ]);
6022
+ for (const filePath of projectLocalFilePaths) {
6023
+ for (const chunk of this.database.getChunksByFile(filePath)) {
6024
+ chunkIds.add(chunk.chunkId);
6025
+ }
6026
+ for (const symbol of this.database.getSymbolsByFile(filePath)) {
6027
+ symbolIds.add(symbol.id);
6028
+ }
6029
+ }
6030
+ return { chunkIds, symbolIds };
6031
+ }
6032
+ getProjectScopedBranchCatalogCleanupKeys(projectChunkIds, projectSymbolIds) {
6033
+ if (this.config.scope !== "global") {
6034
+ return this.getBranchCatalogCleanupKeys();
6035
+ }
6036
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
6037
+ const keys = /* @__PURE__ */ new Set();
6038
+ const projectChunkIdSet = new Set(projectChunkIds);
6039
+ const projectSymbolIdSet = new Set(projectSymbolIds);
6040
+ for (const branchKey of this.database?.getAllBranches() ?? []) {
6041
+ if (branchKey.startsWith(`${projectHash}:`)) {
6042
+ keys.add(branchKey);
6043
+ continue;
6044
+ }
6045
+ if (branchKey.includes(":")) {
6046
+ continue;
6047
+ }
6048
+ const referencesProjectChunks = this.database?.getBranchChunkIds(branchKey).some((chunkId) => projectChunkIdSet.has(chunkId)) ?? false;
6049
+ const referencesProjectSymbols = this.database?.getBranchSymbolIds(branchKey).some((symbolId) => projectSymbolIdSet.has(symbolId)) ?? false;
6050
+ if (referencesProjectChunks || referencesProjectSymbols) {
6051
+ keys.add(branchKey);
6052
+ }
6053
+ }
6054
+ for (const branchKey of this.getBranchCatalogCleanupKeys()) {
6055
+ keys.add(branchKey);
6056
+ }
6057
+ return Array.from(keys);
6058
+ }
6059
+ isFileInCurrentScope(filePath, roots) {
6060
+ return roots.some((root) => isPathWithinRoot(filePath, root));
6061
+ }
6062
+ clearScopedFileHashCache(roots) {
6063
+ for (const filePath of Array.from(this.fileHashCache.keys())) {
6064
+ if (this.isFileInCurrentScope(filePath, roots)) {
6065
+ this.fileHashCache.delete(filePath);
6066
+ }
6067
+ }
6068
+ this.saveFileHashCache();
6069
+ }
6070
+ replaceScopedFileHashCache(currentFileHashes, roots) {
6071
+ for (const filePath of Array.from(this.fileHashCache.keys())) {
6072
+ if (this.isFileInCurrentScope(filePath, roots)) {
6073
+ this.fileHashCache.delete(filePath);
6074
+ }
6075
+ }
6076
+ for (const [filePath, hash] of currentFileHashes) {
6077
+ this.fileHashCache.set(filePath, hash);
6078
+ }
6079
+ this.saveFileHashCache();
6080
+ }
6081
+ partitionFailedBatches(roots, maxChunkTokens) {
6082
+ const scoped = [];
6083
+ const retained = [];
6084
+ for (const batch of this.loadSerializedFailedBatches()) {
6085
+ const scopedChunks = batch.chunks.filter((chunk) => {
6086
+ const filePath = getPendingChunkFilePath(chunk);
6087
+ return filePath !== null && this.isFileInCurrentScope(filePath, roots);
6088
+ });
6089
+ const retainedChunks = batch.chunks.filter((chunk) => {
6090
+ const filePath = getPendingChunkFilePath(chunk);
6091
+ return filePath === null || !this.isFileInCurrentScope(filePath, roots);
6092
+ });
6093
+ if (scopedChunks.length > 0) {
6094
+ const normalizedBatch = normalizeFailedBatch({ ...batch, chunks: scopedChunks }, maxChunkTokens);
6095
+ if (normalizedBatch) {
6096
+ scoped.push(normalizedBatch);
6097
+ }
6098
+ }
6099
+ if (retainedChunks.length > 0) {
6100
+ retained.push({ ...batch, chunks: retainedChunks });
6101
+ }
6102
+ }
6103
+ return { scoped, retained };
6104
+ }
6105
+ clearScopedFailedBatches(roots) {
6106
+ const { retained: retainedBatches } = this.partitionFailedBatches(roots);
6107
+ this.saveFailedBatches(retainedBatches);
6108
+ }
6109
+ hasForeignScopedFileHashData(roots) {
6110
+ return Array.from(this.fileHashCache.keys()).some((filePath) => !this.isFileInCurrentScope(filePath, roots));
6111
+ }
6112
+ hasForeignScopedFailedBatches(roots) {
6113
+ const { retained } = this.partitionFailedBatches(roots);
6114
+ return retained.length > 0;
6115
+ }
6116
+ hasForeignScopedBranchData() {
6117
+ if (!this.database || this.config.scope !== "global") {
6118
+ return false;
6119
+ }
6120
+ const projectHash = hashContent(path8.resolve(this.projectRoot)).slice(0, 16);
6121
+ const roots = this.getScopedRoots();
6122
+ const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6123
+ return this.database.getAllBranches().some(
6124
+ (branchKey) => {
6125
+ const branchChunkIds = this.database.getBranchChunkIds(branchKey);
6126
+ const branchSymbolIds = this.database.getBranchSymbolIds(branchKey);
6127
+ const hasBranchData = branchChunkIds.length > 0 || branchSymbolIds.length > 0;
6128
+ if (!hasBranchData) {
6129
+ return false;
6130
+ }
6131
+ if (branchKey.startsWith(`${projectHash}:`)) {
6132
+ return false;
6133
+ }
6134
+ if (!branchKey.includes(":")) {
6135
+ const referencesCurrentProjectChunks = branchChunkIds.some((chunkId) => projectLocalChunkIds.has(chunkId));
6136
+ const referencesCurrentProjectSymbols = branchSymbolIds.some((symbolId) => projectLocalSymbolIds.has(symbolId));
6137
+ return !(referencesCurrentProjectChunks || referencesCurrentProjectSymbols);
6138
+ }
6139
+ return true;
6140
+ }
6141
+ );
6142
+ }
6143
+ saveScopedFailedBatches(batches, roots) {
6144
+ const { retained } = this.partitionFailedBatches(roots);
6145
+ this.saveFailedBatches([...retained, ...batches]);
6146
+ }
6147
+ clearSharedIndexProjectData(store, invertedIndex, database, roots) {
6148
+ const allMetadata = store.getAllMetadata();
6149
+ const scopedEntries = allMetadata.filter(({ metadata }) => this.isFileInCurrentScope(metadata.filePath, roots));
6150
+ const filePaths = /* @__PURE__ */ new Set([
6151
+ ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6152
+ ...scopedEntries.map(({ metadata }) => metadata.filePath)
6153
+ ]);
6154
+ const projectRootPath = path8.resolve(this.projectRoot);
6155
+ const projectLocalFilePaths = new Set(
6156
+ Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6157
+ );
6158
+ const removedChunkIds = new Set(scopedEntries.map(({ key }) => key));
6159
+ for (const filePath of filePaths) {
6160
+ for (const chunk of database.getChunksByFile(filePath)) {
6161
+ removedChunkIds.add(chunk.chunkId);
6162
+ }
6163
+ }
6164
+ const removedChunkIdList = Array.from(removedChunkIds);
6165
+ const projectLocalChunkIds = new Set(
6166
+ scopedEntries.filter(({ metadata }) => isPathWithinRoot(metadata.filePath, projectRootPath)).map(({ key }) => key)
6167
+ );
6168
+ for (const filePath of projectLocalFilePaths) {
6169
+ for (const chunk of database.getChunksByFile(filePath)) {
6170
+ projectLocalChunkIds.add(chunk.chunkId);
6171
+ }
6172
+ }
6173
+ const symbolIds = [];
6174
+ const projectLocalSymbolIds = /* @__PURE__ */ new Set();
6175
+ for (const filePath of filePaths) {
6176
+ for (const symbol of database.getSymbolsByFile(filePath)) {
6177
+ symbolIds.push(symbol.id);
6178
+ if (projectLocalFilePaths.has(filePath)) {
6179
+ projectLocalSymbolIds.add(symbol.id);
6180
+ }
6181
+ }
6182
+ }
6183
+ for (const branchKey of this.getProjectScopedBranchCatalogCleanupKeys(Array.from(projectLocalChunkIds), Array.from(projectLocalSymbolIds))) {
6184
+ database.deleteBranchChunksForBranch(branchKey, removedChunkIdList);
6185
+ }
6186
+ const sharedChunkIds = new Set(database.getReferencedChunkIds(removedChunkIdList));
6187
+ const removableChunkIds = removedChunkIdList.filter((chunkId) => !sharedChunkIds.has(chunkId));
6188
+ if (removableChunkIds.length > 0) {
6189
+ this.rebuildVectorStoreExcludingChunkIds(store, database, removableChunkIds);
6190
+ for (const chunkId of removableChunkIds) {
6191
+ invertedIndex.removeChunk(chunkId);
6192
+ }
6193
+ }
6194
+ for (const branchKey of this.getProjectScopedBranchCatalogCleanupKeys(Array.from(projectLocalChunkIds), Array.from(projectLocalSymbolIds))) {
6195
+ database.deleteBranchSymbolsForBranch(branchKey, symbolIds);
6196
+ }
6197
+ const sharedSymbolIds = new Set(database.getReferencedSymbolIds(symbolIds));
6198
+ const removableSymbolIds = symbolIds.filter((symbolId) => !sharedSymbolIds.has(symbolId));
6199
+ database.clearCallEdgeTargetsForSymbols(removableSymbolIds);
6200
+ for (const filePath of filePaths) {
6201
+ const fileChunkIds = database.getChunksByFile(filePath).map((chunk) => chunk.chunkId);
6202
+ const fileSymbols = database.getSymbolsByFile(filePath);
6203
+ if (fileChunkIds.every((chunkId) => !sharedChunkIds.has(chunkId))) {
6204
+ database.deleteChunksByFile(filePath);
6205
+ }
6206
+ if (fileSymbols.every((symbol) => !sharedSymbolIds.has(symbol.id))) {
6207
+ database.deleteCallEdgesByFile(filePath);
6208
+ database.deleteSymbolsByFile(filePath);
6209
+ }
6210
+ }
6211
+ database.gcOrphanCallEdges();
6212
+ database.gcOrphanSymbols();
6213
+ database.gcOrphanEmbeddings();
6214
+ database.gcOrphanChunks();
6215
+ store.save();
6216
+ invertedIndex.save();
6217
+ return {
6218
+ removedChunkIds: removedChunkIdList,
6219
+ hasForeignData: allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots))
6220
+ };
5201
6221
  }
5202
6222
  checkForInterruptedIndexing() {
5203
- return (0, import_fs5.existsSync)(this.indexingLockPath);
6223
+ return (0, import_fs7.existsSync)(this.indexingLockPath);
5204
6224
  }
5205
6225
  acquireIndexingLock() {
5206
6226
  const lockData = {
5207
6227
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5208
6228
  pid: process.pid
5209
6229
  };
5210
- (0, import_fs5.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
6230
+ (0, import_fs7.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
5211
6231
  }
5212
6232
  releaseIndexingLock() {
5213
- if ((0, import_fs5.existsSync)(this.indexingLockPath)) {
5214
- (0, import_fs5.unlinkSync)(this.indexingLockPath);
6233
+ if ((0, import_fs7.existsSync)(this.indexingLockPath)) {
6234
+ (0, import_fs7.unlinkSync)(this.indexingLockPath);
5215
6235
  }
5216
6236
  }
5217
6237
  async recoverFromInterruptedIndexing() {
5218
6238
  this.logger.warn("Detected interrupted indexing session, recovering...");
5219
- if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
5220
- (0, import_fs5.unlinkSync)(this.fileHashCachePath);
6239
+ if ((0, import_fs7.existsSync)(this.fileHashCachePath)) {
6240
+ (0, import_fs7.unlinkSync)(this.fileHashCachePath);
5221
6241
  }
5222
6242
  await this.healthCheck();
5223
6243
  this.releaseIndexingLock();
5224
6244
  this.logger.info("Recovery complete, next index will re-process all files");
5225
6245
  }
5226
- loadFailedBatches() {
6246
+ loadFailedBatches(maxChunkTokens) {
5227
6247
  try {
5228
- if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
5229
- const data = (0, import_fs5.readFileSync)(this.failedBatchesPath, "utf-8");
5230
- return JSON.parse(data);
5231
- }
6248
+ return this.loadSerializedFailedBatches().map((batch) => normalizeFailedBatch(batch, maxChunkTokens)).filter((batch) => batch !== null);
5232
6249
  } catch {
5233
6250
  return [];
5234
6251
  }
5235
- return [];
6252
+ }
6253
+ loadSerializedFailedBatches() {
6254
+ if (!(0, import_fs7.existsSync)(this.failedBatchesPath)) {
6255
+ return [];
6256
+ }
6257
+ const data = (0, import_fs7.readFileSync)(this.failedBatchesPath, "utf-8");
6258
+ const parsed = JSON.parse(data);
6259
+ return parsed.map((batch) => {
6260
+ const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
6261
+ if (chunks.length === 0) {
6262
+ return null;
6263
+ }
6264
+ return {
6265
+ chunks,
6266
+ error: typeof batch.error === "string" ? batch.error : "Unknown embedding error",
6267
+ attemptCount: typeof batch.attemptCount === "number" ? batch.attemptCount : 1,
6268
+ lastAttempt: typeof batch.lastAttempt === "string" ? batch.lastAttempt : (/* @__PURE__ */ new Date()).toISOString()
6269
+ };
6270
+ }).filter((batch) => batch !== null);
5236
6271
  }
5237
6272
  saveFailedBatches(batches) {
5238
6273
  if (batches.length === 0) {
5239
- if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
5240
- import_fs5.promises.unlink(this.failedBatchesPath).catch(() => {
5241
- });
6274
+ if ((0, import_fs7.existsSync)(this.failedBatchesPath)) {
6275
+ try {
6276
+ (0, import_fs7.unlinkSync)(this.failedBatchesPath);
6277
+ } catch {
6278
+ }
5242
6279
  }
5243
6280
  return;
5244
6281
  }
5245
- (0, import_fs5.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
6282
+ (0, import_fs7.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
5246
6283
  }
5247
- addFailedBatch(batch, error) {
5248
- const existing = this.loadFailedBatches();
5249
- existing.push({
5250
- chunks: batch,
5251
- error,
5252
- attemptCount: 1,
5253
- lastAttempt: (/* @__PURE__ */ new Date()).toISOString()
5254
- });
5255
- this.saveFailedBatches(existing);
6284
+ collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
6285
+ const retryableById = /* @__PURE__ */ new Map();
6286
+ for (const batch of this.loadFailedBatches(maxChunkTokens)) {
6287
+ for (const chunk of batch.chunks) {
6288
+ const filePath = chunk.metadata.filePath;
6289
+ if (!currentFileHashes.has(filePath)) {
6290
+ continue;
6291
+ }
6292
+ if (!unchangedFilePaths.has(filePath)) {
6293
+ continue;
6294
+ }
6295
+ const existing = retryableById.get(chunk.id);
6296
+ if (!existing || batch.attemptCount > existing.attemptCount) {
6297
+ retryableById.set(chunk.id, {
6298
+ chunk,
6299
+ attemptCount: batch.attemptCount
6300
+ });
6301
+ }
6302
+ }
6303
+ }
6304
+ return Array.from(retryableById.values());
5256
6305
  }
5257
6306
  getProviderRateLimits(provider) {
5258
6307
  switch (provider) {
@@ -5410,7 +6459,7 @@ var Indexer = class {
5410
6459
  const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
5411
6460
  parts.push(`intent_hint: ${intent}`);
5412
6461
  try {
5413
- const fileContent = await import_fs5.promises.readFile(candidate.metadata.filePath, "utf-8");
6462
+ const fileContent = await import_fs7.promises.readFile(candidate.metadata.filePath, "utf-8");
5414
6463
  const lines = fileContent.split("\n");
5415
6464
  const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
5416
6465
  const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
@@ -5455,33 +6504,56 @@ var Indexer = class {
5455
6504
  });
5456
6505
  }
5457
6506
  }
5458
- await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
6507
+ await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
5459
6508
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
5460
- const storePath = path6.join(this.indexPath, "vectors");
6509
+ const storePath = path8.join(this.indexPath, "vectors");
5461
6510
  this.store = new VectorStore(storePath, dimensions);
5462
- const indexFilePath = path6.join(this.indexPath, "vectors.usearch");
5463
- if ((0, import_fs5.existsSync)(indexFilePath)) {
6511
+ const indexFilePath = path8.join(this.indexPath, "vectors.usearch");
6512
+ if ((0, import_fs7.existsSync)(indexFilePath)) {
5464
6513
  this.store.load();
5465
6514
  }
5466
- const invertedIndexPath = path6.join(this.indexPath, "inverted-index.json");
6515
+ const invertedIndexPath = path8.join(this.indexPath, "inverted-index.json");
5467
6516
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
5468
6517
  try {
5469
6518
  this.invertedIndex.load();
5470
6519
  } catch {
5471
- if ((0, import_fs5.existsSync)(invertedIndexPath)) {
5472
- await import_fs5.promises.unlink(invertedIndexPath);
6520
+ if ((0, import_fs7.existsSync)(invertedIndexPath)) {
6521
+ await import_fs7.promises.unlink(invertedIndexPath);
5473
6522
  }
5474
6523
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
5475
6524
  }
5476
- const dbPath = path6.join(this.indexPath, "codebase.db");
5477
- const dbIsNew = !(0, import_fs5.existsSync)(dbPath);
5478
- this.database = new Database(dbPath);
6525
+ const dbPath = path8.join(this.indexPath, "codebase.db");
6526
+ let dbIsNew = !(0, import_fs7.existsSync)(dbPath);
6527
+ try {
6528
+ this.database = new Database(dbPath);
6529
+ } catch (error) {
6530
+ if (!await this.tryResetCorruptedIndex("initializing index database", error)) {
6531
+ throw error;
6532
+ }
6533
+ this.store = new VectorStore(storePath, dimensions);
6534
+ this.invertedIndex = new InvertedIndex(invertedIndexPath);
6535
+ this.database = new Database(dbPath);
6536
+ dbIsNew = true;
6537
+ }
6538
+ if (isGitRepo(this.projectRoot)) {
6539
+ this.currentBranch = getBranchOrDefault(this.projectRoot);
6540
+ this.baseBranch = getBaseBranch(this.projectRoot);
6541
+ this.logger.branch("info", "Detected git repository", {
6542
+ currentBranch: this.currentBranch,
6543
+ baseBranch: this.baseBranch
6544
+ });
6545
+ } else {
6546
+ this.currentBranch = "default";
6547
+ this.baseBranch = "default";
6548
+ this.logger.branch("debug", "Not a git repository, using default branch");
6549
+ }
5479
6550
  if (this.checkForInterruptedIndexing()) {
5480
6551
  await this.recoverFromInterruptedIndexing();
5481
6552
  }
5482
6553
  if (dbIsNew && this.store.count() > 0) {
5483
6554
  this.migrateFromLegacyIndex();
5484
6555
  }
6556
+ this.loadFileHashCache();
5485
6557
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
5486
6558
  if (!this.indexCompatibility.compatible) {
5487
6559
  this.logger.warn("Index compatibility issue detected", {
@@ -5490,18 +6562,6 @@ var Indexer = class {
5490
6562
  configuredProviderInfo: this.configuredProviderInfo
5491
6563
  });
5492
6564
  }
5493
- if (isGitRepo(this.projectRoot)) {
5494
- this.currentBranch = getBranchOrDefault(this.projectRoot);
5495
- this.baseBranch = getBaseBranch(this.projectRoot);
5496
- this.logger.branch("info", "Detected git repository", {
5497
- currentBranch: this.currentBranch,
5498
- baseBranch: this.baseBranch
5499
- });
5500
- } else {
5501
- this.currentBranch = "default";
5502
- this.baseBranch = "default";
5503
- this.logger.branch("debug", "Not a git repository, using default branch");
5504
- }
5505
6565
  if (this.config.indexing.autoGc) {
5506
6566
  await this.maybeRunAutoGc();
5507
6567
  }
@@ -5521,20 +6581,169 @@ var Indexer = class {
5521
6581
  }
5522
6582
  }
5523
6583
  if (shouldRunGc) {
5524
- await this.healthCheck();
6584
+ const result = await this.healthCheck();
6585
+ if (result.warning) {
6586
+ this.database.setMetadata(STARTUP_WARNING_METADATA_KEY, result.warning);
6587
+ } else {
6588
+ this.database.deleteMetadata(STARTUP_WARNING_METADATA_KEY);
6589
+ }
5525
6590
  this.database.setMetadata("lastGcTimestamp", now.toString());
5526
6591
  }
5527
6592
  }
5528
6593
  async maybeRunOrphanGc() {
5529
- if (!this.database) return;
6594
+ if (!this.database) return null;
5530
6595
  const stats = this.database.getStats();
5531
- if (!stats) return;
6596
+ if (!stats) return null;
5532
6597
  const orphanCount = stats.embeddingCount - stats.chunkCount;
5533
6598
  if (orphanCount > this.config.indexing.gcOrphanThreshold) {
5534
- this.database.gcOrphanEmbeddings();
5535
- this.database.gcOrphanChunks();
6599
+ try {
6600
+ this.database.gcOrphanEmbeddings();
6601
+ this.database.gcOrphanChunks();
6602
+ } catch (error) {
6603
+ if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6604
+ return {
6605
+ resetCorruptedIndex: true,
6606
+ warning: this.getCorruptedIndexWarning(path8.join(this.indexPath, "codebase.db"))
6607
+ };
6608
+ }
6609
+ throw error;
6610
+ }
5536
6611
  this.database.setMetadata("lastGcTimestamp", Date.now().toString());
5537
6612
  }
6613
+ return null;
6614
+ }
6615
+ rebuildVectorStoreExcludingChunkIds(store, database, excludedChunkIds) {
6616
+ const excludedSet = new Set(excludedChunkIds);
6617
+ if (excludedSet.size === 0) {
6618
+ return;
6619
+ }
6620
+ const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6621
+ const storeBasePath = path8.join(this.indexPath, "vectors");
6622
+ const storeIndexPath = `${storeBasePath}.usearch`;
6623
+ const storeMetadataPath = `${storeBasePath}.meta.json`;
6624
+ const backupIndexPath = `${storeIndexPath}.bak`;
6625
+ const backupMetadataPath = `${storeMetadataPath}.bak`;
6626
+ let backedUpIndex = false;
6627
+ let backedUpMetadata = false;
6628
+ let rebuiltCount = 0;
6629
+ let skippedCount = 0;
6630
+ if ((0, import_fs7.existsSync)(backupIndexPath)) {
6631
+ (0, import_fs7.unlinkSync)(backupIndexPath);
6632
+ }
6633
+ if ((0, import_fs7.existsSync)(backupMetadataPath)) {
6634
+ (0, import_fs7.unlinkSync)(backupMetadataPath);
6635
+ }
6636
+ try {
6637
+ if ((0, import_fs7.existsSync)(storeIndexPath)) {
6638
+ (0, import_fs7.renameSync)(storeIndexPath, backupIndexPath);
6639
+ backedUpIndex = true;
6640
+ }
6641
+ if ((0, import_fs7.existsSync)(storeMetadataPath)) {
6642
+ (0, import_fs7.renameSync)(storeMetadataPath, backupMetadataPath);
6643
+ backedUpMetadata = true;
6644
+ }
6645
+ store.clear();
6646
+ for (const { key, metadata } of retainedEntries) {
6647
+ const chunk = database.getChunk(key);
6648
+ if (!chunk) {
6649
+ skippedCount += 1;
6650
+ continue;
6651
+ }
6652
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
6653
+ if (!embeddingBuffer) {
6654
+ skippedCount += 1;
6655
+ continue;
6656
+ }
6657
+ const vector = bufferToFloat32Array(embeddingBuffer);
6658
+ store.add(key, Array.from(vector), metadata);
6659
+ rebuiltCount += 1;
6660
+ }
6661
+ store.save();
6662
+ if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
6663
+ (0, import_fs7.unlinkSync)(backupIndexPath);
6664
+ }
6665
+ if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
6666
+ (0, import_fs7.unlinkSync)(backupMetadataPath);
6667
+ }
6668
+ this.logger.gc("info", "Rebuilt vector store to avoid native remove", {
6669
+ excludedChunks: excludedSet.size,
6670
+ rebuiltChunks: rebuiltCount,
6671
+ skippedChunks: skippedCount
6672
+ });
6673
+ } catch (error) {
6674
+ try {
6675
+ store.clear();
6676
+ } catch {
6677
+ }
6678
+ if ((0, import_fs7.existsSync)(storeIndexPath)) {
6679
+ (0, import_fs7.unlinkSync)(storeIndexPath);
6680
+ }
6681
+ if ((0, import_fs7.existsSync)(storeMetadataPath)) {
6682
+ (0, import_fs7.unlinkSync)(storeMetadataPath);
6683
+ }
6684
+ if (backedUpIndex && (0, import_fs7.existsSync)(backupIndexPath)) {
6685
+ (0, import_fs7.renameSync)(backupIndexPath, storeIndexPath);
6686
+ }
6687
+ if (backedUpMetadata && (0, import_fs7.existsSync)(backupMetadataPath)) {
6688
+ (0, import_fs7.renameSync)(backupMetadataPath, storeMetadataPath);
6689
+ }
6690
+ if (backedUpIndex || backedUpMetadata) {
6691
+ store.load();
6692
+ }
6693
+ throw error;
6694
+ }
6695
+ }
6696
+ getCorruptedIndexWarning(dbPath) {
6697
+ if (this.config.scope === "global") {
6698
+ return `Detected a corrupted shared global SQLite index at ${dbPath}. Automatic repair is disabled for global scope because it may delete other projects' index data. Remove or repair the shared index manually, then rerun index_codebase with force=true.`;
6699
+ }
6700
+ return `Detected a corrupted local SQLite index at ${dbPath} and reset the local index. Run index_codebase to rebuild search data.`;
6701
+ }
6702
+ async tryResetCorruptedIndex(stage, error) {
6703
+ if (!isSqliteCorruptionError(error)) {
6704
+ return false;
6705
+ }
6706
+ const dbPath = path8.join(this.indexPath, "codebase.db");
6707
+ const warning = this.getCorruptedIndexWarning(dbPath);
6708
+ const errorMessage = getErrorMessage(error);
6709
+ if (this.config.scope === "global") {
6710
+ this.logger.error("Detected corrupted shared global index database", {
6711
+ stage,
6712
+ dbPath,
6713
+ error: errorMessage
6714
+ });
6715
+ throw new Error(`${warning} Original SQLite error: ${errorMessage}`);
6716
+ }
6717
+ this.logger.warn("Detected corrupted local index database, resetting local index", {
6718
+ stage,
6719
+ dbPath,
6720
+ error: errorMessage
6721
+ });
6722
+ this.store = null;
6723
+ this.invertedIndex = null;
6724
+ this.database?.close();
6725
+ this.database = null;
6726
+ this.indexCompatibility = null;
6727
+ this.fileHashCache.clear();
6728
+ const resetPaths = [
6729
+ path8.join(this.indexPath, "codebase.db"),
6730
+ path8.join(this.indexPath, "codebase.db-shm"),
6731
+ path8.join(this.indexPath, "codebase.db-wal"),
6732
+ path8.join(this.indexPath, "vectors.usearch"),
6733
+ path8.join(this.indexPath, "inverted-index.json"),
6734
+ path8.join(this.indexPath, "file-hashes.json"),
6735
+ path8.join(this.indexPath, "failed-batches.json"),
6736
+ path8.join(this.indexPath, "indexing.lock"),
6737
+ path8.join(this.indexPath, "vectors")
6738
+ ];
6739
+ await Promise.all(resetPaths.map(async (targetPath) => {
6740
+ try {
6741
+ await import_fs7.promises.rm(targetPath, { recursive: true, force: true });
6742
+ } catch {
6743
+ }
6744
+ }));
6745
+ await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
6746
+ return true;
5538
6747
  }
5539
6748
  migrateFromLegacyIndex() {
5540
6749
  if (!this.store || !this.database) return;
@@ -5558,7 +6767,7 @@ var Indexer = class {
5558
6767
  if (chunkDataBatch.length > 0) {
5559
6768
  this.database.upsertChunksBatch(chunkDataBatch);
5560
6769
  }
5561
- this.database.addChunksToBranchBatch(this.currentBranch || "default", chunkIds);
6770
+ this.database.addChunksToBranchBatch(this.getBranchCatalogKey(), chunkIds);
5562
6771
  }
5563
6772
  loadIndexMetadata() {
5564
6773
  if (!this.database) return null;
@@ -5569,6 +6778,7 @@ var Indexer = class {
5569
6778
  embeddingProvider: this.database.getMetadata("index.embeddingProvider") ?? "",
5570
6779
  embeddingModel: this.database.getMetadata("index.embeddingModel") ?? "",
5571
6780
  embeddingDimensions: parseInt(this.database.getMetadata("index.embeddingDimensions") ?? "0", 10),
6781
+ embeddingStrategyVersion: this.loadStoredEmbeddingStrategyVersion() ?? EMBEDDING_STRATEGY_VERSION,
5572
6782
  createdAt: this.database.getMetadata("index.createdAt") ?? "",
5573
6783
  updatedAt: this.database.getMetadata("index.updatedAt") ?? ""
5574
6784
  };
@@ -5577,10 +6787,22 @@ var Indexer = class {
5577
6787
  if (!this.database) return;
5578
6788
  const now = (/* @__PURE__ */ new Date()).toISOString();
5579
6789
  const existingCreatedAt = this.database.getMetadata("index.createdAt");
6790
+ const completeProjectEmbeddingStrategyReset = !this.hasProjectForceReembedPending();
5580
6791
  this.database.setMetadata("index.version", INDEX_METADATA_VERSION);
5581
6792
  this.database.setMetadata("index.embeddingProvider", provider.provider);
5582
6793
  this.database.setMetadata("index.embeddingModel", provider.modelInfo.model);
5583
6794
  this.database.setMetadata("index.embeddingDimensions", provider.modelInfo.dimensions.toString());
6795
+ if (this.config.scope === "global") {
6796
+ if (completeProjectEmbeddingStrategyReset) {
6797
+ this.database.setMetadata(this.getProjectEmbeddingStrategyMetadataKey(), EMBEDDING_STRATEGY_VERSION);
6798
+ }
6799
+ this.database.setMetadata(this.getLegacyMigrationMetadataKey(), "done");
6800
+ if (completeProjectEmbeddingStrategyReset) {
6801
+ this.database.deleteMetadata(this.getProjectForceReembedMetadataKey());
6802
+ }
6803
+ } else {
6804
+ this.database.setMetadata("index.embeddingStrategyVersion", EMBEDDING_STRATEGY_VERSION);
6805
+ }
5584
6806
  this.database.setMetadata("index.updatedAt", now);
5585
6807
  if (!existingCreatedAt) {
5586
6808
  this.database.setMetadata("index.createdAt", now);
@@ -5610,6 +6832,14 @@ var Indexer = class {
5610
6832
  storedMetadata
5611
6833
  };
5612
6834
  }
6835
+ if (storedMetadata.embeddingStrategyVersion !== EMBEDDING_STRATEGY_VERSION) {
6836
+ return {
6837
+ compatible: false,
6838
+ code: "EMBEDDING_STRATEGY_MISMATCH" /* EMBEDDING_STRATEGY_MISMATCH */,
6839
+ reason: `Embedding strategy mismatch: index was built with embedding strategy v${storedMetadata.embeddingStrategyVersion}, but the current code requires v${EMBEDDING_STRATEGY_VERSION}. Run index_codebase with force=true to rebuild cached embeddings.`,
6840
+ storedMetadata
6841
+ };
6842
+ }
5613
6843
  if (storedMetadata.embeddingProvider !== currentProvider) {
5614
6844
  this.logger.warn("Provider changed", {
5615
6845
  storedProvider: storedMetadata.embeddingProvider,
@@ -5657,6 +6887,10 @@ var Indexer = class {
5657
6887
  }
5658
6888
  async index(onProgress) {
5659
6889
  const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
6890
+ const scopedRoots = this.config.scope === "global" ? this.getScopedRoots() : null;
6891
+ const branchCatalogKey = this.getBranchCatalogKey();
6892
+ const forceScopedReembed = scopedRoots !== null && database.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
6893
+ const failedForcedChunkIds = /* @__PURE__ */ new Set();
5660
6894
  if (!this.indexCompatibility?.compatible) {
5661
6895
  throw new Error(
5662
6896
  `${this.indexCompatibility?.reason} Run index_codebase with force=true to rebuild the index.`
@@ -5678,6 +6912,7 @@ var Indexer = class {
5678
6912
  skippedFiles: [],
5679
6913
  parseFailures: []
5680
6914
  };
6915
+ const failedBatchesForCurrentRun = [];
5681
6916
  onProgress?.({
5682
6917
  phase: "scanning",
5683
6918
  filesProcessed: 0,
@@ -5712,7 +6947,7 @@ var Indexer = class {
5712
6947
  unchangedFilePaths.add(f.path);
5713
6948
  this.logger.recordCacheHit();
5714
6949
  } else {
5715
- const content = await import_fs5.promises.readFile(f.path, "utf-8");
6950
+ const content = await import_fs7.promises.readFile(f.path, "utf-8");
5716
6951
  changedFiles.push({ path: f.path, content, hash: currentHash });
5717
6952
  this.logger.recordCacheMiss();
5718
6953
  }
@@ -5737,6 +6972,12 @@ var Indexer = class {
5737
6972
  const existingChunks = /* @__PURE__ */ new Map();
5738
6973
  const existingChunksByFile = /* @__PURE__ */ new Map();
5739
6974
  for (const { key, metadata } of store.getAllMetadata()) {
6975
+ if (scopedRoots && !this.isFileInCurrentScope(metadata.filePath, scopedRoots)) {
6976
+ continue;
6977
+ }
6978
+ if (forceScopedReembed && scopedRoots && this.isFileInCurrentScope(metadata.filePath, scopedRoots)) {
6979
+ continue;
6980
+ }
5740
6981
  existingChunks.set(key, metadata.hash);
5741
6982
  const fileChunks = existingChunksByFile.get(metadata.filePath) || /* @__PURE__ */ new Set();
5742
6983
  fileChunks.add(key);
@@ -5758,7 +6999,7 @@ var Indexer = class {
5758
6999
  for (const parsed of parsedFiles) {
5759
7000
  currentFilePaths.add(parsed.path);
5760
7001
  if (parsed.chunks.length === 0) {
5761
- const relativePath = path6.relative(this.projectRoot, parsed.path);
7002
+ const relativePath = path8.relative(this.projectRoot, parsed.path);
5762
7003
  stats.parseFailures.push(relativePath);
5763
7004
  }
5764
7005
  let fileChunkCount = 0;
@@ -5794,7 +7035,10 @@ var Indexer = class {
5794
7035
  fileChunkCount++;
5795
7036
  continue;
5796
7037
  }
5797
- const text = createEmbeddingText(chunk, parsed.path);
7038
+ const texts = createEmbeddingTexts(chunk, parsed.path, getSafeEmbeddingChunkTokenLimit(configuredProviderInfo)).map((text) => ({
7039
+ text,
7040
+ tokenCount: estimateTokens2(text)
7041
+ }));
5798
7042
  const metadata = {
5799
7043
  filePath: parsed.path,
5800
7044
  startLine: chunk.startLine,
@@ -5804,10 +7048,38 @@ var Indexer = class {
5804
7048
  language: chunk.language,
5805
7049
  hash: contentHash
5806
7050
  };
5807
- pendingChunks.push({ id, text, content: chunk.content, contentHash, metadata });
7051
+ pendingChunks.push({
7052
+ id,
7053
+ texts,
7054
+ storageText: createPendingChunkStorageText(texts),
7055
+ content: chunk.content,
7056
+ contentHash,
7057
+ metadata
7058
+ });
5808
7059
  fileChunkCount++;
5809
7060
  }
5810
7061
  }
7062
+ const retryableFailedChunks = this.collectRetryableFailedChunks(
7063
+ currentFileHashes,
7064
+ unchangedFilePaths,
7065
+ getSafeEmbeddingChunkTokenLimit(configuredProviderInfo)
7066
+ );
7067
+ const retryableFailedAttemptCounts = /* @__PURE__ */ new Map();
7068
+ const retryableChunksWithExistingData = /* @__PURE__ */ new Set();
7069
+ if (retryableFailedChunks.length > 0) {
7070
+ const pendingChunkIds = new Set(pendingChunks.map((chunk) => chunk.id));
7071
+ for (const { chunk, attemptCount } of retryableFailedChunks) {
7072
+ retryableFailedAttemptCounts.set(chunk.id, attemptCount);
7073
+ if (existingChunks.has(chunk.id)) {
7074
+ retryableChunksWithExistingData.add(chunk.id);
7075
+ }
7076
+ if (!pendingChunkIds.has(chunk.id)) {
7077
+ pendingChunks.push(chunk);
7078
+ pendingChunkIds.add(chunk.id);
7079
+ currentChunkIds.add(chunk.id);
7080
+ }
7081
+ }
7082
+ }
5811
7083
  if (chunkDataBatch.length > 0) {
5812
7084
  database.upsertChunksBatch(chunkDataBatch);
5813
7085
  }
@@ -5836,17 +7108,20 @@ var Indexer = class {
5836
7108
  fileSymbols.push(symbol);
5837
7109
  allSymbolIds.add(symbolId);
5838
7110
  }
7111
+ const fileLanguage = parsed.chunks[0]?.language;
7112
+ const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
7113
+ const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
5839
7114
  const symbolsByName = /* @__PURE__ */ new Map();
5840
7115
  for (const symbol of fileSymbols) {
5841
- const existing = symbolsByName.get(symbol.name) ?? [];
7116
+ const key = normalizeSymbolKey(symbol.name);
7117
+ const existing = symbolsByName.get(key) ?? [];
5842
7118
  existing.push(symbol);
5843
- symbolsByName.set(symbol.name, existing);
7119
+ symbolsByName.set(key, existing);
5844
7120
  }
5845
7121
  if (fileSymbols.length > 0) {
5846
7122
  database.upsertSymbolsBatch(fileSymbols);
5847
7123
  symbolsByFile.set(parsed.path, fileSymbols);
5848
7124
  }
5849
- const fileLanguage = parsed.chunks[0]?.language;
5850
7125
  if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
5851
7126
  const callSites = extractCalls(changedFile.content, fileLanguage);
5852
7127
  if (callSites.length === 0) continue;
@@ -5871,7 +7146,7 @@ var Indexer = class {
5871
7146
  if (edges.length > 0) {
5872
7147
  database.upsertCallEdgesBatch(edges);
5873
7148
  for (const edge of edges) {
5874
- const candidates = symbolsByName.get(edge.targetName);
7149
+ const candidates = symbolsByName.get(normalizeSymbolKey(edge.targetName));
5875
7150
  if (candidates && candidates.length === 1) {
5876
7151
  database.resolveCallEdge(edge.id, candidates[0].id);
5877
7152
  }
@@ -5884,14 +7159,20 @@ var Indexer = class {
5884
7159
  allSymbolIds.add(sym.id);
5885
7160
  }
5886
7161
  }
5887
- let removedCount = 0;
7162
+ const removedChunkIds = [];
5888
7163
  for (const [chunkId] of existingChunks) {
5889
7164
  if (!currentChunkIds.has(chunkId)) {
5890
- store.remove(chunkId);
7165
+ removedChunkIds.push(chunkId);
7166
+ }
7167
+ }
7168
+ if (removedChunkIds.length > 0) {
7169
+ this.rebuildVectorStoreExcludingChunkIds(store, database, removedChunkIds);
7170
+ for (const chunkId of removedChunkIds) {
5891
7171
  invertedIndex.removeChunk(chunkId);
5892
- removedCount++;
5893
7172
  }
7173
+ database.deleteChunksByIds(removedChunkIds);
5894
7174
  }
7175
+ const removedCount = removedChunkIds.length;
5895
7176
  stats.totalChunks = pendingChunks.length;
5896
7177
  stats.existingChunks = currentChunkIds.size - pendingChunks.length;
5897
7178
  stats.removedChunks = removedCount;
@@ -5903,12 +7184,20 @@ var Indexer = class {
5903
7184
  removed: removedCount
5904
7185
  });
5905
7186
  if (pendingChunks.length === 0 && removedCount === 0) {
5906
- database.clearBranch(this.currentBranch);
5907
- database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5908
- database.clearBranchSymbols(this.currentBranch);
5909
- database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
5910
- this.fileHashCache = currentFileHashes;
5911
- this.saveFileHashCache();
7187
+ database.clearBranch(branchCatalogKey);
7188
+ database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7189
+ database.clearBranchSymbols(branchCatalogKey);
7190
+ database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
7191
+ if (scopedRoots) {
7192
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7193
+ this.clearScopedFailedBatches(scopedRoots);
7194
+ } else {
7195
+ this.fileHashCache = currentFileHashes;
7196
+ this.saveFileHashCache();
7197
+ this.saveFailedBatches([]);
7198
+ }
7199
+ this.saveIndexMetadata(configuredProviderInfo);
7200
+ this.indexCompatibility = { compatible: true };
5912
7201
  stats.durationMs = Date.now() - startTime;
5913
7202
  onProgress?.({
5914
7203
  phase: "complete",
@@ -5921,14 +7210,22 @@ var Indexer = class {
5921
7210
  return stats;
5922
7211
  }
5923
7212
  if (pendingChunks.length === 0) {
5924
- database.clearBranch(this.currentBranch);
5925
- database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5926
- database.clearBranchSymbols(this.currentBranch);
5927
- database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
7213
+ database.clearBranch(branchCatalogKey);
7214
+ database.addChunksToBranchBatch(branchCatalogKey, Array.from(currentChunkIds));
7215
+ database.clearBranchSymbols(branchCatalogKey);
7216
+ database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
5928
7217
  store.save();
5929
7218
  invertedIndex.save();
5930
- this.fileHashCache = currentFileHashes;
5931
- this.saveFileHashCache();
7219
+ if (scopedRoots) {
7220
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7221
+ this.clearScopedFailedBatches(scopedRoots);
7222
+ } else {
7223
+ this.fileHashCache = currentFileHashes;
7224
+ this.saveFileHashCache();
7225
+ this.saveFailedBatches([]);
7226
+ }
7227
+ this.saveIndexMetadata(configuredProviderInfo);
7228
+ this.indexCompatibility = { compatible: true };
5932
7229
  stats.durationMs = Date.now() - startTime;
5933
7230
  onProgress?.({
5934
7231
  phase: "complete",
@@ -5949,8 +7246,9 @@ var Indexer = class {
5949
7246
  });
5950
7247
  const allContentHashes = pendingChunks.map((c) => c.contentHash);
5951
7248
  const missingHashes = new Set(database.getMissingEmbeddings(allContentHashes));
5952
- const chunksNeedingEmbedding = pendingChunks.filter((c) => missingHashes.has(c.contentHash));
5953
- const chunksWithExistingEmbedding = pendingChunks.filter((c) => !missingHashes.has(c.contentHash));
7249
+ const forcedReembedChunkIds = forceScopedReembed ? new Set(pendingChunks.map((chunk) => chunk.id)) : /* @__PURE__ */ new Set();
7250
+ const chunksNeedingEmbedding = pendingChunks.filter((c) => forcedReembedChunkIds.has(c.id) || missingHashes.has(c.contentHash));
7251
+ const chunksWithExistingEmbedding = pendingChunks.filter((c) => !forcedReembedChunkIds.has(c.id) && !missingHashes.has(c.contentHash));
5954
7252
  this.logger.cache("info", "Embedding cache lookup", {
5955
7253
  needsEmbedding: chunksNeedingEmbedding.length,
5956
7254
  fromCache: chunksWithExistingEmbedding.length
@@ -5972,17 +7270,24 @@ var Indexer = class {
5972
7270
  interval: providerRateLimits.intervalMs,
5973
7271
  intervalCap: providerRateLimits.concurrency
5974
7272
  });
5975
- const dynamicBatches = createDynamicBatches(chunksNeedingEmbedding);
7273
+ const pendingChunksById = new Map(chunksNeedingEmbedding.map((chunk) => [chunk.id, chunk]));
7274
+ const embeddingPartsByChunk = /* @__PURE__ */ new Map();
7275
+ const completedChunkIds = /* @__PURE__ */ new Set();
7276
+ const failedChunkIds = /* @__PURE__ */ new Set();
7277
+ const requestBatches = createPendingEmbeddingRequestBatches(
7278
+ chunksNeedingEmbedding,
7279
+ getDynamicBatchOptions(configuredProviderInfo)
7280
+ );
5976
7281
  let rateLimitBackoffMs = 0;
5977
- for (const batch of dynamicBatches) {
7282
+ for (const requestBatch of requestBatches) {
5978
7283
  queue.add(async () => {
5979
7284
  if (rateLimitBackoffMs > 0) {
5980
- await new Promise((resolve6) => setTimeout(resolve6, rateLimitBackoffMs));
7285
+ await new Promise((resolve8) => setTimeout(resolve8, rateLimitBackoffMs));
5981
7286
  }
5982
7287
  try {
5983
7288
  const result = await pRetry(
5984
7289
  async () => {
5985
- const texts = batch.map((c) => c.text);
7290
+ const texts = requestBatch.map((request) => request.text);
5986
7291
  return provider.embedBatch(texts);
5987
7292
  },
5988
7293
  {
@@ -6012,29 +7317,82 @@ var Indexer = class {
6012
7317
  if (rateLimitBackoffMs > 0) {
6013
7318
  rateLimitBackoffMs = Math.max(0, rateLimitBackoffMs - 2e3);
6014
7319
  }
6015
- const items = batch.map((chunk, idx) => ({
6016
- id: chunk.id,
6017
- vector: result.embeddings[idx],
6018
- metadata: chunk.metadata
6019
- }));
6020
- store.addBatch(items);
6021
- const embeddingBatchItems = batch.map((chunk, i) => ({
6022
- contentHash: chunk.contentHash,
6023
- embedding: float32ArrayToBuffer(result.embeddings[i]),
6024
- chunkText: chunk.text,
6025
- model: configuredProviderInfo.modelInfo.model
6026
- }));
6027
- database.upsertEmbeddingsBatch(embeddingBatchItems);
6028
- for (const chunk of batch) {
6029
- invertedIndex.removeChunk(chunk.id);
6030
- invertedIndex.addChunk(chunk.id, chunk.content);
7320
+ const touchedChunkIds = /* @__PURE__ */ new Set();
7321
+ requestBatch.forEach((request, idx) => {
7322
+ if (failedChunkIds.has(request.chunk.id) || completedChunkIds.has(request.chunk.id)) {
7323
+ return;
7324
+ }
7325
+ const vector = result.embeddings[idx];
7326
+ if (!vector) {
7327
+ throw new Error(`Embedding API returned too few vectors for chunk ${request.chunk.id}`);
7328
+ }
7329
+ const parts = embeddingPartsByChunk.get(request.chunk.id) ?? [];
7330
+ parts[request.partIndex] = {
7331
+ vector,
7332
+ tokenCount: request.tokenCount
7333
+ };
7334
+ embeddingPartsByChunk.set(request.chunk.id, parts);
7335
+ touchedChunkIds.add(request.chunk.id);
7336
+ });
7337
+ const pooledResults = [];
7338
+ for (const chunkId of touchedChunkIds) {
7339
+ if (failedChunkIds.has(chunkId) || completedChunkIds.has(chunkId)) {
7340
+ continue;
7341
+ }
7342
+ const chunk = pendingChunksById.get(chunkId);
7343
+ if (!chunk) {
7344
+ continue;
7345
+ }
7346
+ const parts = embeddingPartsByChunk.get(chunk.id) ?? [];
7347
+ if (!hasAllEmbeddingParts(parts, chunk.texts.length)) {
7348
+ continue;
7349
+ }
7350
+ const orderedParts = parts;
7351
+ pooledResults.push({
7352
+ chunk,
7353
+ vector: poolEmbeddingVectors(
7354
+ orderedParts.map((part) => part.vector),
7355
+ orderedParts.map((part) => part.tokenCount)
7356
+ )
7357
+ });
7358
+ }
7359
+ if (pooledResults.length > 0) {
7360
+ const items = pooledResults.map(({ chunk, vector }) => ({
7361
+ id: chunk.id,
7362
+ vector,
7363
+ metadata: chunk.metadata
7364
+ }));
7365
+ store.addBatch(items);
7366
+ const embeddingBatchItems = pooledResults.map(({ chunk, vector }) => ({
7367
+ contentHash: chunk.contentHash,
7368
+ embedding: float32ArrayToBuffer(vector),
7369
+ chunkText: chunk.storageText,
7370
+ model: configuredProviderInfo.modelInfo.model
7371
+ }));
7372
+ try {
7373
+ database.upsertEmbeddingsBatch(embeddingBatchItems);
7374
+ } catch (dbError) {
7375
+ this.rebuildVectorStoreExcludingChunkIds(
7376
+ store,
7377
+ database,
7378
+ pooledResults.map(({ chunk }) => chunk.id)
7379
+ );
7380
+ throw dbError;
7381
+ }
7382
+ for (const { chunk } of pooledResults) {
7383
+ invertedIndex.removeChunk(chunk.id);
7384
+ invertedIndex.addChunk(chunk.id, chunk.content);
7385
+ completedChunkIds.add(chunk.id);
7386
+ embeddingPartsByChunk.delete(chunk.id);
7387
+ }
7388
+ stats.indexedChunks += pooledResults.length;
7389
+ this.logger.recordChunksEmbedded(pooledResults.length);
6031
7390
  }
6032
- stats.indexedChunks += batch.length;
6033
7391
  stats.tokensUsed += result.totalTokensUsed;
6034
- this.logger.recordChunksEmbedded(batch.length);
6035
7392
  this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
6036
7393
  this.logger.embedding("debug", `Embedded batch`, {
6037
- batchSize: batch.length,
7394
+ batchSize: pooledResults.length,
7395
+ requestCount: requestBatch.length,
6038
7396
  tokens: result.totalTokensUsed
6039
7397
  });
6040
7398
  onProgress?.({
@@ -6045,17 +7403,49 @@ var Indexer = class {
6045
7403
  totalChunks: pendingChunks.length
6046
7404
  });
6047
7405
  } catch (error) {
6048
- stats.failedChunks += batch.length;
6049
- this.addFailedBatch(batch, getErrorMessage(error));
7406
+ const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id));
7407
+ const failureMessage = getErrorMessage(error);
7408
+ const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
7409
+ for (const chunk of failedChunks) {
7410
+ if (!failedChunkIds.has(chunk.id)) {
7411
+ failedChunkIds.add(chunk.id);
7412
+ stats.failedChunks += 1;
7413
+ }
7414
+ if (forceScopedReembed) {
7415
+ failedForcedChunkIds.add(chunk.id);
7416
+ }
7417
+ embeddingPartsByChunk.delete(chunk.id);
7418
+ const existingFailedBatchIndex = failedBatchesForCurrentRun.findIndex(
7419
+ (failedBatch2) => failedBatch2.chunks[0]?.id === chunk.id
7420
+ );
7421
+ const existingFailedBatch = existingFailedBatchIndex === -1 ? void 0 : failedBatchesForCurrentRun[existingFailedBatchIndex];
7422
+ const failedBatch = {
7423
+ chunks: [chunk],
7424
+ error: failureMessage,
7425
+ attemptCount: (existingFailedBatch?.attemptCount ?? retryableFailedAttemptCounts.get(chunk.id) ?? 0) + 1,
7426
+ lastAttempt: failureTimestamp
7427
+ };
7428
+ if (existingFailedBatchIndex === -1) {
7429
+ failedBatchesForCurrentRun.push(failedBatch);
7430
+ } else {
7431
+ failedBatchesForCurrentRun[existingFailedBatchIndex] = failedBatch;
7432
+ }
7433
+ }
6050
7434
  this.logger.recordEmbeddingError();
6051
7435
  this.logger.embedding("error", `Failed to embed batch after retries`, {
6052
- batchSize: batch.length,
6053
- error: getErrorMessage(error)
7436
+ batchSize: failedChunks.length,
7437
+ requestCount: requestBatch.length,
7438
+ error: failureMessage
6054
7439
  });
6055
7440
  }
6056
7441
  });
6057
7442
  }
6058
7443
  await queue.onIdle();
7444
+ if (scopedRoots) {
7445
+ this.saveScopedFailedBatches(coalesceFailedBatches(failedBatchesForCurrentRun), scopedRoots);
7446
+ } else {
7447
+ this.saveFailedBatches(coalesceFailedBatches(failedBatchesForCurrentRun));
7448
+ }
6059
7449
  onProgress?.({
6060
7450
  phase: "storing",
6061
7451
  filesProcessed: files.length,
@@ -6063,18 +7453,48 @@ var Indexer = class {
6063
7453
  chunksProcessed: stats.indexedChunks,
6064
7454
  totalChunks: pendingChunks.length
6065
7455
  });
6066
- database.clearBranch(this.currentBranch);
6067
- database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
6068
- database.clearBranchSymbols(this.currentBranch);
6069
- database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
7456
+ const branchChunkIds = Array.from(currentChunkIds).filter(
7457
+ (chunkId) => {
7458
+ const isNewlyFailed = failedChunkIds.has(chunkId) && !retryableChunksWithExistingData.has(chunkId);
7459
+ const isForcedFailed = forceScopedReembed && failedForcedChunkIds.has(chunkId);
7460
+ return !isNewlyFailed && !isForcedFailed;
7461
+ }
7462
+ );
7463
+ database.clearBranch(branchCatalogKey);
7464
+ database.addChunksToBranchBatch(branchCatalogKey, branchChunkIds);
7465
+ database.clearBranchSymbols(branchCatalogKey);
7466
+ database.addSymbolsToBranchBatch(branchCatalogKey, Array.from(allSymbolIds));
6070
7467
  store.save();
6071
7468
  invertedIndex.save();
6072
- this.fileHashCache = currentFileHashes;
6073
- this.saveFileHashCache();
7469
+ if (scopedRoots) {
7470
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
7471
+ } else {
7472
+ this.fileHashCache = currentFileHashes;
7473
+ this.saveFileHashCache();
7474
+ }
6074
7475
  if (this.config.indexing.autoGc && stats.removedChunks > 0) {
6075
- await this.maybeRunOrphanGc();
7476
+ const gcReset = await this.maybeRunOrphanGc();
7477
+ if (gcReset) {
7478
+ stats.durationMs = Date.now() - startTime;
7479
+ stats.warning = gcReset.warning;
7480
+ stats.resetCorruptedIndex = true;
7481
+ this.logger.recordIndexingEnd();
7482
+ this.logger.warn("Indexing ended after resetting corrupted local index during automatic GC", {
7483
+ files: stats.totalFiles,
7484
+ indexed: stats.indexedChunks,
7485
+ existing: stats.existingChunks,
7486
+ removed: stats.removedChunks,
7487
+ failed: stats.failedChunks,
7488
+ tokens: stats.tokensUsed,
7489
+ durationMs: stats.durationMs
7490
+ });
7491
+ return stats;
7492
+ }
6076
7493
  }
6077
7494
  stats.durationMs = Date.now() - startTime;
7495
+ if (forceScopedReembed && failedForcedChunkIds.size === 0) {
7496
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
7497
+ }
6078
7498
  this.saveIndexMetadata(configuredProviderInfo);
6079
7499
  this.indexCompatibility = { compatible: true };
6080
7500
  this.logger.recordIndexingEnd();
@@ -6203,27 +7623,30 @@ var Indexer = class {
6203
7623
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
6204
7624
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
6205
7625
  let branchChunkIds = null;
6206
- if (filterByBranch && this.currentBranch !== "default") {
6207
- branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
7626
+ if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
7627
+ branchChunkIds = new Set(
7628
+ this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
7629
+ );
6208
7630
  }
6209
7631
  const prefilterStartTime = import_perf_hooks.performance.now();
6210
- const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
7632
+ const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
7633
+ const allowBranchPrefilterFallback = this.config.scope !== "global";
6211
7634
  const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
6212
7635
  const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
6213
- const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
6214
- const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
7636
+ const semanticCandidates = allowBranchPrefilterFallback && shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
7637
+ const keywordCandidates = allowBranchPrefilterFallback && shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
6215
7638
  const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
6216
- if (branchChunkIds && branchChunkIds.size === 0) {
7639
+ if (this.config.scope !== "global" && branchChunkIds && branchChunkIds.size === 0) {
6217
7640
  this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
6218
7641
  branch: this.currentBranch
6219
7642
  });
6220
7643
  }
6221
- if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
7644
+ if (allowBranchPrefilterFallback && shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
6222
7645
  this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
6223
7646
  branch: this.currentBranch
6224
7647
  });
6225
7648
  }
6226
- if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
7649
+ if (allowBranchPrefilterFallback && shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
6227
7650
  this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
6228
7651
  branch: this.currentBranch
6229
7652
  });
@@ -6276,26 +7699,13 @@ var Indexer = class {
6276
7699
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
6277
7700
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
6278
7701
  const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
6279
- const baseFiltered = tiered.filter((r) => {
6280
- if (r.score < this.config.search.minScore) return false;
6281
- if (options?.fileType) {
6282
- const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
6283
- if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
6284
- }
6285
- if (options?.directory) {
6286
- const normalizedDir = options.directory.replace(/^\/|\/$/g, "");
6287
- if (!r.metadata.filePath.includes(`/${normalizedDir}/`) && !r.metadata.filePath.includes(`${normalizedDir}/`)) return false;
6288
- }
6289
- if (options?.chunkType) {
6290
- if (r.metadata.chunkType !== options.chunkType) return false;
6291
- }
6292
- return true;
6293
- });
7702
+ const baseFiltered = tiered.filter((r) => matchesSearchFilters(r, options, this.config.search.minScore));
6294
7703
  const implementationOnly = baseFiltered.filter(
6295
7704
  (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
6296
7705
  );
6297
7706
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
6298
- const finalResults = filtered;
7707
+ const identifierFallback = !options?.definitionIntent && filtered.length === 0 && identifierHints.length > 0 ? buildSymbolDefinitionLane(query, database, branchChunkIds, maxResults, union, true).filter((r) => matchesSearchFilters(r, options, this.config.search.minScore)).slice(0, maxResults) : [];
7708
+ const finalResults = filtered.length > 0 ? filtered : identifierFallback;
6299
7709
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
6300
7710
  this.logger.recordSearch(totalSearchMs, {
6301
7711
  embeddingMs,
@@ -6321,7 +7731,7 @@ var Indexer = class {
6321
7731
  let contextEndLine = r.metadata.endLine;
6322
7732
  if (!metadataOnly && this.config.search.includeContext) {
6323
7733
  try {
6324
- const fileContent = await import_fs5.promises.readFile(
7734
+ const fileContent = await import_fs7.promises.readFile(
6325
7735
  r.metadata.filePath,
6326
7736
  "utf-8"
6327
7737
  );
@@ -6365,7 +7775,8 @@ var Indexer = class {
6365
7775
  return results.slice(0, limit);
6366
7776
  }
6367
7777
  async getStatus() {
6368
- const { store, configuredProviderInfo } = await this.ensureInitialized();
7778
+ const { store, configuredProviderInfo, database } = await this.ensureInitialized();
7779
+ const failedBatchesCount = this.getFailedBatchesCount();
6369
7780
  return {
6370
7781
  indexed: store.count() > 0,
6371
7782
  vectorCount: store.count(),
@@ -6374,23 +7785,86 @@ var Indexer = class {
6374
7785
  indexPath: this.indexPath,
6375
7786
  currentBranch: this.currentBranch,
6376
7787
  baseBranch: this.baseBranch,
6377
- compatibility: this.indexCompatibility
7788
+ compatibility: this.indexCompatibility,
7789
+ failedBatchesCount,
7790
+ failedBatchesPath: failedBatchesCount > 0 ? this.failedBatchesPath : void 0,
7791
+ warning: database.getMetadata(STARTUP_WARNING_METADATA_KEY) ?? void 0
6378
7792
  };
6379
7793
  }
6380
7794
  async clearIndex() {
6381
7795
  const { store, invertedIndex, database } = await this.ensureInitialized();
7796
+ if (this.config.scope === "global") {
7797
+ store.load();
7798
+ invertedIndex.load();
7799
+ this.loadFileHashCache();
7800
+ const roots = this.getScopedRoots();
7801
+ const compatibility = this.checkCompatibility();
7802
+ const allMetadata = store.getAllMetadata();
7803
+ const hasForeignData = allMetadata.some(({ metadata }) => !this.isFileInCurrentScope(metadata.filePath, roots)) || this.hasForeignScopedBranchData() || this.hasForeignScopedFileHashData(roots) || this.hasForeignScopedFailedBatches(roots);
7804
+ if (!compatibility.compatible && hasForeignData) {
7805
+ if (compatibility.code === "EMBEDDING_STRATEGY_MISMATCH" /* EMBEDDING_STRATEGY_MISMATCH */) {
7806
+ this.clearSharedIndexProjectData(store, invertedIndex, database, roots);
7807
+ this.clearScopedFileHashCache(roots);
7808
+ this.clearScopedFailedBatches(roots);
7809
+ database.setMetadata(this.getProjectForceReembedMetadataKey(), "true");
7810
+ database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey());
7811
+ this.indexCompatibility = { compatible: true };
7812
+ return;
7813
+ }
7814
+ throw new Error(
7815
+ `Global index compatibility reset is unsafe because the shared index contains files from other projects. The current global index cannot be force-rebuilt for ${this.projectRoot} without deleting other repositories' indexed data. Use scope="project" for isolated rebuilds, or manually delete the shared global index if you intend to rebuild all projects.`
7816
+ );
7817
+ }
7818
+ if (!hasForeignData) {
7819
+ store.clear();
7820
+ store.save();
7821
+ invertedIndex.clear();
7822
+ invertedIndex.save();
7823
+ this.fileHashCache.clear();
7824
+ this.saveFileHashCache();
7825
+ database.clearAllIndexedData();
7826
+ this.saveFailedBatches([]);
7827
+ database.deleteMetadata("index.version");
7828
+ database.deleteMetadata("index.embeddingProvider");
7829
+ database.deleteMetadata("index.embeddingModel");
7830
+ database.deleteMetadata("index.embeddingDimensions");
7831
+ database.deleteMetadata("index.embeddingStrategyVersion");
7832
+ database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey());
7833
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
7834
+ database.deleteMetadata(this.getLegacyMigrationMetadataKey());
7835
+ database.deleteMetadata("index.createdAt");
7836
+ database.deleteMetadata("index.updatedAt");
7837
+ this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
7838
+ return;
7839
+ }
7840
+ this.clearSharedIndexProjectData(store, invertedIndex, database, roots);
7841
+ this.clearScopedFileHashCache(roots);
7842
+ this.clearScopedFailedBatches(roots);
7843
+ this.indexCompatibility = compatibility;
7844
+ return;
7845
+ }
7846
+ const localProjectIndexPath = path8.join(this.projectRoot, ".opencode", "index");
7847
+ if (path8.resolve(this.indexPath) !== path8.resolve(localProjectIndexPath)) {
7848
+ throw new Error(
7849
+ "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
7850
+ );
7851
+ }
6382
7852
  store.clear();
6383
7853
  store.save();
6384
7854
  invertedIndex.clear();
6385
7855
  invertedIndex.save();
6386
7856
  this.fileHashCache.clear();
6387
7857
  this.saveFileHashCache();
6388
- database.clearBranch(this.currentBranch);
6389
- database.clearBranchSymbols(this.currentBranch);
7858
+ database.clearAllIndexedData();
7859
+ this.saveFailedBatches([]);
6390
7860
  database.deleteMetadata("index.version");
6391
7861
  database.deleteMetadata("index.embeddingProvider");
6392
7862
  database.deleteMetadata("index.embeddingModel");
6393
7863
  database.deleteMetadata("index.embeddingDimensions");
7864
+ database.deleteMetadata("index.embeddingStrategyVersion");
7865
+ database.deleteMetadata(this.getProjectEmbeddingStrategyMetadataKey());
7866
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
7867
+ database.deleteMetadata(this.getLegacyMigrationMetadataKey());
6394
7868
  database.deleteMetadata("index.createdAt");
6395
7869
  database.deleteMetadata("index.updatedAt");
6396
7870
  this.indexCompatibility = this.validateIndexCompatibility(this.configuredProviderInfo);
@@ -6406,28 +7880,61 @@ var Indexer = class {
6406
7880
  filePathsToChunkKeys.set(metadata.filePath, existing);
6407
7881
  }
6408
7882
  const removedFilePaths = [];
6409
- let removedCount = 0;
7883
+ const removedChunkKeys = [];
7884
+ const chunkKeysByRemovedFile = /* @__PURE__ */ new Map();
6410
7885
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
6411
- if (!(0, import_fs5.existsSync)(filePath)) {
7886
+ if (!(0, import_fs7.existsSync)(filePath)) {
7887
+ chunkKeysByRemovedFile.set(filePath, chunkKeys);
6412
7888
  for (const key of chunkKeys) {
6413
- store.remove(key);
6414
- invertedIndex.removeChunk(key);
6415
- removedCount++;
7889
+ removedChunkKeys.push(key);
6416
7890
  }
6417
- database.deleteChunksByFile(filePath);
6418
- database.deleteCallEdgesByFile(filePath);
6419
- database.deleteSymbolsByFile(filePath);
6420
7891
  removedFilePaths.push(filePath);
6421
7892
  }
6422
7893
  }
7894
+ if (removedChunkKeys.length > 0) {
7895
+ this.rebuildVectorStoreExcludingChunkIds(store, database, removedChunkKeys);
7896
+ for (const key of removedChunkKeys) {
7897
+ invertedIndex.removeChunk(key);
7898
+ }
7899
+ }
7900
+ for (const filePath of removedFilePaths) {
7901
+ const fileChunkKeys = chunkKeysByRemovedFile.get(filePath) ?? [];
7902
+ if (fileChunkKeys.length > 0) {
7903
+ database.deleteChunksByIds(fileChunkKeys);
7904
+ }
7905
+ database.deleteCallEdgesByFile(filePath);
7906
+ database.deleteSymbolsByFile(filePath);
7907
+ }
7908
+ const removedCount = removedChunkKeys.length;
6423
7909
  if (removedCount > 0) {
6424
7910
  store.save();
6425
7911
  invertedIndex.save();
6426
7912
  }
6427
- const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
6428
- const gcOrphanChunks = database.gcOrphanChunks();
6429
- const gcOrphanSymbols = database.gcOrphanSymbols();
6430
- const gcOrphanCallEdges = database.gcOrphanCallEdges();
7913
+ let gcOrphanEmbeddings;
7914
+ let gcOrphanChunks;
7915
+ let gcOrphanSymbols;
7916
+ let gcOrphanCallEdges;
7917
+ try {
7918
+ gcOrphanEmbeddings = database.gcOrphanEmbeddings();
7919
+ gcOrphanChunks = database.gcOrphanChunks();
7920
+ gcOrphanSymbols = database.gcOrphanSymbols();
7921
+ gcOrphanCallEdges = database.gcOrphanCallEdges();
7922
+ } catch (error) {
7923
+ if (!await this.tryResetCorruptedIndex("running index health check", error)) {
7924
+ throw error;
7925
+ }
7926
+ await this.ensureInitialized();
7927
+ return {
7928
+ removed: 0,
7929
+ filePaths: [],
7930
+ gcOrphanEmbeddings: 0,
7931
+ gcOrphanChunks: 0,
7932
+ gcOrphanSymbols: 0,
7933
+ gcOrphanCallEdges: 0,
7934
+ resetCorruptedIndex: true,
7935
+ warning: this.getCorruptedIndexWarning(path8.join(this.indexPath, "codebase.db"))
7936
+ };
7937
+ }
6431
7938
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
6432
7939
  this.logger.gc("info", "Health check complete", {
6433
7940
  removedStale: removedCount,
@@ -6438,8 +7945,12 @@ var Indexer = class {
6438
7945
  return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
6439
7946
  }
6440
7947
  async retryFailedBatches() {
6441
- const { store, provider, invertedIndex } = await this.ensureInitialized();
6442
- const failedBatches = this.loadFailedBatches();
7948
+ const { store, provider, invertedIndex, database, configuredProviderInfo } = await this.ensureInitialized();
7949
+ const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
7950
+ const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
7951
+ const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
7952
+ const { scoped: scopedFailedBatches, retained: retainedFailedBatches } = roots ? this.partitionFailedBatches(roots, maxChunkTokens) : { scoped: this.loadFailedBatches(maxChunkTokens), retained: [] };
7953
+ const failedBatches = scopedFailedBatches;
6443
7954
  if (failedBatches.length === 0) {
6444
7955
  return { succeeded: 0, failed: 0, remaining: 0 };
6445
7956
  }
@@ -6447,49 +7958,170 @@ var Indexer = class {
6447
7958
  let failed = 0;
6448
7959
  const stillFailing = [];
6449
7960
  for (const batch of failedBatches) {
7961
+ const batchChunksById = new Map(batch.chunks.map((chunk) => [chunk.id, chunk]));
7962
+ const embeddingPartsByChunk = /* @__PURE__ */ new Map();
7963
+ const completedChunkIds = /* @__PURE__ */ new Set();
7964
+ const failedChunkIds = /* @__PURE__ */ new Set();
7965
+ const failedChunksForBatch = /* @__PURE__ */ new Map();
7966
+ const pooledResults = [];
6450
7967
  try {
6451
- const result = await pRetry(
6452
- async () => {
6453
- const texts = batch.chunks.map((c) => c.text);
6454
- return provider.embedBatch(texts);
6455
- },
6456
- {
6457
- retries: this.config.indexing.retries,
6458
- minTimeout: this.config.indexing.retryDelayMs
6459
- }
7968
+ const requestBatches = createPendingEmbeddingRequestBatches(
7969
+ batch.chunks,
7970
+ getDynamicBatchOptions(configuredProviderInfo)
6460
7971
  );
6461
- const items = batch.chunks.map((chunk, idx) => ({
7972
+ for (const requestBatch of requestBatches) {
7973
+ try {
7974
+ const result = await pRetry(
7975
+ async () => {
7976
+ const texts = requestBatch.map((request) => request.text);
7977
+ return provider.embedBatch(texts);
7978
+ },
7979
+ {
7980
+ retries: this.config.indexing.retries,
7981
+ minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
7982
+ maxTimeout: providerRateLimits.maxRetryMs,
7983
+ factor: 2,
7984
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError)
7985
+ }
7986
+ );
7987
+ const touchedChunkIds = /* @__PURE__ */ new Set();
7988
+ requestBatch.forEach((request, idx) => {
7989
+ if (failedChunkIds.has(request.chunk.id) || completedChunkIds.has(request.chunk.id)) {
7990
+ return;
7991
+ }
7992
+ const vector = result.embeddings[idx];
7993
+ if (!vector) {
7994
+ throw new Error(`Embedding API returned too few vectors for chunk ${request.chunk.id}`);
7995
+ }
7996
+ const parts = embeddingPartsByChunk.get(request.chunk.id) ?? [];
7997
+ parts[request.partIndex] = {
7998
+ vector,
7999
+ tokenCount: request.tokenCount
8000
+ };
8001
+ embeddingPartsByChunk.set(request.chunk.id, parts);
8002
+ touchedChunkIds.add(request.chunk.id);
8003
+ });
8004
+ for (const chunkId of touchedChunkIds) {
8005
+ if (failedChunkIds.has(chunkId) || completedChunkIds.has(chunkId)) {
8006
+ continue;
8007
+ }
8008
+ const chunk = batchChunksById.get(chunkId);
8009
+ if (!chunk) {
8010
+ continue;
8011
+ }
8012
+ const parts = embeddingPartsByChunk.get(chunk.id) ?? [];
8013
+ if (!hasAllEmbeddingParts(parts, chunk.texts.length)) {
8014
+ continue;
8015
+ }
8016
+ const orderedParts = parts;
8017
+ pooledResults.push({
8018
+ chunk,
8019
+ vector: poolEmbeddingVectors(
8020
+ orderedParts.map((part) => part.vector),
8021
+ orderedParts.map((part) => part.tokenCount)
8022
+ )
8023
+ });
8024
+ }
8025
+ this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
8026
+ } catch (error) {
8027
+ const failureMessage = String(error);
8028
+ const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8029
+ const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id) && !failedChunkIds.has(chunk.id));
8030
+ for (const chunk of failedChunks) {
8031
+ failedChunkIds.add(chunk.id);
8032
+ embeddingPartsByChunk.delete(chunk.id);
8033
+ failedChunksForBatch.set(chunk.id, {
8034
+ chunks: [chunk],
8035
+ attemptCount: batch.attemptCount + 1,
8036
+ lastAttempt: failureTimestamp,
8037
+ error: failureMessage
8038
+ });
8039
+ }
8040
+ failed += failedChunks.length;
8041
+ this.logger.recordEmbeddingError();
8042
+ }
8043
+ }
8044
+ const successfulResults = pooledResults.filter(({ chunk }) => !failedChunkIds.has(chunk.id));
8045
+ const items = successfulResults.map(({ chunk, vector }) => ({
6462
8046
  id: chunk.id,
6463
- vector: result.embeddings[idx],
8047
+ vector,
6464
8048
  metadata: chunk.metadata
6465
8049
  }));
6466
- store.addBatch(items);
6467
- for (const chunk of batch.chunks) {
8050
+ if (items.length > 0) {
8051
+ store.addBatch(items);
8052
+ }
8053
+ if (successfulResults.length > 0) {
8054
+ try {
8055
+ database.upsertEmbeddingsBatch(
8056
+ successfulResults.map(({ chunk, vector }) => ({
8057
+ contentHash: chunk.contentHash,
8058
+ embedding: float32ArrayToBuffer(vector),
8059
+ chunkText: chunk.storageText,
8060
+ model: configuredProviderInfo.modelInfo.model
8061
+ }))
8062
+ );
8063
+ } catch (dbError) {
8064
+ this.rebuildVectorStoreExcludingChunkIds(
8065
+ store,
8066
+ database,
8067
+ successfulResults.map(({ chunk }) => chunk.id)
8068
+ );
8069
+ throw dbError;
8070
+ }
8071
+ }
8072
+ for (const { chunk } of successfulResults) {
6468
8073
  invertedIndex.removeChunk(chunk.id);
6469
8074
  invertedIndex.addChunk(chunk.id, chunk.content);
8075
+ completedChunkIds.add(chunk.id);
8076
+ embeddingPartsByChunk.delete(chunk.id);
6470
8077
  }
6471
- this.logger.recordChunksEmbedded(batch.chunks.length);
6472
- this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
6473
- succeeded += batch.chunks.length;
8078
+ database.addChunksToBranchBatch(
8079
+ this.getBranchCatalogKey(),
8080
+ successfulResults.map(({ chunk }) => chunk.id)
8081
+ );
8082
+ this.logger.recordChunksEmbedded(successfulResults.length);
8083
+ succeeded += successfulResults.length;
8084
+ stillFailing.push(...failedChunksForBatch.values());
6474
8085
  } catch (error) {
6475
- failed += batch.chunks.length;
8086
+ const failureMessage = getErrorMessage(error);
8087
+ const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8088
+ const unaccountedChunks = batch.chunks.filter(
8089
+ (chunk) => !failedChunksForBatch.has(chunk.id) && !completedChunkIds.has(chunk.id)
8090
+ );
8091
+ for (const chunk of unaccountedChunks) {
8092
+ failedChunksForBatch.set(chunk.id, {
8093
+ chunks: [chunk],
8094
+ attemptCount: batch.attemptCount + 1,
8095
+ lastAttempt: failureTimestamp,
8096
+ error: failureMessage
8097
+ });
8098
+ }
8099
+ failed += unaccountedChunks.length;
6476
8100
  this.logger.recordEmbeddingError();
6477
- stillFailing.push({
6478
- ...batch,
6479
- attemptCount: batch.attemptCount + 1,
6480
- lastAttempt: (/* @__PURE__ */ new Date()).toISOString(),
6481
- error: String(error)
6482
- });
8101
+ stillFailing.push(...coalesceFailedBatches(Array.from(failedChunksForBatch.values())));
6483
8102
  }
6484
8103
  }
6485
- this.saveFailedBatches(stillFailing);
8104
+ const persistedStillFailing = coalesceFailedBatches(stillFailing);
8105
+ if (roots) {
8106
+ this.saveFailedBatches([...retainedFailedBatches, ...persistedStillFailing]);
8107
+ } else {
8108
+ this.saveFailedBatches(persistedStillFailing);
8109
+ }
6486
8110
  if (succeeded > 0) {
6487
8111
  store.save();
6488
8112
  invertedIndex.save();
6489
8113
  }
6490
- return { succeeded, failed, remaining: stillFailing.length };
8114
+ if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
8115
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
8116
+ this.saveIndexMetadata(configuredProviderInfo);
8117
+ this.indexCompatibility = { compatible: true };
8118
+ }
8119
+ return { succeeded, failed, remaining: persistedStillFailing.length };
6491
8120
  }
6492
8121
  getFailedBatchesCount() {
8122
+ if (this.config.scope === "global") {
8123
+ return this.partitionFailedBatches(this.getScopedRoots()).scoped.length;
8124
+ }
6493
8125
  return this.loadFailedBatches().length;
6494
8126
  }
6495
8127
  getCurrentBranch() {
@@ -6538,20 +8170,23 @@ var Indexer = class {
6538
8170
  const semanticResults = store.search(embedding, limit * 2);
6539
8171
  const vectorMs = import_perf_hooks.performance.now() - vectorStartTime;
6540
8172
  let branchChunkIds = null;
6541
- if (filterByBranch && this.currentBranch !== "default") {
6542
- branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
8173
+ if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
8174
+ branchChunkIds = new Set(
8175
+ this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
8176
+ );
6543
8177
  }
6544
8178
  const prefilterStartTime = import_perf_hooks.performance.now();
6545
- const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
8179
+ const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
8180
+ const allowBranchPrefilterFallback = this.config.scope !== "global";
6546
8181
  const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
6547
- const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
8182
+ const semanticCandidates = allowBranchPrefilterFallback && shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
6548
8183
  const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
6549
- if (branchChunkIds && branchChunkIds.size === 0) {
8184
+ if (this.config.scope !== "global" && branchChunkIds && branchChunkIds.size === 0) {
6550
8185
  this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
6551
8186
  branch: this.currentBranch
6552
8187
  });
6553
8188
  }
6554
- if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
8189
+ if (allowBranchPrefilterFallback && shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
6555
8190
  this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
6556
8191
  branch: this.currentBranch
6557
8192
  });
@@ -6600,7 +8235,7 @@ var Indexer = class {
6600
8235
  let content = "";
6601
8236
  if (this.config.search.includeContext) {
6602
8237
  try {
6603
- const fileContent = await import_fs5.promises.readFile(
8238
+ const fileContent = await import_fs7.promises.readFile(
6604
8239
  r.metadata.filePath,
6605
8240
  "utf-8"
6606
8241
  );
@@ -6624,11 +8259,39 @@ var Indexer = class {
6624
8259
  }
6625
8260
  async getCallers(targetName) {
6626
8261
  const { database } = await this.ensureInitialized();
6627
- return database.getCallersWithContext(targetName, this.currentBranch);
8262
+ const seen = /* @__PURE__ */ new Set();
8263
+ const results = [];
8264
+ for (const branchKey of this.getBranchCatalogKeys()) {
8265
+ for (const edge of database.getCallersWithContext(targetName, branchKey)) {
8266
+ if (!seen.has(edge.id)) {
8267
+ seen.add(edge.id);
8268
+ results.push(edge);
8269
+ }
8270
+ }
8271
+ }
8272
+ return results;
6628
8273
  }
6629
8274
  async getCallees(symbolId) {
6630
8275
  const { database } = await this.ensureInitialized();
6631
- return database.getCallees(symbolId, this.currentBranch);
8276
+ const seen = /* @__PURE__ */ new Set();
8277
+ const results = [];
8278
+ for (const branchKey of this.getBranchCatalogKeys()) {
8279
+ for (const edge of database.getCallees(symbolId, branchKey)) {
8280
+ if (!seen.has(edge.id)) {
8281
+ seen.add(edge.id);
8282
+ results.push(edge);
8283
+ }
8284
+ }
8285
+ }
8286
+ return results;
8287
+ }
8288
+ async close() {
8289
+ await this.database?.close();
8290
+ this.database = null;
8291
+ this.store = null;
8292
+ this.invertedIndex = null;
8293
+ this.provider = null;
8294
+ this.reranker = null;
6632
8295
  }
6633
8296
  };
6634
8297
 
@@ -6879,9 +8542,9 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6879
8542
  }
6880
8543
 
6881
8544
  // src/eval/schema.ts
6882
- var import_fs6 = require("fs");
8545
+ var import_fs8 = require("fs");
6883
8546
  function parseJsonFile(filePath) {
6884
- const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
8547
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
6885
8548
  return JSON.parse(content);
6886
8549
  }
6887
8550
  function isRecord(value) {
@@ -6890,23 +8553,23 @@ function isRecord(value) {
6890
8553
  function isStringArray2(value) {
6891
8554
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6892
8555
  }
6893
- function asPositiveNumber(value, path11) {
8556
+ function asPositiveNumber(value, path13) {
6894
8557
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6895
- throw new Error(`${path11} must be a non-negative number`);
8558
+ throw new Error(`${path13} must be a non-negative number`);
6896
8559
  }
6897
8560
  return value;
6898
8561
  }
6899
- function parseQueryType(value, path11) {
8562
+ function parseQueryType(value, path13) {
6900
8563
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6901
8564
  return value;
6902
8565
  }
6903
8566
  throw new Error(
6904
- `${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
8567
+ `${path13} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6905
8568
  );
6906
8569
  }
6907
- function parseExpected(input, path11) {
8570
+ function parseExpected(input, path13) {
6908
8571
  if (!isRecord(input)) {
6909
- throw new Error(`${path11} must be an object`);
8572
+ throw new Error(`${path13} must be an object`);
6910
8573
  }
6911
8574
  const filePathRaw = input.filePath;
6912
8575
  const acceptableFilesRaw = input.acceptableFiles;
@@ -6915,16 +8578,16 @@ function parseExpected(input, path11) {
6915
8578
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6916
8579
  const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6917
8580
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6918
- throw new Error(`${path11} must include either expected.filePath or expected.acceptableFiles`);
8581
+ throw new Error(`${path13} must include either expected.filePath or expected.acceptableFiles`);
6919
8582
  }
6920
8583
  if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6921
- throw new Error(`${path11}.acceptableFiles must be an array of strings`);
8584
+ throw new Error(`${path13}.acceptableFiles must be an array of strings`);
6922
8585
  }
6923
8586
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6924
- throw new Error(`${path11}.symbol must be a string when provided`);
8587
+ throw new Error(`${path13}.symbol must be a string when provided`);
6925
8588
  }
6926
8589
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6927
- throw new Error(`${path11}.branch must be a string when provided`);
8590
+ throw new Error(`${path13}.branch must be a string when provided`);
6928
8591
  }
6929
8592
  return {
6930
8593
  filePath,
@@ -6934,25 +8597,25 @@ function parseExpected(input, path11) {
6934
8597
  };
6935
8598
  }
6936
8599
  function parseQuery(input, index) {
6937
- const path11 = `queries[${index}]`;
8600
+ const path13 = `queries[${index}]`;
6938
8601
  if (!isRecord(input)) {
6939
- throw new Error(`${path11} must be an object`);
8602
+ throw new Error(`${path13} must be an object`);
6940
8603
  }
6941
8604
  const id = input.id;
6942
8605
  const query = input.query;
6943
8606
  const queryType = input.queryType;
6944
8607
  const expected = input.expected;
6945
8608
  if (typeof id !== "string" || id.trim().length === 0) {
6946
- throw new Error(`${path11}.id must be a non-empty string`);
8609
+ throw new Error(`${path13}.id must be a non-empty string`);
6947
8610
  }
6948
8611
  if (typeof query !== "string" || query.trim().length === 0) {
6949
- throw new Error(`${path11}.query must be a non-empty string`);
8612
+ throw new Error(`${path13}.query must be a non-empty string`);
6950
8613
  }
6951
8614
  return {
6952
8615
  id,
6953
8616
  query,
6954
- queryType: parseQueryType(queryType, `${path11}.queryType`),
6955
- expected: parseExpected(expected, `${path11}.expected`)
8617
+ queryType: parseQueryType(queryType, `${path13}.queryType`),
8618
+ expected: parseExpected(expected, `${path13}.expected`)
6956
8619
  };
6957
8620
  }
6958
8621
  function parseGoldenDataset(raw, sourceLabel) {
@@ -7049,34 +8712,82 @@ function loadBudget(budgetPath) {
7049
8712
 
7050
8713
  // src/eval/runner.ts
7051
8714
  function toAbsolute(projectRoot, maybeRelative) {
7052
- return path7.isAbsolute(maybeRelative) ? maybeRelative : path7.join(projectRoot, maybeRelative);
8715
+ return path9.isAbsolute(maybeRelative) ? maybeRelative : path9.join(projectRoot, maybeRelative);
8716
+ }
8717
+ function isProjectScopedConfigPath(configPath) {
8718
+ return path9.basename(configPath) === "codebase-index.json" && path9.basename(path9.dirname(configPath)) === ".opencode";
8719
+ }
8720
+ function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
8721
+ const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
8722
+ if (!Array.isArray(config.knowledgeBases)) {
8723
+ return config;
8724
+ }
8725
+ config.knowledgeBases = isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
8726
+ config.knowledgeBases,
8727
+ path9.dirname(path9.dirname(resolvedConfigPath)),
8728
+ projectRoot
8729
+ ) : rebasePathEntries(
8730
+ config.knowledgeBases,
8731
+ path9.dirname(resolvedConfigPath),
8732
+ projectRoot
8733
+ );
8734
+ return config;
7053
8735
  }
7054
8736
  function loadRawConfig(projectRoot, configPath) {
7055
8737
  const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
7056
- if (fromPath && (0, import_fs7.existsSync)(fromPath)) {
7057
- return JSON.parse((0, import_fs8.readFileSync)(fromPath, "utf-8"));
8738
+ if (fromPath && (0, import_fs9.existsSync)(fromPath)) {
8739
+ return normalizeEvalConfigKnowledgeBases(
8740
+ JSON.parse((0, import_fs11.readFileSync)(fromPath, "utf-8")),
8741
+ projectRoot,
8742
+ fromPath
8743
+ );
7058
8744
  }
7059
- const projectConfig = path7.join(projectRoot, ".opencode", "codebase-index.json");
7060
- if ((0, import_fs7.existsSync)(projectConfig)) {
7061
- return JSON.parse((0, import_fs8.readFileSync)(projectConfig, "utf-8"));
8745
+ const projectConfig = resolveProjectConfigPath(projectRoot);
8746
+ if ((0, import_fs9.existsSync)(projectConfig)) {
8747
+ return normalizeEvalConfigKnowledgeBases(
8748
+ JSON.parse((0, import_fs11.readFileSync)(projectConfig, "utf-8")),
8749
+ projectRoot,
8750
+ projectConfig
8751
+ );
7062
8752
  }
7063
- const globalConfig = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
7064
- if ((0, import_fs7.existsSync)(globalConfig)) {
7065
- return JSON.parse((0, import_fs8.readFileSync)(globalConfig, "utf-8"));
8753
+ const globalConfig = path9.join(os5.homedir(), ".config", "opencode", "codebase-index.json");
8754
+ if ((0, import_fs9.existsSync)(globalConfig)) {
8755
+ return JSON.parse((0, import_fs11.readFileSync)(globalConfig, "utf-8"));
7066
8756
  }
7067
8757
  return {};
7068
8758
  }
7069
8759
  function getIndexRootPath(projectRoot, scope) {
7070
- if (scope === "global") {
7071
- return path7.join(os3.homedir(), ".opencode", "global-index");
7072
- }
7073
- return path7.join(projectRoot, ".opencode", "index");
8760
+ return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
8761
+ }
8762
+ function getLocalProjectIndexRoot(projectRoot) {
8763
+ return path9.join(projectRoot, ".opencode", "index");
8764
+ }
8765
+ function getLocalProjectConfigPath(projectRoot) {
8766
+ return path9.join(projectRoot, ".opencode", "codebase-index.json");
7074
8767
  }
7075
8768
  function clearIndexRoot(projectRoot, scope) {
7076
- const indexRoot = getIndexRootPath(projectRoot, scope);
7077
- if ((0, import_fs7.existsSync)(indexRoot)) {
7078
- (0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
8769
+ const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
8770
+ if ((0, import_fs9.existsSync)(indexRoot)) {
8771
+ (0, import_fs12.rmSync)(indexRoot, { recursive: true, force: true });
8772
+ }
8773
+ }
8774
+ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
8775
+ const localConfigPath = getLocalProjectConfigPath(projectRoot);
8776
+ const resolvedConfigPath = configPath ? toAbsolute(projectRoot, configPath) : resolveProjectConfigPath(projectRoot);
8777
+ if (!configPath && (0, import_fs9.existsSync)(localConfigPath)) {
8778
+ return localConfigPath;
7079
8779
  }
8780
+ if (!(0, import_fs9.existsSync)(resolvedConfigPath) || resolvedConfigPath === localConfigPath) {
8781
+ return resolvedConfigPath;
8782
+ }
8783
+ const sourceConfig = normalizeEvalConfigKnowledgeBases(
8784
+ JSON.parse((0, import_fs11.readFileSync)(resolvedConfigPath, "utf-8")),
8785
+ projectRoot,
8786
+ resolvedConfigPath
8787
+ );
8788
+ (0, import_fs10.mkdirSync)(path9.dirname(localConfigPath), { recursive: true });
8789
+ (0, import_fs13.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
8790
+ return localConfigPath;
7080
8791
  }
7081
8792
  function loadParsedConfig(projectRoot, configPath) {
7082
8793
  const raw = loadRawConfig(projectRoot, configPath);
@@ -7108,94 +8819,99 @@ async function runEvaluation(options) {
7108
8819
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
7109
8820
  const budgetPath = options.budgetPath ? toAbsolute(options.projectRoot, options.budgetPath) : void 0;
7110
8821
  const dataset = loadGoldenDataset(datasetPath);
7111
- const parsedConfig = loadParsedConfig(options.projectRoot, options.configPath);
8822
+ const resolvedEvalConfigPath = options.reindex ? ensureLocalEvalProjectConfig(options.projectRoot, options.configPath) : options.configPath;
8823
+ const parsedConfig = loadParsedConfig(options.projectRoot, resolvedEvalConfigPath);
7112
8824
  const effectiveConfig = resolveSearchConfig(parsedConfig, options.searchOverrides);
7113
8825
  if (options.reindex) {
7114
8826
  clearIndexRoot(options.projectRoot, effectiveConfig.scope);
7115
8827
  }
7116
8828
  const indexer = new Indexer(options.projectRoot, effectiveConfig);
7117
- await indexer.index();
7118
- const perQuery = [];
7119
- for (const query of dataset.queries) {
7120
- if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
7121
- throw new Error(
7122
- `Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
7123
- );
7124
- }
7125
- const start = import_perf_hooks2.performance.now();
7126
- const result = await indexer.search(query.query, 10, {
7127
- metadataOnly: true,
7128
- filterByBranch: query.expected.branch ? true : false
7129
- });
7130
- const elapsed = import_perf_hooks2.performance.now() - start;
7131
- const materialized = result.map((item) => ({
7132
- filePath: item.filePath,
7133
- startLine: item.startLine,
7134
- endLine: item.endLine,
7135
- score: item.score,
7136
- chunkType: item.chunkType,
7137
- name: item.name
7138
- }));
7139
- perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
7140
- }
7141
- const logger = indexer.getLogger();
7142
- const metricSnapshot = logger.getMetrics();
7143
- const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
7144
- const summary = {
7145
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7146
- projectRoot: options.projectRoot,
7147
- datasetPath,
7148
- datasetName: dataset.name,
7149
- datasetVersion: dataset.version,
7150
- queryCount: dataset.queries.length,
7151
- topK: 10,
7152
- searchConfig: {
7153
- fusionStrategy: effectiveConfig.search.fusionStrategy,
7154
- hybridWeight: effectiveConfig.search.hybridWeight,
7155
- rrfK: effectiveConfig.search.rrfK,
7156
- rerankTopN: effectiveConfig.search.rerankTopN
7157
- },
7158
- metrics: computeEvalMetrics(
7159
- dataset.queries,
7160
- perQuery,
7161
- metricSnapshot.embeddingApiCalls,
7162
- metricSnapshot.embeddingTokensUsed,
7163
- costPer1MTokensUsd
7164
- )
7165
- };
7166
- const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
7167
- const perQueryArtifact = buildPerQueryArtifact(perQuery);
7168
- writeJson(path7.join(outputDir, "summary.json"), summary);
7169
- writeJson(path7.join(outputDir, "per-query.json"), perQueryArtifact);
7170
- let comparison;
7171
- if (againstPath) {
7172
- const baseline = loadSummary(againstPath);
7173
- comparison = compareSummaries(summary, baseline, againstPath);
7174
- writeJson(path7.join(outputDir, "compare.json"), comparison);
7175
- }
7176
- let gate;
7177
- if (options.ciMode) {
7178
- if (!budgetPath) {
7179
- throw new Error("CI mode requires --budget path");
7180
- }
7181
- const budget = loadBudget(budgetPath);
7182
- if (!comparison && budget.baselinePath) {
7183
- const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
7184
- if ((0, import_fs7.existsSync)(resolvedBaseline)) {
7185
- const baselineSummary = loadSummary(resolvedBaseline);
7186
- comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
7187
- writeJson(path7.join(outputDir, "compare.json"), comparison);
7188
- } else if (budget.failOnMissingBaseline) {
8829
+ try {
8830
+ await indexer.index();
8831
+ const perQuery = [];
8832
+ for (const query of dataset.queries) {
8833
+ if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
7189
8834
  throw new Error(
7190
- `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
8835
+ `Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
7191
8836
  );
7192
8837
  }
8838
+ const start = import_perf_hooks2.performance.now();
8839
+ const result = await indexer.search(query.query, 10, {
8840
+ metadataOnly: true,
8841
+ filterByBranch: query.expected.branch ? true : false
8842
+ });
8843
+ const elapsed = import_perf_hooks2.performance.now() - start;
8844
+ const materialized = result.map((item) => ({
8845
+ filePath: item.filePath,
8846
+ startLine: item.startLine,
8847
+ endLine: item.endLine,
8848
+ score: item.score,
8849
+ chunkType: item.chunkType,
8850
+ name: item.name
8851
+ }));
8852
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
8853
+ }
8854
+ const logger = indexer.getLogger();
8855
+ const metricSnapshot = logger.getMetrics();
8856
+ const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
8857
+ const summary = {
8858
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8859
+ projectRoot: options.projectRoot,
8860
+ datasetPath,
8861
+ datasetName: dataset.name,
8862
+ datasetVersion: dataset.version,
8863
+ queryCount: dataset.queries.length,
8864
+ topK: 10,
8865
+ searchConfig: {
8866
+ fusionStrategy: effectiveConfig.search.fusionStrategy,
8867
+ hybridWeight: effectiveConfig.search.hybridWeight,
8868
+ rrfK: effectiveConfig.search.rrfK,
8869
+ rerankTopN: effectiveConfig.search.rerankTopN
8870
+ },
8871
+ metrics: computeEvalMetrics(
8872
+ dataset.queries,
8873
+ perQuery,
8874
+ metricSnapshot.embeddingApiCalls,
8875
+ metricSnapshot.embeddingTokensUsed,
8876
+ costPer1MTokensUsd
8877
+ )
8878
+ };
8879
+ const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
8880
+ const perQueryArtifact = buildPerQueryArtifact(perQuery);
8881
+ writeJson(path9.join(outputDir, "summary.json"), summary);
8882
+ writeJson(path9.join(outputDir, "per-query.json"), perQueryArtifact);
8883
+ let comparison;
8884
+ if (againstPath) {
8885
+ const baseline = loadSummary(againstPath);
8886
+ comparison = compareSummaries(summary, baseline, againstPath);
8887
+ writeJson(path9.join(outputDir, "compare.json"), comparison);
8888
+ }
8889
+ let gate;
8890
+ if (options.ciMode) {
8891
+ if (!budgetPath) {
8892
+ throw new Error("CI mode requires --budget path");
8893
+ }
8894
+ const budget = loadBudget(budgetPath);
8895
+ if (!comparison && budget.baselinePath) {
8896
+ const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
8897
+ if ((0, import_fs9.existsSync)(resolvedBaseline)) {
8898
+ const baselineSummary = loadSummary(resolvedBaseline);
8899
+ comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
8900
+ writeJson(path9.join(outputDir, "compare.json"), comparison);
8901
+ } else if (budget.failOnMissingBaseline) {
8902
+ throw new Error(
8903
+ `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
8904
+ );
8905
+ }
8906
+ }
8907
+ gate = evaluateBudgetGate(budget, summary, comparison);
7193
8908
  }
7194
- gate = evaluateBudgetGate(budget, summary, comparison);
8909
+ const markdown = createSummaryMarkdown(summary, comparison, gate);
8910
+ writeText(path9.join(outputDir, "summary.md"), markdown);
8911
+ return { outputDir, summary, perQuery, comparison, gate };
8912
+ } finally {
8913
+ await indexer.close();
7195
8914
  }
7196
- const markdown = createSummaryMarkdown(summary, comparison, gate);
7197
- writeText(path7.join(outputDir, "summary.md"), markdown);
7198
- return { outputDir, summary, perQuery, comparison, gate };
7199
8915
  }
7200
8916
  async function runSweep(options, sweep) {
7201
8917
  const fusionValues = sweep.fusionStrategy && sweep.fusionStrategy.length > 0 ? [...sweep.fusionStrategy] : [void 0];
@@ -7249,15 +8965,15 @@ async function runSweep(options, sweep) {
7249
8965
  bestByMrrAt10,
7250
8966
  bestByP95Latency
7251
8967
  };
7252
- writeJson(path7.join(outputDir, "compare.json"), aggregate);
8968
+ writeJson(path9.join(outputDir, "compare.json"), aggregate);
7253
8969
  const md = createSummaryMarkdown(
7254
8970
  bestByHitAt5?.summary ?? runs[0].summary,
7255
8971
  bestByHitAt5?.comparison,
7256
8972
  void 0,
7257
8973
  aggregate
7258
8974
  );
7259
- writeText(path7.join(outputDir, "summary.md"), md);
7260
- writeJson(path7.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
8975
+ writeText(path9.join(outputDir, "summary.md"), md);
8976
+ writeJson(path9.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
7261
8977
  return { outputDir, aggregate };
7262
8978
  }
7263
8979
 
@@ -7333,12 +9049,12 @@ function parseEvalArgs(argv, cwd) {
7333
9049
  const arg = argv[i];
7334
9050
  const next = argv[i + 1];
7335
9051
  if (arg === "--project" && next) {
7336
- parsed.projectRoot = path8.resolve(cwd, next);
9052
+ parsed.projectRoot = path10.resolve(cwd, next);
7337
9053
  i += 1;
7338
9054
  continue;
7339
9055
  }
7340
9056
  if (arg === "--config" && next) {
7341
- parsed.configPath = path8.resolve(cwd, next);
9057
+ parsed.configPath = path10.resolve(cwd, next);
7342
9058
  i += 1;
7343
9059
  continue;
7344
9060
  }
@@ -7532,22 +9248,22 @@ async function handleEvalCommand(args, cwd) {
7532
9248
  if (!parsed.againstPath.endsWith(".json")) {
7533
9249
  throw new Error("eval diff --against must point to a summary JSON file");
7534
9250
  }
7535
- const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath), {
9251
+ const currentSummary = loadSummary(path10.resolve(parsed.projectRoot, currentPath), {
7536
9252
  allowLegacyDiversityMetrics: true
7537
9253
  });
7538
- const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath), {
9254
+ const baselineSummary = loadSummary(path10.resolve(parsed.projectRoot, parsed.againstPath), {
7539
9255
  allowLegacyDiversityMetrics: true
7540
9256
  });
7541
9257
  const comparison = compareSummaries(
7542
9258
  currentSummary,
7543
9259
  baselineSummary,
7544
- path8.resolve(parsed.projectRoot, parsed.againstPath)
9260
+ path10.resolve(parsed.projectRoot, parsed.againstPath)
7545
9261
  );
7546
- const outputDir = createRunDirectory(path8.resolve(parsed.projectRoot, parsed.outputRoot));
9262
+ const outputDir = createRunDirectory(path10.resolve(parsed.projectRoot, parsed.outputRoot));
7547
9263
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
7548
- writeJson(path8.join(outputDir, "compare.json"), comparison);
7549
- writeText(path8.join(outputDir, "summary.md"), summaryMd);
7550
- writeJson(path8.join(outputDir, "summary.json"), currentSummary);
9264
+ writeJson(path10.join(outputDir, "compare.json"), comparison);
9265
+ writeText(path10.join(outputDir, "summary.md"), summaryMd);
9266
+ writeJson(path10.join(outputDir, "summary.json"), currentSummary);
7551
9267
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
7552
9268
  return 0;
7553
9269
  }
@@ -7557,6 +9273,8 @@ async function handleEvalCommand(args, cwd) {
7557
9273
  // src/mcp-server.ts
7558
9274
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
7559
9275
  var import_zod = require("zod");
9276
+ var path11 = __toESM(require("path"), 1);
9277
+ var import_fs14 = require("fs");
7560
9278
 
7561
9279
  // src/tools/utils.ts
7562
9280
  var MAX_CONTENT_LINES = 30;
@@ -7566,36 +9284,24 @@ function truncateContent(content) {
7566
9284
  return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
7567
9285
  // ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
7568
9286
  }
7569
- function formatDefinitionLookup(results, query) {
7570
- if (results.length === 0) {
7571
- return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
7572
- }
7573
- const formatted = results.map((r, idx) => {
7574
- const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
7575
- return `${header} (score: ${r.score.toFixed(2)})
7576
- \`\`\`
7577
- ${truncateContent(r.content)}
7578
- \`\`\``;
7579
- });
7580
- return formatted.join("\n\n");
7581
- }
7582
-
7583
- // src/mcp-server.ts
7584
- var MAX_CONTENT_LINES2 = 30;
7585
- function truncateContent2(content) {
7586
- const lines = content.split("\n");
7587
- if (lines.length <= MAX_CONTENT_LINES2) return content;
7588
- return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
7589
- // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
7590
- }
7591
9287
  function formatIndexStats(stats, verbose = false) {
9288
+ if (stats.resetCorruptedIndex) {
9289
+ return stats.warning ?? "Detected a corrupted local index and reset it during indexing. Run index_codebase again to rebuild search data.";
9290
+ }
7592
9291
  const lines = [];
9292
+ if (stats.failedChunks > 0) {
9293
+ lines.push(`INDEXING WARNING: ${stats.failedChunks} chunks failed to embed.`);
9294
+ if (stats.failedBatchesPath) {
9295
+ lines.push(`Inspect failed batches at: ${stats.failedBatchesPath}`);
9296
+ }
9297
+ lines.push("");
9298
+ }
7593
9299
  if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
7594
- lines.push(`Indexed. ${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
9300
+ lines.push(`${stats.totalFiles} files processed, ${stats.existingChunks} code chunks already up to date.`);
7595
9301
  } else if (stats.indexedChunks === 0) {
7596
- lines.push(`Indexed. ${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
9302
+ lines.push(`${stats.totalFiles} files, removed ${stats.removedChunks} stale chunks, ${stats.existingChunks} chunks remain.`);
7597
9303
  } else {
7598
- let main2 = `Indexed. ${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
9304
+ let main2 = `${stats.totalFiles} files processed, ${stats.indexedChunks} new chunks embedded.`;
7599
9305
  if (stats.existingChunks > 0) {
7600
9306
  main2 += ` ${stats.existingChunks} unchanged chunks skipped.`;
7601
9307
  }
@@ -7634,21 +9340,103 @@ function formatIndexStats(stats, verbose = false) {
7634
9340
  }
7635
9341
  function formatStatus(status) {
7636
9342
  if (!status.indexed) {
9343
+ if (status.warning) {
9344
+ return status.warning;
9345
+ }
9346
+ if (status.failedBatchesCount > 0) {
9347
+ const lines2 = [
9348
+ "Codebase is not indexed. The last indexing run left failed embedding batches.",
9349
+ "Fix the provider/model configuration, then rerun index_codebase normally to retry the saved failed batches. Use force=true only for a full rebuild or compatibility reset."
9350
+ ];
9351
+ if (status.failedBatchesPath) {
9352
+ lines2.push(`Failed batches: ${status.failedBatchesPath}`);
9353
+ }
9354
+ return lines2.join("\n");
9355
+ }
7637
9356
  return "Codebase is not indexed. Run index_codebase to create an index.";
7638
9357
  }
7639
9358
  const lines = [
7640
- `Index status:`,
7641
- ` Indexed chunks: ${status.vectorCount.toLocaleString()}`,
7642
- ` Provider: ${status.provider}`,
7643
- ` Model: ${status.model}`,
7644
- ` Location: ${status.indexPath}`
9359
+ `Indexed chunks: ${status.vectorCount.toLocaleString()}`,
9360
+ `Provider: ${status.provider}`,
9361
+ `Model: ${status.model}`,
9362
+ `Location: ${status.indexPath}`
7645
9363
  ];
7646
9364
  if (status.currentBranch !== "default") {
7647
- lines.push(` Current branch: ${status.currentBranch}`);
7648
- lines.push(` Base branch: ${status.baseBranch}`);
9365
+ lines.push(`Current branch: ${status.currentBranch}`);
9366
+ lines.push(`Base branch: ${status.baseBranch}`);
9367
+ }
9368
+ if (status.failedBatchesCount > 0) {
9369
+ lines.push("");
9370
+ lines.push(`INDEXING WARNING: ${status.failedBatchesCount} failed embedding batch${status.failedBatchesCount === 1 ? " remains" : "es remain"}.`);
9371
+ if (status.failedBatchesPath) {
9372
+ lines.push(`Failed batches: ${status.failedBatchesPath}`);
9373
+ }
9374
+ }
9375
+ if (status.compatibility && !status.compatibility.compatible) {
9376
+ lines.push("");
9377
+ lines.push(`COMPATIBILITY WARNING: ${status.compatibility.reason}`);
9378
+ if (status.compatibility.storedMetadata) {
9379
+ const stored = status.compatibility.storedMetadata;
9380
+ lines.push(`Index was built with: ${stored.embeddingProvider}/${stored.embeddingModel} (${stored.embeddingDimensions}D)`);
9381
+ lines.push(`Current config: ${status.provider}/${status.model}`);
9382
+ }
9383
+ } else if (!status.compatibility) {
9384
+ lines.push(`Compatibility: No compatibility information found. Maybe the index is not initialized yet, try running index_codebase.`);
9385
+ } else {
9386
+ lines.push(`Compatibility: Index is compatible with the current provider and model.`);
9387
+ }
9388
+ return lines.join("\n");
9389
+ }
9390
+ function formatHealthCheck(result) {
9391
+ if (result.resetCorruptedIndex) {
9392
+ return result.warning ?? "Detected a corrupted local index and reset it. Run index_codebase to rebuild search data.";
9393
+ }
9394
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
9395
+ return "Index is healthy. No stale entries found.";
9396
+ }
9397
+ const lines = [];
9398
+ if (result.removed > 0) {
9399
+ lines.push(`Removed stale entries: ${result.removed}`);
9400
+ }
9401
+ if (result.gcOrphanEmbeddings > 0) {
9402
+ lines.push(`Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
9403
+ }
9404
+ if (result.gcOrphanChunks > 0) {
9405
+ lines.push(`Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
9406
+ }
9407
+ if (result.gcOrphanSymbols > 0) {
9408
+ lines.push(`Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
9409
+ }
9410
+ if (result.gcOrphanCallEdges > 0) {
9411
+ lines.push(`Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
9412
+ }
9413
+ if (result.filePaths.length > 0) {
9414
+ lines.push(`Cleaned paths: ${result.filePaths.join(", ")}`);
7649
9415
  }
7650
9416
  return lines.join("\n");
7651
9417
  }
9418
+ function formatDefinitionLookup(results, query) {
9419
+ if (results.length === 0) {
9420
+ return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
9421
+ }
9422
+ const formatted = results.map((r, idx) => {
9423
+ const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
9424
+ return `${header} (score: ${r.score.toFixed(2)})
9425
+ \`\`\`
9426
+ ${truncateContent(r.content)}
9427
+ \`\`\``;
9428
+ });
9429
+ return formatted.join("\n\n");
9430
+ }
9431
+
9432
+ // src/mcp-server.ts
9433
+ var MAX_CONTENT_LINES2 = 30;
9434
+ function truncateContent2(content) {
9435
+ const lines = content.split("\n");
9436
+ if (lines.length <= MAX_CONTENT_LINES2) return content;
9437
+ return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
9438
+ // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
9439
+ }
7652
9440
  var CHUNK_TYPE_ENUM = [
7653
9441
  "function",
7654
9442
  "class",
@@ -7667,8 +9455,25 @@ function createMcpServer(projectRoot, config) {
7667
9455
  name: "opencode-codebase-index",
7668
9456
  version: "0.5.1"
7669
9457
  });
7670
- const indexer = new Indexer(projectRoot, config);
9458
+ const runtimeConfig = config;
9459
+ let indexer = new Indexer(projectRoot, runtimeConfig);
7671
9460
  let initialized = false;
9461
+ function refreshIndexerFromConfig() {
9462
+ indexer = new Indexer(projectRoot, runtimeConfig);
9463
+ initialized = false;
9464
+ }
9465
+ function shouldForceLocalizeProjectIndex() {
9466
+ if (runtimeConfig.scope !== "project") {
9467
+ return false;
9468
+ }
9469
+ const localIndexPath = path11.join(projectRoot, ".opencode", "index");
9470
+ const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
9471
+ if (!mainRepoRoot) {
9472
+ return false;
9473
+ }
9474
+ const inheritedIndexPath = path11.join(mainRepoRoot, ".opencode", "index");
9475
+ return !(0, import_fs14.existsSync)(localIndexPath) && (0, import_fs14.existsSync)(inheritedIndexPath);
9476
+ }
7672
9477
  async function ensureInitialized() {
7673
9478
  if (!initialized) {
7674
9479
  await indexer.initialize();
@@ -7751,13 +9556,22 @@ Use Read tool to examine specific files.` }] };
7751
9556
  verbose: import_zod.z.boolean().optional().default(false).describe("Show detailed info about skipped files and parsing failures")
7752
9557
  },
7753
9558
  async (args) => {
7754
- await ensureInitialized();
7755
9559
  if (args.estimateOnly) {
9560
+ await ensureInitialized();
7756
9561
  const estimate = await indexer.estimateCost();
7757
9562
  return { content: [{ type: "text", text: formatCostEstimate(estimate) }] };
7758
9563
  }
7759
9564
  if (args.force) {
9565
+ if (shouldForceLocalizeProjectIndex()) {
9566
+ materializeLocalProjectConfig(projectRoot, loadProjectConfigLayer(projectRoot));
9567
+ refreshIndexerFromConfig();
9568
+ }
9569
+ await ensureInitialized();
7760
9570
  await indexer.clearIndex();
9571
+ refreshIndexerFromConfig();
9572
+ await ensureInitialized();
9573
+ } else {
9574
+ await ensureInitialized();
7761
9575
  }
7762
9576
  const stats = await indexer.index();
7763
9577
  return { content: [{ type: "text", text: formatIndexStats(stats, args.verbose ?? false) }] };
@@ -7780,29 +9594,7 @@ Use Read tool to examine specific files.` }] };
7780
9594
  async () => {
7781
9595
  await ensureInitialized();
7782
9596
  const result = await indexer.healthCheck();
7783
- if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
7784
- return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] };
7785
- }
7786
- const lines = [`Health check complete:`];
7787
- if (result.removed > 0) {
7788
- lines.push(` Removed stale entries: ${result.removed}`);
7789
- }
7790
- if (result.gcOrphanEmbeddings > 0) {
7791
- lines.push(` Garbage collected orphan embeddings: ${result.gcOrphanEmbeddings}`);
7792
- }
7793
- if (result.gcOrphanChunks > 0) {
7794
- lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
7795
- }
7796
- if (result.gcOrphanSymbols > 0) {
7797
- lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
7798
- }
7799
- if (result.gcOrphanCallEdges > 0) {
7800
- lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
7801
- }
7802
- if (result.filePaths.length > 0) {
7803
- lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
7804
- }
7805
- return { content: [{ type: "text", text: lines.join("\n") }] };
9597
+ return { content: [{ type: "text", text: formatHealthCheck(result) }] };
7806
9598
  }
7807
9599
  );
7808
9600
  server.tool(
@@ -8031,115 +9823,15 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
8031
9823
  return server;
8032
9824
  }
8033
9825
 
8034
- // src/config/merger.ts
8035
- var import_fs10 = require("fs");
8036
- var path9 = __toESM(require("path"), 1);
8037
- var os4 = __toESM(require("os"), 1);
8038
- function loadJsonFile(filePath) {
8039
- try {
8040
- if ((0, import_fs10.existsSync)(filePath)) {
8041
- const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
8042
- return JSON.parse(content);
8043
- }
8044
- } catch {
8045
- }
8046
- return null;
8047
- }
8048
- function loadMergedConfig(projectRoot) {
8049
- const globalConfigPath = path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8050
- const globalConfig = loadJsonFile(globalConfigPath);
8051
- const projectConfigPath = path9.join(projectRoot, ".opencode", "codebase-index.json");
8052
- const projectConfig = loadJsonFile(projectConfigPath);
8053
- if (!globalConfig && !projectConfig) {
8054
- return {};
8055
- }
8056
- if (!projectConfig && globalConfig) {
8057
- return globalConfig;
8058
- }
8059
- if (!globalConfig && projectConfig) {
8060
- return projectConfig;
8061
- }
8062
- const merged = { ...globalConfig };
8063
- if (projectConfig && "embeddingProvider" in projectConfig) {
8064
- merged.embeddingProvider = projectConfig.embeddingProvider;
8065
- } else if (globalConfig && globalConfig.embeddingProvider) {
8066
- merged.embeddingProvider = globalConfig.embeddingProvider;
8067
- }
8068
- if (projectConfig && "customProvider" in projectConfig) {
8069
- merged.customProvider = projectConfig.customProvider;
8070
- } else if (globalConfig && globalConfig.customProvider) {
8071
- merged.customProvider = globalConfig.customProvider;
8072
- }
8073
- if (projectConfig && "embeddingModel" in projectConfig) {
8074
- merged.embeddingModel = projectConfig.embeddingModel;
8075
- } else if (globalConfig && globalConfig.embeddingModel) {
8076
- merged.embeddingModel = globalConfig.embeddingModel;
8077
- }
8078
- if (projectConfig && "reranker" in projectConfig) {
8079
- merged.reranker = projectConfig.reranker;
8080
- } else if (globalConfig && globalConfig.reranker) {
8081
- merged.reranker = globalConfig.reranker;
8082
- }
8083
- if (projectConfig && "include" in projectConfig) {
8084
- merged.include = projectConfig.include;
8085
- } else if (globalConfig && globalConfig.include) {
8086
- merged.include = globalConfig.include;
8087
- }
8088
- if (projectConfig && "exclude" in projectConfig) {
8089
- merged.exclude = projectConfig.exclude;
8090
- } else if (globalConfig && globalConfig.exclude) {
8091
- merged.exclude = globalConfig.exclude;
8092
- }
8093
- if (projectConfig && "indexing" in projectConfig) {
8094
- merged.indexing = projectConfig.indexing;
8095
- } else if (globalConfig && globalConfig.indexing) {
8096
- merged.indexing = globalConfig.indexing;
8097
- }
8098
- if (projectConfig && "search" in projectConfig) {
8099
- merged.search = projectConfig.search;
8100
- } else if (globalConfig && globalConfig.search) {
8101
- merged.search = globalConfig.search;
8102
- }
8103
- if (projectConfig && "debug" in projectConfig) {
8104
- merged.debug = projectConfig.debug;
8105
- } else if (globalConfig && globalConfig.debug) {
8106
- merged.debug = globalConfig.debug;
8107
- }
8108
- if (projectConfig && "scope" in projectConfig) {
8109
- merged.scope = projectConfig.scope;
8110
- } else if (globalConfig && "scope" in globalConfig) {
8111
- merged.scope = globalConfig.scope;
8112
- }
8113
- if (projectConfig) {
8114
- for (const key of Object.keys(projectConfig)) {
8115
- if (key === "embeddingProvider" || key === "customProvider" || key === "embeddingModel" || key === "reranker" || key === "include" || key === "exclude" || key === "indexing" || key === "search" || key === "debug" || key === "scope" || key === "knowledgeBases" || key === "additionalInclude") {
8116
- continue;
8117
- }
8118
- merged[key] = projectConfig[key];
8119
- }
8120
- }
8121
- const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
8122
- const projectKbs = projectConfig && Array.isArray(projectConfig.knowledgeBases) ? projectConfig.knowledgeBases : [];
8123
- const allKbs = [...globalKbs, ...projectKbs];
8124
- const uniqueKbs = [...new Set(allKbs.map((p) => String(p).trim()))];
8125
- merged.knowledgeBases = uniqueKbs;
8126
- const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
8127
- const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
8128
- const allAdditional = [...globalAdditional, ...projectAdditional];
8129
- const uniqueAdditional = [...new Set(allAdditional.map((p) => String(p).trim()))];
8130
- merged.additionalInclude = uniqueAdditional;
8131
- return merged;
8132
- }
8133
-
8134
9826
  // src/cli.ts
8135
9827
  function parseArgs(argv) {
8136
9828
  let project = process.cwd();
8137
9829
  let config;
8138
9830
  for (let i = 2; i < argv.length; i++) {
8139
9831
  if (argv[i] === "--project" && argv[i + 1]) {
8140
- project = path10.resolve(argv[++i]);
9832
+ project = path12.resolve(argv[++i]);
8141
9833
  } else if (argv[i] === "--config" && argv[i + 1]) {
8142
- config = path10.resolve(argv[++i]);
9834
+ config = path12.resolve(argv[++i]);
8143
9835
  }
8144
9836
  }
8145
9837
  return { project, config };