@ia-qa/self-healing 1.19.1 → 1.23.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 (50) hide show
  1. package/README.md +55 -0
  2. package/TUTORIAL.md +22 -0
  3. package/dist/aom.d.ts +20 -0
  4. package/dist/aom.js +25 -0
  5. package/dist/aom.js.map +1 -1
  6. package/dist/browser/resolve.js +4 -1
  7. package/dist/browser/resolve.js.map +1 -1
  8. package/dist/cli/args.js +1 -1
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/cli/diff.d.ts +62 -0
  11. package/dist/cli/diff.js +104 -4
  12. package/dist/cli/diff.js.map +1 -1
  13. package/dist/cli/explain.js +57 -12
  14. package/dist/cli/explain.js.map +1 -1
  15. package/dist/cli/fix.js +2 -1
  16. package/dist/cli/fix.js.map +1 -1
  17. package/dist/cli/index.js +6 -0
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/map.d.ts +2 -0
  20. package/dist/cli/map.js +13 -0
  21. package/dist/cli/map.js.map +1 -1
  22. package/dist/cli-ai/index.js +2 -1
  23. package/dist/cli-ai/index.js.map +1 -1
  24. package/dist/explain.d.ts +78 -3
  25. package/dist/explain.js +87 -9
  26. package/dist/explain.js.map +1 -1
  27. package/dist/htmlReport.js +39 -0
  28. package/dist/htmlReport.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/ingest.d.ts +30 -0
  32. package/dist/ingest.js +46 -1
  33. package/dist/ingest.js.map +1 -1
  34. package/dist/junit.js +41 -0
  35. package/dist/junit.js.map +1 -1
  36. package/dist/mcp/server.js +11 -0
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/redirectFix.d.ts +60 -0
  39. package/dist/redirectFix.js +73 -0
  40. package/dist/redirectFix.js.map +1 -0
  41. package/dist/resolution.d.ts +41 -4
  42. package/dist/resolution.js +66 -5
  43. package/dist/resolution.js.map +1 -1
  44. package/dist/routeDrift.d.ts +105 -0
  45. package/dist/routeDrift.js +96 -0
  46. package/dist/routeDrift.js.map +1 -0
  47. package/dist/ui/server.js +5 -6
  48. package/dist/ui/server.js.map +1 -1
  49. package/package.json +1 -1
  50. package/skills/ia-qa-heal/SKILL.md +22 -0
package/README.md CHANGED
@@ -422,8 +422,63 @@ rewrites the literal in your files. Four outcomes, one repaired automatically:
422
422
  | reaches **nothing**, and its element is still there | 🔧 rewritten |
423
423
  | reaches a **different** element | ⛔ never rewritten — your test passes and acts on the wrong thing |
424
424
  | reaches **several** elements | ⛔ a coin flip |
425
+ | reached **several** and now reaches **none** | ⛔ the candidates are named, none is chosen for you |
425
426
  | its element is **gone** | ⛔ nothing to rewrite towards |
426
427
 
