@ia-qa/self-healing 1.7.13 → 1.7.15

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.
package/README.md CHANGED
@@ -209,6 +209,33 @@ extensions and the code paths are exercised, but the test suite's rewrite cases
209
209
  against `.js`/`.ts` files. Treat non-JS rewriting as working-but-less-travelled: run
210
210
  `fix --dry-run` first, which you should be doing anyway.
211
211
 
212
+ ### One more axis: how much of the page is under contract at all
213
+
214
+ `map` records the state a page **loads in**. Everything behind a dropdown, a tab, an accordion
215
+ or a modal is in no contract — so when it drifts, nothing says so. It is a coverage hole and
216
+ never a false alarm, which is what makes it survive: the gate stays green over a surface it
217
+ never looked at.
218
+
219
+ ```bash
220
+ ia-qa-heal map --deep # open them, and contract what appears
221
+ ```
222
+
223
+ Measured on ia-qa.com: one page 105 → 133 elements, another 126 → 264. Across eight unrelated
224
+ sites (MDN, Bootstrap, Material UI, GOV.UK, Wikipedia, Docusaurus…), seven gained elements —
225
+ the gain is largest on component-framework apps and near zero on content sites, so it is worth
226
+ saying which one you have before reaching for it.
227
+
228
+ - **It cannot mutate your app.** Every non-GET request is aborted for the walk, and controls
229
+ named like actions (*log out*, *delete*, *pay*…) are never clicked — a logout handler that
230
+ only clears local storage sends no request for the network guard to catch.
231
+ - **What it reaches but cannot contract is reported**, not hidden (`↩ N left out`): a selector
232
+ must name exactly one element, and a positional one captured inside a menu can collide with
233
+ the page behind it.
234
+ - **A deep baseline is only comparable to a deep capture.** Mixing the two reports everything
235
+ behind a menu as `lost`; `diff` and the MCP both warn before the verdict.
236
+ - **~1 minute per page** against seconds for a plain `map`. It is for building a baseline, not
237
+ for a per-commit gate.
238
+
212
239
  ## The verdict
213
240
 
