Gunnar

nordisk/facett

.nornir/bundle-groups-design.md

<!-- nornir:class: design -->
# facett — bundle groups (second wasm bundle), as built on oden 2026-09-29

Owner order 2026-09-29: docview, manual and model3d must not grow the default wasm bundle.
Groups are declared in the build thing; one wasm row per group; the routing table is emitted
from the recipe, never a hand list.

## Where each fact lives

| fact | source | derived / held by |
|---|---|---|
| groups, rows, dists, wheel dirs, **entry → group** | `facett-build-thing/src/lib.rs` `BUNDLE_GROUPS` (`BundleGroup { name, variant, package, features, no_default_features, dist_dir, wheel_dir, entries }`) | emitted `.nornir/build-variants.toml` (`toml_parity`), emitted `pkg-facet-wrapped/src/facet_wrapped_web/assets/bundles.json` (`thin_door::the_committed_routing_table_is_the_emission`, xtask `the_loader_routes_by_the_emitted_table_and_names_no_bundle_file`) |
| bundle file names | package stem (`facet-wrapped` → `facet_wrapped{_bg.wasm,.js}`, `facet-wrapped-group` → `facet_wrapped_group{_bg.wasm,.js}`) + `manifest.json` — `BundleGroup::outputs()` | `thin_door`: equal to edda's `WasmBuild::outputs` for every group |
| the wasm's own refusal list | `facet-wrapped/src/groups.rs` `GROUP_ENTRIES` (compile-time copy: the recipe cannot be a dep of the wasm without moving core's bytes) | `facet-wrapped-group/tests/groups.rs::group_entries_are_the_recipes` (both directions) |

| group | row | argv | dist | wheel |
|---|---|---|---|---|
| `core` (default) | `facet-wrapped-wasm` | `-p facet-wrapped` (unchanged) | `facet-wrapped/dist` | `assets/` |
| `docs` (docview, manual) | `facet-wrapped-docs-wasm` | `-p facet-wrapped-group --features docs` | `facet-wrapped-group/dist-docs` | `assets/docs/` |
| `model3d` (model3d) | `facet-wrapped-model3d-wasm` | `-p facet-wrapped-group --features model3d` | `facet-wrapped-group/dist-model3d` | `assets/model3d/` |
| `sim` (simview) | `facet-wrapped-sim-wasm` | `-p facet-wrapped-group --features sim` | `facet-wrapped-group/dist-sim` | `assets/sim/` |

`facet-wrapped-group` is lane A's package: same crate root as facet-wrapped (`[lib] path`),
lib name `facet_wrapped_group`, because ANY `[features]` table on facet-wrapped moves the core
wasm. It has no default features, so a group row carries no `--no-default-features`.

## Loader

`index.html` fetches `./bundles.json`, loads the ONE bundle the first named component routes
to (`import("./" + g.js)`), and refuses a component another bundle holds with the wasm's own
words plus the file: ``component `docview` is not in the `core` bundle: it is in the `docs`
bundle (docs/facet_wrapped_group.js) — …``. Hooks are published at `facet_ready` and answer
through the loaded bundle; `window.facet_bundle` names it. An unreadable routing table is a
refusal, not a fallback.

## Doors

`cargo xtask wrap-wasm [group…]` walks `BUNDLE_GROUPS` (core first); stages run with the
recipe's `stages::GroupStageRunner` (the manifest example with the row's features).
`publish-pip` checks, stages and verifies every group from the package cell's `inputs`
(`pip::staged_bundle_files`, `pip::required_assets`); package-data carries `assets/*/*`.
`gallery_probe --group-dist <g>=<dir>` serves each group at its routing `dir`, asserts the
bundle each component came from, and runs one wrong-bundle refusal per non-core group.

## Measured on oden (rustc 1.97.1, RTX 4090), branch `lane-a/bundle-groups-oden`

| bundle | wasm | js | manifest |
|---|---|---|---|
| core | 12 615 380 B `9f8c16bf…` | 113 754 B `f52df600…` | 8 140 B `d4644f69…` |
| docs | 9 043 762 B `ead26131…` | 110 338 B `76fbab14…` | 389 B `bab71a44…` |
| model3d | 7 237 379 B `3ded1c45…` | 110 336 B `0779586b…` | 195 B `4b993b29…` |

* Core vs main `e2bdcab` built on oden: **identical** (wasm, js, manifest) when both are
  built with the same `ahash` artifact. Across two FRESH target dirs main's own core differs
  from itself (`f31820b2…` vs `9f8c16bf…`, same size, code section identical): 73 runs of 64
  bytes in the data section are `ahash`'s compile-time random seeds — `arrow-array` turns on
  `ahash/compile-time-rng` (`const-random`). **The core bundle is not reproducible across
  target dirs on main either**; a byte-identity claim must name the target dir, or the seed
  must go (ahash without `compile-time-rng`).
* `gallery_probe --headless --groups all --dev` + both group dists: **48/48 PASS** (45 core, 2
  docs, 1 model3d), wrong-bundle refusals **2/2**, every adapter `nvidia lovelace` (Chrome:
  "Selected adapter: NVIDIA GeForce RTX 4090 … Discrete GPU"), `adapter_attempts` 1 on 47 and
  2 on the first mount (`table`), 6 distinct navgraph frames on all 48.
* Tests: facett-build-thing 18, facet-wrapped 103 (+1 ignored), facet-wrapped-group plain 73 /
  docs 69 / model3d 68, xtask 104 — all green.
* **Merged onto main `9813d5a`** (lane B's GPU gate: facett-core ortho fix moves the core
  wasm): core `a66943ac…` (12 615 349 B), js `9bb2bf6d…`, manifest `d4644f69…` — identical to
  `9813d5a`'s own core built on oden; docs wasm `55f0377c…` (9 043 762 B), model3d wasm
  `70104284…` (7 237 379 B). Same test counts green; probe again 48/48 + wrong-bundle 2/2 on the 4090.

## Open

* edda's `from_wasm` names ONE producer cell; the wheel has three, so the edge is `None` and
  the bijection is `tests/declared.rs` (edda needs a list).
* A Streamlit page mounting a docs component downloads only the docs bundle; a page that
  wants core AND docs components needs two frames (one canvas, one module).