CPPB-stratified, three ports plus three optional capabilities. Dependencies point down; effects live only at the boundary.
BOUNDARY addon/handlers · watch/supervisor · boundary ← ports injected as arguments
PIPELINE plan (scenario + manifest → run-plan)
watch (build event + policy → decisions)
PROMOTE verdict (raw payload → verdict / report)
step (authored datum → op, OCP rule chain)
COLLECT manifest (raw EDN → normalized, defaults resolved)
TYPES schema (malli value objects) · ports · profile (provider behaviour as data)
ADAPTERS shadow/relay → IBuildTool shadow/nrepl → ICljsEval
browser/playwright → IBrowserDriver
A vendor is named only in an adapter namespace. Everything above depends on
hive-cljs.ports.
(defprotocol IBuildTool
(builds [this]) (build-status [this build-id]) (compile-once! [this build-id])
(subscribe! [this k f]) (unsubscribe! [this k]))
(defprotocol IBrowserDriver
(open-session! [this opts]) (perform! [this session op]) (close-session! [this session]))
(defprotocol ICljsEval
(eval-cljs [this build-id form-str]) (runtime-available? [this build-id]))
ICljsEval is what makes this more than a Playwright wrapper — it lets one
scenario assert on the DOM and on live re-frame state.
Segregated rather than folded into the ports above: an adapter that implements none still works, and third-party implementations of the three ports keep compiling.
(defprotocol IPageMarker ; browser side
(mark-session! [this session token]))
(defprotocol IRuntimeAffinity ; runtime side — mutates
(bind-runtime! [this build-id token]) (unbind-runtime! [this]))
(defprotocol IRuntimeInventory ; runtime side — observes
(connected-runtimes [this build-id]) (pinned-runtime [this]))
IRuntimeInventory is what doctor reports under :runtimes. It is kept apart
from IRuntimeAffinity because observing is not binding: an adapter may be able
to say what is attached without being able to pin anything, and adding a method
to the shipped affinity protocol would break every implementation of it.
The first two answer which page am I asserting about. The driver stamps every document its session loads with a token; the eval channel finds the connected runtime carrying that token and pins evaluation to it. Without this the CLJS REPL answers from whatever runtime the toolchain happens to pick, so any other open tab silently decides every state assertion — a whole class of tests that grade the wrong page while looking green.
Two constraints shape the wiring in boundary/run-plan!:
:goto, which
discards anything set on the previous document — so the mark is installed as an
init script on the browser context, not evaluated once.:goto loads it. So
the bind is lazy — once per run, immediately before the first runtime step — and
released in a finally.boundary/affinity-possible? gates the whole thing on satisfies?, so this is a
capability, not a requirement.
schema is the single source: it drives m/=> contracts on the pure fns AND
the property/mutation facets synthesized by hive-schemas.test. Tighten a schema
and the tests tighten with it.
profile holds provider behaviour as data — relay op spelling, sync-db table
and attribute names, status vocabulary, browser launch defaults — in a registry
that is the DIP swap point. Swapping a toolchain is a register!, not a code
hunt. Values were read off shadow-cljs's own source, not third-party clients:
every op lives in the shadow.cljs namespace, and build status arrives via the
sync-db rather than a subscribe topic.
step is an ordered IStepRule chain — first match wins, so a new step kind
is an appended rule and an earlier rule can shadow a built-in. See
steps.md.
plan is pure orchestration: it resolves the base URL, compiles steps to ops
and produces a RunPlan as data. No port is touched.
boundary is the only place a plan meets a port, and every collaborator
arrives as an argument ({:build-tool … :driver … :cljs-eval …}).
watch decides; watch/supervisor executes. Debounce is decided purely
from timestamps; only sleeping, subscribing and running live in the supervisor.
Every test injects a stub through the ports — StubBuildTool (with
emit-build! to simulate a compile finishing), StubDriver (recording, with a
fail-on variant), StubCljsEval. No test namespace names a vendor, so the
suite runs with nothing installed:
clojure -M:test # 122 tests, 568 assertions
The stubs also model the absence of the optional capabilities —
driver-without-marking and cljs-eval-without-affinity — so the degradation
path is covered rather than assumed.
A stub can only discharge a contract it actually mirrors. The runtime-affinity
work was additionally verified against real ports by an A/B on a live app with a
decoy browser open: pinned passed, and the same run with IPageMarker reified
away failed on the decoy's state. The inventory report was verified the same way
— one runtime and no warning, then a second browser opened on the same build and
the :runtime/ambiguous warning appeared with both user-agents named. A stub
holds one runtime by construction and cannot express either hazard; where that is
true, the real-port check is the evidence.
Pure layers additionally carry schema-synthesized property + mutation facets via
hive-schemas.test/deftrifecta-from-schema — no hand-written generators.
One caveat worth repeating: deftrifecta-predicate fits only a predicate that IS
its schema. A state check like build-ok? ignores most keys, so schema
corruption cannot flip it; use deftrifecta-from-schema with :out :boolean and
a :rel restating the decision.
| Want to | Do |
|---|---|
| add a step kind | append an IStepRule (+ a perform-op defmethod for browser kinds) |
| support another build tool | implement IBuildTool, register a relay profile |
| swap the browser | implement IBrowserDriver (+ IPageMarker to keep runtime pinning) |
| swap the runtime channel | implement ICljsEval (+ IRuntimeAffinity to keep runtime pinning, IRuntimeInventory to keep doctor's runtime report) |
| change what a build event triggers | add a :hive.cljs/watch action and a watch/action->decision case |
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |