Focus: A runtime-switchable 6502 core with TypeScript and Rust/WASM engines. Principle: Personal hobby project for learning — keep it fun and exploratory.
- NEVER commit to master — always use a feature branch (
feat/,fix/,refactor/,docs/,test/,chore/). - ALWAYS use Context7 MCP for WASM/Rust documentation (pinned in
.mcp.json). - Maintain feature parity between JS and WASM engines; test both for every change.
- Keep the JS engine as fallback; document performance differences.
- No
anytypes (strict mode), noconsole.log(use LoggingService), no hardcoded colors (design tokens — except the CRT display). - Bump
src/version.tsbefore every PR — single source of truth for the app version. Branch-prefix → bump:feat/= minor;fix/refactor/chore/docs/test/= patch; major needs explicit approval. (Arefactor/may take a minor for a notable user-facing change — an owner override; resolve the review thread rather than concede.)
git branch --show-current # 1. NEVER work on master
git checkout -b <type>/<description> # 2. branch if needed
yarn test:ci # 3. verify starting state
cat src/version.ts # 4. check current version
yarn wasm:build:release # 5. build WASM (release = speed-first)
# 6. Use the task tools for any multi-step work.yarn wasm:build:release # speed-first release build (~155KB) — what yarn dev ships
yarn wasm:build:dev # debug build, ~10x slower — Rust debugging only
yarn wasm:check # cd wasm-cpu && cargo check (quick rebuild)
yarn wasm:test # cd wasm-cpu && cargo test
yarn test # all tests, including engine-parity testsToolchain: rustc 1.70+ (have 1.89.0), wasm-pack 0.12+ (have 0.13.1).
Local wasm-pack builds need the rustup toolchain bin on PATH (Homebrew rustc
shadows it).
- wasm-opt fails: the metadata key must be profile-scoped —
[package.metadata.wasm-pack.profile.release]— and enable--enable-bulk-memory --enable-nontrapping-float-to-int. - Build from wrong directory: always
cd wasm-cpufirst (or use theyarnscripts above). - Missing target:
rustup target add wasm32-unknown-unknown. - Homebrew rustc shadows rustup —
wasm-packfails withwasm32-unknown-unknown target not found in sysroot. The rustup toolchain has the target but Homebrew'srustcwins on PATH. Prepend the rustup toolchain bin (the concrete bin, not the~/.cargo/binshim) so the WASM rebuild inyarn dev/yarn buildworks:env PATH="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin:$PATH" yarn dev. For a CSS-only change useyarn dev:vite— it reuses the already-builtsrc/wasm/*and skips the rebuild.
Full detail: docs/active/wasm-performance.md. Key facts:
- Raw throughput (cycles/sec & IPS) is measured headless (
BENCH=1benchmark). WASM ≈ 14× JS. ⚠️ In-app IPS is throttle-locked by theClockto ~1MHz (both engines ~331K) — it does NOT show the WASM gain. Never compare engines by in-app IPS.- Host CPU / headroom (
hostMillisPerSecond) is the in-app signal of the real difference: WASM costs ~3–4× less at the same IPS. - WASM binary: ~155KB release (speed-first
opt-level = 3+-O3wasm-opt). The old "<100KB / 90KB" target was an abandoned size-first strategy — don't restore it without approval.
What is Apple1JS? Browser-based, cycle-accurate Apple 1 emulator (TypeScript/React) with a dual-engine (JS + WASM) 6502 core, worker-based architecture, runtime engine switching, and comprehensive debugging tools.
Where things live:
src/
├── core/ # Emulation engine (CPU, Bus, Memory)
├── apple1/ # System integration, Worker
├── components/ # React UI components
├── services/ # Logging, Worker comm, State persistence
├── contexts/ # React state management
└── hooks/ # Reusable React patterns
Dual-engine internals, the migration history, and the WASM directory map live in
docs/active/wasm-migration-history.md.
- EXPLORE — read relevant files; use
docs/active/architecture.mdas the map. - PLAN — use the task tools for 3+ step work; think through edge cases/tests.
- CODE — follow neighboring patterns; write tests alongside; type-safe.
- COMMIT — run checks (below), then conventional commit.
For multiple agents working different features at once, use git worktrees:
claude --worktree <name>. See docs/active/parallel-agents.md for the full
workflow, config inheritance, and cleanup.
ALL TESTS MUST PASS BEFORE COMMITTING. Never break the suite; add tests for new features; update tests when behavior changes.
For core emulation changes, use TDD: write the failing test first
(yarn test:watch), write minimal code to pass, then refactor.
yarn test # run all tests
yarn test:watch # watch mode for TDD
yarn test:ci # full CI suite
yarn test:coverage # coverageTest guidelines: docs/active/cpu_test_guidelines.md.
WASM core tests need a real browser. Don't add native Rust tests on the CPU/WasmSystem
path — CPU6502::new() / WasmSystem::initialize() call wasm-bindgen imports that panic under
native cargo test (yarn wasm:test only runs the pure bus.rs/ram.rs/rom.rs component
tests). The Node-vitest engine-parity suites load the wasm-pack "web" build via fetch() (absent
in Node), so they skipIf/skip in CI. A Rust core change therefore gets cargo check + a
skipped parity test — verify actual WASM behavior in a browser (see the test guidelines).
yarn run lint && yarn run type-check && yarn run test:ci
npx markdownlint-cli2 --fix "<file.md>" # ONLY the file(s) you edited
# ⚠️ NEVER run the repo-wide `yarn lint:md:fix`: it corrupts the machine block in
# .claude/rules/lcd-conventions.md and reflows nested lists (prettier wants 4-space,
# markdownlint MD007 wants 2-space). md-lint is NOT in CI, so the breakage is silent.
# bump src/version.ts — see the branch-prefix mapping in Critical Development Rules #6
git add -A && git commit -m "type: description" # feat:, fix:, docs:, ...
git push- State management: emulated-hardware classes (CPU, Bus, RAM, ROM, PIA,
Clock, Apple1) implement
IVersionedStatefulComponentwith version + migration support. These contracts are scoped to the core/IO layer, not React components — see the applicability rule + audit indocs/active/architecture.md("Where these contracts apply"). Presentational components (Actions,RegisterRow, …) must NOT implement them. - Formatting: use the
Formattersutility (e.g.Formatters.hexWord(addr)) — never manualtoString(16). - Worker communication: type-safe via
sendWorkerMessage(worker, WORKER_MESSAGES.SET_BREAKPOINT, address). Messages defined insrc/apple1/types/worker-messages.ts. - Component inspection: emulated-hardware classes implement
IInspectableComponent; return structured data fromgetInspectable()for debugger visibility. React components consume the inspectable tree (they don't implement it).
For UI changes, iterate against visuals: ask for current-state screenshots / mockups and concrete success criteria up front, then screenshot progress and iterate 2–3 times.
Verifying in a real browser: the emulator runs a continuous worker/rAF loop, so the page
never reaches document_idle — idle-gated screenshot / page-text tooling times out (don't retry
it). Drive and inspect the DOM via in-page JS instead. For auto-dismissing UI (toasts clear after
UI_TIMINGS.TOAST_DURATION ≈ 4s), sample within a single in-page async loop rather than across
separate tool round-trips — the round-trip latency can miss the transient state and look like a
bug. Dev server: yarn dev (builds the WASM release, then Vite on :3000); yarn dev:vite reuses
prebuilt WASM for CSS/markup-only changes.
gh is available for PRs, issues, CI status, and CodeQL findings, e.g.:
gh pr create --title "feat: description" --body "..."
gh pr checks <n>
gh api repos/owner/repo/code-scanning/alerts # security findings- ✅ All tests pass (
yarn test:ci) - ✅ No lint errors (
yarn lint) - ✅ No type errors (
yarn type-check) - ✅ Version bumped in
src/version.ts - ✅ Feature works as described
- ✅ No
console.logleft - ✅ Docs updated if needed
Non-trivial new work (new feature, ≥3 files, or any architecture change) → invoke
lcd:triage, which picks a lane (Quick / Standard / Deep) and states it in one line.
Trivial work (typo, one-liner, dep bump, known-cause fix) → go direct, no triage.
Artifacts live under docs/lcd/ (see .claude/rules/lcd-conventions.md).
Project map: docs/lcd/MAP.md · Decisions: docs/lcd/DECISIONS.md
Resume any work-item after a context reset: /lcd:resume <slug>.
- Documentation Hub:
docs/README.md - Architecture:
docs/active/architecture.md - Roadmap & Priorities:
docs/active/consolidated_roadmap.md - WASM Performance:
docs/active/wasm-performance.md - Migration History:
docs/active/wasm-migration-history.md - Parallel Agents:
docs/active/parallel-agents.md - Test Guidelines:
docs/active/cpu_test_guidelines.md - 6502 Opcodes · Apple-1 Manual
💡 This is a learning project. If something seems interesting to explore, let's do it — frame ideas as opportunities, not requirements.