214
241
  ```
@@ -341,6 +368,18 @@ and they never fail your tests. Each worker writes its own shard to
341
368
  page contracts `map` produces — byte-compatible, named from `config.pages` so they pair with
342
369
  your baseline.
343
370
 
371
+ **One suite, several applications.** A page name is a filename, so two URLs sharing one name
372
+ claim to be the same page — and five products each with a `/login` are not. A URL on a host
373
+ other than your `baseUrl` is therefore named `<host>--<path>`
374
+ (`partner.example.com--login`), never `login`. Single-host suites are unaffected: the prefix
375
+ only appears where two applications would otherwise have overwritten each other's contract, and
376
+ `www.`/apex and `http`/`https` count as the same host. To choose the name yourself, declare the
377
+ page with its **absolute** URL:
378
+
379
+ ```json
380
+ { "name": "partner-login", "url": "https://partner.example.com/login" }
381
+ ```
382
+
344
383
  **Any other framework or language.** The shard format *is* the interface; the helpers above are
345
384
  sugar over it. Run the extractor in the page (`extract.js` in this package, also a copy-paste
346
385
  DevTools snippet on ia-qa.com) and drop a JSON file into `.ia-qa/mapping/.capture/`:
@@ -297,6 +297,24 @@ Requirements when it is built:
297
297
  silently compared against one captured by `map`. Same reasoning as `run`'s
298
298
  source-mismatch warning.
299
299
 
300
+ ### 7.1 `map --deep` reaches states, and resolution deliberately does not follow it
301
+
302
+ `--deep` (see CLAUDE.md) opens menus, tabs and dialogs and contracts what they reveal. It would
303
+ be easy to assume selector resolution comes along — it does not, and the restraint is deliberate.
304
+ `resolvePageSelectors` runs against the **loaded** state, before exploration starts, because
305
+ exploration ends with the page in whatever state its walk finished in. Resolving after it would
306
+ probe the suite's selectors against an open modal and report a *working* binding as broken —
307
+ the one error this package treats as worse than the drift it heals.
308
+
309
+ So a deep contract holds elements with **no binding row**. That is `unresolved`-shaped, and §2.2
310
+ already governs it: silence is never evidence of absence. What it costs is real and worth stating
311
+ — the highest-value finding here, a selector that now reaches a *different* element, cannot be
312
+ made about anything behind a menu.
313
+
314
+ Closing that gap is phase 2's job, not a patch to `map`: the states `--deep` reaches are the same
315
+ states the shards see for free, and resolution belongs where the page is already standing in one.
316
+ Do not "fix" it by moving `resolvePageSelectors` after `explorePage`.
317
+
300
318
  ---
301
319
 
302
320
  ## 8. What this feature is not
package/TUTORIAL.md CHANGED
@@ -11,7 +11,7 @@
11
11
  2. [The 30-second mental model](#2-the-30-second-mental-model)
12
12
  3. [Before you start](#3-before-you-start)
13
13
  4. [Path A — I use an AI agent (easiest)](#4-path-a--i-use-an-ai-agent-easiest)
14
- 5. [Path B — I use a terminal](#5-path-b--i-use-a-terminal)
14
+ 5. [Path B — I use a terminal](#5-path-b--i-use-a-terminal) — including [menus, tabs and modals: `map --deep`](#step-53b--what-about-my-menus-tabs-and-modals-map---deep)
15
15
  6. [Your first real workflow](#6-your-first-real-workflow-the-whole-point) — including [a test just failed, is it drift?](#step-64b--you-didnt-run-diff-you-got-a-red-build--explain) · [the console, if you don't want a terminal](#step-68--no-terminal-open-the-console)
16
16
  7. [Reading the verdict](#7-reading-the-verdict)
17
17
  8. [Putting it in CI](#8-putting-it-in-ci) — [JUnit trends](#82-trend-charts-for-free--junit) · [the run history](#83-the-trend--ia-qa-heal-history)
@@ -344,6 +344,48 @@ Plus, for the whole app:
344
344
 
345
345
  Commit them.
346
346
 
347
+ ### Step 5.3b — What about my menus, tabs and modals? `map --deep`
348
+
349
+ A page has more than one state, and `map` records the one it loads in. Everything behind a
350
+ dropdown, a tab, an accordion or a dialog is simply not there — so if it drifts, nothing tells
351
+ you. It is a **coverage** hole rather than a false alarm, which is what makes it easy to miss:
352
+ the gate stays green over a surface it never looked at.
353
+
354
+ ```bash
355
+ npx ia-qa-heal map --deep
356
+ ```
357
+
358
+ It opens what can be opened and contracts what appears. On ia-qa.com: `/method` 105 → 133
359
+ elements, the home page 126 → 264.
360
+
361
+ ```
362
+ ✔ 264 interactive elements → .ia-qa/mapping/home.json + .ia-qa/mapping/home.md
363
+ 🔍 deep: +140 behind interaction (124 on load) · 37 states · 54 clicks, 9 reloads
364
+ ↩ 133 revealed elements left out — selector already names another element
365
+ 🛡 34 non-GET requests blocked during exploration (nothing mutating left the browser)
366
+ ```
367
+
368
+ Four things worth knowing before you use it:
369
+
370
+ **It cannot break your app.** Every non-GET request is aborted for the whole walk, so a click
371
+ that would have deleted, paid or submitted anything never reaches your server — and controls
372
+ whose name reads like an action (*log out*, *delete*, *pay*…) are not clicked at all, because a
373
+ logout handler that just clears local storage sends no request for the guard to stop.
374
+
375
+ **Elements it reaches but cannot contract are reported, not hidden** (`↩ 133 left out`). A
376
+ selector must name exactly one element; a positional one captured inside a menu can collide with
377
+ an element on the page behind it, and a contract holding the same selector twice makes the diff
378
+ pair rows at random. Those are dropped and counted.
379
+
380
+ **A deep baseline may only be diffed against a deep capture.** Mixing them reports everything
381
+ behind a menu as `lost` — the tool warns you before the verdict, in the CLI and over MCP alike.
382
+ Decide once, per baseline.
383
+
384
+ **It costs about a minute per page** where a plain `map` costs seconds. Use it to build or
385
+ refresh a baseline, not on every commit. `--depth 2` goes one click deeper; it was measured at
386
+ three times the time for a contract that never finishes, so leave it alone unless you have a
387
+ reason.
388
+
347
389
  ### Step 5.4 — Day one, before any drift: `audit`
348
390
 
349
391
  Everything after this point pays off at the *next* UI change. `audit` pays off now: it asks **does my test suite still name things that exist?**
@@ -858,6 +900,7 @@ npx ia-qa-heal init # one-time setup
858
900
  npx ia-qa-heal discover [--crawl] [--apply] # optional: propose pages for config.json (sitemap or safe crawl)
859
901
  npx ia-qa-heal map # scan → *.json + *.md + _overview.md + _navigation.md/.svg
860
902
  npx ia-qa-heal map --screenshots # + one JPEG per page, shown in the --report HTML (gitignore _shots/)
903
+ npx ia-qa-heal map --deep # + open menus/tabs/dialogs and contract what they reveal (see below)
861
904
  npx ia-qa-heal baseline # promote mapping/ → baseline/ (commit it)
862
905
  # …change the app, then `map` again…
863
906
  npx ia-qa-heal diff # verdict: PASS / FIX / BLOCK (baseline/ vs mapping/)
package/dist/aom.d.ts CHANGED
@@ -102,6 +102,35 @@ export declare function isSameCapture(baseline: {
102
102
  } | null | undefined, current: {
103
103
  capturedAt?: string;
104
104
  } | null | undefined): boolean;
105
+ /**
106
+ * Were these two contracts explored to the same depth?
107
+ *
108
+ * A deep contract holds the elements behind menus, tabs and dialogs; a shallow one cannot, so
109
+ * diffing one against the other reports every one of them as `lost`. That is not a handful of
110
+ * noisy rows — most of a page's surface can sit behind interaction, so the mismatch manufactures
111
+ * the whole BLOCK out of how the two captures were taken.
112
+ *
113
+ * Absent means depth 0 — the loaded state only. That is a *known* value, not an unknown one:
114
+ * every contract written before `--deep` existed describes exactly one state. (`captureState` is
115
+ * the opposite case, where absent genuinely means "cannot tell", and must never read as a match.)
116
+ *
117
+ * Lives here, beside `isSameCapture`, for the same reason: both doors into a diff — the CLI and
118
+ * the MCP server — have to agree on what is not a comparison. One warning and the other silent
119
+ * would just move the phantom verdict to whichever door the user knocked on.
120
+ */
121
+ export declare function depthMismatch(baseline: {
122
+ exploredDepth?: number;
123
+ } | null | undefined, current: {
124
+ exploredDepth?: number;
125
+ } | null | undefined): {
126
+ baseline: number;
127
+ current: number;
128
+ } | null;
129
+ /** The sentence every door prints or attaches when `depthMismatch` returns non-null. */
130
+ export declare function depthMismatchMessage(m: {
131
+ baseline: number;
132
+ current: number;
133
+ }): string;
105
134
  export declare function extractInteractiveElements(page: Page): Promise<MappedElement[]>;
106
135
  export declare function saveMapping(mapping: PageMapping, dir?: string): string;
107
136
  export declare function loadMapping(pageName: string, dir?: string): PageMapping;
package/dist/aom.js CHANGED
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.isSameCapture = isSameCapture;
37
+ exports.depthMismatch = depthMismatch;
38
+ exports.depthMismatchMessage = depthMismatchMessage;
37
39
  exports.extractInteractiveElements = extractInteractiveElements;
38
40
  exports.saveMapping = saveMapping;
39
41
  exports.loadMapping = loadMapping;
@@ -63,6 +65,36 @@ const extract_1 = require("./browser/extract");
63
65
  function isSameCapture(baseline, current) {
64
66
  return Boolean(baseline?.capturedAt) && baseline.capturedAt === current?.capturedAt;
65
67
  }
68
+ /**
69
+ * Were these two contracts explored to the same depth?
70
+ *
71
+ * A deep contract holds the elements behind menus, tabs and dialogs; a shallow one cannot, so
72
+ * diffing one against the other reports every one of them as `lost`. That is not a handful of
73
+ * noisy rows — most of a page's surface can sit behind interaction, so the mismatch manufactures
74
+ * the whole BLOCK out of how the two captures were taken.
75
+ *
76
+ * Absent means depth 0 — the loaded state only. That is a *known* value, not an unknown one:
77
+ * every contract written before `--deep` existed describes exactly one state. (`captureState` is
78
+ * the opposite case, where absent genuinely means "cannot tell", and must never read as a match.)
79
+ *
80
+ * Lives here, beside `isSameCapture`, for the same reason: both doors into a diff — the CLI and
81
+ * the MCP server — have to agree on what is not a comparison. One warning and the other silent
82
+ * would just move the phantom verdict to whichever door the user knocked on.
83
+ */
84
+ function depthMismatch(baseline, current) {
85
+ const b = baseline?.exploredDepth ?? 0;
86
+ const c = current?.exploredDepth ?? 0;
87
+ return b === c ? null : { baseline: b, current: c };
88
+ }
89
+ /** The sentence every door prints or attaches when `depthMismatch` returns non-null. */
90
+ function depthMismatchMessage(m) {
91
+ const deeper = m.baseline > m.current ? 'baseline' : 'current capture';
92
+ const shallower = deeper === 'baseline' ? 'current capture' : 'baseline';
93
+ return (`Exploration depth mismatch — the ${deeper} was captured with \`map --deep\`, the ${shallower} ` +
94
+ `was not (depth ${m.baseline} vs ${m.current}). A deep contract holds the elements behind ` +
95
+ `menus, tabs and dialogs; a shallow one cannot, so those elements read as \`lost\` here. Most ` +
96
+ `of this verdict is probably that, not drift in your app. Capture both sides the same way.`);
97
+ }
66
98
  async function extractInteractiveElements(page) {
67
99
  return page.evaluate(extract_1.extractInPage);
68
100
  }
