@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
|
@@ -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":"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"}
|
|
@@ -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
|
+
};
|