@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
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLASSIFICATION_FILENAME = exports.CLASSIFICATION_SCHEMA = void 0;
4
+ exports.entryPointsOf = entryPointsOf;
5
+ exports.offeredPaths = offeredPaths;
6
+ exports.buildPrompt = buildPrompt;
7
+ exports.parseClassifications = parseClassifications;
8
+ exports.validate = validate;
9
+ exports.collect = collect;
10
+ exports.egressNotice = egressNotice;
11
+ exports.providerError = providerError;
12
+ exports.classifyPage = classifyPage;
13
+ const taxonomy_1 = require("../taxonomy");
14
+ const citations_1 = require("../citations");
15
+ /**
16
+ * F2 — classify each entry point, offline, from F1's capture.
17
+ *
18
+ * The model never sees the app: it reads JSON that was captured deterministically,
19
+ * and every answer it gives must point back into that JSON. Nothing here opens a
20
+ * browser or touches the user's application.
21
+ *
22
+ * **On duplication, stated because this project punishes silent drift:** the
23
+ * provider/model catalogue is imported from `@ia-qa/self-healing` rather than
24
+ * copied — there must not be a third model list in this repo. The HTTP transport
25
+ * below *is* a second implementation, because self-healing does not export its
26
+ * own (`callModel` is internal). Any change to a provider's request shape owes an
27
+ * edit here as well as there.
28
+ */
29
+ /** The document writes, and the two strings a later stage keys on. */
30
+ exports.CLASSIFICATION_SCHEMA = 'qa-discovery-classification@1';
31
+ exports.CLASSIFICATION_FILENAME = 'classification.json';
32
+ const DEFAULTS = { timeoutMs: 30000, temperature: 0 };
33
+ /**
34
+ * The citable units of a page.
35
+ *
36
+ * A `<form>` is one entry point. A loose field is its own, because grouping them
37
+ * would need a path that does not exist in the capture and a citation must
38
+ * resolve to something real. If that proves too granular in practice, grouping is
39
+ * a v0.2 question — answered by looking at real output, not guessed now.
40
+ */
41
+ function entryPointsOf(page) {
42
+ const out = [];
43
+ page.forms.forEach((form, i) => {
44
+ out.push({ path: `pages/${page.page}.json#forms[${i}]`, kind: 'form', page: page.page, detail: form });
45
+ });
46
+ page.looseFields.forEach((field, i) => {
47
+ out.push({ path: `pages/${page.page}.json#looseFields[${i}]`, kind: 'field', page: page.page, detail: field });
48
+ });
49
+ return out;
50
+ }
51
+ /**
52
+ * What the model is given. Deliberately narrow: the page's identity and headings
53
+ * for context, and the entry points verbatim. It is not handed the whole capture,
54
+ * because a model given everything cites the most quotable thing rather than the
55
+ * most relevant one.
56
+ */
57
+ /**
58
+ * Every path the prompt offers, and therefore the only paths a citation may
59
+ * use. The model picks from a menu; it does not compose.
60
+ *
61
+ * This is the same guardrail `@ia-qa/self-healing` puts on its resolver — "the
62
+ * model chooses a live candidate by index, so it cannot emit a broken selector".
63
+ * Measured need: across three real runs the model cited `#page.description`,
64
+ * `#looseFields[0].detail` and a bare `#description`, all of which existed in the
65
+ * *prompt's* shape and none in the capture's. Resolving-and-rejecting caught
66
+ * them, but only after the fact; refusing a constructed path removes the whole
67
+ * class of error instead.
68
+ */
69
+ function offeredPaths(page, entryPoints) {
70
+ const at = (p) => `pages/${page.page}.json#${p}`;
71
+ const out = new Set([at('meta.title'), at('meta.description')]);
72
+ page.headings.slice(0, 25).forEach((_, i) => out.add(at(`headings[${i}]`)));
73
+ page.apiCalls.forEach((_, i) => out.add(at(`apiCalls[${i}]`)));
74
+ for (const e of entryPoints) {
75
+ out.add(e.path);
76
+ // A form's own fields are shown inside it and are the best evidence there
77
+ // is — "this form is authentication BECAUSE field[1] is a password" beats
78
+ // citing the page title. They are a sub-path of the entry point, so they
79
+ // still satisfy the no-self-citation rule.
80
+ if (e.kind === 'form') {
81
+ const fields = e.detail?.fields ?? [];
82
+ fields.forEach((_, j) => out.add(`${e.path}.fields[${j}]`));
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ function buildPrompt(page, entryPoints) {
88
+ // EVERY citable thing carries its `at` path, including the metadata.
89
+ // Measured on a real run: the first version showed title/description flattened
90
+ // and pathless while giving api calls an `at`, so the model cited
91
+ // `#page.description` — the shape of the prompt, not the shape of the capture,
92
+ // where it lives at `#meta.description`. The prompt was lying about the data,
93
+ // and the model was right to trust it. If a field can be cited, it is shown
94
+ // with the path that addresses it; nothing is left to be guessed.
95
+ const at = (p) => `pages/${page.page}.json#${p}`;
96
+ const context = {
97
+ page: page.page,
98
+ url: page.url,
99
+ title: { at: at('meta.title'), value: page.meta.title },
100
+ description: { at: at('meta.description'), value: page.meta.description },
101
+ headings: page.headings.slice(0, 25).map((h, i) => ({ at: at(`headings[${i}]`), level: h.level, text: h.text })),
102
+ apiCalls: page.apiCalls.map((c, i) => ({ at: at(`apiCalls[${i}]`), method: c.method, url: c.urlPattern, status: c.status })),
103
+ };
104
+ return [
105
+ 'You are classifying the entry points of a web page for a QA test plan.',
106
+ 'You are reading a capture that was taken deterministically. You cannot see the app.',
107
+ '',
108
+ (0, taxonomy_1.taxonomyForPrompt)(),
109
+ '',
110
+ 'PAGE CONTEXT (for understanding only — cite it if you use it):',
111
+ JSON.stringify(context, null, 2),
112
+ '',
113
+ // Flattened, not wrapped: an earlier version nested the field under a
114
+ // `detail` key and the model dutifully cited `#looseFields[0].detail`, a
115
+ // path that exists in the prompt and nowhere in the capture.
116
+ 'ENTRY POINTS to classify, each with the path that addresses it:',
117
+ JSON.stringify(entryPoints.map((e) => ({ at: e.path, kind: e.kind, ...e.detail })), null, 2),
118
+ '',
119
+ 'RULES.',
120
+ `1. Answer with ONE JSON object: {"classifications":[...]}. No prose outside it.`,
121
+ '2. One entry per entry point above, with "entryPoint" set to its exact path.',
122
+ `3. "intent" must be one of: ${taxonomy_1.INTENT_IDS.join(', ')} — or "${taxonomy_1.UNKNOWN_INTENT}".`,
123
+ `4. "sensitivity" must be one of: ${taxonomy_1.SENSITIVITY_IDS.join(', ')}.`,
124
+ '5. "evidence" is a non-empty array of paths, and each one MUST be copied verbatim',
125
+ ' from an "at" value above. Do not build, shorten, extend or guess a path — if it',
126
+ ' is not offered as an "at" above, you may not cite it. Anything else is discarded',
127
+ ' before it is even looked up.',
128
+ ' At least one citation MUST point somewhere other than the entry point itself:',
129
+ ' citing the thing you are classifying proves nothing, since it resolves by',
130
+ ' definition. Cite what SUPPORTS your reading — the page title, its description,',
131
+ ' a heading, an api call the page makes.',
132
+ '6. A citation may be a bare path, or {"path": "...", "expect": {"type": "password"}}',
133
+ ' to state what you believe is there. The second form is preferred: it proves you',
134
+ ' read the element rather than its neighbourhood.',
135
+ `7. "why" is one sentence a QA engineer reads. "confidence" is 0-1, your own honest`,
136
+ ' estimate. Do not inflate it — a low number is useful, a wrong label is not.',
137
+ `8. If the evidence does not support any intent, answer "${taxonomy_1.UNKNOWN_INTENT}". Refusing`,
138
+ ' is a correct answer and is preferred over a plausible guess.',
139
+ ].join('\n');
140
+ }
141
+ /** Pull the JSON object out of a model reply, tolerating fences and prose around it. */
142
+ function parseClassifications(text) {
143
+ if (!text || typeof text !== 'string')
144
+ return null;
145
+ const attempts = [text];
146
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
147
+ if (fenced)
148
+ attempts.push(fenced[1]);
149
+ const first = text.indexOf('{');
150
+ const last = text.lastIndexOf('}');
151
+ if (first !== -1 && last > first)
152
+ attempts.push(text.slice(first, last + 1));
153
+ for (const candidate of attempts) {
154
+ try {
155
+ const parsed = JSON.parse(candidate.trim());
156
+ const list = Array.isArray(parsed) ? parsed : parsed?.classifications;
157
+ if (Array.isArray(list))
158
+ return list;
159
+ }
160
+ catch {
161
+ /* try the next shape */
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ /**
167
+ * Validate one classification against the closed taxonomy, then against the
168
+ * capture. Order matters: an off-list label is rejected before any citation is
169
+ * resolved, because a model inventing vocabulary is not a model whose evidence
170
+ * is worth checking.
171
+ */
172
+ function validate(raw, index, offered) {
173
+ const fail = (reason, rejected = []) => ({
174
+ ok: false,
175
+ dropped: { classification: raw, reason, rejected },
176
+ });
177
+ if (typeof raw?.entryPoint !== 'string')
178
+ return fail('No entryPoint path.');
179
+ if (!(0, taxonomy_1.isIntent)(raw.intent))
180
+ return fail(`Intent "${raw.intent}" is not in ${taxonomy_1.TAXONOMY_VERSION}.`);
181
+ if (!(0, taxonomy_1.isSensitivity)(raw.sensitivity))
182
+ return fail(`Sensitivity "${raw.sensitivity}" is not in ${taxonomy_1.TAXONOMY_VERSION}.`);
183
+ if (typeof raw.confidence !== 'number' || raw.confidence < 0 || raw.confidence > 1) {
184
+ return fail(`Confidence must be a number in 0-1, got ${JSON.stringify(raw.confidence)}.`);
185
+ }
186
+ if (typeof raw.why !== 'string' || raw.why.trim() === '')
187
+ return fail('No "why" a human could read.');
188
+ // A citation that points at the entry point being classified proves nothing:
189
+ // the path resolves because it IS the subject. Measured on the first real
190
+ // model run — every classification cited itself, the verifier answered "✓"
191
+ // three times, and had checked nothing. Evidence must come from somewhere
192
+ // other than the thing being claimed about.
193
+ const citedPaths = (raw.evidence ?? []).map((c) => (typeof c === 'string' ? c : c?.path));
194
+ if (!citedPaths.some((p) => p !== raw.entryPoint)) {
195
+ return fail('Every citation is the entry point itself — a self-citation is not evidence.');
196
+ }
197
+ // A path the prompt never offered was constructed, not read. Refused before
198
+ // resolution: the point is that the model picks from a menu.
199
+ if (offered) {
200
+ const invented = citedPaths.filter((p) => typeof p !== 'string' || !offered.has(p));
201
+ if (invented.length > 0) {
202
+ return fail(`Cited path(s) that were never offered: ${invented.join(', ')}.`);
203
+ }
204
+ }
205
+ const verified = (0, citations_1.verifyClaim)({ evidence: raw.evidence ?? [] }, index);
206
+ if (!verified.kept)
207
+ return fail(verified.reason ?? 'Citations did not check out.', verified.rejected);
208
+ const value = {
209
+ entryPoint: raw.entryPoint,
210
+ intent: raw.intent,
211
+ sensitivity: raw.sensitivity,
212
+ confidence: raw.confidence,
213
+ why: raw.why.trim(),
214
+ evidence: raw.evidence ?? [],
215
+ };
216
+ // Deterministic, never the model's call: which existing checks this pulls in.
217
+ const tools = (0, taxonomy_1.securityToolsFor)(value.intent, value.sensitivity);
218
+ if (tools.length > 0)
219
+ value.securityTools = tools;
220
+ return { ok: true, value };
221
+ }
222
+ /** Sort validated classifications into kept / refused, and collect the drops. */
223
+ function collect(raws, index, offered) {
224
+ const kept = [];
225
+ const refused = [];
226
+ const dropped = [];
227
+ for (const raw of raws) {
228
+ const r = validate(raw, index, offered);
229
+ if (!r.ok) {
230
+ dropped.push(r.dropped);
231
+ }
232
+ else if (r.value.intent === taxonomy_1.UNKNOWN_INTENT) {
233
+ // Not a failure: the model looked and said it could not tell. Kept
234
+ // separately so a human sees what needs a person, and so nobody reads a
235
+ // short `kept` list as "the app has few entry points".
236
+ refused.push(r.value);
237
+ }
238
+ else {
239
+ kept.push(r.value);
240
+ }
241
+ }
242
+ return { taxonomyVersion: taxonomy_1.TAXONOMY_VERSION, kept, refused, dropped };
243
+ }
244
+ // ── Provider transport ───────────────────────────────────────────────────────
245
+ const HOSTS = {
246
+ anthropic: 'api.anthropic.com',
247
+ openai: 'api.openai.com',
248
+ google: 'generativelanguage.googleapis.com',
249
+ };
250
+ /**
251
+ * What leaves this machine, said before it leaves — the one thing that makes
252
+ * "everything is local except the model call" a promise rather than a claim.
253
+ *
254
+ * It lives beside `HOSTS` on purpose: the notice names the very constant
255
+ * `buildRequest` sends to, so a new provider cannot be reachable while the
256
+ * notice still lists three. And it is derived from `buildPrompt` — every line
257
+ * below is a field that function actually puts in the payload; adding one there
258
+ * owes a line here.
259
+ *
260
+ * Written to stderr in every mode, `--json` included: it is a security
261
+ * announcement, not decoration, and stdout must stay one parseable document.
262
+ *
263
+ * The last clause is the one that took a check to get right. Query values are
264
+ * stripped at capture *except* an allowlist, and that allowlist contains `q` —
265
+ * a search term, i.e. real user input. Writing "query values are stripped" here
266
+ * would have been a false reassurance in the one place a reader cannot afford
267
+ * one, so the allowlist is named instead of summarised.
268
+ */
269
+ function egressNotice(provider, model, pages, planned = false) {
270
+ const n = `${pages} page${pages === 1 ? '' : 's'}`;
271
+ return (`\n 🌐 ${planned ? 'Would send' : 'Sending'} ${n} to ${HOSTS[provider]} (${provider} · ${model}) — your key, your account.\n` +
272
+ ` Leaves this machine: each page's full URL, title and description, up to 25 headings,\n` +
273
+ ` every observed API call (method, normalised path, status), and each form field's name,\n` +
274
+ ` type, label and selector. Headings and labels are live text from your app.\n` +
275
+ ` Query values are stripped at capture EXCEPT an allowlist that includes \`q\` — a search\n` +
276
+ ` term reaches the model as typed (\`capture.safeQueryParams\` in config.json narrows it).\n` +
277
+ ` Does NOT leave: page HTML, screenshots, cookies or your session file, your test files,\n` +
278
+ ` your API key (sent as a header to ${HOSTS[provider]} only).\n` +
279
+ ` ${planned ? '' : 'Ctrl-C to stop. '}Deterministic \`ia-qa-discover\` sends nothing, ever.\n`);
280
+ }
281
+ function buildRequest(provider, model, apiKey, prompt, temperature) {
282
+ if (provider === 'anthropic') {
283
+ const body = { model, max_tokens: 4096, messages: [{ role: 'user', content: prompt }] };
284
+ if (temperature !== undefined)
285
+ body.temperature = temperature;
286
+ return {
287
+ url: `https://${HOSTS.anthropic}/v1/messages`,
288
+ headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
289
+ body,
290
+ };
291
+ }
292
+ if (provider === 'openai') {
293
+ const body = { model, messages: [{ role: 'user', content: prompt }] };
294
+ if (temperature !== undefined)
295
+ body.temperature = temperature;
296
+ return {
297
+ url: `https://${HOSTS.openai}/v1/chat/completions`,
298
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
299
+ body,
300
+ };
301
+ }
302
+ const body = { contents: [{ parts: [{ text: prompt }] }] };
303
+ if (temperature !== undefined)
304
+ body.generationConfig = { temperature };
305
+ return {
306
+ url: `https://${HOSTS.google}/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`,
307
+ headers: { 'content-type': 'application/json' },
308
+ body,
309
+ };
310
+ }
311
+ function extractText(provider, data) {
312
+ if (provider === 'anthropic')
313
+ return data?.content?.[0]?.text ?? '';
314
+ if (provider === 'openai')
315
+ return data?.choices?.[0]?.message?.content ?? '';
316
+ return data?.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
317
+ }
318
+ async function attempt(opts, prompt, temperature) {
319
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
320
+ if (typeof fetchFn !== 'function')
321
+ return null;
322
+ const req = buildRequest(opts.provider, opts.model, opts.apiKey, prompt, temperature);
323
+ const controller = new AbortController();
324
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULTS.timeoutMs);
325
+ try {
326
+ return await fetchFn(req.url, {
327
+ method: 'POST',
328
+ headers: req.headers,
329
+ body: JSON.stringify(req.body),
330
+ signal: controller.signal,
331
+ });
332
+ }
333
+ catch {
334
+ return null; // network error or timeout — never throws at the caller
335
+ }
336
+ finally {
337
+ clearTimeout(timer);
338
+ }
339
+ }
340
+ /**
341
+ * What the provider itself said went wrong.
342
+ *
343
+ * Found by execution, not review: a Gemini key that is simply wrong comes back
344
+ * **400**, not 401 — so the previous message read `refused the call with HTTP
345
+ * 400`, said nothing about the key, and left the one person who could fix it in
346
+ * sixty seconds with nothing to go on. Mapping more status codes by hand is the
347
+ * wrong shape of fix (the list rots, and it is different per provider); the
348
+ * providers already answer the question in the body, and all three happen to
349
+ * use the same `{error:{message}}` envelope.
350
+ *
351
+ * Two guards. The body is untrusted text that lands in a terminal and in
352
+ * `classification.json`, so it is capped and flattened. And anything shaped like
353
+ * a credential is redacted before it is printed — a provider that echoes the
354
+ * request back would otherwise put the user's own key in a file they commit.
355
+ */
356
+ async function providerError(res) {
357
+ const status = `HTTP ${res.status}`;
358
+ // Kept alongside the provider's own words, not replaced by them: a 401 with
359
+ // an empty body still has exactly one likely cause, and an existing test went
360
+ // red the moment this hint was dropped — correctly.
361
+ const hint = res.status === 401 || res.status === 403 ? ' Check the API key.' : '';
362
+ let body = '';
363
+ try {
364
+ body = await res.text();
365
+ }
366
+ catch {
367
+ return `${status}.${hint}`;
368
+ }
369
+ let message = '';
370
+ try {
371
+ const parsed = JSON.parse(body);
372
+ message = parsed?.error?.message ?? parsed?.message ?? '';
373
+ }
374
+ catch {
375
+ message = body;
376
+ }
377
+ message = String(message).replace(/\s+/g, ' ').trim();
378
+ // sk-…, AIza…, ghp_…, and any long opaque run that could be a secret.
379
+ message = message.replace(/\b(?:sk-|AIza|ghp_|gsk_)[A-Za-z0-9_\-]{8,}/g, '[redacted]');
380
+ if (!message)
381
+ return `${status}.${hint}`;
382
+ if (message.length > 200)
383
+ message = message.slice(0, 200) + '…';
384
+ if (hint && !/[.!?…]$/.test(message))
385
+ message += '.';
386
+ return `${status} — ${message}${hint}`;
387
+ }
388
+ /**
389
+ * One page in, validated classifications out. Returns an empty result rather
390
+ * than throwing on a network failure, an unparseable reply or an empty page:
391
+ * this layer is optional by construction, and a broken key must never take down
392
+ * a deterministic scan that already succeeded.
393
+ */
394
+ async function classifyPage(page, index, opts) {
395
+ const entryPoints = entryPointsOf(page);
396
+ const empty = (failed) => ({
397
+ taxonomyVersion: taxonomy_1.TAXONOMY_VERSION,
398
+ kept: [],
399
+ refused: [],
400
+ dropped: [],
401
+ ...(failed ? { failed: { reason: failed } } : {}),
402
+ });
403
+ // No entry points is a genuine result: nothing to classify, nothing failed.
404
+ if (entryPoints.length === 0)
405
+ return empty();
406
+ const prompt = buildPrompt(page, entryPoints);
407
+ const offered = offeredPaths(page, entryPoints);
408
+ // The newest models reject sampling params outright (Anthropic Opus 4.8 /
409
+ // Sonnet 5, OpenAI reasoning models) with a 400. Rather than maintain a
410
+ // capability matrix, send temperature and retry once without it — the same
411
+ // trick, and the same reasoning, as self-healing's resolver.
412
+ let res = await attempt(opts, prompt, opts.temperature ?? DEFAULTS.temperature);
413
+ if (res && res.status === 400)
414
+ res = await attempt(opts, prompt, undefined);
415
+ if (!res)
416
+ return empty(`No reply from ${opts.provider} — network error or timeout.`);
417
+ if (!res.ok)
418
+ return empty(`${opts.provider} refused the call: ${await providerError(res)}`);
419
+ let text;
420
+ try {
421
+ text = extractText(opts.provider, await res.json());
422
+ }
423
+ catch {
424
+ return empty(`Could not read ${opts.provider}'s reply as JSON.`);
425
+ }
426
+ const raws = parseClassifications(text);
427
+ if (!raws)
428
+ return empty(`${opts.provider} replied, but not with the JSON that was asked for.`);
429
+ return collect(raws, index, offered);
430
+ }
431
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/ai/classify.ts"],"names":[],"mappings":";;;AAqGA,sCASC;AAoBD,oCAiBC;AAED,kCAsDC;AAGD,oDAmBC;AAQD,4BAgDC;AAGD,0BAmBC;AA6BD,oCAaC;AA8ED,sCA0BC;AAQD,oCAkCC;AAzeD,0CAUqB;AACrB,4CAAmG;AAEnG;;;;;;;;;;;;;GAaG;AAEH,uEAAuE;AAC1D,QAAA,qBAAqB,GAAG,+BAA+B,CAAC;AACxD,QAAA,uBAAuB,GAAG,qBAAqB,CAAC;AAY7D,MAAM,QAAQ,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;AAiDtD;;;;;;;GAOG;AACH,SAAgB,aAAa,CAAC,IAAiB;IAC7C,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC7B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QACpC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,IAAI,qBAAqB,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACjH,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH;;;;;;;;;;;GAWG;AACH,SAAgB,YAAY,CAAC,IAAiB,EAAE,WAAyB;IACvE,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,SAAS,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC;IACzD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAS,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAChB,0EAA0E;QAC1E,0EAA0E;QAC1E,yEAAyE;QACzE,2CAA2C;QAC3C,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACtB,MAAM,MAAM,GAAI,CAAC,CAAC,MAAiC,EAAE,MAAM,IAAI,EAAE,CAAC;YAClE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAgB,WAAW,CAAC,IAAiB,EAAE,WAAyB;IACtE,qEAAqE;IACrE,+EAA+E;IAC/E,kEAAkE;IAClE,+EAA+E;IAC/E,8EAA8E;IAC9E,4EAA4E;IAC5E,kEAAkE;IAClE,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,SAAS,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC;IACzD,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;QACvD,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;QACzE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAChH,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;KAC7H,CAAC;IAEF,OAAO;QACL,wEAAwE;QACxE,qFAAqF;QACrF,EAAE;QACF,IAAA,4BAAiB,GAAE;QACnB,EAAE;QACF,gEAAgE;QAChE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,EAAE;QACF,sEAAsE;QACtE,yEAAyE;QACzE,6DAA6D;QAC7D,iEAAiE;QACjE,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAI,CAAC,CAAC,MAAiB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACxG,EAAE;QACF,QAAQ;QACR,iFAAiF;QACjF,8EAA8E;QAC9E,+BAA+B,qBAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,yBAAc,IAAI;QAChF,oCAAoC,0BAAe,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QACjE,mFAAmF;QACnF,oFAAoF;QACpF,qFAAqF;QACrF,iCAAiC;QACjC,kFAAkF;QAClF,8EAA8E;QAC9E,mFAAmF;QACnF,2CAA2C;QAC3C,sFAAsF;QACtF,oFAAoF;QACpF,oDAAoD;QACpD,oFAAoF;QACpF,gFAAgF;QAChF,2DAA2D,yBAAc,aAAa;QACtF,iEAAiE;KAClE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,wFAAwF;AACxF,SAAgB,oBAAoB,CAAC,IAAY;IAC/C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,KAAK;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IAE7E,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,eAAe,CAAC;YACtE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,wBAAwB;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,QAAQ,CAAC,GAA4B,EAAE,KAAmB,EAAE,OAAqB;IAC/F,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,WAA8C,EAAE,EAAiD,EAAE,CAAC,CAAC;QACjI,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE;KACnD,CAAC,CAAC;IAEH,IAAI,OAAO,GAAG,EAAE,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC5E,IAAI,CAAC,IAAA,mBAAQ,EAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,eAAe,2BAAgB,GAAG,CAAC,CAAC;IAChG,IAAI,CAAC,IAAA,wBAAa,EAAC,GAAG,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC,gBAAgB,GAAG,CAAC,WAAW,eAAe,2BAAgB,GAAG,CAAC,CAAC;IACpH,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;QACnF,OAAO,IAAI,CAAC,2CAA2C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC5F,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,8BAA8B,CAAC,CAAC;IAEtG,6EAA6E;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,4CAA4C;IAC5C,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1F,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,6EAA6E,CAAC,CAAC;IAC7F,CAAC;IAED,4EAA4E;IAC5E,6DAA6D;IAC7D,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,0CAA0C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IACtE,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,8BAA8B,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEtG,MAAM,KAAK,GAAmB;QAC5B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,MAAM,EAAE,GAAG,CAAC,MAAO;QACnB,WAAW,EAAE,GAAG,CAAC,WAAY;QAC7B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;QACnB,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE;KAC7B,CAAC;IACF,8EAA8E;IAC9E,MAAM,KAAK,GAAG,IAAA,2BAAgB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAChE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;IAClD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,iFAAiF;AACjF,SAAgB,OAAO,CAAC,IAA+B,EAAE,KAAmB,EAAE,OAAqB;IACjG,MAAM,IAAI,GAAqB,EAAE,CAAC;IAClC,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,MAAM,OAAO,GAA4B,EAAE,CAAC;IAE5C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;aAAM,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,yBAAc,EAAE,CAAC;YAC7C,mEAAmE;YACnE,wEAAwE;YACxE,uDAAuD;YACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,eAAe,EAAE,2BAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACvE,CAAC;AAED,gFAAgF;AAEhF,MAAM,KAAK,GAA+B;IACxC,SAAS,EAAE,mBAAmB;IAC9B,MAAM,EAAE,gBAAgB;IACxB,MAAM,EAAE,mCAAmC;CAC5C,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,YAAY,CAAC,QAAoB,EAAE,KAAa,EAAE,KAAa,EAAE,OAAO,GAAG,KAAK;IAC9F,MAAM,CAAC,GAAG,GAAG,KAAK,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IACnD,OAAO,CACL,WAAW,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,QAAQ,MAAM,KAAK,+BAA+B;QAChI,8FAA8F;QAC9F,gGAAgG;QAChG,oFAAoF;QACpF,iGAAiG;QACjG,kGAAkG;QAClG,gGAAgG;QAChG,2CAA2C,KAAK,CAAC,QAAQ,CAAC,WAAW;QACrE,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,yDAAyD,CACpG,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,QAAoB,EACpB,KAAa,EACb,MAAc,EACd,MAAc,EACd,WAA+B;IAE/B,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QAC7F,IAAI,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC9D,OAAO;YACL,GAAG,EAAE,WAAW,KAAK,CAAC,SAAS,cAAc;YAC7C,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,EAAE,mBAAmB,EAAE,YAAY,EAAE;YACvG,IAAI;SACL,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QAC3E,IAAI,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC9D,OAAO;YACL,GAAG,EAAE,WAAW,KAAK,CAAC,MAAM,sBAAsB;YAClD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE;YAClF,IAAI;SACL,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAChE,IAAI,WAAW,KAAK,SAAS;QAAE,IAAI,CAAC,gBAAgB,GAAG,EAAE,WAAW,EAAE,CAAC;IACvE,OAAO;QACL,GAAG,EAAE,WAAW,KAAK,CAAC,MAAM,kBAAkB,kBAAkB,CAAC,KAAK,CAAC,wBAAwB,kBAAkB,CAAC,MAAM,CAAC,EAAE;QAC3H,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI;KACL,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,QAAoB,EAAE,IAAS;IAClD,IAAI,QAAQ,KAAK,WAAW;QAAE,OAAO,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;IACpE,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC;IAC7E,OAAO,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAqB,EAAE,MAAc,EAAE,WAA+B;IAC3F,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAK,UAAU,CAAC,KAAsB,CAAC;IACnE,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC/C,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IACtF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC;IACzF,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAC9B,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wDAAwD;IACvE,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,aAAa,CAAC,GAAoD;IACtF,MAAM,MAAM,GAAG,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,8EAA8E;IAC9E,oDAAoD;IACpD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;IAC7B,CAAC;IACD,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,GAAG,MAAM,EAAE,KAAK,EAAE,OAAO,IAAI,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,sEAAsE;IACtE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,6CAA6C,EAAE,YAAY,CAAC,CAAC;IACvF,IAAI,CAAC,OAAO;QAAE,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;IACzC,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;IAChE,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,GAAG,CAAC;IACrD,OAAO,GAAG,MAAM,MAAM,OAAO,GAAG,IAAI,EAAE,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAAC,IAAiB,EAAE,KAAmB,EAAE,IAAqB;IAC9F,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,CAAC,MAAe,EAAkB,EAAE,CAAC,CAAC;QAClD,eAAe,EAAE,2BAAgB;QACjC,IAAI,EAAE,EAAE;QACR,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;QACX,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAC,CAAC;IACH,4EAA4E;IAC5E,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,EAAE,CAAC;IAE7C,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAEhD,0EAA0E;IAC1E,wEAAwE;IACxE,2EAA2E;IAC3E,6DAA6D;IAC7D,IAAI,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC;IAChF,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5E,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC,iBAAiB,IAAI,CAAC,QAAQ,8BAA8B,CAAC,CAAC;IACrF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,sBAAsB,MAAM,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAE5F,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC,kBAAkB,IAAI,CAAC,QAAQ,mBAAmB,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,qDAAqD,CAAC,CAAC;IAC/F,OAAO,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Page-structure extraction — meta, headings, forms.
3
+ *
4
+ * Self-contained (no imports, no closures over module scope, no TypeScript
5
+ * syntax) because Playwright serializes this via `toString()` and injects it
6
+ * into the page with `page.evaluate()`. Deliberately NOT the same extraction
7
+ * as `@ia-qa/self-healing`'s `browser/extract.js` (interactive elements for
8
+ * selector healing) — this reads what recon needs: what the page is about
9
+ * (title/description/headings) and what it can submit (forms + fields).
10
+ */
11
+ export function extractPageInPage(): {
12
+ meta: {
13
+ title: any;
14
+ description: string;
15
+ lang: string;
16
+ };
17
+ headings: {
18
+ level: number;
19
+ text: any;
20
+ }[];
21
+ forms: {
22
+ selector: string;
23
+ method: string | null;
24
+ action: any;
25
+ hasSubmit: boolean;
26
+ fields: {
27
+ type: any;
28
+ name: any;
29
+ selector: string;
30
+ stableSelector: boolean;
31
+ required: any;
32
+ }[];
33
+ }[];
34
+ looseFields: {
35
+ type: any;
36
+ name: any;
37
+ selector: string;
38
+ stableSelector: boolean;
39
+ required: any;
40
+ }[];
41
+ };