Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

.nornir/brokkr-design.md

⚒ brokkr — the CI/build-server — design

📍 THIS DOC LIVES IN dwarves (moved from nornir/.nornir/, 2026-07-22, move=delete). The code it documents is dwarves' own (crates/dwarves/src/…), so the doc follows the code. nornir keeps only the read-only viz surface that renders this engine's output. Source links below are dwarves-repo-relative.

Class: design · Function axis: f0 (the whole: pipeline-as-data → run → historize → notify) · f1 the model + descriptor · f2 the plan (DAG + matrix) · f3 the run engine + StepRunner seam · f4 the host wiring (CI_BUILD job, real runner, trigger bridge) · f5 the read surface (5 facets + Tab::Brokkr in nornir viz; the dwarves forge CLI + badge). Strict: this file states only what brokkr is. Sibling how-to: brokkr-guide.md; zero-knowledge walkthrough: brokkr-for-idiots.md.

Named for Brokkr, the dwarf-smith who forged Mjölnir — the one who builds.

f0 — brokkr, the whole (dwarves::brokkr, crates/dwarves/src/brokkr/)

A repo declares its CI/build pipeline as DATA in a brokkr.toml: triggers, an optional build matrix, ordered stages of DAG-ordered steps, cross-project artifact chains, secrets and notify routing. brokkr discovers + validates it, topo-orders the steps into a plan, fans that plan out over the matrix, runs each step through one execution seam (cargo build_variant / a bollard container / a VM boot), historizes every run and step, and notifies on the configured outcomes. It is the twin of nornir-build-thing: pipeline-as-data is to CI what BuildVariantSet is to a single build.

The engine is the deliberately dep-light leaf crate brokkr (serde / petgraph / anyhow / chrono / sha2 only — no iceberg, arrow, eframe, gix or bollard), the dwarves::brokkr module consumed host-side by nornir. Everything heavy lives behind the StepRunner trait in the dwarves host, so a leaf repo's xtask can pull just the model + planner cheaply. Zero shell: runners are cargo/bollard Rust APIs, never sh -c.

Reuse map (what brokkr uses). The leaf mirrors nornir's own engines: the DAG is petgraph::toposort exactly like funnel-core::topo_ready; the descriptor is the twin of BuildVariantSet::discover/validate; the facet Elm cores follow build-thing's elm.rs pattern; history rows are Iceberg-shaped like build_provenance. It is used by the dwarves forge CLI, the host engine (a CI_BUILD nornir-jobs record), the monitor poll loop (the trigger bridge), and the viz (Tab::Brokkr, five facets).

part lives in what it is
f1 model + descriptor model.rs Pipeline model.rs:26; descriptor.rs validate descriptor.rs:77 the serde shape of brokkr.toml + the front-door validator
f2 plan dag.rs Dag::build dag.rs:47; plan.rs PipelinePlan::build plan.rs:50 topo order + cycle detection, then matrix fan-out
f3 engine + seam engine.rs run_pipeline engine.rs:74; runner.rs StepRunner runner.rs:52 walk the DAG through the one execution boundary
f4 host wiring dwarves host run_pipeline_job engine.rs:26, RealRunner runner_real.rs:25 wrap a run as a CI_BUILD job; real cargo backend
f5 read surface 5 facets facets.rs + elm.rs; brokkr_tab.rs; the dwarves forge CLI + badge the Brokkr tab (nornir viz) + the dwarves CLI/badge

f1 — the model + descriptor (pipeline as data)

A brokkr.toml is rooted at [pipeline] (PipelineFile, model.rs:18) — mirroring build-thing's [build] root. A Pipeline (model.rs:26) carries:

PipelineDescriptor (descriptor.rs:18) discovers <repo_root>/brokkr.toml (discover, descriptor.rs:49) and validate() (descriptor.rs:77) is the front door — no job is ever enqueued for a structurally-broken pipeline. It rejects, with a helpful message: an empty name, no steps, a duplicate step name (the DAG id must be unique), a needs edge to an unknown step, a self-needs, an empty container image or argv, a cron trigger with no expression, a webhook/poll trigger that gates on nothing.

f2 — the plan: DAG + matrix fan-out

