@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/ROADMAP.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# @ia-qa/qa-discovery — roadmap
|
|
2
|
+
|
|
3
|
+
## What this is for
|
|
4
|
+
|
|
5
|
+
Hand a senior QA engineer an application they have never seen, with no documentation and
|
|
6
|
+
no test plan. Watch what they do. They do **not** start by listing pages — they work out
|
|
7
|
+
what the app is *for*, who uses it, and where it would hurt most if it broke. Then they
|
|
8
|
+
decide what to test, and in what order, and they can tell you **why** for every line.
|
|
9
|
+
|
|
10
|
+
That movement is what this package automates. The goal is not an inventory of an app; it
|
|
11
|
+
is **the test plan a senior would write after a day with it** — prioritised, justified, and
|
|
12
|
+
arriving in hours instead of days. Discovery is the judgement step that everything else in
|
|
13
|
+
the ia-qa.com ecosystem assumes has already happened: `generate_test_cases` writes the
|
|
14
|
+
scenario you hand it, the eval and security tools check the thing you point them at,
|
|
15
|
+
`@ia-qa/self-healing` keeps a suite alive once it exists. All of them start *after*
|
|
16
|
+
somebody decided what mattered. This is that somebody.
|
|
17
|
+
|
|
18
|
+
Three consequences shape everything below.
|
|
19
|
+
|
|
20
|
+
**"Why" is the deliverable, not the list.** A plan nobody can question is a plan nobody
|
|
21
|
+
can trust or edit. So every claim traces to something observed in the app — that is what
|
|
22
|
+
the governing rule below is for, and why an LLM never touches the capture itself.
|
|
23
|
+
|
|
24
|
+
**It is used repeatedly, not once.** An app changes; a discovery is a perishable snapshot.
|
|
25
|
+
The point is to re-run it and see what moved — which is why reconnaissance already diffs, keeps a
|
|
26
|
+
history, and writes files meant to be committed. A tool that produces one impressive
|
|
27
|
+
report and is never opened again has failed, however good the report.
|
|
28
|
+
|
|
29
|
+
**A human and an agent are the same user.** Whatever the CLI does, `ia-qa-discover-mcp`
|
|
30
|
+
exposes identically — `scan_app`, `classify_app`, `discovery_history`. There is no simplified
|
|
31
|
+
view for one of them and a richer one for the other: one contract, served twice. The two
|
|
32
|
+
exceptions are named rather than quiet, and both are the same exception: `login` waits for a
|
|
33
|
+
person at a browser, which an agent cannot be.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
How the pipeline grows past reconnaissance, and — just as important — what it will **not**
|
|
38
|
+
become. One rule governs every phase:
|
|
39
|
+
|
|
40
|
+
> **The LLM infers, the system decides.** Every claim a later stage makes carries two
|
|
41
|
+
> things: a sentence a human reads, and a **path into the capture** that the system
|
|
42
|
+
> resolves and checks before the claim is kept. A stage that cannot cite must be able to
|
|
43
|
+
> say *"not enough evidence"* and stop. If a user configures no key, the tool behaves
|
|
44
|
+
> exactly as it does today.
|
|
45
|
+
>
|
|
46
|
+
> **And the exact limit of that check, so nobody mistakes it for more:** the path proves
|
|
47
|
+
> the **premise**, never the **conclusion**. A model can cite a password field that really
|
|
48
|
+
> exists and still call the page a checkout. The machine can verify that what the model
|
|
49
|
+
> leaned on is real; only a human can verify that the inference follows. That gap is the
|
|
50
|
+
> whole reason the sentence must stay readable and the confidence must stay visible.
|
|
51
|
+
|
|
52
|
+
The guardrails that make this safe, kept across all phases:
|
|
53
|
+
|
|
54
|
+
1. **`scan` never calls an LLM.** Reconnaissance is deterministic, and stays the only thing that
|
|
55
|
+
touches the app. Everything inferential reads its output offline.
|
|
56
|
+
2. **A separate binary** (`ia-qa-discover-ai`), like `ia-qa-heal-ai`. Absent key ⇒ the
|
|
57
|
+
AI layer is inert and the deterministic verbs behave identically.
|
|
58
|
+
3. **Citations are checked, not trusted.** A classification naming
|
|
59
|
+
`pages/checkout.json#looseFields[2]` is only kept if that path exists and holds what
|
|
60
|
+
was claimed. An uncited claim is dropped, and the drop is reported.
|
|
61
|
+
4. **A warning's register matches the claim's status.** A deterministic finding is a fact
|
|
62
|
+
about the app — stated flatly, once, no hedging ("2 of 2 fields cannot be located
|
|
63
|
+
durably"). An inference carries its doubt inside itself, so it does not also need a
|
|
64
|
+
klaxon on every row: the general caveat goes once at the top of the section, confidence
|
|
65
|
+
is shown per row, and a per-row alert is **reserved** for rows that earn it (below the
|
|
66
|
+
confidence floor, citation rejected, contradicted by another signal). Stamping "VERIFY
|
|
67
|
+
THIS" on 200 lines produces alarm fatigue, which is worse than no warning at all — the
|
|
68
|
+
three rows that needed attention drown in the 197 that did not.
|
|
69
|
+
5. **A refusal path is mandatory.** An LLM asked to classify will always classify.
|
|
70
|
+
Without an "insufficient evidence" outcome, we ship confident nonsense — the exact
|
|
71
|
+
failure family reconnaissance already met four times (login wall, href blindness, fields outside
|
|
72
|
+
a form, non-unique selectors), each one a confident number that was wrong.
|
|
73
|
+
6. **No verdict, still.** Nothing in this pipeline emits a CI gate. A test plan is a
|
|
74
|
+
proposal a human edits; the gate is `@ia-qa/self-healing`, downstream, once tests exist.
|
|
75
|
+
7. **Zero bundled model.** Provider calls are a raw `fetch`, keys by reference, never on disk.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## The stages
|
|
80
|
+
|
|
81
|
+
Seven of them. The `F` codes come from the original specification and are kept only so the
|
|
82
|
+
two documents line up — the name is what matters, and nothing below should need the code to
|
|
83
|
+
be understood.
|
|
84
|
+
|
|
85
|
+
| Stage | The question it answers | Status |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **Value and personas** (F0) | Who uses this app, for what, and where would it hurt most if it broke? | not built |
|
|
88
|
+
| **Surface reconnaissance** (F1) | What pages exist, what do they take as input, what do they call? | ✅ shipped (0.1.0) |
|
|
89
|
+
| **Component classification** (F2) | What is each entry point *for*, and how sensitive is what it handles? | in progress |
|
|
90
|
+
| **Zones and synthesis** (F2.5) | How do the pages group into functional areas, and what is the app as a whole? | not built |
|
|
91
|
+
| **Scenario generation** (F3) | What are the concrete test cases for this entry point? | not built |
|
|
92
|
+
| **Risk prioritisation** (F4) | What should be tested first, and why? | not built |
|
|
93
|
+
| **Export** (F5) | How does the plan reach Jira and the automation tooling? | not built |
|
|
94
|
+
|
|
95
|
+
At run time they compose in that order — value first, export last. **The sections below are
|
|
96
|
+
in _build_ order instead**, which is different and argued under Component classification: the
|
|
97
|
+
order things get written in is driven by what unblocks what.
|
|
98
|
+
|
|
99
|
+
## Surface reconnaissance (F1) — ✅ shipped in 0.1.0
|
|
100
|
+
|
|
101
|
+
Crawl + capture: pages, meta, headings, forms and fields, observed API calls, and the
|
|
102
|
+
coverage caveats that keep a page count from reading as coverage. Sitemap, crawl and
|
|
103
|
+
declared pages as three merged sources; `login` for what is behind a wall; `--deep` for
|
|
104
|
+
what is behind a click. Deterministic, local, no verdict.
|
|
105
|
+
|
|
106
|
+
**What it measured on real apps, and what that changed:** an app with 11 routes and zero
|
|
107
|
+
`<a href>` (a page count of "1 of 1 found" that meant one page); a catch-all route serving
|
|
108
|
+
the login page at HTTP 200 on every URL; 464 source inputs against 26 `<form>` tags; one
|
|
109
|
+
page with 64 clickable controls and 2 visible fields. Every one of those is now a stated
|
|
110
|
+
caveat rather than a silent number.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Component classification (F2) — **in progress**
|
|
115
|
+
|
|
116
|
+
For each page and each entry point: what is this *for*, and how sensitive is what it
|
|
117
|
+
handles? Input is the reconnaissance capture, offline. Output is a classification with citations.
|
|
118
|
+
|
|
119
|
+
### Build order: classification is a quick win, value framing is a game changer
|
|
120
|
+
|
|
121
|
+
The conceptual order is value-first: a senior reads what the app is *for* before judging what a
|
|
122
|
+
given form is worth. The build order is the other way round, and the reason is not that classification
|
|
123
|
+
matters more — it is that **the two are different kinds of work**.
|
|
124
|
+
|
|
125
|
+
**Classification is a quick win, and its output is checkable in seconds.** "This entry point is
|
|
126
|
+
authentication" → is there a password field, yes or no. That is what makes it the right
|
|
127
|
+
place to *prove the citation architecture*: if the mechanism is wrong, it shows immediately.
|
|
128
|
+
It also unlocks something deterministic straight away — an entry point classified
|
|
129
|
+
`authentication` or `payment` can pull in security tools that already exist on the
|
|
130
|
+
ia-qa.com MCP (`security_headers_check`, `cookie_security_audit`, `cors_test`,
|
|
131
|
+
`ssl_certificate_check`, `secret_scan`) instead of asking an LLM to imagine those scenarios
|
|
132
|
+
at the scenario stage.
|
|
133
|
+
|
|
134
|
+
**Value framing is the game changer, and it deserves the design time.** "The moment of truth is
|
|
135
|
+
checkout" cannot be verified mechanically — it needs domain judgement. So it needs
|
|
136
|
+
something classification does not: **a validation protocol**. Label a handful of real apps by hand,
|
|
137
|
+
compare, measure the agreement, and only then decide what the stage may assert. That is
|
|
138
|
+
design work, not coding, and rushing it is how a game changer turns into a plausible-
|
|
139
|
+
sounding paragraph nobody can check.
|
|
140
|
+
|
|
141
|
+
**The risk of this order, written down so it is not discovered later:** on its own, this stage
|
|
142
|
+
is only a labeller. It will look useful, it will demo well, and the value layer that makes
|
|
143
|
+
this a *senior's* plan rather than a sophisticated inventory can quietly never get built.
|
|
144
|
+
"Later" is how a game changer becomes a backlog item. Two things guard against it: it is
|
|
145
|
+
scheduled as careful work, not optional work; and **the classification schema must leave a hole for
|
|
146
|
+
the value map to fill** — a component's importance is a field the value stage supplies, never something
|
|
147
|
+
the classifier invents from the DOM — so shipping it later completes the picture instead of reopening it.
|
|
148
|
+
|
|
149
|
+
### Decided
|
|
150
|
+
|
|
151
|
+
**Citation = a sentence *and* a path.** Not a choice between them: the sentence is what a
|
|
152
|
+
human reads, the path is what the machine resolves. The claim is kept only if the path
|
|
153
|
+
checks out, and the sentence is what makes the surviving claim auditable — see the limit
|
|
154
|
+
stated in the governing rule (premise, not conclusion).
|
|
155
|
+
|
|
156
|
+
```jsonc
|
|
157
|
+
{
|
|
158
|
+
"entryPoint": "pages/home.json#forms[0]",
|
|
159
|
+
"intent": "authentication",
|
|
160
|
+
"confidence": 0.92,
|
|
161
|
+
"why": "The form takes an email and a password, and the page calls /auth/profile.",
|
|
162
|
+
"evidence": [
|
|
163
|
+
"pages/home.json#forms[0].fields[1]", // type: "password"
|
|
164
|
+
"pages/home.json#apiCalls[3]" // GET /auth/profile → 401
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Granularity = per entry point, not per page.** A page label is not actionable. Measured:
|
|
170
|
+
`/llm-playground` on ia-qa.com holds 12 fields — a model selector, temperature sliders, a
|
|
171
|
+
prompt box, and an API key field. One label for that page says nothing; per entry point,
|
|
172
|
+
the API key field is a sensitive surface that should pull in `secret_scan` while the
|
|
173
|
+
temperature slider is not. The grouping this needs already exists: reconnaissance stamps a `context` on
|
|
174
|
+
every loose field (`main › Billing address`) for exactly this.
|
|
175
|
+
|
|
176
|
+
**The vocabulary is closed and versioned** (`src/taxonomy.ts`, `qa-discovery-taxonomy@1`). The
|
|
177
|
+
model picks an id or refuses; it cannot invent a label, because an invented vocabulary
|
|
178
|
+
drifts between runs and two scans of one app stop being comparable — which kills the diff
|
|
179
|
+
this package is built on.
|
|
180
|
+
|
|
181
|
+
*The admission rule that keeps the list from bloating:* **an intent earns its place only if
|
|
182
|
+
it changes what you would test.** Two labels leading to the same test plan are one label.
|
|
183
|
+
Same discipline as `check`'s admission rule in self-healing; without it every plausible noun
|
|
184
|
+
becomes a category and the classification discriminates nothing.
|
|
185
|
+
|
|
186
|
+
*Two axes, not one* — the spec asks "what intention, **what data sensitivity**", and
|
|
187
|
+
collapsing them makes both wrong. `admin` is not an intent (an admin page can be data entry,
|
|
188
|
+
configuration or navigation); it is an access dimension. A test pins that, along with the
|
|
189
|
+
rule that no tool mapping may name an MCP tool that does not exist.
|
|
190
|
+
|
|
191
|
+
**Built so far** (the deterministic half, no LLM involved yet):
|
|
192
|
+
- `src/citations.ts` — the verifier, written and tested *before* anything that produces
|
|
193
|
+
claims. A judge built after the accused tends to be shaped to acquit.
|
|
194
|
+
- `src/taxonomy.ts` — the closed referentials, plus the intent/sensitivity → security-tool
|
|
195
|
+
mapping that is this stage's deterministic payoff.
|
|
196
|
+
|
|
197
|
+
**On the refusal threshold — ship first, then set it from data.** "Empirical" does not mean
|
|
198
|
+
a labelling protocol before any code; it means the number comes from looking at real output
|
|
199
|
+
instead of picking one that sounds right. Two things make that safe here: the safety net is
|
|
200
|
+
the **citation verifier**, not the threshold (invented evidence is rejected at any setting),
|
|
201
|
+
and there is already a categorical refusal that needs no number at all — the model can
|
|
202
|
+
answer `unknown`, a value in the taxonomy.
|
|
203
|
+
|
|
204
|
+
So v0.1 **discards nothing on confidence**. Every classification is emitted with its score
|
|
205
|
+
visible, because *a threshold applied on the first run hides exactly the data needed to set
|
|
206
|
+
it*: ship at 0.7 and drop the rest, and you never learn whether 0.6 was fine or 0.8 was
|
|
207
|
+
needed. The numeric floor is a second layer, added once the distribution over real captures
|
|
208
|
+
says where it belongs.
|
|
209
|
+
|
|
210
|
+
Still open:
|
|
211
|
+
- **The prompt and the BYOK call**, mirroring `ia-qa-heal-ai`'s shape (raw `fetch`, three
|
|
212
|
+
providers, temperature 0).
|
|
213
|
+
|
|
214
|
+
## Zones and app-level synthesis (F2.5)
|
|
215
|
+
|
|
216
|
+
Cluster pages into functional zones (deterministic first: route structure and the
|
|
217
|
+
navigation graph), then a grounded synthesis on top. Reconciles with the value stage once it exists.
|
|
218
|
+
|
|
219
|
+
## Value and personas (F0) — *the game changer, built with care*
|
|
220
|
+
|
|
221
|
+
Who uses this app, for what, and where the moment of truth is. This is the stage that makes
|
|
222
|
+
the output a senior's plan instead of a classified inventory, and the one whose claims are
|
|
223
|
+
hardest to check — so it is scheduled as **careful** work, not as later work.
|
|
224
|
+
|
|
225
|
+
**It needs a validation protocol before it needs code.** Classification can be checked in seconds
|
|
226
|
+
against the capture; this cannot. Label a handful of real apps by hand, compare the stage's
|
|
227
|
+
framing to the labels, measure the agreement, and let that decide what this stage is allowed to
|
|
228
|
+
assert and where its refusal threshold sits. Shipping a persuasive paragraph nobody can
|
|
229
|
+
verify would be the uncalibrated-judge anti-pattern, one layer up.
|
|
230
|
+
|
|
231
|
+
**One open question is now answered by evidence**: the original plan asked whether this stage needs
|
|
232
|
+
access to the front-end bundle for its deterministic guardrails. Probably not — reconnaissance already
|
|
233
|
+
captures every page's `<title>`, meta description and full heading outline, which is where
|
|
234
|
+
the value proposition literally lives. Measured: one app's home title read
|
|
235
|
+
*"139 LLM Testing Tools | Prompt Tests, RAG Audits, CI-Ready Assertions"*, and its method
|
|
236
|
+
page carried 132 headings. That is the marketing content, verbatim, already on disk.
|
|
237
|
+
|
|
238
|
+
## Risk prioritisation (F4)
|
|
239
|
+
|
|
240
|
+
Deterministic scoring, visible and editable formula. **Part of it needs no LLM and could
|
|
241
|
+
ship before classification**: reconnaissance already observes password fields (an auth surface), 4xx/5xx responses
|
|
242
|
+
on a valid session, fields no test could locate durably, third-party origins, and forms
|
|
243
|
+
with no submit control. Worth separating "risk signals reconnaissance already has" from "risk informed
|
|
244
|
+
by the classifier's intent", so the first half is not blocked on the second.
|
|
245
|
+
|
|
246
|
+
## Scenario generation (F3)
|
|
247
|
+
|
|
248
|
+
**The audit found the assumed inputs do not exist.** There is no closed grid of test
|
|
249
|
+
patterns anywhere in the ecosystem to derive from — `QA_AGENT_RECIPES` and
|
|
250
|
+
`AUGMENTED_QA_PLAYBOOK` are editorial content, not structured data. So the intent→scenario
|
|
251
|
+
grid is net-new. What *does* exist and should be reused rather than reimplemented:
|
|
252
|
+
`generate_test_cases` (MCP) already turns a declared field constraint into valid / edge /
|
|
253
|
+
invalid / pairwise cases — that is the field-level half, done.
|
|
254
|
+
|
|
255
|
+
## Export (F5)
|
|
256
|
+
|
|
257
|
+
**No standard test-suite format exists to target.** `jira_to_test_suite` returns whatever
|
|
258
|
+
JSON the LLM produced, validated only for Gherkin syntax — so `test-plan.json` has to be
|
|
259
|
+
designed, not adopted. The ADF/Gherkin rendering helpers in `atlassianHandler.js` are
|
|
260
|
+
reusable for the Jira half.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Surfaces, not stages
|
|
267
|
+
|
|
268
|
+
Two things that are not pipeline stages, deferred deliberately — with what would
|
|
269
|
+
make them worth building, so the decision can be re-taken on evidence rather than
|
|
270
|
+
on enthusiasm.
|
|
271
|
+
|
|
272
|
+
### A local console (`ui`)
|
|
273
|
+
|
|
274
|
+
`@ia-qa/self-healing` has one, and its console earns its place by being a **decision
|
|
275
|
+
surface**: it serves the rows where the tool gave up and needs a human to choose,
|
|
276
|
+
with the verbs as buttons.
|
|
277
|
+
|
|
278
|
+
Discovery had no decisions to offer, which is why this was declined once already —
|
|
279
|
+
a console would have been a prettier rendering of the HTML report. **Classification
|
|
280
|
+
changes that argument**: refused entry points and dropped claims are exactly rows
|
|
281
|
+
that need a person, and a risk ranking is something you want to sort and filter
|
|
282
|
+
rather than read. The trigger is therefore risk prioritisation landing: once there
|
|
283
|
+
is something to rank, there is something to explore. Building it before that would
|
|
284
|
+
repeat the mistake the report already covers.
|
|
285
|
+
|
|
286
|
+
Whatever gets built inherits self-healing's four containment rules without
|
|
287
|
+
restatement: loopback only, a per-process token, Host pinning, and no new authority
|
|
288
|
+
(actions spawn this package's own CLI with a fixed argv, never a string from the
|
|
289
|
+
request).
|
|
290
|
+
|
|
291
|
+
### A tutorial
|
|
292
|
+
|
|
293
|
+
There is none, and that was the right call at three verbs: the README plus a real
|
|
294
|
+
session transcript *was* the tutorial. **At three binaries and six verbs it is
|
|
295
|
+
becoming the wrong call** — the loop is now scan → login → deep → classify, with a
|
|
296
|
+
BYOK key and a taxonomy in the middle, and that is more than a README section can
|
|
297
|
+
carry without becoming a manual.
|
|
298
|
+
|
|
299
|
+
The trigger is scenario generation landing, because that is the point at which the
|
|
300
|
+
output stops being "a description of your app" and becomes "a thing you act on" —
|
|
301
|
+
which is what a tutorial has to teach. Note the shipping cost before starting: a
|
|
302
|
+
`TUTORIAL.md` rendered by ia-qa.com owes a **site deploy as well as an npm publish**
|
|
303
|
+
(see the README-image trap in the root CLAUDE.md).
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Explicitly out of scope (won't build)
|
|
308
|
+
|
|
309
|
+
- **An LLM anywhere in `scan`.** The capture is the evidence. Contaminating it would leave nothing
|
|
310
|
+
for later stages to be checked against.
|
|
311
|
+
- **A CI gate on any of this.** A test plan is a proposal. Gating on an LLM's classification
|
|
312
|
+
is the uncalibrated-judge anti-pattern the project rejects everywhere else.
|
|
313
|
+
- **A hosted crawl.** Everything runs on the user's machine, on their app, with their key.
|
|
314
|
+
ia-qa.com ships tools, not a service that holds other people's applications.
|
|
315
|
+
- **Autonomous test execution.** Discovery says what to test; running it is the Automation
|
|
316
|
+
skills' job, and self-healing's once tests exist.
|
|
317
|
+
- **A second npm package for the AI layer.** It consumes the reconnaissance capture, so it cannot be
|
|
318
|
+
standalone; a third `bin` here gives the optional-add-on shape without a second release
|
|
319
|
+
pipeline.
|
|
320
|
+
- **A `--junit` export.** There is no verdict, so it would be a gate that can never fail —
|
|
321
|
+
the most expensive thing this ecosystem can ship.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { AiProvider } from '@ia-qa/self-healing';
|
|
2
|
+
import type { PageCapture } from '../capture/page';
|
|
3
|
+
import { type ToolTrigger } from '../taxonomy';
|
|
4
|
+
import { type CaptureIndex, type Citation, type CitationVerdict } from '../citations';
|
|
5
|
+
/**
|
|
6
|
+
* F2 — classify each entry point, offline, from F1's capture.
|
|
7
|
+
*
|
|
8
|
+
* The model never sees the app: it reads JSON that was captured deterministically,
|
|
9
|
+
* and every answer it gives must point back into that JSON. Nothing here opens a
|
|
10
|
+
* browser or touches the user's application.
|
|
11
|
+
*
|
|
12
|
+
* **On duplication, stated because this project punishes silent drift:** the
|
|
13
|
+
* provider/model catalogue is imported from `@ia-qa/self-healing` rather than
|
|
14
|
+
* copied — there must not be a third model list in this repo. The HTTP transport
|
|
15
|
+
* below *is* a second implementation, because self-healing does not export its
|
|
16
|
+
* own (`callModel` is internal). Any change to a provider's request shape owes an
|
|
17
|
+
* edit here as well as there.
|
|
18
|
+
*/
|
|
19
|
+
/** The document writes, and the two strings a later stage keys on. */
|
|
20
|
+
export declare const CLASSIFICATION_SCHEMA = "qa-discovery-classification@1";
|
|
21
|
+
export declare const CLASSIFICATION_FILENAME = "classification.json";
|
|
22
|
+
export interface ClassifyOptions {
|
|
23
|
+
provider: AiProvider;
|
|
24
|
+
model: string;
|
|
25
|
+
apiKey: string;
|
|
26
|
+
/** Injected in tests; defaults to the global fetch. */
|
|
27
|
+
fetchFn?: typeof fetch;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
temperature?: number;
|
|
30
|
+
}
|
|
31
|
+
/** One thing on a page that takes input, addressed by a path a citation can resolve. */
|
|
32
|
+
export interface EntryPoint {
|
|
33
|
+
/** e.g. `pages/checkout.json#forms[0]` or `pages/checkout.json#looseFields[2]` */
|
|
34
|
+
path: string;
|
|
35
|
+
kind: 'form' | 'field';
|
|
36
|
+
page: string;
|
|
37
|
+
/** Everything the model is allowed to reason from, verbatim from the capture. */
|
|
38
|
+
detail: unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface Classification {
|
|
41
|
+
entryPoint: string;
|
|
42
|
+
intent: string;
|
|
43
|
+
sensitivity: string;
|
|
44
|
+
/** The model's own number. v0.1 discards nothing on it — see ROADMAP. */
|
|
45
|
+
confidence: number;
|
|
46
|
+
/** The sentence a human reads. The citations are what the machine checks. */
|
|
47
|
+
why: string;
|
|
48
|
+
evidence: Citation[];
|
|
49
|
+
/** Filled in deterministically from the taxonomy, never by the model. */
|
|
50
|
+
securityTools?: ToolTrigger[];
|
|
51
|
+
}
|
|
52
|
+
export interface DroppedClassification {
|
|
53
|
+
classification: Partial<Classification>;
|
|
54
|
+
reason: string;
|
|
55
|
+
rejected: Array<Extract<CitationVerdict, {
|
|
56
|
+
ok: false;
|
|
57
|
+
}>>;
|
|
58
|
+
}
|
|
59
|
+
export interface ClassifyResult {
|
|
60
|
+
taxonomyVersion: string;
|
|
61
|
+
kept: Classification[];
|
|
62
|
+
dropped: DroppedClassification[];
|
|
63
|
+
/** Entry points the model declined to label. A refusal is a result, not a gap. */
|
|
64
|
+
refused: Classification[];
|
|
65
|
+
/**
|
|
66
|
+
* Set when the call did not succeed at all — network down, key rejected,
|
|
67
|
+
* reply unreadable.
|
|
68
|
+
*
|
|
69
|
+
* Without this, a failed call and a page with nothing to classify are the same
|
|
70
|
+
* empty result, and "0 classified" reads as a fact about the app instead of a
|
|
71
|
+
* fact about the request. Measured: an invalid key produced
|
|
72
|
+
* "0 classified · 0 refused · 0 dropped", exit 0, and a written file.
|
|
73
|
+
*/
|
|
74
|
+
failed?: {
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The citable units of a page.
|
|
80
|
+
*
|
|
81
|
+
* A `<form>` is one entry point. A loose field is its own, because grouping them
|
|
82
|
+
* would need a path that does not exist in the capture and a citation must
|
|
83
|
+
* resolve to something real. If that proves too granular in practice, grouping is
|
|
84
|
+
* a v0.2 question — answered by looking at real output, not guessed now.
|
|
85
|
+
*/
|
|
86
|
+
export declare function entryPointsOf(page: PageCapture): EntryPoint[];
|
|
87
|
+
/**
|
|
88
|
+
* What the model is given. Deliberately narrow: the page's identity and headings
|
|
89
|
+
* for context, and the entry points verbatim. It is not handed the whole capture,
|
|
90
|
+
* because a model given everything cites the most quotable thing rather than the
|
|
91
|
+
* most relevant one.
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* Every path the prompt offers, and therefore the only paths a citation may
|
|
95
|
+
* use. The model picks from a menu; it does not compose.
|
|
96
|
+
*
|
|
97
|
+
* This is the same guardrail `@ia-qa/self-healing` puts on its resolver — "the
|
|
98
|
+
* model chooses a live candidate by index, so it cannot emit a broken selector".
|
|
99
|
+
* Measured need: across three real runs the model cited `#page.description`,
|
|
100
|
+
* `#looseFields[0].detail` and a bare `#description`, all of which existed in the
|
|
101
|
+
* *prompt's* shape and none in the capture's. Resolving-and-rejecting caught
|
|
102
|
+
* them, but only after the fact; refusing a constructed path removes the whole
|
|
103
|
+
* class of error instead.
|
|
104
|
+
*/
|
|
105
|
+
export declare function offeredPaths(page: PageCapture, entryPoints: EntryPoint[]): Set<string>;
|
|
106
|
+
export declare function buildPrompt(page: PageCapture, entryPoints: EntryPoint[]): string;
|
|
107
|
+
/** Pull the JSON object out of a model reply, tolerating fences and prose around it. */
|
|
108
|
+
export declare function parseClassifications(text: string): Partial<Classification>[] | null;
|
|
109
|
+
/**
|
|
110
|
+
* Validate one classification against the closed taxonomy, then against the
|
|
111
|
+
* capture. Order matters: an off-list label is rejected before any citation is
|
|
112
|
+
* resolved, because a model inventing vocabulary is not a model whose evidence
|
|
113
|
+
* is worth checking.
|
|
114
|
+
*/
|
|
115
|
+
export declare function validate(raw: Partial<Classification>, index: CaptureIndex, offered?: Set<string>): {
|
|
116
|
+
ok: true;
|
|
117
|
+
value: Classification;
|
|
118
|
+
} | {
|
|
119
|
+
ok: false;
|
|
120
|
+
dropped: DroppedClassification;
|
|
121
|
+
};
|
|
122
|
+
/** Sort validated classifications into kept / refused, and collect the drops. */
|
|
123
|
+
export declare function collect(raws: Partial<Classification>[], index: CaptureIndex, offered?: Set<string>): ClassifyResult;
|
|
124
|
+
/**
|
|
125
|
+
* What leaves this machine, said before it leaves — the one thing that makes
|
|
126
|
+
* "everything is local except the model call" a promise rather than a claim.
|
|
127
|
+
*
|
|
128
|
+
* It lives beside `HOSTS` on purpose: the notice names the very constant
|
|
129
|
+
* `buildRequest` sends to, so a new provider cannot be reachable while the
|
|
130
|
+
* notice still lists three. And it is derived from `buildPrompt` — every line
|
|
131
|
+
* below is a field that function actually puts in the payload; adding one there
|
|
132
|
+
* owes a line here.
|
|
133
|
+
*
|
|
134
|
+
* Written to stderr in every mode, `--json` included: it is a security
|
|
135
|
+
* announcement, not decoration, and stdout must stay one parseable document.
|
|
136
|
+
*
|
|
137
|
+
* The last clause is the one that took a check to get right. Query values are
|
|
138
|
+
* stripped at capture *except* an allowlist, and that allowlist contains `q` —
|
|
139
|
+
* a search term, i.e. real user input. Writing "query values are stripped" here
|
|
140
|
+
* would have been a false reassurance in the one place a reader cannot afford
|
|
141
|
+
* one, so the allowlist is named instead of summarised.
|
|
142
|
+
*/
|
|
143
|
+
export declare function egressNotice(provider: AiProvider, model: string, pages: number, planned?: boolean): string;
|
|
144
|
+
/**
|
|
145
|
+
* What the provider itself said went wrong.
|
|
146
|
+
*
|
|
147
|
+
* Found by execution, not review: a Gemini key that is simply wrong comes back
|
|
148
|
+
* **400**, not 401 — so the previous message read `refused the call with HTTP
|
|
149
|
+
* 400`, said nothing about the key, and left the one person who could fix it in
|
|
150
|
+
* sixty seconds with nothing to go on. Mapping more status codes by hand is the
|
|
151
|
+
* wrong shape of fix (the list rots, and it is different per provider); the
|
|
152
|
+
* providers already answer the question in the body, and all three happen to
|
|
153
|
+
* use the same `{error:{message}}` envelope.
|
|
154
|
+
*
|
|
155
|
+
* Two guards. The body is untrusted text that lands in a terminal and in
|
|
156
|
+
* `classification.json`, so it is capped and flattened. And anything shaped like
|
|
157
|
+
* a credential is redacted before it is printed — a provider that echoes the
|
|
158
|
+
* request back would otherwise put the user's own key in a file they commit.
|
|
159
|
+
*/
|
|
160
|
+
export declare function providerError(res: {
|
|
161
|
+
status: number;
|
|
162
|
+
text: () => Promise<string>;
|
|
163
|
+
}): Promise<string>;
|
|
164
|
+
/**
|
|
165
|
+
* One page in, validated classifications out. Returns an empty result rather
|
|
166
|
+
* than throwing on a network failure, an unparseable reply or an empty page:
|
|
167
|
+
* this layer is optional by construction, and a broken key must never take down
|
|
168
|
+
* a deterministic scan that already succeeded.
|
|
169
|
+
*/
|
|
170
|
+
export declare function classifyPage(page: PageCapture, index: CaptureIndex, opts: ClassifyOptions): Promise<ClassifyResult>;
|