Ask an agent to verify a UI and it will happily screenshot a staging environment and report whatever it saw once. The problem is not the agent’s eyesight; it is that a staging environment is a shared, stateful, half-deployed thing, and “it looked right at 14:32” is not a result you can build a gate on. What the agent actually needs is a repeatable state: a declared world, a fixed scenario, and a deterministic clock, so that a green check means the same thing tomorrow.
Direct (@hraness/direct, an MIT-licensed Hraness package) is the composition layer built around that idea. Its model is small: the product owns a semantic port, the port gets two adapters, and verification runs against the deterministic one.
the port above the protocol
The first move is to define a port in product vocabulary above the provider protocol. A task interface depends on a task repository, not a simulated database client. The product owns request, response, event, and failure meanings; the port sits between interface state and the world outside it.
interface and feature state
|
product-owned port
/ \
production deterministic
adapter adapter
Use the lowest port that preserves the behavior under review. The production adapter talks to the real service. The deterministic adapter answers from the declared world, synchronously, with a logical clock. Nothing about the UI under test knows which side it is mounted on, which is the entire point: the behavior under review is the real interface, reducers, parsing, and navigation.
the world as a seed
A Direct definition (defineDirect, or parseDirectDefinition for a genuinely unknown value) validates the product’s world parser, its named scenarios, a default activation, and each claim’s coverage mode: fixture, mixed, or direct. A scenario selects one world, one route, and one logical-runtime snapshot.
The world is a bounded, strict, versioned JSON document parsed from unknown, with unknown fields rejected. Strictness is what makes it a seed rather than a fixture pile: every activation, reset, and transact passes back through the same parser, so a scenario that ran yesterday is bit-identical today. A measured hot path can opt into transactReplacements for bounded primitive leaf updates, but the replacement rows are themselves parsed, container changes and any growth in aggregate string bytes are rejected, and a semantic validator captured at store construction must approve the exact generation before publication.
Because the world is data, the session can own things staging never could: an immutable seed, a generation-fenced store, a logical clock, an activity ledger, and reverse-order cleanup.
The coverage claim on each catalog entry is part of the contract, not metadata for later. fixture means the deterministic scenarios fully exercise the claim. mixed means the deterministic half is necessary but not sufficient; a named live or direct gate owns the rest. direct means the deterministic composition cannot close the claim at all. Declaring the mode up front is what lets a verification run report “we exercised exactly the evidence this claim accepts” instead of implying a stronger result than the run produced.
the loop in a browser
For a web product, installDirectBrowser({ session }) publishes the contract on window.__direct: the direct.browser-bridge schema at version v2, a driver-neutral session manifest, a live probe, and a reset surface. Installation and rollback are atomic. The manifest carries the query keys, ordered public scenario catalog, active selection, and a coverage snapshot, but no worlds or product actions; agents, Playwright MCP, and other browser tools all read the same value through page evaluation.
The verification loop is then mechanical:
- Read
window.__directsynchronously and requirebridgeSchemato equal thedirect.browser-bridgenamespace at versionv2. Parse manifest and probe fromunknown. - Navigate to the product’s Direct entry with
manifest.queries.scenarioset to a declared scenario. - Reacquire the whole sample after navigation (the document changed) and require
manifest.active.source,manifest.active.scenario, andmanifest.active.routeto equal what you asked for, withmanifest.active.activationHashequal to the probe’sactivationHash. - Wait for quiescence by contract: zero active operations in the current store generation, every product-named pending counter at zero, and the same generation, revision, and counters stable across a bounded settle interval. A single quiet sample proves one quiet sample; re-read and compare.
- Assert route and semantic content, perform the product action, join quiescence again, assert the resulting state.
There is no fixed sleep anywhere in that loop, because logical fixture duration says nothing about when a real browser is ready.
Two bindings keep the loop honest against drift and staleness. parseDefinitionCoverageSnapshot binds manifest.coverage back to the authored definition, so a valid manifest served by a stale page cannot be mistaken for the catalog under review. And after product interactions, the verifier re-reads the bridge atomically and requires the same catalog metadata, catalog hash, full active selection, and probe activation identity: the world you asserted about must be the world you actually drove.
honest coverage
The design’s most useful property is that it refuses to overclaim. classifyCoverageEvidence maps exercised scenarios to a closed vocabulary: verified, fixture-verified, partial, not-exercised, and direct-required. A browser-only deterministic run reports a completed fixture claim as verified, a completed fixture half of a mixed claim as fixture-verified, and a direct claim as direct-required, meaning the claim cannot be closed by this kind of run at all.
The rule underneath: never report a fixture scenario as proof of the adapter, service, host, browser assembly, operating system, or device it replaced. Deterministic evidence covers the product’s own composition. The live system it stands in for needs its own gate.
keeping it out of production
Determinism has a supply-chain edge: fixture code must never ship. Direct keeps this structural rather than conventional. The production entry and the Direct entry are different compositions; the package lives in devDependencies; and emitted production assets are scanned for package, wire-schema, query-key, fixture, bridge-global, and workbench markers. checkBundleBoundary, DIRECT_WIRE_MARKERS, and inspectExactVersionedMarkers from @hraness/direct/tooling/bundle-boundary provide the shared scan, while the product owns the directory, file patterns, and required positive identity evidence.
The docs are careful about what a clean scan means: the scanned files did not contain the configured markers. It does not prove native linkage, runtime loading, or service behavior. An absence-only scan can even pass on an unrelated bundle, so the gate positively requires markers of the intended production entry too.
where this runs
The browser mechanics are deliberately unglamorous. The canonical local policy is one task-owned agent-browser session, one Chromium process, and a sequential batch of at most eight scenarios, with a fresh window new BrowserContext per scenario and an exact --allowed-domains list declared before navigation. Fresh contexts are required because permissions, IndexedDB, Cache Storage, and service workers cannot be reset reliably in place. Tab-close results are retained rather than trusted (agent-browser 0.32.3 can ignore Target.closeTarget errors), the whole-browser close is the disposal boundary, and a nonzero close invalidates the batch. Direct supplies no coordinator, no browser pool, and no performance evidence; the product verifier owns commands, process lifetime, and claims.
Quiescence has its own discipline worth copying. A snapshot is quiet when the current store generation shows zero active operations and every product-named pending counter is zero, and the same state holds across the verifier’s bounded settle interval. Violation counters are excluded from quiescence by design: blocked network calls, unused script steps, leaked subscriptions, and console errors are a separate rejection surface, not something a busy page can hide inside.
In the monorepo behind this site, these verifications run inside the resource scheduler: ordinary browser verification rides the heavy permit lane, fixed-port and machine-global surfaces take the exclusive lane, and concurrent verifiers derive disjoint port blocks from their admitted lane so two agents cannot collide on the same ports.
The transferable pattern fits in one sentence: replace the nondeterministic boundary with a product-owned port, make the test world a strict JSON seed, let the agent drive a real browser against a declared scenario, and report coverage in a vocabulary that cannot silently promote a fixture into a live-systems proof.
sources
- direct: the deterministic composition package; architecture in
docs/architecture.md, the verifier contract indocs/verification.md. - The Direct product page: the public summary of the same model.
- The monorepo behind this site (projects): where the verification batches run inside the resource scheduler.