Dag::build (dag.rs:47) builds a petgraph::DiGraph with a need → step edge and toposorts it; a cycle is a hard error naming the offending node (dag.rs:81) — exactly how funnel-core reports. ready_after(done) (dag.rs:104) is brokkr's topo_ready: given the done-set, the steps whose every need is satisfied — the concurrent scheduler's "what can run now?". roots() (dag.rs:113) is the first independent wave.

PipelinePlan::build (plan.rs:50) fans the DAG out over matrix.combinations(): a rust × target matrix runs every step once per cell, so the host enqueues one job per (cell, step). PlannedStep::label (plan.rs:28) is the stable step[k=v,k=v] id.

f3 — the run engine + the StepRunner seam

run_pipeline (engine.rs:74) walks the topo order for one matrix cell:

It returns a fully-historized RunReport (engine.rs:22): one PipelineRunRow + a BuildRow per step. run_matrix (engine.rs:183) runs run_pipeline once per cell, suffixing the run_id with the cell index.

The StepRunner trait is brokkr's one execution boundary — the dep-light core never links cargo/bollard/qemu. Tests supply a FakeRunner (runner.rs:62) that records order and returns scripted outcomes, so the whole planner/engine is proven without a toolchain.

The leaf-owned side models each own a pure, tested decision the host backs with real state:

leaf decision key fn
queue.rs admit-or-queue-with-a-reason (never a silent stall) admit queue.rs:50, BuildQueue::verdicts queue.rs:101
agent.rs capability/requirement matching AgentPool::pick agent.rs:58, capable agent.rs:64
artifact.rs content-addressed artifacts + dep resolve content_id artifact.rs:15, resolve artifact.rs:56
notify.rs route + bounded-retry deliver should_notify notify.rs:15, deliver_with_retries notify.rs:44
trigger.rs does this event fire this pipeline/step? trigger_fires trigger.rs:28, When::passes trigger.rs:60
badge.rs the status SVG pill svg badge.rs:25
history.rs the historized row shapes PipelineRunRow history.rs:74, BuildRow history.rs:42

f4 — host wiring (nornir-only)

The dwarves host module is the host side the leaf's seams plug into:

f5 — the read surface: 5 facets, one tab, the CLI

The five facett Elm pure cores each hold all observable state in one serializable model, mutate only through explicit methods, and expose a state_json a viz pane + the headless robot harness read:

facet core what it shows
Results elm::Brokkr elm.rs:83 Run buttons + the run list with status chips
Tree TreeFacet facets.rs:54 project → pipeline → stage → step chips
DAG DagFacet facets.rs:118 the stage/step graph, nodes lit by run status
Agents AgentsFacet facets.rs:174 the build-agent pool (busy/idle + current job)
Log LogFacet facets.rs:265 the streaming step-log tail (bounded ring)

The results facet is Elm to the letter: BrokkrMsg::Run(name) (elm.rs:64) returns a BrokkrEffect::RunPipeline (elm.rs:76) the host executes and feeds back as RunFinished; clicking an unknown pipeline is refused with a logged reason.

Tab::Brokkr composes all five behind a sub-view selector (BrokkrView brokkr_tab.rs:39). Pressing Run (run_pipeline, brokkr_tab.rs:235) drives the pipeline through the injected StepRunner and folds the one historized result into every facet at once, assigning + releasing an agent; the DAG renders through facett_graphview::MetroView (metro_map, brokkr_tab.rs:418 — one transit line per stage, stations lit green as steps pass). state_json (brokkr_tab.rs:203) emits one slice per facet, so tests/viz_brokkr_robot.rs drives the whole tab headless.

The forge CLI now lives in the dwarves tool (the nornir brokkr verb was retired 2026-07-18, move=delete). It reads the local brokkr.toml off the filesystem — no server round-trip. Each leaf emits a CommandOutcome (the CLI twin of a facet's state_json), printed as JSON with --json or a human render.

Feature flags

flag scope effect
(none) crates/brokkr default the model + descriptor + DAG + plan + engine + facets, dep-light, always built
testmatrix crates/brokkr lights selftest::emit so descriptor/plan/dag record honest nornir-testmatrix rows; OFF strips the emit to an #[inline] no-op and does not link testmatrix
viz nornir host default compiles Tab::Brokkr (src/viz/brokkr_tab.rs) + its robot test

The CLI, host engine, RealRunner, DemoRunner and JSONL store are always present in the nornir binary (no gate) — brokkr is a first-class subsystem, not an opt-in.