Gunnar

nordisk/dwarves

.nornir/dwarves-embed-live-report-design.md

<!-- 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`.

![GetRunReport + dwarves-embed flow](assets/dwarves-embed-live-report.svg)

## (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`".