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

nordisk/facett

facett-build-thing/src/lib.rs

//! # facett-build-thing — facett's recipe, as code
//!
//! ## What facett declares, and what it deliberately does not
//!
//! facett is a library constellation — seventy-odd crates a consumer links. It has
//! no exhaustive library catalogue. Falsett declares a focused feature matrix
//! because its embedders unify optional rendering features in the same build.
//!
//! What it does have is what is PRODUCED rather than linked: the two PyPI releases
//! (owner decision 2026-10-02, version [`PYPI_VERSION`]) — `facett`, the components
//! wheel (the wasm bundles + loader, Streamlit glue behind the `[streamlit]` extra),
//! and `facett-demo`, the Streamlit demo app that depends on `facett[streamlit]`.
//! Each is a wheel, the `SHA256SUMS` a downloader runs `sha256sum -c` against, and a
//! provenance record over the produced bytes; `facett-demo` also has an sdist (it is
//! pure Python, so its source distribution can rebuild it). `facett` has NONE: its
//! wheel carries prebuilt wasm, and a source distribution that cannot rebuild that
//! wasm from what it contains would be a binary in a source costume.
//!
//! That release was real and shipped before this crate existed, and reachable only
//! through `cargo ops publish-pip`. `facett-ops/tests/release_manifest.rs` wrote the
//! reason into its own module docs rather than working around it:
//!
//! > The brief's door is `dwarves buildthing facett --variant facet-wrapped-web`.
//! > That needs the forge to have an artifact kind for a **package**, and it does
//! > not: `ArtifactKind` is `Lib | Exe | Wasm | Kernel | Container | Service | Vm |
//! > Iso | Installer`. A wheel is none of those, and `Wasm` would make the forge
//! > build a bare `.wasm` — not run wasm-bindgen, not build the wheel, not verify
//! > it.
//!
//! edda 0.1.13 added the kind ([`ArtifactKind::Package`]). This crate is what gives
//! the forge something to route to.
//!
//! ## THE MODEL
//!
//! The 2026-08-27 law: **the CODE is the source.** [`FacettBuildThing`] is the
//! declaration; `.nornir/build-variants.toml` is EMITTED from it by
//! [`write_emitted`] to DESCRIBE the build for readers and for the doors that
//! discover a repo by that path. The file is never read back as authority, and
//! editing it changes nothing. `tests/toml_parity.rs` holds the committed bytes
//! equal to this code's emission — not two mirrors kept in step by hand, but a
//! writer and a check that it ran.
//!
//! ## The two rows, and why the wasm one is not a `from_variant`
//!
//! The wheel carries a wasm-bindgen bundle: `facet_wrapped_bg.wasm`,
//! `facet_wrapped.js`, `manifest.json`, produced by `cargo xtask wrap-wasm` from
//! the `facet-wrapped` cdylib. Those are declared as the package cell's `inputs`,
//! so a forge run before `wrap-wasm` refuses BY NAME on a missing or 0-byte input
//! instead of building a wheel that loads nothing — the same question
//! `pip::check_wasm_bundle` asks at the xtask door, now said in the declaration so
//! both doors ask it.
//!
//! They are **inputs and not `from_variant`**, and the distinction is honest rather
//! than pedantic. `from_variant` means "the forge builds this cargo variant first",
//! and a cargo build of the cdylib does NOT produce those three files —
//! wasm-bindgen does, in a step that is `cargo xtask wrap-wasm` and not a variant.
//! Declaring the chain would promise the forge produces an input it cannot, and the
//! failure would arrive as a missing file after a build that reported success.
//!
//! ## What this crate does NOT decide
//!
//! The no-JavaScript-graphics rule (`xtask/src/jsguard.rs`) is not declared here,
//! and the forge therefore does not enforce it — see `PackageStager` in edda, which
//! states the difference between the two doors and why closing it means moving that
//! rule into this crate's `validate`. It needs to know that `getContext` inside
//! `facet_canvas_api` is the probe proving the renderer is WebGPU and not a painter
//! acquiring a surface, and no declaration can say that.

use std::path::{Path, PathBuf};

use anyhow::Result;
use nornir_build_thing::cargo_container::{CargoToolchain, CARGO_TOOLCHAIN_IMAGE};
use nornir_build_thing::emit;
use nornir_build_thing::container::ContainerBuild;
use nornir_build_thing::package::{PackageBuild, PackageFormat};
use nornir_build_thing::wasm::{BindgenStage, BindgenTarget, ComponentManifestStage, WasmBuild};
use nornir_build_thing::plugin::{Manifest, ABI_VERSION};

/// Re-exported so a consumer of this crate (the `[[bin]]`, the tests) can call
/// [`FacettBuildThing::variants`] without also naming edda — the trait has to be in
/// scope for its methods to exist, and having to import two crates to ask one
/// declaration a question is friction with no upside.
pub use nornir_build_thing::plugin::BuildThing;
/// **The engine's error type, re-exported for the same reason the engine is**: the
/// seams xtask implements (`wasm::WasmStageRunner`, `cargo_container::
/// CargoContainerRunner`) return `anyhow::Result`, and a second `anyhow` edge in
/// xtask would be a second way to spell one type.
pub use anyhow;
use nornir_build_thing::variant::{AllowEntry, ArtifactKind, BuildVariant, BuildVariantSet, Profile};

/// **The engine, re-exported** — so facett's xtask names ONE dependency (this
/// crate) and reaches every row type, the argv writer (`build_variant`) and the
/// wasm stage runner through it. Two `nornir-build-thing` edges, one here and one
/// in xtask, could resolve two different `BuildVariant` types; one edge cannot.
pub use nornir_build_thing;

/// **The declaration, as the set every door resolves.** `FacettBuildThing.variants()`
/// by its plain name, so a door asks the CODE (never the emitted toml) with one call.
#[must_use]
pub fn declared() -> BuildVariantSet {
    FacettBuildThing.variants()
}

/// **The door that DOES build a row cargo cannot**, named — `None` for a cargo kind.
///
/// `cargo xtask build` materializes cargo argv and nothing else, so a `package` or
/// `container` row asked of it is REFUSED; refusing it with "use the image builder"
/// sends the reader hunting. Which door builds a row is a fact about the
/// declaration, so it lives here beside the rows rather than as a list of names in
/// xtask. Keyed on the KIND with the row's own name carried in, so a fourth image
/// needs no edit here.
#[must_use]
pub fn door_for(v: &BuildVariant) -> Option<String> {
    if v.kind.is_cargo() {
        return None;
    }
    let forge = format!("`dwarves buildthing facett --variant {}`", v.name);
    Some(match v.kind {
        ArtifactKind::Package => format!(
            "built by `cargo ops publish-pip` (the facett + facett-demo wheels, the demo's sdist, \
             SHA256SUMS + provenance, inside the pinned `{PIP_TOOLCHAIN_IMAGE}` container, after \
             `cargo xtask wrap-wasm` has built every bundle group), or by the forge: {forge}"
        ),
        ArtifactKind::Container => format!(
            "built by the forge: {forge} (file-mode [[build.container]] over {CONTAINERFILE_DIR}/); \
             the facett-ops verbs that run it (publish-pip, verify-install, streamlit, test-demo) \
             mint it themselves"
        ),
        other => format!("kind '{}' — built by the forge: {forge}", other.as_str()),
    })
}

/// This crate, as the receipt names it.
pub const KRATE: &str = "facett-build-thing";

/// **The delivery facett's rows ship in.** `suites` is a LIST — one row may
/// ship in more than one flavour — but facett has exactly one, and writing it
/// down is the whole point: an EMPTY list is not a third flavour, it is a row
/// that `cargo xtask build --suite ORIGINAL` cannot select. All SEVEN rows here
/// were exactly that until 2026-09-20. The only other flavour anywhere today is
/// holger's MVP; every other product declares ORIGINAL alone and TAGS it, so
/// the build verb has ONE shape everywhere.
pub const SUITE: &str = "ORIGINAL";

/// The declared PACKAGE variant of the COMPONENTS wheel (PyPI `facett`) — the name
/// `dwarves buildthing facett --variant …` takes for it. Until 0.2.0 this row was
/// `facet-wrapped-web`, after the distribution it built.
pub const VARIANT: &str = "facett-wheel";

