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