@orkestrel/scaffold 0.0.77 → 0.0.79

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 (158) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
  9. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
  10. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
  12. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
  13. package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
  14. package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
  15. package/dist/agents/templates/brief.md +55 -0
  16. package/dist/bin/main.js +58 -6
  17. package/dist/bin/main.js.map +1 -1
  18. package/dist/host/AGENTS.md +77 -135
  19. package/dist/host/agents/orchestration.md +147 -998
  20. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
  21. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  22. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
  23. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
  24. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
  25. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
  26. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
  27. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
  28. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  29. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
  30. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
  31. package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
  32. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
  33. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
  34. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
  35. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
  36. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
  37. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
  38. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
  42. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
  43. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
  44. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
  45. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
  46. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
  47. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
  48. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
  49. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
  50. package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
  51. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
  52. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
  53. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
  54. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
  55. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
  56. package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
  57. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
  58. package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
  59. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
  60. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
  61. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
  62. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
  63. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
  64. package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
  65. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
  66. package/dist/host/agents/templates/brief.md +21 -142
  67. package/dist/host/agents/transports/claude-cli.md +21 -0
  68. package/dist/host/agents/transports/codex.md +38 -159
  69. package/dist/host/agents/transports/cursor.md +16 -65
  70. package/dist/host/claude/AGENTS.md +38 -0
  71. package/dist/host/claude/agents/analyst.md +14 -53
  72. package/dist/host/claude/agents/astra.md +26 -0
  73. package/dist/host/claude/agents/builder.md +14 -30
  74. package/dist/host/claude/agents/checker.md +13 -57
  75. package/dist/host/claude/agents/distiller.md +11 -26
  76. package/dist/host/claude/agents/grok.md +12 -35
  77. package/dist/host/claude/agents/opus.md +14 -30
  78. package/dist/host/claude/agents/orkestrel.md +4 -4
  79. package/dist/host/claude/agents/planner.md +10 -44
  80. package/dist/host/claude/agents/researcher.md +11 -30
  81. package/dist/host/claude/agents/reviewer.md +11 -95
  82. package/dist/host/claude/agents/scout.md +9 -23
  83. package/dist/host/claude/agents/verifier.md +15 -33
  84. package/dist/host/claude/rules/documentation.md +8 -2
  85. package/dist/host/claude/rules/portability.md +7 -1
  86. package/dist/host/claude/rules/quality.md +36 -96
  87. package/dist/host/claude/rules/styles.md +3 -0
  88. package/dist/host/claude/rules/tests.md +6 -3
  89. package/dist/host/claude/rules/workspace.md +21 -15
  90. package/dist/host/claude/rules/writing.md +57 -108
  91. package/dist/host/claude/settings.json +5 -3
  92. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  93. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  95. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  96. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  97. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  99. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  100. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  101. package/dist/host/codex/agents/analyst.toml +14 -31
  102. package/dist/host/codex/agents/astra.toml +25 -0
  103. package/dist/host/codex/agents/builder.toml +13 -20
  104. package/dist/host/codex/agents/checker.toml +13 -27
  105. package/dist/host/codex/agents/distiller.toml +9 -22
  106. package/dist/host/codex/agents/grok.toml +11 -30
  107. package/dist/host/codex/agents/opus.toml +14 -22
  108. package/dist/host/codex/agents/orkestrel.toml +1 -1
  109. package/dist/host/codex/agents/planner.toml +11 -28
  110. package/dist/host/codex/agents/researcher.toml +10 -22
  111. package/dist/host/codex/agents/reviewer.toml +11 -27
  112. package/dist/host/codex/agents/scout.toml +11 -17
  113. package/dist/host/codex/agents/verifier.toml +16 -12
  114. package/dist/host/codex/config.toml +18 -21
  115. package/dist/host/cursor/mcp.json +0 -4
  116. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  117. package/dist/host/dotfiles/mcp.json +0 -4
  118. package/dist/host/dotfiles/oxlintrc.json +7 -0
  119. package/dist/host/guides/probe.md +18 -14
  120. package/dist/host/guides/scaffold.md +147 -83
  121. package/dist/host/guides/test.md +442 -148
  122. package/dist/host/manifest.json +322 -185
  123. package/dist/host/scripts/codex.sh +0 -0
  124. package/dist/host/scripts/cursor.sh +0 -0
  125. package/dist/host/scripts/deps.sh +0 -0
  126. package/dist/host/scripts/ollama.sh +0 -0
  127. package/dist/host/tests/config.test.ts +86 -55
  128. package/dist/host/tests/policy.test.ts +1 -5
  129. package/dist/host/tests/setupPolicy.ts +179 -4
  130. package/dist/src/core/index.cjs +264 -89
  131. package/dist/src/core/index.cjs.map +1 -1
  132. package/dist/src/core/index.d.cts +95 -30
  133. package/dist/src/core/index.d.ts +95 -30
  134. package/dist/src/core/index.js +262 -90
  135. package/dist/src/core/index.js.map +1 -1
  136. package/dist/src/server/index.cjs +55 -9
  137. package/dist/src/server/index.cjs.map +1 -1
  138. package/dist/src/server/index.d.cts +29 -4
  139. package/dist/src/server/index.d.ts +29 -4
  140. package/dist/src/server/index.js +56 -11
  141. package/dist/src/server/index.js.map +1 -1
  142. package/package.json +16 -12
  143. package/dist/host/CLAUDE.md +0 -61
  144. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  145. package/dist/host/agents/transports/claude.md +0 -49
  146. package/dist/host/claude/agents/application.md +0 -36
  147. package/dist/host/claude/agents/sol.md +0 -61
  148. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  149. package/dist/host/codex/agents/application.toml +0 -25
  150. package/dist/host/codex/agents/sol.toml +0 -19
  151. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  155. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  156. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  157. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  158. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -254,6 +254,7 @@ a union's arms escaped as `\|`.
254
254
  | `Color` | type | `readonly [red, green, blue, alpha]` | Represents one rendered color as straight sRGB channels and its alpha. |
255
255
  | `ElementOptions` | interface | `{ classes?, text?, attributes? }` | Configures one built element: its class list, its text, and its attributes. |
256
256
  | `FrameOptions` | interface | `{ path, width, height, element? }` | Configures one captured frame: where it is written, the viewport it is shot at, and what it shoots. |
257
+ | `FrameOffset` | interface | `{ top, left }` | Represents how far an element frame moves the tester frame from the runner window's origin for the shot, in CSS pixels. |
257
258
  | `FrameReading` | interface | `{ width, height, floor }` | Represents one written frame read back from the file a capture produced: its size in device pixels, and the single color its bottom row paints. |
258
259
  | `CaptureVariant` | interface | `JourneyVariant` plus `{ apply? }` | Adds to a journey variant the document change a capture run applies before resizing. |
259
260
  | `PortfolioOptions` | interface | `{ states, variants, variant, directory, enabled? }` | Configures a capture portfolio: the state registry, the variant matrix, this run's variant, where it writes, and whether it writes at all. |
@@ -261,6 +262,7 @@ a union's arms escaped as `\|`.
261
262
  | `JournalStep` | interface | `{ action, trigger, result }` | Represents one scripted step a journal recorded, and what the surface did about it. |
262
263
  | `JournalInterface` | interface | `{ steps, output }` plus `start` / `stop` / `record` | Records one scenario: every step it took and everything the page said while it ran. |
263
264
  | `StateOptions` | interface | `WaitOptions` plus `{ absent? }` | Configures a bounded wait over the states a control announces. |
265
+ | `MediaOptions` | interface | `{ print?, motion?, forced? }` | Configures the tester's print medium, motion preference, and forced colours. |
264
266
  | `StorageOptions` | interface | `{ values?, reads?, writes?, quota? }` | Configures an inert `Storage`: its seed, which operations the host permits, and its quota. |
265
267
  | `WebStorageInterface` | interface | `Storage` plus `{}` plus `permit` | Holds a store the host can withhold and later grant. |
266
268
  | `CensusReading` | interface | `{ elements, tokens, undeclared }` | Reports an authored-class census: the population walked, the tokens found, and the undeclared. |
@@ -285,79 +287,106 @@ A `Shape` cell holds the constant's declared type.
285
287
  | `FOCUSABLE_SELECTOR` | const | `string` | Names what sequential keyboard navigation can reach, before disabled and unrendered elements go. |
286
288
  | `HEADER_ROLES` | const | `Readonly<Record<string, string>>` | Names the role a `th` carries for the header axis its `scope` names. |
287
289
  | `IMPLICIT_ROLES` | const | `Readonly<Record<string, string>>` | Names the role each listed tag carries in the accessibility tree when it declares none of its own. |
290
+ | `MEDIA_STAGE` | const | `string` | Names the tester root's attribute holding the media readings observed before the first stage. |
291
+ | `POINTER_HOLD` | const | `string` | Names the tester root's attribute holding the pressed pointer's page coordinates. |
288
292
 
289
293
  #### Helpers
290
294
 
