@unotest/grounder-client 0.10.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/CHANGELOG.md ADDED
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@unotest/grounder-client` are documented in this file.
4
+
5
+ ## [0.10.0] - 2026-08-06
6
+
7
+ ### Minor Changes
8
+
9
+ - 7324966: `createDomHelpers()` grows shared accname/state building blocks, so injected algorithms with divergent naming policies stop hand-copying the identical parts: `makeAccname(hostLanguageName)` (fixed prologue: cycle guard → aria-labelledby → aria-label → form-control labels; the per-tag chain is supplied by the caller), plus `nameFromLabelledby`, `svgAccname`, `formControlName`, `formFieldRole`, `ariaStateFlags`, `headingLevel` and the `AriaStateFlags` type. `accname`/`collectState`/`implicitRole` behavior is unchanged — they are now built on the same blocks.
10
+ - b5b9710: Assert steps ground as whole units. `Intent.prefer` gains `"unit"`: the
11
+ grounder returns the semantic unit itself (heading / record / text) and
12
+ suppresses the record→control hop — the profile for read-only targets.
13
+ Web assert actions (`assert_text/visible/hidden/value/count`) declare
14
+ `intentTarget: "unit"` on their plugins, and the intent resolver
15
+ materializes a DOM ref for unit resolutions via the new
16
+ `materializeRefScript(nodeId)` (grounder-client dom-walker): one lazy
17
+ attribute write on the unit's own element, reused by later captures.
18
+ Previously an assert intent like "the page heading" was rejected with a
19
+ RECORD error and forced a `find_element` detour. Control-target errors
20
+ now name the unit kind correctly (heading / record / text block) and
21
+ point at assert steps for check-not-click intents.
22
+
23
+ Materialized record containers now resolve to a stable locator. A record
24
+ row / card / tree-item (`<li>`, `<tr>`, `<article>`) carries no accessible
25
+ name of its own — its text lives in children — so `RefResolver` gains a
26
+ container strategy: `getByRole(role).filter({hasText})`, keyed off the
27
+ container's short descendant text (capped so a whole table is never
28
+ captured) and verified for uniqueness + ref identity. Fixes `assert_visible`
29
+ on a grounded tree item failing with `no stable identifier`.
30
+
31
+ - 574841f: Dedup pass across the tree (jscpd 0 clones, ratchet threshold 0). New public export in `@unotest/grounder-client`: `intEnvInRange(env, name, def, min, max)` — the `UNOTEST_GROUNDER_*` integer-env parser shared by the consumer side and the grounder server. Everything else is internal refactoring with no behavior change: protocol gains a shared fs-safe-name validator behind `validateCollectionName` / `validateSegmentName` and a shared env-file assignment iterator; core's `RuntimeInspection` becomes a `Pick` of `RuntimeStateFile`; dsl's `ExecutionWalker` threads one `WalkCtx` object instead of an eight-argument clump and AST nodes use constructor parameter properties; web deduplicates the MCP tool scaffolds, action plugins, DSL contracts, perception LRU caches, and semantic-dom grouping passes.
32
+ - 65eb77c: Shared injected DOM helpers: one `createDomHelpers()` factory (visibility, ARIA roles, W3C accname, state flags, ref minting) now backs the grounder-client DOM walker and the web snapshot/find algorithms, composed into the page by `composeInjectedScript` — replaces four hand-maintained copies of the same in-page logic.
33
+
34
+ **Breaking (`@unotest/grounder-client`):** `captureRawTree` takes the helpers object as its first parameter, so passing it straight to `page.evaluate(captureRawTree)` no longer works (evaluate arguments are JSON-serialized and cannot carry the helpers). Migration: inject the serialized script instead — `page.evaluate(domWalkerScript())` — which composes the helpers automatically. New exports: `createDomHelpers`, `composeInjectedScript`, `InjectedDomHelpers`, `DomHelperOptions`, `InjectedScriptOptions`.
35
+
36
+ `@unotest/web` behavior deltas from unifying on the walker's ARIA mapping: `get_aria_snapshot` now maps `dl` to `list` (`dt`/`dd` stay name-only) and `tbody`/`thead`/`tfoot` to `rowgroup`, and never names structural containers from their text content; `find_element` matches against the full W3C accname (label-for lookup, composed multi-child names) instead of a reduced approximation.
37
+
38
+ - f106cf8: The DOM walker records `fontWeight` on captured elements when it deviates
39
+ from normal (400), looking a few levels down so emphasis wrapped inside
40
+ the element counts (`<a><b>Technic</b></a>` reports 700).
41
+
42
+ Weight is a LEVEL cue: in a flat list the bold entry heads the plain ones
43
+ below it. Unlike indentation it survives multi-column layouts, where a
44
+ subtree continues in the next column at the same offset — the case that
45
+ made five same-named links in a 1200-link catalog tree indistinguishable.
46
+ Consumers reading `RawCapture` see one optional numeric field; nothing
47
+ else changes.
48
+
49
+ - 856f087: `ResolveDiagnostics` gains `nameEvidence` — the page strings an intent
50
+ quoted verbatim plus whether the resolved unit owned one of them. The
51
+ grounder now answers NONE when an intent quotes page text that the
52
+ picked unit does not own (its own name/value or its record's identity
53
+ and fields); a collection label or region name counts as context, not
54
+ ownership. Consumers that surface grounding diagnostics can report the
55
+ new field; the shape is additive and optional.
56
+ - 78f850f: `ResolveDiagnostics` gains `ordinalSubset` — set when a typed ordinal was
57
+ resolved deterministically inside a value-filtered subset ("the last
58
+ Pending row" over the 48 Pending rows of a 295-row table) instead of by
59
+ the picker. Carries the matched value, the subset size and the 0-based
60
+ index taken; absent when the picker decided the position.
61
+
62
+ ### Patch Changes
63
+
64
+ - 1bd55ba: `ResolveDiagnostics.recordHop` gains an optional `prompt` — the exact
65
+ user message the record→control hop picker saw. Debug surface: the eval
66
+ console prints it verbatim; no behavior change.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Volkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @unotest/grounder-client
2
+
3
+ Consumer-side contract of the unotest **semantic UI grounder** — the service
4
+ that resolves a typed intent ("the edit button in the R-00042 row", "the last
5
+ pending row") to element ref(s) on a captured page.
6
+
7
+ This package is everything a consumer needs to capture a page and talk to the
8
+ grounding service:
9
+
10
+ - **Raw capture contract** — `RawCapture` and friends: the dumb visible-DOM
11
+ tree the grounder consumes. Map any accessibility tree to it, or use the
12
+ bundled walker.
13
+ - **DOM walker** (`@unotest/grounder-client/dom-walker`) — a self-contained
14
+ in-page capture function plus evaluate-ready scripts (ref highlight,
15
+ node-registry lookups).
16
+ - **Content hash** — `hashRawCapture`, the "is the page still the same
17
+ state?" cache key. Semantics-only: bounds/scroll don't flip it.
18
+ - **Typed intent → resolution** — `Intent` (text + typed `ordinal` /
19
+ `predicate`), `Resolution` (`ref | set | none` + diagnostics).
20
+ - **Wire protocol + HTTP client** — `GrounderHttpClient` over
21
+ `/index`, `/resolve`, `/session/close`; per-session index isolation.
22
+
23
+ The grounding engine itself (graph perception, embedding recall, picker
24
+ models) runs as a service and is not part of this package.
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { GrounderHttpClient, hashRawCapture, type RawCapture } from "@unotest/grounder-client";
30
+ import { domWalkerScript } from "@unotest/grounder-client/dom-walker";
31
+
32
+ const raw = (await page.evaluate(domWalkerScript())) as RawCapture;
33
+ const hash = hashRawCapture(raw);
34
+
35
+ const client = new GrounderHttpClient({ baseUrl, token });
36
+ await client.index(raw, hash);
37
+ const { resolution } = await client.resolve({ text: "the login field" }, hash);
38
+ ```
@@ -0,0 +1,115 @@
1
+ import { R as RawState, a as RawCapture } from '../types-CMkcL3q9.js';
2
+
3
+ interface DomHelperOptions {
4
+ /** Accname length cap (default 80). */
5
+ maxName?: number;
6
+ /** Where `aria-hidden="true"` hides an element: "ancestors" walks the
7
+ * composed tree up (snapshot captures), "self" checks only the element
8
+ * itself — the walker's recursive visibility gate already prunes hidden
9
+ * subtrees, and the per-element ancestor climb would be O(depth²) on
10
+ * its hot path. Default "ancestors". */
11
+ ariaHidden?: "ancestors" | "self";
12
+ }
13
+ /** Subset of accessibility state shared by the walker's `RawState` and
14
+ * outline capture in @unotest/web (`disabled` and `level` handling
15
+ * deliberately differ per side and stay out). */
16
+ interface AriaStateFlags {
17
+ checked?: true | "mixed";
18
+ pressed?: true | "mixed";
19
+ expanded?: true;
20
+ selected?: true;
21
+ active?: true;
22
+ }
23
+ declare function createDomHelpers(opts?: DomHelperOptions): {
24
+ parentOrHost: (node: Node) => Element | null;
25
+ rootForScope: (el: Node) => Document | ShadowRoot;
26
+ getElementByIdInScope: (el: Element, id: string) => Element | null;
27
+ deepWalk: (root: Document | Element | ShadowRoot) => Generator<Element>;
28
+ isVisible: (el: Element) => boolean;
29
+ trim: (s: string | null | undefined) => string;
30
+ cap: (s: string) => string;
31
+ cssEscape: (s: string) => string;
32
+ textContentVisible: (el: Element) => string;
33
+ interactiveRoles: Set<string>;
34
+ formFieldRole: (el: Element, tag: string) => string | null;
35
+ implicitRole: (el: Element) => string | null;
36
+ computeRole: (el: Element) => string | null;
37
+ nameFromLabelledby: (el: Element, seen: Set<Element>, resolve: (ref: Element, seen: Set<Element>) => string) => string;
38
+ svgAccname: (svg: Element, seen: Set<Element>, resolve: (ref: Element, seen: Set<Element>) => string) => string;
39
+ formControlName: (el: Element) => string;
40
+ makeAccname: (hostLanguageName: (el: Element, tag: string, seen: Set<Element>, self: (el: Element, visited?: Set<Element>) => string) => string) => (el: Element, visited?: Set<Element>) => string;
41
+ accname: (el: Element, visited?: Set<Element>) => string;
42
+ ariaStateFlags: (el: Element) => AriaStateFlags;
43
+ headingLevel: (el: Element) => number | undefined;
44
+ collectState: (el: Element, role: string) => RawState | undefined;
45
+ pageOrigin: string;
46
+ normaliseHref: (raw: string | null) => string | undefined;
47
+ isInteractive: (el: Element, role: string | null) => boolean;
48
+ idStable: (id: string) => boolean;
49
+ stableDataAttr: (el: Element) => {
50
+ name: string;
51
+ value: string;
52
+ } | undefined;
53
+ clickAffordance: (el: Element) => boolean;
54
+ testIdOf: (el: Element) => string | undefined;
55
+ fontWeight: (el: Element) => number;
56
+ refRegex: RegExp;
57
+ ensureRef: (el: Element, kind: "i" | "n") => string;
58
+ persistRefCounter: () => void;
59
+ };
60
+ type InjectedDomHelpers = ReturnType<typeof createDomHelpers>;
61
+ interface InjectedScriptOptions {
62
+ helperOpts?: DomHelperOptions;
63
+ /** JS expression literals appended as algorithm arguments after the
64
+ * helpers object (already-serialized, e.g. `JSON.stringify(args)`). */
65
+ argLiterals?: string[];
66
+ }
67
+ /** Evaluate-ready expression running `algorithm(h, ...args)` in the page.
68
+ * The `__name` shim neutralizes esbuild/tsx keepNames helper calls that
69
+ * may be injected into the serialized function bodies. */
70
+ declare function composeInjectedScript(algorithm: (h: InjectedDomHelpers, ...rest: never[]) => unknown, opts?: InjectedScriptOptions): string;
71
+
72
+ declare function captureRawTree(h: InjectedDomHelpers): RawCapture;
73
+
74
+ /** DOM attribute carrying an element's `eN` ref (interactive elements). */
75
+ declare const REF_ATTRIBUTE = "data-unotest-ref";
76
+ /** <html> attribute persisting the ref counter across captures. */
77
+ declare const REF_COUNTER_ATTRIBUTE = "data-unotest-ref-counter";
78
+ /** window global holding the live-node registry of the LAST capture:
79
+ * `window.__unotestGrounderNodes[nodeId] === Element`. This is the
80
+ * highlight handle for graph units — including `rN` record roots, which
81
+ * have no DOM ref attribute. Replaced wholesale on every capture.
82
+ * Must match the literal used inside walker.ts (the function is
83
+ * serialized and cannot reference module scope). */
84
+ declare const NODE_REGISTRY_GLOBAL = "__unotestGrounderNodes";
85
+ /** Serialized walker as an evaluate-ready expression. `ariaHidden: "self"`
86
+ * keeps the walker's original visibility semantics: its recursive walk
87
+ * already prunes hidden subtrees, so the per-element ancestor climb is
88
+ * redundant on this hot path. */
89
+ declare function domWalkerScript(): string;
90
+ /** Evaluate-ready expression resolving a nodeId to its live element —
91
+ * building block for consumer-side highlight overlays. Returns null when
92
+ * the registry is stale (page re-rendered) or the id is unknown. */
93
+ declare function nodeByIdScript(nodeId: number): string;
94
+ /** Evaluate-ready script materializing a `data-unotest-ref` (eN) on the
95
+ * element behind `nodeId` — the DOM handle for graph units that never get
96
+ * a ref at capture time (headings, record roots, text blocks; the walker
97
+ * only stamps interactive elements). Lazy by design: one attribute write
98
+ * per resolve, instead of stamping thousands of non-interactive elements
99
+ * on every capture. Mirrors the walker's `ensureRef` conventions exactly
100
+ * (same attribute, same <html> counter, owner kind `n` = named
101
+ * non-interactive), so the next capture reuses the ref instead of
102
+ * re-minting. Returns the eN, or null when the registry is stale (page
103
+ * re-rendered since the capture). */
104
+ declare function materializeRefScript(nodeId: number): string;
105
+ /** Evaluate-ready script drawing outline overlays over the elements behind
106
+ * `nodeIds` (live-node registry of the LAST capture). Overlays are
107
+ * position:fixed and TRACK their elements — recomputed on every scroll
108
+ * (capture phase, so inner scroll containers count) and resize; the first
109
+ * target is scrolled into view BEFORE the boxes are placed. Re-running
110
+ * replaces the previous highlight. Returns the number of elements found. */
111
+ declare function highlightByNodeIdScript(nodeIds: number[], color?: string): string;
112
+ /** Evaluate-ready script removing the highlight overlay, if any. */
113
+ declare function clearHighlightScript(): string;
114
+
115
+ export { type DomHelperOptions, type InjectedDomHelpers, type InjectedScriptOptions, NODE_REGISTRY_GLOBAL, REF_ATTRIBUTE, REF_COUNTER_ATTRIBUTE, RawCapture, captureRawTree, clearHighlightScript, composeInjectedScript, createDomHelpers, domWalkerScript, highlightByNodeIdScript, materializeRefScript, nodeByIdScript };