Library M9: the app as a library¶
M9 is the ninth of the eleven milestones of #48, which turns Henad into a library published on crates.io. henad-app's library gains
AppOptions,AppOpening,OpenAtat the root,run_native,results_folder, and on wasm32start_webandinit_web_logger, and the official binary builds its options overexample_models()and calls one of the two.state,ui,HenadAppandwgpu_configurationare private, and henad-app re-exports no eframe item. henad-models is an optional dependency behind the defaultexample-modelsfeature, which the binary requires. The About window, the menu, the Copy command and the Sweep tab's advice read the product from the options. A review pass followed, with the maintainer's decision to name the official app "Henad" everywhere, its eframe app name and storage folder included.
State before¶
48-library stood at 4e1bd2b, M8's commit, with a clean tree.
henad-app's lib.rs declared pub mod state and pub mod ui, and exported HenadApp with new(cc, models) and open_results, wgpu_configuration, requested_threads and, on wasm32, init_thread_pool.
main.rs filled eframe's NativeOptions with the icon and the title "Henad Engine", parsed --open itself, and on wasm32 started the worker pool, found the canvas and started WebRunner, panicking when eframe failed.
henad-models was a normal dependency.
The About window held Henad's logo, tagline, links and CARGO_PKG_LICENSE as constants, the menu read "About Henad", and HOST_BUILD, henad-app's own build_info!(), was the host the About window, the run details and every sweep's manifest recorded.
The runs table's Copy command always wrote henad-cli, and the Sweep tab named henad-cli in three places.
The Model panel's message for a set with no model the device runs had landed in M4 (NO_MODEL_RUNS).
What was done¶
The library¶
henad-app's public API is [4.5.1]'s, in three new modules beside lib.rs.
options.rsholdsAppOptions,AppOpening,AppErrorandWebStartError.AppOptions::new(models, product, host)defaults to Henad's icon, no links, no licence, no command line and no opening, andicon_png,source_url,documentation_url,license,cli_commandandopeningset each. The fields sit in the crate-privateProduct, whichAppStateholds asproduct.Product::officialmarks Henad's own app.main.rsis a crate of its own and cannot reach apub(crate)item, so the flag is set through the hiddenAppOptions::__official.Product'sDebugprints the icon's byte length in place of its bytes, andAppOptionsderivesDebugalone.AppOptions::check_openingis [4.2.5]'s check, before any device exists: the set has to hold the opening's id, aRunhas to set the declared number of parameters, and aSetup's entry has to share the set's entry'sschema_hash. A refusal is a privateOpeningError, held insideAppErrororWebStartError.native.rsholdsrun_native, which checks the opening, decodes the icon (a PNG that fails to decode is logged and left out), fillsNativeOptionsand runs eframe under the product name, andresults_folder, moved frommain.rswith its two tests.web.rsholdsstart_webandinit_web_logger.start_webreads?threads=, awaitsinit_thread_pool, logs a failure as before, and only then calls the options closure. A pool that failed leavesthread_pool_note, which the Performance tab shows as a banner. It checks the opening, findsthe_canvas_id, startsWebRunner, and removesloading_text. A missing window or document, a refusal and an eframe failure are written intoloading_textwhere there is one, and returned, where the oldmainpanicked.main.rslogs the returned error, andstart_webdoes not, so a start failure is logged once.AppError'sDebugwrites itsDisplayand, for eframe's error, the source, asShaderBuildError's does. Amainreturning it prints a readable message.lib.rsre-exportsAppOptions,AppOpening,OpenAt,run_nativeandresults_folder(native),AppError(native), andstart_web,init_web_loggerandWebStartError(wasm32).requested_threadsandinit_thread_poolstay public as before. No eframe item is re-exported. The crate doc has ano_runexample over a host's ownModelSet.state,ui,HenadAppandwgpu_configurationare private.HenadApp::new(cc, options)builds theAppStateand hands the opening toAppState::open.HOST_BUILDis gone, and the sweep'sProvenanceand the run details readAppState::product.host.
Openings¶
AppState::open(AppOpening)opens a results folder as--opendid, or callsopen_runor the newopen_setup. A run or setup it cannot open, such as a GPU model the compute filter hid, leaves nothing selected, keeps anOpeningRefusalinopening_refusal, and brings the Model tab to the front. The refusal is a lead line, "Run not opened" or "Model not opened", and the reason. The Model panel shows both with "Select a model to continue." while nothing is selected, andselect_modelclears it. Where no model of the set runs here, the panel shows the refusal above "No model in this build runs on this device." in place ofNO_MODEL_RUNS, which would name the GPU a second time, and a set without models reads "This build includes no models." in place of a sentence about GPUs.- The parameter counts of
OpeningErrorandopen_run's message go throughui::plural. AppState::open_setup(&RunSetup, OpenAt)looks the setup's id up in the app's own set, checks the setup's values, seed and schedule against that entry throughRunSetup::from_parts, and writes them into the editing fields. A default seed staysNonewith an empty Seed field. It records noOpenedRun.open_runandopen_setupshareopen_values, the old tail ofopen_run.- The Model panel's message for a set the compute filter leaves empty (
NO_MODEL_RUNS) landed in M4. M9 adds its test.
The product in the UI¶
- The About window draws the product's name and links. The official app keeps Henad's white logo and tagline, and another product shows its icon and no tagline. The rows are Version, Commit, Sources and Build from the host build, License when set, Built on (from
ENGINE_BUILD, with henad-core's version when it differs) for any product but the official one, and one Models row per distinct model-source build. A build without a commit, which is every henad-models build since its build script stamps a source hash alone, shows the hash of its sources. Copy copies the product name and every row. - The About menu shows a link only when the options set it, and its entry reads "About" and the product name, "About Henad" in the official app.
- The About window's image is decoded once into a
OnceCell, a failed decode included, where a host icon that is no PNG was decoded again every frame. ResultsStore::cli_command(program, run_id)writes the given program. The runs table hides Copy command without acli_command, and its tooltip names the program.AppOptions::cli_commandis documented as one program name, which the Copy command quotes as one shell word.- The Sweep tab's three mentions of henad-cli (the browser's GPU refusal, the run limit in
SweepDraft::check, and the values editor) name the options' command throughoptions::cli_phrase, or "on the command line" without one. The panel holds the command, andcached_checkcopies it onto the draft before it checks. The draft is pure and has 43 test calls ofcheck, which keep their signature. - The run details read
engine_versionfromENGINE_BUILD, as M8 did for the CLI'sinfoline.
Dead code¶
Private modules turned up the dead code that pub had kept alive.
NoteLine::is_issueis deleted andResultsStore::used_bytesis behind#[cfg(test)], as the design names.SeriesCache::is_emptyhad no caller either, and is deleted.ui/mcs.rslists every MCS entry, and about 60 are unused. The module allowsdead_codewith that reason, since AGENTS.md has it hold one constant per entry.- clippy's
needless_pass_by_ref_mutfired onAppState::open_file,performance_ui,stats_uiandsystem_ui, which now take shared references. - On wasm32,
make_room,record_empty_series,series_roomandinsert_seriesare native only behind#[cfg(not(target_arch = "wasm32"))], andOpenResult::FolderandResultsSource::Foldercarry a wasm-onlyexpect(dead_code), since both targets match on them.AppErroris native only, since onlyrun_nativereturns it.
The binary, the manifest and the web¶
main.rsbuilds the official options, "Henad" with the icon, the two links,CARGO_PKG_LICENSE,cli_command("henad-cli")and__official(), and callsrun_native, with--openthroughresults_folder, orstart_webafterinit_web_logger. The eframe app name "Henad" names the window and the storage folderHenad, and 0.2.0'sHenad-Enginefolder is left behind once, by the maintainer's decision. In a browser eframe keeps its state under the fixed keyegui_memory_ron(eframe-0.36.1src/web/storage.rs:46, 65), and the rename leaves it alone.Cargo.tomlgains[lib], the[[bin]]withrequired-features = ["example-models"],default = ["example-models"],example-models = ["dep:henad-models"], henad-models as an optional normal dependency, and henad-models as a dev-dependency for the tests.index.htmlgains thecrossOriginIsolatedcheck after the loading element. It appends a note under the spinner, saying the app might run on one thread or not start, and its comment andTrunk.toml's say Chrome starts the build without the headers, on one thread. Its two older comments namestart_webin place ofmain.rs.- The CI
webjob runsscripts/build_web.sh build --releaseand then checksdist/henad-app.jsforwbg_rayon_start_workerandinitThreadPooland thatdist/snippets/wasm-bindgen-rayon-*/src/workerHelpers.no-bundler.jsexists.
Tests¶
- New:
a_mismatched_opening_is_refused(options.rs), with a test-onlyGridModelregistered under the idsirfor the schema mismatch, andan_opened_setup_keeps_the_default_seed,a_gpu_only_set_without_compute_opens_with_nothing_selectedandan_opening_of_a_hidden_gpu_model_opens_with_nothing_selected(state.rs). - New:
a_product_other_than_henad_names_the_engine_it_is_built_on(ui/about.rs), and the crate doc'sno_runexample. open_names_the_folder_after_it_in_either_formandopen_never_takes_a_flag_for_its_foldermoved tonative.rsunchanged.- The two headless helpers of
state.rsandui/sweep/session.rsare one#[cfg(test)] AppState::headless(models, compute).
Docs¶
guide/app.md: the About menu and window in the Overview, and the Copy command's program in Opening a run.- henad-app's README says the crate is also a library.
- AGENTS.md: the
Debugrule (henad-app's public types included, its private modules out), the diagram line, the dependency rule (henad-models behind the feature and the dev-dependency edge), and the henad-app paragraph's opening, the openings, the product's readers and the Copy command. - CHANGELOG: six Added, four Changed and one Removed entry. The Removed entry is "Breaking:" and replaces M4's Changed entry on
HenadApp::newandwgpu_configuration, which named items no 0.3 release will have.
The review pass¶
After the first hand-over the maintainer folded M9's review in before the commit, with one decision of their own.
- The official app is "Henad" everywhere: the window title, the About heading, the menu entry "About Henad" and the eframe app name, so the storage folder is
Henadand 0.2.0's saved window state is left behind once. The CHANGELOG says so. AppError'sDebugprints the readable message,start_webreturns an error for a missing window, logs nothing a caller logs, and leaves the Performance tab note when the pool fails. The page's note is appended under the spinner.Product::is_official's package-name test became the explicitofficialflag,Productgot its manualDebug,AppOptionslostClone,OpenAtbecame#[non_exhaustive], andAppOptions::newdocumentshostas the build the manifest, the run details and the About window record.- The Model panel's refusal text,
ui::pluralfor parameter counts,cli_command's documentation, the guide's About paragraph ("Select About Henad", "Press Copy"), and the cached icon decode. - The CHANGELOG's visible text changes, and AGENTS.md's dependency rule and Copy command passage rewrapped to its width.
Edited tree¶
.
├── .github/workflows/ci.yml ~ web job: --release, worker-glue check
├── AGENTS.md ~ henad-app as a library, the feature, the dependency rule, Debug
├── CHANGELOG.md ~ M9's Added, Changed and Removed
├── Trunk.toml ~ the headers comment: Chrome starts on one thread
├── index.html ~ crossOriginIsolated note under the spinner, comments name start_web
├── zensical.toml ~ nav entry #38
├── crates/henad-app/
│ ├── Cargo.toml ~ [lib], [[bin]] required-features, example-models, henad-models optional and dev
│ ├── README.md ~ the library
│ └── src/
│ ├── lib.rs ~ crate doc, private modules, re-exports, HenadApp::new(cc, options)
│ ├── main.rs ~ the official options over run_native or start_web
│ ├── options.rs + AppOptions, Product, AppOpening, opening check, AppError, WebStartError
│ ├── native.rs + run_native, results_folder (moved from main.rs)
│ ├── web.rs + start_web, init_web_logger
│ ├── state.rs ~ product, opening_refusal, open, open_setup, open_values, headless
│ └── ui/
│ ├── about.rs ~ product block, Built on, Models rows, Copy
│ ├── menu_bar.rs ~ links from the options, "About" and the product name
│ ├── model.rs ~ the opening's refusal
│ ├── mcs.rs ~ dead_code allowed for the palette
│ ├── performance.rs ~ &AppState, the thread pool banner
│ ├── stats.rs, system.rs ~ &AppState
│ ├── export/metadata.rs ~ host and engine_version
│ ├── files/mod.rs ~ Folder expect on wasm32
│ ├── results/{mod,store,table}.rs ~ Copy command from the options, native-only series loading
│ └── sweep/{mod,builder,draft,header,layout,parameters,session}.rs ~ the command in the advice, host build
└── docs/
├── guide/app.md ~ About menu and window, Copy command
└── developing/agent-record/20261002-38-library-app.md +
State after¶
M9 is implemented and uncommitted on 48-library, on top of 4e1bd2b, in the main checkout as asked, with no worktree and no branch.
The workspace version stays 0.2.0.
HENAD_REQUIRE_GPU=1 ./check.shpasses: 1046 tests, none failed, with the wasm32 typecheck, the packaging, cargo-deny and docs steps and the web build. M8 passed at 1040.uv run --locked zensical buildpasses with no issues.cargo clippy -p henad-app --all-targetswith-D warnings -W clippy::allpasses with all features and with none.cargo tree -p henad-app --no-default-features -e normal -i henad-modelsprints nothing on stdout, andcargo build -p henad-app --no-default-featuresbuilds the library alone.
The web gate, on start_web¶
scripts/build_web.sh serve --release served the release build with Trunk's COOP and COEP headers, and dist/ passed the worker-glue check the CI job now runs.
The Claude in Chrome extension was not connected, so a separate Chrome on a temporary profile, on the second display, was driven through the DevTools protocol.
With ?threads=8#dev (#dev skips the service worker), the console showed thread pool: 8 workers, 14 reported by the browser from henad_app::web and no "thread pool init failed", window.crossOriginIsolated was true, the loading element was gone, and the System tab's Worker threads row read 8.
Served without the two headers, the same build logged "thread pool init failed" with Chrome's DataCloneError (SharedArrayBuffer transfer requires self.crossOriginIsolated), then started on one thread and removed the loading element, the new message with it.
That finding led to the review's web changes.
After the review the gate ran again on the revised start_web, in a headless Chrome, since the second display had been detached.
The release build was served again, and dist/ passed the worker-glue check.
- With
?threads=8,web.rslogged 8 workers,crossOriginIsolatedwastrue, the loading element was gone, the Performance tab had no banner, and the System tab's Worker threads row read 8. - Served without the headers, the pool failed as before, and the Performance tab showed the "Thread pool failed to start" banner with its note.
- The same page with the wasm file blocked stayed on the spinner, with the headers note appended under "Loading…".
- The About menu read Source code, Documentation and About Henad. The window's heading read "Henad", with the tagline, the links, and the Models row "henad-models 0.2.0 (sources 5d34734087eb6a85)".
The egui MCP run¶
The first run came before the review and the rename.
The release app with inspection ran on the second display, with Henad-Engine/app.ron backed up, its window moved there, and the backup restored afterwards, byte for byte.
The window reopened at the size written into app.ron.
- Picked Game of Life and built it at 1024² (314,116 alive at tick 0).
- In the Sweep tab, varied Initial Density over
0.2,0.3,0.4. The values editor read "Same format as henad-cli --vary". Start ran "Sweep finished: 3 runs, 0 failed, in 341 ms". - In Results, selected run 1. Copy command put
henad-cli game_of_life --seed 4320778953317010875 --warmup 0 --steps 1000 --export-stats run-1.csvon the clipboard. Open at end built the run, stepped it to tick 1000, and the Statistics tab read 46,147 alive, the run's final value in the table. - The About menu listed Source code, Documentation and About Henad Engine. The window showed the name, tagline, links, Version 0.2.0, the commit marked modified, Sources, Build, License
MIT OR Apache-2.0and Models. Copy put the name and the six rows on the clipboard. - The first run showed the Models row as "henad-models 0.2.0 (Unknown commit)", which led to the source-hash fallback. After it the row read "henad-models 0.2.0 (sources 5d34734087eb6a85)".
The second run, after the rename, seeded a new Henad/app.ron with a window entry on the second display.
The second display had been detached by then, and macOS listed the main display alone, so the window opened on the main display at 1512 by 949.
The egui inspection label and the window title, read through CGWindowListCopyWindowInfo, both read "Henad".
The app was closed at once without driving it, and the seeded Henad folder was removed, since it did not exist before. Henad-Engine/app.ron is the same as its backup.
The About heading, the menu entry and the Performance tab banner were then seen in the web build above.
The third run drove the renamed app on the main display, which the maintainer allowed with the second display detached.
With no Henad/app.ron the window opened at eframe's default 400 by 300, and the egui MCP server resized it to 1400 by 850.
- Picked Game of Life and built it at 1024² (314,116 alive).
- Varied Initial Density over
0.2,0.3,0.4in the Sweep tab, and Start ran "Sweep finished: 3 runs, 0 failed, in 357 ms". - Selected run 1 in Results. Copy command put the same
henad-cli game_of_life --seed 4320778953317010875 --warmup 0 --steps 1000 --export-stats run-1.csvon the clipboard, and Open at end reached tick 1000 with 46,147 alive, the run's final value. - The About menu's entry read "About Henad". The window's heading read "Henad", with the tagline, the links, License
MIT OR Apache-2.0and Models "henad-models 0.2.0 (sources 5d34734087eb6a85)", and Copy put "Henad" and the six rows on the clipboard. - The app autosaved its state to a new
~/Library/Application Support/Henad/app.ronduring the run, the folder the rename names. The folder did not exist before the run and was removed after it.Henad-Engine/app.ronis the same as its backup.
Proposed commit message: feat: app as a library
Issues found & future directions¶
- Deviations from [4.5.1].
AppErroris native only, where the design leaves it on both targets: on wasm32 nothing returns it.WebStartError::source()is alwaysNone, since eframe's web error is aJsValueand nostd::error::Error. Its text is kept in the message.AppOptions::__officialis a hidden public setter, which the design does not have. - The official About window keeps Henad's logo and tagline through
Product::official. The design says only that the window shows "a product block". Another product gets itsicon_pngand no tagline, and no setter offers a tagline or a separate logo. - [4.3.5] expects a page stuck on "Loading…" without isolation. Chrome instantiates the threaded build without isolation and fails only the pool. The page's note now says the app might run on one thread or not start, and a failed pool leaves a banner in the Performance tab. The design's template
index.htmlreplaces the loading text with the headers message, and M10d writes it, so the template page wants the same wording. - The Performance tab note cannot tell why the pool failed. web-sys 0.3 has no binding for
Window::crossOriginIsolated, and the note names the headers as the likely cause and points at the console. Reading the flag needs js-sys, which henad-app does not depend on. - The
example-modelslines of henad-cli and henad-app carry no "Breaking:". Neither crate had features in 0.2.0, so no 0.2 dependent can have turned one off, and their libraries never re-exported henad-models. - The nightly atomics clippy of [4.10] fails on henad-app, before and after M9. On the pinned nightly with
-D warnings -W clippy::all, 12 findings fire both at 4e1bd2b and here:arc_with_non_send_sync(four, wgpu handles are notSendwith atomics),manual_midpoint,as_chunks_mut, a tuple-to-array conversion, an unnecessaryget, andneedless_pass_by_ref_mut,unused_selfandlet_underscore_untypedin two cfg-split functions ofui/sweep/mod.rs. M9 adds none. Lint henad-app alone with--no-deps, since the newer clippy also fires in henad-compute. M10b'slintjob adds this pass, and these come first. - A wheel event sent through the DevTools protocol did not scroll an egui scroll area. Closing tabs made room instead, in the gate's throwaway profile.
- No CI line builds henad-app without default features. The checks above ran by hand, as M8's did, and M10b's
featuresjob is their home. - Next. M10a (the testing kit) depends on M5, M6 and M7, and M10b's facade on M8, M9 and M10a.