@ia-qa/qa-discovery 0.1.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 +378 -0
- package/ROADMAP.md +321 -0
- package/dist/ai/classify.d.ts +170 -0
- package/dist/ai/classify.js +431 -0
- package/dist/ai/classify.js.map +1 -0
- package/dist/browser/extractPage.d.ts +41 -0
- package/dist/browser/extractPage.js +427 -0
- package/dist/browser/extractPage.js.map +1 -0
- package/dist/browser/openables.d.ts +19 -0
- package/dist/browser/openables.js +127 -0
- package/dist/browser/openables.js.map +1 -0
- package/dist/capture/page.d.ts +128 -0
- package/dist/capture/page.js +78 -0
- package/dist/capture/page.js.map +1 -0
- package/dist/citations.d.ts +80 -0
- package/dist/citations.js +197 -0
- package/dist/citations.js.map +1 -0
- package/dist/classificationView.d.ts +118 -0
- package/dist/classificationView.js +178 -0
- package/dist/classificationView.js.map +1 -0
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +38 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/history.d.ts +1 -0
- package/dist/cli/history.js +35 -0
- package/dist/cli/history.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +172 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/login.d.ts +29 -0
- package/dist/cli/login.js +179 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/scan.d.ts +11 -0
- package/dist/cli/scan.js +246 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli-ai/index.d.ts +7 -0
- package/dist/cli-ai/index.js +360 -0
- package/dist/cli-ai/index.js.map +1 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.js +178 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +43 -0
- package/dist/diff.js.map +1 -0
- package/dist/explore.d.ts +59 -0
- package/dist/explore.js +119 -0
- package/dist/explore.js.map +1 -0
- package/dist/healingLink.d.ts +30 -0
- package/dist/healingLink.js +127 -0
- package/dist/healingLink.js.map +1 -0
- package/dist/history.d.ts +38 -0
- package/dist/history.js +120 -0
- package/dist/history.js.map +1 -0
- package/dist/htmlReport.d.ts +2 -0
- package/dist/htmlReport.js +371 -0
- package/dist/htmlReport.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/sandbox.d.ts +9 -0
- package/dist/mcp/sandbox.js +82 -0
- package/dist/mcp/sandbox.js.map +1 -0
- package/dist/mcp/server.d.ts +120 -0
- package/dist/mcp/server.js +315 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/network.d.ts +60 -0
- package/dist/network.js +99 -0
- package/dist/network.js.map +1 -0
- package/dist/overview.d.ts +40 -0
- package/dist/overview.js +370 -0
- package/dist/overview.js.map +1 -0
- package/dist/overviewFile.d.ts +31 -0
- package/dist/overviewFile.js +101 -0
- package/dist/overviewFile.js.map +1 -0
- package/dist/scan.d.ts +159 -0
- package/dist/scan.js +402 -0
- package/dist/scan.js.map +1 -0
- package/dist/sharedCalls.d.ts +28 -0
- package/dist/sharedCalls.js +61 -0
- package/dist/sharedCalls.js.map +1 -0
- package/dist/taxonomy.d.ts +77 -0
- package/dist/taxonomy.js +153 -0
- package/dist/taxonomy.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# @ia-qa/qa-discovery
|
|
2
|
+
|
|
3
|
+
**You have been handed a web app and no documentation. What is in it, and where would testing it start?**
|
|
4
|
+
|
|
5
|
+
`ia-qa-discover` crawls an app you can reach and writes down what it actually observes: every page it could load, the headings that say what the page is for, every input the app takes, and every API call the browser really made. Deterministic — no LLM decides anything here. Local — it runs a headless browser on your machine and nothing is uploaded, ever. (One separate, optional binary, [`ia-qa-discover-ai`](#ia-qa-discover-ai--optional-byok-and-the-only-thing-here-that-sends-anything), calls a model with your own key; it is the only thing in this package that sends anything anywhere, and it says so before it does.)
|
|
6
|
+
|
|
7
|
+
It answers *what is there*. It does not yet tell you what to test, and it never returns a pass/fail verdict: this is reconnaissance, and pretending otherwise would be the most useful-looking lie a tool like this could tell.
|
|
8
|
+
|
|
9
|
+
**Where this is going.** Reconnaissance is step one of a longer goal: reproduce what a senior QA engineer does when handed an unfamiliar app — work out what it is *for*, where it would hurt most if it broke, and produce a prioritised test plan you can question line by line. Every stage after this one reads the capture below and must cite it, because a plan nobody can interrogate is a plan nobody can trust. See [ROADMAP.md](./ROADMAP.md) for the arc and for what this will deliberately never become.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @ia-qa/qa-discovery scan https://your-app.example.com --save --report --open
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[](https://www.ia-qa.com/media/discover-report-full.png)
|
|
16
|
+
|
|
17
|
+
▶ **[See the full report](https://www.ia-qa.com/media/discover-report-full.png)** — one self-contained HTML file, written by `--report`. That is `ia-qa.com` scanned by its own tool, and it found something: all 37 of its input fields carry no `data-testid`, no `id` and no `name`, so any test written against them can only use a positional selector.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What a real session looks like
|
|
22
|
+
|
|
23
|
+
Every line below is genuine output from scanning a real React app (11 routes, most of them behind a login). Nothing here is a mock-up.
|
|
24
|
+
|
|
25
|
+
**1. First run, anonymous.** It finds three pages — and refuses to let that read as success:
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ ia-qa-discover scan https://app.example.com --save --report
|
|
29
|
+
Discovering pages from https://app.example.com…
|
|
30
|
+
🌐 Using bundled Chromium (shared Playwright cache)
|
|
31
|
+
visited / (2 links)
|
|
32
|
+
visited /terms (1 links)
|
|
33
|
+
visited /policy/cookies (0 links)
|
|
34
|
+
Found 3 page(s). Capturing…
|
|
35
|
+
✔ home
|
|
36
|
+
✔ terms
|
|
37
|
+
✔ policy-cookies
|
|
38
|
+
|
|
39
|
+
✔ 3 of 3 page(s) captured
|
|
40
|
+
+ added: home, policy-cookies, terms
|
|
41
|
+
|
|
42
|
+
⚠ This app has a login (home), and no session was used.
|
|
43
|
+
What you have is the PUBLIC surface only — whatever is behind the login was never reached.
|
|
44
|
+
Run `ia-qa-discover login` to log in by hand once, then scan again.
|
|
45
|
+
|
|
46
|
+
Written to .ia-qa-discovery/capture/surface.json
|
|
47
|
+
Readable index: .ia-qa-discovery/capture/_overview.md
|
|
48
|
+
HTML report: ia-qa-discover-report.html
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**2. Log in by hand, once.** A visible browser opens; you do whatever the app asks.
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
$ ia-qa-discover login
|
|
55
|
+
|
|
56
|
+
🔑 ia-qa-discover login
|
|
57
|
+
Opens a visible browser at https://app.example.com/.
|
|
58
|
+
Log in there however your app asks — SSO, MFA, a consent screen, all of it.
|
|
59
|
+
Nothing is typed for you, and no credential is read or stored by this tool.
|
|
60
|
+
|
|
61
|
+
When you say you are done, the browser session is saved to:
|
|
62
|
+
.ia-qa-discovery/session.json
|
|
63
|
+
That file holds live session cookies: anyone who has it is logged in as you.
|
|
64
|
+
|
|
65
|
+
Open the browser now? [Y/n]
|
|
66
|
+
Logged in? (leave the browser open — press Enter here when the app is showing)
|
|
67
|
+
|
|
68
|
+
✅ Session saved — 7 cookies, 1 origin with local storage.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**3. Scan again — and hit the *second* way a page count lies.** The app navigates with React Router's `useNavigate()`, so there is not one `<a href>` to follow:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
$ ia-qa-discover scan
|
|
75
|
+
🔓 Reusing the saved session (.ia-qa-discovery/session.json).
|
|
76
|
+
visited / (0 links)
|
|
77
|
+
Found 1 page(s). Capturing…
|
|
78
|
+
✔ home
|
|
79
|
+
|
|
80
|
+
✔ 1 of 1 page(s) captured
|
|
81
|
+
|
|
82
|
+
⚠ No internal links were found, so discovery could not see past the entry URL.
|
|
83
|
+
"1 of 1 found" here means ONE page — not the whole app.
|
|
84
|
+
Discovery follows <a href>. An app that navigates programmatically (React Router's
|
|
85
|
+
navigate(), a button with an onClick handler) exposes no href for a crawl to follow.
|
|
86
|
+
Declare the routes you know in `.ia-qa-discovery/config.json` → "pages", and scan again.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**4. Declare the routes** (they were sitting in the router file) and get the real surface:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
$ ia-qa-discover scan --report
|
|
93
|
+
🔓 Reusing the saved session (.ia-qa-discovery/session.json).
|
|
94
|
+
Found 9 page(s) (9 declared in config.pages). Capturing…
|
|
95
|
+
✔ dashboard ✔ admin-dashboard ✔ feature-manager
|
|
96
|
+
✔ user-dashboard ✔ billing ✔ marketplace
|
|
97
|
+
✔ smart-tools ✔ terms ✔ policy-cookies
|
|
98
|
+
|
|
99
|
+
✔ 9 of 9 page(s) captured
|
|
100
|
+
+ added: admin-dashboard, billing, dashboard, feature-manager, marketplace, …
|
|
101
|
+
|
|
102
|
+
🔓 Scanned with the saved session (.ia-qa-discovery/session.json).
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**What that scan then told us about the app** — three things nobody had noticed, each traced to an observation:
|
|
106
|
+
|
|
107
|
+
- All 9 pages share one `<title>`. The tab, the browser history and a screen reader announcing a route change cannot tell them apart.
|
|
108
|
+
- `GET /api/integration/status` → **401** and `GET /api/billing/payment-history` → **404**, on a valid session.
|
|
109
|
+
- Every input field on the login form has no `id`, no `name` and no `data-testid` — so any test written against it can only use a positional selector, which breaks the day a field is inserted above it.
|
|
110
|
+
|
|
111
|
+
**5. Re-run any time.** An unchanged app must diff clean — that is the whole point of committing `capture/`:
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
$ ia-qa-discover scan
|
|
115
|
+
✔ 9 of 9 page(s) captured
|
|
116
|
+
(no change since the last scan)
|
|
117
|
+
|
|
118
|
+
$ ia-qa-discover history
|
|
119
|
+
2026-09-04T12:19:39Z 9/9 pages +9 -1 ~0 300df55 (main)
|
|
120
|
+
2026-09-04T12:33:48Z 9/9 pages +0 -0 ~9 300df55 (main)
|
|
121
|
+
2026-09-04T12:35:06Z 9/9 pages +0 -0 ~0 (no change) 300df55 (main)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## What you get
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
.ia-qa-discovery/
|
|
130
|
+
config.json # baseUrl, crawl + capture settings, declared pages
|
|
131
|
+
history.jsonl # one line per scan — the trend no single run can show
|
|
132
|
+
session.json # only if you ran `login`. Live cookies: gitignored automatically
|
|
133
|
+
capture/
|
|
134
|
+
surface.json # the manifest — machine-readable index, the entry point for tooling
|
|
135
|
+
_overview.md # the same scan, readable — start here as a human (or an LLM)
|
|
136
|
+
_shared-calls.json # calls that fire on most pages (session refresh, telemetry)
|
|
137
|
+
<page>.json # one capture per page
|
|
138
|
+
ia-qa-discover-report.html # optional branded HTML dossier (--report)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`capture/` is meant to be committed. It diffs cleanly in git, and re-running the scan tells you what moved.
|
|
142
|
+
|
|
143
|
+
### One page's capture
|
|
144
|
+
|
|
145
|
+
```jsonc
|
|
146
|
+
{
|
|
147
|
+
"schema": "qa-discovery-page@1",
|
|
148
|
+
"page": "checkout",
|
|
149
|
+
"url": "https://app.example.com/checkout",
|
|
150
|
+
"source": "declared", // "crawl" | "sitemap" | "declared"
|
|
151
|
+
"capturedAt": "2026-09-04T12:00:00.000Z",
|
|
152
|
+
"http": { "status": 200, "redirected": false },
|
|
153
|
+
"meta": { "title": "Checkout", "description": "…", "lang": "en" },
|
|
154
|
+
"headings": [{ "level": 1, "text": "Checkout" }],
|
|
155
|
+
"forms": [{
|
|
156
|
+
"selector": "form#checkout",
|
|
157
|
+
"method": null, // null = the DOM states none (a JS-handled submit)
|
|
158
|
+
"action": null, // never synthesized from the page URL
|
|
159
|
+
"hasSubmit": true,
|
|
160
|
+
"fields": [
|
|
161
|
+
{ "type": "email", "name": "email", "selector": "#email",
|
|
162
|
+
"stableSelector": true, "required": true }
|
|
163
|
+
]
|
|
164
|
+
}],
|
|
165
|
+
"looseFields": [ // inputs NOT inside any <form> — most React apps
|
|
166
|
+
{ "type": "search", "name": "q", "selector": "#sidebar > input:nth-of-type(1)",
|
|
167
|
+
"stableSelector": false, "required": false, "context": "main › Filters" }
|
|
168
|
+
],
|
|
169
|
+
"apiCalls": [
|
|
170
|
+
{ "method": "GET", "urlPattern": "/api/cart?page=2", "sameOrigin": true,
|
|
171
|
+
"origin": "https://app.example.com", "status": 200, "resourceType": "fetch", "count": 1 }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Three fields carry a promise worth knowing about:
|
|
177
|
+
|
|
178
|
+
- **`stableSelector: false`** — the field has no `data-testid`, no `id`, no `name`. The selector given is positional: it resolves *today* and breaks the moment a field is inserted above it. That is a fact about the app, not a caveat about this tool, and it is worth fixing before a suite is written against it.
|
|
179
|
+
- **`ambiguousSelector: true`** — rarer, and stronger: no selector could be made to resolve to exactly one element. Nothing downstream may act on that field. Uniqueness is *verified in the page*, never assumed.
|
|
180
|
+
- **`method: null` / `action: null`** — the form states neither. A form with no `action` submits to the current URL per the HTML spec, but a React form usually has none because submission never reaches the network. Reporting the page URL there would invent a POST target the app never declared.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Commands
|
|
185
|
+
|
|
186
|
+
### `scan [url]`
|
|
187
|
+
|
|
188
|
+
Crawl and capture. With `--save`, the URL is remembered and later runs are just `ia-qa-discover scan`.
|
|
189
|
+
|
|
190
|
+
| Flag | |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `--depth <n>` | crawl link-depth (default 2) |
|
|
193
|
+
| `--max <n>` | page cap (default 60) |
|
|
194
|
+
| `--no-reveal` | do not open menus/dropdowns looking for hidden links |
|
|
195
|
+
| `--strict-host` | treat `www.` and the apex as different hosts |
|
|
196
|
+
| `--session <file>` | reuse an existing Playwright `storageState` |
|
|
197
|
+
| `--no-network` | skip API/network capture this run |
|
|
198
|
+
| `--network-threshold <n>` | share threshold for promoting a call, 0-1 (default 0.6) |
|
|
199
|
+
| `--save` | persist this baseUrl to `config.json` |
|
|
200
|
+
| `--report [file.html]` | write the HTML dossier (default `ia-qa-discover-report.html`) |
|
|
201
|
+
| `--open` | open that report in a browser |
|
|
202
|
+
| `--json` | the whole result as one parseable document on stdout |
|
|
203
|
+
|
|
204
|
+
### `login`
|
|
205
|
+
|
|
206
|
+
Opens a **visible** browser, you log in by hand, and the session is saved for `scan` to reuse.
|
|
207
|
+
|
|
208
|
+
This verb models nothing on purpose. A declarative `auth` block describes *one shape of login* — a username field, a password field, a submit button — and every app whose login is not that shape is out of reach: federated SSO, MFA, a consent screen, a magic link, a device check. Since you perform the login yourself, all of them work.
|
|
209
|
+
|
|
210
|
+
It refuses under CI and without a TTY: it waits for a person twice over, so a pipeline that runs it just hangs. **An agent cannot run this** — it must ask you to. For automation, point `--session` at a `storageState` your own setup already writes.
|
|
211
|
+
|
|
212
|
+
The file it produces holds live cookies — whoever has it is logged in as you. The path is announced before the browser opens, `.ia-qa-discovery/.gitignore` is written on the way out, and nothing here uploads it.
|
|
213
|
+
|
|
214
|
+
### `history`
|
|
215
|
+
|
|
216
|
+
The trend no single scan can reconstruct: pages, forms, API surface, and what changed run over run.
|
|
217
|
+
|
|
218
|
+
### Global
|
|
219
|
+
|
|
220
|
+
`--config <dir>` resolves `.ia-qa-discovery/` somewhere else (also `IAQA_DISCOVERY_CONFIG_DIR`).
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## A page count is not coverage
|
|
225
|
+
|
|
226
|
+
This is the failure mode this tool works hardest to avoid, because every version of it *looks* like success. Three checks exist for it, and each one fires in the terminal, in `_overview.md` and in the HTML report:
|
|
227
|
+
|
|
228
|
+
**The login wall.** An app whose catch-all route renders the login page answers HTTP 200 at *every* URL. Without a check, a scan reports "3 of 3 pages captured" having captured the same wall three times. Detection is a password field observed in the DOM — inside a `<form>` or not.
|
|
229
|
+
|
|
230
|
+
**Where the page list comes from.** Three sources, merged and deduped by path, and the report says which contributed what:
|
|
231
|
+
|
|
232
|
+
1. **`sitemap.xml`** (falling back to `robots.txt`) — one HTTP GET, no browser, and the only source that does not care how the app navigates. Read first, and its hits also seed the crawl. `--no-sitemap` skips it. Apex and `www.` are folded together, so an apex sitemap against a `www.` baseUrl works — that mismatch is the most common way the two silently disagree.
|
|
233
|
+
2. **The crawl** — follows `<a href>` from the entry URL, same-origin GET only, never a URL that acts (`/logout`, `/delete`…).
|
|
234
|
+
3. **`config.pages`** — what you declare.
|
|
235
|
+
|
|
236
|
+
⚠️ **A sitemap describes the *public* surface.** It is written for search crawlers, and crawlers are not logged in — so on an app whose real surface is behind a login, expect it to declare the marketing pages and nothing else. Measured on one: the sitemap listed 3 pages; the app had 11. Behind a wall, `login` plus `config.pages` is still the answer.
|
|
237
|
+
|
|
238
|
+
And the cap is a budget, not a census: if more pages are known than `--max` allows, the scan says how many it left out rather than quietly shrinking the denominator.
|
|
239
|
+
|
|
240
|
+
**Href blindness.** Discovery follows `<a href>`. An app that navigates programmatically — React Router's `useNavigate()`, a button with an onClick handler — exposes no href at all, so the crawl reaches exactly the entry URL and reports "1 of 1 found", i.e. 100%. Measured on a real app: 11 routes, zero `<Link>`, one page found. When zero internal links are harvested, the scan says so and points at `config.pages`.
|
|
241
|
+
|
|
242
|
+
**Declared pages.** The way out of href blindness — the routes are usually sitting in your router file in plain sight:
|
|
243
|
+
|
|
244
|
+
```jsonc
|
|
245
|
+
{
|
|
246
|
+
"baseUrl": "https://app.example.com",
|
|
247
|
+
"pages": [
|
|
248
|
+
{ "name": "billing", "url": "/billing" },
|
|
249
|
+
{ "name": "marketplace", "url": "/marketplace" }
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Declared pages are scanned **in addition** to whatever the crawl finds, deduped by path — declaring some does not turn discovery off.
|
|
255
|
+
|
|
256
|
+
**The loaded state is one state.** A page shows some of its input surface on load and hides the rest behind a click — a dialog, a tab, an accordion, a "create" button. Measured on a real app: one page carried 64 clickable controls and 2 visible fields, so a load-only capture described about 3% of its entry points while looking complete.
|
|
257
|
+
|
|
258
|
+
Every scan therefore **counts those controls** and says so, whether or not you explore. `--deep` then opens them one level down and captures what they reveal, each field carrying the click that reaches it:
|
|
259
|
+
|
|
260
|
+
```console
|
|
261
|
+
$ ia-qa-discover scan --deep
|
|
262
|
+
🔍 --deep opened 120 control(s): 2 field(s) found that are not visible at load.
|
|
263
|
+
⚠ 2 page(s) hit the click budget, so their exploration is partial.
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
| Field | Type | Where / how to reach | Locatable |
|
|
267
|
+
| --- | --- | --- | --- |
|
|
268
|
+
| _(unnamed)_ | text | 🔍 click “$ search_tools…Ctrl+K” | ⚠ positional |
|
|
269
|
+
|
|
270
|
+
Three things keep it safe and honest:
|
|
271
|
+
|
|
272
|
+
- **Every non-GET request is blocked** for the duration of the walk, routed on the page — a click cannot mutate anything server-side. A name denylist is the second layer, for handlers that issue no request at all (a "log out" that just clears storage).
|
|
273
|
+
- **One level only.** `@ia-qa/self-healing` measured its own equivalent: depth 1 finished in 59 s over 43 clicks; depth 2 took 183 s over 150 clicks and *never completed at any budget*. A permanently partial capture is a false green wearing a feature's clothes.
|
|
274
|
+
- **A selector may not name two elements.** Exploration unions captures taken in different states, and a positional selector can address one element at load and another after a click. A revealed field whose selector is already spoken for is **dropped and counted** — reached but not captured is a coverage loss, and this tool states those rather than emit a selector that means two things.
|
|
275
|
+
|
|
276
|
+
`--deep` roughly doubles scan time and is deterministic in practice: two consecutive runs over an unchanged app diff clean.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Using it with `@ia-qa/self-healing`
|
|
281
|
+
|
|
282
|
+
They are two halves of the same loop, and neither replaces the other.
|
|
283
|
+
|
|
284
|
+
| | `@ia-qa/qa-discovery` | `@ia-qa/self-healing` |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| Question | *What is in this app?* | *My tests broke — did a selector move?* |
|
|
287
|
+
| Needs | a URL | an existing test suite |
|
|
288
|
+
| Output | app surface, no verdict | PASS / FIX / BLOCK, a CI gate |
|
|
289
|
+
| Verb | `scan` | `map` |
|
|
290
|
+
|
|
291
|
+
⚠️ **`scan` and `map` are not the same operation.** `map` writes a page *contract* — every interactive element by role and accessible name, for repairing broken locators. `scan` writes a *surface* — pages, headings, inputs, API calls, for deciding what to test. Do not expect one to produce the other.
|
|
292
|
+
|
|
293
|
+
**Discovery → healing.** Once you know the routes, hand them to healing and start writing tests against contracts:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
ia-qa-discover scan https://app.example.com --save # find the surface
|
|
297
|
+
ia-qa-heal init # then map it for test maintenance
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`config.pages` here has the same `{ name, url }` shape as self-healing's, and both packages resolve page names through the *same* function (`pageNameFromUrl`), so a page called `checkout` in one is `checkout` in the other. The two sets of artifacts line up 1:1.
|
|
301
|
+
|
|
302
|
+
**Healing → discovery.** Healing knows what your suite *covers*. Discovery knows what *exists*. The gap between them is your real coverage — and neither tool can tell you that alone.
|
|
303
|
+
|
|
304
|
+
**Sessions are interchangeable.** Both consume a Playwright `storageState`, so a session from either `login` verb works for the other: `ia-qa-discover scan --session ../.ia-qa/session.json`.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## MCP server
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npx ia-qa-discover-mcp
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A dependency-free JSON-RPC 2.0 stdio server exposing the **same** operations the CLI runs — a human and an agent get an identical contract, never a simplified view for one of them.
|
|
315
|
+
|
|
316
|
+
- `scan_app` — crawl and capture. Check `loginWall` and `hrefBlind` in the result before reporting coverage.
|
|
317
|
+
- `classify_app` — the optional BYOK layer below, for an agent. The key is read from an environment variable **named** in the call (`api_key_env`); a raw key is never a tool argument.
|
|
318
|
+
- `discovery_history` — the trend.
|
|
319
|
+
|
|
320
|
+
`login` is deliberately **not** a tool: it waits for a person at a browser, which an agent cannot be. Every path and URL an agent supplies is checked against the project root and the configured origin.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## `ia-qa-discover-ai` — optional, BYOK, and the only thing here that sends anything
|
|
325
|
+
|
|
326
|
+
Everything above is deterministic and never leaves your machine. This one binary is the exception, and it is a separate binary precisely so that boundary is a thing you install rather than a flag you might forget.
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
ia-qa-discover-ai classify --dry-run # see exactly what would be sent, and to whom
|
|
330
|
+
ia-qa-discover-ai classify
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
It reads the capture **offline** — it opens no browser and never touches your app again — and asks a model what each entry point is *for* (`authentication`, `payment`, `search`, `data-entry`…) and how sensitive what it handles is. The vocabulary is closed and versioned: a label outside it is rejected before anything else is checked.
|
|
334
|
+
|
|
335
|
+
**Every claim cites the capture, and every citation is resolved and checked.** A classification whose evidence does not exist, or does not say what it was claimed to say, is dropped before you see it, and the drop is reported. That proves the *premise*, never the conclusion — a model can cite a real password field and still be wrong about what the page is for — so read the `why` and the confidence, not the label alone.
|
|
336
|
+
|
|
337
|
+
Three things it will not do: it is **not a gate** and emits no verdict; a `refused` entry point is an answer needing a person, not a retry; and a failed call is reported as a failed call, never as "this page has nothing on it".
|
|
338
|
+
|
|
339
|
+
Configure it in `.ia-qa-discovery/config.json` — the file holds the *name* of the variable, never the key:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
"ai": { "provider": "anthropic", "model": "claude-haiku-4-5",
|
|
343
|
+
"apiKey": { "source": "env", "key": "ANTHROPIC_API_KEY" } }
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Before the first request it prints what leaves and where it goes:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
🌐 Sending 3 pages to api.anthropic.com (anthropic · claude-haiku-4-5) — your key, your account.
|
|
350
|
+
Leaves this machine: each page's full URL, title and description, up to 25 headings,
|
|
351
|
+
every observed API call (method, normalised path, status), and each form field's name,
|
|
352
|
+
type, label and selector. Headings and labels are live text from your app.
|
|
353
|
+
Query values are stripped at capture EXCEPT an allowlist that includes `q` — a search
|
|
354
|
+
term reaches the model as typed (`capture.safeQueryParams` in config.json narrows it).
|
|
355
|
+
Does NOT leave: page HTML, screenshots, cookies or your session file, your test files,
|
|
356
|
+
your API key (sent as a header to api.anthropic.com only).
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Read that list before pointing it at an authenticated or private app. The result lands in `.ia-qa-discovery/classification.json` **and** in `_overview.md`, which is rewritten so the reading appears next to the capture it was made from.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Library
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { scanApp, readHistory } from '@ia-qa/qa-discovery';
|
|
367
|
+
|
|
368
|
+
const result = await scanApp({ url: 'https://app.example.com', write: false });
|
|
369
|
+
console.log(result.pagesCaptured, result.loginWall, result.hrefBlind);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Requirements
|
|
373
|
+
|
|
374
|
+
Node ≥ 18, and Playwright (`peerDependency`) with a Chromium available — `npx playwright install chromium`, or point at a browser you already have via `IAQA_BROWSER_CHANNEL=chrome`.
|
|
375
|
+
|
|
376
|
+
## License
|
|
377
|
+
|
|
378
|
+
MIT
|