@ia-qa/qa-discovery 0.2.3 → 0.4.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/dist/cli/args.js CHANGED
@@ -23,6 +23,7 @@ exports.KNOWN_FLAGS = {
23
23
  login: ['--url', '--session'],
24
24
  history: ['--json', '--limit'],
25
25
  coverage: ['--json', '--report', '--open'],
26
+ generate: ['--json', '--out', '--force'],
26
27
  };
27
28
  function rejectUnknownFlags(command, args) {
28
29
  const known = exports.KNOWN_FLAGS[command];
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAuBA,gDAQC;AA/BD,sGAAsG;AACzF,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE;QACJ,SAAS;QACT,OAAO;QACP,aAAa;QACb,eAAe;QACf,WAAW;QACX,cAAc;QACd,cAAc;QACd,QAAQ;QACR,eAAe;QACf,qBAAqB;QACrB,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,QAAQ;KACT;IACD,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC;CAC3C,CAAC;AAEF,SAAgB,kBAAkB,CAAC,OAAe,EAAE,IAAc;IAChE,MAAM,KAAK,GAAG,mBAAW,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,yBAAyB,OAAO,kDAAkD,CAAC,CAAC;IACxH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAwBA,gDAQC;AAhCD,sGAAsG;AACzF,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE;QACJ,SAAS;QACT,OAAO;QACP,aAAa;QACb,eAAe;QACf,WAAW;QACX,cAAc;QACd,cAAc;QACd,QAAQ;QACR,eAAe;QACf,qBAAqB;QACrB,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,QAAQ;KACT;IACD,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC;IAC1C,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;CACzC,CAAC;AAEF,SAAgB,kBAAkB,CAAC,OAAe,EAAE,IAAc;IAChE,MAAM,KAAK,GAAG,mBAAW,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,yBAAyB,OAAO,kDAAkD,CAAC,CAAC;IACxH,CAAC;AACH,CAAC"}
@@ -0,0 +1,56 @@
1
+ import type { PageMapping } from '@ia-qa/self-healing';
2
+ /**
3
+ * `ia-qa-discover generate` — a starting suite for the pages nothing tests.
4
+ *
5
+ * **Why it lives here and not in `@ia-qa/self-healing`.** It needs both halves: the contract,
6
+ * to name elements the way healing will repair them, and the capture, to know which fields a
7
+ * page takes and which of them carry a durable identity. Discovery already depends on
8
+ * healing; the reverse would invert a publish order that exists to be enforced. So the
9
+ * package that already holds both is the one that gets the verb.
10
+ *
11
+ * **What it proposes, never applies.** Files land in their own directory and an existing one
12
+ * is never overwritten without `--force` — the same restraint as `discover --apply` and
13
+ * `heal skill`. A generated test is a proposal a human reads, and nothing here adds it to
14
+ * anyone's suite.
15
+ *
16
+ * **Default target is the gap, not the app.** With no arguments it generates for the pages
17
+ * the coverage map says nothing tests, because generating over pages already covered is how
18
+ * a suite doubles in size without covering one more thing. Named pages override that.
19
+ */
20
+ export declare const GENERATED_DIRNAME = "generated";
21
+ export declare function generatedDir(cwd?: string): string;
22
+ export declare function runGenerate(args: string[]): Promise<void>;
23
+ /**
24
+ * Whether healing would ever read what was just written.
25
+ *
26
+ * This fires on the default, and that is the point. Measured on a real project: the specs
27
+ * landed in `.ia-qa-discovery/generated/` while healing's `testPaths` was `["tests/"]`, so
28
+ * `ingest` inventoried nothing from them — no name drift would escalate, `fix` would never
29
+ * rewrite them, and "born already maintained" was simply false. The first check of it looked
30
+ * green for the wrong reason: the placeholders were in `usage.json` because the project's own
31
+ * suite already used them, and every recorded site was in `tests/`.
32
+ *
33
+ * Writing into someone's suite by default would be the other error — this verb produces
34
+ * proposals, and a proposal does not install itself. So the default stays out of the way and
35
+ * says so, with both remedies, only when it is actually out of reach.
36
+ */
37
+ export declare function healingReach(outDir: string, cwd?: string): {
38
+ covered: boolean;
39
+ why: string;
40
+ };
41
+ /**
42
+ * The contract for a captured page, by name and then by URL.
43
+ *
44
+ * The URL fallback is not belt-and-braces, it is the ordinary case on a configured project.
45
+ * Both packages resolve a page name through the same `pageNameFromUrl`, but healing's
46
+ * `config.pages` may *declare* a name, and that declaration wins there and is invisible here:
47
+ * measured on a real app, discovery captured `/` as `home` while the contract for the very
48
+ * same URL was filed as `login`. Matching on the name alone found nothing, so the generator
49
+ * fell back to the page's own placeholder text — a locator that works, but not the one the
50
+ * contract holds and therefore not one healing will rewrite. Silently producing the weaker
51
+ * form is exactly the failure this whole feature is built to avoid.
52
+ *
53
+ * A page with no contract at all is still generated, from its capture alone — thinner, and
54
+ * never fatal.
55
+ */
56
+ export declare function contractOf(name: string, url: string, mappingPath: string): PageMapping | null;
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GENERATED_DIRNAME = void 0;
37
+ exports.generatedDir = generatedDir;
38
+ exports.runGenerate = runGenerate;
39
+ exports.healingReach = healingReach;
40
+ exports.contractOf = contractOf;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const self_healing_1 = require("@ia-qa/self-healing");
44
+ const config_1 = require("../config");
45
+ const coverage_1 = require("../coverage");
46
+ const generate_1 = require("../generate");
47
+ /** `--out <dir>`, and the remaining arguments with it removed. */
48
+ function takeValueFlag(args, flag, fallback) {
49
+ const at = args.indexOf(flag);
50
+ if (at === -1)
51
+ return { value: fallback, rest: args };
52
+ const value = args[at + 1];
53
+ if (value === undefined || value.startsWith('-')) {
54
+ throw new Error(`${flag} needs a directory, e.g. \`${flag} tests/generated\`.`);
55
+ }
56
+ return { value: path.resolve(process.cwd(), value), rest: [...args.slice(0, at), ...args.slice(at + 2)] };
57
+ }
58
+ /**
59
+ * `ia-qa-discover generate` — a starting suite for the pages nothing tests.
60
+ *
61
+ * **Why it lives here and not in `@ia-qa/self-healing`.** It needs both halves: the contract,
62
+ * to name elements the way healing will repair them, and the capture, to know which fields a
63
+ * page takes and which of them carry a durable identity. Discovery already depends on
64
+ * healing; the reverse would invert a publish order that exists to be enforced. So the
65
+ * package that already holds both is the one that gets the verb.
66
+ *
67
+ * **What it proposes, never applies.** Files land in their own directory and an existing one
68
+ * is never overwritten without `--force` — the same restraint as `discover --apply` and
69
+ * `heal skill`. A generated test is a proposal a human reads, and nothing here adds it to
70
+ * anyone's suite.
71
+ *
72
+ * **Default target is the gap, not the app.** With no arguments it generates for the pages
73
+ * the coverage map says nothing tests, because generating over pages already covered is how
74
+ * a suite doubles in size without covering one more thing. Named pages override that.
75
+ */
76
+ exports.GENERATED_DIRNAME = 'generated';
77
+ function generatedDir(cwd = (0, config_1.baseDir)()) {
78
+ return path.join(cwd, '.ia-qa-discovery', exports.GENERATED_DIRNAME);
79
+ }
80
+ async function runGenerate(args) {
81
+ const json = args.includes('--json');
82
+ const force = args.includes('--force');
83
+ const { value: outDir, rest } = takeValueFlag(args, '--out', generatedDir());
84
+ const named = rest.filter((a) => !a.startsWith('-'));
85
+ const surface = (0, coverage_1.loadSurface)((0, config_1.captureDir)());
86
+ if (!surface) {
87
+ return refuse(json, 'No capture to generate from.', ['Run `ia-qa-discover scan` first — a test cannot be written for a page nobody has looked at.']);
88
+ }
89
+ const byName = new Map(surface.pages.map((p) => [p.page, p]));
90
+ let targets;
91
+ if (named.length > 0) {
92
+ const unknown = named.filter((n) => !byName.has(n));
93
+ if (unknown.length > 0) {
94
+ return refuse(json, `Not in this capture: ${unknown.join(', ')}.`, [
95
+ `Pages captured: ${[...byName.keys()].join(', ')}`,
96
+ ]);
97
+ }
98
+ targets = named;
99
+ }
100
+ else {
101
+ const config = (() => {
102
+ try {
103
+ return (0, config_1.loadConfig)();
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ })();
109
+ const coverage = (0, coverage_1.computeCoverage)({
110
+ captureDirPath: (0, config_1.captureDir)(),
111
+ healingDirPath: path.join((0, config_1.baseDir)(), coverage_1.HEALING_DIR),
112
+ scope: config?.outOfScope,
113
+ });
114
+ if (!coverage.ok) {
115
+ // Generating for every captured page instead would bury the pages that need a test
116
+ // under the ones already covered — a bigger suite, not a better one. The refusal names
117
+ // the measurement that is missing, and the pages remain reachable by name.
118
+ return refuse(json, `Cannot tell which pages are untested: ${coverage.refusal.message}`, [
119
+ ...coverage.refusal.available,
120
+ `Or name the pages yourself: ia-qa-discover generate <page…>`,
121
+ ]);
122
+ }
123
+ targets = coverage.map.uncovered.map((u) => u.page);
124
+ if (targets.length === 0) {
125
+ return refuse(json, 'Every page this scan reached is already visited by your suite.', [
126
+ `Name a page to generate for it anyway: ia-qa-discover generate <page…>`,
127
+ ]);
128
+ }
129
+ }
130
+ const mappingPath = path.join((0, config_1.baseDir)(), coverage_1.HEALING_DIR, 'mapping');
131
+ const specs = targets.map((name) => {
132
+ const captured = byName.get(name);
133
+ return (0, generate_1.buildSpec)({ page: captured, contract: contractOf(name, captured.url, mappingPath) });
134
+ });
135
+ const written = [];
136
+ const kept = [];
137
+ fs.mkdirSync(outDir, { recursive: true });
138
+ for (const spec of specs) {
139
+ const file = path.join(outDir, `${spec.page}.spec.js`);
140
+ if (fs.existsSync(file) && !force) {
141
+ kept.push(path.relative(process.cwd(), file) || file);
142
+ continue;
143
+ }
144
+ fs.writeFileSync(file, spec.code, 'utf8');
145
+ written.push({
146
+ page: spec.page,
147
+ file: path.relative(process.cwd(), file) || file,
148
+ locators: spec.locators.length,
149
+ skipped: spec.skipped.length,
150
+ });
151
+ }
152
+ if (json) {
153
+ console.log(JSON.stringify({ written, kept, specs: specs.map(({ code, ...rest }) => rest) }, null, 2));
154
+ return;
155
+ }
156
+ console.log(`\n✍️ Generated ${written.length} spec${written.length === 1 ? '' : 's'} → ${path.relative(process.cwd(), outDir) || outDir}`);
157
+ for (const w of written) {
158
+ const skipped = w.skipped > 0 ? ` · ${w.skipped} field${w.skipped === 1 ? '' : 's'} left out` : '';
159
+ console.log(` · ${w.page} — ${w.locators} locator${w.locators === 1 ? '' : 's'}${skipped}`);
160
+ }
161
+ if (kept.length > 0) {
162
+ console.log(`\n Left untouched (already there, --force to replace):`);
163
+ for (const k of kept)
164
+ console.log(` · ${k}`);
165
+ }
166
+ const totalSkipped = specs.reduce((n, s) => n + s.skipped.length, 0);
167
+ if (totalSkipped > 0) {
168
+ console.log(`\n ${totalSkipped} field(s) were not generated — each says why at the top of its file.\n` +
169
+ ` Most often: no test id, id or name, so the only selector is positional. That is a\n` +
170
+ ` finding about the app, not about this tool: such a field cannot be tested reliably\n` +
171
+ ` by anyone, and healing could not repair it either.`);
172
+ }
173
+ const reach = healingReach(outDir);
174
+ if (written.length > 0 && !reach.covered) {
175
+ console.log(`\n ⚠ Healing cannot see these files, so it will never repair them.\n` +
176
+ ` ${reach.why}\n` +
177
+ ` The whole point of generating them this way is that a label change is REWRITTEN\n` +
178
+ ` instead of turning the suite red — and that only happens for files \`ingest\`\n` +
179
+ ` inventories. Either write them where your suite lives:\n` +
180
+ ` ia-qa-discover generate --out <a path under your testPaths>\n` +
181
+ ` or add this directory to "testPaths" in .ia-qa/config.json, then run:\n` +
182
+ ` ia-qa-heal ingest`);
183
+ }
184
+ console.log(`\n These are proposals. Nothing was added to your suite, and the assertions are yours:\n` +
185
+ ` the navigation and the actions are generated, what the app should DO is a TODO in\n` +
186
+ ` each file. An app declares what it accepts, never what it promises.`);
187
+ }
188
+ /**
189
+ * Whether healing would ever read what was just written.
190
+ *
191
+ * This fires on the default, and that is the point. Measured on a real project: the specs
192
+ * landed in `.ia-qa-discovery/generated/` while healing's `testPaths` was `["tests/"]`, so
193
+ * `ingest` inventoried nothing from them — no name drift would escalate, `fix` would never
194
+ * rewrite them, and "born already maintained" was simply false. The first check of it looked
195
+ * green for the wrong reason: the placeholders were in `usage.json` because the project's own
196
+ * suite already used them, and every recorded site was in `tests/`.
197
+ *
198
+ * Writing into someone's suite by default would be the other error — this verb produces
199
+ * proposals, and a proposal does not install itself. So the default stays out of the way and
200
+ * says so, with both remedies, only when it is actually out of reach.
201
+ */
202
+ function healingReach(outDir, cwd = (0, config_1.baseDir)()) {
203
+ let testPaths;
204
+ try {
205
+ // Healing's own resolver, never a copy of its rule: it resolves against the project root
206
+ // rather than the config directory, and getting that wrong points the check at a
207
+ // directory nobody has.
208
+ testPaths = (0, self_healing_1.configuredTestPaths)(cwd);
209
+ }
210
+ catch (err) {
211
+ return { covered: false, why: `Healing's config could not be read (${err.message}).` };
212
+ }
213
+ if (testPaths.length === 0) {
214
+ return { covered: false, why: `Healing declares no "testPaths", so \`ingest\` reads nothing.` };
215
+ }
216
+ const target = path.resolve(outDir);
217
+ const covered = testPaths.some((abs) => {
218
+ const rel = path.relative(abs, target);
219
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
220
+ });
221
+ return {
222
+ covered,
223
+ why: covered
224
+ ? ''
225
+ : `They are outside "testPaths" (${testPaths.map((p) => path.relative(cwd, p) || p).join(', ')}), which is all \`ingest\` reads.`,
226
+ };
227
+ }
228
+ /**
229
+ * The contract for a captured page, by name and then by URL.
230
+ *
231
+ * The URL fallback is not belt-and-braces, it is the ordinary case on a configured project.
232
+ * Both packages resolve a page name through the same `pageNameFromUrl`, but healing's
233
+ * `config.pages` may *declare* a name, and that declaration wins there and is invisible here:
234
+ * measured on a real app, discovery captured `/` as `home` while the contract for the very
235
+ * same URL was filed as `login`. Matching on the name alone found nothing, so the generator
236
+ * fell back to the page's own placeholder text — a locator that works, but not the one the
237
+ * contract holds and therefore not one healing will rewrite. Silently producing the weaker
238
+ * form is exactly the failure this whole feature is built to avoid.
239
+ *
240
+ * A page with no contract at all is still generated, from its capture alone — thinner, and
241
+ * never fatal.
242
+ */
243
+ function contractOf(name, url, mappingPath) {
244
+ try {
245
+ return (0, self_healing_1.loadMapping)(name, mappingPath);
246
+ }
247
+ catch {
248
+ /* not filed under this name — try the URL */
249
+ }
250
+ const target = normalizeUrl(url);
251
+ if (!target)
252
+ return null;
253
+ let files;
254
+ try {
255
+ files = fs.readdirSync(mappingPath).filter((f) => f.endsWith('.json') && !f.startsWith('_'));
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ for (const file of files) {
261
+ try {
262
+ const mapping = JSON.parse(fs.readFileSync(path.join(mappingPath, file), 'utf8'));
263
+ if (normalizeUrl(mapping.url) === target)
264
+ return mapping;
265
+ }
266
+ catch {
267
+ // One unreadable contract costs one candidate, never the generation.
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+ /** Host and path only: a trailing slash or a query string does not make it another page. */
273
+ function normalizeUrl(raw) {
274
+ if (!raw)
275
+ return null;
276
+ try {
277
+ const u = new URL(raw);
278
+ return `${u.host.replace(/^www\./, '')}${u.pathname.replace(/\/$/, '')}`;
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ function refuse(json, message, available) {
285
+ if (json) {
286
+ console.log(JSON.stringify({ generated: false, reason: message, available }, null, 2));
287
+ return;
288
+ }
289
+ console.log(`\nℹ️ ${message}`);
290
+ for (const a of available)
291
+ console.log(`\n ${a}`);
292
+ }
293
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.js","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- declare const HELP = "ia-qa-discover \u2014 deterministic surface reconnaissance for a web app\n\nF1 of QA Discovery: crawl an app's same-origin pages and capture what each one\nis structurally about \u2014 title, headings, forms, and observed API calls. No\nLLM, no verdict: this is recon, not a gate. Runs entirely on your machine;\nnothing is uploaded anywhere.\n\n[FOR AI AGENTS] You are driving this for a human. Run `scan` to capture the\napp's surface, then read .ia-qa-discovery/capture/surface.json (the index) and\nthe per-page files it points at \u2014 that is the evidence for anything you\nreport about this app. Never assert a page's purpose or risk from the URL\nalone; ground it in the captured meta/headings/forms/apiCalls. Never read a\npage count as coverage: if scan warns that pages still show a login form, you\nmeasured the wall, not the app. You CANNOT run `login` \u2014 it waits for a human\nat a browser; ask them to run it, then scan again.\n`coverage` answers \"what does their suite NOT test\" \u2014 but only from a suite that was\nwatched running (@ia-qa/self-healing + one `ia-qa-heal run`). If it says there is no\nmeasurement, report that, never \"the suite covers nothing\": a contract means someone\nconfigured a page, only a watched run means a test went there.\n\nUsage:\n ia-qa-discover login Open a visible browser, log in by hand, keep the\n session (.ia-qa-discovery/session.json) for scan\n to reuse. The general answer to a login wall: it\n models nothing, so SSO, MFA, a consent screen or\n a magic link all work \u2014 you do them.\n Needs a terminal, refuses under CI. The file holds\n live cookies: gitignored, stays on this machine.\n --url <path> page to open (default: your baseUrl)\n --session <f> where to write it\n Already have a storageState (a Playwright globalSetup\n writes one)? Skip this verb: point scan at it with\n --session <file> or \"session\" in config.json.\n ia-qa-discover scan [url] Crawl same-origin pages from <url> (or the saved\n baseUrl) and capture each page's structure.\n Writes .ia-qa-discovery/capture/, diffs against\n the previous run, appends history.jsonl.\n --depth <n> crawl link-depth (default 2)\n --max <n> page cap (default 60)\n --deep also open menus, tabs and dialogs one level\n down and capture the fields they reveal. Every\n non-GET request is blocked for the duration,\n so a click cannot mutate anything server-side.\n --deep-budget <n> max controls clicked per page (default 60)\n --no-sitemap do not read sitemap.xml / robots.txt\n --no-reveal do not open menus/dropdowns for hidden links\n --strict-host treat www./apex as different hosts\n --session <file> reuse an existing storageState (login) file\n --no-network skip API/network-call capture this run\n --network-threshold <n> shared-call promotion threshold 0-1 (default 0.6)\n --save persist this baseUrl to config.json\n --json machine-readable summary on stdout\n --report [file.html] branded ia-qa.com HTML dossier of the\n app (default ia-qa-discover-report.html)\n --open open that report in the browser\n ia-qa-discover coverage What your test suite does NOT test. Compares the\n pages this scan found against the pages your suite\n was OBSERVED visiting, ranks the gap by what each\n page takes as input, and writes coverage-map.json.\n Reads local files only \u2014 no browser, no network.\n Needs @ia-qa/self-healing and one \"ia-qa-heal run\":\n without a watched run there is no measurement, and\n it says so instead of showing a number.\n Declare pages you deliberately do not test in\n \"outOfScope\" in config.json \u2014 they stay counted\n and named, never silently dropped.\n --json machine-readable map on stdout\n --report [file.html] branded HTML dossier with the gap\n (default ia-qa-discover-coverage.html)\n --open open that report in the browser\n ia-qa-discover history Recorded scan runs \u2014 pages/forms/API-surface trend.\n --json --limit <n> (default 500)\n ia-qa-discover version Print the installed version (also --version, -v)\n ia-qa-discover help Show this help\n\nGlobal:\n --config <dir> Resolve .ia-qa-discovery/ under <dir> instead of the\n current directory. Also IAQA_DISCOVERY_CONFIG_DIR=<dir>.\n";
2
+ declare const HELP = "ia-qa-discover \u2014 deterministic surface reconnaissance for a web app\n\nF1 of QA Discovery: crawl an app's same-origin pages and capture what each one\nis structurally about \u2014 title, headings, forms, and observed API calls. No\nLLM, no verdict: this is recon, not a gate. Runs entirely on your machine;\nnothing is uploaded anywhere.\n\n[FOR AI AGENTS] You are driving this for a human. Run `scan` to capture the\napp's surface, then read .ia-qa-discovery/capture/surface.json (the index) and\nthe per-page files it points at \u2014 that is the evidence for anything you\nreport about this app. Never assert a page's purpose or risk from the URL\nalone; ground it in the captured meta/headings/forms/apiCalls. Never read a\npage count as coverage: if scan warns that pages still show a login form, you\nmeasured the wall, not the app. You CANNOT run `login` \u2014 it waits for a human\nat a browser; ask them to run it, then scan again.\n`coverage` answers \"what does their suite NOT test\" \u2014 but only from a suite that was\nwatched running (one `ia-qa-heal run`, or one `ia-qa-pal tour --suite`). If it says there is no\nmeasurement, report that, never \"the suite covers nothing\": a contract means someone\nconfigured a page, only a watched run means a test went there.\n\nUsage:\n ia-qa-discover login Open a visible browser, log in by hand, keep the\n session (.ia-qa-discovery/session.json) for scan\n to reuse. The general answer to a login wall: it\n models nothing, so SSO, MFA, a consent screen or\n a magic link all work \u2014 you do them.\n Needs a terminal, refuses under CI. The file holds\n live cookies: gitignored, stays on this machine.\n --url <path> page to open (default: your baseUrl)\n --session <f> where to write it\n Already have a storageState (a Playwright globalSetup\n writes one)? Skip this verb: point scan at it with\n --session <file> or \"session\" in config.json.\n ia-qa-discover scan [url] Crawl same-origin pages from <url> (or the saved\n baseUrl) and capture each page's structure.\n Writes .ia-qa-discovery/capture/, diffs against\n the previous run, appends history.jsonl.\n --depth <n> crawl link-depth (default 2)\n --max <n> page cap (default 60)\n --deep also open menus, tabs and dialogs one level\n down and capture the fields they reveal. Every\n non-GET request is blocked for the duration,\n so a click cannot mutate anything server-side.\n --deep-budget <n> max controls clicked per page (default 60)\n --no-sitemap do not read sitemap.xml / robots.txt\n --no-reveal do not open menus/dropdowns for hidden links\n --strict-host treat www./apex as different hosts\n --session <file> reuse an existing storageState (login) file\n --no-network skip API/network-call capture this run\n --network-threshold <n> shared-call promotion threshold 0-1 (default 0.6)\n --save persist this baseUrl to config.json\n --json machine-readable summary on stdout\n --report [file.html] branded ia-qa.com HTML dossier of the\n app (default ia-qa-discover-report.html)\n --open open that report in the browser\n ia-qa-discover coverage What your test suite does NOT test. Compares the\n pages this scan found against the pages your suite\n was OBSERVED visiting, ranks the gap by what each\n page takes as input, and writes coverage-map.json.\n Reads local files only \u2014 no browser, no network.\n Needs one watched run of your suite \u2014\n \"ia-qa-heal run\" or \"ia-qa-pal tour --suite\":\n without a watched run there is no measurement, and\n it says so instead of showing a number.\n Declare pages you deliberately do not test in\n \"outOfScope\" in config.json \u2014 they stay counted\n and named, never silently dropped.\n --json machine-readable map on stdout\n --report [file.html] branded HTML dossier with the gap\n (default ia-qa-discover-coverage.html)\n --open open that report in the browser\n ia-qa-discover generate [page\u2026] A starting Playwright suite for the pages nothing\n tests \u2014 one file per page, into its own directory.\n Every locator names an element the healing contract\n holds, so the file is REPAIRED when the app moves\n instead of going red. A field with no test id, id or\n name is left out and says so: its selector would be\n positional, which nobody can test reliably.\n Proposals \u2014 nothing is added to your suite, and the\n assertions stay yours (the app declares what it\n accepts, never what it promises).\n With no page, it generates for the coverage gap.\n --out <dir> where to write (default\n .ia-qa-discovery/generated/)\n --force replace files already there\n --json machine-readable summary on stdout\n ia-qa-discover history Recorded scan runs \u2014 pages/forms/API-surface trend.\n --json --limit <n> (default 500)\n ia-qa-discover version Print the installed version (also --version, -v)\n ia-qa-discover help Show this help\n\nGlobal:\n --config <dir> Resolve .ia-qa-discovery/ under <dir> instead of the\n current directory. Also IAQA_DISCOVERY_CONFIG_DIR=<dir>.\n";
3
3
  declare function verbHelp(command: string | undefined): string | null;
4
4
  declare function main(): Promise<void>;
5
5
  export { HELP, verbHelp, main };
package/dist/cli/index.js CHANGED
@@ -10,6 +10,7 @@ const scan_1 = require("./scan");
10
10
  const login_1 = require("./login");
11
11
  const history_1 = require("./history");
12
12
  const coverage_1 = require("./coverage");
13
+ const generate_1 = require("./generate");
13
14
  // dist/cli/ → package root; npm ships package.json in every tarball.
14
15
  const VERSION = require('../../package.json').version;
15
16
  const HELP = `ia-qa-discover — deterministic surface reconnaissance for a web app
@@ -28,7 +29,7 @@ page count as coverage: if scan warns that pages still show a login form, you
28
29
  measured the wall, not the app. You CANNOT run \`login\` — it waits for a human
29
30
  at a browser; ask them to run it, then scan again.
30
31
  \`coverage\` answers "what does their suite NOT test" — but only from a suite that was
31
- watched running (@ia-qa/self-healing + one \`ia-qa-heal run\`). If it says there is no
32
+ watched running (one \`ia-qa-heal run\`, or one \`ia-qa-pal tour --suite\`). If it says there is no
32
33
  measurement, report that, never "the suite covers nothing": a contract means someone
33
34
  configured a page, only a watched run means a test went there.
34
35
 
@@ -72,7 +73,8 @@ Usage:
72
73
  was OBSERVED visiting, ranks the gap by what each
73
74
  page takes as input, and writes coverage-map.json.
74
75
  Reads local files only — no browser, no network.
75
- Needs @ia-qa/self-healing and one "ia-qa-heal run":
76
+ Needs one watched run of your suite —
77
+ "ia-qa-heal run" or "ia-qa-pal tour --suite":
76
78
  without a watched run there is no measurement, and
77
79
  it says so instead of showing a number.
78
80
  Declare pages you deliberately do not test in
@@ -82,6 +84,21 @@ Usage:
82
84
  --report [file.html] branded HTML dossier with the gap
83
85
  (default ia-qa-discover-coverage.html)
84
86
  --open open that report in the browser
87
+ ia-qa-discover generate [page…] A starting Playwright suite for the pages nothing
88
+ tests — one file per page, into its own directory.
89
+ Every locator names an element the healing contract
90
+ holds, so the file is REPAIRED when the app moves
91
+ instead of going red. A field with no test id, id or
92
+ name is left out and says so: its selector would be
93
+ positional, which nobody can test reliably.
94
+ Proposals — nothing is added to your suite, and the
95
+ assertions stay yours (the app declares what it
96
+ accepts, never what it promises).
97
+ With no page, it generates for the coverage gap.
98
+ --out <dir> where to write (default
99
+ .ia-qa-discovery/generated/)
100
+ --force replace files already there
101
+ --json machine-readable summary on stdout
85
102
  ia-qa-discover history Recorded scan runs — pages/forms/API-surface trend.
86
103
  --json --limit <n> (default 500)
87
104
  ia-qa-discover version Print the installed version (also --version, -v)
@@ -157,6 +174,9 @@ async function main() {
157
174
  case 'coverage':
158
175
  await (0, coverage_1.runCoverage)(rest);
159
176
  break;
177
+ case 'generate':
178
+ await (0, generate_1.runGenerate)(rest);
179
+ break;
160
180
  case 'history':
161
181
  await (0, history_1.runHistory)(rest);
162
182
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ExercisedRecord, TestDepthReport } from '@ia-qa/self-healing';
1
+ import type { ExercisedRecord, TestDepthReport, PageChurn } from '@ia-qa/self-healing';
2
2
  import type { PageCapture } from './capture/page';
3
3
  import type { SurfaceManifest } from './scan';
4
4
  /**
@@ -38,6 +38,12 @@ export interface UncoveredPage {
38
38
  page: string;
39
39
  url: string;
40
40
  factors: string[];
41
+ /**
42
+ * What the page holds, shown beside the factors and never counted with them — see
43
+ * `rankFactors`. Describing a page and ranking it are two jobs, and merging them let
44
+ * "1 input · 1 API call" out-rank a page that had actually changed.
45
+ */
46
+ context: string[];
41
47
  fields: number;
42
48
  passwordFields: number;
43
49
  /** Forms the page declares with `method="post"` — the app's own statement that it writes. */
@@ -48,6 +54,15 @@ export interface UncoveredPage {
48
54
  */
49
55
  writeCalls: number;
50
56
  apiCalls: number;
57
+ /**
58
+ * How many of the measured runs this page drifted in, from `@ia-qa/self-healing`'s history.
59
+ *
60
+ * The one factor neither package can compute alone, and the strongest one here: a page that
61
+ * changes every week and that no test visits is where bugs are born, while a page untouched
62
+ * for six months does not need a new test. Absent when there is no history, or too few runs
63
+ * to separate anything — an absent factor stays absent.
64
+ */
65
+ churnRuns?: number;
51
66
  }
52
67
  export interface CoverageMap {
53
68
  schema: typeof COVERAGE_SCHEMA;
@@ -68,6 +83,17 @@ export interface CoverageMap {
68
83
  notMeasured: SurfaceManifest['coverage'];
69
84
  /** How much of each visited page's contract the suite names. */
70
85
  depth: TestDepthReport;
86
+ /**
87
+ * What the drift factor was computed over, and what it had to leave out to mean anything.
88
+ * Absent when no history was available. A filter nobody can see is a blind spot, so this
89
+ * travels with the map rather than being recomputed by whoever renders it.
90
+ */
91
+ churn?: {
92
+ measuredRuns: number;
93
+ excludedRuns: number;
94
+ ranked: boolean;
95
+ reason?: string;
96
+ };
71
97
  suite: {
72
98
  exitCode: number | null;
73
99
  recordedAt: string;
@@ -82,6 +108,12 @@ export interface CoverageInput {
82
108
  depth: TestDepthReport;
83
109
  scope?: string[];
84
110
  baseUrl: string;
111
+ /**
112
+ * Which pages actually move, from `@ia-qa/self-healing`'s run history. Optional: a project
113
+ * that has never run `diff` or `run` has none, and the gap is then ranked on structure
114
+ * alone — which is what a QA arriving on day one does too.
115
+ */
116
+ churn?: PageChurn;
85
117
  }
86
118
  /**
87
119
  * Why this page would hurt, from facts the scan observed.
@@ -109,12 +141,30 @@ export interface CoverageInput {
109
141
  * React app that submits in JS and states nothing — and an absent factor is the honest
110
142
  * outcome there, not a reason to go back to guessing from traffic.
111
143
  */
112
- export declare function rankFactors(page: PageCapture): {
144
+ export declare function rankFactors(page: PageCapture,
145
+ /**
146
+ * This page's entry in the drift history, when there is one worth ranking on. The caller
147
+ * decides whether there is — `pageChurn` observes, the policy of "enough runs to mean
148
+ * something" belongs here, with the ranking it feeds.
149
+ */
150
+ churn?: {
151
+ runs: number;
152
+ measuredRuns: number;
153
+ }): {
113
154
  factors: string[];
155
+ context: string[];
114
156
  passwordFields: number;
115
157
  postForms: number;
116
158
  writeCalls: number;
159
+ churnRuns?: number;
117
160
  };
161
+ /**
162
+ * Below this, "changed in N of M runs" separates nothing: with two runs every page that ever
163
+ * drifted carries the factor, which is the inert-factor failure `writeCalls` was removed for.
164
+ * The history is not wrong there — it is just too short to rank on yet, and saying so beats
165
+ * ranking on it.
166
+ */
167
+ export declare const MIN_CHURN_RUNS = 3;
118
168
  /** Pages the human declared out of scope. One dialect: the same globs as `volatile`. */
119
169
  export declare function matchesScope(page: string, scope: string[] | undefined): boolean;
120
170
  export declare function buildCoverageMap(input: CoverageInput): CoverageMap;