code-gauge 10.2.2 → 10.2.4

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 +2 -2
  2. package/THIRD-PARTY-NOTICES.txt +116 -74
  3. package/dist/checkCommand.cjs.map +1 -1
  4. package/dist/checkCommand.js.map +1 -1
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cliConfig.cjs.map +1 -1
  8. package/dist/cliConfig.js.map +1 -1
  9. package/dist/crossFileDuplication.cjs.map +1 -1
  10. package/dist/crossFileDuplication.js.map +1 -1
  11. package/dist/crossFileNearMiss.cjs.map +1 -1
  12. package/dist/crossFileNearMiss.js.map +1 -1
  13. package/dist/duplicateSelection.cjs.map +1 -1
  14. package/dist/duplicateSelection.js.map +1 -1
  15. package/dist/duplication.cjs.map +1 -1
  16. package/dist/duplication.js.map +1 -1
  17. package/dist/exclusion.cjs.map +1 -1
  18. package/dist/exclusion.js.map +1 -1
  19. package/dist/git.cjs.map +1 -1
  20. package/dist/git.js.map +1 -1
  21. package/dist/languages.cjs.map +1 -1
  22. package/dist/languages.js.map +1 -1
  23. package/dist/metrics.cjs.map +1 -1
  24. package/dist/metrics.js.map +1 -1
  25. package/dist/nativeMetrics.cjs.map +1 -1
  26. package/dist/nativeMetrics.js.map +1 -1
  27. package/dist/scan.cjs.map +1 -1
  28. package/dist/scan.js.map +1 -1
  29. package/dist/thresholdCheck.cjs.map +1 -1
  30. package/dist/thresholdCheck.js.map +1 -1
  31. package/dist/thresholds.cjs.map +1 -1
  32. package/dist/thresholds.js.map +1 -1
  33. package/dist/wasmBinding.cjs +1 -1
  34. package/dist/wasmBinding.cjs.map +1 -1
  35. package/dist/wasmBinding.js +1 -1
  36. package/dist/wasmBinding.js.map +1 -1
  37. package/dist/worker.cjs.map +1 -1
  38. package/dist/worker.js.map +1 -1
  39. package/native/Cargo.lock +58 -58
  40. package/native/Cargo.toml +15 -13
  41. package/native/code-gauge.wasm +0 -0
  42. package/package.json +14 -14
