# nornir's build thing — and why it is the ONE repo that has no recipe crate
**Status: BLOCKED on a name, not on the model. 2026-08-30, lane AG.**
Nine repos got an `impl BuildThing` this lane and their
`.nornir/build-variants.toml` became an emission. nornir is the one that did not,
and the reason is worth writing down, because it is not a shortcoming of nornir's
build and it will not go away by trying harder.
## The convention
dwarves' forge (`dwarves::build_thing::code::recipe_crate`) finds a repo's recipe
by **package name**: it looks for a package named exactly `<repo>-build-thing`,
under `crates/` or the repo root, and refuses anything else BY NAME. Then
`code::take` builds it with
```
cargo build -p <package> --lib --release --target wasm32-unknown-unknown
```
## Why that cannot work here
This repo is `nornir`. So its recipe crate would have to be named
**`nornir-build-thing`** — which is the name of **edda's engine crate**, the one
every recipe in the constellation is written against, and the one nornir already
depends on twice.
That is not a hypothetical collision. MEASURED in this checkout, 2026-08-30,
**before any recipe crate exists**:
```
$ cargo pkgid nornir-build-thing
error: specification `nornir-build-thing` is ambiguous
help: re-run this command with one of the following specifications
nornir-build-thing@0.1.11 # the registry copy, via published transitives
nornir-build-thing@0.1.12 # the path dep on ../edda/crates/nornir-build-thing
```
`-p nornir-build-thing` is **already** ambiguous in nornir's workspace. A third
package of that name — a local member at `0.1.0` — makes it worse, and the
forge's build command is exactly the spec that cannot resolve. A recipe crate the
forge cannot build is not a recipe crate.
The obvious dodges do not survive contact:
| attempt | result |
|---|---|
| rename the engine dep (`nbt = { package = "nornir-build-thing", … }`) inside the recipe crate | compiles, with `extern crate nbt as nornir_build_thing;` for the `export_build_thing!` paths — but `-p nornir-build-thing` in the workspace is still ambiguous, so the forge still cannot build it |
| name the recipe crate something else (`nornir-recipe`) | the repo gets the whole local win — one declaration, an emitted toml, a byte-for-byte parity guard — and the forge finds **nothing**, because it matches on the name |
| `[lib] name = "…"` to disambiguate | the cdylib's file name is derived from the PACKAGE name, so this hides the `.wasm` from the loader instead |
## What has to change first, and it is one decision
The convention keys on a NAME, and one repo's name collides with the engine's.
Two ways out, and they are the forge's call, not nornir's:
1. **A second accepted name.** `recipe_crate` already reads each candidate's
manifest and compares `[package] name`; teaching it to accept
`<repo>-recipe` as well as `<repo>-build-thing` is a few lines. It is still a
convention keyed on a name, but it is a general one, not a table.
2. **Find the recipe by what it IS, not what it is called** — the cdylib in the
repo whose manifest depends on `nornir-build-thing` and is not
`nornir-build-thing` itself. That is a fact rather than a spelling, and it is
the answer that survives the next collision as well as this one. It needs the
ambiguity handled at the build too (`-p <name>@<version>`, which the manifest
scan already knows).
Until one of those lands, this file **is** nornir's declaration of record, beside
the hand-written `.nornir/build-variants.toml` — which stays hand-written, and is
the last one in the constellation that is.
## What nornir's build would declare
Recorded here so the work is a transcription and not a rediscovery when the name
question is answered. From the current `.nornir/build-variants.toml`: the suite
atoms of the five-shape matrix that nornir genuinely has, over the workspace
members `nornir` (root), `crates/nornir-selftest`, `crates/nornir-guard`,
`crates/nornir-jobs`, `crates/nornir-introspect`, `crates/nornir-viz-web`,
`crates/bindrift-fixtures`, `nornir-demo` and `xtask`. Nothing about that shape
resists `variant → features → settings/sub-settings → typ`; it is only the crate
NAME that has nowhere to go.
---
# CORRECTION — 2026-08-30, lane AR: the forge's spec RESOLVES. The blocker is `cargo pkgid`'s, and only `cargo pkgid`'s.
**The conclusion above does not follow from the measurement above.** The measurement is
real and reproduces exactly; it is a measurement of `cargo pkgid`, and it was used to
predict the behaviour of `cargo build -p`. Those two do not share a matcher, and the
difference is precisely the thing the recipe crate would introduce: **a workspace
member**.
## What was re-measured
`cargo pkgid nornir-build-thing` in this checkout, 2026-08-30 — AG's red, verbatim:
```
error: specification `nornir-build-thing` is ambiguous
help: re-run this command with one of the following specifications
nornir-build-thing@0.1.11 # registry, pulled in by modgunn 0.0.6 (a path sibling)
nornir-build-thing@0.1.12 # the path dep on ../edda/crates/nornir-build-thing
```
`cargo tree -i` names the source of the registry copy: `nornir-build-thing 0.1.11 ←
modgunn 0.0.6 (path) ← holger-server-lib 0.6.10 (path) ← nornir`. Neither copy is a
member of nornir's workspace (`cargo metadata --no-deps` lists 9 members, none of them
named `nornir-build-thing`), and *that* is why the spec is ambiguous: with no member to
prefer, cargo has two equal candidates and refuses to guess.
## The three-copy experiment
A synthetic workspace built to mirror this repo's shape exactly — a workspace MEMBER
named `X`, a non-member path copy of `X` outside the workspace, and a registry copy of
`X` pulled in through a path sibling. Three packages, one name, in one resolve graph:
| command | result |
|---|---|
| `cargo pkgid X` | **ambiguous**, all three listed |
| `cargo tree -p X` | resolves to the **workspace member** |
| `cargo build -p X --lib` | compiles the **workspace member** |
| `cargo build -p X --lib --release --target wasm32-unknown-unknown` | compiles the **workspace member**, no output-filename collision |
The last row is `dwarves::build_thing::code::take`'s command verbatim. The emitted
`.wasm` was checked for its exports rather than trusted from cargo's own progress line:
it exports the member's `recipe_marker` and not the engine copy's `engine_marker`.
`cargo build --workspace` in the same tree also builds all three copies without a
filename collision.
**So a recipe crate at `nornir/crates/nornir-build-thing` would be found by
`recipe_crate` (it matches on `[package] name`) and built by `code::take`.** The
discovery convention does not need a second accepted name, and it does not need to be
re-keyed onto "what the crate IS". Option 1 and option 2 in the table above are still
reasonable designs — but neither of them is *required*, and nornir is not blocked.
## What IS still true, and it is the opposite of what publishing would fix
`cargo pkgid`/`cargo tree -p` stay ambiguous for the bare name in any of these shapes.
That is a nuisance for a human at a shell; it is not on the forge's path.
And **publishing `nornir-build-thing 0.1.12` does not cure the ambiguity — it makes the
remaining disambiguation strictly worse.** Measured on the same synthetic, with the path
copy's version set equal to the registry copy's:
```
# versions differ (today): `pkgid name` ambiguous
# `pkgid name@0.1.12` resolves cleanly to the path copy
# versions equal (after publish): `pkgid name` ambiguous
# `pkgid name@1.0.0` ALSO ambiguous
# the only spec left is the fully source-qualified one:
# path+file:///…#name@1.0.0 vs registry+https://…#name@1.0.0
```
Publishing 0.1.12 is needed for other reasons — dwarves cannot publish at all while
three of its crates declare `nornir-build-thing = "0.1.12"` against a registry whose
maximum is 0.1.11, which `release_gates` calls an UNSATISFIABLE requirement — but it is
**irrelevant to this file's blocker in one direction and harmful in the other**. Do not
publish it expecting nornir's recipe crate to become buildable; it already would be.
> Lane AR did not touch `edda/crates/nornir-build-thing` (lane AP owns it) and did not
> write nornir's recipe crate — that is the owning lane's call, now that the reason not
> to has been withdrawn.