@reticlehq/browser 2.13.1 → 3.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 (244) hide show
  1. package/dist/actions/actions-dom.d.ts +8 -20
  2. package/dist/actions/actions-dom.js +70 -69
  3. package/dist/actions/actions-press.d.ts +14 -0
  4. package/dist/actions/actions-press.js +123 -0
  5. package/dist/actions/actions.d.ts +1 -89
  6. package/dist/actions/actions.js +64 -365
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +6 -36
  9. package/dist/actions/danger-context.d.ts +3 -0
  10. package/dist/actions/danger-context.js +39 -0
  11. package/dist/actions/scroll.d.ts +8 -10
  12. package/dist/actions/scroll.js +28 -17
  13. package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
  14. package/dist/actions/synthetic/synthetic-input.js +13 -0
  15. package/dist/actions/value-input.d.ts +0 -30
  16. package/dist/actions/value-input.js +0 -33
  17. package/dist/commands/commands.d.ts +0 -2
  18. package/dist/commands/commands.js +10 -70
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +3 -90
  22. package/dist/dom/addressing/auto-anchor.d.ts +25 -0
  23. package/dist/dom/addressing/auto-anchor.js +41 -0
  24. package/dist/dom/addressing/refs.d.ts +14 -0
  25. package/dist/dom/addressing/refs.js +114 -0
  26. package/dist/dom/addressing/source.d.ts +9 -0
  27. package/dist/dom/addressing/source.js +29 -0
  28. package/dist/dom/chart.d.ts +0 -61
  29. package/dist/dom/chart.js +0 -60
  30. package/dist/dom/desktop-capture.d.ts +0 -30
  31. package/dist/dom/desktop-capture.js +1 -73
  32. package/dist/dom/dom-ignore.d.ts +0 -30
  33. package/dist/dom/dom-ignore.js +1 -48
  34. package/dist/dom/occlusion.d.ts +0 -7
  35. package/dist/dom/occlusion.js +31 -12
  36. package/dist/dom/paint-context.d.ts +6 -0
  37. package/dist/dom/paint-context.js +38 -0
  38. package/dist/dom/query.d.ts +0 -14
  39. package/dist/dom/query.js +50 -216
  40. package/dist/dom/realm.d.ts +0 -20
  41. package/dist/dom/realm.js +0 -28
  42. package/dist/dom/shadow-registry.d.ts +0 -33
  43. package/dist/dom/shadow-registry.js +2 -72
  44. package/dist/dom/snapshot.d.ts +2 -43
  45. package/dist/dom/snapshot.js +27 -152
  46. package/dist/dom/theme.d.ts +0 -37
  47. package/dist/dom/theme.js +2 -52
  48. package/dist/dom/virtualized.d.ts +0 -17
  49. package/dist/dom/virtualized.js +0 -27
  50. package/dist/edit-epoch.d.ts +1 -23
  51. package/dist/edit-epoch.js +1 -53
  52. package/dist/index.d.ts +4 -5
  53. package/dist/index.js +4 -28
  54. package/dist/observers/animation.d.ts +0 -1
  55. package/dist/observers/animation.js +1 -4
  56. package/dist/observers/blind-spots.d.ts +0 -26
  57. package/dist/observers/blind-spots.js +3 -62
  58. package/dist/observers/console.d.ts +1 -2
  59. package/dist/observers/console.js +41 -30
  60. package/dist/observers/context-open.d.ts +1 -11
  61. package/dist/observers/context-open.js +10 -14
  62. package/dist/observers/departure.d.ts +3 -0
  63. package/dist/observers/departure.js +52 -0
  64. package/dist/observers/dialogs.d.ts +2 -0
  65. package/dist/observers/dialogs.js +39 -0
  66. package/dist/observers/dom.js +28 -52
  67. package/dist/observers/download.d.ts +1 -2
  68. package/dist/observers/download.js +39 -56
  69. package/dist/observers/focus.d.ts +0 -6
  70. package/dist/observers/focus.js +0 -7
  71. package/dist/observers/frames.d.ts +0 -9
  72. package/dist/observers/frames.js +1 -28
  73. package/dist/observers/health.d.ts +0 -5
  74. package/dist/observers/health.js +2 -34
  75. package/dist/observers/install-all.d.ts +2 -0
  76. package/dist/observers/install-all.js +26 -27
  77. package/dist/observers/ipc.d.ts +0 -25
  78. package/dist/observers/ipc.js +0 -65
  79. package/dist/observers/navigation.d.ts +2 -0
  80. package/dist/observers/navigation.js +33 -0
  81. package/dist/observers/net-detail/network-body.d.ts +12 -0
  82. package/dist/observers/net-detail/network-body.js +71 -0
  83. package/dist/observers/net-detail/network-redact.d.ts +1 -0
  84. package/dist/observers/net-detail/network-redact.js +1 -0
  85. package/dist/observers/network-stream.d.ts +0 -21
  86. package/dist/observers/network-stream.js +3 -46
  87. package/dist/observers/network.d.ts +3 -27
  88. package/dist/observers/network.js +178 -295
  89. package/dist/observers/perf.d.ts +0 -10
  90. package/dist/observers/perf.js +0 -16
  91. package/dist/observers/route.d.ts +1 -2
  92. package/dist/observers/route.js +13 -19
  93. package/dist/observers/scroll.d.ts +0 -1
  94. package/dist/observers/scroll.js +2 -11
  95. package/dist/observers/sdk-failure.d.ts +1 -9
  96. package/dist/observers/sdk-failure.js +1 -30
  97. package/dist/observers/state.d.ts +1 -11
  98. package/dist/observers/state.js +13 -41
  99. package/dist/observers/storage.d.ts +0 -14
  100. package/dist/observers/storage.js +10 -34
  101. package/dist/observers/types.d.ts +0 -15
  102. package/dist/observers/types.js +0 -14
  103. package/dist/patching/capture-method.d.ts +0 -24
  104. package/dist/patching/capture-method.js +0 -26
  105. package/dist/presenter/chrome/layers.d.ts +2 -0
  106. package/dist/presenter/chrome/layers.js +2 -0
  107. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  108. package/dist/presenter/chrome/log-kinds.js +7 -0
  109. package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
  110. package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
  111. package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
  112. package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
  113. package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
  114. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
  115. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
  116. package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
  117. package/dist/presenter/chrome/presenter-log.d.ts +17 -0
  118. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
  119. package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
  120. package/dist/presenter/chrome/presenter-report-copy.js +95 -0
  121. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  122. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  123. package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
  124. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
  125. package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
  126. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
  127. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
  128. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
  129. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
  130. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  131. package/dist/presenter/presenter-account-styles.js +99 -0
  132. package/dist/presenter/presenter-account.d.ts +40 -0
  133. package/dist/presenter/presenter-account.js +178 -0
  134. package/dist/presenter/presenter-config.d.ts +10 -107
  135. package/dist/presenter/presenter-config.js +5 -92
  136. package/dist/presenter/presenter-controls.d.ts +1 -35
  137. package/dist/presenter/presenter-controls.js +10 -133
  138. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  139. package/dist/presenter/presenter-dock-layout.js +1 -5
  140. package/dist/presenter/presenter-drag.d.ts +2 -42
  141. package/dist/presenter/presenter-drag.js +3 -76
  142. package/dist/presenter/presenter-glow.d.ts +0 -21
  143. package/dist/presenter/presenter-glow.js +2 -28
  144. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  145. package/dist/presenter/presenter-hud-position.js +50 -0
  146. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  147. package/dist/presenter/presenter-report-styles.js +22 -7
  148. package/dist/presenter/presenter-report.d.ts +5 -7
  149. package/dist/presenter/presenter-report.js +44 -95
  150. package/dist/presenter/presenter-run-state.d.ts +1 -8
  151. package/dist/presenter/presenter-run-state.js +1 -6
  152. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  153. package/dist/presenter/presenter-settings-styles.js +2 -2
  154. package/dist/presenter/presenter-settings.d.ts +5 -23
  155. package/dist/presenter/presenter-settings.js +21 -33
  156. package/dist/presenter/presenter-shell-styles.js +23 -20
  157. package/dist/presenter/presenter-shell.d.ts +6 -18
  158. package/dist/presenter/presenter-shell.js +26 -77
  159. package/dist/presenter/presenter-styles.d.ts +2 -10
  160. package/dist/presenter/presenter-styles.js +6 -12
  161. package/dist/presenter/presenter-workspace.d.ts +4 -4
  162. package/dist/presenter/presenter-workspace.js +30 -10
  163. package/dist/presenter/presenter.d.ts +3 -63
  164. package/dist/presenter/presenter.js +29 -132
  165. package/dist/presenter/tour/tour-view.d.ts +33 -0
  166. package/dist/presenter/tour/tour-view.js +188 -0
  167. package/dist/presenter/tour/tour.d.ts +25 -0
  168. package/dist/presenter/tour/tour.js +245 -0
  169. package/dist/recorder/recorder-styles.d.ts +0 -6
  170. package/dist/recorder/recorder-styles.js +0 -6
  171. package/dist/recorder/recorder.d.ts +0 -34
  172. package/dist/recorder/recorder.js +6 -49
  173. package/dist/registry/auto-testids.d.ts +0 -19
  174. package/dist/registry/auto-testids.js +1 -21
  175. package/dist/registry/capabilities.d.ts +0 -31
  176. package/dist/registry/capabilities.js +1 -46
  177. package/dist/registry/commit-and-signal.d.ts +0 -6
  178. package/dist/registry/commit-and-signal.js +1 -7
  179. package/dist/registry/declared-channels.d.ts +2 -0
  180. package/dist/registry/declared-channels.js +14 -0
  181. package/dist/registry/declared-platform.d.ts +4 -0
  182. package/dist/registry/declared-platform.js +34 -0
  183. package/dist/registry/domains.d.ts +0 -8
  184. package/dist/registry/domains.js +0 -7
  185. package/dist/registry/emitter.d.ts +0 -14
  186. package/dist/registry/emitter.js +1 -11
  187. package/dist/registry/store-adapters.d.ts +0 -186
  188. package/dist/registry/store-adapters.js +1 -164
  189. package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
  190. package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
  191. package/dist/registry/stores.d.ts +0 -33
  192. package/dist/registry/stores.js +1 -60
  193. package/dist/reticle-presenter-helpers.d.ts +0 -13
  194. package/dist/reticle-presenter-helpers.js +10 -20
  195. package/dist/reticle-singleton.d.ts +2 -0
  196. package/dist/reticle-singleton.js +3 -0
  197. package/dist/reticle.d.ts +1 -52
  198. package/dist/reticle.js +115 -180
  199. package/dist/review/annotator-styles.js +1 -8
  200. package/dist/review/annotator.d.ts +2 -13
  201. package/dist/review/annotator.js +2 -42
  202. package/dist/review/mark-anchor.d.ts +0 -11
  203. package/dist/review/mark-anchor.js +3 -11
  204. package/dist/security/serialization.d.ts +0 -20
  205. package/dist/security/serialization.js +1 -66
  206. package/dist/session-continuity.d.ts +0 -16
  207. package/dist/session-continuity.js +1 -20
  208. package/dist/timers/clock.d.ts +1 -14
  209. package/dist/timers/clock.js +59 -60
  210. package/dist/timers/console/native-console.js +5 -0
  211. package/dist/timers/native/native-timers.d.ts +11 -0
  212. package/dist/timers/native/native-timers.js +48 -0
  213. package/dist/transport/transport.d.ts +0 -51
  214. package/dist/transport/transport.js +5 -131
  215. package/dist/transport/unreachable-message.d.ts +0 -17
  216. package/dist/transport/unreachable-message.js +0 -17
  217. package/dist/util/captured-method.d.ts +0 -35
  218. package/dist/util/captured-method.js +0 -35
  219. package/package.json +5 -4
  220. package/dist/actions/synthetic-input.d.ts +0 -9
  221. package/dist/actions/synthetic-input.js +0 -40
  222. package/dist/dom/auto-anchor.d.ts +0 -65
  223. package/dist/dom/auto-anchor.js +0 -74
  224. package/dist/dom/refs.d.ts +0 -90
  225. package/dist/dom/refs.js +0 -245
  226. package/dist/dom/source.d.ts +0 -53
  227. package/dist/dom/source.js +0 -74
  228. package/dist/observers/network-body.d.ts +0 -35
  229. package/dist/observers/network-body.js +0 -168
  230. package/dist/observers/network-redact.d.ts +0 -20
  231. package/dist/observers/network-redact.js +0 -80
  232. package/dist/presenter/presenter-brand.d.ts +0 -22
  233. package/dist/presenter/presenter-effects.d.ts +0 -13
  234. package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
  235. package/dist/presenter/presenter-log.d.ts +0 -64
  236. package/dist/presenter/presenter-report-copy.d.ts +0 -112
  237. package/dist/presenter/presenter-report-copy.js +0 -156
  238. package/dist/presenter/presenter-test-helpers.d.ts +0 -16
  239. package/dist/presenter/presenter-test-helpers.js +0 -41
  240. package/dist/timers/native-console.js +0 -9
  241. package/dist/timers/native-timers.d.ts +0 -36
  242. package/dist/timers/native-timers.js +0 -78
  243. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  244. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
