opencode-codebase-index 0.21.0 → 0.22.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.
@@ -333,7 +333,7 @@ var require_ignore = __commonJS({
333
333
  // path matching.
334
334
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
335
335
  // @returns {TestResult} true if a file is ignored
336
- test(path20, checkUnignored, mode) {
336
+ test(path21, checkUnignored, mode) {
337
337
  let ignored = false;
338
338
  let unignored = false;
339
339
  let matchedRule;
@@ -342,7 +342,7 @@ var require_ignore = __commonJS({
342
342
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
343
343
  return;
344
344
  }
345
- const matched = rule[mode].test(path20);
345
+ const matched = rule[mode].test(path21);
346
346
  if (!matched) {
347
347
  return;
348
348
  }
@@ -363,17 +363,17 @@ var require_ignore = __commonJS({
363
363
  var throwError = (message, Ctor) => {
364
364
  throw new Ctor(message);
365
365
  };
366
- var checkPath = (path20, originalPath, doThrow) => {
367
- if (!isString(path20)) {
366
+ var checkPath = (path21, originalPath, doThrow) => {
367
+ if (!isString(path21)) {
368
368
  return doThrow(
369
369
  `path must be a string, but got \`${originalPath}\``,
370
370
  TypeError
371
371
  );
372
372
  }
373
- if (!path20) {
373
+ if (!path21) {
374
374
  return doThrow(`path must not be empty`, TypeError);
375
375
  }
376
- if (checkPath.isNotRelative(path20)) {
376
+ if (checkPath.isNotRelative(path21)) {
377
377
  const r = "`path.relative()`d";
378
378
  return doThrow(
379
379
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -382,7 +382,7 @@ var require_ignore = __commonJS({
382
382
  }
383
383
  return true;
384
384
  };
385
- var isNotRelative = (path20) => REGEX_TEST_INVALID_PATH.test(path20);
385
+ var isNotRelative = (path21) => REGEX_TEST_INVALID_PATH.test(path21);
386
386
  checkPath.isNotRelative = isNotRelative;
387
387
  checkPath.convert = (p) => p;
388
388
  var Ignore2 = class {
@@ -412,19 +412,19 @@ var require_ignore = __commonJS({
412
412
  }
413
413
  // @returns {TestResult}
414
414
  _test(originalPath, cache, checkUnignored, slices) {
415
- const path20 = originalPath && checkPath.convert(originalPath);
415
+ const path21 = originalPath && checkPath.convert(originalPath);
416
416
  checkPath(
417
- path20,
417
+ path21,
418
418
  originalPath,
419
419
  this._strictPathCheck ? throwError : RETURN_FALSE
420
420
  );
421
- return this._t(path20, cache, checkUnignored, slices);
421
+ return this._t(path21, cache, checkUnignored, slices);
422
422
  }
423
- checkIgnore(path20) {
424
- if (!REGEX_TEST_TRAILING_SLASH.test(path20)) {
425
- return this.test(path20);
423
+ checkIgnore(path21) {
424
+ if (!REGEX_TEST_TRAILING_SLASH.test(path21)) {
425
+ return this.test(path21);
426
426
  }
427
- const slices = path20.split(SLASH).filter(Boolean);
427
+ const slices = path21.split(SLASH).filter(Boolean);
428
428
  slices.pop();
429
429
  if (slices.length) {
430
430
  const parent = this._t(
@@ -437,18 +437,18 @@ var require_ignore = __commonJS({
437
437
  return parent;
438
438
  }
439
439
  }
440
- return this._rules.test(path20, false, MODE_CHECK_IGNORE);
440
+ return this._rules.test(path21, false, MODE_CHECK_IGNORE);
441
441
  }
442
- _t(path20, cache, checkUnignored, slices) {
443
- if (path20 in cache) {
444
- return cache[path20];
442
+ _t(path21, cache, checkUnignored, slices) {
443
+ if (path21 in cache) {
444
+ return cache[path21];
445
445
  }
446
446
  if (!slices) {
447
- slices = path20.split(SLASH).filter(Boolean);
447
+ slices = path21.split(SLASH).filter(Boolean);
448
448
  }
449
449
  slices.pop();
450
450
  if (!slices.length) {
451
- return cache[path20] = this._rules.test(path20, checkUnignored, MODE_IGNORE);
451
+ return cache[path21] = this._rules.test(path21, checkUnignored, MODE_IGNORE);
452
452
  }
453
453
  const parent = this._t(
454
454
  slices.join(SLASH) + SLASH,
@@ -456,29 +456,29 @@ var require_ignore = __commonJS({
456
456
  checkUnignored,
457
457
  slices
458
458
  );
459
- return cache[path20] = parent.ignored ? parent : this._rules.test(path20, checkUnignored, MODE_IGNORE);
459
+ return cache[path21] = parent.ignored ? parent : this._rules.test(path21, checkUnignored, MODE_IGNORE);
460
460
  }
461
- ignores(path20) {
462
- return this._test(path20, this._ignoreCache, false).ignored;
461
+ ignores(path21) {
462
+ return this._test(path21, this._ignoreCache, false).ignored;
463
463
  }
464
464
  createFilter() {
465
- return (path20) => !this.ignores(path20);
465
+ return (path21) => !this.ignores(path21);
466
466
  }
467
467
  filter(paths) {
468
468
  return makeArray(paths).filter(this.createFilter());
469
469
  }
470
470
  // @returns {TestResult}
471
- test(path20) {
472
- return this._test(path20, this._testCache, true);
471
+ test(path21) {
472
+ return this._test(path21, this._testCache, true);
473
473
  }
474
474
  };
475
475
  var factory = (options) => new Ignore2(options);
476
- var isPathValid = (path20) => checkPath(path20 && checkPath.convert(path20), path20, RETURN_FALSE);
476
+ var isPathValid = (path21) => checkPath(path21 && checkPath.convert(path21), path21, RETURN_FALSE);
477
477
  var setupWindows = () => {
478
478
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
479
479
  checkPath.convert = makePosix;
480
480
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
481
- checkPath.isNotRelative = (path20) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path20) || isNotRelative(path20);
481
+ checkPath.isNotRelative = (path21) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path21) || isNotRelative(path21);
482
482
  };
483
483
  if (
484
484
  // Detect `process` so that it can run in browsers.
@@ -816,7 +816,8 @@ function getDefaultSearchConfig() {
816
816
  contextLines: 0,
817
817
  routingHints: true,
818
818
  routingGraphHandoffHints: false,
819
- routingHintRole: "system"
819
+ routingHintRole: "system",
820
+ communityBoost: 0
820
821
  };
821
822
  }
822
823
  function getDefaultRerankerBaseUrl(provider) {
@@ -949,7 +950,8 @@ function parseConfig(raw) {
949
950
  contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines,
950
951
  routingHints: typeof rawSearch.routingHints === "boolean" ? rawSearch.routingHints : defaultSearch.routingHints,
951
952
  routingGraphHandoffHints: typeof rawSearch.routingGraphHandoffHints === "boolean" ? rawSearch.routingGraphHandoffHints : defaultSearch.routingGraphHandoffHints,
952
- routingHintRole: rawSearch.routingHintRole === "developer" || rawSearch.routingHintRole === "system" ? rawSearch.routingHintRole : defaultSearch.routingHintRole
953
+ routingHintRole: rawSearch.routingHintRole === "developer" || rawSearch.routingHintRole === "system" ? rawSearch.routingHintRole : defaultSearch.routingHintRole,
954
+ communityBoost: typeof rawSearch.communityBoost === "number" && Number.isFinite(rawSearch.communityBoost) ? Math.min(1, Math.max(0, rawSearch.communityBoost)) : defaultSearch.communityBoost
953
955
  };
954
956
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
955
957
  const debug = {
@@ -1449,9 +1451,122 @@ function formatPrImpact(result) {
1449
1451
  return lines.join("\n");
1450
1452
  }
1451
1453
 
1454
+ // src/tools/format-communities.ts
1455
+ function compareText(left, right) {
1456
+ return left < right ? -1 : left > right ? 1 : 0;
1457
+ }
1458
+ function buildCodeCommunitiesResult(communities, centrality, couplings = [], options = {}) {
1459
+ const minSize = options.minSize ?? 1;
1460
+ const limit = options.limit ?? 20;
1461
+ const hubThreshold = options.hubThreshold ?? 5;
1462
+ const minCoupling = options.minCoupling ?? 1;
1463
+ const couplingLimit = options.couplingLimit ?? 20;
1464
+ const communityMap = /* @__PURE__ */ new Map();
1465
+ for (const c of communities) {
1466
+ let entry = communityMap.get(c.communityId);
1467
+ if (!entry) {
1468
+ entry = { label: c.communityLabel, members: [] };
1469
+ communityMap.set(c.communityId, entry);
1470
+ }
1471
+ entry.members.push(c);
1472
+ }
1473
+ const sortedCommunities = Array.from(communityMap.entries()).map(([id, entry]) => ({
1474
+ id,
1475
+ label: entry.label,
1476
+ symbolCount: entry.members.length,
1477
+ members: entry.members.map((m) => ({
1478
+ symbolId: m.symbolId,
1479
+ symbolName: m.symbolName,
1480
+ filePath: m.filePath
1481
+ })).sort((a, b) => compareText(a.symbolName, b.symbolName) || compareText(a.symbolId, b.symbolId))
1482
+ })).filter((c) => c.symbolCount >= minSize).sort((a, b) => b.symbolCount - a.symbolCount || compareText(a.label, b.label) || a.id - b.id).slice(0, limit);
1483
+ const communityBySymbol = new Map(communities.map((community) => [community.symbolId, community]));
1484
+ const communityLabelById = new Map(communities.map((community) => [community.communityId, community.communityLabel]));
1485
+ const hubNodes = centrality.map((c) => ({
1486
+ symbolId: c.symbolId,
1487
+ symbolName: c.symbolName,
1488
+ filePath: c.filePath,
1489
+ callerCount: c.callerCount,
1490
+ calleeCount: c.calleeCount,
1491
+ totalConnections: c.totalConnections,
1492
+ crossCommunityConnections: communityBySymbol.get(c.symbolId)?.crossCommunityConnections ?? 0
1493
+ })).filter((h) => h.crossCommunityConnections >= hubThreshold).sort(
1494
+ (a, b) => b.crossCommunityConnections - a.crossCommunityConnections || b.totalConnections - a.totalConnections || compareText(a.symbolId, b.symbolId)
1495
+ ).slice(0, limit);
1496
+ const canonicalCoupling = (value) => Math.trunc(value);
1497
+ const couplingItems = couplings.map((entry) => {
1498
+ const relationships = entry.relationships ?? entry.representativeRelationships ?? [];
1499
+ const normalizedRelationships = relationships.map((relationship) => ({
1500
+ fromSymbolId: relationship.fromSymbolId,
1501
+ fromSymbolName: relationship.fromSymbolName,
1502
+ fromFilePath: relationship.fromFilePath,
1503
+ toSymbolId: relationship.toSymbolId,
1504
+ toSymbolName: relationship.toSymbolName,
1505
+ toFilePath: relationship.toFilePath
1506
+ })).sort(
1507
+ (left, right) => compareText(left.fromSymbolName, right.fromSymbolName) || compareText(left.fromSymbolId, right.fromSymbolId) || compareText(left.toSymbolName, right.toSymbolName) || compareText(left.toSymbolId, right.toSymbolId) || compareText(left.fromFilePath, right.fromFilePath) || compareText(left.toFilePath, right.toFilePath)
1508
+ ).slice(0, 5);
1509
+ const communityA = canonicalCoupling(Math.min(entry.communityA, entry.communityB));
1510
+ const communityB = canonicalCoupling(Math.max(entry.communityA, entry.communityB));
1511
+ return {
1512
+ communityA,
1513
+ communityB,
1514
+ communityAName: communityLabelById.get(communityA) ?? `Community ${communityA}`,
1515
+ communityBName: communityLabelById.get(communityB) ?? `Community ${communityB}`,
1516
+ distinctConnections: canonicalCoupling(entry.count),
1517
+ representativeRelationships: normalizedRelationships
1518
+ };
1519
+ }).filter((entry) => entry.distinctConnections >= minCoupling).sort(
1520
+ (left, right) => right.distinctConnections - left.distinctConnections || compareText(left.communityAName, right.communityAName) || compareText(left.communityBName, right.communityBName) || compareText(left.communityAName + left.communityBName, right.communityAName + right.communityBName) || left.communityA - right.communityA || left.communityB - right.communityB
1521
+ ).slice(0, Math.max(1, Math.floor(couplingLimit)));
1522
+ return {
1523
+ communities: sortedCommunities,
1524
+ hubNodes,
1525
+ totalSymbols: communities.length,
1526
+ totalCommunities: communityMap.size,
1527
+ couplings: couplingItems
1528
+ };
1529
+ }
1530
+ function formatCodeCommunities(result) {
1531
+ const lines = [];
1532
+ lines.push(`\u2192 Communities: ${result.totalCommunities} (${result.communities.length} shown, ${result.totalSymbols} symbols total)`);
1533
+ for (const community of result.communities) {
1534
+ lines.push(` Community ${community.id} (${community.label}): ${community.symbolCount} symbols`);
1535
+ const shownMembers = community.members.slice(0, 8);
1536
+ for (const m of shownMembers) {
1537
+ lines.push(` - ${m.symbolName} (${m.filePath})`);
1538
+ }
1539
+ if (community.members.length > 8) {
1540
+ lines.push(` ... and ${community.members.length - 8} more`);
1541
+ }
1542
+ }
1543
+ if (result.hubNodes.length > 0) {
1544
+ lines.push(`\u2192 Hub nodes (${result.hubNodes.length} shown, cross-community connections):`);
1545
+ for (const hub of result.hubNodes) {
1546
+ lines.push(
1547
+ ` - ${hub.symbolName} (${hub.crossCommunityConnections} cross-community, ${hub.callerCount} callers, ${hub.calleeCount} callees) at ${hub.filePath}`
1548
+ );
1549
+ }
1550
+ } else {
1551
+ lines.push("\u2192 Hub nodes: none with significant cross-community connections");
1552
+ }
1553
+ if (result.couplings.length > 0) {
1554
+ lines.push(`\u2192 Community couplings: ${result.couplings.length} shown`);
1555
+ for (const coupling of result.couplings) {
1556
+ lines.push(` - ${coupling.communityAName} \u2194 ${coupling.communityBName}: ${coupling.distinctConnections} distinct connections`);
1557
+ for (const relationship of coupling.representativeRelationships) {
1558
+ lines.push(` - ${relationship.fromSymbolName} (${relationship.fromFilePath}) -> ${relationship.toSymbolName} (${relationship.toFilePath})`);
1559
+ }
1560
+ }
1561
+ } else {
1562
+ lines.push("\u2192 Community couplings: none above minCoupling threshold");
1563
+ }
1564
+ return lines.join("\n");
1565
+ }
1566
+
1452
1567
  // src/tools/operations.ts
1453
1568
  var import_fs13 = require("fs");
1454
- var path19 = __toESM(require("path"), 1);
1569
+ var path20 = __toESM(require("path"), 1);
1455
1570
 
1456
1571
  // src/config/paths.ts
1457
1572
  var import_fs4 = require("fs");
@@ -1937,6 +2052,15 @@ function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot3) {
1937
2052
  );
1938
2053
  }
1939
2054
 
2055
+ // src/tools/contracts.ts
2056
+ var CODE_COMMUNITIES_MIN_SIZE = 1;
2057
+ var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
2058
+ var CODE_COMMUNITIES_MAX_LIMIT = 100;
2059
+ var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
2060
+ var CODE_COMMUNITIES_MIN_COUPLING = 1;
2061
+ var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
2062
+ var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
2063
+
1940
2064
  // src/tools/context-pack.ts
1941
2065
  var import_tiktoken = require("tiktoken");
1942
2066
 
@@ -2580,8 +2704,8 @@ function formatExactSearchHandoff(results) {
2580
2704
  }
2581
2705
  function formatContextEvidence(result, index) {
2582
2706
  const symbol = result.name ? ` ${JSON.stringify(compactEvidenceValue(result.name, 80))}` : "";
2583
- const path20 = compactEvidenceValue(result.filePath, 120);
2584
- return `[${index}] ${result.chunkType}${symbol} in ${path20}:${result.startLine}-${result.endLine} (score ${result.score.toFixed(2)})`;
2707
+ const path21 = compactEvidenceValue(result.filePath, 120);
2708
+ return `[${index}] ${result.chunkType}${symbol} in ${path21}:${result.startLine}-${result.endLine} (score ${result.score.toFixed(2)})`;
2585
2709
  }
2586
2710
  function formatContextPack(heading, selected, candidateCount, duplicateCount, limitOmittedCount, budgetOmittedCount, includeExactSearchHandoff) {
2587
2711
  const lines = selected.map((result, index) => formatContextEvidence(result, index + 1));
@@ -4755,7 +4879,7 @@ function saveConfig(projectRoot3, config, host) {
4755
4879
 
4756
4880
  // src/indexer/index.ts
4757
4881
  var import_fs12 = require("fs");
4758
- var path18 = __toESM(require("path"), 1);
4882
+ var path19 = __toESM(require("path"), 1);
4759
4883
  var import_perf_hooks = require("perf_hooks");
4760
4884
  var import_child_process4 = require("child_process");
4761
4885
  var import_util4 = require("util");
@@ -7023,6 +7147,9 @@ function createMockNativeBinding() {
7023
7147
  detectCommunities() {
7024
7148
  throw error;
7025
7149
  }
7150
+ detectCommunityCouplings() {
7151
+ throw error;
7152
+ }
7026
7153
  computeCentrality() {
7027
7154
  throw error;
7028
7155
  }
@@ -7260,6 +7387,18 @@ var Database = class _Database {
7260
7387
  }
7261
7388
  this.closed = true;
7262
7389
  }
7390
+ beginWriteTransaction() {
7391
+ this.throwIfClosed();
7392
+ this.inner.beginWriteTransaction();
7393
+ }
7394
+ commitWriteTransaction() {
7395
+ this.throwIfClosed();
7396
+ this.inner.commitWriteTransaction();
7397
+ }
7398
+ rollbackWriteTransaction() {
7399
+ this.throwIfClosed();
7400
+ this.inner.rollbackWriteTransaction();
7401
+ }
7263
7402
  embeddingExists(contentHash) {
7264
7403
  this.throwIfClosed();
7265
7404
  return this.inner.embeddingExists(contentHash);
@@ -7519,6 +7658,13 @@ var Database = class _Database {
7519
7658
  this.throwIfClosed();
7520
7659
  return this.inner.computeCentrality(branch);
7521
7660
  }
7661
+ detectCommunityCouplings(branch) {
7662
+ this.throwIfClosed();
7663
+ return this.inner.detectCommunityCouplings(branch).map((entry) => ({
7664
+ ...entry,
7665
+ relationships: entry.representativeRelationships ?? []
7666
+ }));
7667
+ }
7522
7668
  };
7523
7669
 
7524
7670
  // src/git/branch-materialization.ts
@@ -8288,6 +8434,21 @@ async function getChunkGitBlame(projectRoot3, filePath, startLine, endLine) {
8288
8434
  }
8289
8435
 
8290
8436
  // src/indexer/search-ranking.ts
8437
+ function applyCommunityBoost(candidates, sameCommunityCandidateIds, boost) {
8438
+ if (boost <= 0 || sameCommunityCandidateIds.size === 0 || candidates.length <= 1) {
8439
+ return candidates;
8440
+ }
8441
+ const result = candidates.map((candidate) => sameCommunityCandidateIds.has(candidate.id) ? { ...candidate, score: candidate.score * (1 + boost) } : candidate);
8442
+ for (let index = 1; index < result.length; index += 1) {
8443
+ const candidate = result[index];
8444
+ const previous = result[index - 1];
8445
+ if (candidate && previous && sameCommunityCandidateIds.has(candidate.id) && !sameCommunityCandidateIds.has(previous.id) && candidate.score > previous.score) {
8446
+ result[index - 1] = candidate;
8447
+ result[index] = previous;
8448
+ }
8449
+ }
8450
+ return result;
8451
+ }
8291
8452
  var RANK_HYBRID_CACHE_LIMIT = 256;
8292
8453
  var rankHybridResultsCache = /* @__PURE__ */ new WeakMap();
8293
8454
  function classifyQueryIntentRaw(query) {
@@ -8466,6 +8627,117 @@ function rankSemanticOnlyResults(query, semanticResults, options) {
8466
8627
  });
8467
8628
  }
8468
8629
 
8630
+ // src/tools/symbol-inference.ts
8631
+ var IDENTIFIER_RE = /[A-Za-z_$][A-Za-z0-9_$]*/g;
8632
+ var QUOTED_BACKTICK_RE = /`([^`]+)`/g;
8633
+ var QUOTED_SINGLE_RE = /'([^'\\]+)'/g;
8634
+ var QUOTED_DOUBLE_RE = /"([^"]+)"/g;
8635
+ var SYMBOL_LIKE_RE = /^(?:[A-Za-z_$][A-Za-z0-9_$]*)$/;
8636
+ var CAMEL_CASE_RE = /^[a-z_][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*$/;
8637
+ var PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9_$]*$/;
8638
+ var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*_[a-z0-9_]+$/;
8639
+ var DEFINITION_INTENT_RE = /\b(where|defined|definition|define|declaration|symbol|function|method|class|interface|type)\b/i;
8640
+ var STOP_WORDS = /* @__PURE__ */ new Set([
8641
+ "a",
8642
+ "an",
8643
+ "and",
8644
+ "are",
8645
+ "at",
8646
+ "for",
8647
+ "find",
8648
+ "how",
8649
+ "i",
8650
+ "in",
8651
+ "is",
8652
+ "it",
8653
+ "of",
8654
+ "on",
8655
+ "that",
8656
+ "the",
8657
+ "definition",
8658
+ "show",
8659
+ "to",
8660
+ "where",
8661
+ "which",
8662
+ "what",
8663
+ "you",
8664
+ "your",
8665
+ "with"
8666
+ ]);
8667
+ function stripCallSuffix(token) {
8668
+ return token.replace(/\(\s*\)$/, "");
8669
+ }
8670
+ function isLikelySymbolName(token) {
8671
+ if (!SYMBOL_LIKE_RE.test(token)) {
8672
+ return false;
8673
+ }
8674
+ if (STOP_WORDS.has(token.toLowerCase())) {
8675
+ return false;
8676
+ }
8677
+ return CAMEL_CASE_RE.test(token) || PASCAL_CASE_RE.test(token) || SNAKE_CASE_RE.test(token);
8678
+ }
8679
+ function extractQuotedIdentifiers(query) {
8680
+ const identifiers = /* @__PURE__ */ new Set();
8681
+ for (const match of query.matchAll(QUOTED_BACKTICK_RE)) {
8682
+ const candidate = stripCallSuffix(match[1].trim());
8683
+ if (candidate && isLikelySymbolName(candidate)) {
8684
+ identifiers.add(candidate);
8685
+ }
8686
+ }
8687
+ for (const match of query.matchAll(QUOTED_SINGLE_RE)) {
8688
+ const candidate = stripCallSuffix(match[1].trim());
8689
+ if (candidate && isLikelySymbolName(candidate)) {
8690
+ identifiers.add(candidate);
8691
+ }
8692
+ }
8693
+ for (const match of query.matchAll(QUOTED_DOUBLE_RE)) {
8694
+ const candidate = stripCallSuffix(match[1].trim());
8695
+ if (candidate && isLikelySymbolName(candidate)) {
8696
+ identifiers.add(candidate);
8697
+ }
8698
+ }
8699
+ return [...identifiers];
8700
+ }
8701
+ function extractBareIdentifiers(query) {
8702
+ const unquoted = query.replace(QUOTED_BACKTICK_RE, " ").replace(QUOTED_SINGLE_RE, " ").replace(QUOTED_DOUBLE_RE, " ");
8703
+ const identifiers = /* @__PURE__ */ new Set();
8704
+ for (const match of unquoted.matchAll(IDENTIFIER_RE)) {
8705
+ const candidate = stripCallSuffix(match[0]);
8706
+ if (isLikelySymbolName(candidate)) {
8707
+ identifiers.add(candidate);
8708
+ }
8709
+ }
8710
+ return [...identifiers];
8711
+ }
8712
+ function isSingleMeaningfulToken(query, symbol) {
8713
+ const tokens = query.replace(/[`'"()]/g, " ").split(/[^A-Za-z0-9_$]+/).map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0).filter((token) => !STOP_WORDS.has(token));
8714
+ return tokens.length === 1 && tokens[0] === symbol.toLowerCase();
8715
+ }
8716
+ function inferExactSymbolFromQuery(query) {
8717
+ if (analyzeQueryIntent(query).explicitArtifactIntent) {
8718
+ return void 0;
8719
+ }
8720
+ const quoted = extractQuotedIdentifiers(query);
8721
+ if (quoted.length === 1) {
8722
+ return quoted[0];
8723
+ }
8724
+ if (quoted.length > 1) {
8725
+ return void 0;
8726
+ }
8727
+ const candidates = extractBareIdentifiers(query);
8728
+ if (candidates.length !== 1) {
8729
+ return void 0;
8730
+ }
8731
+ const candidate = candidates[0];
8732
+ if (DEFINITION_INTENT_RE.test(query)) {
8733
+ return candidate;
8734
+ }
8735
+ if (isSingleMeaningfulToken(query, candidate)) {
8736
+ return candidate;
8737
+ }
8738
+ return void 0;
8739
+ }
8740
+
8469
8741
  // src/indexer/call-graph-constants.ts
8470
8742
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
8471
8743
  "function_declaration",
@@ -8968,22 +9240,6 @@ function getUniquePendingChunksFromRequests(requests) {
8968
9240
  }
8969
9241
  return Array.from(uniqueChunks.values());
8970
9242
  }
8971
- function coalesceFailedBatches(batches) {
8972
- const grouped = /* @__PURE__ */ new Map();
8973
- for (const batch of batches) {
8974
- const key = `${batch.attemptCount}:${batch.lastAttempt}:${batch.error}`;
8975
- const existing = grouped.get(key);
8976
- if (!existing) {
8977
- grouped.set(key, {
8978
- ...batch,
8979
- chunks: [...batch.chunks]
8980
- });
8981
- continue;
8982
- }
8983
- existing.chunks.push(...batch.chunks);
8984
- }
8985
- return Array.from(grouped.values());
8986
- }
8987
9243
  function poolEmbeddingVectors(vectors, weights) {
8988
9244
  const firstVector = vectors[0];
8989
9245
  if (!firstVector) {
@@ -9016,9 +9272,268 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
9016
9272
  return true;
9017
9273
  }
9018
9274
 
9275
+ // src/indexer/failed-state-persistence.ts
9276
+ var fs2 = __toESM(require("fs"), 1);
9277
+ var import_node_crypto = require("crypto");
9278
+ var path18 = __toESM(require("path"), 1);
9279
+ var import_node_string_decoder = require("string_decoder");
9280
+ var CURRENT_FAILED_BATCH_VERSION = 1;
9281
+ var DEFAULT_MALFORMED_LINE_ACTION = "skip";
9282
+ function* readFailedBatchRecords(filePath, options = {}) {
9283
+ if (!fs2.existsSync(filePath)) {
9284
+ return;
9285
+ }
9286
+ const fileFormat = detectFailedBatchFileFormat(filePath);
9287
+ if (fileFormat === "legacy") {
9288
+ yield* readLegacyFailedBatchRecords(filePath, options);
9289
+ return;
9290
+ }
9291
+ yield* readJsonlFailedBatchRecords(filePath, options);
9292
+ }
9293
+ function createFailedBatchWriter(targetPath) {
9294
+ const temporaryPath = createTemporaryPath(targetPath);
9295
+ let finalized = false;
9296
+ fs2.mkdirSync(path18.dirname(targetPath), { recursive: true });
9297
+ fs2.closeSync(fs2.openSync(temporaryPath, "w"));
9298
+ const write = (record) => {
9299
+ if (finalized) {
9300
+ throw new Error("Failed batch writer has been finalized");
9301
+ }
9302
+ const lines = record.chunks.map((chunk) => {
9303
+ const lineRecord = {
9304
+ version: CURRENT_FAILED_BATCH_VERSION,
9305
+ chunks: [chunk],
9306
+ error: record.error,
9307
+ attemptCount: record.attemptCount,
9308
+ lastAttempt: record.lastAttempt
9309
+ };
9310
+ return JSON.stringify(lineRecord);
9311
+ });
9312
+ if (lines.length === 0) {
9313
+ return;
9314
+ }
9315
+ fs2.mkdirSync(path18.dirname(targetPath), { recursive: true });
9316
+ fs2.appendFileSync(temporaryPath, `${lines.join("\n")}
9317
+ `, "utf-8");
9318
+ };
9319
+ const commit = () => {
9320
+ if (finalized) {
9321
+ return;
9322
+ }
9323
+ fs2.mkdirSync(path18.dirname(targetPath), { recursive: true });
9324
+ fs2.renameSync(temporaryPath, targetPath);
9325
+ finalized = true;
9326
+ };
9327
+ const cleanup = () => {
9328
+ if (finalized) {
9329
+ return;
9330
+ }
9331
+ fs2.rmSync(temporaryPath, { force: true });
9332
+ };
9333
+ return {
9334
+ write,
9335
+ commit,
9336
+ cleanup,
9337
+ temporaryPath
9338
+ };
9339
+ }
9340
+ function* readLegacyFailedBatchRecords(filePath, options) {
9341
+ const rawData = fs2.readFileSync(filePath, "utf-8");
9342
+ const trimmed = stripLeadingBomAndWhitespace(rawData).trim();
9343
+ if (trimmed.length === 0) {
9344
+ return;
9345
+ }
9346
+ let parsed;
9347
+ try {
9348
+ parsed = JSON.parse(trimmed);
9349
+ } catch (error) {
9350
+ handleMalformedLine(filePath, 1, trimmed, error, options);
9351
+ return;
9352
+ }
9353
+ if (!Array.isArray(parsed)) {
9354
+ handleMalformedLine(filePath, 1, trimmed, new Error("Expected legacy failed-batch file to contain a JSON array"), options);
9355
+ return;
9356
+ }
9357
+ for (const entry of parsed) {
9358
+ const normalized = normalizeFailedBatchRecord(entry);
9359
+ if (normalized) {
9360
+ yield normalized;
9361
+ }
9362
+ }
9363
+ }
9364
+ function* readJsonlFailedBatchRecords(filePath, options) {
9365
+ const handle = fs2.openSync(filePath, "r");
9366
+ const decoder = new import_node_string_decoder.StringDecoder("utf8");
9367
+ const readBuffer = Buffer.allocUnsafe(64 * 1024);
9368
+ let buffer = "";
9369
+ let lineNumber = 0;
9370
+ try {
9371
+ let bytesRead = 0;
9372
+ do {
9373
+ bytesRead = fs2.readSync(handle, readBuffer, 0, readBuffer.length, null);
9374
+ buffer += decoder.write(readBuffer.subarray(0, bytesRead));
9375
+ let newlineIndex = buffer.indexOf("\n");
9376
+ while (newlineIndex >= 0) {
9377
+ const rawLine = buffer.slice(0, newlineIndex);
9378
+ buffer = buffer.slice(newlineIndex + 1);
9379
+ lineNumber += 1;
9380
+ const normalized = parseFailedBatchLine(rawLine, filePath, lineNumber, options);
9381
+ if (normalized) {
9382
+ yield normalized;
9383
+ }
9384
+ newlineIndex = buffer.indexOf("\n");
9385
+ }
9386
+ } while (bytesRead > 0);
9387
+ buffer += decoder.end();
9388
+ const finalLine = buffer.trimEnd();
9389
+ if (finalLine.length > 0) {
9390
+ lineNumber += 1;
9391
+ const normalized = parseFailedBatchLine(finalLine, filePath, lineNumber, options);
9392
+ if (normalized) {
9393
+ yield normalized;
9394
+ }
9395
+ }
9396
+ } finally {
9397
+ fs2.closeSync(handle);
9398
+ }
9399
+ }
9400
+ function parseFailedBatchLine(rawLine, filePath, lineNumber, options) {
9401
+ const line = rawLine.trimEnd();
9402
+ if (line.length === 0) {
9403
+ return null;
9404
+ }
9405
+ try {
9406
+ const parsed = JSON.parse(line);
9407
+ const normalized = normalizeFailedBatchRecord(parsed);
9408
+ if (!normalized) {
9409
+ handleMalformedLine(filePath, lineNumber, line, new Error("Malformed failed-batch record"), options);
9410
+ return null;
9411
+ }
9412
+ return normalized;
9413
+ } catch (error) {
9414
+ handleMalformedLine(filePath, lineNumber, line, error, options);
9415
+ return null;
9416
+ }
9417
+ }
9418
+ function normalizeFailedBatchRecord(rawRecord) {
9419
+ if (!rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord)) {
9420
+ return null;
9421
+ }
9422
+ const typed = rawRecord;
9423
+ const chunks = Array.isArray(typed.chunks) ? typed.chunks : null;
9424
+ if (!chunks || chunks.length === 0) {
9425
+ return null;
9426
+ }
9427
+ return {
9428
+ version: typeof typed.version === "number" && Number.isFinite(typed.version) ? typed.version : CURRENT_FAILED_BATCH_VERSION,
9429
+ chunks,
9430
+ error: typeof typed.error === "string" ? typed.error : "Unknown embedding error",
9431
+ attemptCount: typeof typed.attemptCount === "number" && Number.isFinite(typed.attemptCount) ? typed.attemptCount : 1,
9432
+ lastAttempt: typeof typed.lastAttempt === "string" ? typed.lastAttempt : (/* @__PURE__ */ new Date()).toISOString()
9433
+ };
9434
+ }
9435
+ function detectFailedBatchFileFormat(filePath) {
9436
+ const handle = fs2.openSync(filePath, "r");
9437
+ try {
9438
+ const buffer = Buffer.alloc(4096);
9439
+ const bytesRead = fs2.readSync(handle, buffer, 0, buffer.length, 0);
9440
+ if (bytesRead <= 0) {
9441
+ return "jsonl";
9442
+ }
9443
+ const prefix = stripLeadingBomAndWhitespace(buffer.subarray(0, bytesRead).toString("utf-8"));
9444
+ return prefix.startsWith("[") ? "legacy" : "jsonl";
9445
+ } finally {
9446
+ fs2.closeSync(handle);
9447
+ }
9448
+ }
9449
+ function stripLeadingBomAndWhitespace(value) {
9450
+ let result = value.trimStart();
9451
+ if (result.charCodeAt(0) === 65279) {
9452
+ result = result.slice(1);
9453
+ }
9454
+ return result;
9455
+ }
9456
+ function createTemporaryPath(targetPath) {
9457
+ const randomId = (0, import_node_crypto.createHash)("sha1").update(`${Date.now()}:${(0, import_node_crypto.randomBytes)(8).toString("hex")}`).digest("hex");
9458
+ const targetDir = path18.dirname(targetPath);
9459
+ const baseName = path18.basename(targetPath);
9460
+ return path18.join(targetDir, `.${baseName}.${randomId}.tmp`);
9461
+ }
9462
+ function handleMalformedLine(filePath, lineNumber, line, error, options) {
9463
+ const action = options.malformedLineAction ?? DEFAULT_MALFORMED_LINE_ACTION;
9464
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
9465
+ if (options.onMalformedLine) {
9466
+ options.onMalformedLine(normalizedError, line, lineNumber, filePath);
9467
+ }
9468
+ if (action === "fail") {
9469
+ throw normalizedError;
9470
+ }
9471
+ }
9472
+
9473
+ // src/indexer/file-batches.ts
9474
+ var INDEX_FILE_BATCH_LIMITS = Object.freeze({
9475
+ maxFiles: 64,
9476
+ maxBytes: 8 * 1024 * 1024
9477
+ });
9478
+ function* iterateOrderedFileBatches(items, getBytes, limits = INDEX_FILE_BATCH_LIMITS) {
9479
+ const maxFiles = Math.max(1, Math.floor(limits.maxFiles));
9480
+ const maxBytes = Math.max(1, Math.floor(limits.maxBytes));
9481
+ let batch = [];
9482
+ let batchBytes = 0;
9483
+ for (const item of items) {
9484
+ const itemBytes = Math.max(0, Math.floor(getBytes(item)));
9485
+ if (batch.length > 0 && (batch.length >= maxFiles || batchBytes + itemBytes > maxBytes)) {
9486
+ yield batch;
9487
+ batch = [];
9488
+ batchBytes = 0;
9489
+ }
9490
+ batch.push(item);
9491
+ batchBytes += itemBytes;
9492
+ }
9493
+ if (batch.length > 0) {
9494
+ yield batch;
9495
+ }
9496
+ }
9497
+
9019
9498
  // src/indexer/index.ts
9020
9499
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "swift", "php", "apex", "zig", "gdscript", "matlab", "bash", "c", "cpp", "metal"]);
9021
9500
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex", "php"]);
9501
+ function candidateOverlapsSymbol(candidate, symbol) {
9502
+ return candidate.metadata.filePath === symbol.filePath && candidate.metadata.startLine <= symbol.endLine && candidate.metadata.endLine >= symbol.startLine;
9503
+ }
9504
+ function resolveSameCommunityCandidateIds(query, candidates, database, branchCatalogKeys) {
9505
+ const anchorName = inferExactSymbolFromQuery(query);
9506
+ if (!anchorName || candidates.length === 0) {
9507
+ return /* @__PURE__ */ new Set();
9508
+ }
9509
+ const catalogs = branchCatalogKeys.map((branchKey) => ({
9510
+ branchKey,
9511
+ symbols: database.getSymbolsForBranch(branchKey)
9512
+ }));
9513
+ const exactAnchors = catalogs.flatMap(({ branchKey, symbols }) => symbols.filter((symbol) => symbol.name === anchorName).map((symbol) => ({ branchKey, symbol })));
9514
+ const anchors = exactAnchors.length > 0 ? exactAnchors : catalogs.flatMap(({ branchKey, symbols }) => symbols.filter((symbol) => symbol.name.toLowerCase() === anchorName.toLowerCase()).map((symbol) => ({ branchKey, symbol })));
9515
+ const uniqueAnchors = new Map(anchors.map((anchor2) => [anchor2.symbol.id, anchor2]));
9516
+ if (uniqueAnchors.size !== 1) {
9517
+ return /* @__PURE__ */ new Set();
9518
+ }
9519
+ const anchor = uniqueAnchors.values().next().value;
9520
+ const branchSymbols = catalogs.find((catalog) => catalog.branchKey === anchor.branchKey)?.symbols ?? [];
9521
+ const candidateSymbols = branchSymbols.filter(
9522
+ (symbol) => candidates.some((candidate) => candidateOverlapsSymbol(candidate, symbol))
9523
+ );
9524
+ const assignments = database.detectCommunities(
9525
+ anchor.branchKey,
9526
+ [anchor.symbol.id, ...candidateSymbols.map((symbol) => symbol.id)]
9527
+ );
9528
+ const anchorCommunity = assignments.find((assignment) => assignment.symbolId === anchor.symbol.id)?.communityId;
9529
+ if (anchorCommunity === void 0) {
9530
+ return /* @__PURE__ */ new Set();
9531
+ }
9532
+ const sameCommunitySymbolIds = new Set(assignments.filter((assignment) => assignment.communityId === anchorCommunity).map((assignment) => assignment.symbolId));
9533
+ return new Set(candidates.filter((candidate) => candidateSymbols.some(
9534
+ (symbol) => sameCommunitySymbolIds.has(symbol.id) && candidateOverlapsSymbol(candidate, symbol)
9535
+ )).map((candidate) => candidate.id));
9536
+ }
9022
9537
  var CALL_GRAPH_RESOLUTION_VERSION = "4";
9023
9538
  var PHP_FUNCTION_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
9024
9539
  "function_declaration",
@@ -9119,6 +9634,16 @@ function isSqliteCorruptionError(error) {
9119
9634
  }
9120
9635
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
9121
9636
  var READER_ARTIFACT_RETRY_INTERVAL_MS = 1e3;
9637
+ function getFailedBatchGroupKey(record) {
9638
+ return `${record.attemptCount}:${record.lastAttempt}:${record.error}`;
9639
+ }
9640
+ function getPendingChunkId(rawChunk) {
9641
+ if (!rawChunk || typeof rawChunk !== "object") {
9642
+ return null;
9643
+ }
9644
+ const id = rawChunk.id;
9645
+ return typeof id === "string" ? id : null;
9646
+ }
9122
9647
  function metadataFromBlame(blame) {
9123
9648
  if (!blame) {
9124
9649
  return {};
@@ -9166,9 +9691,9 @@ var SWIFT_PARSER_VERSION = "1";
9166
9691
  var METAL_PARSER_VERSION = "1";
9167
9692
  var SYMBOL_EXTRACTOR_VERSION = "1";
9168
9693
  function isPathWithinRoot2(filePath, rootPath) {
9169
- const normalizedFilePath = path18.resolve(filePath);
9170
- const normalizedRoot = path18.resolve(rootPath);
9171
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path18.sep}`);
9694
+ const normalizedFilePath = path19.resolve(filePath);
9695
+ const normalizedRoot = path19.resolve(rootPath);
9696
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path19.sep}`);
9172
9697
  }
9173
9698
  function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
9174
9699
  if (combined.length === 0) {
@@ -9499,10 +10024,10 @@ function matchesHardSearchFilters(candidate, options, projectRoot3) {
9499
10024
  }
9500
10025
  if (options?.directory) {
9501
10026
  const candidatePath = canonicalizePathForComparison(
9502
- path18.resolve(projectRoot3, candidate.metadata.filePath.replace(/\\/g, path18.sep))
10027
+ path19.resolve(projectRoot3, candidate.metadata.filePath.replace(/\\/g, path19.sep))
9503
10028
  );
9504
10029
  const directoryPath = canonicalizePathForComparison(
9505
- path18.resolve(projectRoot3, options.directory.trim().replace(/\\/g, path18.sep))
10030
+ path19.resolve(projectRoot3, options.directory.trim().replace(/\\/g, path19.sep))
9506
10031
  );
9507
10032
  if (!isPathWithinRoot2(candidatePath, directoryPath)) return false;
9508
10033
  }
@@ -9578,6 +10103,7 @@ var Indexer = class _Indexer {
9578
10103
  readerArtifactFingerprint = null;
9579
10104
  writerArtifactFingerprint = null;
9580
10105
  readerArtifactRetryAfter = /* @__PURE__ */ new Map();
10106
+ fileBatchLimits;
9581
10107
  constructor(projectRoot3, config, host, runtimeOptions = {}) {
9582
10108
  this.projectRoot = projectRoot3;
9583
10109
  this.projectIdentityHash = hashContent(this.getCanonicalPath(projectRoot3)).slice(0, 16);
@@ -9589,6 +10115,7 @@ var Indexer = class _Indexer {
9589
10115
  }
9590
10116
  this.expectedCommitOverride = runtimeOptions.expectedCommit?.toLowerCase();
9591
10117
  this.indexPathOverride = runtimeOptions.indexPath;
10118
+ this.fileBatchLimits = runtimeOptions.fileBatchLimits;
9592
10119
  this.config = config;
9593
10120
  this.host = host;
9594
10121
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -9606,26 +10133,26 @@ var Indexer = class _Indexer {
9606
10133
  return this.indexPathOverride ?? resolveProjectIndexPath(this.projectRoot, this.config.scope, this.host);
9607
10134
  }
9608
10135
  toCanonicalFilePath(filePath) {
9609
- if (!path18.isAbsolute(filePath)) {
10136
+ if (!path19.isAbsolute(filePath)) {
9610
10137
  return this.resolveStoredFilePath(filePath, this.projectRoot);
9611
10138
  }
9612
- if (path18.resolve(this.materializedProjectRoot) === path18.resolve(this.projectRoot) || !isPathWithinRoot2(filePath, this.materializedProjectRoot)) {
10139
+ if (path19.resolve(this.materializedProjectRoot) === path19.resolve(this.projectRoot) || !isPathWithinRoot2(filePath, this.materializedProjectRoot)) {
9613
10140
  return filePath;
9614
10141
  }
9615
- return path18.resolve(this.projectRoot, path18.relative(this.materializedProjectRoot, filePath));
10142
+ return path19.resolve(this.projectRoot, path19.relative(this.materializedProjectRoot, filePath));
9616
10143
  }
9617
10144
  toStoredFilePath(filePath) {
9618
10145
  const canonicalFilePath = this.toCanonicalFilePath(filePath);
9619
10146
  if (this.config.scope !== "project" || !isPathWithinRoot2(canonicalFilePath, this.projectRoot)) {
9620
10147
  return canonicalFilePath;
9621
10148
  }
9622
- return path18.relative(this.projectRoot, canonicalFilePath).split(path18.sep).join("/");
10149
+ return path19.relative(this.projectRoot, canonicalFilePath).split(path19.sep).join("/");
9623
10150
  }
9624
10151
  resolveStoredFilePath(filePath, rootPath = this.projectRoot) {
9625
- if (path18.isAbsolute(filePath)) {
10152
+ if (path19.isAbsolute(filePath)) {
9626
10153
  return filePath;
9627
10154
  }
9628
- const resolvedPath = path18.resolve(rootPath, ...filePath.split("/"));
10155
+ const resolvedPath = path19.resolve(rootPath, ...filePath.split("/"));
9629
10156
  if (!isPathWithinRoot2(resolvedPath, rootPath)) {
9630
10157
  throw new Error(`Stored project path escapes project root: ${JSON.stringify(filePath)}`);
9631
10158
  }
@@ -9649,7 +10176,7 @@ var Indexer = class _Indexer {
9649
10176
  }
9650
10177
  toMaterializedFilePath(filePath) {
9651
10178
  const storedFilePath = this.toStoredFilePath(filePath);
9652
- if (path18.isAbsolute(storedFilePath)) {
10179
+ if (path19.isAbsolute(storedFilePath)) {
9653
10180
  return storedFilePath;
9654
10181
  }
9655
10182
  return this.resolveStoredFilePath(storedFilePath, this.materializedProjectRoot);
@@ -9666,10 +10193,10 @@ var Indexer = class _Indexer {
9666
10193
  }
9667
10194
  getRuntimeArtifactPath(fileName) {
9668
10195
  const namespace = this.getRuntimeArtifactNamespace();
9669
- if (!namespace) return path18.join(this.indexPath, fileName);
9670
- const extension = path18.extname(fileName);
10196
+ if (!namespace) return path19.join(this.indexPath, fileName);
10197
+ const extension = path19.extname(fileName);
9671
10198
  const baseName = fileName.slice(0, fileName.length - extension.length);
9672
- return path18.join(this.indexPath, `${baseName}.${namespace}${extension}`);
10199
+ return path19.join(this.indexPath, `${baseName}.${namespace}${extension}`);
9673
10200
  }
9674
10201
  refreshRuntimeArtifactPaths() {
9675
10202
  this.fileHashCachePath = this.getRuntimeArtifactPath("file-hashes.json");
@@ -9682,14 +10209,14 @@ var Indexer = class _Indexer {
9682
10209
  getMaterializedKnowledgeBases() {
9683
10210
  const canonicalProjectRoot = this.getCanonicalPath(this.projectRoot);
9684
10211
  return this.config.knowledgeBases.map((knowledgeBase) => {
9685
- const configuredPath = path18.isAbsolute(knowledgeBase) ? knowledgeBase : path18.resolve(this.projectRoot, knowledgeBase);
10212
+ const configuredPath = path19.isAbsolute(knowledgeBase) ? knowledgeBase : path19.resolve(this.projectRoot, knowledgeBase);
9686
10213
  const canonicalPath = this.getCanonicalPath(configuredPath);
9687
10214
  if (!isPathWithinRoot2(canonicalPath, canonicalProjectRoot)) {
9688
10215
  return canonicalPath;
9689
10216
  }
9690
- return path18.resolve(
10217
+ return path19.resolve(
9691
10218
  this.materializedProjectRoot,
9692
- path18.relative(canonicalProjectRoot, canonicalPath)
10219
+ path19.relative(canonicalProjectRoot, canonicalPath)
9693
10220
  );
9694
10221
  });
9695
10222
  }
@@ -9697,7 +10224,7 @@ var Indexer = class _Indexer {
9697
10224
  try {
9698
10225
  return canonicalizePathForComparison(targetPath);
9699
10226
  } catch {
9700
- return path18.resolve(targetPath);
10227
+ return path19.resolve(targetPath);
9701
10228
  }
9702
10229
  }
9703
10230
  isProjectOwnedIndexPath() {
@@ -9817,7 +10344,7 @@ var Indexer = class _Indexer {
9817
10344
  atomicWriteSync(targetPath, data) {
9818
10345
  const lease = this.requireActiveLease();
9819
10346
  const tempPath = createLeaseTemporaryPath(targetPath, lease.owner, "tmp");
9820
- (0, import_fs12.mkdirSync)(path18.dirname(targetPath), { recursive: true });
10347
+ (0, import_fs12.mkdirSync)(path19.dirname(targetPath), { recursive: true });
9821
10348
  try {
9822
10349
  (0, import_fs12.writeFileSync)(tempPath, data);
9823
10350
  (0, import_fs12.renameSync)(tempPath, targetPath);
@@ -9827,14 +10354,14 @@ var Indexer = class _Indexer {
9827
10354
  }
9828
10355
  saveInvertedIndex(invertedIndex) {
9829
10356
  this.atomicWriteSync(
9830
- path18.join(this.indexPath, "inverted-index.json"),
10357
+ path19.join(this.indexPath, "inverted-index.json"),
9831
10358
  invertedIndex.serialize()
9832
10359
  );
9833
10360
  }
9834
10361
  getScopedRoots() {
9835
10362
  const roots = /* @__PURE__ */ new Set([this.getCanonicalPath(this.projectRoot)]);
9836
10363
  for (const kbRoot of this.config.knowledgeBases) {
9837
- roots.add(this.getCanonicalPath(path18.resolve(this.projectRoot, kbRoot)));
10364
+ roots.add(this.getCanonicalPath(path19.resolve(this.projectRoot, kbRoot)));
9838
10365
  }
9839
10366
  return Array.from(roots);
9840
10367
  }
@@ -9850,6 +10377,9 @@ var Indexer = class _Indexer {
9850
10377
  }
9851
10378
  return `${this.projectIdentityHash}:${branchName}`;
9852
10379
  }
10380
+ resolveBranchCatalogKey(branchName) {
10381
+ return branchName === void 0 ? this.getBranchCatalogKey() : this.getBranchCatalogKeyFor(branchName);
10382
+ }
9853
10383
  getBranchCommitMetadataKey(catalogIdentity = this.getBranchCatalogIdentity()) {
9854
10384
  const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
9855
10385
  return `index.branchCommit.${hashContent(branchKey).slice(0, 24)}`;
@@ -9944,13 +10474,13 @@ var Indexer = class _Indexer {
9944
10474
  if (Array.from(this.fileHashCache.keys()).some((filePath) => this.isFileInCurrentScope(filePath, roots))) {
9945
10475
  return true;
9946
10476
  }
9947
- if (this.loadSerializedFailedBatches().some(
9948
- (batch) => batch.chunks.some((chunk) => {
10477
+ for (const batch of this.loadSerializedFailedBatches()) {
10478
+ if (batch.chunks.some((chunk) => {
9949
10479
  const filePath = getPendingChunkFilePath(chunk);
9950
10480
  return filePath !== null && this.isFileInCurrentScope(filePath, roots);
9951
- })
9952
- )) {
9953
- return true;
10481
+ })) {
10482
+ return true;
10483
+ }
9954
10484
  }
9955
10485
  if (!this.database) {
9956
10486
  return false;
@@ -10091,40 +10621,25 @@ var Indexer = class _Indexer {
10091
10621
  }
10092
10622
  this.saveFileHashCache();
10093
10623
  }
10094
- partitionFailedBatches(roots, maxChunkTokens) {
10095
- const scoped = [];
10096
- const retained = [];
10097
- for (const batch of this.loadSerializedFailedBatches()) {
10098
- const scopedChunks = batch.chunks.filter((chunk) => {
10099
- const filePath = getPendingChunkFilePath(chunk);
10100
- return filePath !== null && this.isFileInCurrentScope(filePath, roots);
10101
- });
10102
- const retainedChunks = batch.chunks.filter((chunk) => {
10103
- const filePath = getPendingChunkFilePath(chunk);
10104
- return filePath === null || !this.isFileInCurrentScope(filePath, roots);
10105
- });
10106
- if (scopedChunks.length > 0) {
10107
- const normalizedBatch = normalizeFailedBatch({ ...batch, chunks: scopedChunks }, maxChunkTokens);
10108
- if (normalizedBatch) {
10109
- scoped.push(normalizedBatch);
10110
- }
10111
- }
10112
- if (retainedChunks.length > 0) {
10113
- retained.push({ ...batch, chunks: retainedChunks });
10114
- }
10115
- }
10116
- return { scoped, retained };
10117
- }
10118
10624
  clearScopedFailedBatches(roots) {
10119
- const { retained: retainedBatches } = this.partitionFailedBatches(roots);
10120
- this.saveFailedBatches(retainedBatches);
10625
+ this.rewriteFailedBatchState((chunk) => {
10626
+ const filePath = getPendingChunkFilePath(chunk);
10627
+ return filePath === null || !this.isFileInCurrentScope(filePath, roots);
10628
+ });
10121
10629
  }
10122
10630
  hasForeignScopedFileHashData(roots) {
10123
10631
  return Array.from(this.fileHashCache.keys()).some((filePath) => !this.isFileInCurrentScope(filePath, roots));
10124
10632
  }
10125
10633
  hasForeignScopedFailedBatches(roots) {
10126
- const { retained } = this.partitionFailedBatches(roots);
10127
- return retained.length > 0;
10634
+ for (const batch of this.loadSerializedFailedBatches()) {
10635
+ if (batch.chunks.some((chunk) => {
10636
+ const filePath = getPendingChunkFilePath(chunk);
10637
+ return filePath === null || !this.isFileInCurrentScope(filePath, roots);
10638
+ })) {
10639
+ return true;
10640
+ }
10641
+ }
10642
+ return false;
10128
10643
  }
10129
10644
  hasForeignScopedBranchData() {
10130
10645
  if (!this.database || this.config.scope !== "global") {
@@ -10149,10 +10664,6 @@ var Indexer = class _Indexer {
10149
10664
  }
10150
10665
  );
10151
10666
  }
10152
- saveScopedFailedBatches(batches, roots) {
10153
- const { retained } = this.partitionFailedBatches(roots);
10154
- this.saveFailedBatches([...retained, ...batches]);
10155
- }
10156
10667
  clearSharedIndexProjectData(store, invertedIndex, database, roots) {
10157
10668
  const allMetadata = store.getAllMetadata();
10158
10669
  const scopedEntries = allMetadata.filter(({ metadata }) => this.isFileInCurrentScope(metadata.filePath, roots));
@@ -10249,70 +10760,137 @@ var Indexer = class _Indexer {
10249
10760
  }
10250
10761
  this.logger.info("Recovery complete, next index will re-process all files");
10251
10762
  }
10252
- loadFailedBatches(maxChunkTokens) {
10253
- try {
10254
- return this.loadSerializedFailedBatches().map((batch) => normalizeFailedBatch(batch, maxChunkTokens)).filter((batch) => batch !== null);
10255
- } catch (error) {
10256
- const message = error instanceof Error ? error.message : String(error);
10763
+ *loadSerializedFailedBatches() {
10764
+ let warned = false;
10765
+ const warn = (error) => {
10766
+ if (warned) return;
10767
+ warned = true;
10257
10768
  this.logger.warn("Failed to load failed batch state, skipping persisted retries", {
10258
10769
  failedBatchesPath: this.failedBatchesPath,
10259
- error: message
10770
+ error: getErrorMessage4(error)
10260
10771
  });
10261
- return [];
10772
+ };
10773
+ try {
10774
+ for (const record of readFailedBatchRecords(this.failedBatchesPath, {
10775
+ malformedLineAction: "skip",
10776
+ onMalformedLine: (error) => warn(error)
10777
+ })) {
10778
+ yield {
10779
+ chunks: record.chunks,
10780
+ error: record.error,
10781
+ attemptCount: record.attemptCount,
10782
+ lastAttempt: record.lastAttempt
10783
+ };
10784
+ }
10785
+ } catch (error) {
10786
+ warn(error);
10262
10787
  }
10263
10788
  }
10264
- loadSerializedFailedBatches() {
10265
- if (!(0, import_fs12.existsSync)(this.failedBatchesPath)) {
10266
- return [];
10789
+ createFailedBatchWriteState() {
10790
+ return {
10791
+ writer: createFailedBatchWriter(this.failedBatchesPath),
10792
+ recordsWritten: 0
10793
+ };
10794
+ }
10795
+ writeFailedBatchRecord(state, record) {
10796
+ state.writer.write(record);
10797
+ state.recordsWritten += record.chunks.length;
10798
+ }
10799
+ finalizeFailedBatchWriteState(state) {
10800
+ if (state.recordsWritten > 0) {
10801
+ state.writer.commit();
10802
+ return;
10267
10803
  }
10268
- const data = (0, import_fs12.readFileSync)(this.failedBatchesPath, "utf-8");
10269
- const parsed = JSON.parse(data);
10270
- return parsed.map((batch) => {
10271
- const chunks = Array.isArray(batch.chunks) ? batch.chunks : [];
10272
- if (chunks.length === 0) {
10273
- return null;
10804
+ state.writer.cleanup();
10805
+ this.clearFailedBatchState();
10806
+ }
10807
+ clearFailedBatchState() {
10808
+ if ((0, import_fs12.existsSync)(this.failedBatchesPath)) {
10809
+ try {
10810
+ (0, import_fs12.unlinkSync)(this.failedBatchesPath);
10811
+ } catch {
10274
10812
  }
10275
- return {
10276
- chunks,
10277
- error: typeof batch.error === "string" ? batch.error : "Unknown embedding error",
10278
- attemptCount: typeof batch.attemptCount === "number" ? batch.attemptCount : 1,
10279
- lastAttempt: typeof batch.lastAttempt === "string" ? batch.lastAttempt : (/* @__PURE__ */ new Date()).toISOString()
10280
- };
10281
- }).filter((batch) => batch !== null);
10813
+ }
10282
10814
  }
10283
- saveFailedBatches(batches) {
10284
- if (batches.length === 0) {
10285
- if ((0, import_fs12.existsSync)(this.failedBatchesPath)) {
10286
- try {
10287
- (0, import_fs12.unlinkSync)(this.failedBatchesPath);
10288
- } catch {
10815
+ rewriteFailedBatchState(shouldRetain) {
10816
+ const state = this.createFailedBatchWriteState();
10817
+ try {
10818
+ for (const batch of this.loadSerializedFailedBatches()) {
10819
+ const retainedChunks = batch.chunks.filter(shouldRetain);
10820
+ if (retainedChunks.length > 0) {
10821
+ this.writeFailedBatchRecord(state, { ...batch, chunks: retainedChunks });
10289
10822
  }
10290
10823
  }
10291
- return;
10824
+ this.finalizeFailedBatchWriteState(state);
10825
+ } catch (error) {
10826
+ state.writer.cleanup();
10827
+ throw error;
10828
+ }
10829
+ }
10830
+ prepareFailedBatchProcessing(roots, shouldProcess) {
10831
+ const state = this.createFailedBatchWriteState();
10832
+ const latestById = /* @__PURE__ */ new Map();
10833
+ try {
10834
+ for (const batch of this.loadSerializedFailedBatches()) {
10835
+ for (const rawChunk of batch.chunks) {
10836
+ const filePath = getPendingChunkFilePath(rawChunk);
10837
+ const inScope = roots === null || filePath !== null && this.isFileInCurrentScope(filePath, roots);
10838
+ if (!inScope) {
10839
+ this.writeFailedBatchRecord(state, { ...batch, chunks: [rawChunk] });
10840
+ continue;
10841
+ }
10842
+ if (!shouldProcess(filePath)) {
10843
+ continue;
10844
+ }
10845
+ const chunkId = getPendingChunkId(rawChunk);
10846
+ if (!chunkId) {
10847
+ continue;
10848
+ }
10849
+ const existing = latestById.get(chunkId);
10850
+ if (!existing || batch.attemptCount >= existing.attemptCount) {
10851
+ latestById.set(chunkId, {
10852
+ attemptCount: batch.attemptCount,
10853
+ error: batch.error,
10854
+ lastAttempt: batch.lastAttempt
10855
+ });
10856
+ }
10857
+ }
10858
+ }
10859
+ return { state, latestById };
10860
+ } catch (error) {
10861
+ state.writer.cleanup();
10862
+ throw error;
10292
10863
  }
10293
- this.atomicWriteSync(this.failedBatchesPath, JSON.stringify(batches, null, 2));
10294
10864
  }
10295
- collectRetryableFailedChunks(currentFileHashes, unchangedFilePaths, maxChunkTokens) {
10296
- const retryableById = /* @__PURE__ */ new Map();
10297
- for (const batch of this.loadFailedBatches(maxChunkTokens)) {
10298
- for (const chunk of batch.chunks) {
10299
- const filePath = chunk.metadata.filePath;
10300
- if (!currentFileHashes.has(filePath)) {
10865
+ *iterateLatestFailedChunks(latestById, roots, shouldProcess, maxChunkTokens) {
10866
+ const yielded = /* @__PURE__ */ new Set();
10867
+ for (const batch of this.loadSerializedFailedBatches()) {
10868
+ for (const rawChunk of batch.chunks) {
10869
+ const chunkId = getPendingChunkId(rawChunk);
10870
+ if (!chunkId || yielded.has(chunkId)) {
10301
10871
  continue;
10302
10872
  }
10303
- if (!unchangedFilePaths.has(filePath)) {
10873
+ const latest = latestById.get(chunkId);
10874
+ if (!latest || latest.attemptCount !== batch.attemptCount || latest.error !== batch.error || latest.lastAttempt !== batch.lastAttempt) {
10304
10875
  continue;
10305
10876
  }
10306
- const existing = retryableById.get(chunk.id);
10307
- if (!existing || batch.attemptCount > existing.attemptCount) {
10308
- retryableById.set(chunk.id, {
10309
- chunk,
10310
- attemptCount: batch.attemptCount
10311
- });
10877
+ const filePath = getPendingChunkFilePath(rawChunk);
10878
+ const inScope = roots === null || filePath !== null && this.isFileInCurrentScope(filePath, roots);
10879
+ if (!inScope || !shouldProcess(filePath)) {
10880
+ continue;
10881
+ }
10882
+ const normalized = normalizeFailedBatch({ ...batch, chunks: [rawChunk] }, maxChunkTokens);
10883
+ const chunk = normalized?.chunks[0];
10884
+ if (!chunk) {
10885
+ continue;
10312
10886
  }
10887
+ yielded.add(chunkId);
10888
+ yield {
10889
+ chunk,
10890
+ attemptCount: batch.attemptCount
10891
+ };
10313
10892
  }
10314
10893
  }
10315
- return Array.from(retryableById.values());
10316
10894
  }
10317
10895
  getProviderRateLimits(provider) {
10318
10896
  switch (provider) {
@@ -10337,6 +10915,242 @@ var Indexer = class _Indexer {
10337
10915
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
10338
10916
  }
10339
10917
  }
10918
+ async processPendingChunkBatch(chunks, options) {
10919
+ const result = {
10920
+ indexedChunks: 0,
10921
+ failedChunks: 0,
10922
+ tokensUsed: 0,
10923
+ failedChunkIds: /* @__PURE__ */ new Set()
10924
+ };
10925
+ if (chunks.length === 0) {
10926
+ return result;
10927
+ }
10928
+ const chunksNeedingEmbedding = [];
10929
+ let cachedChunkCount = 0;
10930
+ if (options.reuseCachedEmbeddings && !options.forceReembed) {
10931
+ const missingHashes = new Set(options.database.getMissingEmbeddings(chunks.map((chunk) => chunk.contentHash)));
10932
+ for (const chunk of chunks) {
10933
+ if (missingHashes.has(chunk.contentHash)) {
10934
+ chunksNeedingEmbedding.push(chunk);
10935
+ continue;
10936
+ }
10937
+ const embeddingBuffer = options.database.getEmbedding(chunk.contentHash);
10938
+ if (!embeddingBuffer) {
10939
+ chunksNeedingEmbedding.push(chunk);
10940
+ continue;
10941
+ }
10942
+ options.store.add(chunk.id, Array.from(bufferToFloat32Array(embeddingBuffer)), chunk.metadata);
10943
+ options.invertedIndex.removeChunk(chunk.id);
10944
+ options.invertedIndex.addChunk(chunk.id, chunk.content);
10945
+ options.onSucceeded?.([chunk]);
10946
+ result.indexedChunks += 1;
10947
+ cachedChunkCount += 1;
10948
+ }
10949
+ } else {
10950
+ chunksNeedingEmbedding.push(...chunks);
10951
+ }
10952
+ this.logger.cache("info", "Embedding cache lookup", {
10953
+ needsEmbedding: chunksNeedingEmbedding.length,
10954
+ fromCache: cachedChunkCount
10955
+ });
10956
+ if (cachedChunkCount > 0) {
10957
+ this.logger.recordChunksFromCache(cachedChunkCount);
10958
+ options.onProgress?.(result);
10959
+ }
10960
+ if (chunksNeedingEmbedding.length === 0) {
10961
+ return result;
10962
+ }
10963
+ const pendingChunksById = new Map(chunksNeedingEmbedding.map((chunk) => [chunk.id, chunk]));
10964
+ const embeddingPartsByChunk = /* @__PURE__ */ new Map();
10965
+ const completedVectorsByChunkId = /* @__PURE__ */ new Map();
10966
+ const completedChunkIds = /* @__PURE__ */ new Set();
10967
+ const requestBatches = createPendingEmbeddingRequestBatches(
10968
+ chunksNeedingEmbedding,
10969
+ getDynamicBatchOptions(options.configuredProviderInfo)
10970
+ );
10971
+ let fatalError;
10972
+ for (const requestBatch of requestBatches) {
10973
+ await options.queue.onSizeLessThan(Math.max(1, options.providerRateLimits.concurrency));
10974
+ const task = options.queue.add(async () => {
10975
+ if (options.rateLimitState.backoffMs > 0) {
10976
+ await new Promise((resolve13) => setTimeout(resolve13, options.rateLimitState.backoffMs));
10977
+ }
10978
+ try {
10979
+ const embeddingResult = await pRetry(
10980
+ async () => {
10981
+ const texts = requestBatch.map((request) => request.text);
10982
+ return options.provider.embedBatch(texts);
10983
+ },
10984
+ {
10985
+ retries: this.config.indexing.retries,
10986
+ minTimeout: Math.max(this.config.indexing.retryDelayMs, options.providerRateLimits.minRetryMs),
10987
+ maxTimeout: options.providerRateLimits.maxRetryMs,
10988
+ factor: 2,
10989
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
10990
+ onFailedAttempt: (error) => {
10991
+ const message = getErrorMessage4(error);
10992
+ if (isRateLimitError(error)) {
10993
+ options.rateLimitState.backoffMs = Math.min(
10994
+ options.providerRateLimits.maxRetryMs,
10995
+ (options.rateLimitState.backoffMs || options.providerRateLimits.minRetryMs) * 2
10996
+ );
10997
+ this.logger.embedding("warn", "Rate limited, backing off", {
10998
+ attempt: error.attemptNumber,
10999
+ retriesLeft: error.retriesLeft,
11000
+ backoffMs: options.rateLimitState.backoffMs
11001
+ });
11002
+ } else {
11003
+ this.logger.embedding("error", "Embedding batch failed", {
11004
+ attempt: error.attemptNumber,
11005
+ error: message
11006
+ });
11007
+ }
11008
+ }
11009
+ }
11010
+ );
11011
+ if (options.rateLimitState.backoffMs > 0) {
11012
+ options.rateLimitState.backoffMs = Math.max(0, options.rateLimitState.backoffMs - 2e3);
11013
+ }
11014
+ const touchedChunkIds = /* @__PURE__ */ new Set();
11015
+ requestBatch.forEach((request, index) => {
11016
+ if (result.failedChunkIds.has(request.chunk.id) || completedChunkIds.has(request.chunk.id)) {
11017
+ return;
11018
+ }
11019
+ const vector = embeddingResult.embeddings[index];
11020
+ if (!vector) {
11021
+ throw new Error(`Embedding API returned too few vectors for chunk ${request.chunk.id}`);
11022
+ }
11023
+ const parts = embeddingPartsByChunk.get(request.chunk.id) ?? [];
11024
+ parts[request.partIndex] = {
11025
+ vector,
11026
+ tokenCount: request.tokenCount
11027
+ };
11028
+ embeddingPartsByChunk.set(request.chunk.id, parts);
11029
+ touchedChunkIds.add(request.chunk.id);
11030
+ });
11031
+ const pooledResults = [];
11032
+ for (const chunkId of touchedChunkIds) {
11033
+ if (result.failedChunkIds.has(chunkId) || completedChunkIds.has(chunkId)) {
11034
+ continue;
11035
+ }
11036
+ const chunk = pendingChunksById.get(chunkId);
11037
+ if (!chunk) {
11038
+ continue;
11039
+ }
11040
+ const parts = embeddingPartsByChunk.get(chunk.id) ?? [];
11041
+ if (!hasAllEmbeddingParts(parts, chunk.texts.length)) {
11042
+ continue;
11043
+ }
11044
+ const orderedParts = parts;
11045
+ pooledResults.push({
11046
+ chunk,
11047
+ vector: poolEmbeddingVectors(
11048
+ orderedParts.map((part) => part.vector),
11049
+ orderedParts.map((part) => part.tokenCount)
11050
+ )
11051
+ });
11052
+ }
11053
+ if (pooledResults.length > 0) {
11054
+ options.database.upsertEmbeddingsBatch(pooledResults.map(({ chunk, vector }) => ({
11055
+ contentHash: chunk.contentHash,
11056
+ embedding: float32ArrayToBuffer(vector),
11057
+ chunkText: chunk.storageText,
11058
+ model: options.configuredProviderInfo.modelInfo.model
11059
+ })));
11060
+ const succeededChunks = pooledResults.map(({ chunk }) => chunk);
11061
+ for (const { chunk, vector } of pooledResults) {
11062
+ completedVectorsByChunkId.set(chunk.id, vector);
11063
+ }
11064
+ for (const chunk of succeededChunks) {
11065
+ completedChunkIds.add(chunk.id);
11066
+ embeddingPartsByChunk.delete(chunk.id);
11067
+ }
11068
+ }
11069
+ result.tokensUsed += embeddingResult.totalTokensUsed;
11070
+ this.logger.recordEmbeddingApiCall(embeddingResult.totalTokensUsed);
11071
+ this.logger.embedding("debug", "Embedded batch", {
11072
+ batchSize: pooledResults.length,
11073
+ requestCount: requestBatch.length,
11074
+ tokens: embeddingResult.totalTokensUsed
11075
+ });
11076
+ } catch (error) {
11077
+ const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id)).filter((chunk) => options.incrementRepeatedFailures || !result.failedChunkIds.has(chunk.id));
11078
+ const failureMessage = getErrorMessage4(error);
11079
+ const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
11080
+ for (const chunk of failedChunks) {
11081
+ if (!result.failedChunkIds.has(chunk.id)) {
11082
+ result.failedChunkIds.add(chunk.id);
11083
+ result.failedChunks += 1;
11084
+ }
11085
+ embeddingPartsByChunk.delete(chunk.id);
11086
+ const attemptCount = (options.attemptCounts.get(chunk.id) ?? 0) + 1;
11087
+ options.attemptCounts.set(chunk.id, attemptCount);
11088
+ this.writeFailedBatchRecord(options.failedState, {
11089
+ chunks: [chunk],
11090
+ error: failureMessage,
11091
+ attemptCount,
11092
+ lastAttempt: failureTimestamp
11093
+ });
11094
+ }
11095
+ this.logger.recordEmbeddingError();
11096
+ this.logger.embedding("error", "Failed to embed batch after retries", {
11097
+ batchSize: failedChunks.length,
11098
+ requestCount: requestBatch.length,
11099
+ error: failureMessage
11100
+ });
11101
+ }
11102
+ options.onProgress?.(result);
11103
+ });
11104
+ void task.catch((error) => {
11105
+ fatalError ??= error;
11106
+ });
11107
+ }
11108
+ await options.queue.onIdle();
11109
+ if (fatalError !== void 0) {
11110
+ throw fatalError;
11111
+ }
11112
+ const orderedSucceededChunks = chunksNeedingEmbedding.filter((chunk) => completedVectorsByChunkId.has(chunk.id));
11113
+ if (orderedSucceededChunks.length > 0) {
11114
+ try {
11115
+ options.store.addBatch(orderedSucceededChunks.map((chunk) => ({
11116
+ id: chunk.id,
11117
+ vector: completedVectorsByChunkId.get(chunk.id),
11118
+ metadata: chunk.metadata
11119
+ })));
11120
+ for (const chunk of orderedSucceededChunks) {
11121
+ options.invertedIndex.removeChunk(chunk.id);
11122
+ options.invertedIndex.addChunk(chunk.id, chunk.content);
11123
+ }
11124
+ options.onSucceeded?.(orderedSucceededChunks);
11125
+ result.indexedChunks += orderedSucceededChunks.length;
11126
+ this.logger.recordChunksEmbedded(orderedSucceededChunks.length);
11127
+ } catch (error) {
11128
+ const failureMessage = getErrorMessage4(error);
11129
+ const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
11130
+ for (const chunk of orderedSucceededChunks) {
11131
+ options.store.remove(chunk.id);
11132
+ options.invertedIndex.removeChunk(chunk.id);
11133
+ result.failedChunkIds.add(chunk.id);
11134
+ result.failedChunks += 1;
11135
+ const attemptCount = (options.attemptCounts.get(chunk.id) ?? 0) + 1;
11136
+ options.attemptCounts.set(chunk.id, attemptCount);
11137
+ this.writeFailedBatchRecord(options.failedState, {
11138
+ chunks: [chunk],
11139
+ error: failureMessage,
11140
+ attemptCount,
11141
+ lastAttempt: failureTimestamp
11142
+ });
11143
+ }
11144
+ this.logger.recordEmbeddingError();
11145
+ this.logger.embedding("error", "Failed to publish embedded chunks", {
11146
+ batchSize: orderedSucceededChunks.length,
11147
+ error: failureMessage
11148
+ });
11149
+ }
11150
+ options.onProgress?.(result);
11151
+ }
11152
+ return result;
11153
+ }
10340
11154
  async rerankCandidatesWithApi(query, candidates, options) {
10341
11155
  const reranker = this.config.reranker;
10342
11156
  if (!reranker || !reranker.enabled || candidates.length <= 1) {
@@ -10578,12 +11392,12 @@ var Indexer = class _Indexer {
10578
11392
  }
10579
11393
  }
10580
11394
  captureReaderArtifactFingerprint() {
10581
- const storePath = path18.join(this.indexPath, "vectors");
11395
+ const storePath = path19.join(this.indexPath, "vectors");
10582
11396
  return {
10583
11397
  vectors: `${this.getReaderFileFingerprint(storePath)}|${this.getReaderFileFingerprint(`${storePath}.meta.json`)}`,
10584
- keyword: this.getReaderFileFingerprint(path18.join(this.indexPath, "inverted-index.json")),
10585
- database: this.getReaderFileFingerprint(path18.join(this.indexPath, "codebase.db")),
10586
- databaseIdentity: this.getReaderFileFingerprint(path18.join(this.indexPath, "codebase.db"), true)
11398
+ keyword: this.getReaderFileFingerprint(path19.join(this.indexPath, "inverted-index.json")),
11399
+ database: this.getReaderFileFingerprint(path19.join(this.indexPath, "codebase.db")),
11400
+ databaseIdentity: this.getReaderFileFingerprint(path19.join(this.indexPath, "codebase.db"), true)
10587
11401
  };
10588
11402
  }
10589
11403
  refreshReaderArtifacts() {
@@ -10608,10 +11422,10 @@ var Indexer = class _Indexer {
10608
11422
  issues.set(component, this.createReadIssue(component, message));
10609
11423
  this.readerArtifactRetryAfter.set(component, Date.now() + READER_ARTIFACT_RETRY_INTERVAL_MS);
10610
11424
  };
10611
- const storePath = path18.join(this.indexPath, "vectors");
11425
+ const storePath = path19.join(this.indexPath, "vectors");
10612
11426
  const vectorMetadataPath = `${storePath}.meta.json`;
10613
- const invertedIndexPath = path18.join(this.indexPath, "inverted-index.json");
10614
- const dbPath = path18.join(this.indexPath, "codebase.db");
11427
+ const invertedIndexPath = path19.join(this.indexPath, "inverted-index.json");
11428
+ const dbPath = path19.join(this.indexPath, "codebase.db");
10615
11429
  if (vectorsChanged || retryDue("vectors")) {
10616
11430
  const vectorStoreExists = (0, import_fs12.existsSync)(storePath);
10617
11431
  const vectorMetadataExists = (0, import_fs12.existsSync)(vectorMetadataPath);
@@ -10736,10 +11550,10 @@ var Indexer = class _Indexer {
10736
11550
  }
10737
11551
  }
10738
11552
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
10739
- const storePath = path18.join(this.indexPath, "vectors");
11553
+ const storePath = path19.join(this.indexPath, "vectors");
10740
11554
  const vectorMetadataPath = `${storePath}.meta.json`;
10741
- const invertedIndexPath = path18.join(this.indexPath, "inverted-index.json");
10742
- const dbPath = path18.join(this.indexPath, "codebase.db");
11555
+ const invertedIndexPath = path19.join(this.indexPath, "inverted-index.json");
11556
+ const dbPath = path19.join(this.indexPath, "codebase.db");
10743
11557
  let dbIsNew = !(0, import_fs12.existsSync)(dbPath);
10744
11558
  const readerArtifactFingerprint = mode === "reader" ? this.captureReaderArtifactFingerprint() : null;
10745
11559
  if (mode === "writer") {
@@ -10904,7 +11718,7 @@ var Indexer = class _Indexer {
10904
11718
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
10905
11719
  return {
10906
11720
  resetCorruptedIndex: true,
10907
- warning: this.getCorruptedIndexWarning(path18.join(this.indexPath, "codebase.db"))
11721
+ warning: this.getCorruptedIndexWarning(path19.join(this.indexPath, "codebase.db"))
10908
11722
  };
10909
11723
  }
10910
11724
  throw error;
@@ -10919,7 +11733,7 @@ var Indexer = class _Indexer {
10919
11733
  return;
10920
11734
  }
10921
11735
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
10922
- const storeBasePath = path18.join(this.indexPath, "vectors");
11736
+ const storeBasePath = path19.join(this.indexPath, "vectors");
10923
11737
  const storeIndexPath = storeBasePath;
10924
11738
  const storeMetadataPath = `${storeBasePath}.meta.json`;
10925
11739
  const lease = this.requireActiveLease();
@@ -11006,7 +11820,7 @@ var Indexer = class _Indexer {
11006
11820
  const names = await import_fs12.promises.readdir(this.indexPath);
11007
11821
  const runtimeStatePattern = /^(?:file-hashes|failed-batches)(?:\.[a-f0-9]{16})?\.json$/;
11008
11822
  await Promise.all(
11009
- names.filter((name) => runtimeStatePattern.test(name)).map((name) => import_fs12.promises.rm(path18.join(this.indexPath, name), { force: true }))
11823
+ names.filter((name) => runtimeStatePattern.test(name)).map((name) => import_fs12.promises.rm(path19.join(this.indexPath, name), { force: true }))
11010
11824
  );
11011
11825
  }
11012
11826
  async resetLocalIndexArtifacts() {
@@ -11022,13 +11836,13 @@ var Indexer = class _Indexer {
11022
11836
  this.readerArtifactRetryAfter.clear();
11023
11837
  this.fileHashCache.clear();
11024
11838
  const resetPaths = [
11025
- path18.join(this.indexPath, "codebase.db"),
11026
- path18.join(this.indexPath, "codebase.db-shm"),
11027
- path18.join(this.indexPath, "codebase.db-wal"),
11028
- path18.join(this.indexPath, "vectors"),
11029
- path18.join(this.indexPath, "vectors.usearch"),
11030
- path18.join(this.indexPath, "vectors.meta.json"),
11031
- path18.join(this.indexPath, "inverted-index.json")
11839
+ path19.join(this.indexPath, "codebase.db"),
11840
+ path19.join(this.indexPath, "codebase.db-shm"),
11841
+ path19.join(this.indexPath, "codebase.db-wal"),
11842
+ path19.join(this.indexPath, "vectors"),
11843
+ path19.join(this.indexPath, "vectors.usearch"),
11844
+ path19.join(this.indexPath, "vectors.meta.json"),
11845
+ path19.join(this.indexPath, "inverted-index.json")
11032
11846
  ];
11033
11847
  await Promise.all(resetPaths.map((targetPath) => import_fs12.promises.rm(targetPath, { recursive: true, force: true })));
11034
11848
  await this.removeProjectRuntimeStateArtifacts();
@@ -11038,7 +11852,7 @@ var Indexer = class _Indexer {
11038
11852
  if (!isSqliteCorruptionError(error)) {
11039
11853
  return false;
11040
11854
  }
11041
- const dbPath = path18.join(this.indexPath, "codebase.db");
11855
+ const dbPath = path19.join(this.indexPath, "codebase.db");
11042
11856
  const warning = this.getCorruptedIndexWarning(dbPath);
11043
11857
  const errorMessage = getErrorMessage4(error);
11044
11858
  if (this.config.scope === "global") {
@@ -11347,7 +12161,6 @@ var Indexer = class _Indexer {
11347
12161
  skippedFiles: [],
11348
12162
  parseFailures: []
11349
12163
  };
11350
- const failedBatchesForCurrentRun = [];
11351
12164
  onProgress?.({
11352
12165
  phase: "scanning",
11353
12166
  filesProcessed: 0,
@@ -11362,14 +12175,10 @@ var Indexer = class _Indexer {
11362
12175
  const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
11363
12176
  const symbolExtractorMetadataKey = this.getSymbolExtractorVersionMetadataKey();
11364
12177
  const refreshCachedSymbols = database.getMetadata(symbolExtractorMetadataKey) !== SYMBOL_EXTRACTOR_VERSION;
11365
- if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
11366
- (filePath) => path18.extname(filePath).toLowerCase() === ".swift"
11367
- )) {
12178
+ if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some((filePath) => path19.extname(filePath).toLowerCase() === ".swift")) {
11368
12179
  this.logger.info("Reindexing cached Swift files for parser support");
11369
12180
  }
11370
- if (reparseCachedMetalFiles && Array.from(this.fileHashCache.keys()).some(
11371
- (filePath) => path18.extname(filePath).toLowerCase() === ".metal"
11372
- )) {
12181
+ if (reparseCachedMetalFiles && Array.from(this.fileHashCache.keys()).some((filePath) => path19.extname(filePath).toLowerCase() === ".metal")) {
11373
12182
  this.logger.info("Reindexing cached Metal files for parser support");
11374
12183
  }
11375
12184
  const includePatterns = [...this.config.include, ...this.config.additionalInclude];
@@ -11391,46 +12200,44 @@ var Indexer = class _Indexer {
11391
12200
  totalFiles: files.length,
11392
12201
  skippedFiles: skipped.length
11393
12202
  });
11394
- const changedFiles = [];
12203
+ const changedFileDescriptors = [];
11395
12204
  const unchangedFilePaths = /* @__PURE__ */ new Set();
11396
12205
  const currentFileHashes = /* @__PURE__ */ new Map();
11397
12206
  const needsCallGraphResolutionMigration = database.getMetadata(this.getCallGraphResolutionMetadataKey()) !== CALL_GRAPH_RESOLUTION_VERSION;
11398
- for (const f of files) {
11399
- const storedPath = this.toStoredFilePath(f.path);
11400
- const currentHash = hashFile(f.path);
12207
+ for (const file of files) {
12208
+ const storedPath = this.toStoredFilePath(file.path);
12209
+ const currentHash = hashFile(file.path);
11401
12210
  currentFileHashes.set(storedPath, currentHash);
11402
12211
  const cachedHashMatches = this.fileHashCache.get(storedPath) === currentHash;
11403
12212
  const needsCallGraphRefresh = cachedHashMatches && needsCallGraphResolutionMigration && database.getChunksByFile(storedPath).some(
11404
12213
  (chunk) => chunk.language === "php" || chunk.language === "c" || chunk.language === "cpp"
11405
12214
  );
11406
- const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path18.extname(storedPath).toLowerCase() === ".swift";
11407
- const requiresMetalParserUpgrade = reparseCachedMetalFiles && path18.extname(storedPath).toLowerCase() === ".metal";
12215
+ const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path19.extname(storedPath).toLowerCase() === ".swift";
12216
+ const requiresMetalParserUpgrade = reparseCachedMetalFiles && path19.extname(storedPath).toLowerCase() === ".metal";
11408
12217
  if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
11409
12218
  unchangedFilePaths.add(storedPath);
11410
12219
  this.logger.recordCacheHit();
11411
12220
  } else {
11412
- const content = await import_fs12.promises.readFile(f.path, "utf-8");
11413
- changedFiles.push({ path: storedPath, content, hash: currentHash });
12221
+ changedFileDescriptors.push({
12222
+ storedPath,
12223
+ materializedPath: file.path,
12224
+ hash: currentHash,
12225
+ sourceBytes: file.size
12226
+ });
11414
12227
  this.logger.recordCacheMiss();
11415
12228
  }
11416
12229
  }
11417
12230
  this.logger.cache("info", "File hash cache results", {
11418
12231
  unchanged: unchangedFilePaths.size,
11419
- changed: changedFiles.length
12232
+ changed: changedFileDescriptors.length
11420
12233
  });
11421
12234
  onProgress?.({
11422
12235
  phase: "parsing",
11423
- filesProcessed: 0,
12236
+ filesProcessed: unchangedFilePaths.size,
11424
12237
  totalFiles: files.length,
11425
12238
  chunksProcessed: 0,
11426
12239
  totalChunks: 0
11427
12240
  });
11428
- const parseStartTime = import_perf_hooks.performance.now();
11429
- const parsedFiles = parseFiles(changedFiles);
11430
- const parseMs = import_perf_hooks.performance.now() - parseStartTime;
11431
- this.logger.recordFilesParsed(parsedFiles.length);
11432
- this.logger.recordParseDuration(parseMs);
11433
- this.logger.debug("Parsed changed files", { parsedCount: parsedFiles.length, parseMs: parseMs.toFixed(2) });
11434
12241
  const existingChunks = /* @__PURE__ */ new Map();
11435
12242
  const existingChunksByFile = /* @__PURE__ */ new Map();
11436
12243
  const existingMetadataById = /* @__PURE__ */ new Map();
@@ -11446,17 +12253,17 @@ var Indexer = class _Indexer {
11446
12253
  }
11447
12254
  existingChunks.set(key, metadata.hash);
11448
12255
  existingMetadataById.set(key, metadata);
11449
- const fileChunks = existingChunksByFile.get(metadata.filePath) || /* @__PURE__ */ new Set();
12256
+ const fileChunks = existingChunksByFile.get(metadata.filePath) ?? /* @__PURE__ */ new Set();
11450
12257
  fileChunks.add(key);
11451
12258
  existingChunksByFile.set(metadata.filePath, fileChunks);
11452
12259
  }
11453
12260
  const currentChunkIds = /* @__PURE__ */ new Set();
11454
- const currentFilePaths = /* @__PURE__ */ new Set();
11455
- const pendingChunks = [];
12261
+ const allSymbolIds = /* @__PURE__ */ new Set();
12262
+ const failedChunkIds = /* @__PURE__ */ new Set();
12263
+ const retryableChunksWithExistingData = /* @__PURE__ */ new Set();
11456
12264
  const gitBlameEnabled = this.config.indexing.gitBlame.enabled && isGitRepo(this.materializedProjectRoot);
11457
12265
  let backfilledBlameMetadata = false;
11458
12266
  for (const filePath of unchangedFilePaths) {
11459
- currentFilePaths.add(filePath);
11460
12267
  const fileChunks = existingChunksByFile.get(filePath);
11461
12268
  if (fileChunks) {
11462
12269
  for (const chunkId of fileChunks) {
@@ -11464,635 +12271,552 @@ var Indexer = class _Indexer {
11464
12271
  }
11465
12272
  }
11466
12273
  }
11467
- const chunkDataBatch = [];
11468
- if (gitBlameEnabled) {
11469
- const backfillItems = [];
11470
- for (const chunkId of currentChunkIds) {
11471
- const metadata = existingMetadataById.get(chunkId);
11472
- if (!metadata || hasBlameMetadata(metadata)) {
11473
- continue;
11474
- }
11475
- const chunk = database.getChunk(chunkId);
11476
- if (!chunk) {
11477
- continue;
12274
+ const shouldRetryFailedPath = (filePath) => filePath !== null && currentFileHashes.has(filePath) && unchangedFilePaths.has(filePath);
12275
+ const failedProcessing = this.prepareFailedBatchProcessing(scopedRoots, shouldRetryFailedPath);
12276
+ const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
12277
+ const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
12278
+ const queue = new PQueue({
12279
+ concurrency: providerRateLimits.concurrency,
12280
+ interval: providerRateLimits.intervalMs,
12281
+ intervalCap: providerRateLimits.concurrency
12282
+ });
12283
+ const rateLimitState = { backoffMs: 0 };
12284
+ let writeTransactionActive = false;
12285
+ try {
12286
+ database.beginWriteTransaction();
12287
+ writeTransactionActive = true;
12288
+ const blameChunkDataBatch = [];
12289
+ if (gitBlameEnabled) {
12290
+ const backfillItems = [];
12291
+ for (const chunkId of currentChunkIds) {
12292
+ const metadata = existingMetadataById.get(chunkId);
12293
+ if (!metadata || hasBlameMetadata(metadata)) {
12294
+ continue;
12295
+ }
12296
+ const chunk = database.getChunk(chunkId);
12297
+ if (!chunk) {
12298
+ continue;
12299
+ }
12300
+ const blame = await getChunkGitBlame(
12301
+ this.materializedProjectRoot,
12302
+ this.toMaterializedFilePath(chunk.filePath),
12303
+ chunk.startLine,
12304
+ chunk.endLine
12305
+ );
12306
+ const blameMetadata = metadataFromBlame(blame);
12307
+ if (!blameMetadata.blameSha) {
12308
+ continue;
12309
+ }
12310
+ blameChunkDataBatch.push({
12311
+ ...chunk,
12312
+ blameSha: blameMetadata.blameSha,
12313
+ blameAuthor: blameMetadata.blameAuthor,
12314
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
12315
+ blameCommittedAt: blameMetadata.blameCommittedAt,
12316
+ blameSummary: blameMetadata.blameSummary
12317
+ });
12318
+ const embeddingBuffer = database.getEmbedding(chunk.contentHash);
12319
+ if (embeddingBuffer) {
12320
+ backfillItems.push({
12321
+ id: chunkId,
12322
+ vector: Array.from(bufferToFloat32Array(embeddingBuffer)),
12323
+ metadata: { ...metadata, ...blameMetadata }
12324
+ });
12325
+ }
11478
12326
  }
11479
- const blame = await getChunkGitBlame(
11480
- this.materializedProjectRoot,
11481
- this.toMaterializedFilePath(chunk.filePath),
11482
- chunk.startLine,
11483
- chunk.endLine
11484
- );
11485
- const blameMetadata = metadataFromBlame(blame);
11486
- if (!blameMetadata.blameSha) {
11487
- continue;
12327
+ if (blameChunkDataBatch.length > 0) {
12328
+ database.upsertChunksBatch(blameChunkDataBatch);
11488
12329
  }
11489
- chunkDataBatch.push({
11490
- ...chunk,
11491
- blameSha: blameMetadata.blameSha,
11492
- blameAuthor: blameMetadata.blameAuthor,
11493
- blameAuthorEmail: blameMetadata.blameAuthorEmail,
11494
- blameCommittedAt: blameMetadata.blameCommittedAt,
11495
- blameSummary: blameMetadata.blameSummary
11496
- });
11497
- const embeddingBuffer = database.getEmbedding(chunk.contentHash);
11498
- if (!embeddingBuffer) {
11499
- continue;
12330
+ if (backfillItems.length > 0) {
12331
+ store.addBatch(backfillItems);
12332
+ backfilledBlameMetadata = true;
11500
12333
  }
11501
- backfillItems.push({
11502
- id: chunkId,
11503
- vector: Array.from(bufferToFloat32Array(embeddingBuffer)),
11504
- metadata: {
11505
- ...metadata,
11506
- ...blameMetadata
11507
- }
11508
- });
11509
12334
  }
11510
- if (backfillItems.length > 0) {
11511
- store.addBatch(backfillItems);
11512
- backfilledBlameMetadata = true;
11513
- }
11514
- }
11515
- for (const parsed of parsedFiles) {
11516
- currentFilePaths.add(parsed.path);
11517
- if (parsed.chunks.length === 0) {
11518
- stats.parseFailures.push(path18.isAbsolute(parsed.path) ? path18.relative(this.projectRoot, parsed.path) : parsed.path);
11519
- }
11520
- let chunksToProcess = parsed.chunks;
11521
- if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
11522
- const changedFile = changedFiles.find((f) => f.path === parsed.path);
11523
- if (changedFile) {
11524
- const textChunks = parseFileAsText(parsed.path, changedFile.content);
11525
- chunksToProcess = textChunks;
12335
+ for (const filePath of unchangedFilePaths) {
12336
+ for (const symbol of database.getSymbolsByFile(filePath)) {
12337
+ if (!restrictExistingChunksToBranch || previousBranchSymbolIdSet.has(symbol.id)) {
12338
+ allSymbolIds.add(symbol.id);
12339
+ }
11526
12340
  }
11527
12341
  }
11528
- chunksToProcess = selectIndexableChunks(
11529
- chunksToProcess,
11530
- this.config.indexing.maxChunksPerFile,
11531
- this.config.indexing.semanticOnly
11532
- );
11533
- for (const chunk of chunksToProcess) {
11534
- const id = this.getPreparedChunkId(generateChunkId(parsed.path, chunk));
11535
- const contentHash = generateChunkHash(chunk);
11536
- const existingContentHash = existingChunks.get(id);
11537
- const existingChunk = gitBlameEnabled ? database.getChunk(id) : null;
11538
- const blame = gitBlameEnabled && existingContentHash !== contentHash ? await getChunkGitBlame(
11539
- this.materializedProjectRoot,
11540
- this.toMaterializedFilePath(parsed.path),
11541
- chunk.startLine,
11542
- chunk.endLine
11543
- ) : blameFromChunkData(existingChunk);
11544
- const blameMetadata = metadataFromBlame(blame);
11545
- currentChunkIds.add(id);
11546
- chunkDataBatch.push({
11547
- chunkId: id,
11548
- contentHash,
11549
- filePath: parsed.path,
11550
- startLine: chunk.startLine,
11551
- endLine: chunk.endLine,
11552
- nodeType: chunk.chunkType,
11553
- name: chunk.name,
11554
- language: chunk.language,
11555
- blameSha: blameMetadata.blameSha,
11556
- blameAuthor: blameMetadata.blameAuthor,
11557
- blameAuthorEmail: blameMetadata.blameAuthorEmail,
11558
- blameCommittedAt: blameMetadata.blameCommittedAt,
11559
- blameSummary: blameMetadata.blameSummary
12342
+ let processedChangedFiles = 0;
12343
+ for (const descriptorBatch of iterateOrderedFileBatches(
12344
+ changedFileDescriptors,
12345
+ (descriptor) => descriptor.sourceBytes,
12346
+ this.fileBatchLimits
12347
+ )) {
12348
+ const loadedFiles = await Promise.all(descriptorBatch.map(async (descriptor) => ({
12349
+ path: descriptor.storedPath,
12350
+ content: await import_fs12.promises.readFile(descriptor.materializedPath, "utf-8"),
12351
+ hash: descriptor.hash
12352
+ })));
12353
+ const loadedByPath = new Map(loadedFiles.map((file) => [file.path, file]));
12354
+ const descriptorByPath = new Map(descriptorBatch.map((descriptor) => [descriptor.storedPath, descriptor]));
12355
+ const parseStartTime = import_perf_hooks.performance.now();
12356
+ const parsedFiles = parseFiles(loadedFiles);
12357
+ const parseMs = import_perf_hooks.performance.now() - parseStartTime;
12358
+ this.logger.recordFilesParsed(parsedFiles.length);
12359
+ this.logger.recordParseDuration(parseMs);
12360
+ this.logger.debug("Parsed changed file batch", {
12361
+ parsedCount: parsedFiles.length,
12362
+ parseMs: parseMs.toFixed(2)
11560
12363
  });
11561
- if (existingContentHash === contentHash) {
11562
- continue;
12364
+ const chunkDataBatch = [];
12365
+ const pendingChunks = [];
12366
+ const symbolBatch = [];
12367
+ const edgeBatch = [];
12368
+ for (const parsed of parsedFiles) {
12369
+ const loadedFile = loadedByPath.get(parsed.path);
12370
+ const descriptor = descriptorByPath.get(parsed.path);
12371
+ if (!loadedFile || !descriptor) {
12372
+ throw new Error(`Parsed file was not present in its source batch: ${parsed.path}`);
12373
+ }
12374
+ if (parsed.chunks.length === 0) {
12375
+ stats.parseFailures.push(path19.isAbsolute(parsed.path) ? path19.relative(this.projectRoot, parsed.path) : parsed.path);
12376
+ }
12377
+ let chunksToProcess = parsed.chunks;
12378
+ if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
12379
+ chunksToProcess = parseFileAsText(parsed.path, loadedFile.content);
12380
+ }
12381
+ chunksToProcess = selectIndexableChunks(
12382
+ chunksToProcess,
12383
+ this.config.indexing.maxChunksPerFile,
12384
+ this.config.indexing.semanticOnly
12385
+ );
12386
+ for (const chunk of chunksToProcess) {
12387
+ const id = this.getPreparedChunkId(generateChunkId(parsed.path, chunk));
12388
+ const contentHash = generateChunkHash(chunk);
12389
+ const existingContentHash = existingChunks.get(id);
12390
+ const existingChunk = gitBlameEnabled ? database.getChunk(id) : null;
12391
+ const blame = gitBlameEnabled && existingContentHash !== contentHash ? await getChunkGitBlame(
12392
+ this.materializedProjectRoot,
12393
+ descriptor.materializedPath,
12394
+ chunk.startLine,
12395
+ chunk.endLine
12396
+ ) : blameFromChunkData(existingChunk);
12397
+ const blameMetadata = metadataFromBlame(blame);
12398
+ currentChunkIds.add(id);
12399
+ chunkDataBatch.push({
12400
+ chunkId: id,
12401
+ contentHash,
12402
+ filePath: parsed.path,
12403
+ startLine: chunk.startLine,
12404
+ endLine: chunk.endLine,
12405
+ nodeType: chunk.chunkType,
12406
+ name: chunk.name,
12407
+ language: chunk.language,
12408
+ blameSha: blameMetadata.blameSha,
12409
+ blameAuthor: blameMetadata.blameAuthor,
12410
+ blameAuthorEmail: blameMetadata.blameAuthorEmail,
12411
+ blameCommittedAt: blameMetadata.blameCommittedAt,
12412
+ blameSummary: blameMetadata.blameSummary
12413
+ });
12414
+ if (existingContentHash === contentHash) {
12415
+ continue;
12416
+ }
12417
+ const texts = createEmbeddingTexts(chunk, parsed.path, maxChunkTokens).map((text3) => ({
12418
+ text: text3,
12419
+ tokenCount: estimateTokens(text3)
12420
+ }));
12421
+ pendingChunks.push({
12422
+ id,
12423
+ texts,
12424
+ storageText: createPendingChunkStorageText(texts),
12425
+ content: chunk.content,
12426
+ contentHash,
12427
+ metadata: {
12428
+ filePath: parsed.path,
12429
+ startLine: chunk.startLine,
12430
+ endLine: chunk.endLine,
12431
+ chunkType: chunk.chunkType,
12432
+ name: chunk.name,
12433
+ language: chunk.language,
12434
+ hash: contentHash,
12435
+ ...blameMetadata
12436
+ }
12437
+ });
12438
+ }
12439
+ const fileSymbols = [];
12440
+ for (const parsedSymbol of parsed.symbols) {
12441
+ if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) {
12442
+ continue;
12443
+ }
12444
+ const preparedNamespace = this.getPreparedBranchNamespace();
12445
+ const symbolId = `sym_${hashContent(
12446
+ (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + parsedSymbol.name + ":" + parsedSymbol.kind + ":" + parsedSymbol.startLine + ":" + parsedSymbol.startCol + ":" + descriptor.hash
12447
+ ).slice(0, 16)}`;
12448
+ const symbol = {
12449
+ id: symbolId,
12450
+ filePath: parsed.path,
12451
+ name: parsedSymbol.name,
12452
+ kind: parsedSymbol.kind,
12453
+ startLine: parsedSymbol.startLine,
12454
+ startCol: parsedSymbol.startCol,
12455
+ endLine: parsedSymbol.endLine,
12456
+ endCol: parsedSymbol.endCol,
12457
+ language: parsedSymbol.language
12458
+ };
12459
+ fileSymbols.push(symbol);
12460
+ symbolBatch.push(symbol);
12461
+ allSymbolIds.add(symbolId);
12462
+ }
12463
+ const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
12464
+ if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) {
12465
+ continue;
12466
+ }
12467
+ const isCaseInsensitiveLanguage = CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
12468
+ const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
12469
+ const symbolsByName = /* @__PURE__ */ new Map();
12470
+ for (const symbol of fileSymbols) {
12471
+ const key = normalizeSymbolKey(symbol.name);
12472
+ const symbols = symbolsByName.get(key) ?? [];
12473
+ symbols.push(symbol);
12474
+ symbolsByName.set(key, symbols);
12475
+ }
12476
+ for (const site of extractCalls(loadedFile.content, fileLanguage)) {
12477
+ const enclosingSymbol = findEnclosingSymbol(fileSymbols, site.line, site.column);
12478
+ if (!enclosingSymbol) {
12479
+ continue;
12480
+ }
12481
+ let candidates = symbolsByName.get(normalizeSymbolKey(site.calleeName));
12482
+ if (fileLanguage === "php" && candidates) {
12483
+ if (site.callType === "Constructor") {
12484
+ candidates = candidates.filter((candidate) => PHP_CLASS_SYMBOL_CHUNK_TYPES.has(candidate.kind));
12485
+ } else if (site.callType === "Call") {
12486
+ candidates = candidates.filter((candidate) => PHP_FUNCTION_SYMBOL_CHUNK_TYPES.has(candidate.kind));
12487
+ }
12488
+ }
12489
+ candidates = candidates?.filter(
12490
+ (symbol) => isCompatibleCFamilyCallTarget(fileLanguage, site.callType, symbol.kind)
12491
+ );
12492
+ const resolvedTarget = candidates?.length === 1 ? candidates[0] : void 0;
12493
+ edgeBatch.push({
12494
+ id: `edge_${hashContent(
12495
+ enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column
12496
+ ).slice(0, 16)}`,
12497
+ fromSymbolId: enclosingSymbol.id,
12498
+ targetName: site.calleeName,
12499
+ toSymbolId: resolvedTarget?.id,
12500
+ callType: site.callType,
12501
+ confidence: site.confidence,
12502
+ line: site.line,
12503
+ col: site.column,
12504
+ isResolved: resolvedTarget !== void 0
12505
+ });
12506
+ }
11563
12507
  }
11564
- const texts = createEmbeddingTexts(chunk, parsed.path, getSafeEmbeddingChunkTokenLimit(configuredProviderInfo)).map((text3) => ({
11565
- text: text3,
11566
- tokenCount: estimateTokens(text3)
11567
- }));
11568
- const metadata = {
11569
- filePath: parsed.path,
11570
- startLine: chunk.startLine,
11571
- endLine: chunk.endLine,
11572
- chunkType: chunk.chunkType,
11573
- name: chunk.name,
11574
- language: chunk.language,
11575
- hash: contentHash,
11576
- ...blameMetadata
11577
- };
11578
- pendingChunks.push({
11579
- id,
11580
- texts,
11581
- storageText: createPendingChunkStorageText(texts),
11582
- content: chunk.content,
11583
- contentHash,
11584
- metadata
11585
- });
11586
- }
11587
- }
11588
- const retryableFailedChunks = this.collectRetryableFailedChunks(
11589
- currentFileHashes,
11590
- unchangedFilePaths,
11591
- getSafeEmbeddingChunkTokenLimit(configuredProviderInfo)
11592
- );
11593
- const retryableFailedAttemptCounts = /* @__PURE__ */ new Map();
11594
- const retryableChunksWithExistingData = /* @__PURE__ */ new Set();
11595
- if (retryableFailedChunks.length > 0) {
11596
- const pendingChunkIds = new Set(pendingChunks.map((chunk) => chunk.id));
11597
- for (const { chunk, attemptCount } of retryableFailedChunks) {
11598
- retryableFailedAttemptCounts.set(chunk.id, attemptCount);
11599
- if (existingChunks.has(chunk.id)) {
11600
- retryableChunksWithExistingData.add(chunk.id);
12508
+ if (chunkDataBatch.length > 0) {
12509
+ database.upsertChunksBatch(chunkDataBatch);
11601
12510
  }
11602
- if (!pendingChunkIds.has(chunk.id)) {
11603
- pendingChunks.push(chunk);
11604
- pendingChunkIds.add(chunk.id);
11605
- currentChunkIds.add(chunk.id);
12511
+ if (symbolBatch.length > 0) {
12512
+ database.upsertSymbolsBatch(symbolBatch);
11606
12513
  }
11607
- }
11608
- }
11609
- if (chunkDataBatch.length > 0) {
11610
- database.upsertChunksBatch(chunkDataBatch);
11611
- }
11612
- const allSymbolIds = /* @__PURE__ */ new Set();
11613
- const symbolsByFile = /* @__PURE__ */ new Map();
11614
- for (let i = 0; i < parsedFiles.length; i++) {
11615
- const parsed = parsedFiles[i];
11616
- const changedFile = changedFiles[i];
11617
- const fileSymbols = [];
11618
- for (const parsedSymbol of parsed.symbols) {
11619
- if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) continue;
11620
- const preparedNamespace = this.getPreparedBranchNamespace();
11621
- const symbolId = `sym_${hashContent(
11622
- (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + parsedSymbol.name + ":" + parsedSymbol.kind + ":" + parsedSymbol.startLine + ":" + parsedSymbol.startCol + ":" + changedFile.hash
11623
- ).slice(0, 16)}`;
11624
- const symbol = {
11625
- id: symbolId,
11626
- filePath: parsed.path,
11627
- name: parsedSymbol.name,
11628
- kind: parsedSymbol.kind,
11629
- startLine: parsedSymbol.startLine,
11630
- startCol: parsedSymbol.startCol,
11631
- endLine: parsedSymbol.endLine,
11632
- endCol: parsedSymbol.endCol,
11633
- language: parsedSymbol.language
11634
- };
11635
- fileSymbols.push(symbol);
11636
- allSymbolIds.add(symbolId);
11637
- }
11638
- const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
11639
- const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
11640
- const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
11641
- const symbolsByName = /* @__PURE__ */ new Map();
11642
- for (const symbol of fileSymbols) {
11643
- const key = normalizeSymbolKey(symbol.name);
11644
- const existing = symbolsByName.get(key) ?? [];
11645
- existing.push(symbol);
11646
- symbolsByName.set(key, existing);
11647
- }
11648
- if (fileSymbols.length > 0) {
11649
- database.upsertSymbolsBatch(fileSymbols);
11650
- symbolsByFile.set(parsed.path, fileSymbols);
11651
- }
11652
- if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
11653
- const callSites = extractCalls(changedFile.content, fileLanguage);
11654
- if (callSites.length === 0) continue;
11655
- const edges = [];
11656
- for (const site of callSites) {
11657
- const enclosingSymbol = findEnclosingSymbol(
11658
- fileSymbols,
11659
- site.line,
11660
- site.column
11661
- );
11662
- if (!enclosingSymbol) continue;
11663
- const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
11664
- edges.push({
11665
- id: edgeId,
11666
- fromSymbolId: enclosingSymbol.id,
11667
- targetName: site.calleeName,
11668
- toSymbolId: void 0,
11669
- callType: site.callType,
11670
- confidence: site.confidence,
11671
- line: site.line,
11672
- col: site.column,
11673
- isResolved: false
12514
+ if (edgeBatch.length > 0) {
12515
+ database.upsertCallEdgesBatch(edgeBatch);
12516
+ }
12517
+ processedChangedFiles += descriptorBatch.length;
12518
+ stats.totalChunks += pendingChunks.length;
12519
+ onProgress?.({
12520
+ phase: "parsing",
12521
+ filesProcessed: unchangedFilePaths.size + processedChangedFiles,
12522
+ totalFiles: files.length,
12523
+ chunksProcessed: stats.indexedChunks,
12524
+ totalChunks: stats.totalChunks
11674
12525
  });
11675
- }
11676
- if (edges.length > 0) {
11677
- database.upsertCallEdgesBatch(edges);
11678
- for (const edge of edges) {
11679
- let candidates = symbolsByName.get(normalizeSymbolKey(edge.targetName));
11680
- if (fileLanguage === "php" && candidates) {
11681
- if (edge.callType === "Constructor") {
11682
- candidates = candidates.filter(
11683
- (candidate) => PHP_CLASS_SYMBOL_CHUNK_TYPES.has(candidate.kind)
11684
- );
11685
- } else if (edge.callType === "Call") {
11686
- candidates = candidates.filter(
11687
- (candidate) => PHP_FUNCTION_SYMBOL_CHUNK_TYPES.has(candidate.kind)
11688
- );
12526
+ if (pendingChunks.length > 0) {
12527
+ onProgress?.({
12528
+ phase: "embedding",
12529
+ filesProcessed: unchangedFilePaths.size + processedChangedFiles,
12530
+ totalFiles: files.length,
12531
+ chunksProcessed: stats.indexedChunks,
12532
+ totalChunks: stats.totalChunks
12533
+ });
12534
+ const batchResult = await this.processPendingChunkBatch(pendingChunks, {
12535
+ store,
12536
+ provider,
12537
+ invertedIndex,
12538
+ database,
12539
+ configuredProviderInfo,
12540
+ queue,
12541
+ providerRateLimits,
12542
+ rateLimitState,
12543
+ failedState: failedProcessing.state,
12544
+ attemptCounts: /* @__PURE__ */ new Map(),
12545
+ forceReembed: forceScopedReembed,
12546
+ reuseCachedEmbeddings: true,
12547
+ incrementRepeatedFailures: true,
12548
+ onProgress: (batchProgress) => onProgress?.({
12549
+ phase: "embedding",
12550
+ filesProcessed: unchangedFilePaths.size + processedChangedFiles,
12551
+ totalFiles: files.length,
12552
+ chunksProcessed: stats.indexedChunks + batchProgress.indexedChunks,
12553
+ totalChunks: stats.totalChunks
12554
+ })
12555
+ });
12556
+ stats.indexedChunks += batchResult.indexedChunks;
12557
+ stats.failedChunks += batchResult.failedChunks;
12558
+ stats.tokensUsed += batchResult.tokensUsed;
12559
+ for (const chunkId of batchResult.failedChunkIds) {
12560
+ failedChunkIds.add(chunkId);
12561
+ if (forceScopedReembed) {
12562
+ failedForcedChunkIds.add(chunkId);
11689
12563
  }
11690
12564
  }
11691
- candidates = candidates?.filter(
11692
- (symbol) => isCompatibleCFamilyCallTarget(fileLanguage, edge.callType, symbol.kind)
11693
- );
11694
- if (candidates && candidates.length === 1) {
11695
- database.resolveCallEdge(edge.id, candidates[0].id);
12565
+ }
12566
+ }
12567
+ const retryableFailedChunks = this.iterateLatestFailedChunks(
12568
+ failedProcessing.latestById,
12569
+ scopedRoots,
12570
+ shouldRetryFailedPath,
12571
+ maxChunkTokens
12572
+ );
12573
+ for (const retryBatch of iterateOrderedFileBatches(
12574
+ retryableFailedChunks,
12575
+ ({ chunk }) => Buffer.byteLength(chunk.content, "utf-8"),
12576
+ this.fileBatchLimits
12577
+ )) {
12578
+ const pendingChunks = retryBatch.map(({ chunk }) => chunk);
12579
+ const attemptCounts = new Map(retryBatch.map(({ chunk, attemptCount }) => [chunk.id, attemptCount]));
12580
+ for (const chunk of pendingChunks) {
12581
+ currentChunkIds.add(chunk.id);
12582
+ if (existingChunks.has(chunk.id)) {
12583
+ retryableChunksWithExistingData.add(chunk.id);
12584
+ }
12585
+ }
12586
+ stats.totalChunks += pendingChunks.length;
12587
+ onProgress?.({
12588
+ phase: "embedding",
12589
+ filesProcessed: files.length,
12590
+ totalFiles: files.length,
12591
+ chunksProcessed: stats.indexedChunks,
12592
+ totalChunks: stats.totalChunks
12593
+ });
12594
+ const batchResult = await this.processPendingChunkBatch(pendingChunks, {
12595
+ store,
12596
+ provider,
12597
+ invertedIndex,
12598
+ database,
12599
+ configuredProviderInfo,
12600
+ queue,
12601
+ providerRateLimits,
12602
+ rateLimitState,
12603
+ failedState: failedProcessing.state,
12604
+ attemptCounts,
12605
+ forceReembed: forceScopedReembed,
12606
+ reuseCachedEmbeddings: true,
12607
+ incrementRepeatedFailures: true,
12608
+ onProgress: (batchProgress) => onProgress?.({
12609
+ phase: "embedding",
12610
+ filesProcessed: files.length,
12611
+ totalFiles: files.length,
12612
+ chunksProcessed: stats.indexedChunks + batchProgress.indexedChunks,
12613
+ totalChunks: stats.totalChunks
12614
+ })
12615
+ });
12616
+ stats.indexedChunks += batchResult.indexedChunks;
12617
+ stats.failedChunks += batchResult.failedChunks;
12618
+ stats.tokensUsed += batchResult.tokensUsed;
12619
+ for (const chunkId of batchResult.failedChunkIds) {
12620
+ failedChunkIds.add(chunkId);
12621
+ if (forceScopedReembed) {
12622
+ failedForcedChunkIds.add(chunkId);
11696
12623
  }
11697
12624
  }
11698
12625
  }
11699
- }
11700
- for (const filePath of unchangedFilePaths) {
11701
- const existingSymbols = database.getSymbolsByFile(filePath);
11702
- for (const sym of existingSymbols) {
11703
- if (!restrictExistingChunksToBranch || previousBranchSymbolIdSet.has(sym.id)) {
11704
- allSymbolIds.add(sym.id);
12626
+ const removedChunkIds = [];
12627
+ for (const [chunkId] of existingChunks) {
12628
+ if (!currentChunkIds.has(chunkId)) {
12629
+ removedChunkIds.push(chunkId);
11705
12630
  }
11706
12631
  }
11707
- }
11708
- const removedChunkIds = [];
11709
- for (const [chunkId] of existingChunks) {
11710
- if (!currentChunkIds.has(chunkId)) {
11711
- removedChunkIds.push(chunkId);
12632
+ const removedCount = removedChunkIds.length;
12633
+ stats.existingChunks = currentChunkIds.size - stats.totalChunks;
12634
+ stats.removedChunks = removedCount;
12635
+ this.logger.recordChunksProcessed(currentChunkIds.size);
12636
+ this.logger.recordChunksRemoved(removedCount);
12637
+ this.logger.info("Chunk analysis complete", {
12638
+ pending: stats.totalChunks,
12639
+ existing: stats.existingChunks,
12640
+ removed: removedCount
12641
+ });
12642
+ if (stats.totalChunks === 0 && removedCount === 0) {
12643
+ const removedStoredChunks = this.replaceBranchCatalog(
12644
+ store,
12645
+ invertedIndex,
12646
+ database,
12647
+ branchCatalogKey,
12648
+ previousBranchChunkIds,
12649
+ Array.from(currentChunkIds),
12650
+ previousBranchSymbolIds,
12651
+ Array.from(allSymbolIds)
12652
+ );
12653
+ const vectorPath = path19.join(this.indexPath, "vectors");
12654
+ const shouldFingerprintLegacyPair = !store.hasFingerprint() && (0, import_fs12.existsSync)(vectorPath) && (0, import_fs12.existsSync)(`${vectorPath}.meta.json`);
12655
+ if (backfilledBlameMetadata || shouldFingerprintLegacyPair || removedStoredChunks) {
12656
+ store.save();
12657
+ }
12658
+ if (removedStoredChunks) {
12659
+ this.saveInvertedIndex(invertedIndex);
12660
+ }
12661
+ if (scopedRoots) {
12662
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12663
+ } else {
12664
+ this.fileHashCache = currentFileHashes;
12665
+ this.saveFileHashCache();
12666
+ }
12667
+ this.finalizeFailedBatchWriteState(failedProcessing.state);
12668
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12669
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12670
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
12671
+ this.saveBranchCommit(database, indexedCommit);
12672
+ this.saveIndexMetadata(configuredProviderInfo);
12673
+ this.indexCompatibility = { compatible: true };
12674
+ database.commitWriteTransaction();
12675
+ writeTransactionActive = false;
12676
+ stats.durationMs = Date.now() - startTime;
12677
+ onProgress?.({
12678
+ phase: "complete",
12679
+ filesProcessed: files.length,
12680
+ totalFiles: files.length,
12681
+ chunksProcessed: 0,
12682
+ totalChunks: 0
12683
+ });
12684
+ return stats;
11712
12685
  }
11713
- }
11714
- const removedCount = removedChunkIds.length;
11715
- stats.totalChunks = pendingChunks.length;
11716
- stats.existingChunks = currentChunkIds.size - pendingChunks.length;
11717
- stats.removedChunks = removedCount;
11718
- this.logger.recordChunksProcessed(currentChunkIds.size);
11719
- this.logger.recordChunksRemoved(removedCount);
11720
- this.logger.info("Chunk analysis complete", {
11721
- pending: pendingChunks.length,
11722
- existing: stats.existingChunks,
11723
- removed: removedCount
11724
- });
11725
- if (pendingChunks.length === 0 && removedCount === 0) {
11726
- const removedStoredChunks = this.replaceBranchCatalog(
11727
- store,
11728
- invertedIndex,
11729
- database,
11730
- branchCatalogKey,
11731
- previousBranchChunkIds,
11732
- Array.from(currentChunkIds),
11733
- previousBranchSymbolIds,
11734
- Array.from(allSymbolIds)
11735
- );
11736
- const vectorPath = path18.join(this.indexPath, "vectors");
11737
- const shouldFingerprintLegacyPair = !store.hasFingerprint() && (0, import_fs12.existsSync)(vectorPath) && (0, import_fs12.existsSync)(`${vectorPath}.meta.json`);
11738
- if (backfilledBlameMetadata || shouldFingerprintLegacyPair || removedStoredChunks) {
12686
+ if (stats.totalChunks === 0) {
12687
+ this.replaceBranchCatalog(
12688
+ store,
12689
+ invertedIndex,
12690
+ database,
12691
+ branchCatalogKey,
12692
+ previousBranchChunkIds,
12693
+ Array.from(currentChunkIds),
12694
+ previousBranchSymbolIds,
12695
+ Array.from(allSymbolIds)
12696
+ );
11739
12697
  store.save();
11740
- }
11741
- if (removedStoredChunks) {
11742
12698
  this.saveInvertedIndex(invertedIndex);
12699
+ if (scopedRoots) {
12700
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12701
+ } else {
12702
+ this.fileHashCache = currentFileHashes;
12703
+ this.saveFileHashCache();
12704
+ }
12705
+ this.finalizeFailedBatchWriteState(failedProcessing.state);
12706
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12707
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12708
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
12709
+ this.saveBranchCommit(database, indexedCommit);
12710
+ this.saveIndexMetadata(configuredProviderInfo);
12711
+ this.indexCompatibility = { compatible: true };
12712
+ database.commitWriteTransaction();
12713
+ writeTransactionActive = false;
12714
+ stats.durationMs = Date.now() - startTime;
12715
+ onProgress?.({
12716
+ phase: "complete",
12717
+ filesProcessed: files.length,
12718
+ totalFiles: files.length,
12719
+ chunksProcessed: 0,
12720
+ totalChunks: 0
12721
+ });
12722
+ return stats;
11743
12723
  }
11744
- if (scopedRoots) {
11745
- this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
11746
- this.clearScopedFailedBatches(scopedRoots);
11747
- } else {
11748
- this.fileHashCache = currentFileHashes;
11749
- this.saveFileHashCache();
11750
- this.saveFailedBatches([]);
11751
- }
11752
- database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
11753
- database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
11754
- database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
11755
- this.saveBranchCommit(database, indexedCommit);
11756
- this.saveIndexMetadata(configuredProviderInfo);
11757
- this.indexCompatibility = { compatible: true };
11758
- stats.durationMs = Date.now() - startTime;
11759
12724
  onProgress?.({
11760
- phase: "complete",
12725
+ phase: "storing",
11761
12726
  filesProcessed: files.length,
11762
12727
  totalFiles: files.length,
11763
- chunksProcessed: 0,
11764
- totalChunks: 0
12728
+ chunksProcessed: stats.indexedChunks,
12729
+ totalChunks: stats.totalChunks
12730
+ });
12731
+ const branchChunkIds = Array.from(currentChunkIds).filter((chunkId) => {
12732
+ const isNewlyFailed = failedChunkIds.has(chunkId) && !retryableChunksWithExistingData.has(chunkId);
12733
+ const isForcedFailed = forceScopedReembed && failedForcedChunkIds.has(chunkId);
12734
+ return !isNewlyFailed && !isForcedFailed;
11765
12735
  });
11766
- return stats;
11767
- }
11768
- if (pendingChunks.length === 0) {
11769
12736
  this.replaceBranchCatalog(
11770
12737
  store,
11771
12738
  invertedIndex,
11772
12739
  database,
11773
12740
  branchCatalogKey,
11774
12741
  previousBranchChunkIds,
11775
- Array.from(currentChunkIds),
12742
+ branchChunkIds,
11776
12743
  previousBranchSymbolIds,
11777
12744
  Array.from(allSymbolIds)
11778
12745
  );
11779
12746
  store.save();
11780
12747
  this.saveInvertedIndex(invertedIndex);
11781
- if (scopedRoots) {
11782
- this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
11783
- this.clearScopedFailedBatches(scopedRoots);
11784
- } else {
11785
- this.fileHashCache = currentFileHashes;
11786
- this.saveFileHashCache();
11787
- this.saveFailedBatches([]);
11788
- }
11789
- database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
11790
- database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
11791
- database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
11792
- this.saveBranchCommit(database, indexedCommit);
11793
- this.saveIndexMetadata(configuredProviderInfo);
11794
- this.indexCompatibility = { compatible: true };
11795
- stats.durationMs = Date.now() - startTime;
11796
- onProgress?.({
11797
- phase: "complete",
11798
- filesProcessed: files.length,
11799
- totalFiles: files.length,
11800
- chunksProcessed: 0,
11801
- totalChunks: 0
11802
- });
11803
- return stats;
11804
- }
11805
- onProgress?.({
11806
- phase: "embedding",
11807
- filesProcessed: files.length,
11808
- totalFiles: files.length,
11809
- chunksProcessed: 0,
11810
- totalChunks: pendingChunks.length
11811
- });
11812
- const allContentHashes = pendingChunks.map((c) => c.contentHash);
11813
- const missingHashes = new Set(database.getMissingEmbeddings(allContentHashes));
11814
- const forcedReembedChunkIds = forceScopedReembed ? new Set(pendingChunks.map((chunk) => chunk.id)) : /* @__PURE__ */ new Set();
11815
- const chunksNeedingEmbedding = pendingChunks.filter((c) => forcedReembedChunkIds.has(c.id) || missingHashes.has(c.contentHash));
11816
- const chunksWithExistingEmbedding = pendingChunks.filter((c) => !forcedReembedChunkIds.has(c.id) && !missingHashes.has(c.contentHash));
11817
- this.logger.cache("info", "Embedding cache lookup", {
11818
- needsEmbedding: chunksNeedingEmbedding.length,
11819
- fromCache: chunksWithExistingEmbedding.length
11820
- });
11821
- this.logger.recordChunksFromCache(chunksWithExistingEmbedding.length);
11822
- for (const chunk of chunksWithExistingEmbedding) {
11823
- const embeddingBuffer = database.getEmbedding(chunk.contentHash);
11824
- if (embeddingBuffer) {
11825
- const vector = bufferToFloat32Array(embeddingBuffer);
11826
- store.add(chunk.id, Array.from(vector), chunk.metadata);
11827
- invertedIndex.removeChunk(chunk.id);
11828
- invertedIndex.addChunk(chunk.id, chunk.content);
11829
- stats.indexedChunks++;
11830
- }
11831
- }
11832
- const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
11833
- const queue = new PQueue({
11834
- concurrency: providerRateLimits.concurrency,
11835
- interval: providerRateLimits.intervalMs,
11836
- intervalCap: providerRateLimits.concurrency
11837
- });
11838
- const pendingChunksById = new Map(chunksNeedingEmbedding.map((chunk) => [chunk.id, chunk]));
11839
- const embeddingPartsByChunk = /* @__PURE__ */ new Map();
11840
- const completedChunkIds = /* @__PURE__ */ new Set();
11841
- const failedChunkIds = /* @__PURE__ */ new Set();
11842
- const requestBatches = createPendingEmbeddingRequestBatches(
11843
- chunksNeedingEmbedding,
11844
- getDynamicBatchOptions(configuredProviderInfo)
11845
- );
11846
- let rateLimitBackoffMs = 0;
11847
- for (const requestBatch of requestBatches) {
11848
- queue.add(async () => {
11849
- if (rateLimitBackoffMs > 0) {
11850
- await new Promise((resolve13) => setTimeout(resolve13, rateLimitBackoffMs));
11851
- }
11852
- try {
11853
- const result = await pRetry(
11854
- async () => {
11855
- const texts = requestBatch.map((request) => request.text);
11856
- return provider.embedBatch(texts);
11857
- },
11858
- {
11859
- retries: this.config.indexing.retries,
11860
- minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
11861
- maxTimeout: providerRateLimits.maxRetryMs,
11862
- factor: 2,
11863
- shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
11864
- onFailedAttempt: (error) => {
11865
- const message = getErrorMessage4(error);
11866
- if (isRateLimitError(error)) {
11867
- rateLimitBackoffMs = Math.min(providerRateLimits.maxRetryMs, (rateLimitBackoffMs || providerRateLimits.minRetryMs) * 2);
11868
- this.logger.embedding("warn", `Rate limited, backing off`, {
11869
- attempt: error.attemptNumber,
11870
- retriesLeft: error.retriesLeft,
11871
- backoffMs: rateLimitBackoffMs
11872
- });
11873
- } else {
11874
- this.logger.embedding("error", `Embedding batch failed`, {
11875
- attempt: error.attemptNumber,
11876
- error: message
11877
- });
11878
- }
11879
- }
11880
- }
11881
- );
11882
- if (rateLimitBackoffMs > 0) {
11883
- rateLimitBackoffMs = Math.max(0, rateLimitBackoffMs - 2e3);
11884
- }
11885
- const touchedChunkIds = /* @__PURE__ */ new Set();
11886
- requestBatch.forEach((request, idx) => {
11887
- if (failedChunkIds.has(request.chunk.id) || completedChunkIds.has(request.chunk.id)) {
11888
- return;
11889
- }
11890
- const vector = result.embeddings[idx];
11891
- if (!vector) {
11892
- throw new Error(`Embedding API returned too few vectors for chunk ${request.chunk.id}`);
11893
- }
11894
- const parts = embeddingPartsByChunk.get(request.chunk.id) ?? [];
11895
- parts[request.partIndex] = {
11896
- vector,
11897
- tokenCount: request.tokenCount
11898
- };
11899
- embeddingPartsByChunk.set(request.chunk.id, parts);
11900
- touchedChunkIds.add(request.chunk.id);
11901
- });
11902
- const pooledResults = [];
11903
- for (const chunkId of touchedChunkIds) {
11904
- if (failedChunkIds.has(chunkId) || completedChunkIds.has(chunkId)) {
11905
- continue;
11906
- }
11907
- const chunk = pendingChunksById.get(chunkId);
11908
- if (!chunk) {
11909
- continue;
11910
- }
11911
- const parts = embeddingPartsByChunk.get(chunk.id) ?? [];
11912
- if (!hasAllEmbeddingParts(parts, chunk.texts.length)) {
11913
- continue;
11914
- }
11915
- const orderedParts = parts;
11916
- pooledResults.push({
11917
- chunk,
11918
- vector: poolEmbeddingVectors(
11919
- orderedParts.map((part) => part.vector),
11920
- orderedParts.map((part) => part.tokenCount)
11921
- )
11922
- });
11923
- }
11924
- if (pooledResults.length > 0) {
11925
- const items = pooledResults.map(({ chunk, vector }) => ({
11926
- id: chunk.id,
11927
- vector,
11928
- metadata: chunk.metadata
11929
- }));
11930
- store.addBatch(items);
11931
- const embeddingBatchItems = pooledResults.map(({ chunk, vector }) => ({
11932
- contentHash: chunk.contentHash,
11933
- embedding: float32ArrayToBuffer(vector),
11934
- chunkText: chunk.storageText,
11935
- model: configuredProviderInfo.modelInfo.model
11936
- }));
11937
- try {
11938
- database.upsertEmbeddingsBatch(embeddingBatchItems);
11939
- } catch (dbError) {
11940
- this.rebuildVectorStoreExcludingChunkIds(
11941
- store,
11942
- database,
11943
- pooledResults.map(({ chunk }) => chunk.id)
11944
- );
11945
- throw dbError;
11946
- }
11947
- for (const { chunk } of pooledResults) {
11948
- invertedIndex.removeChunk(chunk.id);
11949
- invertedIndex.addChunk(chunk.id, chunk.content);
11950
- completedChunkIds.add(chunk.id);
11951
- embeddingPartsByChunk.delete(chunk.id);
11952
- }
11953
- stats.indexedChunks += pooledResults.length;
11954
- this.logger.recordChunksEmbedded(pooledResults.length);
11955
- }
11956
- stats.tokensUsed += result.totalTokensUsed;
11957
- this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
11958
- this.logger.embedding("debug", `Embedded batch`, {
11959
- batchSize: pooledResults.length,
11960
- requestCount: requestBatch.length,
11961
- tokens: result.totalTokensUsed
11962
- });
11963
- onProgress?.({
11964
- phase: "embedding",
11965
- filesProcessed: files.length,
11966
- totalFiles: files.length,
11967
- chunksProcessed: stats.indexedChunks,
11968
- totalChunks: pendingChunks.length
11969
- });
11970
- } catch (error) {
11971
- const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id));
11972
- const failureMessage = getErrorMessage4(error);
11973
- const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
11974
- for (const chunk of failedChunks) {
11975
- if (!failedChunkIds.has(chunk.id)) {
11976
- failedChunkIds.add(chunk.id);
11977
- stats.failedChunks += 1;
11978
- }
11979
- if (forceScopedReembed) {
11980
- failedForcedChunkIds.add(chunk.id);
11981
- }
11982
- embeddingPartsByChunk.delete(chunk.id);
11983
- const existingFailedBatchIndex = failedBatchesForCurrentRun.findIndex(
11984
- (failedBatch2) => failedBatch2.chunks[0]?.id === chunk.id
11985
- );
11986
- const existingFailedBatch = existingFailedBatchIndex === -1 ? void 0 : failedBatchesForCurrentRun[existingFailedBatchIndex];
11987
- const failedBatch = {
11988
- chunks: [chunk],
11989
- error: failureMessage,
11990
- attemptCount: (existingFailedBatch?.attemptCount ?? retryableFailedAttemptCounts.get(chunk.id) ?? 0) + 1,
11991
- lastAttempt: failureTimestamp
11992
- };
11993
- if (existingFailedBatchIndex === -1) {
11994
- failedBatchesForCurrentRun.push(failedBatch);
11995
- } else {
11996
- failedBatchesForCurrentRun[existingFailedBatchIndex] = failedBatch;
11997
- }
11998
- }
11999
- this.logger.recordEmbeddingError();
12000
- this.logger.embedding("error", `Failed to embed batch after retries`, {
12001
- batchSize: failedChunks.length,
12002
- requestCount: requestBatch.length,
12003
- error: failureMessage
12748
+ if (scopedRoots) {
12749
+ this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12750
+ } else {
12751
+ this.fileHashCache = currentFileHashes;
12752
+ this.saveFileHashCache();
12753
+ }
12754
+ this.finalizeFailedBatchWriteState(failedProcessing.state);
12755
+ database.commitWriteTransaction();
12756
+ writeTransactionActive = false;
12757
+ if (this.config.indexing.autoGc && stats.removedChunks > 0) {
12758
+ const gcReset = await this.maybeRunOrphanGc();
12759
+ if (gcReset) {
12760
+ stats.durationMs = Date.now() - startTime;
12761
+ stats.warning = gcReset.warning;
12762
+ stats.resetCorruptedIndex = true;
12763
+ this.logger.recordIndexingEnd();
12764
+ this.logger.warn("Indexing ended after resetting corrupted local index during automatic GC", {
12765
+ files: stats.totalFiles,
12766
+ indexed: stats.indexedChunks,
12767
+ existing: stats.existingChunks,
12768
+ removed: stats.removedChunks,
12769
+ failed: stats.failedChunks,
12770
+ tokens: stats.tokensUsed,
12771
+ durationMs: stats.durationMs
12004
12772
  });
12773
+ return stats;
12005
12774
  }
12775
+ }
12776
+ stats.durationMs = Date.now() - startTime;
12777
+ if (forceScopedReembed && failedForcedChunkIds.size === 0) {
12778
+ database.deleteMetadata(this.getProjectForceReembedMetadataKey());
12779
+ }
12780
+ database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12781
+ database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12782
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
12783
+ this.saveBranchCommit(database, indexedCommit);
12784
+ this.saveIndexMetadata(configuredProviderInfo);
12785
+ this.indexCompatibility = { compatible: true };
12786
+ this.logger.recordIndexingEnd();
12787
+ this.logger.info("Indexing complete", {
12788
+ files: stats.totalFiles,
12789
+ indexed: stats.indexedChunks,
12790
+ existing: stats.existingChunks,
12791
+ removed: stats.removedChunks,
12792
+ failed: stats.failedChunks,
12793
+ tokens: stats.tokensUsed,
12794
+ durationMs: stats.durationMs
12006
12795
  });
12007
- }
12008
- await queue.onIdle();
12009
- if (scopedRoots) {
12010
- this.saveScopedFailedBatches(coalesceFailedBatches(failedBatchesForCurrentRun), scopedRoots);
12011
- } else {
12012
- this.saveFailedBatches(coalesceFailedBatches(failedBatchesForCurrentRun));
12013
- }
12014
- onProgress?.({
12015
- phase: "storing",
12016
- filesProcessed: files.length,
12017
- totalFiles: files.length,
12018
- chunksProcessed: stats.indexedChunks,
12019
- totalChunks: pendingChunks.length
12020
- });
12021
- const branchChunkIds = Array.from(currentChunkIds).filter(
12022
- (chunkId) => {
12023
- const isNewlyFailed = failedChunkIds.has(chunkId) && !retryableChunksWithExistingData.has(chunkId);
12024
- const isForcedFailed = forceScopedReembed && failedForcedChunkIds.has(chunkId);
12025
- return !isNewlyFailed && !isForcedFailed;
12796
+ if (stats.failedChunks > 0) {
12797
+ stats.failedBatchesPath = this.failedBatchesPath;
12026
12798
  }
12027
- );
12028
- this.replaceBranchCatalog(
12029
- store,
12030
- invertedIndex,
12031
- database,
12032
- branchCatalogKey,
12033
- previousBranchChunkIds,
12034
- branchChunkIds,
12035
- previousBranchSymbolIds,
12036
- Array.from(allSymbolIds)
12037
- );
12038
- store.save();
12039
- this.saveInvertedIndex(invertedIndex);
12040
- if (scopedRoots) {
12041
- this.replaceScopedFileHashCache(currentFileHashes, scopedRoots);
12042
- } else {
12043
- this.fileHashCache = currentFileHashes;
12044
- this.saveFileHashCache();
12045
- }
12046
- if (this.config.indexing.autoGc && stats.removedChunks > 0) {
12047
- const gcReset = await this.maybeRunOrphanGc();
12048
- if (gcReset) {
12049
- stats.durationMs = Date.now() - startTime;
12050
- stats.warning = gcReset.warning;
12051
- stats.resetCorruptedIndex = true;
12052
- this.logger.recordIndexingEnd();
12053
- this.logger.warn("Indexing ended after resetting corrupted local index during automatic GC", {
12054
- files: stats.totalFiles,
12055
- indexed: stats.indexedChunks,
12056
- existing: stats.existingChunks,
12057
- removed: stats.removedChunks,
12058
- failed: stats.failedChunks,
12059
- tokens: stats.tokensUsed,
12060
- durationMs: stats.durationMs
12061
- });
12062
- return stats;
12799
+ onProgress?.({
12800
+ phase: "complete",
12801
+ filesProcessed: files.length,
12802
+ totalFiles: files.length,
12803
+ chunksProcessed: stats.indexedChunks,
12804
+ totalChunks: stats.totalChunks
12805
+ });
12806
+ return stats;
12807
+ } catch (error) {
12808
+ failedProcessing.state.writer.cleanup();
12809
+ if (writeTransactionActive) {
12810
+ try {
12811
+ database.rollbackWriteTransaction();
12812
+ } catch (rollbackError) {
12813
+ this.logger.error("Failed to roll back indexing database transaction", {
12814
+ error: getErrorMessage4(rollbackError)
12815
+ });
12816
+ }
12063
12817
  }
12818
+ throw error;
12064
12819
  }
12065
- stats.durationMs = Date.now() - startTime;
12066
- if (forceScopedReembed && failedForcedChunkIds.size === 0) {
12067
- database.deleteMetadata(this.getProjectForceReembedMetadataKey());
12068
- }
12069
- database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
12070
- database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
12071
- database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
12072
- this.saveBranchCommit(database, indexedCommit);
12073
- this.saveIndexMetadata(configuredProviderInfo);
12074
- this.indexCompatibility = { compatible: true };
12075
- this.logger.recordIndexingEnd();
12076
- this.logger.info("Indexing complete", {
12077
- files: stats.totalFiles,
12078
- indexed: stats.indexedChunks,
12079
- existing: stats.existingChunks,
12080
- removed: stats.removedChunks,
12081
- failed: stats.failedChunks,
12082
- tokens: stats.tokensUsed,
12083
- durationMs: stats.durationMs
12084
- });
12085
- if (stats.failedChunks > 0) {
12086
- stats.failedBatchesPath = this.failedBatchesPath;
12087
- }
12088
- onProgress?.({
12089
- phase: "complete",
12090
- filesProcessed: files.length,
12091
- totalFiles: files.length,
12092
- chunksProcessed: stats.indexedChunks,
12093
- totalChunks: pendingChunks.length
12094
- });
12095
- return stats;
12096
12820
  }
12097
12821
  async getQueryEmbedding(query, provider) {
12098
12822
  const now2 = Date.now();
@@ -12330,10 +13054,31 @@ var Indexer = class _Indexer {
12330
13054
  const baseFiltered = tiered.filter(
12331
13055
  (r) => matchesSearchFilters(r, options, this.config.search.minScore, this.projectRoot)
12332
13056
  );
12333
- const implementationOnly = baseFiltered.filter(
13057
+ let communityRanked = baseFiltered;
13058
+ if (this.config.search.communityBoost > 0) {
13059
+ try {
13060
+ const sameCommunityCandidateIds = resolveSameCommunityCandidateIds(
13061
+ query,
13062
+ baseFiltered,
13063
+ database,
13064
+ this.getBranchCatalogKeys()
13065
+ );
13066
+ communityRanked = applyCommunityBoost(
13067
+ baseFiltered,
13068
+ sameCommunityCandidateIds,
13069
+ this.config.search.communityBoost
13070
+ );
13071
+ } catch (error) {
13072
+ this.logger.search("debug", "Community-aware ranking unavailable; using existing ranking", {
13073
+ query,
13074
+ error: getErrorMessage4(error)
13075
+ });
13076
+ }
13077
+ }
13078
+ const implementationOnly = communityRanked.filter(
12334
13079
  (r) => isLikelyImplementationPath2(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
12335
13080
  );
12336
- const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
13081
+ const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : communityRanked).slice(0, maxResults);
12337
13082
  const identifierFallback = !options?.definitionIntent && filtered.length === 0 && identifierHints.length > 0 ? buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, maxResults, union, true).filter((r) => matchesSearchFilters(r, options, this.config.search.minScore, this.projectRoot)).slice(0, maxResults) : [];
12338
13083
  const finalResults = filtered.length > 0 ? filtered : identifierFallback;
12339
13084
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
@@ -12550,7 +13295,7 @@ var Indexer = class _Indexer {
12550
13295
  this.saveFileHashCache();
12551
13296
  database.clearAllIndexedData();
12552
13297
  this.deleteBranchCommitMetadata(database, clearedBranchKeys2);
12553
- this.saveFailedBatches([]);
13298
+ this.clearFailedBatchState();
12554
13299
  database.deleteMetadata("index.version");
12555
13300
  database.deleteMetadata("index.pathStorageVersion");
12556
13301
  database.deleteMetadata("index.embeddingProvider");
@@ -12681,7 +13426,7 @@ var Indexer = class _Indexer {
12681
13426
  gcOrphanSymbols: 0,
12682
13427
  gcOrphanCallEdges: 0,
12683
13428
  resetCorruptedIndex: true,
12684
- warning: this.getCorruptedIndexWarning(path18.join(this.indexPath, "codebase.db"))
13429
+ warning: this.getCorruptedIndexWarning(path19.join(this.indexPath, "codebase.db"))
12685
13430
  };
12686
13431
  }
12687
13432
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -12711,180 +13456,94 @@ var Indexer = class _Indexer {
12711
13456
  const maxChunkTokens = getSafeEmbeddingChunkTokenLimit(configuredProviderInfo);
12712
13457
  const providerRateLimits = this.getProviderRateLimits(configuredProviderInfo.provider);
12713
13458
  const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
12714
- const { scoped: scopedFailedBatches, retained: retainedFailedBatches } = roots ? this.partitionFailedBatches(roots, maxChunkTokens) : { scoped: this.loadFailedBatches(maxChunkTokens), retained: [] };
12715
- const failedBatches = scopedFailedBatches;
12716
- if (failedBatches.length === 0) {
13459
+ const failedProcessing = this.prepareFailedBatchProcessing(roots, () => true);
13460
+ if (failedProcessing.latestById.size === 0) {
13461
+ this.finalizeFailedBatchWriteState(failedProcessing.state);
12717
13462
  return { succeeded: 0, failed: 0, remaining: 0 };
12718
13463
  }
13464
+ const queue = new PQueue({ concurrency: 1 });
13465
+ const rateLimitState = { backoffMs: 0 };
12719
13466
  let succeeded = 0;
12720
13467
  let failed = 0;
12721
- const stillFailing = [];
12722
- for (const batch of failedBatches) {
12723
- const batchChunksById = new Map(batch.chunks.map((chunk) => [chunk.id, chunk]));
12724
- const embeddingPartsByChunk = /* @__PURE__ */ new Map();
12725
- const completedChunkIds = /* @__PURE__ */ new Set();
12726
- const failedChunkIds = /* @__PURE__ */ new Set();
12727
- const failedChunksForBatch = /* @__PURE__ */ new Map();
12728
- const pooledResults = [];
12729
- try {
12730
- const requestBatches = createPendingEmbeddingRequestBatches(
12731
- batch.chunks,
12732
- getDynamicBatchOptions(configuredProviderInfo)
12733
- );
12734
- for (const requestBatch of requestBatches) {
12735
- try {
12736
- const result = await pRetry(
12737
- async () => {
12738
- const texts = requestBatch.map((request) => request.text);
12739
- return provider.embedBatch(texts);
12740
- },
12741
- {
12742
- retries: this.config.indexing.retries,
12743
- minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
12744
- maxTimeout: providerRateLimits.maxRetryMs,
12745
- factor: 2,
12746
- shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError)
12747
- }
12748
- );
12749
- const touchedChunkIds = /* @__PURE__ */ new Set();
12750
- requestBatch.forEach((request, idx) => {
12751
- if (failedChunkIds.has(request.chunk.id) || completedChunkIds.has(request.chunk.id)) {
12752
- return;
12753
- }
12754
- const vector = result.embeddings[idx];
12755
- if (!vector) {
12756
- throw new Error(`Embedding API returned too few vectors for chunk ${request.chunk.id}`);
12757
- }
12758
- const parts = embeddingPartsByChunk.get(request.chunk.id) ?? [];
12759
- parts[request.partIndex] = {
12760
- vector,
12761
- tokenCount: request.tokenCount
12762
- };
12763
- embeddingPartsByChunk.set(request.chunk.id, parts);
12764
- touchedChunkIds.add(request.chunk.id);
12765
- });
12766
- for (const chunkId of touchedChunkIds) {
12767
- if (failedChunkIds.has(chunkId) || completedChunkIds.has(chunkId)) {
12768
- continue;
12769
- }
12770
- const chunk = batchChunksById.get(chunkId);
12771
- if (!chunk) {
12772
- continue;
12773
- }
12774
- const parts = embeddingPartsByChunk.get(chunk.id) ?? [];
12775
- if (!hasAllEmbeddingParts(parts, chunk.texts.length)) {
12776
- continue;
12777
- }
12778
- const orderedParts = parts;
12779
- pooledResults.push({
12780
- chunk,
12781
- vector: poolEmbeddingVectors(
12782
- orderedParts.map((part) => part.vector),
12783
- orderedParts.map((part) => part.tokenCount)
12784
- )
12785
- });
12786
- }
12787
- this.logger.recordEmbeddingApiCall(result.totalTokensUsed);
12788
- } catch (error) {
12789
- const failureMessage = String(error);
12790
- const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
12791
- const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id) && !failedChunkIds.has(chunk.id));
12792
- for (const chunk of failedChunks) {
12793
- failedChunkIds.add(chunk.id);
12794
- embeddingPartsByChunk.delete(chunk.id);
12795
- failedChunksForBatch.set(chunk.id, {
12796
- chunks: [chunk],
12797
- attemptCount: batch.attemptCount + 1,
12798
- lastAttempt: failureTimestamp,
12799
- error: failureMessage
12800
- });
12801
- }
12802
- failed += failedChunks.length;
12803
- this.logger.recordEmbeddingError();
12804
- }
12805
- }
12806
- const successfulResults = pooledResults.filter(({ chunk }) => !failedChunkIds.has(chunk.id));
12807
- const items = successfulResults.map(({ chunk, vector }) => ({
12808
- id: chunk.id,
12809
- vector,
12810
- metadata: chunk.metadata
12811
- }));
12812
- if (items.length > 0) {
12813
- store.addBatch(items);
12814
- }
12815
- if (successfulResults.length > 0) {
12816
- try {
12817
- database.upsertEmbeddingsBatch(
12818
- successfulResults.map(({ chunk, vector }) => ({
12819
- contentHash: chunk.contentHash,
12820
- embedding: float32ArrayToBuffer(vector),
12821
- chunkText: chunk.storageText,
12822
- model: configuredProviderInfo.modelInfo.model
12823
- }))
12824
- );
12825
- } catch (dbError) {
12826
- this.rebuildVectorStoreExcludingChunkIds(
12827
- store,
12828
- database,
12829
- successfulResults.map(({ chunk }) => chunk.id)
13468
+ try {
13469
+ const retryableChunks = this.iterateLatestFailedChunks(
13470
+ failedProcessing.latestById,
13471
+ roots,
13472
+ () => true,
13473
+ maxChunkTokens
13474
+ );
13475
+ for (const retryBatch of iterateOrderedFileBatches(
13476
+ retryableChunks,
13477
+ ({ chunk }) => Buffer.byteLength(chunk.content, "utf-8"),
13478
+ this.fileBatchLimits
13479
+ )) {
13480
+ const chunks = retryBatch.map(({ chunk }) => chunk);
13481
+ const attemptCounts = new Map(retryBatch.map(({ chunk, attemptCount }) => [chunk.id, attemptCount]));
13482
+ const batchResult = await this.processPendingChunkBatch(chunks, {
13483
+ store,
13484
+ provider,
13485
+ invertedIndex,
13486
+ database,
13487
+ configuredProviderInfo,
13488
+ queue,
13489
+ providerRateLimits,
13490
+ rateLimitState,
13491
+ failedState: failedProcessing.state,
13492
+ attemptCounts,
13493
+ forceReembed: false,
13494
+ reuseCachedEmbeddings: false,
13495
+ incrementRepeatedFailures: false,
13496
+ onSucceeded: (succeededChunks) => {
13497
+ database.addChunksToBranchBatch(
13498
+ this.getBranchCatalogKey(),
13499
+ succeededChunks.map((chunk) => chunk.id)
12830
13500
  );
12831
- throw dbError;
12832
13501
  }
12833
- }
12834
- for (const { chunk } of successfulResults) {
12835
- invertedIndex.removeChunk(chunk.id);
12836
- invertedIndex.addChunk(chunk.id, chunk.content);
12837
- completedChunkIds.add(chunk.id);
12838
- embeddingPartsByChunk.delete(chunk.id);
12839
- }
12840
- database.addChunksToBranchBatch(
12841
- this.getBranchCatalogKey(),
12842
- successfulResults.map(({ chunk }) => chunk.id)
12843
- );
12844
- this.logger.recordChunksEmbedded(successfulResults.length);
12845
- succeeded += successfulResults.length;
12846
- stillFailing.push(...failedChunksForBatch.values());
12847
- } catch (error) {
12848
- const failureMessage = getErrorMessage4(error);
12849
- const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
12850
- const unaccountedChunks = batch.chunks.filter(
12851
- (chunk) => !failedChunksForBatch.has(chunk.id) && !completedChunkIds.has(chunk.id)
12852
- );
12853
- for (const chunk of unaccountedChunks) {
12854
- failedChunksForBatch.set(chunk.id, {
12855
- chunks: [chunk],
12856
- attemptCount: batch.attemptCount + 1,
12857
- lastAttempt: failureTimestamp,
12858
- error: failureMessage
12859
- });
12860
- }
12861
- failed += unaccountedChunks.length;
12862
- this.logger.recordEmbeddingError();
12863
- stillFailing.push(...coalesceFailedBatches(Array.from(failedChunksForBatch.values())));
13502
+ });
13503
+ succeeded += batchResult.indexedChunks;
13504
+ failed += batchResult.failedChunks;
12864
13505
  }
13506
+ this.finalizeFailedBatchWriteState(failedProcessing.state);
13507
+ } catch (error) {
13508
+ failedProcessing.state.writer.cleanup();
13509
+ throw error;
12865
13510
  }
12866
- const persistedStillFailing = coalesceFailedBatches(stillFailing);
12867
- if (roots) {
12868
- this.saveFailedBatches([...retainedFailedBatches, ...persistedStillFailing]);
12869
- } else {
12870
- this.saveFailedBatches(persistedStillFailing);
12871
- }
13511
+ const remaining = this.getFailedBatchesCount();
12872
13512
  if (succeeded > 0) {
12873
13513
  store.save();
12874
13514
  this.saveInvertedIndex(invertedIndex);
12875
13515
  }
12876
- if (roots && succeeded > 0 && persistedStillFailing.length === 0 && this.hasProjectForceReembedPending()) {
13516
+ if (roots && succeeded > 0 && remaining === 0 && this.hasProjectForceReembedPending()) {
12877
13517
  database.deleteMetadata(this.getProjectForceReembedMetadataKey());
12878
13518
  this.saveIndexMetadata(configuredProviderInfo);
12879
13519
  this.indexCompatibility = { compatible: true };
12880
13520
  }
12881
- return { succeeded, failed, remaining: persistedStillFailing.length };
13521
+ return { succeeded, failed, remaining };
12882
13522
  }
12883
13523
  getFailedBatchesCount() {
12884
- if (this.config.scope === "global") {
12885
- return this.partitionFailedBatches(this.getScopedRoots()).scoped.length;
13524
+ const roots = this.config.scope === "global" ? this.getScopedRoots() : null;
13525
+ const latestById = /* @__PURE__ */ new Map();
13526
+ for (const batch of this.loadSerializedFailedBatches()) {
13527
+ for (const rawChunk of batch.chunks) {
13528
+ const filePath = getPendingChunkFilePath(rawChunk);
13529
+ if (roots && (filePath === null || !this.isFileInCurrentScope(filePath, roots))) {
13530
+ continue;
13531
+ }
13532
+ const chunkId = getPendingChunkId(rawChunk);
13533
+ if (!chunkId) {
13534
+ continue;
13535
+ }
13536
+ const existing = latestById.get(chunkId);
13537
+ if (!existing || batch.attemptCount >= existing.attemptCount) {
13538
+ latestById.set(chunkId, {
13539
+ attemptCount: batch.attemptCount,
13540
+ error: batch.error,
13541
+ lastAttempt: batch.lastAttempt
13542
+ });
13543
+ }
13544
+ }
12886
13545
  }
12887
- return this.loadFailedBatches().length;
13546
+ return new Set(Array.from(latestById.values(), getFailedBatchGroupKey)).size;
12888
13547
  }
12889
13548
  getCurrentBranch() {
12890
13549
  return this.currentBranch;
@@ -13072,9 +13731,9 @@ var Indexer = class _Indexer {
13072
13731
  this.requireReadableComponents(readIssues, "database");
13073
13732
  let shortest = [];
13074
13733
  for (const branchKey of this.getBranchCatalogKeys()) {
13075
- const path20 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
13076
- if (path20.length > 0 && (shortest.length === 0 || path20.length < shortest.length)) {
13077
- shortest = path20;
13734
+ const path21 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
13735
+ if (path21.length > 0 && (shortest.length === 0 || path21.length < shortest.length)) {
13736
+ shortest = path21;
13078
13737
  }
13079
13738
  }
13080
13739
  return shortest.map((hop) => this.resolveFilePathRecord(hop));
@@ -13122,13 +13781,13 @@ var Indexer = class _Indexer {
13122
13781
  }
13123
13782
  }
13124
13783
  if (!found) continue;
13125
- const path20 = [];
13784
+ const path21 = [];
13126
13785
  let currentSymbolId = toSymbolId;
13127
13786
  while (true) {
13128
13787
  const symbol = symbolsById.get(currentSymbolId);
13129
13788
  if (!symbol) break;
13130
13789
  const parent = parentBySymbolId.get(currentSymbolId);
13131
- path20.push({
13790
+ path21.push({
13132
13791
  symbolId: symbol.id,
13133
13792
  symbolName: symbol.name,
13134
13793
  filePath: symbol.filePath,
@@ -13138,9 +13797,9 @@ var Indexer = class _Indexer {
13138
13797
  if (!parent) break;
13139
13798
  currentSymbolId = parent.parentId;
13140
13799
  }
13141
- path20.reverse();
13142
- if (path20.length > 0 && (shortest.length === 0 || path20.length < shortest.length)) {
13143
- shortest = path20;
13800
+ path21.reverse();
13801
+ if (path21.length > 0 && (shortest.length === 0 || path21.length < shortest.length)) {
13802
+ shortest = path21;
13144
13803
  }
13145
13804
  }
13146
13805
  return shortest.map((hop) => this.resolveFilePathRecord(hop));
@@ -13159,13 +13818,13 @@ var Indexer = class _Indexer {
13159
13818
  async getSymbolsForBranch(branch) {
13160
13819
  const { database, readIssues } = await this.ensureInitialized();
13161
13820
  this.requireReadableComponents(readIssues, "database");
13162
- const resolvedBranch = branch ?? this.getBranchCatalogKey();
13821
+ const resolvedBranch = this.resolveBranchCatalogKey(branch);
13163
13822
  return database.getSymbolsForBranch(resolvedBranch).map((symbol) => this.resolveFilePathRecord(symbol));
13164
13823
  }
13165
13824
  async getSymbolsForFiles(filePaths, branch) {
13166
13825
  const { database, readIssues } = await this.ensureInitialized();
13167
13826
  this.requireReadableComponents(readIssues, "database");
13168
- const resolvedBranch = branch ?? this.getBranchCatalogKey();
13827
+ const resolvedBranch = this.resolveBranchCatalogKey(branch);
13169
13828
  const storedFilePaths = filePaths.map((filePath) => this.toStoredFilePath(filePath));
13170
13829
  return database.getSymbolsForFiles(storedFilePaths, resolvedBranch).map((symbol) => this.resolveFilePathRecord(symbol));
13171
13830
  }
@@ -13178,13 +13837,26 @@ var Indexer = class _Indexer {
13178
13837
  async detectCommunities(branch, symbolIds) {
13179
13838
  const { database, readIssues } = await this.ensureInitialized();
13180
13839
  this.requireReadableComponents(readIssues, "database");
13181
- const resolvedBranch = branch ?? this.getBranchCatalogKey();
13840
+ const resolvedBranch = this.resolveBranchCatalogKey(branch);
13182
13841
  return database.detectCommunities(resolvedBranch, symbolIds).map((entry) => this.resolveFilePathRecord(entry));
13183
13842
  }
13843
+ async detectCommunityCouplings(branch) {
13844
+ const { database, readIssues } = await this.ensureInitialized();
13845
+ this.requireReadableComponents(readIssues, "database");
13846
+ const resolvedBranch = this.resolveBranchCatalogKey(branch);
13847
+ return database.detectCommunityCouplings(resolvedBranch).map((entry) => ({
13848
+ ...entry,
13849
+ relationships: (entry.relationships ?? entry.representativeRelationships ?? []).map((relationship) => ({
13850
+ ...relationship,
13851
+ fromFilePath: this.resolveStoredFilePath(relationship.fromFilePath),
13852
+ toFilePath: this.resolveStoredFilePath(relationship.toFilePath)
13853
+ }))
13854
+ }));
13855
+ }
13184
13856
  async computeCentrality(branch) {
13185
13857
  const { database, readIssues } = await this.ensureInitialized();
13186
13858
  this.requireReadableComponents(readIssues, "database");
13187
- const resolvedBranch = branch ?? this.getBranchCatalogKey();
13859
+ const resolvedBranch = this.resolveBranchCatalogKey(branch);
13188
13860
  return database.computeCentrality(resolvedBranch).map((entry) => this.resolveFilePathRecord(entry));
13189
13861
  }
13190
13862
  async getPrImpact(opts, onPreparationProgress) {
@@ -13278,7 +13950,7 @@ var Indexer = class _Indexer {
13278
13950
  );
13279
13951
  }
13280
13952
  }
13281
- const toStoredChangedFiles = (filePaths) => filePaths.map((filePath) => this.toStoredFilePath(path18.resolve(this.projectRoot, filePath)));
13953
+ const toStoredChangedFiles = (filePaths) => filePaths.map((filePath) => this.toStoredFilePath(path19.resolve(this.projectRoot, filePath)));
13282
13954
  const storedChangedFiles = toStoredChangedFiles(changedFiles);
13283
13955
  const directSymbols = database.getSymbolsForFiles(storedChangedFiles, branchKey);
13284
13956
  const directIds = directSymbols.map((s) => s.id);
@@ -13427,12 +14099,12 @@ var Indexer = class _Indexer {
13427
14099
  if (meta.filePath) filePaths.add(meta.filePath);
13428
14100
  }
13429
14101
  const directory = options?.directory?.replace(/\/$/, "");
13430
- const absoluteDirectoryFilter = directory ? path18.resolve(this.projectRoot, directory) : void 0;
14102
+ const absoluteDirectoryFilter = directory ? path19.resolve(this.projectRoot, directory) : void 0;
13431
14103
  for (const filePath of filePaths) {
13432
14104
  if (directory) {
13433
14105
  const absoluteFilePath = this.resolveStoredFilePath(filePath);
13434
14106
  const matchesRelative = filePath === directory || filePath.startsWith(directory + "/");
13435
- const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path18.sep));
14107
+ const matchesProjectRelative = absoluteDirectoryFilter !== void 0 && (absoluteFilePath === absoluteDirectoryFilter || absoluteFilePath.startsWith(absoluteDirectoryFilter + path19.sep));
13436
14108
  if (!matchesRelative && !matchesProjectRelative) {
13437
14109
  continue;
13438
14110
  }
@@ -13592,7 +14264,7 @@ function trimOrUndefined(value) {
13592
14264
  return normalized || void 0;
13593
14265
  }
13594
14266
  function normalizeCallGraphPath(value) {
13595
- let normalized = path19.posix.normalize(value.trim().replaceAll("\\", "/"));
14267
+ let normalized = path20.posix.normalize(value.trim().replaceAll("\\", "/"));
13596
14268
  if (normalized.startsWith("./")) {
13597
14269
  normalized = normalized.slice(2);
13598
14270
  }
@@ -13776,12 +14448,12 @@ async function getCallGraphPath(projectRoot3, host, from, to, maxDepth, fromFile
13776
14448
  if (fromResolution.status !== "resolved" || toResolution.status !== "resolved") {
13777
14449
  return { from: fromResolution, to: toResolution, path: [] };
13778
14450
  }
13779
- const path20 = await indexer.findCallPathBySymbolIds(
14451
+ const path21 = await indexer.findCallPathBySymbolIds(
13780
14452
  fromResolution.symbolId,
13781
14453
  toResolution.symbolId,
13782
14454
  maxDepth
13783
14455
  );
13784
- return { from: fromResolution, to: toResolution, path: path20 };
14456
+ return { from: fromResolution, to: toResolution, path: path21 };
13785
14457
  }
13786
14458
  async function runIndexCodebase(projectRoot3, host, args, onProgress) {
13787
14459
  const root = getProjectRoot(projectRoot3, host);
@@ -13857,6 +14529,34 @@ async function getPrImpact(projectRoot3, host, params) {
13857
14529
  direction: params.direction
13858
14530
  });
13859
14531
  }
14532
+ async function getCodeCommunities(projectRoot3, host, params) {
14533
+ await ensureAutoIndexReadyForRetrieval(projectRoot3, host);
14534
+ const indexer = getIndexerForProject(projectRoot3, host);
14535
+ const [communities, centrality, couplings] = await Promise.all([
14536
+ indexer.detectCommunities(params.branch),
14537
+ indexer.computeCentrality(params.branch),
14538
+ indexer.detectCommunityCouplings(params.branch)
14539
+ ]);
14540
+ return buildCodeCommunitiesResult(communities, centrality, couplings, {
14541
+ minSize: Math.max(CODE_COMMUNITIES_MIN_SIZE, Math.floor(params.minSize ?? CODE_COMMUNITIES_MIN_SIZE)),
14542
+ limit: Math.min(
14543
+ CODE_COMMUNITIES_MAX_LIMIT,
14544
+ Math.max(1, Math.floor(params.limit ?? CODE_COMMUNITIES_DEFAULT_LIMIT))
14545
+ ),
14546
+ hubThreshold: Math.max(
14547
+ 0,
14548
+ Math.floor(params.hubThreshold ?? CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD)
14549
+ ),
14550
+ minCoupling: Math.max(
14551
+ CODE_COMMUNITIES_MIN_COUPLING,
14552
+ Math.floor(params.minCoupling ?? CODE_COMMUNITIES_MIN_COUPLING)
14553
+ ),
14554
+ couplingLimit: Math.min(
14555
+ CODE_COMMUNITIES_MAX_COUPLING_LIMIT,
14556
+ Math.max(1, Math.floor(params.couplingLimit ?? CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT))
14557
+ )
14558
+ });
14559
+ }
13860
14560
  async function getIndexMetrics(projectRoot3, host, args = {}) {
13861
14561
  const root = getProjectRoot(projectRoot3, host);
13862
14562
  const key = getIndexerCacheKey(root, host);
@@ -13948,8 +14648,8 @@ async function getIndexLogs(projectRoot3, host, args) {
13948
14648
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
13949
14649
  const root = getProjectRoot(projectRoot3, host);
13950
14650
  const inputPath = knowledgeBasePath.trim();
13951
- const normalizedPath = path19.resolve(
13952
- path19.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14651
+ const normalizedPath = path20.resolve(
14652
+ path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
13953
14653
  );
13954
14654
  if (!(0, import_fs13.existsSync)(normalizedPath)) {
13955
14655
  return `Error: Directory does not exist: ${normalizedPath}`;
@@ -13985,7 +14685,7 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
13985
14685
  }
13986
14686
  }
13987
14687
  for (const dotDir of sensitiveDotDirs) {
13988
- const sensitiveDir = path19.join(homeDir, dotDir);
14688
+ const sensitiveDir = path20.join(homeDir, dotDir);
13989
14689
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
13990
14690
  return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
13991
14691
  }
@@ -14048,7 +14748,7 @@ function listKnowledgeBases(projectRoot3, host) {
14048
14748
  }
14049
14749
  result += "\n";
14050
14750
  }
14051
- const hasHostConfig = (0, import_fs13.existsSync)(path19.join(root, getHostProjectConfigRelativePath(host)));
14751
+ const hasHostConfig = (0, import_fs13.existsSync)(path20.join(root, getHostProjectConfigRelativePath(host)));
14052
14752
  if (hasHostConfig) {
14053
14753
  result += `
14054
14754
  Config sources: 1 file(s).`;
@@ -14081,117 +14781,6 @@ Run /index to rebuild the index without the removed knowledge base.`;
14081
14781
  return result;
14082
14782
  }
14083
14783
 
14084
- // src/tools/symbol-inference.ts
14085
- var IDENTIFIER_RE = /[A-Za-z_$][A-Za-z0-9_$]*/g;
14086
- var QUOTED_BACKTICK_RE = /`([^`]+)`/g;
14087
- var QUOTED_SINGLE_RE = /'([^'\\]+)'/g;
14088
- var QUOTED_DOUBLE_RE = /"([^"]+)"/g;
14089
- var SYMBOL_LIKE_RE = /^(?:[A-Za-z_$][A-Za-z0-9_$]*)$/;
14090
- var CAMEL_CASE_RE = /^[a-z_][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*$/;
14091
- var PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9_$]*$/;
14092
- var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*_[a-z0-9_]+$/;
14093
- var DEFINITION_INTENT_RE = /\b(where|defined|definition|define|declaration|symbol|function|method|class|interface|type)\b/i;
14094
- var STOP_WORDS = /* @__PURE__ */ new Set([
14095
- "a",
14096
- "an",
14097
- "and",
14098
- "are",
14099
- "at",
14100
- "for",
14101
- "find",
14102
- "how",
14103
- "i",
14104
- "in",
14105
- "is",
14106
- "it",
14107
- "of",
14108
- "on",
14109
- "that",
14110
- "the",
14111
- "definition",
14112
- "show",
14113
- "to",
14114
- "where",
14115
- "which",
14116
- "what",
14117
- "you",
14118
- "your",
14119
- "with"
14120
- ]);
14121
- function stripCallSuffix(token) {
14122
- return token.replace(/\(\s*\)$/, "");
14123
- }
14124
- function isLikelySymbolName(token) {
14125
- if (!SYMBOL_LIKE_RE.test(token)) {
14126
- return false;
14127
- }
14128
- if (STOP_WORDS.has(token.toLowerCase())) {
14129
- return false;
14130
- }
14131
- return CAMEL_CASE_RE.test(token) || PASCAL_CASE_RE.test(token) || SNAKE_CASE_RE.test(token);
14132
- }
14133
- function extractQuotedIdentifiers(query) {
14134
- const identifiers = /* @__PURE__ */ new Set();
14135
- for (const match of query.matchAll(QUOTED_BACKTICK_RE)) {
14136
- const candidate = stripCallSuffix(match[1].trim());
14137
- if (candidate && isLikelySymbolName(candidate)) {
14138
- identifiers.add(candidate);
14139
- }
14140
- }
14141
- for (const match of query.matchAll(QUOTED_SINGLE_RE)) {
14142
- const candidate = stripCallSuffix(match[1].trim());
14143
- if (candidate && isLikelySymbolName(candidate)) {
14144
- identifiers.add(candidate);
14145
- }
14146
- }
14147
- for (const match of query.matchAll(QUOTED_DOUBLE_RE)) {
14148
- const candidate = stripCallSuffix(match[1].trim());
14149
- if (candidate && isLikelySymbolName(candidate)) {
14150
- identifiers.add(candidate);
14151
- }
14152
- }
14153
- return [...identifiers];
14154
- }
14155
- function extractBareIdentifiers(query) {
14156
- const unquoted = query.replace(QUOTED_BACKTICK_RE, " ").replace(QUOTED_SINGLE_RE, " ").replace(QUOTED_DOUBLE_RE, " ");
14157
- const identifiers = /* @__PURE__ */ new Set();
14158
- for (const match of unquoted.matchAll(IDENTIFIER_RE)) {
14159
- const candidate = stripCallSuffix(match[0]);
14160
- if (isLikelySymbolName(candidate)) {
14161
- identifiers.add(candidate);
14162
- }
14163
- }
14164
- return [...identifiers];
14165
- }
14166
- function isSingleMeaningfulToken(query, symbol) {
14167
- const tokens = query.replace(/[`'"()]/g, " ").split(/[^A-Za-z0-9_$]+/).map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0).filter((token) => !STOP_WORDS.has(token));
14168
- return tokens.length === 1 && tokens[0] === symbol.toLowerCase();
14169
- }
14170
- function inferExactSymbolFromQuery(query) {
14171
- if (analyzeQueryIntent(query).explicitArtifactIntent) {
14172
- return void 0;
14173
- }
14174
- const quoted = extractQuotedIdentifiers(query);
14175
- if (quoted.length === 1) {
14176
- return quoted[0];
14177
- }
14178
- if (quoted.length > 1) {
14179
- return void 0;
14180
- }
14181
- const candidates = extractBareIdentifiers(query);
14182
- if (candidates.length !== 1) {
14183
- return void 0;
14184
- }
14185
- const candidate = candidates[0];
14186
- if (DEFINITION_INTENT_RE.test(query)) {
14187
- return candidate;
14188
- }
14189
- if (isSingleMeaningfulToken(query, candidate)) {
14190
- return candidate;
14191
- }
14192
- return void 0;
14193
- }
14194
-
14195
14784
  // src/tools/context-search.ts
14196
14785
  var MIN_CONTEXT_RESULT_LIMIT = 1;
14197
14786
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -14542,7 +15131,7 @@ async function resolveCodebaseContextUnmeasured(projectRoot3, host, input) {
14542
15131
  const directory = input.directory ?? void 0;
14543
15132
  const tokenBudget = input.tokenBudget ?? void 0;
14544
15133
  if (from && to) {
14545
- const path20 = await getCallGraphPath(
15134
+ const path21 = await getCallGraphPath(
14546
15135
  projectRoot3,
14547
15136
  host,
14548
15137
  from,
@@ -14551,25 +15140,25 @@ async function resolveCodebaseContextUnmeasured(projectRoot3, host, input) {
14551
15140
  fromFilePath,
14552
15141
  toFilePath
14553
15142
  );
14554
- const pathText = formatCallGraphPathResult(path20);
14555
- if (path20.path.length > 0) {
15143
+ const pathText = formatCallGraphPathResult(path21);
15144
+ if (path21.path.length > 0) {
14556
15145
  const fitted2 = fitTextToContextBudget(
14557
15146
  pathText,
14558
15147
  tokenBudget
14559
15148
  );
14560
15149
  return {
14561
15150
  text: fitted2.text,
14562
- details: fittedDetails("path", fitted2, path20.path.length)
15151
+ details: fittedDetails("path", fitted2, path21.path.length)
14563
15152
  };
14564
15153
  }
14565
- if (path20.from.status !== "resolved" || path20.to.status !== "resolved") {
15154
+ if (path21.from.status !== "resolved" || path21.to.status !== "resolved") {
14566
15155
  const fitted2 = fitTextToContextBudget(pathText, tokenBudget);
14567
15156
  return {
14568
15157
  text: fitted2.text,
14569
15158
  details: fittedDetails("path", fitted2, 0)
14570
15159
  };
14571
15160
  }
14572
- const resolvedFrom = path20.from;
15161
+ const resolvedFrom = path21.from;
14573
15162
  const { callers } = await getCallGraphData(projectRoot3, host, {
14574
15163
  name: to,
14575
15164
  direction: "callers",
@@ -14689,6 +15278,7 @@ var TOOL_NAME = {
14689
15278
  CALL_GRAPH: "call_graph",
14690
15279
  CALL_GRAPH_PATH: "call_graph_path",
14691
15280
  PR_IMPACT: "pr_impact",
15281
+ CODE_COMMUNITIES: "code_communities",
14692
15282
  ADD_KNOWLEDGE_BASE: "add_knowledge_base",
14693
15283
  LIST_KNOWLEDGE_BASES: "list_knowledge_bases",
14694
15284
  REMOVE_KNOWLEDGE_BASE: "remove_knowledge_base",
@@ -14710,7 +15300,8 @@ var PORTABLE_TOOL_NAMES = [
14710
15300
  TOOL_NAME.IMPLEMENTATION_LOOKUP,
14711
15301
  TOOL_NAME.CALL_GRAPH,
14712
15302
  TOOL_NAME.CALL_GRAPH_PATH,
14713
- TOOL_NAME.PR_IMPACT
15303
+ TOOL_NAME.PR_IMPACT,
15304
+ TOOL_NAME.CODE_COMMUNITIES
14714
15305
  ];
14715
15306
  var OPENCODE_TOOL_NAMES = [
14716
15307
  TOOL_NAME.CODEBASE_CONTEXT,
@@ -14729,6 +15320,7 @@ var OPENCODE_TOOL_NAMES = [
14729
15320
  TOOL_NAME.LIST_KNOWLEDGE_BASES,
14730
15321
  TOOL_NAME.REMOVE_KNOWLEDGE_BASE,
14731
15322
  TOOL_NAME.PR_IMPACT,
15323
+ TOOL_NAME.CODE_COMMUNITIES,
14732
15324
  TOOL_NAME.INDEX_VISUALIZE
14733
15325
  ];
14734
15326
  var PI_TOOL_NAMES = [
@@ -14745,6 +15337,7 @@ var PI_TOOL_NAMES = [
14745
15337
  TOOL_NAME.CALL_GRAPH,
14746
15338
  TOOL_NAME.CALL_GRAPH_PATH,
14747
15339
  TOOL_NAME.PR_IMPACT,
15340
+ TOOL_NAME.CODE_COMMUNITIES,
14748
15341
  TOOL_NAME.PI_KNOWLEDGE_BASE_LIST,
14749
15342
  TOOL_NAME.PI_KNOWLEDGE_BASE_ADD,
14750
15343
  TOOL_NAME.PI_KNOWLEDGE_BASE_REMOVE
@@ -15039,6 +15632,25 @@ Check index_status first when index readiness is unknown. For repository questio
15039
15632
  return text2(formatPrImpact(result), result);
15040
15633
  }
15041
15634
  });
15635
+ pi.registerTool({
15636
+ name: TOOL_NAME.CODE_COMMUNITIES,
15637
+ label: "Code Communities",
15638
+ description: "Discover natural module boundaries and hub symbols using graph community detection. Clusters symbols by call-graph connectivity to reveal architecture.",
15639
+ parameters: import_typebox2.Type.Object({
15640
+ branch: import_typebox2.Type.Optional(import_typebox2.Type.String()),
15641
+ minSize: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: CODE_COMMUNITIES_MIN_SIZE, default: CODE_COMMUNITIES_MIN_SIZE })),
15642
+ limit: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: 1, maximum: CODE_COMMUNITIES_MAX_LIMIT, default: CODE_COMMUNITIES_DEFAULT_LIMIT })),
15643
+ hubThreshold: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: 0, default: CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD })),
15644
+ minCoupling: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: CODE_COMMUNITIES_MIN_COUPLING, default: CODE_COMMUNITIES_MIN_COUPLING })),
15645
+ couplingLimit: import_typebox2.Type.Optional(
15646
+ import_typebox2.Type.Integer({ minimum: 1, maximum: CODE_COMMUNITIES_MAX_COUPLING_LIMIT, default: CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT })
15647
+ )
15648
+ }),
15649
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15650
+ const result = await getCodeCommunities(projectRoot2(ctx), HOST2, params);
15651
+ return text2(formatCodeCommunities(result), result);
15652
+ }
15653
+ });
15042
15654
  pi.registerTool({
15043
15655
  name: TOOL_NAME.PI_KNOWLEDGE_BASE_LIST,
15044
15656
  label: "List Knowledge Bases",