@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.
Files changed (84) hide show
  1. package/README.md +378 -0
  2. package/ROADMAP.md +321 -0
  3. package/dist/ai/classify.d.ts +170 -0
  4. package/dist/ai/classify.js +431 -0
  5. package/dist/ai/classify.js.map +1 -0
  6. package/dist/browser/extractPage.d.ts +41 -0
  7. package/dist/browser/extractPage.js +427 -0
  8. package/dist/browser/extractPage.js.map +1 -0
  9. package/dist/browser/openables.d.ts +19 -0
  10. package/dist/browser/openables.js +127 -0
  11. package/dist/browser/openables.js.map +1 -0
  12. package/dist/capture/page.d.ts +128 -0
  13. package/dist/capture/page.js +78 -0
  14. package/dist/capture/page.js.map +1 -0
  15. package/dist/citations.d.ts +80 -0
  16. package/dist/citations.js +197 -0
  17. package/dist/citations.js.map +1 -0
  18. package/dist/classificationView.d.ts +118 -0
  19. package/dist/classificationView.js +178 -0
  20. package/dist/classificationView.js.map +1 -0
  21. package/dist/cli/args.d.ts +3 -0
  22. package/dist/cli/args.js +38 -0
  23. package/dist/cli/args.js.map +1 -0
  24. package/dist/cli/history.d.ts +1 -0
  25. package/dist/cli/history.js +35 -0
  26. package/dist/cli/history.js.map +1 -0
  27. package/dist/cli/index.d.ts +5 -0
  28. package/dist/cli/index.js +172 -0
  29. package/dist/cli/index.js.map +1 -0
  30. package/dist/cli/login.d.ts +29 -0
  31. package/dist/cli/login.js +179 -0
  32. package/dist/cli/login.js.map +1 -0
  33. package/dist/cli/scan.d.ts +11 -0
  34. package/dist/cli/scan.js +246 -0
  35. package/dist/cli/scan.js.map +1 -0
  36. package/dist/cli-ai/index.d.ts +7 -0
  37. package/dist/cli-ai/index.js +360 -0
  38. package/dist/cli-ai/index.js.map +1 -0
  39. package/dist/config.d.ts +112 -0
  40. package/dist/config.js +178 -0
  41. package/dist/config.js.map +1 -0
  42. package/dist/diff.d.ts +12 -0
  43. package/dist/diff.js +43 -0
  44. package/dist/diff.js.map +1 -0
  45. package/dist/explore.d.ts +59 -0
  46. package/dist/explore.js +119 -0
  47. package/dist/explore.js.map +1 -0
  48. package/dist/healingLink.d.ts +30 -0
  49. package/dist/healingLink.js +127 -0
  50. package/dist/healingLink.js.map +1 -0
  51. package/dist/history.d.ts +38 -0
  52. package/dist/history.js +120 -0
  53. package/dist/history.js.map +1 -0
  54. package/dist/htmlReport.d.ts +2 -0
  55. package/dist/htmlReport.js +371 -0
  56. package/dist/htmlReport.js.map +1 -0
  57. package/dist/index.d.ts +24 -0
  58. package/dist/index.js +65 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/mcp/sandbox.d.ts +9 -0
  61. package/dist/mcp/sandbox.js +82 -0
  62. package/dist/mcp/sandbox.js.map +1 -0
  63. package/dist/mcp/server.d.ts +120 -0
  64. package/dist/mcp/server.js +315 -0
  65. package/dist/mcp/server.js.map +1 -0
  66. package/dist/network.d.ts +60 -0
  67. package/dist/network.js +99 -0
  68. package/dist/network.js.map +1 -0
  69. package/dist/overview.d.ts +40 -0
  70. package/dist/overview.js +370 -0
  71. package/dist/overview.js.map +1 -0
  72. package/dist/overviewFile.d.ts +31 -0
  73. package/dist/overviewFile.js +101 -0
  74. package/dist/overviewFile.js.map +1 -0
  75. package/dist/scan.d.ts +159 -0
  76. package/dist/scan.js +402 -0
  77. package/dist/scan.js.map +1 -0
  78. package/dist/sharedCalls.d.ts +28 -0
  79. package/dist/sharedCalls.js +61 -0
  80. package/dist/sharedCalls.js.map +1 -0
  81. package/dist/taxonomy.d.ts +77 -0
  82. package/dist/taxonomy.js +153 -0
  83. package/dist/taxonomy.js.map +1 -0
  84. 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>;