# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

VARASYS PolyMeter: a polymetric groove-trainer / metronome **engine** that ships in three guises from one repo — a set of self-contained web pages (editor + form-factor gallery + embeddable widget), MicroPython/CircuitPython **firmware** for real hardware, and a native **Rust** port. `README.md` is the product-level reference (share-language grammar, page list, features, keyboard shortcuts) — read it for "what does this token mean"; this file is for how the pieces fit and how to work on them.

## Commands

```sh
./build.sh                 # assemble self-contained pages into dist/ (also precompiles firmware .mpy + zips bundles)
./deploy.sh                # build, stamp version, copy to the Caddy web root, smoke-test (auto-run after changes — see memory)
./release.sh [X.Y.Z]       # bump VERSION (optional) + tag v<VERSION>; requires clean tree

node tests/run.mjs         # track-format conformance: every golden vector through engine.js AND pico-cp/app.py
node tests/run.mjs -v      # + print expected/actual diffs for unexpected failures

./rust/run.sh              # cargo test (Rust track-format crate vs the same golden vectors), in container
./rust/run.sh cargo build  # or `bash` for a shell

./hardware/eda/run.sh                                  # interactive shell in the KiCad/ngspice container (lands in hardware/kicad/)
./hardware/eda/run.sh kicad-cli sch erc pm_k1_core.kicad_sch   # ERC on the board schematic
./hardware/eda/run.sh ngspice -b ../eda/sim/<deck>.cir         # run a SPICE deck
```

There is no lint step and no single-test filter — `node tests/run.mjs` is the one gate; its exit code is non-zero on any unexpected failure or round-trip break, so it doubles as CI.

## The track format is the contract

The "program" / "patch" / share-language string is the spine of the whole project: the same string drives the web editor, a hardware device, and the Rust crate. Its grammar is **hand-implemented in three places that can silently drift**, so it is formally specced and conformance-tested:

- **Spec (source of truth):** `docs/track-format.md`
- **Web:** `src/engine.js` — `patchToSetup` / `laneStrToCfg` / `setupToPatch` / `laneCfgToStr`
- **Firmware:** `pico-cp/app.py` — `parse_program` / `_parse_lane` / `lane_to_str` / `_prog_str`
- **Rust:** `rust/track-format/src/lib.rs`
- **Golden vectors:** `tests/fixtures/track-format.json` — each case has `in` (a patch), `norm` (expected meaning), a `status`, and optional `expectFail` listing impls known to differ today.

**Rule: any change to the grammar or its meaning must update the spec, add/adjust a golden vector, and keep all three implementations passing.** When you fix a divergence in one impl, delete it from that case's `expectFail`. The test adapters parse the *real* `engine.js` and `app.py` (the Python one via `ast` extraction) rather than copies, so a code change is what the suite actually sees.

## Web build system

Every deployed page is **one self-contained `.html` file, zero runtime dependencies** — no framework, no CDN, no audio samples (all voices are synthesized in Web Audio). Pages stay in sync by sharing code through build markers that `build.sh` resolves:

- `/*@BUILD:include:src/<file>@*/` inlines a shared partial. The important ones: `engine.js` (audio scheduler + DSL), `setlists.js` (seed set lists baked into every page), `base.css`, `header.html`/`footer.html`/`chrome.js`, `progbox.{html,js}` (program box), `infoembed.{html,js}` (info-page live widget), `livesync.js` (beta only).
- `@BUILD:favicon@` / `@BUILD:logo-dark@` / `@BUILD:logo-light@` inline base64 blobs from `assets/`.

`build.sh` asserts no `@BUILD:` markers survive. **`dist/` is generated and git-ignored — never edit it by hand.** Edit the source `*.html` in the repo root and the partials in `src/`; `deploy.sh` always builds first. Each page exists as a lean widget (`<device>.html`, what `?embed=1` serves) plus a spec page (`info-<device>.html` that embeds it). `editor.html` is the main app; `editor-beta.html` is identical plus `livesync.js` (live mirror to a connected device over Web-MIDI SysEx — protocol in `docs/livesync-protocol.md`).

State (set lists, practice log, theme) lives in `localStorage`; nothing is uploaded — share links encode everything in the URL hash (`#p=` patch, `#sl=` base64url set list).

## Firmware editions

All run the same DSL and `programs.json`. When you touch the grammar, the `pico-cp/app.py` half is covered by the conformance suite; the others are not, so keep them in step by hand.

- `pico/main.py` — single-file **MicroPython**, the simple no-computer fallback (52Pi EP-0172 Kit).
- `pico-cp/` — **CircuitPython** appliance (USB-drive, push-programming over USB-MIDI, on-device practice log, one-click A/B firmware update). `build.sh` precompiles `app.py` → `app.mpy` (the RP2040 OOMs compiling the ~56KB source at boot).
- `pico-explorer/` — CircuitPython sibling for the Pimoroni Explorer (RP2350, buttons-only, no touch).
- `pico-scroll/` — CircuitPython sibling for the Pimoroni Pico Scroll Pack (PIM545: 17×7 mono LED matrix + 4 buttons on a plain RP2040 Pico). Engine/scheduler/SysEx copied from `pico-explorer/`; vendors a bulk-framebuffer IS31FL3731 driver; renders three LED views (Grid/Pendulum/BPM). No speaker (audio over USB-MIDI). This is the UI prototype for the eventual Rust `pm-grid` board — see `docs/rust-port.md`.

Firmware device IDs (reported on the SysEx `0x02→0x03` version query, used by the editor's firmware-push to pick the right `.mpy`): `K` Kit, `X` Explorer, `G` Grid.

**`pico-cp/app.py` and `pico-explorer/app.py` must stay pure ASCII** — they are pushed over USB-MIDI as 7-bit data, and a stray non-ASCII char gets mangled to NUL and bricks the device. `build.sh` enforces this with an assert.

## Hardware (PM_K-1 custom board) & Rust port — containerized only

Per the standing rule (see memory), **all EDA and Rust tooling runs in containers via the `run.sh` wrappers — never install these on the host.** Add tools to the respective `Containerfile`. `hardware/eda/` holds the KiCad 9 + ngspice environment; the board design is `hardware/DESIGN.md` + `BOM*.csv` + `kicad/` + the code-defined circuits in `eda/circuits/`. The Rust port is staged inside-out (`docs/rust-port.md`): the pure track-format crate is done and passing; drivers/firmware come only after the engine is proven against the vectors.

## Versioning

`VERSION` holds the formal version. `deploy.sh` stamps the served pages: a clean tree on a commit tagged `v<VERSION>` → `X.Y.Z`; anything else → `X.Y.Z-dev.<utc-ts>.<sha>[.dirty]`. Source files keep a placeholder `APP_VERSION`; only the deployed copy is stamped.