package/dist/aom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,sCAKC;AAED,gEAEC;AAED,kCAwBC;AAED,kCAQC;AAxJD,uCAAyB;AACzB,2CAA6B;AAE7B,qCAAmD;AACnD,+CAAkD;AAoFlD;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,aAAa,CAC3B,QAAoD,EACpD,OAAmD;IAEnD,OAAO,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,QAAS,CAAC,UAAU,KAAK,OAAO,EAAE,UAAU,CAAC;AACvF,CAAC;AAEM,KAAK,UAAU,0BAA0B,CAAC,IAAU;IACzD,OAAO,IAAI,CAAC,QAAQ,CAAC,uBAAa,CAA6B,CAAC;AAClE,CAAC;AAED,SAAgB,WAAW,CAAC,OAAoB,EAAE,MAAc,IAAA,mBAAU,GAAE;IAC1E,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAA,oBAAW,EAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC5C,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;QACtE,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC;YAC9D,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,8BAA8B,IAAI,CAAC,UAAU,iBAAiB,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACxD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAExC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,KAAK,KAAK,GAAG,OAAO,CAAC,IAAI,OAAO;YAAE,SAAS;QAC/C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAChE,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,OAAO,EAAE,CAAC;gBAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBAC3C,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,wBAAwB,KAAK,oCAAoC,CAAC,CAAC;YACpG,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB,EAAE,MAAc,IAAA,mBAAU,GAAE;IACtE,MAAM,IAAI,GAAG,IAAA,oBAAW,EAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,8BAA8B,QAAQ,MAAM,IAAI,2BAA2B,QAAQ,WAAW,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;AAClE,CAAC"}
1
+ {"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":"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"}
@@ -40,6 +40,7 @@ exports.mergeCapturesAnnounced = mergeCapturesAnnounced;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const config_1 = require("./config");
43
+ const pageName_1 = require("./pageName");
43
44
  const aom_1 = require("./aom");
