Library M10b: the facade¶
M10b is the second part of the tenth of the eleven milestones of #48, which turns Henad into a library published on crates.io. The new
henadcrate re-exports the engine, the sweeps and the model authoring API under one module tree, withhenad::preludefor a program andhenad::authoring::preludefor a model's source. Its four features,example-models,app,cliandtesting, only add modules, and the two hosts come in with their default features off.crates/henad/examples/complete.rsis the program of [4.2.7], copied byte for byte into the facade's README, which the crate docs include and a doc test compiles.tests/facade_paths.rscompiles against facade paths alone. CI gains afeaturesjob, the facade's doc test, its two no-atomics wasm32 lines, and clippy on the pinned nightly with atomics, whose 33 existing findings were fixed first as a separate handover.
State before¶
48-library stood at dada65c (M10a, the testing kit), with a clean tree.
The workspace held seven crates, and a program depended on henad-core, henad-compute and henad-explore separately, naming items by their crate paths.
Record #38 listed 12 findings of the nightly atomics clippy on henad-app, and records #31 and #39 noted drop_non_drop in henad-compute and henad-explore.
No CI job ran that clippy.
docs/license.html lacked henad-build, which M5 added, so the lint job's licence check would have failed on this branch.
docs/developing/releasing.md does not exist yet, and [5.5] holds the release checklist.
What was done¶
Handover 1: the nightly atomics clippy findings¶
The lint of [4.10], RUSTFLAGS="-C target-feature=+atomics,+bulk-memory" cargo clippy --target wasm32-unknown-unknown on the pinned nightly, stopped at henad-compute, so record #38's 12 findings in henad-app were only the last layer.
It found 33 in four crates (14 in henad-compute, 2 in henad-explore, 5 in henad-models and 12 in henad-app), every one fixed or expected before the facade.
| Crate | Finding | Fix |
|---|---|---|
| henad-compute | manual_midpoint (2, cpu/layout.rs) |
f32::midpoint |
| henad-compute | tuple to array (gpu/grid_engine.rs) |
tex.into() |
| henad-compute | drop_non_drop (gpu/sim_thread.rs, wasm track) |
The parameter takes _submission |
| henad-compute | arc_with_non_send_sync (6 in entry/mod.rs, runner/mod.rs twice, gpu/view/display.rs, gpu/agent_engine.rs) |
expect with its reason, under cfg_attr(all(target_arch = "wasm32", target_feature = "atomics"), ..) |
| henad-explore | drop_non_drop (output/mod.rs, File has no Drop on wasm32) |
The file lives in a block that ends before the rename |
| henad-explore | unnecessary get before / (exec/mod.rs) |
workers / lanes, dividing by the NonZeroUsize |
| henad-models | manual_midpoint (boids/mod.rs twice, gpu_boids/mod.rs, virus_network/wiring.rs) |
f32::midpoint and f64::midpoint |
| henad-models | tuple to array (gpu_ants/mod.rs) |
geom.display.into() |
| henad-app | arc_with_non_send_sync (state.rs, ui/agent_layer.rs three times) |
expect with its reason, as above |
| henad-app | manual_midpoint (ui/charts.rs), tuple to array (ui/edge_layer.rs), unnecessary get (ui/sweep/builder.rs), as_chunks_mut (ui/export/image.rs) |
The suggested form |
| henad-app | needless_pass_by_ref_mut, unused_self and let_underscore_untyped in ui/sweep/mod.rs's cfg-split ColumnsBuild::poll and open_folder_results |
Both functions and their calls are native only. A browser never has a running build or an output folder |
The arc_with_non_send_sync findings cannot be fixed in place.
wgpu's handles are Send on wasm32 only through fragile-send-sync-non-atomic-wasm, which turns itself off under atomics, as henad_core::send_sync documents, and the Arcs are part of public types (SharedSlot, WakeFn, ModelEntry's parts).
The expects fire only in the threaded web build, so a native or a no-atomics build never sees an unfulfilled expectation.
The midpoint rewrites in henad-models sit outside every kernel: boids' initial and randomised speed, gpu_boids' action uniform, and Virus on a Network's wiring bisection.
f32::midpoint computes in f64 and rounds once, which equals 0.5 * (a + b) in f32 for finite inputs that do not overflow, and f64::midpoint is (a + b) / 2 below f64::MAX / 2.
The consistency fixtures, the golden CLI output and every thread-count test still pass, so no result moved.
Handover 2: the facade¶
crates/henad/ holds Cargo.toml, README.md, the licence copies, src/lib.rs, examples/complete.rs and tests/facade_paths.rs.
It has no build script.
- Manifest. Normal dependencies on henad-core, henad-compute, henad-explore and bytemuck.
henad-models and henad-app are optional, and henad-cli is optional under
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]. The root[workspace.dependencies]gains henad-cli and henad-app withdefault-features = false, sohenad/appandhenad/clicompile no example model.testingturns onhenad-explore/testing. The example carriesrequired-features = ["example-models", "app"], and the docs.rs metadata is [5.6]'s. - Module tree.
src/lib.rsdefines nothing but re-exports, in the modules of [4.1]'s table: the root,params,stats,views,action,gpu,runner,engine,explore,benchmark(native),authoringwithprimitivesandprelude, and the gatedmodels,app,cliandtesting.henad::authoringglobs the sevenauthoring::modelmodules andhelpersflat under#![deny(ambiguous_glob_reexports)], and no two of them share a name today.henad::testingre-exports the kit module whole, andhenad::modelsre-exports the ten modules with their model types. Native-only items (run_spec,plan_spec,read_directory_series,DirectorySeries,acquire_headless,DeviceError,benchmark,run_native,results_folder,AppError) carry the same gate, and every gated item takescfg_attr(docsrs, doc(cfg(..))). - Root macros.
pub use henad_core::paramsimports the macro and henad-core'sparamsmodule together, which collides with the facade's ownhenad::params. The seven macros come in through a glob of a privateroot_macrosmodule instead, and the facade'sparamsmodule shadows the glob's module while the macro reaches the root. - Preludes.
henad::preludeandhenad::authoring::preludehold [4.1]'s lists. The authoring prelude takesNoFieldas one of "the field layers", and the three neighbourhood offset tablesMOORE_ROW_MAJOR,MOORE_COLUMN_MAJORandVON_NEUMANN. - The complete program.
examples/complete.rsis [4.2.7]'s program with three changes the workspace's lints asked for. The Cargo.toml header is//comments, since//!lines trippeddoc_link_with_quoteson the feature list.#![expect(clippy::print_stdout, reason = ..)]follows the CLI's crate-level precedent.run_sampledreturns a#[must_use]ControlFlow, which [4.2.7] dropped, so the callback breaks once nobody is infected and the program reports the tick. - README. The crate's README opens with a feature table, then holds the program in a
rust,no_runfence. The crate docs include it withappandexample-modelsboth on, and read "Henad, a parallel agent-based modelling engine." otherwise.the_readme_program_matches_the_example, an inline test inlib.rs, compares the firstrust,no_runfence with the example file byte for byte. tests/facade_paths.rs. Every path starts withhenad::. Five tests run without a model: aGpuAgentActionfrom aPassSpecwith an inline shader and empty bindings, aSweepSpecfromhenad::explore::spec::{BlockSpec, RunSettings, MeasureSettings, SeedSettings, ActionSpec}with twoFactorSpecs and aSweepOptionswithConcurrencyandShardset, a match onSetupError::Param(ValueError), a match onDeviceError, andSpatialHash,HashGridandlabel_componentswith itsComponentStatsover a four-node network. The modulewith_an_entrycompiles the parts that need an entry and never runs:run_specwith those options, a paced host that builds aFaultSink, handsSimThread::newa state with an optionalWakeFnand reads theSnapshotoftake_snapshot, and a function reading aRunRow's status as aRunStatus. It carries#[expect(dead_code)]with its reason.
CI, check.sh and the checklist¶
check.shruns the facade's two wasm32 lines,cargo check -p henad --liband--features example-models,testing --lib, after the existing one, andcargo test -p henad --doc --features example-models,appafter the workspace's doc tests..github/workflows/ci.yml: thelintjob adds the facade's wasm32 lines, the pinned nightly's install and "clippy with atomics" over-p henad-appand-p henad --features app,example-models, intarget/clippy-atomics. Thetestjob adds the facade's doc test. A newfeaturesjob, on pull requests, runscargo check -p henadwith no features and then once per feature.docs/license.htmlis regenerated with cargo-about 0.9.1, adding henad and henad-build.- [5.5]'s release checklist, step 2, now gives the facade's verified pass as a command,
CARGO_TARGET_DIR="$(mktemp -d)" cargo package --workspace --exclude henad-tutorial --locked --features henad/example-models,henad/app,henad/cli,henad/testing, and says both passes take a fresh target directory. The design sits under the gitignoreddev-docs/, so that edit is not part of the handover.
AGENTS.md and the changelog¶
- AGENTS.md: "The workspace has 8 crates", the facade in the diagram and its legend, the facade's feature edges in the dependency rule, a
henadcrate bullet after henad-cli's, and the three new lines in Commands with the CI-only list naming thefeaturesjob and the atomics clippy. - CHANGELOG: two Added lines, the facade with its features and preludes, and the complete example. The lint fixes change no public item and get no line.
Edited tree¶
.
├── AGENTS.md ~ 8 crates, the facade's bullet, Commands
├── CHANGELOG.md ~ the facade, the complete example
├── Cargo.toml ~ henad-cli and henad-app as workspace dependencies
├── Cargo.lock ~ the henad package
├── check.sh ~ the facade's wasm32 lines and doc test
├── zensical.toml ~ nav entry #40
├── .github/workflows/ci.yml ~ lint: facade wasm32, atomics clippy; test: facade doc test; features job
├── crates/henad/ + the facade
│ ├── Cargo.toml
│ ├── README.md + the program in a rust,no_run fence
│ ├── LICENSE-MIT, LICENSE-APACHE
│ ├── src/lib.rs + the module tree, the preludes, the README test
│ ├── examples/complete.rs + the program of [4.2.7]
│ └── tests/facade_paths.rs + the gate
├── crates/henad-compute/src/ (handover 1)
│ ├── cpu/layout.rs ~ midpoint
│ ├── entry/mod.rs ~ expect under atomics
│ ├── gpu/{agent_engine,view/display}.rs ~ expect under atomics
│ ├── gpu/grid_engine.rs ~ tuple into array
│ ├── gpu/sim_thread.rs ~ no drop of a non-Drop value
│ └── runner/mod.rs ~ expect under atomics
├── crates/henad-explore/src/ (handover 1)
│ ├── exec/mod.rs ~ division by the NonZeroUsize
│ └── output/mod.rs ~ the manifest file closes at the end of a block
├── crates/henad-models/src/ (handover 1)
│ ├── boids/mod.rs, gpu_boids/mod.rs ~ midpoint
│ ├── gpu_ants/mod.rs ~ tuple into array
│ └── virus_network/wiring.rs ~ midpoint
├── crates/henad-app/src/ (handover 1)
│ ├── state.rs, ui/agent_layer.rs ~ expect under atomics
│ ├── ui/{charts,edge_layer}.rs ~ midpoint, tuple into array
│ ├── ui/export/image.rs ~ as_chunks_mut
│ └── ui/sweep/{mod,builder}.rs ~ native-only poll and open_folder_results, division
└── docs/
├── license.html ~ henad and henad-build
└── developing/agent-record/20261002-40-library-facade.md +
State after¶
Both handovers are uncommitted on 48-library, on top of dada65c, in the main checkout as asked. Nothing is staged.
HENAD_REQUIRE_GPU=1 ./check.shpasses: 1064 tests, none failed, with the wasm32 typechecks the facade's two included, packaging, cargo-deny, the docs, the facade's doc test and the web build. M10a ended at 1057, and the facade adds its fivefacade_pathstests, the README test and the README doc test.uv run --locked zensical buildpasses with no issues.- The nightly atomics clippy passes on
-p henad-appand on-p henad --features app,example-models. cargo check -p henadpasses with no features and with each feature alone, and both wasm32 lines pass.cargo test -p henad --doc --features example-models,appcompiles the README program.RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc -p henad --no-deps --features example-models,cli,app,testingpasses on the pinned nightly, the docs.rs build of [5.6].cargo tree -p henad -e normal -i henad-modelsfinds no henad-models with no features, withapp, withcli, and withapp,cli,testing.cargo package -p henad --no-verify --listlistsCargo.toml,Cargo.lock,README.md, both licences,src/lib.rs,examples/complete.rsandtests/facade_paths.rs, besides the generated.cargo_vcs_info.jsonandCargo.toml.orig.- The checklist's verified pass with the facade's features ran from an empty target directory, with
--allow-dirtyfor the uncommitted tree. It verified all eight crates, the facade compiled with henad-cli and henad-models from the tarballs, in 1 min 31 s. - The complete example ran end to end in a debug build: the build, the live edit and the action, a 12-run sweep with 12 runs ok, the readback, the replay of run 5, and the app opening on it, whose Playback read "Sweep run 5: config 1, replicate 1" through the egui MCP server.
The window opened on the second display from a seeded
SIR study/app.ron, and the folder and the sweep'ssir-ratesfolder were removed afterwards.
Proposed commits, in order:
fix: nightly atomics clippy findings, the 20 files undercrates/henad-{compute,explore,models,app}/srclisted in the tree as handover 1.feat: facade crate, everything else.
Issues found & future directions¶
- The program's second half shows little at [4.2.7]'s values. SIR at infection rate 0.3 on 256 by 256 has burnt out by tick 300, so the second outbreak finds no susceptible cell and
run_sampledbreaks at its first sample, printing "the outbreak ended by tick 300". The program is correct. A smaller firstrun_to, or the outbreak before the epidemic ends, would show the live edit.guide/library.mdin M11 is the place to retune it, in both copies at once. - The README program carries
#![expect(clippy::print_stdout)]. Henad's workspace lints deny printing. A reader who copies the program into a crate that runs clippy without that lint gets an unfulfilled-expectation warning. - No region markers in
complete.rs. [8] includes the example inguide/library.mdby region, and a marker line would also land in the README copy. The README test then has to skip--8<--lines, or the guide includes the whole file. M11 decides. henad::authoring::preludereads [4.1]'s lists literally. It takes the random draws,Boundary,cell_index,offset_cell,dist_sq, the three offset tables,NoField,CaFieldandScalarField, and leaveswrap_index,wrap_coord,axis_delta,offsets,for_each_neighborandheading_octantathenad::authoring::primitives::space. M10c's tutorial crate will show whether a model page misses any of them.- The facade's reach is checked by hand. [4.1] promises a facade path for every Henad type a listed item's signature or public field reaches. With the rustdoc gate cut,
facade_paths.rscovers M10b's list, and types such ashenad_explore::exec::RunRequest(Q5) stay without a facade path on purpose. A type a signature reaches and the tree misses would show up as a crate path in M10c's tutorial or M10d's template. - The removed
from_iter_instead_of_collectlint. The pinned nightly warns that the workspace configures a lint clippy removed. It is a warning,-D warningsleaves it alone, and stable 1.97 still knows the lint. docs/license.htmlwas stale before this session, without henad-build. The page is regenerated here, and the lint job's check had not run on48-librarysince M5.- Next. M10c moves the tutorial into
examples/tutorialon facade paths, and M10d builds the template on the facade. Both depend on M10b alone.