Gunnar

nordisk/znippy

.nornir/build-thing-design.md

# znippy's build thing — what the recipe declares, and why

**The code is the source (lane AG, 2026-08-30).** `znippy-build-thing/` is
znippy's `impl BuildThing`; `.nornir/build-variants.toml` is **emitted** from it
and read back as authority by nothing:

```
cargo run -p znippy-build-thing -- emit      # regenerate the file
cargo run -p znippy-build-thing -- toml      # the emission, on stdout
cargo run -p znippy-build-thing -- --list    # the roster and its cargo argv
```

`znippy-build-thing/tests/toml_parity.rs` fails for **any** reason the file and
the code differ. The build DOOR is unchanged: `cargo xtask build --suite ORIGINAL`
is `suite_cli` over the emitted file, and `dwarves buildthing znippy` comes
through the same one.

## What it declares

Two atoms, and no more:

| suite | atom | role | linkage | package · bin | features |
|---|---|---|---|---|---|
| ORIGINAL | `znippy` ⭐ | tool | not-applicable | `znippy` · `znippy` | — |
| ORIGINAL | `znippy-full` | tool | not-applicable | `znippy` · `znippy` | `iceberg`, `sign` |

**The five-shape ladder does not apply.** `<x>-server` ⭐ · `<x>` · `<x>-cli` ·
`<x>-ui` · `<x>-solo` is an axis about ONE question — where does the server live
— and znippy has no server. It is a compressor: an archiver you run, that exits.
Nothing to be thin against, nothing to embed, no port, no unit, no deploy.
`no_row_claims_a_server_shape_because_znippy_has_no_server` holds that.

`linkage = "not-applicable"` is stated out loud rather than omitted, and the
difference is not cosmetic: `ServiceShape::default()` is `FatService`, so an
omitted key reads as a positive claim of self-containment that nobody made and
nobody checked. `validate_shape` holds the value to `embeds` and `reaches` both
being empty, so the claim is enforced.

**Carrying a `suites` tag is what makes an atom a Build Thing** (Rickard,
2026-08-03). Before that pass znippy declared nine atoms and not one carried the
tag, so the whole repo sat outside the system: `--suite ORIGINAL` matched nothing
and `dwarves buildthing znippy` had no delivery to ask for.
`every_atom_carries_the_delivery_tag` keeps it that way.

Both rows write the same bin name deliberately — they are the same artifact at
two feature sets, never built in one command. That is exactly why the *removed*
`znippy-cli` bin had to go: a SECOND package writing the SAME file name is one
artifact built twice, with whichever ran last surviving.
`both_atoms_are_the_same_binary_of_the_same_package` pins it.

## The feature axis

Clean with **nothing excused**, which is the strongest form the gate has: the
`znippy` package offers `iceberg` and `sign`, and `znippy-full` builds both.
`the_feature_axis_gate_is_clean_with_nothing_excused` asserts the allowlist is
EMPTY, so an excuse added later is caught as the stale rot it would be.

`offers` covers the `znippy` package's own axis and stops there. The workspace's
other crates declare features of their own (`znippy-common`: `openzl`,
`wasm-plugins`, `host-decompressors`, `sign-holger-test`, `testmatrix`;
`znippy-cli`: `testmatrix`) and those belong to *their* manifests — widening
`offers` to cover them would make this recipe claim an axis it does not drive.

---

## Appendix — the reasoning that used to sit between the rows

**An archive, kept verbatim, in file order. 2026-08-30, lane AG.**

Until this day `.nornir/build-variants.toml` was hand-written, and the reasoning
above and between its rows was the most valuable thing in it. The file is
**emitted** now (`code → toml`, one-directional), and an emission carries data,
not argument — a comment typed into it is deleted by the next `emit` without
anybody noticing. So the prose is kept here rather than lost.

**New reasoning goes beside the code that acts on it** — a doc comment in
`crates/<repo>-build-thing/src/recipe.rs` — or in the section above. Never back
into the toml. This appendix is an archive of what was already written, not a
place to add to.

### after `(the file preamble)`

