code-gauge 10.1.0 → 10.2.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 (42) hide show
  1. package/README.md +37 -15
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +1 -1
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.js +1 -1
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/crossFileDuplication.cjs +1 -1
  8. package/dist/crossFileDuplication.cjs.map +1 -1
  9. package/dist/crossFileDuplication.d.ts +3 -2
  10. package/dist/crossFileDuplication.js +1 -1
  11. package/dist/crossFileDuplication.js.map +1 -1
  12. package/dist/crossFileNearMiss.cjs +1 -1
  13. package/dist/crossFileNearMiss.cjs.map +1 -1
  14. package/dist/crossFileNearMiss.d.ts +9 -4
  15. package/dist/crossFileNearMiss.js +1 -1
  16. package/dist/crossFileNearMiss.js.map +1 -1
  17. package/dist/duplication.cjs +1 -1
  18. package/dist/duplication.cjs.map +1 -1
  19. package/dist/duplication.d.ts +28 -7
  20. package/dist/duplication.js +1 -1
  21. package/dist/duplication.js.map +1 -1
  22. package/dist/thresholdCheck.cjs +1 -1
  23. package/dist/thresholdCheck.cjs.map +1 -1
  24. package/dist/thresholdCheck.d.ts +2 -0
  25. package/dist/thresholdCheck.js +1 -1
  26. package/dist/thresholdCheck.js.map +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +2 -3
  29. package/dist/thresholds.js.map +1 -1
  30. package/dist/types.d.ts +20 -8
  31. package/native/Cargo.lock +16 -16
  32. package/native/Cargo.toml +8 -8
  33. package/native/code-gauge.wasm +0 -0
  34. package/native/src/complexity.rs +10 -1
  35. package/native/src/container.rs +158 -7
  36. package/native/src/dep_degree.rs +13 -14
  37. package/native/src/duplication.rs +302 -137
  38. package/native/src/functions.rs +112 -24
  39. package/native/src/languages.rs +5 -0
  40. package/native/src/measure.rs +9 -4
  41. package/native/src/near_miss.rs +303 -52
  42. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"crossFileNearMiss.js","names":[],"sources":["../src/crossFileNearMiss.ts"],"sourcesContent":["import {\n createLcsLengthCounter,\n lcsLength,\n type CountedOccurrence,\n type Token,\n type TokenRange,\n} from './duplication.js';\n\n/**\n * Cross-file near-miss (Type-3) clone detection, following the within-file detector's model\n * (native/src/near_miss.rs): candidate block pairs are filtered through an n-gram inverted index\n * (NIL, Nakagawa et al. 2021), then verified by token-level longest common subsequence against the\n * larger block (NiCad's per-fragment similarity), backed by an information-weighted content gate,\n * with a statement-order-insensitive fallback and a local match over the anchored cores of two\n * blocks. Only pairs of blocks in different files are compared: a same-file pair is the\n * within-file detector's concern.\n */\n\nexport interface NearMissSourceFile {\n tokens?: Token[];\n containerStatements?: TokenRange[][];\n nearMissBlocks?: TokenRange[];\n}\n\n/** One copy (a block or its matched cores) in a near-miss group; anchors carry `spanCountedElsewhere` (see collectCrossFileNearMissGroups). */\nexport interface NearMissOccurrence extends CountedOccurrence {\n fileIndex: number;\n}\n\n/** N-gram size of the candidate index and local-match anchors (NIL's default). */\nconst ngramSize = 5;\n/** Filtration threshold: shared distinct n-grams over the smaller block's (NIL's default). */\nconst filtrationPercent = 10;\n/**\n * Pairs whose longer block exceeds this multiple of the shorter are compared only when whole-block\n * similarity still allows their ratio (below a minSimilarityPercent of 34). The candidate scan\n * stops at this floor while walking length-ordered postings, so pairs of very different lengths are\n * neither counted nor verified; `maxNgramBlockFrequency` is what bounds the scan's total cost.\n */\nconst maxLengthRatio = 3;\n/**\n * A structural match must also share content: more than this percent of the larger side's\n * information-weighted content-bearing tokens (names and literal values), so blocks of the same\n * shape that call different APIs on different data are not clones.\n */\nconst minContentSimilarityPercent = 50;\n/**\n * Caps content weights so only names and values spread over more than a quarter of the blocks are\n * discounted: a family of copies shares its content across several blocks, and uncapped rarity\n * weighting would let each copy's few unique edits outweigh everything the family shares.\n */\nconst maxContentWeight = 3;\n/**\n * Anchors must cover at least this percent of the shorter core: sparser chains are coincidental\n * runs of common n-grams in merely similar-looking code, and the cheap bound spares their content\n * and LCS checks. Not the similarity threshold itself, since n-grams repeated within a block\n * (repetitive statements) never anchor.\n */\nconst minAnchorCoveragePercent = 50;\n/** Anchors farther apart than this (in either block) split a local match into separate chains. */\nconst maxAnchorGapTokens = 30;\n/** Statement-order-insensitive comparison needs this many top-level statements per block. */\nconst minReorderStatementCount = 2;\n/**\n * N-grams occurring in more blocks than this are stop n-grams (syntax boilerplate such as a chain\n * of closing braces), left out of the index and of each block's n-gram count. Counting shared\n * n-grams costs the square of an n-gram's block frequency, so without the cap a project's most\n * common n-grams make filtration quadratic in the block count, while they discriminate nothing.\n */\nconst maxNgramBlockFrequency = 1000;\n\ninterface NormalizedBlock {\n fileIndex: number;\n range: TokenRange;\n /** Interned non-identifier symbols (>= 0) and identifiers as -(file-level id + 1). */\n symbols: Int32Array;\n isContent: Uint8Array;\n /** Identifiers anonymized by first occurrence within the block. */\n sequence: Int32Array;\n /** The sequence sorted, for the token-bag upper bound on the LCS. */\n sortedSequence: Int32Array;\n /** Distinct non-stop n-gram hashes. */\n ngrams: Int32Array;\n /**\n * The n-grams occurring exactly once in the block, sorted, with their offsets in the parallel\n * array: two blocks' local-match anchors intersect by merging.\n */\n uniqueNgrams: Int32Array;\n uniqueNgramOffsets: Int32Array;\n contentCounts: Map<number, number>;\n /**\n * The sequence with its top-level statements in canonical order, when it has enough of them for\n * statement-order-insensitive comparison.\n */\n canonicalSequence: Int32Array | undefined;\n}\n\n/** A verified core in each block of a pair, as file token ranges. */\ntype CorePair = [[number, number], [number, number]];\n\n/** How a verified pair matched: whole blocks, or every anchored core pair (one per gap-split chain segment). */\ntype PairMatch = { kind: 'whole' } | { kind: 'local'; cores: CorePair[] };\n\n/**\n * Clusters verified cross-file near-miss pairs into groups. A node (a whole block or a matched\n * core) overlapping an occurrence of `reportedSpansByFile` (the exact cross-file groups) is an\n * anchor: it links near-miss copies to the content an exact group already reports, and appears in\n * the near-miss group marked `spanCountedElsewhere` so block counting does not count its span\n * twice. Pairs of two anchors are skipped (blocks wholly covered by reported spans are not even\n * compared), and a group needs at least one non-anchor node. A block that matched only locally is\n * reported as its matched cores (overlapping cores merged), each clustered with its own partners,\n * so code no verified pair matched never counts as duplicated.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minTokens: number,\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const matcher = createMatcher(blocks, minTokens, minSimilarityPercent);\n const overlapsReportedSpan = reportedSpansByFile.map(createOverlapTest);\n const coveredByReportedSpans = reportedSpansByFile.map(createCoverageTest);\n const fullyReported = blocks.map(({ fileIndex, range }) => coveredByReportedSpans[fileIndex]?.(range) ?? false);\n const touchesReported = blocks.map(({ fileIndex, range }) => overlapsReportedSpan[fileIndex]?.(range) ?? false);\n const edges: [number, [number, number] | undefined, number, [number, number] | undefined][] = [];\n forEachCandidatePair(blocks, fullyReported, minSimilarityPercent, (left, right) => {\n const leftBlock = blocks[left];\n const rightBlock = blocks[right];\n const match = leftBlock && rightBlock && matcher(leftBlock, rightBlock, right);\n if (match) {\n if (match.kind === 'whole') {\n // A whole match between two blocks that both overlap reported spans could never join a\n // group, and recording it would collapse the blocks' core nodes.\n if (!(touchesReported[left] && touchesReported[right])) {\n edges.push([left, undefined, right, undefined]);\n }\n } else {\n for (const [leftCore, rightCore] of match.cores) {\n edges.push([left, leftCore, right, rightCore]);\n }\n }\n }\n });\n\n // Clustering runs over (block, core) nodes: a block with a recorded whole match is one node,\n // and otherwise each union of its overlapping local cores is its own node, so disjoint cores\n // matched with different partners fall into separate groups.\n const matchedWhole = blocks.map(() => false);\n const localCores = blocks.map((): [number, number][] => []);\n for (const [left, leftCore, right, rightCore] of edges) {\n for (const [index, core] of [\n [left, leftCore],\n [right, rightCore],\n ] as const) {\n if (core) {\n localCores[index]?.push(core);\n } else {\n matchedWhole[index] = true;\n }\n }\n }\n const nodes: { blockIndex: number; core: [number, number] | undefined }[] = [];\n const firstNodeByBlock: number[] = [];\n for (const blockIndex of blocks.keys()) {\n firstNodeByBlock.push(nodes.length);\n const cores = matchedWhole[blockIndex] ? [] : mergeOverlappingCores(localCores[blockIndex] ?? []);\n if (cores.length === 0) {\n nodes.push({ blockIndex, core: undefined });\n }\n for (const core of cores) {\n nodes.push({ blockIndex, core });\n }\n }\n const nodeOf = (blockIndex: number, core: [number, number] | undefined): number => {\n const first = firstNodeByBlock[blockIndex] ?? 0;\n if (!core || matchedWhole[blockIndex]) {\n return first;\n }\n for (let node = first; nodes[node]?.blockIndex === blockIndex; node += 1) {\n const span = nodes[node]?.core;\n if (span && span[0] <= core[0] && core[1] <= span[1]) {\n return node;\n }\n }\n throw new Error(\"every local core lies in one of its block's merged cores\");\n };\n\n const parent = nodes.map((_, index) => index);\n const find = (index: number): number => {\n let root = index;\n while (parent[root] !== root) {\n root = parent[root] ?? root;\n }\n for (let current = index; parent[current] !== root;) {\n const next = parent[current] ?? root;\n parent[current] = root;\n current = next;\n }\n return root;\n };\n // Anchoring is judged per node: a core is an anchor only when a reported span overlaps the core\n // itself, not merely elsewhere in its block.\n const anchored = nodes.map(({ blockIndex, core }) => {\n const block = blocks[blockIndex];\n const [startTokenIndex, endTokenIndex] = core ?? [\n block?.range.startTokenIndex ?? 0,\n block?.range.endTokenIndex ?? 0,\n ];\n return overlapsReportedSpan[block?.fileIndex ?? 0]?.({ startTokenIndex, endTokenIndex }) ?? false;\n });\n for (const [left, leftCore, right, rightCore] of edges) {\n const leftNode = nodeOf(left, leftCore);\n const rightNode = nodeOf(right, rightCore);\n if (anchored[leftNode] && anchored[rightNode]) {\n continue;\n }\n const leftRoot = find(leftNode);\n const rightRoot = find(rightNode);\n parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);\n }\n\n const membersByRoot = new Map<number, number[]>();\n for (const node of nodes.keys()) {\n const root = find(node);\n const members = membersByRoot.get(root) ?? [];\n members.push(node);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((node) => anchored[node])) {\n continue;\n }\n // A group's nodes from one block become ONE occurrence whose segments are its cores, so the\n // fragment-weighted count charges the block as one copy (as for gapped clones), not once per core.\n const coresByBlock = new Map<number, { cores: ([number, number] | undefined)[]; anchor: boolean }>();\n for (const node of members) {\n const { blockIndex = 0, core } = nodes[node] ?? {};\n const entry = coresByBlock.get(blockIndex) ?? { cores: [], anchor: false };\n entry.cores.push(core);\n entry.anchor ||= anchored[node] ?? false;\n coresByBlock.set(blockIndex, entry);\n }\n groups.push(\n [...coresByBlock].flatMap(([blockIndex, { cores, anchor }]) => {\n const block = blocks[blockIndex];\n return block ? [toOccurrence(block, files, cores, anchor)] : [];\n })\n );\n }\n return groups;\n}\n\n/** The unions of overlapping cores, in position order. */\nfunction mergeOverlappingCores(cores: [number, number][]): [number, number][] {\n const merged: [number, number][] = [];\n for (const [start, end] of cores.toSorted((left, right) => left[0] - right[0])) {\n const last = merged.at(-1);\n if (last && start < last[1]) {\n last[1] = Math.max(last[1], end);\n } else {\n merged.push([start, end]);\n }\n }\n return merged;\n}\n\n/** Whether the spans, merged, cover every token of a range. */\nfunction createCoverageTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n // Touching spans merge too: together they cover a range across their boundary.\n const merged: [number, number][] = [];\n for (const { startTokenIndex, endTokenIndex } of spans.toSorted(\n (left, right) => left.startTokenIndex - right.startTokenIndex\n )) {\n const last = merged.at(-1);\n if (last && startTokenIndex <= last[1]) {\n last[1] = Math.max(last[1], endTokenIndex);\n } else {\n merged.push([startTokenIndex, endTokenIndex]);\n }\n }\n return (range) => {\n let low = 0;\n let high = merged.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((merged[middle]?.[0] ?? 0) <= range.startTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return (merged[low - 1]?.[1] ?? -1) >= range.endTokenIndex;\n };\n}\n\n/**\n * Whether a range overlaps any of the spans: among the spans starting before the range ends\n * (binary search over sorted starts), the furthest end reaches past the range's start.\n */\nfunction createOverlapTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n const sorted = spans.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex);\n const maxEndPrefix = new Int32Array(sorted.length);\n let maxEnd = -1;\n for (const [index, span] of sorted.entries()) {\n maxEnd = Math.max(maxEnd, span.endTokenIndex);\n maxEndPrefix[index] = maxEnd;\n }\n return (range) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < range.endTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low > 0 && (maxEndPrefix[low - 1] ?? -1) > range.startTokenIndex;\n };\n}\n\n/**\n * The block's occurrence with one segment per entry of `cores` (the whole block for `undefined`,\n * a whole match). Source offsets stay the block's: tokens carry none, and near-miss occurrences\n * report lines only.\n */\nfunction toOccurrence(\n { fileIndex, range }: NormalizedBlock,\n files: NearMissSourceFile[],\n cores: ([number, number] | undefined)[],\n anchor: boolean\n): NearMissOccurrence {\n const whole = cores.includes(undefined);\n const segments = cores\n .map((core): [number, number] => core ?? [range.startTokenIndex, range.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0])\n .map(([startTokenIndex, endTokenIndex]) => ({ startTokenIndex, endTokenIndex }));\n const start = segments[0]?.startTokenIndex ?? range.startTokenIndex;\n const end = segments.at(-1)?.endTokenIndex ?? range.endTokenIndex;\n const tokens = files[fileIndex]?.tokens;\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments,\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: whole ? range.startLine : (tokens?.[start]?.startRow ?? 0) + 1,\n endLine: whole ? range.endLine : (tokens?.[end - 1]?.endRow ?? 0) + 1,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two blocks wholly covered by reported spans and pairs whose\n * length ratio rules out both\n * whole-block similarity and `maxLengthRatio`. Blocks are\n * indexed in ascending length, so each posting list is scanned backwards only while its blocks\n * are long enough; shared counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n fullyReported: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const reportedFlags = Uint8Array.from(fullyReported, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightReported = reportedFlags[right] === 1;\n const minLeftLength = Math.min(\n Math.ceil((lengths[right] ?? 0) / maxLengthRatio),\n Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100)\n );\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightReported && reportedFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Returns the pair verifier. Content symbols are weighted by integer self-information,\n * 1 + floor(log2((N + 1) / df)) over N blocks capped at `maxContentWeight`, so rare names and\n * values (the logic a copy preserves) outweigh ubiquitous ones, following the\n * information-theoretic weighting of ECScan's essence-clone detection (2025).\n */\nfunction createMatcher(\n blocks: NormalizedBlock[],\n minTokens: number,\n minSimilarityPercent: number\n): (left: NormalizedBlock, right: NormalizedBlock, rightIndex: number) => PairMatch | undefined {\n const documentFrequencies = new Map<number, number>();\n for (const block of blocks) {\n for (const symbol of block.contentCounts.keys()) {\n documentFrequencies.set(symbol, (documentFrequencies.get(symbol) ?? 0) + 1);\n }\n }\n const selfInformation = (documentFrequency: number): number =>\n Math.min(31 - Math.clz32(Math.floor((blocks.length + 1) / documentFrequency)) + 1, maxContentWeight);\n const weights = new Map<number, number>();\n for (const [symbol, frequency] of documentFrequencies) {\n weights.set(symbol, selfInformation(frequency));\n }\n const weigh = (counts: Map<number, number>): WeightedContent => {\n const symbols = Int32Array.from(counts.keys()).toSorted();\n // Span content comes from blocks, so every symbol has a weight.\n const weightedCounts = Int32Array.from(symbols, (symbol) => (counts.get(symbol) ?? 0) * (weights.get(symbol) ?? 0));\n let total = 0;\n for (const count of weightedCounts) {\n total += count;\n }\n return { symbols, weightedCounts, total };\n };\n const blockContents = new Map(blocks.map((block) => [block, weigh(block.contentCounts)]));\n\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: NormalizedBlock, rightIndex: number, sequence: Int32Array): number => {\n if (counterBlock !== rightIndex || !counter) {\n counterBlock = rightIndex;\n counter = createLcsLengthCounter(right.sequence);\n }\n return counter(sequence);\n };\n\n /**\n * Matches the cores two blocks share inside different surroundings (a copy wrapped in added\n * code, or two copies embedded in different code), which whole-block similarity misses\n * (CCAligner's large-gap and LVMapper's large-variance clones). N-grams unique to each block\n * anchor the alignment; their longest chain increasing in both blocks (a run filter keeps only\n * anchors continuing a diagonal, but the chain may shift diagonals at small insertions), split at\n * gaps, delimits the cores, and every core pair that is a near-miss clone in its own right is\n * returned.\n */\n const matchLocally = (left: NormalizedBlock, right: NormalizedBlock): PairMatch | undefined => {\n const anchors: [number, number][] = [];\n for (\n let leftIndex = 0, rightIndex = 0;\n leftIndex < left.uniqueNgrams.length && rightIndex < right.uniqueNgrams.length;\n ) {\n const leftHash = left.uniqueNgrams[leftIndex] ?? 0;\n const rightHash = right.uniqueNgrams[rightIndex] ?? 0;\n if (leftHash === rightHash) {\n anchors.push([left.uniqueNgramOffsets[leftIndex] ?? 0, right.uniqueNgramOffsets[rightIndex] ?? 0]);\n }\n if (leftHash <= rightHash) {\n leftIndex += 1;\n }\n if (rightHash <= leftHash) {\n rightIndex += 1;\n }\n }\n anchors.sort((first, second) => first[0] - second[0]);\n // An isolated 5-gram match is often coincidental (n-grams are identifier-blind); a copied core\n // yields runs of consecutive anchors, so only anchors continuing a diagonal run are chained.\n const runAnchors = anchors.filter(([leftOffset, rightOffset], index) => {\n const previous = anchors[index - 1];\n const next = anchors[index + 1];\n return (\n (previous?.[0] === leftOffset - 1 && previous[1] === rightOffset - 1) ||\n (next?.[0] === leftOffset + 1 && next[1] === rightOffset + 1)\n );\n });\n const leftOffset = left.range.startTokenIndex;\n const rightOffset = right.range.startTokenIndex;\n const cores: CorePair[] = [];\n for (const segment of chainSegments(longestIncreasingChain(runAnchors))) {\n const [leftStart, rightStart] = segment[0] ?? [0, 0];\n const [leftLast, rightLast] = segment.at(-1) ?? [0, 0];\n const leftEnd = leftLast + ngramSize;\n const rightEnd = rightLast + ngramSize;\n const leftLength = leftEnd - leftStart;\n const rightLength = rightEnd - rightStart;\n const shorter = Math.min(leftLength, rightLength);\n const required = minSimilarityPercent * Math.max(leftLength, rightLength);\n if (\n shorter >= minTokens &&\n shorter * 100 >= required &&\n anchoredTokenCount(segment) * 100 >= minAnchorCoveragePercent * shorter &&\n sharesContent(\n weigh(countContent(left.symbols, left.isContent, leftStart, leftEnd)),\n weigh(countContent(right.symbols, right.isContent, rightStart, rightEnd))\n ) &&\n lcsLength(\n anonymize(left.symbols.subarray(leftStart, leftEnd)),\n anonymize(right.symbols.subarray(rightStart, rightEnd))\n ) *\n 100 >=\n required\n ) {\n cores.push([\n [leftOffset + leftStart, leftOffset + leftEnd],\n [rightOffset + rightStart, rightOffset + rightEnd],\n ]);\n }\n }\n return cores.length > 0 ? { kind: 'local', cores } : undefined;\n };\n\n /** Cheapest bounds first: the LCS cannot exceed the shorter block's length nor the bag overlap. */\n return (left, right, rightIndex) => {\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (\n Math.min(left.sequence.length, right.sequence.length) * 100 >= required &&\n sharesContent(blockContents.get(left), blockContents.get(right)) &&\n ((sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 >= required &&\n lcsLengthWithRight(right, rightIndex, left.sequence) * 100 >= required) ||\n matchesReordered(left, right, required))\n ) {\n return { kind: 'whole' };\n }\n return matchLocally(left, right);\n };\n}\n\n/** Content-bearing symbols, sorted, with their information-weighted counts. */\ninterface WeightedContent {\n symbols: Int32Array;\n weightedCounts: Int32Array;\n total: number;\n}\n\n/**\n * A structural match must be backed by shared content: more than `minContentSimilarityPercent` of\n * the larger side's information-weighted names and literal values. Two sides without content never\n * pass.\n */\nfunction sharesContent(left: WeightedContent | undefined, right: WeightedContent | undefined): boolean {\n if (!left || !right) {\n return false;\n }\n let overlap = 0;\n for (let leftIndex = 0, rightIndex = 0; leftIndex < left.symbols.length && rightIndex < right.symbols.length;) {\n const leftSymbol = left.symbols[leftIndex] ?? 0;\n const rightSymbol = right.symbols[rightIndex] ?? 0;\n if (leftSymbol === rightSymbol) {\n overlap += Math.min(left.weightedCounts[leftIndex] ?? 0, right.weightedCounts[rightIndex] ?? 0);\n }\n if (leftSymbol <= rightSymbol) {\n leftIndex += 1;\n }\n if (rightSymbol <= leftSymbol) {\n rightIndex += 1;\n }\n }\n return overlap * 100 > minContentSimilarityPercent * Math.max(left.total, right.total);\n}\n\n/**\n * Compares the blocks with their top-level statements (each anonymized on its own) in a canonical\n * order, so a copy whose independent statements were swapped still matches.\n */\nfunction matchesReordered(left: NormalizedBlock, right: NormalizedBlock, required: number): boolean {\n return (\n left.canonicalSequence !== undefined &&\n right.canonicalSequence !== undefined &&\n lcsLength(left.canonicalSequence, right.canonicalSequence) * 100 >= required\n );\n}\n\n/**\n * The block's units (its top-level statements, given in file token indexes, and the token runs\n * between them), each anonymized on its own and sorted, concatenated; undefined with too few\n * statements.\n */\nfunction canonicalSequenceOf(\n symbols: Int32Array,\n statements: [number, number][],\n blockStart: number\n): Int32Array | undefined {\n if (statements.length < minReorderStatementCount) {\n return undefined;\n }\n const units: Int32Array[] = [];\n let cursor = 0;\n for (const [statementStart, statementEnd] of statements) {\n const start = statementStart - blockStart;\n if (cursor < start) {\n units.push(anonymize(symbols.subarray(cursor, start)));\n }\n units.push(anonymize(symbols.subarray(start, statementEnd - blockStart)));\n cursor = statementEnd - blockStart;\n }\n if (cursor < symbols.length) {\n units.push(anonymize(symbols.subarray(cursor)));\n }\n units.sort(compareSequences);\n const canonical = new Int32Array(symbols.length);\n let offset = 0;\n for (const unit of units) {\n canonical.set(unit, offset);\n offset += unit.length;\n }\n return canonical;\n}\n\n/** Lexicographic order, matching Rust's Vec<i32> ordering. */\nfunction compareSequences(left: Int32Array, right: Int32Array): number {\n for (let index = 0; index < Math.min(left.length, right.length); index += 1) {\n const difference = (left[index] ?? 0) - (right[index] ?? 0);\n if (difference !== 0) {\n return difference;\n }\n }\n return left.length - right.length;\n}\n\n/**\n * The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),\n * via patience sorting over right offsets.\n */\nfunction longestIncreasingChain(anchors: [number, number][]): [number, number][] {\n const tailIndexes: number[] = [];\n const predecessors: number[] = [];\n for (const [index, [, rightOffset]] of anchors.entries()) {\n let low = 0;\n let high = tailIndexes.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((anchors[tailIndexes[middle] ?? 0]?.[1] ?? 0) < rightOffset) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n predecessors.push(low > 0 ? (tailIndexes[low - 1] ?? -1) : -1);\n tailIndexes[low] = index;\n }\n const chain: [number, number][] = [];\n for (let cursor = tailIndexes.at(-1) ?? -1; cursor >= 0; cursor = predecessors[cursor] ?? -1) {\n const anchor = anchors[cursor];\n if (anchor) {\n chain.push(anchor);\n }\n }\n return chain.toReversed();\n}\n\n/** The chain's segments, split where consecutive anchors lie more than `maxAnchorGapTokens` apart in either block. */\nfunction chainSegments(chain: [number, number][]): [number, number][][] {\n const segments: [number, number][][] = [];\n for (const [index, anchor] of chain.entries()) {\n const previous = chain[index - 1];\n const continues =\n previous !== undefined &&\n anchor[0] - (previous[0] + ngramSize) <= maxAnchorGapTokens &&\n anchor[1] - (previous[1] + ngramSize) <= maxAnchorGapTokens;\n if (continues) {\n segments.at(-1)?.push(anchor);\n } else {\n segments.push([anchor]);\n }\n }\n return segments;\n}\n\n/** Left-block tokens the segment's anchors cover (overlapping anchors count once). */\nfunction anchoredTokenCount(segment: [number, number][]): number {\n let count = ngramSize;\n for (let index = 1; index < segment.length; index += 1) {\n count += Math.min((segment[index]?.[0] ?? 0) - (segment[index - 1]?.[0] ?? 0), ngramSize);\n }\n return count;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector. Non-identifier symbols are interned\n * project-wide from the tokens' hash pairs, so equal tokens compare equal across files, while\n * identifiers are interned per file and re-anonymized per compared range.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, containerStatements, nearMissBlocks }] of files.entries()) {\n if (!tokens || !nearMissBlocks?.length) {\n continue;\n }\n const symbols = new Int32Array(tokens.length);\n const isContent = new Uint8Array(tokens.length);\n const idByIdentifier = new Map<string, number>();\n for (const [index, token] of tokens.entries()) {\n if (token.kind === 'id') {\n let id = idByIdentifier.get(token.text);\n if (id === undefined) {\n id = idByIdentifier.size;\n idByIdentifier.set(token.text, id);\n }\n symbols[index] = -(id + 1);\n continue;\n }\n const key = tokenKey(token);\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n symbols[index] = symbol;\n isContent[index] = token.isName || token.literalHash !== undefined ? 1 : 0;\n }\n const findStatements = createTopLevelStatementFinder(containerStatements ?? []);\n for (const range of nearMissBlocks) {\n const { startTokenIndex: start, endTokenIndex: end } = range;\n const blockSymbols = symbols.subarray(start, end);\n const blockIsContent = isContent.subarray(start, end);\n const sequence = anonymize(blockSymbols);\n const ngramHashes = collectNgramHashes(blockSymbols);\n const occurrenceCounts = new Map<number, number>();\n for (const hash of ngramHashes) {\n occurrenceCounts.set(hash, (occurrenceCounts.get(hash) ?? 0) + 1);\n }\n const uniqueOffsets = ngramHashes\n .keys()\n .filter((offset) => occurrenceCounts.get(ngramHashes[offset] ?? 0) === 1)\n .toArray()\n .toSorted((first, second) => (ngramHashes[first] ?? 0) - (ngramHashes[second] ?? 0));\n blocks.push({\n fileIndex,\n range,\n symbols: blockSymbols,\n isContent: blockIsContent,\n sequence,\n sortedSequence: sequence.toSorted(),\n ngrams: Int32Array.from(occurrenceCounts.keys()),\n uniqueNgrams: Int32Array.from(uniqueOffsets, (offset) => ngramHashes[offset] ?? 0),\n uniqueNgramOffsets: Int32Array.from(uniqueOffsets),\n contentCounts: countContent(blockSymbols, blockIsContent, 0, blockSymbols.length),\n canonicalSequence: canonicalSequenceOf(blockSymbols, findStatements(start, end), start),\n });\n }\n }\n return blocks;\n}\n\n/**\n * Returns a lookup of the outermost container statements inside a token range, excluding a\n * statement spanning the whole range (the block itself).\n */\nfunction createTopLevelStatementFinder(\n containerStatements: TokenRange[][]\n): (start: number, end: number) => [number, number][] {\n const statements = containerStatements\n .flat()\n .filter((statement) => statement.startTokenIndex < statement.endTokenIndex)\n .map((statement): [number, number] => [statement.startTokenIndex, statement.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0] || right[1] - left[1]);\n return (start, end) => {\n let low = 0;\n let high = statements.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((statements[middle]?.[0] ?? 0) < start) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const topLevel: [number, number][] = [];\n for (let index = low; index < statements.length; index += 1) {\n const statement = statements[index];\n if (!statement || statement[0] >= end) {\n break;\n }\n const last = topLevel.at(-1);\n const nested = last !== undefined && statement[0] < last[1];\n if (statement[1] <= end && !(statement[0] === start && statement[1] === end) && !nested) {\n topLevel.push(statement);\n }\n }\n return topLevel;\n };\n}\n\nfunction countContent(symbols: Int32Array, isContent: Uint8Array, start: number, end: number): Map<number, number> {\n const counts = new Map<number, number>();\n for (let offset = start; offset < end; offset += 1) {\n if (isContent[offset] === 1) {\n const symbol = symbols[offset] ?? 0;\n counts.set(symbol, (counts.get(symbol) ?? 0) + 1);\n }\n }\n return counts;\n}\n\n/** Identifiers renumbered by first occurrence within `symbols`, so a range compares the same wherever it sits in its file. */\nfunction anonymize(symbols: Int32Array): Int32Array {\n const indexByIdentifier = new Map<number, number>();\n return symbols.map((symbol) => {\n if (symbol >= 0) {\n return symbol;\n }\n let index = indexByIdentifier.get(symbol);\n if (index === undefined) {\n index = indexByIdentifier.size;\n indexByIdentifier.set(symbol, index);\n }\n return -(index + 1);\n });\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\n/**\n * N-gram hash per start offset, identifier-blind (every identifier hashes as -1) so a block copied\n * into different surroundings (renumbering its identifiers) or with reordered statements still\n * shares its n-grams.\n */\nfunction collectNgramHashes(symbols: Int32Array): Int32Array {\n const hashes = new Int32Array(Math.max(symbols.length - ngramSize + 1, 0));\n for (let start = 0; start < hashes.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n const symbol = symbols[start + offset] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 like the native n-gram hash.\n hash = (Math.imul(hash, 31) + (symbol < 0 ? -1 : symbol)) | 0;\n }\n hashes[start] = hash;\n }\n return hashes;\n}\n"],"mappings":"yEAiHA,SAAgB,EACd,EACA,EACA,EACA,EACwB,CACxB,GAAI,GAAwB,IAC1B,MAAO,CAAC,EAEV,IAAM,EAAS,EAAgB,CAAK,EAC9B,EAAU,EAAc,EAAQ,EAAW,CAAoB,EAC/D,EAAuB,EAAoB,IAAI,CAAiB,EAChE,EAAyB,EAAoB,IAAI,CAAkB,EACnE,EAAgB,EAAO,KAAK,CAAE,YAAW,WAAY,EAAuB,EAAU,GAAG,CAAK,GAAK,EAAK,EACxG,EAAkB,EAAO,KAAK,CAAE,YAAW,WAAY,EAAqB,EAAU,GAAG,CAAK,GAAK,EAAK,EACxG,EAAwF,CAAC,EAC/F,EAAqB,EAAQ,EAAe,GAAuB,EAAM,IAAU,CACjF,IAAM,EAAY,EAAO,GACnB,EAAa,EAAO,GACpB,EAAQ,GAAa,GAAc,EAAQ,EAAW,EAAY,CAAK,EAC7E,GAAI,EAAO,CACT,GAAI,EAAM,OAAS,QAGX,EAAgB,IAAS,EAAgB,IAC7C,EAAM,KAAK,CAAC,EAAM,IAAA,GAAW,EAAO,IAAA,EAAS,CAAC,OAGhD,IAAK,GAAM,CAAC,EAAU,KAAc,EAAM,MACxC,EAAM,KAAK,CAAC,EAAM,EAAU,EAAO,CAAS,CAAC,CAGnD,CACF,CAAC,EAKD,IAAM,EAAe,EAAO,QAAU,EAAK,EACrC,EAAa,EAAO,QAA8B,CAAC,CAAC,EAC1D,IAAK,GAAM,CAAC,EAAM,EAAU,EAAO,KAAc,EAC/C,IAAK,GAAM,CAAC,EAAO,IAAS,CAC1B,CAAC,EAAM,CAAQ,EACf,CAAC,EAAO,CAAS,CACnB,EACM,EACF,EAAW,EAAM,EAAE,KAAK,CAAI,EAE5B,EAAa,GAAS,GAI5B,IAAM,EAAsE,CAAC,EACvE,EAA6B,CAAC,EACpC,IAAK,IAAM,KAAc,EAAO,KAAK,EAAG,CACtC,EAAiB,KAAK,EAAM,MAAM,EAClC,IAAM,EAAQ,EAAa,GAAc,CAAC,EAAI,EAAsB,EAAW,IAAe,CAAC,CAAC,EAC5F,EAAM,SAAW,GACnB,EAAM,KAAK,CAAE,aAAY,KAAM,IAAA,EAAU,CAAC,EAE5C,IAAK,IAAM,KAAQ,EACjB,EAAM,KAAK,CAAE,aAAY,MAAK,CAAC,CAEnC,CACA,IAAM,GAAU,EAAoB,IAA+C,CACjF,IAAM,EAAQ,EAAiB,IAAe,EAC9C,GAAI,CAAC,GAAQ,EAAa,GACxB,OAAO,EAET,IAAK,IAAI,EAAO,EAAO,EAAM,EAAK,EAAE,aAAe,EAAY,GAAQ,EAAG,CACxE,IAAM,EAAO,EAAM,EAAK,EAAE,KAC1B,GAAI,GAAQ,EAAK,IAAM,EAAK,IAAM,EAAK,IAAM,EAAK,GAChD,OAAO,CAEX,CACA,MAAU,MAAM,0DAA0D,CAC5E,EAEM,EAAS,EAAM,KAAK,EAAG,IAAU,CAAK,EACtC,EAAQ,GAA0B,CACtC,IAAI,EAAO,EACX,KAAO,EAAO,KAAU,GACtB,EAAO,EAAO,IAAS,EAEzB,IAAK,IAAI,EAAU,EAAO,EAAO,KAAa,GAAO,CACnD,IAAM,EAAO,EAAO,IAAY,EAChC,EAAO,GAAW,EAClB,EAAU,CACZ,CACA,OAAO,CACT,EAGM,EAAW,EAAM,KAAK,CAAE,aAAY,UAAW,CACnD,IAAM,EAAQ,EAAO,GACf,CAAC,EAAiB,GAAiB,GAAQ,CAC/C,GAAO,MAAM,iBAAmB,EAChC,GAAO,MAAM,eAAiB,CAChC,EACA,OAAO,EAAqB,GAAO,WAAa,EAAE,GAAG,CAAE,kBAAiB,eAAc,CAAC,GAAK,EAC9F,CAAC,EACD,IAAK,GAAM,CAAC,EAAM,EAAU,EAAO,KAAc,EAAO,CACtD,IAAM,EAAW,EAAO,EAAM,CAAQ,EAChC,EAAY,EAAO,EAAO,CAAS,EACzC,GAAI,EAAS,IAAa,EAAS,GACjC,SAEF,IAAM,EAAW,EAAK,CAAQ,EACxB,EAAY,EAAK,CAAS,EAChC,EAAO,KAAK,IAAI,EAAU,CAAS,GAAK,KAAK,IAAI,EAAU,CAAS,CACtE,CAEA,IAAM,kBAAgB,IAAI,IAC1B,IAAK,IAAM,KAAQ,EAAM,KAAK,EAAG,CAC/B,IAAM,EAAO,EAAK,CAAI,EAChB,EAAU,EAAc,IAAI,CAAI,GAAK,CAAC,EAC5C,EAAQ,KAAK,CAAI,EACjB,EAAc,IAAI,EAAM,CAAO,CACjC,CACA,IAAM,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAW,EAAc,OAAO,EAAG,CAE5C,GAAI,EAAQ,OAAS,GAAK,EAAQ,MAAO,GAAS,EAAS,EAAK,EAC9D,SAIF,IAAM,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAQ,EAAS,CAC1B,GAAM,CAAE,aAAa,EAAG,QAAS,EAAM,IAAS,CAAC,EAC3C,EAAQ,EAAa,IAAI,CAAU,GAAK,CAAE,MAAO,CAAC,EAAG,OAAQ,EAAM,EACzE,EAAM,MAAM,KAAK,CAAI,EACrB,EAAM,SAAW,EAAS,IAAS,GACnC,EAAa,IAAI,EAAY,CAAK,CACpC,CACA,EAAO,KACL,CAAC,GAAG,CAAY,CAAC,CAAC,SAAS,CAAC,EAAY,CAAE,QAAO,aAAc,CAC7D,IAAM,EAAQ,EAAO,GACrB,OAAO,EAAQ,CAAC,EAAa,EAAO,EAAO,EAAO,CAAM,CAAC,EAAI,CAAC,CAChE,CAAC,CACH,CACF,CACA,OAAO,CACT,CAGA,SAAS,EAAsB,EAA+C,CAC5E,IAAM,EAA6B,CAAC,EACpC,IAAK,GAAM,CAAC,EAAO,KAAQ,EAAM,UAAU,EAAM,IAAU,EAAK,GAAK,EAAM,EAAE,EAAG,CAC9E,IAAM,EAAO,EAAO,GAAG,EAAE,EACrB,GAAQ,EAAQ,EAAK,GACvB,EAAK,GAAK,KAAK,IAAI,EAAK,GAAI,CAAG,EAE/B,EAAO,KAAK,CAAC,EAAO,CAAG,CAAC,CAE5B,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACwE,CAExE,IAAM,EAA6B,CAAC,EACpC,IAAK,GAAM,CAAE,kBAAiB,mBAAmB,EAAM,UACpD,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAChD,EAAG,CACD,IAAM,EAAO,EAAO,GAAG,EAAE,EACrB,GAAQ,GAAmB,EAAK,GAClC,EAAK,GAAK,KAAK,IAAI,EAAK,GAAI,CAAa,EAEzC,EAAO,KAAK,CAAC,EAAiB,CAAa,CAAC,CAEhD,CACA,MAAQ,IAAU,CAChB,IAAI,EAAM,EACN,EAAO,EAAO,OAClB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,IAAU,GAC3B,EAAO,EAAO,GAAG,IAAM,IAAM,EAAM,gBACtC,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,OAAQ,EAAO,EAAM,EAAE,GAAG,IAAM,KAAO,EAAM,aAC/C,CACF,CAMA,SAAS,EACP,EACwE,CACxE,IAAM,EAAS,EAAM,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,EACrF,EAAe,IAAI,WAAW,EAAO,MAAM,EAC7C,EAAS,GACb,IAAK,GAAM,CAAC,EAAO,KAAS,EAAO,QAAQ,EACzC,EAAS,KAAK,IAAI,EAAQ,EAAK,aAAa,EAC5C,EAAa,GAAS,EAExB,MAAQ,IAAU,CAChB,IAAI,EAAM,EACN,EAAO,EAAO,OAClB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,IAAU,GAC3B,EAAO,EAAO,EAAE,iBAAmB,GAAK,EAAM,cACjD,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,OAAO,EAAM,IAAM,EAAa,EAAM,IAAM,IAAM,EAAM,eAC1D,CACF,CAOA,SAAS,EACP,CAAE,YAAW,SACb,EACA,EACA,EACoB,CACpB,IAAM,EAAQ,EAAM,SAAS,IAAA,EAAS,EAChC,EAAW,EACd,IAAK,GAA2B,GAAQ,CAAC,EAAM,gBAAiB,EAAM,aAAa,CAAC,CAAC,CACrF,UAAU,EAAM,IAAU,EAAK,GAAK,EAAM,EAAE,CAAC,CAC7C,KAAK,CAAC,EAAiB,MAAoB,CAAE,kBAAiB,eAAc,EAAE,EAC3E,EAAQ,EAAS,EAAE,EAAE,iBAAmB,EAAM,gBAC9C,EAAM,EAAS,GAAG,EAAE,CAAC,EAAE,eAAiB,EAAM,cAC9C,EAAS,EAAM,EAAU,EAAE,OACjC,MAAO,CACL,YACA,qBAAsB,GAAU,IAAA,GAChC,WACA,WAAY,EAAS,QAAQ,EAAK,IAAY,EAAM,EAAQ,cAAgB,EAAQ,gBAAiB,CAAC,EACtG,gBAAiB,EACjB,cAAe,EACf,WAAY,EAAM,WAClB,SAAU,EAAM,SAChB,UAAW,EAAQ,EAAM,WAAa,IAAS,EAAM,EAAE,UAAY,GAAK,EACxE,QAAS,EAAQ,EAAM,SAAW,IAAS,EAAM,EAAE,EAAE,QAAU,GAAK,CACtE,CACF,CAUA,SAAS,EACP,EACA,EACA,EACA,EACM,CACN,IAAM,kBAAiB,IAAI,IAC3B,IAAK,IAAM,KAAS,EAClB,IAAK,IAAM,KAAS,EAAM,OACxB,EAAe,IAAI,GAAQ,EAAe,IAAI,CAAK,GAAK,GAAK,CAAC,EAGlE,IAAK,IAAM,KAAS,EAClB,EAAM,OAAS,EAAM,OAAO,OAAQ,IAAW,EAAe,IAAI,CAAK,GAAK,IAAM,GAAsB,EAI1G,IAAM,EAAc,WAAW,KAAK,EAAS,GAAU,EAAM,SAAS,EAChE,EAAgB,WAAW,KAAK,EAAe,MAAM,EACrD,EAAU,WAAW,KAAK,EAAS,GAAU,EAAM,SAAS,MAAM,EAClE,EAAc,WAAW,KAAK,EAAS,GAAU,EAAM,OAAO,MAAM,EACpE,EAAQ,CAAC,GAAG,EAAO,KAAK,CAAC,CAAC,CAAC,UAAU,EAAM,KAAW,EAAQ,IAAS,IAAM,EAAQ,IAAU,EAAE,EACjG,kBAAW,IAAI,IACf,EAAe,IAAI,WAAW,EAAO,MAAM,EAC3C,EAAoB,CAAC,EAC3B,IAAK,IAAM,KAAS,EAAO,CACzB,IAAM,EAAY,EAAY,GACxB,EAAgB,EAAc,KAAW,EACzC,EAAgB,KAAK,IACzB,KAAK,MAAM,EAAQ,IAAU,GAAK,CAAc,EAChD,KAAK,KAAM,GAAwB,EAAQ,IAAU,GAAM,GAAG,CAChE,EACM,EAAS,EAAO,EAAM,EAAE,QAAU,CAAC,EACzC,IAAK,IAAM,KAAS,EAAQ,CAC1B,IAAM,EAAU,EAAS,IAAI,CAAK,EAClC,GAAI,CAAC,EAAS,CACZ,EAAS,IAAI,EAAO,CAAC,CAAK,CAAC,EAC3B,QACF,CACA,IAAK,IAAI,EAAW,EAAQ,OAAS,EAAG,GAAY,EAAG,IAAe,CACpE,IAAM,EAAO,EAAQ,IAAa,EAClC,IAAK,EAAQ,IAAS,GAAK,EACzB,MAEE,EAAY,KAAU,GAAc,GAAiB,EAAc,KAAU,IAG7E,EAAa,KAAU,GACzB,EAAQ,KAAK,CAAI,EAEnB,EAAa,IAAS,EAAa,IAAS,GAAK,EACnD,CACA,EAAQ,KAAK,CAAK,CACpB,CACA,IAAK,IAAM,KAAQ,EAAS,CAC1B,IAAM,EAAS,EAAa,IAAS,EACrC,EAAa,GAAQ,EACjB,EAAS,KAAO,GAAoB,KAAK,IAAI,EAAY,IAAS,EAAG,EAAO,MAAM,GACpF,EAAM,EAAM,CAAK,CAErB,CACA,EAAQ,OAAS,CACnB,CACF,CAQA,SAAS,EACP,EACA,EACA,EAC8F,CAC9F,IAAM,kBAAsB,IAAI,IAChC,IAAK,IAAM,KAAS,EAClB,IAAK,IAAM,KAAU,EAAM,cAAc,KAAK,EAC5C,EAAoB,IAAI,GAAS,EAAoB,IAAI,CAAM,GAAK,GAAK,CAAC,EAG9E,IAAM,EAAmB,GACvB,KAAK,IAAI,GAAK,KAAK,MAAM,KAAK,OAAO,EAAO,OAAS,GAAK,CAAiB,CAAC,EAAI,EAAG,CAAgB,EAC/F,kBAAU,IAAI,IACpB,IAAK,GAAM,CAAC,EAAQ,KAAc,EAChC,EAAQ,IAAI,EAAQ,EAAgB,CAAS,CAAC,EAEhD,IAAM,EAAS,GAAiD,CAC9D,IAAM,EAAU,WAAW,KAAK,EAAO,KAAK,CAAC,CAAC,CAAC,SAAS,EAElD,EAAiB,WAAW,KAAK,EAAU,IAAY,EAAO,IAAI,CAAM,GAAK,IAAM,EAAQ,IAAI,CAAM,GAAK,EAAE,EAC9G,EAAQ,EACZ,IAAK,IAAM,KAAS,EAClB,GAAS,EAEX,MAAO,CAAE,UAAS,iBAAgB,OAAM,CAC1C,EACM,EAAgB,IAAI,IAAI,EAAO,IAAK,GAAU,CAAC,EAAO,EAAM,EAAM,aAAa,CAAC,CAAC,CAAC,EAIpF,EAAe,GACf,EACE,GAAsB,EAAwB,EAAoB,MAClE,IAAiB,GAAc,CAAC,KAClC,EAAe,EACf,EAAU,EAAuB,EAAM,QAAQ,GAE1C,EAAQ,CAAQ,GAYnB,GAAgB,EAAuB,IAAkD,CAC7F,IAAM,EAA8B,CAAC,EACrC,IACE,IAAI,EAAY,EAAG,EAAa,EAChC,EAAY,EAAK,aAAa,QAAU,EAAa,EAAM,aAAa,QACxE,CACA,IAAM,EAAW,EAAK,aAAa,IAAc,EAC3C,EAAY,EAAM,aAAa,IAAe,EAChD,IAAa,GACf,EAAQ,KAAK,CAAC,EAAK,mBAAmB,IAAc,EAAG,EAAM,mBAAmB,IAAe,CAAC,CAAC,EAE/F,GAAY,IACd,GAAa,GAEX,GAAa,IACf,GAAc,EAElB,CACA,EAAQ,MAAM,EAAO,IAAW,EAAM,GAAK,EAAO,EAAE,EAGpD,IAAM,EAAa,EAAQ,QAAQ,CAAC,EAAY,GAAc,IAAU,CACtE,IAAM,EAAW,EAAQ,EAAQ,GAC3B,EAAO,EAAQ,EAAQ,GAC7B,OACG,IAAW,KAAO,EAAa,GAAK,EAAS,KAAO,EAAc,GAClE,IAAO,KAAO,EAAa,GAAK,EAAK,KAAO,EAAc,CAE/D,CAAC,EACK,EAAa,EAAK,MAAM,gBACxB,EAAc,EAAM,MAAM,gBAC1B,EAAoB,CAAC,EAC3B,IAAK,IAAM,KAAW,EAAc,EAAuB,CAAU,CAAC,EAAG,CACvE,GAAM,CAAC,EAAW,GAAc,EAAQ,IAAM,CAAC,EAAG,CAAC,EAC7C,CAAC,EAAU,GAAa,EAAQ,GAAG,EAAE,GAAK,CAAC,EAAG,CAAC,EAC/C,EAAU,EAAW,EACrB,EAAW,EAAY,EACvB,EAAa,EAAU,EACvB,EAAc,EAAW,EACzB,EAAU,KAAK,IAAI,EAAY,CAAW,EAC1C,EAAW,EAAuB,KAAK,IAAI,EAAY,CAAW,EAEtE,GAAW,GACX,EAAU,KAAO,GACjB,EAAmB,CAAO,EAAI,KAAO,GAA2B,GAChE,EACE,EAAM,EAAa,EAAK,QAAS,EAAK,UAAW,EAAW,CAAO,CAAC,EACpE,EAAM,EAAa,EAAM,QAAS,EAAM,UAAW,EAAY,CAAQ,CAAC,CAC1E,GACA,EACE,EAAU,EAAK,QAAQ,SAAS,EAAW,CAAO,CAAC,EACnD,EAAU,EAAM,QAAQ,SAAS,EAAY,CAAQ,CAAC,CACxD,EACE,KACA,GAEF,EAAM,KAAK,CACT,CAAC,EAAa,EAAW,EAAa,CAAO,EAC7C,CAAC,EAAc,EAAY,EAAc,CAAQ,CACnD,CAAC,CAEL,CACA,OAAO,EAAM,OAAS,EAAI,CAAE,KAAM,QAAS,OAAM,EAAI,IAAA,EACvD,EAGA,OAAQ,EAAM,EAAO,IAAe,CAClC,IAAM,EAAW,EAAuB,KAAK,IAAI,EAAK,SAAS,OAAQ,EAAM,SAAS,MAAM,EAU5F,OARE,KAAK,IAAI,EAAK,SAAS,OAAQ,EAAM,SAAS,MAAM,EAAI,KAAO,GAC/D,EAAc,EAAc,IAAI,CAAI,EAAG,EAAc,IAAI,CAAK,CAAC,IAC7D,EAAc,EAAK,eAAgB,EAAM,cAAc,EAAI,KAAO,GAClE,EAAmB,EAAO,EAAY,EAAK,QAAQ,EAAI,KAAO,GAC9D,EAAiB,EAAM,EAAO,CAAQ,GAEjC,CAAE,KAAM,OAAQ,EAElB,EAAa,EAAM,CAAK,CACjC,CACF,CAcA,SAAS,EAAc,EAAmC,EAA6C,CACrG,GAAI,CAAC,GAAQ,CAAC,EACZ,MAAO,GAET,IAAI,EAAU,EACd,IAAK,IAAI,EAAY,EAAG,EAAa,EAAG,EAAY,EAAK,QAAQ,QAAU,EAAa,EAAM,QAAQ,QAAS,CAC7G,IAAM,EAAa,EAAK,QAAQ,IAAc,EACxC,EAAc,EAAM,QAAQ,IAAe,EAC7C,IAAe,IACjB,GAAW,KAAK,IAAI,EAAK,eAAe,IAAc,EAAG,EAAM,eAAe,IAAe,CAAC,GAE5F,GAAc,IAChB,GAAa,GAEX,GAAe,IACjB,GAAc,EAElB,CACA,OAAO,EAAU,IAAM,GAA8B,KAAK,IAAI,EAAK,MAAO,EAAM,KAAK,CACvF,CAMA,SAAS,EAAiB,EAAuB,EAAwB,EAA2B,CAClG,OACE,EAAK,oBAAsB,IAAA,IAC3B,EAAM,oBAAsB,IAAA,IAC5B,EAAU,EAAK,kBAAmB,EAAM,iBAAiB,EAAI,KAAO,CAExE,CAOA,SAAS,EACP,EACA,EACA,EACwB,CACxB,GAAI,EAAW,OAAS,EACtB,OAEF,IAAM,EAAsB,CAAC,EACzB,EAAS,EACb,IAAK,GAAM,CAAC,EAAgB,KAAiB,EAAY,CACvD,IAAM,EAAQ,EAAiB,EAC3B,EAAS,GACX,EAAM,KAAK,EAAU,EAAQ,SAAS,EAAQ,CAAK,CAAC,CAAC,EAEvD,EAAM,KAAK,EAAU,EAAQ,SAAS,EAAO,EAAe,CAAU,CAAC,CAAC,EACxE,EAAS,EAAe,CAC1B,CACI,EAAS,EAAQ,QACnB,EAAM,KAAK,EAAU,EAAQ,SAAS,CAAM,CAAC,CAAC,EAEhD,EAAM,KAAK,CAAgB,EAC3B,IAAM,EAAY,IAAI,WAAW,EAAQ,MAAM,EAC3C,EAAS,EACb,IAAK,IAAM,KAAQ,EACjB,EAAU,IAAI,EAAM,CAAM,EAC1B,GAAU,EAAK,OAEjB,OAAO,CACT,CAGA,SAAS,EAAiB,EAAkB,EAA2B,CACrE,IAAK,IAAI,EAAQ,EAAG,EAAQ,KAAK,IAAI,EAAK,OAAQ,EAAM,MAAM,EAAG,GAAS,EAAG,CAC3E,IAAM,GAAc,EAAK,IAAU,IAAM,EAAM,IAAU,GACzD,GAAI,IAAe,EACjB,OAAO,CAEX,CACA,OAAO,EAAK,OAAS,EAAM,MAC7B,CAMA,SAAS,EAAuB,EAAiD,CAC/E,IAAM,EAAwB,CAAC,EACzB,EAAyB,CAAC,EAChC,IAAK,GAAM,CAAC,EAAO,EAAG,MAAiB,EAAQ,QAAQ,EAAG,CACxD,IAAI,EAAM,EACN,EAAO,EAAY,OACvB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,IAAU,GAC3B,EAAQ,EAAY,IAAW,EAAE,GAAG,IAAM,GAAK,EAClD,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,EAAa,KAAK,EAAM,EAAK,EAAY,EAAM,IAAM,GAAM,EAAE,EAC7D,EAAY,GAAO,CACrB,CACA,IAAM,EAA4B,CAAC,EACnC,IAAK,IAAI,EAAS,EAAY,GAAG,EAAE,GAAK,GAAI,GAAU,EAAG,EAAS,EAAa,IAAW,GAAI,CAC5F,IAAM,EAAS,EAAQ,GACnB,GACF,EAAM,KAAK,CAAM,CAErB,CACA,OAAO,EAAM,WAAW,CAC1B,CAGA,SAAS,EAAc,EAAiD,CACtE,IAAM,EAAiC,CAAC,EACxC,IAAK,GAAM,CAAC,EAAO,KAAW,EAAM,QAAQ,EAAG,CAC7C,IAAM,EAAW,EAAM,EAAQ,GAE7B,IAAa,IAAA,IACb,EAAO,IAAM,EAAS,GAAK,IAAc,IACzC,EAAO,IAAM,EAAS,GAAK,IAAc,GAEzC,EAAS,GAAG,EAAE,CAAC,EAAE,KAAK,CAAM,EAE5B,EAAS,KAAK,CAAC,CAAM,CAAC,CAE1B,CACA,OAAO,CACT,CAGA,SAAS,EAAmB,EAAqC,CAC/D,IAAI,EAAQ,EACZ,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,GAAS,EACnD,GAAS,KAAK,KAAK,EAAQ,EAAM,GAAG,IAAM,IAAM,EAAQ,EAAQ,EAAE,GAAG,IAAM,GAAI,CAAS,EAE1F,OAAO,CACT,CAGA,SAAS,EAAc,EAAkB,EAA2B,CAClE,IAAI,EAAU,EACV,EAAY,EACZ,EAAa,EACjB,KAAO,EAAY,EAAK,QAAU,EAAa,EAAM,QAAQ,CAC3D,IAAM,EAAY,EAAK,IAAc,EAC/B,EAAa,EAAM,IAAe,EACpC,IAAc,GAChB,GAAW,EACX,GAAa,EACb,GAAc,GACL,EAAY,EACrB,GAAa,EAEb,GAAc,CAElB,CACA,OAAO,CACT,CAOA,SAAS,EAAgB,EAAgD,CACvE,IAAM,kBAAmB,IAAI,IACvB,EAA4B,CAAC,EACnC,IAAK,GAAM,CAAC,EAAW,CAAE,SAAQ,sBAAqB,qBAAqB,EAAM,QAAQ,EAAG,CAC1F,GAAI,CAAC,GAAU,CAAC,GAAgB,OAC9B,SAEF,IAAM,EAAU,IAAI,WAAW,EAAO,MAAM,EACtC,EAAY,IAAI,WAAW,EAAO,MAAM,EACxC,kBAAiB,IAAI,IAC3B,IAAK,GAAM,CAAC,EAAO,KAAU,EAAO,QAAQ,EAAG,CAC7C,GAAI,EAAM,OAAS,KAAM,CACvB,IAAI,EAAK,EAAe,IAAI,EAAM,IAAI,EAClC,IAAO,IAAA,KACT,EAAK,EAAe,KACpB,EAAe,IAAI,EAAM,KAAM,CAAE,GAEnC,EAAQ,GAAS,EAAE,EAAK,GACxB,QACF,CACA,IAAM,EAAM,EAAS,CAAK,EACtB,EAAS,EAAiB,IAAI,CAAG,EACjC,IAAW,IAAA,KACb,EAAS,EAAiB,KAC1B,EAAiB,IAAI,EAAK,CAAM,GAElC,EAAQ,GAAS,EACjB,EAAU,GAAS,EAAM,QAAU,EAAM,cAAgB,IAAA,GAAY,EAAI,CAC3E,CACA,IAAM,EAAiB,EAA8B,GAAuB,CAAC,CAAC,EAC9E,IAAK,IAAM,KAAS,EAAgB,CAClC,GAAM,CAAE,gBAAiB,EAAO,cAAe,GAAQ,EACjD,EAAe,EAAQ,SAAS,EAAO,CAAG,EAC1C,EAAiB,EAAU,SAAS,EAAO,CAAG,EAC9C,EAAW,EAAU,CAAY,EACjC,EAAc,EAAmB,CAAY,EAC7C,kBAAmB,IAAI,IAC7B,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,IAAM,EAAgB,EACnB,KAAK,CAAC,CACN,OAAQ,GAAW,EAAiB,IAAI,EAAY,IAAW,CAAC,IAAM,CAAC,CAAC,CACxE,QAAQ,CAAC,CACT,UAAU,EAAO,KAAY,EAAY,IAAU,IAAM,EAAY,IAAW,EAAE,EACrF,EAAO,KAAK,CACV,YACA,QACA,QAAS,EACT,UAAW,EACX,WACA,eAAgB,EAAS,SAAS,EAClC,OAAQ,WAAW,KAAK,EAAiB,KAAK,CAAC,EAC/C,aAAc,WAAW,KAAK,EAAgB,GAAW,EAAY,IAAW,CAAC,EACjF,mBAAoB,WAAW,KAAK,CAAa,EACjD,cAAe,EAAa,EAAc,EAAgB,EAAG,EAAa,MAAM,EAChF,kBAAmB,EAAoB,EAAc,EAAe,EAAO,CAAG,EAAG,CAAK,CACxF,CAAC,CACH,CACF,CACA,OAAO,CACT,CAMA,SAAS,EACP,EACoD,CACpD,IAAM,EAAa,EAChB,KAAK,CAAC,CACN,OAAQ,GAAc,EAAU,gBAAkB,EAAU,aAAa,CAAC,CAC1E,IAAK,GAAgC,CAAC,EAAU,gBAAiB,EAAU,aAAa,CAAC,CAAC,CAC1F,UAAU,EAAM,IAAU,EAAK,GAAK,EAAM,IAAM,EAAM,GAAK,EAAK,EAAE,EACrE,OAAQ,EAAO,IAAQ,CACrB,IAAI,EAAM,EACN,EAAO,EAAW,OACtB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,IAAU,GAC3B,EAAW,EAAO,GAAG,IAAM,GAAK,EACnC,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,IAAM,EAA+B,CAAC,EACtC,IAAK,IAAI,EAAQ,EAAK,EAAQ,EAAW,OAAQ,GAAS,EAAG,CAC3D,IAAM,EAAY,EAAW,GAC7B,GAAI,CAAC,GAAa,EAAU,IAAM,EAChC,MAEF,IAAM,EAAO,EAAS,GAAG,EAAE,EACrB,EAAS,IAAS,IAAA,IAAa,EAAU,GAAK,EAAK,GACrD,EAAU,IAAM,IAAS,EAAU,KAAO,GAAS,EAAU,KAAO,IAAQ,CAAC,GAC/E,EAAS,KAAK,CAAS,CAE3B,CACA,OAAO,CACT,CACF,CAEA,SAAS,EAAa,EAAqB,EAAuB,EAAe,EAAkC,CACjH,IAAM,kBAAS,IAAI,IACnB,IAAK,IAAI,EAAS,EAAO,EAAS,EAAK,GAAU,EAC/C,GAAI,EAAU,KAAY,EAAG,CAC3B,IAAM,EAAS,EAAQ,IAAW,EAClC,EAAO,IAAI,GAAS,EAAO,IAAI,CAAM,GAAK,GAAK,CAAC,CAClD,CAEF,OAAO,CACT,CAGA,SAAS,EAAU,EAAiC,CAClD,IAAM,kBAAoB,IAAI,IAC9B,OAAO,EAAQ,IAAK,GAAW,CAC7B,GAAI,GAAU,EACZ,OAAO,EAET,IAAI,EAAQ,EAAkB,IAAI,CAAM,EAKxC,OAJI,IAAU,IAAA,KACZ,EAAQ,EAAkB,KAC1B,EAAkB,IAAI,EAAQ,CAAK,GAE9B,EAAE,EAAQ,EACnB,CAAC,CACH,CAOA,SAAS,EAAS,EAAsB,CACtC,IAAM,EAAU,EAAM,SAAW,KAAK,KAAK,EAAM,aAAe,EAAG,UAAa,EAC1E,EAAY,EAAM,UAAY,KAAK,KAAK,EAAM,cAAgB,EAAG,UAAa,EACpF,OAAQ,IAAY,GAAK,SAAc,IAAc,GACvD,CAOA,SAAS,EAAmB,EAAiC,CAC3D,IAAM,EAAS,IAAI,WAAW,KAAK,IAAI,EAAQ,OAAS,EAAY,EAAG,CAAC,CAAC,EACzE,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAG,CACrD,IAAI,EAAO,KACX,IAAK,IAAI,EAAS,EAAG,EAAS,EAAW,GAAU,EAAG,CACpD,IAAM,EAAS,EAAQ,EAAQ,IAAW,EAE1C,EAAQ,KAAK,KAAK,EAAM,EAAE,GAAK,EAAS,EAAI,GAAK,GAAW,CAC9D,CACA,EAAO,GAAS,CAClB,CACA,OAAO,CACT"}