428
+ That fourth row is the one a route migration produces — every `a[href="/index.php/cart"]`
429
+ becoming `a[href="/cart"]` in one commit. The string used to match two links and now matches
430
+ neither, so your suite is red; what it could have meant is still on the page and is listed with
431
+ its new address. No rewrite is offered, and that is not timidity: the binding was already
432
+ ambiguous, so which of the two your test meant was never established. Picking one would be a
433
+ coin flip wearing a fix's clothes.
434
+
435
+ ### A link that leads somewhere else
436
+
437
+ `href` is a **destination**, not an address. Because `stableSelector` prefers `a[href="…"]`,
438
+ a link whose route moved shows up as a moved selector — reported, until now, on the same line
439
+ as a button that slid one slot down the DOM:
440
+
441
+ ```
442
+ 🧭 2 links now lead somewhere else (the element is the same — its destination is not):
443
+ • Write a contract in the browser → [home]
444
+ /devtools/eval-contract-runner → /qatools/eval-contract
445
+ • ? [mcp-server]
446
+ /devtools/eval-contract-runner → /qatools/eval-contract
447
+ Not failing the run — no test navigates to these. Check the ones you reach by URL.
448
+ ```
449
+
450
+ It **does not fail your run on its own**: an app is allowed to reorganise its URLs, and a gate
451
+ that goes red on that is a gate someone switches off. It fails only when your suite navigates
452
+ to the route that moved — `ingest` now inventories `page.goto`, `cy.visit`, `driver.get` and
453
+ `browser.url`, so that case arrives with its `file:line`. No `ingest`, no escalation.
454
+
455
+ Nothing here is ever rewritten. A `page.goto` is not a locator, and the new route may be the
456
+ same page renamed or a different page entirely — you can tell in a second from two URLs, and
457
+ the tool cannot tell at all.
458
+
459
+ Measured across two real captures of a 142-page app: **2 findings over 4 563 named links**, and
460
+ zero on a second corpus. Details, including the false positives the first measurement produced,
461
+ are in `SPEC-route-drift.md`.
462
+
463
+ ### When a page you declared has moved
464
+
465
+ `map` already notices that the browser landed somewhere other than the URL in `config.json`.
466
+ It now offers the correction, and `--fix-redirects` writes it:
467
+
468
+ ```
469
+ ↪️ 1 declared page is no longer at the URL config.json declares:
470
+ articles: /articles → /resources
471
+ Run `ia-qa-heal map --fix-redirects` to write these, or edit config.json yourself.
472
+ ```
473
+
474
+ Left uncorrected, the contract for `/resources` is written under the name `articles`, which is
475
+ the same wrong-identity problem two apps sharing a `/login` produce.
476
+
477
+ **It refuses when several declared pages land on the same place.** That is not a migration —
478
+ it is an expired session, a redirect to sign-in, or an outage, and applying it would rewrite
479
+ your whole page list to point at the login screen. The group is named and skipped, a genuine
480
+ single move in the same run still applies, and the message sends you to `session --check`.
481
+
427
482
  **Your app may already carry the answer.** The contract holds interactive elements only, so a
428
483
  selector on a status div, a cell or an alert is not in it. Usually that stays quiet — such an
429
484
  element has no identity independent of what drifts, so nothing could be repaired anyway. But
package/TUTORIAL.md CHANGED
@@ -558,6 +558,28 @@ the `file:line` the error message never carried.
558
558
  > **cannot tell which locator the failure is about**, that's not a pass — usually the failing
559
559
  > file was never `ingest`ed, or the locator is built at runtime from a variable.
560
560
 
561
+ > 🧭 **A locator can be fine and the page still wrong.** If a link's *destination* moved —
562
+ > `/devtools/eval-contract-runner` → `/qatools/eval-contract` — the element is untouched, so
563
+ > every locator check passes. `explain` used to answer PASS there, which is true and sends you
564
+ > to look at timing and test data instead of at the route. It now intersects the failure text
565
+ > with the URLs your suite navigates to and answers **BLOCK**, naming both routes and the line
566
+ > that goes to the old one. `diff` reports the same thing under *"N links now lead somewhere else"*, with
567
+ > the page each is on. It does not fail your run unless your suite navigates to the old route,
568
+ > which it knows because `ingest` inventories `page.goto`, `cy.visit`, `driver.get` and
569
+ > `browser.url`. Nothing there is ever rewritten: only you can say whether the new route is the
570
+ > same page renamed or a different page. And when it is a page *you declared* that moved,
571
+ > `ia-qa-heal map --fix-redirects` updates `config.json` — refusing if several pages landed in
572
+ > the same place, because that is an expired session, not a migration.
573
+
574
+ > 🐛 **Fixed in 1.20.0, and worth knowing if you are on an older version.** `explain` used to
575
+ > answer *"this string reached nothing at either moment — it is not drift"* about locators that
576
+ > had reached something at baseline. It read only the current capture and phrased the verdict as
577
+ > if it had read both. That is the worst place in this tool for a false negative: "not drift" is
578
+ > the sentence that sends you away from the selectors and off to look at timing and test data.
579
+ > It now reads `baseline/_resolved/` too, and a locator that resolved then and resolves no longer
580
+ > comes back as drift, with `never-bound` kept for what it always meant — probed at both moments,
581
+ > reached nothing at either.
582
+
561
583
  Read-only: it opens no browser, re-runs nothing, and edits nothing. Repairs still go through
