Working on Tetravox (for humans and agents)
Read docs/ARCHITECTURE.md first — it is the contract, and its section numbers are cited from code comments and tests. docs/ROADMAP.md says what is open. docs/DECISIONS.md is append-only.
Commands
pnpm install·pnpm wasm(buildscrates/tvx-wasm→packages/wasm/pkg) ·pnpm build·pnpm test(cargo test + wasm + vitest) ·pnpm e2e(Playwright) ·pnpm dev·pnpm package- Rust only:
cargo test --workspace,cargo clippy --workspace --all-targets -- -D warnings,cargo bench -p <crate> pnpm lintiseslint .+prettier --check .. Prettier does not formatdocs/— keep it that way./developers/testingis the operator's manual for the suites, the golden policy and the CI workflow.docs/RELEASING.mdis the operator's manual for packaging:scripts/release.sh <version>bumps every version in one place,scripts/package-linux.shbuilds the Linux artefacts in Docker, andscripts/smoke-artefact.mjslaunches a packaged binary with--joband asserts it rendered.CITATION.cffis bumped by it too. Never push a tag —release.shdeliberately stops at a local commit and tag.
Test data
Synthetic fixtures live in testdata/, generated by scripts/gen-fixtures.py and committed, with testdata/manifest.json carrying their expected values — produced by nibabel, SimNIBS and Gmsh reading the fixtures back, never by the writer that made them (§11).
Real-data tests skip, never fail, when TETRAVOX_TESTDATA is unset:
export TETRAVOX_TESTDATA=/Users/idohaber/datasets/000/derivatives/SimNIBS/sub-ernieReference values for that dataset are scripts/refvalues/*.json, not this file. Regenerate them; never retype a number from memory or from an older revision:
~/Applications/SimNIBS-4.6/bin/simnibs_python scripts/refvalues/mesh_refvalues.py
python3 scripts/refvalues/nifti_refvalues.py
python3 scripts/refvalues/contour_refvalues.py
python3 scripts/refvalues/mni_refvalues.py
python3 scripts/refvalues/mgz_refvalues.py > scripts/refvalues/mgz_refvalues.json # TETRAVOX_MGZ; see its docstringThe files that matter and why:
| File | Why it is the test file |
|---|---|
m2m_ernie/T1.nii.gz | float32, max exactly 65535.0 — which is why R16F is not the default (half-float tops out at 65504). Also the qfac = −1 reference |
m2m_ernie/DTI_coregT1_tensor.nii.gz | FSL six-component tensors with no tensor intent — frame count alone cannot select tensor mode. scripts/refvalues/tensor_refvalues.py regenerates the NumPy eigen/shape reference. |
m2m_ernie/segmentation/labeling.nii.gz | a float32 label volume, 57 integral values to 530 — an is_label heuristic that requires an integer dtype misclassifies it |
m2m_ernie/ernie.msh | 184 MB, 847,165 nodes / 1,177,213 tris / 4,722,625 tets. No $PhysicalNames — its .msh.opt is the only source of tissue names |
Simulations/*/high_Frequency/mesh/ernie_TDCS_1_scalar.msh | 420 MB, the only reference file with a vector field (E, 3 components) — the test file for glyphs, component: 0|1|2 and the electrode/gel palette |
Simulations/Thalamus/TI/mesh/grey_Thalamus_TI.msh | 1,340,029 tets and 0 triangles — anything assuming a mesh ships its own surface renders an empty 3D view |
m2m_ernie/ernie_seeg.msh / m2m_ernie-seeg/ernie-seeg.msh | two different files, both over 2²¹ nodes, so both break a 3×21-bit packed face key. Either is the face-key-width test |
m2m_ernie/surfaces/lh.pial.gii | GIfTI GZipBase64Binary (a zlib stream — ZlibDecoder, not GzDecoder); the contour reference |
m2m_ernie/eeg_positions/GSN-HydroCel-185.geo | a Gmsh parsed view: 187 SP points + 187 T3 labels |
Also on this machine: ~/Applications/SimNIBS-4.6/bin/gmsh (the only local way to make Gmsh 4.1 fixtures), and simnibs/mesh_tools/mesh_io.py as a read-only cross-check — note its element-block skip arithmetic is wrong (it hard-codes 2 tags into a 3); §6.2 has the correct layout.
Rules
- Rendering is verified by numbers first, pictures second (§11). Every rendering feature ships an analytic pixel assertion — expected RGBA computed from first principles on a synthetic fixture — plus a golden PNG. Regenerating a golden requires a commit body stating what changed visually. Do not "verify" a rendering change by looking at a PNG and declaring it fine.
- Every parser/geometry function ships synthetic tests plus a real-data test gated on
TETRAVOX_TESTDATA. - Frozen interfaces (§12.3):
packages/protocol/src/index.ts,packages/engine/src/scene/types.ts,packages/engine/src/api.ts,packages/wasm/src/index.ts, and every §6 Rust signature. Changing any of them requires editingdocs/ARCHITECTURE.mdin the same commit. Additive is the normal case, and absent must reproduce the previous behaviour — that is what makes "additive" a guarantee. - Lockfiles are frozen.
pnpm-lock.yamlandCargo.lockare never merged: on conflict takemain's version and re-runpnpm install/cargo check --workspace. A new dependency is a deliberate change with a line indocs/DECISIONS.md— that line, not the diff itself, is what makes apnpm-lock.yamlchange reviewable. - Conventional commits, no
Co-Authored-Bytrailers.mainis usually checked out in another session's primary worktree — never check it out or push to it directly. Work on your own branch in your own worktree instead:git worktree add ../tetravox-wt-<name> -b feat/<name> origin/main, push that branch, and open a PR. - Never block the UI thread with parsing or geometry. De-indexing, normal generation and any vertex-buffer expansion count as geometry: they happen in the dataset's worker and arrive as transferables. Raw file bytes never touch the UI thread or IPC (§5).
- No
rayon, no wasm threads, no nightly Rust. Parallelism is worker-per-dataset. - A test run may not hijack the monitor. E2E is windowless by default on macOS and the default must stay that way — neither leg gives up any GPU coverage for it (
/developers/testing§2.1 has the measurements). Do not "fix" a flaky test by making its window visible, and do not add a Playwright project withheadless: false. Prove it withscripts/e2e-quiet-check.sh(exportTETRAVOX_TESTDATAfirst: that script proves what a run showed, never what it covered).
Gotchas that still bite
pnpm wasmbefore typechecking. Nevertscagainst a missingpkg/.- Electron ≥ 42 downloads its ~100 MB binary on first launch, not on install. Run
pnpm exec electron --versiononce on a cold machine beforepnpm e2e. On Linux add--no-sandbox, even for--version— the npm tarball'schrome-sandboxis not root-owned setuid and Chromium aborts. pnpm packagebuilds this platform's artefacts only. Linux artefacts come from CI ordocker run electronuserland/builder.- The golden authority (SwiftShader) has no
EXT_texture_norm16, so goldens pin the R32F branch of §6.1's ladder while the shipping renderer uses R16. Format-path coverage comes fromforceCapson thechromium-angleleg, not from a picture. - Never use
gl_CullDistance—MAX_CULL_DISTANCES_WEBGLis 0 on ANGLE/Metal but 8 under SwiftShader, so CI would pass while every real Mac fails. A lint forbids the identifier. gl.lineWidth()is a no-op. Every*WidthPxon line geometry is instanced screen-space quad expansion.- A
LINEARfilter on a non-filterable format samples 0 with no GL error. Checkcapsfirst. - Cap geometry must have its own clip plane disabled for that draw — cap vertices lie exactly on it, and
gl_ClipDistance == −1e-7deletes the primitive entirely. - Gmsh element numbers are not internal indices. The UI always reports the Gmsh number; tets are stored in Morton order and
tet_permis the way back. - Mesh tags are not contiguous — tag 4 is absent from ernie — and a SimNIBS simulation mesh adds electrode/gel tags in the 101/501/1101/2101 ranges. Code that assumes
1..10is wrong. scl_slopeis not NaN on disk for any reference volume;nib.load(p).headerreports NaN because nibabel hands scaling to the array proxy. Read the raw 348-byte header.- Cancellation is
worker.terminate(), always. There is noSharedArrayBufferto poll (§1, §5 rule 6). VolumeDataset.datais cloned, never transferred, when a label volume goes to a mesh worker. Transferring it detaches the array every probe reads.