/// The declared PACKAGE variant of the DEMO wheel (PyPI `facett-demo`). Not
/// `facett-demo`: that name is the native demo binary's row ([`DEMO_VARIANT`]).
pub const DEMO_PIP_VARIANT: &str = "facett-demo-wheel";

/// **The ONE version both PyPI packages carry** (owner decision 2026-10-02: 0.2.0;
/// 0.2.1 on 2026-10-03 — the re-release with no personal name in any packaged file,
/// held by `cargo ops publish-pypi`'s name gate). Separate from the cargo workspace
/// version (the crates move on their own line, even when the numbers coincide); `pkg-facet-wrapped/pyproject.toml`, `py/pyproject.toml` and the demo's
/// `facett[streamlit]` pin are held to it by
/// `xtask/tests/python_versions_track_the_workspace.rs`.
pub const PYPI_VERSION: &str = "0.2.1";

/// The PyPI project names, as a user types them (`pip install facett-demo`).
pub const PYPI_PROJECT: &str = "facett";
/// See [`PYPI_PROJECT`].
pub const DEMO_PYPI_PROJECT: &str = "facett-demo";

/// The declared WASM variant — the bundle the wheel carries.
///
/// It is on the roster in its own right, and that is half the point: a variant the
/// roster does not have answers `no such variant`, which is how tunnr ended up with
/// seven ISO cells nobody can dispatch.
pub const WASM_VARIANT: &str = "facet-wrapped-wasm";

/// The cdylib crate the bundle is built from.
pub const WASM_PACKAGE: &str = "facet-wrapped";

/// The components wheel's distribution name as its filename spells it — which is
/// also its IMPORT package (`import facett`), the same constant
/// `facett_ops::pip::IMPORT_PACKAGE` carries.
pub const DISTRIBUTION: &str = "facett";

/// The pre-0.2.0 import name, shipped in the same wheel as a thin alias of
/// [`DISTRIBUTION`] for ONE release (removed in 0.3.0).
pub const COMPAT_IMPORT: &str = "facet_wrapped_web";

/// The demo wheel's distribution name as its filename spells it
/// (`facett_demo-0.2.0-py3-none-any.whl`), which is also its import package.
pub const DEMO_DISTRIBUTION: &str = "facett_demo";

/// The demo's package source (pure Python) — the app, its payload table, the
/// error-code catalog.
pub const DEMO_SOURCE: &str = "py";

/// Where the demo's release set lands.
pub const DEMO_OUT_DIR: &str = "py/dist";

/// The console script the demo wheel installs (`[project.scripts]`), and what it runs.
pub const DEMO_ENTRY_POINT: (&str, &str) = ("facett-demo", "facett_demo.cli:main");

/// The pinned toolchain image `python -m build` runs inside. **No python runs on
/// the host**, ever: the image carries the exact PEP 517 backend, which is why the
/// build can run with the network off and get the same backend every time.
pub const PIP_TOOLCHAIN_IMAGE: &str = "nordisk-pip-toolchain";

/// Where `cargo xtask wrap-wasm` writes the bundle this package carries.
pub const WASM_DIST: &str = "facet-wrapped/dist";

/// Where the produced release set lands — the directory `publish-pip` already
/// writes and `publish-holger` already serves.
pub const OUT_DIR: &str = "pkg-facet-wrapped/dist";

/// The package SOURCE the format's tool is run over.
pub const SOURCE: &str = "pkg-facet-wrapped";

/// The three files `cargo xtask wrap-wasm` produces and the wheel must carry.
///
/// `index.html` is NOT here and must not be: it is package SOURCE, the loader page
/// that lives in `pkg-facet-wrapped/src/…/assets/`. Staging it as a build output
/// would mean the page that ships came from somewhere other than the committed
/// package — and that page is the one file facett's js guard scans.
pub const WASM_OUTPUTS: [&str; 3] =
    ["facet_wrapped_bg.wasm", "facet_wrapped.js", "manifest.json"];

// ── THE BUNDLE GROUPS — facet-wrapped's facets, split into loadable bundles ─────
//
// Owner, 2026-09-29: docview + manual + model3d grow the one bundle from 12.64 to
// ~16.44 MB, so they ship in bundles of their own, and the grouping goes THROUGH the
// build thing (variant -> features -> settings -> typ). Each group becomes, by
// construction and nowhere else:
//   * one `kind = "wasm"` [[build.variant]] row (suites ORIGINAL),
//   * one [[build.wasm]] cell (bindgen + the component manifest, into its dist dir),
//   * its files' place in the wheel (`assets/` for core, `assets/<group>/` else),
//   * one line of the ROUTING TABLE the loader reads ([`routing_json`]).
//
// WHICH PACKAGE (measured by lane A, 2026-09-29): `facet-wrapped` can carry NO cargo
// features — even an empty `[features]` table changes 14 517 bytes of its release
// wasm — so the core group is `facet-wrapped` exactly as it always was, and every
// other group is `facet-wrapped-group` (same crate root, `[lib] path` =
// facet-wrapped's `src/lib.rs`, lib name `facet_wrapped_group`) built with ONE group
// feature. That package has no default features, so a group row needs no
// `--no-default-features`; its `group-only` feature (pulled by each group feature)
// drops the core registrations.
//
// ENTRY -> GROUP has ONE source, [`BundleGroup::entries`] here. facet-wrapped's
// `groups::GROUP_ENTRIES` (the refusal the group wasm makes) is held to it by
// `facet-wrapped-group/tests/groups.rs::group_entries_are_the_recipes`, and the
// loader reads it from the emitted `bundles.json` — never from a hand list.

/// **One bundle group.** Every field is a fact the rows, the cells, the wheel and
/// the routing table are DERIVED from; nothing downstream re-states any of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BundleGroup {
    /// The group's name — what a registry entry is tagged with and what the
    /// routing table keys on. `core` is the default bundle.
    pub name: &'static str,
    /// The `[[build.variant]]` / `[[build.wasm]]` name of this group's row.
    pub variant: &'static str,
    /// The cargo package the row builds. Its lib stem (`-` → `_`) names the
    /// bundle's files: `<stem>_bg.wasm` and `<stem>.js` (edda's `wasm::crate_stem`).
    pub package: &'static str,
    /// The package's cargo features this group compiles with.
    pub features: &'static [&'static str],
    /// Build WITHOUT the package's default features. `false` for every group today
    /// (`facet-wrapped-group` declares no defaults); a field so the row says it.
    pub no_default_features: bool,
    /// Repo-relative dist dir the cell writes (rebuilt every run).
    pub dist_dir: &'static str,
    /// Where the group's files sit under the wheel's `assets/`: `""` = `assets/`
    /// itself (core, unchanged), else `assets/<wheel_dir>/`.
    pub wheel_dir: &'static str,
    /// The registry entries this group holds. EMPTY for core, which holds every
    /// entry no other group names (the routing table's `default`).
    pub entries: &'static [&'static str],
}

impl BundleGroup {
    /// This group's files under `src/<DISTRIBUTION>/assets/`, as the wheel holds them.
    #[must_use]
    pub fn wheel_path(&self, file: &str) -> String {
        if self.wheel_dir.is_empty() {
            file.to_string()
        } else {
            format!("{}/{file}", self.wheel_dir)
        }
    }

    /// The three files the group's cell writes, in [`WASM_OUTPUTS`] order: the
    /// bindgen'd wasm, its JS glue, the component manifest. Derived from the package
    /// the way edda derives them (`tests/thin_door.rs` holds the two equal).
    #[must_use]
    pub fn outputs(&self) -> [String; 3] {
        let stem = self.package.replace('-', "_");
        [format!("{stem}_bg.wasm"), format!("{stem}.js"), WASM_OUTPUTS[2].to_string()]
    }
}

/// The default group: today's bundle, byte-identical (no features, defaults on).
pub const CORE_GROUP: &str = "core";

/// The package every NON-core group builds (see the section comment).
pub const GROUP_PACKAGE: &str = "facet-wrapped-group";