562
584
  `fix`, below.
563
585
 
package/dist/aom.d.ts CHANGED
@@ -132,5 +132,25 @@ export declare function depthMismatchMessage(m: {
132
132
  current: number;
133
133
  }): string;
134
134
  export declare function extractInteractiveElements(page: Page): Promise<MappedElement[]>;
135
+ /**
136
+ * The `.json` files in a contract directory that are **contracts**.
137
+ *
138
+ * A leading underscore is how this codebase spells "not a page" — `_exercised.json`,
139
+ * `_resolved/`, `_layouts/`. The rule was written down as *"every consumer that enumerates
140
+ * contracts already filters it out"*, and it was true of three of them and false of two.
141
+ *
142
+ * What that cost, reproduced end to end: `run` writes `mapping/_exercised.json`, `baseline`
143
+ * promotes it (this directory is copied wholesale, which is right — the record belongs to the
144
+ * moment), and then **`diff --dir` and `fix --dir` both read it as a page named `_exercised`**,
145
+ * fail on `has no "elements" array`, put it in `unreadable` and refuse the entire comparison
146
+ * with exit 2. The remedy they printed — *restore the file from git, or re-capture it* — could
147
+ * not work: re-capturing writes the same file again. A tool breaking its own documented loop
148
+ * with a file it wrote itself, and telling the user to fix it by hand.
149
+ *
150
+ * So the rule lives here now, in the module that knows what a contract is, and the five call
151
+ * sites share it. Three of them already agreed; the point is that agreeing by coincidence is
152
+ * what produced this.
153
+ */
154
+ export declare function contractFilesIn(dir: string): string[];
135
155
  export declare function saveMapping(mapping: PageMapping, dir?: string): string;
136
156
  export declare function loadMapping(pageName: string, dir?: string): PageMapping;
package/dist/aom.js CHANGED
@@ -37,6 +37,7 @@ exports.isSameCapture = isSameCapture;
37
37
  exports.depthMismatch = depthMismatch;
38
38
  exports.depthMismatchMessage = depthMismatchMessage;
39
39
  exports.extractInteractiveElements = extractInteractiveElements;
40
+ exports.contractFilesIn = contractFilesIn;
40
41
  exports.saveMapping = saveMapping;
41
42
  exports.loadMapping = loadMapping;
42
43
  const fs = __importStar(require("fs"));
@@ -112,6 +113,30 @@ function depthMismatchMessage(m) {
112
113
  async function extractInteractiveElements(page) {
113
114
  return page.evaluate(extract_1.extractInPage);
114
115
  }
116
+ /**
117
+ * The `.json` files in a contract directory that are **contracts**.
118
+ *
119
+ * A leading underscore is how this codebase spells "not a page" — `_exercised.json`,
120
+ * `_resolved/`, `_layouts/`. The rule was written down as *"every consumer that enumerates
121
+ * contracts already filters it out"*, and it was true of three of them and false of two.
122
+ *
123
+ * What that cost, reproduced end to end: `run` writes `mapping/_exercised.json`, `baseline`
124
+ * promotes it (this directory is copied wholesale, which is right — the record belongs to the
125
+ * moment), and then **`diff --dir` and `fix --dir` both read it as a page named `_exercised`**,
126
+ * fail on `has no "elements" array`, put it in `unreadable` and refuse the entire comparison
127
+ * with exit 2. The remedy they printed — *restore the file from git, or re-capture it* — could
128
+ * not work: re-capturing writes the same file again. A tool breaking its own documented loop
129
+ * with a file it wrote itself, and telling the user to fix it by hand.
130
+ *
131
+ * So the rule lives here now, in the module that knows what a contract is, and the five call
132
+ * sites share it. Three of them already agreed; the point is that agreeing by coincidence is
133
+ * what produced this.
134
+ */
135
+ function contractFilesIn(dir) {
136
+ if (!fs.existsSync(dir))
137
+ return [];
138
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('.') && !f.startsWith('_'));
139
+ }
115
140
  function saveMapping(mapping, dir = (0, config_1.mappingDir)()) {
116
141
  fs.mkdirSync(dir, { recursive: true });
117
142
  const file = (0, config_1.mappingPath)(mapping.page, dir);
package/dist/aom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":"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"}
@@ -157,7 +157,10 @@ function resolveSelectors(args) {
157
157
  out[pom] = anchor ? { status: 'anchored', selector: anchor } : { status: 'outside' };
158
158
  }
159
159
  else {
160
- out[pom] = { status: 'ambiguous', count: nodes.length };
160
+ // `mapped` is every contracted element this string could have meant. Recording it is
161
+ // what lets the next capture ask whether they survived: without it an ambiguous
162
+ // binding is a dead end, and the drift it hides is invisible to every other check.
163
+ out[pom] = { status: 'ambiguous', count: nodes.length, selectors: mapped };
161
164
  }
162
165
  }
