@qa-ai-stlc/explorer 0.3.0 → 0.5.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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Turns human-meaningful, untrusted text (an accessible name, an ARIA label) into a valid camelCase
3
+ * identifier, falling back to `fallback` (a domain word guaranteed non-empty, like an element
4
+ * `kind`) when `text` has no letters or digits at all (e.g. an icon-only "→" accessible name).
5
+ * Never returns a reserved word or an identifier starting with a digit.
6
+ */
7
+ export declare function toCamelCaseIdentifier(text: string, fallback: string): string;
8
+ /**
9
+ * Produces the camelCase export name a registry element's locator module entry will use
10
+ * (ADR-006). Each call to the returned function assigns one element's name; two elements
11
+ * resolving to the same base name (e.g. two buttons both named "Submit") get a numeric suffix,
12
+ * tracked across every call against the same namer, so no two elements it names ever collide.
13
+ */
14
+ export declare function createElementNamer(): (text: string, fallback: string) => string;
15
+ //# sourceMappingURL=naming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAiEA;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU5E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAQ/E"}
package/dist/naming.js ADDED
@@ -0,0 +1,94 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Splits on anything that is not a letter or digit, dropping empty segments, so accented and
4
+ // non-Latin text collapses to nothing rather than surviving as invalid identifier characters.
5
+ function wordsOf(text) {
6
+ return text.split(/[^A-Za-z0-9]+/u).filter((word) => word.length > 0);
7
+ }
8
+ // An accessible name equal to a reserved word ("Export", "Delete") would otherwise produce a
9
+ // locator module export that fails to compile.
10
+ const RESERVED_WORDS = new Set([
11
+ 'break',
12
+ 'case',
13
+ 'catch',
14
+ 'class',
15
+ 'const',
16
+ 'continue',
17
+ 'debugger',
18
+ 'default',
19
+ 'delete',
20
+ 'do',
21
+ 'else',
22
+ 'enum',
23
+ 'export',
24
+ 'extends',
25
+ 'false',
26
+ 'finally',
27
+ 'for',
28
+ 'function',
29
+ 'if',
30
+ 'implements',
31
+ 'import',
32
+ 'in',
33
+ 'instanceof',
34
+ 'interface',
35
+ 'let',
36
+ 'new',
37
+ 'null',
38
+ 'package',
39
+ 'private',
40
+ 'protected',
41
+ 'public',
42
+ 'return',
43
+ 'static',
44
+ 'super',
45
+ 'switch',
46
+ 'this',
47
+ 'throw',
48
+ 'true',
49
+ 'try',
50
+ 'typeof',
51
+ 'var',
52
+ 'void',
53
+ 'while',
54
+ 'with',
55
+ 'yield',
56
+ ]);
57
+ // `.charAt(0)` (unlike `word[0]`) always returns a plain `string` even under
58
+ // `noUncheckedIndexedAccess`, so this never needs an `undefined` fallback for an empty `word`.
59
+ function upperFirst(word) {
60
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
61
+ }
62
+ /**
63
+ * Turns human-meaningful, untrusted text (an accessible name, an ARIA label) into a valid camelCase
64
+ * identifier, falling back to `fallback` (a domain word guaranteed non-empty, like an element
65
+ * `kind`) when `text` has no letters or digits at all (e.g. an icon-only "→" accessible name).
66
+ * Never returns a reserved word or an identifier starting with a digit.
67
+ */
68
+ export function toCamelCaseIdentifier(text, fallback) {
69
+ const words = wordsOf(text);
70
+ const source = words.length > 0 ? words : wordsOf(fallback);
71
+ const identifier = source
72
+ .map((word, index) => (index === 0 ? word.toLowerCase() : upperFirst(word.toLowerCase())))
73
+ .join('');
74
+ if (/^[0-9]/u.test(identifier)) {
75
+ return `element${upperFirst(identifier)}`;
76
+ }
77
+ return RESERVED_WORDS.has(identifier) ? `${identifier}Element` : identifier;
78
+ }
79
+ /**
80
+ * Produces the camelCase export name a registry element's locator module entry will use
81
+ * (ADR-006). Each call to the returned function assigns one element's name; two elements
82
+ * resolving to the same base name (e.g. two buttons both named "Submit") get a numeric suffix,
83
+ * tracked across every call against the same namer, so no two elements it names ever collide.
84
+ */
85
+ export function createElementNamer() {
86
+ const seenNameCounts = new Map();
87
+ return (text, fallback) => {
88
+ const base = toCamelCaseIdentifier(text, fallback);
89
+ const seen = seenNameCounts.get(base) ?? 0;
90
+ seenNameCounts.set(base, seen + 1);
91
+ return seen === 0 ? base : `${base}${String(seen + 1)}`;
92
+ };
93
+ }
94
+ //# sourceMappingURL=naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC,6FAA6F;AAC7F,8FAA8F;AAC9F,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,6FAA6F;AAC7F,+CAA+C;AAC/C,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,OAAO;IACP,MAAM;IACN,OAAO;IACP,OAAO;IACP,OAAO;IACP,UAAU;IACV,UAAU;IACV,SAAS;IACT,QAAQ;IACR,IAAI;IACJ,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,OAAO;IACP,SAAS;IACT,KAAK;IACL,UAAU;IACV,IAAI;IACJ,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ,YAAY;IACZ,WAAW;IACX,KAAK;IACL,KAAK;IACL,MAAM;IACN,SAAS;IACT,SAAS;IACT,WAAW;IACX,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,KAAK;IACL,QAAQ;IACR,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AAEH,6EAA6E;AAC7E,+FAA+F;AAC/F,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,QAAgB;IAClE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,MAAM;SACtB,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;SACzF,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,UAAU,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IAC5C,CAAC;IACD,OAAO,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IACjD,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;QACxB,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;QACnC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;IAC1D,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,83 @@
1
+ import { type AuthPage, type ViewportSize } from '@qa-ai-stlc/core';
2
+ import type { InteractiveElement, LocatorCandidate, SelectorElement } from '@qa-ai-stlc/schemas';
3
+ import { type LocatorPolicy } from './synthesize-locators.js';
4
+ /**
5
+ * One element a human captured by clicking it in the overlay, before locator synthesis: the same
6
+ * signal `extractPageElements()` reads for a crawled element (development plan section 6.1, "Manual
7
+ * pick mode"), scoped to the same five interactive tags a crawl recognizes so a picked element
8
+ * always resolves to a valid `InteractiveElementKind`.
9
+ */
10
+ export interface PickModeCapture {
11
+ readonly pickId: string;
12
+ readonly kind: InteractiveElement['kind'];
13
+ readonly tagName: string;
14
+ readonly nthOfType: number;
15
+ readonly accessibleName?: string;
16
+ readonly testId?: string;
17
+ readonly role?: string;
18
+ readonly label?: string;
19
+ readonly placeholder?: string;
20
+ readonly htmlId?: string;
21
+ }
22
+ interface PickModeState {
23
+ readonly done: boolean;
24
+ readonly captures: readonly PickModeCapture[];
25
+ }
26
+ /**
27
+ * Injects the pick-mode overlay into the current page (development plan section 6.1): a fixed
28
+ * toolbar with a "Finish picking" button, plus a capture-phase click listener that records the
29
+ * clicked element (or its closest interactive ancestor) instead of letting the click reach the
30
+ * application, so a click on a submit button never actually submits the form (safe mode, AGENTS.md
31
+ * 12.4). A second call on the same page is a no-op — the overlay is idempotent across re-navigation
32
+ * within the same pick-mode session.
33
+ */
34
+ export declare function injectPickModeOverlay(page: AuthPage): Promise<void>;
35
+ /** Reads the overlay's current state: every element captured so far, and whether the human is done. */
36
+ export declare function readPickModeState(page: AuthPage): Promise<PickModeState>;
37
+ export interface WaitForPickModeCompletionOptions {
38
+ readonly pollIntervalMs?: number;
39
+ readonly timeoutMs?: number;
40
+ readonly signal?: AbortSignal;
41
+ /** Injected so tests do not wait on real timers (AGENTS.md 5.5, 13). */
42
+ readonly wait?: (ms: number) => Promise<void>;
43
+ }
44
+ /**
45
+ * Polls the overlay until the human clicks "Finish picking" (or the given `signal` cancels the
46
+ * session), returning every element captured. Never resolves early because nothing was clicked yet:
47
+ * an empty pick-mode session is a human decision, not a failure, so it only rejects on cancellation
48
+ * or on exceeding `timeoutMs`.
49
+ */
50
+ export declare function waitForPickModeCompletion(page: AuthPage, options?: WaitForPickModeCompletionOptions): Promise<readonly PickModeCapture[]>;
51
+ /** One captured element with its locator candidates synthesized and its primary candidate scored. */
52
+ export interface PickModeElement {
53
+ readonly pickId: string;
54
+ readonly elementId: string;
55
+ readonly kind: string;
56
+ /** The human-meaningful text a confirmed name is derived from, before camelCasing (naming.ts). */
57
+ readonly defaultNameText: string;
58
+ readonly locatorCandidates: readonly LocatorCandidate[];
59
+ readonly stabilityScore: number;
60
+ }
61
+ export interface CapturePickModeElementsOptions {
62
+ readonly policy?: LocatorPolicy;
63
+ readonly viewports?: readonly ViewportSize[];
64
+ }
65
+ /**
66
+ * Synthesizes locator candidates and scores the primary candidate for every element a human
67
+ * captured, against the live page they were picked from (development plan section 6.1 step 3). The
68
+ * `elementId` assigned here is the same one a crawl would assign the same element (build-selector-
69
+ * registry.ts's `computeElementId`), so a page later crawled does not duplicate a manually-picked
70
+ * entry.
71
+ */
72
+ export declare function capturePickModeElements(page: AuthPage, url: string, captures: readonly PickModeCapture[], options?: CapturePickModeElementsOptions): Promise<PickModeElement[]>;
73
+ /**
74
+ * Turns captured, scored elements into `source: 'manual'` registry entries (development plan
75
+ * section 6.1: "the agent proposes logical names ... the human confirms"). Naming itself never
76
+ * happens here — the engine makes no model calls (AGENTS.md non-negotiable boundary 1) — so
77
+ * `nameOverrides` carries whatever name the human confirmed for a given `pickId`; an element with
78
+ * no override falls back to its `defaultNameText`. Names are deduplicated across the whole batch the
79
+ * same way a crawled registry's names are (naming.ts's `createElementNamer`).
80
+ */
81
+ export declare function finalizeManualSelectorEntries(elements: readonly PickModeElement[], nameOverrides: ReadonlyMap<string, string>, generatedAt: string): SelectorElement[];
82
+ export {};
83
+ //# sourceMappingURL=pick-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pick-mode.d.ts","sourceRoot":"","sources":["../src/pick-mode.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAIjG,OAAO,EAA+B,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAoIzE;AAwBD,uGAAuG;AACvG,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC,CAM9E;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAUD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAqBrC;AAED,qGAAqG;AACrG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACxD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CAC9C;AA6BD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,SAAS,eAAe,EAAE,EACpC,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,eAAe,EAAE,CAAC,CA4B5B;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,SAAS,eAAe,EAAE,EACpC,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1C,WAAW,EAAE,MAAM,GAClB,eAAe,EAAE,CAanB"}
@@ -0,0 +1,265 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { QaError } from '@qa-ai-stlc/core';
4
+ import { computeElementId, elementNameForId } from './build-selector-registry.js';
5
+ import { createElementNamer } from './naming.js';
6
+ import { scoreLocatorStability } from './stability-scoring.js';
7
+ import { synthesizeLocatorCandidates } from './synthesize-locators.js';
8
+ /**
9
+ * Injects the pick-mode overlay into the current page (development plan section 6.1): a fixed
10
+ * toolbar with a "Finish picking" button, plus a capture-phase click listener that records the
11
+ * clicked element (or its closest interactive ancestor) instead of letting the click reach the
12
+ * application, so a click on a submit button never actually submits the form (safe mode, AGENTS.md
13
+ * 12.4). A second call on the same page is a no-op — the overlay is idempotent across re-navigation
14
+ * within the same pick-mode session.
15
+ */
16
+ export async function injectPickModeOverlay(page) {
17
+ /* v8 ignore next 95 -- runs in the browser's own V8 instance, invisible to Node coverage */
18
+ await page.evaluate(() => {
19
+ const globalWindow = window;
20
+ if (globalWindow.__qaPickMode !== undefined) {
21
+ return;
22
+ }
23
+ const state = { done: false, captures: [] };
24
+ globalWindow.__qaPickMode = state;
25
+ let nextPickId = 1;
26
+ function accessibleName(element) {
27
+ const ariaLabel = element.getAttribute('aria-label');
28
+ if (ariaLabel !== null && ariaLabel.trim().length > 0) {
29
+ return ariaLabel;
30
+ }
31
+ const text = element.textContent.trim();
32
+ return text.length > 0 ? text : undefined;
33
+ }
34
+ function computeRole(element, kind) {
35
+ const explicit = element.getAttribute('role');
36
+ if (explicit !== null && explicit.trim().length > 0) {
37
+ return explicit;
38
+ }
39
+ if (kind === 'textarea') {
40
+ return 'textbox';
41
+ }
42
+ if (kind === 'select') {
43
+ return 'combobox';
44
+ }
45
+ if (kind === 'input') {
46
+ const type = (element.getAttribute('type') ?? 'text').toLowerCase();
47
+ if (type === 'checkbox' || type === 'radio') {
48
+ return type;
49
+ }
50
+ if (type === 'button' || type === 'submit' || type === 'reset') {
51
+ return 'button';
52
+ }
53
+ return 'textbox';
54
+ }
55
+ return kind === 'button' || kind === 'link' ? kind : undefined;
56
+ }
57
+ function labelText(element) {
58
+ const id = element.getAttribute('id');
59
+ if (id !== null && id.length > 0) {
60
+ const associated = document.querySelector(`label[for="${CSS.escape(id)}"]`);
61
+ const text = associated?.textContent.trim();
62
+ if (text !== undefined && text.length > 0) {
63
+ return text;
64
+ }
65
+ }
66
+ const wrapping = element.closest('label')?.textContent.trim();
67
+ return wrapping !== undefined && wrapping.length > 0 ? wrapping : undefined;
68
+ }
69
+ function nthOfType(element) {
70
+ const parent = element.parentElement;
71
+ if (parent === null) {
72
+ return 1;
73
+ }
74
+ let index = 0;
75
+ for (const sibling of parent.children) {
76
+ if (sibling.tagName === element.tagName) {
77
+ index += 1;
78
+ if (sibling === element) {
79
+ return index;
80
+ }
81
+ }
82
+ }
83
+ return 1;
84
+ }
85
+ const kindByTagName = {
86
+ a: 'link',
87
+ button: 'button',
88
+ input: 'input',
89
+ select: 'select',
90
+ textarea: 'textarea',
91
+ };
92
+ const overlay = document.createElement('div');
93
+ overlay.id = 'qa-pick-mode-overlay';
94
+ overlay.style.cssText =
95
+ 'position:fixed;top:0;right:0;z-index:2147483647;background:#111;color:#fff;' +
96
+ 'padding:8px 12px;font:13px sans-serif;display:flex;gap:8px;align-items:center;';
97
+ overlay.textContent = 'QA pick mode: click an element to capture it.';
98
+ const finishButton = document.createElement('button');
99
+ finishButton.id = 'qa-pick-mode-finish';
100
+ finishButton.type = 'button';
101
+ finishButton.textContent = 'Finish picking';
102
+ finishButton.addEventListener('click', () => {
103
+ state.done = true;
104
+ });
105
+ overlay.appendChild(finishButton);
106
+ document.body.appendChild(overlay);
107
+ document.addEventListener('click', (event) => {
108
+ const target = event.target;
109
+ if (!(target instanceof Element) || target.closest('#qa-pick-mode-overlay') !== null) {
110
+ return;
111
+ }
112
+ const interactive = target.closest('button, a[href], input, select, textarea');
113
+ if (interactive === null) {
114
+ return;
115
+ }
116
+ event.preventDefault();
117
+ event.stopPropagation();
118
+ const tagName = interactive.tagName.toLowerCase();
119
+ const kind = kindByTagName[tagName] ?? tagName;
120
+ state.captures.push({
121
+ pickId: `pick-${String(nextPickId)}`,
122
+ kind,
123
+ tagName,
124
+ nthOfType: nthOfType(interactive),
125
+ accessibleName: accessibleName(interactive),
126
+ testId: interactive.getAttribute('data-testid') ?? undefined,
127
+ role: computeRole(interactive, kind),
128
+ label: labelText(interactive),
129
+ placeholder: interactive.getAttribute('placeholder') ?? undefined,
130
+ htmlId: interactive.getAttribute('id') ?? undefined,
131
+ });
132
+ nextPickId += 1;
133
+ }, { capture: true });
134
+ });
135
+ }
136
+ function isPickModeCapture(value) {
137
+ if (typeof value !== 'object' || value === null) {
138
+ return false;
139
+ }
140
+ const record = value;
141
+ return (typeof record.pickId === 'string' && typeof record.kind === 'string' && typeof record.tagName === 'string');
142
+ }
143
+ function isPickModeState(value) {
144
+ if (typeof value !== 'object' || value === null) {
145
+ return false;
146
+ }
147
+ const record = value;
148
+ return (typeof record.done === 'boolean' &&
149
+ Array.isArray(record.captures) &&
150
+ record.captures.every(isPickModeCapture));
151
+ }
152
+ /** Reads the overlay's current state: every element captured so far, and whether the human is done. */
153
+ export async function readPickModeState(page) {
154
+ /* v8 ignore next 3 -- runs in the browser's own V8 instance, invisible to Node coverage */
155
+ const result = await page.evaluate(() => {
156
+ return window.__qaPickMode;
157
+ });
158
+ return isPickModeState(result) ? result : { done: false, captures: [] };
159
+ }
160
+ const DEFAULT_POLL_INTERVAL_MS = 500;
161
+ // A human research session, not a machine operation: generous, but still bounded (AGENTS.md 5.5).
162
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
163
+ function defaultWait(ms) {
164
+ return new Promise((resolve) => setTimeout(resolve, ms));
165
+ }
166
+ /**
167
+ * Polls the overlay until the human clicks "Finish picking" (or the given `signal` cancels the
168
+ * session), returning every element captured. Never resolves early because nothing was clicked yet:
169
+ * an empty pick-mode session is a human decision, not a failure, so it only rejects on cancellation
170
+ * or on exceeding `timeoutMs`.
171
+ */
172
+ export async function waitForPickModeCompletion(page, options = {}) {
173
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
174
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
175
+ const wait = options.wait ?? defaultWait;
176
+ const deadline = Date.now() + timeoutMs;
177
+ for (;;) {
178
+ options.signal?.throwIfAborted();
179
+ const state = await readPickModeState(page);
180
+ if (state.done) {
181
+ return state.captures;
182
+ }
183
+ if (Date.now() >= deadline) {
184
+ throw new QaError('explorer.pick_mode.timeout', `Pick mode did not finish within ${String(timeoutMs)}ms`, { remediation: 'Click "Finish picking" in the overlay once every element has been captured.' });
185
+ }
186
+ await wait(pollIntervalMs);
187
+ }
188
+ }
189
+ function toInteractiveElement(capture) {
190
+ const element = {
191
+ kind: capture.kind,
192
+ tagName: capture.tagName,
193
+ nthOfType: capture.nthOfType,
194
+ };
195
+ if (capture.accessibleName !== undefined) {
196
+ element.accessibleName = capture.accessibleName;
197
+ }
198
+ if (capture.testId !== undefined) {
199
+ element.testId = capture.testId;
200
+ }
201
+ if (capture.role !== undefined) {
202
+ element.role = capture.role;
203
+ }
204
+ if (capture.label !== undefined) {
205
+ element.label = capture.label;
206
+ }
207
+ if (capture.placeholder !== undefined) {
208
+ element.placeholder = capture.placeholder;
209
+ }
210
+ if (capture.htmlId !== undefined) {
211
+ element.htmlId = capture.htmlId;
212
+ }
213
+ return element;
214
+ }
215
+ /**
216
+ * Synthesizes locator candidates and scores the primary candidate for every element a human
217
+ * captured, against the live page they were picked from (development plan section 6.1 step 3). The
218
+ * `elementId` assigned here is the same one a crawl would assign the same element (build-selector-
219
+ * registry.ts's `computeElementId`), so a page later crawled does not duplicate a manually-picked
220
+ * entry.
221
+ */
222
+ export async function capturePickModeElements(page, url, captures, options = {}) {
223
+ const policy = options.policy ?? 'playwright-default';
224
+ const elements = [];
225
+ for (const capture of captures) {
226
+ const interactiveElement = toInteractiveElement(capture);
227
+ const candidates = synthesizeLocatorCandidates(interactiveElement, policy);
228
+ const primary = candidates[0];
229
+ const stabilityScore = primary === undefined
230
+ ? 0
231
+ : await scoreLocatorStability(page, primary, options.viewports === undefined ? {} : { viewports: options.viewports });
232
+ elements.push({
233
+ pickId: capture.pickId,
234
+ elementId: computeElementId(url, interactiveElement),
235
+ kind: interactiveElement.kind,
236
+ defaultNameText: elementNameForId(interactiveElement),
237
+ locatorCandidates: candidates,
238
+ stabilityScore,
239
+ });
240
+ }
241
+ return elements;
242
+ }
243
+ /**
244
+ * Turns captured, scored elements into `source: 'manual'` registry entries (development plan
245
+ * section 6.1: "the agent proposes logical names ... the human confirms"). Naming itself never
246
+ * happens here — the engine makes no model calls (AGENTS.md non-negotiable boundary 1) — so
247
+ * `nameOverrides` carries whatever name the human confirmed for a given `pickId`; an element with
248
+ * no override falls back to its `defaultNameText`. Names are deduplicated across the whole batch the
249
+ * same way a crawled registry's names are (naming.ts's `createElementNamer`).
250
+ */
251
+ export function finalizeManualSelectorEntries(elements, nameOverrides, generatedAt) {
252
+ const nameFor = createElementNamer();
253
+ return elements.map((element) => ({
254
+ elementId: element.elementId,
255
+ name: nameFor(nameOverrides.get(element.pickId) ?? element.defaultNameText, element.kind),
256
+ kind: element.kind,
257
+ locatorCandidates: [...element.locatorCandidates],
258
+ stabilityScore: element.stabilityScore,
259
+ lastVerifiedAt: generatedAt,
260
+ pii: false,
261
+ dynamicText: false,
262
+ source: 'manual',
263
+ }));
264
+ }
265
+ //# sourceMappingURL=pick-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pick-mode.js","sourceRoot":"","sources":["../src/pick-mode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { AuthPage, PageLocator, ViewportSize } from '@qa-ai-stlc/core';
2
+ import type { LocatorCandidate } from '@qa-ai-stlc/schemas';
3
+ export interface StabilityScoringOptions {
4
+ /** Viewport sizes checked after uniqueness and reload survival; defaults to desktop/tablet/mobile. */
5
+ readonly viewports?: readonly ViewportSize[];
6
+ }
7
+ export declare const DEFAULT_VIEWPORTS: readonly ViewportSize[];
8
+ /** Resolves one of P1-08's `LocatorCandidate` values to a live `PageLocator`, by strategy. */
9
+ export declare function resolveCandidateLocator(page: AuthPage, candidate: LocatorCandidate): PageLocator;
10
+ /**
11
+ * Verifies one locator candidate against the current live page: uniqueness now, then survival
12
+ * across a reload and across each configured viewport size (development plan section 6.3.5).
13
+ * Uniqueness is a hard gate — a candidate that does not resolve to exactly one element right now
14
+ * cannot be called stable, so reload/viewport are never checked against a candidate that has
15
+ * already failed. The page's original viewport size is restored before returning, so scoring one
16
+ * element's candidate does not affect the next.
17
+ */
18
+ export declare function scoreLocatorStability(page: AuthPage, candidate: LocatorCandidate, options?: StabilityScoringOptions): Promise<number>;
19
+ //# sourceMappingURL=stability-scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stability-scoring.d.ts","sourceRoot":"","sources":["../src/stability-scoring.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,sGAAsG;IACtG,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CAC9C;AAED,eAAO,MAAM,iBAAiB,EAAE,SAAS,YAAY,EAIpD,CAAC;AAkCF,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,GAAG,WAAW,CAmBhG;AAOD;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,gBAAgB,EAC3B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CA0BjB"}
@@ -0,0 +1,82 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { QaError } from '@qa-ai-stlc/core';
4
+ export const DEFAULT_VIEWPORTS = [
5
+ { width: 1280, height: 720 },
6
+ { width: 768, height: 1024 },
7
+ { width: 375, height: 667 },
8
+ ];
9
+ function parseRoleCandidateValue(value) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(value);
13
+ }
14
+ catch (cause) {
15
+ throw new QaError('locator-candidate-invalid', `Role locator candidate value is not valid JSON: "${value}".`, { cause });
16
+ }
17
+ if (typeof parsed !== 'object' ||
18
+ parsed === null ||
19
+ typeof parsed.role !== 'string') {
20
+ throw new QaError('locator-candidate-invalid', `Role locator candidate value has no "role" string: "${value}".`);
21
+ }
22
+ const record = parsed;
23
+ return typeof record.name === 'string'
24
+ ? { role: record.role, name: record.name }
25
+ : { role: record.role };
26
+ }
27
+ /** Resolves one of P1-08's `LocatorCandidate` values to a live `PageLocator`, by strategy. */
28
+ export function resolveCandidateLocator(page, candidate) {
29
+ switch (candidate.strategy) {
30
+ case 'role': {
31
+ const { role, name } = parseRoleCandidateValue(candidate.value);
32
+ return page.getByRole(role, name === undefined ? undefined : { name });
33
+ }
34
+ case 'testId':
35
+ return page.getByTestId(candidate.value);
36
+ case 'label':
37
+ return page.getByLabel(candidate.value);
38
+ case 'placeholder':
39
+ return page.getByPlaceholder(candidate.value);
40
+ case 'text':
41
+ return page.getByText(candidate.value);
42
+ case 'css':
43
+ return page.locator(candidate.value);
44
+ default:
45
+ throw new QaError('locator-strategy-unknown', `Unknown locator strategy "${candidate.strategy}".`);
46
+ }
47
+ }
48
+ async function isUniqueMatch(page, candidate) {
49
+ const count = await resolveCandidateLocator(page, candidate).count();
50
+ return count === 1;
51
+ }
52
+ /**
53
+ * Verifies one locator candidate against the current live page: uniqueness now, then survival
54
+ * across a reload and across each configured viewport size (development plan section 6.3.5).
55
+ * Uniqueness is a hard gate — a candidate that does not resolve to exactly one element right now
56
+ * cannot be called stable, so reload/viewport are never checked against a candidate that has
57
+ * already failed. The page's original viewport size is restored before returning, so scoring one
58
+ * element's candidate does not affect the next.
59
+ */
60
+ export async function scoreLocatorStability(page, candidate, options = {}) {
61
+ if (!(await isUniqueMatch(page, candidate))) {
62
+ return 0;
63
+ }
64
+ const viewports = options.viewports ?? DEFAULT_VIEWPORTS;
65
+ const originalViewport = page.viewportSize();
66
+ let survivedChecks = 0;
67
+ await page.reload();
68
+ if (await isUniqueMatch(page, candidate)) {
69
+ survivedChecks += 1;
70
+ }
71
+ for (const viewport of viewports) {
72
+ await page.setViewportSize(viewport);
73
+ if (await isUniqueMatch(page, candidate)) {
74
+ survivedChecks += 1;
75
+ }
76
+ }
77
+ if (originalViewport !== null) {
78
+ await page.setViewportSize(originalViewport);
79
+ }
80
+ return survivedChecks / (1 + viewports.length);
81
+ }
82
+ //# sourceMappingURL=stability-scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stability-scoring.js","sourceRoot":"","sources":["../src/stability-scoring.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAGtC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAQ3C,MAAM,CAAC,MAAM,iBAAiB,GAA4B;IACxD,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IAC5B,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAC5B,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;CAC5B,CAAC;AAOF,SAAS,uBAAuB,CAAC,KAAa;IAC5C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,OAAO,CACf,2BAA2B,EAC3B,oDAAoD,KAAK,IAAI,EAC7D,EAAE,KAAK,EAAE,CACV,CAAC;IACJ,CAAC;IACD,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,OAAQ,MAAkC,CAAC,IAAI,KAAK,QAAQ,EAC5D,CAAC;QACD,MAAM,IAAI,OAAO,CACf,2BAA2B,EAC3B,uDAAuD,KAAK,IAAI,CACjE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,OAAO,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;QACpC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAc,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE;QACpD,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAc,EAAE,CAAC;AACtC,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,IAAc,EAAE,SAA2B;IACjF,QAAQ,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC3B,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,uBAAuB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAChE,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC3C,KAAK,OAAO;YACV,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC1C,KAAK,aAAa;YAChB,OAAO,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAChD,KAAK,MAAM;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACzC,KAAK,KAAK;YACR,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC;YACE,MAAM,IAAI,OAAO,CAAC,0BAA0B,EAAE,6BAA6B,SAAS,CAAC,QAAQ,IAAI,CAAC,CAAC;IACvG,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAc,EAAE,SAA2B;IACtE,MAAM,KAAK,GAAG,MAAM,uBAAuB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,EAAE,CAAC;IACrE,OAAO,KAAK,KAAK,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,IAAc,EACd,SAA2B,EAC3B,UAAmC,EAAE;IAErC,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAC;IACzD,MAAM,gBAAgB,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;IAC7C,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;IACpB,IAAI,MAAM,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;QACzC,cAAc,IAAI,CAAC,CAAC;IACtB,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,MAAM,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;YACzC,cAAc,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,cAAc,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;AACjD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qa-ai-stlc/explorer",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The crawler: route discovery in safe mode, redacted request logging, and the route map every later explorer phase builds on.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -28,8 +28,8 @@
28
28
  "clean": "node ../../scripts/clean-dir.mjs dist"
29
29
  },
30
30
  "dependencies": {
31
- "@qa-ai-stlc/core": "0.2.1",
32
- "@qa-ai-stlc/schemas": "0.3.0"
31
+ "@qa-ai-stlc/core": "0.3.0",
32
+ "@qa-ai-stlc/schemas": "0.4.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@qa-ai-stlc/test-utils": "0.0.0"