@@ -1,120 +1,26 @@
1
1
  import type { RedactionConfig } from '@reticlehq/core';
2
- /**
3
- * What `reticle.connect()` accepts.
4
- *
5
- * Split out of reticle.ts when that file crossed the 600-line cap - the same move `session-info.ts`
6
- * made for the server's Session. A pure description of the SDK's public entry contract has no reason
7
- * to live inside the class that happens to consume it, and separating them keeps the surface every
8
- * integrating app actually reads reviewable on its own.
9
- */
10
2
  export interface ReticleConnectOptions {
11
- /** WS endpoint of the local bridge. Defaults to ws://localhost:<port><path>. */
12
3
  url?: string;
13
- /** Human-friendly session label so the agent can target the right tab. */
14
4
  session?: string;
15
- /**
16
- * Stable project identity, normally stamped by the build plugin (e.g. "acme-web-9f3c1d"). Lets the
17
- * agent scope to the right app even when its dev server boots on an unexpected port. Optional.
18
- */
19
5
  projectId?: string;
20
- /**
21
- * Browser/bridge pairing token. Required whenever the daemon has one, which is the ordinary case:
22
- * it auto-provisions `~/.reticle/pairing-token` on first start and then refuses a hello without it,
23
- * including on localhost. Non-localhost additionally needs `allowNonLocalhost`.
24
- */
25
6
  token?: string;
26
- /** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
27
7
  allowNonLocalhost?: boolean;
28
- /**
29
- * Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
30
- * build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
31
- * otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
32
- * tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
33
- */
34
8
  allowInProduction?: boolean;
35
- /** Show a small in-page status chip (connection + event count). */
36
9
  overlay?: boolean;
37
- /**
38
- * Capture request/response bodies on net.request events (dev-only; text-like content only,
39
- * sensitive keys redacted, per-body capped). Off by default - bodies cost tokens and can carry PII.
40
- */
41
10
  captureNetworkBodies?: boolean;
42
- /**
43
- * Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
44
- *
45
- * The presenter is hidden from every tool by design, for a good reason: an agent that can drive
46
- * Reticle's own interface can fabricate its own impact report, and Reticle chrome in a snapshot is
47
- * noise in every other app on earth.
48
- *
49
- * The cost of that rule is that a HUD change is the only kind of change Reticle cannot be used to
50
- * check — the panel rendering it is invisible to everything that could look at it. This hatch
51
- * exists for exactly that case: the app under test IS Reticle. It reports itself in the app's
52
- * capabilities, so a verdict drawn with it open can never be mistaken for an ordinary one.
53
- */
11
+ sourceMapping?: boolean;
12
+ captureErrorBodies?: boolean;
54
13
  exposePresenter?: boolean;
55
- /**
56
- * The project root, so source paths report repo-relative instead of absolute.
57
- *
58
- * React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
59
- * can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
60
- * Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
61
- * the generated app-side code plain JavaScript - the previous shape made the caller assign a global,
62
- * and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
63
- */
64
14
  root?: string;
65
- /**
66
- * The SDK's own package version, so a version-skewed pair against the daemon can name itself
67
- * rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
68
- * only for a hand-wired connect that has no plugin.
69
- */
70
15
  sdkVersion?: string;
71
- /** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
72
16
  present?: boolean;
73
- /** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
74
17
  pace?: number;
75
- /** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
76
18
  narrationDwellMs?: number;
77
- /**
78
- * Border behavior in presenter mode: 'session' (default) persists the border for the whole
79
- * session; 'busy' restores the fade-after-idle behavior.
80
- */
81
19
  border?: 'session' | 'busy';
82
- /** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
83
20
  logMax?: number;
84
- /**
85
- * Mount the floating human-recorder toolbar (Record/Stop/Annotate).
86
- * Default off - purely additive, dev-only.
87
- */
88
21
  recorder?: boolean;
89
- /**
90
- * Mount the page annotator: expanding the HUD enters click-to-annotate mode, and Reticle emits
91
- * a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the presenter; pass
92
- * `annotate: false` to suppress.
93
- */
94
22
  annotate?: boolean;
95
- /** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
96
23
  endedFadeMs?: number;
97
- /** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
98
24
  idleEndMs?: number;
99
- /**
100
- * Extend the redaction rules with your app's own vocabulary.
101
- *
102
- * ```ts
103
- * reticle.connect({
104
- * redact: {
105
- * keys: ['deviceSecretRef', /^partner[-_]?code$/i], // also redact these
106
- * allow: ['designToken'], // stop redacting this false positive
107
- * },
108
- * });
109
- * ```
110
- *
111
- * Additive only - there is no way to replace the built-in rule, because a config that could would
112
- * eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
113
- * a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
114
- *
115
- * Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
116
- * where request bodies are captured raw from the network stack and never pass through this SDK.
117
- * RegExp entries and `allow` do NOT cross - see docs/usage.md ("Extending the redaction rules") for why, and what that means.
118
- */
119
25
  redact?: RedactionConfig;
120
26
  }
