Skip to content

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 (builds crates/tvx-wasmpackages/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 lint is eslint . + prettier --check .. Prettier does not format docs/ — keep it that way.
  • /developers/testing is the operator's manual for the suites, the golden policy and the CI workflow.
  • docs/RELEASING.md is the operator's manual for packaging: scripts/release.sh <version> bumps every version in one place, scripts/package-linux.sh builds the Linux artefacts in Docker, and scripts/smoke-artefact.mjs launches a packaged binary with --job and asserts it rendered. CITATION.cff is bumped by it too. Never push a tagrelease.sh deliberately 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:

sh
export TETRAVOX_TESTDATA=/Users/idohaber/datasets/000/derivatives/SimNIBS/sub-ernie

Reference values for that dataset are scripts/refvalues/*.json, not this file. Regenerate them; never retype a number from memory or from an older revision:

sh
~/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 docstring

The files that matter and why:

FileWhy it is the test file
m2m_ernie/T1.nii.gzfloat32, 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.gzFSL 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.gza float32 label volume, 57 integral values to 530 — an is_label heuristic that requires an integer dtype misclassifies it
m2m_ernie/ernie.msh184 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.msh420 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.msh1,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.mshtwo 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.giiGIfTI GZipBase64Binary (a zlib stream — ZlibDecoder, not GzDecoder); the contour reference
m2m_ernie/eeg_positions/GSN-HydroCel-185.geoa 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

  1. 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.
  2. Every parser/geometry function ships synthetic tests plus a real-data test gated on TETRAVOX_TESTDATA.
  3. 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 editing docs/ARCHITECTURE.md in the same commit. Additive is the normal case, and absent must reproduce the previous behaviour — that is what makes "additive" a guarantee.
  4. Lockfiles are frozen. pnpm-lock.yaml and Cargo.lock are never merged: on conflict take main's version and re-run pnpm install / cargo check --workspace. A new dependency is a deliberate change with a line in docs/DECISIONS.md — that line, not the diff itself, is what makes a pnpm-lock.yaml change reviewable.
  5. Conventional commits, no Co-Authored-By trailers. main is 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.
  6. 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).
  7. No rayon, no wasm threads, no nightly Rust. Parallelism is worker-per-dataset.
  8. 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 with headless: false. Prove it with scripts/e2e-quiet-check.sh (export TETRAVOX_TESTDATA first: that script proves what a run showed, never what it covered).

Gotchas that still bite

  • pnpm wasm before typechecking. Never tsc against a missing pkg/.
  • Electron ≥ 42 downloads its ~100 MB binary on first launch, not on install. Run pnpm exec electron --version once on a cold machine before pnpm e2e. On Linux add --no-sandbox, even for --version — the npm tarball's chrome-sandbox is not root-owned setuid and Chromium aborts.
  • pnpm package builds this platform's artefacts only. Linux artefacts come from CI or docker 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 from forceCaps on the chromium-angle leg, not from a picture.
  • Never use gl_CullDistanceMAX_CULL_DISTANCES_WEBGL is 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 *WidthPx on line geometry is instanced screen-space quad expansion.
  • A LINEAR filter on a non-filterable format samples 0 with no GL error. Check caps first.
  • Cap geometry must have its own clip plane disabled for that draw — cap vertices lie exactly on it, and gl_ClipDistance == −1e-7 deletes 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_perm is 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..10 is wrong.
  • scl_slope is not NaN on disk for any reference volume; nib.load(p).header reports NaN because nibabel hands scaling to the array proxy. Read the raw 348-byte header.
  • Cancellation is worker.terminate(), always. There is no SharedArrayBuffer to poll (§1, §5 rule 6).
  • VolumeDataset.data is cloned, never transferred, when a label volume goes to a mesh worker. Transferring it detaches the array every probe reads.

MIT licensed. macOS and Linux first; a Windows build is published too.