@ia-qa/self-healing 0.5.1 → 0.7.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.
- package/README.md +53 -19
- package/TUTORIAL.md +40 -12
- package/dist/aom.d.ts +24 -0
- package/dist/aom.js +23 -0
- package/dist/aom.js.map +1 -1
- package/dist/cli/baseline.d.ts +21 -0
- package/dist/cli/baseline.js +113 -0
- package/dist/cli/baseline.js.map +1 -0
- package/dist/cli/diff.d.ts +6 -4
- package/dist/cli/diff.js +80 -7
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +0 -7
- package/dist/cli/init.js +18 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/map.d.ts +51 -0
- package/dist/cli/map.js +162 -0
- package/dist/cli/map.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.js +40 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/mcp/server.js +19 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/overview.js +19 -2
- package/dist/overview.js.map +1 -1
- package/dist/playwright/healer.d.ts +1 -0
- package/dist/playwright/healer.js +16 -7
- package/dist/playwright/healer.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,23 @@ Self-healing E2E toolkit: a **local MCP server** for AI agents, a guided **CLI**
|
|
|
6
6
|
|
|
7
7
|
**It runs entirely on the end user's machine.** It brings its own headless Chromium, so it sees `localhost`, staging, or an app behind a VPN — and **no data ever leaves the machine**. Nothing is hosted.
|
|
8
8
|
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y -p @ia-qa/self-healing ia-qa-heal init # wizard → .ia-qa/config.json
|
|
13
|
+
npx -y -p @ia-qa/self-healing ia-qa-heal map # → .ia-qa/mapping/<page>.json + .md
|
|
14
|
+
npx -y -p @ia-qa/self-healing ia-qa-heal baseline # → .ia-qa/baseline/ — the reference. Commit it.
|
|
15
|
+
# …change the app, then `map` again…
|
|
16
|
+
npx -y -p @ia-qa/self-healing ia-qa-heal diff # baseline/ vs mapping/ → PASS / FIX / BLOCK
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Install it in the project that holds your tests to drop the `-p` dance — and to get the `aiClick` / `aiFill` runtime helpers, which are imported, not run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i -D @ia-qa/self-healing
|
|
23
|
+
npx ia-qa-heal init
|
|
24
|
+
```
|
|
25
|
+
|
|
9
26
|
## Two front doors, one engine
|
|
10
27
|
|
|
11
28
|
| | For | Entry point |
|
|
@@ -24,8 +41,8 @@ Agent config (Claude Code / Desktop, Cursor…):
|
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
```
|
|
27
|
-
`-p` is required, not cosmetic: the package ships **two** bins (`ia-qa-heal`, `ia-qa-heal-mcp`), so `npx @ia-qa/self-healing …` cannot resolve which to run and silently starts nothing.
|
|
28
|
-
The MCP server is a dependency-free JSON-RPC 2.0 stdio implementation (`src/mcp/server.ts`) — no SDK
|
|
44
|
+
`-p` is required, not cosmetic: the package ships **two** bins (`ia-qa-heal`, `ia-qa-heal-mcp`), so `npx @ia-qa/self-healing …` cannot resolve which to run and silently starts nothing.
|
|
45
|
+
The MCP server is a dependency-free JSON-RPC 2.0 stdio implementation (`src/mcp/server.ts`) — no SDK. It does **not** require the separate Playwright MCP: it drives its own browser. stdout is the protocol channel; logs go to stderr.
|
|
29
46
|
|
|
30
47
|
### Browser reuse — no second Chromium (`src/launcher.ts`)
|
|
31
48
|
Playwright's browser cache is **machine-wide and shared** (`%LOCALAPPDATA%\ms-playwright`, `~/.cache/ms-playwright`). If the user already has *any* Playwright install — their own tests or the Playwright MCP — the bundled Chromium is already there and we reuse it for free. There is no "second Chromium" to avoid; the only real cost is **revision skew** (a different Playwright version pins a different Chromium revision). For that case, and for machines that forbid the download, the launcher resolves in this order:
|
|
@@ -53,32 +70,30 @@ Only if all four fail does it error, listing the three fixes. `map_app` exposes
|
|
|
53
70
|
```
|
|
54
71
|
The `<page>.md` groups elements by role and carries a self-describing header telling a model exactly how to use it. **The CI auto-heal loop:** when a UI test fails, hand the failing test file + the current `<page>.md` to an LLM — it finds the element by role + accessible name, reads the current selector, and rewrites the test in **whatever framework and language** it is written in (Cypress, Playwright, Selenium; JS or Python). The contract is the source of truth; the model handles the syntax. The same contract is downloadable from the zero-install **Selector Drift Detector** page on ia-qa.com.
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
When pages link to each other, `map` writes the **navigation graph** next to it, built from link `href`s: **`_navigation.md`** (a Mermaid fence — GitHub, GitLab and the VS Code preview render it natively) and **`_navigation.svg`** (the same graph, hand-rolled, opening in any browser offline — no mermaid.js, no CDN, nothing fetched).
|
|
73
|
+
Alongside the page contracts, `map` rebuilds the app index and the navigation graph — see [Map artifacts](#map-artifacts).
|
|
74
|
+
3. **`ia-qa-heal baseline`** — promote `.ia-qa/mapping/` to `.ia-qa/baseline/`, the reference `diff` compares against. Commit it: a reference that is not in git cannot be reviewed or reverted, and accepting drift is a code-review decision like any other.
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
- **shell links** (navbar, footer) — in the overview, listed once, instead of an edge from every page to every navbar destination;
|
|
62
|
-
- **isolated pages** — a box with no arrow;
|
|
63
|
-
- **hub fans** — a page whose 144 spokes link nowhere else is stated in one sentence (`home` fans out to 144 pages) instead of drawn as 144 identical arrows crossing every other line. That is the catalogue shape, and no node-link diagram survives it.
|
|
76
|
+
Drift is a claim about **two moments**, and `map` only ever captures one. Promotion is the second half, and it is a separate verb on purpose — deliberately *not* a flag on `map`, because promoting in the same breath as capturing would make the reference incapable of ever being older than what it is compared to. `map` records what the app **is**; `baseline` says it is also what the app is **supposed to be**. That second claim is a human judgement.
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
Skip it and the natural workaround is `cp current.json baseline.json && diff` — which compares a capture to a copy of itself and prints `PASS ✅ No drift`. That verdict is green because nothing was compared, not because nothing changed. `diff` now refuses it outright (exit `2`): every side carries a `capturedAt`, equal timestamps mean one `map` run, and one run cannot drift from itself. A gate that reports green without looking is worse than no gate, because it is trusted.
|
|
79
|
+
4. **`ia-qa-heal diff [before.json] [after.json]`** — CI-gate verdict. With no arguments, diffs `baseline/` against `mapping/` — the everyday case. Diffs two mappings (from `map` or the ia-qa.com browser snippet) and reports which selectors survived, which the runtime healer would recover, and which will hard-fail:
|
|
67
80
|
```
|
|
68
81
|
⛔ BLOCK
|
|
69
82
|
before.json → after.json · 3 ok · 2 healable · 1 lost · 2 new
|
|
70
83
|
🔧 healable button "Se connecter" button#login-btn → button.btn-primary (exact name)
|
|
71
84
|
⛔ lost button "Supprimer le compte" button#delete → (no match)
|
|
72
85
|
```
|
|
73
|
-
Exit codes for pipelines: `0` = PASS or FIX, `1` = BLOCK — lost, ambiguous, or rebound (add `--strict` to fail on FIX too), `2` = bad input. Use `--json` for machine-readable output. Same diff engine as the web tool (`src/browser/match.js`).
|
|
86
|
+
Exit codes for pipelines: `0` = PASS or FIX, `1` = BLOCK — lost, ambiguous, or rebound (add `--strict` to fail on FIX too), `2` = bad input, no baseline, or nothing to compare. Use `--json` for machine-readable output. Same diff engine as the web tool (`src/browser/match.js`).
|
|
87
|
+
|
|
88
|
+
A contract that was **not re-mapped** since the baseline carries the baseline's own `capturedAt`, so it cannot show drift. Those are listed and left **out** of the verdict rather than counted as `ok` — padding a gate with comparisons that never happened is the same lie in smaller print. `--json` reports them as `staleExcluded`.
|
|
74
89
|
|
|
75
90
|
Every element carries a **context** (nearest landmark + section heading) so two elements with the same role + accessible name (five "Delete" buttons) stay distinguishable. The diff classifies each baseline element as `ok`, `renamed` (same element, new accessible name — a *content* drift to eyeball), `healable` (deterministic old→new rewrite), `ambiguous` (several equal matches — the tool **refuses to guess**, a human decides), or `lost` (no match).
|
|
76
91
|
|
|
77
92
|
Identity comes from the contract (role + accessible name), never from the selector string. A selector that still resolves only proves the *position* is still occupied: insert one button above a tab bar and every `nth-of-type` below it silently slides onto its neighbour. Any row where that happened is flagged `rebound` — the old selector resolves, but to a **different element**, so the suite is green while clicking the wrong thing. Rebound rows are rewritten like any other healable row, and they **BLOCK**: a `FIX` verdict exits `0`, which is exactly how this ships unnoticed.
|
|
78
|
-
|
|
93
|
+
5. **`ia-qa-heal fix <before.json> <after.json> <test-paths…>`** — the deterministic half. Applies **only** the `healable` rewrites to your test files, replacing the selector only where it appears as a quoted string literal (`'…'`/`"…"`/`` `…` ``, quote-safe for attribute selectors), across any framework/language. Every rewrite is applied in a single pass against the selector each occurrence had in the file, so a *permuted* set (`nth-of-type(1)→(2)`, `(2)→(3)`) can never chain and drag `(1)` to `(3)`. LOST / AMBIGUOUS / RENAMED are printed but never touched — they need judgment. It **edits the working tree and never commits**: review with `git diff`, then push or discard. `--dry-run` previews the plan. This is "prepare the ground, the human decides" as a command.
|
|
79
94
|
|
|
80
95
|
**`--locators`** rewrites to a Playwright role locator — `page.getByRole('button', { name: 'Validator' })` — instead of a selector, wherever role + accessible name identifies exactly one element. A repaired positional selector is only correct until the next insertion; a role locator is what the contract already means, so it does not drift at all. Applies to `.js`/`.ts` files and Playwright call shapes (`page.click('…')`, `page.locator('…')`, `page.fill('…', v)`, …); every other file and every element without an unambiguous role + name keeps the plain selector rewrite. `getByRole` is strict, so a locator is emitted only when it provably resolves to one element — otherwise the tool would trade a silent wrong click for a loud crash.
|
|
81
|
-
|
|
96
|
+
6. **Runtime healing** — in your Playwright tests:
|
|
82
97
|
```ts
|
|
83
98
|
import { aiClick, aiFill } from '@ia-qa/self-healing';
|
|
84
99
|
|
|
@@ -87,6 +102,21 @@ Only if all four fail does it error, listing the three fixes. `map_app` exposes
|
|
|
87
102
|
```
|
|
88
103
|
If the selector fails, the helper re-scans the live page, matches the element by role + accessible name (exact, then fuzzy Dice ≥ 0.6 — or your own `llmResolver`), retries on the healed selector, and logs a warning telling you to update the test and re-map.
|
|
89
104
|
|
|
105
|
+
## Map artifacts
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
**`.ia-qa/mapping/_overview.md`** — the app index, and the file to open first. Every page contract answers "what is on this page?"; none of them answers "what is this app, and which contract do I need?". The overview does: every page with its URL, element count and a link to its contract; the shared layout; and the surface by role. It also surfaces two things no single contract can — same-origin destinations your app links to but your config never maps (the coverage gap), and the external domains you link out to.
|
|
110
|
+
|
|
111
|
+
**`_navigation.md` / `_navigation.svg`** — the navigation graph, written when pages link to each other, built from link `href`s. The `.md` is a Mermaid fence (GitHub, GitLab and the VS Code preview render it natively); the `.svg` is the same graph, hand-rolled, opening in any browser offline — no mermaid.js, no CDN, nothing fetched.
|
|
112
|
+
|
|
113
|
+
The graph draws **what a page table cannot tell you: pages that link to each other**. Three things are deliberately left out, on one principle — a picture should not spend ink on what the page list already says:
|
|
114
|
+
- **shell links** (navbar, footer) — in the overview, listed once, instead of an edge from every page to every navbar destination;
|
|
115
|
+
- **isolated pages** — a box with no arrow;
|
|
116
|
+
- **hub fans** — a page whose spokes link nowhere else is stated in one sentence (`home` fans out to 144 pages) instead of drawn as 144 identical arrows crossing every other line. That is the catalogue shape, and no node-link diagram survives it.
|
|
117
|
+
|
|
118
|
+
What is left is the real structure: a checkout tunnel stays, its noise does not. If nothing survives, both files are removed rather than left stale — for that app, the page list genuinely is the whole story.
|
|
119
|
+
|
|
90
120
|
## Design notes
|
|
91
121
|
|
|
92
122
|
- `page.accessibility.snapshot()` is deprecated **and returns no selectors**, so mapping walks the DOM directly and computes ARIA role + accname + a stable selector (`data-testid` → `id` → `name` → short structural path).
|
|
@@ -94,7 +124,15 @@ Only if all four fail does it error, listing the three fixes. `map_app` exposes
|
|
|
94
124
|
- Secrets: never written to disk. `env` reads `process.env`; `aws-ssm` calls `GetParameter` with decryption using the AWS SDK default credential chain (needs `ssm:GetParameter`, plus `kms:Decrypt` for SecureString).
|
|
95
125
|
- `llmResolverStub` in `src/playwright/healer.ts` is the extension point for a real LLM/embedding-based resolver (e.g. via `POST https://www.ia-qa.com/mcp/call`).
|
|
96
126
|
|
|
97
|
-
##
|
|
127
|
+
## Peer dependencies
|
|
128
|
+
|
|
129
|
+
`playwright` is a peer dependency — the consuming test project provides it (`npx playwright install chromium` if needed). This is deliberate: bundling our own copy would let a project on a different Playwright version end up with two installs and a browser-revision skew, which is exactly what the shared launcher exists to avoid.
|
|
130
|
+
|
|
131
|
+
`@aws-sdk/client-ssm` is an **optional** peer, needed only by the `aws-ssm` secret source. It is ~5 MB and imported lazily, so it is not a tax on installs that use the `env` source: `npm i @aws-sdk/client-ssm` if you want SSM, and the CLI tells you so if you pick it without.
|
|
132
|
+
|
|
133
|
+
## Build from source
|
|
134
|
+
|
|
135
|
+
For work on the package itself — installing from npm needs none of this.
|
|
98
136
|
|
|
99
137
|
```bash
|
|
100
138
|
cd packages/self-healing
|
|
@@ -102,7 +140,3 @@ npm run build # tsc → dist/
|
|
|
102
140
|
node dist/cli/index.js init
|
|
103
141
|
node dist/cli/index.js map
|
|
104
142
|
```
|
|
105
|
-
|
|
106
|
-
`playwright` is a peer dependency — the consuming test project provides it (`npx playwright install chromium` if needed). This is deliberate: bundling our own copy would let a project on a different Playwright version end up with two installs and a browser-revision skew, which is exactly what the shared launcher exists to avoid.
|
|
107
|
-
|
|
108
|
-
`@aws-sdk/client-ssm` is an **optional** peer, needed only by the `aws-ssm` secret source. It is ~5 MB and imported lazily, so it is not a tax on installs that use the `env` source: `npm i @aws-sdk/client-ssm` if you want SSM, and the CLI tells you so if you pick it without.
|
package/TUTORIAL.md
CHANGED
|
@@ -165,10 +165,12 @@ The agent launches a browser in the background and gives you back something like
|
|
|
165
165
|
| Valider | main › Paiement | `button[data-testid="pay"]` |
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
**That's
|
|
168
|
+
**That's a capture of what the app is right now.** To make it the reference everything is
|
|
169
|
+
measured against, promote it — then commit both, they live in git like code:
|
|
169
170
|
|
|
170
171
|
```bash
|
|
171
|
-
|
|
172
|
+
npx ia-qa-heal baseline # .ia-qa/mapping → .ia-qa/baseline
|
|
173
|
+
git add .ia-qa/mapping .ia-qa/baseline
|
|
172
174
|
git commit -m "Add page contract baseline"
|
|
173
175
|
```
|
|
174
176
|
|
|
@@ -238,13 +240,28 @@ Commit them.
|
|
|
238
240
|
Here's the loop you'll actually live in.
|
|
239
241
|
|
|
240
242
|
### Step 6.1 — You have a baseline
|
|
241
|
-
From step 4.4 or 5.3
|
|
243
|
+
From step 4.4 or 5.3: promoted with `ia-qa-heal baseline` and committed to git.
|
|
242
244
|
|
|
243
245
|
```bash
|
|
244
|
-
|
|
245
|
-
git add .ia-qa/mapping && git commit -m "baseline"
|
|
246
|
+
npx ia-qa-heal baseline
|
|
247
|
+
git add .ia-qa/mapping .ia-qa/baseline && git commit -m "baseline"
|
|
246
248
|
```
|
|
247
249
|
|
|
250
|
+
Promotion is its own step because **drift is a claim about two moments**, and `map` only
|
|
251
|
+
ever captures one. `map` records what the app *is*; `baseline` says it is also what the app
|
|
252
|
+
is *supposed to be*. That second claim is yours to make, so it gets its own command.
|
|
253
|
+
|
|
254
|
+
Do it now and `diff` will refuse — correctly. Nothing has been re-mapped, so both sides are
|
|
255
|
+
the same capture, and a capture cannot drift from itself:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
❌ Nothing to compare: all 2 pairs are the same capture on both sides.
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
That refusal *is* the feature. The obvious shortcut — copy the file, diff against the copy —
|
|
262
|
+
prints `PASS ✅ No drift — your suite is safe to run`, which sounds like an answer and is
|
|
263
|
+
not one: nothing was compared. A green gate that never looked is worse than no gate.
|
|
264
|
+
|
|
248
265
|
### Step 6.2 — Someone changes the UI
|
|
249
266
|
A developer ships a redesign. Your tests start failing. **Normal.**
|
|
250
267
|
|
|
@@ -256,18 +273,22 @@ Terminal: `npx ia-qa-heal map checkout`
|
|
|
256
273
|
Agent: **"Diff the baseline against the new contract"**
|
|
257
274
|
Terminal:
|
|
258
275
|
```bash
|
|
259
|
-
npx ia-qa-heal diff
|
|
276
|
+
npx ia-qa-heal diff # baseline/ against mapping/ — no arguments needed
|
|
260
277
|
```
|
|
261
278
|
|
|
279
|
+
Now there are two moments, so there is a verdict. Any page you did **not** re-map still
|
|
280
|
+
carries the baseline's own timestamp; it is listed and left out of the verdict rather than
|
|
281
|
+
counted as `ok`, because it was never compared either.
|
|
282
|
+
|
|
262
283
|
### Step 6.5 — Apply the safe fixes
|
|
263
284
|
Agent: **"Fix my tests in the tests/ folder using that diff"**
|
|
264
285
|
Terminal:
|
|
265
286
|
```bash
|
|
266
287
|
# Preview first — nothing is written
|
|
267
|
-
npx ia-qa-heal fix .ia-qa/
|
|
288
|
+
npx ia-qa-heal fix .ia-qa/baseline/checkout.json .ia-qa/mapping/checkout.json tests/ --dry-run
|
|
268
289
|
|
|
269
290
|
# Do it
|
|
270
|
-
npx ia-qa-heal fix .ia-qa/
|
|
291
|
+
npx ia-qa-heal fix .ia-qa/baseline/checkout.json .ia-qa/mapping/checkout.json tests/
|
|
271
292
|
```
|
|
272
293
|
|
|
273
294
|
### Step 6.5b — Stop fixing the same test twice (`--locators`)
|
|
@@ -351,9 +372,14 @@ jobs:
|
|
|
351
372
|
run: npx ia-qa-heal map
|
|
352
373
|
|
|
353
374
|
- name: Fail if selectors drifted with no safe fix
|
|
354
|
-
run: npx ia-qa-heal diff
|
|
375
|
+
run: npx ia-qa-heal diff
|
|
355
376
|
```
|
|
356
377
|
|
|
378
|
+
`diff` with no arguments compares the committed `.ia-qa/baseline/` against the `.ia-qa/mapping/`
|
|
379
|
+
the previous step just re-captured — which is why the baseline has to be in git for CI to have
|
|
380
|
+
anything to check out. It exits `2` if there is no baseline, or if nothing was re-mapped, so a
|
|
381
|
+
misconfigured job fails loudly instead of passing without comparing anything.
|
|
382
|
+
|
|
357
383
|
Add `--strict` if you want **FIX** to fail the build too (forces tests to be updated, not just healed).
|
|
358
384
|
|
|
359
385
|
---
|
|
@@ -419,9 +445,11 @@ Yes, any URL your machine can reach.
|
|
|
419
445
|
```bash
|
|
420
446
|
npx ia-qa-heal init # one-time setup
|
|
421
447
|
npx ia-qa-heal map # scan → *.json + *.md + _overview.md + _navigation.md/.svg
|
|
422
|
-
npx ia-qa-heal
|
|
423
|
-
|
|
424
|
-
npx ia-qa-heal
|
|
448
|
+
npx ia-qa-heal baseline # promote mapping/ → baseline/ (commit it)
|
|
449
|
+
# …change the app, then `map` again…
|
|
450
|
+
npx ia-qa-heal diff # verdict: PASS / FIX / BLOCK (baseline/ vs mapping/)
|
|
451
|
+
npx ia-qa-heal fix .ia-qa/baseline/checkout.json .ia-qa/mapping/checkout.json tests/ --dry-run # preview
|
|
452
|
+
npx ia-qa-heal fix .ia-qa/baseline/checkout.json .ia-qa/mapping/checkout.json tests/ # apply (never commits)
|
|
425
453
|
```
|
|
426
454
|
|
|
427
455
|
Talking to an agent instead:
|
package/dist/aom.d.ts
CHANGED
|
@@ -23,6 +23,30 @@ export interface PageMapping {
|
|
|
23
23
|
capturedAt: string;
|
|
24
24
|
elements: MappedElement[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Did these two mappings come out of the same `map` run?
|
|
28
|
+
*
|
|
29
|
+
* `capturedAt` is stamped once per page per run, so equality means one capture is being
|
|
30
|
+
* compared to a copy of itself. That is what a hand-rolled baseline (`cp current.json
|
|
31
|
+
* baseline.json && diff`) always produces on its first run, and the diff of a file against
|
|
32
|
+
* itself is `PASS · N ok · 0 healable` — printed under "✅ No drift — your suite is safe to
|
|
33
|
+
* run". Every word of that is false: nothing was compared, so nothing is known about the
|
|
34
|
+
* suite. A green gate that never looked is worse than no gate, because it is trusted.
|
|
35
|
+
*
|
|
36
|
+
* Two real `map` runs collide only if stamped on the same millisecond, which does not
|
|
37
|
+
* happen across the app changes a drift check exists to find. A *missing* timestamp is
|
|
38
|
+
* malformed input, not a self-comparison: two `undefined`s share nothing meaningful, and
|
|
39
|
+
* blocking on them would refuse a legitimate diff over a field the diff never reads.
|
|
40
|
+
*
|
|
41
|
+
* Lives here rather than beside either caller because both doors into the diff — the CLI
|
|
42
|
+
* and the MCP server — have to agree on what is not a comparison. One guarding and the
|
|
43
|
+
* other not would just move the false green to whichever door the user knocked on.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isSameCapture(baseline: {
|
|
46
|
+
capturedAt?: string;
|
|
47
|
+
} | null | undefined, current: {
|
|
48
|
+
capturedAt?: string;
|
|
49
|
+
} | null | undefined): boolean;
|
|
26
50
|
export declare function extractInteractiveElements(page: Page): Promise<MappedElement[]>;
|
|
27
51
|
export declare function saveMapping(mapping: PageMapping, dir?: string): string;
|
|
28
52
|
export declare function loadMapping(pageName: string, dir?: string): PageMapping;
|
package/dist/aom.js
CHANGED
|
@@ -33,12 +33,35 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isSameCapture = isSameCapture;
|
|
36
37
|
exports.extractInteractiveElements = extractInteractiveElements;
|
|
37
38
|
exports.saveMapping = saveMapping;
|
|
38
39
|
exports.loadMapping = loadMapping;
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
40
41
|
const config_1 = require("./config");
|
|
41
42
|
const extract_1 = require("./browser/extract");
|
|
43
|
+
/**
|
|
44
|
+
* Did these two mappings come out of the same `map` run?
|
|
45
|
+
*
|
|
46
|
+
* `capturedAt` is stamped once per page per run, so equality means one capture is being
|
|
47
|
+
* compared to a copy of itself. That is what a hand-rolled baseline (`cp current.json
|
|
48
|
+
* baseline.json && diff`) always produces on its first run, and the diff of a file against
|
|
49
|
+
* itself is `PASS · N ok · 0 healable` — printed under "✅ No drift — your suite is safe to
|
|
50
|
+
* run". Every word of that is false: nothing was compared, so nothing is known about the
|
|
51
|
+
* suite. A green gate that never looked is worse than no gate, because it is trusted.
|
|
52
|
+
*
|
|
53
|
+
* Two real `map` runs collide only if stamped on the same millisecond, which does not
|
|
54
|
+
* happen across the app changes a drift check exists to find. A *missing* timestamp is
|
|
55
|
+
* malformed input, not a self-comparison: two `undefined`s share nothing meaningful, and
|
|
56
|
+
* blocking on them would refuse a legitimate diff over a field the diff never reads.
|
|
57
|
+
*
|
|
58
|
+
* Lives here rather than beside either caller because both doors into the diff — the CLI
|
|
59
|
+
* and the MCP server — have to agree on what is not a comparison. One guarding and the
|
|
60
|
+
* other not would just move the false green to whichever door the user knocked on.
|
|
61
|
+
*/
|
|
62
|
+
function isSameCapture(baseline, current) {
|
|
63
|
+
return Boolean(baseline?.capturedAt) && baseline.capturedAt === current?.capturedAt;
|
|
64
|
+
}
|
|
42
65
|
async function extractInteractiveElements(page) {
|
|
43
66
|
return page.evaluate(extract_1.extractInPage);
|
|
44
67
|
}
|
package/dist/aom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,sCAKC;AAED,gEAEC;AAED,kCAKC;AAED,kCAQC;AA7ED,uCAAyB;AAEzB,qCAAmD;AACnD,+CAAkD;AA6BlD;;;;;;;;;;;;;;;;;;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,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IACxE,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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ia-qa-heal baseline` — promote the current mappings to the reference `diff` compares against.
|
|
3
|
+
*
|
|
4
|
+
* Drift is a claim about two moments, so it needs a promoted "before". Without a command for
|
|
5
|
+
* it, everyone invents one, and the obvious invention — copy the current mapping, then diff
|
|
6
|
+
* against the copy — compares a capture to itself and reports `PASS ✅ No drift`. The
|
|
7
|
+
* verdict is green because nothing was compared, which is the one failure mode a CI gate
|
|
8
|
+
* must never have. So the promotion is a command, and `diff` refuses a verdict when it
|
|
9
|
+
* catches the copy anyway (see `assertComparable` in `diff.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not a flag on `map`: promoting in the same breath as capturing would rebuild
|
|
12
|
+
* the trap, since the reference could then never be older than what it is compared to. The
|
|
13
|
+
* separation *is* the feature — `map` records what the app is now, `baseline` says it is
|
|
14
|
+
* also what the app is supposed to be. That second claim is a human judgement, and it gets
|
|
15
|
+
* its own verb.
|
|
16
|
+
*
|
|
17
|
+
* Copies `.json` only. `diff` reads nothing else, and the markdown contracts in `mapping/`
|
|
18
|
+
* already describe the app as it is today — a frozen second copy of them would be prose that
|
|
19
|
+
* silently goes stale, and twice the diff to review on every promotion.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runBaseline(args?: string[]): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runBaseline = runBaseline;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const config_1 = require("../config");
|
|
40
|
+
/**
|
|
41
|
+
* `ia-qa-heal baseline` — promote the current mappings to the reference `diff` compares against.
|
|
42
|
+
*
|
|
43
|
+
* Drift is a claim about two moments, so it needs a promoted "before". Without a command for
|
|
44
|
+
* it, everyone invents one, and the obvious invention — copy the current mapping, then diff
|
|
45
|
+
* against the copy — compares a capture to itself and reports `PASS ✅ No drift`. The
|
|
46
|
+
* verdict is green because nothing was compared, which is the one failure mode a CI gate
|
|
47
|
+
* must never have. So the promotion is a command, and `diff` refuses a verdict when it
|
|
48
|
+
* catches the copy anyway (see `assertComparable` in `diff.ts`).
|
|
49
|
+
*
|
|
50
|
+
* Deliberately not a flag on `map`: promoting in the same breath as capturing would rebuild
|
|
51
|
+
* the trap, since the reference could then never be older than what it is compared to. The
|
|
52
|
+
* separation *is* the feature — `map` records what the app is now, `baseline` says it is
|
|
53
|
+
* also what the app is supposed to be. That second claim is a human judgement, and it gets
|
|
54
|
+
* its own verb.
|
|
55
|
+
*
|
|
56
|
+
* Copies `.json` only. `diff` reads nothing else, and the markdown contracts in `mapping/`
|
|
57
|
+
* already describe the app as it is today — a frozen second copy of them would be prose that
|
|
58
|
+
* silently goes stale, and twice the diff to review on every promotion.
|
|
59
|
+
*/
|
|
60
|
+
async function runBaseline(args = []) {
|
|
61
|
+
const from = (0, config_1.mappingDir)();
|
|
62
|
+
const to = (0, config_1.baselineDir)();
|
|
63
|
+
if (!fs.existsSync(from)) {
|
|
64
|
+
console.error(`❌ No mappings found in ${rel(from)} — run \`ia-qa-heal map\` first.`);
|
|
65
|
+
process.exitCode = 2;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const pages = jsonFilesIn(from);
|
|
69
|
+
const layouts = jsonFilesIn(path.join(from, config_1.LAYOUT_DIRNAME));
|
|
70
|
+
if (pages.length === 0 && layouts.length === 0) {
|
|
71
|
+
console.error(`❌ ${rel(from)} holds no mapping files — run \`ia-qa-heal map\` first.`);
|
|
72
|
+
process.exitCode = 2;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const replaced = jsonFilesIn(to).length + jsonFilesIn(path.join(to, config_1.LAYOUT_DIRNAME)).length;
|
|
76
|
+
fs.mkdirSync(to, { recursive: true });
|
|
77
|
+
for (const file of pages) {
|
|
78
|
+
fs.copyFileSync(path.join(from, file), path.join(to, file));
|
|
79
|
+
}
|
|
80
|
+
if (layouts.length > 0) {
|
|
81
|
+
fs.mkdirSync(path.join(to, config_1.LAYOUT_DIRNAME), { recursive: true });
|
|
82
|
+
for (const file of layouts) {
|
|
83
|
+
fs.copyFileSync(path.join(from, config_1.LAYOUT_DIRNAME, file), path.join(to, config_1.LAYOUT_DIRNAME, file));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const total = pages.length + layouts.length;
|
|
87
|
+
console.log(`\n📌 Baseline promoted → ${rel(to)}`);
|
|
88
|
+
console.log(` ${pages.length} ${pages.length === 1 ? 'page' : 'pages'}` +
|
|
89
|
+
(layouts.length > 0 ? ` · ${layouts.length} ${layouts.length === 1 ? 'layout' : 'layouts'}` : '') +
|
|
90
|
+
(replaced > 0 ? ` (replaced a baseline of ${replaced} ${replaced === 1 ? 'file' : 'files'})` : ''));
|
|
91
|
+
// Promoting over a reference that had drift in it accepts that drift as correct, silently
|
|
92
|
+
// and irreversibly outside of git. Saying so is the difference between "I meant that" and
|
|
93
|
+
// "I did not know that was what the command did".
|
|
94
|
+
if (replaced > 0) {
|
|
95
|
+
console.log(` Any drift against the old baseline is now the expected state.`);
|
|
96
|
+
}
|
|
97
|
+
console.log(`\n Commit it — a reference that is not in git cannot be reviewed or reverted:`);
|
|
98
|
+
console.log(` git add ${rel(to).replace(/\\/g, '/')} && git commit -m "baseline: ${total} contracts"`);
|
|
99
|
+
console.log(`\n Then change the app, \`ia-qa-heal map\` again, and \`ia-qa-heal diff\` has two`);
|
|
100
|
+
console.log(` different moments to compare.\n`);
|
|
101
|
+
if (args.length > 0) {
|
|
102
|
+
console.log(` (ignored: ${args.join(' ')} — \`baseline\` takes no arguments)\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function jsonFilesIn(dir) {
|
|
106
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
107
|
+
return [];
|
|
108
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('.'));
|
|
109
|
+
}
|
|
110
|
+
function rel(p) {
|
|
111
|
+
return path.relative(process.cwd(), p) || p;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/cli/baseline.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,kCAsDC;AA9ED,uCAAyB;AACzB,2CAA6B;AAC7B,sCAAoE;AAEpE;;;;;;;;;;;;;;;;;;;GAmBG;AACI,KAAK,UAAU,WAAW,CAAC,OAAiB,EAAE;IACnD,MAAM,IAAI,GAAG,IAAA,mBAAU,GAAE,CAAC;IAC1B,MAAM,EAAE,GAAG,IAAA,oBAAW,GAAE,CAAC;IAEzB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,0BAA0B,GAAG,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACrF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,uBAAc,CAAC,CAAC,CAAC;IAE7D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;QACvF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,uBAAc,CAAC,CAAC,CAAC,MAAM,CAAC;IAE5F,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,uBAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,uBAAc,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,uBAAc,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,4BAA4B,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CACT,MAAM,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE;QAC3D,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,6BAA6B,QAAQ,IAAI,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CACtG,CAAC;IAEF,0FAA0F;IAC1F,0FAA0F;IAC1F,kDAAkD;IAClD,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,kEAAkE,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,iFAAiF,CAAC,CAAC;IAC/F,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,gCAAgC,KAAK,aAAa,CAAC,CAAC;IAC3G,OAAO,CAAC,GAAG,CAAC,qFAAqF,CAAC,CAAC;IACnG,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;IAElD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACrF,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE;QAAE,OAAO,EAAE,CAAC;IACtE,OAAO,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/cli/diff.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ia-qa-heal diff
|
|
2
|
+
* `ia-qa-heal diff [baseline] [current] [--strict] [--json] [--dir]`
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* no arguments — the everyday case: the promoted `baseline/` against what `map` just
|
|
6
|
+
* wrote to `mapping/`. Implies `--dir`.
|
|
7
|
+
* file mode — diffs two individual mapping files.
|
|
6
8
|
* directory mode (`--dir`) — both args are directories of mapping files.
|
|
7
9
|
* Finds matching pairs by filename, diffs each, and produces an aggregate
|
|
8
10
|
* verdict. Layouts in `_layouts/` are diffed first (once instead of N
|
|
@@ -11,6 +13,6 @@
|
|
|
11
13
|
* Exit codes (so a pipeline can gate on it):
|
|
12
14
|
* 0 — PASS, or FIX without `--strict`
|
|
13
15
|
* 1 — BLOCK (a selector broke with no semantic match), or FIX with `--strict`
|
|
14
|
-
* 2 — bad usage / unreadable input
|
|
16
|
+
* 2 — bad usage / unreadable input / nothing to compare
|
|
15
17
|
*/
|
|
16
18
|
export declare function runDiff(args: string[]): Promise<void>;
|