Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/build-thing-design.md

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.