@@ -1,38 +1,9 @@
1
1
  import { ElementState, type ElementDescriptor } from '@reticlehq/core';
2
- /** Compute the ARIA role (explicit wins, else implicit from the tag). */
3
2
  export declare function getRole(el: Element): string;
4
3
  export declare function getAccessibleName(el: Element): string;
5
- /**
6
- * The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
7
- * already computed it (describe) pass it in so it isn't resolved twice per element.
8
- */
9
4
  export declare function getStates(el: Element, visible?: boolean): ElementState[];
10
- /**
11
- * True when a form field holds a secret the SDK must never capture verbatim — a password input, a
12
- * sensitive `autocomplete` (cc-number, one-time-code, …), or a name/id/testid/aria-label that trips
13
- * `isSensitiveKey`. The single source of truth for both live-snapshot redaction (`getValue`) and the
14
- * flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
15
- */
16
5
  export declare function isSensitiveField(el: Element): boolean;
17
6
  export declare function getValue(el: Element): string | undefined;
18
- /**
19
- * Whether the element is actually visible (not display:none/hidden/aria-hidden/opacity:0), walking to
20
- * root. This is an O(depth) forced-style walk PER node; `memo` (optional, scoped to ONE synchronous
21
- * query pass) caches the full inherited result per element so a broad state-filtered query stops
22
- * re-resolving getComputedStyle up the same ancestor chain for every sibling. Sound because the DOM is
23
- * static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
24
- * stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
25
- */
26
- /**
27
- * True when the element is inside the viewport right now: visible AND its bounding box intersects
28
- * the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
29
- * container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
30
- * "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
31
- * already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
32
- * the predicate pass.
33
- */
34
7
  export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
