@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,128 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Candidate } from '@ia-qa/self-healing';
3
+ import { type ApiCall } from '../network';
4
+ export interface PageMeta {
5
+ title: string;
6
+ description: string;
7
+ lang: string;
8
+ }
9
+ export interface Heading {
10
+ level: number;
11
+ text: string;
12
+ }
13
+ export interface FormField {
14
+ type: string;
15
+ name: string;
16
+ selector: string;
17
+ /**
18
+ * False when the field carries no durable identity (no test-id, id or name)
19
+ * and `selector` is a positional fallback scoped to the form — it resolves to
20
+ * exactly one element today, and breaks the moment a field is inserted above
21
+ * it. A field with `stableSelector: false` is also a testability finding
22
+ * about the app, not just a caveat about this capture.
23
+ */
24
+ /**
25
+ * What a human reads to know what this field is — the <label>, an aria-label,
26
+ * or failing those the placeholder. Absent when the app labels the field
27
+ * nowhere, which is itself a finding: a field with no label cannot be
28
+ * understood by a screen reader, by a later stage, or by whoever writes the
29
+ * test.
30
+ */
31
+ label?: string;
32
+ /**
33
+ * Where the label came from. means it was read from text sitting
34
+ * near the field rather than declared by the app — useful, but a guess, and a
35
+ * later stage should weigh it accordingly.
36
+ */
37
+ labelSource?: 'aria-label' | 'aria-labelledby' | 'label-for' | 'label-wrapping' | 'placeholder' | 'title' | 'proximity';
38
+ stableSelector: boolean;
39
+ /**
40
+ * Present (and true) only when `selector` could NOT be made to resolve to
41
+ * exactly one element — verified in the page, not assumed. Nothing
42
+ * downstream may act on such a field: it is a locator coin flip.
43
+ */
44
+ ambiguousSelector?: boolean;
45
+ required: boolean;
46
+ /**
47
+ * The click path that reveals this field, when it was not visible at load
48
+ * (`--deep`). Absent on a field the page shows on its own. Never part of
49
+ * identity — a field that stops being hidden is the same field — but a test
50
+ * author cannot reach it without knowing this.
51
+ */
52
+ via?: string[];
53
+ /**
54
+ * Nearest landmark and/or heading above the field. Present on loose fields
55
+ * only — without it, a page's hundred un-grouped inputs are one blob.
56
+ */
57
+ context?: string;
58
+ }
59
+ export interface CapturedForm {
60
+ selector: string;
61
+ /** null when the form states no `method` — a JS-handled submit, not a GET to self. */
62
+ method: string | null;
63
+ /** null when the form states no `action`. Never synthesized from the page URL. */
64
+ action: string | null;
65
+ /** Present only when there is an `action` to classify. */
66
+ sameOriginAction?: boolean;
67
+ hasSubmit: boolean;
68
+ fields: FormField[];
69
+ }
70
+ export declare const PAGE_CAPTURE_SCHEMA = "qa-discovery-page@1";
71
+ export interface PageCapture {
72
+ schema: typeof PAGE_CAPTURE_SCHEMA;
73
+ page: string;
74
+ url: string;
75
+ finalUrl: string;
76
+ source: Candidate['source'];
77
+ capturedAt: string;
78
+ http: {
79
+ status: number;
80
+ redirected: boolean;
81
+ };
82
+ meta: PageMeta;
83
+ headings: Heading[];
84
+ forms: CapturedForm[];
85
+ /**
86
+ * Input fields outside any `<form>` — the majority of the input surface in a
87
+ * modern React/Vue app, which submits in JS and wraps nothing. Capturing only
88
+ * `<form>` descendants reported "0 fields" on an app with 464 inputs.
89
+ */
90
+ looseFields: FormField[];
91
+ apiCalls: ApiCall[];
92
+ /**
93
+ * State exploration, when `--deep` ran. `openable` is counted on EVERY scan
94
+ * even without `--deep`, because a page with 64 clickable controls and 2
95
+ * visible fields must not report those 2 as its input surface.
96
+ */
97
+ exploration: {
98
+ /** Controls that could reveal more, counted before any click. */
99
+ openable: number;
100
+ /** False when only the loaded state was captured. */
101
+ explored: boolean;
102
+ clicked?: number;
103
+ revealed?: number;
104
+ collided?: number;
105
+ truncated?: boolean;
106
+ blockedMutations?: number;
107
+ };
108
+ }
109
+ export interface CapturePageOptions {
110
+ /** Observe API/network calls. Default true. */
111
+ network?: boolean;
112
+ safeQueryParams?: string[];
113
+ timeout?: number;
114
+ /** Click one level of disclosures and capture what they reveal. Default false. */
115
+ deep?: boolean;
116
+ /** Max controls clicked per page. Default 60. */
117
+ deepBudget?: number;
118
+ }
119
+ /**
120
+ * Capture one page's structural surface: meta/headings/forms via a single
121
+ * `page.evaluate`, and — unless disabled — observed API calls via a passive
122
+ * `page.on('response')` listener attached for the duration of this navigation
123
+ * only (see network.ts for why never `page.route()`).
124
+ *
125
+ * `candidate.url` is a path (self-healing's `Candidate` shape), resolved here
126
+ * against `baseUrl` before navigating.
127
+ */
128
+ export declare function capturePage(page: Page, candidate: Candidate, baseUrl: string, options?: CapturePageOptions): Promise<PageCapture>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAGE_CAPTURE_SCHEMA = void 0;
4
+ exports.capturePage = capturePage;
5
+ const extractPage_1 = require("../browser/extractPage");
6
+ const network_1 = require("../network");
7
+ const explore_1 = require("../explore");
8
+ exports.PAGE_CAPTURE_SCHEMA = 'qa-discovery-page@1';
9
+ /**
10
+ * Capture one page's structural surface: meta/headings/forms via a single
11
+ * `page.evaluate`, and — unless disabled — observed API calls via a passive
12
+ * `page.on('response')` listener attached for the duration of this navigation
13
+ * only (see network.ts for why never `page.route()`).
14
+ *
15
+ * `candidate.url` is a path (self-healing's `Candidate` shape), resolved here
16
+ * against `baseUrl` before navigating.
17
+ */
18
+ async function capturePage(page, candidate, baseUrl, options = {}) {
19
+ const network = options.network !== false;
20
+ const timeout = options.timeout ?? 30000;
21
+ const absoluteUrl = new URL(candidate.url, baseUrl).toString();
22
+ const responses = [];
23
+ const detach = network ? (0, network_1.attachNetworkListener)(page, (r) => responses.push(r)) : null;
24
+ try {
25
+ const response = await page.goto(absoluteUrl, { waitUntil: 'networkidle', timeout });
26
+ const finalUrl = page.url();
27
+ const status = response ? response.status() : 0;
28
+ const redirected = response ? response.request().redirectedFrom() !== null : false;
29
+ const extracted = (await page.evaluate(extractPage_1.extractPageInPage));
30
+ const pageOrigin = new URL(finalUrl).origin;
31
+ const apiCalls = network
32
+ ? (0, network_1.aggregateApiCalls)(responses, pageOrigin, options.safeQueryParams ?? network_1.DEFAULT_SAFE_QUERY_PARAMS)
33
+ : [];
34
+ // LAST, and after the network listener has been read: exploration clicks
35
+ // around and leaves the page in an arbitrary state, so everything that
36
+ // reads the loaded DOM must already have run. See explore.ts rule 3.
37
+ let looseFields = extracted.looseFields ?? [];
38
+ let exploration;
39
+ if (options.deep) {
40
+ const { revealed, stats } = await (0, explore_1.exploreFields)(page, { budget: options.deepBudget });
41
+ looseFields = [...looseFields, ...revealed];
42
+ exploration = {
43
+ openable: stats.openable,
44
+ explored: true,
45
+ clicked: stats.clicked,
46
+ revealed: stats.revealed,
47
+ collided: stats.collided,
48
+ truncated: stats.truncated,
49
+ blockedMutations: stats.blockedMutations,
50
+ };
51
+ }
52
+ else {
53
+ // Counted even when not exploring: this is what makes "2 fields" honest
54
+ // on a page that hides the rest behind 64 buttons.
55
+ exploration = { openable: await (0, explore_1.countOpenables)(page), explored: false };
56
+ }
57
+ return {
58
+ schema: exports.PAGE_CAPTURE_SCHEMA,
59
+ page: candidate.name,
60
+ url: absoluteUrl,
61
+ finalUrl,
62
+ source: candidate.source,
63
+ capturedAt: new Date().toISOString(),
64
+ http: { status, redirected },
65
+ meta: extracted.meta,
66
+ headings: extracted.headings,
67
+ forms: extracted.forms,
68
+ looseFields,
69
+ apiCalls,
70
+ exploration,
71
+ };
72
+ }
73
+ finally {
74
+ if (detach)
75
+ detach();
76
+ }
77
+ }
78
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/capture/page.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import type { PageCapture } from './capture/page';
2
+ import type { SharedCallsFile } from './sharedCalls';
3
+ /**
4
+ * Citation resolution — the mechanism the whole pipeline's honesty rests on.
5
+ *
6
+ * Written and tested BEFORE anything that produces claims, deliberately: this is
7
+ * the judge, and a judge built after the accused tends to be shaped to acquit.
8
+ * Nothing here calls an LLM, opens a browser, or touches the network.
9
+ *
10
+ * A later stage may assert whatever it likes about the app, but each assertion
11
+ * carries a path into F1's capture. This module resolves that path and checks
12
+ * that what is there is what the claim said is there. A claim whose citation
13
+ * does not resolve — or resolves to something else — is **dropped**, and the
14
+ * drop is reported rather than quietly swallowed.
15
+ *
16
+ * THE LIMIT, restated here because this is the file people will read to
17
+ * understand it: this proves the **premise**, never the **conclusion**. A model
18
+ * can cite a password field that genuinely exists and still call the page a
19
+ * checkout. What is mechanised is "the thing you leaned on is real"; whether the
20
+ * inference follows is a human's call, which is why every surviving claim keeps
21
+ * a readable sentence and a visible confidence next to it.
22
+ */
23
+ /** Page name → its capture. Plus the cross-page file, addressable as `_shared-calls.json`. */
24
+ export interface CaptureIndex {
25
+ pages: Map<string, PageCapture>;
26
+ shared?: SharedCallsFile;
27
+ }
28
+ export declare const SHARED_REF = "_shared-calls.json";
29
+ /**
30
+ * A citation. The bare string form only asserts existence; the object form also
31
+ * states what the citing stage believes is there, which is strictly stronger —
32
+ * it catches a model that points at a real element while being wrong about it.
33
+ */
34
+ export type Citation = string | {
35
+ path: string;
36
+ expect?: Record<string, unknown>;
37
+ };
38
+ export type CitationFailure =
39
+ /** Not a `<file>#<path>` reference at all. */
40
+ 'malformed'
41
+ /** `pages/x.json` — the capture holds no such page. */
42
+ | 'unknown-page'
43
+ /** The file is right but the path does not resolve inside it. */
44
+ | 'no-such-node'
45
+ /** It resolves, but not to what the claim said was there. */
46
+ | 'mismatch';
47
+ export type CitationVerdict = {
48
+ ok: true;
49
+ path: string;
50
+ value: unknown;
51
+ } | {
52
+ ok: false;
53
+ path: string;
54
+ reason: CitationFailure;
55
+ detail: string;
56
+ };
57
+ export declare function verifyCitation(citation: Citation, index: CaptureIndex): CitationVerdict;
58
+ export interface VerifiedClaim<T> {
59
+ claim: T;
60
+ kept: boolean;
61
+ supported: CitationVerdict[];
62
+ rejected: Array<Extract<CitationVerdict, {
63
+ ok: false;
64
+ }>>;
65
+ /** Why a dropped claim was dropped, in one sentence a human can act on. */
66
+ reason?: string;
67
+ }
68
+ /**
69
+ * Keep a claim only if it cites, and every citation resolves.
70
+ *
71
+ * All-or-nothing on purpose: a claim standing on three facts and one invention
72
+ * is not three-quarters true. Letting it through with the bad citation stripped
73
+ * would hide exactly the failure this check exists to surface — and the stage
74
+ * that produced it would never learn.
75
+ */
76
+ export declare function verifyClaim<T extends {
77
+ evidence?: Citation[];
78
+ }>(claim: T, index: CaptureIndex): VerifiedClaim<T>;
79
+ /** Load the capture written by `scan` into an index the verifier can use. */
80
+ export declare function loadCaptureIndex(dir?: string): CaptureIndex;
@@ -0,0 +1,197 @@
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.SHARED_REF = void 0;
37
+ exports.verifyCitation = verifyCitation;
38
+ exports.verifyClaim = verifyClaim;
39
+ exports.loadCaptureIndex = loadCaptureIndex;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const config_1 = require("./config");
43
+ exports.SHARED_REF = '_shared-calls.json';
44
+ /** `pages/checkout.json#forms[0].fields[1]` → { file, segments }. */
45
+ function parseRef(ref) {
46
+ if (typeof ref !== 'string')
47
+ return null;
48
+ const hash = ref.indexOf('#');
49
+ if (hash === -1)
50
+ return null;
51
+ const file = ref.slice(0, hash).trim();
52
+ const rest = ref.slice(hash + 1).trim();
53
+ if (!file)
54
+ return null;
55
+ const segments = [];
56
+ // `forms[0].fields[1]` → ['forms', 0, 'fields', 1]
57
+ for (const part of rest.split('.')) {
58
+ if (part === '')
59
+ continue;
60
+ const m = /^([A-Za-z_][A-Za-z0-9_]*)((?:\[\d+\])*)$/.exec(part);
61
+ if (!m)
62
+ return null;
63
+ segments.push(m[1]);
64
+ for (const idx of m[2].matchAll(/\[(\d+)\]/g))
65
+ segments.push(Number(idx[1]));
66
+ }
67
+ return { file, segments };
68
+ }
69
+ function walk(root, segments) {
70
+ let node = root;
71
+ const trail = [];
72
+ for (const seg of segments) {
73
+ trail.push(typeof seg === 'number' ? `[${seg}]` : (trail.length ? '.' : '') + seg);
74
+ if (node === null || node === undefined)
75
+ return { ok: false, at: trail.join('') };
76
+ if (typeof seg === 'number') {
77
+ if (!Array.isArray(node) || seg >= node.length)
78
+ return { ok: false, at: trail.join('') };
79
+ }
80
+ else if (typeof node !== 'object' || !(seg in node)) {
81
+ return { ok: false, at: trail.join('') };
82
+ }
83
+ node = node[seg];
84
+ }
85
+ return { ok: true, value: node };
86
+ }
87
+ /** `pages/checkout.json` → `checkout`. Tolerates a bare page name too. */
88
+ function pageNameFromRef(file) {
89
+ const m = /^pages\/(.+)\.json$/.exec(file);
90
+ if (m)
91
+ return m[1];
92
+ if (!file.includes('/') && !file.endsWith('.json'))
93
+ return file;
94
+ return null;
95
+ }
96
+ function verifyCitation(citation, index) {
97
+ const ref = typeof citation === 'string' ? citation : citation?.path;
98
+ const expect = typeof citation === 'string' ? undefined : citation?.expect;
99
+ const parsed = parseRef(ref);
100
+ if (!parsed) {
101
+ return {
102
+ ok: false,
103
+ path: String(ref ?? ''),
104
+ reason: 'malformed',
105
+ detail: 'Expected "<file>#<path>", e.g. pages/checkout.json#forms[0].fields[1].',
106
+ };
107
+ }
108
+ let root;
109
+ if (parsed.file === exports.SHARED_REF) {
110
+ if (!index.shared) {
111
+ return { ok: false, path: ref, reason: 'unknown-page', detail: `This capture has no ${exports.SHARED_REF}.` };
112
+ }
113
+ root = index.shared;
114
+ }
115
+ else {
116
+ const name = pageNameFromRef(parsed.file);
117
+ const page = name ? index.pages.get(name) : undefined;
118
+ if (!page) {
119
+ const known = [...index.pages.keys()].slice(0, 8).join(', ');
120
+ return {
121
+ ok: false,
122
+ path: ref,
123
+ reason: 'unknown-page',
124
+ detail: `No page "${name ?? parsed.file}" in this capture${known ? ` (have: ${known}…)` : ''}.`,
125
+ };
126
+ }
127
+ root = page;
128
+ }
129
+ const found = walk(root, parsed.segments);
130
+ if (!found.ok) {
131
+ return { ok: false, path: ref, reason: 'no-such-node', detail: `Nothing at "${found.at}" in ${parsed.file}.` };
132
+ }
133
+ if (expect && Object.keys(expect).length > 0) {
134
+ const value = found.value;
135
+ for (const [key, want] of Object.entries(expect)) {
136
+ const got = value === null || typeof value !== 'object' ? undefined : value[key];
137
+ if (got !== want) {
138
+ return {
139
+ ok: false,
140
+ path: ref,
141
+ reason: 'mismatch',
142
+ detail: `Claimed ${key}=${JSON.stringify(want)}, capture has ${JSON.stringify(got)}.`,
143
+ };
144
+ }
145
+ }
146
+ }
147
+ return { ok: true, path: ref, value: found.value };
148
+ }
149
+ /**
150
+ * Keep a claim only if it cites, and every citation resolves.
151
+ *
152
+ * All-or-nothing on purpose: a claim standing on three facts and one invention
153
+ * is not three-quarters true. Letting it through with the bad citation stripped
154
+ * would hide exactly the failure this check exists to surface — and the stage
155
+ * that produced it would never learn.
156
+ */
157
+ function verifyClaim(claim, index) {
158
+ const citations = claim?.evidence ?? [];
159
+ if (citations.length === 0) {
160
+ return { claim, kept: false, supported: [], rejected: [], reason: 'No evidence cited.' };
161
+ }
162
+ const verdicts = citations.map((c) => verifyCitation(c, index));
163
+ const rejected = verdicts.filter((v) => !v.ok);
164
+ return {
165
+ claim,
166
+ kept: rejected.length === 0,
167
+ supported: verdicts.filter((v) => v.ok),
168
+ rejected,
169
+ reason: rejected.length > 0 ? `${rejected.length} of ${verdicts.length} citations did not check out.` : undefined,
170
+ };
171
+ }
172
+ /** Load the capture written by `scan` into an index the verifier can use. */
173
+ function loadCaptureIndex(dir = (0, config_1.captureDir)()) {
174
+ const pages = new Map();
175
+ let shared;
176
+ if (!fs.existsSync(dir))
177
+ return { pages };
178
+ for (const entry of fs.readdirSync(dir)) {
179
+ if (!entry.endsWith('.json'))
180
+ continue;
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(fs.readFileSync(path.join(dir, entry), 'utf8'));
184
+ }
185
+ catch {
186
+ continue; // an unreadable file is not a citable one
187
+ }
188
+ if (entry === exports.SHARED_REF) {
189
+ shared = parsed;
190
+ }
191
+ else if (entry !== 'surface.json' && parsed && typeof parsed.page === 'string') {
192
+ pages.set(parsed.page, parsed);
193
+ }
194
+ }
195
+ return { pages, ...(shared ? { shared } : {}) };
196
+ }
197
+ //# sourceMappingURL=citations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citations.js","sourceRoot":"","sources":["../src/citations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ import { type Classification, type ClassifyResult } from './ai/classify';
2
+ import type { PageCapture } from './capture/page';
3
+ /**
4
+ * Classification, as the two reports show it.
5
+ *
6
+ * One module because the markdown and the HTML must not be able to word the
7
+ * same thing differently — the `checkHeadline` rule from `@ia-qa/self-healing`,
8
+ * for the same reason: two renderers of one artifact are two sources of truth
9
+ * the day one of them is edited alone.
10
+ *
11
+ * Three things this layer owes the reader, none of which the raw file carries.
12
+ *
13
+ * **It is not the same kind of evidence as everything around it.** The rest of
14
+ * both reports is a literal DOM observation; this is a model's reading of one.
15
+ * Presented in the same table, in the same ink, it inherits a credibility it did
16
+ * not earn — so it is a section of its own, labelled, carrying the confidence
17
+ * and the sentence, never the label alone.
18
+ *
19
+ * **It expires, and the file cannot say so.** `classification.json` is written
20
+ * from a capture taken at a moment; `scan` overwrites captures in place. A
21
+ * classification older than the page it describes may be describing a form that
22
+ * no longer exists — the confident-wrong-number defect this package keeps
23
+ * meeting. `stale` is computed here, per page, and stated.
24
+ *
25
+ * **Silence has two meanings.** A page with entry points and no classification
26
+ * was either never sent or its call failed; a page with no entry points was
27
+ * never a candidate. Collapsing those into "not classified" is the same defect
28
+ * as reading an empty history as "nothing drifted", so they are counted apart.
29
+ */
30
+ export interface ClassificationDocument {
31
+ schema: string;
32
+ taxonomyVersion: string;
33
+ classifiedAt: string;
34
+ model: string;
35
+ totals: {
36
+ kept: number;
37
+ refused: number;
38
+ dropped: number;
39
+ };
40
+ pages: Record<string, ClassifyResult>;
41
+ written?: boolean;
42
+ }
43
+ export interface ClassifiedPage {
44
+ page: string;
45
+ kept: Classification[];
46
+ refused: Classification[];
47
+ dropped: number;
48
+ /** The call did not succeed — a fact about the model, never about the app. */
49
+ failed?: string;
50
+ /**
51
+ * The page was re-captured after it was classified, so this reading describes
52
+ * a version of the page that is no longer on disk.
53
+ */
54
+ stale: boolean;
55
+ }
56
+ export interface ClassificationView {
57
+ model: string;
58
+ classifiedAt: string;
59
+ taxonomyVersion: string;
60
+ pages: ClassifiedPage[];
61
+ /** Pages that offer entry points but carry no classification at all. */
62
+ unclassified: string[];
63
+ /** Pages with nothing to classify — not a gap, and never counted as one. */
64
+ noEntryPoints: number;
65
+ staleCount: number;
66
+ keptTotal: number;
67
+ refusedTotal: number;
68
+ droppedTotal: number;
69
+ /** Security tools the kept classifications select, deduplicated. */
70
+ securityTools: Array<{
71
+ tool: string;
72
+ pages: string[];
73
+ }>;
74
+ }
75
+ /** Where `ia-qa-discover-ai` writes. Never `capture/`: that directory is pages. */
76
+ export declare function classificationPath(cwd?: string): string;
77
+ /**
78
+ * Read the classification, or null when there is none.
79
+ *
80
+ * A file whose `schema` is not the one this build understands is treated as
81
+ * absent rather than parsed optimistically: a report that renders fields it
82
+ * guessed at is worse than a report with no classification section.
83
+ */
84
+ export declare function loadClassification(file?: string): ClassificationDocument | null;
85
+ /**
86
+ * Pair a classification document with the capture the reports are rendering.
87
+ *
88
+ * `pages` is the current capture, so every count below is relative to what is on
89
+ * disk NOW — a classification naming a page that no longer exists is dropped
90
+ * from the view entirely rather than shown against nothing.
91
+ */
92
+ export declare function buildClassificationView(doc: ClassificationDocument, pages: PageCapture[]): ClassificationView;
93
+ /**
94
+ * The one-line summary both renderers put at the top of the section.
95
+ *
96
+ * It leads with what was kept and immediately qualifies it, because a bare
97
+ * "12 classified" is precisely the confident number this package spends its
98
+ * design on refusing to print.
99
+ */
100
+ export declare function classificationHeadline(v: ClassificationView): string;
101
+ /**
102
+ * The reminder, deliberately softer than the deterministic one.
103
+ *
104
+ * The tool already refuses a claim whose citation does not resolve, so telling
105
+ * the reader to check everything again would be alarm fatigue — a warning that
106
+ * fires on the good rows too is a warning people learn to skip, and then it is
107
+ * not there when it matters. So it names the exact residual risk instead: the
108
+ * check proved the citation, never the reading built on it.
109
+ */
110
+ export declare const CLASSIFICATION_CAVEAT: string;
111
+ /** Why a stale row is not simply out of date. */
112
+ export declare const STALE_CAVEAT: string;
113
+ /**
114
+ * The whole read-and-pair step, for a caller that has pages and wants the
115
+ * section. Returns null when there is no classification — the normal case, and
116
+ * every renderer must be complete without one.
117
+ */
118
+ export declare function classificationFor(pages: PageCapture[], file?: string): ClassificationView | null;