/// **THE groups, in load order.** `core` first and unchanged — its row keeps the
/// name `facet-wrapped-wasm`, its package `facet-wrapped` and its dir
/// `facet-wrapped/dist`, so every door that already names the default bundle keeps
/// working and its bytes do not move.
pub const BUNDLE_GROUPS: &[BundleGroup] = &[
    BundleGroup {
        name: CORE_GROUP,
        variant: WASM_VARIANT,
        package: WASM_PACKAGE,
        features: &[],
        no_default_features: false,
        dist_dir: WASM_DIST,
        wheel_dir: "",
        entries: &[],
    },
    BundleGroup {
        name: "docs",
        variant: "facet-wrapped-docs-wasm",
        package: GROUP_PACKAGE,
        features: &["docs"],
        no_default_features: false,
        dist_dir: "facet-wrapped-group/dist-docs",
        wheel_dir: "docs",
        entries: &["docview", "manual"],
    },
    BundleGroup {
        name: "model3d",
        variant: "facet-wrapped-model3d-wasm",
        package: GROUP_PACKAGE,
        features: &["model3d"],
        no_default_features: false,
        dist_dir: "facet-wrapped-group/dist-model3d",
        wheel_dir: "model3d",
        entries: &["model3d"],
    },
    BundleGroup {
        name: "sim",
        variant: "facet-wrapped-sim-wasm",
        package: GROUP_PACKAGE,
        features: &["sim"],
        no_default_features: false,
        dist_dir: "facet-wrapped-group/dist-sim",
        wheel_dir: "sim",
        entries: &["simview"],
    },
];

/// A group by name.
#[must_use]
pub fn bundle_group(name: &str) -> Option<&'static BundleGroup> {
    BUNDLE_GROUPS.iter().find(|g| g.name == name)
}

/// The group an ENTRY is in: the group that names it, else core.
#[must_use]
pub fn group_of_entry(entry: &str) -> &'static BundleGroup {
    BUNDLE_GROUPS
        .iter()
        .find(|g| g.entries.contains(&entry))
        .unwrap_or(&BUNDLE_GROUPS[0])
}

/// The routing table's file name, in the wheel's `assets/`.
pub const ROUTING_FILE: &str = "bundles.json";

/// Where the emitted routing table is written — package SOURCE, beside the loader
/// page that reads it, so the wheel carries it without a build step.
pub const ROUTING_PATH: &str = "pkg-facet-wrapped/src/facett/assets/bundles.json";

/// **The ROUTING TABLE, emitted from [`BUNDLE_GROUPS`]** — one direction, like the
/// toml: `{"schema":2,"default":"core","groups":[{"name","dir","wasm","js",
/// "manifest","features","entries"}]}`, paths relative to the wheel's `assets/`.
/// The loader routes an entry to the group whose `entries` name it, else to
/// `default`. Hand-formatted: this crate carries no serde, and the shape is flat.
#[must_use]
pub fn routing_json() -> String {
    let q = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
    let list = |xs: &[&str]| xs.iter().map(|x| q(x)).collect::<Vec<_>>().join(", ");
    let groups: Vec<String> = BUNDLE_GROUPS
        .iter()
        .map(|g| {
            let [wasm, js, manifest] = g.outputs();
            format!(
                "    {{\"name\": {}, \"dir\": {}, \"wasm\": {}, \"js\": {}, \"manifest\": {}, \"features\": [{}], \"entries\": [{}]}}",
                q(g.name),
                q(g.wheel_dir),
                q(&g.wheel_path(&wasm)),
                q(&g.wheel_path(&js)),
                q(&g.wheel_path(&manifest)),
                list(g.features),
                list(g.entries)
            )
        })
        .collect();
    format!(
        "{{\n  \"_\": \"EMITTED by facett-build-thing (BUNDLE_GROUPS) — do not edit; re-run `cargo run -p facett-build-thing -- emit`\",\n  \"schema\": 2,\n  \"default\": {},\n  \"groups\": [\n{}\n  ]\n}}\n",
        q(CORE_GROUP),
        groups.join(",\n")
    )
}

// ── THE DEMO, which is a PRODUCT and not a library ───────────────────────────
//
// Added 2026-09-18. Until this pass the declaration held the wheel and the bundle
// it carries, and the argument for stopping there — "facett's seventy crates are
// LINKED by consumers, not built" — is right about the LIBRARIES and says nothing
// about `facett-demo`. The demo is not linked by anybody: it is a binary a human
// runs and a wasm bundle a browser loads, and both were reachable only by typing
// `cargo xtask demo-wasm` from memory. That is the same shape Road B closed for
// `wrap-wasm` one row up — a produced artifact with no declared door.

/// The declared EXE variant — the native demo binary.
pub const DEMO_VARIANT: &str = "facett-demo";

/// The declared WASM variant — the gh-pages bundle `deploy-pages` publishes.
///
/// A row of its own rather than a flag on [`DEMO_VARIANT`]: the two are built from
/// one crate but they are different artifacts with different toolchains, and a
/// variant that meant "the native one, unless…" could not be dispatched by name.
pub const DEMO_WASM_VARIANT: &str = "facett-demo-wasm";

/// The crate both demo rows build.
pub const DEMO_PACKAGE: &str = "facett-demo";

/// Where the `facett-demo-wasm` cell writes the gh-pages bundle (`xtask demo-wasm`
/// builds the row in the pinned image; edda `nornir_pages::stage_dist` adds the page).
pub const DEMO_WASM_DIST: &str = "dist";

/// The demo HOST's `panel` fixture row (a cargo EXAMPLE, see the row).
pub const PANEL_WASM_VARIANT: &str = "facett-wasm-demo-panel";
/// The reusable demo HOST every `<repo>-demo` mounts on.
pub const HARNESS_PACKAGE: &str = "facett-wasm-demo";
/// The example the fixture row builds.
pub const PANEL_EXAMPLE: &str = "panel";
/// Where the fixture's bundle lands (`panel.js`, `panel_bg.wasm`, + the page).
pub const PANEL_DIST: &str = "facett-wasm-demo/dist";

/// **The demo's wasm feature axis**, and it is not decoration: the native-only
/// `region-clip`/osm-katana lanes do not build for wasm32, and the GPU lanes are
/// what give the browser build the wgpu (WebGPU) backend and the real 3D OSM
/// render instead of the old glow/CPU painters; `gpu` (the instanced graph cloud) and
/// `graph-layout-gpu` (the compute-shader force layout, lent the host's device) joined
/// on 2026-09-19 — the same two lanes the wheel already selects and installs. `graph-wgpu`
/// (the 2-D GraphView on the same cloud) joined on 2026-10-01, the lane the wheel
/// already carries as `facett-graph/wgpu`. `flow-wgpu` (Falsett's resident curve and
/// packet renderer, `facett-flow/wgpu`) joined on 2026-10-04, so the pages demo draws
/// the full flow graph instead of the 2 000-point CPU preview. The same strings
/// `pages_cfg().extra_build_args` passes — one axis, declared where the forge can
/// read it rather than living only inside an xtask literal.
pub const DEMO_WASM_FEATURES: [&str; 6] =
    ["map-wgpu", "map3d-wgpu", "gpu", "graph-layout-gpu", "graph-wgpu", "flow-wgpu"];

/// The same axis as a comma-joined cargo argument.
///
/// Held EQUAL to [`DEMO_WASM_FEATURES`] by `tests/declared.rs` — two spellings of
/// one fact inside one crate is still two writers, and the test is what makes the
/// pair safe rather than the comment.
pub const DEMO_WASM_FEATURE_LIST: &str = "map-wgpu,map3d-wgpu,gpu,graph-layout-gpu,graph-wgpu,flow-wgpu";

/// **The demo's wasm build arguments, verbatim** — what `nornir_pages` passes and
/// what the `facett-demo-wasm` row declares, from ONE place.
///
/// This constant exists because the axis had two writers: a string literal in
/// `xtask::pages_cfg` and (since the row landed) the declaration. They agreed on
/// the day the row was written, which is the only day drift is impossible. The
/// xtask now reads this, so `cargo xtask deploy-pages` and
/// `cargo xtask build --variant facett-demo-wasm` cannot compile different code and
/// call it the same demo.
pub const DEMO_WASM_CARGO_ARGS: [&str; 3] =
    ["--no-default-features", "--features", DEMO_WASM_FEATURE_LIST];

