@lzehrung/codegraph 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/codegraph-skill/codegraph/SKILL.md +3 -2
  2. package/dist/agent/explain.d.ts.map +1 -1
  3. package/dist/agent/explain.js +12 -7
  4. package/dist/agent/explain.js.map +1 -1
  5. package/dist/agent/search.d.ts +13 -0
  6. package/dist/agent/search.d.ts.map +1 -1
  7. package/dist/agent/search.js +10 -4
  8. package/dist/agent/search.js.map +1 -1
  9. package/dist/bin/cli.js +1644 -675
  10. package/dist/cli/command-table.d.ts.map +1 -1
  11. package/dist/cli/command-table.js +4 -0
  12. package/dist/cli/command-table.js.map +1 -1
  13. package/dist/cli/duplicates.d.ts +5 -0
  14. package/dist/cli/duplicates.d.ts.map +1 -1
  15. package/dist/cli/duplicates.js +15 -2
  16. package/dist/cli/duplicates.js.map +1 -1
  17. package/dist/cli/help.d.ts +4 -4
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +15 -9
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/cli/mcp.d.ts.map +1 -1
  22. package/dist/cli/mcp.js +4 -0
  23. package/dist/cli/mcp.js.map +1 -1
  24. package/dist/cli/options.d.ts.map +1 -1
  25. package/dist/cli/options.js +5 -1
  26. package/dist/cli/options.js.map +1 -1
  27. package/dist/cli/progress.d.ts.map +1 -1
  28. package/dist/cli/progress.js +42 -16
  29. package/dist/cli/progress.js.map +1 -1
  30. package/dist/cli/server.d.ts.map +1 -1
  31. package/dist/cli/server.js +2 -0
  32. package/dist/cli/server.js.map +1 -1
  33. package/dist/cli/viewer.js +1 -1
  34. package/dist/cli/viewer.js.map +1 -1
  35. package/dist/drift/git.d.ts.map +1 -1
  36. package/dist/drift/git.js +10 -2
  37. package/dist/drift/git.js.map +1 -1
  38. package/dist/duplicates/candidates.d.ts +61 -0
  39. package/dist/duplicates/candidates.d.ts.map +1 -0
  40. package/dist/duplicates/candidates.js +873 -0
  41. package/dist/duplicates/candidates.js.map +1 -0
  42. package/dist/duplicates/progress.d.ts +17 -0
  43. package/dist/duplicates/progress.d.ts.map +1 -0
  44. package/dist/duplicates/progress.js +57 -0
  45. package/dist/duplicates/progress.js.map +1 -0
  46. package/dist/duplicates/scoring.d.ts +9 -40
  47. package/dist/duplicates/scoring.d.ts.map +1 -1
  48. package/dist/duplicates/scoring.js +34 -414
  49. package/dist/duplicates/scoring.js.map +1 -1
  50. package/dist/duplicates/types.d.ts +30 -1
  51. package/dist/duplicates/types.d.ts.map +1 -1
  52. package/dist/duplicates/units.d.ts +2 -1
  53. package/dist/duplicates/units.d.ts.map +1 -1
  54. package/dist/duplicates/units.js +24 -3
  55. package/dist/duplicates/units.js.map +1 -1
  56. package/dist/duplicates.d.ts +1 -0
  57. package/dist/duplicates.d.ts.map +1 -1
  58. package/dist/duplicates.js +116 -62
  59. package/dist/duplicates.js.map +1 -1
  60. package/dist/impact/providers/base.js +1 -1
  61. package/dist/impact/providers/base.js.map +1 -1
  62. package/dist/mcp/cancellation.d.ts +6 -0
  63. package/dist/mcp/cancellation.d.ts.map +1 -0
  64. package/dist/mcp/cancellation.js +11 -0
  65. package/dist/mcp/cancellation.js.map +1 -0
  66. package/dist/mcp/handlers.d.ts +33 -3
  67. package/dist/mcp/handlers.d.ts.map +1 -1
  68. package/dist/mcp/handlers.js +128 -30
  69. package/dist/mcp/handlers.js.map +1 -1
  70. package/dist/mcp/http-transport.d.ts.map +1 -1
  71. package/dist/mcp/http-transport.js +7 -4
  72. package/dist/mcp/http-transport.js.map +1 -1
  73. package/dist/mcp/index-readiness.d.ts +114 -0
  74. package/dist/mcp/index-readiness.d.ts.map +1 -0
  75. package/dist/mcp/index-readiness.js +264 -0
  76. package/dist/mcp/index-readiness.js.map +1 -0
  77. package/dist/mcp/protocol.d.ts.map +1 -1
  78. package/dist/mcp/protocol.js +23 -10
  79. package/dist/mcp/protocol.js.map +1 -1
  80. package/dist/mcp/server.d.ts +2 -0
  81. package/dist/mcp/server.d.ts.map +1 -1
  82. package/dist/mcp/server.js +9 -4
  83. package/dist/mcp/server.js.map +1 -1
  84. package/dist/mcp/stale-install.d.ts +13 -0
  85. package/dist/mcp/stale-install.d.ts.map +1 -0
  86. package/dist/mcp/stale-install.js +30 -0
  87. package/dist/mcp/stale-install.js.map +1 -0
  88. package/dist/review/deleted.js +1 -1
  89. package/dist/review/deleted.js.map +1 -1
  90. package/dist/review.d.ts +6 -0
  91. package/dist/review.d.ts.map +1 -1
  92. package/dist/review.js +1 -1
  93. package/dist/review.js.map +1 -1
  94. package/dist/types.d.ts +2 -0
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/util/concurrency.d.ts +7 -0
  97. package/dist/util/concurrency.d.ts.map +1 -1
  98. package/dist/util/concurrency.js +11 -0
  99. package/dist/util/concurrency.js.map +1 -1
  100. package/docs/mcp.md +56 -7
  101. package/package.json +3 -3
