@ia-qa/self-healing 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.
Files changed (54) hide show
  1. package/README.md +91 -0
  2. package/TUTORIAL.md +399 -0
  3. package/dist/aom.d.ts +22 -0
  4. package/dist/aom.js +58 -0
  5. package/dist/aom.js.map +1 -0
  6. package/dist/browser/contract.d.ts +1 -0
  7. package/dist/browser/contract.js +107 -0
  8. package/dist/browser/contract.js.map +1 -0
  9. package/dist/browser/extract.d.ts +25 -0
  10. package/dist/browser/extract.js +194 -0
  11. package/dist/browser/extract.js.map +1 -0
  12. package/dist/browser/match.d.ts +88 -0
  13. package/dist/browser/match.js +179 -0
  14. package/dist/browser/match.js.map +1 -0
  15. package/dist/cli/diff.d.ts +13 -0
  16. package/dist/cli/diff.js +143 -0
  17. package/dist/cli/diff.js.map +1 -0
  18. package/dist/cli/fix.d.ts +17 -0
  19. package/dist/cli/fix.js +144 -0
  20. package/dist/cli/fix.js.map +1 -0
  21. package/dist/cli/index.d.ts +2 -0
  22. package/dist/cli/index.js +50 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/init.d.ts +8 -0
  25. package/dist/cli/init.js +185 -0
  26. package/dist/cli/init.js.map +1 -0
  27. package/dist/cli/map.d.ts +9 -0
  28. package/dist/cli/map.js +118 -0
  29. package/dist/cli/map.js.map +1 -0
  30. package/dist/config.d.ts +55 -0
  31. package/dist/config.js +111 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/fixEngine.d.ts +49 -0
  34. package/dist/fixEngine.js +154 -0
  35. package/dist/fixEngine.js.map +1 -0
  36. package/dist/index.d.ts +12 -0
  37. package/dist/index.js +29 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/launcher.d.ts +38 -0
  40. package/dist/launcher.js +38 -0
  41. package/dist/launcher.js.map +1 -0
  42. package/dist/mapUrl.d.ts +34 -0
  43. package/dist/mapUrl.js +90 -0
  44. package/dist/mapUrl.js.map +1 -0
  45. package/dist/markdown.d.ts +10 -0
  46. package/dist/markdown.js +54 -0
  47. package/dist/markdown.js.map +1 -0
  48. package/dist/mcp/server.d.ts +2 -0
  49. package/dist/mcp/server.js +251 -0
  50. package/dist/mcp/server.js.map +1 -0
  51. package/dist/playwright/healer.d.ts +39 -0
  52. package/dist/playwright/healer.js +84 -0
  53. package/dist/playwright/healer.js.map +1 -0
  54. package/package.json +54 -0
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Page contract as Markdown — the framework- and language-agnostic artifact an
4
+ * LLM reads to auto-heal a broken UI test.
5
+ *
6
+ * Pure, dependency-free JS (no imports, no fs) so both the CLI and ia-qa.com's
7
+ * web tool render byte-identical contracts from the same mapping. The CLI writes
8
+ * the result to `<page>.md`; the browser offers it as a download.
9
+ *
10
+ * The workflow it enables: `map` emits `<page>.json` (for tooling / diff) AND
11
+ * `<page>.md` (for a model). When a Cypress/Playwright/Selenium test breaks in
12
+ * CI, you hand the failing test file + this `.md` to an LLM: it finds the element
13
+ * by role + accessible name, reads the current selector, and rewrites the test in
14
+ * whatever framework/language it is written in. The self-describing header tells
15
+ * the model how to use the file.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.renderPageContract = renderPageContract;
19
+ const ROLE_ORDER = [
20
+ 'textbox',
21
+ 'combobox',
22
+ 'checkbox',
23
+ 'radio',
24
+ 'slider',
25
+ 'button',
26
+ 'link',
27
+ 'tab',
28
+ 'menuitem',
29
+ ];
30
+ function escapeCell(s) {
31
+ return (s || '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
32
+ }
33
+ function groupByRole(elements) {
34
+ const groups = new Map();
35
+ for (const el of elements) {
36
+ const list = groups.get(el.role);
37
+ if (list)
38
+ list.push(el);
39
+ else
40
+ groups.set(el.role, [el]);
41
+ }
42
+ const roles = Array.from(groups.keys()).sort((a, b) => {
43
+ const ia = ROLE_ORDER.indexOf(a);
44
+ const ib = ROLE_ORDER.indexOf(b);
45
+ if (ia === -1 && ib === -1)
46
+ return a.localeCompare(b);
47
+ if (ia === -1)
48
+ return 1;
49
+ if (ib === -1)
50
+ return -1;
51
+ return ia - ib;
52
+ });
53
+ const ordered = new Map();
54
+ for (const role of roles)
55
+ ordered.set(role, groups.get(role));
56
+ return ordered;
57
+ }
58
+ function renderPageContract(mapping) {
59
+ const page = mapping.page || 'page';
60
+ const url = mapping.url || '';
61
+ const capturedAt = mapping.capturedAt || '';
62
+ const elements = (mapping && mapping.elements) || [];
63
+ const groups = groupByRole(elements);
64
+ const lines = [];
65
+ lines.push('# Page contract — ' + page);
66
+ lines.push('');
67
+ lines.push('- **URL:** `' + url + '`');
68
+ lines.push('- **Captured:** ' + capturedAt);
69
+ lines.push('- **Interactive elements:** ' + elements.length);
70
+ lines.push('');
71
+ lines.push('> Generated by `@ia-qa/self-healing`. This file is the **current source of truth** for the');
72
+ lines.push('> selectors on this page. **To heal a broken UI test** (any framework — Cypress, Playwright,');
73
+ lines.push('> Selenium… — any language): find the element below by its **role** and **accessible name**,');
74
+ lines.push('> then replace the stale selector in the test with the current `selector`. If an element from');
75
+ lines.push('> the test is absent here, it was removed or renamed — flag it instead of guessing.');
76
+ lines.push('');
77
+ if (elements.length === 0) {
78
+ lines.push('_No visible interactive elements were captured on this page._');
79
+ lines.push('');
80
+ return lines.join('\n');
81
+ }
82
+ const anyContext = elements.some((el) => el.context);
83
+ for (const [role, els] of groups) {
84
+ lines.push('## ' + role + ' (' + els.length + ')');
85
+ lines.push('');
86
+ if (anyContext) {
87
+ lines.push('| Accessible name | Context | Selector |');
88
+ lines.push('| --- | --- | --- |');
89
+ for (const el of els) {
90
+ const name = el.name ? escapeCell(el.name) : '_(no accessible name)_';
91
+ const ctx = el.context ? escapeCell(el.context) : '—';
92
+ lines.push('| ' + name + ' | ' + ctx + ' | `' + escapeCell(el.selector) + '` |');
93
+ }
94
+ }
95
+ else {
96
+ lines.push('| Accessible name | Selector |');
97
+ lines.push('| --- | --- |');
98
+ for (const el of els) {
99
+ const name = el.name ? escapeCell(el.name) : '_(no accessible name)_';
100
+ lines.push('| ' + name + ' | `' + escapeCell(el.selector) + '` |');
101
+ }
102
+ }
103
+ lines.push('');
104
+ }
105
+ return lines.join('\n');
106
+ }
107
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/browser/contract.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Accessibility-oriented DOM extraction — the single source of truth.
3
+ *
4
+ * This function is used three ways and MUST stay self-contained (no imports, no
5
+ * closures over module scope, no TypeScript syntax):
6
+ * 1. `ia-qa-heal map` passes it to Playwright's `page.evaluate()`, which
7
+ * serializes it via `toString()` and injects it into the page.
8
+ * 2. The healer re-runs it on the live page to find healing candidates.
9
+ * 3. ia-qa.com imports this file with `?raw` and shows it as a copy-paste
10
+ * DevTools snippet, so the zero-install flow produces byte-identical
11
+ * mappings to the CLI.
12
+ *
13
+ * Any dependency added here silently breaks (1) and (3).
14
+ *
15
+ * `page.accessibility.snapshot()` is deprecated and returns nodes *without any
16
+ * selector*, which makes it useless for a mapping meant to drive clicks. We walk
17
+ * the DOM ourselves and compute, per visible interactive element: its ARIA role,
18
+ * its accessible name (accname priority order), and a stable selector.
19
+ */
20
+ export function extractInPage(): {
21
+ role: any;
22
+ name: string;
23
+ selector: string;
24
+ context: string;
25
+ }[];
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractInPage = extractInPage;
4
+ /**
5
+ * Accessibility-oriented DOM extraction — the single source of truth.
6
+ *
7
+ * This function is used three ways and MUST stay self-contained (no imports, no
8
+ * closures over module scope, no TypeScript syntax):
9
+ * 1. `ia-qa-heal map` passes it to Playwright's `page.evaluate()`, which
10
+ * serializes it via `toString()` and injects it into the page.
11
+ * 2. The healer re-runs it on the live page to find healing candidates.
12
+ * 3. ia-qa.com imports this file with `?raw` and shows it as a copy-paste
13
+ * DevTools snippet, so the zero-install flow produces byte-identical
14
+ * mappings to the CLI.
15
+ *
16
+ * Any dependency added here silently breaks (1) and (3).
17
+ *
18
+ * `page.accessibility.snapshot()` is deprecated and returns nodes *without any
19
+ * selector*, which makes it useless for a mapping meant to drive clicks. We walk
20
+ * the DOM ourselves and compute, per visible interactive element: its ARIA role,
21
+ * its accessible name (accname priority order), and a stable selector.
22
+ */
23
+ function extractInPage() {
24
+ const INTERACTIVE_SELECTOR = [
25
+ 'button',
26
+ 'a[href]',
27
+ 'input:not([type="hidden"])',
28
+ 'select',
29
+ 'textarea',
30
+ '[role="button"]',
31
+ '[role="link"]',
32
+ '[role="textbox"]',
33
+ '[role="combobox"]',
34
+ '[role="checkbox"]',
35
+ '[role="radio"]',
36
+ '[role="menuitem"]',
37
+ '[role="tab"]',
38
+ '[onclick]',
39
+ ].join(', ');
40
+ function isVisible(el) {
41
+ const rect = el.getBoundingClientRect();
42
+ if (rect.width === 0 || rect.height === 0)
43
+ return false;
44
+ const style = window.getComputedStyle(el);
45
+ return style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
46
+ }
47
+ function role(el) {
48
+ const explicit = el.getAttribute('role');
49
+ if (explicit)
50
+ return explicit;
51
+ const tag = el.tagName.toLowerCase();
52
+ if (tag === 'a')
53
+ return 'link';
54
+ if (tag === 'button')
55
+ return 'button';
56
+ if (tag === 'select')
57
+ return 'combobox';
58
+ if (tag === 'textarea')
59
+ return 'textbox';
60
+ if (tag === 'input') {
61
+ const type = el.type;
62
+ if (type === 'checkbox')
63
+ return 'checkbox';
64
+ if (type === 'radio')
65
+ return 'radio';
66
+ if (type === 'submit' || type === 'button' || type === 'reset')
67
+ return 'button';
68
+ if (type === 'range')
69
+ return 'slider';
70
+ return 'textbox';
71
+ }
72
+ return 'generic';
73
+ }
74
+ function accessibleName(el) {
75
+ const clean = (s) => (s == null ? '' : String(s)).trim().replace(/\s+/g, ' ').slice(0, 120);
76
+ const ariaLabel = el.getAttribute('aria-label');
77
+ if (ariaLabel)
78
+ return clean(ariaLabel);
79
+ const labelledBy = el.getAttribute('aria-labelledby');
80
+ if (labelledBy) {
81
+ const joined = labelledBy
82
+ .split(/\s+/)
83
+ .map((id) => {
84
+ const node = document.getElementById(id);
85
+ return node ? node.textContent : '';
86
+ })
87
+ .join(' ');
88
+ if (clean(joined))
89
+ return clean(joined);
90
+ }
91
+ const tag = el.tagName.toLowerCase();
92
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
93
+ const label = el.labels && el.labels.length > 0 ? el.labels[0].textContent : '';
94
+ if (clean(label))
95
+ return clean(label);
96
+ if (tag === 'input' && ['submit', 'button', 'reset'].indexOf(el.type) !== -1 && el.value) {
97
+ return clean(el.value);
98
+ }
99
+ const placeholder = el.getAttribute('placeholder');
100
+ if (placeholder)
101
+ return clean(placeholder);
102
+ const nameAttr = el.getAttribute('name');
103
+ if (nameAttr)
104
+ return clean(nameAttr);
105
+ }
106
+ const title = el.getAttribute('title');
107
+ const text = clean(el.textContent);
108
+ return text || clean(title);
109
+ }
110
+ function stableSelector(el) {
111
+ const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
112
+ if (testId)
113
+ return '[data-testid="' + testId + '"]';
114
+ if (el.id)
115
+ return el.tagName.toLowerCase() + '#' + CSS.escape(el.id);
116
+ const nameAttr = el.getAttribute('name');
117
+ if (nameAttr)
118
+ return el.tagName.toLowerCase() + '[name="' + nameAttr + '"]';
119
+ const parts = [];
120
+ let node = el;
121
+ while (node && node !== document.body && parts.length < 5) {
122
+ if (node.id) {
123
+ parts.unshift('#' + CSS.escape(node.id));
124
+ break;
125
+ }
126
+ let part = node.tagName.toLowerCase();
127
+ const parent = node.parentElement;
128
+ if (parent) {
129
+ const current = node;
130
+ const sameTag = Array.prototype.filter.call(parent.children, (c) => c.tagName === current.tagName);
131
+ if (sameTag.length > 1)
132
+ part += ':nth-of-type(' + (sameTag.indexOf(current) + 1) + ')';
133
+ }
134
+ parts.unshift(part);
135
+ node = parent;
136
+ }
137
+ return parts.join(' > ');
138
+ }
139
+ const LANDMARK_TAGS = { MAIN: 'main', NAV: 'nav', HEADER: 'banner', FOOTER: 'contentinfo', ASIDE: 'complementary', FORM: 'form' };
140
+ const LANDMARK_ROLES = ['main', 'navigation', 'banner', 'contentinfo', 'complementary', 'form', 'search', 'region', 'dialog'];
141
+ function clean(s) {
142
+ return (s == null ? '' : String(s)).trim().replace(/\s+/g, ' ').slice(0, 80);
143
+ }
144
+ // Disambiguating context: what container this element lives in, so two elements
145
+ // with the same role + accessible name (e.g. many "Delete" buttons) stay
146
+ // distinguishable. We surface the nearest landmark and the nearest section
147
+ // heading — both human-meaningful and stable against class/id churn.
148
+ function context(el) {
149
+ let landmark = '';
150
+ let node = el.parentElement;
151
+ while (node && node !== document.body) {
152
+ const roleAttr = node.getAttribute('role');
153
+ if (roleAttr && LANDMARK_ROLES.indexOf(roleAttr) !== -1) {
154
+ landmark = clean(node.getAttribute('aria-label')) || roleAttr;
155
+ break;
156
+ }
157
+ if (LANDMARK_TAGS[node.tagName]) {
158
+ landmark = clean(node.getAttribute('aria-label')) || LANDMARK_TAGS[node.tagName];
159
+ break;
160
+ }
161
+ node = node.parentElement;
162
+ }
163
+ // Nearest heading that precedes the element in document order.
164
+ let heading = '';
165
+ const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"]');
166
+ for (let i = headings.length - 1; i >= 0; i--) {
167
+ const h = headings[i];
168
+ const pos = h.compareDocumentPosition(el);
169
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
170
+ heading = clean(h.textContent);
171
+ if (heading)
172
+ break;
173
+ }
174
+ }
175
+ if (landmark && heading)
176
+ return landmark + ' › ' + heading;
177
+ return landmark || heading || '';
178
+ }
179
+ const seen = {};
180
+ const elements = [];
181
+ const found = document.querySelectorAll(INTERACTIVE_SELECTOR);
182
+ for (let i = 0; i < found.length; i++) {
183
+ const el = found[i];
184
+ if (!isVisible(el))
185
+ continue;
186
+ const selector = stableSelector(el);
187
+ if (seen[selector])
188
+ continue;
189
+ seen[selector] = true;
190
+ elements.push({ role: role(el), name: accessibleName(el), selector: selector, context: context(el) });
191
+ }
192
+ return elements;
193
+ }
194
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/browser/extract.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Semantic matching + mapping diff — shared by the Playwright healer and by the
3
+ * zero-install web flow on ia-qa.com.
4
+ *
5
+ * Plain, dependency-free JS so the browser can import it as-is. Keep it that way:
6
+ * if the healer and the web tool ever compute different matches, a selector that
7
+ * ia-qa.com reports as "auto-healable" would still fail in CI, which is worse
8
+ * than reporting nothing.
9
+ */
10
+ export function normalize(s: any): string;
11
+ /** Sørensen–Dice similarity over character bigrams. */
12
+ export function diceSimilarity(a: any, b: any): number;
13
+ /**
14
+ * Resolve `target` against live `candidates`. Returns one of:
15
+ * - { matched: true, selector, score, matchType, name, disambiguatedBy? }
16
+ * - { ambiguous: true, count } — several equally-good elements; refuse to
17
+ * guess (healing the wrong one is worse than failing). A human decides.
18
+ * - null — no candidate matches.
19
+ *
20
+ * Disambiguation: when role + accessible name collide (many "Delete" buttons),
21
+ * the element's `context` (nearest landmark + section heading) breaks the tie.
22
+ * If context can't reduce it to one, we return `ambiguous` rather than pick.
23
+ */
24
+ export function matchElement(target: any, candidates: any): {
25
+ ambiguous: boolean;
26
+ count: any;
27
+ matched?: undefined;
28
+ selector?: undefined;
29
+ score?: undefined;
30
+ matchType?: undefined;
31
+ name?: undefined;
32
+ disambiguatedBy?: undefined;
33
+ } | {
34
+ matched: boolean;
35
+ selector: any;
36
+ score: any;
37
+ matchType: string;
38
+ name: any;
39
+ disambiguatedBy: string | undefined;
40
+ ambiguous?: undefined;
41
+ count?: undefined;
42
+ } | {
43
+ matched: boolean;
44
+ selector: any;
45
+ score: any;
46
+ matchType: string;
47
+ name: any;
48
+ ambiguous?: undefined;
49
+ count?: undefined;
50
+ disambiguatedBy?: undefined;
51
+ } | null;
52
+ /** Healer-facing shim: selector only when unambiguously matched, else null. */
53
+ export function heuristicMatch(target: any, candidates: any): any;
54
+ /**
55
+ * Diff a baseline mapping against a current one and produce a CI-style verdict.
56
+ *
57
+ * Per baseline element:
58
+ * - `ok` — selector still resolves and the accessible name is unchanged
59
+ * - `renamed` — selector still resolves but the accessible name changed. The
60
+ * test still PASSES, but this is a *content* drift (i18n / copy /
61
+ * a real product change) a human should eyeball — opposite of a
62
+ * structural break.
63
+ * - `healable` — selector is gone, but role + name (+ context) still identify
64
+ * exactly one element → deterministic old→new rewrite
65
+ * - `ambiguous` — selector is gone and several elements match equally; healing
66
+ * would be a guess, so we refuse and hand it to a human
67
+ * - `lost` — selector is gone and nothing matches: the test will hard-fail
68
+ * - `added` — element present now, absent from the baseline
69
+ *
70
+ * Verdict (drives the CI exit code): BLOCK if anything is lost or ambiguous,
71
+ * FIX if anything is only healable, else PASS (renamed does not fail the gate —
72
+ * the test still runs — but is surfaced for review).
73
+ */
74
+ export function diffMappings(before: any, after: any): {
75
+ verdict: string;
76
+ counts: {
77
+ total: any;
78
+ ok: any;
79
+ renamed: any;
80
+ healable: any;
81
+ ambiguous: any;
82
+ lost: any;
83
+ added: any;
84
+ };
85
+ rows: any;
86
+ added: any;
87
+ };
88
+ export const FUZZY_THRESHOLD: 0.6;
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Semantic matching + mapping diff — shared by the Playwright healer and by the
4
+ * zero-install web flow on ia-qa.com.
5
+ *
6
+ * Plain, dependency-free JS so the browser can import it as-is. Keep it that way:
7
+ * if the healer and the web tool ever compute different matches, a selector that
8
+ * ia-qa.com reports as "auto-healable" would still fail in CI, which is worse
9
+ * than reporting nothing.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FUZZY_THRESHOLD = void 0;
13
+ exports.normalize = normalize;
14
+ exports.diceSimilarity = diceSimilarity;
15
+ exports.matchElement = matchElement;
16
+ exports.heuristicMatch = heuristicMatch;
17
+ exports.diffMappings = diffMappings;
18
+ function normalize(s) {
19
+ return (s == null ? '' : String(s)).toLowerCase().trim().replace(/\s+/g, ' ');
20
+ }
21
+ /** Sørensen–Dice similarity over character bigrams. */
22
+ function diceSimilarity(a, b) {
23
+ if (!a || !b)
24
+ return 0;
25
+ if (a === b)
26
+ return 1;
27
+ if (a.length < 2 || b.length < 2)
28
+ return 0;
29
+ const bigrams = (s) => {
30
+ const map = new Map();
31
+ for (let i = 0; i < s.length - 1; i++) {
32
+ const bg = s.slice(i, i + 2);
33
+ map.set(bg, (map.get(bg) || 0) + 1);
34
+ }
35
+ return map;
36
+ };
37
+ const aBigrams = bigrams(a);
38
+ const bBigrams = bigrams(b);
39
+ let overlap = 0;
40
+ for (const [bg, count] of aBigrams) {
41
+ overlap += Math.min(count, bBigrams.get(bg) || 0);
42
+ }
43
+ return (2 * overlap) / (a.length - 1 + b.length - 1);
44
+ }
45
+ exports.FUZZY_THRESHOLD = 0.6;
46
+ /**
47
+ * Resolve `target` against live `candidates`. Returns one of:
48
+ * - { matched: true, selector, score, matchType, name, disambiguatedBy? }
49
+ * - { ambiguous: true, count } — several equally-good elements; refuse to
50
+ * guess (healing the wrong one is worse than failing). A human decides.
51
+ * - null — no candidate matches.
52
+ *
53
+ * Disambiguation: when role + accessible name collide (many "Delete" buttons),
54
+ * the element's `context` (nearest landmark + section heading) breaks the tie.
55
+ * If context can't reduce it to one, we return `ambiguous` rather than pick.
56
+ */
57
+ function matchElement(target, candidates) {
58
+ const sameRole = candidates.filter((c) => c.role === target.role);
59
+ const wanted = normalize(target.name);
60
+ const wantedCtx = normalize(target.context);
61
+ const exact = sameRole.filter((c) => normalize(c.name) === wanted && wanted !== '');
62
+ const exactPick = disambiguate(exact, wantedCtx);
63
+ if (exactPick.one) {
64
+ return { matched: true, selector: exactPick.one.selector, score: 1, matchType: 'exact', name: exactPick.one.name, disambiguatedBy: exactPick.by };
65
+ }
66
+ if (exactPick.ambiguous)
67
+ return { ambiguous: true, count: exact.length };
68
+ const scored = sameRole
69
+ .map((c) => ({ c, score: diceSimilarity(normalize(c.name), wanted) }))
70
+ .filter((x) => x.score >= exports.FUZZY_THRESHOLD)
71
+ .sort((a, b) => b.score - a.score);
72
+ if (scored.length === 0)
73
+ return null;
74
+ const top = scored[0].score;
75
+ const tied = scored.filter((x) => top - x.score < 0.05).map((x) => x.c);
76
+ const fuzzyPick = disambiguate(tied, wantedCtx);
77
+ if (fuzzyPick.one) {
78
+ const chosen = scored.find((x) => x.c === fuzzyPick.one);
79
+ return { matched: true, selector: fuzzyPick.one.selector, score: chosen.score, matchType: 'fuzzy', name: fuzzyPick.one.name, disambiguatedBy: fuzzyPick.by };
80
+ }
81
+ if (fuzzyPick.ambiguous)
82
+ return { ambiguous: true, count: tied.length };
83
+ return { matched: true, selector: scored[0].c.selector, score: top, matchType: 'fuzzy', name: scored[0].c.name };
84
+ }
85
+ /**
86
+ * Reduce a set of same-name candidates to one.
87
+ * - 0 → { one: null }
88
+ * - 1 → { one }
89
+ * - >1 → try context: exactly one shares the target context → { one, by:'context' };
90
+ * otherwise { ambiguous: true }.
91
+ */
92
+ function disambiguate(cands, wantedCtx) {
93
+ if (cands.length === 0)
94
+ return { one: null };
95
+ if (cands.length === 1)
96
+ return { one: cands[0] };
97
+ if (wantedCtx) {
98
+ const byCtx = cands.filter((c) => normalize(c.context) === wantedCtx);
99
+ if (byCtx.length === 1)
100
+ return { one: byCtx[0], by: 'context' };
101
+ }
102
+ return { ambiguous: true };
103
+ }
104
+ /** Healer-facing shim: selector only when unambiguously matched, else null. */
105
+ function heuristicMatch(target, candidates) {
106
+ const match = matchElement(target, candidates);
107
+ return match && match.matched ? match.selector : null;
108
+ }
109
+ /**
110
+ * Diff a baseline mapping against a current one and produce a CI-style verdict.
111
+ *
112
+ * Per baseline element:
113
+ * - `ok` — selector still resolves and the accessible name is unchanged
114
+ * - `renamed` — selector still resolves but the accessible name changed. The
115
+ * test still PASSES, but this is a *content* drift (i18n / copy /
116
+ * a real product change) a human should eyeball — opposite of a
117
+ * structural break.
118
+ * - `healable` — selector is gone, but role + name (+ context) still identify
119
+ * exactly one element → deterministic old→new rewrite
120
+ * - `ambiguous` — selector is gone and several elements match equally; healing
121
+ * would be a guess, so we refuse and hand it to a human
122
+ * - `lost` — selector is gone and nothing matches: the test will hard-fail
123
+ * - `added` — element present now, absent from the baseline
124
+ *
125
+ * Verdict (drives the CI exit code): BLOCK if anything is lost or ambiguous,
126
+ * FIX if anything is only healable, else PASS (renamed does not fail the gate —
127
+ * the test still runs — but is surfaced for review).
128
+ */
129
+ function diffMappings(before, after) {
130
+ const beforeElements = (before && before.elements) || [];
131
+ const afterElements = (after && after.elements) || [];
132
+ const afterBySelector = new Map(afterElements.map((e) => [e.selector, e]));
133
+ const beforeSelectors = new Set(beforeElements.map((e) => e.selector));
134
+ const rows = beforeElements.map((el) => {
135
+ const still = afterBySelector.get(el.selector);
136
+ if (still) {
137
+ if (normalize(still.name) === normalize(el.name)) {
138
+ return { status: 'ok', role: el.role, name: el.name, selector: el.selector, context: el.context };
139
+ }
140
+ return { status: 'renamed', role: el.role, name: el.name, selector: el.selector, context: el.context, newName: still.name };
141
+ }
142
+ const match = matchElement(el, afterElements);
143
+ if (match && match.matched) {
144
+ return {
145
+ status: 'healable',
146
+ role: el.role,
147
+ name: el.name,
148
+ selector: el.selector,
149
+ context: el.context,
150
+ healedSelector: match.selector,
151
+ healedName: match.name,
152
+ score: match.score,
153
+ matchType: match.matchType,
154
+ disambiguatedBy: match.disambiguatedBy,
155
+ };
156
+ }
157
+ if (match && match.ambiguous) {
158
+ return { status: 'ambiguous', role: el.role, name: el.name, selector: el.selector, context: el.context, candidateCount: match.count };
159
+ }
160
+ return { status: 'lost', role: el.role, name: el.name, selector: el.selector, context: el.context };
161
+ });
162
+ const healedSelectors = new Set(rows.filter((r) => r.status === 'healable').map((r) => r.healedSelector));
163
+ const added = afterElements
164
+ .filter((el) => !beforeSelectors.has(el.selector) && !healedSelectors.has(el.selector))
165
+ .map((el) => ({ status: 'added', role: el.role, name: el.name, selector: el.selector, context: el.context }));
166
+ const count = (s) => rows.filter((r) => r.status === s).length;
167
+ const counts = {
168
+ total: beforeElements.length,
169
+ ok: count('ok'),
170
+ renamed: count('renamed'),
171
+ healable: count('healable'),
172
+ ambiguous: count('ambiguous'),
173
+ lost: count('lost'),
174
+ added: added.length,
175
+ };
176
+ const verdict = counts.lost > 0 || counts.ambiguous > 0 ? 'BLOCK' : counts.healable > 0 ? 'FIX' : 'PASS';
177
+ return { verdict, counts, rows, added };
178
+ }
179
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../src/browser/match.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `ia-qa-heal diff <baseline.json> <current.json>` — CI-gate verdict.
3
+ *
4
+ * Reads two mappings (produced by `ia-qa-heal map` or the browser snippet on
5
+ * ia-qa.com) and reports which selectors survived, which the runtime healer
6
+ * would recover, and which will hard-fail. Same engine as the web tool.
7
+ *
8
+ * Exit codes (so a pipeline can gate on it):
9
+ * 0 — PASS, or FIX without `--strict`
10
+ * 1 — BLOCK (a selector broke with no semantic match), or FIX with `--strict`
11
+ * 2 — bad usage / unreadable input
12
+ */
13
+ export declare function runDiff(args: string[]): Promise<void>;