// ── THE IMAGES THIS REPO MASTERS ─────────────────────────────────────────────
//
// `..BuildVariantSet::default()` used to carry the comment "no containers/vms/isos
// /installers/kernels (facett masters no media)". Half of that was true and the
// container half was not: three Containerfiles live in `facett-ops/src/containerfiles/`,
// this repo is the only writer of them, and `facett-ops` mints all three. They are
// declared in FILE MODE — the bytes on disk are the build input, so `RUN`, `ARG`
// and the digest-pinned `FROM` in each recipe are the declaration, not a synthesis
// this crate would have to re-say and drift from.

/// The image the Streamlit demo RUNS in.
pub const STREAMLIT_IMAGE: &str = "nordisk-streamlit";

/// The clean-consumer image `verify-install` proves the published wheel in.
pub const PIP_CONSUMER_IMAGE: &str = "nordisk-pip-consumer";

/// Where this repo's Containerfiles live, repo-root-relative. The context dir a
/// file-mode cell derives from each path is this directory, which is what
/// `facett_ops::holger`/`facett_ops::pip` stage as the build context.
pub const CONTAINERFILE_DIR: &str = "facett-ops/src/containerfiles";

/// **The MANIFEST digest of the one cargo toolchain image this constellation
/// compiles in** — the digest the committed edda recipe mints (see the re-pin note
/// below; it used to say "gunnar's value, to the character", and gunnar's value
/// was not what the recipe mints), because a second spelling would be a second
/// image nobody minted.
///
/// It is a digest and not a tag on purpose, and the sentence the forge refuses with
/// is the reason: *a named tag is a convenience, NOT a pin, and an unpinned build
/// image can be replaced under you between two builds that report the same
/// provenance*.
///
/// **What this digest identifies, said exactly, because the answer changed.** Until
/// edda `ac3852e` (2026-09-05) `image_sha256` was a PER-BOX fact: the cargo
/// Containerfile left its base, its archive and its clock unpinned, so every mint
/// landed a new digest and a pin named a minting EVENT rather than a recipe —
/// gunnar moved this line twice in one day for that reason, and
/// `edda/crates/nornir-build-thing/tests/toolchain_images_live.rs:67` still carries
/// the old finding ("a pinned digest needs an image PULLED from a shared
/// registry"). `ac3852e` made the cargo file make the four moves its kernel sibling
/// already made — base by digest, a `snapshot.debian.org` instant, `ENV
/// SOURCE_DATE_EPOCH=1783900800`, the clock's logs deleted in the layer that writes
/// them — and `DraupnirCargoRunner::build_toolchain_image` now mints through
/// `build_image_reproducible`. So the digest identifies the RECIPE
/// (`edda/crates/nornir-build-thing/src/containerfiles/cargo-toolchain.Containerfile`),
/// not one box's copy of it, and no registry is behind the name: there is nothing
/// to pull `nordisk-cargo-toolchain` FROM (`short-name did not resolve`, `403
/// denied`). A box that has never minted it mints it —
/// `cargo run -p nornir-build-thing --features cargo-container --example
/// mint_cargo_toolchain` — and lands these 64 characters.
///
/// MEASURED on t14s 2026-09-06 by this row: the box's own
/// `nordisk-cargo-toolchain:1.97.1` is `f29e517c1ba34b920c…`, minted 2026-08-25 —
/// eleven days BEFORE the recipe became reproducible, which is why it is a
/// different number and not a contradiction. That stale digest is also the constant
/// `dwarves/crates/dwarves/src/build_thing/code.rs:768` still hands its own test
/// scaffolding.
///
/// **RE-PINNED 2026-09-29 (lane C, container road), `2cf9b012…` → `3d5115ba…`.**
/// The first `cargo xtask build` that actually ENTERED this image refused by name:
/// the committed recipe (edda `nornir-build-thing` at `114fabb`) re-minted on this
/// box as a layer-cache hit — id `ab4b8dc004e8…`, `created` = 2026-07-13T00:00:00Z,
/// which is `SOURCE_DATE_EPOCH=1783900800`, i.e. the recipe's instant, not the
/// minute of the mint — and its MANIFEST digest is `3d5115ba…`. Nothing on this box
/// answered to `2cf9b012…`. The new value is not this box's opinion: it is the same
/// 64 characters edda's own recipe pins (`edda-build-thing/src/recipe.rs:237`) and
/// holger measured on loki on 2026-09-17 (`holger/xtask/src/main.rs`, "building in
/// nordisk-cargo-toolchain@sha256:3d5115ba…"). Two boxes, one recipe, one digest.
/// `2cf9b012…` was gunnar's value on the day this row was written and nothing had
/// ever built in it here — a pin nobody entered is a pin nobody checked.
///
/// **RE-PINNED 2026-09-30 (lane P, dwarf), `3d5115ba…` → `668468e7…`**: edda
/// `6b5f905` adds `clang llvm` to the recipe (C for wasm32 — zstd-sys under parquet
/// `zstd`, which `facett-demo-wasm` needs). Minted twice on dwarf, same digest.
pub const CARGO_TOOLCHAIN_SHA256: &str =
    "668468e775ad80f32d700cbc9709d41cac726f40d00649461a0932f30d1b2067";

/// The rustc release baked into the image above, and compared against what the
/// container reports — a mismatch is a NAMED refusal, not a shrug.
pub const RUST_VERSION: &str = "1.97.1";

/// **The sibling CHECKOUTS facett's path dependencies escape into**, by directory
/// name under the repo's own parent. Not discovered — DECLARED, because each name is
/// one more tree that ends up inside the build container.
///
/// MEASURED 2026-09-06 by walking every `Cargo.toml` in the repo (the same walk
/// `escaping_path_deps` does): `facett-core`, `facett-cfd`, `facett-docview`,
/// `facett-graph3d`, `facett-graphnav`, `facett-map3d` and `facett-demo` reach into
/// `znippy-zoomies`; `xtask`, `facett-about`, `facett-claude`, `facett-deploykey`,
/// `facett-zoneui`, `facett-pipeline`, `facett-demo`, `facett-buildthing` and THIS
/// crate reach into `edda`; `facett-pipeline` and `facett-demo` into `knut`;
/// `facett-demo` into `korp-collectors`; `xtask` into `draupnir`; `bench` into
/// `nornir`. Undeclared, the container holds facett alone and cargo dies on
/// `failed to load manifest for workspace member … failed to read
/// ../znippy-zoomies/Cargo.toml` — an error naming a path instead of a
/// declaration.
///
/// **`funnel-core` is on this list and it is NOT one of facett's own escapes**, so
/// it is worth saying why rather than letting a reader hunt for the dependency that
/// is not there. `check_escaping_path_deps` walks THIS repo's manifests only, and by
/// that walk the list was complete — and the container still refused, MEASURED at
/// the door on 2026-09-07 with the six above declared:
///
/// ```text
/// error: failed to get `funnel-core` as a dependency of package
///        `dwarves-runa v0.2.2 (/work/edda/crates/dwarves-runa)`
/// Caused by: failed to read `/work/funnel-core/Cargo.toml`
/// ```
///
/// A mounted sibling has escapes of its own. `facett-pipeline` and `facett-demo`
/// reach into `edda/crates/dwarves-runa`, whose manifest reaches on into
/// `../../../funnel-core` — a second hop the checker never looks at, because it
/// starts and stops inside the repo it is given. So this list is the TRANSITIVE
/// closure and the check is only its first level; the closure was read off `cargo
/// metadata` (`draupnir`, `edda`, `funnel-core`, `knut`, `znippy-zoomies` resolve;
/// `korp-collectors` and `nornir` are declared-but-unresolved — optional, and
/// `bench/` is a detached workspace — and stay listed because the first-level check
/// demands every name it can see).
pub const SIBLINGS: [&str; 7] =
    ["draupnir", "edda", "funnel-core", "knut", "korp-collectors", "nornir", "znippy-zoomies"];