291
- | API | Kind | Signature | Summary |
292
- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
293
- | `resolveAccessible` | function | `(name: string) => HTMLElement` / `(role: string, name: string) => HTMLElement` | Resolves one visible, focus-reachable interactive element by its exact accessible name. A wholly-off-viewport target is scrolled into view before reachability is measured. |
294
- | `resolveRendered` | function | `(first: string, second?: string) => HTMLElement` | Resolves one rendered, focus-reachable interactive element without requiring it to intersect the viewport yet. |
295
- | `computeNamePattern` | function | `(name: string) => RegExp` | Computes the pattern that matches one accessible name a decorative glyph may sit beside. |
296
- | `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean` | Determines whether a rectangle lies wholly outside the browser viewport. |
297
- | `isRendered` | function | `(element: Element) => boolean` | Determines whether the accessibility tree presents one element at all. |
298
- | `isReachable` | function | `(element: Element) => boolean` | Determines whether a person can click one element where it sits. |
299
- | `readHit` | function | `(element: Element) => Element \| undefined` | Reads the topmost element at one element's bounding-box centre. |
300
- | `clickAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Clicks one visible, focus-reachable control by its accessible name through the browser provider. |
301
- | `clickAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<void>` | Clicks one human-reachable control by role and accessible-name text inside a named region. |
302
- | `clickDisclosure` | function | `(name: string) => Promise<void>` | Opens or closes one native details disclosure by its rendered summary. |
303
- | `typeAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value through focus, select-all, deletion, and real keystrokes. |
304
- | `fillAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value in one operation, for text too long to type key by key. |
305
- | `traverseAccessible` | function | `(name: string) => Promise<HTMLElement>` | Reaches a named control only through natural forward Tab traversal from the current focus. |
306
- | `pressKeys` | function | `(keys: string) => Promise<void>` | Sends a key sequence to whatever holds focus, and refuses to send it to nothing. |
307
- | `readPerception` | function | `(name: string) => string` | Reads the normalized visible text of one named region, dialog, table, tab panel, or alert. |
308
- | `readPage` | function | `() => string` | Reads the normalized visible text of the whole page. |
309
- | `readFocus` | function | `() => string \| undefined` | Reads the rendered text of the element that holds focus. |
310
- | `readValue` | function | `(role: string, name: string) => string` | Reads the value a resolved control renders. |
311
- | `readRefusal` | function | `(name: string) => string \| undefined` / `(role: string, name: string) => string \| undefined` | Reads the refusal one named target answers with, or nothing when it resolves. |
312
- | `readText` | function | `(element: Element) => string` | Reads one element's rendered text the way a name computation reads it. |
313
- | `readRole` | function | `(element: Element) => string \| undefined` | Reads the role one element carries in the accessibility tree. |
314
- | `readName` | function | `(element: Element) => string` | Reads the accessible name one element is announced under. |
315
- | `readStates` | function | `(element: Element) => readonly string[]` | Reads the states one element is announced in. |
316
- | `describeTree` | function | `(element: Element) => string` | Describes the accessible tree one rendered element presents. |
317
- | `describeFocus` | function | `(element: Element) => string` | Describes the order sequential keyboard navigation visits one element's controls in. |
318
- | `waitForFrame` | function | `() => Promise<void>` | Waits for one animation frame to settle pending browser paint work. |
319
- | `waitForState` | function | `(name, state, options?) => Promise<readonly string[]>` / `(role, name, state, options?) => Promise<readonly string[]>` | Waits until one named control announces a state, or stops announcing it. |
320
- | `waitForAnimations` | function | `(element: Element, options?: WaitOptions) => Promise<void>` | Waits until every finite animation on one element and its subtree has stopped moving. |
321
- | `build` | function | `<K extends keyof HTMLElementTagNameMap>(tag: K, options?: ElementOptions) => HTMLElementTagNameMap[K]` | Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for. |
322
- | `mount` | function | `<T extends Element>(element: T) => T` | Puts one element into the document and hands it straight back. |
323
- | `render` | function | `(markup: string) => HTMLDivElement` / `(tag: K, classes: string) => HTMLElementTagNameMap[K]` | Renders one fixture into the document from trusted markup. |
324
- | `typeInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and announces it the way typing into the field does. |
325
- | `commitInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and commits it, the way typing and then leaving the field does. |
326
- | `clearStorage` | function | `() => void` | Clears both browser storage surfaces. |
327
- | `removeDatabase` | function | `(name: string) => Promise<void>` | Deletes one IndexedDB database and reports what the request actually did. |
328
- | `parseColor` | function | `(value: string) => Color \| undefined` | Parses one computed CSS color value into straight sRGB channels. |
329
- | `parseCSSColor` | function | `(value: string) => Color \| undefined` | Resolves any CSS color expression to straight sRGB channels, by asking the browser. |
330
- | `matchesColor` | function | `(first: string \| Color, second: string \| Color) => boolean` | Determines whether two colors render the same, within the rounding a browser does. |
331
- | `blendColor` | function | `(front: Color, back: Color) => Color` | Composites one color over another. |
332
- | `measureLuminance` | function | `(color: Color) => number` | Measures one opaque color's WCAG relative luminance. |
333
- | `measureContrast` | function | `(front: Color, back: Color) => number` | Measures the WCAG 2.x contrast ratio between two opaque colors. |
334
- | `readLayers` | function | `(element: Element) => readonly Color[]` | Collects the painted layers standing between one element and the surface it sits on. |
335
- | `readBackdrop` | function | `(element: Element, floor: Color) => Color` | Resolves the opaque color standing behind one element. |
336
- | `readContrast` | function | `(element: Element, floor?: Color) => number` | Measures the WCAG 2.x contrast ratio between an element's computed text and background colors. |
337
- | `readRing` | function | `(control: Element, worn?: Element) => number \| undefined` | Measures the contrast the focus chrome painted on one control reaches against its own backdrop. |
338
- | `measureContent` | function | `() => number` | Measures the row the document's own content ends on, in document coordinates. |
339
- | `stagePane` | function | `(width: number, height: number) => Promise<void>` | Sets the tester's viewport and renders the runner's pane at the size that viewport claims. |
340
- | `releasePane` | function | `() => Promise<void>` | Hands the tester pane back to the runner's own layout, at the viewport it had before staging. |
341
- | `captureFrame` | function | `(options: FrameOptions) => Promise<string>` | Shoots one frame at one viewport size and proves the file on disk holds this run's bytes. |
342
- | `readFrame` | function | `(path: string) => Promise<FrameReading>` | Reads one written frame back and reports its size and the color its bottom row paints. |
343
- | `readCascade` | function | `() => ReadonlySet<string>` | Collects every class token the stylesheets loaded into this document actually define. |
344
- | `readClasses` | function | `(root: ParentNode) => ReadonlySet<string>` | Collects every class token the markup under one root carries. |
345
- | `readCensus` | function | `(root: ParentNode) => CensusReading` | Takes the authored-class census of one subtree against the cascade this document loaded. |
346
- | `readRules` | function | `() => readonly CSSRule[]` | Collects every rule the stylesheets loaded into this document hold, nested grouping rules included. |
347
- | `findRule` | function | `(selector: string) => CSSStyleRule \| undefined` | Finds the first style rule in the cascade whose selector carries a fragment. |
348
- | `findKeyframes` | function | `(name: string) => CSSKeyframesRule \| undefined` | Finds the animation the cascade declares under one name. |
349
- | `readRows` | function | `(root: ParentNode, selector: string) => readonly string[]` | Reads the normalized visible text of every element a selector matches, in document order. |
350
- | `extractOrphans` | function | `(root: ParentNode, child: string, parent: string) => readonly string[]` | Collects every element carrying a component class rendered outside the container it belongs to. |
351
- | `extractStyles` | function | `(root: ParentNode) => readonly string[]` | Collects the markup of every element carrying a non-empty `style` attribute and of every `<style>` element, in document order, `root` included in both populations when it is an `Element`. |
352
- | `readStyle` | function | `(element: Element, property: string) => string` | Reads one resolved CSS property from a real browser element. |
353
- | `readToken` | function | `(element: Element, name: string) => string` | Reads one custom property from an element's resolved style. |
354
- | `readRootToken` | function | `(name: string) => string` | Reads one custom property from the document element. |
355
- | `readPixels` | function | `(element: Element, property: string) => number` | Reads one resolved CSS length as a number of pixels. |
356
- | `expandCaptures` | function | `(states: readonly string[], variants: readonly CaptureVariant[]) => readonly string[]` | Expands a capture registry across every variant into the filenames a complete portfolio holds. |
357
- | `buildDenial` | function | `(operation: string, key?: string) => DOMException` | Builds the refusal a host withholding a storage operation raises. |
358
- | `buildContrast` | function | `(bar: number) => ContrastFixture` | Builds a detached translucent stack whose composited and flat contrast readings straddle one bar. |
359
- | `buildEscapes` | function | `(permitted: string) => EscapeFixture` | Builds detached markup carrying one style escape of each kind, plus the sheet a project allows. |
360
- | `buildCensus` | function | `() => CensusFixture` | Builds detached markup carrying one undeclared class token on HTML and another on SVG. |
295
+ | API | Kind | Signature | Summary |
296
+ | -------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
297
+ | `resolveAccessible` | function | `(name: string) => HTMLElement` / `(role: string, name: string) => HTMLElement` | Resolves one visible, focus-reachable interactive element by its exact accessible name. A wholly-off-viewport target is scrolled into view before reachability is measured. |
298
+ | `resolveAccessibleWithin` | function | `(region: string, role: string, name: string) => HTMLElement` | Resolves one human-reachable control by role and accessible-name text inside a named region. |
299
+ | `resolveRendered` | function | `(first: string, second?: string) => HTMLElement` | Resolves one rendered, focus-reachable interactive element without requiring it to intersect the viewport yet. |
300
+ | `computeNamePattern` | function | `(name: string) => RegExp` | Computes the pattern that matches one accessible name a decorative glyph may sit beside. |
301
+ | `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean` | Determines whether a rectangle lies wholly outside the browser viewport. |
302
+ | `isRendered` | function | `(element: Element) => boolean` | Determines whether the accessibility tree presents one element at all. |
303
+ | `isReachable` | function | `(element: Element) => boolean` | Determines whether a person can click one element where it sits. |
304
+ | `readHit` | function | `(element: Element) => Element \| undefined` | Reads the topmost element at one element's bounding-box centre. |
305
+ | `clickAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Clicks one visible, focus-reachable control by its accessible name through the browser provider. |
306
+ | `clickAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<void>` | Clicks one human-reachable control by role and accessible-name text inside a named region. |
307
+ | `clickDisclosure` | function | `(name: string) => Promise<void>` | Opens or closes one native details disclosure by its rendered summary. |
308
+ | `sendProtocol` | function | `(method: string, params: Readonly<Record<string, unknown>>) => Promise<void>` | Sends one DevTools protocol command through the browser provider. |
309
+ | `hoverAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Hovers one visible, focus-reachable control by its accessible name through the browser provider. |
310
+ | `holdAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Holds the primary pointer button on one visible, focus-reachable control by its accessible name. |
311
+ | `holdAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<void>` | Holds the primary pointer button on one control by role and accessible-name text inside a named region. |
312
+ | `driveHold` | function | `(resolve: () => HTMLElement, name: string) => Promise<void>` | Holds the primary pointer button on the control a resolver returns, through the browser provider. |
313
+ | `releasePointer` | function | `() => Promise<void>` | Releases a held pointer and parks it outside the page, clearing hover. |
314
+ | `typeAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value through focus, select-all, deletion, and real keystrokes. |
315
+ | `fillAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value in one operation, for text too long to type key by key. |
316
+ | `traverseAccessible` | function | `(name: string) => Promise<HTMLElement>` | Reaches a named control only through natural forward Tab traversal from the current focus. |
317
+ | `traverseAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<HTMLElement>` | Reaches a control by role and accessible-name text inside a named region, only through natural forward Tab traversal from the current focus. |
318
+ | `driveTraversal` | function | `(resolve: () => HTMLElement, name: string) => Promise<HTMLElement>` | Reaches the control a resolver returns, only through natural forward Tab traversal from the current focus. |
319
+ | `pressKeys` | function | `(keys: string) => Promise<void>` | Sends a key sequence to whatever holds focus, and refuses to send it to nothing. |
320
+ | `readPerception` | function | `(name: string) => string` | Reads the normalized visible text of one named region, dialog, table, tab panel, or alert. |
321
+ | `readPage` | function | `() => string` | Reads the normalized visible text of the whole page. |
322
+ | `readFocus` | function | `() => string \| undefined` | Reads the rendered text of the element that holds focus. |
323
+ | `readValue` | function | `(role: string, name: string) => string` | Reads the value a resolved control renders. |
324
+ | `readRefusal` | function | `(name: string) => string \| undefined` / `(role: string, name: string) => string \| undefined` | Reads the refusal one named target answers with, or nothing when it resolves. |
325
+ | `readText` | function | `(element: Element) => string` | Reads one element's rendered text the way a name computation reads it. |
326
+ | `readRole` | function | `(element: Element) => string \| undefined` | Reads the role one element carries in the accessibility tree. |
327
+ | `readName` | function | `(element: Element) => string` | Reads the accessible name one element is announced under. |
328
+ | `readStates` | function | `(element: Element) => readonly string[]` | Reads the states one element is announced in. |
329
+ | `describeTree` | function | `(element: Element) => string` | Describes the accessible tree one rendered element presents. |
330
+ | `describeFocus` | function | `(element: Element) => string` | Describes the order sequential keyboard navigation visits one element's controls in. |
331
+ | `waitForFrame` | function | `() => Promise<void>` | Waits for one animation frame to settle pending browser paint work. |
332
+ | `waitForState` | function | `(name, state, options?) => Promise<readonly string[]>` / `(role, name, state, options?) => Promise<readonly string[]>` | Waits until one named control announces a state, or stops announcing it. |
333
+ | `waitForAnimations` | function | `(element: Element, options?: WaitOptions) => Promise<void>` | Waits until every finite animation on one element and its subtree has stopped moving. |
334
+ | `build` | function | `<K extends keyof HTMLElementTagNameMap>(tag: K, options?: ElementOptions) => HTMLElementTagNameMap[K]` | Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for. |
335
+ | `mount` | function | `<T extends Element>(element: T) => T` | Puts one element into the document and hands it straight back. |
336
+ | `render` | function | `(markup: string) => HTMLDivElement` / `(tag: K, classes: string) => HTMLElementTagNameMap[K]` | Renders one fixture into the document from trusted markup. |
337
+ | `typeInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and announces it the way typing into the field does. |
338
+ | `commitInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and commits it, the way typing and then leaving the field does. |
339
+ | `clearStorage` | function | `() => void` | Clears both browser storage surfaces. |
340
+ | `removeDatabase` | function | `(name: string) => Promise<void>` | Deletes one IndexedDB database and reports what the request actually did. |
341
+ | `convertSRGB` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts normalized encoded sRGB channels to the clipped paint scale. |
342
+ | `convertLinearSRGB` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts linear sRGB channels to encoded, clipped paint channels. |
343
+ | `convertXYZD65` | function | `(x: number, y: number, z: number, alpha?: number) => Color` | Converts D65 XYZ coordinates to clipped sRGB paint channels. |
344
+ | `convertXYZD50` | function | `(x: number, y: number, z: number, alpha?: number) => Color` | Converts D50 XYZ coordinates to clipped sRGB paint channels. |
345
+ | `convertOKLab` | function | `(lightness: number, a: number, b: number, alpha?: number) => Color` | Converts OKLab coordinates to clipped sRGB paint channels. |
346
+ | `convertLab` | function | `(lightness: number, a: number, b: number, alpha?: number) => Color` | Converts CIE Lab coordinates relative to D50 to clipped sRGB paint channels. |
347
+ | `convertDisplayP3` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts encoded Display P3 channels to clipped sRGB paint channels. |
348
+ | `convertA98RGB` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts encoded A98 RGB channels to clipped sRGB paint channels. |
349
+ | `convertProPhotoRGB` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts encoded ProPhoto RGB channels to clipped sRGB paint channels. |
350
+ | `convertRec2020` | function | `(red: number, green: number, blue: number, alpha?: number) => Color` | Converts encoded Rec. 2020 channels to clipped sRGB paint channels. |
351
+ | `parseColor` | function | `(value: string) => Color \| undefined` | Parses computed CSS Color 4 values into clipped straight sRGB channels. |
352
+ | `parseCSSColor` | function | `(value: string) => Color \| undefined` | Resolves any CSS color expression to straight sRGB channels, by asking the browser. |
353
+ | `matchesColor` | function | `(first: string \| Color, second: string \| Color) => boolean` | Determines whether two colors render the same, within the rounding a browser does. |
354
+ | `blendColor` | function | `(front: Color, back: Color) => Color` | Composites one color over another. |
355
+ | `measureLuminance` | function | `(color: Color) => number` | Measures one opaque color's WCAG relative luminance. |
356
+ | `measureContrast` | function | `(front: Color, back: Color) => number` | Measures the WCAG 2.x contrast ratio between two opaque colors. |
357
+ | `readLayers` | function | `(element: Element) => readonly Color[]` | Collects readable background color layers and refuses an unreadable painted layer. |
358
+ | `readBackdrop` | function | `(element: Element, floor: Color) => Color` | Resolves the opaque color standing behind one element. |
359
+ | `readContrast` | function | `(element: Element, floor?: Color) => number` | Measures the WCAG 2.x contrast ratio between an element's computed text and background colors. |
360
+ | `readRing` | function | `(control: Element, worn?: Element) => number \| undefined` | Measures the contrast the focus chrome painted on one control reaches against its own backdrop. |
361
+ | `readClipEdge` | function | `(element: Element) => number \| undefined` | Measures the row a clipping element cuts its content off at, in document coordinates. |
362
+ | `readClipMargin` | function | `(element: Element) => number` | Measures how far past its own box a clipping element lets its content show. |
363
+ | `clipsOverflow` | function | `(element: Element) => boolean` | Reports whether an element clips its descendants' overflow. |
364
+ | `measureContent` | function | `() => number` | Measures the row the document's own content ends on, in document coordinates. |
365
+ | `stagePane` | function | `(width: number, height: number) => Promise<void>` | Sets the tester's viewport and renders the runner's pane at the size that viewport claims. |
366
+ | `releasePane` | function | `() => Promise<void>` | Hands the tester pane back to the runner's own layout, at the viewport it had before staging. |
367
+ | `computeOffset` | function | `(box: DOMRectReadOnly, width: number, height: number) => FrameOffset` | Computes how far an element frame moves the tester frame so the element is shot inside the runner's window. |
368
+ | `stageMedia` | function | `(options: MediaOptions) => Promise<void>` | Stages the tester's print medium, motion preference, and forced colours through the browser provider. |
369
+ | `releaseMedia` | function | `() => Promise<void>` | Restores the media readings observed before the first stage as explicit emulation. With nothing staged, clears every override and waits for a stable reading, not a proved engine baseline. Checks the budget between polls, so a frame that never paints is not bounded by it. |
370
+ | `captureFrame` | function | `(options: FrameOptions) => Promise<string>` | Shoots one frame at one viewport size and proves the file on disk holds this run's bytes. |
371
+ | `readFrame` | function | `(path: string) => Promise<FrameReading>` | Reads one written frame back and reports its size and the color its bottom row paints. |
372
+ | `readCascade` | function | `() => ReadonlySet<string>` | Collects every class token the stylesheets loaded into this document actually define. |
373
+ | `readClasses` | function | `(root: ParentNode) => ReadonlySet<string>` | Collects every class token the markup under one root carries. |
374
+ | `readCensus` | function | `(root: ParentNode) => CensusReading` | Takes the authored-class census of one subtree against the cascade this document loaded. |
375
+ | `readRules` | function | `() => readonly CSSRule[]` | Collects every rule the stylesheets loaded into this document hold, nested grouping rules included. |
376
+ | `findRule` | function | `(selector: string) => CSSStyleRule \| undefined` | Finds the first style rule in the cascade whose selector carries a fragment. |
377
+ | `findKeyframes` | function | `(name: string) => CSSKeyframesRule \| undefined` | Finds the animation the cascade declares under one name. |
378
+ | `readRows` | function | `(root: ParentNode, selector: string) => readonly string[]` | Reads the normalized visible text of every element a selector matches, in document order. |
379
+ | `extractOrphans` | function | `(root: ParentNode, child: string, parent: string) => readonly string[]` | Collects every element carrying a component class rendered outside the container it belongs to. |
380
+ | `extractStyles` | function | `(root: ParentNode) => readonly string[]` | Collects the markup of every element carrying a non-empty `style` attribute and of every `<style>` element, in document order, `root` included in both populations when it is an `Element`. |
381
+ | `readStyle` | function | `(element: Element, property: string, pseudo?: string) => string` | Reads one resolved CSS property from a real browser element or a named pseudo-element. |
382
+ | `readToken` | function | `(element: Element, name: string) => string` | Reads one custom property from an element's resolved style. |
383
+ | `readRootToken` | function | `(name: string) => string` | Reads one custom property from the document element. |
384
+ | `readPixels` | function | `(element: Element, property: string, pseudo?: string) => number` | Reads one resolved CSS length as a number of pixels. |
385
+ | `expandCaptures` | function | `(states: readonly string[], variants: readonly CaptureVariant[]) => readonly string[]` | Expands a capture registry across every variant into the filenames a complete portfolio holds. |
386
+ | `buildDenial` | function | `(operation: string, key?: string) => DOMException` | Builds the refusal a host withholding a storage operation raises. |
387
+ | `buildContrast` | function | `(bar: number) => ContrastFixture` | Builds a detached translucent stack whose composited and flat contrast readings straddle one bar. |
388
+ | `buildEscapes` | function | `(permitted: string) => EscapeFixture` | Builds detached markup carrying one style escape of each kind, plus the sheet a project allows. |
389
+ | `buildCensus` | function | `() => CensusFixture` | Builds detached markup carrying one undeclared class token on HTML and another on SVG. |
361
390
 