```text
znippy — the suite build matrix, as data.

Read by `nornir_build_thing::suite_cli`, which is the WHOLE body of
`cargo xtask build --suite ORIGINAL [--atom <name>]` (xtask/src/main.rs) and the
same door `dwarves buildthing znippy --suite ORIGINAL` comes through. There is no
second build path: this file names the package, the bin, the features and the
profile, and the xtask contributes none of them.

═══════════════════════════════════════════════════════════════════════════════
WHAT znippy IS, AND WHY THE FIVE-SHAPE LADDER DOES NOT APPLY
═══════════════════════════════════════════════════════════════════════════════

korp, dwarves, nornir and modgunn were reshaped on 2026-08-02/03 onto the five
shapes of `workspace_nordisk/.nornir/build-matrix-2026-08-02.md`:

  <x>-server ⭐ · <x> · <x>-cli · <x>-ui · <x>-solo

That ladder is an axis about ONE question — *where does the server live* — and
znippy has no server. It is a compressor: an archiver you run, that exits. There
is nothing to be thin against, nothing to embed, no port, no unit, no deploy.

So every atom here is `role = "tool"` and `linkage = "not-applicable"`.

`linkage` is stated OUT LOUD rather than omitted, and the difference is not
cosmetic: `ServiceShape::default()` is `FatService`, so an omitted key reads as a
positive claim of self-containment that nobody made and nobody checked. The value
`not-applicable` says *this axis has no answer here* in a way a reader can tell
apart from *somebody forgot to fill this in* — and `validate_shape` holds it to
`embeds` and `reaches` both being empty, so the claim is enforced, not decorative.
znippy's library atoms already carried it before this pass; the tools now do too.

═══════════════════════════════════════════════════════════════════════════════
THE RULE THAT DECIDES WHAT MAY BE IN THIS FILE
═══════════════════════════════════════════════════════════════════════════════

Rickard, 2026-08-03: **carrying a `suites` tag is what makes an atom a Build
Thing.** Before this pass znippy declared NINE atoms and not one carried the tag,
so the whole repo sat outside the Build Thing system — `--suite ORIGINAL` matched
nothing here and `dwarves buildthing znippy` had no delivery to ask for.

The matrix Rickard approved is TWO rows, and this file is exactly those two:

  SUITE     ATOM          ROLE  LINKAGE         PACKAGE  BIN     FEATURES
  ORIGINAL  znippy ⭐     tool  not-applicable  znippy   znippy  —
  ORIGINAL  znippy-full   tool  not-applicable  znippy   znippy  iceberg, sign

── WHAT LEFT, AND WHY ────────────────────────────────────────────────────────

✗ `znippy-cli` / `znippy-cli-full` — the DUPLICATE BINARY, removed (the bin only;
  the crate stays, and it is where all the code lives). Measured 2026-08-03:
    * `znippy/src/main.rs` and `znippy-cli/src/main.rs` were BYTE-IDENTICAL
      three-liners — sha256 24abd778… for both — each `fn main() {
      znippy_cli::run().unwrap() }`.
    * `znippy-cli` is the crate that holds the code (`pub fn run()`); the `znippy`
      package is a wrapper added nearly a month later (crate created 2025-06-30 vs
      2025-07-25) purely to get the right binary name.
    * The repo's own `.nornir/` docs invoke `znippy <verb>` 50 times and
      `znippy-cli <verb>` twice.
    * Skidbladnir, korp-installer, korp-collectors and garmr all depend on the
      `znippy` CRATE; none depends on the `znippy-cli` BINARY.
  Two build points writing two files off one `run()` is the twin LAW 5 forbids,
  and it had already cost something concrete: `sign` was declared ONLY on
  `znippy-cli-full`, i.e. only on the name nobody uses. That is why `znippy-full`
  below carries `iceberg` AND `sign` — the feature level survives the collapse
  instead of quietly leaving with the binary.

✗ `znippy-common` · `znippy-compress` · `znippy-decompress` ·
  `znippy-plugin-skidbladnir` · `znippy-plugin-media` — the five LIBRARY atoms.
  They are dependencies and plugins, not deliverables: they are linked by whoever
  consumes them, with that consumer's own feature selection, and they belong in
  `Cargo.toml` (where they still are — NOTHING was deleted from the workspace).
  A Build Thing is a declared BUILD POINT, and `cargo build -p znippy-common` is
  not something anybody ships.

── WHY TWO ATOMS SHARE ONE `bin`, AND WHY THAT IS NOT THE dwarves DEFECT ─────

`znippy` and `znippy-full` both emit bin `znippy` from package `znippy` at profile
`release`, so `BuildVariantSet::check_bins` reports them as one artifact path. It
is RIGHT to report and WRONG to forbid, and the distinction is the point:

  * The defect it was written for (korp 3× bin `korp`, nornir 3× bin `nornir`,
    dwarves 3× bin `dwarves`) was shapes that had to COEXIST — a fat build and a
    thin build a deployer needs side by side, that could not sit on one machine.
  * These two are a FEATURE VARIANT: one binary, two feature levels, built one or
    the other. That is what a variant IS. Giving them separate `[[bin]]` names
    would twin `main.rs` again — the exact thing removed above.

`tests/tests/build_variants.rs` therefore does not assert `check_bins().is_empty()`
(which would be false) nor skip the axis (which would be blind). It expresses the
real rule — *a shared artifact path is legal only between feature variants of one
source: same package, same bin, same kind/role/profile/target, and pairwise-
DIFFERENT feature sets* — and drives it RED with a true twin (identical features)
and with a cross-package collision.
```

### after `[build]`

```text
The feature axis this repo offers. `validate` rejects any variant enabling a
feature not listed here, so this is an allowlist, not a description.

Both are OPT-IN and both are OFF in the ⭐ atom: `iceberg` pulls tokio + iceberg +
parquet + arrow-58 for `--meta-format iceberg` and the `seal` verb; `sign` pulls
RustCrypto for detached CMS provenance (`compress --sign` / `verify --signed`).
The lean archiver is the normal case and carries neither.
```

### after `offers = ["iceberg", "sign"]`

```text
The delivery every atom is tagged into. znippy ships ONE.
```

### after `base_suite = "ORIGINAL"`

```text
── ⭐ znippy — the lean default archiver ─────────────────────────────────────

`default = true` marks the normal case of the `tool` role; `check_defaults`
requires exactly one per (suite, role), so this is the answer to "which one do I
build if nobody said".
```

### after `linkage = "not-applicable"`

```text
Stated, not inherited: `znippy`'s Cargo `default` is `[]`, so this and
`--no-default-features` describe the same closure — and saying it explicitly means
a `default` feature added tomorrow cannot join the shipped binary unannounced.
```

### after `profile = "release"`

```text
── znippy-full — the same binary with both opt-in levels compiled in ─────────

The ONLY difference from the ⭐ atom is `features`, which is precisely what makes
it a variant rather than a second artifact.
```

### after `no_default_features = true`

```text
`sign` reaches znippy-cli's `sign` → znippy-common's `sign` (detached CMS).
`iceberg` reaches znippy-cli's `iceberg` → the znippy-iceberg backend.
Both are passthroughs declared in znippy/Cargo.toml; before 2026-08-03 the `sign`
passthrough did not exist there at all and this atom could not have been declared.
```