/// **The `[build.toolchain]` cell — the pinned image every CARGO build of this repo
/// runs in, including the forge's build of THIS crate.**
///
/// ## The defect this closes
///
/// Until 2026-09-06 facett declared no toolchain at all, and the comment where it
/// should have been said *"no `toolchain` (the package cell names its own pinned
/// image)"*. That conflated two images that are not the same image.
/// [`PIP_TOOLCHAIN_IMAGE`] is where `python -m build` runs; this one is where CARGO
/// runs. The consequence was measured at the real door on t14s:
///
/// ```text
/// ── kod · cargo build -p facett-build-thing --lib --release --target wasm32-unknown-unknown
/// ── kod · VÄGRAT: [build.toolchain] i `/home/rickard/git/facett`: [build.toolchain]
///    `nordisk-cargo-toolchain`: no `image_sha256` — a named tag is a convenience, NOT a
///    pin, and an unpinned build image can be replaced under you between two builds that
///    report the same provenance
/// ── väg · deklarationen (toml) — kodvägen VÄGRAD: …
/// ```
///
/// The forge's code road builds this crate to wasm32 INSIDE the repo's declared
/// toolchain, and it resolves that toolchain with `…toolchain.unwrap_or_default()`
/// followed by `validate()`
/// (`dwarves/crates/dwarves/src/build_thing/code.rs:605-608`). An undeclared
/// toolchain therefore arrives as `CargoToolchain::default()` — the right IMAGE
/// NAME and an empty digest — and is refused for being unpinned. The refusal is
/// correct; what it cost is the inversion this crate exists to prevent. With the
/// code road shut, the forge falls back to PARSING `.nornir/build-variants.toml`,
/// so the file that is EMITTED to describe the build becomes the thing that drives
/// it, and the declaration nobody could load is the one in Rust.
///
/// ## Why the shape is gunnar's (the digest is the recipe's)
///
/// LAW 5, reuse and never twin. gunnar is the repo that brought
/// `[build.toolchain]` into loader vocabulary
/// (`nornir-build-thing` 0.1.9, 2026-08-26) and `gunnar-recipe`'s `toolchain()` is
/// the shape this follows key for key. The rows differ in [`SIBLINGS`] — a fact
/// about THIS repo's path dependencies — and, since 2026-09-29, in the digest:
/// facett pins what the committed recipe mints (see [`CARGO_TOOLCHAIN_SHA256`]).
///
/// `targets` names all three triples although facett builds on only one of them
/// (`wasm32-unknown-unknown`, the recipe crate and the `facet-wrapped-wasm` bundle).
/// The field is a statement about what the IMAGE carries, not about what this repo
/// uses — `check_declared_targets` holds it against the Containerfile's own `ARG
/// RUST_TARGETS`, and that is the only thing that can put a std inside — so
/// declaring two of three would be declaring the image wrong.
///
/// `network = false` is what makes the compile airgapped: the runner adds
/// `--network none --offline --locked` and binds the host registry read-only.
fn the_cargo_toolchain() -> CargoToolchain {
    CargoToolchain {
        image: CARGO_TOOLCHAIN_IMAGE.to_string(),
        image_sha256: CARGO_TOOLCHAIN_SHA256.to_string(),
        rust_version: RUST_VERSION.to_string(),
        targets: vec![
            "x86_64-unknown-linux-gnu".to_string(),
            "x86_64-unknown-linux-musl".to_string(),
            "wasm32-unknown-unknown".to_string(),
        ],
        timeout_secs: 3600,
        mem_limit_mb: 8192,
        cpus: 8,
        network: false,
        remap_paths: true,
        siblings: SIBLINGS.iter().map(|s| (*s).to_string()).collect(),
    }
}

/// facett's declaration.
#[derive(Debug, Clone, Copy, Default)]
pub struct FacettBuildThing;

// **The forge loads this crate as CODE**, not as the file it emits.
//
// One source, two link targets: the `cdylib` is the wasm32 module
// `dwarves buildthing facett` loads and CALLS, in a sandbox granted nothing; the
// `rlib` is what the tests and facett's own xtask link. Without it the forge falls
// back to parsing the emitted toml and says so out loud — which is reading the
// description instead of asking the thing it describes.
nornir_build_thing::export_build_thing!(FacettBuildThing);

/// The `[[build.wasm]]` cell of ONE bundle group — the TYPED STAGES that turn cargo's raw
/// `facet_wrapped.wasm` into the bundle the wheel carries.
///
/// This is Road B, and it replaces a step nobody declared. Until 2026-09-06 the
/// three files below were produced by a human running `cargo xtask wrap-wasm` from
/// memory, and the wheel's door worked only afterwards — two lanes hand-built that
/// day because the step is invisible to the declaration.
///
/// Both stages are TYPED. There is no command field here and there must never be
/// one: a build declaration with a command in it is a shell script with extra
/// steps. `BindgenTarget::Web` is an enum value, not a string; the output filenames
/// are DERIVED from `facet-wrapped` (→ `facet_wrapped.js`, `facet_wrapped_bg.wasm`)
/// so the `_bg` split cannot be misspelled into a wheel that stages a file nobody
/// wrote; and the bindgen VERSION is not here at all — it comes from `Cargo.lock`,
/// because a CLI that does not match the lock produces a bundle that loads, runs
/// and paints nothing.
fn the_wasm_bundle(g: &BundleGroup) -> WasmBuild {
    WasmBuild {
        name: g.variant.to_string(),
        out_dir: g.dist_dir.to_string(),
        // ① `--target web --no-typescript`, which is what `wrap-wasm` has always
        // passed: the loader page is a plain `<script type="module">`, not a
        // bundler build, and a `.d.ts` nobody consumes is a file in the bundle that
        // no declaration accounts for.
        bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
        // ② the component manifest — `facet-wrapped/examples/manifest.rs`, ten lines
        // that print `Registry::with_builtins().manifest_json()` on stdout so a
        // loader can enumerate components without instantiating the module first.
        //
        // THIS STAGE IS WHY ROAD B IS TWO STAGES AND NOT ONE. `wrap-wasm` is three
        // steps, not two, and `manifest.json` is a cargo-example capture rather
        // than bindgen output. A bindgen-only cell would have left the wheel's
        // third input undeclared and the door refusing on it one stage later —
        // which is the very "promises a product it does not produce" defect the
        // `from_variant` chain was refuted for.
        manifest: Some(ComponentManifestStage {
            example: "manifest".to_string(),
            file: "manifest.json".to_string(),
        }),
    }
}

/// The COMPONENTS wheel's `[[build.package]]` cell (PyPI `facett`) — what the release IS.
fn the_package() -> PackageBuild {
    PackageBuild {
        name: VARIANT.to_string(),
        format: PackageFormat::Pip,
        distribution: DISTRIBUTION.to_string(),
        out_dir: OUT_DIR.to_string(),
        source: Some(SOURCE.to_string()),
        // WHEEL ONLY, since 0.2.0 (row 10's facet-wrapped-web release carried an sdist).
        // This wheel's payload is PREBUILT wasm: the Rust that produces it is not in
        // the package and cannot be rebuilt by `pip install` from a source tarball. An
        // sdist would be a tarball of the same binaries wearing the "source" label —
        // what PyPI users and auditors take an sdist to be is exactly what it would not
        // be. The source is the repository; the wheel says where (Project-URL).
        sdist: false,
        image: Some(PIP_TOOLCHAIN_IMAGE.to_string()),
        // EVERY bundle group's three files, each where the wheel holds it
        // (`assets/` for core — unchanged — and `assets/<group>/` for the others),
        // derived from [`BUNDLE_GROUPS`]. The routing table is package SOURCE
        // (emitted into `pkg-facet-wrapped/src/…/assets/`), so it is not an input.
        inputs: BUNDLE_GROUPS
            .iter()
            .flat_map(|g| {
                g.outputs().into_iter().map(move |f| {
                    format!("{}/{f}:src/{DISTRIBUTION}/assets/{}", g.dist_dir, g.wheel_path(&f))
                })
            })
            .collect(),
        // NOT a `from_variant` chain, and the field below is why. `from_variant`
        // means "cargo-build this variant", and cargo does not produce a wasm
        // BUNDLE: it writes `facet_wrapped.wasm` (11 647 012 B) while this package
        // stages `facet_wrapped_bg.wasm` (10 116 976 B — bindgen's rewrite), a JS
        // glue module and a manifest. Measured 2026-09-06.
        from_variant: Vec::new(),
        // ⚠ NO `from_wasm` since the bundle groups (2026-09-29), and this is a
        // LOSS, said as one. edda's `from_wasm` is an `Option<String>` — ONE
        // producer cell — and `validate_package` refuses any input that one cell
        // does not write, so a wheel carrying three groups' bundles cannot name a
        // producer at all. What moved to facett until edda takes a LIST: the
        // bijection (every input is written by exactly one group cell, every group
        // output is staged) is `tests/declared.rs::the_wheel_stages_exactly_what_the_group_cells_produce`,
        // and the forge now refuses a missing group input BY NAME at staging instead
        // of building the cells first.
        from_wasm: None,
    }
}