362
391
  #### Factories
363
392
 
@@ -412,13 +441,20 @@ rendered status completes its accessible name: the region supplies the context,
412
441
  has to be recognisable inside it. The loose match reads a computed name that includes the hidden
413
442
  subtrees, so a glyph joins the text rather than displacing it and the control is still recognisable
414
443
  under the words beside it. That verb owns one refusal for a control it cannot reach, absent or
415
- hidden, so it needs no second pass to tell the two apart.
444
+ hidden, so it needs no second pass to tell the two apart. `resolveAccessibleWithin` is that
445
+ resolution on its own, and `holdAccessibleWithin` and `traverseAccessibleWithin` drive the control it
446
+ resolves the way `holdAccessible` and `traverseAccessible` drive a document-wide one, so a twin of
447
+ the same name in another region is left alone.
416
448
 
417
449
  `traverseAccessible` charges a step only when focus actually lands on an element, ends when focus
418
450
  revisits one — that is a complete cycle of the tab order — and re-resolves the target by name on
419
451
  every step, because a framework may replace the node between resolution and focus arrival. Its hard
420
452
  cap is three times the page's candidate count plus ten, including disabled controls and elements
421
453
  with `tabindex="-1"`, so a page whose focus never settles fails instead of hanging.
454
+ `driveTraversal` is that loop over any resolver, and `driveHold` is the one pointer drive every hold
455
+ verb shares, releasing the pointer before refusing when the frame wait or the pressed-state read
456
+ fails, as well as when the press misses; each verb supplies its resolver and the name its refusals
457
+ voice.
422
458
 
