This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
docs/design.md— full rationale + the end-to-end pipeline (§2) and runtime/build split (§9).docs/spec.md— normative requirements (FR-*,NFR-*).docs/plan.md— the phasedT-NNtask breakdown with dependencies and the critical path; its checkboxes are the live source of truth for what's built vs. pending. (Task IDs are stable but the set grows/renumbers — cite specific tasks by ID, not by range.)docs/style.md— the mandatory, enforced Rust style guide (backed by committed config + blocking CI).
Keep these in sync with any code you write.
A Rust CLI that deterministically turns text into BB-8-like droid sound, where
semantically similar text sounds similar (a learnable sound-language). Pipeline detail
is in docs/design.md §2; the workspace has three crates:
dootdoot-core— pure, deterministic engine (functional core):.dootasset parser, tokenizer, mapping,VOICE_V12word-class table (PosTablesidecar parser + class markers/resolutions), synth, owned math, WAV,VOICE_V*constants. No I/O, no audio device.dootdoot— thin CLI shell (imperative shell):clap, stdin,rodioplayback,--explain, error/exit mapping. Holds essentially all side effects.xtask— build-time only, never shipped: generatesassets/dootdoot_asset_v1.dootfrompotion-base-8Mviamodel2vec-rs, and (viapos-table) bakes theVOICE_V12class-table sidecarassets/dootdoot_pos_v1.dootfrom the pinnedassets/pos/tagged_counts.tsvstatistics snapshot.
- model2vec /
candleare BUILD-TIME ONLY. The shipped binary has no tensor runtime: the token→4-axis mapping and tokenizer JSON are precomputed intoassets/dootdoot_asset_v1.dootandinclude_bytes!-embedded. (Sound because PCA projection is linear: pooling baked vectors == pooling-then-projecting, exact before the int16 quantization the table uses.) - The sequence baseline is dootdoot's own pooling, NOT
model2vec.encode().encode()L2-normalizes the pooled vector (potion-base-8Mhasnormalize: true); that step is nonlinear and does not commute with the projection, so it can't be recovered from baked 4-axis vectors. dootdoot's baseline is the token-weight-scaled mean in PCA space, denominator = token count, no L2 norm — a documented,VOICE_V1-pinned divergence (design.md §4.2, FR-11). Goal: relative semantic ordering, notencode()equivalence. - Determinism is bit-exact on the CI-verified platforms (macOS + Linux); Windows is
intended but not yet guaranteed. No libm transcendentals in the audio path
(
sin/exp/tanhare our own pinned impls); synthesis inf64; one fixed float→i16 rounding rule; no fast-math/FMA. Any parallelism must be byte-identical to serial. VOICE_V1is a versioned contract over everything affecting an output sample (mapping, quantization scales, tokenizer config, synth + timing constants, punctuation rules, chirp, float→i16 rounding, WAV serialization, math version). Any change that alters even one sample MUST bump the version (V1→V2) and regenerate golden fixtures. Golden-WAV hash tests are that contract.- One canonical audio buffer feeds both file output and playback, so what plays equals what's saved.
Implement every behavior test-first: red (write a failing test, confirm it fails for
the right reason) → green (minimum code to pass) → refactor (clean up, stay green).
The pure functional core exists to make this easy. Pick the cheapest test level that pins
the behavior — value test, proptest invariant, insta snapshot, or golden-WAV hash
(see docs/style.md §9). If a test is hard to write, fix the design, don't skip it.
Uses jj (Jujutsu), colocated with .git — use jj, not git. Segment work into
small, focused revisions, each a single coherent change with a clear description (imperative
summary + a "why" body when non-obvious). Aim for ~one T-* task / one red-green cycle per
revision. Start a new revision (jj new) before beginning the next logical change.
# Run
cargo run -p dootdoot -- "hello there" # play live
cargo run -p dootdoot -- "hi" -o hi.wav # write WAV (no playback)
cargo run -p dootdoot -- "hi" -o hi.wav --play
# Test (includes doctests)
cargo test
cargo test -p dootdoot-core <test_name> # one crate / a single test by name
# Format / lint the WHOLE repo (Rust + Markdown/TOML/YAML/JSON/…). Prefer these:
scripts/fmt # cargo +nightly fmt + oxfmt (writes in place)
scripts/lint # clippy + fmt --check + oxfmt --check (CI-safe, no writes)
# Both skip the Rust steps until a Cargo workspace exists, and resolve oxfmt from
# PATH or via npx. oxfmt (non-Rust files) is configured by .oxfmtrc.json.
# The underlying tools, if you need them directly:
# Format — NIGHTLY rustfmt required (rustfmt.toml uses nightly-only options);
# build/test stay on pinned stable.
cargo +nightly fmt # --check in CI
# Lint (warnings are errors in CI)
cargo clippy --all-targets -- -D warnings
# Coverage (≥95% on dootdoot-core), dependency hygiene
cargo llvm-cov
cargo deny check && cargo machete
# Regenerate the baked asset (ONLY when intentionally changing mapping/tokenizer inputs)
cargo run -p xtask
# Regenerate the VOICE_V12 class-table sidecar (ONLY when intentionally changing the
# POS snapshot or classification policy; classification changes are a voice bump)
cargo run -p xtask -- pos-table # then copy target/generated/dootdoot_pos_v1.doot to assets/
uv run scripts/derive_pos_table.py # only if the statistics snapshot itself changes
# Directional BB-8 acoustic comparison (TUNING AID, not the voice contract).
# Renders a phrase, decodes a reference clip, and reports gap-analysis metrics
# (active fraction, max internal gap, dominant-peak range, harmonicity IQR,
# spectral centroid, 2-5 kHz share) via a locked uv (numpy/scipy) environment.
scripts/acoustics "<phrase>" /path/to/inquisitive-then-chatty.mp3
# Underlying locked PEP 723 script (numpy/scipy pinned by acoustic_metrics.py.lock):
uv run scripts/acoustic_metrics.py reference=ref.wav dootdoot=render.wav
# Regenerate the committed golden WAV fixtures (tests/fixtures/golden/*.wav)
# after an intentional, version-bumped voice change:
DOOTDOOT_REGEN_GOLDEN=1 cargo test -p dootdoot-core --test golden_wav
# Regenerate package-lock.json (ONLY when package.json changes) with the EXACT npm
# version the Documentation workflow pins — npm minors disagree about wasi
# optional-peer lockfile entries and `npm ci` hard-fails on the mismatch. If the pin
# in .github/workflows/docs.yml is bumped, regenerate the lockfile and update the
# expected step list in tests/docs-site.test.mjs in the same change. (docs/README.md)
npx npm@11.16.0 install --package-lock-only- Functional core / imperative shell — pure logic in
dootdoot-core, effects behind injected traits in the binary. This is what makes ~99% of code deterministically testable. forbid(unsafe_code)workspace-wide. Noasfor numeric casts (useFrom/TryFromor the named quantization helper). No reliance onHashMapiteration order.- Files: one primary construct per file, named after it, namesake at top, ordered by
relevance (public first); no
mod.rs. Complex tests in<namesake>/tests.rs. - Public API is a curated
pub usefacade over private modules; don't leak dependency types (hound/rodio/tokenizers) across public boundaries. - Errors:
thiserrorin libs,anyhowonly in the binary; nounwrapoutside tests; panic only for invariants, never bad input.