/// The DEMO wheel's `[[build.package]]` cell (PyPI `facett-demo`).
///
/// Pure Python: the app, its panes, the gallery's payload table and the error-code
/// catalog, all package SOURCE under [`DEMO_SOURCE`] — so it has NO `inputs` (nothing
/// is built into it) and it DOES have an sdist, because that tarball genuinely is the
/// source and `pip` can rebuild the wheel from it. It names the components wheel only
/// as a dependency (`facett[streamlit]`), never by staging its files.
fn the_demo_package() -> PackageBuild {
    PackageBuild {
        name: DEMO_PIP_VARIANT.to_string(),
        format: PackageFormat::Pip,
        distribution: DEMO_DISTRIBUTION.to_string(),
        out_dir: DEMO_OUT_DIR.to_string(),
        source: Some(DEMO_SOURCE.to_string()),
        sdist: true,
        image: Some(PIP_TOOLCHAIN_IMAGE.to_string()),
        inputs: Vec::new(),
        from_variant: Vec::new(),
        from_wasm: None,
    }
}

/// The `[[build.wasm]]` cell for the DEMO bundle — the second wasm row, and
/// deliberately a thinner one than [`the_wasm_bundle`].
///
/// One stage, not two. `facet-wrapped`'s bundle needs a `manifest` stage because
/// the wheel carries a component manifest a loader reads before instantiating the
/// module; the demo carries no such file — `nornir_pages::stage_dist` writes a site
/// shell around the bundle instead, and a manifest stage here would declare a file
/// nothing produces and nothing reads.
fn the_demo_wasm_bundle() -> WasmBuild {
    WasmBuild {
        name: DEMO_WASM_VARIANT.to_string(),
        out_dir: DEMO_WASM_DIST.to_string(),
        // `--target web --no-typescript`, the same two flags `nornir_pages` passes:
        // the loader is a plain `<script type="module">` and a `.d.ts` nobody
        // consumes is a file in the bundle no declaration accounts for.
        bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
        manifest: None,
    }
}

/// The fixture's cell: bindgen only, like the demo's — the harness's own
/// `IndexHtml` writes the page around it (`xtask panel-wasm`).
fn the_panel_wasm_bundle() -> WasmBuild {
    WasmBuild {
        name: PANEL_WASM_VARIANT.to_string(),
        out_dir: PANEL_DIST.to_string(),
        bindgen: Some(BindgenStage { target: BindgenTarget::Web, typescript: false }),
        manifest: None,
    }
}

/// The three `[[build.container]]` cells — every image this repo masters, in FILE
/// MODE.
///
/// File mode and not synthesis, for a reason that is the same law as everywhere
/// else here: the Containerfiles exist, they are the input `xtask` already builds,
/// and re-saying their `FROM debian@sha256:…`, their `ARG STREAMLIT_VERSION` and
/// their self-asserting `RUN` steps as `base`/`layers` in Rust would be a second
/// copy of a recipe that is already written down. A synthesized cell cannot express
/// a `RUN` at all, so the copy would also be a WORSE one.
///
/// What this does NOT declare is the run: `xtask streamlit` starts the demo and
/// `xtask test-demo` runs its suite, and neither is a build. A build thing builds
/// artifacts; running one belongs in a pipeline.
fn the_containers() -> Vec<ContainerBuild> {
    [
        (PIP_TOOLCHAIN_IMAGE, "pip-toolchain.Containerfile"),
        (PIP_CONSUMER_IMAGE, "pip-consumer.Containerfile"),
        (STREAMLIT_IMAGE, "streamlit.Containerfile"),
    ]
    .into_iter()
    .map(|(name, file)| ContainerBuild {
        name: name.to_string(),
        containerfile: format!("{CONTAINERFILE_DIR}/{file}"),
        ..ContainerBuild::default()
    })
    .collect()
}


/// **The `[[build.variant]]` row of one bundle group.** The core group's row is
/// exactly the row `facet-wrapped-wasm` has always been (no features, defaults on),
/// so its argv — and so its bytes — do not move.
fn group_row(g: &BundleGroup) -> BuildVariant {
    BuildVariant {
        name: g.variant.to_string(),
        suites: vec![SUITE.to_string()],
        kind: ArtifactKind::Wasm,
        package: Some(g.package.to_string()),
        // Mandatory for `kind = "wasm"` and it rides `cargo_args`.
        target: Some("wasm32-unknown-unknown".to_string()),
        profile: Profile::Release,
        no_default_features: g.no_default_features,
        features: g.features.iter().map(|f| (*f).to_string()).collect(),
        ..BuildVariant::default()
    }
}

/// Rendering capabilities whose isolated and unified builds must stay supported.
pub const FLOW_FEATURES: &[&str] = &[
    "host", "labels", "boxes", "arrows", "status", "rate", "layered",
    "compact-state", "hit-test", "lod",
];

fn flow_rows() -> Vec<BuildVariant> {
    let row = |name: String, features: Vec<String>, wasm: bool| BuildVariant {
        name,
        suites: vec![SUITE.to_string()],
        kind: ArtifactKind::Lib,
        shape: nornir_build_thing::variant::ServiceShape::NotApplicable,
        package: Some("facett-flow".to_string()),
        target: wasm.then(|| "wasm32-unknown-unknown".to_string()),
        profile: Profile::Release,
        no_default_features: true,
        features,
        ..BuildVariant::default()
    };
    let mut rows = vec![
        row("facett-flow-plain".into(), vec![], false),
        row("facett-flow-wgpu".into(), vec!["wgpu".into()], false),
    ];
    for feature in FLOW_FEATURES {
        rows.push(row(format!("facett-flow-{feature}"), vec![(*feature).into()], false));
        rows.push(row(format!("facett-flow-{feature}-wgpu"), vec![(*feature).into(), "wgpu".into()], false));
    }
    let all: Vec<String> = FLOW_FEATURES.iter().copied().chain(["wgpu"]).map(str::to_string).collect();
    rows.push(row("facett-flow-all".into(), all.clone(), false));
    rows.push(row("facett-flow-all-wasm32".into(), all, true));
    rows
}

impl BuildThing for FacettBuildThing {
    fn manifest(&self) -> Manifest {
        Manifest {
            abi: ABI_VERSION,
            repo: "facett".to_string(),
            source_crate: KRATE.to_string(),
            summary: "what facett PRODUCES, in three groups: the two PyPI releases, \
                      facett (the components wheel) and facett-demo (the Streamlit \
                      app wheel + sdist), built inside a pinned container with the \
                      network off, beside the SHA256SUMS a downloader checks them \
                      with and a provenance record over the produced bytes, plus the \
                      wasm bundles the facett wheel carries; the DEMO, \
                      native and as the gh-pages wasm bundle; and the three container \
                      IMAGES this repo masters. facett's seventy library crates are \
                      linked, not catalogued here; Falsett additionally declares \
                      isolated and unified feature verification builds."
                .to_string(),
        }
    }