@@ -1 +1 @@
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 `listedSpansByFile` has nothing to add to them and is dropped before clustering\n * (two blocks wholly covered by those 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. Those are the\n * reported spans without the groups that hold nested copies: such a group lists a part of larger\n * copies with a copy standing elsewhere, and the blocks around its copies may be copies as wholes,\n * which only their pair can list at that size. 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: TokenSegment[][],\n listedSpansByFile: TokenSegment[][],\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 countListed = listedSpansByFile.map(createReportedTokenCounter);\n const unlistedCount = (fileIndex: number, runs: [number, number][]): number =>\n runs.reduce((sum, [start, end]) => sum + end - start - (countListed[fileIndex]?.(start, end) ?? 0), 0);\n const fullyListed = blocks.map((block) => unlistedCount(block.fileIndex, [spanOf(block)]) === 0);\n const edges: MatchEdge[] = [];\n forEachCandidatePair(blocks, fullyListed, 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 unlistedCount(leftBlock.fileIndex, leftRuns) + unlistedCount(rightBlock.fileIndex, rightRuns) > 0;\n // Fully listed 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(spans: TokenSegment[]): (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 listed 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 fullyListed: 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 listedFlags = Uint8Array.from(fullyListed, 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 rightListed = listedFlags[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 || (rightListed && listedFlags[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
+ {"version":3,"file":"crossFileNearMiss.js","names":["collectCrossFileNearMissGroups","minSimilarityPercent","blocks","normalizeBlocks","files","matcher","createMatcher","minTokens","countReported","reportedSpansByFile","createReportedTokenCounter","reportedCount","start","end","fileIndex","countListed","listedSpansByFile","unlistedCount","runs","sum","fullyListed","block","spanOf","edges","forEachCandidatePair","leftBlock","left","rightBlock","right","match","cores","leftSpan","rightSpan","addsLines","leftRuns","rightRuns","matchedRuns","alignPair","matchedWhole","localCores","edge","index","core","sidesOf","nodes","firstNodeByBlock","blockIndex","mergeOverlappingCores","nodeOf","first","node","span","parent","find","root","current","next","nodeRanges","startTokenIndex","endTokenIndex","anchored","reported","range","edgeNodes","leftNode","rightNode","leftRoot","rightRoot","matchedRunsByNode","edgeIndex","membersByRoot","members","groups","coresByBlock","entry","copy","toOccurrence","leftCompared","comparedSequence","reordered","rightCompared","compareSequences","contentOf","leftMatched","rightMatched","markLcs","toRuns","sequence","symbol","positions","position","matched","last","blockStart","anonymize","offset","merged","spans","covered","coveredBefore","countBefore","low","high","middle","whole","segments","tokens","anchor","segment","blockFrequency","ngram","fileIndexes","listedFlags","lengths","ngramCounts","order","postings","sharedCounts","touched","rightListed","minLeftLength","ngrams","posting","shared","visit","documentFrequencies","selfInformation","documentFrequency","weights","frequency","weigh","symbols","counts","weightedCounts","total","count","blockContents","counterBlock","counter","lcsLengthWithRight","rightIndex","createLcsLengthCounter","matchLocally","anchors","leftIndex","leftHash","rightHash","second","runAnchors","leftOffset","rightOffset","previous","chainSegments","longestIncreasingChain","leftStart","rightStart","leftLast","rightLast","leftEnd","rightEnd","leftLength","rightLength","shorter","required","anchoredTokenCount","sharesContent","countContent","lcsLength","sortedOverlap","swapped","rightCore","leftCore","alignment","overlap","leftSymbol","rightSymbol","canonicalSequenceOf","statements","units","pushUnit","unit","content","tokenKeys","cursor","statementStart","statementEnd","offsets","difference","tailIndexes","predecessors","chain","leftValue","rightValue","symbolByTokenKey","nearMissBlocks","isContent","fileTokenKeys","idByIdentifier","token","id","key","tokenKey","findStatements","createTopLevelStatementFinder","containerStatements","blockSymbols","blockIsContent","ngramHashes","collectNgramHashes","occurrenceCounts","hash","uniqueOffsets","statement","topLevel","nested","indexByIdentifier","primary","secondary","hashes","value"],"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 `listedSpansByFile` has nothing to add to them and is dropped before clustering\n * (two blocks wholly covered by those 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. Those are the\n * reported spans without the groups that hold nested copies: such a group lists a part of larger\n * copies with a copy standing elsewhere, and the blocks around its copies may be copies as wholes,\n * which only their pair can list at that size. 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: TokenSegment[][],\n listedSpansByFile: TokenSegment[][],\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 countListed = listedSpansByFile.map(createReportedTokenCounter);\n const unlistedCount = (fileIndex: number, runs: [number, number][]): number =>\n runs.reduce((sum, [start, end]) => sum + end - start - (countListed[fileIndex]?.(start, end) ?? 0), 0);\n const fullyListed = blocks.map((block) => unlistedCount(block.fileIndex, [spanOf(block)]) === 0);\n const edges: MatchEdge[] = [];\n forEachCandidatePair(blocks, fullyListed, 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 unlistedCount(leftBlock.fileIndex, leftRuns) + unlistedCount(rightBlock.fileIndex, rightRuns) > 0;\n // Fully listed 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(spans: TokenSegment[]): (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 listed 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 fullyListed: 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 listedFlags = Uint8Array.from(fullyListed, 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 rightListed = listedFlags[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 || (rightListed && listedFlags[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":"sFAuJA,SAAgBA,EACd,EACA,EACA,EACA,EACA,EACwB,CACxB,GAAIC,GAAwB,IAC1B,MAAO,CAAC,EAEV,IAAMC,EAASC,EAAgBC,CAAK,EAC9BC,EAAUC,EAAcJ,EAAQK,EAAWN,CAAoB,EAC/DO,EAAgBC,EAAoB,IAAIC,CAA0B,EAClEC,GAAiB,EAAmB,CAACC,EAAOC,KAChDL,EAAcM,EAAU,GAAGF,EAAOC,CAAG,GAAK,EACtCE,EAAcC,EAAkB,IAAIN,CAA0B,EAC9DO,GAAiB,EAAmB,IACxCC,EAAK,QAAQ,EAAK,CAACN,EAAOC,KAASM,EAAMN,EAAMD,GAASG,EAAYD,EAAU,GAAGF,EAAOC,CAAG,GAAK,GAAI,CAAC,EACjGO,EAAclB,EAAO,IAAK,GAAUe,EAAcI,EAAM,UAAW,CAACC,EAAOD,CAAK,CAAC,CAAC,IAAM,CAAC,EACzFE,EAAqB,CAAC,EAC5BC,EAAqBtB,EAAQkB,EAAanB,GAAuB,EAAM,IAAU,CAC/E,IAAMwB,EAAYvB,EAAOwB,GACnBC,EAAazB,EAAO0B,GACpBC,EAAQJ,GAAaE,GAActB,EAAQoB,EAAWE,EAAYC,CAAK,EAC7E,GAAKC,EAOL,IAAK,GAAM,CAAE,KAAMC,EAAO,eAAeD,EAAM,OAAS,CAAC,CAAE,KAAM,IAAA,GAAW,UAAWA,EAAM,SAAU,CAAC,EAAG,CACzG,GAAM,CAACE,EAAUC,GAAaF,GAAS,CAACR,EAAOG,CAAS,EAAGH,EAAOK,CAAU,CAAC,EACvEM,GAAa,EAA8B,IAC/ChB,EAAcQ,EAAU,UAAWS,CAAQ,EAAIjB,EAAcU,EAAW,UAAWQ,CAAS,EAAI,EAElG,GAAI,CAACF,EAAU,CAACF,CAAQ,EAAG,CAACC,CAAS,CAAC,EACpC,SAEF,IAAMI,EAAcC,EAAUZ,EAAWE,EAAY,CAAE,QAAO,WAAU,CAAC,EACrEM,EAAU,GAAGG,CAAW,GAC1Bb,EAAM,KAAK,CAAE,OAAM,QAAO,QAAO,aAAY,CAAC,CAElD,CACF,CAAC,EAKD,IAAMe,EAAepC,EAAO,QAAU,EAAK,EACrCqC,EAAarC,EAAO,QAA8B,CAAC,CAAC,EAC1D,IAAK,IAAMsC,KAAQjB,EACjB,IAAK,GAAM,CAACkB,EAAOC,KAASC,EAAQH,CAAI,EAClCE,EACFH,EAAWE,EAAM,EAAE,KAAKC,CAAI,EAE5B,EAAaD,GAAS,GAI5B,IAAMG,EAAsE,CAAC,EACvEC,EAA6B,CAAC,EACpC,IAAK,IAAMC,KAAc5C,EAAO,KAAK,EAAG,CACtC2C,EAAiB,KAAKD,EAAM,MAAM,EAClC,IAAMd,EAAQQ,EAAaQ,GAAc,CAAC,EAAIC,EAAsBR,EAAWO,IAAe,CAAC,CAAC,EAC5FhB,EAAM,SAAW,GACnBc,EAAM,KAAK,CAAE,aAAY,KAAM,IAAA,EAAU,CAAC,EAE5C,IAAK,IAAMF,KAAQZ,EACjBc,EAAM,KAAK,CAAE,aAAY,MAAK,CAAC,CAEnC,CACA,IAAMI,GAAU,EAAoB,IAA+C,CACjF,IAAMC,EAAQJ,EAAiBC,IAAe,EAC9C,GAAI,CAACJ,GAAQJ,EAAaQ,GACxB,OAAOG,EAET,IAAK,IAAIC,EAAOD,EAAOL,EAAMM,EAAK,EAAE,aAAeJ,EAAY,GAAQ,EAAG,CACxE,IAAMK,EAAOP,EAAMM,EAAK,EAAE,KAC1B,GAAIC,GAAQA,EAAK,IAAMT,EAAK,IAAMA,EAAK,IAAMS,EAAK,GAChD,OAAOD,CAEX,CACA,MAAU,MAAM,0DAA0D,CAC5E,EAEME,EAASR,EAAM,KAAK,EAAG,IAAUH,CAAK,EACtCY,EAAQ,GAA0B,CACtC,IAAIC,EAAOb,EACX,KAAOW,EAAOE,KAAUA,GACtB,EAAOF,EAAOE,IAASA,EAEzB,IAAK,IAAIC,EAAUd,EAAOW,EAAOG,KAAaD,GAAO,CACnD,IAAME,EAAOJ,EAAOG,IAAYD,EAChC,EAAOC,GAAWD,EAClB,EAAUE,CACZ,CACA,OAAOF,CACT,EAGMG,EAAab,EAAM,KAAK,CAAE,aAAY,UAAW,CACrD,IAAMvB,EAAQnB,EAAO4C,GACf,CAACY,EAAiBC,GAAiBjB,GAAQ,CAC/CrB,GAAO,MAAM,iBAAmB,EAChCA,GAAO,MAAM,eAAiB,CAChC,EACA,MAAO,CACL,OAAQsC,EAAgBD,EACxB,SAAU/C,EAAcU,GAAO,WAAa,EAAG,CAACqC,EAAiBC,CAAa,CAAC,CACjF,CACF,CAAC,EACKC,EAAWH,EAAW,KAAK,CAAE,cAAeI,EAAW,CAAC,EACxDA,EAAWJ,EAAW,IAAK,GAAUK,EAAM,WAAaA,EAAM,MAAM,EACpEC,EAAYxC,EAAM,IAAK,GAASoB,EAAQH,CAAI,CAAC,CAAC,KAAK,CAACC,EAAOC,KAAUM,EAAOP,EAAOC,CAAI,CAAC,CAAC,EAC/F,IAAK,GAAM,CAACsB,EAAW,EAAGC,EAAY,KAAMF,EAAW,CACrD,IAAMG,EAAWb,EAAKW,CAAQ,EACxBG,EAAYd,EAAKY,CAAS,EAChC,EAAO,KAAK,IAAIC,EAAUC,CAAS,GAAK,KAAK,IAAID,EAAUC,CAAS,CACtE,CACA,IAAMC,EAAoBxB,EAAM,QAA8B,CAAC,CAAC,EAChE,IAAK,GAAM,CAACyB,EAAW,CAAE,kBAAkB9C,EAAM,QAAQ,EAAG,CAC1D,GAAM,CAACyC,EAAW,EAAGC,EAAY,GAAKF,EAAUM,IAAc,CAAC,EAC/DD,EAAkBJ,EAAS,EAAE,KAAK,GAAG5B,EAAY,EAAE,EACnDgC,EAAkBH,EAAU,EAAE,KAAK,GAAG7B,EAAY,EAAE,CACtD,CAEA,IAAMkC,kBAAgB,IAAI,IAC1B,IAAK,IAAMpB,KAAQN,EAAM,KAAK,EAAG,CAC/B,IAAMU,EAAOD,EAAKH,CAAI,EAChBqB,EAAUD,EAAc,IAAIhB,CAAI,GAAK,CAAC,EAC5CiB,EAAQ,KAAKrB,CAAI,EACjBoB,EAAc,IAAIhB,EAAMiB,CAAO,CACjC,CACA,IAAMC,EAAiC,CAAC,EACxC,IAAK,IAAMD,KAAWD,EAAc,OAAO,EAAG,CAE5C,GAAIC,EAAQ,OAAS,GAAKA,EAAQ,MAAO,GAASV,EAASX,EAAK,EAC9D,SAIF,IAAMuB,kBAAe,IAAI,IACzB,IAAK,IAAMvB,KAAQqB,EAAS,CAC1B,GAAM,CAAE,aAAa,EAAG,QAAS3B,EAAMM,IAAS,CAAC,EAC3CwB,EAAQD,EAAa,IAAI3B,CAAU,GAAK,CAAE,MAAO,CAAC,EAAG,OAAQ,GAAO,YAAa,CAAC,CAAE,EAC1F4B,EAAM,MAAM,KAAKhC,CAAI,EACrB,EAAM,SAAWkB,EAASV,IAAS,GACnCwB,EAAM,YAAY,KAAK,GAAIN,EAAkBlB,IAAS,CAAC,CAAE,EACzDuB,EAAa,IAAI3B,EAAY4B,CAAK,CACpC,CACAF,EAAO,KACL,CAAC,GAAGC,CAAY,CAAC,CAAC,SAAS,CAAC3B,EAAY6B,KAAU,CAChD,IAAMtD,EAAQnB,EAAO4C,GACrB,OAAOzB,EAAQ,CAACuD,EAAavD,EAAOjB,EAAOuE,CAAI,CAAC,EAAI,CAAC,CACvD,CAAC,CACH,CACF,CACA,OAAOH,CACT,CASA,SAASlD,EAAO,CAAE,SAA4C,CAC5D,MAAO,CAACwC,EAAM,gBAAiBA,EAAM,aAAa,CACpD,CAGA,SAASnB,EAAQ,CAAE,OAAM,QAAO,SAA8D,CAC5F,MAAO,CACL,CAACjB,EAAMI,IAAQ,EAAE,EACjB,CAACF,EAAOE,IAAQ,EAAE,CACpB,CACF,CAMA,SAASO,EACP,EACA,EACA,CAAE,QAAO,aACiC,CAC1C,IAAMwC,EAAeC,EAAiBpD,EAAMI,IAAQ,GAAIiD,CAAS,EAC3DC,EAAgBF,EAAiBlD,EAAOE,IAAQ,GAAIiD,CAAS,EAGnE,GAAIE,EAAiBC,EAAUxD,EAAMI,IAAQ,EAAE,EAAGoD,EAAUtD,EAAOE,IAAQ,EAAE,CAAC,GAAK,EAAG,CACpF,GAAM,CAACqD,EAAaC,GAAgBC,EAAQR,EAAa,SAAUG,EAAc,QAAQ,EACzF,MAAO,CAACM,EAAOT,EAAa,UAAWM,CAAW,EAAGG,EAAON,EAAc,UAAWI,CAAY,CAAC,CACpG,CACA,GAAM,CAACA,EAAcD,GAAeE,EAAQL,EAAc,SAAUH,EAAa,QAAQ,EACzF,MAAO,CAACS,EAAOT,EAAa,UAAWM,CAAW,EAAGG,EAAON,EAAc,UAAWI,CAAY,CAAC,CACpG,CAMA,SAASF,EAAU,EAAwB,EAAkD,CAC3F,GAAI,CAACxC,EACH,OAAOrB,EAAM,eAEf,GAAM,CAAE,WAAU,aAAcyD,EAAiBzD,EAAOqB,EAAM,EAAK,EACnE,OAAO,aAAa,KAAK6C,GAAW,EAAQ,IAC1CC,EAAS,EAAIA,EAAUnE,EAAM,cAAcoE,EAAUhD,IAAU,IAAM,CACvE,CACF,CAGA,SAAS6C,EAAO,EAAuB,EAAyC,CAC9E,IAAMpE,EAA2B,CAAC,EAClC,IAAK,IAAMwE,KAAYD,EAAU,QAAQ,EAAG,IAAUE,EAAQlD,KAAW,CAAC,CAAC,CAAC,SAAS,EAAG,CACtF,IAAMmD,EAAO1E,EAAK,GAAG,EAAE,EACnB0E,IAAO,KAAOF,EAChB,EAAK,GAAKA,EAAW,EAErBxE,EAAK,KAAK,CAACwE,EAAUA,EAAW,CAAC,CAAC,CAEtC,CACA,OAAOxE,CACT,CAMA,SAAS4D,EACP,EACA,EACA,EACiD,CACjD,IAAMe,EAAaxE,EAAM,MAAM,gBAa/B,OAZIqB,EACK,CACL,SAAUoD,EAAUzE,EAAM,QAAQ,SAASqB,EAAK,GAAKmD,EAAYnD,EAAK,GAAKmD,CAAU,CAAC,EACtF,UAAW,WAAW,KAAK,CAAE,OAAQnD,EAAK,GAAKA,EAAK,EAAG,GAAI,EAAG,IAAUA,EAAK,GAAKD,CAAK,CACzF,EAEEsC,GAAa1D,EAAM,UACd,CACL,SAAUA,EAAM,UAAU,SAC1B,UAAWA,EAAM,UAAU,QAAQ,IAAK,GAAWwE,EAAaE,CAAM,CACxE,EAEK,CAAE,SAAU1E,EAAM,SAAU,UAAWA,EAAM,SAAS,KAAK,EAAG,IAAUwE,EAAapD,CAAK,CAAE,CACrG,CAGA,SAASM,EAAsB,EAA+C,CAC5E,IAAMiD,EAA6B,CAAC,EACpC,IAAK,GAAM,CAACpF,EAAOC,KAAQiB,EAAM,UAAU,EAAM,IAAUJ,EAAK,GAAKE,EAAM,EAAE,EAAG,CAC9E,IAAMgE,EAAOI,EAAO,GAAG,EAAE,EACrBJ,GAAQhF,EAAQgF,EAAK,GACvB,EAAK,GAAK,KAAK,IAAIA,EAAK,GAAI/E,CAAG,EAE/BmF,EAAO,KAAK,CAACpF,EAAOC,CAAG,CAAC,CAE5B,CACA,OAAOmF,CACT,CAGA,SAAStF,EAA2B,EAA+D,CACjG,IAAMsF,EAASjD,EAAsBkD,EAAM,IAAK,GAAS,CAAC9C,EAAK,gBAAiBA,EAAK,aAAa,CAAC,CAAC,EAChG+C,EAAU,EACRC,EAAgB,CAAC,CAAC,EACxB,IAAK,GAAM,CAACvF,EAAOC,KAAQmF,EACzB,GAAWnF,EAAMD,EACjBuF,EAAc,KAAKD,CAAO,EAE5B,IAAME,EAAe,GAA6B,CAChD,IAAIC,EAAM,EACNC,EAAON,EAAO,OAClB,KAAOK,EAAMC,GAAM,CACjB,IAAMC,EAAUF,EAAMC,IAAU,GAC3BN,EAAOO,EAAO,GAAG,IAAM,GAAKb,EAC/B,EAAMa,EAAS,EAEf,EAAOA,CAEX,CACA,OAAQJ,EAAcE,IAAQ,GAAK,KAAK,IAAI,GAAIL,EAAOK,EAAM,EAAE,GAAG,IAAM,GAAKX,CAAQ,CACvF,EACA,OAAQ,EAAO,IAAQU,EAAYvF,CAAG,EAAIuF,EAAYxF,CAAK,CAC7D,CAOA,SAASgE,EACP,CAAE,YAAW,SACb,EACA,CAAE,QAAO,SAAQ,eACG,CACpB,IAAM4B,EAAQ1E,EAAM,SAAS,IAAA,EAAS,EAChC2E,EAAW3E,EACd,IAAK,GAA2BY,GAAQ,CAACoB,EAAM,gBAAiBA,EAAM,aAAa,CAAC,CAAC,CACrF,UAAU,EAAM,IAAUpC,EAAK,GAAKE,EAAM,EAAE,CAAC,CAC7C,KAAK,CAAC8B,EAAiBC,MAAoB,CAAE,kBAAiB,eAAc,EAAE,EAC3E/C,EAAQ6F,EAAS,EAAE,EAAE,iBAAmB3C,EAAM,gBAC9CjD,EAAM4F,EAAS,GAAG,EAAE,CAAC,EAAE,eAAiB3C,EAAM,cAC9C4C,EAAStG,EAAMU,EAAU,EAAE,OACjC,MAAO,CACL,YACA,qBAAsB6F,GAAU,IAAA,GAChC,WACA,YAAa5D,EAAsBX,CAAW,CAAC,CAAC,KAAK,CAACsB,EAAiBC,MAAkC,CACvG,kBACA,eACF,EAAE,EACF,WAAY8C,EAAS,QAAQ,EAAK,IAAYtF,EAAMyF,EAAQ,cAAgBA,EAAQ,gBAAiB,CAAC,EACtG,gBAAiBhG,EACjB,cAAeC,EACf,WAAYiD,EAAM,WAClB,SAAUA,EAAM,SAChB,UAAW0C,EAAQ1C,EAAM,WAAa4C,IAAS9F,EAAM,EAAE,UAAY,GAAK,EACxE,QAAS4F,EAAQ1C,EAAM,SAAW4C,IAAS7F,EAAM,EAAE,EAAE,QAAU,GAAK,CACtE,CACF,CAUA,SAASW,EACP,EACA,EACA,EACA,EACM,CACN,IAAMqF,kBAAiB,IAAI,IAC3B,IAAK,IAAMxF,KAASnB,EAClB,IAAK,IAAM4G,KAASzF,EAAM,OACxBwF,EAAe,IAAIC,GAAQD,EAAe,IAAIC,CAAK,GAAK,GAAK,CAAC,EAGlE,IAAK,IAAMzF,KAASnB,EAClB,EAAM,OAASmB,EAAM,OAAO,OAAQ,IAAWwF,EAAe,IAAIC,CAAK,GAAK,IAAM,GAAsB,EAI1G,IAAMC,EAAc,WAAW,KAAK7G,EAAS,GAAUmB,EAAM,SAAS,EAChE2F,EAAc,WAAW,KAAK5F,EAAa,MAAM,EACjD6F,EAAU,WAAW,KAAK/G,EAAS,GAAUmB,EAAM,SAAS,MAAM,EAClE6F,EAAc,WAAW,KAAKhH,EAAS,GAAUmB,EAAM,OAAO,MAAM,EACpE8F,EAAQ,CAAC,GAAGjH,EAAO,KAAK,CAAC,CAAC,CAAC,UAAU,EAAM,KAAW+G,EAAQvF,IAAS,IAAMuF,EAAQrF,IAAU,EAAE,EACjGwF,kBAAW,IAAI,IACfC,EAAe,IAAI,WAAWnH,EAAO,MAAM,EAC3CoH,EAAoB,CAAC,EAC3B,IAAK,IAAM1F,KAASuF,EAAO,CACzB,IAAMrG,EAAYiG,EAAYnF,GACxB2F,EAAcP,EAAYpF,KAAW,EACrC4F,EAAgB,KAAK,IACzB,KAAK,MAAMP,EAAQrF,IAAU,GAAK,CAAc,EAChD,KAAK,KAAM3B,GAAwBgH,EAAQrF,IAAU,GAAM,GAAG,CAChE,EACM6F,EAASvH,EAAO0B,EAAM,EAAE,QAAU,CAAC,EACzC,IAAK,IAAMkF,KAASW,EAAQ,CAC1B,IAAMC,EAAUN,EAAS,IAAIN,CAAK,EAClC,GAAI,CAACY,EAAS,CACZN,EAAS,IAAIN,EAAO,CAAClF,CAAK,CAAC,EAC3B,QACF,CACA,IAAK,IAAI8D,EAAWgC,EAAQ,OAAS,EAAGhC,GAAY,EAAG,IAAe,CACpE,IAAMhE,EAAOgG,EAAQhC,IAAa,EAClC,IAAKuB,EAAQvF,IAAS,GAAK8F,EACzB,MAEET,EAAYrF,KAAUZ,GAAcyG,GAAeP,EAAYtF,KAAU,IAGzE2F,EAAa3F,KAAU,GACzB4F,EAAQ,KAAK5F,CAAI,EAEnB,EAAaA,IAAS2F,EAAa3F,IAAS,GAAK,EACnD,CACAgG,EAAQ,KAAK9F,CAAK,CACpB,CACA,IAAK,IAAMF,KAAQ4F,EAAS,CAC1B,IAAMK,EAASN,EAAa3F,IAAS,EACrC,EAAaA,GAAQ,EACjBiG,EAAS,KAAO,GAAoB,KAAK,IAAIT,EAAYxF,IAAS,EAAG+F,EAAO,MAAM,GACpFG,EAAMlG,EAAME,CAAK,CAErB,CACA,EAAQ,OAAS,CACnB,CACF,CAQA,SAAStB,EACP,EACA,EACA,EAC8F,CAC9F,IAAMuH,kBAAsB,IAAI,IAChC,IAAK,IAAMxG,KAASnB,EAClB,IAAK,IAAMsF,KAAUnE,EAAM,cAAc,KAAK,EAC5CwG,EAAoB,IAAIrC,GAASqC,EAAoB,IAAIrC,CAAM,GAAK,GAAK,CAAC,EAG9E,IAAMsC,EAAmB,GACvB,KAAK,IAAI,GAAK,KAAK,MAAM,KAAK,OAAO5H,EAAO,OAAS,GAAK6H,CAAiB,CAAC,EAAI,EAAG,CAAgB,EAC/FC,kBAAU,IAAI,IACpB,IAAK,GAAM,CAACxC,EAAQyC,KAAcJ,EAChCG,EAAQ,IAAIxC,EAAQsC,EAAgBG,CAAS,CAAC,EAEhD,IAAMC,EAAS,GAAiD,CAC9D,IAAMC,EAAU,WAAW,KAAKC,EAAO,KAAK,CAAC,CAAC,CAAC,SAAS,EAElDC,EAAiB,WAAW,KAAKF,EAAU,IAAYC,EAAO,IAAI5C,CAAM,GAAK,IAAMwC,EAAQ,IAAIxC,CAAM,GAAK,EAAE,EAC9G8C,EAAQ,EACZ,IAAK,IAAMC,KAASF,EAClB,GAASE,EAEX,MAAO,CAAE,UAAS,iBAAgB,OAAM,CAC1C,EACMC,EAAgB,IAAI,IAAItI,EAAO,IAAK,GAAU,CAACmB,EAAO6G,EAAM7G,EAAM,aAAa,CAAC,CAAC,CAAC,EAIpFoH,EAAe,GACfC,EACEC,GAAsB,EAAwB,EAAoB,MAClEF,IAAiBG,GAAc,CAACF,KAClC,EAAeE,EACf,EAAUC,EAAuBjH,EAAM,QAAQ,GAE1C8G,EAAQnD,CAAQ,GAYnBuD,GAAgB,EAAuB,IAAkD,CAC7F,IAAMC,EAA8B,CAAC,EACrC,IACE,IAAIC,EAAY,EAAGJ,EAAa,EAChCI,EAAYtH,EAAK,aAAa,QAAUkH,EAAahH,EAAM,aAAa,QACxE,CACA,IAAMqH,EAAWvH,EAAK,aAAasH,IAAc,EAC3CE,EAAYtH,EAAM,aAAagH,IAAe,EAChDK,IAAaC,GACfH,EAAQ,KAAK,CAACrH,EAAK,mBAAmBsH,IAAc,EAAGpH,EAAM,mBAAmBgH,IAAe,CAAC,CAAC,EAE/FK,GAAYC,IACd,GAAa,GAEXA,GAAaD,IACf,GAAc,EAElB,CACAF,EAAQ,MAAM,EAAO,IAAW9F,EAAM,GAAKkG,EAAO,EAAE,EAGpD,IAAMC,EAAaL,EAAQ,QAAQ,CAACM,EAAYC,GAAc,IAAU,CACtE,IAAMC,EAAWR,EAAQtG,EAAQ,GAC3Be,EAAOuF,EAAQtG,EAAQ,GAC7B,OACG8G,IAAW,KAAOF,EAAa,GAAKE,EAAS,KAAOD,EAAc,GAClE9F,IAAO,KAAO6F,EAAa,GAAK7F,EAAK,KAAO8F,EAAc,CAE/D,CAAC,EACKD,EAAa3H,EAAK,MAAM,gBACxB4H,EAAc1H,EAAM,MAAM,gBAC1BE,EAA4C,CAAC,EACnD,IAAK,IAAM8E,KAAW4C,EAAcC,EAAuBL,CAAU,CAAC,EAAG,CACvE,GAAM,CAACM,EAAWC,GAAc/C,EAAQ,IAAM,CAAC,EAAG,CAAC,EAC7C,CAACgD,EAAUC,GAAajD,EAAQ,GAAG,EAAE,GAAK,CAAC,EAAG,CAAC,EAC/CkD,EAAUF,EAAW,EACrBG,EAAWF,EAAY,EACvBG,EAAaF,EAAUJ,EACvBO,EAAcF,EAAWJ,EACzBO,EAAU,KAAK,IAAIF,EAAYC,CAAW,EAC1CE,EAAWlK,EAAuB,KAAK,IAAI+J,EAAYC,CAAW,EAEtEC,EAAU3J,GACV2J,EAAU,IAAMC,GAChBC,EAAmBxD,CAAO,EAAI,IAAM,GAA2BsD,GAC/D,CAACG,EACCnC,EAAMoC,EAAa5I,EAAK,QAASA,EAAK,UAAWgI,EAAWI,CAAO,CAAC,EACpE5B,EAAMoC,EAAa1I,EAAM,QAASA,EAAM,UAAW+H,EAAYI,CAAQ,CAAC,CAC1E,GAIoBQ,EACpBzE,EAAUpE,EAAK,QAAQ,SAASgI,EAAWI,CAAO,CAAC,EACnDhE,EAAUlE,EAAM,QAAQ,SAAS+H,EAAYI,CAAQ,CAAC,CAExC,EAAI,KAAOI,GACzBrI,EAAM,KAAK,CACT,KAAM,CACJ,CAACuH,EAAaK,EAAWL,EAAaS,CAAO,EAC7C,CAACR,EAAcK,EAAYL,EAAcS,CAAQ,CACnD,EACA,UAAW,EACb,CAAC,CAEL,CACA,OAAOjI,EAAM,OAAS,EAAI,CAAE,OAAM,EAAI,IAAA,EACxC,EAGA,OAAQ,EAAM,EAAO,IAAe,CAClC,IAAMqI,EAAWlK,EAAuB,KAAK,IAAIyB,EAAK,SAAS,OAAQE,EAAM,SAAS,MAAM,EAC5F,GACE,KAAK,IAAIF,EAAK,SAAS,OAAQE,EAAM,SAAS,MAAM,EAAI,KAAOuI,GAC/DE,EAAc7B,EAAc,IAAI9G,CAAI,EAAG8G,EAAc,IAAI5G,CAAK,CAAC,EAC/D,CACA,GAAI4I,EAAc9I,EAAK,eAAgBE,EAAM,cAAc,EAAI,KAAOuI,GACpDxB,EAAmB/G,EAAOgH,EAAYlH,EAAK,QACjD,EAAI,KAAOyI,EACnB,MAAO,CAAE,UAAW,EAAM,EAK9B,GAAIzI,EAAK,WAAaE,EAAM,WACR2I,EAAU7I,EAAK,UAAU,SAAUE,EAAM,UAAU,QACzD,EAAI,KAAOuI,EACrB,MAAO,CAAE,UAAW,EAAK,CAG/B,CAGA,GAAIlF,EAAiBrD,EAAM,eAAgBF,EAAK,cAAc,EAAI,EAAG,CACnE,IAAM+I,EAAU3B,EAAalH,EAAOF,CAAI,EACxC,OACE+I,GAAS,OAAS,CAChB,MAAOA,EAAQ,MAAM,KAAK,CAAE,KAAM,CAACC,EAAWC,GAAW,GAAGC,MAAiB,CAC3E,GAAGA,EACH,KAAM,CAACD,EAAUD,CAAS,CAC5B,EAAE,CACJ,CAEJ,CACA,OAAO5B,EAAapH,EAAME,CAAK,CACjC,CACF,CAcA,SAASyI,EAAc,EAAmC,EAA6C,CACrG,GAAI,CAAC3I,GAAQ,CAACE,EACZ,MAAO,GAET,IAAIiJ,EAAU,EACd,IAAK,IAAI7B,EAAY,EAAGJ,EAAa,EAAGI,EAAYtH,EAAK,QAAQ,QAAUkH,EAAahH,EAAM,QAAQ,QAAS,CAC7G,IAAMkJ,EAAapJ,EAAK,QAAQsH,IAAc,EACxC+B,EAAcnJ,EAAM,QAAQgH,IAAe,EAC7CkC,IAAeC,IACjB,GAAW,KAAK,IAAIrJ,EAAK,eAAesH,IAAc,EAAGpH,EAAM,eAAegH,IAAe,CAAC,GAE5FkC,GAAcC,IAChB,GAAa,GAEXA,GAAeD,IACjB,GAAc,EAElB,CACA,OAAOD,EAAU,IAAM,GAA8B,KAAK,IAAInJ,EAAK,MAAOE,EAAM,KAAK,CACvF,CAQA,SAASoJ,EACP,EACA,EACA,EACA,EAC8B,CAC9B,GAAIC,EAAW,OAAS,EACtB,OAEF,IAAMC,EAAsE,CAAC,EACvEC,GAAY,EAAe,IAAsB,CACrD,IAAMC,EAAOtF,EAAUqC,EAAQ,SAASvH,EAAOC,CAAG,CAAC,EAG7CwK,EAAU,aAAa,KAAKD,GAAO,EAAQ,IAAW5F,EAAS,EAAIA,EAAU8F,EAAU1K,EAAQ6B,IAAU,CAAG,EAClHyI,EAAM,KAAK,CAAE,OAAM,UAAS,OAAM,CAAC,CACrC,EACIK,EAAS,EACb,IAAK,GAAM,CAACC,EAAgBC,KAAiBR,EAAY,CACvD,IAAMrK,EAAQ4K,EAAiB3F,EAC3B0F,EAAS3K,GACXuK,EAASI,EAAQ3K,CAAK,EAExBuK,EAASvK,EAAO6K,EAAe5F,CAAU,EACzC,EAAS4F,EAAe5F,CAC1B,CACI0F,EAASpD,EAAQ,QACnBgD,EAASI,EAAQpD,EAAQ,MAAM,EAEjC+C,EAAM,MAAM,EAAM,IAAUjG,EAAiBvD,EAAK,QAASE,EAAM,OAAO,GAAKF,EAAK,MAAQE,EAAM,KAAK,EACrG,IAAM2D,EAAW,IAAI,WAAW4C,EAAQ,MAAM,EACxCuD,EAAU,IAAI,WAAWvD,EAAQ,MAAM,EACzCzC,EAAW,EACf,IAAK,GAAM,CAAE,OAAM,WAAWwF,EAAO,CACnC3F,EAAS,IAAI6F,EAAM1F,CAAQ,EAC3B,IAAK,IAAIjD,EAAQ,EAAGA,EAAQ2I,EAAK,OAAQ,GAAS,EAChD,EAAQ1F,EAAWjD,GAAS7B,EAAQ6B,EAEtC,GAAY2I,EAAK,MACnB,CACA,MAAO,CAAE,WAAU,SAAQ,CAC7B,CAGA,SAASnG,EAAiB,EAAoB,EAA6B,CACzE,IAAK,IAAIxC,EAAQ,EAAGA,EAAQ,KAAK,IAAIf,EAAK,OAAQE,EAAM,MAAM,EAAG,GAAS,EAAG,CAC3E,IAAM+J,GAAcjK,EAAKe,IAAU,IAAMb,EAAMa,IAAU,GACzD,GAAIkJ,IAAe,EACjB,OAAOA,CAEX,CACA,OAAOjK,EAAK,OAASE,EAAM,MAC7B,CAMA,SAAS6H,EAAuB,EAAiD,CAC/E,IAAMmC,EAAwB,CAAC,EACzBC,EAAyB,CAAC,EAChC,IAAK,GAAM,CAACpJ,EAAO,EAAG6G,MAAiBP,EAAQ,QAAQ,EAAG,CACxD,IAAI1C,EAAM,EACNC,EAAOsF,EAAY,OACvB,KAAOvF,EAAMC,GAAM,CACjB,IAAMC,EAAUF,EAAMC,IAAU,GAC3ByC,EAAQ6C,EAAYrF,IAAW,EAAE,GAAG,IAAM,GAAK+C,EAClD,EAAM/C,EAAS,EAEf,EAAOA,CAEX,CACAsF,EAAa,KAAKxF,EAAM,EAAKuF,EAAYvF,EAAM,IAAM,GAAM,EAAE,EAC7D,EAAYA,GAAO5D,CACrB,CACA,IAAMqJ,EAA4B,CAAC,EACnC,IAAK,IAAIP,EAASK,EAAY,GAAG,EAAE,GAAK,GAAIL,GAAU,EAAG,EAASM,EAAaN,IAAW,GAAI,CAC5F,IAAM5E,EAASoC,EAAQwC,GACnB5E,GACFmF,EAAM,KAAKnF,CAAM,CAErB,CACA,OAAOmF,EAAM,WAAW,CAC1B,CAGA,SAAStC,EAAc,EAAiD,CACtE,IAAM/C,EAAiC,CAAC,EACxC,IAAK,GAAM,CAAChE,EAAOkE,KAAWmF,EAAM,QAAQ,EAAG,CAC7C,IAAMvC,EAAWuC,EAAMrJ,EAAQ,GAE7B8G,IAAa,IAAA,IACb5C,EAAO,IAAM4C,EAAS,GAAK,IAAc,IACzC5C,EAAO,IAAM4C,EAAS,GAAK,IAAc,GAEzC9C,EAAS,GAAG,EAAE,CAAC,EAAE,KAAKE,CAAM,EAE5BF,EAAS,KAAK,CAACE,CAAM,CAAC,CAE1B,CACA,OAAOF,CACT,CAGA,SAAS2D,EAAmB,EAAqC,CAC/D,IAAI7B,EAAQ,EACZ,IAAK,IAAI9F,EAAQ,EAAGA,EAAQmE,EAAQ,OAAQ,GAAS,EACnD,GAAS,KAAK,KAAKA,EAAQnE,EAAM,GAAG,IAAM,IAAMmE,EAAQnE,EAAQ,EAAE,GAAG,IAAM,GAAI,CAAS,EAE1F,OAAO8F,CACT,CAGA,SAASiC,EAAc,EAAkB,EAA2B,CAClE,IAAIK,EAAU,EACV7B,EAAY,EACZJ,EAAa,EACjB,KAAOI,EAAYtH,EAAK,QAAUkH,EAAahH,EAAM,QAAQ,CAC3D,IAAMmK,EAAYrK,EAAKsH,IAAc,EAC/BgD,EAAapK,EAAMgH,IAAe,EACpCmD,IAAcC,GAChB,GAAW,EACX,GAAa,EACb,GAAc,GACLD,EAAYC,EACrB,GAAa,EAEb,GAAc,CAElB,CACA,OAAOnB,CACT,CAOA,SAAS1K,EAAgB,EAAgD,CACvE,IAAM8L,kBAAmB,IAAI,IACvB/L,EAA4B,CAAC,EACnC,IAAK,GAAM,CAACY,EAAW,CAAE,SAAQ,sBAAqB,qBAAqBV,EAAM,QAAQ,EAAG,CAC1F,GAAI,CAACsG,GAAU,CAACwF,GAAgB,OAC9B,SAEF,IAAM/D,EAAU,IAAI,WAAWzB,EAAO,MAAM,EACtCyF,EAAY,IAAI,WAAWzF,EAAO,MAAM,EACxC0F,EAAgB,IAAI,aAAa1F,EAAO,MAAM,EAC9C2F,kBAAiB,IAAI,IAC3B,IAAK,GAAM,CAAC5J,EAAO6J,KAAU5F,EAAO,QAAQ,EAAG,CAC7C,GAAI4F,EAAM,OAAS,KAAM,CACvB,IAAIC,EAAKF,EAAe,IAAIC,EAAM,IAAI,EAClCC,IAAO,IAAA,KACT,EAAKF,EAAe,KACpBA,EAAe,IAAIC,EAAM,KAAMC,CAAE,GAEnC,EAAQ9J,GAAS,EAAE8J,EAAK,GACxB,QACF,CACA,IAAMC,EAAMC,EAASH,CAAK,EAC1B,EAAc7J,GAAS+J,EACvB,IAAIhH,EAASyG,EAAiB,IAAIO,CAAG,EACjChH,IAAW,IAAA,KACb,EAASyG,EAAiB,KAC1BA,EAAiB,IAAIO,EAAKhH,CAAM,GAElC,EAAQ/C,GAAS+C,EACjB,EAAU/C,GAAS6J,EAAM,QAAUA,EAAM,cAAgB,IAAA,GAAY,EAAI,CAC3E,CACA,IAAMI,EAAiBC,EAA8BC,GAAuB,CAAC,CAAC,EAC9E,IAAK,IAAM9I,KAASoI,EAAgB,CAClC,GAAM,CAAE,gBAAiBtL,EAAO,cAAeC,GAAQiD,EACjD+I,EAAe1E,EAAQ,SAASvH,EAAOC,CAAG,EAC1CiM,EAAiBX,EAAU,SAASvL,EAAOC,CAAG,EAC9C0E,EAAWO,EAAU+G,CAAY,EACjCE,EAAcC,EAAmBH,EAAcT,EAAc,SAASxL,EAAOC,CAAG,CAAC,EACjFoM,kBAAmB,IAAI,IAC7B,IAAK,IAAMC,KAAQH,EACjBE,EAAiB,IAAIC,GAAOD,EAAiB,IAAIC,CAAI,GAAK,GAAK,CAAC,EAElE,IAAMC,EAAgBJ,EACnB,KAAK,CAAC,CACN,OAAQ,GAAWE,EAAiB,IAAIF,EAAYhH,IAAW,CAAC,IAAM,CAAC,CAAC,CACxE,QAAQ,CAAC,CACT,UAAU,EAAO,KAAYgH,EAAY9J,IAAU,IAAM8J,EAAY5D,IAAW,EAAE,EACrFjJ,EAAO,KAAK,CACV,YACA,QACA,QAAS2M,EACT,UAAWC,EACX,gBACA,WACA,eAAgB,aAAa,KAAKvH,GAAW,EAAQ,IACnDC,EAAS,EAAIA,EAAU4G,EAAcxL,EAAQ6B,IAAU,CACzD,EACA,eAAgB8C,EAAS,SAAS,EAClC,OAAQ,WAAW,KAAK0H,EAAiB,KAAK,CAAC,EAC/C,aAAc,WAAW,KAAKE,EAAgB,GAAWJ,EAAYhH,IAAW,CAAC,EACjF,mBAAoB,WAAW,KAAKoH,CAAa,EACjD,cAAe7C,EAAauC,EAAcC,EAAgB,EAAGD,EAAa,MAAM,EAChF,UAAW7B,EACT6B,EACAT,EAAc,SAASxL,EAAOC,CAAG,EACjC6L,EAAe9L,EAAOC,CAAG,EACzBD,CACF,CACF,CAAC,CACH,CACF,CACA,OAAOV,CACT,CAMA,SAASyM,EACP,EACoD,CACpD,IAAM1B,EAAa2B,EAChB,KAAK,CAAC,CACN,OAAQ,GAAcQ,EAAU,gBAAkBA,EAAU,aAAa,CAAC,CAC1E,IAAK,GAAgC,CAACA,EAAU,gBAAiBA,EAAU,aAAa,CAAC,CAAC,CAC1F,UAAU,EAAM,IAAU1L,EAAK,GAAKE,EAAM,IAAMA,EAAM,GAAKF,EAAK,EAAE,EACrE,OAAQ,EAAO,IAAQ,CACrB,IAAI2E,EAAM,EACNC,EAAO2E,EAAW,OACtB,KAAO5E,EAAMC,GAAM,CACjB,IAAMC,EAAUF,EAAMC,IAAU,GAC3B2E,EAAW1E,EAAO,GAAG,IAAM,GAAK3F,EACnC,EAAM2F,EAAS,EAEf,EAAOA,CAEX,CACA,IAAM8G,EAA+B,CAAC,EACtC,IAAK,IAAI5K,EAAQ4D,EAAK5D,EAAQwI,EAAW,OAAQ,GAAS,EAAG,CAC3D,IAAMmC,EAAYnC,EAAWxI,GAC7B,GAAI,CAAC2K,GAAaA,EAAU,IAAMvM,EAChC,MAEF,IAAM+E,EAAOyH,EAAS,GAAG,EAAE,EACrBC,EAAS1H,IAAS,IAAA,IAAawH,EAAU,GAAKxH,EAAK,GACrDwH,EAAU,IAAMvM,IAASuM,EAAU,KAAOxM,GAASwM,EAAU,KAAOvM,IAAQ,CAACyM,GAC/ED,EAAS,KAAKD,CAAS,CAE3B,CACA,OAAOC,CACT,CACF,CAEA,SAAS/C,EAAa,EAAqB,EAAuB,EAAe,EAAkC,CACjH,IAAMlC,kBAAS,IAAI,IACnB,IAAK,IAAIrC,EAASnF,EAAOmF,EAASlF,EAAK,GAAU,EAC/C,GAAIsL,EAAUpG,KAAY,EAAG,CAC3B,IAAMP,EAAS2C,EAAQpC,IAAW,EAClCqC,EAAO,IAAI5C,GAAS4C,EAAO,IAAI5C,CAAM,GAAK,GAAK,CAAC,CAClD,CAEF,OAAO4C,CACT,CAGA,SAAStC,EAAU,EAAiC,CAClD,IAAMyH,kBAAoB,IAAI,IAC9B,OAAOpF,EAAQ,IAAK,GAAW,CAC7B,GAAI3C,GAAU,EACZ,OAAOA,EAET,IAAI/C,EAAQ8K,EAAkB,IAAI/H,CAAM,EAKxC,OAJI/C,IAAU,IAAA,KACZ,EAAQ8K,EAAkB,KAC1BA,EAAkB,IAAI/H,EAAQ/C,CAAK,GAE9B,EAAEA,EAAQ,EACnB,CAAC,CACH,CAOA,SAASgK,EAAS,EAAsB,CACtC,IAAMe,EAAUlB,EAAM,SAAW,KAAK,KAAKA,EAAM,aAAe,EAAG,UAAa,EAC1EmB,EAAYnB,EAAM,UAAY,KAAK,KAAKA,EAAM,cAAgB,EAAG,UAAa,EACpF,OAAQkB,IAAY,GAAK,SAAcC,IAAc,GACvD,CAQA,SAAST,EAAmB,EAAqB,EAAqC,CACpF,IAAMU,EAAS,IAAI,WAAW,KAAK,IAAIvF,EAAQ,OAAS,EAAY,EAAG,CAAC,CAAC,EACzE,IAAK,IAAIvH,EAAQ,EAAGA,EAAQ8M,EAAO,OAAQ,GAAS,EAAG,CACrD,IAAIR,EAAO,KACX,IAAK,IAAInH,EAAS,EAAGA,EAAS,EAAW,GAAU,EAAG,CACpD,IAAM0G,EAAWnB,EAAU1K,EAAQmF,IAAW,EAExC4H,GAASxF,EAAQvH,EAAQmF,IAAW,GAAK,EAAI,GAAK0G,EAAW,KAAK,MAAMA,EAAW,UAAe,EAExG,EAAQ,KAAK,KAAKS,EAAM,EAAE,EAAIS,EAAS,CACzC,CACA,EAAO/M,GAASsM,CAClB,CACA,OAAOQ,CACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"duplicateSelection.cjs","names":[],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection for the cross-file detector (the native\n * within-file detector ports it as select_maximal_duplicates). Candidates are grouped by\n * fingerprint, ranked by total coverage, kept greedily without overlapping a kept region, and\n * groups that fall below the survivor requirement are shed (largest first) so their regions stop\n * blocking smaller groups. A copy lying entirely inside a larger group's region stays with its\n * group as a nested copy, so a standalone copy elsewhere is still reported as duplicating it.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /** Regions can only overlap within the same bucket: the file index. */\n regionBucket?: number;\n /**\n * Set by selectMaximalGroups on a copy nested inside a larger group's region: it is reported with\n * its group, but its span is already counted by that larger group.\n */\n nestedInLargerGroup?: boolean;\n}\n\n/**\n * Caps how often the maximal-region selection reruns after shedding one failed duplicate group,\n * and then how often after shedding every failed group at once.\n */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. Past the rerun cap, which bounds degenerate inputs, every failed group is\n // removed at once, which converges in few passes; past the same cap again the remaining failed\n // groups are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, T[]>();\n const counted = new Map<string, T[]>();\n const nestedByFingerprint = new Map<string, T[]>();\n // Two passes. The first keeps each candidate that overlaps no kept region. The second lets a\n // candidate left over take the place of the kept copies it encloses, unless it partially\n // overlaps a kept region: a larger clone must not displace a clone of the first pass it merely\n // overlaps (one extending a kept run by a few tokens would hide the clone that follows the run).\n let leftOver: T[] = [];\n // Per fingerprint, the fingerprints of the kept regions that kept a candidate of it out.\n const blockersByFingerprint = new Map<string, Set<string>>();\n for (const encloses of [false, true]) {\n const candidates = encloses ? leftOver : duplicates;\n leftOver = [];\n for (const candidate of candidates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n // A plain loop: this runs once per candidate over every kept region of the bucket, so\n // allocating a filtered array per candidate would dominate project-scale runs. Kept regions\n // never overlap each other, so a candidate inside one cannot partially overlap another.\n let containedInKept = false;\n let partiallyOverlaps = false;\n let enclosedKept: T[] | undefined;\n for (const region of keptRegions) {\n if (region.startIndex >= candidate.endIndex || candidate.startIndex >= region.endIndex) {\n continue;\n }\n if (region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex) {\n containedInKept = true;\n break;\n }\n if (encloses && candidate.startIndex <= region.startIndex && region.endIndex <= candidate.endIndex) {\n (enclosedKept ??= []).push(region);\n } else {\n partiallyOverlaps = true;\n if (encloses) {\n const blockers = blockersByFingerprint.get(candidate.fingerprint) ?? new Set();\n blockers.add(region.fingerprint);\n blockersByFingerprint.set(candidate.fingerprint, blockers);\n }\n break;\n }\n }\n if (containedInKept) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n continue;\n }\n if (partiallyOverlaps) {\n leftOver.push(candidate);\n continue;\n }\n // The kept copies of smaller groups that the candidate encloses become nested copies of\n // their groups.\n const enclosed = enclosedKept;\n for (const inner of enclosed ?? []) {\n const group = counted.get(inner.fingerprint) ?? [];\n const index = group.indexOf(inner);\n if (index !== -1) {\n group[index] = { ...inner, nestedInLargerGroup: true };\n }\n }\n // The enclosed regions give way to the enclosing one, keeping kept regions mutually\n // non-overlapping: a later candidate inside this region must see it, not a region it\n // swallowed (which the candidate could straddle instead).\n const occupied = enclosed ? keptRegions.filter((region) => !enclosed.includes(region)) : keptRegions;\n occupied.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, occupied);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n }\n // A candidate still left over overlapped a kept region partially when its turn came. Where an\n // enclosing candidate took that region since, the candidate lies inside a kept region now and\n // is a nested copy like one visited later: containment must not depend on the greedy order.\n for (const candidate of leftOver) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex)\n ) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n }\n }\n // Nested copies join only a group that kept a standalone copy; on their own they would merely\n // restate the larger group.\n for (const [fingerprint, nested] of nestedByFingerprint) {\n counted.get(fingerprint)?.push(...nested);\n }\n for (const [fingerprint, group] of counted) {\n if (group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n }\n }\n\n // A failed group that another failed group kept a copy from is not shed before it: shedding\n // the blocking group can rescue it, while shedding it frees nothing the other waits for.\n const failed = (fingerprint: string): boolean => {\n const group = counted.get(fingerprint);\n return group !== undefined && !isSurvivingGroup(group);\n };\n let failedFingerprint: string | undefined;\n let failedRank = -1;\n for (const [fingerprint, group] of counted) {\n if (isSurvivingGroup(group)) {\n continue;\n }\n const blockedByFailed = [...(blockersByFingerprint.get(fingerprint) ?? [])].some(\n (other) => other !== fingerprint && failed(other)\n );\n // Unblocked groups first, then the largest.\n const rank = (blockedByFailed ? 0 : 2 ** 40) + (group[0]?.tokenCount ?? 0);\n if (rank > failedRank) {\n failedFingerprint = fingerprint;\n failedRank = rank;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= 2 * maxSelectionRerunCount) {\n dropFailedGroups(counted, isSurvivingGroup);\n return counted;\n }\n\n const failedFingerprints = new Set(\n rerun < maxSelectionRerunCount\n ? [failedFingerprint]\n : [...counted].filter(([, group]) => !isSurvivingGroup(group)).map(([fingerprint]) => fingerprint)\n );\n duplicates = duplicates.filter((candidate) => !failedFingerprints.has(candidate.fingerprint));\n }\n}\n\n/**\n * Past the rerun caps, still-failing groups are dropped without another selection pass. A dropped\n * group's regions may have been what nested copies of surviving groups lay inside, and such a copy\n * would then be counted by no group at all, so those copies are dropped too and the shrunk groups\n * are re-checked until nothing changes.\n */\nfunction dropFailedGroups<T extends SelectableRegion>(\n counted: Map<string, T[]>,\n isSurvivingGroup: (group: T[]) => boolean\n): void {\n for (let changed = true; changed;) {\n changed = false;\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group) || group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n changed = true;\n }\n }\n const standalone = [...counted.values()].flat().filter((candidate) => !candidate.nestedInLargerGroup);\n for (const [fingerprint, group] of counted) {\n const kept = group.filter(\n (candidate) =>\n !candidate.nestedInLargerGroup ||\n standalone.some(\n (region) =>\n (region.regionBucket ?? 0) === (candidate.regionBucket ?? 0) &&\n region.startIndex <= candidate.startIndex &&\n candidate.endIndex <= region.endIndex\n )\n );\n if (kept.length !== group.length) {\n counted.set(fingerprint, kept);\n changed = true;\n }\n }\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplicateSelection.cjs","names":["selectMaximalGroups","byFingerprint","candidate","candidates","group","groups","dedupeByRegion","isSurvivingGroup","groupSizeByFingerprint","coverage","duplicates","right","left","compareTies","rerun","keptRegionsByBucket","counted","nestedByFingerprint","leftOver","blockersByFingerprint","encloses","keptRegions","containedInKept","partiallyOverlaps","enclosedKept","region","blockers","nested","enclosed","inner","index","occupied","fingerprint","failed","failedFingerprint","failedRank","rank","other","dropFailedGroups","failedFingerprints","changed","standalone","kept","byRegion","key","existing"],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection for the cross-file detector (the native\n * within-file detector ports it as select_maximal_duplicates). Candidates are grouped by\n * fingerprint, ranked by total coverage, kept greedily without overlapping a kept region, and\n * groups that fall below the survivor requirement are shed (largest first) so their regions stop\n * blocking smaller groups. A copy lying entirely inside a larger group's region stays with its\n * group as a nested copy, so a standalone copy elsewhere is still reported as duplicating it.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /** Regions can only overlap within the same bucket: the file index. */\n regionBucket?: number;\n /**\n * Set by selectMaximalGroups on a copy nested inside a larger group's region: it is reported with\n * its group, but its span is already counted by that larger group.\n */\n nestedInLargerGroup?: boolean;\n}\n\n/**\n * Caps how often the maximal-region selection reruns after shedding one failed duplicate group,\n * and then how often after shedding every failed group at once.\n */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. Past the rerun cap, which bounds degenerate inputs, every failed group is\n // removed at once, which converges in few passes; past the same cap again the remaining failed\n // groups are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, T[]>();\n const counted = new Map<string, T[]>();\n const nestedByFingerprint = new Map<string, T[]>();\n // Two passes. The first keeps each candidate that overlaps no kept region. The second lets a\n // candidate left over take the place of the kept copies it encloses, unless it partially\n // overlaps a kept region: a larger clone must not displace a clone of the first pass it merely\n // overlaps (one extending a kept run by a few tokens would hide the clone that follows the run).\n let leftOver: T[] = [];\n // Per fingerprint, the fingerprints of the kept regions that kept a candidate of it out.\n const blockersByFingerprint = new Map<string, Set<string>>();\n for (const encloses of [false, true]) {\n const candidates = encloses ? leftOver : duplicates;\n leftOver = [];\n for (const candidate of candidates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n // A plain loop: this runs once per candidate over every kept region of the bucket, so\n // allocating a filtered array per candidate would dominate project-scale runs. Kept regions\n // never overlap each other, so a candidate inside one cannot partially overlap another.\n let containedInKept = false;\n let partiallyOverlaps = false;\n let enclosedKept: T[] | undefined;\n for (const region of keptRegions) {\n if (region.startIndex >= candidate.endIndex || candidate.startIndex >= region.endIndex) {\n continue;\n }\n if (region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex) {\n containedInKept = true;\n break;\n }\n if (encloses && candidate.startIndex <= region.startIndex && region.endIndex <= candidate.endIndex) {\n (enclosedKept ??= []).push(region);\n } else {\n partiallyOverlaps = true;\n if (encloses) {\n const blockers = blockersByFingerprint.get(candidate.fingerprint) ?? new Set();\n blockers.add(region.fingerprint);\n blockersByFingerprint.set(candidate.fingerprint, blockers);\n }\n break;\n }\n }\n if (containedInKept) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n continue;\n }\n if (partiallyOverlaps) {\n leftOver.push(candidate);\n continue;\n }\n // The kept copies of smaller groups that the candidate encloses become nested copies of\n // their groups.\n const enclosed = enclosedKept;\n for (const inner of enclosed ?? []) {\n const group = counted.get(inner.fingerprint) ?? [];\n const index = group.indexOf(inner);\n if (index !== -1) {\n group[index] = { ...inner, nestedInLargerGroup: true };\n }\n }\n // The enclosed regions give way to the enclosing one, keeping kept regions mutually\n // non-overlapping: a later candidate inside this region must see it, not a region it\n // swallowed (which the candidate could straddle instead).\n const occupied = enclosed ? keptRegions.filter((region) => !enclosed.includes(region)) : keptRegions;\n occupied.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, occupied);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n }\n // A candidate still left over overlapped a kept region partially when its turn came. Where an\n // enclosing candidate took that region since, the candidate lies inside a kept region now and\n // is a nested copy like one visited later: containment must not depend on the greedy order.\n for (const candidate of leftOver) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex)\n ) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n }\n }\n // Nested copies join only a group that kept a standalone copy; on their own they would merely\n // restate the larger group.\n for (const [fingerprint, nested] of nestedByFingerprint) {\n counted.get(fingerprint)?.push(...nested);\n }\n for (const [fingerprint, group] of counted) {\n if (group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n }\n }\n\n // A failed group that another failed group kept a copy from is not shed before it: shedding\n // the blocking group can rescue it, while shedding it frees nothing the other waits for.\n const failed = (fingerprint: string): boolean => {\n const group = counted.get(fingerprint);\n return group !== undefined && !isSurvivingGroup(group);\n };\n let failedFingerprint: string | undefined;\n let failedRank = -1;\n for (const [fingerprint, group] of counted) {\n if (isSurvivingGroup(group)) {\n continue;\n }\n const blockedByFailed = [...(blockersByFingerprint.get(fingerprint) ?? [])].some(\n (other) => other !== fingerprint && failed(other)\n );\n // Unblocked groups first, then the largest.\n const rank = (blockedByFailed ? 0 : 2 ** 40) + (group[0]?.tokenCount ?? 0);\n if (rank > failedRank) {\n failedFingerprint = fingerprint;\n failedRank = rank;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= 2 * maxSelectionRerunCount) {\n dropFailedGroups(counted, isSurvivingGroup);\n return counted;\n }\n\n const failedFingerprints = new Set(\n rerun < maxSelectionRerunCount\n ? [failedFingerprint]\n : [...counted].filter(([, group]) => !isSurvivingGroup(group)).map(([fingerprint]) => fingerprint)\n );\n duplicates = duplicates.filter((candidate) => !failedFingerprints.has(candidate.fingerprint));\n }\n}\n\n/**\n * Past the rerun caps, still-failing groups are dropped without another selection pass. A dropped\n * group's regions may have been what nested copies of surviving groups lay inside, and such a copy\n * would then be counted by no group at all, so those copies are dropped too and the shrunk groups\n * are re-checked until nothing changes.\n */\nfunction dropFailedGroups<T extends SelectableRegion>(\n counted: Map<string, T[]>,\n isSurvivingGroup: (group: T[]) => boolean\n): void {\n for (let changed = true; changed;) {\n changed = false;\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group) || group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n changed = true;\n }\n }\n const standalone = [...counted.values()].flat().filter((candidate) => !candidate.nestedInLargerGroup);\n for (const [fingerprint, group] of counted) {\n const kept = group.filter(\n (candidate) =>\n !candidate.nestedInLargerGroup ||\n standalone.some(\n (region) =>\n (region.regionBucket ?? 0) === (candidate.regionBucket ?? 0) &&\n region.startIndex <= candidate.startIndex &&\n candidate.endIndex <= region.endIndex\n )\n );\n if (kept.length !== group.length) {\n counted.set(fingerprint, kept);\n changed = true;\n }\n }\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"aAkCA,SAAgBA,EACd,EACA,EACA,EACkB,CAClB,IAAMC,kBAAgB,IAAI,IAC1B,IAAK,IAAMC,KAAaC,EAAY,CAClC,IAAMC,EAAQH,EAAc,IAAIC,EAAU,WAAW,GAAK,CAAC,EAC3DE,EAAM,KAAKF,CAAS,EACpBD,EAAc,IAAIC,EAAU,YAAaE,CAAK,CAChD,CAEA,IAAMC,EAAS,CAAC,GAAGJ,EAAc,OAAO,CAAC,CAAC,CAAC,IAAIK,CAAc,CAAC,CAAC,OAAOC,CAAgB,EAIhFC,EAAyB,IAAI,IAAIH,EAAO,IAAK,GAAU,CAACD,EAAM,EAAE,EAAE,aAAe,GAAIA,EAAM,MAAM,CAAC,CAAC,EACnGK,EAAY,GAChBP,EAAU,YAAcM,EAAuB,IAAIN,EAAU,WAAW,GAAK,GAC3EQ,EAAaL,EAAO,KAAK,EAC7BK,EAAW,MAAM,EAAM,IAAUD,EAASE,CAAK,EAAIF,EAASG,CAAI,IAAMC,EAAcA,EAAYD,EAAMD,CAAK,EAAI,EAAE,EAQjH,IAAK,IAAIG,EAAQ,GAAK,GAAS,EAAG,CAChC,IAAMC,kBAAsB,IAAI,IAC1BC,kBAAU,IAAI,IACdC,kBAAsB,IAAI,IAK5BC,EAAgB,CAAC,EAEfC,kBAAwB,IAAI,IAClC,IAAK,IAAMC,IAAY,CAAC,GAAO,EAAI,EAAG,CACpC,IAAMjB,EAAaiB,EAAWF,EAAWR,EACzC,EAAW,CAAC,EACZ,IAAK,IAAMR,KAAaC,EAAY,CAClC,IAAMkB,EAAcN,EAAoB,IAAIb,EAAU,cAAgB,CAAC,GAAK,CAAC,EAIzEoB,EAAkB,GAClBC,EAAoB,GACpBC,EACJ,IAAK,IAAMC,KAAUJ,EACnB,GAAI,IAAO,YAAcnB,EAAU,UAAYA,EAAU,YAAcuB,EAAO,UAG9E,IAAIA,EAAO,YAAcvB,EAAU,YAAcA,EAAU,UAAYuB,EAAO,SAAU,CACtF,EAAkB,GAClB,KACF,CACA,GAAIL,GAAYlB,EAAU,YAAcuB,EAAO,YAAcA,EAAO,UAAYvB,EAAU,UACvF,IAAiB,CAAC,EAAA,CAAG,KAAKuB,CAAM,MAC5B,CAEL,GADA,EAAoB,GAChBL,EAAU,CACZ,IAAMM,EAAWP,EAAsB,IAAIjB,EAAU,WAAW,mBAAK,IAAI,IACzEwB,EAAS,IAAID,EAAO,WAAW,EAC/BN,EAAsB,IAAIjB,EAAU,YAAawB,CAAQ,CAC3D,CACA,KACF,CAXA,CAaF,GAAIJ,EAAiB,CACnB,IAAMK,EAASV,EAAoB,IAAIf,EAAU,WAAW,GAAK,CAAC,EAClEyB,EAAO,KAAK,CAAE,GAAGzB,EAAW,oBAAqB,EAAK,CAAC,EACvDe,EAAoB,IAAIf,EAAU,YAAayB,CAAM,EACrD,QACF,CACA,GAAIJ,EAAmB,CACrBL,EAAS,KAAKhB,CAAS,EACvB,QACF,CAGA,IAAM0B,EAAWJ,EACjB,IAAK,IAAMK,KAASD,GAAY,CAAC,EAAG,CAClC,IAAMxB,EAAQY,EAAQ,IAAIa,EAAM,WAAW,GAAK,CAAC,EAC3CC,EAAQ1B,EAAM,QAAQyB,CAAK,EAC7BC,IAAU,KACZ,EAAMA,GAAS,CAAE,GAAGD,EAAO,oBAAqB,EAAK,EAEzD,CAIA,IAAME,EAAWH,EAAWP,EAAY,OAAQ,GAAW,CAACO,EAAS,SAASH,CAAM,CAAC,EAAIJ,EACzFU,EAAS,KAAK7B,CAAS,EACvBa,EAAoB,IAAIb,EAAU,cAAgB,EAAG6B,CAAQ,EAC7D,IAAM3B,EAAQY,EAAQ,IAAId,EAAU,WAAW,GAAK,CAAC,EACrDE,EAAM,KAAKF,CAAS,EACpBc,EAAQ,IAAId,EAAU,YAAaE,CAAK,CAC1C,CACF,CAIA,IAAK,IAAMF,KAAagB,EAEtB,IADoBH,EAAoB,IAAIb,EAAU,cAAgB,CAAC,GAAK,CAAC,EAAA,CAE/D,KAAM,GAAWuB,EAAO,YAAcvB,EAAU,YAAcA,EAAU,UAAYuB,EAAO,QAAQ,EAC/G,CACA,IAAME,EAASV,EAAoB,IAAIf,EAAU,WAAW,GAAK,CAAC,EAClEyB,EAAO,KAAK,CAAE,GAAGzB,EAAW,oBAAqB,EAAK,CAAC,EACvDe,EAAoB,IAAIf,EAAU,YAAayB,CAAM,CACvD,CAIF,IAAK,GAAM,CAACK,EAAaL,KAAWV,EAClCD,EAAQ,IAAIgB,CAAW,CAAC,EAAE,KAAK,GAAGL,CAAM,EAE1C,IAAK,GAAM,CAACK,EAAa5B,KAAUY,EAC7BZ,EAAM,MAAO,GAAcF,EAAU,mBAAmB,GAC1Dc,EAAQ,OAAOgB,CAAW,EAM9B,IAAMC,EAAU,GAAiC,CAC/C,IAAM7B,EAAQY,EAAQ,IAAIgB,CAAW,EACrC,OAAO5B,IAAU,IAAA,IAAa,CAACG,EAAiBH,CAAK,CACvD,EACI8B,EACAC,EAAa,GACjB,IAAK,GAAM,CAACH,EAAa5B,KAAUY,EAAS,CAC1C,GAAIT,EAAiBH,CAAK,EACxB,SAMF,IAAMgC,GAJkB,CAAC,GAAIjB,EAAsB,IAAIa,CAAW,GAAK,CAAC,CAAE,CAAC,CAAC,KACzE,GAAUK,IAAUL,GAAeC,EAAOI,CAAK,CAGtB,EAAI,EAAI,GAAK,KAAOjC,EAAM,EAAE,EAAE,YAAc,GACpEgC,EAAOD,IACT,EAAoBH,EACpB,EAAaI,EAEjB,CAEA,GAAIF,IAAsB,IAAA,GACxB,OAAOlB,EAET,GAAIF,GAAS,GAEX,OADAwB,EAAiBtB,EAAST,CAAgB,EACnCS,EAGT,IAAMuB,EAAqB,IAAI,IAC7BzB,EAAQ,GACJ,CAACoB,CAAiB,EAClB,CAAC,GAAGlB,CAAO,CAAC,CAAC,QAAQ,EAAGZ,KAAW,CAACG,EAAiBH,CAAK,CAAC,CAAC,CAAC,KAAK,CAAC4B,KAAiBA,CAAW,CACrG,EACA,EAAatB,EAAW,OAAQ,GAAc,CAAC6B,EAAmB,IAAIrC,EAAU,WAAW,CAAC,CAC9F,CACF,CAQA,SAASoC,EACP,EACA,EACM,CACN,IAAK,IAAIE,EAAU,GAAMA,GAAU,CACjC,EAAU,GACV,IAAK,GAAM,CAACR,EAAa5B,KAAUY,GAC7B,CAACT,EAAiBH,CAAK,GAAKA,EAAM,MAAO,GAAcF,EAAU,mBAAmB,KACtFc,EAAQ,OAAOgB,CAAW,EAC1B,EAAU,IAGd,IAAMS,EAAa,CAAC,GAAGzB,EAAQ,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAQ,GAAc,CAACd,EAAU,mBAAmB,EACpG,IAAK,GAAM,CAAC8B,EAAa5B,KAAUY,EAAS,CAC1C,IAAM0B,EAAOtC,EAAM,OAChB,GACC,CAACF,EAAU,qBACXuC,EAAW,KACR,IACEhB,EAAO,cAAgB,MAAQvB,EAAU,cAAgB,IAC1DuB,EAAO,YAAcvB,EAAU,YAC/BA,EAAU,UAAYuB,EAAO,QACjC,CACJ,EACIiB,EAAK,SAAWtC,EAAM,SACxBY,EAAQ,IAAIgB,EAAaU,CAAI,EAC7B,EAAU,GAEd,CACF,CACF,CAGA,SAAgBpC,EAA2C,EAAiB,CAC1E,IAAMqC,kBAAW,IAAI,IACrB,IAAK,IAAMzC,KAAaE,EAAO,CAC7B,IAAMwC,EAAM,GAAG1C,EAAU,cAAgB,EAAE,GAAGA,EAAU,WAAW,GAAGA,EAAU,WAC1E2C,EAAWF,EAAS,IAAIC,CAAG,GAC7B,CAACC,GAAY3C,EAAU,WAAa2C,EAAS,aAC/CF,EAAS,IAAIC,EAAK1C,CAAS,CAE/B,CACA,MAAO,CAAC,GAAGyC,EAAS,OAAO,CAAC,CAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"duplicateSelection.js","names":[],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection for the cross-file detector (the native\n * within-file detector ports it as select_maximal_duplicates). Candidates are grouped by\n * fingerprint, ranked by total coverage, kept greedily without overlapping a kept region, and\n * groups that fall below the survivor requirement are shed (largest first) so their regions stop\n * blocking smaller groups. A copy lying entirely inside a larger group's region stays with its\n * group as a nested copy, so a standalone copy elsewhere is still reported as duplicating it.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /** Regions can only overlap within the same bucket: the file index. */\n regionBucket?: number;\n /**\n * Set by selectMaximalGroups on a copy nested inside a larger group's region: it is reported with\n * its group, but its span is already counted by that larger group.\n */\n nestedInLargerGroup?: boolean;\n}\n\n/**\n * Caps how often the maximal-region selection reruns after shedding one failed duplicate group,\n * and then how often after shedding every failed group at once.\n */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. Past the rerun cap, which bounds degenerate inputs, every failed group is\n // removed at once, which converges in few passes; past the same cap again the remaining failed\n // groups are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, T[]>();\n const counted = new Map<string, T[]>();\n const nestedByFingerprint = new Map<string, T[]>();\n // Two passes. The first keeps each candidate that overlaps no kept region. The second lets a\n // candidate left over take the place of the kept copies it encloses, unless it partially\n // overlaps a kept region: a larger clone must not displace a clone of the first pass it merely\n // overlaps (one extending a kept run by a few tokens would hide the clone that follows the run).\n let leftOver: T[] = [];\n // Per fingerprint, the fingerprints of the kept regions that kept a candidate of it out.\n const blockersByFingerprint = new Map<string, Set<string>>();\n for (const encloses of [false, true]) {\n const candidates = encloses ? leftOver : duplicates;\n leftOver = [];\n for (const candidate of candidates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n // A plain loop: this runs once per candidate over every kept region of the bucket, so\n // allocating a filtered array per candidate would dominate project-scale runs. Kept regions\n // never overlap each other, so a candidate inside one cannot partially overlap another.\n let containedInKept = false;\n let partiallyOverlaps = false;\n let enclosedKept: T[] | undefined;\n for (const region of keptRegions) {\n if (region.startIndex >= candidate.endIndex || candidate.startIndex >= region.endIndex) {\n continue;\n }\n if (region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex) {\n containedInKept = true;\n break;\n }\n if (encloses && candidate.startIndex <= region.startIndex && region.endIndex <= candidate.endIndex) {\n (enclosedKept ??= []).push(region);\n } else {\n partiallyOverlaps = true;\n if (encloses) {\n const blockers = blockersByFingerprint.get(candidate.fingerprint) ?? new Set();\n blockers.add(region.fingerprint);\n blockersByFingerprint.set(candidate.fingerprint, blockers);\n }\n break;\n }\n }\n if (containedInKept) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n continue;\n }\n if (partiallyOverlaps) {\n leftOver.push(candidate);\n continue;\n }\n // The kept copies of smaller groups that the candidate encloses become nested copies of\n // their groups.\n const enclosed = enclosedKept;\n for (const inner of enclosed ?? []) {\n const group = counted.get(inner.fingerprint) ?? [];\n const index = group.indexOf(inner);\n if (index !== -1) {\n group[index] = { ...inner, nestedInLargerGroup: true };\n }\n }\n // The enclosed regions give way to the enclosing one, keeping kept regions mutually\n // non-overlapping: a later candidate inside this region must see it, not a region it\n // swallowed (which the candidate could straddle instead).\n const occupied = enclosed ? keptRegions.filter((region) => !enclosed.includes(region)) : keptRegions;\n occupied.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, occupied);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n }\n // A candidate still left over overlapped a kept region partially when its turn came. Where an\n // enclosing candidate took that region since, the candidate lies inside a kept region now and\n // is a nested copy like one visited later: containment must not depend on the greedy order.\n for (const candidate of leftOver) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex)\n ) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n }\n }\n // Nested copies join only a group that kept a standalone copy; on their own they would merely\n // restate the larger group.\n for (const [fingerprint, nested] of nestedByFingerprint) {\n counted.get(fingerprint)?.push(...nested);\n }\n for (const [fingerprint, group] of counted) {\n if (group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n }\n }\n\n // A failed group that another failed group kept a copy from is not shed before it: shedding\n // the blocking group can rescue it, while shedding it frees nothing the other waits for.\n const failed = (fingerprint: string): boolean => {\n const group = counted.get(fingerprint);\n return group !== undefined && !isSurvivingGroup(group);\n };\n let failedFingerprint: string | undefined;\n let failedRank = -1;\n for (const [fingerprint, group] of counted) {\n if (isSurvivingGroup(group)) {\n continue;\n }\n const blockedByFailed = [...(blockersByFingerprint.get(fingerprint) ?? [])].some(\n (other) => other !== fingerprint && failed(other)\n );\n // Unblocked groups first, then the largest.\n const rank = (blockedByFailed ? 0 : 2 ** 40) + (group[0]?.tokenCount ?? 0);\n if (rank > failedRank) {\n failedFingerprint = fingerprint;\n failedRank = rank;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= 2 * maxSelectionRerunCount) {\n dropFailedGroups(counted, isSurvivingGroup);\n return counted;\n }\n\n const failedFingerprints = new Set(\n rerun < maxSelectionRerunCount\n ? [failedFingerprint]\n : [...counted].filter(([, group]) => !isSurvivingGroup(group)).map(([fingerprint]) => fingerprint)\n );\n duplicates = duplicates.filter((candidate) => !failedFingerprints.has(candidate.fingerprint));\n }\n}\n\n/**\n * Past the rerun caps, still-failing groups are dropped without another selection pass. A dropped\n * group's regions may have been what nested copies of surviving groups lay inside, and such a copy\n * would then be counted by no group at all, so those copies are dropped too and the shrunk groups\n * are re-checked until nothing changes.\n */\nfunction dropFailedGroups<T extends SelectableRegion>(\n counted: Map<string, T[]>,\n isSurvivingGroup: (group: T[]) => boolean\n): void {\n for (let changed = true; changed;) {\n changed = false;\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group) || group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n changed = true;\n }\n }\n const standalone = [...counted.values()].flat().filter((candidate) => !candidate.nestedInLargerGroup);\n for (const [fingerprint, group] of counted) {\n const kept = group.filter(\n (candidate) =>\n !candidate.nestedInLargerGroup ||\n standalone.some(\n (region) =>\n (region.regionBucket ?? 0) === (candidate.regionBucket ?? 0) &&\n region.startIndex <= candidate.startIndex &&\n candidate.endIndex <= region.endIndex\n )\n );\n if (kept.length !== group.length) {\n counted.set(fingerprint, kept);\n changed = true;\n }\n }\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplicateSelection.js","names":["selectMaximalGroups","byFingerprint","candidate","candidates","group","groups","dedupeByRegion","isSurvivingGroup","groupSizeByFingerprint","coverage","duplicates","right","left","compareTies","rerun","keptRegionsByBucket","counted","nestedByFingerprint","leftOver","blockersByFingerprint","encloses","keptRegions","containedInKept","partiallyOverlaps","enclosedKept","region","blockers","nested","enclosed","inner","index","occupied","fingerprint","failed","failedFingerprint","failedRank","rank","other","dropFailedGroups","failedFingerprints","changed","standalone","kept","byRegion","key","existing"],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection for the cross-file detector (the native\n * within-file detector ports it as select_maximal_duplicates). Candidates are grouped by\n * fingerprint, ranked by total coverage, kept greedily without overlapping a kept region, and\n * groups that fall below the survivor requirement are shed (largest first) so their regions stop\n * blocking smaller groups. A copy lying entirely inside a larger group's region stays with its\n * group as a nested copy, so a standalone copy elsewhere is still reported as duplicating it.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /** Regions can only overlap within the same bucket: the file index. */\n regionBucket?: number;\n /**\n * Set by selectMaximalGroups on a copy nested inside a larger group's region: it is reported with\n * its group, but its span is already counted by that larger group.\n */\n nestedInLargerGroup?: boolean;\n}\n\n/**\n * Caps how often the maximal-region selection reruns after shedding one failed duplicate group,\n * and then how often after shedding every failed group at once.\n */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. Past the rerun cap, which bounds degenerate inputs, every failed group is\n // removed at once, which converges in few passes; past the same cap again the remaining failed\n // groups are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, T[]>();\n const counted = new Map<string, T[]>();\n const nestedByFingerprint = new Map<string, T[]>();\n // Two passes. The first keeps each candidate that overlaps no kept region. The second lets a\n // candidate left over take the place of the kept copies it encloses, unless it partially\n // overlaps a kept region: a larger clone must not displace a clone of the first pass it merely\n // overlaps (one extending a kept run by a few tokens would hide the clone that follows the run).\n let leftOver: T[] = [];\n // Per fingerprint, the fingerprints of the kept regions that kept a candidate of it out.\n const blockersByFingerprint = new Map<string, Set<string>>();\n for (const encloses of [false, true]) {\n const candidates = encloses ? leftOver : duplicates;\n leftOver = [];\n for (const candidate of candidates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n // A plain loop: this runs once per candidate over every kept region of the bucket, so\n // allocating a filtered array per candidate would dominate project-scale runs. Kept regions\n // never overlap each other, so a candidate inside one cannot partially overlap another.\n let containedInKept = false;\n let partiallyOverlaps = false;\n let enclosedKept: T[] | undefined;\n for (const region of keptRegions) {\n if (region.startIndex >= candidate.endIndex || candidate.startIndex >= region.endIndex) {\n continue;\n }\n if (region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex) {\n containedInKept = true;\n break;\n }\n if (encloses && candidate.startIndex <= region.startIndex && region.endIndex <= candidate.endIndex) {\n (enclosedKept ??= []).push(region);\n } else {\n partiallyOverlaps = true;\n if (encloses) {\n const blockers = blockersByFingerprint.get(candidate.fingerprint) ?? new Set();\n blockers.add(region.fingerprint);\n blockersByFingerprint.set(candidate.fingerprint, blockers);\n }\n break;\n }\n }\n if (containedInKept) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n continue;\n }\n if (partiallyOverlaps) {\n leftOver.push(candidate);\n continue;\n }\n // The kept copies of smaller groups that the candidate encloses become nested copies of\n // their groups.\n const enclosed = enclosedKept;\n for (const inner of enclosed ?? []) {\n const group = counted.get(inner.fingerprint) ?? [];\n const index = group.indexOf(inner);\n if (index !== -1) {\n group[index] = { ...inner, nestedInLargerGroup: true };\n }\n }\n // The enclosed regions give way to the enclosing one, keeping kept regions mutually\n // non-overlapping: a later candidate inside this region must see it, not a region it\n // swallowed (which the candidate could straddle instead).\n const occupied = enclosed ? keptRegions.filter((region) => !enclosed.includes(region)) : keptRegions;\n occupied.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, occupied);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n }\n // A candidate still left over overlapped a kept region partially when its turn came. Where an\n // enclosing candidate took that region since, the candidate lies inside a kept region now and\n // is a nested copy like one visited later: containment must not depend on the greedy order.\n for (const candidate of leftOver) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex <= candidate.startIndex && candidate.endIndex <= region.endIndex)\n ) {\n const nested = nestedByFingerprint.get(candidate.fingerprint) ?? [];\n nested.push({ ...candidate, nestedInLargerGroup: true });\n nestedByFingerprint.set(candidate.fingerprint, nested);\n }\n }\n // Nested copies join only a group that kept a standalone copy; on their own they would merely\n // restate the larger group.\n for (const [fingerprint, nested] of nestedByFingerprint) {\n counted.get(fingerprint)?.push(...nested);\n }\n for (const [fingerprint, group] of counted) {\n if (group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n }\n }\n\n // A failed group that another failed group kept a copy from is not shed before it: shedding\n // the blocking group can rescue it, while shedding it frees nothing the other waits for.\n const failed = (fingerprint: string): boolean => {\n const group = counted.get(fingerprint);\n return group !== undefined && !isSurvivingGroup(group);\n };\n let failedFingerprint: string | undefined;\n let failedRank = -1;\n for (const [fingerprint, group] of counted) {\n if (isSurvivingGroup(group)) {\n continue;\n }\n const blockedByFailed = [...(blockersByFingerprint.get(fingerprint) ?? [])].some(\n (other) => other !== fingerprint && failed(other)\n );\n // Unblocked groups first, then the largest.\n const rank = (blockedByFailed ? 0 : 2 ** 40) + (group[0]?.tokenCount ?? 0);\n if (rank > failedRank) {\n failedFingerprint = fingerprint;\n failedRank = rank;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= 2 * maxSelectionRerunCount) {\n dropFailedGroups(counted, isSurvivingGroup);\n return counted;\n }\n\n const failedFingerprints = new Set(\n rerun < maxSelectionRerunCount\n ? [failedFingerprint]\n : [...counted].filter(([, group]) => !isSurvivingGroup(group)).map(([fingerprint]) => fingerprint)\n );\n duplicates = duplicates.filter((candidate) => !failedFingerprints.has(candidate.fingerprint));\n }\n}\n\n/**\n * Past the rerun caps, still-failing groups are dropped without another selection pass. A dropped\n * group's regions may have been what nested copies of surviving groups lay inside, and such a copy\n * would then be counted by no group at all, so those copies are dropped too and the shrunk groups\n * are re-checked until nothing changes.\n */\nfunction dropFailedGroups<T extends SelectableRegion>(\n counted: Map<string, T[]>,\n isSurvivingGroup: (group: T[]) => boolean\n): void {\n for (let changed = true; changed;) {\n changed = false;\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group) || group.every((candidate) => candidate.nestedInLargerGroup)) {\n counted.delete(fingerprint);\n changed = true;\n }\n }\n const standalone = [...counted.values()].flat().filter((candidate) => !candidate.nestedInLargerGroup);\n for (const [fingerprint, group] of counted) {\n const kept = group.filter(\n (candidate) =>\n !candidate.nestedInLargerGroup ||\n standalone.some(\n (region) =>\n (region.regionBucket ?? 0) === (candidate.regionBucket ?? 0) &&\n region.startIndex <= candidate.startIndex &&\n candidate.endIndex <= region.endIndex\n )\n );\n if (kept.length !== group.length) {\n counted.set(fingerprint, kept);\n changed = true;\n }\n }\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"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"}