@unhingged/vizu-core 0.1.0

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.
@@ -0,0 +1,295 @@
1
+ // src/fingerprint.ts
2
+ var TEXT_SNIPPET_MAX = 80;
3
+ var ANCESTOR_DEPTH = 4;
4
+ var ANCESTOR_MAX_SHIFT = 3;
5
+ var TEXT_RATIO_THRESHOLD = 0.75;
6
+ var CLASS_JACCARD_THRESHOLD = 0.6;
7
+ function buildSelector(el) {
8
+ const parts = [];
9
+ let current = el;
10
+ let depth = 0;
11
+ while (current && current !== document.documentElement && depth < 8) {
12
+ let part = current.tagName.toLowerCase();
13
+ if (current.id) {
14
+ parts.unshift("#" + CSS.escape(current.id));
15
+ break;
16
+ }
17
+ const cls = Array.from(current.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")).slice(0, 2);
18
+ if (cls.length) part += "." + cls.map((c) => CSS.escape(c)).join(".");
19
+ const parent = current.parentElement;
20
+ if (parent) {
21
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
22
+ if (sameTag.length > 1) {
23
+ const idx = sameTag.indexOf(current);
24
+ part += ":nth-of-type(" + (idx + 1) + ")";
25
+ }
26
+ }
27
+ parts.unshift(part);
28
+ current = parent;
29
+ depth++;
30
+ }
31
+ return parts.join(" > ");
32
+ }
33
+ function fingerprint(el) {
34
+ const text = el.innerText || el.textContent || "";
35
+ const parent = el.parentElement;
36
+ let siblingIndex = 0;
37
+ if (parent) {
38
+ siblingIndex = Array.from(parent.children).indexOf(el);
39
+ }
40
+ return {
41
+ selector: buildSelector(el),
42
+ parentSelector: parent ? buildSelector(parent) : "",
43
+ tagName: el.tagName,
44
+ textSnippet: text.trim().slice(0, TEXT_SNIPPET_MAX),
45
+ siblingIndex,
46
+ attributes: {
47
+ id: el.id || void 0,
48
+ classList: Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")),
49
+ role: el.getAttribute("role") || void 0,
50
+ ariaLabel: el.getAttribute("aria-label") || void 0,
51
+ dataKey: el.getAttribute("data-vizu-key") || void 0
52
+ },
53
+ ancestorChain: captureAncestorChain(el, ANCESTOR_DEPTH),
54
+ algorithmVersion: 2
55
+ };
56
+ }
57
+ function captureAncestorChain(el, depth) {
58
+ const steps = [];
59
+ let current = el.parentElement;
60
+ while (current && steps.length < depth) {
61
+ const parent = current.parentElement;
62
+ let nthOfType = 1;
63
+ if (parent) {
64
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
65
+ nthOfType = sameTag.indexOf(current) + 1;
66
+ }
67
+ steps.push({ tag: current.tagName, nthOfType });
68
+ current = parent;
69
+ }
70
+ return steps.length > 0 ? { steps } : void 0;
71
+ }
72
+ function findByFingerprint(fp, root = document) {
73
+ if (fp.attributes.dataKey) {
74
+ try {
75
+ const byKey = root.querySelector(`[data-vizu-key="${CSS.escape(fp.attributes.dataKey)}"]`);
76
+ if (byKey) return { element: byKey, confidence: "exact" };
77
+ } catch {
78
+ }
79
+ return { element: null, confidence: "orphaned" };
80
+ }
81
+ if (fp.attributes.id) {
82
+ try {
83
+ const byId = root.querySelector("#" + CSS.escape(fp.attributes.id));
84
+ if (byId) return { element: byId, confidence: "exact" };
85
+ } catch {
86
+ }
87
+ }
88
+ const candidates = [];
89
+ const classes = fp.attributes.classList;
90
+ if (classes && classes.length > 0) {
91
+ const m = findByClassSignature(root, fp.tagName, classes);
92
+ if (m?.element) candidates.push({ el: m.element, rung: "class" });
93
+ }
94
+ try {
95
+ const bySelector = root.querySelector(fp.selector);
96
+ if (bySelector) candidates.push({ el: bySelector, rung: "selector" });
97
+ } catch {
98
+ }
99
+ if (fp.ancestorChain && fp.ancestorChain.steps.length > 0) {
100
+ const m = findByAncestorChain(root, fp.tagName, fp.ancestorChain.steps);
101
+ if (m?.element) candidates.push({ el: m.element, rung: "chain" });
102
+ } else {
103
+ try {
104
+ const parent = fp.parentSelector ? root.querySelector(fp.parentSelector) : root;
105
+ if (parent) {
106
+ const candidate = parent.children[fp.siblingIndex];
107
+ if (candidate && candidate.tagName === fp.tagName) {
108
+ candidates.push({ el: candidate, rung: "chain" });
109
+ }
110
+ }
111
+ } catch {
112
+ }
113
+ }
114
+ if (fp.textSnippet) {
115
+ const m = findByTextSnippet(root, fp.tagName, fp.textSnippet);
116
+ if (m?.element) candidates.push({ el: m.element, rung: "text" });
117
+ }
118
+ if (candidates.length === 0) {
119
+ return { element: null, confidence: "orphaned" };
120
+ }
121
+ const votes = /* @__PURE__ */ new Map();
122
+ for (const c of candidates) {
123
+ let s = votes.get(c.el);
124
+ if (!s) {
125
+ s = /* @__PURE__ */ new Set();
126
+ votes.set(c.el, s);
127
+ }
128
+ s.add(c.rung);
129
+ }
130
+ let best = null;
131
+ for (const [el, rungs] of votes) {
132
+ if (!best || rungs.size > best.rungs.size) best = { el, rungs };
133
+ }
134
+ if (!best) return { element: null, confidence: "orphaned" };
135
+ if (best.rungs.size >= 3) return { element: best.el, confidence: "likely" };
136
+ if (best.rungs.size >= 2) return { element: best.el, confidence: "drifted" };
137
+ if (best.rungs.has("class") || best.rungs.has("chain")) {
138
+ return { element: best.el, confidence: "drifted" };
139
+ }
140
+ return { element: null, confidence: "orphaned" };
141
+ }
142
+ function findByClassSignature(root, tagName, needle) {
143
+ const needleSet = new Set(needle.filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")));
144
+ if (needleSet.size === 0) return null;
145
+ const all = root.querySelectorAll(tagName.toLowerCase());
146
+ let best = null;
147
+ let secondScore = 0;
148
+ for (const el of all) {
149
+ const haystack = new Set(
150
+ Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-"))
151
+ );
152
+ if (haystack.size === 0) continue;
153
+ const score = jaccardSimilarity(needleSet, haystack);
154
+ if (score < CLASS_JACCARD_THRESHOLD) continue;
155
+ if (!best || score > best.score) {
156
+ secondScore = best?.score ?? 0;
157
+ best = { el, score };
158
+ } else if (score > secondScore) {
159
+ secondScore = score;
160
+ }
161
+ }
162
+ if (!best) return null;
163
+ if (best.score - secondScore < 0.1) return null;
164
+ return { element: best.el, confidence: "likely" };
165
+ }
166
+ function findByAncestorChain(root, tagName, steps) {
167
+ const all = root.querySelectorAll(tagName.toLowerCase());
168
+ let best = null;
169
+ for (const el of all) {
170
+ const ancestors = collectAncestorSteps(el, steps.length + ANCESTOR_MAX_SHIFT);
171
+ const score = scoreChainAgainstAncestors(ancestors, steps, ANCESTOR_MAX_SHIFT);
172
+ if (score < 2) continue;
173
+ if (!best || score > best.score) best = { el, score };
174
+ }
175
+ if (!best) return null;
176
+ return {
177
+ element: best.el,
178
+ confidence: best.score === steps.length ? "likely" : "drifted"
179
+ };
180
+ }
181
+ function collectAncestorSteps(el, limit) {
182
+ const steps = [];
183
+ let current = el.parentElement;
184
+ while (current && steps.length < limit) {
185
+ const parent = current.parentElement;
186
+ let nth = 1;
187
+ if (parent) {
188
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
189
+ nth = sameTag.indexOf(current) + 1;
190
+ }
191
+ steps.push({ tag: current.tagName, nthOfType: nth });
192
+ current = parent;
193
+ }
194
+ return steps;
195
+ }
196
+ function findByTextSnippet(root, tagName, snippet) {
197
+ const needle = normalizeText(snippet);
198
+ if (!needle) return null;
199
+ const all = root.querySelectorAll(tagName.toLowerCase());
200
+ let best = null;
201
+ for (const el of all) {
202
+ const raw = el.innerText || el.textContent || "";
203
+ const candidate = normalizeText(raw.slice(0, TEXT_SNIPPET_MAX * 2));
204
+ if (!candidate) continue;
205
+ const score = levenshteinRatio(needle, candidate);
206
+ if (score < TEXT_RATIO_THRESHOLD) continue;
207
+ if (!best || score > best.score) {
208
+ best = { el, score };
209
+ }
210
+ }
211
+ return best ? { element: best.el, confidence: "drifted" } : null;
212
+ }
213
+ function normalizeText(input) {
214
+ return input.toLowerCase().replace(/\s+/g, " ").trim();
215
+ }
216
+ function levenshteinRatio(a, b) {
217
+ if (a === b) return 1;
218
+ if (a.length === 0 || b.length === 0) return a.length === b.length ? 1 : 0;
219
+ const maxLen = Math.max(a.length, b.length);
220
+ const distance = levenshteinDistance(a, b);
221
+ return 1 - distance / maxLen;
222
+ }
223
+ function levenshteinDistance(a, b) {
224
+ if (a.length > b.length) {
225
+ const tmp = a;
226
+ a = b;
227
+ b = tmp;
228
+ }
229
+ const prev = new Array(a.length + 1);
230
+ const curr = new Array(a.length + 1);
231
+ for (let i = 0; i <= a.length; i++) prev[i] = i;
232
+ for (let j = 1; j <= b.length; j++) {
233
+ curr[0] = j;
234
+ for (let i = 1; i <= a.length; i++) {
235
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
236
+ curr[i] = Math.min(
237
+ curr[i - 1] + 1,
238
+ // insert
239
+ prev[i] + 1,
240
+ // delete
241
+ prev[i - 1] + cost
242
+ // substitute
243
+ );
244
+ }
245
+ for (let i = 0; i <= a.length; i++) prev[i] = curr[i];
246
+ }
247
+ return prev[a.length];
248
+ }
249
+ function scoreChainAgainstAncestors(actual, captured, maxShift) {
250
+ let best = 0;
251
+ for (let offset = 0; offset <= maxShift; offset++) {
252
+ let score = 0;
253
+ for (let i = 0; i < captured.length; i++) {
254
+ const a = actual[i + offset];
255
+ if (!a) break;
256
+ if (a.tag === captured[i].tag && a.nthOfType === captured[i].nthOfType) {
257
+ score++;
258
+ }
259
+ }
260
+ if (score > best) best = score;
261
+ }
262
+ return best;
263
+ }
264
+ function jaccardSimilarity(a, b) {
265
+ if (a.size === 0 && b.size === 0) return 1;
266
+ let intersection = 0;
267
+ for (const item of a) if (b.has(item)) intersection++;
268
+ const union = a.size + b.size - intersection;
269
+ return union === 0 ? 0 : intersection / union;
270
+ }
271
+ function findElementByFingerprint(fp, root) {
272
+ return findByFingerprint(fp, root).element;
273
+ }
274
+ function fingerprintKey(fp) {
275
+ return fp.selector + "|" + fp.textSnippet;
276
+ }
277
+ function fingerprintLabel(fp) {
278
+ const tag = fp.tagName.toLowerCase();
279
+ if (fp.textSnippet) {
280
+ const snippet = fp.textSnippet.length > 24 ? fp.textSnippet.slice(0, 24) + "\u2026" : fp.textSnippet;
281
+ return tag + ": " + snippet;
282
+ }
283
+ if (fp.attributes.id) return tag + "#" + fp.attributes.id;
284
+ if (fp.attributes.classList && fp.attributes.classList.length) return tag + "." + fp.attributes.classList[0];
285
+ return tag;
286
+ }
287
+
288
+ export {
289
+ fingerprint,
290
+ findByFingerprint,
291
+ findElementByFingerprint,
292
+ fingerprintKey,
293
+ fingerprintLabel
294
+ };
295
+ //# sourceMappingURL=chunk-OMIFOSQ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fingerprint.ts"],"sourcesContent":["import type { AncestorStep, ElementFingerprint, FingerprintMatch, MatchConfidence } from './types';\n\nconst TEXT_SNIPPET_MAX = 80;\n\n/**\n * Depth of the ancestor chain we capture and match against. Four steps\n * gives deep elements one extra disambiguating entry, and lets shallow\n * elements reach all the way to `<html>` for shift-search anchor.\n */\nconst ANCESTOR_DEPTH = 4;\n\n/**\n * Maximum number of wrapper additions we tolerate when matching an\n * ancestor chain. A wrapper insertion shifts the captured chain up by\n * one in the live tree; we shift-search through 0..N offsets to absorb\n * that without losing the match. Set to 3 to cover most real-world\n * refactors (Tailwind component extraction, A/B-test wrappers, design-\n * system migrations typically add 1-2 layers).\n */\nconst ANCESTOR_MAX_SHIFT = 3;\n\n/**\n * Tunable thresholds for the fuzzy matchers below. Tradeoff: lower =\n * more false-positive \"drifted\" matches (already wear a warning chip),\n * higher = more orphaned real matches (worse UX: comment leaves the\n * page). Both failure modes are visible to the user; the lower setting\n * loses less work. The stress harness (Phase 11.4.3) is the source of\n * truth for these values; do not relax them further without numbers.\n */\nconst TEXT_RATIO_THRESHOLD = 0.75;\nconst CLASS_JACCARD_THRESHOLD = 0.6;\n\nexport function buildSelector(el: Element): string {\n const parts: string[] = [];\n let current: Element | null = el;\n let depth = 0;\n while (current && current !== document.documentElement && depth < 8) {\n let part = current.tagName.toLowerCase();\n if (current.id) {\n parts.unshift('#' + CSS.escape(current.id));\n break;\n }\n const cls = Array.from(current.classList)\n .filter((c) => !c.startsWith('vz-') && !c.startsWith('vizu-'))\n .slice(0, 2);\n if (cls.length) part += '.' + cls.map((c) => CSS.escape(c)).join('.');\n const parent: Element | null = current.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === current!.tagName);\n if (sameTag.length > 1) {\n const idx = sameTag.indexOf(current);\n part += ':nth-of-type(' + (idx + 1) + ')';\n }\n }\n parts.unshift(part);\n current = parent;\n depth++;\n }\n return parts.join(' > ');\n}\n\nexport function fingerprint(el: Element): ElementFingerprint {\n const text = (el as HTMLElement).innerText || el.textContent || '';\n const parent = el.parentElement;\n let siblingIndex = 0;\n if (parent) {\n siblingIndex = Array.from(parent.children).indexOf(el);\n }\n return {\n selector: buildSelector(el),\n parentSelector: parent ? buildSelector(parent) : '',\n tagName: el.tagName,\n textSnippet: text.trim().slice(0, TEXT_SNIPPET_MAX),\n siblingIndex,\n attributes: {\n id: el.id || undefined,\n classList: Array.from(el.classList).filter((c) => !c.startsWith('vz-') && !c.startsWith('vizu-')),\n role: el.getAttribute('role') || undefined,\n ariaLabel: el.getAttribute('aria-label') || undefined,\n dataKey: el.getAttribute('data-vizu-key') || undefined,\n },\n ancestorChain: captureAncestorChain(el, ANCESTOR_DEPTH),\n algorithmVersion: 2,\n };\n}\n\n/**\n * Walk up from the element, collecting up to `depth` ancestors as\n * `{tag, nthOfType}` pairs. Includes `<html>` so shallow elements\n * (top-level sections, page-level h1s) still capture three chain\n * entries — without HTML, those bottom out at `[MAIN, BODY]` which\n * isn't long enough to score ≥2 after a single wrapper insertion.\n * `<html>` is always nthOfType 1; the value adds no discriminating\n * info on its own, but provides a fixed anchor for shift-search to\n * align against.\n *\n * Returns undefined when the element is detached.\n */\nexport function captureAncestorChain(\n el: Element,\n depth: number,\n): { steps: AncestorStep[] } | undefined {\n const steps: AncestorStep[] = [];\n let current: Element | null = el.parentElement;\n while (current && steps.length < depth) {\n const parent: Element | null = current.parentElement;\n let nthOfType = 1;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === current!.tagName);\n nthOfType = sameTag.indexOf(current) + 1;\n }\n steps.push({ tag: current.tagName, nthOfType });\n current = parent;\n }\n return steps.length > 0 ? { steps } : undefined;\n}\n\n/**\n * Resolve a fingerprint to a live DOM element with a confidence tag.\n *\n * Two-phase ladder:\n *\n * **Strong identifiers (short-circuit).** `data-vizu-key` and `#id`\n * are host-explicit anchors — if the captured key exists in the DOM\n * we trust it 100% and return **exact**. For `data-vizu-key`, the\n * *absence* of the captured key is itself evidence: the host marked\n * that element explicitly and the mark is gone, so we **orphan**\n * without consulting weaker rungs. `id` gets the gentler treatment\n * (fall through on miss) because ids get renamed without the element\n * itself going away.\n *\n * **Fuzzy rungs vote (Phase 11.4.4 / #135).** When no strong\n * identifier resolves, every fuzzy rung (class signature, CSS selector,\n * ancestor chain, text snippet) returns its best candidate. We tally\n * votes per element and require ≥ 2 rungs to agree on the same target\n * before returning a match. 3+ votes → **likely**, 2 votes → **drifted**,\n * ≤ 1 vote → **orphaned**.\n *\n * The vote requirement is what catches `element_deleted`: the actual\n * element is gone, so each fuzzy rung finds a *different* nearby\n * element. Votes scatter, no consensus, orphan. Before this, the\n * matcher returned the first rung's hit and confidently mis-anchored\n * the comment to a similar element nearby — the false-positive\n * surfaced by the Phase 11.4.3 stress harness.\n */\nexport function findByFingerprint(fp: ElementFingerprint, root: ParentNode = document): FingerprintMatch {\n // ─── Strong identifier: data-vizu-key ─────────────────────────────\n if (fp.attributes.dataKey) {\n try {\n const byKey = root.querySelector(`[data-vizu-key=\"${CSS.escape(fp.attributes.dataKey)}\"]`);\n if (byKey) return { element: byKey, confidence: 'exact' };\n } catch {}\n // Strong-identifier honor: the host explicitly anchored this element\n // with a key. The key is gone. Don't fuzzy-guess to a similar\n // element nearby — that's the worst kind of false positive.\n return { element: null, confidence: 'orphaned' };\n }\n\n // ─── Strong identifier: #id ───────────────────────────────────────\n if (fp.attributes.id) {\n try {\n const byId = root.querySelector('#' + CSS.escape(fp.attributes.id));\n if (byId) return { element: byId, confidence: 'exact' };\n } catch {}\n // Don't orphan on missing id — ids get renamed in design-system\n // migrations without the element going away. Fall through.\n }\n\n // ─── Fuzzy rungs cast votes ───────────────────────────────────────\n type Rung = 'class' | 'selector' | 'chain' | 'text';\n const candidates: Array<{ el: Element; rung: Rung }> = [];\n\n const classes = fp.attributes.classList;\n if (classes && classes.length > 0) {\n const m = findByClassSignature(root, fp.tagName, classes);\n if (m?.element) candidates.push({ el: m.element, rung: 'class' });\n }\n\n try {\n const bySelector = root.querySelector(fp.selector);\n if (bySelector) candidates.push({ el: bySelector, rung: 'selector' });\n } catch {}\n\n if (fp.ancestorChain && fp.ancestorChain.steps.length > 0) {\n const m = findByAncestorChain(root, fp.tagName, fp.ancestorChain.steps);\n if (m?.element) candidates.push({ el: m.element, rung: 'chain' });\n } else {\n // Legacy v1 fingerprint: structural fallback (parent + sibling index + tag).\n try {\n const parent = fp.parentSelector\n ? (root.querySelector(fp.parentSelector) as Element | null)\n : (root as Element);\n if (parent) {\n const candidate = parent.children[fp.siblingIndex];\n if (candidate && candidate.tagName === fp.tagName) {\n candidates.push({ el: candidate, rung: 'chain' });\n }\n }\n } catch {}\n }\n\n if (fp.textSnippet) {\n const m = findByTextSnippet(root, fp.tagName, fp.textSnippet);\n if (m?.element) candidates.push({ el: m.element, rung: 'text' });\n }\n\n if (candidates.length === 0) {\n return { element: null, confidence: 'orphaned' };\n }\n\n // Tally votes per element. We dedupe via a Map keyed by element identity\n // (Map handles Element references natively — no need to materialize keys).\n const votes = new Map<Element, Set<Rung>>();\n for (const c of candidates) {\n let s = votes.get(c.el);\n if (!s) {\n s = new Set<Rung>();\n votes.set(c.el, s);\n }\n s.add(c.rung);\n }\n\n let best: { el: Element; rungs: Set<Rung> } | null = null;\n for (const [el, rungs] of votes) {\n if (!best || rungs.size > best.rungs.size) best = { el, rungs };\n }\n\n if (!best) return { element: null, confidence: 'orphaned' };\n if (best.rungs.size >= 3) return { element: best.el, confidence: 'likely' };\n if (best.rungs.size >= 2) return { element: best.el, confidence: 'drifted' };\n // Single vote. Trust it only when the rung is structurally precise:\n // - class signature already filters out ambiguous matches upstream\n // - ancestor chain requires ≥2/3 score\n // Selector-alone and text-alone stay too noisy — they're the rungs\n // that confidently mis-anchor when the captured element is gone.\n if (best.rungs.has('class') || best.rungs.has('chain')) {\n return { element: best.el, confidence: 'drifted' };\n }\n return { element: null, confidence: 'orphaned' };\n}\n\n/**\n * Scan elements of the captured tag, score each by classList Jaccard\n * similarity, and return the best match only when it's UNAMBIGUOUSLY\n * the top candidate. Ambiguous results fall through to the more\n * specific selector / chain / text rungs — a misfire here would assign\n * the comment to the wrong element, which is worse than orphaning.\n *\n * \"Unambiguous\" = best score ≥ {@link CLASS_JACCARD_THRESHOLD} AND\n * gap to the runner-up is ≥ 0.1. Returns \"likely\" or null.\n */\nfunction findByClassSignature(\n root: ParentNode,\n tagName: string,\n needle: string[],\n): FingerprintMatch | null {\n const needleSet = new Set(needle.filter((c) => !c.startsWith('vz-') && !c.startsWith('vizu-')));\n if (needleSet.size === 0) return null;\n const all = root.querySelectorAll(tagName.toLowerCase());\n let best: { el: Element; score: number } | null = null;\n let secondScore = 0;\n for (const el of all) {\n const haystack = new Set(\n Array.from(el.classList).filter((c) => !c.startsWith('vz-') && !c.startsWith('vizu-')),\n );\n if (haystack.size === 0) continue;\n const score = jaccardSimilarity(needleSet, haystack);\n if (score < CLASS_JACCARD_THRESHOLD) continue;\n if (!best || score > best.score) {\n secondScore = best?.score ?? 0;\n best = { el, score };\n } else if (score > secondScore) {\n secondScore = score;\n }\n }\n if (!best) return null;\n if (best.score - secondScore < 0.1) return null; // ambiguous — fall through\n return { element: best.el, confidence: 'likely' };\n}\n\n/**\n * Match candidates of the captured tag by walking their actual ancestor\n * chain and scoring against the captured chain. Returns \"likely\" when\n * every captured step lines up (perfect chain) at some shift offset,\n * \"drifted\" when at least 2/3 line up at the best offset. Anything less\n * is not returned — fall through to the fuzzy text matcher below.\n */\nfunction findByAncestorChain(\n root: ParentNode,\n tagName: string,\n steps: AncestorStep[],\n): FingerprintMatch | null {\n const all = root.querySelectorAll(tagName.toLowerCase());\n let best: { el: Element; score: number } | null = null;\n for (const el of all) {\n const ancestors = collectAncestorSteps(el, steps.length + ANCESTOR_MAX_SHIFT);\n const score = scoreChainAgainstAncestors(ancestors, steps, ANCESTOR_MAX_SHIFT);\n if (score < 2) continue; // threshold: at least 2 ancestors line up\n if (!best || score > best.score) best = { el, score };\n }\n if (!best) return null;\n return {\n element: best.el,\n confidence: best.score === steps.length ? 'likely' : 'drifted',\n };\n}\n\n/**\n * Walk up from the candidate, collecting `limit` ancestors as\n * `AncestorStep` records. Same shape as {@link captureAncestorChain},\n * extracted so the matcher can collect more than the captured depth\n * (to allow shift-search through wrapper insertions).\n */\nfunction collectAncestorSteps(el: Element, limit: number): AncestorStep[] {\n const steps: AncestorStep[] = [];\n let current: Element | null = el.parentElement;\n while (current && steps.length < limit) {\n const parent: Element | null = current.parentElement;\n let nth = 1;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === current!.tagName);\n nth = sameTag.indexOf(current) + 1;\n }\n steps.push({ tag: current.tagName, nthOfType: nth });\n current = parent;\n }\n return steps;\n}\n\n/**\n * Levenshtein-scored scan of every element matching the captured tag.\n * Both the candidate text and the needle are whitespace-normalized\n * before scoring so trailing commas, line-wraps, and adjacent emoji\n * don't kill the match.\n */\nfunction findByTextSnippet(\n root: ParentNode,\n tagName: string,\n snippet: string,\n): FingerprintMatch | null {\n const needle = normalizeText(snippet);\n if (!needle) return null;\n const all = root.querySelectorAll(tagName.toLowerCase());\n let best: { el: Element; score: number } | null = null;\n for (const el of all) {\n const raw = (el as HTMLElement).innerText || el.textContent || '';\n const candidate = normalizeText(raw.slice(0, TEXT_SNIPPET_MAX * 2));\n if (!candidate) continue;\n const score = levenshteinRatio(needle, candidate);\n if (score < TEXT_RATIO_THRESHOLD) continue;\n if (!best || score > best.score) {\n best = { el, score };\n }\n }\n return best ? { element: best.el, confidence: 'drifted' } : null;\n}\n\n/* ─── Pure helpers (testable without a DOM) ────────────────────────────── */\n\n/**\n * Lowercase, collapse internal whitespace, trim. Stable input to the\n * Levenshtein scorer — without it, \"Try it now\" vs \"Try it now\\n\"\n * would score as different strings even though a human reads them\n * identically.\n */\nexport function normalizeText(input: string): string {\n return input.toLowerCase().replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Normalized Levenshtein similarity in [0, 1]. 1 means identical, 0\n * means completely different. Computed as\n * `1 - distance / max(len)`. Two empty strings score 1.0.\n *\n * O(n*m) time / O(min(n,m)) space — fine for our 80-char snippets even\n * on pages with thousands of candidates.\n */\nexport function levenshteinRatio(a: string, b: string): number {\n if (a === b) return 1;\n if (a.length === 0 || b.length === 0) return a.length === b.length ? 1 : 0;\n const maxLen = Math.max(a.length, b.length);\n const distance = levenshteinDistance(a, b);\n return 1 - distance / maxLen;\n}\n\nfunction levenshteinDistance(a: string, b: string): number {\n // Make `b` the longer one to keep the row buffer small.\n if (a.length > b.length) {\n const tmp = a;\n a = b;\n b = tmp;\n }\n const prev = new Array<number>(a.length + 1);\n const curr = new Array<number>(a.length + 1);\n for (let i = 0; i <= a.length; i++) prev[i] = i;\n for (let j = 1; j <= b.length; j++) {\n curr[0] = j;\n for (let i = 1; i <= a.length; i++) {\n const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;\n curr[i] = Math.min(\n curr[i - 1] + 1, // insert\n prev[i] + 1, // delete\n prev[i - 1] + cost, // substitute\n );\n }\n for (let i = 0; i <= a.length; i++) prev[i] = curr[i];\n }\n return prev[a.length];\n}\n\n/**\n * Score how many captured ancestor steps line up against an actual\n * ancestor list. Tries every offset in `[0, maxShift]` (to absorb up\n * to `maxShift` wrapper insertions between target and root) and\n * returns the best score across offsets.\n *\n * Pure helper — testable without a DOM. The DOM-aware\n * {@link findByAncestorChain} feeds it the captured chain plus the\n * candidate's actual ancestor list (collected one level deeper than\n * the captured chain so the shift-search has room to slide).\n */\nexport function scoreChainAgainstAncestors(\n actual: AncestorStep[],\n captured: AncestorStep[],\n maxShift: number,\n): number {\n let best = 0;\n for (let offset = 0; offset <= maxShift; offset++) {\n let score = 0;\n for (let i = 0; i < captured.length; i++) {\n const a = actual[i + offset];\n if (!a) break;\n if (a.tag === captured[i].tag && a.nthOfType === captured[i].nthOfType) {\n score++;\n }\n }\n if (score > best) best = score;\n }\n return best;\n}\n\n/**\n * Jaccard similarity = |A ∩ B| / |A ∪ B|. Returns 1 for identical\n * non-empty sets, 0 when disjoint. Two empty sets score 1.0 — the\n * caller checks size beforehand for that case.\n */\nexport function jaccardSimilarity<T>(a: Set<T>, b: Set<T>): number {\n if (a.size === 0 && b.size === 0) return 1;\n let intersection = 0;\n for (const item of a) if (b.has(item)) intersection++;\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\n/** Backwards-compat: callers that only need the element. */\nexport function findElementByFingerprint(fp: ElementFingerprint, root?: ParentNode): Element | null {\n return findByFingerprint(fp, root).element;\n}\n\nexport function fingerprintKey(fp: ElementFingerprint): string {\n return fp.selector + '|' + fp.textSnippet;\n}\n\n/** Short human-readable label for an element, used in `#` reference chips. */\nexport function fingerprintLabel(fp: ElementFingerprint): string {\n const tag = fp.tagName.toLowerCase();\n if (fp.textSnippet) {\n const snippet = fp.textSnippet.length > 24 ? fp.textSnippet.slice(0, 24) + '…' : fp.textSnippet;\n return tag + ': ' + snippet;\n }\n if (fp.attributes.id) return tag + '#' + fp.attributes.id;\n if (fp.attributes.classList && fp.attributes.classList.length) return tag + '.' + fp.attributes.classList[0];\n return tag;\n}\n"],"mappings":"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","names":[]}