    fn variants(&self) -> BuildVariantSet {
        // The container ROWS are derived from the container CELLS, not typed out
        // beside them: a roster entry and a cell that disagree about an image name
        // is a variant the forge refuses as a dangling `container_ref`, and the only
        // way two lists cannot disagree is for there to be one list.
        let container_rows = the_containers().into_iter().map(|c| BuildVariant {
            name: c.name.clone(),
            suites: vec![SUITE.to_string()],
            kind: ArtifactKind::Container,
            container_ref: Some(c.name.clone()),
            ..BuildVariant::default()
        });
        BuildVariantSet {
            // ONE delivery, named rather than left implicit, so --suite ORIGINAL
            // resolves here instead of falling through to UNSUITED.
            base_suite: Some(SUITE.to_string()),
            // THE BUNDLE GROUPS first — one wasm row per [`BUNDLE_GROUPS`] entry,
            // core (`facet-wrapped-wasm`) leading and unchanged — then the rest.
            variants: BUNDLE_GROUPS.iter().map(group_row).chain(vec![
            BuildVariant {
                name: VARIANT.to_string(),
                suites: vec![SUITE.to_string()],
                kind: ArtifactKind::Package,
                // The crate the wheel's wasm comes from. It is the package the
                // release is ABOUT, and recording it means the receipt names the
                // Rust that ended up in the browser.
                package: Some("facet-wrapped".to_string()),
                profile: Profile::Release,
                ..BuildVariant::default()
            }, BuildVariant {
                // THE DEMO WHEEL (PyPI `facett-demo`): pure Python, no cargo package
                // behind it — the app that mounts the components wheel.
                name: DEMO_PIP_VARIANT.to_string(),
                suites: vec![SUITE.to_string()],
                kind: ArtifactKind::Package,
                profile: Profile::Release,
                ..BuildVariant::default()
            }, BuildVariant {
                // THE DEMO, native. `cargo build -p facett-demo --release` and
                // nothing else — no feature axis, because the native build is the
                // default one and a variant that re-stated the defaults would be a
                // second writer of them.
                name: DEMO_VARIANT.to_string(),
                suites: vec![SUITE.to_string()],
                kind: ArtifactKind::Exe,
                package: Some(DEMO_PACKAGE.to_string()),
                profile: Profile::Release,
                ..BuildVariant::default()
            }, BuildVariant {
                // THE DEMO, in a browser. Same crate, different artifact, and the
                // feature axis is the difference: `--no-default-features` drops the
                // native-only passthroughs that do not build for wasm32, and the two
                // GPU lanes put the WebGPU backend and the real 3D OSM render back.
                name: DEMO_WASM_VARIANT.to_string(),
                suites: vec![SUITE.to_string()],
                kind: ArtifactKind::Wasm,
                package: Some(DEMO_PACKAGE.to_string()),
                // The crate's DEFAULT bin, named: cargo writes a bin's module under
                // its name as written (`facett-demo.wasm`) and wasm-bindgen keeps that
                // stem (`facett-demo.js`, the names the live page has always served).
                // Unnamed, the stage looked for the cdylib spelling `facett_demo.wasm`
                // (MEASURED on dwarf 2026-09-29) — and `--bin` also stops the build
                // compiling the five native-only demo bins for wasm32.
                bin: Some(DEMO_PACKAGE.to_string()),
                target: Some("wasm32-unknown-unknown".to_string()),
                profile: Profile::Release,
                no_default_features: true,
                features: DEMO_WASM_FEATURES.iter().map(|f| (*f).to_string()).collect(),
                ..BuildVariant::default()
            }, BuildVariant {
                // THE DEMO HOST'S FIXTURE — the `panel` EXAMPLE of `facett-wasm-demo`,
                // the smallest complete bundle on the harness's web path, which the
                // WebGPU render guard (`facett-wasm-demo/tests/verify_webgpu_render.py`)
                // drives. It is a cargo example, and until edda's `BuildVariant` grew
                // `example` (2026-09-29) it could not be a row, so `xtask panel-wasm`
                // spelled its own argv. Not a shipped artifact: a guard's input.
                name: PANEL_WASM_VARIANT.to_string(),
                suites: vec![SUITE.to_string()],
                kind: ArtifactKind::Wasm,
                package: Some(HARNESS_PACKAGE.to_string()),
                example: Some(PANEL_EXAMPLE.to_string()),
                target: Some("wasm32-unknown-unknown".to_string()),
                profile: Profile::Release,
                ..BuildVariant::default()
            }])
            .chain(container_rows)
            .chain(flow_rows())
            .collect(),
            wasms: BUNDLE_GROUPS
                .iter()
                .map(the_wasm_bundle)
                .chain(std::iter::once(the_demo_wasm_bundle()))
                .chain(std::iter::once(the_panel_wasm_bundle()))
                .collect(),
            packages: vec![the_package(), the_demo_package()],
            containers: the_containers(),
            // **The pinned CARGO image.** It used to say "no `toolchain` (the
            // package cell names its own pinned image)", and that sentence was the
            // defect: the package cell's image is where PYTHON runs, and naming it
            // said nothing about where cargo runs. Undeclared, the forge's code
            // road resolved `CargoToolchain::default()`, refused it for carrying no
            // digest, and fell back to parsing the toml this code EMITS — the
            // description driving the build it was written to describe.
            toolchain: Some(the_cargo_toolchain()),
            allow_unbuilt_bins: allow_unbuilt_bins(),
            // Not declared by facett, and each absence is a fact rather than a gap:
            // no `offers`/`required`/`exclusive` (neither row has a feature axis), no
            // containers/vms/isos/installers/kernels (facett masters no media).
            ..BuildVariantSet::default()
        }
    }
}

/// **The `[[bin]]` targets no row builds, each with its reason** —
/// `[build.allow_unbuilt_bins]`. ⚒ Build's gate (`buildgate::tally_bins`) counts every
/// bin cargo can see in this tree; MEASURED 2026-10-16 on loki these nine had neither
/// a row nor a reason (`tests/build_gate.rs` now asks the same question).
fn allow_unbuilt_bins() -> std::collections::BTreeMap<String, AllowEntry> {
    [
        ("facett-build-thing", "this recipe crate's own emit door (`cargo run -p facett-build-thing -- emit`); the forge loads the crate as wasm, the bin is repository tooling, never shipped."),
        ("xtask", "the `cargo xtask` thin door onto this recipe — repository tooling, never shipped."),
        ("facett-ops", "`cargo ops <cmd>`: the verbs that are NOT builds (publish a wheel, pack holger's index, run the Streamlit demo, push the Pages site) — repository tooling, never shipped."),
        ("devid_release_probe", "facett-app's RELEASE-profile probe for the DEV-ID chip, behind `required-features = [\"devid-probe\"]`; built and run by facett-app/tests/devid_release_profile.rs, never by hand — a test subject, not an artifact."),
        ("facett_robot", "facett-demo's robot-UI driver (`required-features = [\"robotui\"]`): the headless/watch replayer the demo's UI tests drive; the native demo row builds the default features and so never this bin."),
        ("errcode_registry_json", "a generator: exports the compiled facett_core::errcode::REGISTRY as JSON for the Python/Streamlit demo; run by hand to regenerate, never shipped."),
        ("errcode_registry_md", "a generator: regenerates the error-code registry markdown from the compiled REGISTRY; run by hand and redirected, never shipped."),
        ("gen_skull", "a one-off asset baker (`cargo run -p facett-model3d --bin gen_skull`): the committed assets/skull.glb is its output; re-run only to regenerate."),
        ("facett-flow-bench", "dev/flow-bench — a development-only timing harness in its own [workspace]; no runtime crate depends on it and nothing ships it."),
    ]
    .into_iter()
    .map(|(k, v)| (k.to_string(), AllowEntry::Reason(v.to_string())))
    .collect()
}

