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

nordisk/skade

.nornir/build-thing-design.md

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

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

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

tests/toml_parity.rs fails for any reason the file and the code differ.

What it declares

Two rows, because skade ships two things:

variant kind package · bin what it is
skade ⭐ lib skade the embeddable Iceberg library, with the s3 object-store backend
skade-remote ⭐ service skade-katalog-serve · skade-katalog the catalog service that owns the state

skade is tri-modal from the consumer's side — compile it in and own the RedbCatalog, or reach a running skade-katalog serve over gRPC / Iceberg-REST — but that is the consumer's linkage axis, not skade's.

The root invariant, as a check

Nothing is downstream of skade. The [build.downstream] skade = [] allowlist is EMPTY and PRESENT, which are different claims from an absent one, and nothing_is_downstream_of_skade_and_an_embed_is_refused proves the refusal by building a synthetic variant with embeds = ["korp"] and asserting validate rejects it. The service row states embeds and reaches as explicitly empty rather than omitting them.

What the emission changed

The six declared-but-never-built features — aws-s3, rustfs, lineage, stream, lineage-http, testmatrix — were recorded on 2026-07-22 in prose. They carry a written reason in [build.allow_unbuilt] now, and the_feature_axis_gate_is_clean runs FeatureAxis::gate() every run.

sql is held out by a test of its own. The DataFusion surface moved to the sibling skade-sql crate on 2026-08-14 (0369d66), and this recipe kept offering it until 2026-08-23 — nine days in which the ⭐ library row named a build point cargo refused outright ("the package skade does not contain this feature: sql"). That is exactly what a hand-maintained recipe cannot catch and an emitted one can.

What is deliberately NOT offered

offers covers the skade crate's own axis and stops there. Two names the workspace declares are left out on purpose, and it is worth saying so rather than letting the omission look like an oversight:

The sibling crates' feature names (object-store in serve/, uring and rest in bench/) belong to their manifests; widening offers to cover them would make this recipe claim an axis it does not drive.

One note on the dependency

crates/skade-build-thing is the ONE place in this repo with a path dep on edda's nornir-build-thing. skade deliberately keeps its own dev-dependency on the published crate, and that is unchanged. The recipe crate needs 0.1.12 vocabulary (the emitter and the schema rule; the published 0.1.11 has neither), and it is publish = false, so skade's release cascade is untouched.


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)

skade — the warehouse ENGINE, and the GRAPH ROOT of the nordisk suite build
matrix (workspace_nordisk/.nornir/suite-build-matrix.md).

skade is TRI-MODAL: a consumer either compiles skade in-process and owns the
RedbCatalog directly (the embedded/fat case), or reaches a running
`skade-katalog serve` over gRPC / Iceberg-REST (the thin case). Note WHOSE axis
that is — it is the CONSUMER's linkage, not skade's. What skade itself ships is
a library and the server binary that owns the state.

⚠ THE ROOT INVARIANT: skade is the graph ROOT — NOTHING is downstream of it, so
it embeds NO sibling and reaches none. The EMPTY downstream allowlist below makes
any `embeds = [...]` on a skade variant a dep-graph inversion that
`BuildVariantSet::validate_shape` rejects.

after [build]

The feature space skade offers across its crates (the real skade features:
object-store backends + the lineage/stream/testmatrix seams). A variant feature
outside this set is rejected.