163
166
  return out;
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/browser/resolve.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/browser/resolve.js"],"names":[],"mappings":"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"}
package/dist/cli/args.js CHANGED
@@ -20,7 +20,7 @@ exports.KNOWN_FLAGS = {
20
20
  login: ['--url', '--session'],
21
21
  secret: ['--json'],
22
22
  session: ['--check', '--offline', '--url', '--session', '--json'],
23
- map: ['--screenshots', '--session', '--deep', '--depth', '--deep-budget'],
23
+ map: ['--screenshots', '--session', '--deep', '--depth', '--deep-budget', '--fix-redirects'],
24
24
  baseline: ['--dry-run'],
25
25
  diff: ['--strict', '--json', '--junit', '--dir', '--report', '--open'],
26
26
  fix: ['--dry-run', '--locators', '--dir', '--json'],
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAkCA,gDAeC;AAjDD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC;IAClG,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC;IAC9G,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC;IACjE,GAAG,EAAE,CAAC,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,CAAC;IACzE,QAAQ,EAAE,CAAC,WAAW,CAAC;IACvB,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtE,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC;IACnD,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC;IACvD,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAC3D,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IAChD,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACtF,KAAK,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IAC7D,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,YAAY,EAAE,CAAC,QAAQ,CAAC;IACxB,EAAE,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,CAAC;CACnF,CAAC;AAEF,SAAgB,kBAAkB,CAAC,OAAe,EAAE,IAAc;IAChE,MAAM,KAAK,GAAG,mBAAW,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CACb,eAAe,OAAO,0FAA0F;gBAC9G,4GAA4G;gBAC5G,8FAA8F,CACjG,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,qBAAqB,OAAO,8CAA8C,CAAC,CAAC;IAChH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAkCA,gDAeC;AAjDD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC;IAClG,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC;IAC9G,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC;IACjE,GAAG,EAAE,CAAC,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,CAAC;IAC5F,QAAQ,EAAE,CAAC,WAAW,CAAC;IACvB,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtE,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC;IACnD,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC;IACvD,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAC3D,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IAChD,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACtF,KAAK,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IAC7D,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,YAAY,EAAE,CAAC,QAAQ,CAAC;IACxB,EAAE,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,CAAC;CACnF,CAAC;AAEF,SAAgB,kBAAkB,CAAC,OAAe,EAAE,IAAc;IAChE,MAAM,KAAK,GAAG,mBAAW,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CACb,eAAe,OAAO,0FAA0F;gBAC9G,4GAA4G;gBAC5G,8FAA8F,CACjG,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,qBAAqB,OAAO,8CAA8C,CAAC,CAAC;IAChH,CAAC;AACH,CAAC"}
@@ -2,6 +2,7 @@ import { PageMapping } from '../aom';
2
2
  import { MaskOutcome } from '../nameMask';
3
3
  import { BindingDrift } from '../resolution';
4
4
  import { Usage } from '../ingest';
5
+ import { RouteChange } from '../routeDrift';
5
6
  import { NameDriftFinding } from '../nameDrift';
6
7
  import { FixOutcome, ReportEvidence } from '../htmlReport';
7
8
  import { SystemicOutcome } from '../systemicDelta';
@@ -284,6 +285,12 @@ export interface DirReport {
284
285
  nameDrift?: NameDriftFinding[];
285
286
  /** `config.nameMask`'s effect across the walk — applied patterns and refusals, unioned. */
286
287
  nameMask?: MaskOutcome;
288
+ /**
289
+ * Links whose **destination** changed while their identity held. Reported beside the
290
+ * `healable` row, never instead of it: the row's promise holds at the element level and
291
+ * breaks at the navigation level. Absent when nothing moved. See SPEC-route-drift.md.
292
+ */
293
+ routeDrift?: RouteChange[];
287
294
  /**
288
295
  * What to run next, or `null` when the next step is not a command this caller should
289
296
  * run. See `nextStep.ts` — BLOCK is deliberately null.
@@ -387,6 +394,12 @@ export interface Report {
387
394
  * refused with the reason. Absent when no mask is configured.
388
395
  */
389
396
  nameMask?: MaskOutcome;
397
+ /**
398
+ * Links whose **destination** changed while their identity held. Reported beside the
399
+ * `healable` row, never instead of it: the row's promise holds at the element level and
400
+ * breaks at the navigation level. Absent when nothing moved. See SPEC-route-drift.md.
401
+ */
402
+ routeDrift?: RouteChange[];
390
403
  /**
391
404
  * Selector strings from the suite whose binding to a contracted element broke.
392
405
  * Present only once `ingest` has inventoried the tests **and** the capture has
@@ -420,3 +433,52 @@ export interface BindingCoverage {
420
433
  inventoried: number;
421
434
  side: 'baseline' | 'current' | 'both';
422
435
  }
436
+ /**
437
+ * Re-attach link destinations from the baseline elements onto the diff rows.
438
+ * Done here rather than in `match.js`: href is not element identity (it never
439
+ * feeds matching), and `browser/` ships twice — once on npm, once served by the
440
+ * web tool — so it stays dependency-free and untouched.
441
+ */
442
+ /**
443
+ * A link that now goes somewhere else — computed once, applied at every door.
444
+ *
445
+ * Sits beside `attachBaselineHrefs` because it is the same fact about the same field, and
446
+ * outside `match.js` for the same stated reason: href is not identity, and that file ships
447
+ * three times. See `SPEC-route-drift.md`.
448
+ *
449
+ * It raises the verdict **only** when the suite navigates to the old route. An app is allowed
450
+ * to reorganise its URLs; a gate that goes red on that is a gate someone switches off. With
451
+ * `usage.routes` absent — an old inventory, or a project that never ran `ingest` — nothing
452
+ * escalates and the report is byte-identical to before.
453
+ */
454
+ export declare function attachRouteDrift(report: Report, baseline: PageMapping, current: PageMapping, usage: Usage | null): Report;
455
+ /**
456
+ * What happened to the selector strings the suite actually writes.
457
+ *
458
+ * This is the half a CSS-anchored Page Object suite could never see: the contract stores
459
+ * a synthesized selector, the suite writes its own, and until the two are bound every
460
+ * drift row reads `not referenced by your tests`. Printed with the test sites so the
461
+ * reader goes straight to the line, exactly like the name-drift findings.
462
+ *
463
+ * Never prints what did not break. A Page Object is mostly selectors for rows, cells and
464
+ * containers — real elements the contract does not hold — and listing those as findings
465
+ * is the wall of false positives this whole feature is designed around.
466
+ */
467
+ /**
468
+ * Exported for its test only. Two rows here carry `status: 'ambiguous'` and describe
469
+ * opposite facts, and the sentence that tells them apart is derived rather than stored —
470
+ * the kind of thing that can be wrong for a release without anyone noticing, because both
471
+ * outputs look plausible.
472
+ */
473
+ export declare function bindingDriftNote(drift: BindingDrift[] | undefined, usage: Usage | null): void;
474
+ /**
475
+ * Links that now lead somewhere else.
476
+ *
477
+ * Its own section, and that is the whole feature: these lines were already in the report,
478
+ * rendered as `🔧 drifted` beside a button that slid down the DOM. Same icon, same list, and
479
+ * one of them means the user ends up on a different page.
480
+ *
481
+ * Exported for its test: the sentence is derived, and a wrong one here is well-formed prose
482
+ * describing a fact that did not happen.
483
+ */
484
+ export declare function routeDriftNote(changes: RouteChange[] | undefined): void;
package/dist/cli/diff.js CHANGED
@@ -57,6 +57,9 @@ exports.dirDiffAggregate = dirDiffAggregate;
57
57
  exports.aggregateReports = aggregateReports;
58
58
  exports.printAggregate = printAggregate;
59
59
  exports.readMapping = readMapping;
60
+ exports.attachRouteDrift = attachRouteDrift;
61
+ exports.bindingDriftNote = bindingDriftNote;
62
+ exports.routeDriftNote = routeDriftNote;
60
63
  const fs = __importStar(require("fs"));
61
64
  const path = __importStar(require("path"));
62
65
  const aom_1 = require("../aom");
@@ -67,6 +70,7 @@ const config_2 = require("../config");
67
70
  const ingest_1 = require("../ingest");
68
71
  const captureMerge_1 = require("../captureMerge");
69
72
  const nameHint_1 = require("../nameHint");
73
+ const routeDrift_1 = require("../routeDrift");
70
74
  const nameDrift_1 = require("../nameDrift");
71
75
  const htmlReport_1 = require("../htmlReport");
72
76
  const evidence_1 = require("../evidence");
@@ -285,7 +289,7 @@ function isDirectory(p) {
285
289
  }
286
290
  function capturedAtOf(dir) {
287
291
  try {
288
- const file = fs.readdirSync(dir).find((f) => f.endsWith('.json') && !f.startsWith('.') && !f.startsWith('_'));
292
+ const file = (0, aom_1.contractFilesIn)(dir)[0];
289
293
  if (!file)
290
294
  return undefined;
291
295
  const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
@@ -402,6 +406,7 @@ async function runDiff(args) {
402
406
  // raised here rather than inside the shared matcher.
403
407
  report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
404
408
  report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
409
+ attachRouteDrift(report, baseline, current, usage);
405
410
  if (json) {
406
411
  console.log(JSON.stringify(report, null, 2));
407
412
  }
@@ -921,6 +926,7 @@ opts = {}) {
921
926
  const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, masks, usage), baseline);
922
927
  report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
923
928
  report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
929
+ attachRouteDrift(report, baseline, current, usage);
924
930
  return attachBindingDrift(report, baselineFile, currentFile, page, baseline, current);
925
931
  };
926
932
  const layoutsDir = path.join(baselineDir, '_layouts');
@@ -950,8 +956,7 @@ opts = {}) {
950
956
  }
951
957
  }