44
45
  const markdown_1 = require("./markdown");
45
46
  const union_1 = require("./browser/union");
@@ -58,6 +59,39 @@ exports.CAPTURE_DIRNAME = '.capture';
58
59
  function captureDir(cwd = (0, config_1.baseDir)()) {
59
60
  return path.join((0, config_1.mappingDir)(cwd), exports.CAPTURE_DIRNAME);
60
61
  }
62
+ /**
63
+ * One contract, two applications — say so instead of writing it.
64
+ *
65
+ * A page name is an identity claim, and `resolvePageName` keeps it honest by putting
66
+ * the host in the name whenever the URL is not the app's own. That needs a `baseUrl`,
67
+ * so it needs a config; without one every host's `/login` still resolves to `login`,
68
+ * and this merge would union five products into one contract, then diff it against
69
+ * whichever one it happened to hold last — a permanent phantom drift on a suite that
70
+ * is green, offered to `fix` as a rename.
71
+ *
72
+ * We cannot pick the namespace here (there is no declared app to be foreign *to*, and
73
+ * inventing one from whichever host appeared most would make the names depend on
74
+ * worker scheduling). So we name the collision and the remedy, rather than let silence
75
+ * read as "checked, fine". The remedy differs by cause, and the cause is *checked*, not
76
+ * assumed: with a config in place these names cannot come from this version's capture,
77
+ * so they came from a stale `.capture/` or a hand-written shard.
78
+ */
79
+ function warnHostCollisions(byPage, configured) {
80
+ for (const [name, { hosts }] of byPage) {
81
+ if (hosts.size < 2)
82
+ continue;
83
+ const remedy = configured
84
+ ? ` This version names an off-\`baseUrl\` page \`<host>--<path>\`, so these shards were ` +
85
+ `written by an older version or by hand. Delete \`${config_1.CONFIG_DIR}/mapping/.capture/\`, re-run ` +
86
+ `the suite, and declare any page you want named yourself in "pages" with its absolute URL.`
87
+ : ` There is no ${config_1.CONFIG_DIR}/config.json here, so nothing names the app these pages ` +
88
+ `belong to; with a "baseUrl", every other host is prefixed into the page name instead. ` +
89
+ `Run \`ia-qa-heal init\`, or declare each one in "pages" with its absolute URL.`;
90
+ console.warn(`⚠ Page "${name}" was captured from ${hosts.size} different hosts this run ` +
91
+ `(${Array.from(hosts).join(', ')}) — they are sharing one contract, so its diff is not ` +
92
+ `meaningful.\n${remedy}`);
93
+ }
94
+ }
61
95
  function mergeCaptures(cwd = (0, config_1.baseDir)()) {
62
96
  const dir = captureDir(cwd);
63
97
  if (!fs.existsSync(dir))
@@ -84,9 +118,17 @@ function mergeCaptures(cwd = (0, config_1.baseDir)()) {
84
118
  if (!p?.page || !Array.isArray(p.elements))
85
119
  continue;
86
120
  const prev = byPage.get(p.page);
121
+ const hosts = prev ? prev.hosts : new Set();
122
+ try {
123
+ hosts.add((0, pageName_1.foldHost)(new URL(p.url)));
124
+ }
125
+ catch {
126
+ /* an unparseable URL says nothing about which app this was */
127
+ }
87
128
  byPage.set(p.page, {
88
129
  url: p.url,
89
130
  elements: prev ? (0, union_1.unionElements)(prev.elements, p.elements) : p.elements,
131
+ hosts,
90
132
  });
91
133
  }
92
134
  }
