@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
@@ -0,0 +1,873 @@
1
+ import { appendToArrayMap } from "../util/collections.js";
2
+ import { compareUnitRefs, isScorablePair } from "./scoring.js";
3
+ import { normalizeDetectionFile } from "./units.js";
4
+ const MIN_SIMILARITY_HINT_INDEX = 80;
5
+ /** Groups units by cheap fingerprints before expensive pair scoring. */
6
+ export function prepareDuplicateCandidateBuckets(units) {
7
+ const rawHashBuckets = new Map();
8
+ const normalizedHashBuckets = new Map();
9
+ const astShapeBuckets = new Map();
10
+ const signatureBuckets = new Map();
11
+ for (const unit of units) {
12
+ const languagePrefix = `${unit.languageId}:`;
13
+ appendToArrayMap(rawHashBuckets, `${languagePrefix}${unit.rawHash}`, unit);
14
+ appendToArrayMap(normalizedHashBuckets, `${languagePrefix}${unit.normalizedHash}`, unit);
15
+ if (unit.astShapeHash !== undefined) {
16
+ appendToArrayMap(astShapeBuckets, `${languagePrefix}${unit.astShapeHash}`, unit);
17
+ }
18
+ for (const signature of unit.signatures) {
19
+ appendToArrayMap(signatureBuckets, `${languagePrefix}${signature}`, unit);
20
+ }
21
+ }
22
+ return {
23
+ units,
24
+ rawHashBuckets,
25
+ normalizedHashBuckets,
26
+ astShapeBuckets,
27
+ signatureBuckets,
28
+ };
29
+ }
30
+ function filterPreparedDuplicateBucketMap(buckets, unitFilter) {
31
+ const filteredBuckets = new Map();
32
+ for (const [key, bucket] of buckets) {
33
+ const filteredBucket = bucket.filter(unitFilter);
34
+ if (filteredBucket.length)
35
+ filteredBuckets.set(key, filteredBucket);
36
+ }
37
+ return filteredBuckets;
38
+ }
39
+ export function filterPreparedDuplicateBuckets(preparedBuckets, unitFilter) {
40
+ return {
41
+ units: preparedBuckets.units.filter(unitFilter),
42
+ rawHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.rawHashBuckets, unitFilter),
43
+ normalizedHashBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.normalizedHashBuckets, unitFilter),
44
+ astShapeBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.astShapeBuckets, unitFilter),
45
+ signatureBuckets: filterPreparedDuplicateBucketMap(preparedBuckets.signatureBuckets, unitFilter),
46
+ };
47
+ }
48
+ function createUnitIndex(units) {
49
+ const positions = new Map();
50
+ const firstById = new Map();
51
+ const canon = new Int32Array(units.length);
52
+ let hasSharedIds = false;
53
+ for (let position = 0; position < units.length; position++) {
54
+ const unit = units[position];
55
+ positions.set(unit, position);
56
+ const first = firstById.get(unit.id);
57
+ if (first === undefined) {
58
+ firstById.set(unit.id, position);
59
+ canon[position] = position;
60
+ }
61
+ else {
62
+ canon[position] = first;
63
+ hasSharedIds = true;
64
+ }
65
+ }
66
+ return { units, positions, canon, hasSharedIds };
67
+ }
68
+ function positionOf(index, unit) {
69
+ const position = index.positions.get(unit);
70
+ if (position === undefined)
71
+ throw new Error(`Duplicate unit ${unit.id} is not part of the prepared candidates.`);
72
+ return position;
73
+ }
74
+ function canonOf(index, unit) {
75
+ return index.canon[positionOf(index, unit)];
76
+ }
77
+ /** The pair identity for two canonical positions. Positions stay below 2^26, so the number is exact. */
78
+ function pairNumber(index, left, right) {
79
+ const radix = index.units.length;
80
+ return left <= right ? left * radix + right : right * radix + left;
81
+ }
82
+ export function orderedPair(left, right) {
83
+ if (left.absoluteFile < right.absoluteFile)
84
+ return [left, right];
85
+ if (left.absoluteFile > right.absoluteFile)
86
+ return [right, left];
87
+ if (left.startLine <= right.startLine)
88
+ return [left, right];
89
+ return [right, left];
90
+ }
91
+ function blankEvidence(left, right) {
92
+ return { left, right, rawHash: false, normalizedHash: false, astShape: false, signature: false, signatureMatches: 0 };
93
+ }
94
+ /** Tallies a pair the scorers can never score, once, while the tally has room. */
95
+ function tallyUnscorable(state, key) {
96
+ if (state.unscorable.size < state.limit)
97
+ state.unscorable.add(key);
98
+ }
99
+ /**
100
+ * Where a pair that has the evidence to be a candidate goes. A pair already kept is returned to gain the evidence. A pair
101
+ * the scorers can never score is only tallied, so it never takes a slot from one they can. Any other pair takes a slot
102
+ * while one is free and counts as skipped when none is. Returns the evidence to complete, or nothing when the pair is
103
+ * not kept.
104
+ */
105
+ function takePair(state, key, first, second) {
106
+ const existing = state.pairs.get(key);
107
+ if (existing)
108
+ return existing;
109
+ if (!isScorablePair(first, second, state.crossFileOnly)) {
110
+ tallyUnscorable(state, key);
111
+ return undefined;
112
+ }
113
+ if (state.pairs.size >= state.limit) {
114
+ state.skipped++;
115
+ return undefined;
116
+ }
117
+ const [left, right] = orderedPair(first, second);
118
+ const evidence = blankEvidence(left, right);
119
+ state.pairs.set(key, evidence);
120
+ return evidence;
121
+ }
122
+ function addStrongEvidence(state, kind, first, second, key) {
123
+ const evidence = takePair(state, key, first, second);
124
+ if (evidence)
125
+ evidence[kind] = true;
126
+ }
127
+ /** Visits each pair that touches a target unit once per bucket, target first. */
128
+ function forEachFilteredBucketPair(index, bucket, filter, visit) {
129
+ const targetUnits = bucket.filter(filter.unit);
130
+ const seenPairs = new Set();
131
+ for (const targetUnit of targetUnits) {
132
+ const targetCanon = canonOf(index, targetUnit);
133
+ for (const otherUnit of bucket) {
134
+ if (targetUnit.id === otherUnit.id)
135
+ continue;
136
+ const [left, right] = orderedPair(targetUnit, otherUnit);
137
+ const key = pairNumber(index, targetCanon, canonOf(index, otherUnit));
138
+ if (seenPairs.has(key))
139
+ continue;
140
+ seenPairs.add(key);
141
+ if (!filter.pair(left, right))
142
+ continue;
143
+ visit(left, right, key);
144
+ }
145
+ }
146
+ }
147
+ function filteredPairCountExceeds(index, bucket, limit, filter) {
148
+ let count = 0;
149
+ const targetUnits = bucket.filter(filter.unit);
150
+ if (!targetUnits.length)
151
+ return false;
152
+ const seenPairs = new Set();
153
+ for (const targetUnit of targetUnits) {
154
+ const targetCanon = canonOf(index, targetUnit);
155
+ for (const otherUnit of bucket) {
156
+ if (targetUnit.id === otherUnit.id)
157
+ continue;
158
+ const [left, right] = orderedPair(targetUnit, otherUnit);
159
+ const key = pairNumber(index, targetCanon, canonOf(index, otherUnit));
160
+ if (seenPairs.has(key))
161
+ continue;
162
+ seenPairs.add(key);
163
+ if (!filter.pair(left, right))
164
+ continue;
165
+ count++;
166
+ if (count > limit)
167
+ return true;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ /** A bucket is paired when it is small enough, or when a targeted run touches few enough of its pairs. */
173
+ function classifyBucket(index, bucket, maxBucketSize, filter) {
174
+ if (bucket.length < 2)
175
+ return "skip";
176
+ if (filter && !bucket.some(filter.unit))
177
+ return "skip";
178
+ if (bucket.length > maxBucketSize) {
179
+ if (!filter || filteredPairCountExceeds(index, bucket, maxBucketSize, filter))
180
+ return "oversized";
181
+ }
182
+ return "pair";
183
+ }
184
+ /** Adds one shared-evidence bucket in the order its pairs were always visited. */
185
+ function addBucketPairs(state, bucket, kind, filter) {
186
+ const { index } = state;
187
+ if (filter) {
188
+ forEachFilteredBucketPair(index, bucket, filter, (left, right, key) => {
189
+ addStrongEvidence(state, kind, left, right, key);
190
+ });
191
+ return;
192
+ }
193
+ const canons = new Int32Array(bucket.length);
194
+ for (let i = 0; i < bucket.length; i++)
195
+ canons[i] = canonOf(index, bucket[i]);
196
+ for (let i = 0; i < bucket.length; i++) {
197
+ for (let j = i + 1; j < bucket.length; j++) {
198
+ addStrongEvidence(state, kind, bucket[i], bucket[j], pairNumber(index, canons[i], canons[j]));
199
+ }
200
+ }
201
+ }
202
+ async function addEvidenceBuckets(state, buckets, kind, options) {
203
+ let oversizedBuckets = 0;
204
+ for (const bucket of buckets.values()) {
205
+ const verdict = classifyBucket(state.index, bucket, options.maxBucketSize, options.filter);
206
+ if (verdict === "skip")
207
+ continue;
208
+ if (verdict === "oversized") {
209
+ oversizedBuckets++;
210
+ continue;
211
+ }
212
+ addBucketPairs(state, bucket, kind, options.filter);
213
+ const pending = options.yieldPoint?.();
214
+ if (pending)
215
+ await pending;
216
+ }
217
+ return oversizedBuckets;
218
+ }
219
+ /** Requires enough shared fingerprints to avoid incidental syntax matches. */
220
+ function hasEnoughFingerprints(sharedFingerprints, smallerConsideredCount) {
221
+ if (!sharedFingerprints || !smallerConsideredCount)
222
+ return false;
223
+ return sharedFingerprints >= Math.max(2, Math.ceil(smallerConsideredCount * 0.25));
224
+ }
225
+ /** Keeps the best few pairs seen: the worst kept pair sits at the root so a better one can replace it. */
226
+ class BestPairs {
227
+ capacity;
228
+ size = 0;
229
+ priority;
230
+ first;
231
+ second;
232
+ matches;
233
+ constructor(capacity) {
234
+ this.capacity = capacity;
235
+ const initial = Math.min(capacity, 1024);
236
+ this.priority = new Float64Array(initial);
237
+ this.first = new Int32Array(initial);
238
+ this.second = new Int32Array(initial);
239
+ this.matches = new Int32Array(initial);
240
+ }
241
+ /** Better means more of the smaller unit's fingerprints shared, then the earlier unit ids. */
242
+ isWorse(a, b) {
243
+ if (this.priority[a] !== this.priority[b])
244
+ return this.priority[a] < this.priority[b];
245
+ if (this.first[a] !== this.first[b])
246
+ return this.first[a] > this.first[b];
247
+ return this.second[a] > this.second[b];
248
+ }
249
+ swap(a, b) {
250
+ const priority = this.priority[a];
251
+ this.priority[a] = this.priority[b];
252
+ this.priority[b] = priority;
253
+ const first = this.first[a];
254
+ this.first[a] = this.first[b];
255
+ this.first[b] = first;
256
+ const second = this.second[a];
257
+ this.second[a] = this.second[b];
258
+ this.second[b] = second;
259
+ const matches = this.matches[a];
260
+ this.matches[a] = this.matches[b];
261
+ this.matches[b] = matches;
262
+ }
263
+ grow() {
264
+ const next = Math.min(this.capacity, this.priority.length * 2);
265
+ const priority = new Float64Array(next);
266
+ priority.set(this.priority);
267
+ this.priority = priority;
268
+ for (const name of ["first", "second", "matches"]) {
269
+ const grown = new Int32Array(next);
270
+ grown.set(this[name]);
271
+ this[name] = grown;
272
+ }
273
+ }
274
+ siftUp(start) {
275
+ let slot = start;
276
+ while (slot > 0) {
277
+ const parent = (slot - 1) >> 1;
278
+ if (!this.isWorse(slot, parent))
279
+ return;
280
+ this.swap(slot, parent);
281
+ slot = parent;
282
+ }
283
+ }
284
+ siftDown(start) {
285
+ let slot = start;
286
+ for (;;) {
287
+ const left = slot * 2 + 1;
288
+ const right = left + 1;
289
+ let worst = slot;
290
+ if (left < this.size && this.isWorse(left, worst))
291
+ worst = left;
292
+ if (right < this.size && this.isWorse(right, worst))
293
+ worst = right;
294
+ if (worst === slot)
295
+ return;
296
+ this.swap(slot, worst);
297
+ slot = worst;
298
+ }
299
+ }
300
+ /** Whether a pair ranks above the worst pair kept, by the same order `isWorse` uses. */
301
+ beatsWorst(priority, first, second) {
302
+ if (priority !== this.priority[0])
303
+ return priority > this.priority[0];
304
+ if (first !== this.first[0])
305
+ return first < this.first[0];
306
+ return second < this.second[0];
307
+ }
308
+ offer(priority, first, second, matches) {
309
+ if (!this.capacity)
310
+ return;
311
+ if (this.size === this.capacity) {
312
+ if (!this.beatsWorst(priority, first, second))
313
+ return;
314
+ this.priority[0] = priority;
315
+ this.first[0] = first;
316
+ this.second[0] = second;
317
+ this.matches[0] = matches;
318
+ this.siftDown(0);
319
+ return;
320
+ }
321
+ if (this.size === this.priority.length)
322
+ this.grow();
323
+ const slot = this.size++;
324
+ this.priority[slot] = priority;
325
+ this.first[slot] = first;
326
+ this.second[slot] = second;
327
+ this.matches[slot] = matches;
328
+ this.siftUp(slot);
329
+ }
330
+ entries() {
331
+ const entries = [];
332
+ for (let slot = 0; slot < this.size; slot++) {
333
+ entries.push({ first: this.first[slot], second: this.second[slot], matches: this.matches[slot] });
334
+ }
335
+ return entries;
336
+ }
337
+ }
338
+ /** Buckets whose pairs count toward fingerprint evidence, and how many were too large. */
339
+ function eligibleFingerprintBuckets(state, buckets, options) {
340
+ const eligible = [];
341
+ let oversizedBuckets = 0;
342
+ for (const bucket of buckets.values()) {
343
+ const verdict = classifyBucket(state.index, bucket, options.maxBucketSize, options.filter);
344
+ if (verdict === "skip")
345
+ continue;
346
+ if (verdict === "oversized") {
347
+ oversizedBuckets++;
348
+ continue;
349
+ }
350
+ eligible.push(bucket);
351
+ }
352
+ return { eligible, oversizedBuckets };
353
+ }
354
+ function layoutFingerprintBuckets(index, eligible) {
355
+ const unitCount = index.units.length;
356
+ const bucketCount = eligible.length;
357
+ const bucketStart = new Int32Array(bucketCount + 1);
358
+ const rankBase = new Float64Array(bucketCount);
359
+ let total = 0;
360
+ let rank = 0;
361
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
362
+ const size = eligible[bucket].length;
363
+ bucketStart[bucket] = total;
364
+ rankBase[bucket] = rank;
365
+ total += size;
366
+ rank += size * size;
367
+ }
368
+ bucketStart[bucketCount] = total;
369
+ const memberUnit = new Int32Array(total);
370
+ const memberCanon = new Int32Array(total);
371
+ const entryStart = new Int32Array(unitCount + 1);
372
+ const consideredCount = new Int32Array(unitCount);
373
+ const lastBucket = new Int32Array(unitCount).fill(-1);
374
+ for (let bucket = 0, member = 0; bucket < bucketCount; bucket++) {
375
+ for (const unit of eligible[bucket]) {
376
+ const position = positionOf(index, unit);
377
+ const unitCanon = index.canon[position];
378
+ memberUnit[member] = position;
379
+ memberCanon[member] = unitCanon;
380
+ member++;
381
+ entryStart[unitCanon + 1] = entryStart[unitCanon + 1] + 1;
382
+ if (lastBucket[unitCanon] !== bucket) {
383
+ lastBucket[unitCanon] = bucket;
384
+ consideredCount[unitCanon] = consideredCount[unitCanon] + 1;
385
+ }
386
+ }
387
+ }
388
+ for (let unit = 0; unit < unitCount; unit++)
389
+ entryStart[unit + 1] = entryStart[unit + 1] + entryStart[unit];
390
+ const entryMember = new Int32Array(total);
391
+ const entryBucket = new Int32Array(total);
392
+ const nextEntry = entryStart.slice(0, unitCount);
393
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
394
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
395
+ const unitCanon = memberCanon[member];
396
+ const slot = nextEntry[unitCanon];
397
+ nextEntry[unitCanon] = slot + 1;
398
+ entryMember[slot] = member;
399
+ entryBucket[slot] = bucket;
400
+ }
401
+ }
402
+ return { bucketStart, rankBase, memberUnit, memberCanon, entryStart, entryMember, entryBucket, consideredCount };
403
+ }
404
+ /**
405
+ * Decides which pairs that share fingerprint buckets become candidates, for the whole project and for a targeted run
406
+ * alike. A pair qualifies when it shares at least two eligible buckets and a quarter of the smaller unit's. One that
407
+ * already has stronger evidence only gains the fingerprint evidence, and one the scorers can never score is only
408
+ * tallied. The rest compete for the room the limit leaves, the pairs that share the largest fraction of their buckets
409
+ * first, and only a qualifying pair that does not fit counts as skipped.
410
+ */
411
+ class FingerprintSelection {
412
+ state;
413
+ layout;
414
+ pairFilter;
415
+ best;
416
+ qualifying = 0;
417
+ constructor(state, layout, pairFilter) {
418
+ this.state = state;
419
+ this.layout = layout;
420
+ this.pairFilter = pairFilter;
421
+ this.best = new BestPairs(Math.max(0, state.limit - state.pairs.size));
422
+ }
423
+ /** Takes two canonical units that share `count` eligible buckets; fewer than two cannot qualify. */
424
+ consider(first, second, count) {
425
+ if (count < 2)
426
+ return;
427
+ const { index, pairs } = this.state;
428
+ const smaller = Math.min(this.layout.consideredCount[first], this.layout.consideredCount[second]);
429
+ if (!hasEnoughFingerprints(count, smaller))
430
+ return;
431
+ const firstUnit = index.units[first];
432
+ const secondUnit = index.units[second];
433
+ if (this.pairFilter) {
434
+ const [left, right] = orderedPair(firstUnit, secondUnit);
435
+ if (!this.pairFilter(left, right))
436
+ return;
437
+ }
438
+ const key = pairNumber(index, first, second);
439
+ const existing = pairs.get(key);
440
+ if (existing) {
441
+ existing.signatureMatches = count;
442
+ existing.signature = true;
443
+ return;
444
+ }
445
+ if (!isScorablePair(firstUnit, secondUnit, this.state.crossFileOnly)) {
446
+ tallyUnscorable(this.state, key);
447
+ return;
448
+ }
449
+ this.qualifying++;
450
+ this.best.offer(count / smaller, Math.min(first, second), Math.max(first, second), count);
451
+ }
452
+ /** Adds the kept pairs in the order a walk of the buckets would first have met them, and counts the rest skipped. */
453
+ admit(visitOf) {
454
+ const { index, pairs } = this.state;
455
+ const admissions = this.best.entries().map(({ first, second, matches }) => ({
456
+ ...visitOf(first, second),
457
+ key: pairNumber(index, first, second),
458
+ count: matches,
459
+ }));
460
+ admissions.sort((left, right) => left.rank - right.rank);
461
+ for (const admission of admissions) {
462
+ pairs.set(admission.key, {
463
+ left: admission.left,
464
+ right: admission.right,
465
+ rawHash: false,
466
+ normalizedHash: false,
467
+ astShape: false,
468
+ signature: true,
469
+ signatureMatches: admission.count,
470
+ });
471
+ }
472
+ this.state.skipped += this.qualifying - admissions.length;
473
+ }
474
+ }
475
+ /**
476
+ * Where a pair-by-pair walk of every bucket first meets two canonical units, and the units of its second visit. The walk
477
+ * visits each pair of a bucket in unit order, and units that share an id can meet more than once in one bucket.
478
+ */
479
+ function wholeProjectVisit(layout, units, first, second) {
480
+ const { bucketStart, rankBase, memberUnit, entryStart, entryMember, entryBucket } = layout;
481
+ const visits = [];
482
+ let a = entryStart[first];
483
+ const aEnd = entryStart[first + 1];
484
+ let b = entryStart[second];
485
+ const bEnd = entryStart[second + 1];
486
+ while (a < aEnd && b < bEnd && visits.length < 2) {
487
+ const bucketA = entryBucket[a];
488
+ const bucketB = entryBucket[b];
489
+ if (bucketA < bucketB) {
490
+ a++;
491
+ continue;
492
+ }
493
+ if (bucketB < bucketA) {
494
+ b++;
495
+ continue;
496
+ }
497
+ let aNext = a;
498
+ while (aNext < aEnd && entryBucket[aNext] === bucketA)
499
+ aNext++;
500
+ let bNext = b;
501
+ while (bNext < bEnd && entryBucket[bNext] === bucketA)
502
+ bNext++;
503
+ const start = bucketStart[bucketA];
504
+ const size = bucketStart[bucketA + 1] - start;
505
+ for (let x = a; x < aNext; x++) {
506
+ for (let y = b; y < bNext; y++) {
507
+ if (first === second && y <= x)
508
+ continue;
509
+ const low = Math.min(entryMember[x], entryMember[y]);
510
+ const high = Math.max(entryMember[x], entryMember[y]);
511
+ visits.push({ rank: rankBase[bucketA] + (low - start) * size + (high - start), low, high });
512
+ }
513
+ }
514
+ a = aNext;
515
+ b = bNext;
516
+ }
517
+ visits.sort((left, right) => left.rank - right.rank);
518
+ const firstVisit = visits[0];
519
+ const secondVisit = visits[1];
520
+ const [left, right] = orderedPair(units[memberUnit[secondVisit.low]], units[memberUnit[secondVisit.high]]);
521
+ return { rank: firstVisit.rank, left, right };
522
+ }
523
+ /**
524
+ * Fingerprint evidence for the whole project.
525
+ *
526
+ * Every unit counts, per other unit, the eligible buckets they share, using one counter per unit instead of a table
527
+ * of all pairs that share a fingerprint. The kept pairs are then admitted in the order a pair-by-pair walk of the
528
+ * buckets would have first met them.
529
+ */
530
+ async function addProjectFingerprintPairs(state, layout, options) {
531
+ const { index } = state;
532
+ const unitCount = index.units.length;
533
+ const { bucketStart, memberUnit, memberCanon, entryStart, entryMember, entryBucket } = layout;
534
+ const selection = new FingerprintSelection(state, layout, undefined);
535
+ const sharedBuckets = new Int32Array(unitCount);
536
+ const touched = new Int32Array(unitCount);
537
+ const sharesIds = index.hasSharedIds;
538
+ const reportEvery = Math.max(1, Math.floor(unitCount / 100));
539
+ for (let unit = 0; unit < unitCount; unit++) {
540
+ const firstEntry = entryStart[unit];
541
+ const lastEntry = entryStart[unit + 1];
542
+ let touchedCount = 0;
543
+ // A visit between two bucket members belongs to the lower canonical unit. Units that share an id
544
+ // can sit on either side of a visit, so a later copy also looks at the members before it.
545
+ const lowestPartner = sharesIds ? unit : 0;
546
+ for (let slot = firstEntry; slot < lastEntry; slot++) {
547
+ const member = entryMember[slot];
548
+ const bucket = entryBucket[slot];
549
+ const bucketEnd = bucketStart[bucket + 1];
550
+ for (let other = member + 1; other < bucketEnd; other++) {
551
+ const partner = memberCanon[other];
552
+ if (partner < lowestPartner)
553
+ continue;
554
+ const shared = sharedBuckets[partner];
555
+ if (!shared)
556
+ touched[touchedCount++] = partner;
557
+ sharedBuckets[partner] = shared + 1;
558
+ }
559
+ if (sharesIds && memberUnit[member] !== unit) {
560
+ for (let other = bucketStart[bucket]; other < member; other++) {
561
+ const partner = memberCanon[other];
562
+ if (partner <= unit)
563
+ continue;
564
+ const shared = sharedBuckets[partner];
565
+ if (!shared)
566
+ touched[touchedCount++] = partner;
567
+ sharedBuckets[partner] = shared + 1;
568
+ }
569
+ }
570
+ }
571
+ for (let at = 0; at < touchedCount; at++) {
572
+ const partner = touched[at];
573
+ const count = sharedBuckets[partner];
574
+ sharedBuckets[partner] = 0;
575
+ selection.consider(unit, partner, count);
576
+ }
577
+ if (unit % reportEvery === 0)
578
+ options.onFingerprintProgress?.(unit + 1, unitCount);
579
+ const pending = options.yieldPoint?.();
580
+ if (pending)
581
+ await pending;
582
+ }
583
+ options.onFingerprintProgress?.(unitCount, unitCount);
584
+ selection.admit((first, second) => wholeProjectVisit(layout, index.units, first, second));
585
+ }
586
+ /**
587
+ * Where a walk that starts from the targets first meets two canonical units, and the units of its second visit. That
588
+ * walk of a bucket takes each target in turn and pairs it with every other member, so it meets a pair from the target
589
+ * that comes first in the bucket, or from the pair's only target, and pairs it with the other unit's first member.
590
+ */
591
+ function targetedVisit(layout, units, targets, first, second) {
592
+ const { bucketStart, rankBase, memberUnit, entryStart, entryMember, entryBucket } = layout;
593
+ let a = entryStart[first];
594
+ const aEnd = entryStart[first + 1];
595
+ let b = entryStart[second];
596
+ const bEnd = entryStart[second + 1];
597
+ const firstIsTarget = targets.isTarget[first] === 1;
598
+ const secondIsTarget = targets.isTarget[second] === 1;
599
+ let firstRank = -1;
600
+ while (a < aEnd && b < bEnd) {
601
+ const bucket = entryBucket[a];
602
+ if (bucket < entryBucket[b]) {
603
+ a++;
604
+ continue;
605
+ }
606
+ if (entryBucket[b] < bucket) {
607
+ b++;
608
+ continue;
609
+ }
610
+ // Both units are in this bucket, and `a` and `b` are their first members in it. The walk reaches the pair from its
611
+ // only target, or from whichever of the two targets comes first.
612
+ const slotA = entryMember[a];
613
+ const slotB = entryMember[b];
614
+ const walkedFromA = firstIsTarget && (!secondIsTarget || slotA < slotB);
615
+ const targetSlot = walkedFromA ? slotA : slotB;
616
+ const otherSlot = walkedFromA ? slotB : slotA;
617
+ if (firstRank < 0) {
618
+ const start = bucketStart[bucket];
619
+ const size = bucketStart[bucket + 1] - start;
620
+ firstRank = rankBase[bucket] + targets.before[targetSlot] * size + (otherSlot - start);
621
+ }
622
+ else {
623
+ const [left, right] = orderedPair(units[memberUnit[targetSlot]], units[memberUnit[otherSlot]]);
624
+ return { rank: firstRank, left, right };
625
+ }
626
+ while (a < aEnd && entryBucket[a] === bucket)
627
+ a++;
628
+ while (b < bEnd && entryBucket[b] === bucket)
629
+ b++;
630
+ }
631
+ throw new Error("A candidate pair from shared fingerprints shares fewer than two buckets.");
632
+ }
633
+ /**
634
+ * Fingerprint evidence for a targeted run.
635
+ *
636
+ * Every target counts, per other unit, the eligible buckets they share, with one counter per unit as in the
637
+ * whole-project pass, and a pair of two targets is counted from the lower one. Pairs qualify, compete for the limit,
638
+ * and are admitted exactly as they are there, in the order a walk that starts from the targets would first have met
639
+ * them.
640
+ */
641
+ function addTargetedFingerprintPairs(state, layout, filter) {
642
+ const { index } = state;
643
+ const unitCount = index.units.length;
644
+ const { bucketStart, memberCanon, entryStart, entryBucket } = layout;
645
+ const bucketCount = bucketStart.length - 1;
646
+ const isTarget = new Uint8Array(unitCount);
647
+ for (let unit = 0; unit < unitCount; unit++) {
648
+ if (entryStart[unit + 1] > entryStart[unit] && filter.unit(index.units[unit]))
649
+ isTarget[unit] = 1;
650
+ }
651
+ // How many targets come before each member of its bucket: where the walk of the bucket reaches it.
652
+ const before = new Int32Array(memberCanon.length);
653
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
654
+ let targetsSoFar = 0;
655
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
656
+ before[member] = targetsSoFar;
657
+ targetsSoFar += isTarget[memberCanon[member]];
658
+ }
659
+ }
660
+ const selection = new FingerprintSelection(state, layout, filter.pair);
661
+ const sharedBuckets = new Int32Array(unitCount);
662
+ const touched = new Int32Array(unitCount);
663
+ // Units that share an id can be in one bucket twice, and a pair counts once per bucket.
664
+ const countedIn = index.hasSharedIds ? new Int32Array(unitCount) : undefined;
665
+ let walk = 0;
666
+ for (let target = 0; target < unitCount; target++) {
667
+ if (!isTarget[target])
668
+ continue;
669
+ let touchedCount = 0;
670
+ let previousBucket = -1;
671
+ for (let slot = entryStart[target]; slot < entryStart[target + 1]; slot++) {
672
+ const bucket = entryBucket[slot];
673
+ if (bucket === previousBucket)
674
+ continue;
675
+ previousBucket = bucket;
676
+ walk++;
677
+ for (let member = bucketStart[bucket]; member < bucketStart[bucket + 1]; member++) {
678
+ const partner = memberCanon[member];
679
+ // A unit is not paired with its own id, and a pair of two targets is counted from the lower one.
680
+ if (partner === target || (partner < target && isTarget[partner]))
681
+ continue;
682
+ if (countedIn) {
683
+ if (countedIn[partner] === walk)
684
+ continue;
685
+ countedIn[partner] = walk;
686
+ }
687
+ const shared = sharedBuckets[partner];
688
+ if (!shared)
689
+ touched[touchedCount++] = partner;
690
+ sharedBuckets[partner] = shared + 1;
691
+ }
692
+ }
693
+ for (let at = 0; at < touchedCount; at++) {
694
+ const partner = touched[at];
695
+ const count = sharedBuckets[partner];
696
+ sharedBuckets[partner] = 0;
697
+ selection.consider(target, partner, count);
698
+ }
699
+ }
700
+ selection.admit((first, second) => targetedVisit(layout, index.units, { isTarget, before }, first, second));
701
+ }
702
+ function normalizeSimilarityIndex(value) {
703
+ if (!Number.isFinite(value))
704
+ return undefined;
705
+ const bounded = Math.max(0, Math.min(100, Math.round(value)));
706
+ if (bounded < MIN_SIMILARITY_HINT_INDEX)
707
+ return undefined;
708
+ return bounded;
709
+ }
710
+ function normalizeSimilarityHintFile(file, projectRoot) {
711
+ try {
712
+ return normalizeDetectionFile(file, projectRoot);
713
+ }
714
+ catch {
715
+ return undefined;
716
+ }
717
+ }
718
+ function addSimilarityHintPair(state, left, right, similarityIndex) {
719
+ const key = pairNumber(state.index, canonOf(state.index, left), canonOf(state.index, right));
720
+ const evidence = takePair(state, key, left, right);
721
+ if (evidence)
722
+ evidence.gitSimilarity = Math.max(evidence.gitSimilarity ?? 0, similarityIndex);
723
+ }
724
+ function similarityHintPairCountExceeds(leftUnits, rightUnits, limit, pairFilter) {
725
+ let count = 0;
726
+ for (const leftUnit of leftUnits) {
727
+ for (const rightUnit of rightUnits) {
728
+ if (leftUnit.languageId !== rightUnit.languageId)
729
+ continue;
730
+ const [left, right] = orderedPair(leftUnit, rightUnit);
731
+ if (pairFilter && !pairFilter(left, right))
732
+ continue;
733
+ count++;
734
+ if (count > limit)
735
+ return true;
736
+ }
737
+ }
738
+ return false;
739
+ }
740
+ function similarityAlignmentKey(unit) {
741
+ return [unit.languageId, unit.kind, unit.symbolKind ?? ""].join(":");
742
+ }
743
+ function addAlignedSimilarityHintPairs(state, leftUnits, rightUnits, similarityIndex, maxPairs, pairFilter) {
744
+ const rightByKey = new Map();
745
+ for (const unit of rightUnits) {
746
+ const key = similarityAlignmentKey(unit);
747
+ const bucket = rightByKey.get(key);
748
+ if (bucket)
749
+ bucket.push(unit);
750
+ else
751
+ rightByKey.set(key, [unit]);
752
+ }
753
+ for (const bucket of rightByKey.values()) {
754
+ bucket.sort(compareUnitRefs);
755
+ }
756
+ const leftByKey = new Map();
757
+ for (const unit of leftUnits) {
758
+ const key = similarityAlignmentKey(unit);
759
+ const bucket = leftByKey.get(key);
760
+ if (bucket)
761
+ bucket.push(unit);
762
+ else
763
+ leftByKey.set(key, [unit]);
764
+ }
765
+ for (const bucket of leftByKey.values()) {
766
+ bucket.sort(compareUnitRefs);
767
+ }
768
+ const seenPairs = new Set();
769
+ let addedPairs = 0;
770
+ for (const [key, sortedLeftUnits] of Array.from(leftByKey.entries()).sort(([left], [right]) => left.localeCompare(right))) {
771
+ const sortedRightUnits = rightByKey.get(key);
772
+ if (!sortedRightUnits?.length)
773
+ continue;
774
+ const limit = Math.min(sortedLeftUnits.length, sortedRightUnits.length);
775
+ for (let index = 0; index < limit; index++) {
776
+ if (addedPairs >= maxPairs)
777
+ return;
778
+ const [left, right] = orderedPair(sortedLeftUnits[index], sortedRightUnits[index]);
779
+ const pairId = pairNumber(state.index, canonOf(state.index, left), canonOf(state.index, right));
780
+ if (seenPairs.has(pairId))
781
+ continue;
782
+ seenPairs.add(pairId);
783
+ if (pairFilter && !pairFilter(left, right))
784
+ continue;
785
+ addSimilarityHintPair(state, left, right, similarityIndex);
786
+ addedPairs++;
787
+ }
788
+ }
789
+ }
790
+ function addSimilarityHintPairs(state, similarityHints, projectRoot, maxBucketSize, pairFilter) {
791
+ if (!similarityHints?.length)
792
+ return 0;
793
+ const unitsByFile = new Map();
794
+ for (const unit of state.index.units) {
795
+ const bucket = unitsByFile.get(unit.absoluteFile);
796
+ if (bucket) {
797
+ bucket.push(unit);
798
+ }
799
+ else {
800
+ unitsByFile.set(unit.absoluteFile, [unit]);
801
+ }
802
+ }
803
+ let oversizedHints = 0;
804
+ for (const hint of similarityHints) {
805
+ const similarityIndex = normalizeSimilarityIndex(hint.similarityIndex);
806
+ if (similarityIndex === undefined)
807
+ continue;
808
+ const leftFile = normalizeSimilarityHintFile(hint.leftFile, projectRoot);
809
+ const rightFile = normalizeSimilarityHintFile(hint.rightFile, projectRoot);
810
+ if (!leftFile || !rightFile || leftFile === rightFile)
811
+ continue;
812
+ const leftUnits = unitsByFile.get(leftFile);
813
+ const rightUnits = unitsByFile.get(rightFile);
814
+ if (!leftUnits?.length || !rightUnits?.length)
815
+ continue;
816
+ if (similarityHintPairCountExceeds(leftUnits, rightUnits, maxBucketSize, pairFilter)) {
817
+ oversizedHints++;
818
+ addAlignedSimilarityHintPairs(state, leftUnits, rightUnits, similarityIndex, maxBucketSize, pairFilter);
819
+ continue;
820
+ }
821
+ for (const leftUnit of leftUnits) {
822
+ for (const rightUnit of rightUnits) {
823
+ if (leftUnit.languageId !== rightUnit.languageId)
824
+ continue;
825
+ const [left, right] = orderedPair(leftUnit, rightUnit);
826
+ if (pairFilter && !pairFilter(left, right))
827
+ continue;
828
+ addSimilarityHintPair(state, left, right, similarityIndex);
829
+ }
830
+ }
831
+ }
832
+ return oversizedHints;
833
+ }
834
+ /**
835
+ * Finds the unit pairs worth scoring from the prepared fingerprint buckets.
836
+ *
837
+ * Evidence is admitted from strongest to weakest: identical text, identical normalized tokens, identical AST shape,
838
+ * Git similarity hints, then shared fingerprints. A pair the scorers can never score takes no part in the limit and is
839
+ * only tallied. At most `maxCandidatePairs` of the others are kept; the rest are counted in `skippedPairs`, never
840
+ * dropped silently.
841
+ */
842
+ export async function buildCandidatePairs(preparedBuckets, options) {
843
+ const state = {
844
+ index: createUnitIndex(preparedBuckets.units),
845
+ crossFileOnly: options.crossFileOnly,
846
+ pairs: new Map(),
847
+ unscorable: new Set(),
848
+ limit: options.maxCandidatePairs,
849
+ skipped: 0,
850
+ };
851
+ let oversizedBuckets = 0;
852
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.rawHashBuckets, "rawHash", options);
853
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.normalizedHashBuckets, "normalizedHash", options);
854
+ oversizedBuckets += await addEvidenceBuckets(state, preparedBuckets.astShapeBuckets, "astShape", options);
855
+ oversizedBuckets += addSimilarityHintPairs(state, options.similarityHints, options.projectRoot, options.maxBucketSize, options.filter?.pair);
856
+ const fingerprints = eligibleFingerprintBuckets(state, preparedBuckets.signatureBuckets, options);
857
+ oversizedBuckets += fingerprints.oversizedBuckets;
858
+ const layout = layoutFingerprintBuckets(state.index, fingerprints.eligible);
859
+ if (options.filter) {
860
+ addTargetedFingerprintPairs(state, layout, options.filter);
861
+ }
862
+ else {
863
+ await addProjectFingerprintPairs(state, layout, options);
864
+ }
865
+ return {
866
+ pairs: state.pairs,
867
+ oversizedBuckets,
868
+ skippedPairs: state.skipped,
869
+ unscorablePairs: state.unscorable.size,
870
+ overlappingPairs: state.crossFileOnly ? 0 : state.unscorable.size,
871
+ };
872
+ }
873
+ //# sourceMappingURL=candidates.js.map