952
958
  }
953
- const baselineFiles = fs.readdirSync(baselineDir)
954
- .filter((f) => f.endsWith('.json') && !f.startsWith('.'));
959
+ const baselineFiles = (0, aom_1.contractFilesIn)(baselineDir);
955
960
  const scope = opts.pages ? new Set(opts.pages) : null;
956
961
  const outOfScope = [];
957
962
  for (const file of baselineFiles) {
@@ -1060,6 +1065,9 @@ function aggregateReports(reports) {
1060
1065
  // inherits the raise for free — this only collects the findings for reporting.
1061
1066
  const nameDrift = (0, nameDrift_1.mergeFindings)(reports.map((r) => r.report.nameDrift ?? []));
1062
1067
  const bindingDrift = reports.flatMap((r) => r.report.bindingDrift ?? []);
1068
+ // Collected like the rest: a finding that exists per page and vanishes from the total is
1069
+ // a finding nobody sees, since --dir is how a whole app is judged.
1070
+ const routeChanges = reports.flatMap((r) => r.report.routeDrift ?? []);
1063
1071
  const nameMask = (0, nameMask_1.mergeMaskOutcomes)(reports.map((r) => r.report.nameMask));
1064
1072
  const depthUnmeasured = (0, depthDelta_1.mergeDepthUnmeasured)(reports.map((r) => r.report.depthUnmeasured));
1065
1073
  // One fact about the run, not one per page: every page shares the same _resolved/ dir.
@@ -1068,6 +1076,7 @@ function aggregateReports(reports) {
1068
1076
  verdict,
1069
1077
  ...(nameDrift.length > 0 ? { nameDrift } : {}),
1070
1078
  ...(bindingDrift.length > 0 ? { bindingDrift } : {}),
1079
+ ...(routeChanges.length > 0 ? { routeDrift: routeChanges } : {}),
1071
1080
  ...(nameMask ? { nameMask } : {}),
1072
1081
  ...(depthUnmeasured ? { depthUnmeasured } : {}),
1073
1082
  ...(bindingCoverage ? { bindingCoverage } : {}),
@@ -1180,6 +1189,7 @@ function printAggregate(agg, reports, baselineDir, currentDir, usage) {
1180
1189
  nameDriftNote(reports.flatMap((r) => r.report.rows));
1181
1190
  }
1182
1191
  bindingDriftNote(reports.flatMap((r) => r.report.bindingDrift ?? []), (0, ingest_1.loadUsage)());
1192
+ routeDriftNote(reports.flatMap((r) => r.report.routeDrift ?? []));
1183
1193
  console.log('');
1184
1194
  }
1185
1195
  function statusIcon(status) {
@@ -1239,6 +1249,33 @@ function readMapping(file) {
1239
1249
  * feeds matching), and `browser/` ships twice — once on npm, once served by the
1240
1250
  * web tool — so it stays dependency-free and untouched.
1241
1251
  */
1252
+ /**
1253
+ * A link that now goes somewhere else — computed once, applied at every door.
1254
+ *
1255
+ * Sits beside `attachBaselineHrefs` because it is the same fact about the same field, and
1256
+ * outside `match.js` for the same stated reason: href is not identity, and that file ships
1257
+ * three times. See `SPEC-route-drift.md`.
1258
+ *
1259
+ * It raises the verdict **only** when the suite navigates to the old route. An app is allowed
1260
+ * to reorganise its URLs; a gate that goes red on that is a gate someone switches off. With
1261
+ * `usage.routes` absent — an old inventory, or a project that never ran `ingest` — nothing
1262
+ * escalates and the report is byte-identical to before.
1263
+ */
1264
+ function attachRouteDrift(report, baseline, current, usage) {
1265
+ const changes = (0, routeDrift_1.routeDrift)({
1266
+ rows: report.rows,
1267
+ before: baseline.elements,
1268
+ after: current.elements,
1269
+ baseUrl: (0, config_2.configuredBaseUrl)() ?? undefined,
1270
+ usage,
1271
+ page: current.page || baseline.page,
1272
+ });
1273
+ if (changes.length > 0)
1274
+ report.routeDrift = changes;
1275
+ if ((0, routeDrift_1.routeDriftBlocks)(changes).length > 0)
1276
+ report.verdict = 'BLOCK';
1277
+ return report;
1278
+ }
1242
1279
  function attachBaselineHrefs(report, baseline) {
1243
1280
  const hrefBySelector = new Map(baseline.elements.filter((e) => e.href).map((e) => [e.selector, e.href]));
1244
1281
  for (const row of report.rows) {
@@ -1291,6 +1328,12 @@ const MAX_HINT_GROUPS = 6;
1291
1328
  * containers — real elements the contract does not hold — and listing those as findings
1292
1329
  * is the wall of false positives this whole feature is designed around.
1293
1330
  */
1331
+ /**
1332
+ * Exported for its test only. Two rows here carry `status: 'ambiguous'` and describe
1333
+ * opposite facts, and the sentence that tells them apart is derived rather than stored —
1334
+ * the kind of thing that can be wrong for a release without anyone noticing, because both
1335
+ * outputs look plausible.
1336
+ */
1294
1337
  function bindingDriftNote(drift, usage) {
1295
1338
  if (!drift || drift.length === 0)
1296
1339
  return;
@@ -1313,17 +1356,73 @@ function bindingDriftNote(drift, usage) {
1313
1356
  site(d, usage);
1314
1357
  }
1315
1358
  for (const d of rest) {
1359
+ // Two opposite facts share the word `ambiguous`, and the discriminator is structural:
1360
+ // a baseline that BOUND one element carries `was`, so the ambiguity is new (it now
1361
+ // matches several). Without `was` the ambiguity is the *baseline's* — it matched
1362
+ // several and now matches none. Printing the first sentence over the second case
1363
+ // would describe the exact opposite of what happened.
1364
+ const lostFromAmbiguous = d.status === 'ambiguous' && !d.was;
1316
1365
  const why = d.status === 'gone'
1317
1366
  ? 'the element it named is gone — nothing to rewrite towards'
1318
1367
  : d.status === 'dangling'
1319
1368
  ? 'it matched an element the contract does not hold, and now matches nothing —\n' +
1320
1369
  ' the test is red, and there is no recorded identity to rewrite towards'
1321
- : 'it now matches several elements — a coin flip, not a repair';
1370
+ : lostFromAmbiguous
1371
+ ? 'it used to match several elements and now matches none — the test is red'
1372
+ : 'it now matches several elements — a coin flip, not a repair';
1322
1373
  console.log(` ⛔ ${d.selector}${d.was ? ` (was ${d.was})` : ''}`);
1323
1374
  console.log(` ${why}.`);
1375
+ if (lostFromAmbiguous) {
1376
+ // Named, never offered as a rewrite: the baseline never established which of them
1377
+ // the test meant, so choosing is the reader's job and the tool says so out loud.
1378
+ if (d.candidates && d.candidates.length > 0) {
1379
+ console.log(` what it could have meant is still on the page, now at:`);
1380
+ for (const c of d.candidates)
1381
+ console.log(` ${c}`);
1382
+ console.log(` pick the one your test meant — no rewrite is offered for a coin flip.`);
1383
+ }
1384
+ else {
1385
+ console.log(` none of what it could have meant is still on the page.`);
1386
+ }
1387
+ }
1324
1388
  site(d, usage);
1325
1389
  }
1326
1390
  }
1391
+ /**
1392
+ * Links that now lead somewhere else.
1393
+ *
1394
+ * Its own section, and that is the whole feature: these lines were already in the report,
1395
+ * rendered as `🔧 drifted` beside a button that slid down the DOM. Same icon, same list, and
1396
+ * one of them means the user ends up on a different page.
1397
+ *
1398
+ * Exported for its test: the sentence is derived, and a wrong one here is well-formed prose
1399
+ * describing a fact that did not happen.
1400
+ */
1401
+ function routeDriftNote(changes) {
1402
+ if (!changes || changes.length === 0)
1403
+ return;
1404
+ const n = changes.length;
1405
+ console.log(`\n 🧭 ${n} link${n === 1 ? '' : 's'} now lead${n === 1 ? 's' : ''} somewhere else ` +
1406
+ `(the element is the same — its destination is not):`);
1407
+ for (const c of changes) {
1408
+ const gating = (c.usedAt?.length ?? 0) > 0;
1409
+ console.log(` ${gating ? '⛔' : '•'} ${c.name || '(no name)'}${c.page ? ` [${c.page}]` : ''}`);
1410
+ console.log(` ${c.from} → ${c.to}`);
1411
+ if (gating) {
1412
+ // The only thing that makes this more than a remark: the suite goes to the old route,
1413
+ // so it is red now and the tool can prove it.
1414
+ const shown = c.usedAt.slice(0, 3).map((s) => `${s.file}:${s.line}`).join(', ');
1415
+ const more = c.usedAt.length > 3 ? ` (+${c.usedAt.length - 3} more)` : '';
1416
+ console.log(` your suite navigates to the old one: ${shown}${more}`);
1417
+ }
1418
+ }
1419
+ if (!changes.some((c) => (c.usedAt?.length ?? 0) > 0)) {
1420
+ // Said out loud rather than implied by the absence of a line: an app is allowed to
1421
+ // reorganise its URLs, and a reader who cannot tell "not gating" from "not checked"
1422
+ // will treat the next one the same way.
1423
+ console.log(` Not failing the run — no test navigates to these. Check the ones you reach by URL.`);
1424
+ }
1425
+ }
1327
1426
  /** Where that string is written, so the reader opens the file instead of grepping. */
1328
1427
  function site(d, usage) {
1329
1428
  const sites = usage?.selectors?.[d.selector];
@@ -1427,6 +1526,7 @@ function printHuman(report, files, usage) {
1427
1526
  nameDriftNote(rows);
1428
1527
  }
1429
1528
  bindingDriftNote(report.bindingDrift, usage ?? null);
1529
+ routeDriftNote(report.routeDrift);
1430
1530
  if (verdict === 'PASS' && counts.renamed === 0) {
1431
1531
  console.log(' ✅ No drift — your suite is safe to run.');
1432
1532
  }