35
8
  export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
36
- /** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
37
- * visibility cache with the query's state filter so ancestors aren't re-walked per element. */
38
9
  export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
package/dist/dom/a11y.js CHANGED
@@ -1,22 +1,9 @@
1
1
  import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
2
2
  import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
3
- import { refs } from './refs.js';
3
+ import { refs } from './addressing/refs.js';
4
4
  import { inspectChart } from './chart.js';
5
5
  import { isSensitiveKey } from '../security/serialization.js';
6
- import { formatSource, sourceFromDom } from './source.js';
7
- /**
8
- * Roles whose accessible name comes from their text content (ARIA's `nameFrom: author content`).
9
- *
10
- * `radio`, `checkbox`, `row` and `tooltip` were missing, and the gap is not cosmetic: a segmented
11
- * filter written as `<button role="radio">held</button>` — an extremely ordinary design-system
12
- * control — reported as a nameless `radio`, so `by: role` + name could not address it at all and an
13
- * agent had to fall back to a testid the app has no reason to carry. Measured on a shipments console:
14
- * six filters, six nameless radios, none reachable by name.
15
- *
16
- * `listitem`, `status` and `alert` are NOT in the spec's list. They are kept as a deliberate
17
- * over-approximation — a name computed from content is more useful than no name — and removing them
18
- * has no measured symptom to justify the risk.
19
- */
6
+ import { formatSource, sourceFromDom } from './addressing/source.js';
20
7
  const NAME_FROM_CONTENT = new Set([
21
8
  'button',
22
9
  'link',
@@ -57,16 +44,6 @@ function inputRole(input) {
57
44
  return 'button';
58
45
  return 'textbox';
59
46
  }
60
- /**
61
- * Cheap author-supplied-naming probe: an explicit `aria-label`, any `aria-labelledby`, or a
62
- * `title`. Attribute reads only - no name computation - so `getRole` can consult it without
63
- * recursion into `getAccessibleName`.
64
- *
65
- * This decides `section` -> `region`, which the implicit-role table makes CONDITIONAL: an unnamed
66
- * `<section>` is a plain `generic` container, while one carrying an accessible name is exposed as
67
- * `region`. Getting that backwards either floods every page with phantom regions or hides real
68
- * ones, so both halves are pinned by tests.
69
- */
70
47
  function hasAuthorNaming(el) {
71
48
  const label = el.getAttribute('aria-label');
72
49
  if (label !== null && label.trim().length > 0)
@@ -77,7 +54,6 @@ function hasAuthorNaming(el) {
77
54
  const title = el.getAttribute('title');
78
55
  return title !== null && title.trim().length > 0;
79
56
  }
80
- /** Compute the ARIA role (explicit wins, else implicit from the tag). */
81
57
  export function getRole(el) {
82
58
  const explicit = el.getAttribute('role');
83
59
  if (explicit !== null && explicit.trim().length > 0)
@@ -124,10 +100,6 @@ export function getRole(el) {
124
100
  case 'thead':
125
101
  case 'tfoot':
126
102
  return 'rowgroup';
127
- // A cell's role follows its grid context: plain tables expose `cell`/`columnheader`,
128
- // while inside an explicit `role="grid"`/`role="treegrid"` the same markup is exposed as
129
- // `gridcell` - the pair data-grid queries actually reach for (`{ role: "cell" }` against a
130
- // CSS grid pretending to be a table would otherwise answer zero).
131
103
  case 'td':
132
104
  return el.closest('[role~="grid"], [role~="treegrid"]') !== null ? 'gridcell' : 'cell';
133
105
  case 'th': {
@@ -186,22 +158,6 @@ function labelledByText(el) {
186
158
  const joined = parts.join(' ').trim();
187
159
  return joined.length > 0 ? joined : null;
188
160
  }
189
- /** Accessible name via a practical subset of the accname algorithm. */
190
- /** Accessible name via a practical subset of the accname algorithm. */
191
- /**
192
- * Text content with `aria-hidden` subtrees removed.
193
- *
194
- * The accessible-name spec excludes them, and so does the matcher, because THIS function is what
195
- * `by: role` + name matches through. Reading raw `textContent` here made the name we REPORT differ
196
- * from the name that can be SELECTED: Material UI renders its required-field marker as
197
- * `<span aria-hidden="true"> *</span>`, so a login field was reported as `"Username *"` and
198
- * addressable only as `"Username"`. Reporting a name the agent cannot use defeats the purpose of
199
- * reporting it at all, so both come from the same rule.
200
- *
201
- * An `<img alt>` contributes its alt text the way the spec's subtree step treats embedded
202
- * alternatives: `<button><img alt="Close"></button>` is named "Close", not nameless. Pieces are
203
- * joined with spaces so an icon followed by a word never fuses into one unmatchable token.
204
- */
205
161
  function textWithoutHidden(node) {
206
162
  if (node.nodeType === Node.TEXT_NODE)
207
163
  return node.textContent ?? '';
@@ -244,10 +200,6 @@ export function getAccessibleName(el) {
244
200
  if (text.length > 0)
245
201
  return text;
246
202
  }
247
- // Submit-like inputs carry their name on `value`, exactly where the visible caption comes
248
- // from: `<input type="submit" value="Send">` renders a button reading Send. Without this the
249
- // descriptor printed `button ""` while `by: text` found the very same input by "Send", so the
250
- // two locators disagreed about one element - the disagreement this engine exists to prevent.
251
203
  if (isInput(el)) {
252
204
  const type = el.type.toLowerCase();
253
205
  if ('submit' === type || 'button' === type || 'reset' === type) {
@@ -278,10 +230,6 @@ function ariaBool(el, attr) {
278
230
  return undefined;
279
231
  return 'true' === value;
280
232
  }
281
- /**
282
- * The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
283
- * already computed it (describe) pass it in so it isn't resolved twice per element.
284
- */
285
233
  export function getStates(el, visible = isVisible(el)) {
286
234
  const states = [ElementState.PRESENT];
287
235
  states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
@@ -297,12 +245,6 @@ export function getStates(el, visible = isVisible(el)) {
297
245
  states.push(ElementState.FOCUSED);
298
246
  return states;
299
247
  }
300
- /**
301
- * True when a form field holds a secret the SDK must never capture verbatim — a password input, a
302
- * sensitive `autocomplete` (cc-number, one-time-code, …), or a name/id/testid/aria-label that trips
303
- * `isSensitiveKey`. The single source of truth for both live-snapshot redaction (`getValue`) and the
304
- * flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
305
- */
306
248
  export function isSensitiveField(el) {
307
249
  if (!isInput(el) && !isTextArea(el) && !isSelect(el)) {
308
250
  return false;
@@ -328,7 +270,6 @@ export function getValue(el) {
328
270
  const valueNow = el.getAttribute('aria-valuenow');
329
271
  return valueNow ?? undefined;
330
272
  }
331
- /** Whether the element's OWN box hides it — one forced-style resolution, no ancestor walk. */
332
273
  function selfHidden(el) {
333
274
  if ('true' === el.getAttribute('aria-hidden'))
334
275
  return true;
@@ -347,22 +288,6 @@ function selfHidden(el) {
347
288
  }
348
289
  return false;
349
290
  }
350
- /**
351
- * Whether the element is actually visible (not display:none/hidden/aria-hidden/opacity:0), walking to
352
- * root. This is an O(depth) forced-style walk PER node; `memo` (optional, scoped to ONE synchronous
353
- * query pass) caches the full inherited result per element so a broad state-filtered query stops
354
- * re-resolving getComputedStyle up the same ancestor chain for every sibling. Sound because the DOM is
355
- * static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
356
- * stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
357
- */
358
- /**
359
- * True when the element is inside the viewport right now: visible AND its bounding box intersects
360
- * the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
361
- * container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
362
- * "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
363
- * already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
364
- * the predicate pass.
365
- */
366
291
  export function isInViewport(el, memo) {
367
292
  if (!isVisible(el, memo))
368
293
  return false;
@@ -381,9 +306,6 @@ export function isVisible(el, memo) {
381
306
  if (cached !== undefined)
382
307
  return cached;
383
308
  const parent = el.parentElement;
384
- // Each cached boolean already folds in that node's own aria-hidden/[hidden]/display/visibility/opacity,
385
- // so inherited visibility composes by AND up the chain and a sibling short-circuits at the first
386
- // cached ancestor.
387
309
  const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
388
310
  if (memo !== undefined)
389
311
  memo.set(el, result);
@@ -394,13 +316,11 @@ function getVisibleText(el) {
394
316
  const text = collapse(el.textContent ?? '');
395
317
  return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
396
318
  }
397
- /** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
398
- * visibility cache with the query's state filter so ancestors aren't re-walked per element. */
399
319
  export function describe(el, memo) {
400
320
  const value = getValue(el);
401
321
  const text = getVisibleText(el);
402
322
  const name = getAccessibleName(el);
403
- const visible = isVisible(el, memo); // O(depth) style walk — computed ONCE and reused by getStates
323
+ const visible = isVisible(el, memo);
404
324
  const base = {
405
325
  ref: refs.refFor(el),
406
326
  role: getRole(el),
@@ -412,16 +332,9 @@ export function describe(el, memo) {
412
332
  base.value = value;
413
333
  if (text.length > 0 && text !== name)
414
334
  base.text = text;
415
- // DOM-only lookup on purpose: describe() runs once per matched element, so the adapter's fiber walk
416
- // would turn a broad query into thousands of tree traversals. The stamped attribute answers the
417
- // same question for a fraction of the cost, and single-element paths that can afford the better
418
- // answer (inspect, act, review) use sourceFor() instead.
419
335
  const source = formatSource(sourceFromDom(el));
420
336
  if (source !== undefined)
421
337
  base.source = source;
422
- // Chart faults, only when there are any. Gated on the element actually containing plot geometry so
423
- // the common case — every non-chart element on the page — pays one querySelector miss and nothing
424
- // else, and a HEALTHY chart adds no bytes to the wire either.
425
338
  if (el.querySelector('path, polyline, polygon') !== null) {
426
339
  const faults = inspectChart(el).findings;
427
340
  if (faults.length > 0)
@@ -0,0 +1,25 @@
1
+ export declare const AnchorStrategy: {
2
+ readonly TESTID: "testid";
3
+ readonly COMPONENT: "component";
4
+ readonly ROLE: "role";
5
+ readonly POSITION: "position";
6
+ };
7
+ export type AnchorStrategy = (typeof AnchorStrategy)[keyof typeof AnchorStrategy];
8
+ export interface AnchorInput {
9
+ testid?: string;
10
+ component?: string;
11
+ source?: {
12
+ file: string;
13
+ line: number;
14
+ };
15
+ role?: string;
16
+ name?: string;
17
+ nth?: number;
18
+ }
19
+ interface SynthesizedAnchor {
20
+ strategy: AnchorStrategy;
21
+ value: string;
22
+ stable: boolean;
23
+ }
24
+ export declare function synthesizeAnchor(input: AnchorInput): SynthesizedAnchor;
25
+ export {};
@@ -0,0 +1,41 @@
1
+ export const AnchorStrategy = {
2
+ TESTID: 'testid',
3
+ COMPONENT: 'component',
4
+ ROLE: 'role',
5
+ POSITION: 'position',
6
+ };
7
+ function nonEmpty(s) {
8
+ return 'string' === typeof s && s.length > 0;
9
+ }
10
+ function sourceTag(source) {
11
+ const base = source.file.split('/').pop() ?? source.file;
12
+ return `${base}:${source.line}`;
13
+ }
14
+ export function synthesizeAnchor(input) {
15
+ if (nonEmpty(input.testid)) {
16
+ return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
17
+ }
18
+ if (nonEmpty(input.component) && input.source !== undefined) {
19
+ return {
20
+ strategy: AnchorStrategy.COMPONENT,
21
+ value: `${input.component}@${sourceTag(input.source)}`,
22
+ stable: true,
23
+ };
24
+ }
25
+ if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
26
+ const qualifier = nonEmpty(input.name) ? input.name : input.role;
27
+ return {
28
+ strategy: AnchorStrategy.COMPONENT,
29
+ value: `${input.component}[${qualifier ?? ''}]`,
30
+ stable: true,
31
+ };
32
+ }
33
+ if (nonEmpty(input.role) && nonEmpty(input.name)) {
34
+ return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
35
+ }
36
+ if (nonEmpty(input.role)) {
37
+ const suffix = input.nth !== undefined ? `#${input.nth}` : '';
38
+ return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
39
+ }
40
+ return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
41
+ }
@@ -0,0 +1,14 @@
1
+ import { type Ref } from '@reticlehq/core';
2
+ export declare const MAX_TRACKED_REFS = 10000;
3
+ export declare function echoRef(ref: string): string;
4
+ export declare const REF_BLOCK = 100;
5
+ export declare class RefRegistry {
6
+ #private;
7
+ constructor(storage?: () => Storage | undefined);
8
+ get size(): number;
9
+ markEdited(): void;
10
+ mintedBeforeLastEdit(ref: string): boolean;
11
+ refFor(el: Element): Ref;
12
+ resolve(ref: string): Element | null;
13
+ }
14
+ export declare const refs: RefRegistry;
@@ -0,0 +1,114 @@
1
+ import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
2
+ export const MAX_TRACKED_REFS = 10000;
3
+ const REF_ELISION = '…';
4
+ const REF_PREFIX = 'e';
5
+ export function echoRef(ref) {
6
+ const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
7
+ return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
8
+ }
9
+ const SWEEP_EVERY_MINTS = 1000;
10
+ const REF_BASE_KEY = '__reticle_ref_base';
11
+ export const REF_BLOCK = 100;
12
+ function readBase(store) {
13
+ try {
14
+ const raw = store?.getItem(REF_BASE_KEY);
15
+ const value = null === raw || undefined === raw ? Number.NaN : Number(raw);
16
+ return Number.isSafeInteger(value) && value > 0 ? value : 0;
17
+ }
18
+ catch {
19
+ return 0;
20
+ }
21
+ }
22
+ export class RefRegistry {
23
+ #toRef = new WeakMap();
24
+ #fromRef = new Map();
25
+ #storage;
26
+ #seq = 0;
27
+ #reserved = 0;
28
+ #mintsSinceSweep = 0;
29
+ #editedAtSeq = 0;
30
+ constructor(storage = () => globalThis.sessionStorage) {
31
+ this.#storage = storage;
32
+ }
33
+ #reserve() {
34
+ let store;
35
+ try {
36
+ store = this.#storage();
37
+ }
38
+ catch {
39
+ store = undefined;
40
+ }
41
+ const base = Math.max(readBase(store), this.#seq);
42
+ this.#reserved = base + REF_BLOCK;
43
+ try {
44
+ store?.setItem(REF_BASE_KEY, String(this.#reserved));
45
+ }
46
+ catch {
47
+ }
48
+ this.#seq = base;
49
+ }
50
+ get size() {
51
+ return this.#fromRef.size;
52
+ }
53
+ #evictOldest() {
54
+ const oldest = this.#fromRef.keys().next();
55
+ if (oldest.done !== true)
56
+ this.#fromRef.delete(oldest.value);
57
+ }
58
+ #sweep() {
59
+ for (const [ref, weak] of this.#fromRef) {
60
+ const el = weak.deref();
61
+ if (el === undefined || !el.isConnected)
62
+ this.#fromRef.delete(ref);
63
+ }
64
+ while (this.#fromRef.size > MAX_TRACKED_REFS)
65
+ this.#evictOldest();
66
+ }
67
+ markEdited() {
68
+ this.#editedAtSeq = this.#seq;
69
+ }
70
+ mintedBeforeLastEdit(ref) {
71
+ if (0 === this.#editedAtSeq || !ref.startsWith(REF_PREFIX))
72
+ return false;
73
+ const n = Number(ref.slice(REF_PREFIX.length));
74
+ return Number.isSafeInteger(n) && n > 0 && n <= this.#editedAtSeq;
75
+ }
76
+ refFor(el) {
77
+ const existing = this.#toRef.get(el);
78
+ if (existing !== undefined) {
79
+ const weak = this.#fromRef.get(existing);
80
+ this.#fromRef.delete(existing);
81
+ this.#fromRef.set(existing, weak ?? new WeakRef(el));
82
+ return existing;
83
+ }
84
+ if (this.#seq >= this.#reserved)
85
+ this.#reserve();
86
+ this.#seq += 1;
87
+ const ref = asRef(`${REF_PREFIX}${String(this.#seq)}`);
88
+ this.#toRef.set(el, ref);
89
+ this.#fromRef.set(ref, new WeakRef(el));
90
+ this.#mintsSinceSweep += 1;
91
+ if (this.#mintsSinceSweep >= SWEEP_EVERY_MINTS) {
92
+ this.#mintsSinceSweep = 0;
93
+ this.#sweep();
94
+ }
95
+ else if (this.#fromRef.size > MAX_TRACKED_REFS) {
96
+ this.#evictOldest();
97
+ }
98
+ return ref;
99
+ }
100
+ resolve(ref) {
101
+ const weak = this.#fromRef.get(ref);
102
+ if (weak === undefined)
103
+ return null;
104
+ const el = weak.deref();
105
+ if (el === undefined || !el.isConnected) {
106
+ this.#fromRef.delete(ref);
107
+ return null;
108
+ }
109
+ this.#fromRef.delete(ref);
110
+ this.#fromRef.set(ref, weak);
111
+ return el;
112
+ }
113
+ }
114
+ export const refs = new RefRegistry();
@@ -0,0 +1,9 @@
1
+ export interface SourceLocation {
2
+ file: string;
3
+ line: number;
4
+ }
5
+ export declare function parseSourceAttr(value: string | null): SourceLocation | undefined;
6
+ export declare function sourceFromDom(el: Element): SourceLocation | undefined;
7
+ export declare function sourceFor(el: Element, adapterSource: SourceLocation | undefined): SourceLocation | undefined;
8
+ export declare function documentHasSourceStamps(doc: Document): boolean;
9
+ export declare function formatSource(source: SourceLocation | undefined): string | undefined;
@@ -0,0 +1,29 @@
1
+ import { DATA_RETICLE_SOURCE_ATTR } from '@reticlehq/core';
2
+ const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
3
+ export function parseSourceAttr(value) {
4
+ if (null === value)
5
+ return undefined;
6
+ const m = /^(.*):(\d+):(\d+)$/.exec(value);
7
+ if (null === m)
8
+ return undefined;
9
+ const file = m[1];
10
+ const line = Number(m[2]);
11
+ if (file === undefined || 0 === file.length || !Number.isFinite(line))
12
+ return undefined;
13
+ return { file, line };
14
+ }
15
+ export function sourceFromDom(el) {
16
+ const host = el.closest(`[${SOURCE_ATTR}]`);
17
+ return host !== null ? parseSourceAttr(host.getAttribute(SOURCE_ATTR)) : undefined;
18
+ }
19
+ export function sourceFor(el, adapterSource) {
20
+ if (adapterSource !== undefined)
21
+ return { file: adapterSource.file, line: adapterSource.line };
22
+ return sourceFromDom(el);
23
+ }
24
+ export function documentHasSourceStamps(doc) {
25
+ return doc.querySelector(`[${SOURCE_ATTR}]`) !== null;
26
+ }
27
+ export function formatSource(source) {
28
+ return source === undefined ? undefined : `${source.file}:${String(source.line)}`;
29
+ }