<!-- ⚠ GENERATED by nornir from .nornir/README.md — DO NOT EDIT this file -->

<!-- nornir:class: readme -->
<!--
  GOLD README PATTERN. This is the *source* — the repo-root README.md is GENERATED
  from `.nornir/README.md` by `nornir docs render` and carries a
  "⚠ GENERATED by nornir from .nornir/README.md — DO NOT EDIT this file" banner.
  Edit here, never the root copy.
-->

![dwarves](assets/dwarves.webp)

# Dwarves

CI/CD software forge inspired by the dwarven craftsmanship that created Odin's spear Gungnir and the gods' treasures, as recorded in the Icelandic Prose Edda (c. 1220 CE).

It binds tightly to **[Nornir](https://git.gunnar.rs/nordisk/nornir)** — handling code's time-space dimensional warehouse — and to **[Holger](https://git.gunnar.rs/nordisk/holger)**, the artifact registry guarding your artifacts at rest.

## Measured — the forge's planner hot paths

<!--
  `benches`, not `mashup`: dwarves benches no RIVAL system, so there is nothing for
  mashup's N-way head-to-head to compare against — and mashup infers its "system"
  token from the leading `_`-delimited segment of a result name, which folds
  `dwarves.incremental_cold` and `dwarves.incremental_warm` into ONE row and silently
  drops the warm (cache-hit) arm. `benches winner=row` keeps every arm as its own row.
  Numbers come from `nornir bench run dwarves` (bench/examples/nornir-bench.rs).
-->
<!-- nornir:gen:start:benches winner=row -->
**v0.1.4 · oden · 32 cores · 2026-07-22**

| workload | build_ms | builds_per_sec | cache_hits_per_sec | elapsed_ms | ids_per_sec | iters | replan_ms | run_ms | steps | steps_per_sec | telem_cores_busy_avg | telem_cores_busy_max | telem_cpu_pct_avg | telem_cpu_pct_max | telem_elapsed_ms | telem_mem_pct_max | telem_mem_peak_mb | telem_n_cores |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dwarves.dag_build | 0.12 | 8546 | | 2340 | | 20000 | | | 150 | | 2 | 2 | 6.63 | 7.04 | 2342 | 5.44 | 16.98 | 32 |
| dwarves.incremental_cold | | | | **78** | | 200 | | 0.39 | 150 | 386271 | 2 | 2 | 6.25 | 6.25 | **79.07** | 5.44 | 17.55 | 32 |
| dwarves.incremental_warm | | | 520476 | 144 | | 500 | 0.29 | | 150 | | 2 | 2 | 6.44 | 6.44 | 145.91 | 5.45 | 17.55 | 32 |
| dwarves.step_input_id | | | | 361 | 1.38M | 500000 | | | | | 2 | 2 | 7.03 | 7.03 | 361.39 | 5.46 | 17.55 | 32 |
<!-- nornir:gen:end:benches -->

---

## Dwarves release — usage

**Dwarves release** is a multi-language, industrialised implementation of cunning, the cargo/crate spec, and academia — built to solve the Codeberg/Nordisk release: **154 crates across 17 repos**, cross-repo cycles and all.

```sh
dwarves workspace-release .             # DEFAULT dry-run — PLAN ONLY: plan the constellation, no build, no branch
dwarves workspace-release . --verify    # + the pre-publish cargo-test suite
dwarves workspace-release . --rehearse  # + Gate-B: rehearse the cascade into a throwaway local registry
dwarves workspace-release . --execute   # for real — verify + rehearse, then crates.io (only after both gates pass)
```

The run is **tiered** and **cheap-first, build-last**: the default dry-run **plans only** (deps-first order, ship-versions, the static Gate-A precheck, advisory graph analysis) and STOPS — no build, no `cargo test`, no branch — in **milliseconds**. `--verify` adds the cargo-test suite; `--rehearse` adds the Gate-B `/sparring` rehearsal; `--execute` implies **both** (a real publish always verifies + rehearses), then uploads. Nothing reaches crates.io until **two gates go green** — *Gate A* (static) and *Gate B* (a rehearsal into a throwaway local registry). A build never runs on an already-failed run, and a `--verify`/`--rehearse` dry-run builds into the **same target `--execute` reuses**, warming its cache.

In the Dwarves UI, the picture below shows a dry-run error — caught by the static Gate A in **milliseconds**, before any build. With `--rehearse`/`--execute`, Dwarves also **builds** the whole cascade into a **staged Holger** (a local repo):

<p align="center"><img src="assets/codevault-red.webp" alt="dwarves codevault — dry-run error: holger-server-lib red, missing the online feature" width="760"></p>

Fix the culprit and run again — green means the cascade publishes **deps-first**:

<p align="center"><img src="assets/codevault-green.webp" alt="dwarves codevault — both gates green, publishing deps-first" width="760"></p>

### Several repositories in one run, and the live dry-run

`dwarves release` takes several repository paths and runs them in the order given.
`--only` spans all of them. A name that no path publishes, or that two paths publish,
is refused. `monetize-impl` is never published: a run that names it, or finds it, is
refused before anything is packaged.

```sh
# from the estate's parent directory: read-only, uploads nothing
dwarves release tunnr edda monetize Skidbladnir --only korp-installer,appliance-grow --live-dry-run
# the same line with --execute publishes (byte-identical crates are skipped, a 429 waits and retries)
```

`--live-dry-run` asks crates.io what is already published, runs the size gate, and
runs `cargo publish --dry-run` for each crate that would go up, then runs Gate B (the
loopback `/sparring` rehearsal) exactly as `--execute` would.

### The pre-flight size gate

`dwarves release <repo>` weighs every crate it is about to upload **before** it uploads
anything, because **crates.io refuses an oversized upload with a bare HTTP 503** that
names neither a size nor a reason — cargo prints `failed to get a 200 OK response, got
503` and nothing else.

```sh
dwarves release .                       # dry-run: prints the registry's limit and where the number came from
dwarves release . --check-size          # + packages every crate NOW and weighs the .crate
dwarves release . --execute             # the size pass runs automatically, before any upload
dwarves release . --max-upload-mib 20   # a granted limit increase (or another registry's limit)
```

An over-limit crate **refuses the whole cascade** — exactly like the same-version drift
halt — naming the crate, its compressed size, the limit *and its source*, and the
**biggest directories and files in the `.crate`**, so the `exclude` to write is on
screen instead of behind a local `cargo package`. The two honest remedies are printed
with it: ship less, or get the limit raised and pass `--max-upload-mib`.

The limit is never a magic number. crates.io's **10 MiB** is a documented constant (no
registry endpoint publishes it, and a staff-granted per-crate increase is invisible to
the API); it is overridden by `--max-upload-mib`, `DWARVES_MAX_UPLOAD_BYTES_<ALIAS>` or
`DWARVES_MAX_UPLOAD_BYTES`, and for a non-crates.io `--registry` with no override the
limit is honestly **unknown** and refuses nothing. If a 503 still arrives mid-cascade,
it is translated: over the limit ⇒ "this 503 IS the size refusal", within it ⇒ "reads
as transient — retrying", never measured ⇒ it says so.

📖 **[Release pseudocode (Rust)](dry-run.md)** — the dry-run step by step, the two gates, and the formal model, with diagrams.

📐 **[Release invariants](release-invariants.md)** — what a release *guarantees*, independent of how it is sequenced: the cross-repo dependency model, the invariants upheld today, the ones measured as absent, and why a gate can state the right condition and still be green by construction.

*The release process is **AI-free** — 100% dwarven-forged: deterministic Rust, no model in the loop.*

---

Every crate in the constellation is held to **[Brokkr's Software Criteria](brokkr-software-criteria.md)** —
baremetal, minimal-dependency, zero-copy / multi-core / SIMD.
