Skip to content

Run a library model in the reader's browser - #515

Open
GondekNP wants to merge 1 commit into
feat/runnable-static-docsfrom
feat/docs-browser-runner
Open

Run a library model in the reader's browser#515
GondekNP wants to merge 1 commit into
feat/runnable-static-docsfrom
feat/docs-browser-runner

Conversation

@GondekNP

@GondekNP GondekNP commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Phase 2 of the runnable-docs work: a library page whose model can execute in a browser gets a Run it here button.

What a reader gets

Five pages today — the three guides, plus hello_debug and spinup. The button runs the model on the same engine the CLI uses, then plots the result with the demo's existing scrub timeline and grid heatmap.

The part that matters

The run box reads the model out of the listing already on the page. It does not carry its own copy, so the code that runs is necessarily the code the reader is looking at. There is nothing to keep in sync because there is no second copy — the same promise the rest of this pipeline makes, extended to execution.

The plotted variables are discovered from the result, not declared: the records know their own attribute names. A page gains a new plot by the model exporting a new value, with no front-matter change.

browserRunnable is derived, not declared

The harvest asks the jar (inspect-exports) whether every target a simulation declares uses the memory protocol. WebAssembly has no filesystem, so a file:// target aborts the run. A model declaring no target at all qualifies — it still executes, which is the whole point for one whose assert handlers are the result, and that is how spinup earns a button. spinup_export is correctly excluded for its file:// target. A unit naming no simulation is excluded rather than guessed at, and a model the engine rejected is never offered.

Cost is 10 extra JVM starts, gated by the same sound text check inspect-externals already uses: harvest goes 40s → 52s.

Data publishing moves into the renderer

joshdocs render now decompresses the committed .jshdz beside the page that declares it, at data/<unit id>/<name>.jshd. This retires the Organize tutorial data for guides step in deployStatic.

One rule in one language, and a local preview serves the same bytes from the same paths as the deployed site — which is what lets the run button work before anything has been deployed. Nesting under the unit id matters: declared filenames are only unique within a directory, so a flat layout would quietly serve one guide's precipitation.jshd to another.

Guide prose now links data by its published name, so those links are checked by the dead-link pass for the first time — the site-absolute /guides/... spellings never were.

Verified by running it, not reading it

Nothing in CI lints or executes JavaScript, so the browser files under landing/js/ are copied byte-for-byte from demo.joshsim.org/js/; runner.js is the only new one. To check it anyway I installed node locally (not on the build path) and drove the engine headlessly:

unit result
hello 34,782 records
hello_debug 34,782 records + 347,820 debug messages
grass_shrub_fire 91,728 records, read from the published .jshd
spinup 10 steps, no exports

Plus: a sabotaged assert surfaces as Assertion failed for assert.alwaysFails, which is the path the status line reports; every runnable page's listing unescapes to the authored .josh byte for byte; the published data is byte-identical to xz -d; and every import specifier in the module graph resolves.

Follow-ups

  • Thread the seed through JoshJsSimFacade.runSimulationwasm.worker.jswasm.js, so a browser run is as deterministic as CI.
  • landing/js/ is now a third copy of six files that editor/js/ and demo.joshsim.org/js/ already share. Five are byte-identical across all three and the sixth (wasm.js) differs by five lines that are a strict improvement — a dedupe is cheap and safe, but it touches the editor and demo, which nothing tests.

178 tests, ruff clean.

🤖 Generated with Claude Code

A page whose model can execute in a browser gets a "Run it here" button. The
engine is the same one the command line uses, compiled to WebAssembly and staged
into the landing site by landing/war/get_from_jar.sh -- the same arrangement the
editor and the demo already have, since each is deployed to its own host.

The run box reads the model out of the listing already on the page rather than
holding a copy of it, so the code that runs is necessarily the code the reader is
looking at. Nothing here transcribes Josh and nothing can drift from the authored
file. The variables offered in the result plot are discovered from the result --
the records know their own attribute names -- so a page gains a new plot by the
model exporting a new value, with no front-matter change.

browserRunnable is derived rather than declared: the harvest asks the jar whether
every target a simulation declares uses the memory protocol. WebAssembly has no
filesystem, so a file:// target aborts the run, and a model declaring no target
at all qualifies -- it still executes, which is the point for one whose assert
handlers are the result. That gives five pages today: the three guides, plus
hello_debug and spinup. spinup_export is correctly excluded for its file://
target.

Data publishing moves from deployStatic into `joshdocs render`, which decompresses
the committed .jshdz beside the page that declares it. One rule in one language,
and a local preview now serves the same bytes from the same paths as the deployed
site -- which is what lets the run button work before anything has been deployed.
Guide prose links data by its published name, so those links are checked by the
dead-link pass for the first time; site-absolute ones never were.

The browser JavaScript is copied byte-for-byte from demo.joshsim.org/js/, with
runner.js the only new file. Nothing in CI lints or executes JavaScript, so a
proven copy beats new code.

Verified by executing the engine headlessly rather than reading it: hello (34,782
records), hello_debug (+347,820 debug messages), grass_shrub_fire (91,728 records
read from the published .jshd) and spinup (10 steps, no exports) all run, a
sabotaged assert surfaces as "Assertion failed for assert.alwaysFails", and each
page's listing unescapes to the authored .josh byte for byte.

Note the engine currently runs as JavaScript rather than WebAssembly: the WasmGC
build has failed validation since #334, tracked in #514. The run box works either
way.

178 tests, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant