<!-- nornir:class: design -->
# dwarves — `GetRunReport` RPC + the `dwarves-embed` live-forge tab
> The last leg of the live-report arc (deferred in `7092669` / `fb0e064`): the demo
> Pipeline tab renders an *in-process* run because the `dwarf.v1` wire only carried the
> parent `RunRow` summary — **no per-step data crossed the network**. This work puts the
> full per-step report on the wire (`GetRunReport`) and adds a thin embed client
> (`dwarves-embed`) that renders a LIVE `dwarves-server` run on the SAME reused viewer.
> Additive (the in-process demo path stays), **no `nornir` crate dep** (that edge is a
> cycle: nornir → dwarves already), and it **reuses** the existing `ForgePipelineAdapter`.

## (1) `RunReport` on the wire — `GetRunReport(run_id)`
`RunStatus(run_id)` returns only the parent `RunRow` (status/counts/duration). To
render a build DAG a client needs each step's status, duration, and — crucially — its
`needs` edges. So `dwarf.proto` (package `dwarf.v1`) gains:
- **`message BuildRow`** — the wire twin of `brokkr::history::BuildRow` (run_id, step,
stage, backend, matrix, status, summary, artifact_id, job_ref, duration_ms,
finished_at) **plus `repeated string needs = 12`**. The `needs` edges live on the run
DAG, not the ledger row, so they ride here too — enough for a client to reconstruct
BOTH the `RunReport` rows AND the build `Dag` from a single reply, with no descriptor
on hand.
- **`message RunReport`** — `RunRow run = 1; repeated BuildRow steps = 2;` (the wire
twin of `brokkr::engine::RunReport`).
- **`rpc GetRunReport (RunStatusRequest) returns (RunReport)`** — reuses the existing
`RunStatusRequest { run_id }` (DRY), so no new request type.
### Server handler (`DwarfSvc::get_run_report`)
1. Find the parent `PipelineRunRow` by id (`store::read_runs`) — absent ⇒ `NotFound`
(parity with `RunStatus`).
2. Read the run's step rows with the new **`store::read_steps(warehouse, run_id)`** (the
per-step twin of `read_runs`; `steps.jsonl` was already persisted by
`append_history`, only a reader was missing — **no new persistence**).
3. Recover the `needs` edges by rebuilding the `Dag` from the still-registered pipeline
(`registry` → `Dag::build`), mapping `step → needs`. If the pipeline is no longer
registered (or the DAG won't build), `needs` default empty — the per-step rows still
cross and the viewer tolerates missing edges (a clean degrade, never an error).
4. Emit `RunReport { run, steps }`, each `BuildRow` carrying its `needs`.
Bearer-gated by the SAME shared `nornir_service::auth_interceptor` as every other RPC.
## (2) `dwarves-embed` — the live-forge `Panel` (backend (2))
A NEW leaf crate, the reusable `<guest>-embed` shape (`nornir-funnel-embed` is the
template):
- **Thin tonic gRPC client.** Compiles ONLY a trimmed `proto/dwarf.proto` (package
`dwarf.v1`, `History` + `GetRunReport`, field numbers matching the server) with
`build_client(true)`/`build_server(false)` via `protox` + `tonic-build` (pure-Rust,
zero-shell). It talks to a live `dwarves-server` WITHOUT linking dwarves' `server`
feature (which would build the whole tonic router + skidbladnir lifecycle).
- **No `nornir` dep, no cycle.** It depends on the `dwarves` crate pulled **lean**
(`default-features = false, features = ["facett-adapter"]`) purely for the domain
types (`Dag`, `DagNode`, `RunReport`, `BuildRow`, `PipelineRunRow`, `RunStatus`) and
the `ForgePipelineAdapter`. `dwarves-embed → dwarves` is a clean one-way edge.
- **`report_from_wire` (pure, unit-testable).** Reconstructs the `(Dag, RunReport)`
pair from a `pb::RunReport`: each wire `BuildRow` → a domain `BuildRow` (status label
parsed leniently; unknown ⇒ `Skipped`, never a false green) **and** a `DagNode`
(backend string → the `&'static` tag, `needs` copied verbatim). The DAG is rebuilt
from the per-step `needs` alone.
- **Render via the REUSED viewer.** `ForgePipelineAdapter::from_report(&dag, &report)`
→ `PipelineView::from_adapter` — the identical path the in-process Pipeline tab uses
(one viewer, many adapters; a live run is *not* a new viewer).
- **The seam.** `fn panel(endpoint) -> Box<dyn Panel>` (token from
`$DWARVES_SERVER_TOKEN`, optional pinned run from `$DWARVES_RUN_ID`; empty ⇒ newest
run via `History(limit=1)`). Auto-poll (1.5s) re-fetches so a run in flight fills in
its steps live. An unreachable server (or a forge with no runs) yields a clean
**"forge unavailable"** state — never a panic. `panel_with(endpoint, token, run_id)`
is the explicit-creds variant.
`dwarves-gui` mounts it as an additional native tab — **Forge (live)** — beside the
in-process **Pipeline** tab (additive; wasm keeps the single Forge panel).
## Why this shape
- **Reuse, never twin (L5).** No new viewer, no new adapter, no hand-rolled auth; the
report is reconstructed onto the existing `ForgePipelineAdapter` + `facett-pipeline`.
- **No cycle.** The thin-proto trick (funnel-embed's) keeps the embed off the `nornir`
crate and off dwarves' heavy `server` feature.
- **Honest.** Per-step status/duration/edges are the SERVER's real historized rows;
unknown statuses degrade to `Skipped`, missing edges to none — never invented green.
## Deferred
- The server recovers `needs` from the live registry; a run whose pipeline was
unregistered renders nodes without edges. Persisting per-run `needs` (or the whole
descriptor) alongside the step rows would make the DAG exact regardless — a follow-up.
- A run-picker in the pane (list runs, choose one) instead of "newest or `$DWARVES_RUN_ID`".