(
content: string,
search: string,
opts: { fuzzyThreshold?: number; fuzzyMargin?: number; allowFuzzy?: boolean } = {},
)
| 99 | * Returns null if no confident match. The threshold/margin guard fuzzy tier. |
| 100 | */ |
| 101 | export function findMatch( |
| 102 | content: string, |
| 103 | search: string, |
| 104 | opts: { fuzzyThreshold?: number; fuzzyMargin?: number; allowFuzzy?: boolean } = {}, |
| 105 | ): MatchResult | null { |
| 106 | const fuzzyThreshold = opts.fuzzyThreshold ?? 0.85; |
| 107 | const fuzzyMargin = opts.fuzzyMargin ?? 0.08; |
| 108 | const allowFuzzy = opts.allowFuzzy ?? true; |
| 109 | |
| 110 | // ── Tier 0: exact ── |
| 111 | { |
| 112 | const first = content.indexOf(search); |
| 113 | if (first !== -1) { |
| 114 | const occurrences = content.split(search).length - 1; |
| 115 | return { start: first, end: first + search.length, matched: search, tier: 'exact', occurrences }; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | const contentLines = content.split('\n'); |
| 120 | const searchLines = search.split('\n'); |
| 121 | // Drop a trailing empty line from a search that ended in \n. |
| 122 | if (searchLines.length > 1 && searchLines[searchLines.length - 1] === '') searchLines.pop(); |
| 123 | const nSearch = searchLines.length; |
| 124 | const lineStarts = computeLineStarts(content); |
| 125 | |
| 126 | // ── Tier 1: line-trimmed exact ── |
| 127 | { |
| 128 | const normSearch = searchLines.map(normLine); |
| 129 | const matches: number[] = []; |
| 130 | for (let i = 0; i + nSearch <= contentLines.length; i++) { |
| 131 | let ok = true; |
| 132 | for (let j = 0; j < nSearch; j++) { |
| 133 | if (normLine(contentLines[i + j]!) !== normSearch[j]) { ok = false; break; } |
| 134 | } |
| 135 | if (ok) matches.push(i); |
| 136 | } |
| 137 | if (matches.length >= 1) { |
| 138 | const i = matches[0]!; |
| 139 | const { start, end } = lineRangeToOffsets(lineStarts, content, i, i + nSearch); |
| 140 | return { |
| 141 | start, end, |
| 142 | matched: content.slice(start, end), |
| 143 | tier: 'whitespace', |
| 144 | occurrences: matches.length, |
| 145 | }; |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // ── Tier 2: fuzzy anchor (only when the block is substantial) ── |
| 150 | const nonBlank = searchLines.filter(l => normLine(l).length > 0).length; |
| 151 | if (!allowFuzzy || nonBlank < 2) return null; |
| 152 | |
| 153 | let best = { score: -1, index: -1 }; |
| 154 | let second = { score: -1, index: -1 }; |
| 155 | for (let i = 0; i + nSearch <= contentLines.length; i++) { |
| 156 | const window = contentLines.slice(i, i + nSearch); |
| 157 | const score = blockSim(window, searchLines); |
| 158 | if (score > best.score) { |
no test coverage detected