code-gauge 10.2.0 → 10.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 extends Alignment {\n left: number;\n right: number;\n cores?: CorePair;\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. A pair of two spans that both overlap reported spans is dropped before clustering (blocks\n * wholly covered by reported spans are not even compared), so it neither collapses the blocks' core\n * nodes nor widens a node beyond what the pairs clustering it matched, and a group needs at least\n * one non-anchor node. A block that matched only locally is reported as its matched cores\n * (overlapping cores merged), each clustered with its own partners, so code no verified pair\n * matched never counts as duplicated.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minTokens: number,\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const matcher = createMatcher(blocks, minTokens, minSimilarityPercent);\n const overlapsReportedSpan = reportedSpansByFile.map(createOverlapTest);\n const coveredByReportedSpans = reportedSpansByFile.map(createCoverageTest);\n const fullyReported = blocks.map(({ fileIndex, range }) => coveredByReportedSpans[fileIndex]?.(range) ?? false);\n const touchesReported = blocks.map(({ fileIndex, range }) => overlapsReportedSpan[fileIndex]?.(range) ?? false);\n const touchesCore = ({ fileIndex }: NormalizedBlock, [startTokenIndex, endTokenIndex]: [number, number]): boolean =>\n overlapsReportedSpan[fileIndex]?.({ startTokenIndex, endTokenIndex }) ?? false;\n const edges: MatchEdge[] = [];\n forEachCandidatePair(blocks, fullyReported, minSimilarityPercent, (left, right) => {\n const leftBlock = blocks[left];\n const rightBlock = blocks[right];\n const match = leftBlock && rightBlock && matcher(leftBlock, rightBlock, right);\n if (!match) {\n return;\n }\n if (!match.cores) {\n if (!(touchesReported[left] && touchesReported[right])) {\n edges.push({ left, right, reordered: match.reordered });\n }\n return;\n }\n for (const { pair, reordered } of match.cores) {\n if (!(touchesCore(leftBlock, pair[0]) && touchesCore(rightBlock, pair[1]))) {\n edges.push({ left, right, cores: pair, reordered });\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 anchored = nodes.map(({ blockIndex, core }) => {\n const block = blocks[blockIndex];\n const [startTokenIndex, endTokenIndex] = core ?? [\n block?.range.startTokenIndex ?? 0,\n block?.range.endTokenIndex ?? 0,\n ];\n return overlapsReportedSpan[block?.fileIndex ?? 0]?.({ startTokenIndex, endTokenIndex }) ?? false;\n });\n 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 = alignPairs(blocks, edges, edgeNodes, nodes.length);\n\n const membersByRoot = new Map<number, number[]>();\n for (const node of nodes.keys()) {\n const root = find(node);\n const members = membersByRoot.get(root) ?? [];\n members.push(node);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((node) => anchored[node])) {\n continue;\n }\n // A group's nodes from one block become ONE occurrence whose segments are its cores, so the\n // fragment-weighted count charges the block as one copy (as for gapped clones), not once per core.\n const coresByBlock = new Map<number, 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\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 * Per node, the token runs its partners match. The lines of a near-miss copy that are duplicated\n * are those a verified partner matches, which takes the pairing itself. Every pair is aligned:\n * verifying it already cost a longest common subsequence of the same sequences, and any choice\n * among a copy's partners would let their number or their order decide what the copy counts.\n */\nfunction alignPairs(\n blocks: NormalizedBlock[],\n edges: MatchEdge[],\n edgeNodes: number[][],\n nodeCount: number\n): [number, number][][] {\n const matchedRunsByNode = Array.from({ length: nodeCount }, (): [number, number][] => []);\n for (const [edgeIndex, edge] of edges.entries()) {\n const left = blocks[edge.left];\n const right = blocks[edge.right];\n if (left && right) {\n const [leftRuns, rightRuns] = alignPair(left, right, edge);\n matchedRunsByNode[edgeNodes[edgeIndex]?.[0] ?? 0]?.push(...leftRuns);\n matchedRunsByNode[edgeNodes[edgeIndex]?.[1] ?? 0]?.push(...rightRuns);\n }\n }\n return matchedRunsByNode;\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 }: MatchEdge\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/** Whether the spans, merged, cover every token of a range. */\nfunction createCoverageTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n // Touching spans merge too: together they cover a range across their boundary.\n const merged: [number, number][] = [];\n for (const { startTokenIndex, endTokenIndex } of spans.toSorted(\n (left, right) => left.startTokenIndex - right.startTokenIndex\n )) {\n const last = merged.at(-1);\n if (last && startTokenIndex <= last[1]) {\n last[1] = Math.max(last[1], endTokenIndex);\n } else {\n merged.push([startTokenIndex, endTokenIndex]);\n }\n }\n return (range) => {\n let low = 0;\n let high = merged.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((merged[middle]?.[0] ?? 0) <= range.startTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return (merged[low - 1]?.[1] ?? -1) >= range.endTokenIndex;\n };\n}\n\n/**\n * Whether a range overlaps any of the spans: among the spans starting before the range ends\n * (binary search over sorted starts), the furthest end reaches past the range's start.\n */\nfunction createOverlapTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n const sorted = spans.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex);\n const maxEndPrefix = new Int32Array(sorted.length);\n let maxEnd = -1;\n for (const [index, span] of sorted.entries()) {\n maxEnd = Math.max(maxEnd, span.endTokenIndex);\n maxEndPrefix[index] = maxEnd;\n }\n return (range) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < range.endTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low > 0 && (maxEndPrefix[low - 1] ?? -1) > range.startTokenIndex;\n };\n}\n\n/**\n * The block's occurrence with one segment per entry of `cores` (the whole block for `undefined`,\n * a whole match). Source offsets stay the block's: tokens carry none, and near-miss occurrences\n * report lines only.\n */\nfunction toOccurrence(\n { fileIndex, range }: NormalizedBlock,\n files: NearMissSourceFile[],\n { cores, anchor, matchedRuns }: BlockCopy\n): NearMissOccurrence {\n const whole = cores.includes(undefined);\n const segments = cores\n .map((core): [number, number] => core ?? [range.startTokenIndex, range.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0])\n .map(([startTokenIndex, endTokenIndex]) => ({ startTokenIndex, endTokenIndex }));\n const start = segments[0]?.startTokenIndex ?? range.startTokenIndex;\n const end = segments.at(-1)?.endTokenIndex ?? range.endTokenIndex;\n const tokens = files[fileIndex]?.tokens;\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments,\n matchedRuns: mergeOverlappingCores(matchedRuns).map(([startTokenIndex, endTokenIndex]): TokenSegment => ({\n startTokenIndex,\n endTokenIndex,\n })),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: whole ? range.startLine : (tokens?.[start]?.startRow ?? 0) + 1,\n endLine: whole ? range.endLine : (tokens?.[end - 1]?.endRow ?? 0) + 1,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two blocks wholly covered by reported spans and pairs whose\n * length ratio rules out both\n * whole-block similarity and `maxLengthRatio`. Blocks are\n * indexed in ascending length, so each posting list is scanned backwards only while its blocks\n * are long enough; shared counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n fullyReported: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const reportedFlags = Uint8Array.from(fullyReported, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightReported = reportedFlags[right] === 1;\n const minLeftLength = Math.min(\n Math.ceil((lengths[right] ?? 0) / maxLengthRatio),\n Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100)\n );\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightReported && reportedFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Returns the pair verifier. Content symbols are weighted by integer self-information,\n * 1 + floor(log2((N + 1) / df)) over N blocks capped at `maxContentWeight`, so rare names and\n * values (the logic a copy preserves) outweigh ubiquitous ones, following the\n * information-theoretic weighting of ECScan's essence-clone detection (2025).\n */\nfunction createMatcher(\n blocks: NormalizedBlock[],\n minTokens: number,\n minSimilarityPercent: number\n): (left: NormalizedBlock, right: NormalizedBlock, rightIndex: number) => PairMatch | undefined {\n const documentFrequencies = new Map<number, number>();\n for (const block of blocks) {\n for (const symbol of block.contentCounts.keys()) {\n documentFrequencies.set(symbol, (documentFrequencies.get(symbol) ?? 0) + 1);\n }\n }\n const selfInformation = (documentFrequency: number): number =>\n Math.min(31 - Math.clz32(Math.floor((blocks.length + 1) / documentFrequency)) + 1, maxContentWeight);\n const weights = new Map<number, number>();\n for (const [symbol, frequency] of documentFrequencies) {\n weights.set(symbol, selfInformation(frequency));\n }\n const weigh = (counts: Map<number, number>): WeightedContent => {\n const symbols = Int32Array.from(counts.keys()).toSorted();\n // Span content comes from blocks, so every symbol has a weight.\n const weightedCounts = Int32Array.from(symbols, (symbol) => (counts.get(symbol) ?? 0) * (weights.get(symbol) ?? 0));\n let total = 0;\n for (const count of weightedCounts) {\n total += count;\n }\n return { symbols, weightedCounts, total };\n };\n const blockContents = new Map(blocks.map((block) => [block, weigh(block.contentCounts)]));\n\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: NormalizedBlock, rightIndex: number, sequence: Int32Array): number => {\n if (counterBlock !== rightIndex || !counter) {\n counterBlock = rightIndex;\n counter = createLcsLengthCounter(right.sequence);\n }\n return counter(sequence);\n };\n\n /**\n * Matches the cores two blocks share inside different surroundings (a copy wrapped in added\n * code, or two copies embedded in different code), which whole-block similarity misses\n * (CCAligner's large-gap and LVMapper's large-variance clones). N-grams unique to each block\n * anchor the alignment; their longest chain increasing in both blocks (a run filter keeps only\n * anchors continuing a diagonal, but the chain may shift diagonals at small insertions), split at\n * gaps, delimits the cores, and every core pair that is a near-miss clone in its own right is\n * returned.\n */\n const matchLocally = (left: NormalizedBlock, right: NormalizedBlock): PairMatch | undefined => {\n const anchors: [number, number][] = [];\n for (\n let leftIndex = 0, rightIndex = 0;\n leftIndex < left.uniqueNgrams.length && rightIndex < right.uniqueNgrams.length;\n ) {\n const leftHash = left.uniqueNgrams[leftIndex] ?? 0;\n const rightHash = right.uniqueNgrams[rightIndex] ?? 0;\n if (leftHash === rightHash) {\n anchors.push([left.uniqueNgramOffsets[leftIndex] ?? 0, right.uniqueNgramOffsets[rightIndex] ?? 0]);\n }\n if (leftHash <= rightHash) {\n leftIndex += 1;\n }\n if (rightHash <= leftHash) {\n rightIndex += 1;\n }\n }\n anchors.sort((first, second) => first[0] - second[0]);\n // An isolated 5-gram match is often coincidental (n-grams are identifier-blind); a copied core\n // yields runs of consecutive anchors, so only anchors continuing a diagonal run are chained.\n const runAnchors = anchors.filter(([leftOffset, rightOffset], index) => {\n const previous = anchors[index - 1];\n const next = anchors[index + 1];\n return (\n (previous?.[0] === leftOffset - 1 && previous[1] === rightOffset - 1) ||\n (next?.[0] === leftOffset + 1 && next[1] === rightOffset + 1)\n );\n });\n const leftOffset = left.range.startTokenIndex;\n const rightOffset = right.range.startTokenIndex;\n const cores: (Alignment & { pair: CorePair })[] = [];\n for (const segment of chainSegments(longestIncreasingChain(runAnchors))) {\n const [leftStart, rightStart] = segment[0] ?? [0, 0];\n const [leftLast, rightLast] = segment.at(-1) ?? [0, 0];\n const leftEnd = leftLast + ngramSize;\n const rightEnd = rightLast + ngramSize;\n const leftLength = leftEnd - leftStart;\n const rightLength = rightEnd - rightStart;\n const shorter = Math.min(leftLength, rightLength);\n const required = minSimilarityPercent * Math.max(leftLength, rightLength);\n if (\n shorter < minTokens ||\n shorter * 100 < required ||\n anchoredTokenCount(segment) * 100 < minAnchorCoveragePercent * shorter ||\n !sharesContent(\n weigh(countContent(left.symbols, left.isContent, leftStart, leftEnd)),\n weigh(countContent(right.symbols, right.isContent, rightStart, rightEnd))\n )\n ) {\n continue;\n }\n const coreLcsLength = lcsLength(\n anonymize(left.symbols.subarray(leftStart, leftEnd)),\n anonymize(right.symbols.subarray(rightStart, rightEnd))\n );\n if (coreLcsLength * 100 >= required) {\n cores.push({\n pair: [\n [leftOffset + leftStart, leftOffset + leftEnd],\n [rightOffset + rightStart, rightOffset + rightEnd],\n ],\n reordered: false,\n });\n }\n }\n return cores.length > 0 ? { cores } : undefined;\n };\n\n /** Cheapest bounds first: the LCS cannot exceed the shorter block's length nor the bag overlap. */\n return (left, right, rightIndex) => {\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (\n Math.min(left.sequence.length, right.sequence.length) * 100 >= required &&\n sharesContent(blockContents.get(left), blockContents.get(right))\n ) {\n if (sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 >= required) {\n const inOrder = lcsLengthWithRight(right, rightIndex, left.sequence);\n if (inOrder * 100 >= required) {\n return { reordered: false };\n }\n }\n // With their top-level statements (each anonymized on its own) in a canonical order, a copy\n // whose independent statements were swapped still matches.\n if (left.canonical && right.canonical) {\n const reordered = lcsLength(left.canonical.sequence, right.canonical.sequence);\n if (reordered * 100 >= required) {\n return { reordered: true };\n }\n }\n }\n // Of the equally long chains of anchors, the one kept depends on which block comes first, so\n // the blocks are taken in the order of their content rather than of their files.\n if (compareSequences(right.writtenContent, left.writtenContent) < 0) {\n const swapped = matchLocally(right, left);\n return (\n swapped?.cores && {\n cores: swapped.cores.map(({ pair: [rightCore, leftCore], ...alignment }) => ({\n ...alignment,\n pair: [leftCore, rightCore],\n })),\n }\n );\n }\n return matchLocally(left, right);\n };\n}\n\n/** Content-bearing symbols, sorted, with their information-weighted counts. */\ninterface WeightedContent {\n symbols: Int32Array;\n weightedCounts: Int32Array;\n total: number;\n}\n\n/**\n * A structural match must be backed by shared content: more than `minContentSimilarityPercent` of\n * the larger side's information-weighted names and literal values. Two sides without content never\n * pass.\n */\nfunction sharesContent(left: WeightedContent | undefined, right: WeightedContent | undefined): boolean {\n if (!left || !right) {\n return false;\n }\n let overlap = 0;\n for (let leftIndex = 0, rightIndex = 0; leftIndex < left.symbols.length && rightIndex < right.symbols.length;) {\n const leftSymbol = left.symbols[leftIndex] ?? 0;\n const rightSymbol = right.symbols[rightIndex] ?? 0;\n if (leftSymbol === rightSymbol) {\n overlap += Math.min(left.weightedCounts[leftIndex] ?? 0, right.weightedCounts[rightIndex] ?? 0);\n }\n if (leftSymbol <= rightSymbol) {\n leftIndex += 1;\n }\n if (rightSymbol <= leftSymbol) {\n rightIndex += 1;\n }\n }\n return overlap * 100 > minContentSimilarityPercent * Math.max(left.total, right.total);\n}\n\n/**\n * The block's units (its top-level statements, given in file token indexes, and the token runs\n * between them), each anonymized on its own and sorted, concatenated, with the block offset of\n * every token; undefined with too few statements. `tokenKeys` holds the content key of each\n * non-identifier token of the block.\n */\nfunction canonicalSequenceOf(\n symbols: Int32Array,\n tokenKeys: Float64Array,\n statements: [number, number][],\n blockStart: number\n): NormalizedBlock['canonical'] {\n if (statements.length < minReorderStatementCount) {\n return undefined;\n }\n const units: { unit: Int32Array; content: Float64Array; start: number }[] = [];\n const pushUnit = (start: number, end: number): void => {\n const unit = anonymize(symbols.subarray(start, end));\n // Ordered by the content of their tokens: symbols, numbered by first occurrence, would order\n // the units by where other code stands.\n const content = Float64Array.from(unit, (symbol, index) => (symbol < 0 ? symbol : (tokenKeys[start + index] ?? 0)));\n units.push({ unit, content, start });\n };\n let cursor = 0;\n for (const [statementStart, statementEnd] of statements) {\n const start = statementStart - blockStart;\n if (cursor < start) {\n pushUnit(cursor, start);\n }\n pushUnit(start, statementEnd - blockStart);\n cursor = statementEnd - blockStart;\n }\n if (cursor < symbols.length) {\n pushUnit(cursor, symbols.length);\n }\n units.sort((left, right) => compareSequences(left.content, right.content) || left.start - right.start);\n const sequence = new Int32Array(symbols.length);\n const offsets = new Int32Array(symbols.length);\n let position = 0;\n for (const { unit, start } of units) {\n sequence.set(unit, position);\n for (let index = 0; index < unit.length; index += 1) {\n offsets[position + index] = start + index;\n }\n position += unit.length;\n }\n return { sequence, offsets };\n}\n\n/** Lexicographic order. */\nfunction compareSequences(left: Float64Array, right: Float64Array): number {\n for (let index = 0; index < Math.min(left.length, right.length); index += 1) {\n const difference = (left[index] ?? 0) - (right[index] ?? 0);\n if (difference !== 0) {\n return difference;\n }\n }\n return left.length - right.length;\n}\n\n/**\n * The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),\n * via patience sorting over right offsets.\n */\nfunction longestIncreasingChain(anchors: [number, number][]): [number, number][] {\n const tailIndexes: number[] = [];\n const predecessors: number[] = [];\n for (const [index, [, rightOffset]] of anchors.entries()) {\n let low = 0;\n let high = tailIndexes.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((anchors[tailIndexes[middle] ?? 0]?.[1] ?? 0) < rightOffset) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n predecessors.push(low > 0 ? (tailIndexes[low - 1] ?? -1) : -1);\n tailIndexes[low] = index;\n }\n const chain: [number, number][] = [];\n for (let cursor = tailIndexes.at(-1) ?? -1; cursor >= 0; cursor = predecessors[cursor] ?? -1) {\n const anchor = anchors[cursor];\n if (anchor) {\n chain.push(anchor);\n }\n }\n return chain.toReversed();\n}\n\n/** The chain's segments, split where consecutive anchors lie more than `maxAnchorGapTokens` apart in either block. */\nfunction chainSegments(chain: [number, number][]): [number, number][][] {\n const segments: [number, number][][] = [];\n for (const [index, anchor] of chain.entries()) {\n const previous = chain[index - 1];\n const continues =\n previous !== undefined &&\n anchor[0] - (previous[0] + ngramSize) <= maxAnchorGapTokens &&\n anchor[1] - (previous[1] + ngramSize) <= maxAnchorGapTokens;\n if (continues) {\n segments.at(-1)?.push(anchor);\n } else {\n segments.push([anchor]);\n }\n }\n return segments;\n}\n\n/** Left-block tokens the segment's anchors cover (overlapping anchors count once). */\nfunction anchoredTokenCount(segment: [number, number][]): number {\n let count = ngramSize;\n for (let index = 1; index < segment.length; index += 1) {\n count += Math.min((segment[index]?.[0] ?? 0) - (segment[index - 1]?.[0] ?? 0), ngramSize);\n }\n return count;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector. Non-identifier symbols are interned\n * project-wide from the tokens' hash pairs, so equal tokens compare equal across files, while\n * identifiers are interned per file and re-anonymized per compared range.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, containerStatements, nearMissBlocks }] of files.entries()) {\n if (!tokens || !nearMissBlocks?.length) {\n continue;\n }\n const symbols = new Int32Array(tokens.length);\n const isContent = new Uint8Array(tokens.length);\n const fileTokenKeys = new Float64Array(tokens.length);\n const idByIdentifier = new Map<string, number>();\n for (const [index, token] of tokens.entries()) {\n if (token.kind === 'id') {\n let id = idByIdentifier.get(token.text);\n if (id === undefined) {\n id = idByIdentifier.size;\n idByIdentifier.set(token.text, id);\n }\n symbols[index] = -(id + 1);\n continue;\n }\n const key = tokenKey(token);\n fileTokenKeys[index] = key;\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n symbols[index] = symbol;\n isContent[index] = token.isName || token.literalHash !== undefined ? 1 : 0;\n }\n const findStatements = createTopLevelStatementFinder(containerStatements ?? []);\n for (const range of nearMissBlocks) {\n const { startTokenIndex: start, endTokenIndex: end } = range;\n const blockSymbols = symbols.subarray(start, end);\n const blockIsContent = isContent.subarray(start, end);\n const sequence = anonymize(blockSymbols);\n const ngramHashes = collectNgramHashes(blockSymbols, fileTokenKeys.subarray(start, end));\n const occurrenceCounts = new Map<number, number>();\n for (const hash of ngramHashes) {\n occurrenceCounts.set(hash, (occurrenceCounts.get(hash) ?? 0) + 1);\n }\n const uniqueOffsets = ngramHashes\n .keys()\n .filter((offset) => occurrenceCounts.get(ngramHashes[offset] ?? 0) === 1)\n .toArray()\n .toSorted((first, second) => (ngramHashes[first] ?? 0) - (ngramHashes[second] ?? 0));\n blocks.push({\n fileIndex,\n range,\n symbols: blockSymbols,\n isContent: blockIsContent,\n fileTokenKeys,\n sequence,\n writtenContent: Float64Array.from(sequence, (symbol, index) =>\n symbol < 0 ? symbol : (fileTokenKeys[start + index] ?? 0)\n ),\n sortedSequence: sequence.toSorted(),\n ngrams: Int32Array.from(occurrenceCounts.keys()),\n uniqueNgrams: Int32Array.from(uniqueOffsets, (offset) => ngramHashes[offset] ?? 0),\n uniqueNgramOffsets: Int32Array.from(uniqueOffsets),\n contentCounts: countContent(blockSymbols, blockIsContent, 0, blockSymbols.length),\n canonical: canonicalSequenceOf(\n blockSymbols,\n fileTokenKeys.subarray(start, end),\n findStatements(start, end),\n start\n ),\n });\n }\n }\n return blocks;\n}\n\n/**\n * Returns a lookup of the outermost container statements inside a token range, excluding a\n * statement spanning the whole range (the block itself).\n */\nfunction createTopLevelStatementFinder(\n containerStatements: TokenRange[][]\n): (start: number, end: number) => [number, number][] {\n const statements = containerStatements\n .flat()\n .filter((statement) => statement.startTokenIndex < statement.endTokenIndex)\n .map((statement): [number, number] => [statement.startTokenIndex, statement.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0] || right[1] - left[1]);\n return (start, end) => {\n let low = 0;\n let high = statements.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((statements[middle]?.[0] ?? 0) < start) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const topLevel: [number, number][] = [];\n for (let index = low; index < statements.length; index += 1) {\n const statement = statements[index];\n if (!statement || statement[0] >= end) {\n break;\n }\n const last = topLevel.at(-1);\n const nested = last !== undefined && statement[0] < last[1];\n if (statement[1] <= end && !(statement[0] === start && statement[1] === end) && !nested) {\n topLevel.push(statement);\n }\n }\n return topLevel;\n };\n}\n\nfunction countContent(symbols: Int32Array, isContent: Uint8Array, start: number, end: number): Map<number, number> {\n const counts = new Map<number, number>();\n for (let offset = start; offset < end; offset += 1) {\n if (isContent[offset] === 1) {\n const symbol = symbols[offset] ?? 0;\n counts.set(symbol, (counts.get(symbol) ?? 0) + 1);\n }\n }\n return counts;\n}\n\n/** Identifiers renumbered by first occurrence within `symbols`, so a range compares the same wherever it sits in its file. */\nfunction anonymize(symbols: Int32Array): Int32Array {\n const indexByIdentifier = new Map<number, number>();\n return symbols.map((symbol) => {\n if (symbol >= 0) {\n return symbol;\n }\n let index = indexByIdentifier.get(symbol);\n if (index === undefined) {\n index = indexByIdentifier.size;\n indexByIdentifier.set(symbol, index);\n }\n return -(index + 1);\n });\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\n/**\n * N-gram hash per start offset, identifier-blind (every identifier hashes as -1) so a block copied\n * into different surroundings (renumbering its identifiers) or with reordered statements still\n * shares its n-grams. Hashed from the content keys of the tokens: with symbols, numbered by first\n * occurrence, which n-grams collide would depend on the order of the files.\n */\nfunction collectNgramHashes(symbols: Int32Array, tokenKeys: Float64Array): Int32Array {\n const hashes = new Int32Array(Math.max(symbols.length - ngramSize + 1, 0));\n for (let start = 0; start < hashes.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n const tokenKey = tokenKeys[start + offset] ?? 0;\n // A token key holds 53 bits; both halves enter the hash.\n const value = (symbols[start + offset] ?? 0) < 0 ? -1 : tokenKey ^ Math.floor(tokenKey / 0x1_00_00_00_00);\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32.\n hash = (Math.imul(hash, 31) + value) | 0;\n }\n hashes[start] = hash;\n }\n return hashes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"crossFileNearMiss.js","names":[],"sources":["../src/crossFileNearMiss.ts"],"sourcesContent":["import {\n createLcsLengthCounter,\n lcsLength,\n markLcs,\n type CountedOccurrence,\n type Token,\n type TokenRange,\n type TokenSegment,\n} from './duplication.js';\n\n/**\n * Cross-file near-miss (Type-3) clone detection, following the within-file detector's model\n * (native/src/near_miss.rs): candidate block pairs are filtered through an n-gram inverted index\n * (NIL, Nakagawa et al. 2021), then verified by token-level longest common subsequence against the\n * larger block (NiCad's per-fragment similarity), backed by an information-weighted content gate,\n * with a statement-order-insensitive fallback and a local match over the anchored cores of two\n * blocks. Only pairs of blocks in different files are compared: a same-file pair is the\n * within-file detector's concern.\n */\n\nexport interface NearMissSourceFile {\n tokens?: Token[];\n containerStatements?: TokenRange[][];\n nearMissBlocks?: TokenRange[];\n}\n\n/** One copy (a block or its matched cores) in a near-miss group; anchors carry `spanCountedElsewhere` (see collectCrossFileNearMissGroups). */\nexport interface NearMissOccurrence extends CountedOccurrence {\n fileIndex: number;\n}\n\n/** N-gram size of the candidate index and local-match anchors (NIL's default). */\nconst ngramSize = 5;\n/** Filtration threshold: shared distinct n-grams over the smaller block's (NIL's default). */\nconst filtrationPercent = 10;\n/**\n * Pairs whose longer block exceeds this multiple of the shorter are compared only when whole-block\n * similarity still allows their ratio (below a minSimilarityPercent of 34). The candidate scan\n * stops at this floor while walking length-ordered postings, so pairs of very different lengths are\n * neither counted nor verified; `maxNgramBlockFrequency` is what bounds the scan's total cost.\n */\nconst maxLengthRatio = 3;\n/**\n * A structural match must also share content: more than this percent of the larger side's\n * information-weighted content-bearing tokens (names and literal values), so blocks of the same\n * shape that call different APIs on different data are not clones.\n */\nconst minContentSimilarityPercent = 50;\n/**\n * Caps content weights so only names and values spread over more than a quarter of the blocks are\n * discounted: a family of copies shares its content across several blocks, and uncapped rarity\n * weighting would let each copy's few unique edits outweigh everything the family shares.\n */\nconst maxContentWeight = 3;\n/**\n * Anchors must cover at least this percent of the shorter core: sparser chains are coincidental\n * runs of common n-grams in merely similar-looking code, and the cheap bound spares their content\n * and LCS checks. Not the similarity threshold itself, since n-grams repeated within a block\n * (repetitive statements) never anchor.\n */\nconst minAnchorCoveragePercent = 50;\n/** Anchors farther apart than this (in either block) split a local match into separate chains. */\nconst maxAnchorGapTokens = 30;\n/** Statement-order-insensitive comparison needs this many top-level statements per block. */\nconst minReorderStatementCount = 2;\n/**\n * N-grams occurring in more blocks than this are stop n-grams (syntax boilerplate such as a chain\n * of closing braces), left out of the index and of each block's n-gram count. Counting shared\n * n-grams costs the square of an n-gram's block frequency, so without the cap a project's most\n * common n-grams make filtration quadratic in the block count, while they discriminate nothing.\n */\nconst maxNgramBlockFrequency = 1000;\n\ninterface NormalizedBlock {\n fileIndex: number;\n range: TokenRange;\n /** Interned non-identifier symbols (>= 0) and identifiers as -(file-level id + 1). */\n symbols: Int32Array;\n isContent: Uint8Array;\n /**\n * Per token of the block's file, a key of its content for a non-identifier: unlike a symbol,\n * numbered by first occurrence, it does not depend on the order of the files.\n */\n fileTokenKeys: Float64Array;\n /** Identifiers anonymized by first occurrence within the block. */\n sequence: Int32Array;\n /**\n * The content of the block as written: per token its content key, or the number of its\n * identifier. Equal for equal content wherever it lies, which symbols, numbered by first\n * occurrence, are not; ordered like `written_content` in native/src/near_miss.rs, so that both\n * detectors take the sides of a pair in the same order.\n */\n writtenContent: Float64Array;\n /** The sequence sorted, for the token-bag upper bound on the LCS. */\n sortedSequence: Int32Array;\n /** Distinct non-stop n-gram hashes. */\n ngrams: Int32Array;\n /**\n * The n-grams occurring exactly once in the block, sorted, with their offsets in the parallel\n * array: two blocks' local-match anchors intersect by merging.\n */\n uniqueNgrams: Int32Array;\n uniqueNgramOffsets: Int32Array;\n contentCounts: Map<number, number>;\n /**\n * The sequence with its top-level statements in canonical order and the block offset each of its\n * tokens comes from, when the block has enough statements for statement-order-insensitive\n * comparison.\n */\n canonical: { sequence: Int32Array; offsets: Int32Array } | undefined;\n}\n\n/** A verified core in each block of a pair, as file token ranges. */\ntype CorePair = [[number, number], [number, number]];\n\n/** What alignPair needs to recover the tokens a verified match pairs. */\ninterface Alignment {\n /** Whether the blocks matched only with their statements in canonical order. */\n reordered: boolean;\n}\n\n/** How a verified pair matched: whole blocks, or every anchored core pair (one per gap-split chain segment). */\ntype PairMatch = (Alignment & { cores?: undefined }) | { cores: (Alignment & { pair: CorePair })[] };\n\n/** A verified near-miss pair of blocks; without `cores`, the blocks match as wholes. */\ninterface MatchEdge {\n left: number;\n right: number;\n cores?: CorePair;\n /** The token runs the pair matches, on the left and on the right. */\n matchedRuns: [[number, number][], [number, number][]];\n}\n\n/**\n * Clusters verified cross-file near-miss pairs into groups. A node (a whole block or a matched\n * core) overlapping an occurrence of `reportedSpansByFile` (the exact cross-file groups) is an\n * anchor: it links near-miss copies to the content an exact group already reports, and appears in\n * the near-miss group marked `spanCountedElsewhere` so block counting does not count its span\n * twice; it still counts the lines its partners match outside those spans. A pair matching\n * nothing outside reported spans has nothing to add to them and is dropped before clustering (two\n * blocks wholly covered by reported spans are not even compared), so it neither collapses the\n * blocks' core nodes nor widens a node beyond what the pairs clustering it matched. Any other\n * pair is kept however much of it is reported: what the exact pipeline reports of a copy depends\n * on where its copies stand, and the lines the copy counts must not. A group needs at least one\n * node reported spans do not cover entirely. A block that matched only locally is reported as its matched cores\n * (overlapping cores merged), each clustered with its own partners, so code no verified pair\n * matched never counts as duplicated.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minTokens: number,\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const matcher = createMatcher(blocks, minTokens, minSimilarityPercent);\n const countReported = reportedSpansByFile.map(createReportedTokenCounter);\n const reportedCount = (fileIndex: number, [start, end]: [number, number]): number =>\n countReported[fileIndex]?.(start, end) ?? 0;\n const unreportedCount = (fileIndex: number, runs: [number, number][]): number =>\n runs.reduce((sum, run) => sum + run[1] - run[0] - reportedCount(fileIndex, run), 0);\n const fullyReported = blocks.map((block) => {\n const span = spanOf(block);\n return reportedCount(block.fileIndex, span) === span[1] - span[0];\n });\n const edges: MatchEdge[] = [];\n forEachCandidatePair(blocks, fullyReported, minSimilarityPercent, (left, right) => {\n const leftBlock = blocks[left];\n const rightBlock = blocks[right];\n const match = leftBlock && rightBlock && matcher(leftBlock, rightBlock, right);\n if (!match) {\n return;\n }\n // The lines of a near-miss copy that are duplicated are those a verified partner matches,\n // which takes the pairing itself. Every pair kept is aligned: verifying it already cost a longest\n // common subsequence of the same sequences, and any choice among a copy's partners would let\n // their number or their order decide what the copy counts.\n for (const { pair: cores, reordered } of match.cores ?? [{ pair: undefined, reordered: match.reordered }]) {\n const [leftSpan, rightSpan] = cores ?? [spanOf(leftBlock), spanOf(rightBlock)];\n const addsLines = (leftRuns: [number, number][], rightRuns: [number, number][]): boolean =>\n unreportedCount(leftBlock.fileIndex, leftRuns) + unreportedCount(rightBlock.fileIndex, rightRuns) > 0;\n // Fully reported spans need no alignment to be dropped.\n if (!addsLines([leftSpan], [rightSpan])) {\n continue;\n }\n const matchedRuns = alignPair(leftBlock, rightBlock, { cores, reordered });\n if (addsLines(...matchedRuns)) {\n edges.push({ left, right, cores, matchedRuns });\n }\n }\n });\n\n // Clustering runs over (block, core) nodes: a block with a recorded whole match is one node,\n // and otherwise each union of its overlapping local cores is its own node, so disjoint cores\n // matched with different partners fall into separate groups.\n const matchedWhole = blocks.map(() => false);\n const localCores = blocks.map((): [number, number][] => []);\n for (const edge of edges) {\n for (const [index, core] of sidesOf(edge)) {\n if (core) {\n localCores[index]?.push(core);\n } else {\n matchedWhole[index] = true;\n }\n }\n }\n const nodes: { blockIndex: number; core: [number, number] | undefined }[] = [];\n const firstNodeByBlock: number[] = [];\n for (const blockIndex of blocks.keys()) {\n firstNodeByBlock.push(nodes.length);\n const cores = matchedWhole[blockIndex] ? [] : mergeOverlappingCores(localCores[blockIndex] ?? []);\n if (cores.length === 0) {\n nodes.push({ blockIndex, core: undefined });\n }\n for (const core of cores) {\n nodes.push({ blockIndex, core });\n }\n }\n const nodeOf = (blockIndex: number, core: [number, number] | undefined): number => {\n const first = firstNodeByBlock[blockIndex] ?? 0;\n if (!core || matchedWhole[blockIndex]) {\n return first;\n }\n for (let node = first; nodes[node]?.blockIndex === blockIndex; node += 1) {\n const span = nodes[node]?.core;\n if (span && span[0] <= core[0] && core[1] <= span[1]) {\n return node;\n }\n }\n throw new Error(\"every local core lies in one of its block's merged cores\");\n };\n\n const parent = nodes.map((_, index) => index);\n const find = (index: number): number => {\n let root = index;\n while (parent[root] !== root) {\n root = parent[root] ?? root;\n }\n for (let current = index; parent[current] !== root;) {\n const next = parent[current] ?? root;\n parent[current] = root;\n current = next;\n }\n return root;\n };\n // Anchoring is judged per node: a core is an anchor only when a reported span overlaps the core\n // itself, not merely elsewhere in its block.\n const nodeRanges = nodes.map(({ blockIndex, core }) => {\n const block = blocks[blockIndex];\n const [startTokenIndex, endTokenIndex] = core ?? [\n block?.range.startTokenIndex ?? 0,\n block?.range.endTokenIndex ?? 0,\n ];\n return {\n length: endTokenIndex - startTokenIndex,\n reported: reportedCount(block?.fileIndex ?? 0, [startTokenIndex, endTokenIndex]),\n };\n });\n const anchored = nodeRanges.map(({ reported }) => reported > 0);\n const reported = nodeRanges.map((range) => range.reported === range.length);\n const edgeNodes = edges.map((edge) => sidesOf(edge).map(([index, core]) => nodeOf(index, core)));\n for (const [leftNode = 0, rightNode = 0] of edgeNodes) {\n const leftRoot = find(leftNode);\n const rightRoot = find(rightNode);\n parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);\n }\n const matchedRunsByNode = nodes.map((): [number, number][] => []);\n for (const [edgeIndex, { matchedRuns }] of edges.entries()) {\n const [leftNode = 0, rightNode = 0] = edgeNodes[edgeIndex] ?? [];\n matchedRunsByNode[leftNode]?.push(...matchedRuns[0]);\n matchedRunsByNode[rightNode]?.push(...matchedRuns[1]);\n }\n\n const membersByRoot = new Map<number, number[]>();\n for (const node of nodes.keys()) {\n const root = find(node);\n const members = membersByRoot.get(root) ?? [];\n members.push(node);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((node) => reported[node])) {\n continue;\n }\n // A group's nodes from one block become ONE occurrence whose segments are its cores, so the\n // fragment-weighted count charges the block as one copy (as for gapped clones), not once per core.\n const coresByBlock = new Map<number, BlockCopy>();\n for (const node of members) {\n const { blockIndex = 0, core } = nodes[node] ?? {};\n const entry = coresByBlock.get(blockIndex) ?? { cores: [], anchor: false, matchedRuns: [] };\n entry.cores.push(core);\n entry.anchor ||= anchored[node] ?? false;\n entry.matchedRuns.push(...(matchedRunsByNode[node] ?? []));\n coresByBlock.set(blockIndex, entry);\n }\n groups.push(\n [...coresByBlock].flatMap(([blockIndex, copy]) => {\n const block = blocks[blockIndex];\n return block ? [toOccurrence(block, files, copy)] : [];\n })\n );\n }\n return groups;\n}\n\n/** The nodes of one block in a group: its cores (`undefined` for the whole block) and what its partners match of them. */\ninterface BlockCopy {\n cores: ([number, number] | undefined)[];\n anchor: boolean;\n matchedRuns: [number, number][];\n}\n\nfunction spanOf({ range }: NormalizedBlock): [number, number] {\n return [range.startTokenIndex, range.endTokenIndex];\n}\n\n/** The (block, core) on each side of a pair. */\nfunction sidesOf({ left, right, cores }: MatchEdge): [number, [number, number] | undefined][] {\n return [\n [left, cores?.[0]],\n [right, cores?.[1]],\n ];\n}\n\n/**\n * The token runs a verified match pairs, per side as file token ranges: those of a longest common\n * subsequence of what the match compared, its cores or else the whole blocks.\n */\nfunction alignPair(\n left: NormalizedBlock,\n right: NormalizedBlock,\n { cores, reordered }: Alignment & { cores?: CorePair }\n): [[number, number][], [number, number][]] {\n const leftCompared = comparedSequence(left, cores?.[0], reordered);\n const rightCompared = comparedSequence(right, cores?.[1], reordered);\n // Of the equally long subsequences, which one is marked depends on which sequence comes first,\n // so the sides are taken in the order of their content rather than of their files.\n if (compareSequences(contentOf(left, cores?.[0]), contentOf(right, cores?.[1])) <= 0) {\n const [leftMatched, rightMatched] = markLcs(leftCompared.sequence, rightCompared.sequence);\n return [toRuns(leftCompared.positions, leftMatched), toRuns(rightCompared.positions, rightMatched)];\n }\n const [rightMatched, leftMatched] = markLcs(rightCompared.sequence, leftCompared.sequence);\n return [toRuns(leftCompared.positions, leftMatched), toRuns(rightCompared.positions, rightMatched)];\n}\n\n/**\n * The content of a block or of a core of it, as written (see `writtenContent`), with the\n * identifiers of a core numbered within it.\n */\nfunction contentOf(block: NormalizedBlock, core: [number, number] | undefined): Float64Array {\n if (!core) {\n return block.writtenContent;\n }\n const { sequence, positions } = comparedSequence(block, core, false);\n return Float64Array.from(sequence, (symbol, index) =>\n symbol < 0 ? symbol : (block.fileTokenKeys[positions[index] ?? 0] ?? 0)\n );\n}\n\n/** The maximal runs of the matched positions, as half-open ranges. */\nfunction toRuns(positions: Int32Array, matched: Uint8Array): [number, number][] {\n const runs: [number, number][] = [];\n for (const position of positions.filter((_, index) => matched[index] === 1).toSorted()) {\n const last = runs.at(-1);\n if (last?.[1] === position) {\n last[1] = position + 1;\n } else {\n runs.push([position, position + 1]);\n }\n }\n return runs;\n}\n\n/**\n * The sequence a match compared, with the file token index of each of its symbols: a core, the\n * block with its statements in canonical order, or the block as written.\n */\nfunction comparedSequence(\n block: NormalizedBlock,\n core: [number, number] | undefined,\n reordered: boolean\n): { sequence: Int32Array; positions: Int32Array } {\n const blockStart = block.range.startTokenIndex;\n if (core) {\n return {\n sequence: anonymize(block.symbols.subarray(core[0] - blockStart, core[1] - blockStart)),\n positions: Int32Array.from({ length: core[1] - core[0] }, (_, index) => core[0] + index),\n };\n }\n if (reordered && block.canonical) {\n return {\n sequence: block.canonical.sequence,\n positions: block.canonical.offsets.map((offset) => blockStart + offset),\n };\n }\n return { sequence: block.sequence, positions: block.sequence.map((_, index) => blockStart + index) };\n}\n\n/** The unions of overlapping cores, in position order. */\nfunction mergeOverlappingCores(cores: [number, number][]): [number, number][] {\n const merged: [number, number][] = [];\n for (const [start, end] of cores.toSorted((left, right) => left[0] - right[0])) {\n const last = merged.at(-1);\n if (last && start < last[1]) {\n last[1] = Math.max(last[1], end);\n } else {\n merged.push([start, end]);\n }\n }\n return merged;\n}\n\n/** Counts the tokens of [start, end) that the spans cover. */\nfunction createReportedTokenCounter(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (start: number, end: number) => number {\n const merged = mergeOverlappingCores(spans.map((span) => [span.startTokenIndex, span.endTokenIndex]));\n let covered = 0;\n const coveredBefore = [0];\n for (const [start, end] of merged) {\n covered += end - start;\n coveredBefore.push(covered);\n }\n const countBefore = (position: number): number => {\n let low = 0;\n let high = merged.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((merged[middle]?.[0] ?? 0) < position) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return (coveredBefore[low] ?? 0) - Math.max(0, (merged[low - 1]?.[1] ?? 0) - position);\n };\n return (start, end) => countBefore(end) - countBefore(start);\n}\n\n/**\n * The block's occurrence with one segment per entry of `cores` (the whole block for `undefined`,\n * a whole match). Source offsets stay the block's: tokens carry none, and near-miss occurrences\n * report lines only.\n */\nfunction toOccurrence(\n { fileIndex, range }: NormalizedBlock,\n files: NearMissSourceFile[],\n { cores, anchor, matchedRuns }: BlockCopy\n): NearMissOccurrence {\n const whole = cores.includes(undefined);\n const segments = cores\n .map((core): [number, number] => core ?? [range.startTokenIndex, range.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0])\n .map(([startTokenIndex, endTokenIndex]) => ({ startTokenIndex, endTokenIndex }));\n const start = segments[0]?.startTokenIndex ?? range.startTokenIndex;\n const end = segments.at(-1)?.endTokenIndex ?? range.endTokenIndex;\n const tokens = files[fileIndex]?.tokens;\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments,\n matchedRuns: mergeOverlappingCores(matchedRuns).map(([startTokenIndex, endTokenIndex]): TokenSegment => ({\n startTokenIndex,\n endTokenIndex,\n })),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: whole ? range.startLine : (tokens?.[start]?.startRow ?? 0) + 1,\n endLine: whole ? range.endLine : (tokens?.[end - 1]?.endRow ?? 0) + 1,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two blocks wholly covered by reported spans and pairs whose\n * length ratio rules out both\n * whole-block similarity and `maxLengthRatio`. Blocks are\n * indexed in ascending length, so each posting list is scanned backwards only while its blocks\n * are long enough; shared counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n fullyReported: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const reportedFlags = Uint8Array.from(fullyReported, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightReported = reportedFlags[right] === 1;\n const minLeftLength = Math.min(\n Math.ceil((lengths[right] ?? 0) / maxLengthRatio),\n Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100)\n );\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightReported && reportedFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Returns the pair verifier. Content symbols are weighted by integer self-information,\n * 1 + floor(log2((N + 1) / df)) over N blocks capped at `maxContentWeight`, so rare names and\n * values (the logic a copy preserves) outweigh ubiquitous ones, following the\n * information-theoretic weighting of ECScan's essence-clone detection (2025).\n */\nfunction createMatcher(\n blocks: NormalizedBlock[],\n minTokens: number,\n minSimilarityPercent: number\n): (left: NormalizedBlock, right: NormalizedBlock, rightIndex: number) => PairMatch | undefined {\n const documentFrequencies = new Map<number, number>();\n for (const block of blocks) {\n for (const symbol of block.contentCounts.keys()) {\n documentFrequencies.set(symbol, (documentFrequencies.get(symbol) ?? 0) + 1);\n }\n }\n const selfInformation = (documentFrequency: number): number =>\n Math.min(31 - Math.clz32(Math.floor((blocks.length + 1) / documentFrequency)) + 1, maxContentWeight);\n const weights = new Map<number, number>();\n for (const [symbol, frequency] of documentFrequencies) {\n weights.set(symbol, selfInformation(frequency));\n }\n const weigh = (counts: Map<number, number>): WeightedContent => {\n const symbols = Int32Array.from(counts.keys()).toSorted();\n // Span content comes from blocks, so every symbol has a weight.\n const weightedCounts = Int32Array.from(symbols, (symbol) => (counts.get(symbol) ?? 0) * (weights.get(symbol) ?? 0));\n let total = 0;\n for (const count of weightedCounts) {\n total += count;\n }\n return { symbols, weightedCounts, total };\n };\n const blockContents = new Map(blocks.map((block) => [block, weigh(block.contentCounts)]));\n\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: NormalizedBlock, rightIndex: number, sequence: Int32Array): number => {\n if (counterBlock !== rightIndex || !counter) {\n counterBlock = rightIndex;\n counter = createLcsLengthCounter(right.sequence);\n }\n return counter(sequence);\n };\n\n /**\n * Matches the cores two blocks share inside different surroundings (a copy wrapped in added\n * code, or two copies embedded in different code), which whole-block similarity misses\n * (CCAligner's large-gap and LVMapper's large-variance clones). N-grams unique to each block\n * anchor the alignment; their longest chain increasing in both blocks (a run filter keeps only\n * anchors continuing a diagonal, but the chain may shift diagonals at small insertions), split at\n * gaps, delimits the cores, and every core pair that is a near-miss clone in its own right is\n * returned.\n */\n const matchLocally = (left: NormalizedBlock, right: NormalizedBlock): PairMatch | undefined => {\n const anchors: [number, number][] = [];\n for (\n let leftIndex = 0, rightIndex = 0;\n leftIndex < left.uniqueNgrams.length && rightIndex < right.uniqueNgrams.length;\n ) {\n const leftHash = left.uniqueNgrams[leftIndex] ?? 0;\n const rightHash = right.uniqueNgrams[rightIndex] ?? 0;\n if (leftHash === rightHash) {\n anchors.push([left.uniqueNgramOffsets[leftIndex] ?? 0, right.uniqueNgramOffsets[rightIndex] ?? 0]);\n }\n if (leftHash <= rightHash) {\n leftIndex += 1;\n }\n if (rightHash <= leftHash) {\n rightIndex += 1;\n }\n }\n anchors.sort((first, second) => first[0] - second[0]);\n // An isolated 5-gram match is often coincidental (n-grams are identifier-blind); a copied core\n // yields runs of consecutive anchors, so only anchors continuing a diagonal run are chained.\n const runAnchors = anchors.filter(([leftOffset, rightOffset], index) => {\n const previous = anchors[index - 1];\n const next = anchors[index + 1];\n return (\n (previous?.[0] === leftOffset - 1 && previous[1] === rightOffset - 1) ||\n (next?.[0] === leftOffset + 1 && next[1] === rightOffset + 1)\n );\n });\n const leftOffset = left.range.startTokenIndex;\n const rightOffset = right.range.startTokenIndex;\n const cores: (Alignment & { pair: CorePair })[] = [];\n for (const segment of chainSegments(longestIncreasingChain(runAnchors))) {\n const [leftStart, rightStart] = segment[0] ?? [0, 0];\n const [leftLast, rightLast] = segment.at(-1) ?? [0, 0];\n const leftEnd = leftLast + ngramSize;\n const rightEnd = rightLast + ngramSize;\n const leftLength = leftEnd - leftStart;\n const rightLength = rightEnd - rightStart;\n const shorter = Math.min(leftLength, rightLength);\n const required = minSimilarityPercent * Math.max(leftLength, rightLength);\n if (\n shorter < minTokens ||\n shorter * 100 < required ||\n anchoredTokenCount(segment) * 100 < minAnchorCoveragePercent * shorter ||\n !sharesContent(\n weigh(countContent(left.symbols, left.isContent, leftStart, leftEnd)),\n weigh(countContent(right.symbols, right.isContent, rightStart, rightEnd))\n )\n ) {\n continue;\n }\n const coreLcsLength = lcsLength(\n anonymize(left.symbols.subarray(leftStart, leftEnd)),\n anonymize(right.symbols.subarray(rightStart, rightEnd))\n );\n if (coreLcsLength * 100 >= required) {\n cores.push({\n pair: [\n [leftOffset + leftStart, leftOffset + leftEnd],\n [rightOffset + rightStart, rightOffset + rightEnd],\n ],\n reordered: false,\n });\n }\n }\n return cores.length > 0 ? { cores } : undefined;\n };\n\n /** Cheapest bounds first: the LCS cannot exceed the shorter block's length nor the bag overlap. */\n return (left, right, rightIndex) => {\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (\n Math.min(left.sequence.length, right.sequence.length) * 100 >= required &&\n sharesContent(blockContents.get(left), blockContents.get(right))\n ) {\n if (sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 >= required) {\n const inOrder = lcsLengthWithRight(right, rightIndex, left.sequence);\n if (inOrder * 100 >= required) {\n return { reordered: false };\n }\n }\n // With their top-level statements (each anonymized on its own) in a canonical order, a copy\n // whose independent statements were swapped still matches.\n if (left.canonical && right.canonical) {\n const reordered = lcsLength(left.canonical.sequence, right.canonical.sequence);\n if (reordered * 100 >= required) {\n return { reordered: true };\n }\n }\n }\n // Of the equally long chains of anchors, the one kept depends on which block comes first, so\n // the blocks are taken in the order of their content rather than of their files.\n if (compareSequences(right.writtenContent, left.writtenContent) < 0) {\n const swapped = matchLocally(right, left);\n return (\n swapped?.cores && {\n cores: swapped.cores.map(({ pair: [rightCore, leftCore], ...alignment }) => ({\n ...alignment,\n pair: [leftCore, rightCore],\n })),\n }\n );\n }\n return matchLocally(left, right);\n };\n}\n\n/** Content-bearing symbols, sorted, with their information-weighted counts. */\ninterface WeightedContent {\n symbols: Int32Array;\n weightedCounts: Int32Array;\n total: number;\n}\n\n/**\n * A structural match must be backed by shared content: more than `minContentSimilarityPercent` of\n * the larger side's information-weighted names and literal values. Two sides without content never\n * pass.\n */\nfunction sharesContent(left: WeightedContent | undefined, right: WeightedContent | undefined): boolean {\n if (!left || !right) {\n return false;\n }\n let overlap = 0;\n for (let leftIndex = 0, rightIndex = 0; leftIndex < left.symbols.length && rightIndex < right.symbols.length;) {\n const leftSymbol = left.symbols[leftIndex] ?? 0;\n const rightSymbol = right.symbols[rightIndex] ?? 0;\n if (leftSymbol === rightSymbol) {\n overlap += Math.min(left.weightedCounts[leftIndex] ?? 0, right.weightedCounts[rightIndex] ?? 0);\n }\n if (leftSymbol <= rightSymbol) {\n leftIndex += 1;\n }\n if (rightSymbol <= leftSymbol) {\n rightIndex += 1;\n }\n }\n return overlap * 100 > minContentSimilarityPercent * Math.max(left.total, right.total);\n}\n\n/**\n * The block's units (its top-level statements, given in file token indexes, and the token runs\n * between them), each anonymized on its own and sorted, concatenated, with the block offset of\n * every token; undefined with too few statements. `tokenKeys` holds the content key of each\n * non-identifier token of the block.\n */\nfunction canonicalSequenceOf(\n symbols: Int32Array,\n tokenKeys: Float64Array,\n statements: [number, number][],\n blockStart: number\n): NormalizedBlock['canonical'] {\n if (statements.length < minReorderStatementCount) {\n return undefined;\n }\n const units: { unit: Int32Array; content: Float64Array; start: number }[] = [];\n const pushUnit = (start: number, end: number): void => {\n const unit = anonymize(symbols.subarray(start, end));\n // Ordered by the content of their tokens: symbols, numbered by first occurrence, would order\n // the units by where other code stands.\n const content = Float64Array.from(unit, (symbol, index) => (symbol < 0 ? symbol : (tokenKeys[start + index] ?? 0)));\n units.push({ unit, content, start });\n };\n let cursor = 0;\n for (const [statementStart, statementEnd] of statements) {\n const start = statementStart - blockStart;\n if (cursor < start) {\n pushUnit(cursor, start);\n }\n pushUnit(start, statementEnd - blockStart);\n cursor = statementEnd - blockStart;\n }\n if (cursor < symbols.length) {\n pushUnit(cursor, symbols.length);\n }\n units.sort((left, right) => compareSequences(left.content, right.content) || left.start - right.start);\n const sequence = new Int32Array(symbols.length);\n const offsets = new Int32Array(symbols.length);\n let position = 0;\n for (const { unit, start } of units) {\n sequence.set(unit, position);\n for (let index = 0; index < unit.length; index += 1) {\n offsets[position + index] = start + index;\n }\n position += unit.length;\n }\n return { sequence, offsets };\n}\n\n/** Lexicographic order. */\nfunction compareSequences(left: Float64Array, right: Float64Array): number {\n for (let index = 0; index < Math.min(left.length, right.length); index += 1) {\n const difference = (left[index] ?? 0) - (right[index] ?? 0);\n if (difference !== 0) {\n return difference;\n }\n }\n return left.length - right.length;\n}\n\n/**\n * The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),\n * via patience sorting over right offsets.\n */\nfunction longestIncreasingChain(anchors: [number, number][]): [number, number][] {\n const tailIndexes: number[] = [];\n const predecessors: number[] = [];\n for (const [index, [, rightOffset]] of anchors.entries()) {\n let low = 0;\n let high = tailIndexes.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((anchors[tailIndexes[middle] ?? 0]?.[1] ?? 0) < rightOffset) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n predecessors.push(low > 0 ? (tailIndexes[low - 1] ?? -1) : -1);\n tailIndexes[low] = index;\n }\n const chain: [number, number][] = [];\n for (let cursor = tailIndexes.at(-1) ?? -1; cursor >= 0; cursor = predecessors[cursor] ?? -1) {\n const anchor = anchors[cursor];\n if (anchor) {\n chain.push(anchor);\n }\n }\n return chain.toReversed();\n}\n\n/** The chain's segments, split where consecutive anchors lie more than `maxAnchorGapTokens` apart in either block. */\nfunction chainSegments(chain: [number, number][]): [number, number][][] {\n const segments: [number, number][][] = [];\n for (const [index, anchor] of chain.entries()) {\n const previous = chain[index - 1];\n const continues =\n previous !== undefined &&\n anchor[0] - (previous[0] + ngramSize) <= maxAnchorGapTokens &&\n anchor[1] - (previous[1] + ngramSize) <= maxAnchorGapTokens;\n if (continues) {\n segments.at(-1)?.push(anchor);\n } else {\n segments.push([anchor]);\n }\n }\n return segments;\n}\n\n/** Left-block tokens the segment's anchors cover (overlapping anchors count once). */\nfunction anchoredTokenCount(segment: [number, number][]): number {\n let count = ngramSize;\n for (let index = 1; index < segment.length; index += 1) {\n count += Math.min((segment[index]?.[0] ?? 0) - (segment[index - 1]?.[0] ?? 0), ngramSize);\n }\n return count;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector. Non-identifier symbols are interned\n * project-wide from the tokens' hash pairs, so equal tokens compare equal across files, while\n * identifiers are interned per file and re-anonymized per compared range.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, containerStatements, nearMissBlocks }] of files.entries()) {\n if (!tokens || !nearMissBlocks?.length) {\n continue;\n }\n const symbols = new Int32Array(tokens.length);\n const isContent = new Uint8Array(tokens.length);\n const fileTokenKeys = new Float64Array(tokens.length);\n const idByIdentifier = new Map<string, number>();\n for (const [index, token] of tokens.entries()) {\n if (token.kind === 'id') {\n let id = idByIdentifier.get(token.text);\n if (id === undefined) {\n id = idByIdentifier.size;\n idByIdentifier.set(token.text, id);\n }\n symbols[index] = -(id + 1);\n continue;\n }\n const key = tokenKey(token);\n fileTokenKeys[index] = key;\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n symbols[index] = symbol;\n isContent[index] = token.isName || token.literalHash !== undefined ? 1 : 0;\n }\n const findStatements = createTopLevelStatementFinder(containerStatements ?? []);\n for (const range of nearMissBlocks) {\n const { startTokenIndex: start, endTokenIndex: end } = range;\n const blockSymbols = symbols.subarray(start, end);\n const blockIsContent = isContent.subarray(start, end);\n const sequence = anonymize(blockSymbols);\n const ngramHashes = collectNgramHashes(blockSymbols, fileTokenKeys.subarray(start, end));\n const occurrenceCounts = new Map<number, number>();\n for (const hash of ngramHashes) {\n occurrenceCounts.set(hash, (occurrenceCounts.get(hash) ?? 0) + 1);\n }\n const uniqueOffsets = ngramHashes\n .keys()\n .filter((offset) => occurrenceCounts.get(ngramHashes[offset] ?? 0) === 1)\n .toArray()\n .toSorted((first, second) => (ngramHashes[first] ?? 0) - (ngramHashes[second] ?? 0));\n blocks.push({\n fileIndex,\n range,\n symbols: blockSymbols,\n isContent: blockIsContent,\n fileTokenKeys,\n sequence,\n writtenContent: Float64Array.from(sequence, (symbol, index) =>\n symbol < 0 ? symbol : (fileTokenKeys[start + index] ?? 0)\n ),\n sortedSequence: sequence.toSorted(),\n ngrams: Int32Array.from(occurrenceCounts.keys()),\n uniqueNgrams: Int32Array.from(uniqueOffsets, (offset) => ngramHashes[offset] ?? 0),\n uniqueNgramOffsets: Int32Array.from(uniqueOffsets),\n contentCounts: countContent(blockSymbols, blockIsContent, 0, blockSymbols.length),\n canonical: canonicalSequenceOf(\n blockSymbols,\n fileTokenKeys.subarray(start, end),\n findStatements(start, end),\n start\n ),\n });\n }\n }\n return blocks;\n}\n\n/**\n * Returns a lookup of the outermost container statements inside a token range, excluding a\n * statement spanning the whole range (the block itself).\n */\nfunction createTopLevelStatementFinder(\n containerStatements: TokenRange[][]\n): (start: number, end: number) => [number, number][] {\n const statements = containerStatements\n .flat()\n .filter((statement) => statement.startTokenIndex < statement.endTokenIndex)\n .map((statement): [number, number] => [statement.startTokenIndex, statement.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0] || right[1] - left[1]);\n return (start, end) => {\n let low = 0;\n let high = statements.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((statements[middle]?.[0] ?? 0) < start) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const topLevel: [number, number][] = [];\n for (let index = low; index < statements.length; index += 1) {\n const statement = statements[index];\n if (!statement || statement[0] >= end) {\n break;\n }\n const last = topLevel.at(-1);\n const nested = last !== undefined && statement[0] < last[1];\n if (statement[1] <= end && !(statement[0] === start && statement[1] === end) && !nested) {\n topLevel.push(statement);\n }\n }\n return topLevel;\n };\n}\n\nfunction countContent(symbols: Int32Array, isContent: Uint8Array, start: number, end: number): Map<number, number> {\n const counts = new Map<number, number>();\n for (let offset = start; offset < end; offset += 1) {\n if (isContent[offset] === 1) {\n const symbol = symbols[offset] ?? 0;\n counts.set(symbol, (counts.get(symbol) ?? 0) + 1);\n }\n }\n return counts;\n}\n\n/** Identifiers renumbered by first occurrence within `symbols`, so a range compares the same wherever it sits in its file. */\nfunction anonymize(symbols: Int32Array): Int32Array {\n const indexByIdentifier = new Map<number, number>();\n return symbols.map((symbol) => {\n if (symbol >= 0) {\n return symbol;\n }\n let index = indexByIdentifier.get(symbol);\n if (index === undefined) {\n index = indexByIdentifier.size;\n indexByIdentifier.set(symbol, index);\n }\n return -(index + 1);\n });\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\n/**\n * N-gram hash per start offset, identifier-blind (every identifier hashes as -1) so a block copied\n * into different surroundings (renumbering its identifiers) or with reordered statements still\n * shares its n-grams. Hashed from the content keys of the tokens: with symbols, numbered by first\n * occurrence, which n-grams collide would depend on the order of the files.\n */\nfunction collectNgramHashes(symbols: Int32Array, tokenKeys: Float64Array): Int32Array {\n const hashes = new Int32Array(Math.max(symbols.length - ngramSize + 1, 0));\n for (let start = 0; start < hashes.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n const tokenKey = tokenKeys[start + offset] ?? 0;\n // A token key holds 53 bits; both halves enter the hash.\n const value = (symbols[start + offset] ?? 0) < 0 ? -1 : tokenKey ^ Math.floor(tokenKey / 0x1_00_00_00_00);\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32.\n hash = (Math.imul(hash, 31) + value) | 0;\n }\n hashes[start] = hash;\n }\n return hashes;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},f=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},p=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!f({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||p.push({containerIndex:e,start:n,length:i})}let m=new Set(p.map(s)),h=p;for(;h.length>0;){let e=[];for(let t of h){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${x(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:g(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}h=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!m.has(s(n))&&d(r,n.length)&&f(n)&&(m.add(s(n)),h.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>S(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=E(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?E(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=d(e);return e=>{let n=0;for(let r of t(e))n+=h(r);return n}}function d(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}return r}}function f(e,t){let n=new Uint8Array(e.length),r=new Uint8Array(t.length);return p(e,t,n,r),[n,r]}function p(e,t,n,r){let i=0;for(;i<e.length&&i<t.length&&e[i]===t[i];)i+=1;let a=0;for(;a<e.length-i&&a<t.length-i&&e[e.length-1-a]===t[t.length-1-a];)a+=1;let o=e.length-a,s=t.length-a;n.fill(1,0,i).fill(1,o),r.fill(1,0,i).fill(1,s);let c=e.subarray(i,o),l=t.subarray(i,s),u=n.subarray(i,o),d=r.subarray(i,s);if(c.length===0||l.length===0)return;if(c.length===1){let e=l.indexOf(c[0]??0);e!==-1&&(u[0]=1,d[e]=1);return}let f=c.length>>>1,h=m(c.subarray(0,f),l),g=m(c.subarray(f).toReversed(),l.toReversed()),_=0,v=-1;for(let e=0;e<=l.length;e+=1){let t=(h[e]??0)+(g[l.length-e]??0);t>v&&(v=t,_=e)}p(c.subarray(0,f),l.subarray(0,_),u.subarray(0,f),d.subarray(0,_)),p(c.subarray(f),l.subarray(_),u.subarray(f),d.subarray(_))}function m(e,t){let n=d(t)(e),r=new Int32Array(t.length+1);for(let e=0;e<t.length;e+=1)r[e+1]=(r[e]??0)+((n[e>>>5]??0)>>>(e&31)&1);return r}function h(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function g(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const _=[],v=[];function y(e){let t=_[e];return t===void 0&&(t=w(`$${e}`),_[e]=t),t}function b(e){let t=v[e];return t===void 0&&(t=T(`$${e}`),v[e]=t),t}function x(e,t,n,r){let[a,o]=C(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function S(e,t,n){let[r,i]=C(e,t,n,!1);return r^Math.imul(i,31)}function C(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=y(e),c=b(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function w(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function T(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function E(e,t){return Math.imul(e,31)+t}function D(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(A);for(let r=!0;r;){r=!1;let i=O(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=j(i,s,t,n),l=c??j(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(A),r=!0;break}}return e}function O(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=k(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function k(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function A(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function j(e,t,n,r){let[i,a]=M(e),[o,s]=M(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,matchedRuns:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function M(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function N(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function P(e,t,n,r){let i=/* @__PURE__ */ new Map,a=/* @__PURE__ */ new Map,o=0;for(let r of e)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){for(;t&&(t[o]?.endTokenIndex??1/0)<=e;)o+=1;let r=!t||(t[o]?.startTokenIndex??1/0)<=e,s=n[e];for(let e=s?.startRow??0;e<=(s?.endRow??-1);e+=1)a.set(e+1,(a.get(e+1)??0)+1),i.set(e+1,(i.get(e+1)??0)+ +!!r)}let s=/* @__PURE__ */ new Set;for(let[e,t]of a)(!r||r.has(e))&&(i.get(e)??0)*2>t&&s.add(e);return s}exports.buildLiteralCountPrefix=a,exports.collectMatchedLines=P,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=N,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.markLcs=f,exports.mergeAdjacentGroups=D,exports.resolveDuplicationOptions=n;
1
+ "use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},f=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},p=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!f({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||p.push({containerIndex:e,start:n,length:i})}let m=new Set(p.map(s)),h=p;for(;h.length>0;){let e=[];for(let t of h){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${x(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:g(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}h=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!m.has(s(n))&&d(r,n.length)&&f(n)&&(m.add(s(n)),h.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>S(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=E(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?E(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=d(e);return e=>{let n=0;for(let r of t(e))n+=h(r);return n}}function d(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}return r}}function f(e,t){let n=new Uint8Array(e.length),r=new Uint8Array(t.length);return p(e,t,n,r),[n,r]}function p(e,t,n,r){let i=0;for(;i<e.length&&i<t.length&&e[i]===t[i];)i+=1;let a=0;for(;a<e.length-i&&a<t.length-i&&e[e.length-1-a]===t[t.length-1-a];)a+=1;let o=e.length-a,s=t.length-a;n.fill(1,0,i).fill(1,o),r.fill(1,0,i).fill(1,s);let c=e.subarray(i,o),l=t.subarray(i,s),u=n.subarray(i,o),d=r.subarray(i,s);if(c.length===0||l.length===0)return;if(c.length===1){let e=l.indexOf(c[0]??0);e!==-1&&(u[0]=1,d[e]=1);return}let f=c.length>>>1,h=m(c.subarray(0,f),l),g=m(c.subarray(f).toReversed(),l.toReversed()),_=0,v=-1;for(let e=0;e<=l.length;e+=1){let t=(h[e]??0)+(g[l.length-e]??0);t>v&&(v=t,_=e)}p(c.subarray(0,f),l.subarray(0,_),u.subarray(0,f),d.subarray(0,_)),p(c.subarray(f),l.subarray(_),u.subarray(f),d.subarray(_))}function m(e,t){let n=d(t)(e),r=new Int32Array(t.length+1);for(let e=0;e<t.length;e+=1)r[e+1]=(r[e]??0)+((n[e>>>5]??0)>>>(e&31)&1);return r}function h(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function g(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const _=[],v=[];function y(e){let t=_[e];return t===void 0&&(t=w(`$${e}`),_[e]=t),t}function b(e){let t=v[e];return t===void 0&&(t=T(`$${e}`),v[e]=t),t}function x(e,t,n,r){let[a,o]=C(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function S(e,t,n){let[r,i]=C(e,t,n,!1);return r^Math.imul(i,31)}function C(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=y(e),c=b(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function w(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function T(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function E(e,t){return Math.imul(e,31)+t}function D(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(A);for(let r=!0;r;){r=!1;let i=O(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=j(i,s,t,n),l=c??j(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(A),r=!0;break}}return e}function O(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=k(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function k(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function A(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function j(e,t,n,r){let[i,a]=M(e),[o,s]=M(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,matchedRuns:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function M(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function N(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function P(e,t,n,r,i=[]){let a=/* @__PURE__ */ new Set;for(let e of i)for(let t=e.startTokenIndex;t<e.endTokenIndex;t+=1){let e=n[t];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)a.add(t+1)}let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=0;for(let r of e)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){for(;t&&(t[c]?.endTokenIndex??1/0)<=e;)c+=1;let r=!t||(t[c]?.startTokenIndex??1/0)<=e,i=n[e];for(let e=i?.startRow??0;e<=(i?.endRow??-1);e+=1)s.set(e+1,(s.get(e+1)??0)+1),o.set(e+1,(o.get(e+1)??0)+ +!!r)}let l=/* @__PURE__ */ new Set;for(let[e,t]of s)(!r||r.has(e))&&((o.get(e)??0)*2>t||a.has(e))&&l.add(e);return l}exports.buildLiteralCountPrefix=a,exports.collectMatchedLines=P,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=N,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.markLcs=f,exports.mergeAdjacentGroups=D,exports.resolveDuplicationOptions=n;
2
2
  //# sourceMappingURL=duplication.cjs.map