package/dist/bin/cli.js CHANGED
@@ -685,6 +685,7 @@ var init_options = __esm({
685
685
  "--min-tokens",
686
686
  "--max-tokens",
687
687
  "--max-bucket-size",
688
+ "--max-candidate-pairs",
688
689
  "--min-confidence",
689
690
  "--max-hits",
690
691
  "--resolution-hint",
@@ -722,6 +723,7 @@ var init_options = __esm({
722
723
  "--graph",
723
724
  "--port",
724
725
  "--idle-timeout-ms",
726
+ "--index-wait-ms",
725
727
  "--startup-timeout-ms",
726
728
  "--offset",
727
729
  "--max-bytes",
@@ -848,6 +850,7 @@ var init_options = __esm({
848
850
  "--include-root-glob",
849
851
  "--limit",
850
852
  "--max-bucket-size",
853
+ "--max-candidate-pairs",
851
854
  "--max-tokens",
852
855
  "--min-confidence",
853
856
  "--min-tokens",
@@ -994,7 +997,7 @@ var init_options = __esm({
994
997
  ],
995
998
  [
996
999
  "mcp",
997
- commandSchema([...SHARED_BUILD_FLAGS, "--allow-build", "--stdio", "--warmup", "--warmup-symbols", "--no-warmup", "--no-watch"], [...SHARED_BUILD_OPTIONS, "--artifact", "--host", "--port", "--idle-timeout-ms"], {
1000
+ commandSchema([...SHARED_BUILD_FLAGS, "--allow-build", "--stdio", "--warmup", "--warmup-symbols", "--no-warmup", "--no-watch"], [...SHARED_BUILD_OPTIONS, "--artifact", "--host", "--port", "--idle-timeout-ms", "--index-wait-ms"], {
998
1001
  kind: "max",
999
1002
  max: 1,
1000
1003
  usage: "Usage: codegraph mcp [serve] [--root <path>] [--stdio | --port <number>]"
@@ -1019,6 +1022,7 @@ var init_options = __esm({
1019
1022
  "--host",
1020
1023
  "--ignore-glob",
1021
1024
  "--include-glob",
1025
+ "--index-wait-ms",
1022
1026
  "--port",
1023
1027
  "--resolution-hint",
1024
1028
  "--root",
@@ -1237,6 +1241,7 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1237
1241
  let current = 0;
1238
1242
  let total = 0;
1239
1243
  let mode = "build";
1244
+ let unit = DEFAULT_COUNT_UNIT;
1240
1245
  let activity = progressAction(mode);
1241
1246
  let interval;
1242
1247
  let delay3;
@@ -1252,7 +1257,7 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1252
1257
  return;
1253
1258
  const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
1254
1259
  frameIndex += 1;
1255
- const count = hasProgressCount(mode, current, total) ? formatProgressCount(current, total) : "";
1260
+ const count = hasProgressCount(mode, current, total) ? formatProgressCount(current, total, unit) : "";
1256
1261
  const elapsed = ` (${formatDuration(performance2.now() - phaseStartedAt)})`;
1257
1262
  write(`${CLEAR_LINE}${activity}... ${frame}${count}${elapsed}`);
1258
1263
  rendered = true;
@@ -1276,6 +1281,7 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1276
1281
  current = update.current;
1277
1282
  total = update.total;
1278
1283
  mode = update.mode ?? "build";
1284
+ unit = update.unit ?? DEFAULT_COUNT_UNIT;
1279
1285
  activity = update.activity ?? `${progressAction(mode)} project index`;
1280
1286
  phaseStartedAt = performance2.now();
1281
1287
  const beginRendering = () => {
@@ -1300,10 +1306,14 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1300
1306
  if (!rendered)
1301
1307
  return;
1302
1308
  clear();
1303
- const verb = progressCompleteVerb(update.mode ?? mode);
1304
- const fileCount = formatFileCount(update.total, "words");
1305
1309
  const elapsed = update.elapsedMs === void 0 ? "" : ` in ${formatDuration(update.elapsedMs)}`;
1306
- write(`${verb} project index: ${fileCount}${elapsed}.
1310
+ if (update.activity !== void 0) {
1311
+ write(`${update.activity}: ${formatCount(update.total, update.unit ?? DEFAULT_COUNT_UNIT, "words")}${elapsed}.
1312
+ `);
1313
+ return;
1314
+ }
1315
+ const verb = progressCompleteVerb(update.mode ?? mode);
1316
+ write(`${verb} project index: ${formatCount(update.total, DEFAULT_COUNT_UNIT, "words")}${elapsed}.
1307
1317
  `);
1308
1318
  };
1309
1319
  return {
@@ -1324,10 +1334,17 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1324
1334
  current = update.current;
1325
1335
  total = update.total;
1326
1336
  mode = update.mode ?? mode;
1327
- if (update.activity !== void 0 && update.activity !== activity)
1337
+ if (update.activity !== void 0)
1338
+ unit = update.unit ?? DEFAULT_COUNT_UNIT;
1339
+ else if (update.unit !== void 0)
1340
+ unit = update.unit;
1341
+ const activityChanged = update.activity !== void 0 && update.activity !== activity;
1342
+ if (activityChanged)
1328
1343
  phaseStartedAt = performance2.now();
1329
1344
  if (update.activity !== void 0)
1330
1345
  activity = update.activity;
1346
+ if (activityChanged && interval !== void 0)
1347
+ render();
1331
1348
  },
1332
1349
  clear,
1333
1350
  dispose: () => {
@@ -1337,15 +1354,15 @@ function createInteractiveProgressDisplay(write, delayMs = 0) {
1337
1354
  }
1338
1355
  };
1339
1356
  }
1340
- function formatFileCount(count, zeroFormat = "numeric") {
1357
+ function formatCount(count, unit, zeroFormat = "numeric") {
1341
1358
  if (!count && zeroFormat === "words")
1342
- return "no files";
1343
- return `${count} file${count === 1 ? "" : "s"}`;
1359
+ return `no ${unit}s`;
1360
+ return `${count} ${unit}${count === 1 ? "" : "s"}`;
1344
1361
  }
1345
- function formatProgressCount(current, total) {
1362
+ function formatProgressCount(current, total, unit) {
1346
1363
  if (total > 0)
1347
- return ` ${current}/${formatFileCount(total)}`;
1348
- return ` ${formatFileCount(current)} processed`;
1364
+ return ` ${current}/${formatCount(total, unit)}`;
1365
+ return ` ${formatCount(current, unit)} processed`;
1349
1366
  }
1350
1367
  function hasProgressCount(mode, current, total) {
1351
1368
  return mode !== "check" || current > 0 || total > 0;
@@ -1356,6 +1373,7 @@ function createLogProgressDisplay(write, delayMs = 0) {
1356
1373
  let current = 0;
1357
1374
  let total = 0;
1358
1375
  let mode = "build";
1376
+ let unit = DEFAULT_COUNT_UNIT;
1359
1377
  let activity = progressAction(mode);
1360
1378
  let delay3;
1361
1379
  let heartbeat;
@@ -1379,7 +1397,7 @@ function createLogProgressDisplay(write, delayMs = 0) {
1379
1397
  heartbeat = setInterval(() => {
1380
1398
  if (!active)
1381
1399
  return;
1382
- const count = hasProgressCount(mode, current, total) ? `:${formatProgressCount(current, total)}` : "";
1400
+ const count = hasProgressCount(mode, current, total) ? `:${formatProgressCount(current, total, unit)}` : "";
1383
1401
  const elapsed = formatDuration(performance2.now() - phaseStartedAt);
1384
1402
  write(`[Progress] ${activity}${count}. (${elapsed})
1385
1403
  `);
@@ -1393,6 +1411,7 @@ function createLogProgressDisplay(write, delayMs = 0) {
1393
1411
  current = update.current;
1394
1412
  total = update.total;
1395
1413
  mode = update.mode ?? "build";
1414
+ unit = update.unit ?? DEFAULT_COUNT_UNIT;
1396
1415
  activity = update.activity ?? `${progressAction(mode)} project index`;
1397
1416
  phaseStartedAt = performance2.now();
1398
1417
  if (delayMs) {
@@ -1415,9 +1434,14 @@ function createLogProgressDisplay(write, delayMs = 0) {
1415
1434
  stopTimers();
1416
1435
  if (!rendered)
1417
1436
  return;
1418
- const verb = progressCompleteVerb(update.mode ?? mode);
1419
1437
  const elapsed = update.elapsedMs === void 0 ? "" : ` in ${formatDuration(update.elapsedMs)}`;
1420
- write(`[Progress] ${verb} project index: ${formatFileCount(update.total, "words")}${elapsed}.
1438
+ if (update.activity !== void 0) {
1439
+ write(`[Progress] ${update.activity}: ${formatCount(update.total, update.unit ?? DEFAULT_COUNT_UNIT, "words")}${elapsed}.
1440
+ `);
1441
+ return;
1442
+ }
1443
+ const verb = progressCompleteVerb(update.mode ?? mode);
1444
+ write(`[Progress] ${verb} project index: ${formatCount(update.total, DEFAULT_COUNT_UNIT, "words")}${elapsed}.
1421
1445
  `);
1422
1446
  return;
1423
1447
  }
@@ -1430,6 +1454,10 @@ function createLogProgressDisplay(write, delayMs = 0) {
1430
1454
  current = update.current;
1431
1455
  total = update.total;
1432
1456
  mode = update.mode ?? mode;
1457
+ if (update.activity !== void 0)
1458
+ unit = update.unit ?? DEFAULT_COUNT_UNIT;
1459
+ else if (update.unit !== void 0)
1460
+ unit = update.unit;
1433
1461
  if (update.activity !== void 0)
1434
1462
  activity = update.activity;
1435
1463
  const activityChanged = activity !== previousActivity;
@@ -1444,10 +1472,10 @@ function createLogProgressDisplay(write, delayMs = 0) {
1444
1472
  if (shouldPrintCount) {
1445
1473
  const elapsed = formatDuration(performance2.now() - phaseStartedAt);
1446
1474
  if (update.activity !== void 0) {
1447
- write(`[Progress] ${activity}:${formatProgressCount(update.current, update.total)}. (${elapsed})
1475
+ write(`[Progress] ${activity}:${formatProgressCount(update.current, update.total, unit)}. (${elapsed})
1448
1476
  `);
1449
1477
  } else {
1450
- write(`[Progress] ${formatFileCount(update.current)} processed. (${elapsed})
1478
+ write(`[Progress] ${formatCount(update.current, unit)} processed. (${elapsed})
1451
1479
  `);
1452
1480
  }
1453
1481
  }
@@ -1480,13 +1508,14 @@ function formatDuration(elapsedMs2) {
1480
1508
  return `${Math.round(durationMs)}ms`;
1481
1509
  return `${(durationMs / 1e3).toFixed(1)}s`;
1482
1510
  }
1483
- var REFRESH_INTERVAL_MS, CLEAR_LINE, SPINNER_FRAMES;
1511
+ var REFRESH_INTERVAL_MS, CLEAR_LINE, SPINNER_FRAMES, DEFAULT_COUNT_UNIT;
1484
1512
  var init_progress = __esm({
1485
1513
  "dist/cli/progress.js"() {
1486
1514
  "use strict";
1487
1515
  REFRESH_INTERVAL_MS = 100;
1488
1516
  CLEAR_LINE = "\r\x1B[2K";
1489
1517
  SPINNER_FRAMES = ["-", "\\", "|", "/"];
1518
+ DEFAULT_COUNT_UNIT = "file";
1490
1519
  }
1491
1520
  });
1492
1521
 
@@ -9499,12 +9528,14 @@ var init_dist2 = __esm({
9499
9528
  // dist/util/concurrency.js
9500
9529
  var concurrency_exports = {};
9501
9530
  __export(concurrency_exports, {
9531
+ EVENT_LOOP_SLICE_MS: () => EVENT_LOOP_SLICE_MS,
9502
9532
  Semaphore: () => Semaphore,
9503
9533
  delay: () => delay,
9504
9534
  getIOSemaphore: () => getIOSemaphore,
9505
9535
  mapLimit: () => mapLimit,
9506
9536
  mapLimitSemaphore: () => mapLimitSemaphore,
9507
- resetIOSemaphore: () => resetIOSemaphore
9537
+ resetIOSemaphore: () => resetIOSemaphore,
9538
+ yieldToEventLoop: () => yieldToEventLoop
9508
9539
  });
9509
9540
  async function mapLimit(items, limit, fn) {
9510
9541
  if (!items.length)
@@ -9572,7 +9603,12 @@ function delay(ms) {
9572
9603
  setTimeout(resolve3, ms);
9573
9604
  return promise3;
9574
9605
  }
9575
- var Semaphore, globalIOSemaphore;
9606
+ function yieldToEventLoop() {
9607
+ const { promise: promise3, resolve: resolve3 } = Promise.withResolvers();
9608
+ setImmediate(resolve3);
9609
+ return promise3;
9610
+ }
9611
+ var Semaphore, globalIOSemaphore, EVENT_LOOP_SLICE_MS;
9576
9612
  var init_concurrency = __esm({
9577
9613
  "dist/util/concurrency.js"() {
9578
9614
  "use strict";
@@ -9625,6 +9661,7 @@ var init_concurrency = __esm({
9625
9661
  }
9626
9662
  };
9627
9663
  globalIOSemaphore = null;
9664
+ EVENT_LOOP_SLICE_MS = 10;
9628
9665
  }
9629
9666
  });
9630
9667
 
@@ -68918,6 +68955,68 @@ var init_unit_cache = __esm({
68918
68955
  }
68919
68956
  });
68920
68957
 
68958
+ // dist/duplicates/progress.js
68959
+ import { performance as performance4 } from "node:perf_hooks";
68960
+ function createDuplicateProgress(onProgress, startedAt) {
68961
+ if (!onProgress)
68962
+ return void 0;
68963
+ const origin = startedAt ?? performance4.now();
68964
+ let opened = false;
68965
+ let lastYield = performance4.now();
68966
+ return {
68967
+ stage: (activity, current = 0, total = 0, unit) => {
68968
+ onProgress({
68969
+ type: "progress",
68970
+ phase: opened ? "update" : "start",
68971
+ mode: "check",
68972
+ message: activity,
68973
+ activity,
68974
+ current,
68975
+ total,
68976
+ ...unit !== void 0 ? { unit } : {}
68977
+ });
68978
+ opened = true;
68979
+ },
68980
+ complete: (units) => {
68981
+ if (!opened)
68982
+ return;
68983
+ onProgress({
68984
+ type: "progress",
68985
+ phase: "complete",
68986
+ mode: "check",
68987
+ message: DUPLICATE_STAGE_DONE,
68988
+ activity: DUPLICATE_STAGE_DONE,
68989
+ current: units,
68990
+ total: units,
68991
+ unit: "unit",
68992
+ elapsedMs: performance4.now() - origin
68993
+ });
68994
+ },
68995
+ yieldPoint: () => {
68996
+ if (performance4.now() - lastYield < YIELD_INTERVAL_MS)
68997
+ return void 0;
68998
+ return new Promise((resolve3) => {
68999
+ setImmediate(() => {
69000
+ lastYield = performance4.now();
69001
+ resolve3();
69002
+ });
69003
+ });
69004
+ }
69005
+ };
69006
+ }
69007
+ var YIELD_INTERVAL_MS, DUPLICATE_STAGE_UNITS, DUPLICATE_STAGE_PAIRS, DUPLICATE_STAGE_SCORING, DUPLICATE_STAGE_GROUPING, DUPLICATE_STAGE_DONE;
69008
+ var init_progress2 = __esm({
69009
+ "dist/duplicates/progress.js"() {
69010
+ "use strict";
69011
+ YIELD_INTERVAL_MS = 50;
69012
+ DUPLICATE_STAGE_UNITS = "Collecting duplicate units";
69013
+ DUPLICATE_STAGE_PAIRS = "Finding candidate pairs";
69014
+ DUPLICATE_STAGE_SCORING = "Scoring candidate pairs";
69015
+ DUPLICATE_STAGE_GROUPING = "Grouping duplicate findings";
69016
+ DUPLICATE_STAGE_DONE = "Detected duplicates";
69017
+ }
69018
+ });
69019
+
68921
69020
  // dist/duplicates/units.js
68922
69021
  var units_exports = {};
68923
69022
  __export(units_exports, {
@@ -69379,7 +69478,7 @@ function shouldKeepUnit(unit, includeSmall, minTokens) {
69379
69478
  return true;
69380
69479
  return unit.tokenCount >= minTokens;
69381
69480
  }
69382
- async function collectDuplicateUnits(index, options) {
69481
+ async function collectDuplicateUnits(index, options, progress) {
69383
69482
  const files = options.files ?? Array.from(index.byFile.values(), (module2) => module2.file);
69384
69483
  const filesByIdentity = /* @__PURE__ */ new Map();
69385
69484
  for (const file2 of files) {
@@ -69392,8 +69491,14 @@ async function collectDuplicateUnits(index, options) {
69392
69491
  const variant = duplicateUnitCacheVariant(index, options.minTokens, options.maxTokens, options.shingleSize, options.windowSize, options.projectRoot);
69393
69492
  let belowThresholdUnits = 0;
69394
69493
  const belowThresholdUnitsByFile = /* @__PURE__ */ new Map();
69494
+ let sliceStartedAt = performance.now();
69395
69495
  const pendingWrites = [];
69496
+ let processedFiles = 0;
69396
69497
  for (const file2 of normalizedFiles) {
69498
+ if (performance.now() - sliceStartedAt >= EVENT_LOOP_SLICE_MS) {
69499
+ await yieldToEventLoop();
69500
+ sliceStartedAt = performance.now();
69501
+ }
69397
69502
  const fileKey = fileIdentityKey(file2);
69398
69503
  const cacheSignature = duplicateUnitCacheSignature(index, file2, options.projectRoot);
69399
69504
  let source = index.parsed?.get(fileKey)?.source;
@@ -69421,9 +69526,18 @@ async function collectDuplicateUnits(index, options) {
69421
69526
  }
69422
69527
  units.push(unit);
69423
69528
  }
69529
+ if (progress) {
69530
+ processedFiles++;
69531
+ progress.stage(DUPLICATE_STAGE_UNITS, processedFiles, normalizedFiles.length, "file");
69532
+ const pending = progress.yieldPoint();
69533
+ if (pending)
69534
+ await pending;
69535
+ }
69424
69536
  }
69425
- if (pendingWrites.length) {
69426
- writeDuplicateUnitsBatchToCache(index, pendingWrites, options.projectRoot);
69537
+ for (let start = 0; start < pendingWrites.length; start += DUPLICATE_CACHE_WRITE_SLICE_FILES) {
69538
+ const slice = pendingWrites.slice(start, start + DUPLICATE_CACHE_WRITE_SLICE_FILES);
69539
+ writeDuplicateUnitsBatchToCache(index, slice, options.projectRoot);
69540
+ await yieldToEventLoop();
69427
69541
  }
69428
69542
  units.sort((left, right) => {
69429
69543
  const fileCompare = left.absoluteFile.localeCompare(right.absoluteFile);
@@ -69464,7 +69578,7 @@ async function buildDuplicateUnitsForFile(index, file2, projectRoot, minTokens,
69464
69578
  const chunkUnits = makeChunkUnits(file2, chunks, projectRoot, shingleSize, windowSize, astContext);
69465
69579
  return [...symbolUnits, ...chunkUnits];
69466
69580
  }
69467
- var DEFAULT_MAX_FINGERPRINTS, textLanguageByExtension, symbolUnitKinds, duplicateImportStatementQueries, graphOnlyImportStatementPatterns;
69581
+ var DEFAULT_MAX_FINGERPRINTS, textLanguageByExtension, symbolUnitKinds, duplicateImportStatementQueries, graphOnlyImportStatementPatterns, DUPLICATE_CACHE_WRITE_SLICE_FILES;
69468
69582
  var init_units = __esm({
69469
69583
  "dist/duplicates/units.js"() {
69470
69584
  "use strict";
@@ -69482,8 +69596,10 @@ var init_units = __esm({
69482
69596
  init_tree_sitter_native();
69483
69597
  init_comments();
69484
69598
  init_lines();
69599
+ init_concurrency();
69485
69600
  init_paths();
69486
69601
  init_unit_cache();
69602
+ init_progress2();
69487
69603
  DEFAULT_MAX_FINGERPRINTS = 128;
69488
69604
  textLanguageByExtension = {
69489
69605
  ".json": "json",
@@ -69555,6 +69671,7 @@ var init_units = __esm({
69555
69671
  astro: /^[\t ]*import\b(?![\t ]*\()(?:[\t ]*["'][^"']*["']|[\s\S]*?\bfrom[\t ]*["'][^"']*["']|[\s\S]*?;)/gmu,
69556
69672
  mdx: /^[\t ]*import\b(?![\t ]*\()(?:[\t ]*["'][^"']*["']|[\s\S]*?\bfrom[\t ]*["'][^"']*["']|[\s\S]*?;)/gmu
69557
69673
  };
69674
+ DUPLICATE_CACHE_WRITE_SLICE_FILES = 25;
69558
69675
  }
69559
69676
  });
69560
69677
 
@@ -69564,27 +69681,16 @@ __export(scoring_exports, {
69564
69681
  DEFAULT_GROUP_VARIANT_LIMIT: () => DEFAULT_GROUP_VARIANT_LIMIT,
69565
69682
  DEFAULT_LIMIT: () => DEFAULT_LIMIT,
69566
69683
  DEFAULT_MAX_BUCKET_SIZE: () => DEFAULT_MAX_BUCKET_SIZE,
69684
+ DEFAULT_MAX_CANDIDATE_PAIRS: () => DEFAULT_MAX_CANDIDATE_PAIRS,
69567
69685
  DEFAULT_MAX_TOKENS: () => DEFAULT_MAX_TOKENS,
69568
69686
  DEFAULT_MIN_TOKENS: () => DEFAULT_MIN_TOKENS,
69569
69687
  DEFAULT_SHINGLE_SIZE: () => DEFAULT_SHINGLE_SIZE,
69570
69688
  DEFAULT_WINDOW_SIZE: () => DEFAULT_WINDOW_SIZE,
69571
69689
  GROUP_PRIMARY_LENGTH_RATIO_FLOOR: () => GROUP_PRIMARY_LENGTH_RATIO_FLOOR,
69572
- MIN_SIMILARITY_HINT_INDEX: () => MIN_SIMILARITY_HINT_INDEX,
69690
+ MAX_CANDIDATE_PAIRS_LIMIT: () => MAX_CANDIDATE_PAIRS_LIMIT,
69573
69691
  NEARBY_CHUNK_VARIANT_MAX_GAP: () => NEARBY_CHUNK_VARIANT_MAX_GAP,
69574
- addAlignedSimilarityHintPairs: () => addAlignedSimilarityHintPairs,
69575
- addBucketPairs: () => addBucketPairs,
69576
- addBucketsToPairs: () => addBucketsToPairs,
69577
- addConsideredSignature: () => addConsideredSignature,
69578
- addPairEvidence: () => addPairEvidence,
69579
- addSignatureBucketsToPairs: () => addSignatureBucketsToPairs,
69580
- addSimilarityHintPair: () => addSimilarityHintPair,
69581
- addSimilarityHintPairs: () => addSimilarityHintPairs,
69582
- addToBucket: () => addToBucket,
69583
69692
  bestCloneType: () => bestCloneType,
69584
69693
  bestConfidence: () => bestConfidence,
69585
- bucketPairCountExceeds: () => bucketPairCountExceeds,
69586
- buildCandidatePairs: () => buildCandidatePairs,
69587
- buildCandidatePairsFromPreparedBuckets: () => buildCandidatePairsFromPreparedBuckets,
69588
69694
  cleanupLabelsForGroup: () => cleanupLabelsForGroup,
69589
69695
  cloneTypeForPair: () => cloneTypeForPair,
69590
69696
  cloneTypeRank: () => cloneTypeRank,
@@ -69603,20 +69709,15 @@ __export(scoring_exports, {
69603
69709
  duplicateUnitRefQuality: () => duplicateUnitRefQuality,
69604
69710
  enrichDuplicateGroups: () => enrichDuplicateGroups,
69605
69711
  estimatedLinesSavedForLocations: () => estimatedLinesSavedForLocations,
69606
- filterPreparedDuplicateBucketMap: () => filterPreparedDuplicateBucketMap,
69607
- filterPreparedDuplicateBuckets: () => filterPreparedDuplicateBuckets,
69608
- forEachBucketPair: () => forEachBucketPair,
69609
69712
  groupClusterSummary: () => groupClusterSummary,
69610
69713
  groupForSuggestions: () => groupForSuggestions,
69611
69714
  groupLocations: () => groupLocations,
69612
69715
  groupLooksLikeBarrelNoise: () => groupLooksLikeBarrelNoise,
69613
69716
  groupLooksLikeImportListNoise: () => groupLooksLikeImportListNoise,
69614
69717
  groupSuggestions: () => groupSuggestions,
69615
- hasEnoughSharedFingerprints: () => hasEnoughSharedFingerprints,
69616
- hasEnoughSharedFingerprintsFor: () => hasEnoughSharedFingerprintsFor,
69617
- hasLineOverlap: () => hasLineOverlap,
69618
69718
  isFixtureLikeFile: () => isFixtureLikeFile,
69619
69719
  isLocationClique: () => isLocationClique,
69720
+ isScorablePair: () => isScorablePair,
69620
69721
  isTestFile: () => isTestFile,
69621
69722
  lineOverlap: () => lineOverlap,
69622
69723
  locationsForSuggestions: () => locationsForSuggestions,
@@ -69627,21 +69728,16 @@ __export(scoring_exports, {
69627
69728
  metricsForPair: () => metricsForPair,
69628
69729
  normalizeConfidence: () => normalizeConfidence,
69629
69730
  normalizeIntegerOption: () => normalizeIntegerOption,
69731
+ normalizeMaxCandidatePairsOption: () => normalizeMaxCandidatePairsOption,
69630
69732
  normalizeNonNegativeIntegerOption: () => normalizeNonNegativeIntegerOption,
69631
69733
  normalizePositiveIntegerOption: () => normalizePositiveIntegerOption,
69632
- normalizeSimilarityHintFile: () => normalizeSimilarityHintFile,
69633
69734
  orderedGroupKey: () => orderedGroupKey,
69634
- orderedPair: () => orderedPair,
69635
69735
  orderedUnitPairKey: () => orderedUnitPairKey,
69636
69736
  orderedUnitRangePairKey: () => orderedUnitRangePairKey,
69637
- pairKey: () => pairKey,
69638
69737
  preferredDuplicateUnitRef: () => preferredDuplicateUnitRef,
69639
- prepareDuplicateCandidateBuckets: () => prepareDuplicateCandidateBuckets,
69640
69738
  reducedLinesForPair: () => reducedLinesForPair,
69641
69739
  scorePair: () => scorePair,
69642
69740
  shouldScoreSignatureEvidence: () => shouldScoreSignatureEvidence,
69643
- similarityAlignmentKey: () => similarityAlignmentKey,
69644
- similarityHintPairCountExceeds: () => similarityHintPairCountExceeds,
69645
69741
  singletonUnitCluster: () => singletonUnitCluster,
69646
69742
  suggestionForPair: () => suggestionForPair,
69647
69743
  suggestionPrimaryRank: () => suggestionPrimaryRank,
@@ -69686,14 +69782,6 @@ function ratio(left, right) {
69686
69782
  return 0;
69687
69783
  return Math.min(left, right) / Math.max(left, right);
69688
69784
  }
69689
- function normalizeSimilarityIndex(value) {
69690
- if (!Number.isFinite(value))
69691
- return void 0;
69692
- const bounded = Math.max(0, Math.min(100, Math.round(value)));
69693
- if (bounded < MIN_SIMILARITY_HINT_INDEX)
69694
- return void 0;
69695
- return bounded;
69696
- }
69697
69785
  function jaccard(left, right) {
69698
69786
  if (!left.size && !right.size)
69699
69787
  return 1;
@@ -69723,6 +69811,13 @@ function normalizePositiveIntegerOption(value, optionName2, fallback) {
69723
69811
  function normalizeNonNegativeIntegerOption(value, optionName2, fallback) {
69724
69812
  return normalizeIntegerOption(value, optionName2, fallback, 0, "a non-negative integer");
69725
69813
  }
69814
+ function normalizeMaxCandidatePairsOption(value) {
69815
+ const resolved = normalizePositiveIntegerOption(value, "maxCandidatePairs", DEFAULT_MAX_CANDIDATE_PAIRS);
69816
+ if (resolved > MAX_CANDIDATE_PAIRS_LIMIT) {
69817
+ throw new Error(`Invalid maxCandidatePairs value "${resolved}". Expected an integer from 1 to ${MAX_CANDIDATE_PAIRS_LIMIT}.`);
69818
+ }
69819
+ return resolved;
69820
+ }
69726
69821
  function bestConfidence(left, right) {
69727
69822
  if (confidenceRank[left] >= confidenceRank[right])
69728
69823
  return left;
@@ -69759,182 +69854,15 @@ function shouldScoreSignatureEvidence(evidence, metrics) {
69759
69854
  }
69760
69855
  return metrics.shingleOverlap >= 0.55;
69761
69856
  }
69762
- function pairKey(left, right) {
69763
- if (left.id < right.id)
69764
- return `${left.id}\0${right.id}`;
69765
- return `${right.id}\0${left.id}`;
69766
- }
69767
- function orderedPair(left, right) {
69768
- if (left.absoluteFile < right.absoluteFile)
69769
- return [left, right];
69770
- if (left.absoluteFile > right.absoluteFile)
69771
- return [right, left];
69772
- if (left.startLine <= right.startLine)
69773
- return [left, right];
69774
- return [right, left];
69775
- }
69776
69857
  function hasLineOverlap(left, right) {
69777
69858
  if (left.absoluteFile !== right.absoluteFile)
69778
69859
  return false;
69779
69860
  return left.startLine <= right.endLine && right.startLine <= left.endLine;
69780
69861
  }
69781
- function addPairEvidence(pairs, evidenceKind, left, right) {
69782
- const key = pairKey(left, right);
69783
- const existing = pairs.get(key);
69784
- if (existing) {
69785
- if (evidenceKind === "signature") {
69786
- existing.signatureMatches++;
69787
- } else {
69788
- existing[evidenceKind] = true;
69789
- }
69790
- return;
69791
- }
69792
- pairs.set(key, {
69793
- left,
69794
- right,
69795
- rawHash: evidenceKind === "rawHash",
69796
- normalizedHash: evidenceKind === "normalizedHash",
69797
- astShape: evidenceKind === "astShape",
69798
- signature: false,
69799
- signatureMatches: evidenceKind === "signature" ? 1 : 0
69800
- });
69801
- }
69802
- function forEachBucketPair(bucket, pairFilter, unitFilter, visit2) {
69803
- if (unitFilter) {
69804
- const targetUnits = bucket.filter(unitFilter);
69805
- const seenPairs = /* @__PURE__ */ new Set();
69806
- for (const targetUnit of targetUnits) {
69807
- for (const otherUnit of bucket) {
69808
- if (targetUnit.id === otherUnit.id)
69809
- continue;
69810
- const [left, right] = orderedPair(targetUnit, otherUnit);
69811
- const key = pairKey(left, right);
69812
- if (seenPairs.has(key))
69813
- continue;
69814
- seenPairs.add(key);
69815
- if (pairFilter && !pairFilter(left, right))
69816
- continue;
69817
- visit2(left, right);
69818
- }
69819
- }
69820
- return;
69821
- }
69822
- for (let i = 0; i < bucket.length; i++) {
69823
- for (let j = i + 1; j < bucket.length; j++) {
69824
- const [left, right] = orderedPair(bucket[i], bucket[j]);
69825
- if (pairFilter && !pairFilter(left, right))
69826
- continue;
69827
- visit2(left, right);
69828
- }
69829
- }
69830
- }
69831
- function addBucketPairs(bucket, pairs, evidenceKind, pairFilter, unitFilter) {
69832
- forEachBucketPair(bucket, pairFilter, unitFilter, (left, right) => {
69833
- addPairEvidence(pairs, evidenceKind, left, right);
69834
- });
69835
- }
69836
- function bucketPairCountExceeds(bucket, limit, pairFilter, unitFilter) {
69837
- let count = 0;
69838
- const leftUnits = unitFilter ? bucket.filter(unitFilter) : bucket;
69839
- if (!leftUnits.length)
69862
+ function isScorablePair(left, right, crossFileOnly) {
69863
+ if (crossFileOnly && left.absoluteFile === right.absoluteFile)
69840
69864
  return false;
69841
- const seenPairs = /* @__PURE__ */ new Set();
69842
- for (const leftUnit of leftUnits) {
69843
- for (const rightUnit of bucket) {
69844
- if (leftUnit.id === rightUnit.id)
69845
- continue;
69846
- const [left, right] = orderedPair(leftUnit, rightUnit);
69847
- const key = pairKey(left, right);
69848
- if (seenPairs.has(key))
69849
- continue;
69850
- seenPairs.add(key);
69851
- if (!pairFilter(left, right))
69852
- continue;
69853
- count++;
69854
- if (count > limit)
69855
- return true;
69856
- }
69857
- }
69858
- return false;
69859
- }
69860
- function addBucketsToPairs(buckets, pairs, evidenceKind, maxBucketSize, pairFilter, unitFilter) {
69861
- let oversizedBuckets = 0;
69862
- for (const bucket of buckets.values()) {
69863
- if (bucket.length < 2)
69864
- continue;
69865
- if (unitFilter && !bucket.some(unitFilter))
69866
- continue;
69867
- if (bucket.length > maxBucketSize) {
69868
- if (!pairFilter || bucketPairCountExceeds(bucket, maxBucketSize, pairFilter, unitFilter)) {
69869
- oversizedBuckets++;
69870
- continue;
69871
- }
69872
- }
69873
- addBucketPairs(bucket, pairs, evidenceKind, pairFilter, unitFilter);
69874
- }
69875
- return oversizedBuckets;
69876
- }
69877
- function addConsideredSignature(consideredSignaturesByUnit, unit, signature) {
69878
- const signatures = consideredSignaturesByUnit.get(unit.id);
69879
- if (signatures) {
69880
- signatures.add(signature);
69881
- return;
69882
- }
69883
- consideredSignaturesByUnit.set(unit.id, /* @__PURE__ */ new Set([signature]));
69884
- }
69885
- function addSignatureBucketsToPairs(buckets, pairs, consideredSignaturesByUnit, maxBucketSize, pairFilter, unitFilter) {
69886
- const signatureMatchCounts = /* @__PURE__ */ new Map();
69887
- const signatureMatchUnits = /* @__PURE__ */ new Map();
69888
- let oversizedBuckets = 0;
69889
- for (const [signature, bucket] of buckets) {
69890
- if (bucket.length < 2)
69891
- continue;
69892
- if (unitFilter && !bucket.some(unitFilter))
69893
- continue;
69894
- if (bucket.length > maxBucketSize) {
69895
- if (!pairFilter || bucketPairCountExceeds(bucket, maxBucketSize, pairFilter, unitFilter)) {
69896
- oversizedBuckets++;
69897
- continue;
69898
- }
69899
- }
69900
- for (const unit of bucket) {
69901
- addConsideredSignature(consideredSignaturesByUnit, unit, signature);
69902
- }
69903
- forEachBucketPair(bucket, pairFilter, unitFilter, (left, right) => {
69904
- const key = pairKey(left, right);
69905
- const next = (signatureMatchCounts.get(key) ?? 0) + 1;
69906
- signatureMatchCounts.set(key, next);
69907
- if (next === 2 && !pairs.has(key) && !signatureMatchUnits.has(key)) {
69908
- signatureMatchUnits.set(key, [left, right]);
69909
- }
69910
- });
69911
- }
69912
- for (const [key, count] of signatureMatchCounts) {
69913
- const existing = pairs.get(key);
69914
- if (existing) {
69915
- existing.signatureMatches = count;
69916
- continue;
69917
- }
69918
- if (count < 2)
69919
- continue;
69920
- const units = signatureMatchUnits.get(key);
69921
- if (!units)
69922
- continue;
69923
- const [left, right] = units;
69924
- if (!hasEnoughSharedFingerprintsFor(count, left.id, right.id, consideredSignaturesByUnit)) {
69925
- continue;
69926
- }
69927
- pairs.set(key, {
69928
- left,
69929
- right,
69930
- rawHash: false,
69931
- normalizedHash: false,
69932
- astShape: false,
69933
- signature: false,
69934
- signatureMatches: count
69935
- });
69936
- }
69937
- return oversizedBuckets;
69865
+ return !hasLineOverlap(left, right);
69938
69866
  }
69939
69867
  function scorePair(evidence, metrics) {
69940
69868
  let score = 0;
@@ -69993,222 +69921,6 @@ function metricsForPair(evidence) {
69993
69921
  }
69994
69922
  return metrics;
69995
69923
  }
69996
- function addToBucket(buckets, key, unit) {
69997
- appendToArrayMap(buckets, key, unit);
69998
- }
69999
- function prepareDuplicateCandidateBuckets(units) {
70000
- const rawHashBuckets = /* @__PURE__ */ new Map();
70001
- const normalizedHashBuckets = /* @__PURE__ */ new Map();
70002
- const astShapeBuckets = /* @__PURE__ */ new Map();
70003
- const signatureBuckets = /* @__PURE__ */ new Map();
70004
- for (const unit of units) {
70005
- const languagePrefix = `${unit.languageId}:`;
70006
- addToBucket(rawHashBuckets, `${languagePrefix}${unit.rawHash}`, unit);
70007
- addToBucket(normalizedHashBuckets, `${languagePrefix}${unit.normalizedHash}`, unit);
70008
- if (unit.astShapeHash !== void 0) {
70009
- addToBucket(astShapeBuckets, `${languagePrefix}${unit.astShapeHash}`, unit);
70010
- }
70011
- for (const signature of unit.signatures) {
70012
- addToBucket(signatureBuckets, `${languagePrefix}${signature}`, unit);
70013
- }
70014
- }
70015
- return {
70016
- units,
70017
- rawHashBuckets,
70018
- normalizedHashBuckets,
70019
- astShapeBuckets,
70020
- signatureBuckets
70021
- };
70022
- }
70023
- function filterPreparedDuplicateBucketMap(buckets, unitFilter) {
70024
- const filteredBuckets = /* @__PURE__ */ new Map();
70025
- for (const [key, bucket] of buckets) {
70026
- const filteredBucket = bucket.filter(unitFilter);
70027
- if (filteredBucket.length)
70028
- filteredBuckets.set(key, filteredBucket);
70029
- }
70030
- return filteredBuckets;
70031
- }
70032
- function filterPreparedDuplicateBuckets(preparedBuckets, unitFilter) {
70033
- return {
70034
- units: preparedBuckets.units.filter(unitFilter),
70035
- rawHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.rawHashBuckets, unitFilter),
70036
- normalizedHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.normalizedHashBuckets, unitFilter),
70037
- astShapeBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.astShapeBuckets, unitFilter),
70038
- signatureBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.signatureBuckets, unitFilter)
70039
- };
70040
- }
70041
- function buildCandidatePairsFromPreparedBuckets(preparedBuckets, maxBucketSize, similarityHints, projectRoot, pairFilter, unitFilter) {
70042
- const pairs = /* @__PURE__ */ new Map();
70043
- const consideredSignaturesByUnit = /* @__PURE__ */ new Map();
70044
- let oversizedBuckets = 0;
70045
- oversizedBuckets += addBucketsToPairs(preparedBuckets.rawHashBuckets, pairs, "rawHash", maxBucketSize, pairFilter, unitFilter);
70046
- oversizedBuckets += addBucketsToPairs(preparedBuckets.normalizedHashBuckets, pairs, "normalizedHash", maxBucketSize, pairFilter, unitFilter);
70047
- oversizedBuckets += addBucketsToPairs(preparedBuckets.astShapeBuckets, pairs, "astShape", maxBucketSize, pairFilter, unitFilter);
70048
- oversizedBuckets += addSimilarityHintPairs(preparedBuckets.units, pairs, similarityHints, projectRoot, maxBucketSize, pairFilter);
70049
- oversizedBuckets += addSignatureBucketsToPairs(preparedBuckets.signatureBuckets, pairs, consideredSignaturesByUnit, maxBucketSize, pairFilter, unitFilter);
70050
- for (const [key, evidence] of pairs) {
70051
- if (hasEnoughSharedFingerprints(evidence, consideredSignaturesByUnit)) {
70052
- evidence.signature = true;
70053
- continue;
70054
- }
70055
- if (!evidence.rawHash && !evidence.normalizedHash && !evidence.astShape && evidence.gitSimilarity === void 0) {
70056
- pairs.delete(key);
70057
- }
70058
- }
70059
- return { pairs, oversizedBuckets };
70060
- }
70061
- function buildCandidatePairs(units, maxBucketSize, similarityHints, projectRoot, pairFilter, unitFilter) {
70062
- return buildCandidatePairsFromPreparedBuckets(prepareDuplicateCandidateBuckets(units), maxBucketSize, similarityHints, projectRoot, pairFilter, unitFilter);
70063
- }
70064
- function addSimilarityHintPairs(units, pairs, similarityHints, projectRoot, maxBucketSize, pairFilter) {
70065
- if (!similarityHints?.length)
70066
- return 0;
70067
- const unitsByFile = /* @__PURE__ */ new Map();
70068
- for (const unit of units) {
70069
- const bucket = unitsByFile.get(unit.absoluteFile);
70070
- if (bucket) {
70071
- bucket.push(unit);
70072
- } else {
70073
- unitsByFile.set(unit.absoluteFile, [unit]);
70074
- }
70075
- }
70076
- let oversizedHints = 0;
70077
- for (const hint of similarityHints) {
70078
- const similarityIndex = normalizeSimilarityIndex(hint.similarityIndex);
70079
- if (similarityIndex === void 0)
70080
- continue;
70081
- const leftFile = normalizeSimilarityHintFile(hint.leftFile, projectRoot);
70082
- const rightFile = normalizeSimilarityHintFile(hint.rightFile, projectRoot);
70083
- if (!leftFile || !rightFile || leftFile === rightFile)
70084
- continue;
70085
- const leftUnits = unitsByFile.get(leftFile);
70086
- const rightUnits = unitsByFile.get(rightFile);
70087
- if (!leftUnits?.length || !rightUnits?.length)
70088
- continue;
70089
- if (similarityHintPairCountExceeds(leftUnits, rightUnits, maxBucketSize, pairFilter)) {
70090
- oversizedHints++;
70091
- addAlignedSimilarityHintPairs(pairs, leftUnits, rightUnits, similarityIndex, maxBucketSize, pairFilter);
70092
- continue;
70093
- }
70094
- for (const leftUnit of leftUnits) {
70095
- for (const rightUnit of rightUnits) {
70096
- if (leftUnit.languageId !== rightUnit.languageId)
70097
- continue;
70098
- const [left, right] = orderedPair(leftUnit, rightUnit);
70099
- if (pairFilter && !pairFilter(left, right))
70100
- continue;
70101
- addSimilarityHintPair(pairs, left, right, similarityIndex);
70102
- }
70103
- }
70104
- }
70105
- return oversizedHints;
70106
- }
70107
- function similarityHintPairCountExceeds(leftUnits, rightUnits, limit, pairFilter) {
70108
- let count = 0;
70109
- for (const leftUnit of leftUnits) {
70110
- for (const rightUnit of rightUnits) {
70111
- if (leftUnit.languageId !== rightUnit.languageId)
70112
- continue;
70113
- const [left, right] = orderedPair(leftUnit, rightUnit);
70114
- if (pairFilter && !pairFilter(left, right))
70115
- continue;
70116
- count++;
70117
- if (count > limit)
70118
- return true;
70119
- }
70120
- }
70121
- return false;
70122
- }
70123
- function similarityAlignmentKey(unit) {
70124
- return [unit.languageId, unit.kind, unit.symbolKind ?? ""].join(":");
70125
- }
70126
- function addAlignedSimilarityHintPairs(pairs, leftUnits, rightUnits, similarityIndex, maxPairs, pairFilter) {
70127
- const rightByKey = /* @__PURE__ */ new Map();
70128
- for (const unit of rightUnits) {
70129
- const key = similarityAlignmentKey(unit);
70130
- const bucket = rightByKey.get(key);
70131
- if (bucket)
70132
- bucket.push(unit);
70133
- else
70134
- rightByKey.set(key, [unit]);
70135
- }
70136
- for (const bucket of rightByKey.values()) {
70137
- bucket.sort(compareUnitRefs);
70138
- }
70139
- const leftByKey = /* @__PURE__ */ new Map();
70140
- for (const unit of leftUnits) {
70141
- const key = similarityAlignmentKey(unit);
70142
- const bucket = leftByKey.get(key);
70143
- if (bucket)
70144
- bucket.push(unit);
70145
- else
70146
- leftByKey.set(key, [unit]);
70147
- }
70148
- for (const bucket of leftByKey.values()) {
70149
- bucket.sort(compareUnitRefs);
70150
- }
70151
- const seenPairs = /* @__PURE__ */ new Set();
70152
- let addedPairs = 0;
70153
- for (const [key, sortedLeftUnits] of Array.from(leftByKey.entries()).sort(([left], [right]) => left.localeCompare(right))) {
70154
- const sortedRightUnits = rightByKey.get(key);
70155
- if (!sortedRightUnits?.length)
70156
- continue;
70157
- const limit = Math.min(sortedLeftUnits.length, sortedRightUnits.length);
70158
- for (let index = 0; index < limit; index++) {
70159
- if (addedPairs >= maxPairs)
70160
- return;
70161
- const [left, right] = orderedPair(sortedLeftUnits[index], sortedRightUnits[index]);
70162
- const pairId = pairKey(left, right);
70163
- if (seenPairs.has(pairId))
70164
- continue;
70165
- seenPairs.add(pairId);
70166
- if (pairFilter && !pairFilter(left, right))
70167
- continue;
70168
- addSimilarityHintPair(pairs, left, right, similarityIndex);
70169
- addedPairs++;
70170
- }
70171
- }
70172
- }
70173
- function normalizeSimilarityHintFile(file2, projectRoot) {
70174
- try {
70175
- return normalizeDetectionFile(file2, projectRoot);
70176
- } catch {
70177
- return void 0;
70178
- }
70179
- }
70180
- function addSimilarityHintPair(pairs, left, right, similarityIndex) {
70181
- const key = pairKey(left, right);
70182
- const existing = pairs.get(key);
70183
- if (existing) {
70184
- existing.gitSimilarity = Math.max(existing.gitSimilarity ?? 0, similarityIndex);
70185
- return;
70186
- }
70187
- pairs.set(key, {
70188
- left,
70189
- right,
70190
- rawHash: false,
70191
- normalizedHash: false,
70192
- astShape: false,
70193
- gitSimilarity: similarityIndex,
70194
- signature: false,
70195
- signatureMatches: 0
70196
- });
70197
- }
70198
- function hasEnoughSharedFingerprintsFor(signatureMatches, leftUnitId, rightUnitId, consideredSignaturesByUnit) {
70199
- if (!signatureMatches)
70200
- return false;
70201
- const leftConsideredSignatures = consideredSignaturesByUnit.get(leftUnitId)?.size ?? 0;
70202
- const rightConsideredSignatures = consideredSignaturesByUnit.get(rightUnitId)?.size ?? 0;
70203
- const smallerConsideredSignatureCount = Math.min(leftConsideredSignatures, rightConsideredSignatures);
70204
- if (!smallerConsideredSignatureCount)
70205
- return false;
70206
- const minimumShared = Math.max(2, Math.ceil(smallerConsideredSignatureCount * 0.25));
70207
- return signatureMatches >= minimumShared;
70208
- }
70209
- function hasEnoughSharedFingerprints(evidence, consideredSignaturesByUnit) {
70210
- return hasEnoughSharedFingerprintsFor(evidence.signatureMatches, evidence.left.id, evidence.right.id, consideredSignaturesByUnit);
70211
- }
70212
69924
  function suggestionForPair(evidence) {
70213
69925
  const metrics = metricsForPair(evidence);
70214
69926
  const { score, reasons } = scorePair(evidence, metrics);
@@ -70661,13 +70373,22 @@ function enrichDuplicateGroups(groups) {
70661
70373
  continue;
70662
70374
  appendToArrayMap(groupIdsByComponentId, componentId, group.id);
70663
70375
  }
70376
+ const clusterByComponentId = /* @__PURE__ */ new Map();
70664
70377
  return groups.map((group) => {
70665
70378
  const localLocations = locationsByGroupId.get(group.id) ?? groupLocations(group);
70666
70379
  const primaryKey = unitRefRangeIdentity(group.primaryLeft);
70667
70380
  const componentLocations = componentByKey.get(primaryKey) ?? localLocations;
70668
70381
  const componentId = componentIdByKey.get(primaryKey);
70669
- const groupIds = componentId ? Array.from(new Set(groupIdsByComponentId.get(componentId) ?? [group.id])).sort() : [group.id];
70670
- const cluster = groupClusterSummary(componentLocations, groupIds, coveredPairs);
70382
+ let cluster;
70383
+ if (!componentId) {
70384
+ cluster = groupClusterSummary(componentLocations, [group.id], coveredPairs);
70385
+ } else if (clusterByComponentId.has(componentId)) {
70386
+ cluster = clusterByComponentId.get(componentId);
70387
+ } else {
70388
+ const groupIds = Array.from(new Set(groupIdsByComponentId.get(componentId) ?? [group.id])).sort();
70389
+ cluster = groupClusterSummary(componentLocations, groupIds, coveredPairs);
70390
+ clusterByComponentId.set(componentId, cluster);
70391
+ }
70671
70392
  const cleanupLabels = cleanupLabelsForGroup(group, localLocations);
70672
70393
  if (!cleanupLabels.includes("test-helper-extraction") && cluster) {
70673
70394
  if (cluster.files.length && cluster.files.every(isTestFile) && cluster.locationCount >= 3) {
@@ -70833,7 +70554,7 @@ function groupSuggestions(suggestions, includeRawPairs) {
70833
70554
  const groups = Array.from(suggestionsByGroup, ([key, value]) => groupForSuggestions(key, value.suggestions, value.left, value.right, variantLimit));
70834
70555
  return enrichDuplicateGroups(coalesceDuplicateGroups(groups, variantLimit));
70835
70556
  }
70836
- var DEFAULT_MIN_TOKENS, DEFAULT_MAX_TOKENS, DEFAULT_LIMIT, DEFAULT_MAX_BUCKET_SIZE, DEFAULT_GROUP_VARIANT_LIMIT, DEFAULT_SHINGLE_SIZE, DEFAULT_WINDOW_SIZE, GROUP_PRIMARY_LENGTH_RATIO_FLOOR, NEARBY_CHUNK_VARIANT_MAX_GAP, MIN_SIMILARITY_HINT_INDEX, confidenceRank, cloneTypeRank;
70557
+ var DEFAULT_MIN_TOKENS, DEFAULT_MAX_TOKENS, DEFAULT_LIMIT, DEFAULT_MAX_BUCKET_SIZE, DEFAULT_MAX_CANDIDATE_PAIRS, MAX_CANDIDATE_PAIRS_LIMIT, DEFAULT_GROUP_VARIANT_LIMIT, DEFAULT_SHINGLE_SIZE, DEFAULT_WINDOW_SIZE, GROUP_PRIMARY_LENGTH_RATIO_FLOOR, NEARBY_CHUNK_VARIANT_MAX_GAP, confidenceRank, cloneTypeRank;
70837
70558
  var init_scoring = __esm({
70838
70559
  "dist/duplicates/scoring.js"() {
70839
70560
  "use strict";
@@ -70843,12 +70564,13 @@ var init_scoring = __esm({
70843
70564
  DEFAULT_MAX_TOKENS = 800;
70844
70565
  DEFAULT_LIMIT = 50;
70845
70566
  DEFAULT_MAX_BUCKET_SIZE = 200;
70567
+ DEFAULT_MAX_CANDIDATE_PAIRS = 2e6;
70568
+ MAX_CANDIDATE_PAIRS_LIMIT = 16e6;
70846
70569
  DEFAULT_GROUP_VARIANT_LIMIT = 5;
70847
70570
  DEFAULT_SHINGLE_SIZE = 5;
70848
70571
  DEFAULT_WINDOW_SIZE = 4;
70849
70572
  GROUP_PRIMARY_LENGTH_RATIO_FLOOR = 0.7;
70850
70573
  NEARBY_CHUNK_VARIANT_MAX_GAP = 2;
70851
- MIN_SIMILARITY_HINT_INDEX = 80;
70852
70574
  confidenceRank = {
70853
70575
  low: 1,
70854
70576
  medium: 2,
@@ -70863,6 +70585,827 @@ var init_scoring = __esm({
70863
70585
  }
70864
70586
  });
70865
70587
 
70588
+ // dist/duplicates/candidates.js
70589
+ var candidates_exports = {};
70590
+ __export(candidates_exports, {
70591
+ buildCandidatePairs: () => buildCandidatePairs,
70592
+ filterPreparedDuplicateBuckets: () => filterPreparedDuplicateBuckets,
70593
+ orderedPair: () => orderedPair,
70594
+ prepareDuplicateCandidateBuckets: () => prepareDuplicateCandidateBuckets
70595
+ });
70596
+ function prepareDuplicateCandidateBuckets(units) {
70597
+ const rawHashBuckets = /* @__PURE__ */ new Map();
70598
+ const normalizedHashBuckets = /* @__PURE__ */ new Map();
70599
+ const astShapeBuckets = /* @__PURE__ */ new Map();
70600
+ const signatureBuckets = /* @__PURE__ */ new Map();
70601
+ for (const unit of units) {
70602
+ const languagePrefix = `${unit.languageId}:`;
70603
+ appendToArrayMap(rawHashBuckets, `${languagePrefix}${unit.rawHash}`, unit);
70604
+ appendToArrayMap(normalizedHashBuckets, `${languagePrefix}${unit.normalizedHash}`, unit);
70605
+ if (unit.astShapeHash !== void 0) {
70606
+ appendToArrayMap(astShapeBuckets, `${languagePrefix}${unit.astShapeHash}`, unit);
70607
+ }
70608
+ for (const signature of unit.signatures) {
70609
+ appendToArrayMap(signatureBuckets, `${languagePrefix}${signature}`, unit);
70610
+ }
70611
+ }
70612
+ return {
70613
+ units,
70614
+ rawHashBuckets,
70615
+ normalizedHashBuckets,
70616
+ astShapeBuckets,
70617
+ signatureBuckets
70618
+ };
70619
+ }
70620
+ function filterPreparedDuplicateBucketMap(buckets, unitFilter) {
70621
+ const filteredBuckets = /* @__PURE__ */ new Map();
70622
+ for (const [key, bucket] of buckets) {
70623
+ const filteredBucket = bucket.filter(unitFilter);
70624
+ if (filteredBucket.length)
70625
+ filteredBuckets.set(key, filteredBucket);
70626
+ }
70627
+ return filteredBuckets;
70628
+ }
70629
+ function filterPreparedDuplicateBuckets(preparedBuckets, unitFilter) {
70630
+ return {
70631
+ units: preparedBuckets.units.filter(unitFilter),
70632
+ rawHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.rawHashBuckets, unitFilter),
70633
+ normalizedHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.normalizedHashBuckets, unitFilter),
70634
+ astShapeBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.astShapeBuckets, unitFilter),
70635
+ signatureBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.signatureBuckets, unitFilter)
70636
+ };
70637
+ }
70638
+ function createUnitIndex(units) {
70639
+ const positions = /* @__PURE__ */ new Map();
70640
+ const firstById = /* @__PURE__ */ new Map();
70641
+ const canon = new Int32Array(units.length);
70642
+ let hasSharedIds = false;
70643
+ for (let position = 0; position < units.length; position++) {
70644
+ const unit = units[position];
70645
+ positions.set(unit, position);
70646
+ const first = firstById.get(unit.id);
70647
+ if (first === void 0) {
70648
+ firstById.set(unit.id, position);
70649
+ canon[position] = position;
70650
+ } else {
70651
+ canon[position] = first;
70652
+ hasSharedIds = true;
70653
+ }
70654
+ }
70655
+ return { units, positions, canon, hasSharedIds };
70656
+ }
70657
+ function positionOf(index, unit) {
70658
+ const position = index.positions.get(unit);
70659
+ if (position === void 0)
70660
+ throw new Error(`Duplicate unit ${unit.id} is not part of the prepared candidates.`);
70661
+ return position;
70662
+ }
70663
+ function canonOf(index, unit) {
70664
+ return index.canon[positionOf(index, unit)];
70665
+ }
70666
+ function pairNumber(index, left, right) {
70667
+ const radix = index.units.length;
70668
+ return left <= right ? left * radix + right : right * radix + left;
70669
+ }
70670
+ function orderedPair(left, right) {
70671
+ if (left.absoluteFile < right.absoluteFile)
70672
+ return [left, right];
70673
+ if (left.absoluteFile > right.absoluteFile)
70674
+ return [right, left];
70675
+ if (left.startLine <= right.startLine)
70676
+ return [left, right];
70677
+ return [right, left];
70678
+ }
70679
+ function blankEvidence(left, right) {
70680
+ return { left, right, rawHash: false, normalizedHash: false, astShape: false, signature: false, signatureMatches: 0 };
70681
+ }
70682
+ function tallyUnscorable(state, key) {
70683
+ if (state.unscorable.size < state.limit)
70684
+ state.unscorable.add(key);
70685
+ }
70686
+ function takePair(state, key, first, second) {
70687
+ const existing = state.pairs.get(key);
70688
+ if (existing)
70689
+ return existing;
70690
+ if (!isScorablePair(first, second, state.crossFileOnly)) {
70691
+ tallyUnscorable(state, key);
70692
+ return void 0;
70693
+ }
70694
+ if (state.pairs.size >= state.limit) {
70695
+ state.skipped++;
70696
+ return void 0;
70697
+ }
70698
+ const [left, right] = orderedPair(first, second);
70699
+ const evidence = blankEvidence(left, right);
70700
+ state.pairs.set(key, evidence);
70701
+ return evidence;
70702
+ }
70703
+ function addStrongEvidence(state, kind, first, second, key) {
70704
+ const evidence = takePair(state, key, first, second);
70705
+ if (evidence)
70706
+ evidence[kind] = true;
70707
+ }
70708
+ function forEachFilteredBucketPair(index, bucket, filter, visit2) {
70709
+ const targetUnits = bucket.filter(filter.unit);
70710
+ const seenPairs = /* @__PURE__ */ new Set();
70711
+ for (const targetUnit of targetUnits) {
70712
+ const targetCanon = canonOf(index, targetUnit);
70713
+ for (const otherUnit of bucket) {
70714
+ if (targetUnit.id === otherUnit.id)
70715
+ continue;
70716
+ const [left, right] = orderedPair(targetUnit, otherUnit);
70717
+ const key = pairNumber(index, targetCanon, canonOf(index, otherUnit));
70718
+ if (seenPairs.has(key))
70719
+ continue;
70720
+ seenPairs.add(key);
70721
+ if (!filter.pair(left, right))
70722
+ continue;
70723
+ visit2(left, right, key);
70724
+ }
70725
+ }
70726
+ }
70727
+ function filteredPairCountExceeds(index, bucket, limit, filter) {
70728
+ let count = 0;
70729
+ const targetUnits = bucket.filter(filter.unit);
70730
+ if (!targetUnits.length)
70731
+ return false;
70732
+ const seenPairs = /* @__PURE__ */ new Set();
70733
+ for (const targetUnit of targetUnits) {
70734
+ const targetCanon = canonOf(index, targetUnit);
70735
+ for (const otherUnit of bucket) {
70736
+ if (targetUnit.id === otherUnit.id)
70737
+ continue;
70738
+ const [left, right] = orderedPair(targetUnit, otherUnit);
70739
+ const key = pairNumber(index, targetCanon, canonOf(index, otherUnit));
70740
+ if (seenPairs.has(key))
70741
+ continue;
70742
+ seenPairs.add(key);
70743
+ if (!filter.pair(left, right))
70744
+ continue;
70745
+ count++;
70746
+ if (count > limit)
70747
+ return true;
70748
+ }
70749
+ }
70750
+ return false;
70751
+ }
70752
+ function classifyBucket(index, bucket, maxBucketSize, filter) {
70753
+ if (bucket.length < 2)
70754
+ return "skip";
70755
+ if (filter && !bucket.some(filter.unit))
70756
+ return "skip";
70757
+ if (bucket.length > maxBucketSize) {
70758
+ if (!filter || filteredPairCountExceeds(index, bucket, maxBucketSize, filter))
70759
+ return "oversized";
70760
+ }
70761
+ return "pair";
70762
+ }
70763
+ function addBucketPairs(state, bucket, kind, filter) {
70764
+ const { index } = state;
70765
+ if (filter) {
70766
+ forEachFilteredBucketPair(index, bucket, filter, (left, right, key) => {
70767
+ addStrongEvidence(state, kind, left, right, key);
70768
+ });
70769
+ return;
70770
+ }
70771
+ const canons = new Int32Array(bucket.length);
70772
+ for (let i = 0; i < bucket.length; i++)
70773
+ canons[i] = canonOf(index, bucket[i]);
70774
+ for (let i = 0; i < bucket.length; i++) {
70775
+ for (let j = i + 1; j < bucket.length; j++) {
70776
+ addStrongEvidence(state, kind, bucket[i], bucket[j], pairNumber(index, canons[i], canons[j]));
70777
+ }
70778
+ }
70779
+ }
70780
+ async function addEvidenceBuckets(state, buckets, kind, options) {
70781
+ let oversizedBuckets = 0;
70782
+ for (const bucket of buckets.values()) {
70783
+ const verdict = classifyBucket(state.index, bucket, options.maxBucketSize, options.filter);
70784
+ if (verdict === "skip")
70785
+ continue;
70786
+ if (verdict === "oversized") {
70787
+ oversizedBuckets++;
70788
+ continue;
70789
+ }
70790
+ addBucketPairs(state, bucket, kind, options.filter);
70791
+ const pending = options.yieldPoint?.();
70792
+ if (pending)
70793
+ await pending;
70794
+ }
70795
+ return oversizedBuckets;
70796
+ }
70797
+ function hasEnoughFingerprints(sharedFingerprints, smallerConsideredCount) {
70798
+ if (!sharedFingerprints || !smallerConsideredCount)
70799
+ return false;
70800
+ return sharedFingerprints >= Math.max(2, Math.ceil(smallerConsideredCount * 0.25));
70801
+ }
70802
+ function eligibleFingerprintBuckets(state, buckets, options) {
70803
+ const eligible = [];
70804
+ let oversizedBuckets = 0;
70805
+ for (const bucket of buckets.values()) {
70806
+ const verdict = classifyBucket(state.index, bucket, options.maxBucketSize, options.filter);
70807
+ if (verdict === "skip")
70808
+ continue;
70809
+ if (verdict === "oversized") {
70810
+ oversizedBuckets++;
70811
+ continue;
70812
+ }
70813
+ eligible.push(bucket);
70814
+ }
70815
+ return { eligible, oversizedBuckets };
70816
+ }
70817
+ function layoutFingerprintBuckets(index, eligible) {
70818
+ const unitCount = index.units.length;
70819
+ const bucketCount = eligible.length;
70820
+ const bucketStart = new Int32Array(bucketCount + 1);
70821
+ const rankBase = new Float64Array(bucketCount);
70822
+ let total = 0;
70823
+ let rank = 0;
70824
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
70825
+ const size = eligible[bucket].length;
70826
+ bucketStart[bucket] = total;
70827
+ rankBase[bucket] = rank;
70828
+ total += size;
70829
+ rank += size * size;
70830
+ }
70831
+ bucketStart[bucketCount] = total;
70832
+ const memberUnit = new Int32Array(total);
70833
+ const memberCanon = new Int32Array(total);
70834
+ const entryStart = new Int32Array(unitCount + 1);
70835
+ const consideredCount = new Int32Array(unitCount);
70836
+ const lastBucket = new Int32Array(unitCount).fill(-1);
70837
+ for (let bucket = 0, member = 0; bucket < bucketCount; bucket++) {
70838
+ for (const unit of eligible[bucket]) {
70839
+ const position = positionOf(index, unit);
70840
+ const unitCanon = index.canon[position];
70841
+ memberUnit[member] = position;
70842
+ memberCanon[member] = unitCanon;
70843
+ member++;
70844
+ entryStart[unitCanon + 1] = entryStart[unitCanon + 1] + 1;
70845
+ if (lastBucket[unitCanon] !== bucket) {
70846
+ lastBucket[unitCanon] = bucket;
70847
+ consideredCount[unitCanon] = consideredCount[unitCanon] + 1;
70848
+ }
70849
+ }
70850
+ }
70851
+ for (let unit = 0; unit < unitCount; unit++)
70852
+ entryStart[unit + 1] = entryStart[unit + 1] + entryStart[unit];
70853
+ const entryMember = new Int32Array(total);
70854
+ const entryBucket = new Int32Array(total);
70855
+ const nextEntry = entryStart.slice(0, unitCount);
70856
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
70857
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
70858
+ const unitCanon = memberCanon[member];
70859
+ const slot = nextEntry[unitCanon];
70860
+ nextEntry[unitCanon] = slot + 1;
70861
+ entryMember[slot] = member;
70862
+ entryBucket[slot] = bucket;
70863
+ }
70864
+ }
70865
+ return { bucketStart, rankBase, memberUnit, memberCanon, entryStart, entryMember, entryBucket, consideredCount };
70866
+ }
70867
+ function wholeProjectVisit(layout, units, first, second) {
70868
+ const { bucketStart, rankBase, memberUnit, entryStart, entryMember, entryBucket } = layout;
70869
+ const visits = [];
70870
+ let a = entryStart[first];
70871
+ const aEnd = entryStart[first + 1];
70872
+ let b = entryStart[second];
70873
+ const bEnd = entryStart[second + 1];
70874
+ while (a < aEnd && b < bEnd && visits.length < 2) {
70875
+ const bucketA = entryBucket[a];
70876
+ const bucketB = entryBucket[b];
70877
+ if (bucketA < bucketB) {
70878
+ a++;
70879
+ continue;
70880
+ }
70881
+ if (bucketB < bucketA) {
70882
+ b++;
70883
+ continue;
70884
+ }
70885
+ let aNext = a;
70886
+ while (aNext < aEnd && entryBucket[aNext] === bucketA)
70887
+ aNext++;
70888
+ let bNext = b;
70889
+ while (bNext < bEnd && entryBucket[bNext] === bucketA)
70890
+ bNext++;
70891
+ const start = bucketStart[bucketA];
70892
+ const size = bucketStart[bucketA + 1] - start;
70893
+ for (let x = a; x < aNext; x++) {
70894
+ for (let y = b; y < bNext; y++) {
70895
+ if (first === second && y <= x)
70896
+ continue;
70897
+ const low = Math.min(entryMember[x], entryMember[y]);
70898
+ const high = Math.max(entryMember[x], entryMember[y]);
70899
+ visits.push({ rank: rankBase[bucketA] + (low - start) * size + (high - start), low, high });
70900
+ }
70901
+ }
70902
+ a = aNext;
70903
+ b = bNext;
70904
+ }
70905
+ visits.sort((left2, right2) => left2.rank - right2.rank);
70906
+ const firstVisit = visits[0];
70907
+ const secondVisit = visits[1];
70908
+ const [left, right] = orderedPair(units[memberUnit[secondVisit.low]], units[memberUnit[secondVisit.high]]);
70909
+ return { rank: firstVisit.rank, left, right };
70910
+ }
70911
+ async function addProjectFingerprintPairs(state, layout, options) {
70912
+ const { index } = state;
70913
+ const unitCount = index.units.length;
70914
+ const { bucketStart, memberUnit, memberCanon, entryStart, entryMember, entryBucket } = layout;
70915
+ const selection = new FingerprintSelection(state, layout, void 0);
70916
+ const sharedBuckets = new Int32Array(unitCount);
70917
+ const touched = new Int32Array(unitCount);
70918
+ const sharesIds = index.hasSharedIds;
70919
+ const reportEvery = Math.max(1, Math.floor(unitCount / 100));
70920
+ for (let unit = 0; unit < unitCount; unit++) {
70921
+ const firstEntry = entryStart[unit];
70922
+ const lastEntry = entryStart[unit + 1];
70923
+ let touchedCount = 0;
70924
+ const lowestPartner = sharesIds ? unit : 0;
70925
+ for (let slot = firstEntry; slot < lastEntry; slot++) {
70926
+ const member = entryMember[slot];
70927
+ const bucket = entryBucket[slot];
70928
+ const bucketEnd = bucketStart[bucket + 1];
70929
+ for (let other = member + 1; other < bucketEnd; other++) {
70930
+ const partner = memberCanon[other];
70931
+ if (partner < lowestPartner)
70932
+ continue;
70933
+ const shared = sharedBuckets[partner];
70934
+ if (!shared)
70935
+ touched[touchedCount++] = partner;
70936
+ sharedBuckets[partner] = shared + 1;
70937
+ }
70938
+ if (sharesIds && memberUnit[member] !== unit) {
70939
+ for (let other = bucketStart[bucket]; other < member; other++) {
70940
+ const partner = memberCanon[other];
70941
+ if (partner <= unit)
70942
+ continue;
70943
+ const shared = sharedBuckets[partner];
70944
+ if (!shared)
70945
+ touched[touchedCount++] = partner;
70946
+ sharedBuckets[partner] = shared + 1;
70947
+ }
70948
+ }
70949
+ }
70950
+ for (let at = 0; at < touchedCount; at++) {
70951
+ const partner = touched[at];
70952
+ const count = sharedBuckets[partner];
70953
+ sharedBuckets[partner] = 0;
70954
+ selection.consider(unit, partner, count);
70955
+ }
70956
+ if (unit % reportEvery === 0)
70957
+ options.onFingerprintProgress?.(unit + 1, unitCount);
70958
+ const pending = options.yieldPoint?.();
70959
+ if (pending)
70960
+ await pending;
70961
+ }
70962
+ options.onFingerprintProgress?.(unitCount, unitCount);
70963
+ selection.admit((first, second) => wholeProjectVisit(layout, index.units, first, second));
70964
+ }
70965
+ function targetedVisit(layout, units, targets, first, second) {
70966
+ const { bucketStart, rankBase, memberUnit, entryStart, entryMember, entryBucket } = layout;
70967
+ let a = entryStart[first];
70968
+ const aEnd = entryStart[first + 1];
70969
+ let b = entryStart[second];
70970
+ const bEnd = entryStart[second + 1];
70971
+ const firstIsTarget = targets.isTarget[first] === 1;
70972
+ const secondIsTarget = targets.isTarget[second] === 1;
70973
+ let firstRank = -1;
70974
+ while (a < aEnd && b < bEnd) {
70975
+ const bucket = entryBucket[a];
70976
+ if (bucket < entryBucket[b]) {
70977
+ a++;
70978
+ continue;
70979
+ }
70980
+ if (entryBucket[b] < bucket) {
70981
+ b++;
70982
+ continue;
70983
+ }
70984
+ const slotA = entryMember[a];
70985
+ const slotB = entryMember[b];
70986
+ const walkedFromA = firstIsTarget && (!secondIsTarget || slotA < slotB);
70987
+ const targetSlot = walkedFromA ? slotA : slotB;
70988
+ const otherSlot = walkedFromA ? slotB : slotA;
70989
+ if (firstRank < 0) {
70990
+ const start = bucketStart[bucket];
70991
+ const size = bucketStart[bucket + 1] - start;
70992
+ firstRank = rankBase[bucket] + targets.before[targetSlot] * size + (otherSlot - start);
70993
+ } else {
70994
+ const [left, right] = orderedPair(units[memberUnit[targetSlot]], units[memberUnit[otherSlot]]);
70995
+ return { rank: firstRank, left, right };
70996
+ }
70997
+ while (a < aEnd && entryBucket[a] === bucket)
70998
+ a++;
70999
+ while (b < bEnd && entryBucket[b] === bucket)
71000
+ b++;
71001
+ }
71002
+ throw new Error("A candidate pair from shared fingerprints shares fewer than two buckets.");
71003
+ }
71004
+ function addTargetedFingerprintPairs(state, layout, filter) {
71005
+ const { index } = state;
71006
+ const unitCount = index.units.length;
71007
+ const { bucketStart, memberCanon, entryStart, entryBucket } = layout;
71008
+ const bucketCount = bucketStart.length - 1;
71009
+ const isTarget = new Uint8Array(unitCount);
71010
+ for (let unit = 0; unit < unitCount; unit++) {
71011
+ if (entryStart[unit + 1] > entryStart[unit] && filter.unit(index.units[unit]))
71012
+ isTarget[unit] = 1;
71013
+ }
71014
+ const before = new Int32Array(memberCanon.length);
71015
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
71016
+ let targetsSoFar = 0;
71017
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
71018
+ before[member] = targetsSoFar;
71019
+ targetsSoFar += isTarget[memberCanon[member]];
71020
+ }
71021
+ }
71022
+ const selection = new FingerprintSelection(state, layout, filter.pair);
71023
+ const sharedBuckets = new Int32Array(unitCount);
71024
+ const touched = new Int32Array(unitCount);
71025
+ const countedIn = index.hasSharedIds ? new Int32Array(unitCount) : void 0;
71026
+ let walk = 0;
71027
+ for (let target = 0; target < unitCount; target++) {
71028
+ if (!isTarget[target])
71029
+ continue;
71030
+ let touchedCount = 0;
71031
+ let previousBucket = -1;
71032
+ for (let slot = entryStart[target]; slot < entryStart[target + 1]; slot++) {
71033
+ const bucket = entryBucket[slot];
71034
+ if (bucket === previousBucket)
71035
+ continue;
71036
+ previousBucket = bucket;
71037
+ walk++;
71038
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
71039
+ const partner = memberCanon[member];
71040
+ if (partner === target || partner < target && isTarget[partner])
71041
+ continue;
71042
+ if (countedIn) {
71043
+ if (countedIn[partner] === walk)
71044
+ continue;
71045
+ countedIn[partner] = walk;
71046
+ }
71047
+ const shared = sharedBuckets[partner];
71048
+ if (!shared)
71049
+ touched[touchedCount++] = partner;
71050
+ sharedBuckets[partner] = shared + 1;
71051
+ }
71052
+ }
71053
+ for (let at = 0; at < touchedCount; at++) {
71054
+ const partner = touched[at];
71055
+ const count = sharedBuckets[partner];
71056
+ sharedBuckets[partner] = 0;
71057
+ selection.consider(target, partner, count);
71058
+ }
71059
+ }
71060
+ selection.admit((first, second) => targetedVisit(layout, index.units, { isTarget, before }, first, second));
71061
+ }
71062
+ function normalizeSimilarityIndex(value) {
71063
+ if (!Number.isFinite(value))
71064
+ return void 0;
71065
+ const bounded = Math.max(0, Math.min(100, Math.round(value)));
71066
+ if (bounded < MIN_SIMILARITY_HINT_INDEX)
71067
+ return void 0;
71068
+ return bounded;
71069
+ }
71070
+ function normalizeSimilarityHintFile(file2, projectRoot) {
71071
+ try {
71072
+ return normalizeDetectionFile(file2, projectRoot);
71073
+ } catch {
71074
+ return void 0;
71075
+ }
71076
+ }
71077
+ function addSimilarityHintPair(state, left, right, similarityIndex) {
71078
+ const key = pairNumber(state.index, canonOf(state.index, left), canonOf(state.index, right));
71079
+ const evidence = takePair(state, key, left, right);
71080
+ if (evidence)
71081
+ evidence.gitSimilarity = Math.max(evidence.gitSimilarity ?? 0, similarityIndex);
71082
+ }
71083
+ function similarityHintPairCountExceeds(leftUnits, rightUnits, limit, pairFilter) {
71084
+ let count = 0;
71085
+ for (const leftUnit of leftUnits) {
71086
+ for (const rightUnit of rightUnits) {
71087
+ if (leftUnit.languageId !== rightUnit.languageId)
71088
+ continue;
71089
+ const [left, right] = orderedPair(leftUnit, rightUnit);
71090
+ if (pairFilter && !pairFilter(left, right))
71091
+ continue;
71092
+ count++;
71093
+ if (count > limit)
71094
+ return true;
71095
+ }
71096
+ }
71097
+ return false;
71098
+ }
71099
+ function similarityAlignmentKey(unit) {
71100
+ return [unit.languageId, unit.kind, unit.symbolKind ?? ""].join(":");
71101
+ }
71102
+ function addAlignedSimilarityHintPairs(state, leftUnits, rightUnits, similarityIndex, maxPairs, pairFilter) {
71103
+ const rightByKey = /* @__PURE__ */ new Map();
71104
+ for (const unit of rightUnits) {
71105
+ const key = similarityAlignmentKey(unit);
71106
+ const bucket = rightByKey.get(key);
71107
+ if (bucket)
71108
+ bucket.push(unit);
71109
+ else
71110
+ rightByKey.set(key, [unit]);
71111
+ }
71112
+ for (const bucket of rightByKey.values()) {
71113
+ bucket.sort(compareUnitRefs);
71114
+ }
71115
+ const leftByKey = /* @__PURE__ */ new Map();
71116
+ for (const unit of leftUnits) {
71117
+ const key = similarityAlignmentKey(unit);
71118
+ const bucket = leftByKey.get(key);
71119
+ if (bucket)
71120
+ bucket.push(unit);
71121
+ else
71122
+ leftByKey.set(key, [unit]);
71123
+ }
71124
+ for (const bucket of leftByKey.values()) {
71125
+ bucket.sort(compareUnitRefs);
71126
+ }
71127
+ const seenPairs = /* @__PURE__ */ new Set();
71128
+ let addedPairs = 0;
71129
+ for (const [key, sortedLeftUnits] of Array.from(leftByKey.entries()).sort(([left], [right]) => left.localeCompare(right))) {
71130
+ const sortedRightUnits = rightByKey.get(key);
71131
+ if (!sortedRightUnits?.length)
71132
+ continue;
71133
+ const limit = Math.min(sortedLeftUnits.length, sortedRightUnits.length);
71134
+ for (let index = 0; index < limit; index++) {
71135
+ if (addedPairs >= maxPairs)
71136
+ return;
71137
+ const [left, right] = orderedPair(sortedLeftUnits[index], sortedRightUnits[index]);
71138
+ const pairId = pairNumber(state.index, canonOf(state.index, left), canonOf(state.index, right));
71139
+ if (seenPairs.has(pairId))
71140
+ continue;
71141
+ seenPairs.add(pairId);
71142
+ if (pairFilter && !pairFilter(left, right))
71143
+ continue;
71144
+ addSimilarityHintPair(state, left, right, similarityIndex);
71145
+ addedPairs++;
71146
+ }
71147
+ }
71148
+ }
71149
+ function addSimilarityHintPairs(state, similarityHints, projectRoot, maxBucketSize, pairFilter) {
71150
+ if (!similarityHints?.length)
71151
+ return 0;
71152
+ const unitsByFile = /* @__PURE__ */ new Map();
71153
+ for (const unit of state.index.units) {
71154
+ const bucket = unitsByFile.get(unit.absoluteFile);
71155
+ if (bucket) {
71156
+ bucket.push(unit);
71157
+ } else {
71158
+ unitsByFile.set(unit.absoluteFile, [unit]);
71159
+ }
71160
+ }
71161
+ let oversizedHints = 0;
71162
+ for (const hint of similarityHints) {
71163
+ const similarityIndex = normalizeSimilarityIndex(hint.similarityIndex);
71164
+ if (similarityIndex === void 0)
71165
+ continue;
71166
+ const leftFile = normalizeSimilarityHintFile(hint.leftFile, projectRoot);
71167
+ const rightFile = normalizeSimilarityHintFile(hint.rightFile, projectRoot);
71168
+ if (!leftFile || !rightFile || leftFile === rightFile)
71169
+ continue;
71170
+ const leftUnits = unitsByFile.get(leftFile);
71171
+ const rightUnits = unitsByFile.get(rightFile);
71172
+ if (!leftUnits?.length || !rightUnits?.length)
71173
+ continue;
71174
+ if (similarityHintPairCountExceeds(leftUnits, rightUnits, maxBucketSize, pairFilter)) {
71175
+ oversizedHints++;
71176
+ addAlignedSimilarityHintPairs(state, leftUnits, rightUnits, similarityIndex, maxBucketSize, pairFilter);
71177
+ continue;
71178
+ }
71179
+ for (const leftUnit of leftUnits) {
71180
+ for (const rightUnit of rightUnits) {
71181
+ if (leftUnit.languageId !== rightUnit.languageId)
71182
+ continue;
71183
+ const [left, right] = orderedPair(leftUnit, rightUnit);
71184
+ if (pairFilter && !pairFilter(left, right))
71185
+ continue;
71186
+ addSimilarityHintPair(state, left, right, similarityIndex);
71187
+ }
71188
+ }
71189
+ }
71190
+ return oversizedHints;
71191
+ }
71192
+ async function buildCandidatePairs(preparedBuckets, options) {
71193
+ const state = {
71194
+ index: createUnitIndex(preparedBuckets.units),
71195
+ crossFileOnly: options.crossFileOnly,
71196
+ pairs: /* @__PURE__ */ new Map(),
71197
+ unscorable: /* @__PURE__ */ new Set(),
71198
+ limit: options.maxCandidatePairs,
71199
+ skipped: 0
71200
+ };
71201
+ let oversizedBuckets = 0;
71202
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.rawHashBuckets, "rawHash", options);
71203
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.normalizedHashBuckets, "normalizedHash", options);
71204
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.astShapeBuckets, "astShape", options);
71205
+ oversizedBuckets += addSimilarityHintPairs(state, options.similarityHints, options.projectRoot, options.maxBucketSize, options.filter?.pair);
71206
+ const fingerprints = eligibleFingerprintBuckets(state, preparedBuckets.signatureBuckets, options);
71207
+ oversizedBuckets += fingerprints.oversizedBuckets;
71208
+ const layout = layoutFingerprintBuckets(state.index, fingerprints.eligible);
71209
+ if (options.filter) {
71210
+ addTargetedFingerprintPairs(state, layout, options.filter);
71211
+ } else {
71212
+ await addProjectFingerprintPairs(state, layout, options);
71213
+ }
71214
+ return {
71215
+ pairs: state.pairs,
71216
+ oversizedBuckets,
71217
+ skippedPairs: state.skipped,
71218
+ unscorablePairs: state.unscorable.size,
71219
+ overlappingPairs: state.crossFileOnly ? 0 : state.unscorable.size
71220
+ };
71221
+ }
71222
+ var MIN_SIMILARITY_HINT_INDEX, BestPairs, FingerprintSelection;
71223
+ var init_candidates = __esm({
71224
+ "dist/duplicates/candidates.js"() {
71225
+ "use strict";
71226
+ init_collections();
71227
+ init_scoring();
71228
+ init_units();
71229
+ MIN_SIMILARITY_HINT_INDEX = 80;
71230
+ BestPairs = class {
71231
+ capacity;
71232
+ size = 0;
71233
+ priority;
71234
+ first;
71235
+ second;
71236
+ matches;
71237
+ constructor(capacity) {
71238
+ this.capacity = capacity;
71239
+ const initial = Math.min(capacity, 1024);
71240
+ this.priority = new Float64Array(initial);
71241
+ this.first = new Int32Array(initial);
71242
+ this.second = new Int32Array(initial);
71243
+ this.matches = new Int32Array(initial);
71244
+ }
71245
+ /** Better means more of the smaller unit's fingerprints shared, then the earlier unit ids. */
71246
+ isWorse(a, b) {
71247
+ if (this.priority[a] !== this.priority[b])
71248
+ return this.priority[a] < this.priority[b];
71249
+ if (this.first[a] !== this.first[b])
71250
+ return this.first[a] > this.first[b];
71251
+ return this.second[a] > this.second[b];
71252
+ }
71253
+ swap(a, b) {
71254
+ const priority = this.priority[a];
71255
+ this.priority[a] = this.priority[b];
71256
+ this.priority[b] = priority;
71257
+ const first = this.first[a];
71258
+ this.first[a] = this.first[b];
71259
+ this.first[b] = first;
71260
+ const second = this.second[a];
71261
+ this.second[a] = this.second[b];
71262
+ this.second[b] = second;
71263
+ const matches = this.matches[a];
71264
+ this.matches[a] = this.matches[b];
71265
+ this.matches[b] = matches;
71266
+ }
71267
+ grow() {
71268
+ const next = Math.min(this.capacity, this.priority.length * 2);
71269
+ const priority = new Float64Array(next);
71270
+ priority.set(this.priority);
71271
+ this.priority = priority;
71272
+ for (const name of ["first", "second", "matches"]) {
71273
+ const grown = new Int32Array(next);
71274
+ grown.set(this[name]);
71275
+ this[name] = grown;
71276
+ }
71277
+ }
71278
+ siftUp(start) {
71279
+ let slot = start;
71280
+ while (slot > 0) {
71281
+ const parent = slot - 1 >> 1;
71282
+ if (!this.isWorse(slot, parent))
71283
+ return;
71284
+ this.swap(slot, parent);
71285
+ slot = parent;
71286
+ }
71287
+ }
71288
+ siftDown(start) {
71289
+ let slot = start;
71290
+ for (; ; ) {
71291
+ const left = slot * 2 + 1;
71292
+ const right = left + 1;
71293
+ let worst = slot;
71294
+ if (left < this.size && this.isWorse(left, worst))
71295
+ worst = left;
71296
+ if (right < this.size && this.isWorse(right, worst))
71297
+ worst = right;
71298
+ if (worst === slot)
71299
+ return;
71300
+ this.swap(slot, worst);
71301
+ slot = worst;
71302
+ }
71303
+ }
71304
+ /** Whether a pair ranks above the worst pair kept, by the same order `isWorse` uses. */
71305
+ beatsWorst(priority, first, second) {
71306
+ if (priority !== this.priority[0])
71307
+ return priority > this.priority[0];
71308
+ if (first !== this.first[0])
71309
+ return first < this.first[0];
71310
+ return second < this.second[0];
71311
+ }
71312
+ offer(priority, first, second, matches) {
71313
+ if (!this.capacity)
71314
+ return;
71315
+ if (this.size === this.capacity) {
71316
+ if (!this.beatsWorst(priority, first, second))
71317
+ return;
71318
+ this.priority[0] = priority;
71319
+ this.first[0] = first;
71320
+ this.second[0] = second;
71321
+ this.matches[0] = matches;
71322
+ this.siftDown(0);
71323
+ return;
71324
+ }
71325
+ if (this.size === this.priority.length)
71326
+ this.grow();
71327
+ const slot = this.size++;
71328
+ this.priority[slot] = priority;
71329
+ this.first[slot] = first;
71330
+ this.second[slot] = second;
71331
+ this.matches[slot] = matches;
71332
+ this.siftUp(slot);
71333
+ }
71334
+ entries() {
71335
+ const entries = [];
71336
+ for (let slot = 0; slot < this.size; slot++) {
71337
+ entries.push({ first: this.first[slot], second: this.second[slot], matches: this.matches[slot] });
71338
+ }
71339
+ return entries;
71340
+ }
71341
+ };
71342
+ FingerprintSelection = class {
71343
+ state;
71344
+ layout;
71345
+ pairFilter;
71346
+ best;
71347
+ qualifying = 0;
71348
+ constructor(state, layout, pairFilter) {
71349
+ this.state = state;
71350
+ this.layout = layout;
71351
+ this.pairFilter = pairFilter;
71352
+ this.best = new BestPairs(Math.max(0, state.limit - state.pairs.size));
71353
+ }
71354
+ /** Takes two canonical units that share `count` eligible buckets; fewer than two cannot qualify. */
71355
+ consider(first, second, count) {
71356
+ if (count < 2)
71357
+ return;
71358
+ const { index, pairs } = this.state;
71359
+ const smaller = Math.min(this.layout.consideredCount[first], this.layout.consideredCount[second]);
71360
+ if (!hasEnoughFingerprints(count, smaller))
71361
+ return;
71362
+ const firstUnit = index.units[first];
71363
+ const secondUnit = index.units[second];
71364
+ if (this.pairFilter) {
71365
+ const [left, right] = orderedPair(firstUnit, secondUnit);
71366
+ if (!this.pairFilter(left, right))
71367
+ return;
71368
+ }
71369
+ const key = pairNumber(index, first, second);
71370
+ const existing = pairs.get(key);
71371
+ if (existing) {
71372
+ existing.signatureMatches = count;
71373
+ existing.signature = true;
71374
+ return;
71375
+ }
71376
+ if (!isScorablePair(firstUnit, secondUnit, this.state.crossFileOnly)) {
71377
+ tallyUnscorable(this.state, key);
71378
+ return;
71379
+ }
71380
+ this.qualifying++;
71381
+ this.best.offer(count / smaller, Math.min(first, second), Math.max(first, second), count);
71382
+ }
71383
+ /** Adds the kept pairs in the order a walk of the buckets would first have met them, and counts the rest skipped. */
71384
+ admit(visitOf) {
71385
+ const { index, pairs } = this.state;
71386
+ const admissions = this.best.entries().map(({ first, second, matches }) => ({
71387
+ ...visitOf(first, second),
71388
+ key: pairNumber(index, first, second),
71389
+ count: matches
71390
+ }));
71391
+ admissions.sort((left, right) => left.rank - right.rank);
71392
+ for (const admission of admissions) {
71393
+ pairs.set(admission.key, {
71394
+ left: admission.left,
71395
+ right: admission.right,
71396
+ rawHash: false,
71397
+ normalizedHash: false,
71398
+ astShape: false,
71399
+ signature: true,
71400
+ signatureMatches: admission.count
71401
+ });
71402
+ }
71403
+ this.state.skipped += this.qualifying - admissions.length;
71404
+ }
71405
+ };
71406
+ }
71407
+ });
71408
+
70866
71409
  // dist/duplicates.js
70867
71410
  async function findDuplicates(index, options = {}) {
70868
71411
  const releaseDuplicateUnitCache = leaseDuplicateUnitCacheForIndex2(index);
@@ -70874,15 +71417,14 @@ async function findDuplicates(index, options = {}) {
70874
71417
  releaseDuplicateUnitCache();
70875
71418
  }
70876
71419
  }
70877
- async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, preparedData) {
70878
- const collectionOptions = duplicateUnitCollectionOptions(index, options);
70879
- const projectRoot = collectionOptions.projectRoot;
70880
- const maxBucketSize = normalizePositiveIntegerOption2(options.maxBucketSize, "maxBucketSize", DEFAULT_MAX_BUCKET_SIZE2);
70881
- const maxPairs = options.maxPairs === void 0 ? Number.POSITIVE_INFINITY : normalizeNonNegativeIntegerOption2(options.maxPairs, "maxPairs", DEFAULT_LIMIT2);
70882
- const crossFileOnly = options.crossFileOnly ?? !(options.includeSameFile ?? false);
70883
- const minConfidence = normalizeConfidence2(options.minConfidence);
70884
- const limit = normalizeNonNegativeIntegerOption2(options.limit, "limit", DEFAULT_LIMIT2);
70885
- const collectedUnits = preparedData?.collectedUnits ?? await collectDuplicateUnits2(index, collectionOptions);
71420
+ function omittedCandidatePairs(uncomparedPairs, budget) {
71421
+ return uncomparedPairs + (budget?.skipped ?? 0);
71422
+ }
71423
+ function foundCandidatePairs(keptPairs, unscorablePairs, maxCandidatePairs) {
71424
+ return Math.min(maxCandidatePairs, keptPairs + unscorablePairs);
71425
+ }
71426
+ async function collectCandidatePairs(index, options, collectionOptions, preparedData, limits, progress) {
71427
+ const collectedUnits = preparedData?.collectedUnits ?? await collectDuplicateUnits2(index, collectionOptions, progress);
70886
71428
  let units = collectedUnits.units;
70887
71429
  let belowThresholdUnits = collectedUnits.belowThresholdUnits;
70888
71430
  let candidateBuckets = preparedData?.preparedBuckets;
@@ -70896,13 +71438,44 @@ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, pre
70896
71438
  belowThresholdUnits += collectedUnits.belowThresholdUnitsByFile.get(file2) ?? 0;
70897
71439
  }
70898
71440
  }
70899
- const candidateResult = candidateBuckets ? buildCandidatePairsFromPreparedBuckets2(candidateBuckets, maxBucketSize, options.similarityHints, projectRoot) : buildCandidatePairs2(units, maxBucketSize, options.similarityHints, projectRoot);
70900
- const { pairs, oversizedBuckets } = candidateResult;
71441
+ progress?.stage(DUPLICATE_STAGE_PAIRS);
71442
+ const { pairs, oversizedBuckets, skippedPairs, unscorablePairs, overlappingPairs } = await buildCandidatePairs2(candidateBuckets ?? prepareDuplicateCandidateBuckets2(units), {
71443
+ ...limits,
71444
+ similarityHints: options.similarityHints,
71445
+ projectRoot: collectionOptions.projectRoot,
71446
+ onFingerprintProgress: progress ? (current, total) => progress.stage(DUPLICATE_STAGE_PAIRS, current, total, "unit") : void 0,
71447
+ yieldPoint: progress?.yieldPoint
71448
+ });
71449
+ return {
71450
+ unitCount: units.length,
71451
+ belowThresholdUnits,
71452
+ pairs,
71453
+ oversizedBuckets,
71454
+ skippedPairs,
71455
+ unscorablePairs,
71456
+ overlappingPairs
71457
+ };
71458
+ }
71459
+ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, preparedData) {
71460
+ const collectionOptions = duplicateUnitCollectionOptions(index, options);
71461
+ const maxBucketSize = normalizePositiveIntegerOption2(options.maxBucketSize, "maxBucketSize", DEFAULT_MAX_BUCKET_SIZE2);
71462
+ const maxCandidatePairs = normalizeMaxCandidatePairsOption2(options.maxCandidatePairs);
71463
+ const maxPairs = options.maxPairs === void 0 ? Number.POSITIVE_INFINITY : normalizeNonNegativeIntegerOption2(options.maxPairs, "maxPairs", DEFAULT_LIMIT2);
71464
+ const crossFileOnly = options.crossFileOnly ?? !(options.includeSameFile ?? false);
71465
+ const minConfidence = normalizeConfidence2(options.minConfidence);
71466
+ const limit = normalizeNonNegativeIntegerOption2(options.limit, "limit", DEFAULT_LIMIT2);
71467
+ const progress = createDuplicateProgress(options.onProgress, options.progressStartedAt);
71468
+ progress?.stage(preparedData ? DUPLICATE_STAGE_PAIRS : DUPLICATE_STAGE_UNITS);
71469
+ const { unitCount, belowThresholdUnits, pairs, oversizedBuckets, skippedPairs, unscorablePairs, overlappingPairs } = await collectCandidatePairs(index, options, collectionOptions, preparedData, { maxBucketSize, maxCandidatePairs, crossFileOnly }, progress);
71470
+ const scorablePairCount = pairs.size;
71471
+ const candidatePairCount = foundCandidatePairs(scorablePairCount, unscorablePairs, maxCandidatePairs);
71472
+ const candidatePairBudget = skippedPairs ? { limit: maxCandidatePairs, skipped: skippedPairs } : void 0;
70901
71473
  if (options.countOnly) {
70902
- const groupCount = countCandidateDuplicateGroups(pairs, crossFileOnly);
71474
+ const groupCount = countCandidateDuplicateGroups(pairs);
71475
+ progress?.complete(unitCount);
70903
71476
  return {
70904
71477
  schemaVersion: 3,
70905
- units: units.length,
71478
+ units: unitCount,
70906
71479
  groups: [],
70907
71480
  omittedCounts: {
70908
71481
  groups: groupCount,
@@ -70910,24 +71483,28 @@ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, pre
70910
71483
  oversizedBuckets,
70911
71484
  belowThresholdUnits,
70912
71485
  overlappingPairs: 0,
70913
- candidatePairs: countComparableCandidatePairs(pairs, crossFileOnly)
71486
+ candidatePairs: omittedCandidatePairs(pairs.size, candidatePairBudget)
70914
71487
  },
70915
71488
  stats: {
70916
71489
  comparedPairs: 0,
70917
- candidatePairs: pairs.size
71490
+ candidatePairs: candidatePairCount,
71491
+ ...candidatePairBudget ? { candidatePairBudget } : {}
70918
71492
  }
70919
71493
  };
70920
71494
  }
70921
71495
  const suggestions = [];
70922
- let overlappingPairs = 0;
70923
71496
  let comparedPairs = 0;
70924
71497
  let skippedCandidatePairs = 0;
71498
+ progress?.stage(DUPLICATE_STAGE_SCORING, 0, scorablePairCount, "pair");
71499
+ const reportEvery = Math.max(1, Math.floor(scorablePairCount / 100));
71500
+ let visitedPairs = 0;
70925
71501
  for (const evidence of pairs.values()) {
70926
- if (crossFileOnly && evidence.left.absoluteFile === evidence.right.absoluteFile)
70927
- continue;
70928
- if (hasLineOverlap2(evidence.left, evidence.right)) {
70929
- overlappingPairs++;
70930
- continue;
71502
+ visitedPairs++;
71503
+ if (progress && visitedPairs % reportEvery === 0) {
71504
+ progress.stage(DUPLICATE_STAGE_SCORING, visitedPairs, scorablePairCount, "pair");
71505
+ const pending = progress.yieldPoint();
71506
+ if (pending)
71507
+ await pending;
70931
71508
  }
70932
71509
  if (comparedPairs >= maxPairs) {
70933
71510
  skippedCandidatePairs++;
@@ -70939,6 +71516,8 @@ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, pre
70939
71516
  continue;
70940
71517
  suggestions.push(suggestion);
70941
71518
  }
71519
+ pairs.clear();
71520
+ progress?.stage(DUPLICATE_STAGE_GROUPING);
70942
71521
  suggestions.sort(compareSuggestions2);
70943
71522
  const includeRawPairs = options.includeRawPairs ?? false;
70944
71523
  const groups = groupSuggestions2(suggestions, includeRawPairs).filter((group) => confidenceRank2[group.confidence] >= confidenceRank2[minConfidence]);
@@ -70947,7 +71526,7 @@ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, pre
70947
71526
  const limitedRawSuggestions = includeRawPairs ? suggestions.slice(0, limit) : [];
70948
71527
  const result = {
70949
71528
  schemaVersion: 3,
70950
- units: units.length,
71529
+ units: unitCount,
70951
71530
  groups: limitedGroups,
70952
71531
  omittedCounts: {
70953
71532
  groups: omittedGroups,
@@ -70955,37 +71534,36 @@ async function findDuplicatesWithOpenDuplicateUnitCache(index, options = {}, pre
70955
71534
  oversizedBuckets,
70956
71535
  belowThresholdUnits,
70957
71536
  overlappingPairs,
70958
- candidatePairs: skippedCandidatePairs
71537
+ candidatePairs: omittedCandidatePairs(skippedCandidatePairs, candidatePairBudget)
70959
71538
  },
70960
71539
  stats: {
70961
71540
  comparedPairs,
70962
- candidatePairs: pairs.size
71541
+ candidatePairs: candidatePairCount,
71542
+ ...candidatePairBudget ? { candidatePairBudget } : {}
70963
71543
  }
70964
71544
  };
70965
71545
  if (includeRawPairs) {
70966
71546
  result.suggestions = limitedRawSuggestions;
70967
71547
  }
71548
+ progress?.complete(unitCount);
70968
71549
  return result;
70969
71550
  }
70970
- function countComparableCandidatePairs(pairs, crossFileOnly) {
70971
- let count = 0;
70972
- for (const evidence of pairs.values()) {
70973
- if (crossFileOnly && evidence.left.absoluteFile === evidence.right.absoluteFile)
70974
- continue;
70975
- if (hasLineOverlap2(evidence.left, evidence.right))
70976
- continue;
70977
- count += 1;
70978
- }
70979
- return count;
70980
- }
70981
- function countCandidateDuplicateGroups(pairs, crossFileOnly) {
71551
+ function countCandidateDuplicateGroups(pairs) {
70982
71552
  const parents = /* @__PURE__ */ new Map();
70983
71553
  const find = (id) => {
70984
- const parent = parents.get(id);
70985
- if (!parent || parent === id)
70986
- return id;
70987
- const root = find(parent);
70988
- parents.set(id, root);
71554
+ let root = id;
71555
+ for (; ; ) {
71556
+ const parent = parents.get(root);
71557
+ if (!parent || parent === root)
71558
+ break;
71559
+ root = parent;
71560
+ }
71561
+ let current = id;
71562
+ while (current !== root) {
71563
+ const next = parents.get(current);
71564
+ parents.set(current, root);
71565
+ current = next;
71566
+ }
70989
71567
  return root;
70990
71568
  };
70991
71569
  const join = (left, right) => {
@@ -70996,10 +71574,6 @@ function countCandidateDuplicateGroups(pairs, crossFileOnly) {
70996
71574
  parents.set(leftRoot, rightRoot);
70997
71575
  };
70998
71576
  for (const evidence of pairs.values()) {
70999
- if (crossFileOnly && evidence.left.absoluteFile === evidence.right.absoluteFile)
71000
- continue;
71001
- if (hasLineOverlap2(evidence.left, evidence.right))
71002
- continue;
71003
71577
  parents.set(evidence.left.id, parents.get(evidence.left.id) ?? evidence.left.id);
71004
71578
  parents.set(evidence.right.id, parents.get(evidence.right.id) ?? evidence.right.id);
71005
71579
  join(evidence.left.id, evidence.right.id);
@@ -71072,7 +71646,9 @@ function duplicateContextFromResult(result, target, options) {
71072
71646
  ...result.omittedCounts,
71073
71647
  groups: omittedGroups,
71074
71648
  rawSuggestions: Math.max(0, rawSuggestions.length - limitedRawSuggestions.length),
71075
- candidatePairs: result.perTargetSkippedCandidateCounts?.get(duplicateTargetKey(normalizedTarget)) ?? 0
71649
+ // The pair limit is shared by every target of one call, so each context carries the whole call's skips,
71650
+ // as it carries the call's `oversizedBuckets` and `overlappingPairs`.
71651
+ candidatePairs: omittedCandidatePairs(result.perTargetSkippedCandidateCounts?.get(duplicateTargetKey(normalizedTarget)) ?? 0, result.stats.candidatePairBudget)
71076
71652
  },
71077
71653
  stats: {
71078
71654
  ...result.stats,
@@ -71160,6 +71736,7 @@ async function findDuplicatesTouchingTargets(index, targets, options, collectedU
71160
71736
  const projectRoot = options.projectRoot ?? index.projectRoot;
71161
71737
  const normalizedTargets = targets.map((target) => normalizeDuplicateTarget(target, projectRoot));
71162
71738
  const maxBucketSize = normalizePositiveIntegerOption2(options.maxBucketSize, "maxBucketSize", DEFAULT_MAX_BUCKET_SIZE2);
71739
+ const maxCandidatePairs = normalizeMaxCandidatePairsOption2(options.maxCandidatePairs);
71163
71740
  const maxPairs = options.maxPairs === void 0 ? Number.POSITIVE_INFINITY : normalizeNonNegativeIntegerOption2(options.maxPairs, "maxPairs", DEFAULT_LIMIT2);
71164
71741
  const crossFileOnly = options.crossFileOnly ?? !(options.includeSameFile ?? false);
71165
71742
  const minConfidence = normalizeConfidence2(options.minConfidence);
@@ -71174,18 +71751,19 @@ async function findDuplicatesTouchingTargets(index, targets, options, collectedU
71174
71751
  }
71175
71752
  const targetsTouchedByPair = (left, right) => normalizedTargets.filter((target) => unitTouchesDuplicateTarget(left, target) || unitTouchesDuplicateTarget(right, target));
71176
71753
  const touchesAnyTarget = (left, right) => targetsTouchedByPair(left, right).length > 0;
71177
- const { pairs, oversizedBuckets } = buildCandidatePairsFromPreparedBuckets2(preparedBuckets ?? prepareDuplicateCandidateBuckets2(units), maxBucketSize, options.similarityHints, projectRoot, touchesAnyTarget, touchesTarget);
71754
+ const { pairs, oversizedBuckets, skippedPairs, unscorablePairs, overlappingPairs } = await buildCandidatePairs2(preparedBuckets ?? prepareDuplicateCandidateBuckets2(units), {
71755
+ maxBucketSize,
71756
+ maxCandidatePairs,
71757
+ crossFileOnly,
71758
+ similarityHints: options.similarityHints,
71759
+ projectRoot,
71760
+ filter: { unit: touchesTarget, pair: touchesAnyTarget }
71761
+ });
71762
+ const candidatePairBudget = skippedPairs ? { limit: maxCandidatePairs, skipped: skippedPairs } : void 0;
71178
71763
  const suggestions = [];
71179
- let overlappingPairs = 0;
71180
71764
  let comparedPairs = 0;
71181
71765
  let skippedCandidatePairs = 0;
71182
71766
  for (const evidence of pairs.values()) {
71183
- if (crossFileOnly && evidence.left.absoluteFile === evidence.right.absoluteFile)
71184
- continue;
71185
- if (hasLineOverlap2(evidence.left, evidence.right)) {
71186
- overlappingPairs++;
71187
- continue;
71188
- }
71189
71767
  const touchedTargets = targetsTouchedByPair(evidence.left, evidence.right);
71190
71768
  if (!touchedTargets.length)
71191
71769
  continue;
@@ -71237,11 +71815,12 @@ async function findDuplicatesTouchingTargets(index, targets, options, collectedU
71237
71815
  oversizedBuckets,
71238
71816
  belowThresholdUnits,
71239
71817
  overlappingPairs,
71240
- candidatePairs: skippedCandidatePairs
71818
+ candidatePairs: omittedCandidatePairs(skippedCandidatePairs, candidatePairBudget)
71241
71819
  },
71242
71820
  stats: {
71243
71821
  comparedPairs,
71244
- candidatePairs: pairs.size
71822
+ candidatePairs: foundCandidatePairs(pairs.size, unscorablePairs, maxCandidatePairs),
71823
+ ...candidatePairBudget ? { candidatePairBudget } : {}
71245
71824
  },
71246
71825
  perTargetCandidateCounts: targetCandidateCounts,
71247
71826
  perTargetComparedCounts: targetCompareCounts,
@@ -71282,17 +71861,21 @@ async function findDuplicateContext(index, target, options = {}) {
71282
71861
  const contexts = await findDuplicateContexts(index, [target], options);
71283
71862
  return contexts[0];
71284
71863
  }
71285
- var collectDuplicateUnits2, normalizeDetectionFile2, DEFAULT_GROUP_VARIANT_LIMIT2, DEFAULT_LIMIT2, DEFAULT_MAX_BUCKET_SIZE2, DEFAULT_MAX_TOKENS2, DEFAULT_MIN_TOKENS2, DEFAULT_SHINGLE_SIZE2, DEFAULT_WINDOW_SIZE2, buildCandidatePairs2, buildCandidatePairsFromPreparedBuckets2, compareSuggestions2, confidenceRank2, filterPreparedDuplicateBuckets2, groupSuggestions2, hasLineOverlap2, lineOverlap2, normalizeConfidence2, normalizeNonNegativeIntegerOption2, normalizePositiveIntegerOption2, prepareDuplicateCandidateBuckets2, suggestionForPair2, suggestionVariantKey2, closeDuplicateUnitCacheForIndex2, leaseDuplicateUnitCacheForIndex2, maintainDuplicateUnitDiskCache2, duplicatePreparedAnalyses;
71864
+ var collectDuplicateUnits2, normalizeDetectionFile2, buildCandidatePairs2, filterPreparedDuplicateBuckets2, prepareDuplicateCandidateBuckets2, DEFAULT_GROUP_VARIANT_LIMIT2, DEFAULT_LIMIT2, DEFAULT_MAX_BUCKET_SIZE2, DEFAULT_MAX_TOKENS2, DEFAULT_MIN_TOKENS2, DEFAULT_SHINGLE_SIZE2, DEFAULT_WINDOW_SIZE2, compareSuggestions2, confidenceRank2, groupSuggestions2, lineOverlap2, normalizeConfidence2, normalizeMaxCandidatePairsOption2, normalizeNonNegativeIntegerOption2, normalizePositiveIntegerOption2, suggestionForPair2, suggestionVariantKey2, closeDuplicateUnitCacheForIndex2, leaseDuplicateUnitCacheForIndex2, maintainDuplicateUnitDiskCache2, duplicatePreparedAnalyses;
71286
71865
  var init_duplicates = __esm({
71287
71866
  "dist/duplicates.js"() {
71288
71867
  "use strict";
71289
71868
  init_paths();
71290
71869
  init_units();
71870
+ init_candidates();
71291
71871
  init_scoring();
71292
71872
  init_unit_cache();
71873
+ init_progress2();
71293
71874
  init_unit_cache();
71875
+ init_scoring();
71294
71876
  ({ collectDuplicateUnits: collectDuplicateUnits2, normalizeDetectionFile: normalizeDetectionFile2 } = units_exports);
71295
- ({ DEFAULT_GROUP_VARIANT_LIMIT: DEFAULT_GROUP_VARIANT_LIMIT2, DEFAULT_LIMIT: DEFAULT_LIMIT2, DEFAULT_MAX_BUCKET_SIZE: DEFAULT_MAX_BUCKET_SIZE2, DEFAULT_MAX_TOKENS: DEFAULT_MAX_TOKENS2, DEFAULT_MIN_TOKENS: DEFAULT_MIN_TOKENS2, DEFAULT_SHINGLE_SIZE: DEFAULT_SHINGLE_SIZE2, DEFAULT_WINDOW_SIZE: DEFAULT_WINDOW_SIZE2, buildCandidatePairs: buildCandidatePairs2, buildCandidatePairsFromPreparedBuckets: buildCandidatePairsFromPreparedBuckets2, compareSuggestions: compareSuggestions2, confidenceRank: confidenceRank2, filterPreparedDuplicateBuckets: filterPreparedDuplicateBuckets2, groupSuggestions: groupSuggestions2, hasLineOverlap: hasLineOverlap2, lineOverlap: lineOverlap2, normalizeConfidence: normalizeConfidence2, normalizeNonNegativeIntegerOption: normalizeNonNegativeIntegerOption2, normalizePositiveIntegerOption: normalizePositiveIntegerOption2, prepareDuplicateCandidateBuckets: prepareDuplicateCandidateBuckets2, suggestionForPair: suggestionForPair2, suggestionVariantKey: suggestionVariantKey2 } = scoring_exports);
71877
+ ({ buildCandidatePairs: buildCandidatePairs2, filterPreparedDuplicateBuckets: filterPreparedDuplicateBuckets2, prepareDuplicateCandidateBuckets: prepareDuplicateCandidateBuckets2 } = candidates_exports);
71878
+ ({ DEFAULT_GROUP_VARIANT_LIMIT: DEFAULT_GROUP_VARIANT_LIMIT2, DEFAULT_LIMIT: DEFAULT_LIMIT2, DEFAULT_MAX_BUCKET_SIZE: DEFAULT_MAX_BUCKET_SIZE2, DEFAULT_MAX_TOKENS: DEFAULT_MAX_TOKENS2, DEFAULT_MIN_TOKENS: DEFAULT_MIN_TOKENS2, DEFAULT_SHINGLE_SIZE: DEFAULT_SHINGLE_SIZE2, DEFAULT_WINDOW_SIZE: DEFAULT_WINDOW_SIZE2, compareSuggestions: compareSuggestions2, confidenceRank: confidenceRank2, groupSuggestions: groupSuggestions2, lineOverlap: lineOverlap2, normalizeConfidence: normalizeConfidence2, normalizeMaxCandidatePairsOption: normalizeMaxCandidatePairsOption2, normalizeNonNegativeIntegerOption: normalizeNonNegativeIntegerOption2, normalizePositiveIntegerOption: normalizePositiveIntegerOption2, suggestionForPair: suggestionForPair2, suggestionVariantKey: suggestionVariantKey2 } = scoring_exports);
71296
71879
  ({ closeDuplicateUnitCacheForIndex: closeDuplicateUnitCacheForIndex2, leaseDuplicateUnitCacheForIndex: leaseDuplicateUnitCacheForIndex2, maintainDuplicateUnitDiskCache: maintainDuplicateUnitDiskCache2 } = unit_cache_exports);
71297
71880
  duplicatePreparedAnalyses = /* @__PURE__ */ new WeakMap();
71298
71881
  }
@@ -73537,27 +74120,27 @@ var init_parsed_cache = __esm({
73537
74120
  });
73538
74121
 
73539
74122
  // dist/indexer/finalize.js
73540
- import { performance as performance4 } from "node:perf_hooks";
74123
+ import { performance as performance5 } from "node:perf_hooks";
73541
74124
  async function finalizeProjectIndex(args) {
73542
74125
  const discoveryTimings = args.opts?.report ? args.opts.report.timings ??= {} : void 0;
73543
74126
  let projectFiles;
73544
74127
  if (args.projectFiles !== void 0) {
73545
74128
  projectFiles = await args.projectFiles;
73546
74129
  } else {
73547
- const metadataDiscoveryStart = performance4.now();
74130
+ const metadataDiscoveryStart = performance5.now();
73548
74131
  projectFiles = await discoverProjectFilesWithGitCandidates(args.projectRoot, {
73549
74132
  ...args.opts?.logLevel ? { logLevel: args.opts.logLevel } : {},
73550
74133
  ...args.knownGitCandidates !== void 0 ? { knownGitCandidates: args.knownGitCandidates } : {},
73551
74134
  ...args.discoveryContext ? { discoveryContext: args.discoveryContext } : {}
73552
74135
  });
73553
74136
  if (discoveryTimings) {
73554
- const metadataDiscoveryMs = Math.round(performance4.now() - metadataDiscoveryStart);
74137
+ const metadataDiscoveryMs = Math.round(performance5.now() - metadataDiscoveryStart);
73555
74138
  discoveryTimings.metadataDiscoveryMs = metadataDiscoveryMs;
73556
74139
  (discoveryTimings.steps ??= []).push({ name: "metadata-discovery", ms: metadataDiscoveryMs });
73557
74140
  }
73558
74141
  }
73559
74142
  if (args.timings)
73560
- args.timings.totalMs = Math.round(performance4.now() - args.totalStart);
74143
+ args.timings.totalMs = Math.round(performance5.now() - args.totalStart);
73561
74144
  const languageExtensions = normalizeLanguageExtensions(args.opts?.languageExtensions);
73562
74145
  const parsed = retainedParsedCache(args.parsedMap, args.opts);
73563
74146
  return {
@@ -73594,7 +74177,7 @@ var init_finalize = __esm({
73594
74177
  // dist/indexer/build-manifest.js
73595
74178
  import fsp27 from "node:fs/promises";
73596
74179
  import path52 from "node:path";
73597
- import { performance as performance5 } from "node:perf_hooks";
74180
+ import { performance as performance6 } from "node:perf_hooks";
73598
74181
  function toManifestFileEntry(entry) {
73599
74182
  if (!entry.sig)
73600
74183
  return void 0;
@@ -73616,22 +74199,22 @@ function serializeDeclaredContainers(projectRoot, index) {
73616
74199
  function recordManifestTimingStep(timings, name, startedAt) {
73617
74200
  if (!timings)
73618
74201
  return;
73619
- (timings.steps ??= []).push({ name, ms: Math.round(performance5.now() - startedAt) });
74202
+ (timings.steps ??= []).push({ name, ms: Math.round(performance6.now() - startedAt) });
73620
74203
  }
73621
74204
  async function writeIndexManifestSnapshot(args) {
73622
74205
  const files = args.files instanceof Map ? Object.fromEntries(args.files) : args.files;
73623
74206
  if (!Object.keys(files).length && !args.allowEmpty)
73624
74207
  return;
73625
- const writeManifestStart = performance5.now();
73626
- const gitHeadStartedAt = performance5.now();
74208
+ const writeManifestStart = performance6.now();
74209
+ const gitHeadStartedAt = performance6.now();
73627
74210
  const lastCommit = await getGitHead(args.projectRoot);
73628
74211
  recordManifestTimingStep(args.timings, "git-head", gitHeadStartedAt);
73629
- const configHashStartedAt = performance5.now();
74212
+ const configHashStartedAt = performance6.now();
73630
74213
  const configHashResult = args.configHash ?? await computeConfigHash(args.projectRoot, args.opts?.logLevel, args.discoveryContext);
73631
74214
  recordManifestTimingStep(args.timings, "config-hash", configHashStartedAt);
73632
74215
  const projectRootMtimeMs = args.symlinkDirectories !== void 0 ? (await fsp27.stat(args.projectRoot)).mtimeMs : void 0;
73633
74216
  const configHash = recordConfigHashResult(args.manifestReport, configHashResult, args.opts?.logLevel);
73634
- const transformStartedAt = performance5.now();
74217
+ const transformStartedAt = performance6.now();
73635
74218
  const manifestData = {
73636
74219
  version: MANIFEST_VERSION,
73637
74220
  projectRoot: path52.resolve(args.projectRoot).replace(/\\/g, "/"),
@@ -73650,16 +74233,16 @@ async function writeIndexManifestSnapshot(args) {
73650
74233
  } : {}
73651
74234
  };
73652
74235
  recordManifestTimingStep(args.timings, "manifest-transform", transformStartedAt);
73653
- const manifestWriteStartedAt = performance5.now();
74236
+ const manifestWriteStartedAt = performance6.now();
73654
74237
  const manifestWritten = await writeManifest2(args.projectRoot, args.opts, manifestData);
73655
74238
  recordManifestTimingStep(args.timings, "manifest-write", manifestWriteStartedAt);
73656
74239
  if (manifestWritten) {
73657
- const cachePruneStartedAt = performance5.now();
74240
+ const cachePruneStartedAt = performance6.now();
73658
74241
  pruneDiskModuleCache(args.projectRoot, Object.keys(files), args.opts);
73659
74242
  recordManifestTimingStep(args.timings, "cache-prune", cachePruneStartedAt);
73660
74243
  }
73661
74244
  if (args.timings) {
73662
- args.timings.writeManifestMs = Math.round(performance5.now() - writeManifestStart);
74245
+ args.timings.writeManifestMs = Math.round(performance6.now() - writeManifestStart);
73663
74246
  }
73664
74247
  }
73665
74248
  var init_build_manifest = __esm({
@@ -73736,7 +74319,7 @@ var init_native_worker_pool = __esm({
73736
74319
  });
73737
74320
 
73738
74321
  // dist/indexer/build-workers.js
73739
- import { performance as performance6 } from "node:perf_hooks";
74322
+ import { performance as performance7 } from "node:perf_hooks";
73740
74323
  function shouldEnableNativeWorkers(opts, fileCount) {
73741
74324
  if (fileCount === 0)
73742
74325
  return false;
@@ -73828,14 +74411,14 @@ async function setupWorkerPool(opts, fileCount) {
73828
74411
  return {
73829
74412
  pool,
73830
74413
  report: report2,
73831
- startTime: pool ? performance6.now() : 0,
74414
+ startTime: pool ? performance7.now() : 0,
73832
74415
  batchSize: NATIVE_WORKER_BATCH_SIZE
73833
74416
  };
73834
74417
  }
73835
74418
  async function teardownWorkerPool(setup, buildReport2) {
73836
74419
  if (setup.pool) {
73837
74420
  if (setup.report) {
73838
- setup.report.wallClockMs = Math.round(performance6.now() - setup.startTime);
74421
+ setup.report.wallClockMs = Math.round(performance7.now() - setup.startTime);
73839
74422
  }
73840
74423
  try {
73841
74424
  await setup.pool.destroy();
@@ -74275,7 +74858,7 @@ import crypto11 from "node:crypto";
74275
74858
  import fs24 from "node:fs";
74276
74859
  import fsp28 from "node:fs/promises";
74277
74860
  import path55 from "node:path";
74278
- import { performance as performance7 } from "node:perf_hooks";
74861
+ import { performance as performance8 } from "node:perf_hooks";
74279
74862
  function isConfinedFileReadError(error62) {
74280
74863
  return error62 instanceof Error && /outside project root|possible path confinement race|changed between verification and open|confined file target/u.test(error62.message);
74281
74864
  }
@@ -74815,7 +75398,7 @@ function createIndexBuildRunState(projectRoot, opts, graphOptions = normalizeGra
74815
75398
  timings: report2?.timings,
74816
75399
  // Delegated rebuilds mint a fresh run state after the preamble; reuse the caller's
74817
75400
  // origin so totalMs covers the whole wait, matching progress completion.
74818
- totalStart: opts?.progressStartedAt ?? performance7.now(),
75401
+ totalStart: opts?.progressStartedAt ?? performance8.now(),
74819
75402
  cacheMode,
74820
75403
  cacheEnabled: cacheMode !== "off",
74821
75404
  graphOptions
@@ -74874,9 +75457,9 @@ async function timeIndexBuildPhase(args) {
74874
75457
  if (args.buildStartedAt !== void 0) {
74875
75458
  emitIndexBuildActivity(args.opts, args.activity, args.current, args.total, args.mode ?? "build");
74876
75459
  }
74877
- const startedAt = performance7.now();
75460
+ const startedAt = performance8.now();
74878
75461
  const result = await args.fn();
74879
- recordBuildTimingStep(args.timings, { name: args.stepName, ms: Math.round(performance7.now() - startedAt) });
75462
+ recordBuildTimingStep(args.timings, { name: args.stepName, ms: Math.round(performance8.now() - startedAt) });
74880
75463
  return result;
74881
75464
  }
74882
75465
  function createCountedCheckProgress(opts, activity, total) {
@@ -74916,9 +75499,9 @@ function recordBuildTimingStep(timings, step) {
74916
75499
  timings.filesystemScanMs = step.ms;
74917
75500
  }
74918
75501
  async function recordTimedBuildStep(timings, name, fn) {
74919
- const startedAt = performance7.now();
75502
+ const startedAt = performance8.now();
74920
75503
  const result = await fn();
74921
- recordBuildTimingStep(timings, { name, ms: Math.round(performance7.now() - startedAt) });
75504
+ recordBuildTimingStep(timings, { name, ms: Math.round(performance8.now() - startedAt) });
74922
75505
  return result;
74923
75506
  }
74924
75507
  function buildConcurrency(opts) {
@@ -75212,7 +75795,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75212
75795
  const fileReport = initFileReport(report2);
75213
75796
  if (fileReport)
75214
75797
  fileReport.total = normalizedFiles.length;
75215
- const manifestStart = performance7.now();
75798
+ const manifestStart = performance8.now();
75216
75799
  const manifest = useManifest && !helperOpts?.ignoreExistingManifest ? await loadManifest(projectRoot, opts, report2) : null;
75217
75800
  const manifestFiles = sanitizeManifestEntriesForRoot(projectRoot, manifest?.files);
75218
75801
  const manifestOptionDiffs = manifest ? diffBuildOptions(manifest.buildOptions, opts) : [];
@@ -75228,7 +75811,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75228
75811
  }
75229
75812
  }
75230
75813
  if (timings && useManifest) {
75231
- timings.manifestMs = Math.round(performance7.now() - manifestStart);
75814
+ timings.manifestMs = Math.round(performance8.now() - manifestStart);
75232
75815
  }
75233
75816
  const edgeProbeConcurrency = buildConcurrency(opts);
75234
75817
  const staleCachedEdgeFiles = /* @__PURE__ */ new Set();
@@ -75257,7 +75840,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75257
75840
  const moduleCacheAvailable = opts?.cache !== "disk" || diskModuleCacheExists(projectRoot, opts);
75258
75841
  const shouldReportFileCacheCheck = Boolean(helperOpts?.reportDiscoveryProgress);
75259
75842
  const reportFileCacheCheck = shouldReportFileCacheCheck ? createCountedCheckProgress(opts, "Checking file cache", normalizedFiles.length) : () => void 0;
75260
- const cacheProbeStart = performance7.now();
75843
+ const cacheProbeStart = performance8.now();
75261
75844
  const evictionSerialAtStart = memoryCacheEvictionSerial();
75262
75845
  const gitAvailable = await isGitRepo(projectRoot, helperOpts?.discoveryContext?.git);
75263
75846
  const needsPersistentSignatures = cacheEnabled || useManifest;
@@ -75327,7 +75910,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75327
75910
  }
75328
75911
  }));
75329
75912
  if (timings) {
75330
- const cacheProbeMs = Math.round(performance7.now() - cacheProbeStart);
75913
+ const cacheProbeMs = Math.round(performance8.now() - cacheProbeStart);
75331
75914
  timings.cacheProbeMs = cacheProbeMs;
75332
75915
  recordBuildTimingStep(timings, { name: "cache-probe", ms: cacheProbeMs });
75333
75916
  }
@@ -75397,7 +75980,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75397
75980
  const bloomFilterCache = useBloomFilters ? new (await Promise.resolve().then(() => (init_bloom_filter(), bloom_filter_exports))).BloomFilterCache() : void 0;
75398
75981
  const persistedBloomFilters = bloomFilterCache ? await tryLoadPersistedBloomFilters(projectRoot, opts, report2) : null;
75399
75982
  const parsedMap = /* @__PURE__ */ new Map();
75400
- const parseStart = performance7.now();
75983
+ const parseStart = performance8.now();
75401
75984
  const graph = { nodes: new Set(normalizedFiles), edges: [] };
75402
75985
  const onFileEdges = manifestEntries ? (file2, entry) => {
75403
75986
  const manifestEntry = toManifestFileEntry(entry);
@@ -75411,7 +75994,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75411
75994
  const ensureBuildProgressStarted = () => {
75412
75995
  if (buildStartedAt !== void 0)
75413
75996
  return;
75414
- buildStartedAt = performance7.now();
75997
+ buildStartedAt = performance8.now();
75415
75998
  emitIndexLifecycleProgress(opts, "start", "build", totalFiles);
75416
75999
  };
75417
76000
  if (cacheMisses.length)
@@ -75538,8 +76121,8 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75538
76121
  }
75539
76122
  });
75540
76123
  if (timings)
75541
- timings.parseMs = Math.round(performance7.now() - parseStart);
75542
- const graphStart = performance7.now();
76124
+ timings.parseMs = Math.round(performance8.now() - parseStart);
76125
+ const graphStart = performance8.now();
75543
76126
  const seenGraphEdges = /* @__PURE__ */ new Set();
75544
76127
  const appendUniqueGraphEdges = (edges) => {
75545
76128
  if (!edges.length)
@@ -75595,7 +76178,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75595
76178
  });
75596
76179
  appendUniqueGraphEdges(workspaceManifestEdges);
75597
76180
  if (timings)
75598
- timings.graphMs = Math.round(performance7.now() - graphStart);
76181
+ timings.graphMs = Math.round(performance8.now() - graphStart);
75599
76182
  for (const jsonPath of jsonDependencies.values()) {
75600
76183
  ensureJsonModule(modules, jsonPath);
75601
76184
  }
@@ -75641,7 +76224,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75641
76224
  return [file2, { ...entry, ...cacheSig ? { cacheSig } : {} }];
75642
76225
  })) : manifestEntriesForIndex;
75643
76226
  const discoveredInsideFinalize = projectFiles === void 0;
75644
- const finalizeStartedAt = performance7.now();
76227
+ const finalizeStartedAt = performance8.now();
75645
76228
  if (buildStartedAt !== void 0) {
75646
76229
  emitIndexBuildActivity(opts, "Finalizing project graph", totalFiles, totalFiles);
75647
76230
  }
@@ -75661,7 +76244,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75661
76244
  manifestEntries: indexManifestEntries
75662
76245
  });
75663
76246
  if (timings) {
75664
- const elapsed = Math.round(performance7.now() - finalizeStartedAt);
76247
+ const elapsed = Math.round(performance8.now() - finalizeStartedAt);
75665
76248
  const discoveryMs = discoveredInsideFinalize ? timings.metadataDiscoveryMs ?? 0 : 0;
75666
76249
  const finalizeMs = discoveredInsideFinalize ? Math.max(0, elapsed - discoveryMs) : elapsed;
75667
76250
  recordBuildTimingStep(timings, { name: "finalize", ms: finalizeMs });
@@ -75680,7 +76263,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
75680
76263
  }
75681
76264
  if (buildStartedAt !== void 0) {
75682
76265
  const progressStartedAt = opts?.progressStartedAt ?? buildStartedAt;
75683
- emitIndexLifecycleProgress(opts, "complete", "build", index.byFile.size, performance7.now() - progressStartedAt);
76266
+ emitIndexLifecycleProgress(opts, "complete", "build", index.byFile.size, performance8.now() - progressStartedAt);
75684
76267
  }
75685
76268
  return index;
75686
76269
  } finally {
@@ -75714,7 +76297,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
75714
76297
  const onDiscoveryTiming = timings ? (step) => recordBuildTimingStep(timings, step) : void 0;
75715
76298
  if (helperOpts?.reportDiscoveryProgress)
75716
76299
  emitIndexCheckActivity(opts, "Discovering source files");
75717
- const sourceDiscoveryStart = performance7.now();
76300
+ const sourceDiscoveryStart = performance8.now();
75718
76301
  const discoveredFiles = await listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
75719
76302
  ...opts?.discovery,
75720
76303
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
@@ -75726,7 +76309,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
75726
76309
  ...discoveryContext ? { discoveryContext } : {}
75727
76310
  });
75728
76311
  if (timings)
75729
- timings.sourceDiscoveryMs = Math.round(performance7.now() - sourceDiscoveryStart);
76312
+ timings.sourceDiscoveryMs = Math.round(performance8.now() - sourceDiscoveryStart);
75730
76313
  const additionalFileCandidates = await normalizeIndexedFileInputsWithinRoot(projectRoot, opts?.additionalFiles ?? [], "Additional index file");
75731
76314
  const additionalFileExistence = await probePathExistence(additionalFileCandidates, buildConcurrency(opts));
75732
76315
  const additionalFiles = additionalFileCandidates.filter((file2) => additionalFileExistence.get(file2));
@@ -75735,7 +76318,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
75735
76318
  const transientFiles = additionalFiles.filter((file2) => !discoveredFileSet.has(file2));
75736
76319
  if (helperOpts?.reportDiscoveryProgress)
75737
76320
  emitIndexCheckActivity(opts, "Discovering project metadata");
75738
- const metadataDiscoveryStart = performance7.now();
76321
+ const metadataDiscoveryStart = performance8.now();
75739
76322
  const projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
75740
76323
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
75741
76324
  ...discoveredSymlinkDirectories !== void 0 ? { knownSymlinkDirectories: discoveredSymlinkDirectories } : {},
@@ -75745,7 +76328,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
75745
76328
  ...discoveryContext ? { discoveryContext } : {}
75746
76329
  });
75747
76330
  if (timings)
75748
- timings.metadataDiscoveryMs = Math.round(performance7.now() - metadataDiscoveryStart);
76331
+ timings.metadataDiscoveryMs = Math.round(performance8.now() - metadataDiscoveryStart);
75749
76332
  return await buildIndexFromFileListShared(projectRoot, files, opts, {
75750
76333
  manifestMode: useDiskCache ? "read-write" : "off",
75751
76334
  warnNoFilesMessage: `Warning: No files found in project root: ${projectRoot}. Check codegraph.config.json globs and CLI --include-glob/--ignore-glob filters. Diagnostic: codegraph doctor`,
@@ -75765,7 +76348,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
75765
76348
  }
75766
76349
  }
75767
76350
  async function buildProjectIndex(projectRoot, opts) {
75768
- const progressStartedAt = opts?.progressStartedAt ?? performance7.now();
76351
+ const progressStartedAt = opts?.progressStartedAt ?? performance8.now();
75769
76352
  const timings = opts?.report ? opts.report.timings ??= {} : void 0;
75770
76353
  resetTransientBuildTimings(timings);
75771
76354
  return buildProjectIndexWithManifestOptions(projectRoot, {
@@ -75775,7 +76358,7 @@ async function buildProjectIndex(projectRoot, opts) {
75775
76358
  }
75776
76359
  async function buildProjectIndexFromFiles(projectRoot, inputFiles, rawOpts) {
75777
76360
  assertNativeRequiredAvailable();
75778
- const opts = { ...rawOpts, progressStartedAt: rawOpts?.progressStartedAt ?? performance7.now() };
76361
+ const opts = { ...rawOpts, progressStartedAt: rawOpts?.progressStartedAt ?? performance8.now() };
75779
76362
  const timings = opts.report ? opts.report.timings ??= {} : void 0;
75780
76363
  resetTransientBuildTimings(timings);
75781
76364
  try {
@@ -75812,7 +76395,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
75812
76395
  assertNativeRequiredAvailable();
75813
76396
  const opts = {
75814
76397
  ...rawOpts,
75815
- progressStartedAt: rawOpts?.progressStartedAt ?? performance7.now()
76398
+ progressStartedAt: rawOpts?.progressStartedAt ?? performance8.now()
75816
76399
  };
75817
76400
  const discoveryContext = opts.discoveryContext ?? createProjectDiscoveryContext(projectRoot);
75818
76401
  const discoveryTimings = opts.report ? opts.report.timings ??= {} : void 0;
@@ -75848,7 +76431,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
75848
76431
  message: "Checked project index",
75849
76432
  current: total,
75850
76433
  total,
75851
- elapsedMs: performance7.now() - totalStart
76434
+ elapsedMs: performance8.now() - totalStart
75852
76435
  });
75853
76436
  };
75854
76437
  try {
@@ -75863,10 +76446,10 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
75863
76446
  return await buildProjectIndexFromExport(projectRoot, opts, { ignoreExistingManifest: true, discoveryContext });
75864
76447
  }
75865
76448
  startCheckProgress();
75866
- const manifestStart = performance7.now();
76449
+ const manifestStart = performance8.now();
75867
76450
  const manifest = await recordTimedBuildStep(timings, "load-manifest", () => loadManifest(projectRoot, opts, report2));
75868
76451
  if (timings)
75869
- timings.manifestMs = Math.round(performance7.now() - manifestStart);
76452
+ timings.manifestMs = Math.round(performance8.now() - manifestStart);
75870
76453
  const manifestUsed = !!manifest;
75871
76454
  const manifestReport = initManifestReport(report2, manifestUsed, false);
75872
76455
  if (manifestReport && !manifestUsed)
@@ -75927,11 +76510,11 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
75927
76510
  if (!discoveryTimings || opts?.filesAreProjectScope)
75928
76511
  return await operation();
75929
76512
  sourceDiscoveryPerformed = true;
75930
- const start = performance7.now();
76513
+ const start = performance8.now();
75931
76514
  try {
75932
76515
  return await operation();
75933
76516
  } finally {
75934
- sourceDiscoveryMs += performance7.now() - start;
76517
+ sourceDiscoveryMs += performance8.now() - start;
75935
76518
  }
75936
76519
  };
75937
76520
  let manifestDiffFiles = [];
@@ -76149,14 +76732,14 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76149
76732
  const snapshot = snapshotLoad.index;
76150
76733
  snapshot.manifestSignaturesFresh = false;
76151
76734
  if (snapshot.projectFiles === void 0) {
76152
- const metadataDiscoveryStart = performance7.now();
76735
+ const metadataDiscoveryStart = performance8.now();
76153
76736
  snapshot.projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
76154
76737
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
76155
76738
  ...discoveredGitCandidates !== void 0 ? { knownGitCandidates: discoveredGitCandidates } : {},
76156
76739
  ...discoveryContext ? { discoveryContext } : {}
76157
76740
  });
76158
76741
  if (discoveryTimings)
76159
- discoveryTimings.metadataDiscoveryMs = Math.round(performance7.now() - metadataDiscoveryStart);
76742
+ discoveryTimings.metadataDiscoveryMs = Math.round(performance8.now() - metadataDiscoveryStart);
76160
76743
  }
76161
76744
  if (opts?.cache) {
76162
76745
  snapshot.cacheMode = opts.cache;
@@ -76192,7 +76775,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76192
76775
  const snapshot = await reuseUnchangedSnapshot();
76193
76776
  if (snapshot) {
76194
76777
  if (timings)
76195
- timings.totalMs = Math.round(performance7.now() - totalStart);
76778
+ timings.totalMs = Math.round(performance8.now() - totalStart);
76196
76779
  completeCheckProgress(allFiles.size);
76197
76780
  return snapshot;
76198
76781
  }
@@ -76267,7 +76850,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76267
76850
  unchangedSnapshot.manifestEntries = new Map(Array.from(fileSignatures, ([file2, signature]) => [file2, toProjectIndexManifestEntry(signature)]));
76268
76851
  unchangedSnapshot.manifestSignaturesFresh = true;
76269
76852
  if (timings)
76270
- timings.totalMs = Math.round(performance7.now() - totalStart);
76853
+ timings.totalMs = Math.round(performance8.now() - totalStart);
76271
76854
  completeCheckProgress(allFiles.size);
76272
76855
  return unchangedSnapshot;
76273
76856
  }
@@ -76363,13 +76946,13 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76363
76946
  workerSetup = await setupWorkerPool(opts, countNativeWorkerEligibleFiles(changedList, opts?.languageExtensions));
76364
76947
  let updateStartedAt;
76365
76948
  if (changedList.length || deletedTrackedFiles.size) {
76366
- updateStartedAt = performance7.now();
76949
+ updateStartedAt = performance8.now();
76367
76950
  emitIndexLifecycleProgress(opts, "start", "update", changedList.length);
76368
76951
  }
76369
76952
  if (fileReport)
76370
76953
  fileReport.changed = changedList.length;
76371
76954
  if (changedList.length) {
76372
- const parseStart = performance7.now();
76955
+ const parseStart = performance8.now();
76373
76956
  let processedFiles = 0;
76374
76957
  const totalFiles = changedList.length;
76375
76958
  const fileResults = await mapLimit(changedList, conc, async (file2) => {
@@ -76431,7 +77014,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76431
77014
  writeModulesToCache(projectRoot, pendingIncrementalCacheWrites, opts);
76432
77015
  }
76433
77016
  if (timings)
76434
- timings.parseMs = Math.round(performance7.now() - parseStart);
77017
+ timings.parseMs = Math.round(performance8.now() - parseStart);
76435
77018
  }
76436
77019
  for (const jsonPath of jsonDependencies.values()) {
76437
77020
  ensureJsonModule(modules, jsonPath);
@@ -76465,7 +77048,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76465
77048
  }
76466
77049
  }
76467
77050
  const filesList = Array.from(changedFiles);
76468
- const graphStart = performance7.now();
77051
+ const graphStart = performance8.now();
76469
77052
  const graph = !filesList.length && baseGraph ? { nodes: new Set(baseGraph.nodes), edges: [...baseGraph.edges] } : await collectGraph(projectRoot, filesList, {
76470
77053
  parsed: parsedMap,
76471
77054
  resolveNodeModules: !!graphOptions.resolveNodeModules,
@@ -76487,7 +77070,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76487
77070
  }
76488
77071
  });
76489
77072
  if (timings)
76490
- timings.graphMs = Math.round(performance7.now() - graphStart);
77073
+ timings.graphMs = Math.round(performance8.now() - graphStart);
76491
77074
  for (const file2 of /* @__PURE__ */ new Set([...changedFiles, ...transientFiles])) {
76492
77075
  const signature = fileSignatures.get(file2);
76493
77076
  if (!signature || manifestEntries.has(file2))
@@ -76571,7 +77154,7 @@ async function buildProjectIndexIncremental(projectRoot, rawOpts) {
76571
77154
  fn: () => writeProjectIndexSnapshot(projectRoot, opts, index, projectSnapshotFilesSignature(manifestEntries, projectRoot))
76572
77155
  });
76573
77156
  if (updateStartedAt !== void 0) {
76574
- emitIndexLifecycleProgress(opts, "complete", "update", index.byFile.size, performance7.now() - updateStartedAt);
77157
+ emitIndexLifecycleProgress(opts, "complete", "update", index.byFile.size, performance8.now() - updateStartedAt);
76575
77158
  } else {
76576
77159
  completeCheckProgress(allFiles.size);
76577
77160
  }
@@ -97014,7 +97597,7 @@ async function readGitFilesAtRevision(projectRoot, revision, files) {
97014
97597
  return /* @__PURE__ */ new Map();
97015
97598
  const maxOutputBytes = Math.min(MAX_GIT_BATCH_OUTPUT_BYTES_PER_FILE * requests.length, MAX_GIT_BATCH_OUTPUT_BYTES);
97016
97599
  try {
97017
- const child = spawn2("git", ["cat-file", "--batch"], { cwd: projectRoot });
97600
+ const child = spawn2("git", ["cat-file", "--batch"], { windowsHide: true, cwd: projectRoot });
97018
97601
  const stdoutChunks = [];
97019
97602
  let stdoutBytes = 0;
97020
97603
  let stderr = "";
@@ -97291,7 +97874,7 @@ var init_deleted = __esm({
97291
97874
  });
97292
97875
 
97293
97876
  // dist/review/candidates.js
97294
- import { performance as performance8 } from "node:perf_hooks";
97877
+ import { performance as performance9 } from "node:perf_hooks";
97295
97878
  function mergeCandidateTestEntries(baseCandidates, additionalCandidates) {
97296
97879
  const merged = /* @__PURE__ */ new Map();
97297
97880
  const upsert = (candidate) => {
@@ -97311,7 +97894,7 @@ function mergeCandidateTestEntries(baseCandidates, additionalCandidates) {
97311
97894
  return Array.from(merged.values());
97312
97895
  }
97313
97896
  async function collectReviewCandidateTests(input2) {
97314
- const candidateStart = performance8.now();
97897
+ const candidateStart = performance9.now();
97315
97898
  const configuredMaxCandidates = input2.appliedOptions.maxCandidates;
97316
97899
  const maxCandidates = typeof configuredMaxCandidates === "number" && Number.isFinite(configuredMaxCandidates) ? Math.max(0, Math.floor(configuredMaxCandidates)) : REVIEW_DEFAULT_CANDIDATE_TEST_LIMIT;
97317
97900
  const candidateTests = mergeCandidateTestEntries(listCandidateTestFiles(input2.index, input2.changedFileList, input2.changedSymbolIds, {
@@ -97323,11 +97906,11 @@ async function collectReviewCandidateTests(input2) {
97323
97906
  file: toProjectDisplayPath(input2.projectRoot, candidate.file)
97324
97907
  })).sort(compareCandidateTests).slice(0, maxCandidates);
97325
97908
  if (input2.reviewTimings) {
97326
- input2.reviewTimings.candidatesMs = Math.round(performance8.now() - candidateStart);
97909
+ input2.reviewTimings.candidatesMs = Math.round(performance9.now() - candidateStart);
97327
97910
  }
97328
97911
  return candidateTests;
97329
97912
  }
97330
- var init_candidates = __esm({
97913
+ var init_candidates2 = __esm({
97331
97914
  "dist/review/candidates.js"() {
97332
97915
  "use strict";
97333
97916
  init_context2();
@@ -97619,7 +98202,7 @@ var init_parse4 = __esm({
97619
98202
  });
97620
98203
 
97621
98204
  // dist/review/changes.js
97622
- import { performance as performance9 } from "node:perf_hooks";
98205
+ import { performance as performance10 } from "node:perf_hooks";
97623
98206
  function deletedPathsForChange(change2) {
97624
98207
  if (change2.kind === "deleted") {
97625
98208
  return [change2.path];
@@ -97637,16 +98220,16 @@ async function collectReviewChanges(projectRoot, appliedOptions, reviewTimings)
97637
98220
  const isIncludedReviewFile = createImpactIncludeMatcher(discoveryGlobRoot, appliedOptions.discovery?.includeGlobs ?? []);
97638
98221
  const changedFiles = /* @__PURE__ */ new Set();
97639
98222
  const explicitFiles = /* @__PURE__ */ new Set();
97640
- const changesStart = performance9.now();
98223
+ const changesStart = performance10.now();
97641
98224
  for (const file2 of appliedOptions.files ?? []) {
97642
98225
  const normalized = normalizeFile(file2, "Review file");
97643
98226
  changedFiles.add(normalized);
97644
98227
  explicitFiles.add(normalized);
97645
98228
  }
97646
98229
  if (reviewTimings) {
97647
- reviewTimings.changesMs = Math.round(performance9.now() - changesStart);
98230
+ reviewTimings.changesMs = Math.round(performance10.now() - changesStart);
97648
98231
  }
97649
- const diffStart = performance9.now();
98232
+ const diffStart = performance10.now();
97650
98233
  const shouldLoadGitDiff = Boolean(appliedOptions.gitBase || appliedOptions.changedSince);
97651
98234
  const diffText = appliedOptions.diffText ?? (shouldLoadGitDiff ? await getUnifiedDiff(projectRoot, {
97652
98235
  base: appliedOptions.gitBase,
@@ -97655,7 +98238,7 @@ async function collectReviewChanges(projectRoot, appliedOptions, reviewTimings)
97655
98238
  }) : "");
97656
98239
  const diff = diffText ? parseUnifiedDiff(diffText) : null;
97657
98240
  if (reviewTimings) {
97658
- reviewTimings.diffMs = Math.round(performance9.now() - diffStart);
98241
+ reviewTimings.diffMs = Math.round(performance10.now() - diffStart);
97659
98242
  }
97660
98243
  const diffHunksByFile = /* @__PURE__ */ new Map();
97661
98244
  const diffKindsByFile = /* @__PURE__ */ new Map();
@@ -99224,10 +99807,10 @@ var init_severity = __esm({
99224
99807
  });
99225
99808
 
99226
99809
  // dist/impact/budgets.js
99227
- import { performance as performance10 } from "node:perf_hooks";
99810
+ import { performance as performance11 } from "node:perf_hooks";
99228
99811
  function createImpactWorkBudget(options) {
99229
99812
  return {
99230
- ...typeof options.timeBudgetMs === "number" && options.timeBudgetMs >= 0 ? { deadlineAt: performance10.now() + options.timeBudgetMs } : {},
99813
+ ...typeof options.timeBudgetMs === "number" && options.timeBudgetMs >= 0 ? { deadlineAt: performance11.now() + options.timeBudgetMs } : {},
99231
99814
  ...typeof options.maxReferenceLookups === "number" && options.maxReferenceLookups >= 0 ? { maxReferenceLookups: options.maxReferenceLookups } : {},
99232
99815
  ...typeof options.maxTotalReferences === "number" && options.maxTotalReferences >= 0 ? { maxTotalReferences: options.maxTotalReferences } : {},
99233
99816
  referenceLookupsStarted: 0,
@@ -99242,7 +99825,7 @@ function isImpactDeadlineExceeded(budget) {
99242
99825
  return true;
99243
99826
  if (budget.deadlineAt === void 0)
99244
99827
  return false;
99245
- if (performance10.now() >= budget.deadlineAt) {
99828
+ if (performance11.now() >= budget.deadlineAt) {
99246
99829
  budget.deadlineExceeded = true;
99247
99830
  return true;
99248
99831
  }
@@ -100233,7 +100816,7 @@ var init_reference_cache = __esm({
100233
100816
  // dist/review/summaries.js
100234
100817
  import fsp39 from "node:fs/promises";
100235
100818
  import path79 from "node:path";
100236
- import { performance as performance11 } from "node:perf_hooks";
100819
+ import { performance as performance12 } from "node:perf_hooks";
100237
100820
  function sortSymbols(symbols) {
100238
100821
  return symbols.slice().sort((left, right) => symbolId(left).localeCompare(symbolId(right)));
100239
100822
  }
@@ -100564,7 +101147,7 @@ async function summarizeChangedFiles2(input2) {
100564
101147
  }
100565
101148
  }
100566
101149
  const defsToResolve = fileEntries.flatMap((entry) => entry.locals);
100567
- const referencesStart = performance11.now();
101150
+ const referencesStart = performance12.now();
100568
101151
  const referenceResults = includeSymbolDetails && maxCallsites > 0 ? await mapLimit(defsToResolve, referenceConcurrency, async (def) => {
100569
101152
  const refs = await referenceCache.getUsages(index, def, {
100570
101153
  maxReferences: maxCallsites
@@ -100572,7 +101155,7 @@ async function summarizeChangedFiles2(input2) {
100572
101155
  return { def, refs };
100573
101156
  }) : [];
100574
101157
  if (reviewTimings) {
100575
- reviewTimings.referencesMs = Math.round(performance11.now() - referencesStart);
101158
+ reviewTimings.referencesMs = Math.round(performance12.now() - referencesStart);
100576
101159
  }
100577
101160
  const referencesByHandle = /* @__PURE__ */ new Map();
100578
101161
  for (const entry of referenceResults) {
@@ -100757,7 +101340,7 @@ var init_summaries = __esm({
100757
101340
  });
100758
101341
 
100759
101342
  // dist/review.js
100760
- import { performance as performance12 } from "node:perf_hooks";
101343
+ import { performance as performance13 } from "node:perf_hooks";
100761
101344
  function applyReviewPresetOptions(opts) {
100762
101345
  if (!opts.reviewDepth)
100763
101346
  return opts;
@@ -100787,7 +101370,7 @@ async function buildReviewIndex(input2) {
100787
101370
  diffChangesByFile,
100788
101371
  ...appliedOptions.graph ? { graphOptions: appliedOptions.graph } : {}
100789
101372
  });
100790
- const indexStart = performance12.now();
101373
+ const indexStart = performance13.now();
100791
101374
  const indexReport = reviewReport?.indexReport ?? (reviewReport ? { timings: {} } : void 0);
100792
101375
  if (reviewReport && !reviewReport.indexReport && indexReport) {
100793
101376
  reviewReport.indexReport = indexReport;
@@ -100819,7 +101402,7 @@ async function buildReviewIndex(input2) {
100819
101402
  });
100820
101403
  }
100821
101404
  if (reviewTimings) {
100822
- reviewTimings.indexMs = Math.round(performance12.now() - indexStart);
101405
+ reviewTimings.indexMs = Math.round(performance13.now() - indexStart);
100823
101406
  }
100824
101407
  return {
100825
101408
  index,
@@ -100837,8 +101420,8 @@ async function buildReviewReport(projectRoot, opts = {}, cached2) {
100837
101420
  if (reviewReport && appliedOptions.duplicateTasks === false) {
100838
101421
  delete reviewReport.duplicateAnalysis;
100839
101422
  }
100840
- const totalStart = performance12.now();
100841
- const { changedFiles, explicitFiles, diffHunksByFile, diffKindsByFile, diffChangesByFile } = await collectReviewChanges(projectRoot, appliedOptions, reviewTimings);
101423
+ const totalStart = performance13.now();
101424
+ const { changedFiles, explicitFiles, diffHunksByFile, diffKindsByFile, diffChangesByFile } = cached2?.changes ?? await collectReviewChanges(projectRoot, appliedOptions, reviewTimings);
100842
101425
  if (changedFiles.size === 0) {
100843
101426
  const riskSummary = computeRiskSummary({
100844
101427
  filesChanged: 0,
@@ -100871,7 +101454,7 @@ async function buildReviewReport(projectRoot, opts = {}, cached2) {
100871
101454
  report3.base = appliedOptions.gitBase;
100872
101455
  report3.head = appliedOptions.gitHead ?? "HEAD";
100873
101456
  if (reviewTimings)
100874
- reviewTimings.totalMs = Math.round(performance12.now() - totalStart);
101457
+ reviewTimings.totalMs = Math.round(performance13.now() - totalStart);
100875
101458
  return report3;
100876
101459
  }
100877
101460
  const changedFileList = Array.from(changedFiles).sort((left, right) => left.localeCompare(right));
@@ -100950,7 +101533,7 @@ async function buildReviewReport(projectRoot, opts = {}, cached2) {
100950
101533
  riskRelevantParseFailures,
100951
101534
  exportedChangedCount
100952
101535
  });
100953
- const duplicateAnalysisStart = performance12.now();
101536
+ const duplicateAnalysisStart = performance13.now();
100954
101537
  if (appliedOptions.duplicateTasks !== false) {
100955
101538
  const duplicateReview = await collectReviewDuplicateTasks({
100956
101539
  projectRoot,
@@ -100971,10 +101554,10 @@ async function buildReviewReport(projectRoot, opts = {}, cached2) {
100971
101554
  }
100972
101555
  }
100973
101556
  if (reviewTimings && appliedOptions.duplicateTasks !== false) {
100974
- reviewTimings.duplicateAnalysisMs = Math.round(performance12.now() - duplicateAnalysisStart);
101557
+ reviewTimings.duplicateAnalysisMs = Math.round(performance13.now() - duplicateAnalysisStart);
100975
101558
  }
100976
101559
  if (reviewTimings)
100977
- reviewTimings.totalMs = Math.round(performance12.now() - totalStart);
101560
+ reviewTimings.totalMs = Math.round(performance13.now() - totalStart);
100978
101561
  return report2;
100979
101562
  }
100980
101563
  function buildSymbolDefLookup(index) {
@@ -101135,7 +101718,7 @@ var init_review2 = __esm({
101135
101718
  init_workspace();
101136
101719
  init_project_files();
101137
101720
  init_concurrency();
101138
- init_candidates();
101721
+ init_candidates2();
101139
101722
  init_changes();
101140
101723
  init_deleted();
101141
101724
  init_report();
@@ -102361,7 +102944,7 @@ async function explainCodegraphTargetWithSession(session, request) {
102361
102944
  const snapshot = await session.loadProject();
102362
102945
  const lookup2 = buildSymbolLookup(snapshot);
102363
102946
  const resolved = resolveTarget(snapshot, lookup2, request.target);
102364
- return await buildExplanation(snapshot, lookup2, resolved, request);
102947
+ return await buildExplanation(snapshot, lookup2, resolved, request, session);
102365
102948
  }
102366
102949
  async function resolveCodegraphTargetWithSession(session, request) {
102367
102950
  const snapshot = await session.loadProject();
@@ -102525,7 +103108,7 @@ function basename2(name) {
102525
103108
  const parts = name.split(".");
102526
103109
  return parts[parts.length - 1] ?? name;
102527
103110
  }
102528
- async function buildExplanation(snapshot, lookup2, resolved, request) {
103111
+ async function buildExplanation(snapshot, lookup2, resolved, request, session) {
102529
103112
  const maxDependencies = defaultAgentLimit(request.maxDependencies, AGENT_EXPLAIN_DEFAULT_DEPENDENCY_LIMIT, AGENT_EXPLAIN_MAX_DEPENDENCY_LIMIT);
102530
103113
  const maxReferences = defaultAgentLimit(request.maxReferences ?? request.maxDependencies, AGENT_EXPLAIN_DEFAULT_DEPENDENCY_LIMIT, AGENT_EXPLAIN_MAX_DEPENDENCY_LIMIT);
102531
103114
  const maxRelatedSqlObjects = defaultAgentLimit(request.maxRelatedSqlObjects ?? request.maxDependencies, AGENT_EXPLAIN_DEFAULT_DEPENDENCY_LIMIT, AGENT_EXPLAIN_MAX_DEPENDENCY_LIMIT);
@@ -102549,7 +103132,7 @@ async function buildExplanation(snapshot, lookup2, resolved, request) {
102549
103132
  const references = referenceContext.references;
102550
103133
  const snippets = referenceContext.snippets;
102551
103134
  const relatedSqlObjects = await collectRelatedSqlObjects(snapshot, lookup2, resolved, file2, maxRelatedSqlObjects);
102552
- const duplicates = await collectDuplicateContext(snapshot, resolved, relFile, maxDuplicates);
103135
+ const duplicates = await collectDuplicateContext(snapshot, session, resolved, relFile, maxDuplicates);
102553
103136
  const followUps = collectFollowUps(snapshot, resolved, boundedSymbols.items, relFile, duplicates.items);
102554
103137
  const changedContext = await collectChangedContext(request);
102555
103138
  return {
@@ -102692,7 +103275,7 @@ function compareDependencies(left, right) {
102692
103275
  return depthDelta;
102693
103276
  return left.file.localeCompare(right.file);
102694
103277
  }
102695
- async function collectDuplicateContext(snapshot, resolved, relFile, limit) {
103278
+ async function collectDuplicateContext(snapshot, session, resolved, relFile, limit) {
102696
103279
  if (resolved.kind === "sql_object")
102697
103280
  return emptyAgentBoundedList();
102698
103281
  const target = resolved.kind === "file" ? { file: relFile } : {
@@ -102700,13 +103283,15 @@ async function collectDuplicateContext(snapshot, resolved, relFile, limit) {
102700
103283
  startLine: resolved.def.range.start.line,
102701
103284
  endLine: resolved.def.range.end.line
102702
103285
  };
102703
- const result = await findDuplicateContext(snapshot.index, target, {
103286
+ const options = {
102704
103287
  projectRoot: snapshot.root,
102705
103288
  minConfidence: "medium",
102706
103289
  includeSameFile: true,
102707
103290
  limit,
102708
103291
  maxPairs: AGENT_DUPLICATE_MAX_PAIRS
102709
- });
103292
+ };
103293
+ const analysis = await session.loadDuplicateAnalysis?.();
103294
+ const result = analysis ? (await findDuplicateContextsWithPreparedAnalysis(analysis, [target], options))[0] : await findDuplicateContext(snapshot.index, target, options);
102710
103295
  return {
102711
103296
  items: result.groups.map((group) => summarizeDuplicateGroup(group, result.target)),
102712
103297
  omitted: result.omittedCounts.groups + result.omittedCounts.candidatePairs
@@ -104071,7 +104656,7 @@ function findQueryIndexChunkCandidates(store, rankTerms, normalizedRankPhrase =
104071
104656
  };
104072
104657
  }
104073
104658
  var QUERY_INDEX_CANDIDATE_VERSION;
104074
- var init_candidates2 = __esm({
104659
+ var init_candidates3 = __esm({
104075
104660
  "dist/agent/query-index/candidates.js"() {
104076
104661
  "use strict";
104077
104662
  init_content();
@@ -104648,9 +105233,8 @@ async function searchCodegraphWithSession(session, request) {
104648
105233
  return freshResponse;
104649
105234
  return { ...freshResponse, query: request.query };
104650
105235
  }
104651
- const mode = request.mode ?? "hybrid";
104652
105236
  let queryIndex;
104653
- if (mode === "hybrid" || mode === "text") {
105237
+ if (searchUsesQueryIndex(request)) {
104654
105238
  const { ensureSessionQueryIndex: ensureSessionQueryIndex2 } = await Promise.resolve().then(() => (init_session_store(), session_store_exports));
104655
105239
  queryIndex = await ensureSessionQueryIndex2(session, snapshot);
104656
105240
  }
@@ -104847,6 +105431,10 @@ function searchNeedsSymbolGraph(request) {
104847
105431
  const mode = request.mode ?? "hybrid";
104848
105432
  return mode !== "path" && mode !== "text" && mode !== "sql";
104849
105433
  }
105434
+ function searchUsesQueryIndex(request) {
105435
+ const mode = request.mode ?? "hybrid";
105436
+ return mode === "hybrid" || mode === "text";
105437
+ }
104850
105438
  function buildQueryTerms(input2, snapshot) {
104851
105439
  const normalized = normalizeSearchText(input2);
104852
105440
  const tokenSequence = normalized.split(/\s+/).filter((token) => token.length);
@@ -105613,7 +106201,7 @@ var init_search = __esm({
105613
106201
  init_session();
105614
106202
  init_follow_ups();
105615
106203
  init_content();
105616
- init_candidates2();
106204
+ init_candidates3();
105617
106205
  init_store();
105618
106206
  init_session_lifecycle();
105619
106207
  DEFAULT_LIMIT3 = 20;
@@ -106715,33 +107303,34 @@ ${SHARED_INDEX_OPTIONS_HELP}
106715
107303
  SERVER_HELP_TEXT = `codegraph server - Manage a shared project-local MCP HTTP server
106716
107304
 
106717
107305
  Usage:
106718
- codegraph server start [--root <path>] [--host <host>] [--port <1-65535>] [--startup-timeout-ms <1-86400000>] [--json] [--replace] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
107306
+ codegraph server start [--root <path>] [--host <host>] [--port <1-65535>] [--startup-timeout-ms <1-86400000>] [--index-wait-ms <ms>] [--json] [--replace] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
106719
107307
  codegraph server status [--root <path>] [--json | --pretty]
106720
107308
  codegraph server stop [--root <path>]
106721
107309
 
106722
107310
  Behavior:
106723
107311
  start runs codegraph mcp serve in a separate process, waits for a challenged /health proof, then writes .codegraph/server.json. --json emits the started registry with status and update fields; startup diagnostics go to .codegraph/server.log.
106724
- The default server listens on 127.0.0.1:7331. A public bind requires an explicit --host. --startup-timeout-ms defaults to 15000 and allows up to 86400000 for long warmups. Startup warms the base session cache by default; --no-warmup starts lazily instead, and --warmup-symbols also warms the detailed symbol graph.
107312
+ The default server listens on 127.0.0.1:7331. A public bind requires an explicit --host. --startup-timeout-ms defaults to 15000 and covers starting the process; the warmup builds in the background and does not count against it. By default the server warms the base session cache and the search index; --no-warmup starts lazily instead, and --warmup-symbols also warms the detailed symbol graph.
106725
107313
  status retries proven health checks and reports a verification remedy for an unreachable or mismatched server. stop removes only confirmed stale registries or signals a Codegraph server whose root, process, and startup time match the registry.
106726
107314
 
106727
107315
  Forwarded startup options:
106728
107316
  --cache, --cache-dir, --cache-strict, --cache-verify, --threads, --workers,
106729
- --include-glob, --ignore-glob, --no-gitignore, --no-watch, and --resolution-hint pass to mcp serve.
107317
+ --include-glob, --ignore-glob, --no-gitignore, --no-watch, --index-wait-ms, and --resolution-hint pass to mcp serve.
106730
107318
 
106731
107319
  Safety:
106732
107320
  The registry stores process metadata and a non-secret credential ID. Its credential remains in the per-user Codegraph state directory, outside the project and separate from compiler cache data. An installed-version change requires an explicit server restart; Codegraph does not replace mapped native files or terminate clients automatically.
106733
107321
  `;
106734
107322
  MCP_HELP_TEXT = `codegraph mcp - Serve MCP tools for agent graph navigation
106735
107323
 
106736
- Usage: codegraph mcp [serve] [--root <path>] [--artifact <path>] [--stdio | --port <number>] [--host <host>] [--idle-timeout-ms <ms>] [--allow-build] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
107324
+ Usage: codegraph mcp [serve] [--root <path>] [--artifact <path>] [--stdio | --port <number>] [--host <host>] [--idle-timeout-ms <ms>] [--index-wait-ms <ms>] [--allow-build] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
106737
107325
 
106738
107326
  Transports:
106739
107327
  --stdio Serve MCP over stdio (default)
106740
107328
  --port <number> Serve Streamable HTTP at /mcp
106741
- --warmup Build the base session cache at startup (default)
106742
- --warmup-symbols Build the base cache and detailed symbol graph at startup
106743
- --no-warmup Skip startup warmup; the first tool call pays for discovery and building
107329
+ --warmup Build the base session cache and the search index in the background after startup (default)
107330
+ --warmup-symbols Also build the detailed symbol graph in the background
107331
+ --no-warmup Skip the startup warmup; the first tool call that needs the index starts the build
106744
107332
  --no-watch Do not watch project files; rely on the freshness check of each tool call
107333
+ --index-wait-ms <ms> Longest wait of search, explore, orient, get_file with includeGraphContext, review, packet_get, workspace_symbols, and impact for a running index build before they answer index_building (default 15000; 0 waits without limit)
106745
107334
 
106746
107335
  Index Options:
106747
107336
  --cache <mode> Session cache mode: disk, memory, off
@@ -106759,7 +107348,7 @@ Tools are read-only unless --allow-build is passed.
106759
107348
  `;
106760
107349
  MCP_SERVE_HELP_TEXT = `codegraph mcp serve - MCP server for agent graph navigation
106761
107350
 
106762
- Usage: codegraph mcp [serve] [--root <path>] [--artifact <path>] [--stdio | --port <number>] [--host <host>] [--idle-timeout-ms <ms>] [--allow-build] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
107351
+ Usage: codegraph mcp [serve] [--root <path>] [--artifact <path>] [--stdio | --port <number>] [--host <host>] [--idle-timeout-ms <ms>] [--index-wait-ms <ms>] [--allow-build] [--warmup | --warmup-symbols | --no-warmup] [--no-watch]
106763
107352
 
106764
107353
  Tools:
106765
107354
  orient Build a compact first-turn repo packet
@@ -106793,7 +107382,8 @@ Index Options:
106793
107382
  Defaults:
106794
107383
  Transport defaults to stdio.
106795
107384
  HTTP transport binds to 127.0.0.1 unless --host is passed and serves /mcp.
106796
- Startup warms the base session cache by default. --warmup-symbols also warms the detailed symbol graph. --no-warmup starts lazily instead, leaving the first tool call to pay for discovery and building.
107385
+ The server answers initialize and tools/list at once. By default it then builds the base session cache and the search index in the background; --warmup-symbols also warms the detailed symbol graph, and --no-warmup builds only when a tool call needs the index.
107386
+ search, explore, orient, get_file with includeGraphContext, review, packet_get, workspace_symbols, and impact wait for a running build for at most --index-wait-ms (default 15000, well below the 30 seconds that clients commonly allow). Past that they answer { "status": "index_building" } with progress and a retry hint; the build continues, and the same call succeeds once it ends. Every other tool waits for the index without a limit.
106797
107387
  Warmup uses the configured session cache and shared index flags.
106798
107388
  The server watches project files and refreshes the index in the background: 500 ms after the last change, at most once every 5 seconds, and paused after 30 minutes without a tool call. --no-watch relies on the freshness check of each tool call instead.
106799
107389
  Tools are read-only unless --allow-build is passed.
@@ -106831,11 +107421,15 @@ Options:
106831
107421
  --min-tokens Minimum unit tokens. Defaults to 40, or 80 under cleanup profile.
106832
107422
  --max-tokens Maximum fallback chunk tokens. Defaults to 800.
106833
107423
  --max-bucket-size Skip candidate buckets larger than this value. Defaults to 200.
107424
+ --max-candidate-pairs
107425
+ Keep at most this many candidate pairs for scoring, strongest evidence first. Defaults to 2000000; maximum 16000000.
106834
107426
 
106835
107427
  Output:
106836
107428
  Pretty output is the default and emits one line per group with file spans, symbol or chunk labels, confidence, clone type, score, reduced lines, estimated reducible lines, token counts, heuristic family annotations, and cleanup labels.
106837
107429
  Pretty output also emits a compact summary footer unless --no-summary is passed.
106838
107430
  JSON output reports grouped duplicate findings, confidence, clone type, metrics, reduced-line fields, cleanup labels, clustered locations, omission counts including skipped candidate pairs, and pair stats.
107431
+ When --max-candidate-pairs drops pairs, stats.candidatePairBudget names the limit and the skipped count, and pretty output says the results are partial.
107432
+ Progress for unit collection, candidate pairs, scoring, and grouping, and the total elapsed time, goes to stderr; use --progress or --no-progress.
106839
107433
  Grouped duplicate JSON uses schemaVersion 3.
106840
107434
  --raw-pairs is only supported with similarity-ranked JSON output and cannot be combined with the cleanup profile.
106841
107435
  `;
@@ -107121,7 +107715,7 @@ var search_exports = {};
107121
107715
  __export(search_exports, {
107122
107716
  handleSearchCommand: () => handleSearchCommand
107123
107717
  });
107124
- import { performance as performance13 } from "node:perf_hooks";
107718
+ import { performance as performance14 } from "node:perf_hooks";
107125
107719
  function parseAgentSearchMode(rawValue) {
107126
107720
  if (rawValue === void 0)
107127
107721
  return "hybrid";
@@ -107131,7 +107725,7 @@ function parseAgentSearchMode(rawValue) {
107131
107725
  throw new Error(`Invalid --mode value "${rawValue}". Expected hybrid, symbol, path, text, graph, or sql.`);
107132
107726
  }
107133
107727
  async function handleSearchCommand(context) {
107134
- const commandStart = performance13.now();
107728
+ const commandStart = performance14.now();
107135
107729
  const query = context.positionals.join(" ").trim();
107136
107730
  if (!query) {
107137
107731
  context.writeStderrLine(SEARCH_HELP_TEXT.trimEnd());
@@ -107155,7 +107749,7 @@ async function handleSearchCommand(context) {
107155
107749
  context.writeStdoutLine(formatAgentSearchResponse(response));
107156
107750
  }
107157
107751
  if (context.commandReport && context.writeCommandReport) {
107158
- context.commandReport.timings.commandMs = Math.round(performance13.now() - commandStart);
107752
+ context.commandReport.timings.commandMs = Math.round(performance14.now() - commandStart);
107159
107753
  context.commandReport.timings.totalMs = context.commandReport.timings.commandMs;
107160
107754
  await context.writeCommandReport(context.commandReport, context.reportFile);
107161
107755
  }
@@ -107632,25 +108226,25 @@ function getCallHierarchyIndex(graph) {
107632
108226
  for (const edge of graph.edges) {
107633
108227
  if (edge.label !== "calls")
107634
108228
  continue;
107635
- const pairKey2 = `${edge.from}->${edge.to}`;
107636
- const relationship = relationshipByPair.get(pairKey2) ?? {
108229
+ const pairKey = `${edge.from}->${edge.to}`;
108230
+ const relationship = relationshipByPair.get(pairKey) ?? {
107637
108231
  caller: edge.from,
107638
108232
  callee: edge.to,
107639
108233
  sites: [],
107640
108234
  unresolvedSites: 0
107641
108235
  };
107642
108236
  if (edge.site) {
107643
- const siteKeys = siteKeysByPair.get(pairKey2) ?? /* @__PURE__ */ new Set();
108237
+ const siteKeys = siteKeysByPair.get(pairKey) ?? /* @__PURE__ */ new Set();
107644
108238
  const siteKey3 = callsiteKey(edge.site);
107645
108239
  if (!siteKeys.has(siteKey3)) {
107646
108240
  siteKeys.add(siteKey3);
107647
108241
  relationship.sites.push(edge.site);
107648
108242
  }
107649
- siteKeysByPair.set(pairKey2, siteKeys);
108243
+ siteKeysByPair.set(pairKey, siteKeys);
107650
108244
  } else {
107651
108245
  relationship.unresolvedSites += 1;
107652
108246
  }
107653
- relationshipByPair.set(pairKey2, relationship);
108247
+ relationshipByPair.set(pairKey, relationship);
107654
108248
  }
107655
108249
  for (const relationship of relationshipByPair.values()) {
107656
108250
  relationship.sites.sort(compareCallsites);
@@ -108904,7 +109498,7 @@ var init_base = __esm({
108904
109498
  const cwd = opts.cwd || process.cwd();
108905
109499
  const args = gitDiffArgs(opts.base, opts.head, ["--no-ext-diff", "--unified=0"]);
108906
109500
  try {
108907
- const child = spawn3("git", args, { cwd });
109501
+ const child = spawn3("git", args, { windowsHide: true, cwd });
108908
109502
  let stderr = "";
108909
109503
  child.stderr.on("data", (data) => {
108910
109504
  stderr += String(data);
@@ -113809,7 +114403,7 @@ __export(orient_exports, {
113809
114403
  formatAgentOrientation: () => formatAgentOrientation,
113810
114404
  handleOrientCommand: () => handleOrientCommand
113811
114405
  });
113812
- import { performance as performance14 } from "node:perf_hooks";
114406
+ import { performance as performance15 } from "node:perf_hooks";
113813
114407
  function parseAgentOrientBudget(rawValue) {
113814
114408
  if (rawValue === void 0)
113815
114409
  return "small";
@@ -113827,7 +114421,7 @@ function parseAgentOrientHealthMode(rawValue) {
113827
114421
  throw new Error(`Invalid --health value "${rawValue}". Expected skip, summary, or full.`);
113828
114422
  }
113829
114423
  async function handleOrientCommand(context) {
113830
- const commandStart = performance14.now();
114424
+ const commandStart = performance15.now();
113831
114425
  const healthMode = parseAgentOrientHealthMode(context.getOpt("--health"));
113832
114426
  const writesJson = context.hasFlag("--json");
113833
114427
  const indexReport = context.commandReport?.index;
@@ -113849,7 +114443,7 @@ async function handleOrientCommand(context) {
113849
114443
  context.writeStdoutLine(formatAgentOrientation(response));
113850
114444
  }
113851
114445
  if (context.commandReport && context.writeCommandReport) {
113852
- context.commandReport.timings.commandMs = Math.round(performance14.now() - commandStart);
114446
+ context.commandReport.timings.commandMs = Math.round(performance15.now() - commandStart);
113853
114447
  context.commandReport.timings.totalMs = context.commandReport.timings.commandMs;
113854
114448
  await context.writeCommandReport(context.commandReport, context.reportFile);
113855
114449
  }
@@ -131149,17 +131743,286 @@ var init_project_watcher = __esm({
131149
131743
  }
131150
131744
  });
131151
131745
 
131746
+ // dist/mcp/cancellation.js
131747
+ function mcpCancellationError(signal) {
131748
+ const reason = signal.reason;
131749
+ if (reason instanceof Error && reason.name !== "AbortError")
131750
+ return reason;
131751
+ return new Error("MCP tool call was cancelled.");
131752
+ }
131753
+ var init_cancellation = __esm({
131754
+ "dist/mcp/cancellation.js"() {
131755
+ "use strict";
131756
+ }
131757
+ });
131758
+
131759
+ // dist/mcp/stale-install.js
131760
+ function isStaleInstallError(error62) {
131761
+ let current = error62;
131762
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && current instanceof Error; depth += 1) {
131763
+ if ("code" in current && (current.code === "ERR_MODULE_NOT_FOUND" || current.code === "MODULE_NOT_FOUND"))
131764
+ return true;
131765
+ if (MODULE_NOT_FOUND_MESSAGE.test(current.message))
131766
+ return true;
131767
+ current = current.cause;
131768
+ }
131769
+ return false;
131770
+ }
131771
+ function describeMcpRuntimeError(error62) {
131772
+ return isStaleInstallError(error62) ? STALE_INSTALL_MESSAGE : errorMessage(error62);
131773
+ }
131774
+ var STALE_INSTALL_MESSAGE, MODULE_NOT_FOUND_MESSAGE, MAX_CAUSE_DEPTH;
131775
+ var init_stale_install = __esm({
131776
+ "dist/mcp/stale-install.js"() {
131777
+ "use strict";
131778
+ init_errors();
131779
+ STALE_INSTALL_MESSAGE = "The codegraph package changed on disk after this MCP server started, so a part of it that had not been loaded yet is gone. Restart the MCP server (or reload the MCP client) to run the installed version.";
131780
+ MODULE_NOT_FOUND_MESSAGE = /^Cannot find (?:module|package)\b/;
131781
+ MAX_CAUSE_DEPTH = 5;
131782
+ }
131783
+ });
131784
+
131785
+ // dist/mcp/index-readiness.js
131786
+ function assertMcpIndexWait(value) {
131787
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_MCP_INDEX_WAIT_MS) {
131788
+ throw new RangeError(`mcpIndexWaitMs must be a whole number from 0 through ${MAX_MCP_INDEX_WAIT_MS}; 0 disables the budget.`);
131789
+ }
131790
+ return value;
131791
+ }
131792
+ function searchNeeds(request) {
131793
+ if (canUsePathFastPath(request))
131794
+ return [];
131795
+ const needs = [searchNeedsSymbolGraph(request) ? "symbolGraph" : "index"];
131796
+ if (searchUsesQueryIndex(request))
131797
+ needs.push("queryIndex");
131798
+ return needs;
131799
+ }
131800
+ function exploreNeeds(request) {
131801
+ const needs = searchNeeds({});
131802
+ if (request.includeSource && request.maxPackets !== 0)
131803
+ needs.push("detailedGraph", "duplicates");
131804
+ return needs;
131805
+ }
131806
+ function describeBuildUpdate(update) {
131807
+ if (update.activity)
131808
+ return update.activity;
131809
+ if (update.phase === "start")
131810
+ return update.message;
131811
+ if (update.mode === "update")
131812
+ return "Updating project index";
131813
+ if (update.mode === "check")
131814
+ return "Checking project index";
131815
+ return "Indexing files";
131816
+ }
131817
+ function createMcpBuildProgress() {
131818
+ let latest;
131819
+ return {
131820
+ record: (update) => {
131821
+ if (update.phase === "complete") {
131822
+ latest = void 0;
131823
+ return;
131824
+ }
131825
+ latest = { activity: describeBuildUpdate(update), current: update.current, total: update.total };
131826
+ },
131827
+ latest: () => latest
131828
+ };
131829
+ }
131830
+ function settleWithinBudget(work, budgetMs, signal) {
131831
+ if (signal?.aborted)
131832
+ return Promise.reject(mcpCancellationError(signal));
131833
+ const { promise: promise3, resolve: resolve3, reject } = Promise.withResolvers();
131834
+ let timer;
131835
+ const onAbort = () => {
131836
+ cleanup();
131837
+ if (signal)
131838
+ reject(mcpCancellationError(signal));
131839
+ };
131840
+ const cleanup = () => {
131841
+ clearTimeout(timer);
131842
+ signal?.removeEventListener("abort", onAbort);
131843
+ };
131844
+ signal?.addEventListener("abort", onAbort, { once: true });
131845
+ if (budgetMs > 0) {
131846
+ timer = setTimeout(() => {
131847
+ cleanup();
131848
+ resolve3("timeout");
131849
+ }, budgetMs);
131850
+ timer.unref();
131851
+ }
131852
+ work.then(() => {
131853
+ cleanup();
131854
+ resolve3("ready");
131855
+ }, (error62) => {
131856
+ cleanup();
131857
+ reject(error62 instanceof Error ? error62 : new Error(String(error62)));
131858
+ });
131859
+ return promise3;
131860
+ }
131861
+ function createMcpIndexReadiness(session, options) {
131862
+ const pending = /* @__PURE__ */ new Set();
131863
+ let busySince;
131864
+ let warmupStarted = false;
131865
+ let invalidations = 0;
131866
+ const countInvalidations = () => {
131867
+ registerSessionInvalidationHook(session, () => {
131868
+ invalidations += 1;
131869
+ countInvalidations();
131870
+ });
131871
+ };
131872
+ countInvalidations();
131873
+ const track = (work) => {
131874
+ if (!pending.size)
131875
+ busySince = performance.now();
131876
+ pending.add(work);
131877
+ const release = () => {
131878
+ pending.delete(work);
131879
+ if (!pending.size)
131880
+ busySince = void 0;
131881
+ };
131882
+ work.then(release, release);
131883
+ return work;
131884
+ };
131885
+ const buildsSettled = async () => {
131886
+ while (pending.size)
131887
+ await Promise.allSettled([...pending]);
131888
+ };
131889
+ const load = async (need) => {
131890
+ if (need === "symbolGraph") {
131891
+ await session.loadProject({ symbolGraph: "basic" });
131892
+ } else if (need === "detailedGraph") {
131893
+ await session.loadProject();
131894
+ } else if (need === "duplicates") {
131895
+ await session.loadDuplicateAnalysis?.();
131896
+ } else if (need === "queryIndex") {
131897
+ const snapshot = await session.loadProject({ symbolGraph: "skip" });
131898
+ const { ensureSessionQueryIndex: ensureSessionQueryIndex2 } = await Promise.resolve().then(() => (init_session_store(), session_store_exports));
131899
+ await ensureSessionQueryIndex2(session, snapshot);
131900
+ } else {
131901
+ await session.loadProject({ symbolGraph: "skip" });
131902
+ }
131903
+ };
131904
+ const trackedLoad = (need) => track(load(need));
131905
+ const startWarmup = () => {
131906
+ if (warmupStarted)
131907
+ return;
131908
+ warmupStarted = true;
131909
+ if (!options.warmup.length)
131910
+ return;
131911
+ void track(Promise.all(options.warmup.map(trackedLoad)).then(() => void 0, (error62) => {
131912
+ console.error(`[codegraph] MCP startup warmup failed: ${describeMcpRuntimeError(error62)}`);
131913
+ }));
131914
+ };
131915
+ const describeProgress = (needs, done, waitedMs) => {
131916
+ const elapsedMs2 = Math.round(busySince === void 0 ? waitedMs : performance.now() - busySince);
131917
+ const update = options.progress.latest();
131918
+ if (update) {
131919
+ return {
131920
+ activity: update.activity,
131921
+ ...update.total > 0 ? { current: update.current, total: update.total } : {},
131922
+ elapsedMs: elapsedMs2
131923
+ };
131924
+ }
131925
+ const unfinished = needs.find((need) => !done.has(need));
131926
+ return { activity: unfinished ? ACTIVITY[unfinished] : CHECKING_ACTIVITY, elapsedMs: elapsedMs2 };
131927
+ };
131928
+ const waitFor = async (tool, needs, signal, followUp) => {
131929
+ if (signal?.aborted)
131930
+ throw mcpCancellationError(signal);
131931
+ const startedAt = performance.now();
131932
+ let wanted = [...needs];
131933
+ const done = /* @__PURE__ */ new Set();
131934
+ const loadAll = async (list) => {
131935
+ await Promise.all(list.map(async (need) => {
131936
+ await trackedLoad(need);
131937
+ done.add(need);
131938
+ }));
131939
+ };
131940
+ const ready = track((async () => {
131941
+ await checkSessionFreshness(session);
131942
+ for (let round = 0; round < MAX_READINESS_ROUNDS; round += 1) {
131943
+ const loadedAt = invalidations;
131944
+ wanted = [...needs];
131945
+ done.clear();
131946
+ await loadAll(needs);
131947
+ if (followUp) {
131948
+ const later = await followUp();
131949
+ wanted.push(...later);
131950
+ await loadAll(later);
131951
+ }
131952
+ await checkSessionFreshness(session);
131953
+ if (invalidations === loadedAt)
131954
+ return;
131955
+ }
131956
+ })());
131957
+ if (await settleWithinBudget(ready, options.waitMs, signal) === "ready")
131958
+ return;
131959
+ const waitedMs = Math.round(performance.now() - startedAt);
131960
+ const progress = describeProgress(wanted, done, waitedMs);
131961
+ const counts = progress.total === void 0 ? "" : ` ${progress.current ?? 0}/${progress.total}`;
131962
+ throw new McpIndexBuildingError({
131963
+ status: "index_building",
131964
+ tool,
131965
+ message: `Codegraph is still building the index for this project (${progress.activity}${counts}, ${Math.round(progress.elapsedMs / 1e3)}s so far). The build continues in the background; call '${tool}' again to wait for it.`,
131966
+ retryAfterMs: MCP_INDEX_RETRY_AFTER_MS,
131967
+ waitedMs,
131968
+ progress
131969
+ });
131970
+ };
131971
+ return { startWarmup, buildsSettled, waitFor };
131972
+ }
131973
+ var DEFAULT_MCP_INDEX_WAIT_MS, MCP_INDEX_RETRY_AFTER_MS, MAX_MCP_INDEX_WAIT_MS, McpIndexBuildingError, ACTIVITY, CHECKING_ACTIVITY, MAX_READINESS_ROUNDS;
131974
+ var init_index_readiness = __esm({
131975
+ "dist/mcp/index-readiness.js"() {
131976
+ "use strict";
131977
+ init_search();
131978
+ init_session();
131979
+ init_session_lifecycle();
131980
+ init_cancellation();
131981
+ init_stale_install();
131982
+ DEFAULT_MCP_INDEX_WAIT_MS = 15e3;
131983
+ MCP_INDEX_RETRY_AFTER_MS = 1e3;
131984
+ MAX_MCP_INDEX_WAIT_MS = 2147483647;
131985
+ McpIndexBuildingError = class extends Error {
131986
+ result;
131987
+ constructor(result) {
131988
+ super(result.message);
131989
+ this.name = "McpIndexBuildingError";
131990
+ this.result = result;
131991
+ }
131992
+ };
131993
+ ACTIVITY = {
131994
+ index: "Building project index",
131995
+ symbolGraph: "Building symbol graph",
131996
+ queryIndex: "Building search index",
131997
+ detailedGraph: "Building detailed symbol graph",
131998
+ duplicates: "Preparing duplicate analysis"
131999
+ };
132000
+ CHECKING_ACTIVITY = "Checking the project for changed files";
132001
+ MAX_READINESS_ROUNDS = 3;
132002
+ }
132003
+ });
132004
+
131152
132005
  // dist/mcp/handlers.js
131153
132006
  import fs42 from "node:fs/promises";
131154
132007
  import path96 from "node:path";
131155
132008
  function assertMcpSessionOptions(options) {
131156
132009
  assertNoPrebuiltSessionWithBuildOptions(options, "MCP server options");
131157
132010
  }
131158
- function createCodegraphMcpSession(options, root) {
132011
+ function createCodegraphMcpSession(options, root, buildProgress) {
131159
132012
  assertMcpSessionOptions(options);
131160
- return options.session ?? createAgentSession({
132013
+ if (options.session)
132014
+ return options.session;
132015
+ const onProgress = options.buildOptions?.onProgress;
132016
+ const buildOptions = buildProgress ? {
132017
+ ...options.buildOptions,
132018
+ onProgress: (update) => {
132019
+ buildProgress.record(update);
132020
+ onProgress?.(update);
132021
+ }
132022
+ } : options.buildOptions;
132023
+ return createAgentSession({
131161
132024
  root,
131162
- ...options.buildOptions ? { buildOptions: options.buildOptions } : {},
132025
+ ...buildOptions ? { buildOptions } : {},
131163
132026
  freshness: { policy: "auto" }
131164
132027
  });
131165
132028
  }
@@ -131170,14 +132033,49 @@ function resolveWatchedCacheDirectory(root, buildOptions) {
131170
132033
  return void 0;
131171
132034
  }
131172
132035
  }
131173
- function startCodegraphMcpWarmup(session, warmup, sessionProvidedExplicitly) {
131174
- const mode = warmup ?? (sessionProvidedExplicitly ? "off" : "base");
132036
+ function resolveStartupWarmupMode(warmup, sessionProvidedExplicitly) {
132037
+ return warmup ?? (sessionProvidedExplicitly ? "off" : "base");
132038
+ }
132039
+ function rebuildSession(session, mode) {
131175
132040
  if (mode === "off")
131176
132041
  return void 0;
131177
132042
  if (mode === "symbols")
131178
132043
  return session.loadProject();
131179
132044
  return session.loadProject({ symbolGraph: "skip" });
131180
132045
  }
132046
+ function warmupNeeds(mode) {
132047
+ if (mode === "off")
132048
+ return [];
132049
+ if (mode === "symbols")
132050
+ return ["symbolGraph", "queryIndex", "detailedGraph"];
132051
+ return ["symbolGraph", "queryIndex"];
132052
+ }
132053
+ function withIndexBudget(handlers, readiness, session) {
132054
+ const budgeted = (tool, handler, needsOf, followUpOf) => async (request, signal) => {
132055
+ const needs = needsOf(request);
132056
+ if (needs)
132057
+ await readiness.waitFor(tool, needs, signal, followUpOf?.(request));
132058
+ return await handler(request, signal);
132059
+ };
132060
+ return {
132061
+ ...handlers,
132062
+ search: budgeted("search", handlers.search, searchNeeds),
132063
+ explore: budgeted("explore", handlers.explore, exploreNeeds),
132064
+ orient: budgeted("orient", handlers.orient, () => ["index"]),
132065
+ workspace_symbols: budgeted("workspace_symbols", handlers.workspace_symbols, () => ["index"]),
132066
+ impact: budgeted("impact", handlers.impact, () => ["index"]),
132067
+ // Only a graph context reads the index; the bytes of a file come from disk.
132068
+ get_file: budgeted("get_file", handlers.get_file, (request) => request.includeGraphContext ? ["index"] : void 0),
132069
+ // A `review:` target builds its own report from git and reads no session build. Any other target reads the detailed
132070
+ // symbol graph, and also the duplicate analysis of its duplicate context, except a SQL object, which has none, and
132071
+ // a target that does not resolve, which fails at once. What the target resolves to is known once the detailed
132072
+ // symbol graph is loaded, so that decision follows it.
132073
+ packet_get: budgeted("packet_get", handlers.packet_get, (request) => request.target.startsWith("review:") ? [] : ["detailedGraph"], (request) => request.target.startsWith("review:") ? void 0 : async () => {
132074
+ const { kind } = await resolveCodegraphTargetWithSession(session, { target: request.target });
132075
+ return kind === "sql_object" || kind === "not_found" ? [] : ["duplicates"];
132076
+ })
132077
+ };
132078
+ }
131181
132079
  function withToolActivity(handlers, noteActivity) {
131182
132080
  return new Proxy(handlers, {
131183
132081
  get(target, property, receiver) {
@@ -131191,19 +132089,25 @@ function withToolActivity(handlers, noteActivity) {
131191
132089
  }
131192
132090
  });
131193
132091
  }
131194
- async function createWarmedCodegraphMcpResources(options) {
132092
+ function createCodegraphMcpResources(options) {
131195
132093
  const root = path96.resolve(options.root);
131196
132094
  const sessionProvidedExplicitly = options.session !== void 0;
131197
- const { warmup, host, port, onHttpListen, watch, watchDebounceMs, watchMinRefreshIntervalMs, watchIdleMs, ...handlerOptions } = options;
132095
+ const { warmup, host, port, onHttpListen, watch, watchDebounceMs, watchMinRefreshIntervalMs, watchIdleMs, mcpIndexWaitMs, ...handlerOptions } = options;
131198
132096
  void host;
131199
132097
  void port;
131200
132098
  void onHttpListen;
131201
132099
  assertMcpWatchMs("watchDebounceMs", watchDebounceMs);
131202
132100
  assertMcpWatchMs("watchMinRefreshIntervalMs", watchMinRefreshIntervalMs);
131203
132101
  assertMcpWatchMs("watchIdleMs", watchIdleMs);
131204
- const session = createCodegraphMcpSession(options, root);
131205
- await startCodegraphMcpWarmup(session, warmup, sessionProvidedExplicitly);
131206
- const { handlers, runExclusive } = createCodegraphMcpHandlersForSession({ ...handlerOptions, root }, session);
132102
+ const indexWaitMs = assertMcpIndexWait(mcpIndexWaitMs ?? DEFAULT_MCP_INDEX_WAIT_MS);
132103
+ const buildProgress = createMcpBuildProgress();
132104
+ const session = createCodegraphMcpSession(options, root, buildProgress);
132105
+ const readiness = createMcpIndexReadiness(session, {
132106
+ waitMs: indexWaitMs,
132107
+ warmup: warmupNeeds(resolveStartupWarmupMode(warmup, sessionProvidedExplicitly)),
132108
+ progress: buildProgress
132109
+ });
132110
+ const { handlers, runExclusive } = createCodegraphMcpHandlersForSession({ ...handlerOptions, root }, session, readiness);
131207
132111
  const watcher = sessionProvidedExplicitly || watch === false ? void 0 : createWatchController(session, {
131208
132112
  root,
131209
132113
  runExclusive,
@@ -131216,10 +132120,12 @@ async function createWarmedCodegraphMcpResources(options) {
131216
132120
  return {
131217
132121
  handlers: watcher ? withToolActivity(handlers, watcher.noteActivity) : handlers,
131218
132122
  session,
131219
- closeWatcher: () => watcher?.close() ?? Promise.resolve()
132123
+ closeWatcher: () => watcher?.close() ?? Promise.resolve(),
132124
+ startWarmup: readiness.startWarmup,
132125
+ buildsSettled: readiness.buildsSettled
131220
132126
  };
131221
132127
  }
131222
- function createCodegraphMcpHandlersForSession(options, session) {
132128
+ function createCodegraphMcpHandlersForSession(options, session, readiness) {
131223
132129
  const root = path96.resolve(options.root);
131224
132130
  const readOnly = options.readOnly ?? true;
131225
132131
  const realRoot = fs42.realpath(root);
@@ -131672,20 +132578,30 @@ function createCodegraphMcpHandlersForSession(options, session) {
131672
132578
  ...DEFAULT_BOUNDED_IMPACT_BUDGETS
131673
132579
  });
131674
132580
  }),
131675
- review: async (request) => await withFreshness(async () => {
131676
- const report2 = await buildReviewReport(root, {
132581
+ review: async (request, signal) => {
132582
+ const reviewOptions = {
131677
132583
  ...options.buildOptions,
131678
132584
  gitBase: request.base,
131679
132585
  gitHead: request.head,
131680
132586
  ...request.reviewDepth !== void 0 ? { reviewDepth: request.reviewDepth } : {}
131681
- }, {
131682
- ...session.loadProject ? {
131683
- loadIndex: async () => (await session.loadProject({ symbolGraph: "skip" })).index
131684
- } : {},
131685
- ...session.loadDuplicateAnalysis ? { loadDuplicateAnalysis: session.loadDuplicateAnalysis } : {}
132587
+ };
132588
+ const changes = await collectReviewChanges(root, reviewOptions);
132589
+ if (!changes.changedFiles.size) {
132590
+ const report2 = await buildReviewReport(root, reviewOptions, { changes });
132591
+ return { ...boundReviewReportForTransport(report2), freshness: { state: "fresh" } };
132592
+ }
132593
+ await readiness?.waitFor("review", ["index", "duplicates"], signal);
132594
+ return await withFreshness(async () => {
132595
+ const report2 = await buildReviewReport(root, reviewOptions, {
132596
+ changes,
132597
+ ...session.loadProject ? {
132598
+ loadIndex: async () => (await session.loadProject({ symbolGraph: "skip" })).index
132599
+ } : {},
132600
+ ...session.loadDuplicateAnalysis ? { loadDuplicateAnalysis: session.loadDuplicateAnalysis } : {}
132601
+ });
132602
+ return boundReviewReportForTransport(report2);
131686
132603
  });
131687
- return boundReviewReportForTransport(report2);
131688
- })
132604
+ }
131689
132605
  };
131690
132606
  const artifactHandlers = {
131691
132607
  query_sqlite: async (request, executionOptions) => {
@@ -131721,7 +132637,7 @@ function createCodegraphMcpHandlersForSession(options, session) {
131721
132637
  sqlitePath = configuredSqlitePath;
131722
132638
  sqliteOutDir = configuredSqliteOutDir;
131723
132639
  sqliteCanRefresh = configuredSqliteCanRefresh;
131724
- await startCodegraphMcpWarmup(session, warmup, true);
132640
+ await rebuildSession(session, warmup);
131725
132641
  })();
131726
132642
  refreshPromise = refresh;
131727
132643
  try {
@@ -131766,8 +132682,14 @@ function createCodegraphMcpHandlersForSession(options, session) {
131766
132682
  session.invalidate();
131767
132683
  }
131768
132684
  };
132685
+ const handlers = {
132686
+ ...searchHandlers,
132687
+ ...navigationHandlers,
132688
+ ...reviewHandlers,
132689
+ ...artifactHandlers
132690
+ };
131769
132691
  return {
131770
- handlers: { ...searchHandlers, ...navigationHandlers, ...reviewHandlers, ...artifactHandlers },
132692
+ handlers: readiness ? withIndexBudget(handlers, readiness, session) : handlers,
131771
132693
  runExclusive: withArtifactWriteLock
131772
132694
  };
131773
132695
  }
@@ -131796,6 +132718,7 @@ var init_handlers = __esm({
131796
132718
  init_impact();
131797
132719
  init_budgets();
131798
132720
  init_review2();
132721
+ init_changes();
131799
132722
  init_types4();
131800
132723
  init_sqlite();
131801
132724
  init_bounds();
@@ -131812,6 +132735,7 @@ var init_handlers = __esm({
131812
132735
  init_runtime_identity();
131813
132736
  init_security();
131814
132737
  init_project_watcher();
132738
+ init_index_readiness();
131815
132739
  MAX_MCP_FRESHNESS_CHANGED_FILES = 25;
131816
132740
  MAX_MCP_FRESHNESS_RETRIES = 3;
131817
132741
  SQLITE_ARTIFACT_STAT_CONCURRENCY = 64;
@@ -135209,6 +136133,10 @@ function createCodegraphMcpProtocolServerWithTracker(handlers, runtimeIdentity,
135209
136133
  } catch (error62) {
135210
136134
  if (error62 instanceof ProtocolError || toolCallAbort.signal.aborted)
135211
136135
  throw error62;
136136
+ if (error62 instanceof McpIndexBuildingError) {
136137
+ await emitFirstToolCallVisibility("info", 1, error62.result.message);
136138
+ return toToolResult(error62.result);
136139
+ }
135212
136140
  return toToolErrorResult(error62);
135213
136141
  }
135214
136142
  } finally {
@@ -135320,16 +136248,10 @@ function awaitMcpToolOperation(signal, operation, onSettled) {
135320
136248
  function withAbortSignal(signal, operation) {
135321
136249
  if (!signal)
135322
136250
  return operation;
135323
- const cancellationError = () => {
135324
- const reason = signal.reason;
135325
- if (reason instanceof Error && reason.name !== "AbortError")
135326
- return reason;
135327
- return new Error("MCP tool call was cancelled.");
135328
- };
135329
136251
  if (signal.aborted)
135330
- return Promise.reject(cancellationError());
136252
+ return Promise.reject(mcpCancellationError(signal));
135331
136253
  const cancellation = Promise.withResolvers();
135332
- const onAbort = () => cancellation.reject(cancellationError());
136254
+ const onAbort = () => cancellation.reject(mcpCancellationError(signal));
135333
136255
  signal.addEventListener("abort", onAbort, { once: true });
135334
136256
  return Promise.race([operation, cancellation.promise]).finally(() => signal.removeEventListener("abort", onAbort));
135335
136257
  }
@@ -135499,7 +136421,7 @@ function firstDuplicateFile(arguments_) {
135499
136421
  return files.find((file2) => typeof file2 === "string") ?? ".";
135500
136422
  }
135501
136423
  function toToolErrorResult(error62) {
135502
- const message = errorMessage(error62).replace(/(?:[A-Za-z]:)?[/\\][^'"\n]*?(?=(?:[/\\][^'"\n]*)?['"]|$)/g, "<path>");
136424
+ const message = describeMcpRuntimeError(error62).replace(/(?:[A-Za-z]:)?[/\\][^'"\n]*?(?=(?:[/\\][^'"\n]*)?['"]|$)/g, "<path>");
135503
136425
  return { isError: true, content: [{ type: "text", text: message }] };
135504
136426
  }
135505
136427
  function getToolCallProgressToken(params) {
@@ -135521,12 +136443,19 @@ function parseMcpToolInput(schema, input2, toolName) {
135521
136443
  return parsed.data;
135522
136444
  throw new ProtocolError(ProtocolErrorCode.InvalidParams, formatMcpInvalidParams(toolName, parsed.error));
135523
136445
  }
136446
+ function describeReceivedValue(value) {
136447
+ if (value === null)
136448
+ return "null";
136449
+ if (value === void 0)
136450
+ return "nothing (the field is missing)";
136451
+ return typeof value;
136452
+ }
135524
136453
  function createCodegraphMcpStdioTransport(input2, output2) {
135525
136454
  return new StdioServerTransport(createParseErrorReportingStdin(input2, output2), output2, {
135526
136455
  maxBufferSize: MAX_MCP_STDIO_FRAME_BYTES
135527
136456
  });
135528
136457
  }
135529
- var DEFAULT_MCP_TOOL_TIMEOUT_MS, MAX_MCP_TOOL_TIMEOUT_MS, MAX_MCP_STDIO_FRAME_BYTES, DEFAULT_MCP_TOOL_CONCURRENCY, mcpProgressTokenSchema, toolCallMetaSchema, searchSchema, workspaceSymbolsSchema, renamePreviewSchema, refactorPlanSchema, callHierarchySchema, callsSchema, typeHierarchySchema, typeHierarchyUnifiedSchema, implementationsSchema, exploreSchema, orientSchema, packetGetSchema, getFileSchema, handleSchema, navigationSchema, refsSchema, fileGraphSchema, fileDepsUnifiedSchema, pathSchema, gitRangeSchema, reviewSchema, querySqliteSchema, refreshIndexSchema, artifactBuildSchema;
136458
+ var DEFAULT_MCP_TOOL_TIMEOUT_MS, MAX_MCP_TOOL_TIMEOUT_MS, MAX_MCP_STDIO_FRAME_BYTES, DEFAULT_MCP_TOOL_CONCURRENCY, mcpProgressTokenSchema, toolCallMetaSchema, searchSchema, workspaceSymbolsSchema, renamePreviewSchema, refactorPlanSchema, callHierarchySchema, callsSchema, typeHierarchySchema, typeHierarchyUnifiedSchema, implementationsSchema, exploreSchema, orientSchema, packetGetSchema, getFilePathSchema, getFileSchema, handleSchema, navigationSchema, refsSchema, fileGraphSchema, fileDepsUnifiedSchema, pathSchema, gitRangeSchema, reviewSchema, querySqliteSchema, refreshIndexSchema, artifactBuildSchema;
135530
136459
  var init_protocol = __esm({
135531
136460
  "dist/mcp/protocol.js"() {
135532
136461
  "use strict";
@@ -135541,7 +136470,10 @@ var init_protocol = __esm({
135541
136470
  init_tools();
135542
136471
  init_runtime();
135543
136472
  init_runtime_identity();
136473
+ init_cancellation();
136474
+ init_index_readiness();
135544
136475
  init_legacy_sessions();
136476
+ init_stale_install();
135545
136477
  DEFAULT_MCP_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
135546
136478
  MAX_MCP_TOOL_TIMEOUT_MS = 2147483647;
135547
136479
  MAX_MCP_STDIO_FRAME_BYTES = 10 * 1024 * 1024;
@@ -135620,8 +136552,11 @@ var init_protocol = __esm({
135620
136552
  maxSnippets: external_exports.number().int().positive().max(50).optional(),
135621
136553
  maxDuplicates: external_exports.number().int().positive().max(20).optional()
135622
136554
  }).strict();
136555
+ getFilePathSchema = external_exports.string({
136556
+ error: (issue2) => `expected a project-relative file path string, received ${describeReceivedValue(issue2.input)}`
136557
+ });
135623
136558
  getFileSchema = external_exports.object({
135624
- file: external_exports.string(),
136559
+ file: getFilePathSchema,
135625
136560
  offset: external_exports.number().int().positive().optional(),
135626
136561
  limit: external_exports.number().int().positive().max(MAX_FILE_VIEW_LINES).optional(),
135627
136562
  maxBytes: external_exports.number().int().positive().max(MAX_FILE_VIEW_BYTES).optional(),
@@ -135762,7 +136697,7 @@ async function startCodegraphMcpHttpServer(options) {
135762
136697
  const configuredMcpToolTimeout = options.mcpToolTimeoutMs === void 0 ? DEFAULT_MCP_TOOL_TIMEOUT_MS : options.mcpToolTimeoutMs;
135763
136698
  const mcpToolTimeoutMs = assertMcpToolTimeout(configuredMcpToolTimeout);
135764
136699
  const host = options.host ?? "127.0.0.1", lifecycleHealth = await Promise.resolve().then(() => (init_lifecycle_health(), lifecycle_health_exports));
135765
- const { handlers, session, closeWatcher } = await createWarmedCodegraphMcpResources(options);
136700
+ const { handlers, session, closeWatcher, startWarmup, buildsSettled } = createCodegraphMcpResources(options);
135766
136701
  const runtimeIdentity = options.runtimeIdentity ?? captureCodegraphRuntimeIdentity(getCurrentNativeBindingOrigin());
135767
136702
  const installedVersionChecker = createInstalledVersionChecker(runtimeIdentity, { warn: () => {
135768
136703
  } });
@@ -135796,6 +136731,7 @@ async function startCodegraphMcpHttpServer(options) {
135796
136731
  } finally {
135797
136732
  await protocolFactory.drain();
135798
136733
  await watcherClosed;
136734
+ await buildsSettled();
135799
136735
  session.invalidate();
135800
136736
  }
135801
136737
  })();
@@ -135837,6 +136773,7 @@ async function startCodegraphMcpHttpServer(options) {
135837
136773
  const urlHost = formatHostForUrl(host);
135838
136774
  const url2 = `http://${urlHost}:${actualPort}${MCP_HTTP_PATH}`;
135839
136775
  allowedHostHeaders = buildAllowedHostHeaders(host, actualPort);
136776
+ startWarmup();
135840
136777
  return {
135841
136778
  server,
135842
136779
  host,
@@ -135953,7 +136890,7 @@ async function serveCodegraphMcp(options) {
135953
136890
  await waitForHttpServerClose(started.server);
135954
136891
  return;
135955
136892
  }
135956
- const { handlers, session, closeWatcher } = await createWarmedCodegraphMcpResources(options);
136893
+ const { handlers, session, closeWatcher, startWarmup, buildsSettled } = createCodegraphMcpResources(options);
135957
136894
  const runtimeIdentity = options.runtimeIdentity ?? captureCodegraphRuntimeIdentity(getCurrentNativeBindingOrigin());
135958
136895
  const protocolFactory = createCodegraphMcpProtocolFactory(handlers, runtimeIdentity, options.mcpToolConcurrency ?? DEFAULT_MCP_TOOL_CONCURRENCY, mcpToolTimeoutMs);
135959
136896
  const transport = createCodegraphMcpStdioTransport(process.stdin, process.stdout);
@@ -135964,6 +136901,7 @@ async function serveCodegraphMcp(options) {
135964
136901
  console.error(`[codegraph] MCP stdio error: ${error62.message}`);
135965
136902
  }
135966
136903
  });
136904
+ startWarmup();
135967
136905
  const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_MCP_STDIO_IDLE_TIMEOUT_MS;
135968
136906
  try {
135969
136907
  await awaitStdioMcpLifecycle(handle, {
@@ -135977,6 +136915,7 @@ async function serveCodegraphMcp(options) {
135977
136915
  const watcherClosed = closeWatcher();
135978
136916
  await protocolFactory.drain();
135979
136917
  await watcherClosed;
136918
+ await buildsSettled();
135980
136919
  session.invalidate();
135981
136920
  }
135982
136921
  process.exitCode = 0;
@@ -135997,6 +136936,7 @@ var init_server = __esm({
135997
136936
  init_protocol();
135998
136937
  init_http_transport();
135999
136938
  init_http_transport();
136939
+ init_index_readiness();
136000
136940
  init_legacy_sessions();
136001
136941
  }
136002
136942
  });
@@ -136015,9 +136955,11 @@ async function handleMcpServeCommand(context) {
136015
136955
  const artifactPath = context.getOpt("--artifact");
136016
136956
  let port;
136017
136957
  let idleTimeoutMs;
136958
+ let indexWaitMs;
136018
136959
  try {
136019
136960
  port = parseOptionalBoundedIntegerOption(context.getOpt("--port"), "--port", 0, 65535);
136020
136961
  idleTimeoutMs = parseOptionalBoundedIntegerOption(context.getOpt("--idle-timeout-ms"), "--idle-timeout-ms", 0, 24 * 60 * 60 * 1e3);
136962
+ indexWaitMs = parseOptionalBoundedIntegerOption(context.getOpt("--index-wait-ms"), "--index-wait-ms", 0, MAX_MCP_INDEX_WAIT_MS);
136021
136963
  } catch (error62) {
136022
136964
  exitWithError(context, error62, 2);
136023
136965
  }
@@ -136045,6 +136987,7 @@ async function handleMcpServeCommand(context) {
136045
136987
  warmup,
136046
136988
  ...context.hasFlag("--no-watch") ? { watch: false } : {},
136047
136989
  ...idleTimeoutMs !== void 0 ? { idleTimeoutMs } : {},
136990
+ ...indexWaitMs !== void 0 ? { mcpIndexWaitMs: indexWaitMs } : {},
136048
136991
  onHttpListen: (info) => {
136049
136992
  context.writeStderrLine(`Codegraph MCP HTTP server listening at ${info.url}`);
136050
136993
  }
@@ -136070,6 +137013,7 @@ var init_mcp = __esm({
136070
137013
  "dist/cli/mcp.js"() {
136071
137014
  "use strict";
136072
137015
  init_server();
137016
+ init_index_readiness();
136073
137017
  init_help();
136074
137018
  init_options();
136075
137019
  init_context();
@@ -136898,6 +137842,7 @@ var init_server2 = __esm({
136898
137842
  "--host",
136899
137843
  "--ignore-glob",
136900
137844
  "--include-glob",
137845
+ "--index-wait-ms",
136901
137846
  "--port",
136902
137847
  "--resolution-hint",
136903
137848
  "--startup-timeout-ms",
@@ -136928,6 +137873,7 @@ var init_server2 = __esm({
136928
137873
  "--cache-dir",
136929
137874
  "--include-glob",
136930
137875
  "--ignore-glob",
137876
+ "--index-wait-ms",
136931
137877
  "--resolution-hint",
136932
137878
  "--threads"
136933
137879
  ];
@@ -137499,7 +138445,7 @@ __export(graph_exports, {
137499
138445
  });
137500
138446
  import fs44 from "node:fs";
137501
138447
  import fsp43 from "node:fs/promises";
137502
- import { performance as performance15 } from "node:perf_hooks";
138448
+ import { performance as performance16 } from "node:perf_hooks";
137503
138449
  function resolveGraphCacheMode(cache) {
137504
138450
  return cache ?? "disk";
137505
138451
  }
@@ -137636,11 +138582,11 @@ function stabilizeSymbolGraph(graph) {
137636
138582
  }
137637
138583
  async function handleGraphCommand(context) {
137638
138584
  const commandReport = context.reportEnabled ? { command: "graph", timings: {} } : void 0;
137639
- const commandStart = performance15.now();
137640
- const resolveStart = performance15.now();
138585
+ const commandStart = performance16.now();
138586
+ const resolveStart = performance16.now();
137641
138587
  const files = await context.resolveFiles();
137642
138588
  if (commandReport) {
137643
- commandReport.timings.resolveFilesMs = Math.round(performance15.now() - resolveStart);
138589
+ commandReport.timings.resolveFilesMs = Math.round(performance16.now() - resolveStart);
137644
138590
  }
137645
138591
  const hasExplicitSymbolFlag = context.hasFlag("--symbols") || context.hasFlag("--symbols-only") || context.hasFlag("--symbols-detailed");
137646
138592
  const outputArg = context.getOpt("--output") ?? context.getOpt("--out");
@@ -137682,7 +138628,7 @@ async function handleGraphCommand(context) {
137682
138628
  const finalizeReport = async () => {
137683
138629
  if (!commandReport)
137684
138630
  return;
137685
- commandReport.timings.commandMs = Math.round(performance15.now() - commandStart);
138631
+ commandReport.timings.commandMs = Math.round(performance16.now() - commandStart);
137686
138632
  commandReport.timings.totalMs = commandReport.timings.commandMs;
137687
138633
  await context.writeCommandReport(commandReport, context.reportFile);
137688
138634
  };
@@ -137915,7 +138861,7 @@ var cli_exports = {};
137915
138861
  __export(cli_exports, {
137916
138862
  handleIndexCommand: () => handleIndexCommand
137917
138863
  });
137918
- import { performance as performance16 } from "node:perf_hooks";
138864
+ import { performance as performance17 } from "node:perf_hooks";
137919
138865
  function formatIndexOutput(output2) {
137920
138866
  const lines = [`Indexed ${output2.files} file(s) with ${output2.edges} edge(s).`];
137921
138867
  if (!isAnalysisComplete(output2.analysis)) {
@@ -137933,16 +138879,16 @@ function formatIndexOutput(output2) {
137933
138879
  async function handleIndexCommand(context) {
137934
138880
  const verbose = context.hasFlag("--verbose");
137935
138881
  const commandReport = context.reportEnabled ? { command: "index", timings: {} } : void 0;
137936
- const commandStart = performance16.now();
138882
+ const commandStart = performance17.now();
137937
138883
  const shouldWriteManifest = !context.includeRootsAbs.length && !context.gitBase && !context.changedSince;
137938
138884
  const hasCliDiscoveryGlob = context.getOpt("--include-glob") !== void 0 || context.getOpt("--ignore-glob") !== void 0;
137939
138885
  const needsResolvedFiles = !shouldWriteManifest || hasCliDiscoveryGlob;
137940
138886
  let files = [];
137941
138887
  if (needsResolvedFiles) {
137942
- const resolveStart = performance16.now();
138888
+ const resolveStart = performance17.now();
137943
138889
  files = await context.resolveFiles();
137944
138890
  if (commandReport) {
137945
- commandReport.timings.resolveFilesMs = Math.round(performance16.now() - resolveStart);
138891
+ commandReport.timings.resolveFilesMs = Math.round(performance17.now() - resolveStart);
137946
138892
  }
137947
138893
  }
137948
138894
  const threads = parseNonNegativeIntegerOption(context.getOpt("--threads"), "--threads", 0);
@@ -138003,7 +138949,7 @@ async function handleIndexCommand(context) {
138003
138949
  }
138004
138950
  }
138005
138951
  if (commandReport) {
138006
- commandReport.timings.commandMs = Math.round(performance16.now() - commandStart);
138952
+ commandReport.timings.commandMs = Math.round(performance17.now() - commandStart);
138007
138953
  commandReport.timings.totalMs = commandReport.timings.commandMs;
138008
138954
  await context.writeCommandReport(commandReport, context.reportFile);
138009
138955
  }
@@ -138810,6 +139756,7 @@ async function cleanupTempDir(dir) {
138810
139756
  async function resolveGitCommit(root, ref) {
138811
139757
  const rev = `${ref}^{commit}`;
138812
139758
  const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", rev], {
139759
+ windowsHide: true,
138813
139760
  cwd: root,
138814
139761
  env: process.env
138815
139762
  });
@@ -138824,9 +139771,16 @@ async function materializeGitRef(root, ref, prefix) {
138824
139771
  return { root };
138825
139772
  const tempRoot = await fsp45.mkdtemp(path101.join(os12.tmpdir(), prefix));
138826
139773
  try {
138827
- await execFileAsync("git", ["clone", "--quiet", "--no-checkout", root, tempRoot], { env: process.env });
139774
+ await execFileAsync("git", ["clone", "--quiet", "--no-checkout", root, tempRoot], {
139775
+ windowsHide: true,
139776
+ env: process.env
139777
+ });
138828
139778
  const checkoutCommit = await resolveGitCommit(root, checkoutRef);
138829
- await execFileAsync("git", ["checkout", "--quiet", checkoutCommit], { cwd: tempRoot, env: process.env });
139779
+ await execFileAsync("git", ["checkout", "--quiet", checkoutCommit], {
139780
+ windowsHide: true,
139781
+ cwd: tempRoot,
139782
+ env: process.env
139783
+ });
138830
139784
  return { root: tempRoot, cleanup: tempRoot };
138831
139785
  } catch (error62) {
138832
139786
  await cleanupTempDir(tempRoot);
@@ -139066,6 +140020,8 @@ function getOptionWithProfileDefault(context, optionName2, profile, cleanupDefau
139066
140020
  function parseDuplicateDetectionOptions(context, profile) {
139067
140021
  const minConfidence = parseConfidenceOption(getOptionWithProfileDefault(context, "--min-confidence", profile, "medium"));
139068
140022
  const minTokens = parsePositiveIntegerOption(getOptionWithProfileDefault(context, "--min-tokens", profile, "80"), "--min-tokens", 40);
140023
+ const maxCandidatePairsRaw = context.getOpt("--max-candidate-pairs");
140024
+ const maxCandidatePairs = maxCandidatePairsRaw === void 0 ? void 0 : parseBoundedIntegerOption(maxCandidatePairsRaw, "--max-candidate-pairs", DEFAULT_MAX_CANDIDATE_PAIRS, 1, MAX_CANDIDATE_PAIRS_LIMIT);
139069
140025
  const options = {
139070
140026
  projectRoot: context.projectRootFs,
139071
140027
  files: context.files,
@@ -139074,6 +140030,7 @@ function parseDuplicateDetectionOptions(context, profile) {
139074
140030
  minTokens,
139075
140031
  maxTokens: parsePositiveIntegerOption(context.getOpt("--max-tokens"), "--max-tokens", 800),
139076
140032
  maxBucketSize: parsePositiveIntegerOption(context.getOpt("--max-bucket-size"), "--max-bucket-size", 200),
140033
+ ...maxCandidatePairs !== void 0 ? { maxCandidatePairs } : {},
139077
140034
  ...context.hasFlag("--include-same-file") ? { includeSameFile: true } : {},
139078
140035
  ...context.hasFlag("--include-small") ? { includeSmall: true } : {},
139079
140036
  ...context.hasFlag("--raw-pairs") ? { includeRawPairs: true } : {}
@@ -139293,6 +140250,10 @@ function formatPrettyDuplicates(result, sortMode, options) {
139293
140250
  `Candidate pairs compared: ${result.stats.comparedPairs}/${result.stats.candidatePairs}`,
139294
140251
  "Family annotations are heuristic and derived from the displayed duplicate pair."
139295
140252
  ];
140253
+ const budget = result.stats.candidatePairBudget;
140254
+ if (budget) {
140255
+ lines.push(`Candidate pair limit reached: kept ${budget.limit}, skipped ${budget.skipped}. Results are partial; raise --max-candidate-pairs or lower --max-bucket-size.`);
140256
+ }
139296
140257
  const omitted = formatOmittedCounts(result);
139297
140258
  if (result.filteredCounts?.cleanupProfileGroups) {
139298
140259
  lines.push(`Filtered by cleanup profile: ${result.filteredCounts.cleanupProfileGroups}`);
@@ -139397,6 +140358,10 @@ async function handleDuplicatesCommand(context) {
139397
140358
  const boundedLimit = Math.max(requestedLimit, Math.min(ACTIONABILITY_SORT_MAX_CANDIDATES, requestedLimit * ACTIONABILITY_SORT_OVERFETCH_MULTIPLIER));
139398
140359
  options.limit = boundedLimit;
139399
140360
  }
140361
+ if (context.progressHandler)
140362
+ options.onProgress = context.progressHandler;
140363
+ if (context.progressStartedAt !== void 0)
140364
+ options.progressStartedAt = context.progressStartedAt;
139400
140365
  const index = await context.loadCurrentIndex();
139401
140366
  const result = await findDuplicates(index, options);
139402
140367
  const sorted = sortedResult(result, sortMode, requestedLimit, profile);
@@ -140962,7 +141927,7 @@ var review_exports = {};
140962
141927
  __export(review_exports, {
140963
141928
  handleReviewCommand: () => handleReviewCommand
140964
141929
  });
140965
- import { performance as performance17 } from "node:perf_hooks";
141930
+ import { performance as performance18 } from "node:perf_hooks";
140966
141931
  import path102 from "node:path";
140967
141932
  function parseReviewDepth(value) {
140968
141933
  if (value === "minimal" || value === "standard" || value === "deep") {
@@ -141154,7 +142119,7 @@ async function collectReviewDuplicateSummary(input2) {
141154
142119
  });
141155
142120
  }
141156
142121
  async function handleReviewCommand(context) {
141157
- const commandStart = performance17.now();
142122
+ const commandStart = performance18.now();
141158
142123
  const changedSince = context.getOpt("--changed-since");
141159
142124
  const base = context.getOpt("--base") ?? (changedSince === void 0 ? "HEAD" : void 0);
141160
142125
  const head = context.getOpt("--head") ?? (changedSince === void 0 ? "WORKTREE" : void 0);
@@ -141246,7 +142211,7 @@ async function handleReviewCommand(context) {
141246
142211
  context.writeJSONLine(report2);
141247
142212
  }
141248
142213
  if (context.commandReport) {
141249
- context.commandReport.timings.commandMs = Math.round(performance17.now() - commandStart);
142214
+ context.commandReport.timings.commandMs = Math.round(performance18.now() - commandStart);
141250
142215
  context.commandReport.timings.totalMs = context.commandReport.timings.commandMs;
141251
142216
  await context.writeCommandReport(context.commandReport, context.reportFile);
141252
142217
  }
@@ -141561,7 +142526,7 @@ __export(inspect_exports, {
141561
142526
  handleHotspotsCommand: () => handleHotspotsCommand,
141562
142527
  handleInspectCommand: () => handleInspectCommand
141563
142528
  });
141564
- import { performance as performance18 } from "node:perf_hooks";
142529
+ import { performance as performance19 } from "node:perf_hooks";
141565
142530
  import fs46 from "node:fs";
141566
142531
  import path103 from "node:path";
141567
142532
  function defaultCacheIndexPath(projectRoot, cacheDir, cacheLocation) {
@@ -141821,18 +142786,18 @@ async function buildInspectReport(projectRoot, includeRoots, files, discovery, g
141821
142786
  };
141822
142787
  }
141823
142788
  async function handleInspectCommand(context) {
141824
- const commandStart = performance18.now();
142789
+ const commandStart = performance19.now();
141825
142790
  const cache = parseCacheModeOption(context.getOpt("--cache"));
141826
142791
  const limit = parsePositiveIntegerOption(context.getOpt("--limit"), "--limit", 20);
141827
- const resolveStart = performance18.now();
142792
+ const resolveStart = performance19.now();
141828
142793
  const files = await context.resolveFilesFromRoots();
141829
142794
  if (context.commandReport) {
141830
- context.commandReport.timings.resolveFilesMs = Math.round(performance18.now() - resolveStart);
142795
+ context.commandReport.timings.resolveFilesMs = Math.round(performance19.now() - resolveStart);
141831
142796
  }
141832
142797
  const report2 = await buildInspectReport(context.projectRootFs, context.includeRootsAbs, files, context.discoveryOptions, context.graphOptions, context.languageExtensions, cache, context.getOpt("--cache-dir"), context.cacheLocation, context.workerOpts, context.progressHandler, context.commandReport?.index, limit, context.hasFlag("--duplicates"), context.writeStderrLine);
141833
142798
  writeCliOutput(context, report2, formatInspectReport);
141834
142799
  if (context.commandReport && context.writeCommandReport) {
141835
- context.commandReport.timings.commandMs = Math.round(performance18.now() - commandStart);
142800
+ context.commandReport.timings.commandMs = Math.round(performance19.now() - commandStart);
141836
142801
  context.commandReport.timings.totalMs = context.commandReport.timings.commandMs;
141837
142802
  await context.writeCommandReport(context.commandReport, context.reportFile);
141838
142803
  }
@@ -141886,6 +142851,7 @@ var init_inspect = __esm({
141886
142851
  });
141887
142852
 
141888
142853
  // dist/cli/command-table.js
142854
+ import { performance as performance20 } from "node:perf_hooks";
141889
142855
  function agentIo(ctx) {
141890
142856
  return {
141891
142857
  positionals: ctx.parsed.positionals,
@@ -142358,6 +143324,7 @@ var init_command_table = __esm({
142358
143324
  duplicates: {
142359
143325
  stage: "project",
142360
143326
  run: async (ctx) => {
143327
+ const startedAt = performance20.now();
142361
143328
  const files = await ctx.resolveFiles();
142362
143329
  const { handleDuplicatesCommand: handleDuplicatesCommand2 } = await Promise.resolve().then(() => (init_duplicates2(), duplicates_exports));
142363
143330
  await handleDuplicatesCommand2({
@@ -142366,6 +143333,8 @@ var init_command_table = __esm({
142366
143333
  getOpt: ctx.getOpt,
142367
143334
  hasFlag: ctx.hasFlag,
142368
143335
  loadCurrentIndex: ctx.createDuplicateIndexLoader(),
143336
+ progressHandler: ctx.progressHandler,
143337
+ progressStartedAt: startedAt,
142369
143338
  writeJSONLine,
142370
143339
  writeStdoutLine,
142371
143340
  writeStderrLine,
@@ -143481,7 +144450,7 @@ function openViewerUrl(url2) {
143481
144450
  command = "xdg-open";
143482
144451
  args = [url2];
143483
144452
  }
143484
- const child = spawn5(command, args, { detached: true, stdio: "ignore" });
144453
+ const child = spawn5(command, args, { windowsHide: true, detached: true, stdio: "ignore" });
143485
144454
  child.once("error", () => {
143486
144455
  });
143487
144456
  child.unref();