@ia-qa/self-healing 1.6.6 → 1.6.8
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 +118 -2
- package/TUTORIAL.md +138 -3
- package/assets/logoKawaiiGreenTiny.png +0 -0
- package/dist/cli/args.js +4 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/diff.d.ts +6 -1
- package/dist/cli/diff.js +114 -4
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/emit.d.ts +71 -0
- package/dist/cli/emit.js +150 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/fix.js +36 -0
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/history.d.ts +12 -0
- package/dist/cli/history.js +95 -0
- package/dist/cli/history.js.map +1 -0
- package/dist/cli/index.js +53 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +32 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/map.js +60 -0
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/run.js +77 -7
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/ui.d.ts +16 -0
- package/dist/cli/ui.js +275 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/evidence.d.ts +130 -0
- package/dist/evidence.js +232 -0
- package/dist/evidence.js.map +1 -0
- package/dist/history.d.ts +145 -0
- package/dist/history.js +230 -0
- package/dist/history.js.map +1 -0
- package/dist/htmlReport.d.ts +47 -0
- package/dist/htmlReport.js +76 -1
- package/dist/htmlReport.js.map +1 -1
- package/dist/junit.d.ts +53 -0
- package/dist/junit.js +150 -0
- package/dist/junit.js.map +1 -0
- package/dist/mcp/server.js +41 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/ui/icon.d.ts +41 -0
- package/dist/ui/icon.js +115 -0
- package/dist/ui/icon.js.map +1 -0
- package/dist/ui/lock.d.ts +40 -0
- package/dist/ui/lock.js +138 -0
- package/dist/ui/lock.js.map +1 -0
- package/dist/ui/page.d.ts +1 -0
- package/dist/ui/page.js +584 -0
- package/dist/ui/page.js.map +1 -0
- package/dist/ui/registry.d.ts +62 -0
- package/dist/ui/registry.js +139 -0
- package/dist/ui/registry.js.map +1 -0
- package/dist/ui/server.d.ts +67 -0
- package/dist/ui/server.js +604 -0
- package/dist/ui/server.js.map +1 -0
- package/dist/ui/shortcut.d.ts +90 -0
- package/dist/ui/shortcut.js +176 -0
- package/dist/ui/shortcut.js.map +1 -0
- package/package.json +2 -1
- package/skills/ia-qa-heal/SKILL.md +27 -0
package/README.md
CHANGED
|
@@ -16,7 +16,29 @@ One command. `ia-qa-heal run` runs your suite, watches it break, diffs the live
|
|
|
16
16
|
|
|
17
17
|
*Real commands against a live site, real stdout — only the typing speed is synthesised.*
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Easiest start — have your agent set it up
|
|
20
|
+
|
|
21
|
+
If you already work with Claude Code, Cursor, Copilot or another coding agent, don't configure this
|
|
22
|
+
by hand. Paste this to it:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Install and set up @ia-qa/self-healing in this project.
|
|
26
|
+
1. First run `npx -y -p @ia-qa/self-healing ia-qa-heal skill --print` and follow it.
|
|
27
|
+
That output is the authoritative doc — do not guess any command or flag from memory.
|
|
28
|
+
2. Register the MCP server so you can drive it yourself:
|
|
29
|
+
command `npx`, args ["-y", "-p", "@ia-qa/self-healing", "ia-qa-heal-mcp"].
|
|
30
|
+
3. Read my e2e tests and my router, propose the page list for .ia-qa/config.json,
|
|
31
|
+
and wait for me to confirm before mapping anything.
|
|
32
|
+
4. Then explain to me, in five lines: what map / baseline / diff / fix each do,
|
|
33
|
+
and which locators you will never rewrite without asking me.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Step 1 matters more than it looks: this package is newer than most models' training data, so an agent
|
|
37
|
+
left to improvise will invent flags that don't exist. `skill --print` hands it the real instructions —
|
|
38
|
+
including [the four prohibitions](#working-with-an-ai-agent--ia-qa-heal-skill) — before it touches
|
|
39
|
+
anything. Step 4 is how you check it actually read them instead of guessing.
|
|
40
|
+
|
|
41
|
+
## Quick start — by hand
|
|
20
42
|
|
|
21
43
|
```bash
|
|
22
44
|
npx -y -p @ia-qa/self-healing ia-qa-heal init # wizard → .ia-qa/config.json
|
|
@@ -37,7 +59,7 @@ npx ia-qa-heal init
|
|
|
37
59
|
|
|
38
60
|
An agent that asks a browser where a button went is doing by hand what `.ia-qa/mapping/*.json` already answers offline: every interactive element's **role, accessible name and selector**, captured. A `link` the suite still calls a `button` shows up in one `grep`.
|
|
39
61
|
|
|
40
|
-
`ia-qa-heal skill` installs that reflex — plus the rules that must not be broken — as a skill your agent loads:
|
|
62
|
+
`ia-qa-heal skill` installs that reflex — plus the rules that must not be broken — as a skill your agent loads (this is what step 1 of the [paste-block above](#easiest-start--have-your-agent-set-it-up) makes it read):
|
|
41
63
|
|
|
42
64
|
```bash
|
|
43
65
|
ia-qa-heal skill # where it would go, and whether it is current — writes nothing
|
|
@@ -213,6 +235,56 @@ Run again with --apply to add the uncovered pages to config.pages.
|
|
|
213
235
|
|
|
214
236
|
Then `ia-qa-heal map` captures their contracts as usual. Discovery never maps, heals, or edits a test — it only ever edits `config.json`, and only with `--apply`.
|
|
215
237
|
|
|
238
|
+
## The console — `ia-qa-heal ui`
|
|
239
|
+
|
|
240
|
+
The report tells you what drifted. The console lets you **act on it**.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npx ia-qa-heal ui
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
…or, for someone who should never have to open a terminal at all:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npx ia-qa-heal ui --shortcut # once per project
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
That puts an icon on the desktop — the ia-qa logo, wired to *this* project. Double-click it: the console starts and the browser opens on it. No URL is ever typed or copied, which is also how the session token stays out of a human's hands. Click it twice and it reuses the console it already opened. **Closing the browser tab stops the server**, so a click never leaves a process running behind your back (`--keep-alive` opts out).
|
|
253
|
+
|
|
254
|
+
It opens a local page with three things the terminal cannot give you:
|
|
255
|
+
|
|
256
|
+
- **The rows waiting on a human** — every `ambiguous`, `lost` and `rebound` row, each with a **highlighted crop** of the candidates as they look on the page right now (needs `map --screenshots`). When five identical "Copy" buttons exist, the row's selector tells you nothing; the picture tells you which one. Alongside them, the `file:line` of every test that will break.
|
|
257
|
+
- **The verbs as buttons** — capture, ingest, diff, audit, check, preview/apply the rewrites — with the output streamed back.
|
|
258
|
+
- **The run history as charts** — verdicts over time, mean coverage, and which pages actually churn.
|
|
259
|
+
|
|
260
|
+
**How it is kept safe**, because a server on a QA's machine is not free:
|
|
261
|
+
|
|
262
|
+
| | |
|
|
263
|
+
|---|---|
|
|
264
|
+
| **Loopback only** | bound to `127.0.0.1`, never `0.0.0.0` — in a container that would be a published port |
|
|
265
|
+
| **A one-time token** | in the URL, required on every request. Without it, *any* page open in your browser could `fetch` the console and read the contracts of your authenticated app, or trigger an action |
|
|
266
|
+
| **Host pinning** | requests whose `Host` is not loopback are refused, which is the DNS-rebinding case a token alone does not cover |
|
|
267
|
+
| **No new authority** | buttons spawn this package's own CLI with a fixed argv from an allowlist — never a string from the request. The console can do **nothing the CLI would refuse**: an `ambiguous` locator is never rewritten from here either |
|
|
268
|
+
|
|
269
|
+
Nothing is uploaded, there is no telemetry and no external asset — the page is inline HTML/CSS/JS and every byte it shows came off your disk. It refuses to start under CI, where an interactive surface would just hang; use `--json`, `--junit` or the exit code there.
|
|
270
|
+
|
|
271
|
+
### Several projects — the switcher
|
|
272
|
+
|
|
273
|
+
The header carries a **Project** dropdown as soon as there is more than one. It is governed by a single rule:
|
|
274
|
+
|
|
275
|
+
> **A project is reachable only if you opened a console in it.**
|
|
276
|
+
|
|
277
|
+
Nothing scans your disk. `~/.ia-qa/projects.json` is a consent list, and there are exactly two ways to consent: run `ia-qa-heal ui` in a folder, or pick **+ Add another project…** in the dropdown and give the path. Switching between listed projects resolves the requested directory against that list *exactly* — a folder outside it cannot be named. Entries whose project has been deleted are dropped rather than offered, and actions are handed `--config <active project>` explicitly rather than inheriting anything.
|
|
278
|
+
|
|
279
|
+
Adding from the console is a deliberate action by whoever holds the session token — the same token that already authorizes *Repair my test files*. What the list defends against is a **drive-by**: another page in your browser reaching localhost. That is stopped by the token and the `Host` check, not by making you walk to a terminal.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
ia-qa-heal ui --forget # clear the whole list
|
|
283
|
+
ia-qa-heal ui --forget ../shop # drop one project
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Forgetting only removes a folder from the switcher — it deletes nothing in the project, and opening a console there again re-adds it. `IAQA_HOME` relocates the list (useful on a shared machine).
|
|
287
|
+
|
|
216
288
|
## Visualize the app — `ia-qa-heal graph`
|
|
217
289
|
|
|
218
290
|
Turn the mappings you already have into a **navigation graph** (page → page, derived from the `href` on each page's links — the same engine `map` uses for `_overview.md`). Read-only: it never rewrites your mapping files, it just renders them.
|
|
@@ -611,6 +683,50 @@ jobs:
|
|
|
611
683
|
|
|
612
684
|
`diff` with no arguments compares the committed `.ia-qa/baseline/` against what `map` just re-captured — which is why the baseline has to be in git. Add `--report drift-report.html` to attach a branded HTML report to the build, and `--strict` to fail on FIX too. The full workflow (report upload, artifact, `--strict`) is in **[TUTORIAL.md → Putting it in CI](TUTORIAL.md#8-putting-it-in-ci)**.
|
|
613
685
|
|
|
686
|
+
### Trend charts for free — `--junit`
|
|
687
|
+
|
|
688
|
+
`diff --junit` and `run --junit` write the verdict as **JUnit XML**, which Jenkins, GitLab, CircleCI, Azure DevOps and the GitHub Actions reporters all ingest natively — including the per-build history graph. You get the trend without this package rendering anything in CI.
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
npx ia-qa-heal diff --junit reports/selector-drift.xml
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
It is language-agnostic by construction: what it describes is your **contract**, not your suite, so a Cypress/JS, Selenium/Python or Playwright/.NET project produces identically shaped XML.
|
|
695
|
+
|
|
696
|
+
| contract row | JUnit | why |
|
|
697
|
+
|---|---|---|
|
|
698
|
+
| `ok` | pass | the locator still finds the same element |
|
|
699
|
+
| `renamed` | pass + `system-out` | the selector still works; the label moved |
|
|
700
|
+
| `healable` | failure `type="healable"` | broken today — auto-repairable, but broken |
|
|
701
|
+
| `ambiguous` · `lost` · `rebound` | failure, typed | broken, and no deterministic repair exists |
|
|
702
|
+
| name drift | failure `type="name-drift-*"` | a name-anchored locator names nothing now |
|
|
703
|
+
| page not compared | **skipped** | not measured — reporting it as a pass would be the false green this tool exists to prevent |
|
|
704
|
+
|
|
705
|
+
> ⚠️ **Publish it under its own file pattern**, separate from your suite's results. Merged into one trend, the two sets of numbers stop meaning anything.
|
|
706
|
+
|
|
707
|
+
`run --json` (and `diff --json`) give the same verdict as one JSON document on stdout. For `run`, every other line moves to stderr so a pipe stays parseable.
|
|
708
|
+
|
|
709
|
+
### The trend, locally — `ia-qa-heal history`
|
|
710
|
+
|
|
711
|
+
Every `diff` and `run` appends one line to `.ia-qa/history.jsonl`: verdict, counters, capture source, page coverage and the git commit. It is the one thing no single invocation can reconstruct afterwards, so it is recorded as it happens.
|
|
712
|
+
|
|
713
|
+
```
|
|
714
|
+
📈 Run history · 6 recorded
|
|
715
|
+
6 comparable runs captured by `map` · 67% ended with drift · 100% mean coverage
|
|
716
|
+
|
|
717
|
+
⛔ BLOCK 8/9/26, 6:25 PM 3 drifted 2/2 pages
|
|
718
|
+
✅ PASS 8/9/26, 6:26 PM 0 drifted 2/2 pages
|
|
719
|
+
…
|
|
720
|
+
🔥 Where the churn lives
|
|
721
|
+
checkout 13 drifted elements across 4 runs
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Runs captured **differently** — a `map` baseline versus a live suite capture — are held **out** of the trend and named, never averaged in: the two read a page differently, so charting both would invent a trend out of a capture artifact. Same for a run that covered far fewer pages than the rest.
|
|
725
|
+
|
|
726
|
+
The file is small, deterministic text (unlike `_shots/`), so committing it is defensible if you want the trend to survive across machines and CI.
|
|
727
|
+
|
|
728
|
+
Agents get the same numbers through the MCP tool **`heal_history`** — one `summarize()`, three renderings (terminal, console charts, MCP).
|
|
729
|
+
|
|
614
730
|
## Map artifacts
|
|
615
731
|
|
|
616
732
|
Beyond one contract per page, every `map` run rebuilds two views of the whole app. Both are derived entirely from the mappings on disk (no extra page visit) and rebuilt on every run, including `map <page>`, so they never drift out of date.
|
package/TUTORIAL.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
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
14
|
5. [Path B — I use a terminal](#5-path-b--i-use-a-terminal)
|
|
15
|
-
6. [Your first real workflow](#6-your-first-real-workflow-the-whole-point)
|
|
15
|
+
6. [Your first real workflow](#6-your-first-real-workflow-the-whole-point) — including [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
|
-
8. [Putting it in CI](#8-putting-it-in-ci)
|
|
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)
|
|
18
18
|
9. [Optional: AI suggestions (BYOK)](#9-optional-ai-suggestions-for-the-lost-ones-byok)
|
|
19
19
|
10. [Troubleshooting](#10-troubleshooting)
|
|
20
20
|
11. [FAQ](#11-faq)
|
|
@@ -83,7 +83,30 @@ Whatever it uses, it tells you: `Browser: system browser (channel "chrome")`.
|
|
|
83
83
|
|
|
84
84
|
*Use this if you have Claude Code, Claude Desktop, Cursor, or another MCP-compatible agent. You'll talk to your app in plain English.*
|
|
85
85
|
|
|
86
|
-
### Step 4.
|
|
86
|
+
### Step 4.0 — The shortcut: let your agent do the whole setup
|
|
87
|
+
|
|
88
|
+
The steps below are the manual version. If your agent can edit files in your project (Claude Code, Cursor, Copilot in your IDE), just paste this to it and skip straight to step 4.4 below:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
Install and set up @ia-qa/self-healing in this project.
|
|
92
|
+
1. First run `npx -y -p @ia-qa/self-healing ia-qa-heal skill --print` and follow it.
|
|
93
|
+
That output is the authoritative doc — do not guess any command or flag from memory.
|
|
94
|
+
2. Register the MCP server so you can drive it yourself:
|
|
95
|
+
command `npx`, args ["-y", "-p", "@ia-qa/self-healing", "ia-qa-heal-mcp"].
|
|
96
|
+
3. Read my e2e tests and my router, propose the page list for .ia-qa/config.json,
|
|
97
|
+
and wait for me to confirm before mapping anything.
|
|
98
|
+
4. Then explain to me, in five lines: what map / baseline / diff / fix each do,
|
|
99
|
+
and which locators you will never rewrite without asking me.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Two things to know about that prompt, because they are the difference between it working and it wasting your afternoon:
|
|
103
|
+
|
|
104
|
+
- **Step 1 is not optional politeness.** This package is newer than most models' training data. An agent left to improvise invents flags that don't exist, they fail, and you conclude the tool is broken. `skill --print` hands it the real instructions — including the four things it must never do on its own — before it touches anything.
|
|
105
|
+
- **Step 4 is your check.** If the five-line explanation is vague or mentions a verb that isn't `map`, `baseline`, `diff` or `fix`, it didn't read the skill. Tell it to run step 1 again.
|
|
106
|
+
|
|
107
|
+
You will still have to **restart your agent** (step 4.2 below) after it writes the MCP config — it can't load a server it registered mid-conversation.
|
|
108
|
+
|
|
109
|
+
### Step 4.1 — Add the tool to your agent (manual)
|
|
87
110
|
|
|
88
111
|
Find your agent's MCP config file and add the `ia-qa-self-healing` block below.
|
|
89
112
|
|
|
@@ -134,6 +157,8 @@ Then point the config at the built file (use the **absolute** path):
|
|
|
134
157
|
|
|
135
158
|
### Step 4.1b — Teach your agent how to use it (recommended)
|
|
136
159
|
|
|
160
|
+
*Skip this if you used step 4.0 — that prompt already made your agent do it.*
|
|
161
|
+
|
|
137
162
|
The MCP block above gives your agent four *tools*. It does not give it the **judgment** to use them: when to read the contract instead of opening a browser, which verb answers which question, and — most importantly — which locators it must never rewrite on its own.
|
|
138
163
|
|
|
139
164
|
```bash
|
|
@@ -434,6 +459,49 @@ Three optional config keys make it better (`ia-qa-heal init` asks for them):
|
|
|
434
459
|
|
|
435
460
|
`run` keeps every judgment where it was: **BLOCK stops for you**, healable rewrites are shown as a dry run and applied only after you confirm (`--yes` for CI), and nothing is ever committed. **After applying the fix it re-runs your suite once more — without `IAQA_CAPTURE`, so it checks rather than re-maps — and reports whether the tests pass now.** The exit code follows that verdict (`0` green, `1` still failing), so CI catches a fix that rewrote the selector but left the test red. `--no-verify` skips the re-run; with `--report`, the HTML also lists the applied `old → new` edits (file + lines) and the verify verdict.
|
|
436
461
|
|
|
462
|
+
### Step 6.8 — No terminal? Open the console
|
|
463
|
+
|
|
464
|
+
Everything above is a command line. If the person who has to *decide* is a manual QA, that is a
|
|
465
|
+
wall — and the decision is the one thing the tool cannot take from them.
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
ia-qa-heal ui
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
A local page opens in your browser with three things a terminal cannot give you:
|
|
472
|
+
|
|
473
|
+
- **The rows waiting on a human.** Each `ambiguous` / `lost` / `rebound` row, with a **highlighted
|
|
474
|
+
picture** of each candidate as it looks on the page right now (needs `map --screenshots`), the
|
|
475
|
+
`file:line` of every test that will break, and — the part that matters — **a sentence saying what
|
|
476
|
+
to do about it**. When five identical "Copy" buttons exist, `div:nth-of-type(4) > … > button`
|
|
477
|
+
tells you nothing; the picture tells you which one.
|
|
478
|
+
- **The verbs as buttons**, in plain words: *Look at the app again*, *Check for changes*,
|
|
479
|
+
*Repair my test files ✎*. Every result is a sentence, not an exit code, with the raw output one
|
|
480
|
+
click away.
|
|
481
|
+
- **The run history as charts** — see [§8.3](#83-the-trend-ia-qa-heal-history).
|
|
482
|
+
|
|
483
|
+
Once, per project, so nobody ever types a command again:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
ia-qa-heal ui --shortcut
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
That puts an icon on the desktop, bound to *this* project. Double-click → the console starts and
|
|
490
|
+
the browser opens on it. `ia-qa-heal init` offers to do this for you at the end of setup.
|
|
491
|
+
|
|
492
|
+
**What it is not:** it is not a website, and nothing is hosted. The server binds `127.0.0.1` only,
|
|
493
|
+
every request needs a one-time token that lives in the URL the tool opened, and requests whose
|
|
494
|
+
`Host` is not loopback are refused. Closing the browser tab stops the server, so a click never
|
|
495
|
+
leaves a process running behind your back. It can do **nothing the CLI would refuse** — an
|
|
496
|
+
`ambiguous` locator is never rewritten from the console either. It refuses to start under CI, where
|
|
497
|
+
it would simply hang.
|
|
498
|
+
|
|
499
|
+
Working on several projects? The header carries a **Project** dropdown. Pick
|
|
500
|
+
**+ Add another project…** and paste the folder's path — it must already contain a `.ia-qa`
|
|
501
|
+
directory (run `ia-qa-heal init` there once if not). Everything you add is remembered, so it is one
|
|
502
|
+
click from then on, and `ia-qa-heal ui --forget [dir]` takes it back out (deleting nothing inside
|
|
503
|
+
the project).
|
|
504
|
+
|
|
437
505
|
---
|
|
438
506
|
|
|
439
507
|
## 7. Reading the verdict
|
|
@@ -523,6 +591,67 @@ misconfigured job fails loudly instead of passing without comparing anything.
|
|
|
523
591
|
|
|
524
592
|
Add `--strict` if you want **FIX** to fail the build too (forces tests to be updated, not just healed).
|
|
525
593
|
|
|
594
|
+
### 8.2 Trend charts for free — `--junit`
|
|
595
|
+
|
|
596
|
+
Your CI already knows how to chart a test run over time. Give it the verdict in the format it
|
|
597
|
+
speaks and you get the history graph without this tool drawing anything:
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
npx ia-qa-heal diff --junit reports/selector-drift.xml
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Jenkins, GitLab, CircleCI, Azure DevOps and the GitHub Actions reporters all ingest JUnit XML —
|
|
604
|
+
including the per-build trend. It is **language-agnostic by construction**: what it describes is
|
|
605
|
+
your *contract*, not your suite, so a Cypress/JS, Selenium/Python or Playwright/.NET project
|
|
606
|
+
produces identically shaped XML.
|
|
607
|
+
|
|
608
|
+
| Your contract | In the CI |
|
|
609
|
+
|---|---|
|
|
610
|
+
| `ok` | pass |
|
|
611
|
+
| `renamed` | pass, with the label change in the output |
|
|
612
|
+
| `healable` | failure, typed `healable` (broken today — repairable, but broken) |
|
|
613
|
+
| `ambiguous` · `lost` · `rebound` | failure, typed |
|
|
614
|
+
| a renamed label your tests use | failure, typed `name-drift-*` |
|
|
615
|
+
| a page that could not be compared | **skipped** |
|
|
616
|
+
|
|
617
|
+
That last row is the one to notice: a page left out of the verdict is **not** a pass, and reporting
|
|
618
|
+
it as `skipped` puts the coverage gap in your CI's own summary instead of only in our log.
|
|
619
|
+
|
|
620
|
+
> ⚠️ **Publish it under its own file pattern**, separate from your suite's results. Merged into one
|
|
621
|
+
> trend, the two sets of numbers stop meaning anything.
|
|
622
|
+
|
|
623
|
+
`--json` gives the same verdict as one document on stdout (`run --json` moves every human line to
|
|
624
|
+
stderr, so a pipe stays parseable).
|
|
625
|
+
|
|
626
|
+
### 8.3 The trend — `ia-qa-heal history`
|
|
627
|
+
|
|
628
|
+
Every `diff` and `run` appends one line to `.ia-qa/history.jsonl`: verdict, counts, how the capture
|
|
629
|
+
was taken, how many pages it covered, and the git commit. It is the one thing no single run can
|
|
630
|
+
reconstruct afterwards — so it is recorded as it happens, and the runs you have not made yet are
|
|
631
|
+
the only ones you can still get.
|
|
632
|
+
|
|
633
|
+
```
|
|
634
|
+
$ ia-qa-heal history
|
|
635
|
+
|
|
636
|
+
📈 Run history · 7 recorded
|
|
637
|
+
7 comparable runs captured by `map` · 71% ended with drift · 100% mean coverage
|
|
638
|
+
|
|
639
|
+
⛔ BLOCK 8/9/26, 6:25 PM 3 drifted 2/2 pages
|
|
640
|
+
✅ PASS 8/9/26, 6:26 PM 0 drifted 2/2 pages
|
|
641
|
+
…
|
|
642
|
+
🔥 Where the churn lives
|
|
643
|
+
checkout 16 drifted elements across 5 runs
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Runs captured **differently** — a `map` baseline versus a live suite capture — are held **out** of
|
|
647
|
+
the trend and named, never quietly averaged in: the two read a page differently, so charting both
|
|
648
|
+
would invent a trend out of a capture artifact. Same for a run that covered far fewer pages than
|
|
649
|
+
the rest.
|
|
650
|
+
|
|
651
|
+
The file is small, deterministic text (unlike `_shots/`), so committing it is defensible if you
|
|
652
|
+
want the trend to survive across machines and CI. Agents get the same numbers through the
|
|
653
|
+
`heal_history` MCP tool.
|
|
654
|
+
|
|
526
655
|
---
|
|
527
656
|
|
|
528
657
|
## 9. Optional: AI suggestions for the `lost` ones (BYOK)
|
|
@@ -655,6 +784,12 @@ npx ia-qa-heal check # is the app sound? dead links · unna
|
|
|
655
784
|
npx ia-qa-heal graph --format markdown # the navigation graph, one card per page (RAG chunk / Confluence)
|
|
656
785
|
npx ia-qa-heal run # the whole loop: capture (your suite, IAQA_CAPTURE=1) → diff → confirm → fix → re-run to verify
|
|
657
786
|
|
|
787
|
+
npx ia-qa-heal ui # the local console: decide with pictures, verbs as buttons, history as charts
|
|
788
|
+
npx ia-qa-heal ui --shortcut # once per project: a desktop icon that opens it in one click
|
|
789
|
+
npx ia-qa-heal ui --forget [dir] # drop a project from the console's switcher
|
|
790
|
+
npx ia-qa-heal history [--json] # the trend: verdicts over time, coverage, which pages churn
|
|
791
|
+
npx ia-qa-heal diff --junit reports/x.xml # JUnit XML — your CI charts the trend natively
|
|
792
|
+
|
|
658
793
|
npx ia-qa-heal skill --install # teach your AI agent the rules (→ .claude/skills/ia-qa-heal/)
|
|
659
794
|
```
|
|
660
795
|
|
|
Binary file
|
package/dist/cli/args.js
CHANGED
|
@@ -19,14 +19,16 @@ exports.KNOWN_FLAGS = {
|
|
|
19
19
|
discover: ['--sitemap', '--crawl', '--no-reveal', '--strict-host', '--apply', '--max', '--depth'],
|
|
20
20
|
map: ['--screenshots'],
|
|
21
21
|
baseline: [],
|
|
22
|
-
diff: ['--strict', '--json', '--dir', '--report', '--open'],
|
|
22
|
+
diff: ['--strict', '--json', '--junit', '--dir', '--report', '--open'],
|
|
23
23
|
fix: ['--dry-run', '--locators', '--dir'],
|
|
24
24
|
ingest: [],
|
|
25
25
|
audit: ['--strict', '--json'],
|
|
26
26
|
check: ['--strict', '--json', '--offline'],
|
|
27
27
|
graph: ['--format', '--kind', '--out', '--open'],
|
|
28
|
-
run: ['--yes', '--no-verify', '--locators', '--report', '--open'],
|
|
28
|
+
run: ['--yes', '--no-verify', '--locators', '--report', '--junit', '--json', '--open'],
|
|
29
29
|
skill: ['--install', '--user', '--dir', '--print', '--force'],
|
|
30
|
+
history: ['--json', '--limit'],
|
|
31
|
+
ui: ['--port', '--no-open', '--shortcut', '--keep-alive', '--forget'],
|
|
30
32
|
};
|
|
31
33
|
function rejectUnknownFlags(command, args) {
|
|
32
34
|
const known = exports.KNOWN_FLAGS[command];
|
package/dist/cli/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AA6BA,gDAeC;AA5CD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,EAAE;IACR,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC;IACjG,GAAG,EAAE,CAAC,eAAe,CAAC;IACtB,QAAQ,EAAE,EAAE;IACZ,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtE,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,CAAC;IACzC,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC;IAC1C,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,EAAE,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,CAAC;CACtE,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"}
|
package/dist/cli/diff.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Usage } from '../ingest';
|
|
2
2
|
import { NameDriftFinding } from '../nameDrift';
|
|
3
|
-
import { FixOutcome } from '../htmlReport';
|
|
3
|
+
import { FixOutcome, ReportEvidence } from '../htmlReport';
|
|
4
4
|
/**
|
|
5
5
|
* `ia-qa-heal diff [baseline] [current] [--strict] [--json] [--dir]`
|
|
6
6
|
*
|
|
@@ -53,6 +53,11 @@ export declare function openInBrowser(file: string): void;
|
|
|
53
53
|
*/
|
|
54
54
|
export declare function pageUrlMap(): Map<string, string> | undefined;
|
|
55
55
|
export declare function pageShotMap(): Map<string, string> | undefined;
|
|
56
|
+
export declare function buildEvidence(reports: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
report: Report;
|
|
59
|
+
isLayout: boolean;
|
|
60
|
+
}>, currentDir: string): ReportEvidence | undefined;
|
|
56
61
|
/** 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. */
|
|
57
62
|
export declare function writeDriftReport(reportPath: string, agg: DirReport, reports: Array<{
|
|
58
63
|
name: string;
|
package/dist/cli/diff.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.openHint = openHint;
|
|
|
38
38
|
exports.openInBrowser = openInBrowser;
|
|
39
39
|
exports.pageUrlMap = pageUrlMap;
|
|
40
40
|
exports.pageShotMap = pageShotMap;
|
|
41
|
+
exports.buildEvidence = buildEvidence;
|
|
41
42
|
exports.writeDriftReport = writeDriftReport;
|
|
42
43
|
exports.runDiff = runDiff;
|
|
43
44
|
exports.printNameDrift = printNameDrift;
|
|
@@ -55,7 +56,9 @@ const captureMerge_1 = require("../captureMerge");
|
|
|
55
56
|
const nameHint_1 = require("../nameHint");
|
|
56
57
|
const nameDrift_1 = require("../nameDrift");
|
|
57
58
|
const htmlReport_1 = require("../htmlReport");
|
|
59
|
+
const evidence_1 = require("../evidence");
|
|
58
60
|
const summary_1 = require("./summary");
|
|
61
|
+
const emit_1 = require("./emit");
|
|
59
62
|
/**
|
|
60
63
|
* `ia-qa-heal diff [baseline] [current] [--strict] [--json] [--dir]`
|
|
61
64
|
*
|
|
@@ -177,10 +180,91 @@ function pageShotMap() {
|
|
|
177
180
|
}
|
|
178
181
|
return shots.size > 0 ? shots : undefined;
|
|
179
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Highlighted crops for every row a human has to decide, assembled from what
|
|
185
|
+
* `map --screenshots` left on disk.
|
|
186
|
+
*
|
|
187
|
+
* Only `ambiguous` and `rebound` rows are considered — the ones where the tool
|
|
188
|
+
* has stopped and handed over. Everything degrades to nothing: no screenshots,
|
|
189
|
+
* no rects, an unreadable mapping, or a page over budget all produce a report
|
|
190
|
+
* identical to the one this function did not exist for.
|
|
191
|
+
*
|
|
192
|
+
* The budget is separate from (and smaller than) the page-thumbnail budget
|
|
193
|
+
* because these images serve a decision rather than an illustration, and a
|
|
194
|
+
* report nobody can open in a browser helps no one. Pages are taken
|
|
195
|
+
* most-decisions-first, so the budget buys the most answers.
|
|
196
|
+
*/
|
|
197
|
+
const EVIDENCE_BUDGET_BYTES = 4 * 1024 * 1024;
|
|
198
|
+
function buildEvidence(reports, currentDir) {
|
|
199
|
+
const needing = reports
|
|
200
|
+
// A layout is compared as a set across pages; it has no single screenshot to
|
|
201
|
+
// crop from, so there is nothing here to show for it.
|
|
202
|
+
.filter((r) => !r.isLayout)
|
|
203
|
+
.map(({ name, report }) => ({
|
|
204
|
+
name,
|
|
205
|
+
rows: report.rows.filter((r) => r.status === 'ambiguous' || r.rebound === true),
|
|
206
|
+
}))
|
|
207
|
+
.filter((p) => p.rows.length > 0)
|
|
208
|
+
.sort((a, b) => b.rows.length - a.rows.length);
|
|
209
|
+
if (needing.length === 0)
|
|
210
|
+
return undefined;
|
|
211
|
+
const css = [];
|
|
212
|
+
const rows = {};
|
|
213
|
+
let spent = 0;
|
|
214
|
+
for (const p of needing) {
|
|
215
|
+
const shot = (0, evidence_1.loadPageShot)(p.name);
|
|
216
|
+
if (!shot)
|
|
217
|
+
continue;
|
|
218
|
+
let after;
|
|
219
|
+
try {
|
|
220
|
+
after = JSON.parse(fs.readFileSync(path.join(currentDir, `${p.name}.json`), 'utf8'));
|
|
221
|
+
if (!Array.isArray(after.elements))
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const cls = `pg-${p.name.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
228
|
+
const pageRows = {};
|
|
229
|
+
let usesImage = false;
|
|
230
|
+
for (const r of p.rows) {
|
|
231
|
+
const ev = (0, evidence_1.rowEvidence)(r, after, shot);
|
|
232
|
+
if (!ev)
|
|
233
|
+
continue;
|
|
234
|
+
if (ev.nowAt || ev.candidates.length > 0)
|
|
235
|
+
usesImage = true;
|
|
236
|
+
pageRows[r.selector] = {
|
|
237
|
+
...(ev.nowAt ? { nowAt: { ...ev.nowAt, crop: { cls, ...ev.nowAt.crop } } } : {}),
|
|
238
|
+
candidates: ev.candidates.map((c) => ({ ...c, crop: { cls, ...c.crop } })),
|
|
239
|
+
...(ev.note ? { note: ev.note } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (Object.keys(pageRows).length === 0)
|
|
243
|
+
continue;
|
|
244
|
+
if (usesImage) {
|
|
245
|
+
if (spent + shot.dataUri.length > EVIDENCE_BUDGET_BYTES)
|
|
246
|
+
continue;
|
|
247
|
+
css.push(`.${cls}{background-image:url("${shot.dataUri}")}`);
|
|
248
|
+
spent += shot.dataUri.length;
|
|
249
|
+
}
|
|
250
|
+
rows[p.name] = pageRows;
|
|
251
|
+
}
|
|
252
|
+
if (Object.keys(rows).length === 0)
|
|
253
|
+
return undefined;
|
|
254
|
+
return { css: css.join('\n'), rows };
|
|
255
|
+
}
|
|
180
256
|
/**
|
|
181
257
|
* When a directory of contracts was captured. Read off any one of them — `map`
|
|
182
258
|
* stamps them all in the same run.
|
|
183
259
|
*/
|
|
260
|
+
function isDirectory(p) {
|
|
261
|
+
try {
|
|
262
|
+
return fs.statSync(p).isDirectory();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
184
268
|
function capturedAtOf(dir) {
|
|
185
269
|
try {
|
|
186
270
|
const file = fs.readdirSync(dir).find((f) => f.endsWith('.json') && !f.startsWith('.') && !f.startsWith('_'));
|
|
@@ -209,6 +293,9 @@ function writeDriftReport(reportPath, agg, reports, baselineDirPath, currentDirP
|
|
|
209
293
|
pageShots: pageShotMap(),
|
|
210
294
|
baselineCapturedAt: capturedAtOf(baselineDirPath),
|
|
211
295
|
currentCapturedAt: capturedAtOf(currentDirPath),
|
|
296
|
+
// Single-file mode passes two files, not two directories; the evidence for a
|
|
297
|
+
// page lives beside its mapping either way.
|
|
298
|
+
evidence: buildEvidence(reports, isDirectory(currentDirPath) ? currentDirPath : path.dirname(currentDirPath)),
|
|
212
299
|
});
|
|
213
300
|
const dest = path.resolve(reportPath);
|
|
214
301
|
fs.writeFileSync(dest, html, 'utf8');
|
|
@@ -229,7 +316,8 @@ function refuseToCompare(reason, human, hint, json) {
|
|
|
229
316
|
}
|
|
230
317
|
async function runDiff(args) {
|
|
231
318
|
(0, captureMerge_1.mergeCapturesAnnounced)();
|
|
232
|
-
const { reportPath: explicitReport, rest:
|
|
319
|
+
const { reportPath: explicitReport, rest: afterReport } = takeReportFlag(args);
|
|
320
|
+
const { value: junitPath, rest: argsNoReport } = (0, emit_1.takeValueFlag)(afterReport, '--junit', emit_1.JUNIT_DEFAULT_PATH);
|
|
233
321
|
const strict = argsNoReport.includes('--strict');
|
|
234
322
|
const json = argsNoReport.includes('--json');
|
|
235
323
|
const open = argsNoReport.includes('--open');
|
|
@@ -262,7 +350,7 @@ async function runDiff(args) {
|
|
|
262
350
|
return;
|
|
263
351
|
}
|
|
264
352
|
if (dirMode) {
|
|
265
|
-
await runDirDiff(files[0], files[1], { strict, json, reportPath, open });
|
|
353
|
+
await runDirDiff(files[0], files[1], { strict, json, reportPath, open, junitPath });
|
|
266
354
|
return;
|
|
267
355
|
}
|
|
268
356
|
const [baseline, current] = files.map((f) => readMapping(f));
|
|
@@ -284,10 +372,19 @@ async function runDiff(args) {
|
|
|
284
372
|
printHuman(report, files, usage);
|
|
285
373
|
printNameDrift(report.nameDrift);
|
|
286
374
|
}
|
|
375
|
+
const single = [{ name: path.basename(files[1]).replace(/\.json$/, ''), report, isLayout: false }];
|
|
376
|
+
const singleAgg = aggregateReports(single);
|
|
287
377
|
if (reportPath) {
|
|
288
|
-
|
|
289
|
-
writeDriftReport(reportPath, aggregateReports(single), single, files[0], files[1], usage, open);
|
|
378
|
+
writeDriftReport(reportPath, singleAgg, single, files[0], files[1], usage, open);
|
|
290
379
|
}
|
|
380
|
+
if (junitPath) {
|
|
381
|
+
(0, emit_1.writeJUnit)(junitPath, singleAgg, single, {
|
|
382
|
+
baseUrl: (0, config_1.configuredBaseUrl)(),
|
|
383
|
+
baselineName: path.basename(files[0]),
|
|
384
|
+
currentName: path.basename(files[1]),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
(0, emit_1.recordDriftRun)('diff', singleAgg, { baseUrl: (0, config_1.configuredBaseUrl)() });
|
|
291
388
|
const failing = report.verdict === 'BLOCK' || (strict && report.verdict === 'FIX');
|
|
292
389
|
process.exitCode = failing ? 1 : 0;
|
|
293
390
|
if (!json)
|
|
@@ -388,6 +485,19 @@ async function runDirDiff(baselineDir, currentDir, opts) {
|
|
|
388
485
|
if (opts.reportPath) {
|
|
389
486
|
writeDriftReport(opts.reportPath, aggregate, reports, baselineDir, currentDir, usage, opts.open);
|
|
390
487
|
}
|
|
488
|
+
if (opts.junitPath) {
|
|
489
|
+
(0, emit_1.writeJUnit)(opts.junitPath, aggregate, reports, {
|
|
490
|
+
baseUrl: (0, config_1.configuredBaseUrl)(),
|
|
491
|
+
baselineName: path.basename(baselineDir),
|
|
492
|
+
currentName: path.basename(currentDir),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
(0, emit_1.recordDriftRun)('diff', aggregate, {
|
|
496
|
+
currentDir,
|
|
497
|
+
reports,
|
|
498
|
+
stale,
|
|
499
|
+
baseUrl: (0, config_1.configuredBaseUrl)(),
|
|
500
|
+
});
|
|
391
501
|
const failing = aggregate.verdict === 'BLOCK' || (opts.strict && aggregate.verdict === 'FIX');
|
|
392
502
|
process.exitCode = failing ? 1 : 0;
|
|
393
503
|
if (!opts.json)
|