facett xtask — thin-door audit (LAW: "xtask is a THIN DOOR onto the build thing")
Measured on
origin/main=537ea35, 2026-09-29, lane C. xtask = 2342 lines (main.rs525 ·pip.rs746 ·holger.rs880 ·jsguard.rs178 ·lib.rs13). The recipe isfacett-build-thing(679 + 70 lines), 7 rows, all taggedORIGINAL.
The law: xtask owns no package name, no feature string, no triple, no list of rows; cargo xtask build [--variant] [--list] resolves through the RECIPE and materializes every argv with nornir_build_thing::build_variant; rows cargo cannot build are listed and refused BY NAME with the door that builds them; one dependency, the recipe, which re-exports nornir_build_thing.
1. Build knowledge xtask owned at 537ea35
| # | where (537ea35) | knowledge | recipe already declared? | disposition |
| K1 | xtask/src/main.rs:42 |
triple wasm32-unknown-unknown (WASM_TARGET) |
yes — target on rows facet-wrapped-wasm, facett-demo-wasm |
deleted; the row's target rides cargo_args() |
| K2 | xtask/src/main.rs:44 |
package facet-wrapped (WRAP_CRATE) |
yes — WASM_PACKAGE, row facet-wrapped-wasm |
deleted; wrap-wasm = the row by name |
| K3 | xtask/src/main.rs:252-257 |
argv build --release --lib -p facet-wrapped --target wasm32-… |
yes — the row materializes build -p facet-wrapped --profile release --target wasm32-… |
replaced by nornir_build_thing::build_variant(set, "facet-wrapped-wasm") |
| K4 | xtask/src/main.rs:259-264 |
cargo artifact path (-→_ stem, <triple>/release/) |
derived in edda wasm::cargo_artifact |
replaced by wasm::cargo_artifact(row) |
| K5 | xtask/src/main.rs:267-279 |
wasm-bindgen argv --target web --no-typescript, out dir facet-wrapped/dist |
yes — [[build.wasm]] facet-wrapped-wasm (BindgenStage{Web, typescript:false}, out_dir) |
replaced by wasm::run_stages(cell, …, XtaskWasmStageRunner) |
| K6 | xtask/src/main.rs:282-292 |
manifest stage argv run --quiet --example manifest -p facet-wrapped → manifest.json |
yes — ComponentManifestStage{example:"manifest", file:"manifest.json"} |
same run_stages call |
| K7 | xtask/src/main.rs:229-241 |
build verb resolved via suite_cli::run, which calls BuildVariantSet::discover_validated = reads the emitted toml |
the recipe is the source; the toml is its description | now resolves FacettBuildThing.variants() (code) and validates it in-process |
| K8 | xtask/src/main.rs:179 (help) |
"--list to see all seven" — a row count |
the recipe's row count | help no longer counts rows |
| K9 | suite_cli::run:219-235 (edda) |
non-cargo rows "skipped (use the image/vm/iso builders)" — no named door | — | xtask refuses them BY NAME: container → dwarves buildthing facett --variant <n> (file-mode cell) · package → cargo xtask publish-pip / the forge |
| K10 | xtask/src/main.rs:61 |
package facett-demo (demo_crate) |
yes — DEMO_PACKAGE |
reads facett_build_thing::DEMO_PACKAGE |
| K11 | xtask/src/main.rs:86 |
demo wasm feature axis | yes — DEMO_WASM_CARGO_ARGS (already read from the recipe) |
unchanged (was already a door) |
| K12 | xtask/src/main.rs:48,317-384 |
panel-wasm: package facett-wasm-demo, example panel, triple, bindgen flags, dist |
no — BuildVariant has no example field, so a cargo example cannot be a row |
left in xtask, VIKTIGT: needs example on edda's BuildVariant (or the fixture moved to a [[bin]]) before it can be a row; it is a WebGPU-guard fixture, not a shipped artifact |
| K13 | xtask/src/pip.rs:42 |
image name nordisk-pip-toolchain |
yes — PIP_TOOLCHAIN_IMAGE |
re-exported from the recipe |
| K14 | xtask/src/pip.rs:52-54 |
distribution facet-wrapped-web / import facet_wrapped_web |
yes — DISTRIBUTION = "facet_wrapped_web" |
IMPORT_PACKAGE = recipe DISTRIBUTION; the PEP 503 spelling stays derived |
| K15 | xtask/src/pip.rs:64-65,334-335 |
the 3 wasm outputs (twice) | yes — WASM_OUTPUTS |
WASM_BUILD_OUTPUTS = recipe WASM_OUTPUTS; REQUIRED_ASSETS = those + the package-source index.html |
| K16 | xtask/src/pip.rs:304-311,442 |
dirs pkg-facet-wrapped/dist, facet-wrapped/dist, pkg-facet-wrapped |
yes — OUT_DIR, WASM_DIST, SOURCE |
read from the recipe |
| K17 | xtask/src/pip.rs:47, holger.rs:341,591 |
Containerfile bytes via include_str!("containerfiles/…") |
yes — file-mode [[build.container]] cells name the same three paths |
kept: include_str! needs a literal, and tests/declared.rs already holds the three declared paths to exist; noted, not moved |
| K18 | xtask/src/holger.rs:339,589 |
image names nordisk-pip-consumer, nordisk-streamlit |
yes — PIP_CONSUMER_IMAGE, STREAMLIT_IMAGE |
re-exported from the recipe |
| K19 | xtask/src/pip.rs:540-545 |
python -m build --wheel --sdist --no-isolation script |
the package cell (format = pip, sdist = true); edda's PackageStager/DraupnirPackageRunner is the forge's road |
left: moving publish-pip onto edda's package road is its own lane (the jsguard residual below is why the two roads still differ) |
| K20 | xtask/src/jsguard.rs (all) |
the no-JS-graphics rule on the staged loader page | explicitly NOT declared (recipe lib.rs:56-63) |
left: it is a content rule, not a build axis; the recipe's docs already say closing the forge gap means moving it into validate |
| K21 | xtask/Cargo.toml |
12 deps (nornir-pages, nornir-xtask-support, facett-wasm-demo, nornir-build-thing, facett-build-thing, draupnir, zip, sha2, base64, 4× znippy) | — | nornir-build-thing dropped: reached only through the recipe's pub use nornir_build_thing (one row type by construction). The rest serve verbs that are not builds (publish/verify/run/deploy) and stay — see §3 |
2. What the recipe declared and what was missing
Declared (7 rows, all suites = ["ORIGINAL"] since 1ad8e85): facet-wrapped-wasm (wasm + [[build.wasm]] bindgen+manifest), facet-wrapped-web (package, from_wasm), facett-demo (exe), facett-demo-wasm (wasm + bindgen cell), three file-mode containers. [build.toolchain] pinned by manifest digest.
Missing for a pure door:
- the
panelfixture has no row (K12 — blocked on edda: noexampleon a variant); demo-wasm/deploy-pagesbuild throughnornir_pages::demo_wasm, which spells its owncargo build --release -p <crate> --target wasm32 <extra>(eddanornir-pages/src/lib.rs:114-118). facett feeds it from the recipe (crate + axis), but the argv writer is nornir-pages, notbuild_variant— an edda-side todo.
3. Why xtask does not have ONE dependency
The law's "done" example (holger 424945a) has 10 [dependencies]; one-dep is not what any "done" repo measures. In facett the extra deps are verbs that build NOTHING: publish-holger (znippy ×4), publish-pip/verify-install/streamlit/test-demo (draupnir, zip, sha2, base64), deploy-pages (nornir-pages), panel-wasm (facett-wasm-demo). Folding them into facett-build-thing would put bollard and znippy into a crate the forge compiles to wasm32 and loads as a sandboxed plugin — the recipe would stop being a declaration. The build knowledge is what had to leave xtask; the dependency that mattered was nornir-build-thing, and it is gone.
4. After (lane C, measured 2026-09-29)
cargo tree -p xtask -e features: 5846 → 5850 lines. The ONLY new nodes arefacett-build-thing feature "wasm-stage"andnornir-build-thing feature "wasm-stage"; no crate enters or leaves the graph (nornir-xtask-supportwas already linked).cargo tree -p facett-build-thing -e features: 3167 → 3167, byte-identical — the forge's view of the recipe is unchanged.Cargo.lock: xtask loses its directnornir-build-thingedge; nornir-build-thing gainsnornir-xtask-support(thewasm-stageleg).cargo xtask build --list: 7 rows from the CODE; 3 cargo rows print edda's argv, 4 (3 containers + the wheel) printREFUSED herewith their door.--variant nordisk-streamlit→refused: 'nordisk-streamlit' is kind 'container' ….cargo xtask wrap-wasmvs 537ea35: same 3 files, and all three sha256 equal —facet_wrapped_bg.wasma3f17ade…(12 636 449 B),facet_wrapped.jsbdac5470… (113 754 B),manifest.jsond4644f69…(8 140 B).- Lines: xtask 2342 → 2477 (
main.rs525 → 495, newdoor.rs158,pip.rs746 → 751,holger.rs880 → 881,lib.rs13 → 14). It grew, and that is not the metric: what left is the argv, the triple, the bindgen flags and the package names; what came in is a door that refuses by name, and it is mostly docs. - Tests:
facett-build-thing15 (11 + newthin_door.rs3 + parity 1);xtask103 (97 + newbuild_is_a_thin_door.rs6), all green.
5. Not done
K12DONE 2026-09-29 (lane P, dwarf): eddapanel-wasmspells its cargo argv2637751addsBuildVariant::example+wasm::artifact_stem; the fixture is thefacett-wasm-demo-panelrow + cell, andpanel-wasm=door::build_one+ the harness'sIndexHtml. Built IN THE PINNED IMAGE (3d5115ba…): cargo 171.0 s, bindgen 2.2 s,panel_bg.wasm5 620 426 B.- The container law:
buildmaterializes on the HOST viabuild_variant, as this item asks. The law's container section saysbuild_variant_in_container(DraupnirCargoRunner, pinned[build.toolchain], already declared here). That switch needs thecargo-containerfeature and a minted image; it is the next lane. DONE 2026-09-29 (lane P): both verbs build thedemo-wasm/deploy-pagesargv is written by eddanornir-pagesfacett-demo-wasmROW throughdoor::build_one(pinned image) and publish with eddanornir_pages::stage_dist/publish_dist(eddacf9816c);xtask/src/pages.rs. The row now namesbin = "facett-demo"(cargo writesfacett-demo.wasm; the stage looked forfacett_demo.wasm). VIKTIGT: the row does NOT build in the pinned image —zstd-sys(parquetzstd, facett-osm; the browser demo decodes the embedded Sigtuna GeoParquet) needsclangfor wasm32 andnordisk-cargo-toolchaincarries none. Measured in a probe image = the pin +apt-get install clang llvm(localhost/nordisk-cargo-toolchain-wasmc-lanep:e1f9c4ff…, viaNORDISK_CARGO_TOOLCHAIN_IMAGE): cargo + bindgen green,facett-demo_bg.wasm26 467 787 B. Owner decision: clang+llvm into the shared cargo-toolchain image (new digest, every repo re-pins) or a facett-only image.- ONE dependency: not achievable without moving the publish/verify/run verbs out of xtask (§3). Holger, the "done" example, has 10.
6. The container road (lane C, oden 2026-09-29)
cargo xtask build / wrap-wasm build every cargo row in the pinned [build.toolchain] image and have no host road (xtask/src/door.rs), and since this commit the row's [[build.wasm]] stages run in the same image too (xtask/src/stages.rs, ContainerStages: a second impl of edda's WasmStageRunner, over DraupnirCargoRunner).
- Pin:
nordisk-cargo-toolchain@sha256:3d5115ba…(idab4b8dc004e8…, created 2026-07-13 =SOURCE_DATE_EPOCH1783900800). It was already in oden's rootless podman, so nothing was minted.enter()now asks the engine for the pin FIRST and mints only when it is absent; the old unconditional mint re-taggednordisk-cargo-toolchain:live, a tag other sessions on a shared box use too. - Measured on oden,
facet-wrapped-wasm, 8 cpus, 8192 MiB, network none: cargo 91.1 s, stages 131.2 s (includes building wasm-bindgen-cli 0.2.126 once, offline, into the bound target dir). All three dist files come out of the image. 0/home/rickardpaths in any of them.manifest.jsond4644f69…= the host's. - Bindgen offline: the registry is bound read-only, and
cargo installmust unpack sources it has never built (Read-only file system (os error 30)on/cargo/registry/src/…/wasm-bindgen-cli-0.2.126). The stage installs with a scratchCARGO_HOMEwhoseindexandcacheare symlinks onto the read-only registry and whosesrccan be written. The scratch is removed afterwards. - Host vs container, same rustc now (oden's host is 1.97.1, the image is 1.97.1), and the host run uses the container's
--remap-path-prefixset: the two are still NOT byte-identical. The wasm is 12 609 748 B (721ab88d…) on the host and 12 601 688 B (834709cc…) in the image. The host module has 14 468 functions and the image's has 14 460. The registry crates get equal crate disambiguators (earcutrCs9PZdXfRgQWKon both sides). The workspace crates do not (facett_mapCsfaj…on the host,Cs9J52…in the image, and.fingerprint/facett-map-edec…against-3c63…). The 7 siblings (../edda,../znippy-zoomies, …) are path dependencies OUTSIDE the workspace root, so cargo hashes their ABSOLUTE path into-C metadata(/work/eddaagainst/home/…/edda). That changes every dependent's symbols, and with them inlining and function order.--remap-path-prefixcannot reach this. Only the same absolute layout on both sides can, which is one more reason the container is the build and the host is not.
7. ONE dependency — design (lane X, dwarf 2026-09-30)
Owner order: xtask carries ONE dependency, the recipe. §3 said that needs the non-build verbs out of xtask, and that folding them into the recipe is wrong (the forge compiles facett-build-thing to wasm32 and loads it as a plugin; bollard and znippy cannot follow it there). No repo has done it yet (holger, the "done" example, has ~10 deps), so this is the worked example.
The split. Two crates, two jobs:
| crate | job | normal deps | verbs |
xtask |
the THIN DOOR: builds rows, nothing else | facett-build-thing (features wasm-stage, cargo-container) — ONE |
build, wrap-wasm, buildgate |
facett-ops (new member, plain bin + lib) |
everything that is not a build: publish, verify, run, deploy | the recipe + nornir-pages, facett-wasm-demo, draupnir, zip, sha2, base64, znippy-* |
publish-pip, publish-holger, verify-install, streamlit, test-demo, demo-wasm, deploy-pages, panel-wasm |
facett-opsmay carry heavy deps because NOTHING loads it as a plugin; it is a normal workspace bin,cargo run -p facett-ops -- <verb>, aliased in.cargo/config.tomlascargo ops <verb>.- xtask does NOT shell to it and does not link it. The owner's muscle memory (
cargo xtask publish-pip) gets a refusal BY NAME that names the new door (moved: cargo ops publish-pip) and exits 2 — the same shape as a container row refused with the door that builds it. So every documentedcargo xtask <verb>still answers, andtests/no_ghost_paths_or_verbs.rskeeps holding the docs. - When an ops verb needs a BUILD (demo-wasm, deploy-pages, panel-wasm), it builds the recipe's ROW through the same writer xtask uses — edda's
nornir_build_thing::container_road(below), reached through the recipe's re-export. It spells no package, feature or triple; the row name is the recipe's constant. One writer of every argv, two doors onto it. - The build road itself leaves xtask for edda:
xtask/src/stages.rs(ContainerStages, the in-imageWasmStageRunner) and theRoad/enter/build_onehalf ofxtask/src/door.rsmove to eddanornir-build-thing/src/container_road.rs(cfg(all(cargo-container, wasm-stage))). That is what the file's own header asked for ("it belongs in edda … once a second repo wants it"): nornir and dwarves want it now (order B), and it is why xtask needs nodraupnir/nornir-xtask-supportedge — the one that remained indoor.rs:158andstages.rs:155. - The three Containerfiles move with the verbs that mint them (
xtask/src/containerfiles/→facett-ops/src/containerfiles/); the recipe'sCONTAINERFILE_DIRfollows and the emitted toml is re-emitted. - Tests move with their verbs (pip/holger/jsguard/pages tests →
facett-ops/tests); the workspace-wide guards that test no verb (gallery, wgsl, silenced tests, pipeline, wasm bundle) stay in xtask as dev-deps — dev-deps are not the door. - Enforcement:
xtask/tests/one_dependency.rsparsesxtask/Cargo.tomland runscargo tree -p xtask -e normal --depth 1, and goes red on any second normal dep. - Invariant measured before/after:
cargo tree -p facett-build-thing -e featuresbyte-identical (the forge's view of the recipe does not move; only a path constant and door text change in it).
Order B in the same stroke (edda): nornir_pages::demo_wasm loses its host cargo build + host wasm-bindgen + wasm-opt. New shape: demo_wasm(cfg, set, variant) builds the caller's DECLARED wasm row with container_road (pinned image, bindgen stage in the image), checks the row's cell produces the files the page imports, and stages the site shell; deploy_pages(cfg, set, variant) = that + publish_dist. A caller without a wasm row gets a refusal by name, never a host build. nornir and dwarves callers pass their recipe's set and row name.
What landed (lane X, dwarf 2026-10-01) — owner option (b): facett ALONE, edda untouched.
The split landed against edda main 6b5f905; the container road did NOT move to edda. It stays in facett (xtask/src/door.rs + xtask/src/stages.rs), DUPLICATED with edda branch laneX/demo-wasm-container-r2 (nornir_build_thing::container_road), until that branch can merge — blocked because it changes nornir_pages::demo_wasm and dwarves_runa::Step::Xtask, which nornir and dwarves call, and neither workspace resolves on dwarf. Two things xtask got from crates it no longer links are spelled locally until then: the "is the pin in the engine?" question is a no-op true run through DraupnirCargoRunner (was draupnir::ContainerBoot::image_present), and stages::locked_wasm_bindgen_version copies nornir_xtask_support's Cargo.lock scan. facett-ops builds its rows through xtask::door (a library edge, never a spawn).
cargo tree -p xtask -e normal --depth 1: 12 → 1 (facett-build-thing).cargo tree -p facett-build-thing -e features: maina5002543167 lines, sha256044af68e…→ branch 3167 lines, byte-identical. The lock diff is facett-ops plus the edges xtask gave up; nothing else re-resolved.cargo test -p xtask -p facett-ops -p facett-build-thing: 130 test fns on main → 135 passed, 0 failed (+3one_dependency, +1 ops-verb guard, +1the_stages_that_point_at_moved_verbs_are_exactly_the_known_three).cargo xtask build --list: 11 rows, 4 REFUSED with their door.cargo xtask build --variant facett-wasm-demo-panelIN the pin668468e7…(found by the probe, nothing minted): cargo 68.5 s, stages 52.2 s,panel_bg.wasm5 619 878 B,panel.js94 652 B.- VIKTIGT — known red at run time:
facett.runa.ron's publish-holger, verify-install and test-demo stages runcargo xtask <cmd>and now hit the moved-verb refusal.alias: "ops"needs dwarves-runa 0.2.4 (same edda branch). Pinned by the test above.
Twin closed (lane X, dwarf 2026-10-01, later the same day). Proof build in a scratch tree (edda e0a2d0b + the holger-gaps-only crates, Skidbladnir holger-gaps crates, lane R's gunnar/modgunn clones) showed the nornir and dwarves callers compile in their real workspaces: nornir -p xtask 2/2 on our test (its no_silenced_tests is red on main, tests/viz_jobs_headless.rs, unrelated); dwarves -p xtask check green and runa_exec 6/6 incl. the new an_xtask_step_runs_through_its_cargo_alias. Merged: edda main e0a2d0b, dwarves main 68c28e1, nornir main fe406ec. So:
facett.runa.ron: publish-holger, verify-install, test-demo carryalias: "ops"; the KNOWN-RED pin is flipped tono_stage_points_xtask_at_a_moved_verb_and_the_three_go_through_ops.- The road is edda's:
xtask/src/door.rswrapsnornir_build_thing::container_road(toolchain,Road::enter,build_row);xtask/src/stages.rs(and with it the local runner probe and Cargo.lock scan) is deleted. facett-ops builds the demo throughnornir_pages::demo_wasm(cfg, set, DEMO_WASM_VARIANT)and the panel throughcontainer_road::build_wasm_row, and no longer links xtask. - Held: xtask 1 dep; recipe features tree 3167 lines
044af68e…, byte-identical (the lock's four incidental re-picks pinned back to main's); tests 135/135;facett-wasm-demo-panelbuilt through edda's road in the pin668468e7…: cargo 76.5 s, stages 54.2 s,panel_bg.wasm5 619 878 B — the same size the facett road produced.