# Dump format
The contract between the oracle service, the comparator, and eventually the engine under
test. Frozen: changing it requires changing every consumer at once. What the comparison
means, explained from zero, is in `docs/geometry-verification.md`.
```mermaid
flowchart LR
req["request line (stdin, JSON)"] --> op{"op"}
op -- "none: geometry" --> dump["Dump per engine rects, style, scroll"]
op -- "trace" --> trace["Trace per engine recorded JS observations"]
op -- "reflect" --> refl["Reflection dump attribute + IDL values"]
op -- "stackingProbe" --> stack["ids at points, front to back"]
dump --> vote["compare.rs vote 1/60 px tolerance"]
trace --> tvote["trace::vote"]
refl --> rvote["reflect::vote exact"]
stack --> svote["vote_stacking exact, unanimous only"]
```
## Oracle protocol
The oracle is a long-lived process speaking newline-delimited JSON on stdio. One JSON
object per line in each direction, no pretty-printing, no interleaved text on stdout.
Diagnostics go to stderr.
Browsers stay open for the process lifetime. Nothing spawns a browser per case.
### Request
```json
{"id": "case-0001", "html": "...", "engines": ["chromium", "firefox", "webkit"]}
```
`engines` is optional; omitted means every engine the service launched. `script` is
optional JavaScript run after `html` loads and before the dump walk
(`docs/feature-cost-experiment.md`), so DOM tree-mutation ground truth is an ordinary
dump.
```json
{"id": "case-0002", "html": "
", "script": "document.getElementById('a').appendChild(document.createElement('b'));"}
```
### Response
```json
{"id": "case-0001", "dumps": {"chromium": {...}, "firefox": {...}}, "errors": {"webkit": "message"}}
```
`dumps` holds one dump per engine that succeeded. `errors` holds a message per engine
that failed, and is omitted when empty. A response always carries the `id` it answers.
### Control
`{"id": "...", "op": "ping"}` responds
`{"id": "...", "ok": true, "engines": [...], "versions": {"chromium": "140.0.7339.5", ...}, "profile": "default"}`.
`versions` maps each launched engine's name to its `engine_version` (see below); `profile`
is the launch profile active for the process (see "Launch profiles" below).
`{"id": "...", "op": "shutdown"}` closes browsers and exits.
## Dump
```json
{
"engine": "chromium",
"engine_version": "140.0.7339.5",
"viewport": {"width": 800, "height": 600},
"overflowed": false,
"nodes": [
{
"index": 0,
"parent": -1,
"tag": "HTML",
"depth": 0,
"bounding": {"x": 0.0, "y": 0.0, "w": 800.0, "h": 100.0},
"rects": [{"x": 0.0, "y": 0.0, "w": 800.0, "h": 100.0}],
"scroll": {"client_width": 800.0, "client_height": 100.0, "scroll_width": 800.0, "scroll_height": 100.0},
"style": {"display": "block", "...": "..."}
}
]
}
```
- `engine_version` is the exact browser build that produced the dump, from Playwright's
`browser.version()` at launch — not from `navigator.userAgent`, which the in-page walk
could report but which is imprecise. Required, since ground truth without a version is
exactly what this field exists to rule out.
- `nodes` is every element in document order, starting at `` at index 0. Document
order means `index` increases monotonically and a parent always precedes its children.
- `parent` is an index into `nodes`, or `-1` for the root.
- `tag` is the uppercase tag name as the DOM reports it.
- `rects` is `getClientRects()`, one entry per box fragment, in content order. An empty
array means the element generates no box — that is the signal, not a null `bounding`.
- `bounding` is `getBoundingClientRect()`, always present, zeroed when `rects` is empty.
- Coordinates are viewport-relative CSS pixels as doubles, serialized at full precision.
- `overflowed` is true when the document scrolls in either axis. It is a report, not a
verdict: trustworthiness is decided per node from `scroll` (see "Scrollbar probe"
below), because one document-level boolean from one engine cannot say whether the
engines agree.
## Scrollbar probe
Additive: everything above (request, response, the rest of the dump, style subset) is
unchanged. `scroll` is a new optional per-node object, alongside `bounding` and `rects`,
carrying the element's client and scroll dimensions as CSSOM-View reports them
(`clientWidth`/`clientHeight`, `scrollWidth`/`scrollHeight`).
```json
"scroll": {"client_width": 200.0, "client_height": 100.0, "scroll_width": 400.0, "scroll_height": 100.0}
```
- The two differ on an axis exactly when the element's content spills past its padding
box on that axis — here, horizontally. This holds whatever the element's own `overflow`
value is: a `visible` box whose content spills reports differing dimensions too, as
Chromium, Firefox and WebKit all do. What `overflow` changes is the other direction: a
box that clips (`hidden`, `scroll` or `auto`) keeps its own spill out of its ancestors'
scroll dimensions.
- The signal is therefore "this element's content overflows it", not "a scrollbar is
painted": a rendered scrollbar follows from it for `scroll` and `auto`, a `hidden` box
clips instead, and a `visible` box simply spills. The dump cannot tell the three apart,
and deliberately does not try — the three engines agree on the overflow and would not
agree on a painted widget's width.
- The root element's client box is the viewport's, so the root reports overflow whenever
the page is larger than the viewport, whatever its own `overflow` value. That is the
condition the whole-document `overflowed` boolean reports on its own.
- Omitted from dumps taken before the probe existed, and from an engine under test with
no scrolling model. Consumers treat an absent `scroll` as unknown, never as "no
scrollbar" — except when scoring an engine under test against the browsers, where an
absent probe is exactly the engine reporting no scrollbar.
- The pixel width a scrollbar steals is platform- and theme-dependent and is deliberately
not part of this contract. Only presence is.
Comparison uses it as the discard rule: a case is untrustworthy when three or more
engines disagree about whether some node overflows on some axis, clustered by the same
pairwise-agreement rule as the geometry vote (`harness::compare::scrollbar_dispute`).
Unanimous agreement that a scrollbar is present does not discard the case — it is a case
an engine that does not model `overflow` is expected to fail.
## Style subset
A fixed, ordered list. Present on every node, values exactly as `getComputedStyle`
returns them.
```
display position box-sizing direction writing-mode float clear
width height min-width max-width min-height max-height
margin-top margin-right margin-bottom margin-left
padding-top padding-right padding-bottom padding-left
border-top-width border-right-width border-bottom-width border-left-width
overflow-x overflow-y
```
A mismatch here indicts the style pipeline rather than layout, which is the second
attribution axis.
## Comparison
Comparison never uses absolute coordinates.
1. **Shape.** The sequence of `(tag, depth, parent)` must match. A mismatch is a parse
divergence and the case reports that and stops — no geometry is compared.
2. **Size.** Each node's `bounding` width and height, and each fragment's width and
height pairwise.
3. **Relative offset.** Each node's `bounding` origin minus its parent's `bounding`
origin. The root compares against the viewport origin. A single wrong offset then
appears once rather than corrupting every descendant.
Tolerance is 1/60 px (0.01667), the coarsest engine quantization — Gecko app units.
Blink and WebKit quantize at 1/64. Differences below tolerance are quantization noise.
Both an exact verdict (within tolerance or not) and the raw signed delta are reported.
The verdict gates pass/fail; the delta is the dense reward signal and needs no threshold.
## Trace protocol
Additive: everything above (request, response, dump, style subset, comparison) is
unchanged. `trace` is a new request operation with its own response shape, for what
script can observe beyond geometry — return values, thrown errors, event order, promise
settlement order, object structure, pixel and audio buffers — the oracle for
`docs/oracle-coverage.md` → "Four kinds of oracle that need no geometry vote" item 4.
### Origin
`Engine.run()` (the geometry path) uses `page.setContent`, which leaves the page on
`about:blank`; many APIs this unlocks need a secure context or a real origin. The oracle
starts one local HTTP server on `127.0.0.1` for its own lifetime and serves trace pages
from `http://localhost:/trace/`. Browsers treat `localhost` as a secure
context regardless of scheme, so `isSecureContext` is `true` without TLS. Every trace
records the page's `origin` and `isSecureContext`.
### Request
```json
{"id": "t1", "op": "trace", "script": "trace.value('x', 1); trace.done();", "html": "", "engines": ["chromium", "firefox", "webkit"]}
```
`script` is required: JavaScript text run in the served page, after the recorder (see
below) is installed and after `html` (optional; omitted means an empty body) has
loaded. `engines` is optional, same meaning as the geometry request.
### Response
```json
{"id": "t1", "traces": {"chromium": {"...": "..."}}, "errors": {"webkit": "message"}}
```
`traces` holds one trace per engine that completed; `errors` holds a message per engine
that failed (including "engine not launched"), omitted when empty.
### Trace
```json
{
"engine": "chromium",
"engine_version": "153.0.8010.12",
"profile": "default",
"origin": "http://localhost:41673",
"is_secure_context": true,
"timed_out": false,
"records": [
{"seq": 0, "kind": "value", "label": "x", "value": 1}
]
}
```
- `profile` is the launch profile active for every engine in this run (see "Launch
profiles" below).
- `timed_out` is true when the recorder's fixed completion timeout ended the trace
rather than the script itself calling `trace.done()`.
- `records` is every observation the recorder pushed, in the order it pushed them; `seq`
is that order as an explicit integer. Order is part of the result: two traces with the
same records in a different order are not the same trace.
### The recorder
A script installed via Playwright's `context.addInitScript`, so it runs before any
script the trace page's own content carries — including inline `