@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,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;
|