1
+ {"version":3,"file":"crossFileNearMiss.js","names":[],"sources":["../src/crossFileNearMiss.ts"],"sourcesContent":["import {\n createLcsLengthCounter,\n lcsLength,\n markLcs,\n type CountedOccurrence,\n type Token,\n type TokenRange,\n type TokenSegment,\n} from './duplication.js';\n\n/**\n * Cross-file near-miss (Type-3) clone detection, following the within-file detector's model\n * (native/src/near_miss.rs): candidate block pairs are filtered through an n-gram inverted index\n * (NIL, Nakagawa et al. 2021), then verified by token-level longest common subsequence against the\n * larger block (NiCad's per-fragment similarity), backed by an information-weighted content gate,\n * with a statement-order-insensitive fallback and a local match over the anchored cores of two\n * blocks. Only pairs of blocks in different files are compared: a same-file pair is the\n * within-file detector's concern.\n */\n\nexport interface NearMissSourceFile {\n tokens?: Token[];\n containerStatements?: TokenRange[][];\n nearMissBlocks?: TokenRange[];\n}\n\n/** One copy (a block or its matched cores) in a near-miss group; anchors carry `spanCountedElsewhere` (see collectCrossFileNearMissGroups). */\nexport interface NearMissOccurrence extends CountedOccurrence {\n fileIndex: number;\n}\n\n/** N-gram size of the candidate index and local-match anchors (NIL's default). */\nconst ngramSize = 5;\n/** Filtration threshold: shared distinct n-grams over the smaller block's (NIL's default). */\nconst filtrationPercent = 10;\n/**\n * Pairs whose longer block exceeds this multiple of the shorter are compared only when whole-block\n * similarity still allows their ratio (below a minSimilarityPercent of 34). The candidate scan\n * stops at this floor while walking length-ordered postings, so pairs of very different lengths are\n * neither counted nor verified; `maxNgramBlockFrequency` is what bounds the scan's total cost.\n */\nconst maxLengthRatio = 3;\n/**\n * A structural match must also share content: more than this percent of the larger side's\n * information-weighted content-bearing tokens (names and literal values), so blocks of the same\n * shape that call different APIs on different data are not clones.\n */\nconst minContentSimilarityPercent = 50;\n/**\n * Caps content weights so only names and values spread over more than a quarter of the blocks are\n * discounted: a family of copies shares its content across several blocks, and uncapped rarity\n * weighting would let each copy's few unique edits outweigh everything the family shares.\n */\nconst maxContentWeight = 3;\n/**\n * Anchors must cover at least this percent of the shorter core: sparser chains are coincidental\n * runs of common n-grams in merely similar-looking code, and the cheap bound spares their content\n * and LCS checks. Not the similarity threshold itself, since n-grams repeated within a block\n * (repetitive statements) never anchor.\n */\nconst minAnchorCoveragePercent = 50;\n/** Anchors farther apart than this (in either block) split a local match into separate chains. */\nconst maxAnchorGapTokens = 30;\n/** Statement-order-insensitive comparison needs this many top-level statements per block. */\nconst minReorderStatementCount = 2;\n/**\n * N-grams occurring in more blocks than this are stop n-grams (syntax boilerplate such as a chain\n * of closing braces), left out of the index and of each block's n-gram count. Counting shared\n * n-grams costs the square of an n-gram's block frequency, so without the cap a project's most\n * common n-grams make filtration quadratic in the block count, while they discriminate nothing.\n */\nconst maxNgramBlockFrequency = 1000;\n\ninterface NormalizedBlock {\n fileIndex: number;\n range: TokenRange;\n /** Interned non-identifier symbols (>= 0) and identifiers as -(file-level id + 1). */\n symbols: Int32Array;\n isContent: Uint8Array;\n /**\n * Per token of the block's file, a key of its content for a non-identifier: unlike a symbol,\n * numbered by first occurrence, it does not depend on the order of the files.\n */\n fileTokenKeys: Float64Array;\n /** Identifiers anonymized by first occurrence within the block. */\n sequence: Int32Array;\n /**\n * The content of the block as written: per token its content key, or the number of its\n * identifier. Equal for equal content wherever it lies, which symbols, numbered by first\n * occurrence, are not; ordered like `written_content` in native/src/near_miss.rs, so that both\n * detectors take the sides of a pair in the same order.\n */\n writtenContent: Float64Array;\n /** The sequence sorted, for the token-bag upper bound on the LCS. */\n sortedSequence: Int32Array;\n /** Distinct non-stop n-gram hashes. */\n ngrams: Int32Array;\n /**\n * The n-grams occurring exactly once in the block, sorted, with their offsets in the parallel\n * array: two blocks' local-match anchors intersect by merging.\n */\n uniqueNgrams: Int32Array;\n uniqueNgramOffsets: Int32Array;\n contentCounts: Map<number, number>;\n /**\n * The sequence with its top-level statements in canonical order and the block offset each of its\n * tokens comes from, when the block has enough statements for statement-order-insensitive\n * comparison.\n */\n canonical: { sequence: Int32Array; offsets: Int32Array } | undefined;\n}\n\n/** A verified core in each block of a pair, as file token ranges. */\ntype CorePair = [[number, number], [number, number]];\n\n/** What alignPair needs to recover the tokens a verified match pairs. */\ninterface Alignment {\n /** Whether the blocks matched only with their statements in canonical order. */\n reordered: boolean;\n}\n\n/** How a verified pair matched: whole blocks, or every anchored core pair (one per gap-split chain segment). */\ntype PairMatch = (Alignment & { cores?: undefined }) | { cores: (Alignment & { pair: CorePair })[] };\n\n/** A verified near-miss pair of blocks; without `cores`, the blocks match as wholes. */\ninterface MatchEdge {\n left: number;\n right: number;\n cores?: CorePair;\n /** The token runs the pair matches, on the left and on the right. */\n matchedRuns: [[number, number][], [number, number][]];\n}\n\n/**\n * Clusters verified cross-file near-miss pairs into groups. A node (a whole block or a matched\n * core) overlapping an occurrence of `reportedSpansByFile` (the exact cross-file groups) is an\n * anchor: it links near-miss copies to the content an exact group already reports, and appears in\n * the near-miss group marked `spanCountedElsewhere` so block counting does not count its span\n * twice; it still counts the lines its partners match outside those spans. A pair matching\n * nothing outside reported spans has nothing to add to them and is dropped before clustering (two\n * blocks wholly covered by reported spans are not even compared), so it neither collapses the\n * blocks' core nodes nor widens a node beyond what the pairs clustering it matched. Any other\n * pair is kept however much of it is reported: what the exact pipeline reports of a copy depends\n * on where its copies stand, and the lines the copy counts must not. A group needs at least one\n * node reported spans do not cover entirely. A block that matched only locally is reported as its matched cores\n * (overlapping cores merged), each clustered with its own partners, so code no verified pair\n * matched never counts as duplicated.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minTokens: number,\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const matcher = createMatcher(blocks, minTokens, minSimilarityPercent);\n const countReported = reportedSpansByFile.map(createReportedTokenCounter);\n const reportedCount = (fileIndex: number, [start, end]: [number, number]): number =>\n countReported[fileIndex]?.(start, end) ?? 0;\n const unreportedCount = (fileIndex: number, runs: [number, number][]): number =>\n runs.reduce((sum, run) => sum + run[1] - run[0] - reportedCount(fileIndex, run), 0);\n const fullyReported = blocks.map((block) => {\n const span = spanOf(block);\n return reportedCount(block.fileIndex, span) === span[1] - span[0];\n });\n const edges: MatchEdge[] = [];\n forEachCandidatePair(blocks, fullyReported, minSimilarityPercent, (left, right) => {\n const leftBlock = blocks[left];\n const rightBlock = blocks[right];\n const match = leftBlock && rightBlock && matcher(leftBlock, rightBlock, right);\n if (!match) {\n return;\n }\n // The lines of a near-miss copy that are duplicated are those a verified partner matches,\n // which takes the pairing itself. Every pair kept is aligned: verifying it already cost a longest\n // common subsequence of the same sequences, and any choice among a copy's partners would let\n // their number or their order decide what the copy counts.\n for (const { pair: cores, reordered } of match.cores ?? [{ pair: undefined, reordered: match.reordered }]) {\n const [leftSpan, rightSpan] = cores ?? [spanOf(leftBlock), spanOf(rightBlock)];\n const addsLines = (leftRuns: [number, number][], rightRuns: [number, number][]): boolean =>\n unreportedCount(leftBlock.fileIndex, leftRuns) + unreportedCount(rightBlock.fileIndex, rightRuns) > 0;\n // Fully reported spans need no alignment to be dropped.\n if (!addsLines([leftSpan], [rightSpan])) {\n continue;\n }\n const matchedRuns = alignPair(leftBlock, rightBlock, { cores, reordered });\n if (addsLines(...matchedRuns)) {\n edges.push({ left, right, cores, matchedRuns });\n }\n }\n });\n\n // Clustering runs over (block, core) nodes: a block with a recorded whole match is one node,\n // and otherwise each union of its overlapping local cores is its own node, so disjoint cores\n // matched with different partners fall into separate groups.\n const matchedWhole = blocks.map(() => false);\n const localCores = blocks.map((): [number, number][] => []);\n for (const edge of edges) {\n for (const [index, core] of sidesOf(edge)) {\n if (core) {\n localCores[index]?.push(core);\n } else {\n matchedWhole[index] = true;\n }\n }\n }\n const nodes: { blockIndex: number; core: [number, number] | undefined }[] = [];\n const firstNodeByBlock: number[] = [];\n for (const blockIndex of blocks.keys()) {\n firstNodeByBlock.push(nodes.length);\n const cores = matchedWhole[blockIndex] ? [] : mergeOverlappingCores(localCores[blockIndex] ?? []);\n if (cores.length === 0) {\n nodes.push({ blockIndex, core: undefined });\n }\n for (const core of cores) {\n nodes.push({ blockIndex, core });\n }\n }\n const nodeOf = (blockIndex: number, core: [number, number] | undefined): number => {\n const first = firstNodeByBlock[blockIndex] ?? 0;\n if (!core || matchedWhole[blockIndex]) {\n return first;\n }\n for (let node = first; nodes[node]?.blockIndex === blockIndex; node += 1) {\n const span = nodes[node]?.core;\n if (span && span[0] <= core[0] && core[1] <= span[1]) {\n return node;\n }\n }\n throw new Error(\"every local core lies in one of its block's merged cores\");\n };\n\n const parent = nodes.map((_, index) => index);\n const find = (index: number): number => {\n let root = index;\n while (parent[root] !== root) {\n root = parent[root] ?? root;\n }\n for (let current = index; parent[current] !== root;) {\n const next = parent[current] ?? root;\n parent[current] = root;\n current = next;\n }\n return root;\n };\n // Anchoring is judged per node: a core is an anchor only when a reported span overlaps the core\n // itself, not merely elsewhere in its block.\n const nodeRanges = nodes.map(({ blockIndex, core }) => {\n const block = blocks[blockIndex];\n const [startTokenIndex, endTokenIndex] = core ?? [\n block?.range.startTokenIndex ?? 0,\n block?.range.endTokenIndex ?? 0,\n ];\n return {\n length: endTokenIndex - startTokenIndex,\n reported: reportedCount(block?.fileIndex ?? 0, [startTokenIndex, endTokenIndex]),\n };\n });\n const anchored = nodeRanges.map(({ reported }) => reported > 0);\n const reported = nodeRanges.map((range) => range.reported === range.length);\n const edgeNodes = edges.map((edge) => sidesOf(edge).map(([index, core]) => nodeOf(index, core)));\n for (const [leftNode = 0, rightNode = 0] of edgeNodes) {\n const leftRoot = find(leftNode);\n const rightRoot = find(rightNode);\n parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);\n }\n const matchedRunsByNode = nodes.map((): [number, number][] => []);\n for (const [edgeIndex, { matchedRuns }] of edges.entries()) {\n const [leftNode = 0, rightNode = 0] = edgeNodes[edgeIndex] ?? [];\n matchedRunsByNode[leftNode]?.push(...matchedRuns[0]);\n matchedRunsByNode[rightNode]?.push(...matchedRuns[1]);\n }\n\n const membersByRoot = new Map<number, number[]>();\n for (const node of nodes.keys()) {\n const root = find(node);\n const members = membersByRoot.get(root) ?? [];\n members.push(node);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((node) => reported[node])) {\n continue;\n }\n // A group's nodes from one block become ONE occurrence whose segments are its cores, so the\n // fragment-weighted count charges the block as one copy (as for gapped clones), not once per core.\n const coresByBlock = new Map<number, BlockCopy>();\n for (const node of members) {\n const { blockIndex = 0, core } = nodes[node] ?? {};\n const entry = coresByBlock.get(blockIndex) ?? { cores: [], anchor: false, matchedRuns: [] };\n entry.cores.push(core);\n entry.anchor ||= anchored[node] ?? false;\n entry.matchedRuns.push(...(matchedRunsByNode[node] ?? []));\n coresByBlock.set(blockIndex, entry);\n }\n groups.push(\n [...coresByBlock].flatMap(([blockIndex, copy]) => {\n const block = blocks[blockIndex];\n return block ? [toOccurrence(block, files, copy)] : [];\n })\n );\n }\n return groups;\n}\n\n/** The nodes of one block in a group: its cores (`undefined` for the whole block) and what its partners match of them. */\ninterface BlockCopy {\n cores: ([number, number] | undefined)[];\n anchor: boolean;\n matchedRuns: [number, number][];\n}\n\nfunction spanOf({ range }: NormalizedBlock): [number, number] {\n return [range.startTokenIndex, range.endTokenIndex];\n}\n\n/** The (block, core) on each side of a pair. */\nfunction sidesOf({ left, right, cores }: MatchEdge): [number, [number, number] | undefined][] {\n return [\n [left, cores?.[0]],\n [right, cores?.[1]],\n ];\n}\n\n/**\n * The token runs a verified match pairs, per side as file token ranges: those of a longest common\n * subsequence of what the match compared, its cores or else the whole blocks.\n */\nfunction alignPair(\n left: NormalizedBlock,\n right: NormalizedBlock,\n { cores, reordered }: Alignment & { cores?: CorePair }\n): [[number, number][], [number, number][]] {\n const leftCompared = comparedSequence(left, cores?.[0], reordered);\n const rightCompared = comparedSequence(right, cores?.[1], reordered);\n // Of the equally long subsequences, which one is marked depends on which sequence comes first,\n // so the sides are taken in the order of their content rather than of their files.\n if (compareSequences(contentOf(left, cores?.[0]), contentOf(right, cores?.[1])) <= 0) {\n const [leftMatched, rightMatched] = markLcs(leftCompared.sequence, rightCompared.sequence);\n return [toRuns(leftCompared.positions, leftMatched), toRuns(rightCompared.positions, rightMatched)];\n }\n const [rightMatched, leftMatched] = markLcs(rightCompared.sequence, leftCompared.sequence);\n return [toRuns(leftCompared.positions, leftMatched), toRuns(rightCompared.positions, rightMatched)];\n}\n\n/**\n * The content of a block or of a core of it, as written (see `writtenContent`), with the\n * identifiers of a core numbered within it.\n */\nfunction contentOf(block: NormalizedBlock, core: [number, number] | undefined): Float64Array {\n if (!core) {\n return block.writtenContent;\n }\n const { sequence, positions } = comparedSequence(block, core, false);\n return Float64Array.from(sequence, (symbol, index) =>\n symbol < 0 ? symbol : (block.fileTokenKeys[positions[index] ?? 0] ?? 0)\n );\n}\n\n/** The maximal runs of the matched positions, as half-open ranges. */\nfunction toRuns(positions: Int32Array, matched: Uint8Array): [number, number][] {\n const runs: [number, number][] = [];\n for (const position of positions.filter((_, index) => matched[index] === 1).toSorted()) {\n const last = runs.at(-1);\n if (last?.[1] === position) {\n last[1] = position + 1;\n } else {\n runs.push([position, position + 1]);\n }\n }\n return runs;\n}\n\n/**\n * The sequence a match compared, with the file token index of each of its symbols: a core, the\n * block with its statements in canonical order, or the block as written.\n */\nfunction comparedSequence(\n block: NormalizedBlock,\n core: [number, number] | undefined,\n reordered: boolean\n): { sequence: Int32Array; positions: Int32Array } {\n const blockStart = block.range.startTokenIndex;\n if (core) {\n return {\n sequence: anonymize(block.symbols.subarray(core[0] - blockStart, core[1] - blockStart)),\n positions: Int32Array.from({ length: core[1] - core[0] }, (_, index) => core[0] + index),\n };\n }\n if (reordered && block.canonical) {\n return {\n sequence: block.canonical.sequence,\n positions: block.canonical.offsets.map((offset) => blockStart + offset),\n };\n }\n return { sequence: block.sequence, positions: block.sequence.map((_, index) => blockStart + index) };\n}\n\n/** The unions of overlapping cores, in position order. */\nfunction mergeOverlappingCores(cores: [number, number][]): [number, number][] {\n const merged: [number, number][] = [];\n for (const [start, end] of cores.toSorted((left, right) => left[0] - right[0])) {\n const last = merged.at(-1);\n if (last && start < last[1]) {\n last[1] = Math.max(last[1], end);\n } else {\n merged.push([start, end]);\n }\n }\n return merged;\n}\n\n/** Counts the tokens of [start, end) that the spans cover. */\nfunction createReportedTokenCounter(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (start: number, end: number) => number {\n const merged = mergeOverlappingCores(spans.map((span) => [span.startTokenIndex, span.endTokenIndex]));\n let covered = 0;\n const coveredBefore = [0];\n for (const [start, end] of merged) {\n covered += end - start;\n coveredBefore.push(covered);\n }\n const countBefore = (position: number): number => {\n let low = 0;\n let high = merged.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((merged[middle]?.[0] ?? 0) < position) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return (coveredBefore[low] ?? 0) - Math.max(0, (merged[low - 1]?.[1] ?? 0) - position);\n };\n return (start, end) => countBefore(end) - countBefore(start);\n}\n\n/**\n * The block's occurrence with one segment per entry of `cores` (the whole block for `undefined`,\n * a whole match). Source offsets stay the block's: tokens carry none, and near-miss occurrences\n * report lines only.\n */\nfunction toOccurrence(\n { fileIndex, range }: NormalizedBlock,\n files: NearMissSourceFile[],\n { cores, anchor, matchedRuns }: BlockCopy\n): NearMissOccurrence {\n const whole = cores.includes(undefined);\n const segments = cores\n .map((core): [number, number] => core ?? [range.startTokenIndex, range.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0])\n .map(([startTokenIndex, endTokenIndex]) => ({ startTokenIndex, endTokenIndex }));\n const start = segments[0]?.startTokenIndex ?? range.startTokenIndex;\n const end = segments.at(-1)?.endTokenIndex ?? range.endTokenIndex;\n const tokens = files[fileIndex]?.tokens;\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments,\n matchedRuns: mergeOverlappingCores(matchedRuns).map(([startTokenIndex, endTokenIndex]): TokenSegment => ({\n startTokenIndex,\n endTokenIndex,\n })),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: whole ? range.startLine : (tokens?.[start]?.startRow ?? 0) + 1,\n endLine: whole ? range.endLine : (tokens?.[end - 1]?.endRow ?? 0) + 1,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two blocks wholly covered by reported spans and pairs whose\n * length ratio rules out both\n * whole-block similarity and `maxLengthRatio`. Blocks are\n * indexed in ascending length, so each posting list is scanned backwards only while its blocks\n * are long enough; shared counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n fullyReported: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const reportedFlags = Uint8Array.from(fullyReported, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightReported = reportedFlags[right] === 1;\n const minLeftLength = Math.min(\n Math.ceil((lengths[right] ?? 0) / maxLengthRatio),\n Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100)\n );\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightReported && reportedFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Returns the pair verifier. Content symbols are weighted by integer self-information,\n * 1 + floor(log2((N + 1) / df)) over N blocks capped at `maxContentWeight`, so rare names and\n * values (the logic a copy preserves) outweigh ubiquitous ones, following the\n * information-theoretic weighting of ECScan's essence-clone detection (2025).\n */\nfunction createMatcher(\n blocks: NormalizedBlock[],\n minTokens: number,\n minSimilarityPercent: number\n): (left: NormalizedBlock, right: NormalizedBlock, rightIndex: number) => PairMatch | undefined {\n const documentFrequencies = new Map<number, number>();\n for (const block of blocks) {\n for (const symbol of block.contentCounts.keys()) {\n documentFrequencies.set(symbol, (documentFrequencies.get(symbol) ?? 0) + 1);\n }\n }\n const selfInformation = (documentFrequency: number): number =>\n Math.min(31 - Math.clz32(Math.floor((blocks.length + 1) / documentFrequency)) + 1, maxContentWeight);\n const weights = new Map<number, number>();\n for (const [symbol, frequency] of documentFrequencies) {\n weights.set(symbol, selfInformation(frequency));\n }\n const weigh = (counts: Map<number, number>): WeightedContent => {\n const symbols = Int32Array.from(counts.keys()).toSorted();\n // Span content comes from blocks, so every symbol has a weight.\n const weightedCounts = Int32Array.from(symbols, (symbol) => (counts.get(symbol) ?? 0) * (weights.get(symbol) ?? 0));\n let total = 0;\n for (const count of weightedCounts) {\n total += count;\n }\n return { symbols, weightedCounts, total };\n };\n const blockContents = new Map(blocks.map((block) => [block, weigh(block.contentCounts)]));\n\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: NormalizedBlock, rightIndex: number, sequence: Int32Array): number => {\n if (counterBlock !== rightIndex || !counter) {\n counterBlock = rightIndex;\n counter = createLcsLengthCounter(right.sequence);\n }\n return counter(sequence);\n };\n\n /**\n * Matches the cores two blocks share inside different surroundings (a copy wrapped in added\n * code, or two copies embedded in different code), which whole-block similarity misses\n * (CCAligner's large-gap and LVMapper's large-variance clones). N-grams unique to each block\n * anchor the alignment; their longest chain increasing in both blocks (a run filter keeps only\n * anchors continuing a diagonal, but the chain may shift diagonals at small insertions), split at\n * gaps, delimits the cores, and every core pair that is a near-miss clone in its own right is\n * returned.\n */\n const matchLocally = (left: NormalizedBlock, right: NormalizedBlock): PairMatch | undefined => {\n const anchors: [number, number][] = [];\n for (\n let leftIndex = 0, rightIndex = 0;\n leftIndex < left.uniqueNgrams.length && rightIndex < right.uniqueNgrams.length;\n ) {\n const leftHash = left.uniqueNgrams[leftIndex] ?? 0;\n const rightHash = right.uniqueNgrams[rightIndex] ?? 0;\n if (leftHash === rightHash) {\n anchors.push([left.uniqueNgramOffsets[leftIndex] ?? 0, right.uniqueNgramOffsets[rightIndex] ?? 0]);\n }\n if (leftHash <= rightHash) {\n leftIndex += 1;\n }\n if (rightHash <= leftHash) {\n rightIndex += 1;\n }\n }\n anchors.sort((first, second) => first[0] - second[0]);\n // An isolated 5-gram match is often coincidental (n-grams are identifier-blind); a copied core\n // yields runs of consecutive anchors, so only anchors continuing a diagonal run are chained.\n const runAnchors = anchors.filter(([leftOffset, rightOffset], index) => {\n const previous = anchors[index - 1];\n const next = anchors[index + 1];\n return (\n (previous?.[0] === leftOffset - 1 && previous[1] === rightOffset - 1) ||\n (next?.[0] === leftOffset + 1 && next[1] === rightOffset + 1)\n );\n });\n const leftOffset = left.range.startTokenIndex;\n const rightOffset = right.range.startTokenIndex;\n const cores: (Alignment & { pair: CorePair })[] = [];\n for (const segment of chainSegments(longestIncreasingChain(runAnchors))) {\n const [leftStart, rightStart] = segment[0] ?? [0, 0];\n const [leftLast, rightLast] = segment.at(-1) ?? [0, 0];\n const leftEnd = leftLast + ngramSize;\n const rightEnd = rightLast + ngramSize;\n const leftLength = leftEnd - leftStart;\n const rightLength = rightEnd - rightStart;\n const shorter = Math.min(leftLength, rightLength);\n const required = minSimilarityPercent * Math.max(leftLength, rightLength);\n if (\n shorter < minTokens ||\n shorter * 100 < required ||\n anchoredTokenCount(segment) * 100 < minAnchorCoveragePercent * shorter ||\n !sharesContent(\n weigh(countContent(left.symbols, left.isContent, leftStart, leftEnd)),\n weigh(countContent(right.symbols, right.isContent, rightStart, rightEnd))\n )\n ) {\n continue;\n }\n const coreLcsLength = lcsLength(\n anonymize(left.symbols.subarray(leftStart, leftEnd)),\n anonymize(right.symbols.subarray(rightStart, rightEnd))\n );\n if (coreLcsLength * 100 >= required) {\n cores.push({\n pair: [\n [leftOffset + leftStart, leftOffset + leftEnd],\n [rightOffset + rightStart, rightOffset + rightEnd],\n ],\n reordered: false,\n });\n }\n }\n return cores.length > 0 ? { cores } : undefined;\n };\n\n /** Cheapest bounds first: the LCS cannot exceed the shorter block's length nor the bag overlap. */\n return (left, right, rightIndex) => {\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (\n Math.min(left.sequence.length, right.sequence.length) * 100 >= required &&\n sharesContent(blockContents.get(left), blockContents.get(right))\n ) {\n if (sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 >= required) {\n const inOrder = lcsLengthWithRight(right, rightIndex, left.sequence);\n if (inOrder * 100 >= required) {\n return { reordered: false };\n }\n }\n // With their top-level statements (each anonymized on its own) in a canonical order, a copy\n // whose independent statements were swapped still matches.\n if (left.canonical && right.canonical) {\n const reordered = lcsLength(left.canonical.sequence, right.canonical.sequence);\n if (reordered * 100 >= required) {\n return { reordered: true };\n }\n }\n }\n // Of the equally long chains of anchors, the one kept depends on which block comes first, so\n // the blocks are taken in the order of their content rather than of their files.\n if (compareSequences(right.writtenContent, left.writtenContent) < 0) {\n const swapped = matchLocally(right, left);\n return (\n swapped?.cores && {\n cores: swapped.cores.map(({ pair: [rightCore, leftCore], ...alignment }) => ({\n ...alignment,\n pair: [leftCore, rightCore],\n })),\n }\n );\n }\n return matchLocally(left, right);\n };\n}\n\n/** Content-bearing symbols, sorted, with their information-weighted counts. */\ninterface WeightedContent {\n symbols: Int32Array;\n weightedCounts: Int32Array;\n total: number;\n}\n\n/**\n * A structural match must be backed by shared content: more than `minContentSimilarityPercent` of\n * the larger side's information-weighted names and literal values. Two sides without content never\n * pass.\n */\nfunction sharesContent(left: WeightedContent | undefined, right: WeightedContent | undefined): boolean {\n if (!left || !right) {\n return false;\n }\n let overlap = 0;\n for (let leftIndex = 0, rightIndex = 0; leftIndex < left.symbols.length && rightIndex < right.symbols.length;) {\n const leftSymbol = left.symbols[leftIndex] ?? 0;\n const rightSymbol = right.symbols[rightIndex] ?? 0;\n if (leftSymbol === rightSymbol) {\n overlap += Math.min(left.weightedCounts[leftIndex] ?? 0, right.weightedCounts[rightIndex] ?? 0);\n }\n if (leftSymbol <= rightSymbol) {\n leftIndex += 1;\n }\n if (rightSymbol <= leftSymbol) {\n rightIndex += 1;\n }\n }\n return overlap * 100 > minContentSimilarityPercent * Math.max(left.total, right.total);\n}\n\n/**\n * The block's units (its top-level statements, given in file token indexes, and the token runs\n * between them), each anonymized on its own and sorted, concatenated, with the block offset of\n * every token; undefined with too few statements. `tokenKeys` holds the content key of each\n * non-identifier token of the block.\n */\nfunction canonicalSequenceOf(\n symbols: Int32Array,\n tokenKeys: Float64Array,\n statements: [number, number][],\n blockStart: number\n): NormalizedBlock['canonical'] {\n if (statements.length < minReorderStatementCount) {\n return undefined;\n }\n const units: { unit: Int32Array; content: Float64Array; start: number }[] = [];\n const pushUnit = (start: number, end: number): void => {\n const unit = anonymize(symbols.subarray(start, end));\n // Ordered by the content of their tokens: symbols, numbered by first occurrence, would order\n // the units by where other code stands.\n const content = Float64Array.from(unit, (symbol, index) => (symbol < 0 ? symbol : (tokenKeys[start + index] ?? 0)));\n units.push({ unit, content, start });\n };\n let cursor = 0;\n for (const [statementStart, statementEnd] of statements) {\n const start = statementStart - blockStart;\n if (cursor < start) {\n pushUnit(cursor, start);\n }\n pushUnit(start, statementEnd - blockStart);\n cursor = statementEnd - blockStart;\n }\n if (cursor < symbols.length) {\n pushUnit(cursor, symbols.length);\n }\n units.sort((left, right) => compareSequences(left.content, right.content) || left.start - right.start);\n const sequence = new Int32Array(symbols.length);\n const offsets = new Int32Array(symbols.length);\n let position = 0;\n for (const { unit, start } of units) {\n sequence.set(unit, position);\n for (let index = 0; index < unit.length; index += 1) {\n offsets[position + index] = start + index;\n }\n position += unit.length;\n }\n return { sequence, offsets };\n}\n\n/** Lexicographic order. */\nfunction compareSequences(left: Float64Array, right: Float64Array): number {\n for (let index = 0; index < Math.min(left.length, right.length); index += 1) {\n const difference = (left[index] ?? 0) - (right[index] ?? 0);\n if (difference !== 0) {\n return difference;\n }\n }\n return left.length - right.length;\n}\n\n/**\n * The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),\n * via patience sorting over right offsets.\n */\nfunction longestIncreasingChain(anchors: [number, number][]): [number, number][] {\n const tailIndexes: number[] = [];\n const predecessors: number[] = [];\n for (const [index, [, rightOffset]] of anchors.entries()) {\n let low = 0;\n let high = tailIndexes.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((anchors[tailIndexes[middle] ?? 0]?.[1] ?? 0) < rightOffset) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n predecessors.push(low > 0 ? (tailIndexes[low - 1] ?? -1) : -1);\n tailIndexes[low] = index;\n }\n const chain: [number, number][] = [];\n for (let cursor = tailIndexes.at(-1) ?? -1; cursor >= 0; cursor = predecessors[cursor] ?? -1) {\n const anchor = anchors[cursor];\n if (anchor) {\n chain.push(anchor);\n }\n }\n return chain.toReversed();\n}\n\n/** The chain's segments, split where consecutive anchors lie more than `maxAnchorGapTokens` apart in either block. */\nfunction chainSegments(chain: [number, number][]): [number, number][][] {\n const segments: [number, number][][] = [];\n for (const [index, anchor] of chain.entries()) {\n const previous = chain[index - 1];\n const continues =\n previous !== undefined &&\n anchor[0] - (previous[0] + ngramSize) <= maxAnchorGapTokens &&\n anchor[1] - (previous[1] + ngramSize) <= maxAnchorGapTokens;\n if (continues) {\n segments.at(-1)?.push(anchor);\n } else {\n segments.push([anchor]);\n }\n }\n return segments;\n}\n\n/** Left-block tokens the segment's anchors cover (overlapping anchors count once). */\nfunction anchoredTokenCount(segment: [number, number][]): number {\n let count = ngramSize;\n for (let index = 1; index < segment.length; index += 1) {\n count += Math.min((segment[index]?.[0] ?? 0) - (segment[index - 1]?.[0] ?? 0), ngramSize);\n }\n return count;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector. Non-identifier symbols are interned\n * project-wide from the tokens' hash pairs, so equal tokens compare equal across files, while\n * identifiers are interned per file and re-anonymized per compared range.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, containerStatements, nearMissBlocks }] of files.entries()) {\n if (!tokens || !nearMissBlocks?.length) {\n continue;\n }\n const symbols = new Int32Array(tokens.length);\n const isContent = new Uint8Array(tokens.length);\n const fileTokenKeys = new Float64Array(tokens.length);\n const idByIdentifier = new Map<string, number>();\n for (const [index, token] of tokens.entries()) {\n if (token.kind === 'id') {\n let id = idByIdentifier.get(token.text);\n if (id === undefined) {\n id = idByIdentifier.size;\n idByIdentifier.set(token.text, id);\n }\n symbols[index] = -(id + 1);\n continue;\n }\n const key = tokenKey(token);\n fileTokenKeys[index] = key;\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n symbols[index] = symbol;\n isContent[index] = token.isName || token.literalHash !== undefined ? 1 : 0;\n }\n const findStatements = createTopLevelStatementFinder(containerStatements ?? []);\n for (const range of nearMissBlocks) {\n const { startTokenIndex: start, endTokenIndex: end } = range;\n const blockSymbols = symbols.subarray(start, end);\n const blockIsContent = isContent.subarray(start, end);\n const sequence = anonymize(blockSymbols);\n const ngramHashes = collectNgramHashes(blockSymbols, fileTokenKeys.subarray(start, end));\n const occurrenceCounts = new Map<number, number>();\n for (const hash of ngramHashes) {\n occurrenceCounts.set(hash, (occurrenceCounts.get(hash) ?? 0) + 1);\n }\n const uniqueOffsets = ngramHashes\n .keys()\n .filter((offset) => occurrenceCounts.get(ngramHashes[offset] ?? 0) === 1)\n .toArray()\n .toSorted((first, second) => (ngramHashes[first] ?? 0) - (ngramHashes[second] ?? 0));\n blocks.push({\n fileIndex,\n range,\n symbols: blockSymbols,\n isContent: blockIsContent,\n fileTokenKeys,\n sequence,\n writtenContent: Float64Array.from(sequence, (symbol, index) =>\n symbol < 0 ? symbol : (fileTokenKeys[start + index] ?? 0)\n ),\n sortedSequence: sequence.toSorted(),\n ngrams: Int32Array.from(occurrenceCounts.keys()),\n uniqueNgrams: Int32Array.from(uniqueOffsets, (offset) => ngramHashes[offset] ?? 0),\n uniqueNgramOffsets: Int32Array.from(uniqueOffsets),\n contentCounts: countContent(blockSymbols, blockIsContent, 0, blockSymbols.length),\n canonical: canonicalSequenceOf(\n blockSymbols,\n fileTokenKeys.subarray(start, end),\n findStatements(start, end),\n start\n ),\n });\n }\n }\n return blocks;\n}\n\n/**\n * Returns a lookup of the outermost container statements inside a token range, excluding a\n * statement spanning the whole range (the block itself).\n */\nfunction createTopLevelStatementFinder(\n containerStatements: TokenRange[][]\n): (start: number, end: number) => [number, number][] {\n const statements = containerStatements\n .flat()\n .filter((statement) => statement.startTokenIndex < statement.endTokenIndex)\n .map((statement): [number, number] => [statement.startTokenIndex, statement.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0] || right[1] - left[1]);\n return (start, end) => {\n let low = 0;\n let high = statements.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((statements[middle]?.[0] ?? 0) < start) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const topLevel: [number, number][] = [];\n for (let index = low; index < statements.length; index += 1) {\n const statement = statements[index];\n if (!statement || statement[0] >= end) {\n break;\n }\n const last = topLevel.at(-1);\n const nested = last !== undefined && statement[0] < last[1];\n if (statement[1] <= end && !(statement[0] === start && statement[1] === end) && !nested) {\n topLevel.push(statement);\n }\n }\n return topLevel;\n };\n}\n\nfunction countContent(symbols: Int32Array, isContent: Uint8Array, start: number, end: number): Map<number, number> {\n const counts = new Map<number, number>();\n for (let offset = start; offset < end; offset += 1) {\n if (isContent[offset] === 1) {\n const symbol = symbols[offset] ?? 0;\n counts.set(symbol, (counts.get(symbol) ?? 0) + 1);\n }\n }\n return counts;\n}\n\n/** Identifiers renumbered by first occurrence within `symbols`, so a range compares the same wherever it sits in its file. */\nfunction anonymize(symbols: Int32Array): Int32Array {\n const indexByIdentifier = new Map<number, number>();\n return symbols.map((symbol) => {\n if (symbol >= 0) {\n return symbol;\n }\n let index = indexByIdentifier.get(symbol);\n if (index === undefined) {\n index = indexByIdentifier.size;\n indexByIdentifier.set(symbol, index);\n }\n return -(index + 1);\n });\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\n/**\n * N-gram hash per start offset, identifier-blind (every identifier hashes as -1) so a block copied\n * into different surroundings (renumbering its identifiers) or with reordered statements still\n * shares its n-grams. Hashed from the content keys of the tokens: with symbols, numbered by first\n * occurrence, which n-grams collide would depend on the order of the files.\n */\nfunction collectNgramHashes(symbols: Int32Array, tokenKeys: Float64Array): Int32Array {\n const hashes = new Int32Array(Math.max(symbols.length - ngramSize + 1, 0));\n for (let start = 0; start < hashes.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n const tokenKey = tokenKeys[start + offset] ?? 0;\n // A token key holds 53 bits; both halves enter the hash.\n const value = (symbols[start + offset] ?? 0) < 0 ? -1 : tokenKey ^ Math.floor(tokenKey / 0x1_00_00_00_00);\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32.\n hash = (Math.imul(hash, 31) + value) | 0;\n }\n hashes[start] = hash;\n }\n return hashes;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(s)),g=m;for(;g.length>0;){let e=[];for(let t of g){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${_(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:f(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}g=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(s(n))&&d(r,n.length)&&p(n)&&(h.add(s(n)),g.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>v(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=S(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?S(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=d(e);return i}}function d(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function f(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const p=[],m=[];function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e){let t=m[e];return t===void 0&&(t=x(`$${e}`),m[e]=t),t}function _(e,t,n,r){let[a,o]=y(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function v(e,t,n){let[r,i]=y(e,t,n,!1);return r^Math.imul(i,31)}function y(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=h(e),c=g(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function b(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function x(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function S(e,t){return Math.imul(e,31)+t}function C(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(E);for(let r=!0;r;){r=!1;let i=w(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=D(i,s,t,n),l=c??D(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(E),r=!0;break}}return e}function w(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=T(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function T(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function E(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function D(e,t,n,r){let[i,a]=O(e),[o,s]=O(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function O(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function k(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function A(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}exports.buildLiteralCountPrefix=a,exports.collectSegmentLines=A,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=k,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.mergeAdjacentGroups=C,exports.resolveDuplicationOptions=n;
1
+ "use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},f=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},p=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!f({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||p.push({containerIndex:e,start:n,length:i})}let m=new Set(p.map(s)),h=p;for(;h.length>0;){let e=[];for(let t of h){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${x(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:g(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}h=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!m.has(s(n))&&d(r,n.length)&&f(n)&&(m.add(s(n)),h.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>S(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=E(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?E(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=d(e);return e=>{let n=0;for(let r of t(e))n+=h(r);return n}}function d(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}return r}}function f(e,t){let n=new Uint8Array(e.length),r=new Uint8Array(t.length);return p(e,t,n,r),[n,r]}function p(e,t,n,r){let i=0;for(;i<e.length&&i<t.length&&e[i]===t[i];)i+=1;let a=0;for(;a<e.length-i&&a<t.length-i&&e[e.length-1-a]===t[t.length-1-a];)a+=1;let o=e.length-a,s=t.length-a;n.fill(1,0,i).fill(1,o),r.fill(1,0,i).fill(1,s);let c=e.subarray(i,o),l=t.subarray(i,s),u=n.subarray(i,o),d=r.subarray(i,s);if(c.length===0||l.length===0)return;if(c.length===1){let e=l.indexOf(c[0]??0);e!==-1&&(u[0]=1,d[e]=1);return}let f=c.length>>>1,h=m(c.subarray(0,f),l),g=m(c.subarray(f).toReversed(),l.toReversed()),_=0,v=-1;for(let e=0;e<=l.length;e+=1){let t=(h[e]??0)+(g[l.length-e]??0);t>v&&(v=t,_=e)}p(c.subarray(0,f),l.subarray(0,_),u.subarray(0,f),d.subarray(0,_)),p(c.subarray(f),l.subarray(_),u.subarray(f),d.subarray(_))}function m(e,t){let n=d(t)(e),r=new Int32Array(t.length+1);for(let e=0;e<t.length;e+=1)r[e+1]=(r[e]??0)+((n[e>>>5]??0)>>>(e&31)&1);return r}function h(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function g(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const _=[],v=[];function y(e){let t=_[e];return t===void 0&&(t=w(`$${e}`),_[e]=t),t}function b(e){let t=v[e];return t===void 0&&(t=T(`$${e}`),v[e]=t),t}function x(e,t,n,r){let[a,o]=C(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function S(e,t,n){let[r,i]=C(e,t,n,!1);return r^Math.imul(i,31)}function C(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=y(e),c=b(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function w(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function T(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function E(e,t){return Math.imul(e,31)+t}function D(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(A);for(let r=!0;r;){r=!1;let i=O(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=j(i,s,t,n),l=c??j(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(A),r=!0;break}}return e}function O(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=k(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function k(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function A(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function j(e,t,n,r){let[i,a]=M(e),[o,s]=M(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,matchedRuns:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function M(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function N(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function P(e,t,n,r,i=[]){let a=/* @__PURE__ */ new Set;for(let e of i)for(let t=e.startTokenIndex;t<e.endTokenIndex;t+=1){let e=n[t];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)a.add(t+1)}let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=0;for(let r of e)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){for(;t&&(t[c]?.endTokenIndex??1/0)<=e;)c+=1;let r=!t||(t[c]?.startTokenIndex??1/0)<=e,i=n[e];for(let e=i?.startRow??0;e<=(i?.endRow??-1);e+=1)s.set(e+1,(s.get(e+1)??0)+1),o.set(e+1,(o.get(e+1)??0)+ +!!r)}let l=/* @__PURE__ */ new Set;for(let[e,t]of s)(!r||r.has(e))&&((o.get(e)??0)*2>t||a.has(e))&&l.add(e);return l}exports.buildLiteralCountPrefix=a,exports.collectMatchedLines=P,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=N,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.markLcs=f,exports.mergeAdjacentGroups=D,exports.resolveDuplicationOptions=n;
2
2
  //# sourceMappingURL=duplication.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplication.cjs","names":["clamp"],"sources":["../src/duplication.ts"],"sourcesContent":["import { clamp } from '@willbooster/shared-lib';\nimport type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value) ? fallback : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\ninterface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /** Matched token runs; more than one once gapped groups are merged. */\n segments: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n\n let length = 0;\n for (const word of v) {\n length += popCount(word);\n }\n return length;\n };\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/** Adds the 1-based code lines the segment's matched tokens cover; shared with cross-file coverage. */\nexport function collectSegmentLines(\n segment: { startTokenIndex: number; endTokenIndex: number },\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n duplicatedLines: Set<number>\n): void {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (!codeLineNumbers || codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplication.cjs","names":["clamp"],"sources":["../src/duplication.ts"],"sourcesContent":["import { clamp } from '@willbooster/shared-lib';\nimport type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value) ? fallback : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\nexport interface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /**\n * The token runs the occurrence covers, matched throughout except in a near-miss copy (see\n * `matchedRuns`); more than one once gapped groups are merged.\n */\n segments: TokenSegment[];\n /**\n * The token runs of a near-miss copy that its partners match, within its segments;\n * absent for an exact or gapped copy, whose segments are matched throughout.\n */\n matchedRuns?: TokenSegment[];\n /**\n * The exact fragments coalesced into a near-miss copy: each line they reach is duplicated,\n * whatever else of the copy shares the line.\n */\n exactSegments?: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const growthBits = createLcsGrowthBits(a);\n return (b) => {\n let length = 0;\n for (const word of growthBits(b)) {\n length += popCount(word);\n }\n return length;\n };\n}\n\n/**\n * Per sequence `b`, one bit per position of `a`, set where the longest common subsequence of `b`\n * with the prefix of `a` ending there is longer than with the prefix before it, so the set bits up\n * to a position count that length. The returned words are overwritten by the next call.\n */\nfunction createLcsGrowthBits(a: Int32Array): (b: Int32Array) => Uint32Array {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n return v;\n };\n}\n\n/**\n * Which symbols of each sequence a longest common subsequence pairs, by Hirschberg's divide and\n * conquer over the bit-parallel recurrence, which alone yields lengths but no pairing.\n */\nexport function markLcs(a: Int32Array, b: Int32Array): [Uint8Array, Uint8Array] {\n const aMatched = new Uint8Array(a.length);\n const bMatched = new Uint8Array(b.length);\n markLcsInto(a, b, aMatched, bMatched);\n return [aMatched, bMatched];\n}\n\nfunction markLcsInto(a: Int32Array, b: Int32Array, aMatched: Uint8Array, bMatched: Uint8Array): void {\n // A common prefix or suffix belongs to some longest common subsequence.\n let prefix = 0;\n while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) {\n prefix += 1;\n }\n let suffix = 0;\n while (\n suffix < a.length - prefix &&\n suffix < b.length - prefix &&\n a[a.length - 1 - suffix] === b[b.length - 1 - suffix]\n ) {\n suffix += 1;\n }\n const aEnd = a.length - suffix;\n const bEnd = b.length - suffix;\n aMatched.fill(1, 0, prefix).fill(1, aEnd);\n bMatched.fill(1, 0, prefix).fill(1, bEnd);\n const aRest = a.subarray(prefix, aEnd);\n const bRest = b.subarray(prefix, bEnd);\n const aRestMatched = aMatched.subarray(prefix, aEnd);\n const bRestMatched = bMatched.subarray(prefix, bEnd);\n if (aRest.length === 0 || bRest.length === 0) {\n return;\n }\n if (aRest.length === 1) {\n const position = bRest.indexOf(aRest[0] ?? 0);\n if (position !== -1) {\n aRestMatched[0] = 1;\n bRestMatched[position] = 1;\n }\n return;\n }\n const middle = aRest.length >>> 1;\n const forward = lcsPrefixLengths(aRest.subarray(0, middle), bRest);\n const backward = lcsPrefixLengths(aRest.subarray(middle).toReversed(), bRest.toReversed());\n let split = 0;\n let best = -1;\n for (let candidate = 0; candidate <= bRest.length; candidate += 1) {\n const length = (forward[candidate] ?? 0) + (backward[bRest.length - candidate] ?? 0);\n if (length > best) {\n best = length;\n split = candidate;\n }\n }\n markLcsInto(\n aRest.subarray(0, middle),\n bRest.subarray(0, split),\n aRestMatched.subarray(0, middle),\n bRestMatched.subarray(0, split)\n );\n markLcsInto(\n aRest.subarray(middle),\n bRest.subarray(split),\n aRestMatched.subarray(middle),\n bRestMatched.subarray(split)\n );\n}\n\n/** The longest-common-subsequence length of `a` with every prefix of `b`. */\nfunction lcsPrefixLengths(a: Int32Array, b: Int32Array): Int32Array {\n const bits = createLcsGrowthBits(b)(a);\n const lengths = new Int32Array(b.length + 1);\n for (let index = 0; index < b.length; index += 1) {\n lengths[index + 1] = (lengths[index] ?? 0) + (((bits[index >>> 5] ?? 0) >>> (index & 31)) & 1);\n }\n return lengths;\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks, and\n // its segments are matched throughout.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n matchedRuns: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/**\n * The 1-based code lines the occurrence's matched tokens cover, given its segments and matched runs\n * as indexes into `tokens`. A line of a near-miss copy counts when its partners match more than half\n * of its tokens: a line the copy adds or rewrites is not duplicated, while a longest common\n * subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.\n * A line one of `exactSegments` reaches counts in any case.\n */\nexport function collectMatchedLines(\n segments: TokenSegment[],\n matchedRuns: TokenSegment[] | undefined,\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n exactSegments: TokenSegment[] = []\n): Set<number> {\n const exactLines = new Set<number>();\n for (const segment of exactSegments) {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n exactLines.add(row + 1);\n }\n }\n }\n const matchedCountByLine = new Map<number, number>();\n const tokenCountByLine = new Map<number, number>();\n let runIndex = 0;\n for (const segment of segments) {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n while (matchedRuns && (matchedRuns[runIndex]?.endTokenIndex ?? Infinity) <= index) {\n runIndex += 1;\n }\n const matched = !matchedRuns || (matchedRuns[runIndex]?.startTokenIndex ?? Infinity) <= index;\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n tokenCountByLine.set(row + 1, (tokenCountByLine.get(row + 1) ?? 0) + 1);\n matchedCountByLine.set(row + 1, (matchedCountByLine.get(row + 1) ?? 0) + (matched ? 1 : 0));\n }\n }\n }\n const lines = new Set<number>();\n for (const [line, tokenCount] of tokenCountByLine) {\n if (\n (!codeLineNumbers || codeLineNumbers.has(line)) &&\n ((matchedCountByLine.get(line) ?? 0) * 2 > tokenCount || exactLines.has(line))\n ) {\n lines.add(line);\n }\n }\n return lines;\n}\n"],"mappings":"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"}