⚠ `sql` IS GONE, and this file kept offering it until 2026-08-23. The DataFusion
surface moved OUT of skade on 2026-08-14 (0369d66, "SQL moves out of skade so
skade can publish again") into the sibling `skade-sql` crate, because a git
datafusion dep with no version makes the whole crate unpublishable — see
skade-sql/FOLDING-BACK.md. `cargo check -p skade --features s3,sql` has failed
with "the package 'skade' does not contain this feature: sql" ever since, which
means the DEFAULT library variant below — the one the build menu offers — named
a build point that could not be built. Removed from both `offers` and the
variant.

⚠ DECLARED-BUT-NEVER-BUILT (2026-07-22): `aws-s3`, `rustfs`, `lineage`,
`stream`, `lineage-http` and `testmatrix` are offered here and enabled by NO
variant below, so no declared build point ever compiles them — the same rot that
let korp's `spark41` and holger's `gui` break invisibly. `geo-http` and `bench`
are real skade features that are not even offered, so they are invisible to this
space entirely. Both gaps are recorded rather than papered over: adding a variant
per combination is a deliberate act, not a silent one.

after offers = ["s3", "aws-s3", "rustfs", "lineage", "stream", "lineage-http", "testmatrix"]

The suite dep graph as an embed-allowlist: member -> siblings strictly
DOWNSTREAM of it. skade is the ROOT — its allowlist is EMPTY, so a fat variant
that tries to embed anything is rejected as a graph inversion.

after skade = []

── skade (the LIBRARY): the engine a consumer compiles IN-PROCESS. ─────────────
`skade` is a library crate (skade/skade — it declares no `[[bin]]`), so this is
`kind = "lib"`. It was previously declared as the default `exe` kind, which asked
cargo for a binary that does not exist.

`linkage = "not-applicable"` is a DECLARATION, not an omission. A library is
linked by whoever consumes it; it composes nothing and runs nowhere, so fat/thin
has no answer for it. Omitting the key would NOT say that — an omitted linkage
reads as the `fat` default, i.e. a positive claim of self-containment that
nothing checked. `validate_kind` now rejects a `lib` that leaves it unsaid.

after profile = "release"

── skade-remote (the SERVE binary): the one lock-owning `skade-katalog serve`
   process that thin, lock-free consumers reach over gRPC / Iceberg-REST. ──────

⚠ THE NAME IS THE CONSUMER'S VIEW; THE LINKAGE IS THIS BINARY'S. This artifact
used to declare `shape = "thin"`, which made the menu say "needs something
running" about the thing that IS the something running. This binary OWNS the
warehouse: it opens the local state directly and needs no other service up —
which is precisely `fat` — and as the graph root it embeds and reaches nothing.
(Recorded as a finding: the handle `skade-remote` names how CONSUMERS reach it
and would read better as `skade-serve`; the rename is deferred because the handle
is a public build-menu identifier.)

after profile = "release"

── NOT declared, and why ─────────────────────────────────────────────────────
`skade-sql` (skade/skade-sql) is a real, shipped, consumed crate — korp and
modgunn both link it — but it gets NO variant here, and the reason is not
oversight. It lives in its OWN detached workspace, so no `cargo` invocation
rooted at this repo's workspace can build it: `-p skade-sql` does not match a
package here. It gets a row the day it folds back into `skade` as a feature,
which is exactly the condition skade-sql/FOLDING-BACK.md is written to detect.

⚠ THAT CONDITION HAS NOW FIRED (2026-08-24). This said `publish = false` "by
construction (its datafusion edge is a git rev, the only build on arrow 59)".
Both halves are stale: `skade-sql/Cargo.toml:47` reads `datafusion = "55"`, a
plain crates.io version, and datafusion 55.0.0 IS the arrow-59 line — verified
by FOLDING-BACK.md's own one-command test against a resolved graph (nornir's
lock: datafusion 55.0.0 from `registry+…crates.io-index`, pulling arrow-array
59.2.0). So the git-rev edge that made it unpublishable is gone, and
FOLDING-BACK.md's instruction applies: this crate is "temporary by design" and
"should be deleted, not maintained" — folded back into `skade` behind `sql`.
Left standing here rather than acted on, because deleting a crate korp and
modgunn both link is a product decision, not a cleanup.

The same detachment is why the two variants above cannot be checked from the
repo root either: `skade/` and `serve/` are detached workspaces (the deliberate
pattern that keeps skade-katalog's manifest and lockfile clean), and a
root-rooted `cargo check -p skade` reports "cannot specify features for
packages outside of workspace". Their `member` key says where they live; a
checker that honours it has to run with `--manifest-path <member>/Cargo.toml`.