@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,31 @@
1
+ /**
2
+ * Rewrite `_overview.md` from what is already on disk.
3
+ *
4
+ * `scan` writes it at capture time, which means a classification produced
5
+ * afterwards would never appear in it fresh — it would first show up on the
6
+ * *next* scan, by which point it is stale by construction. So the AI CLI
7
+ * regenerates it, and this is the one place that knows how.
8
+ *
9
+ * Its own module rather than a function in either neighbour: `overview.ts` is a
10
+ * pure renderer that touches no filesystem, and `classificationView.ts` is
11
+ * imported *by* the renderer — putting this in either creates an import cycle
12
+ * for no gain.
13
+ */
14
+ export interface RefreshResult {
15
+ ok: boolean;
16
+ file?: string;
17
+ /** Why nothing was written. Always a sentence someone can act on. */
18
+ reason?: string;
19
+ }
20
+ /**
21
+ * Refuse rather than write an overview that has lost its warnings.
22
+ *
23
+ * `coverage` (login wall, href blindness, the page cap, the sitemap count) is
24
+ * persisted in `surface.json` precisely so this function can rebuild the file
25
+ * faithfully. A manifest written before that field existed cannot supply it,
26
+ * and defaulting it to "nothing was missed" would turn a scan that measured a
27
+ * login wall into an overview that reports a clean public surface — a false
28
+ * green manufactured by a cosmetic refresh. So an old manifest is a refusal
29
+ * with a one-line remedy, never a silent downgrade.
30
+ */
31
+ export declare function refreshOverview(dir?: string): RefreshResult;
@@ -0,0 +1,101 @@
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.refreshOverview = refreshOverview;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const config_1 = require("./config");
40
+ const citations_1 = require("./citations");
41
+ const overview_1 = require("./overview");
42
+ const classificationView_1 = require("./classificationView");
43
+ const sharedCalls_1 = require("./sharedCalls");
44
+ /**
45
+ * Refuse rather than write an overview that has lost its warnings.
46
+ *
47
+ * `coverage` (login wall, href blindness, the page cap, the sitemap count) is
48
+ * persisted in `surface.json` precisely so this function can rebuild the file
49
+ * faithfully. A manifest written before that field existed cannot supply it,
50
+ * and defaulting it to "nothing was missed" would turn a scan that measured a
51
+ * login wall into an overview that reports a clean public surface — a false
52
+ * green manufactured by a cosmetic refresh. So an old manifest is a refusal
53
+ * with a one-line remedy, never a silent downgrade.
54
+ */
55
+ function refreshOverview(dir = (0, config_1.captureDir)()) {
56
+ const surfaceFile = path.join(dir, 'surface.json');
57
+ if (!fs.existsSync(surfaceFile)) {
58
+ return { ok: false, reason: `No ${path.join(dir, 'surface.json')} — run \`ia-qa-discover scan\` first.` };
59
+ }
60
+ let manifest;
61
+ try {
62
+ manifest = JSON.parse(fs.readFileSync(surfaceFile, 'utf8'));
63
+ }
64
+ catch (e) {
65
+ return { ok: false, reason: `surface.json could not be read: ${e instanceof Error ? e.message : String(e)}` };
66
+ }
67
+ if (!manifest.coverage) {
68
+ return {
69
+ ok: false,
70
+ reason: 'This capture predates coverage being recorded in surface.json, so _overview.md cannot be ' +
71
+ 'rebuilt without dropping its coverage warnings (login wall, unreachable pages, the page cap). ' +
72
+ 'Re-run `ia-qa-discover scan` and classify again.',
73
+ };
74
+ }
75
+ const index = (0, citations_1.loadCaptureIndex)(dir);
76
+ const pages = [...index.pages.values()];
77
+ if (pages.length === 0)
78
+ return { ok: false, reason: `No page captures in ${dir}.` };
79
+ // `_shared-calls.json` is absent for a scan run with --no-network. An empty
80
+ // file is the right stand-in: zero promoted calls is what actually happened,
81
+ // and the renderer already words that case ("no XHR/fetch traffic observed —
82
+ // or the scan ran with --no-network").
83
+ const shared = index.shared ?? {
84
+ schema: sharedCalls_1.SHARED_CALLS_SCHEMA,
85
+ capturedAt: manifest.capturedAt,
86
+ sourcePages: 0,
87
+ threshold: 0,
88
+ calls: [],
89
+ };
90
+ const classification = (0, classificationView_1.classificationFor)(pages);
91
+ const file = path.join(dir, overview_1.OVERVIEW_FILENAME);
92
+ fs.writeFileSync(file, (0, overview_1.renderOverview)({
93
+ manifest,
94
+ pages,
95
+ shared,
96
+ ...manifest.coverage,
97
+ ...(classification ? { classification } : {}),
98
+ }), 'utf8');
99
+ return { ok: true, file };
100
+ }
101
+ //# sourceMappingURL=overviewFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overviewFile.js","sourceRoot":"","sources":["../src/overviewFile.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,0CAoDC;AA7FD,uCAAyB;AACzB,2CAA6B;AAC7B,qCAAsC;AACtC,2CAA+C;AAC/C,yCAA+D;AAC/D,6DAAyD;AACzD,+CAA0E;AAwB1E;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,MAAc,IAAA,mBAAU,GAAE;IACxD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,uCAAuC,EAAE,CAAC;IAC5G,CAAC;IAED,IAAI,QAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAoB,CAAC;IACjF,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mCAAmC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAChH,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EACJ,2FAA2F;gBAC3F,gGAAgG;gBAChG,kDAAkD;SACrD,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,IAAA,4BAAgB,EAAC,GAAG,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,GAAG,GAAG,EAAE,CAAC;IAEpF,4EAA4E;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,uCAAuC;IACvC,MAAM,MAAM,GAAoB,KAAK,CAAC,MAAM,IAAI;QAC9C,MAAM,EAAE,iCAAmB;QAC3B,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,CAAC;QACZ,KAAK,EAAE,EAAE;KACV,CAAC;IACF,MAAM,cAAc,GAAG,IAAA,sCAAiB,EAAC,KAAK,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,4BAAiB,CAAC,CAAC;IAC/C,EAAE,CAAC,aAAa,CACd,IAAI,EACJ,IAAA,yBAAc,EAAC;QACb,QAAQ;QACR,KAAK;QACL,MAAM;QACN,GAAG,QAAQ,CAAC,QAAQ;QACpB,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC,EACF,MAAM,CACP,CAAC;IACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { type Candidate } from '@ia-qa/self-healing';
2
+ import { type PageCapture } from './capture/page';
3
+ import { SHARED_CALLS_SCHEMA, type SharedCallsFile } from './sharedCalls';
4
+ import { type CaptureDiff } from './diff';
5
+ import { type ScanHistoryEntry } from './history';
6
+ /**
7
+ * F1 orchestrator — the one verb (`scan`) both the CLI and the MCP server call.
8
+ *
9
+ * Two-phase by necessity: `crawlApp` (from `@ia-qa/self-healing`, imported not
10
+ * reimplemented — it is the safety-critical same-origin BFS crawler) discovers
11
+ * the page list with its own `Page`, then this function does a second
12
+ * `page.goto()` per confirmed URL to capture meta/headings/forms/API calls.
13
+ * Every page loads twice; the tradeoff is accepted in the plan for this slice.
14
+ */
15
+ export declare const SURFACE_SCHEMA = "qa-discovery-surface@1";
16
+ export interface SurfaceManifest {
17
+ schema: typeof SURFACE_SCHEMA;
18
+ baseUrl: string;
19
+ capturedAt: string;
20
+ pagesFound: number;
21
+ pagesCaptured: number;
22
+ pagesFailed: number;
23
+ pages: Array<{
24
+ name: string;
25
+ url: string;
26
+ source: Candidate['source'];
27
+ file: string;
28
+ }>;
29
+ sharedCallsFile: string;
30
+ /**
31
+ * Every distinct origin the app called, with how many calls and how many
32
+ * pages hit it. `sameOrigin` alone cannot separate an app's own API host
33
+ * (an api.* subdomain, a Heroku backend) from a third-party tracker — both
34
+ * read as cross-origin. This is the observed list; deciding which are
35
+ * first-party is a later stage's judgment, not F1's.
36
+ */
37
+ origins: Array<{
38
+ origin: string;
39
+ calls: number;
40
+ pages: number;
41
+ sameOrigin: boolean;
42
+ }>;
43
+ /**
44
+ * What this scan could NOT see, persisted with what it could.
45
+ *
46
+ * It lives in the manifest because it is the only way any later consumer can
47
+ * rebuild `_overview.md` without silently dropping its coverage warnings —
48
+ * and an overview missing the login-wall caveat is a false green, strictly
49
+ * worse than no overview at all. Also what F2 needs to weigh how much of the
50
+ * app a classification actually covers.
51
+ */
52
+ coverage: {
53
+ loginWall: string[];
54
+ hrefBlind: boolean;
55
+ droppedByCap: number;
56
+ sitemapDeclared: number | null;
57
+ session?: string;
58
+ };
59
+ }
60
+ export interface ScanOptions {
61
+ url?: string;
62
+ maxDepth?: number;
63
+ maxPages?: number;
64
+ reveal?: boolean;
65
+ strictHost?: boolean;
66
+ session?: string;
67
+ locale?: string;
68
+ network?: boolean;
69
+ networkThreshold?: number;
70
+ safeQueryParams?: string[];
71
+ /** Read `sitemap.xml` (falling back to robots.txt) as a page source. Default true. */
72
+ sitemap?: boolean;
73
+ /** Click one level of disclosures per page and capture what they reveal. Default false. */
74
+ deep?: boolean;
75
+ /** Max controls clicked per page under `deep`. Default 60. */
76
+ deepBudget?: number;
77
+ /** Persist { baseUrl, ... } to config.json so later runs need no url. */
78
+ save?: boolean;
79
+ /** Persist capture/ + history.jsonl to disk. Default true — set false for an in-memory preview (e.g. an agent inspecting an app without writing to the project). */
80
+ write?: boolean;
81
+ onProgress?: (message: string) => void;
82
+ }
83
+ export interface ScanResult {
84
+ baseUrl: string;
85
+ pagesFound: number;
86
+ pagesCaptured: number;
87
+ failed: Array<{
88
+ name: string;
89
+ url: string;
90
+ error: string;
91
+ }>;
92
+ diff: CaptureDiff;
93
+ history: ScanHistoryEntry;
94
+ /** Empty string when `write: false` (nothing was persisted). */
95
+ surfacePath: string;
96
+ /** The captures themselves — always populated, written to disk only when `write` is true. */
97
+ pages: PageCapture[];
98
+ sharedCalls: SharedCallsFile;
99
+ /** The index document, built whether or not it was written — what `--report` renders from. */
100
+ manifest: SurfaceManifest;
101
+ /** Whether a saved/〝named〞session was used for this scan, and which file. */
102
+ session?: string;
103
+ /**
104
+ * Captured pages still showing a login form. More than one means the scan
105
+ * very likely measured the wall, not the app — the page count is then a
106
+ * false green. See `loginWallPages`.
107
+ */
108
+ loginWall: string[];
109
+ /**
110
+ * True when the crawl harvested ZERO internal links, so discovery could not
111
+ * see past the entry URL whatever the app actually contains.
112
+ *
113
+ * Discovery follows `<a href>`. A SPA that navigates programmatically —
114
+ * React Router's `useNavigate()`, a button with an onClick handler — exposes
115
+ * no href at all, so the crawl finds exactly one page and reports "1 of 1
116
+ * found", i.e. 100%. Measured on a real app: 11 routes, zero `<Link>`, and
117
+ * the scan claimed complete coverage of one page. This is the login wall's
118
+ * false green in another costume, and the count alone can never reveal it.
119
+ */
120
+ hrefBlind: boolean;
121
+ /**
122
+ * Where the page list came from, and what was left out.
123
+ *
124
+ * A page the sitemap declared but the cap skipped is not a page that does
125
+ * not exist — and the difference has to survive into the report, or the
126
+ * denominator quietly becomes "what we felt like visiting".
127
+ */
128
+ discovery: {
129
+ sitemap: null | {
130
+ declared: number;
131
+ retained: number;
132
+ skippedHost: number;
133
+ otherHosts: string[];
134
+ tried: string[];
135
+ };
136
+ crawled: number;
137
+ declared: number;
138
+ /** Distinct pages known before `maxPages` was applied. */
139
+ merged: number;
140
+ /** How many were dropped by the cap. Zero unless `merged > maxPages`. */
141
+ droppedByCap: number;
142
+ };
143
+ }
144
+ /**
145
+ * Pages that still show a login form.
146
+ *
147
+ * The signal is a password field — observed in the DOM, never inferred. It
148
+ * exists because of a failure this tool met on a real app: a SPA whose
149
+ * catch-all route renders the login page at EVERY unknown URL, with HTTP 200
150
+ * and no redirect. Nothing in the status code, the URL or the page count says
151
+ * anything is wrong — the scan happily reports "3 pages captured" while having
152
+ * captured the same wall three times. A page-count without this check is the
153
+ * false green this package exists to avoid.
154
+ *
155
+ * One such page is ordinary (an app has a login page). Several means the wall.
156
+ */
157
+ export declare function loginWallPages(pages: PageCapture[]): string[];
158
+ export declare function scanApp(options?: ScanOptions): Promise<ScanResult>;
159
+ export { SHARED_CALLS_SCHEMA };