crowsnest v3: the headless DOM oracle
a Bombadil-style state contract for the read boundary; render-conformance without snapshots
Table of Contents
- 1. 1. Scope and the role this adds
- 2. 2. The Bombadil framing
- 3. 3. Discovery (http + hateoas)
- 4. 4. The reduction contract (Bombadil's fold)
- 5. 5. The projection contract (the declared lossy map)
- 6. 6. The semantic DOM contract (what makes the UI observable)
- 7. 7. Harness mechanics
- 8. 8. Security non-requirements (made explicit, with reasons)
- 9. 9. Conformance fixtures (oracle)
- 10. 10. Refutation conditions
- 11. 11. Generalization: from "on my box" to ITIL / system issues
- 12. 12. Review queue (annotation-driven)
1. 1. Scope and the role this adds
v2.1 §11.2's real acceptance test is a human action: open the page, look, confirm amber is not green. That is the render boundary, and it is the only one with no machine check, because the dashboard is an uncontracted UI: it declares no state model, so the only available tooling is snapshot/visual-regression, which asserts "matches the recording," not "satisfies an invariant."
v3 specifies the oracle: a headless process that consumes the read surface, folds the event stream into a canonical state from this spec, and asserts the live DOM is a faithful projection of that state. It is read-only. It never ingests, never binds, never touches the receiver's internals. It converts the render boundary from human-in-the-loop to a sandboxed pass/fail.
| role | normative section | source of truth |
|---|---|---|
| client / receiver / dashboard | v2.1 §5 / §6 / §7 | v2.1 |
| oracle | this document | this document + the v2.1 read contract |
2. 2. The Bombadil framing
Tom Bombadil is the figure outside the system's power: the Ring does not act on him because he is not part of the order it governs. The oracle is Bombadil. It computes the canonical state from the event stream and this spec's reducer, never from the dashboard's code, so a buggy dashboard cannot corrupt the reference it is judged against. The dashboard is inside the system under test; the oracle stands outside it.
The render-conformance predicate, stated once:
observe(DOM) ≡ project( fold( stream ) )
foldis the reference reducer (§4): event stream → canonical state. Spec-derived, total.projectis the declared lossy map (§5): canonical state → expected semantic DOM.observeextracts the actual semantic DOM (§6) from the live page.≡is observational equality over the semantic surface (§6), not pixels.
Everything interesting is in the totality of fold over hostile input and the
exactness of project. If both are honored the predicate is dull, which is the point:
dull is what "verified" should feel like.
3. 3. Discovery (http + hateoas)
The oracle MUST NOT hard-code read paths. It MUST start at GET / (v2.1 §6.2),
read _links, and use rel = info, snapshot, stream to resolve the probe,
the snapshot read, and the live feed. A receiver that moves a path but keeps the
index stays oracle-compatible. The oracle MUST treat a missing _links entry it
needs as a discovery failure, distinct from a render failure.
4. 4. The reduction contract (Bombadil's fold)
4.1. 4.1 Signature and totality
fold : Event* -> State reduce : State -> Event -> State ; left fold, deterministic, pure
reduce MUST be total over every value the read surface can carry, including values
a conformant receiver would have rejected. The oracle may be pointed at a
non-conformant receiver under test, so "the input can't happen" is not available;
the reducer must define the dashboard's correct behavior for it. This is where the
read-side mirrors the receiver's write-side hardening (v2.1 §6.7).
4.2. 4.2 Canonical state
State = {
receiver : { present:bool, version:string|nil, compatible:bool }, ; from the /info probe
mode : live | demo | version-mismatch | access-denied, ; §4.4
log : Sighting[], ; newest-first, cap N=100
counts : { ok:int, error:int, unknown:int }, ; derived from log
poisoned : bool ; §4.3
}
4.3. 4.3 Event kinds and their reductions
Sighting e: normalize e exactly as the receiver does (v2.1 §6.3):statusnot in{ok,error}coerces tounknown, neverok; fillid=/=name=/=service. Prepend, truncate to N. The coercion is a state invariant the oracle owns, so it is checkable on the render side, not just trusted on the receiver side.Probe r(the/inforesponse): setreceiver, derivecompatible= same MAJOR (v2.1 §10.2), setmode=liveorversion-mismatch.ProbeFail:mode=demounless the failure is attributable to a denied local-network grant, in which caseaccess-denied(§4.4, §7 NR-LNA). The oracle can force this distinction because it controls the grant in the harness, which a deployed client cannot.Poison p: a numeric field reaches the wire non-finite (NaN=/=Infinity), which makes the browser's strictr.json()reject the whole document (v2.1 §6.7.3).reducesetspoisoned :true=;projectof a poisoned state is the blank log, fleet-wide. This is the load-bearing coupling: a single bad number is a global render outcome, and the oracle derives it rather than discovering it by luck. Snapshot tests miss exactly this.
4.4. 4.4 State invariants (asserted on every reduced state)
I-ORDER log is newest-first by arrival
I-CAP |log| <= 100
I-ENUM every log[i].status in {ok, error, unknown}
I-COERCE no log[i].status was silently promoted to ok
I-MODE mode in {live, demo, version-mismatch, access-denied} and is single-valued
I-POISON poisoned => project(State) renders an empty log and a poisoned indicator
5. 5. The projection contract (the declared lossy map)
project is the exact set of transforms the DOM applies. The oracle computes them and
asserts the DOM matches. These are the dashboard's render rules promoted to a contract.
| state field | projected to | transform |
|---|---|---|
id |
id cell | first 9 chars |
duration (ms) |
duration cell | ms → ms/s/m human form (§ matches v2.1 §7) |
status |
row status class | ok→green, error→red, unknown→amber/grey |
attrs.host |
origin column | short hostname verbatim |
attrs.sha |
sha cell/tooltip | verbatim |
mode=demo |
demo badge | visible and persistent across ticks |
mode=version-mismatch |
version badge | shows receiver + needed MAJOR; NOT demo |
mode=access-denied |
access hint | "local network access denied; re-enable in site settings" |
live ∧ empty log |
empty state | explicit "0 sightings", NOT seed rows |
poisoned |
poisoned indicator | empty log + visible cause |
Render invariants, the ones worth a fixture each:
P-COLOR for all s != ok : color(s) != green ; the visible end of I-COERCE
P-BADGE mode=demo => badge present on EVERY tick, not just first paint
P-EMPTY live ∧ empty => empty state, never synthetic rows ; provenance, not "never blank"
P-LOSSY the DOM shows ONLY projected forms; the oracle compares against project(state),
so an over-faithful DOM (e.g. full id) is a conformance miss as much as a wrong one
6. 6. The semantic DOM contract (what makes the UI observable)
This is the minimal addition that converts the dashboard from uncontracted to
contracted. To be oracle-checkable, the dashboard MUST expose a stable semantic
surface, so observe reads structure, not pixels and not brittle text position.
- one element per rendered sighting carrying:
data-cn-id, data-cn-name, data-cn-service, data-cn-duration-ms,
data-cn-status (raw enum: ok|error|unknown), data-cn-origin, data-cn-sha
- the status COLOR is a class the oracle can map back to {green,red,amber}
(so P-COLOR is checkable without sampling pixels)
- a root element carrying data-cn-mode = live|demo|version-mismatch|access-denied
- an empty-state element with data-cn-empty when applicable
- a poisoned-state element with data-cn-poisoned when applicable
observe reads these into the same State shape fold produces, and the predicate is a
structural diff. Without this surface the oracle degrades to text scraping, which
reintroduces the snapshot brittleness v3 exists to remove. The semantic DOM contract
IS the UI's state contract: it is the declared projection surface, the thing a normal
dashboard never publishes.
7. 7. Harness mechanics
digraph harness {
rankdir=LR;
bgcolor=white;
graph [fontname="Helvetica", fontsize=11, pad=0.3, nodesep=0.45, ranksep=0.55];
node [fontname="Helvetica", fontsize=10, style="rounded,filled", shape=box, penwidth=1.2];
edge [fontname="Helvetica", fontsize=9, color="#888888", fontcolor="#555555"];
// Fixture stream -- blue family (Tailwind 50/100/700)
S [label="scripted stream\nfixture sequence",
fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"];
// Receiver + browser -- amber family
RCV [label="controlled receiver\n(reference or fixture-driven)",
fillcolor="#fef3c7", color="#b45309", fontcolor="#b45309"];
PG [label="headless browser\nreal dashboard bundle",
fillcolor="#fef3c7", color="#b45309", fontcolor="#b45309"];
// Oracle -- violet family
ORC [label="oracle: fold + project",
fillcolor="#ede9fe", color="#6d28d9", fontcolor="#6d28d9"];
// Compare + report -- green family
CMP [label="≡ ?", shape=diamond,
fillcolor="#dcfce7", color="#15803d", fontcolor="#15803d"];
R [label="conformance report\nper-invariant pass/fail",
fillcolor="#dcfce7", color="#15803d", fontcolor="#15803d"];
S -> RCV;
RCV -> PG [label="read surface"];
S -> ORC;
PG -> CMP [label="observe semantic DOM"];
ORC -> CMP;
CMP -> R;
}
- Headless browser. Playwright or equivalent loading the real production bundle.
- Controlled receiver. The oracle drives a receiver it scripts (the reference receiver in a fixture mode, or a fixture player), so the stream is deterministic.
- Local-network grant. The harness MUST pre-grant local-network access for the
dashboard origin so the loopback probe is not blocked headlessly
(Playwright context permission grant / CDP / launch policy). The oracle is also the
one place
access-deniedis testable: run once granted (expectlive), once denied (expectaccess-denied, NOTdemo). A deployed client cannot script this. - Settling. The dashboard is poll-or-stream (v2.1 §7; the shipped bundle polls). The oracle MUST settle on the read mode it observes: for poll, await the next interval; for SSE, assert prepend-on-event. Conformance is judged on the settled DOM.
- Diff. Structural, over the semantic surface, against
project(fold(stream)).
// scaffold; contract-bearing parts concrete, scripting marked TODO.
import { chromium } from "playwright";
const DASH = "https://wal.sh/tools/crowsnest/";
const ORIGIN = "https://wal.sh";
export async function runOracle(stream /* Event[] */, opts = { grant: true }) {
const browser = await chromium.launch();
const ctx = await browser.newContext();
// LNA: pre-grant (or withhold) the loopback permission for the origin.
if (opts.grant) await ctx.grantPermissions(["local-network-access"], { origin: ORIGIN });
const page = await ctx.newPage();
// TODO(builder): start a controlled receiver, replay `stream` into it.
await page.goto(DASH);
await settle(page); // TODO: await poll tick or SSE drain
const observed = await observe(page); // read data-cn-* into State shape
const expected = project(fold(stream)); // Bombadil reference, spec-derived
const report = diff(expected, observed, INVARIANTS); // per-invariant pass/fail
await browser.close();
return report;
}
// fold / project / observe / diff: §4 §5 §6. INVARIANTS: I-* and P-*.
8. 8. Security non-requirements (made explicit, with reasons)
These are stated as non-requirements, not omitted, because the trust domain is the local box. Each carries the condition under which it would become a requirement (§10).
NR-AUTH the read surface is unauthenticated by loopback trust; the oracle models no
identity, session, or credential. Becomes a requirement off-box (§10).
NR-CONF no confidentiality on-box: any local process can GET the log. The oracle does
not test access control because there is none by design.
NR-INTEG no provenance on sightings: any local process can POST. The oracle tests
render faithfulness, not authenticity of the data rendered.
NR-XSS F1 (a same-origin script can read the whole log) is an acknowledged residual
of the v2.1 boundary audit. Out of oracle scope: the oracle judges whether the
DOM faithfully projects state, not whether the page is XSS-free. (If you want
the XSS-inert-render check, that is a separate dashboard fixture, not the oracle.)
NR-LNA the only browser-plane gate the oracle handles is the local-network grant and
connect-src; both are loopback-scoped and pre-granted in the harness.
The honest framing: these are non-requirements because nothing crosses the machine. Section 10 is the same oracle with the box removed, where every NR above except NR-XSS inverts into a requirement.
9. 9. Conformance fixtures (oracle)
The oracle passes iff every fixture's observe ≡ project(fold) holds. The two marked
[hole] are the regions v2.1 §11.2 structurally could not reach, because §11.2 requires
a live receiver and so never exercises receiver-absent or denied paths.
O1 minimal one ok sighting -> one green row, projected forms O2 coercion status="warning" -> amber row (P-COLOR), never green O3 order three out-of-order arrivals -> newest-first (I-ORDER) O4 cap 120 sightings -> exactly 100 rendered (I-CAP) O5 poison one duration=NaN in stream -> empty log + poisoned indicator (I-POISON) O6 host attrs.host=nexus -> origin column = nexus O7 demo [hole] no receiver -> demo badge, persists across >=3 ticks (P-BADGE) O8 empty live, empty log -> explicit empty state, no seeds (P-EMPTY) O9 vmismatch receiver MAJOR != dashboard -> version badge, not demo O10 denied [hole] receiver up, grant withheld -> access-denied hint, not demo (NR-LNA)
10. 10. Refutation conditions
- A stream whose fold/project says row R should render, and the settled DOM lacks R. - The DOM renders a status as green that fold marked unknown (P-COLOR / I-COERCE). - A demo session whose badge is present at first paint and absent after a later tick. - A poisoned stream that renders any rows instead of the empty+poisoned state. - The oracle cannot distinguish access-denied from demo even with grant control (then §4.4's four-state model is unobservable and collapses to three; spec must say so). - The dashboard exposes no stable semantic surface (§6), forcing text scraping (then v3 has not actually contracted the UI and the render boundary is still open).
11. 11. Generalization: from "on my box" to ITIL / system issues
The sighting is a degenerate case of a general shape: an observation of a monitored entity's state over time. The oracle pattern, fold an event stream into canonical state, project to a view, assert the rendered view, is indifferent to whether the entity is a local agent operation or a production service. The Prisma model you pasted is that general shape already.
11.1. 11.1 The mapping
crowsnest monitoring / ITIL (the Prisma model)
--------- ------------------------------------
Sighting (one event) <-> Check (one probe result: status, latency, message, ts)
the log (events held) <-> Monitor.history (Check[])
status {ok,error,unknown} <-> Check.status, and Monitor.status {UP,DOWN,DEGRADED,PENDING}
the monitored thing <-> Monitor (the entity) -- crowsnest has no entity, only events
11.2. 11.2 The one structural difference that changes the reducer
A sighting's status is terminal: the operation already happened, ok or error, and the
log is append-only. A Monitor's status is standing: UP=/=DOWN=/=DEGRADED=/=PENDING is
the entity's present condition, derived by folding its Check history, and it changes
without a new entity appearing. So fold gains an entity dimension:
fold : Check* -> { byMonitor : Map<MonitorId, {
status : UP|DOWN|DEGRADED|PENDING, ; latest, with debounce/flap rules
latency : int|nil, ; latest or rolling
uptime : float, ; over a retention window
sla : { window, breaches } ; derived
}> }
Two consequences that the crowsnest reducer never had to face, and that the spec must now pin so two oracle authors agree:
- Standing-state derivation rules. DOWN-after-N-failures (debounce), DEGRADED thresholds, PENDING vs first-check. These are exactly the kind of normalization rule that, left as SHOULD, makes independent dashboards diverge. They become the new I-COERCE-class invariants: the projected color must follow the spec's standing-state function, not the implementation's.
- Windowing replaces the cap. The cap-100 ring is fine for ephemeral on-box visibility
(v2.1 calls crowsnest opportunistic, not an audit log). Monitoring needs retention and
SLA windows, so I-CAP becomes I-WINDOW, and
uptime=/=slaare folded aggregates the oracle must compute and assert, not just rows it must order.
11.3. 11.3 The inversion: security non-requirements become requirements
This is the interesting part, and it is the whole reason the on-box version felt easy. For crowsnest every NR in §8 (except NR-XSS) holds because nothing leaves the box. An ITIL/system-issue event source is a shared, networked, multi-tenant service (your changeflow ITIL surface is exactly this), so:
NR-AUTH -> REQUIRED. The view is per-viewer; the receiver is no longer loopback-trusted. NR-CONF -> REQUIRED. Incidents/CIs are not world-readable; confidentiality is now real. NR-INTEG -> REQUIRED. Observations come from authenticated sources; provenance matters.
And the render-conformance predicate gains a clause. The projection is no longer a pure function of state; it is a function of state and the viewer's authorization:
on-box : observe(DOM) ≡ project( fold(stream) ) off-box: observe(DOM) ≡ project( fold(stream), authz(viewer) )
So the oracle acquires a new and load-bearing obligation it never had on-box: assert
that the DOM renders no entity the viewer is not entitled to, i.e. that project is a
faithful restriction under authz, with no leakage. That is a strictly harder oracle,
because a false negative (showing an entitled incident) and a false positive (leaking an
unentitled one) are now distinct failures, and the second is a security finding, not a
render bug. Bombadil must now fold authorization into the canonical state, and the
fixture set gains an O-LEAK class: a viewer with restricted scope, a stream containing
out-of-scope entities, and the assertion that none of them reach the DOM.
11.4. 11.4 What carries over unchanged
The spine survives the generalization intact: discovery via the service index, a total spec-derived reducer as the fixed point, an explicit lossy projection, a semantic DOM contract that makes the view observable, and a headless harness that diffs observed against projected. The crowsnest case is the version where the authz clause is identity (everything is in scope because the box is the boundary). The ITIL case is the version where the authz clause is the point. Same oracle, one added input, and the security non-requirements you could leave implicit on-box become the requirements you cannot.
12. 12. Review queue (annotation-driven)
The :REVIEW: drawers above register this draft into wal-sh.site.annotations.
To inspect open reviews from the REPL:
(require '[wal-sh.site.annotations :as ann])
(def d (ann/scan-site "site/tools/crowsnest")) ; scan this spec's directory
(def v3 (filter #(re-find #"spec-v3" (:file %)) d))
(ann/needs-review v3) ; reviewers see this list
;; clear an item by adding to its drawer: :VERIFIED_AT:, :VERIFIED_BY:, :VERDICT:
;; per-verdict filters:
;; (ann/by-verdict v3 "correct") / "corrected" / "disputed" / "needs-citation"