/// The header every emission starts with — says what the file is, and what it is not.
pub const EMITTED_HEADER: &str = "\
# facett — .nornir/build-variants.toml — EMITTED, NOT SOURCE.
#
# Generated by `cargo run -p facett-build-thing -- emit` from facett-build-thing
# (the Rust `impl BuildThing`). The code is the one source; this file DESCRIBES the
# build for readers and for the doors that discover a repo by this path — the
# dwarves forge reads it with its own pin. Editing it changes NOTHING: re-emit after
# changing the code. Do not edit by hand.
#
# Release and verification rows, in five groups. facett is a library
# constellation: its seventy crates are LINKED by consumers, not built as variants,
# and declaring them would be a build matrix invented to look complete. What is
# declared is what facett PRODUCES, plus Falsett's focused feature checks.
#
# ① THE RELEASES — the two PyPI packages (2026-10-02, 0.2.0): `facett-wheel` (PyPI
# `facett`: the components, the loader, Streamlit glue behind the [streamlit] extra;
# a wheel and NO sdist, because its payload is prebuilt wasm a source tarball could
# not rebuild) and `facett-demo-wheel` (PyPI `facett-demo`: the pure-Python Streamlit
# app, wheel + sdist). Each lands beside the SHA256SUMS a downloader runs
# `sha256sum -c` against and a provenance record over the produced bytes — plus the
# wasm BUNDLES the facett wheel carries, one row per BUNDLE GROUP (core = the default
# bundle; docs and model3d since 2026-09-29, sim since 2026-10-02), each a row of its
# own because cargo does not produce a bundle. The loader routes by `bundles.json`,
# emitted beside this file from the same code (pkg-facet-wrapped/src/facett/assets/).
#
# ② THE DEMO (added 2026-09-18) — `facett-demo` as a native binary and as the
# gh-pages wasm bundle. These are not libraries anybody links: they are a program a
# human runs and a page a browser loads, and until this pass they were reachable
# only by typing `cargo xtask demo-wasm` from memory — a produced artifact with no
# declared door, which is the same gap Road B closed for `wrap-wasm`. The wasm row
# carries the feature axis (`--no-default-features` + the two GPU lanes) that the
# xtask literal used to be the only writer of.
#
# ③ THE IMAGES (added 2026-09-18) — the three Containerfiles in
# `facett-ops/src/containerfiles/`, declared in FILE MODE because the bytes on disk are
# the build input: their digest-pinned `FROM`, their `ARG`s and their self-asserting
# `RUN` steps are the recipe, and a synthesized cell cannot express a `RUN` at all.
# The header used to say facett mastered no media; that was true of vms/isos and
# false of images, and this repo is the only writer of all three.
#
# ④ THE FIXTURE (added 2026-09-29) — `facett-wasm-demo-panel`, the demo HOST's
# `panel` EXAMPLE built to wasm + bindgen: the bundle the WebGPU render guard drives.
# A guard's input, not a shipped artifact; a row because `example` is now a field,
# so `xtask panel-wasm` no longer spells its own cargo argv.
#
# ⑤ FALSETT VERIFICATION — default, GPU, each optional capability alone and with
# GPU, and all capabilities together on native and wasm32. These rows verify
# feature unification used by pipeline, plandag and brokkr embedders.
#
# What is deliberately NOT here: RUNNING any of it. `xtask streamlit` starts the
# demo and `xtask test-demo` runs its suite; neither is a build, and a build thing
# that declared a `podman run` would be a pipeline wearing the wrong hat. `cargo build -p facet-wrapped --target
# wasm32-unknown-unknown` writes ONE file, facet_wrapped.wasm; the wheel stages
# facet_wrapped_bg.wasm (a different, wasm-bindgen-rewritten file), a JS glue module
# and a component manifest. `[[build.wasm]]` declares the typed stages that make
# those, and `from_wasm` on the package chains the two so the forge builds them in
# order — an edge that is CHECKED against what the bundle says it writes, not merely
# recorded. The reasoning is in the crate's own module docs; an emission carries
# data, not argument, and a comment written here is deleted by the next `emit`
# without anybody noticing.
#
# The LAST table is `[build.toolchain]`, and it is what makes every cargo row above
# reachable from the code at all. It pins `nordisk-cargo-toolchain` by MANIFEST
# digest — the image every cargo build of this repo runs in, the forge's build of
# `facett-build-thing` to wasm32 included. Undeclared (before 2026-09-06) the forge
# resolved the default, refused it for being an unpinned tag, and fell back to
# PARSING this file: the emitted description driving the build instead of describing
# it. The digest is what the committed edda recipe mints (the same one edda and holger pin) — one reproducible recipe, one
# image — and a box that has never minted it mints it with
# `cargo run -p nornir-build-thing --features cargo-container --example
# mint_cargo_toolchain`; there is no registry to pull it from.";

/// The emission — what `.nornir/build-variants.toml` must contain.
pub fn emitted_toml() -> Result<String> {
    emit::emitted_toml(EMITTED_HEADER, &FacettBuildThing)
}

/// Regenerate `<repo_root>/.nornir/build-variants.toml` from the code.
pub fn write_emitted(repo_root: &Path) -> Result<PathBuf> {
    emit::write_emitted(EMITTED_HEADER, &FacettBuildThing, repo_root)
}

/// facett's repo root, from this crate's manifest directory.
#[must_use]
pub fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("facett-build-thing has a parent")
        .to_path_buf()
}

/// Regenerate the loader's routing table (`<repo_root>/`[`ROUTING_PATH`]) from
/// [`BUNDLE_GROUPS`]. Output only — like the toml, it DESCRIBES the build and is
/// never read back as authority; `tests/thin_door.rs` holds the committed bytes to
/// [`routing_json`].
pub fn write_routing(repo_root: &Path) -> Result<PathBuf> {
    let p = repo_root.join(ROUTING_PATH);
    std::fs::write(&p, routing_json())
        .map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?;
    Ok(p)
}

/// **The stage runner the bundle groups need** — edda's `XtaskWasmStageRunner`,
/// plus the one thing it cannot do: capture a group's component manifest from the
/// registry built with THAT GROUP'S features.
///
/// edda's `WasmStageRun::ComponentManifest` carries package, example and out file,
/// and its runner spawns `cargo run --example <e> -p <pkg>` with default features.
/// For `core` that is exactly right, so core is delegated untouched (its manifest
/// bytes do not move). For a feature-gated group it would capture the CORE registry
/// into the docs bundle's `manifest.json` — a loader would then route every core
/// entry to the docs bundle and find none of the docs ones. So the feature axis is
/// read off the ROW (never re-spelled) and the example runs with it.
///
/// The live leg of this crate, behind `wasm-stage` like edda's: the forge compiles
/// this crate to wasm32 as a declaration, and a declaration runs no tools.
#[cfg(feature = "wasm-stage")]
pub mod stages {
    use std::path::Path;

    use anyhow::{anyhow, Result};
    use nornir_build_thing::variant::BuildVariant;
    use nornir_build_thing::wasm::{WasmStageRun, WasmStageRunner, XtaskWasmStageRunner};

    /// A runner bound to the row whose stages it runs.
    pub struct GroupStageRunner<'a> {
        /// The group's `[[build.variant]]` row — the feature axis comes from here.
        pub variant: &'a BuildVariant,
    }

    impl GroupStageRunner<'_> {
        /// The feature arguments of the row, for the manifest example — the same
        /// two fields `cargo_args()` renders, in the same spelling.
        #[must_use]
        pub fn feature_args(&self) -> Vec<String> {
            let mut a = Vec::new();
            if self.variant.no_default_features {
                a.push("--no-default-features".to_string());
            }
            if !self.variant.features.is_empty() {
                a.push("--features".to_string());
                a.push(self.variant.features.join(","));
            }
            a
        }
    }

    impl WasmStageRunner for GroupStageRunner<'_> {
        fn describe(&self) -> String {
            format!("{} + the manifest example with row '{}''s features", XtaskWasmStageRunner.describe(), self.variant.name)
        }

        fn run(&self, run: &WasmStageRun, repo_root: &Path) -> Result<()> {
            let feats = self.feature_args();
            let WasmStageRun::ComponentManifest { package, example, out_file } = run else {
                return XtaskWasmStageRunner.run(run, repo_root);
            };
            if feats.is_empty() {
                return XtaskWasmStageRunner.run(run, repo_root);
            }
            let out = std::process::Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
                .current_dir(repo_root)
                .args(["run", "--quiet", "--example", example, "-p", package])
                .args(&feats)
                .output()
                .map_err(|e| anyhow!("spawn the `{example}` example of `{package}` ({}): {e}", feats.join(" ")))?;
            if !out.status.success() {
                let tail: Vec<String> = String::from_utf8_lossy(&out.stderr).lines().rev().take(20).map(str::to_string).collect();
                return Err(anyhow!(
                    "the `{example}` example of `{package}` with {} exited {} — no manifest to capture for row '{}'\n{}",
                    feats.join(" "),
                    out.status,
                    self.variant.name,
                    tail.into_iter().rev().collect::<Vec<_>>().join("\n")
                ));
            }
            if out.stdout.is_empty() {
                return Err(anyhow!(
                    "the `{example}` example of `{package}` with {} exited 0 and printed NOTHING — an empty \
                     manifest would route no entry to row '{}'",
                    feats.join(" "),
                    self.variant.name
                ));
            }
            std::fs::write(out_file, &out.stdout).map_err(|e| anyhow!("write {}: {e}", out_file.display()))
        }
    }
}