@@ -94,12 +136,15 @@ function mergeCaptures(cwd = (0, config_1.baseDir)()) {
94
136
  // here at the merge — one place covering the Playwright, Cypress and Selenium
95
137
  // hooks at once. No config, no patterns: live capture without one stays as-is.
96
138
  let patterns = [];
139
+ let configured = false;
97
140
  try {
98
141
  patterns = (0, volatile_1.parseVolatilePatterns)((0, config_1.loadConfig)(cwd).volatile);
142
+ configured = true;
99
143
  }
100
144
  catch {
101
145
  /* config optional for a merge */
102
146
  }
147
+ warnHostCollisions(byPage, configured);
103
148
  const capturedAt = new Date().toISOString();
104
149
  const outDir = (0, config_1.mappingDir)(cwd);
105
150
  const pages = [];
@@ -1 +1 @@
1
- {"version":3,"file":"captureMerge.js","sourceRoot":"","sources":["../src/captureMerge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"captureMerge.js","sourceRoot":"","sources":["../src/captureMerge.ts"],"names":[],"mappings":"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"}
@@ -167,6 +167,16 @@ export interface DirReport {
167
167
  totals: Report['counts'];
168
168
  /** Pairs left out of the verdict: same capture on both sides, so incapable of showing drift. */
169
169
  staleExcluded?: string[];
170
+ /**
171
+ * The two sides were explored to different depths (`map --deep`) — so much of this verdict is
172
+ * probably an artefact of how the contracts were captured rather than drift in the app.
173
+ * Carried on the aggregate so `--json` and the report state it, not only the terminal.
174
+ */
175
+ depthMismatch?: {
176
+ baseline: number;
177
+ current: number;
178
+ why: string;
179
+ };
170
180
  /**
171
181
  * Renamed labels the suite locates by name, de-duped across pages — a shared
172
182
  * header renames once, not once per page that carries it.
package/dist/cli/diff.js CHANGED
@@ -365,6 +365,11 @@ async function runDiff(args) {
365
365
  refuseToCompare('same-capture', sameCaptureMessage(baseline.capturedAt), 'Change the app and run `ia-qa-heal map` again, so the two sides are different moments.', json);
366
366
  return;
367
367
  }
368
+ // Before the verdict, never after: a depth mismatch does not colour a few rows, it can invent
369
+ // the entire BLOCK. Same check as `diff --dir` and the MCP — one shared function, three doors.
370
+ const depths = (0, aom_1.depthMismatch)(baseline, current);
371
+ if (depths && !json)
372
+ console.error(`\n⚠️ ${(0, aom_1.depthMismatchMessage)(depths)}`);
368
373
  const usage = (0, ingest_1.loadUsage)();
369
374
  const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, (0, nameMask_1.loadNameMasks)(), usage), baseline);
370
375
  // Same rule as the directory walk: the resolutions sit next to the mappings they
@@ -547,7 +552,7 @@ async function runDirDiff(baselineDir, currentDir, opts) {
547
552
  console.error(`❌ "${currentDir}" is not a directory.`);
548
553
  process.exit(2);
549
554
  }
550
- const { reports, stale, systemic, depthMismatch } = collectDirDiff(baselineDir, currentDir);
555
+ const { reports, stale, systemic, depthMismatch: depths } = collectDirDiff(baselineDir, currentDir);
551
556
  if (reports.length === 0 && stale.length > 0) {
552
557
  refuseToCompare('same-capture', `❌ Nothing to compare: ${stale.length === 1 ? 'the only pair is' : `all ${stale.length} pairs are`} ` +
553
558
  `the same capture on both sides.\n\n` +
@@ -566,21 +571,19 @@ async function runDirDiff(baselineDir, currentDir, opts) {
566
571
  // it can invent the entire BLOCK, because everything the deep side found behind a menu is
567
572
  // simply absent from the shallow side and reads as `lost`. Someone who meets the verdict
568
573
  // first will start hand-repairing drift that never happened.
569
- if (depthMismatch) {
570
- const deeper = depthMismatch.baseline > depthMismatch.current ? 'baseline' : 'current capture';
571
- const shallower = deeper === 'baseline' ? 'current capture' : 'baseline';
572
- console.error(`\n⚠️ Exploration depth mismatch — the ${deeper} was captured with \`map --deep\`, the ` +
573
- `${shallower} was not\n (depth ${depthMismatch.baseline} vs ${depthMismatch.current}). ` +
574
- `A deep contract holds the elements behind menus, tabs and dialogs;\n a shallow one ` +
575
- `cannot, so those elements read as \`lost\` here. Most of what follows is probably that,\n` +
576
- ` not drift in your app.\n` +
577
- ` Capture both sides the same way: \`ia-qa-heal map --deep\` before diffing, or re-baseline` +
578
- ` without it.`);
574
+ if (depths) {
575
+ console.error(`\n⚠️ ${(0, aom_1.depthMismatchMessage)(depths)}\n` +
576
+ ` \`ia-qa-heal map --deep\` before diffing, or re-baseline without it.`);
579
577
  }
580
578
  // Aggregate verdict
581
579
  const aggregate = aggregateReports(reports);
582
580
  if (stale.length > 0)
583
581
  aggregate.staleExcluded = stale;
582
+ // Onto the aggregate, not only the terminal: `--json` is what a pipeline reads, and a CI that
583
+ // sees BLOCK without knowing the two sides were captured differently is the same blind spot
584
+ // the MCP had. Every consumer of a verdict gets the caveat attached to it.
585
+ if (depths)
586
+ aggregate.depthMismatch = { ...depths, why: (0, aom_1.depthMismatchMessage)(depths) };
584
587
  if (systemic.groups.length > 0)
585
588
  aggregate.systemic = systemic;
586
589
  Object.assign(aggregate, (0, nextStep_1.nextStepFor)(aggregate.verdict, {
@@ -683,8 +686,7 @@ function collectDirDiff(baselineDir, currentDir) {
683
686
  // Exploration depth, settled the same way and for the same reason. Absent means the loaded
684
687
  // state only, which is depth 0 — a real value here, not unknown: every contract written
685
688
  // before `--deep` existed describes exactly one state, and that is knowable.
686
- let baselineDepth = 0;
687
- let currentDepth = 0;
689
+ let deepest = null;
688
690
  // Kept out of the verdict rather than counted as `ok` — N ok that were never
689
691
  // compared is exactly the false green this guard exists to stop.
690
692
  const stale = [];
@@ -695,8 +697,7 @@ function collectDirDiff(baselineDir, currentDir) {
695
697
  const assess = (baseline, current, baselineFile, currentFile, page) => {
696
698
  baselineState ??= baseline.captureState;
697
699
  currentState ??= current.captureState;
698
- baselineDepth = Math.max(baselineDepth, baseline.exploredDepth ?? 0);
699
- currentDepth = Math.max(currentDepth, current.exploredDepth ?? 0);
700
+ deepest ??= (0, aom_1.depthMismatch)(baseline, current);
700
701
  const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, masks, usage), baseline);
701
702
  report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
702
703
  report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
@@ -760,7 +761,7 @@ function collectDirDiff(baselineDir, currentDir) {
760
761
  reports,
761
762
  stale,
762
763
  systemic,
763
- depthMismatch: baselineDepth === currentDepth ? null : { baseline: baselineDepth, current: currentDepth },
764
+ depthMismatch: deepest,
764
765
  };
765
766
  }
766
767
  function aggregateReports(reports) {