Gunnar

nordisk/dwarves

.nornir/build-pipeline-dag.md

<!-- nornir:class: design -->
# dwarves — the build-pipeline DAG (one adapter, the reused viewer)

The dwarves UI shows the forge's **build pipeline as a live DAG** — the same animated,
themed dependency-graph viewer knut renders its Spark/SDP pipelines on. It is **not a
new viewer**. The doctrine (`facett_pipeline::model`) is explicit: *"show another
engine's pipeline = a new **ADAPTER**, not a new viewer."* So dwarves ships exactly one
new thing — a `PipelineAdapter` — and hands it to the existing `facett-pipeline`.

![build-pipeline DAG](assets/build-pipeline-dag.svg)

## The adapter

`dwarves::brokkr::facett_adapter::ForgePipelineAdapter` (crate `dwarves`, feature
`facett-adapter`, off by default) is the ONLY place dwarves touches facett, and facett's
model types never leak into dwarves' own public API — it takes brokkr types in and hands
a `facett_pipeline::PipelineModel` out. It mirrors knut's `SparkPipelineAdapter` one-for-one.

Inputs: a planned `brokkr::dag::Dag` (structure) + the live job-ledger rows
(`brokkr::history::BuildRow`) + the optional parent `PipelineRunRow`. The mapping:

| brokkr / ledger | → | `PipelineModel` |
|---|---|---|
| each step / sub-job (`DagNode`) | → | a `Stage` (id = step name) |
| the run backend (`variant`/`container`/`vm`) | → | the stage `kind` sublabel |
| the step's `needs` | → | a `PipelineEdge` (`need → step`, source → consumer) |
| the ledger `RunStatus` (Passed/Failed/Running/Skipped) | → | `StageStatus` (Done/Failed/Running/**Idle**) |
| a failed step's `summary` | → | the stage `error` |
| **per-step build `duration_ms`** | → | the **edge throughput label** (`"0.8s"`) |
| the run's steps/passed/duration + ledger counts | → | `PipelineMetrics` (the progress bar) |

Two honesty rules, both asserted in tests:
- `Skipped` (never started — upstream failed / gate skipped) maps to **Idle**, never a
  false green; the failure is surfaced on the step that actually failed.
- a CI build processes **no rows and reads no dataset bytes**, so `rows`/`bytes_read` are
  a hard `0` ("not applicable"), never a fabricated stand-in.

A partial (in-flight) ledger is fine: a DAG node with no matching row renders `Idle`
("not yet started"), so the DAG animates a run as it progresses.

## Where it is wired

`dwarves-gui/src/pipeline.rs::pipeline_view()` builds the adapter over the forge's own
build pipeline fixture and returns `PipelineView::from_adapter(&adapter)` — a
`facett_core::Facet` (⇒ a `Panel`). `main.rs` mounts it as the **"Pipeline"** tab of the
native `TabbedView`, alongside **Forge | Funnel | Claude | Jobs**. Native-only, exactly
like the Funnel/Claude/Jobs panes; the wasm web demo keeps the single Forge panel.

## Coverage (in code, RED-when-broken)

- `dwarves::brokkr::facett_adapter::tests` (feature `facett-adapter`, 5 tests):
  steps→nodes + needs→edges + all-green status; a failed step surfaces `Failed`+error and
  poisons the downstream to `Idle`; build durations → edge labels + pipeline metrics (rows
  not fabricated); partial ledger → unstarted steps `Idle`; feeds `PipelineView::from_adapter`
  end-to-end (proving the viewer is reused).
- `dwarves-gui/src/pipeline.rs::tests::forge_pipeline_renders_on_the_reused_viewer` +
  `tabbed.rs::tests::robot_pipeline_dag_pane_wired_into_the_host` — the pane is wired into
  the tabbed host and drives over the `state_json`/`update_json` remote seam; emits
  `nornir-testmatrix` cells (`build_pipeline_dag_renders`, `pipeline_dag_pane_wired`).

Green default AND `--features facett-adapter` / `--features testmatrix`.

Diagram: `assets/build-pipeline-dag.svg`.