@ia-qa/self-healing 1.5.8 → 1.6.2

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 (49) hide show
  1. package/README.md +80 -6
  2. package/dist/checks.d.ts +152 -0
  3. package/dist/checks.js +0 -0
  4. package/dist/checks.js.map +1 -0
  5. package/dist/cli/args.js +2 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/audit.d.ts +63 -0
  8. package/dist/cli/audit.js +333 -0
  9. package/dist/cli/audit.js.map +1 -0
  10. package/dist/cli/check.d.ts +1 -0
  11. package/dist/cli/check.js +334 -0
  12. package/dist/cli/check.js.map +1 -0
  13. package/dist/cli/diff.d.ts +28 -1
  14. package/dist/cli/diff.js +112 -17
  15. package/dist/cli/diff.js.map +1 -1
  16. package/dist/cli/fix.js +53 -7
  17. package/dist/cli/fix.js.map +1 -1
  18. package/dist/cli/index.js +30 -3
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/ingest.js +26 -9
  21. package/dist/cli/ingest.js.map +1 -1
  22. package/dist/cli/run.js +11 -3
  23. package/dist/cli/run.js.map +1 -1
  24. package/dist/cli/summary.d.ts +13 -0
  25. package/dist/cli/summary.js +18 -0
  26. package/dist/cli/summary.js.map +1 -1
  27. package/dist/cli-ai/index.js +13 -1
  28. package/dist/cli-ai/index.js.map +1 -1
  29. package/dist/config.d.ts +7 -0
  30. package/dist/config.js +15 -0
  31. package/dist/config.js.map +1 -1
  32. package/dist/fixEngine.d.ts +41 -2
  33. package/dist/fixEngine.js +65 -5
  34. package/dist/fixEngine.js.map +1 -1
  35. package/dist/htmlReport.d.ts +19 -0
  36. package/dist/htmlReport.js +86 -9
  37. package/dist/htmlReport.js.map +1 -1
  38. package/dist/ingest.d.ts +15 -1
  39. package/dist/ingest.js +0 -0
  40. package/dist/ingest.js.map +1 -1
  41. package/dist/mcp/server.js +22 -1
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/nameDrift.d.ts +85 -0
  44. package/dist/nameDrift.js +177 -0
  45. package/dist/nameDrift.js.map +1 -0
  46. package/dist/nameLocators.d.ts +103 -0
  47. package/dist/nameLocators.js +235 -0
  48. package/dist/nameLocators.js.map +1 -0
  49. package/package.json +1 -1
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runCheck = runCheck;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const config_1 = require("../config");
40
+ const safety_1 = require("../discovery/safety");
41
+ const checks_1 = require("../checks");
42
+ const summary_1 = require("./summary");
43
+ /**
44
+ * `ia-qa-heal check [--strict] [--json] [--offline]` — is the app you mapped sound?
45
+ *
46
+ * `diff` answers "what moved since the baseline?". This answers the question a
47
+ * diff structurally cannot: "is the current contract healthy at all?" — a green
48
+ * diff over a page whose every link 404s is still a green diff.
49
+ *
50
+ * Four checks, each admitted under one rule: it must be able to fail for a reason
51
+ * a contract diff would not already catch. "The button still exists" is rejected
52
+ * by that rule — `diff` proves it better and without a browser. What survives:
53
+ *
54
+ * dead links the app answered 4xx/5xx (needs the network — see below)
55
+ * unnamed an interactive element with no accessible name
56
+ * ambiguous two elements sharing role + name: locators for them are a coin flip
57
+ * orphans a mapped page nothing links to
58
+ *
59
+ * **Network.** The link check requests URLs from your own `baseUrl` and nothing
60
+ * else — same site only, GET only, and never a URL that *acts* (`/logout`,
61
+ * `/orders/1/delete`…): the denylist is the one `discover --crawl` already uses,
62
+ * because a GET on those is not a check, it is a logout or a deletion. The run
63
+ * announces the host and the count before the first request, and `--offline`
64
+ * drops that half entirely for a purely local run.
65
+ *
66
+ * Exit codes: 0 — PASS, or WARN without `--strict`; 1 — FAIL (a dead link), or
67
+ * WARN with `--strict`; 2 — nothing to check.
68
+ */
69
+ const CONCURRENCY = 6;
70
+ const TIMEOUT_MS = 10_000;
71
+ function readMappings(dir) {
72
+ const out = [];
73
+ const read = (file, page, isLayout) => {
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ if (!parsed || !Array.isArray(parsed.elements))
82
+ return;
83
+ out.push({ page, url: parsed.url, elements: parsed.elements, isLayout });
84
+ };
85
+ for (const f of fs.readdirSync(dir)) {
86
+ if (!f.endsWith('.json') || f.startsWith('.') || f.startsWith('_'))
87
+ continue;
88
+ read(path.join(dir, f), f.replace(/\.json$/, ''), false);
89
+ }
90
+ const layouts = path.join(dir, '_layouts');
91
+ if (fs.existsSync(layouts)) {
92
+ for (const f of fs.readdirSync(layouts).filter((x) => x.endsWith('.json'))) {
93
+ read(path.join(layouts, f), `_layouts/${f.replace(/\.json$/, '')}`, true);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ /** GET the URL and report its status. A transport error is a finding too. */
99
+ async function probe(url) {
100
+ const ac = new AbortController();
101
+ const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
102
+ try {
103
+ // GET, not HEAD: too many app servers answer HEAD with 405 while the page is
104
+ // perfectly fine, and a false dead link is the one thing this must not emit.
105
+ const res = await fetch(url, { method: 'GET', redirect: 'follow', signal: ac.signal });
106
+ return { status: res.status };
107
+ }
108
+ catch (e) {
109
+ const msg = e instanceof Error ? e.message : String(e);
110
+ return { status: null, error: ac.signal.aborted ? `no response in ${TIMEOUT_MS / 1000}s` : msg };
111
+ }
112
+ finally {
113
+ clearTimeout(timer);
114
+ }
115
+ }
116
+ async function probeAll(targets, onTick) {
117
+ const dead = [];
118
+ let index = 0;
119
+ let done = 0;
120
+ const worker = async () => {
121
+ for (;;) {
122
+ const i = index++;
123
+ if (i >= targets.length)
124
+ return;
125
+ const t = targets[i];
126
+ const { status, error } = await probe(t.url);
127
+ if (status === null || (0, checks_1.isDeadStatus)(status)) {
128
+ dead.push({ ...t, status, error });
129
+ }
130
+ onTick(++done);
131
+ }
132
+ };
133
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, targets.length) }, worker));
134
+ return dead.sort((a, b) => (b.from.length - a.from.length) || a.url.localeCompare(b.url));
135
+ }
136
+ async function runCheck(args) {
137
+ const strict = args.includes('--strict');
138
+ const json = args.includes('--json');
139
+ const offline = args.includes('--offline');
140
+ const dir = (0, config_1.mappingDir)();
141
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
142
+ console.error(`❌ Nothing to check — ${path.resolve(dir)} does not exist.\n\n` +
143
+ ` \`check\` reads one capture of the app and asks whether it is sound.\n` +
144
+ ` Capture it first: ia-qa-heal map`);
145
+ process.exitCode = 2;
146
+ return;
147
+ }
148
+ const pages = readMappings(dir);
149
+ const realPages = pages.filter((p) => !p.isLayout);
150
+ if (realPages.length === 0) {
151
+ console.error(`❌ No page contracts in ${path.resolve(dir)} — run \`ia-qa-heal map\` first.`);
152
+ process.exitCode = 2;
153
+ return;
154
+ }
155
+ let baseUrl = '';
156
+ try {
157
+ baseUrl = (0, config_1.loadConfig)().baseUrl ?? '';
158
+ }
159
+ catch {
160
+ /* a missing config only costs the network half; the static checks stand */
161
+ }
162
+ const skipped = [];
163
+ const elementCount = pages.reduce((s, p) => s + p.elements.length, 0);
164
+ const unnamed = (0, checks_1.findUnnamed)(pages);
165
+ const ambiguous = (0, checks_1.findAmbiguousNames)(pages);
166
+ if (!baseUrl) {
167
+ skipped.push({ check: 'orphans', why: 'no baseUrl in .ia-qa/config.json — hrefs cannot be resolved to pages' });
168
+ }
169
+ const orphans = baseUrl
170
+ ? (0, checks_1.findOrphanPages)(pages, (href) => {
171
+ try {
172
+ return (0, safety_1.normalizedPath)(href, baseUrl);
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ })
178
+ : [];
179
+ let deadLinks = [];
180
+ let linksChecked = 0;
181
+ let linksSkipped = 0;
182
+ if (offline) {
183
+ skipped.push({ check: 'dead links', why: '--offline' });
184
+ }
185
+ else if (!baseUrl) {
186
+ skipped.push({ check: 'dead links', why: 'no baseUrl in .ia-qa/config.json' });
187
+ }
188
+ if (!offline && baseUrl) {
189
+ const { targets, skipped } = (0, checks_1.collectLinkTargets)(pages, (href) => {
190
+ const verdict = (0, safety_1.classifyUrl)(href, baseUrl);
191
+ if (!verdict.ok)
192
+ return null;
193
+ try {
194
+ return new URL(href, baseUrl).toString();
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ });
200
+ linksSkipped = skipped;
201
+ linksChecked = targets.length;
202
+ if (targets.length > 0) {
203
+ // Rule 1 of this project: a flow that leaves the machine announces itself
204
+ // without being asked. Said before the first request, not after.
205
+ const host = (() => {
206
+ try {
207
+ return new URL(baseUrl).host;
208
+ }
209
+ catch {
210
+ return baseUrl;
211
+ }
212
+ })();
213
+ if (!json) {
214
+ console.log(`\n🔗 Requesting ${targets.length} distinct link${targets.length === 1 ? '' : 's'} on ${host} —` +
215
+ ` GET only, same site only, action URLs skipped.` +
216
+ (skipped > 0 ? ` ${skipped} href${skipped === 1 ? '' : 's'} left alone.` : ''));
217
+ }
218
+ deadLinks = await probeAll(targets, () => undefined);
219
+ }
220
+ }
221
+ const report = {
222
+ pagesChecked: realPages.length,
223
+ elements: elementCount,
224
+ unnamed,
225
+ ambiguous,
226
+ orphans,
227
+ deadLinks,
228
+ linksChecked,
229
+ linksSkipped,
230
+ offline: offline || !baseUrl,
231
+ skipped,
232
+ };
233
+ const verdict = (0, checks_1.checkVerdict)(report);
234
+ if (json) {
235
+ console.log(JSON.stringify({ verdict, ...report }, null, 2));
236
+ process.exitCode = verdict === 'FAIL' || (strict && verdict === 'WARN') ? 1 : 0;
237
+ return;
238
+ }
239
+ printCheck(report, verdict, strict);
240
+ (0, summary_1.summaryLine)('check', verdict === 'PASS' ? 'ok' : verdict === 'WARN' ? 'fix' : 'block', verdict === 'PASS'
241
+ ? `PASS · ${report.pagesChecked} pages · ${(0, checks_1.checkCoverage)(report).ran}/${(0, checks_1.checkCoverage)(report).total} checks ran`
242
+ : verdict === 'FAIL'
243
+ ? `FAIL · ${deadLinks.length} dead link${deadLinks.length === 1 ? '' : 's'}`
244
+ : `WARN · ${unnamed.length} unnamed · ${ambiguous.length} ambiguous · ${orphans.length} orphan`, [
245
+ `${report.elements} elements`,
246
+ report.offline ? 'offline' : `${linksChecked} links`,
247
+ skipped.length > 0 ? `${skipped.length} check${skipped.length === 1 ? '' : 's'} skipped` : undefined,
248
+ ]);
249
+ process.exitCode = verdict === 'FAIL' || (strict && verdict === 'WARN') ? 1 : 0;
250
+ }
251
+ /**
252
+ * Where a finding lives. An element repeated by the shared shell is ONE thing to
253
+ * fix on N pages, not N findings — saying "142 pages" instead of printing it 142
254
+ * times is the difference between a report someone reads and one they mute.
255
+ */
256
+ function where(pages) {
257
+ if (pages.length === 1)
258
+ return pages[0];
259
+ if (pages.length <= 3)
260
+ return pages.join(', ');
261
+ return `${pages.length} pages (shared shell — ${pages.slice(0, 2).join(', ')}…)`;
262
+ }
263
+ function printCheck(r, verdict, strict) {
264
+ const icon = verdict === 'PASS' ? '✅' : verdict === 'WARN' ? '⚠️ ' : '⛔';
265
+ console.log(`\n${icon} ${verdict} ia-qa-heal check — is the app you mapped sound?`);
266
+ // Which deployment this verdict is about. Two runs, one on localhost and one
267
+ // on production, used to print byte-identical output.
268
+ const against = (0, summary_1.againstLine)((0, config_1.configuredBaseUrl)());
269
+ if (against)
270
+ console.log(against);
271
+ console.log(` ${r.pagesChecked} page${r.pagesChecked === 1 ? '' : 's'} · ${r.elements} elements · ` +
272
+ (r.offline ? 'links not requested (offline)' : `${r.linksChecked} distinct link${r.linksChecked === 1 ? '' : 's'} requested`));
273
+ if (r.deadLinks.length > 0) {
274
+ console.log(`\n ⛔ ${r.deadLinks.length} dead link${r.deadLinks.length === 1 ? '' : 's'}:`);
275
+ for (const d of r.deadLinks.slice(0, 15)) {
276
+ const what = d.status === null ? (d.error ?? 'no response') : `HTTP ${d.status}`;
277
+ const from = d.from.slice(0, 2).map((f) => `${f.page} "${f.name}"`).join(', ');
278
+ const more = d.from.length > 2 ? ` +${d.from.length - 2} more` : '';
279
+ console.log(` ${what.padEnd(10)} ${d.url}`);
280
+ console.log(` linked from ${from}${more}`);
281
+ }
282
+ if (r.deadLinks.length > 15)
283
+ console.log(` … and ${r.deadLinks.length - 15} more.`);
284
+ }
285
+ if (r.ambiguous.length > 0) {
286
+ console.log(`\n ❓ ${r.ambiguous.length} name${r.ambiguous.length === 1 ? '' : 's'} that cannot identify one element:`);
287
+ for (const a of r.ambiguous.slice(0, 10)) {
288
+ console.log(` ${a.count}× ${a.role} "${a.name}" · ${where(a.pages)}`);
289
+ }
290
+ if (r.ambiguous.length > 10)
291
+ console.log(` … and ${r.ambiguous.length - 10} more.`);
292
+ console.log(` Any getByRole/cy.findByRole for these is a coin flip — Playwright raises a` +
293
+ `\n strict-mode violation and the healer reports \`ambiguous\` and refuses.` +
294
+ `\n A data-testid on one of them settles it. This is drift that has not happened yet.`);
295
+ }
296
+ if (r.unnamed.length > 0) {
297
+ const occurrences = r.unnamed.reduce((acc, u) => acc + u.pages.length, 0);
298
+ console.log(`
299
+ 👻 ${r.unnamed.length} interactive element${r.unnamed.length === 1 ? '' : 's'} with no accessible name` +
300
+ (occurrences > r.unnamed.length ? ` (${occurrences} occurrences — shared shell counted once)` : '') +
301
+ ':');
302
+ for (const u of r.unnamed.slice(0, 8)) {
303
+ console.log(` ${u.role.padEnd(8)} ${u.selector}`);
304
+ console.log(` ${where(u.pages)}`);
305
+ }
306
+ if (r.unnamed.length > 8)
307
+ console.log(` … and ${r.unnamed.length - 8} more.`);
308
+ console.log(` A screen reader announces nothing, getByRole cannot target it, and healing` +
309
+ `\n cannot recover it after a move — identity here IS role + name.`);
310
+ }
311
+ if (r.orphans.length > 0) {
312
+ console.log(`\n 🔌 ${r.orphans.length} mapped page${r.orphans.length === 1 ? '' : 's'} nothing links to:`);
313
+ for (const o of r.orphans.slice(0, 10))
314
+ console.log(` ${o.page} (${o.url})`);
315
+ if (r.orphans.length > 10)
316
+ console.log(` … and ${r.orphans.length - 10} more.`);
317
+ console.log(` Either the navigation lost a link, or they are deep-link-only. Worth one look.`);
318
+ }
319
+ if (r.skipped.length > 0) {
320
+ const cov = (0, checks_1.checkCoverage)(r);
321
+ console.log(`
322
+ ⏸ ${cov.ran} of ${cov.total} checks ran. NOT checked:`);
323
+ for (const sk of r.skipped)
324
+ console.log(` ${sk.check.padEnd(12)} ${sk.why}`);
325
+ console.log(` A verdict cannot speak for a question it never asked — whatever those` +
326
+ `
327
+ would have found is invisible above, PASS included.`);
328
+ }
329
+ if (verdict === 'WARN' && !strict) {
330
+ console.log(`\n Advisory — exits 0. Add --strict to fail the pipeline on these too.`);
331
+ }
332
+ console.log('');
333
+ }
334
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { Usage } from '../ingest';
2
+ import { NameDriftFinding } from '../nameDrift';
2
3
  import { FixOutcome } from '../htmlReport';
3
4
  /**
4
5
  * `ia-qa-heal diff [baseline] [current] [--strict] [--json] [--dir]`
@@ -35,6 +36,14 @@ export declare function takeReportFlag(args: string[], defaultPath?: string): {
35
36
  * Opt-in via `--open` only: auto-opening a browser inside a CI gate would be wrong.
36
37
  */
37
38
  export declare function openInBrowser(file: string): void;
39
+ /**
40
+ * Page name → its URL, from `config.pages`.
41
+ *
42
+ * Shared by the terminal summary and the HTML report so the two never disagree
43
+ * about which file is which view. Best-effort: config may be absent, or may not
44
+ * cover every mapped page (a live capture without `pages`), and that is fine.
45
+ */
46
+ export declare function pageUrlMap(): Map<string, string> | undefined;
38
47
  /** Render the drift report to `reportPath` and announce it. Never throws on a bad write path to callers of the gate — reporting is a side effect, not the verdict. */
39
48
  export declare function writeDriftReport(reportPath: string, agg: DirReport, reports: Array<{
40
49
  name: string;
@@ -42,13 +51,21 @@ export declare function writeDriftReport(reportPath: string, agg: DirReport, rep
42
51
  isLayout: boolean;
43
52
  }>, baselineDirPath: string, currentDirPath: string, usage?: Usage | null, open?: boolean, outcome?: FixOutcome): void;
44
53
  export declare function runDiff(args: string[]): Promise<void>;
54
+ /**
55
+ * The name-drift section of a human verdict.
56
+ *
57
+ * Deliberately loud: this is the class of drift that used to exit 0 while the
58
+ * suite went red, so it prints the failing call sites — file:line — rather than
59
+ * a count. A count would be one more thing to eyeball; a file:line is a fix.
60
+ */
61
+ export declare function printNameDrift(findings: NameDriftFinding[] | undefined): void;
45
62
  /**
46
63
  * The `diff`/`run` verdict compressed to the one greppable receipt line. Placed after
47
64
  * the detail block so a `tail` catches the takeaway even when the rows scrolled past —
48
65
  * and never in `--json` mode, where it would corrupt the machine-readable payload.
49
66
  * Shared with `run`, which ends on the same verdict.
50
67
  */
51
- export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts']): void;
68
+ export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts'], nameDrift?: NameDriftFinding[]): void;
52
69
  export interface DirDiffCollection {
53
70
  reports: Array<{
54
71
  name: string;
@@ -80,6 +97,11 @@ export interface DirReport {
80
97
  totals: Report['counts'];
81
98
  /** Pairs left out of the verdict: same capture on both sides, so incapable of showing drift. */
82
99
  staleExcluded?: string[];
100
+ /**
101
+ * Renamed labels the suite locates by name, de-duped across pages — a shared
102
+ * header renames once, not once per page that carries it.
103
+ */
104
+ nameDrift?: NameDriftFinding[];
83
105
  }
84
106
  export declare function aggregateReports(reports: Array<{
85
107
  name: string;
@@ -129,4 +151,9 @@ export interface Report {
129
151
  };
130
152
  rows: DiffRow[];
131
153
  added: DiffRow[];
154
+ /**
155
+ * Renames the suite locates by name. Present only once `ia-qa-heal ingest` has
156
+ * inventoried the tests — absent means "not measured", never "none".
157
+ */
158
+ nameDrift?: NameDriftFinding[];
132
159
  }
package/dist/cli/diff.js CHANGED
@@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.takeReportFlag = takeReportFlag;
37
37
  exports.openInBrowser = openInBrowser;
38
+ exports.pageUrlMap = pageUrlMap;
38
39
  exports.writeDriftReport = writeDriftReport;
39
40
  exports.runDiff = runDiff;
41
+ exports.printNameDrift = printNameDrift;
40
42
  exports.driftSummaryLine = driftSummaryLine;
41
43
  exports.collectDirDiff = collectDirDiff;
42
44
  exports.aggregateReports = aggregateReports;
@@ -49,6 +51,7 @@ const config_1 = require("../config");
49
51
  const ingest_1 = require("../ingest");
50
52
  const captureMerge_1 = require("../captureMerge");
51
53
  const nameHint_1 = require("../nameHint");
54
+ const nameDrift_1 = require("../nameDrift");
52
55
  const htmlReport_1 = require("../htmlReport");
53
56
  const summary_1 = require("./summary");
54
57
  /**
@@ -104,6 +107,40 @@ function openInBrowser(file) {
104
107
  /* best effort — the path was already printed */
105
108
  }
106
109
  }
110
+ /**
111
+ * Page name → its URL, from `config.pages`.
112
+ *
113
+ * Shared by the terminal summary and the HTML report so the two never disagree
114
+ * about which file is which view. Best-effort: config may be absent, or may not
115
+ * cover every mapped page (a live capture without `pages`), and that is fine.
116
+ */
117
+ function pageUrlMap() {
118
+ try {
119
+ const cfg = (0, config_1.loadConfig)();
120
+ if (cfg.pages.length === 0)
121
+ return undefined;
122
+ return new Map(cfg.pages.map((p) => [p.name.replace(/[^a-zA-Z0-9._-]/g, '-'), p.url]));
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ /**
129
+ * When a directory of contracts was captured. Read off any one of them — `map`
130
+ * stamps them all in the same run.
131
+ */
132
+ function capturedAtOf(dir) {
133
+ try {
134
+ const file = fs.readdirSync(dir).find((f) => f.endsWith('.json') && !f.startsWith('.') && !f.startsWith('_'));
135
+ if (!file)
136
+ return undefined;
137
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
138
+ return parsed.capturedAt;
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
107
144
  /** Render the drift report to `reportPath` and announce it. Never throws on a bad write path to callers of the gate — reporting is a side effect, not the verdict. */
108
145
  function writeDriftReport(reportPath, agg, reports, baselineDirPath, currentDirPath, usage, open = false, outcome) {
109
146
  const html = (0, htmlReport_1.renderDriftReportHtml)(agg, reports, {
@@ -112,6 +149,13 @@ function writeDriftReport(reportPath, agg, reports, baselineDirPath, currentDirP
112
149
  filesDiffed: reports.length,
113
150
  usage,
114
151
  outcome,
152
+ // Which app, and which two moments. A report attached to a PR that cannot say
153
+ // whether it was captured against localhost or production is not evidence —
154
+ // and two such reports are byte-identical in everything that identifies them.
155
+ baseUrl: (0, config_1.configuredBaseUrl)(),
156
+ pageUrls: pageUrlMap(),
157
+ baselineCapturedAt: capturedAtOf(baselineDirPath),
158
+ currentCapturedAt: capturedAtOf(currentDirPath),
115
159
  });
116
160
  const dest = path.resolve(reportPath);
117
161
  fs.writeFileSync(dest, html, 'utf8');
@@ -173,21 +217,48 @@ async function runDiff(args) {
173
217
  refuseToCompare('same-capture', sameCaptureMessage(baseline.capturedAt), 'Change the app and run `ia-qa-heal map` again, so the two sides are different moments.', json);
174
218
  return;
175
219
  }
220
+ const usage = (0, ingest_1.loadUsage)();
176
221
  const report = attachBaselineHrefs((0, match_1.diffMappings)(baseline, current), baseline);
222
+ // A rename is harmless to a CSS-anchored suite and fatal to a name-anchored
223
+ // one. Only the test inventory can tell the two apart, so the verdict is
224
+ // raised here rather than inside the shared matcher.
225
+ report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
226
+ report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
177
227
  if (json) {
178
228
  console.log(JSON.stringify(report, null, 2));
179
229
  }
180
230
  else {
181
- printHuman(report, files, (0, ingest_1.loadUsage)());
231
+ printHuman(report, files, usage);
232
+ printNameDrift(report.nameDrift);
182
233
  }
183
234
  if (reportPath) {
184
235
  const single = [{ name: path.basename(files[1]).replace(/\.json$/, ''), report, isLayout: false }];
185
- writeDriftReport(reportPath, aggregateReports(single), single, files[0], files[1], (0, ingest_1.loadUsage)(), open);
236
+ writeDriftReport(reportPath, aggregateReports(single), single, files[0], files[1], usage, open);
186
237
  }
187
238
  const failing = report.verdict === 'BLOCK' || (strict && report.verdict === 'FIX');
188
239
  process.exitCode = failing ? 1 : 0;
189
240
  if (!json)
190
- driftSummaryLine('diff', report.verdict, report.counts);
241
+ driftSummaryLine('diff', report.verdict, report.counts, report.nameDrift);
242
+ }
243
+ /**
244
+ * The name-drift section of a human verdict.
245
+ *
246
+ * Deliberately loud: this is the class of drift that used to exit 0 while the
247
+ * suite went red, so it prints the failing call sites — file:line — rather than
248
+ * a count. A count would be one more thing to eyeball; a file:line is a fix.
249
+ */
250
+ function printNameDrift(findings) {
251
+ if (!findings || findings.length === 0)
252
+ return;
253
+ const fixable = findings.filter((f) => f.fixable);
254
+ const manual = findings.filter((f) => !f.fixable);
255
+ console.log(`\n 🏷 ${findings.length} renamed label${findings.length === 1 ? '' : 's'} your tests locate by name` +
256
+ ` — a selector diff alone would have called this PASS.`);
257
+ for (const f of [...manual, ...fixable])
258
+ console.log((0, nameDrift_1.formatFinding)(f));
259
+ if (fixable.length > 0) {
260
+ console.log(`\n ${fixable.length} of them are deterministic: \`ia-qa-heal fix\` rewrites the call.`);
261
+ }
191
262
  }
192
263
  /**
193
264
  * The `diff`/`run` verdict compressed to the one greppable receipt line. Placed after
@@ -195,16 +266,28 @@ async function runDiff(args) {
195
266
  * and never in `--json` mode, where it would corrupt the machine-readable payload.
196
267
  * Shared with `run`, which ends on the same verdict.
197
268
  */
198
- function driftSummaryLine(verb, verdict, c) {
269
+ function driftSummaryLine(verb, verdict, c, nameDrift = []) {
199
270
  const kind = verdict === 'PASS' ? 'ok' : verdict === 'FIX' ? 'fix' : 'block';
271
+ const nameFixable = nameDrift.filter((f) => f.fixable).length;
272
+ const nameManual = nameDrift.length - nameFixable;
273
+ // Name drift can be the *only* reason for the verdict, and a headline reading
274
+ // "BLOCK · 0 lost · 0 ambiguous · 0 rebound" would send someone hunting for a
275
+ // selector that never broke. Name the actual cause first.
276
+ const blockCause = c.lost + c.ambiguous + c.rebound === 0 && nameManual > 0
277
+ ? `${nameManual} renamed label${nameManual === 1 ? '' : 's'} your tests use`
278
+ : `${c.lost} lost · ${c.ambiguous} ambiguous · ${c.rebound} rebound`;
279
+ const fixCause = c.healable === 0 && nameFixable > 0
280
+ ? `${nameFixable} renamed label${nameFixable === 1 ? '' : 's'}`
281
+ : `${c.healable} healable`;
200
282
  const headline = verdict === 'PASS'
201
283
  ? 'PASS · no drift, suite safe'
202
284
  : verdict === 'FIX'
203
- ? `FIX · ${c.healable} healable → \`fix\``
204
- : `BLOCK · ${c.lost} lost · ${c.ambiguous} ambiguous · ${c.rebound} rebound — human needed`;
285
+ ? `FIX · ${fixCause} → \`fix\``
286
+ : `BLOCK · ${blockCause} — human needed`;
205
287
  (0, summary_1.summaryLine)(verb, kind, headline, [
206
288
  `${c.ok} ok`,
207
289
  c.renamed > 0 ? `${c.renamed} renamed` : undefined,
290
+ nameDrift.length > 0 ? `${nameDrift.length} name-drift` : undefined,
208
291
  c.added > 0 ? `${c.added} new` : undefined,
209
292
  ]);
210
293
  }
@@ -247,6 +330,7 @@ async function runDirDiff(baselineDir, currentDir, opts) {
247
330
  }
248
331
  else {
249
332
  printAggregate(aggregate, reports, baselineDir, currentDir, usage);
333
+ printNameDrift(aggregate.nameDrift);
250
334
  }
251
335
  if (opts.reportPath) {
252
336
  writeDriftReport(opts.reportPath, aggregate, reports, baselineDir, currentDir, usage, opts.open);
@@ -254,7 +338,7 @@ async function runDirDiff(baselineDir, currentDir, opts) {
254
338
  const failing = aggregate.verdict === 'BLOCK' || (opts.strict && aggregate.verdict === 'FIX');
255
339
  process.exitCode = failing ? 1 : 0;
256
340
  if (!opts.json)
257
- driftSummaryLine('diff', aggregate.verdict, aggregate.totals);
341
+ driftSummaryLine('diff', aggregate.verdict, aggregate.totals, aggregate.nameDrift);
258
342
  }
259
343
  /**
260
344
  * The collection half of a directory diff, shared with `ia-qa-heal run`: walk two
@@ -266,6 +350,15 @@ function collectDirDiff(baselineDir, currentDir) {
266
350
  // Kept out of the verdict rather than counted as `ok` — N ok that were never
267
351
  // compared is exactly the false green this guard exists to stop.
268
352
  const stale = [];
353
+ // Read once for the whole walk: `run` and `diff --dir` both come through here,
354
+ // so name drift is assessed on every path that produces a directory verdict.
355
+ const usage = (0, ingest_1.loadUsage)();
356
+ const assess = (baseline, current) => {
357
+ const report = attachBaselineHrefs((0, match_1.diffMappings)(baseline, current), baseline);
358
+ report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
359
+ report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
360
+ return report;
361
+ };
269
362
  const layoutsDir = path.join(baselineDir, '_layouts');
270
363
  if (fs.existsSync(layoutsDir)) {
271
364
  const layoutFiles = fs.readdirSync(layoutsDir).filter((f) => f.endsWith('.json'));
@@ -282,7 +375,7 @@ function collectDirDiff(baselineDir, currentDir) {
282
375
  stale.push(`_layouts/${name}`);
283
376
  continue;
284
377
  }
285
- const report = attachBaselineHrefs((0, match_1.diffMappings)(baseline, current), baseline);
378
+ const report = assess(baseline, current);
286
379
  reports.push({ name: `_layouts/${name}`, report, isLayout: true });
287
380
  }
288
381
  catch (e) {
@@ -307,7 +400,7 @@ function collectDirDiff(baselineDir, currentDir) {
307
400
  stale.push(name);
308
401
  continue;
309
402
  }
310
- const report = attachBaselineHrefs((0, match_1.diffMappings)(baseline, current), baseline);
403
+ const report = assess(baseline, current);
311
404
  reports.push({ name, report, isLayout: false });
312
405
  }
313
406
  catch (e) {
@@ -346,8 +439,12 @@ function aggregateReports(reports) {
346
439
  return 'PASS';
347
440
  };
348
441
  const verdict = reports.reduce((acc, r) => worstV(acc, r.report.verdict), 'PASS');
442
+ // Per-page verdicts are already escalated by `collectDirDiff`, so the aggregate
443
+ // inherits the raise for free — this only collects the findings for reporting.
444
+ const nameDrift = (0, nameDrift_1.mergeFindings)(reports.map((r) => r.report.nameDrift ?? []));
349
445
  return {
350
446
  verdict,
447
+ ...(nameDrift.length > 0 ? { nameDrift } : {}),
351
448
  layoutVerdict,
352
449
  layoutCounts,
353
450
  pageReports: pageReports.map((r) => ({
@@ -364,6 +461,11 @@ function aggregateReports(reports) {
364
461
  function printAggregate(agg, reports, baselineDir, currentDir, usage) {
365
462
  const icon = agg.verdict === 'PASS' ? '✅' : agg.verdict === 'FIX' ? '🔧' : '⛔';
366
463
  console.log(`\n${icon} ${agg.verdict} (directory diff)`);
464
+ // Which deployment this verdict is about. Two runs, one on localhost and one
465
+ // on production, used to print byte-identical output.
466
+ const against = (0, summary_1.againstLine)((0, config_1.configuredBaseUrl)());
467
+ if (against)
468
+ console.log(against);
367
469
  console.log(` ${path.basename(baselineDir)} → ${path.basename(currentDir)}`);
368
470
  console.log(` ${reports.length} files diffed`);
369
471
  console.log(` ${agg.totals.ok} ok · ${agg.totals.renamed} renamed · ${agg.totals.healable} healable · ` +
@@ -380,14 +482,7 @@ function printAggregate(agg, reports, baselineDir, currentDir, usage) {
380
482
  // Page → URL mapping, loaded from config so the user never has to guess
381
483
  // which file is which view. Best-effort: config may not exist or may not
382
484
  // cover every mapped page (live capture without `pages`), and that's fine.
383
- let urlMap;
384
- try {
385
- const cfg = (0, config_1.loadConfig)();
386
- if (cfg.pages.length > 0) {
387
- urlMap = new Map(cfg.pages.map((p) => ({ name: p.name.replace(/[^a-zA-Z0-9._-]/g, '-'), url: p.url })).map(e => [e.name, e.url]));
388
- }
389
- }
390
- catch { /* config optional */ }
485
+ const urlMap = pageUrlMap();
391
486
  // Per-page summary
392
487
  console.log('\n 📄 Pages');
393
488
  for (const pr of agg.pageReports) {