423
459
  `build` and `mount` are the halves of a fixture, and `render` is the pair spelled as one call.
424
460
  `build` creates the element and applies its class list, its text, and its attributes, and leaves it
@@ -451,6 +487,16 @@ another connection is still open, and a suite that swallowed it would leave the
451
487
  the previous test's records through a database that reports itself deleted. The connection holding
452
488
  it open is the caller's to close, and [Voices](#voices) carries the message each refusal spells.
453
489
 
490
+ The paint parser reads computed `rgb()`, `rgba()`, `oklab()`, `oklch()`, `lab()`,
491
+ `lch()`, and `color()` values. Its predefined spaces are `srgb`, `srgb-linear`,
492
+ `display-p3`, `a98-rgb`, `prophoto-rgb`, `rec2020`, `xyz`, `xyz-d50`, and
493
+ `xyz-d65`. Signed channels, percentage lightness, degree hues, scientific notation, and
494
+ `none` components resolve to straight sRGB. The exported conversion helpers apply the
495
+ [CSS Color 4 matrices and white-point adaptation](https://www.w3.org/TR/css-color-4/#color-conversion-code).
496
+ The Rec. 2020 decoder follows Chromium's piecewise transfer curve. Encoded sRGB channels are
497
+ clipped to 0–255 after conversion; this reader does not perform perceptual gamut mapping.
498
+ Alpha is clipped to 0–1.
499
+
454
500
  `parseCSSColor` is the live half of the pair `parseColor` opens. `parseColor` reads text and speaks
455
501
  only the computed syntaxes a cascade hands back; `parseCSSColor` stages a probe element, hands the
456
502
  expression to the real cascade, and reads back what the engine computed — which is the only way a
@@ -483,6 +529,12 @@ layers blend to identical channels over black and over white alike, because the
483
529
  share falls below the last bit a channel carries, so comparing two composited readings admits the
484
530
  stack the refusal exists for.
485
531
 
532
+ An unreadable painted background layer throws an error naming its element and computed value.
533
+ Non-finite calculations such as `color(srgb calc(infinity) 0 0)` are deliberately unreadable.
534
+ A layer with an explicit zero alpha paints nothing and is skipped, including an unreadable one.
535
+ Background images remain outside this color reader. The refusal propagates through `readBackdrop`,
536
+ `readContrast`, and the focused-control reading in `readRing`, even with a supplied floor.
537
+
486
538
  `readBackdrop` composites that stack and takes its floor as an argument rather than reaching for
487
539
  `CANVAS_COLOR` itself, so a measurement over a surface the canvas never shows through names the
488
540
  color it actually sits on. When no layer paints it hands that floor straight back.
@@ -493,7 +545,8 @@ the browser painted once it landed: the `outline` the cascade declares, and the
493
545
  `outline-style: auto` ring, and a focus style that only changes the control's own fill all report
494
546
  `undefined` — in each case no measurement taken here would be about focus. `worn` names the element
495
547
  the chrome is painted onto when that is not the element holding focus, which is the hidden-input
496
- control whose visible label wears every pixel of its chrome.
548
+ control whose visible label wears every pixel of its chrome. Modern colors, including an
549
+ `oklch()` box-shadow, return the ring’s contrast ratio against its backdrop.
497
550
 
498
551
  `readRules` is the one walk over the shipped cascade, and `readCascade`, `findRule`, and
499
552
  `findKeyframes` all read through it. It collects each sheet's own rules in sheet order and then
@@ -556,7 +609,7 @@ command and compares it with the shot itself, which is what separates this run's
556
609
  earlier run left behind. It releases the pane in a `finally`, so a refusal at any stage hands the
557
610
  tester back before it propagates.
558
611
 
559
- The frame covers the whole document at the width it was given, whatever height it was given. The
612
+ A page frame covers the whole document at the width it was given, whatever height it was given. The
560
613
  provider shoots the tester's body in the top-level page's own coordinates, so a document taller than
561
614
  the pane paints for the pane's height and the rows under it are the runner's page: the frame reads
562
615
  as the surface down to the fold and as bare canvas after it. `captureFrame` therefore lays the
@@ -570,9 +623,18 @@ reads back as the document's own height. A capture that staged a pane taller tha
570
623
  not descend from a reading like that — the box, `body.scrollHeight`, `body.offsetHeight`, and
571
624
  `documentElement.scrollHeight` each answer with the pane. `measureContent` walks the elements inside
572
625
  the body instead, taking the largest bottom edge in document coordinates plus that element's own
573
- bottom margin, and adds the body's and the root's bottom padding and margin under them. It rounds
574
- up, which is what covers a body ending part way through a row: a box ending on a fraction under a
575
- half is a row the integer scroll height drops, and that row comes out as the runner's page.
626
+ bottom margin, and adds the body's and the root's bottom padding and margin under them. An
627
+ ancestor that clips its overflow (the `clipsOverflow` helper: an `overflow-y` value other than the
628
+ `visible` keyword, or a paint containment) caps a descendant's edge at that ancestor's clip edge,
629
+ which the `readClipEdge` helper reads. An `overflow-y` value of the `hidden` keyword, the `auto`
630
+ keyword, or the `scroll` keyword clips at the padding box, whatever the ancestor's
631
+ `overflow-clip-margin` value selects. The `clip` keyword and a paint containment over a `visible`
632
+ overflow clip at the box that value selects, the padding box by default, expanded by the length the
633
+ `readClipMargin` helper reads. So a viewport-height specimen
634
+ inside a bounded frame ends, for the reading, where the frame ends rather than stretching the
635
+ document with every pane. It rounds up, which is what covers a body ending part way through a row: a
636
+ box ending on a fraction under a half is a row the integer scroll height drops, and that row comes
637
+ out as the runner's page.
576
638
 
577
639
  The edge is read again after every staging, because a rule bound to the viewport height — a `vh`
578
640
  length, a fixed footer, a full-height panel — lays the document out taller against the taller pane,
@@ -595,13 +657,40 @@ written at a height that is already wrong. That bound is 4: a document holding h
595
657
  fixed block settles in two restagings, one whose growth is capped part way settles in three, and
596
658
  the fourth is headroom.
597
659
 
660
+ An element frame no taller than the declared height is shot in the declared pane, so its viewport
661
+ lengths resolve against the declared viewport whether the element sits above the fold, below it, or
662
+ fixed: a `50vh` element under a 900-row block reads back 422 rows in an 844-row pane. The re-reading
663
+ takes the element's own height, so the pane grows only for an element taller than the declared
664
+ height, and then to that element's height, which is what its viewport lengths resolve against.
665
+ Where the element's box lies outside the pane, the document is scrolled by the nearest distance that
666
+ would bring it inside, and the box is read again. A fixed element past the pane can take a scroll
667
+ that does not move it, and that scroll is handed back with the rest. The document is not scrolled for
668
+ an element already inside the pane.
669
+
670
+ The provider paints an element that fits the runner's window only where that window shows it. Where
671
+ such an element lies past the window, the `captureFrame` function offsets the calling tester frame up
672
+ or left only as far as brings the element inside, and composites the frame so a fixed element that
673
+ starts past the window's height is not culled. The `computeOffset` function computes that move. The
674
+ offset is written on that frame's own `style` attribute, outranks the placement the `stagePane`
675
+ function makes, and is removed as soon as the screenshot settles. An element inside the window, or
676
+ too large for it, is not offset.
677
+
678
+ The capture sends no pointer input. The staging lifts the tester to the window's origin, a scroll
679
+ brings an element outside the pane into it, and an offset brings an element past the window inside;
680
+ each moves content under a pointer resting on the page. A pointer the case placed on the element,
681
+ with the pane already staged at the frame's size, keeps its hover in the frame where the element lies
682
+ inside both the pane and the runner window, because the capture then moves nothing. No hover is
683
+ promised after the pane is released.
684
+
598
685
  `readFrame` reads a written frame back: its size in device pixels, and the single color its bottom
599
686
  row paints. The reading comes off the file through the browser's own image decoding rather than off
600
687
  the document that produced it, which is what makes it evidence about the capture rather than a
601
688
  second look at the style that fed it — a clipped frame reports the runner's white canvas as its
602
689
  floor while every style in the document still resolves to the document's own background. Pass the
603
690
  absolute path `captureFrame` returned: the runner's `readFile` command resolves a relative path
604
- against its own root rather than against the calling test file.
691
+ against its own root rather than against the calling test file. A file that opens with a PNG header
692
+ and still does not decode is refused with the width and height that header declares, in device
693
+ pixels; a file with no PNG header is refused without a size.
605
694
 
606
695
  `createPortfolio` refuses an unregistered variant name at creation, so a run cannot write a filename
607
696
  naming a combination it did not render. A portfolio left un-`enabled` is the ordinary run: `place`
@@ -991,50 +1080,51 @@ what the link reaches, not on what it stores.
991
1080
  Every message `src/browser` throws. Keep them distinct: a journey asserts the one it means, and
992
1081
  absent, present-but-gated, and ambiguous are different findings about an interface.
993
1082
 
994
- | Voice | Thrown by |
995
- | ---------------------------------------------------------------------------------------- | ----------------------- |
996
- | `No interactive element has the accessible name "<name>"` | `resolveRendered` |
997
- | `Interactive target "<name>" is not visible and focus-reachable` | `resolveRendered` |
998
- | `Interactive target "<name>" is ambiguous across <n> elements` | `resolveRendered` |
999
- | `Interactive target "<name>" could not be resolved` | `resolveRendered` |
1000
- | `Interactive target "<name>" is unreachable after scrolling` | `resolveAccessible` |
1001
- | `Interactive target "<name>" is not reachable inside "<region>"` | `clickAccessibleWithin` |
1002
- | `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"` | `clickAccessibleWithin` |
1003
- | `Interactive target "<name>" could not be resolved inside "<region>"` | `clickAccessibleWithin` |
1004
- | `Native disclosure "<name>" is not visible and focus-reachable` | `clickDisclosure` |
1005
- | `Native disclosure "<name>" is ambiguous across <n> elements` | `clickDisclosure` |
1006
- | `Native disclosure "<name>" could not be resolved` | `clickDisclosure` |
1007
- | `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>` | `traverseAccessible` |
1008
- | `Named region "<name>" is not visible` | `readPerception` |
1009
- | `Named region "<name>" is ambiguous across <n> elements` | `readPerception` |
1010
- | `Named region "<name>" could not be resolved` | `readPerception` |
1011
- | `Interactive target "<name>" does not carry a value` | `readValue` |
1012
- | `Computed foreground color is unavailable` | `readContrast` |
1013
- | `Computed background color is unavailable` | `readContrast` |
1014
- | `Tester pane is unavailable for a capture` | `stagePane` |
1015
- | `Tester pane rendered <w>x<h> for a <w>x<h> viewport` | `stagePane` |
1016
- | `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane` | `captureFrame` |
1017
- | `Capture frame was written to <path> where <path> was asked for` | `captureFrame` |
1018
- | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
1019
- | `Capture frame at <path> could not be read` | `readFrame` |
1020
- | `Capture frame at <path> is not an image this browser decodes` | `readFrame` |
1021
- | `Capture frame at <path> cannot be measured without a 2D canvas` | `readFrame` |
1022
- | `Capture variant "<name>" is not registered` | `createPortfolio` |
1023
- | `Capture state "<state>" is not registered` | `place` |
1024
- | `Capture state "<state>" is already placed` | `place` |
1025
- | `IndexedDB database "<name>" could not be deleted` | `removeDatabase` |
1026
- | `IndexedDB database "<name>" is blocked by an open connection` | `removeDatabase` |
1027
- | `Key sequence "<keys>" was sent with nothing focused` | `pressKeys` |
1028
- | `Condition "<subject>" did not hold within <n>ms (waited <n>ms) (last states: <states>)` | `waitForState` |
1029
- | `Animation subject is not connected` | `waitForAnimations` |
1030
- | `Animation "<subject>" did not settle within <n>ms (waited <n>ms): <animations>` | `waitForAnimations` |
1031
- | `Class census walked no element` | `readCensus` |
1032
- | `Contrast control cannot straddle the bar <bar>` | `buildContrast` |
1033
- | `Access is denied for <operation> "<key>"` | `buildDenial` |
1034
- | `No room is left for <key>` | `createStorage` |
1035
- | `Storage quota must be a non-negative integer` | `createStorage` |
1036
- | `Statechart harness mounted no transition` | `createHarness` |
1037
- | `Statechart harness carries no status` | `status` |
1083
+ | Voice | Thrown by |
1084
+ | ---------------------------------------------------------------------------------------- | ------------------------- |
1085
+ | `No interactive element has the accessible name "<name>"` | `resolveRendered` |
1086
+ | `Interactive target "<name>" is not visible and focus-reachable` | `resolveRendered` |
1087
+ | `Interactive target "<name>" is ambiguous across <n> elements` | `resolveRendered` |
1088
+ | `Interactive target "<name>" could not be resolved` | `resolveRendered` |
1089
+ | `Interactive target "<name>" is unreachable after scrolling` | `resolveAccessible` |
1090
+ | `Interactive target "<name>" is not reachable inside "<region>"` | `resolveAccessibleWithin` |
1091
+ | `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"` | `resolveAccessibleWithin` |
1092
+ | `Interactive target "<name>" could not be resolved inside "<region>"` | `resolveAccessibleWithin` |
1093
+ | `Native disclosure "<name>" is not visible and focus-reachable` | `clickDisclosure` |
1094
+ | `Native disclosure "<name>" is ambiguous across <n> elements` | `clickDisclosure` |
1095
+ | `Native disclosure "<name>" could not be resolved` | `clickDisclosure` |
1096
+ | `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>` | `driveTraversal` |
1097
+ | `Named region "<name>" is not visible` | `readPerception` |
1098
+ | `Named region "<name>" is ambiguous across <n> elements` | `readPerception` |
1099
+ | `Named region "<name>" could not be resolved` | `readPerception` |
1100
+ | `Interactive target "<name>" does not carry a value` | `readValue` |
1101
+ | `Computed foreground color is unavailable` | `readContrast` |
1102
+ | `Computed background color is unavailable` | `readContrast` |
1103
+ | `Tester pane is unavailable for a capture` | `stagePane` |
1104
+ | `Tester pane rendered <w>x<h> for a <w>x<h> viewport` | `stagePane` |
1105
+ | `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane` | `captureFrame` |
1106
+ | `Capture frame was written to <path> where <path> was asked for` | `captureFrame` |
1107
+ | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
1108
+ | `Capture frame at <path> could not be read` | `readFrame` |
1109
+ | `Capture frame at <path> is not an image this browser decodes` | `readFrame` |
1110
+ | `Capture frame at <path> is not an image this browser decodes: <w>x<h> device pixels` | `readFrame` |
1111
+ | `Capture frame at <path> cannot be measured without a 2D canvas` | `readFrame` |
1112
+ | `Capture variant "<name>" is not registered` | `createPortfolio` |
1113
+ | `Capture state "<state>" is not registered` | `place` |
1114
+ | `Capture state "<state>" is already placed` | `place` |
1115
+ | `IndexedDB database "<name>" could not be deleted` | `removeDatabase` |
1116
+ | `IndexedDB database "<name>" is blocked by an open connection` | `removeDatabase` |
1117
+ | `Key sequence "<keys>" was sent with nothing focused` | `pressKeys` |
1118
+ | `Condition "<subject>" did not hold within <n>ms (waited <n>ms) (last states: <states>)` | `waitForState` |
1119
+ | `Animation subject is not connected` | `waitForAnimations` |
1120
+ | `Animation "<subject>" did not settle within <n>ms (waited <n>ms): <animations>` | `waitForAnimations` |
1121
+ | `Class census walked no element` | `readCensus` |
1122
+ | `Contrast control cannot straddle the bar <bar>` | `buildContrast` |
1123
+ | `Access is denied for <operation> "<key>"` | `buildDenial` |
1124
+ | `No room is left for <key>` | `createStorage` |
1125
+ | `Storage quota must be a non-negative integer` | `createStorage` |
1126
+ | `Statechart harness mounted no transition` | `createHarness` |
1127
+ | `Statechart harness carries no status` | `status` |
1038
1128
 
1039
1129
  Some of those rows are not plain `Error` messages. `buildDenial` returns a `DOMException` named
1040
1130
  `SecurityError`, `createStorage` raises that one from every operation the permission withholds and a
@@ -1066,9 +1156,27 @@ disk disagrees with the bytes the provider handed back — which a provider that
1066
1156
  never produces, so the suite proves that comparison discriminates with a planted file rather than by
1067
1157
  reaching the refusal. `Capture frame at <path> cannot be measured without a 2D canvas` is narrowing
1068
1158
  of the same kind: a canvas allocated for this reading and asked for no other context type hands one
1069
- back. `readFrame`'s other two refusals are driven, by a path holding no file and by a file holding
1070
- no image, and so is `Capture frame at <path> never settled after <n> restagings`, by a fixture whose
1071
- full-height panel grows with every pane the capture stages.
1159
+ back. The remaining refusals of the `readFrame` function are driven, by a path holding no file, by a
1160
+ file holding no image, and by a PNG header over no image data, and so is
1161
+ `Capture frame at <path> never settled after <n> restagings`, by a fixture whose full-height panel
1162
+ grows with every pane the capture stages.
1163
+
1164
+ The pointer, pseudo-element, and media helpers add the following voices.
1165
+
1166
+ | Voice | Thrown by |
1167
+ | ------------------------------------------------------------- | -------------- |
1168
+ | `Browser provider exposes no DevTools session` | `sendProtocol` |
1169
+ | `Pointer is already held at <x>x<y>` | `driveHold` |
1170
+ | `Interactive target "<name>" did not enter the pressed state` | `driveHold` |
1171
+ | `Pseudo-element "<pseudo>" must start with "::"` | `readStyle` |
1172
+ | `Pseudo-element "<pseudo>" is not one this engine exposes` | `readStyle` |
1173
+ | `Media emulation was staged with nothing to emulate` | `stageMedia` |
1174
+ | `Media emulation did not reach the tester: <query>` | `stageMedia` |
1175
+ | `Media emulation did not clear from the tester` | `releaseMedia` |
1176
+
1177
+ The resolver's absent, gated, ambiguous, and unreachable voices stay unchanged. The DevTools-session
1178
+ and media-delivery refusals guard provider failures the managed Chromium suite doesn't induce.
1179
+ The suite exercises real delivery rather than substituting a provider that manufactures a failure.
1072
1180
 
1073
1181
  ### Refusals outside the journey layer
1074
1182
 
@@ -1356,13 +1464,22 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1356
1464
  element, the `hidden` attribute, a hidden input, and a `display` or `visibility` that takes it
1357
1465
  off the page. `isReachable` reads geometry, and adds connectedness, a visibility check that
1358
1466
  honours opacity, a non-zero box, the sequential focus order, `:disabled` and `aria-disabled`, and
1359
- the `[inert]` ancestor. A control clipped to a
1360
- zero-size rectangle is the case that separates them: the accessibility tree still announces it,
1361
- so `isRendered` accepts it and `isReachable` refuses it. `isReachable` is the one reachability
1362
- filter the layer applies — `resolveRendered`, `clickAccessibleWithin`, and `clickDisclosure` each
1363
- narrow their own candidates and then keep the ones it accepts — so a journey meets one rule
1364
- rather than near-copies of it. Neither asks about the viewport; `resolveAccessible` scrolls a
1365
- wholly off-viewport target into view and measures that separately with `isOutsideViewport`.
1467
+ the `[inert]` ancestor. It adds one reading the element's own facts cannot carry: an open modal
1468
+ dialog. A shown `[aria-modal="true"]` element that does not contain the subject refuses it,
1469
+ because a pointer, a Tab, and a reader honouring that attribute all stop at the dialog while the
1470
+ covered control stays connected, laid out, and focusable. The dialog is put through `isRendered`,
1471
+ so a drawer parked at `visibility: hidden` excludes nothing, and containment follows the flat
1472
+ tree, so the innermost dialog rules and a host it holds carries its shadow content with it.
1473
+ Applying that inside the predicate is what keeps the resolver, the ambiguity count, the Tab
1474
+ trail, and every acting verb agreeing with the person in front of the dialog, and it is what
1475
+ takes away the name splitting a consumer writes to keep a covered control out of the count. A
1476
+ control clipped to a zero-size rectangle is the case that separates them: the accessibility tree
1477
+ still announces it, so `isRendered` accepts it and `isReachable` refuses it. `isReachable` is the
1478
+ one reachability filter the layer applies — `resolveRendered`, `clickAccessibleWithin`, and
1479
+ `clickDisclosure` each narrow their own candidates and then keep the ones it accepts — so a
1480
+ journey meets one rule rather than near-copies of it. Neither asks about the viewport;
1481
+ `resolveAccessible` scrolls a wholly off-viewport target into view and measures that separately
1482
+ with `isOutsideViewport`.
1366
1483
  `readHit` reads beside that pair rather than filtering with it. It hit-tests one point — the
1367
1484
  element's own bounding-box centre — which is how it sees what neither predicate can: a cover
1368
1485
  over a control they both accept, and a wrapped inline target whose centre falls between its line
@@ -1392,19 +1509,25 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1392
1509
  `Tester pane rendered <w>x<h> for a <w>x<h> viewport` rather than writing a wrong frame. The
1393
1510
  coupling therefore fails loudly, and the version this rule names moves with the fix instead of a
1394
1511
  suite shipping thumbnails nobody inspects. The same layout decides what a frame covers: the
1395
- provider shoots the tester's body in the top-level page's coordinates, so `captureFrame` stages
1396
- the pane again at the document's own height wherever the document outruns the declared one, and
1397
- a frame is neither shorter nor taller than the document it photographs. That height is
1512
+ provider shoots the tester's body in the top-level page's coordinates, so the `captureFrame`
1513
+ function stages the pane again at the document's own height wherever the document outruns the
1514
+ declared one, and a page frame is neither shorter nor taller than the document it photographs.
1515
+ That height is
1398
1516
  `measureContent`, the content's own edge rounded up, rather than the body's box: the box is the
1399
1517
  larger of the content and the pane, so a taller pane stretches it and a capture cannot read its
1400
1518
  way back down. The edge is read again after every staging, because a rule bound to the viewport
1401
1519
  height lays the document out taller against the taller pane, and each staging carries the growth
1402
1520
  the one before it produced so a converging document lands on its fixed point rather than
1403
1521
  creeping toward it. The re-reading is bounded by `CAPTURE_STAGINGS`, and a document still
1404
- growing at that bound is refused rather than photographed at a stale height. What the capture borrows it gives back —
1405
- `releasePane` returns the tester to the viewport it held before the staging, so the variant a
1406
- frame was shot at belongs to that frame alone, and a suite that wants a size of its own calls
1407
- `page.viewport` rather than this pair.
1522
+ growing at that bound is refused rather than photographed at a stale height. An element frame
1523
+ keeps the declared pane unless its element is taller, and offsets the calling tester frame only
1524
+ as far as brings the element inside the runner's window. What
1525
+ the capture borrows it gives back: the `releasePane` function returns the tester to the viewport
1526
+ it held before the staging, the capture then restores the tester's scroll position, even where
1527
+ the release rejects, and it restores the offset frame's
1528
+ `style` attribute as soon as the screenshot settles. So the variant a frame was shot at belongs
1529
+ to that frame alone, and a suite that wants a size of its own calls the `page.viewport` method
1530
+ rather than this pair.
1408
1531
  19. **The statechart harness is test-side, and the markup is its whole contract.** A page cannot
1409
1532
  import this package. `@orkestrel/test` is a development dependency, its browser entry imports
1410
1533
  `vitest/browser` at module scope, and that import throws outside Browser Mode — so an
@@ -1536,6 +1659,18 @@ or when a consumer appears the ruling did not consider.
1536
1659
  | A generated or published harness page | Refused | Which transitions a surface owes, where that page is deep-linked, and whether it ships to anyone are product decisions, and framework code stops before them. A page hosting a harness would also have to import this package, which rule 19 rules out: the browser entry imports `vitest/browser` at module scope. The mechanism ships and the page does not. |
1537
1660
  | A framework-class disclosure settle | Refused | A helper that waits for a named element to carry one class and not two others encodes one framework's transition vocabulary, which is that framework's policy rather than a mechanism. `waitForState` waits on what the control announces and `waitForAnimations` waits on the paint itself, and between them they answer the question the class poll was asked. A surface announcing nothing is the finding. |
1538
1661
 
1662
+ The journey additions have these candidate rulings.
1663
+
1664
+ | Candidate | Ruling | Why |
1665
+ | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1666
+ | A hover verb | Ships | `hoverAccessible` adds exact role/name resolution and the journey's reachability boundary to the provider's hover primitive. |
1667
+ | A pointer hold | Ships | `holdAccessible` composes resolution, tester-scale mapping, a trusted press, and a pressed-state read-back through `driveHold`; `holdAccessibleWithin` resolves inside a named region and drives the same hold. `releasePointer` releases the hold and clears hover. Veneer's Button consumes the held paint. |
1668
+ | A pseudo-element style read | Ships | The third argument of `readStyle` and `readPixels` selects another subject of the same CSSOM reading. A separate reader would duplicate that operation. |
1669
+ | A medium emulation | Ships | `stageMedia` bounds the axes through `MediaOptions`, reads back `print: true`, either `motion` value, and either `forced` value, and pairs with `releaseMedia` for teardown. A `print: false` stage is sent and followed by a frame wait without a read-back. |
1670
+ | A DevTools command door | Ships | `sendProtocol` centralizes the unchecked provider boundary shared by pointer and media operations. The scale control consumes it directly. |
1671
+ | A general media-feature map | Refused | The consumer needs print, motion, and forced colours. A free-form map adds an unbounded contract without a consumer. |
1672
+ | A scoped hold taking a callback | Refused | The layer already uses stage-and-release pairs. An `afterEach` hook owns release after a failed test without a second lifecycle form. |
1673
+
1539
1674
  `ScratchInterface`'s own members were ruled the same way, and coherence rather than demand decided
1540
1675
  them. `ensure` ships because it is the one member that produces an empty directory — `write` always
1541
1676
  creates a file. `names` and `link` ship because a fixture that seeds a tree has to list it and to
@@ -1558,6 +1693,50 @@ peer, protocol fixture, and domain builder stays in the package that owns it.
1558
1693
  A shipped helper can still decline the question it looks like it answers. Each bound here belongs to
1559
1694
  the helper rather than to the host, and each names what to reach for instead.
1560
1695
 
1696
+ - **`holdAccessible` maps through the tester iframe's painted scale and reads `:active` back.** It
1697
+ presses the control's centre and releases before refusing a missed press. If that release also
1698
+ rejects, the missed-press refusal carries it as its cause. That path can't be driven from inert
1699
+ input against a conforming engine, because the marker is built from the coordinates the press
1700
+ used; it is covered by review. The measured layout is a single accessible, uniformly scaled tester
1701
+ iframe. A covered centre or unsupported geometry can refuse even when the resolver accepts the
1702
+ target. The verb inherits the resolver's focus reachability conditions.
1703
+ - **`releasePointer` parks the pointer outside the page.** It releases at the recorded point first,
1704
+ which can produce a click. It then clears hover by moving to (-1, -1) in the runner page's
1705
+ coordinates, one pixel above and to the left of that page's viewport. The browser hit-tests
1706
+ nothing outside the viewport, so no element takes a `mouseover` event or hover paint from the
1707
+ parked pointer until the next pointer verb. This holds even where a staging, scroll, or offset
1708
+ lays content over the park point. Register it with `afterEach` before a hover
1709
+ or hold. A second release sends no button-up event. Holds must not overlap. A rejected press
1710
+ leaves no marker. A rejected release keeps the marker for a retry and still attempts the park. If
1711
+ that park also rejects, the park's error surfaces with the release rejection attached in the
1712
+ aggregate's errors; the park rejection is its cause.
1713
+ - **`stageMedia` overrides the provider page, and `releaseMedia` restores the first stage's
1714
+ readings.** Pin a base with `{ motion: true }` rather than assuming the host prefers motion.
1715
+ `motion: false` stages reduced motion; `print: true` stages print, and `print: false` stages
1716
+ screen; `forced: true` stages active forced colours, and `forced: false` stages none. An omitted
1717
+ print, motion, or forced axis keeps its effective reading, as does `prefers-color-scheme`. Any
1718
+ other emulated feature the provider configured is cleared. An empty `{}`
1719
+ refuses. Each staged query has a 1000 ms read-back budget with a 10 ms poll interval; refusal
1720
+ restores the carried pre-call readings before throwing and can take two budgets. A restoration
1721
+ failure is attached as the refusal's cause. The read-back exhaustion path can't be driven from
1722
+ inert input against a conforming engine; its restoration is covered by review and by the shared
1723
+ payload code, not by a case. The first stage records print, reduced motion, dark colour scheme,
1724
+ and forced colours on the tester root under `MEDIA_STAGE`, as a bit string in that order (`1` for
1725
+ a match, `0` otherwise). Later stages keep that marker. Release restores those readings as
1726
+ explicit emulation rather than removing every override, waits per axis for its recorded value, and
1727
+ removes the marker. A release whose wait exhausts keeps `MEDIA_STAGE` on the tester root, so the
1728
+ next `releaseMedia` retries from the same recorded readings. A release with nothing staged clears
1729
+ every override and compares readings taken strictly after the reset until they are stable.
1730
+ Stability doesn't prove the engine's own baseline. Each release read-back has a 1000 ms budget and
1731
+ waits for a frame per poll, with 10 ms between polls. The budget is checked between polls, so a
1732
+ frame that never paints is not bounded by it. Media scopes must not overlap. The browser project
1733
+ runs files serially.
1734
+ - **`readStyle` refuses a pseudo argument rather than ignoring it.** The argument must start with
1735
+ `::` before the engine's selector-support check runs. A supported pseudo-element with no value for
1736
+ the requested property still reads as an empty string. `readPixels` uses the same guard.
1737
+ - **`sendProtocol` reaches a Chromium-family provider alone.** It needs the provider's DevTools
1738
+ session, passes the named command and parameters through, and discards the response. Browser
1739
+ termination cannot promise cleanup. Edge observations belong to a separate run.
1561
1740
  - **`parseCSSColor` resolves an undeclared token to the inherited color.** A `var()` naming a custom
1562
1741
  property nothing declares is not a parse failure: the cascade accepts it and computes the
1563
1742
  inherited color, so `parseCSSColor('var(--absent)')` hands back channels rather than `undefined`.
@@ -1597,6 +1776,13 @@ the helper rather than to the host, and each names what to reach for instead.
1597
1776
  subject: a host the document does not lay out takes the element off the page, and both predicates
1598
1777
  refuse it. Read a `true` for a shadow subject as the element's own answer, and ask the host
1599
1778
  separately where an ancestor attribute is the subject.
1779
+ - **`isReachable` finds an open modal through the `aria-modal` attribute in the element's own
1780
+ document.** Two arrangements carry no such attribute there, and each leaves the page behind it
1781
+ reachable: a native `<dialog>` opened with `showModal`, which a browser makes modal without
1782
+ marking it, and a dialog declared inside a shadow tree, which a document query does not return.
1783
+ Containment itself does cross a boundary, because the subject's host chain is judged beside the
1784
+ subject, so a dialog holding a host holds that host's shadow content too. Read `:modal` or the
1785
+ dialog's own root where a native or shadow-declared dialog is the subject.
1600
1786
  - **`waitForAnimations` waits on the animations a browser reports as running.** A finished animation
1601
1787
  filling its target stays in the list and is already at rest, a paused one is at rest too and
1602
1788
  nothing here resumes it, and an animation declaring infinite iterations never finishes. Each is
@@ -2715,6 +2901,81 @@ await traverseAccessible('Evaluate')
2715
2901
  readPerception('Run') // one visible named region, whitespace collapsed, hidden-but-read text kept
2716
2902
  ```
2717
2903
 
2904
+ ### Hold a control and read the pressed paint
2905
+
2906
+ Register pointer cleanup before a test can fail. This fixture declares `padding-top: 16px` on its
2907
+ `Apply` button and `32px` under `:active`. Read the paint while the button is held, then release it.
2908
+
2909
+ ```ts
2910
+ import {
2911
+ holdAccessible,
2912
+ readPixels,
2913
+ releasePointer,
2914
+ resolveAccessible,
2915
+ } from '@orkestrel/test/browser'
2916
+ import { afterEach } from 'vitest'
2917
+
2918
+ afterEach(releasePointer)
2919
+ const button = resolveAccessible('button', 'Apply')
2920
+ readPixels(button, 'padding-top') // 16
2921
+ await holdAccessible('button', 'Apply')
2922
+ readPixels(button, 'padding-top') // 32
2923
+ await releasePointer()
2924
+ readPixels(button, 'padding-top') // 16
2925
+ ```
2926
+
2927
+ Where a twin elsewhere on the page carries the same name, hold the one inside a named region with
2928
+ `holdAccessibleWithin('Ledger', 'button', 'Apply')`, which composes `resolveAccessibleWithin` with
2929
+ `driveHold`; `traverseAccessibleWithin` composes the same resolver with `driveTraversal`, and each
2930
+ refuses before it sends any input.
2931
+
2932
+ ### Read a pseudo-element's paint
2933
+
2934
+ This fixture declares `padding-top: 0` on its `Marked` button and `7px` on its generated `::after`
2935
+ pseudo-element. Keep the element reading beside the pseudo reading so dropping the argument fails.
2936
+
2937
+ ```ts
2938
+ import { readPixels, readStyle, resolveAccessible } from '@orkestrel/test/browser'
2939
+
2940
+ const button = resolveAccessible('Marked')
2941
+ readStyle(button, 'padding-top', '::after') // '7px'
2942
+ readPixels(button, 'padding-top', '::after') // 7
2943
+ readStyle(button, 'padding-top') // '0px'
2944
+ ```
2945
+
2946
+ ### Emulate reduced motion and print
2947
+
2948
+ This fixture's `Media` button declares `padding-top: 1px`, `2px` under reduced motion, and `3px`
2949
+ under print. Pin the base preference, then read each staged axis. Stage the inverse of the observed
2950
+ motion reading before release so the restore comparison can fail. The initial unstaged release
2951
+ clears overrides and waits for stable readings. The final release returns the readings observed
2952
+ before the first stage, kept as explicit emulation. A further unstaged release clears that emulation.
2953
+
2954
+ ```ts
2955
+ import { readPixels, releaseMedia, resolveAccessible, stageMedia } from '@orkestrel/test/browser'
2956
+ import { afterEach } from 'vitest'
2957
+
2958
+ afterEach(releaseMedia)
2959
+ await releaseMedia()
2960
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches
2961
+ const button = resolveAccessible('Media')
2962
+ await stageMedia({ motion: true })
2963
+ readPixels(button, 'padding-top') // 1
2964
+ await stageMedia({ motion: false })
2965
+ readPixels(button, 'padding-top') // 2
2966
+ matchMedia('(prefers-reduced-motion: reduce)').matches // true
2967
+ await stageMedia({ motion: reduced })
2968
+ matchMedia('(prefers-reduced-motion: reduce)').matches === !reduced // true
2969
+ readPixels(button, 'padding-top') === (reduced ? 1 : 2) // true
2970
+ await stageMedia({ print: true })
2971
+ readPixels(button, 'padding-top') // 3
2972
+ matchMedia('print').matches // true
2973
+ await releaseMedia()
2974
+ matchMedia('print').matches // false
2975
+ matchMedia('(prefers-reduced-motion: reduce)').matches === reduced // true
2976
+ readPixels(button, 'padding-top') === (reduced ? 2 : 1) // true
2977
+ ```
2978
+
2718
2979
  ### Send a key to what holds focus
2719
2980
 
2720
2981
  Bring focus about through a verb, then send the sequence. A key sent while the document body holds
@@ -3210,7 +3471,13 @@ The second pair is what the reading exists for. Every box a document exposes —
3210
3471
  content and the pane, so a caller that has staged too tall a pane reads that pane back and cannot
3211
3472
  descend from it. `measureContent` walks the elements inside the body instead, so it descends. Where
3212
3473
  the document is laid out against the viewport, it moves with the viewport and reports what the
3213
- reflow produced rather than what the pane claimed.
3474
+ reflow produced rather than what the pane claimed, except inside a frame that clips its overflow,
3475
+ where a viewport-bound child ends at the frame's clip edge. The `clipsOverflow` helper names the
3476
+ frames that count, and the `readClipEdge` helper reads each frame's clip edge. An `overflow-y`
3477
+ value of the `hidden` keyword, the `auto` keyword, or the `scroll` keyword ends at the padding box.
3478
+ The `clip` keyword and a paint containment over a
3479
+ `visible` overflow end at the box the frame's `overflow-clip-margin` value selects, the padding box
3480
+ by default, expanded by the margin the `readClipMargin` helper reads.
3214
3481
 
3215
3482
  ### Read a written frame back
3216
3483
 
@@ -3332,11 +3599,16 @@ Each entry names the contracts its file proves. The test names carry the cases.
3332
3599
  that stays there, and `isOutsideViewport` takes a rectangle wholly beyond each edge and one
3333
3600
  straddling an edge. `isReachable` takes a plain control and each condition it drops, a control the
3334
3601
  document no longer holds, a focusable SVG against an element from a foreign namespace, and the
3335
- refused summary that proves it is the one filter the acting verbs apply; `isRendered` takes each
3336
- removal a browser honours and, as the split from `isReachable`, a zero-size announced control. Each
3337
- predicate also takes a subject inside an open and a closed shadow root beside a host that carries
3338
- its own ancestor attribute — `[inert]` for one and `aria-hidden` for the other — and a host the flat
3339
- tree does not lay out, which pins where the boundary falls for each.
3602
+ refused summary that proves it is the one filter the acting verbs apply. It takes the open modal
3603
+ dialog through the readings that fix it: the masthead control the dialog leaves behind against the
3604
+ same name inside it, which resolves and traverses unambiguously; the control a plain dialog, a
3605
+ folded modal, and a blanked modal each leave standing, as the control; the nested dialog and the
3606
+ shadow subject that pin containment on the flat tree; and the native `showModal` dialog and the
3607
+ shadow-declared modal it reports nothing about, which is the bound the guide states. `isRendered`
3608
+ takes each removal a browser honours and, as the split from `isReachable`, a zero-size announced
3609
+ control. Each predicate also takes a subject inside an open and a closed shadow root beside a host
3610
+ that carries its own ancestor attribute — `[inert]` for one and `aria-hidden` for the other — and a
3611
+ host the flat tree does not lay out, which pins where the boundary falls for each.
3340
3612
  `pressKeys` takes a sequence reaching the control a traversal focused and, as the control, the same
3341
3613
  sequence refused while the document body holds focus with no keystroke recorded. `waitForState`
3342
3614
  takes a state a timer flips after the act, a node replaced mid-wait and still resolved by role and
@@ -3434,10 +3706,32 @@ Each entry names the contracts its file proves. The test names carry the cases.
3434
3706
  read — 1322, then 1561 — and its frame lands on 1800, which is the fixed point written out rather
3435
3707
  than read back from the capture that staged it. The same panel uncapped grows with every pane and
3436
3708
  reaches the refusal, whose written-out restaging bound reddens when the source's bound moves and
3437
- whose pane and viewport are handed back anyway. `readFrame` takes a written frame's size and
3438
- floor, read a second way through the cascade's own answer for the same canvas, a bottom row split
3439
- between two colors reported as no floor at all, a path holding no file, and a file holding no
3440
- image. `readCascade` takes class tokens collected from plain and grouped rules and only real ones;
3709
+ whose pane and viewport are handed back anyway. An element frame takes a fixed `30vh` panel whose
3710
+ top lies past the runner's window, shot from a scrolled tester, whose frame is 30% of the declared
3711
+ height on the panel's own color with the scroll handed back; an element below both the pane and
3712
+ the window, whole on the document's floor with the scroll handed back; a `50vh` element and a
3713
+ `30vh` element below the fold, at the declared pane rather than a grown one; an element taller
3714
+ than the pane, in a pane of its own height; an element that outgrows every pane, refused with the
3715
+ pane and the scroll handed back; a second tester frame the offset leaves in place; and a frame
3716
+ carrying no `style` attribute, handed back without one. After the `releasePointer` function, an
3717
+ element frame takes no `mouseover` event for an element inside the window, which also takes no `scroll` event;
3718
+ for a flush-left element the scroll brings to the top, one the offset brings to the left edge, and
3719
+ one too large for the window; for an element past both window edges, whose frame has the
3720
+ element's size; for an element touching the origin and one filling the window; across a scroll
3721
+ the staging clamps, with the scroll handed back; and for an `svg` element in the shadow tree of a
3722
+ fixed host and a fixed `svg` element under a containing-block ancestor. A hover placed after
3723
+ staging on an element at the tester's top-left corner stays in the frame. The `computeOffset`
3724
+ function takes an element inside the window, one touching the origin or starting above it, one
3725
+ filling the window, an element past the bottom edge, a fractional bottom edge rounded up, a
3726
+ fractional top and a fractional left each moved only as far as the window start, a box already
3727
+ ending inside a fractional window left where it is, an element past the right edge,
3728
+ one past both edges, and an element too large for the window.
3729
+ The `readFrame` function takes a written
3730
+ frame's size and floor, read a second way through the cascade's own answer for the same canvas, a
3731
+ bottom row split between two colors reported as no floor at all, a path holding no file,
3732
+ a file holding no image and refused without a size, and a PNG header over no image data, refused
3733
+ with the size the header declares. The `readCascade` function takes class tokens collected from
3734
+ plain and grouped rules and only real ones;
3441
3735
  `readRows` takes a row joined from its own text nodes rather than from run-together content, and
3442
3736
  an empty list; `extractOrphans` takes a child class rendered outside its container with a nested
3443
3737
  one left alone, nothing reported when every child sits inside one, and, as the control, an element