mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
docs: add CLAUDE.md documenting the jsnes harness and repo conventions
New top-level `CLAUDE.md` so future AI agents (and humans) don't have
to rederive the project conventions from the existing docs. Covers:
- The phase layout under `src/` and the expectation that every
module has a co-located `tests.rs`.
- The core `cargo` commands and the fact that `fmt` and `clippy`
are mandatory before committing.
- The jsnes emulator harness end-to-end:
- File layout (`harness.html`, `run_examples.mjs`, `goldens/`,
`actual/`).
- How to run it locally (build release compiler, compile every
example, `npm install`, `node run_examples.mjs`).
- How to update goldens with `UPDATE_GOLDENS=1` and the rules for
when that's allowed.
- The procedure for adding a new example (build, golden, verify,
README).
- What the harness catches (codegen / runtime / PPU / APU / asset
regressions) vs. what it doesn't (input, beyond frame 180).
- How the CI job in `.github/workflows/ci.yml` wires everything up
and uploads `actual/` on failure.
- Zero-page allocation rules (runtime reserves `$00-$0F`,
palette/bg reserves `$11-$17` conditionally, user vars `$10+` or
`$18+`, IR temps `$80+`) so the next agent doesn't accidentally
collide with the runtime.
- "Things to avoid" list: no backwards-compat shims, no silencing
goldens without understanding the diff, no committing the
gitignored harness directories.
https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
This commit is contained in:
parent
d98c7f3d82
commit
25946f74ba
1 changed files with 166 additions and 0 deletions
166
CLAUDE.md
Normal file
166
CLAUDE.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# CLAUDE.md
|
||||
|
||||
Guidance for Claude Code (and any other AI agents) working in this repo.
|
||||
Keep it short and practical — it's here so the next agent doesn't have to
|
||||
re-derive the project conventions from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Project shape
|
||||
|
||||
- **NEScript** is a Rust-based compiler that turns `.ne` source files into
|
||||
iNES ROMs. Single binary, no external assemblers, no external linkers.
|
||||
- `src/` is a flat module layout: each compiler phase is its own directory
|
||||
with `mod.rs` + `tests.rs`. See `docs/architecture.md` for the phase
|
||||
pipeline.
|
||||
- Examples live in `examples/*.ne`. Every example is expected to compile
|
||||
cleanly and has a pinned emulator golden — see below.
|
||||
- `docs/future-work.md` lists the remaining gaps. If you implement
|
||||
something from that file, update the doc in the same PR.
|
||||
|
||||
## Running the basics
|
||||
|
||||
```bash
|
||||
cargo build --release # build the compiler
|
||||
cargo test # all Rust tests (lib + integration)
|
||||
cargo fmt # mandatory before committing
|
||||
cargo clippy --all-targets # mandatory before committing; fix or #[allow]
|
||||
./target/release/nescript build examples/hello_sprite.ne # build one ROM
|
||||
```
|
||||
|
||||
Compile every example at once:
|
||||
|
||||
```bash
|
||||
for f in examples/*.ne; do cargo run --release -- build "$f"; done
|
||||
```
|
||||
|
||||
## The jsnes emulator harness
|
||||
|
||||
This is the most important piece of project-specific tooling. Every `.ne`
|
||||
example has a **pixel-exact PNG golden** and a **sample-exact audio hash**
|
||||
committed under `tests/emulator/goldens/`. Any compiler change that alters
|
||||
observable behaviour — codegen, optimizer, runtime, linker, asset pipeline
|
||||
— will flip at least one golden, and CI will fail loudly with a visible
|
||||
diff. Do not skip or weaken this check.
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
tests/emulator/
|
||||
harness.html # thin wrapper around jsnes; exposes window.nesHarness
|
||||
# with loadRomBase64, runFrames, rawPixelsBase64,
|
||||
# audioHash, audioWavBase64
|
||||
run_examples.mjs # puppeteer-driven runner (headless Chrome)
|
||||
package.json # depends on jsnes, pngjs, puppeteer
|
||||
goldens/
|
||||
<name>.png # 256×240 RGBA framebuffer at frame 180 (~3s at 60fps)
|
||||
<name>.audio.hash # one line: "<fnv1a-hex> <sample-count>"
|
||||
actual/ # gitignored; written on every run for diff artifacts
|
||||
```
|
||||
|
||||
### Running it locally
|
||||
|
||||
The harness is **separate** from `cargo test`. You have to run it by hand:
|
||||
|
||||
```bash
|
||||
# 1. Build every example first. The harness reads pre-built .nes files
|
||||
# from examples/ — it will not invoke cargo.
|
||||
cargo build --release
|
||||
for f in examples/*.ne; do ./target/release/nescript build "$f"; done
|
||||
|
||||
# 2. Install node deps (once per worktree; node_modules/ is gitignored).
|
||||
cd tests/emulator
|
||||
npm install # or `npm ci` in CI
|
||||
|
||||
# 3. Verify every example still matches its golden.
|
||||
node run_examples.mjs
|
||||
# → "21/21 ROMs match their goldens" on success
|
||||
# → FAIL / MISS lines + `actual/<name>.png`, `actual/<name>.diff.png`,
|
||||
# `actual/<name>.wav` written for any ROM that mismatched
|
||||
```
|
||||
|
||||
### Updating goldens
|
||||
|
||||
If a change is supposed to flip goldens (you added a new example, changed
|
||||
a rendering path, fixed a bug that was baked into the old output), update
|
||||
them with:
|
||||
|
||||
```bash
|
||||
cd tests/emulator
|
||||
UPDATE_GOLDENS=1 node run_examples.mjs # rewrites every mismatched golden
|
||||
# or
|
||||
node run_examples.mjs --update-goldens
|
||||
```
|
||||
|
||||
Then `git diff tests/emulator/goldens/` the result, eyeball each change,
|
||||
and include the updated PNG+hash files in the same commit as the code
|
||||
change. Goldens are the contract; the commit message should explain why
|
||||
each diff is legitimate. **Never** `UPDATE_GOLDENS=1` just to silence a
|
||||
failing CI — that defeats the entire purpose of the harness.
|
||||
|
||||
### Adding a new example
|
||||
|
||||
1. Write `examples/<name>.ne`.
|
||||
2. Build it with the release compiler so a `.nes` file lands next to it.
|
||||
3. Run `UPDATE_GOLDENS=1 node run_examples.mjs` to generate
|
||||
`goldens/<name>.png` and `goldens/<name>.audio.hash`. Both files must
|
||||
be committed — the runner treats missing goldens as a hard failure.
|
||||
4. Verify visually that the generated PNG is what you actually intended
|
||||
(open it; you can use Read on the PNG file to have Claude display it).
|
||||
5. Add the example to the tables in `README.md` and `examples/README.md`.
|
||||
|
||||
### What the harness tests (and doesn't)
|
||||
|
||||
- **Tests**: final rendered framebuffer at frame 180, full audio sample
|
||||
stream over the same window. Catches codegen miscompiles, runtime
|
||||
bugs, linker layout changes, PPU timing regressions, APU regressions,
|
||||
asset pipeline bugs — essentially anything that affects the observable
|
||||
behaviour of a whole program.
|
||||
- **Does not test**: input handling (no buttons pressed during the run),
|
||||
anything past frame 180 (~3 seconds), state transitions that require
|
||||
user input. Examples that need input to look non-trivial should
|
||||
structure themselves so a good demo happens on autopilot — e.g. a
|
||||
frame counter that drives the interesting state (`examples/palette_and_background.ne`
|
||||
is a working pattern).
|
||||
|
||||
### CI integration
|
||||
|
||||
The `emulator` job in `.github/workflows/ci.yml` installs Chrome deps,
|
||||
builds all examples, then runs the harness. On failure it uploads the
|
||||
`actual/` directory and `report.json` as an artifact named `emulator-diff`
|
||||
so reviewers can download and inspect the pixel diffs without cloning the
|
||||
repo. The CI job does **not** pass `UPDATE_GOLDENS`; if it flips, the
|
||||
change needs a manual update + review.
|
||||
|
||||
## Conventions worth knowing
|
||||
|
||||
- Every `src/**/mod.rs` has a co-located `tests.rs`. Add unit tests
|
||||
there, not in a separate file.
|
||||
- Big cross-phase tests go in `tests/integration_test.rs`. Use the
|
||||
`compile` / `compile_banked` helpers at the top of that file instead
|
||||
of re-building the pipeline by hand.
|
||||
- Error codes live in `src/errors/diagnostic.rs`. Don't add a new code
|
||||
without emitting it from somewhere — clippy will catch unused variants,
|
||||
but past agents have also let them sit as dead code.
|
||||
- Zero page is tight. `$00-$0F` is reserved for the runtime (frame
|
||||
flag, input, OAM cursor, sfx/music pointers). `$11-$17` is reserved
|
||||
for PPU palette/background updates **when the program declares them**
|
||||
(the analyzer bumps the user ZP start from `$10` to `$18` in that
|
||||
case — programs without palette/bg keep the old `$10` layout to
|
||||
preserve their goldens). User vars go at `$10+` or `$18+`; IR temps
|
||||
land at `$80+`.
|
||||
- `docs/future-work.md` is the authoritative roadmap. If you finish an
|
||||
item, delete its section; if you add a new gap, write one.
|
||||
|
||||
## Things to avoid
|
||||
|
||||
- **Don't add backwards-compat shims.** The repo is pre-1.0; breaking
|
||||
changes are fine if they improve the code. Delete dead code outright
|
||||
rather than `#[allow(dead_code)]`-ing it.
|
||||
- **Don't skip `cargo fmt` / `cargo clippy`.** CI runs both and they
|
||||
are cheap.
|
||||
- **Don't `UPDATE_GOLDENS=1` without reading the diff.** If you can't
|
||||
explain why a golden flipped, the change is probably wrong.
|
||||
- **Don't commit `tests/emulator/actual/` or `tests/emulator/node_modules/`.**
|
||||
Both are gitignored, but it's worth double-checking before a commit
|
||||
that touches the emulator directory.
|
||||
Loading…
Add table
Add a link
Reference in a new issue