Skip to content

Latest commit

 

History

History
284 lines (220 loc) · 166 KB

File metadata and controls

284 lines (220 loc) · 166 KB

AGENTS.md — RustyNES

@/home/parobek/.claude/master-core/AGENTS.base.md @/home/parobek/.claude/master-core/lang/rust.md @/home/parobek/.claude/master-core/modules/10-commits-and-versioning.md @/home/parobek/.claude/master-core/modules/20-testing-and-accuracy.md @/home/parobek/.claude/master-core/modules/30-quality-gates.md @/home/parobek/.claude/master-core/modules/40-docs-and-adrs.md @/home/parobek/.claude/master-core/modules/50-architecture-patterns.md @/home/parobek/.claude/master-core/modules/60-security.md @/home/parobek/.claude/master-core/modules/70-release-ceremony.md @/home/parobek/.claude/master-core/modules/80-phase-sprint-workflow.md @/home/parobek/.claude/master-core/modules/90-multi-language-integration.md @/home/parobek/.claude/master-core/modules/91-agent-system-architecture.md @/home/parobek/.claude/master-core/modules/95-named-pattern-library.md

<<< MC-PROJECT-START >>>

Project: RustyNES

New here since v0.8.x? The emulation core was replaced with the cycle-accurate engine and the repo was re-cut as v1.0.0. Read docs/v1.0.0-synthesis-handoff-2026-06-13.md first — it explains what changed, the rustynes-* architecture, where everything moved, and the hard constraints. Then update this file + your memory as you work.

MOST IMPORTANT RULE — Provenance & license firewall (read first, applies to every task)

This rule outranks everything else in this file. RustyNES exists because of a real, corrected provenance failure (GPL emulator code was reproduced despite a black-box instruction, then the honest "ported from" comments were scrubbed; the project was relicensed to GPL-3.0-or-later and every derived site re-attributed). The full account is docs/provenance-failure-postmortem.md; the preventive ruleset is docs/ai-emulator-provenance-guardrails.md (PDFs of both in ref-docs/). Read the guardrails doc and treat it as binding. The non-negotiable core:

  • REFERENCE FIREWALL. Reference emulators (Mesen2, puNES, FCEUX, Nestopia, higan, ares, GeraNES, TriCNES, tetanes, …) are black-box oracles. You may run them and read their output (framebuffers, traces, audio, logs). You must not open, read, quote, or reproduce their source (.c/.cpp/.h/.cs/.rs), constants, tables, variable names, code ordering, or comments — not "for reference," not once. The local ref-proj/ reference-emulator clone has been removed from disk and stays gitignored (/ref-proj/), so the source is out of reach by design. Do not re-clone it into the working tree. If you find such source in reach, report that it should be removed; do not read it.

  • THE FIREWALL COVERS HDL TOO (ADR 0037, 2026-08-20). The v2.4.1 → v2.5.0 "Fabric" line writes a new NES core in SystemVerilog. NES_MiSTer, fpganes, and any other NES rtl/ are strict black boxes on exactly the same terms as emulator source: never opened, read, quoted or transcribed — not the RTL, not its constants, not its module or signal names. Permitted: instantiating a third-party core as an opaque testbench module and comparing its outputs. Not permitted: reading it. Keep those repositories physically outside the workspace, exactly as was done for ref-proj/. Anything genuinely unimplementable from documentation escalates to a new ADR before any source is opened — this is the rule most at risk of quiet erosion, because the pull toward reading a working core is strongest exactly when the DUT and RustyNES disagree at dot 260 of scanline 241 and nesdev is ambiguous.

  • IMPLEMENT FROM DOCS. Write hardware behavior from public documentation (nesdev_wiki/, ref-docs/, datasheets, die studies) and pin it to public test ROMs / golden vectors. Hardware behavior is a fact; the specific code expression is copyrighted.

  • IF YOU DERIVE, SAY SO — AND STOP. If you do port/adapt/closely-model an external source, (1) it is a derivative work under that source's license; (2) attribute it at the site + in docs/originality-and-provenance.md §1 + in NOTICE + via an SPDX header; (3) the project license must stay compatible (GPL-3.0-or-later) — flag it to the maintainer before proceeding.

  • NEVER LAUNDER. Never reword or delete an honest "ported/derived from X" comment to make code look independent. Scrubbing provenance is the cardinal failure — worse than the original port. The response to "this says GPL code was incorporated" is relicense-and-attribute, never scrub-the-comment.

  • NO OVER-ATTRIBUTION. Do not tag a genuine oracle comparison ("matches Mesen2's behavior," "cross-checked against ares") as "derived from." Attribute real ports; leave genuinely-independent code independent.

  • DO NOT SELF-CERTIFY. Never assert "no third-party code is incorporated" / "license-clean" as a finished claim. Surface provenance status for human + expert review; state uncertainty. AI self-attestation of license compliance is not trustworthy — an outside NESdev reviewer, not the tooling, is what caught this.

  • THE FIREWALL IS PER-REGION, NOT PER-REPO — the oracle's own source has exceptions (2026-08-26). "RustyNES's code is ours, so it is readable" is true of the repository and not uniformly true of every block in it. Eleven files carry a // Provenance: header disclosing that a REGION of them is derived from a GPL reference emulator, and those regions are black boxes for HDL purposes even though the file is ours — writing SystemVerilog from them launders the original expression into the DUT through us, which is precisely what ADR 0037 exists to prevent. This was found on the first task of v2.6.4: its headline job is fixing the five SH-group stores in the DUT, and crates/rustynes-cpu/src/cpu.rs line 3 discloses SHA/SHX/SHY/SHS/TAS as derived from Mesen2's SyaSxaAxa (Core/NES/NesCpu.h) — so the single most relevant block of oracle source for that version is one that must not be read to write the RTL. Nothing in the tooling said so. Before reading oracle source to inform sibling/HDL work, grep -n "Provenance:" <file>; regenerate the list with grep -rn "^// Provenance:" crates/*/src/*.rs rather than trusting a snapshot. The ones that bite HDL work are rustynes-cpu/src/cpu.rs (SH group — rungs 1/5), rustynes-apu/src/blip.rs (BLEP), and six mapper files (rung 7). The escalation ladder, maintainer-directed, in order — exhaust each rung before the next: (1) vendored public documentation; (2) the open Internet — the vendored wiki is PARTIAL, documenting SHX/SHY in full and carrying nothing on SHA/TAS, which one web search supplied; (3) black-box comparison — a per-cycle golden diff needs no source at all and usually resolves faster, because at that point the question is "which cycle differs", not "what is the rule"; (4) the derived oracle source, last resort. Rung 4 is permitted — the licences are compatible, both repos being GPL-3.0-or-later — but the existing attributions live in the ORACLE, and the sibling is a separate repository, so reading it obliges declaring the derivation there too: a site comment, the sibling's provenance doc, NOTICE, and an ADR 0037 amendment naming who authorised it. Never silently. On v2.6.4 rungs 1-3 were sufficient and the escalation went unspent — worth knowing, because the pull toward rung 4 is strongest exactly when the DUT and the oracle disagree, which is the moment this rule matters.

Enforcement lives alongside the prose: /ref-proj/ is gitignored/.dockerignored/.markdownlintignored and excluded from CodeRabbit; deny.toml gates dependency licenses; every derived file carries an SPDX + provenance header. A rule the tooling enforces beats a rule you are merely asked to follow.

What this is

RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in pure Rust. The accuracy bar is Mesen2 / higan / ares: tight lockstep scheduling at PPU-dot resolution on a master-clock-precise timebase, sub-instruction PPU events visible to subsequent CPU code, and a lookup-table non-linear audio mixer with band-limited synthesis. The frontend is pure Rust (winit + wgpu + cpal + egui).

Current release: v2.6.4 "Rubric" (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on v2.6.3 "Mainspring" (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on v2.6.2 "Witness" (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on v2.6.1 "Interleave" (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on v2.6.0 "Assay" (2026-08-24) — the triangle, the noise channel and the sweep unit in the MiSTer co-simulation DUT — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on v2.5.9 "Overture" (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on v2.5.8 "Blanking" (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on v2.5.7 "Collimation" (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on v2.5.6 "Vestige" (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on v2.5.5 "Raster" (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on v2.5.4 "Escapement" (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on v2.5.3 "Hysteresis" (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on v2.5.2 "Dormant" (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on v2.5.1 "Retrace" (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on v2.5.0 "Rungwork" (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on v2.4.9 "Plumbline II" (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on v2.4.8 "Palimpsest" (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on v2.4.7 "Keystone" (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on v2.4.6 "Abacus" (2026-08-22) — the core learns arithmetic. Built on v2.4.5 "Compass" (2026-08-22) — the core reaches memory, and chooses. Built on v2.4.4 "Ignition" (2026-08-22) — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (RustyNES_MiSTer@7f092bd), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the third writer of the oracle's CpuBootTrace format, so cpu_boot_trace_diff reads it with no modification and the rung needed no oracle-side change at all. The oracle settled a question our own prose could not: reset is EIGHT cycles, and docs/cpu-6502.md said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because TSX leaves exactly the flags a wrongly-flagging TXS would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. Built on v2.4.3 "Touchstone" (2026-08-22) — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the two Fabric-plan risks that had to be answered before any RTL exists, and both were answered by evidence that contradicted what the plan assumed. Risk 4, the Quartus subset, is FITTED: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with 0 synthesis warnings, and the 2 KiB array inferred as 2 M10K blocks with 29 total registers — not 16,413 — from the source style alone, no ramstyle attribute. The initial block became a real MIF (so a boot ROM lands inside the block) and the enum was one-hot encoded. Nine constructs are promoted to fitted; plain case, priority case and $bits are deliberately left documented because the kitchen sink does not exercise them. Risk 1, the sys/ licence, inverts the plan's own hedge: 57 files, zero GPL-2.0-only, and hps_io.sv — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream up to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on v2.4.2 "Cairn" (2026-08-22) — the rung-0 compare surface. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace volume, not simulation time, and it is now measured: 3 frames of AccuracyCoin is 89,343 CPU cycles, 5,372,427 bytes of irq.csv against 352 bytes of ckpt.bin — a factor of 15,263 — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. What is hashed is a decision about hardware, not about convenience: CycleRecord carries 29 fields and most are RustyNES's model, so Observable is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and pc is marked DUT-observable rather than pin-observable. The emulation core is untouched. Built on v2.4.1 "Fabric" (2026-08-20) — the oracle release, opening the v2.4.1 → v2.5.0 "Fabric" line: a new NES core written in SystemVerilog from public hardware documentation, in a sibling repository, with this emulator as its verification oracle. RustyNES is not being ported to FPGA and cannot be — a MiSTer core is SystemVerilog compiled by Quartus 17.0.2 into a Cyclone V bitstream, and high-level synthesis of a cycle-accurate emulator's control flow does not produce usable hardware; what is buildable is a NEW implementation verified against this one. crates/rustynes-cosim is the boundary — a narrow C ABI a Verilator testbench links, plus a nes_golden_export CLI emitting five golden formats. The provenance firewall extends to HDL (ADR 0037): NES_MiSTer and fpganes rtl/ are strict black boxes — instantiating one as an opaque testbench module to compare OUTPUTS is permitted, reading its source is not; anything unimplementable from documentation escalates to an ADR BEFORE any source is opened. Three design decisions are locked and each has a reason: replay, not lockstep (Nes exposes run_frame() and step_instruction() and nothing finer, and the determinism contract already makes a pre-recorded trace exactly the trace a lockstep run produces), no DPI-C (it puts `ifdef SIMULATION guards into RTL that must also pass Quartus — the construct that lets a simulated netlist drift from the synthesised one), and hash first, capture on divergence (a 4200-frame AccuracyCoin run is ~125 M cycles, ~7.5 GB as per-cycle CSV against ~480 KB of 4096-cycle checkpoints). The golden framebuffer is exported pre-palette so a palette difference cannot masquerade as a rendering one. v2.5.0 is scoped to "the 6502 rung closes", not a finished core (7–13 months FTE for a full one), and two risks are accepted in writing: NES_MiSTer scores 121/125 on AccuracyCoin where real Famicom AV hardware also scores ~121/125, so there is no published accuracy headroom and the core may be declined as a duplicate; and the oracle can be wrong, since 141/141 is not "matches silicon" — every rung is labelled by whether it has an INDEPENDENT oracle. The exclusion of rustynes-cosim from the workspace is the load-bearing detail, and it exposed a defect in the accuracy gate itself. The crate enables cpu-boot-trace and irq-timing-trace on rustynes-core, and cargo unifies features across a workspace build, so as a MEMBER it made cargo build --workspace compile the core ONCE with the union — measured through --message-format=json, not inferred. irq-timing-trace is not an inert branch: it selects a different for sub_dot in 0..3 loop in Bus::tick_one_cpu_cycle, so CI's cargo test --workspace --release --features test-roms — the accuracy battery — was validating a scheduler no user runs, the same shape as the v2.3.4 defect where the coverage harness tested a load path no user runs. The measured cost was +1.24% / +1.39% / +1.89% across the three full_frame benches, below this project's own 3% adoption bar, and it never touched the shipped binary or the perf gate — published precisely because it shows performance was never the argument. Exclusion has a price (an excluded package cannot use field.workspace = true, and --workspace no longer reaches it), and both halves are closed mechanically: cosim_manifest_audit.rs asserts every duplicated field and lint still equals the workspace's AND that the crate is still excluded (four mutations, all caught), and CI gains explicit fmt, clippy and test steps — the clippy step earning its place on its first run with a must_use_candidate --workspace had never surfaced. Two more findings the crate was not looking for: the first run_frame() after power-on advances ZERO cycles (the PPU is constructed at dot 340 of the pre-render line, so the seven-cycle reset ticks past the frame wrap and leaves frame_complete latched — gate on Nes::frame(), never the call count, or a --frames 60 loop emits a 59-frame golden under a manifest claiming 60), and no CI invocation had ever enabled cpu-boot-trace or irq-timing-trace for clippy, so those two core modules had never passed the lint gate (six pre-existing findings; --workspace --all-targets covers each crate's DEFAULT feature set only). It also carries v2.4.0 "Concordance", which merged to main and was never tagged: the seven-property atomic-write sequence v2.3.9 built for Config::save_to is extracted into crate::atomic_write and adopted everywhere — the plan named three call sites and there were FOUR, the fourth being save_state.rs, where a truncated write is a user's game progress, while per_game.rs was not in the plan at all because it LOOKS correct (it renames a sibling temp file) and held two of seven: no fsync, and a FIXED scratch name shared across every process. Review then found four more places the module reported success it had not earned, each an error discarded under a comment explaining the rest of the operation: set_permissions swallowed (the mode applied is the one the target ALREADY had, so a failure widens a 0600 file to the umask default), the parent-directory fsync swallowed together with its File::open (so the whole barrier could be a no-op while the module's table claimed "yes", and EIO passed as success), a ONE-attempt occupied-scratch retry (justified by "the counter cannot repeat a name within a process", which is true and beside the point — the collision comes from a previous process whose pid was reused), and an exhaustion cleanup that deleted a file this process had not created. Plus a const fn that only failed on Windowsis_transient_rename_error was const and called io::Error::kind, which is not, behind #[cfg(windows)], so it compiled clean on Linux and would have turned main red AFTER merge; the fix moved the predicate into an always-compiled function reached through cfg!(windows) && …, so restoring the const now fails on Linux. Also v2.4.0: Nes::timeline_generation(), a session-local counter deliberately NOT in the save state (serializing it would make a second load of the same slot restore the same generation, so a consumer would miss it — and because it lives outside the snapshot, snapshot_schema_audit cannot see it); the cheat save reporting its failure in the panel instead of a stderr nobody reads on a windowed build; and release_anchor_audit.rs, pinning 15 release anchors across 10 documents. It is not in the v2.3.9 tag — v2.3.9 corrected the eight drifted documents BY HAND, which is what its notes describe and all they claim; the standing gate merged afterwards in #427. (v2.4.1's notes as first published asserted that v2.3.9's body described the audit. It does not; that claim is retracted.) rustynes-core changes in both halves, so AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on v2.3.9 "Crucible" (2026-08-20) — the gates release. A crucible tests something to destruction rather than inspecting it, and that is what this release does to the project's own checks: what they cover, what they only appear to cover, and where a regression could still reach main unchallenged. The v2.3.x line added five tools in four releases and the recurring finding across all of them was never that the emulation was wrong — it was that a check reported a pass it had not earned. The docs-only CI skip had never worked: dorny/paths-filter's predicate-quantifier defaults to some, which includes a file if it matches ANY pattern, so the code filter's leading '**' matched everything and all seven ! exclusions under it were DEAD from the day they were written — proven from a run rather than the docs (a one-file markdown PR logged Filter code = true / Matching files: AGENTS.md). Every documentation PR in the project's history had been running the full matrix, and that stopped being merely wasteful the day two docs-only PRs were blocked by an ARM cross-compile failure on jobs that should never have been scheduled. Fixed with two filter steps because the quantifier is step-level and the two filters need OPPOSITE settings: code needs every, while accuracy is a list of alternatives and becomes unsatisfiable under it — the naive one-line fix would have silently disabled the accuracy battery while repairing a different gate. Both directions are now observed on real PRs. The accuracy battery now runs at review timetest-roms was full-run-only, so a regression landed on main rather than on the PR that caused it; it is now also path-filtered over the chip crates, the core, rustynes-gamedb (it rewrites the iNES header on load, so it changes what the emulator is before a cycle runs), the harness and tests/, measured first at 11 of the last 40 merged PRs so ~72% still pay nothing. Bounds were calibrated against a measurement rather than a claim: the ARM provisioning step failed on three consecutive PRs with NO apt error in the log at all, and the real number was Fetched 4201 kB in 4min 45s (14.7 kB/s) — three orders of magnitude below normal, which made the previous ~40 MB package set hopeless rather than unlucky (~45 minutes; no timeout could have saved it); it was also installing a whole cross toolchain to obtain libc6-dev-<arch>-cross, which the comment above it had already named, because bindgen runs the host clang against --sysroot and never invokes the cross compiler. A freeze from one cartridge kept writing into the next — not a stale label but an active per-frame write into the wrong game, because both memory panels' freezes feed the raw-cheat overlay applied after every frame and neither was registered with the ROM-transition hook; the sweep that closed it now covers every panel under ONE rule: derived output is discarded, user-authored input is kept, and only input that actively writes is neutralised (so RAM Search baselines and reconstructed call stacks clear, while watch lists and breakpoints survive and breakpoints stay ARMED — a breakpoint halts, visible and recoverable, where a freeze writes, silent and continuous). Two negatives are recorded because they cost time to establish: the header editor LOOKS ROM-bound and is not (it is a standalone file tool), and the event panel / trace status / HD-pixel coordinates are per-frame state or preferences. The config file is written atomically and durablyfs::write truncates then writes, and saves became automatic (closing a ROM, moving a mixer slider, finishing a Latency Oracle measurement), so an interruption left the user holding a truncated config.toml; seven properties, and five came from review rather than the first draft (sibling scratch file, fsync before rename, parent-directory sync, create_new(true) for CWE-377, mode applied at creation, symlink resolution including a broken link, and a pid + per-call counter — the last is what makes exclusive creation adoptable at all). Two shipped features told the truth for the first time: movies record TWO ports (FrameInput models P1 and P2) while the Replay panel printed "Four Score (P1..P4)" at the moment a user decides to press Record — widening the format is a .rnm epoch change, so it is disclosed at three levels with the caveat printed directly under the claim it qualifies; and a failed Latency Oracle save now says so instead of being swallowed (remembering is still NOT applying — nothing touches run_ahead, and an inconclusive result is not remembered at all). Also: 257 lines of dead code removed — an APU pair (34), a closed LockstepBus DMA-service island (183), and drain_dma (40), a function called on every CPU read, every CPU write and every bus cycle whose entire body was let _ = read_addr; and whose comments claimed the legacy service below it "stays active for the default build" — alongside 25 of 29 #[allow(dead_code)] attributes suppressing nothing, established by stripping them and re-running clippy across all EIGHT gated combinations (an item can be live by default and dead on wasm, which is precisely the case that would have earned the attribute); the SAFETY-comment rule is now a gate (clippy::undocumented_unsafe_blocks — all 91 unsafe sites already carried a justification, two had it where a human reads correctly and a checker cannot, and the lint is demonstrated to fail); and two cargo deny advisory ignores retired on their own stated condition (their entry said to remove them once the resolve moved past quick-xml 0.40, and it had). rustynes-apu and rustynes-core both change, so AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted — and re-run AGAIN after the second round of deletions rather than only after the first. Built on v2.3.8 "Parallax" (2026-08-20) — which pixels differ, not just which frame. Parallax is the apparent shift of an object seen from two positions, and the displacement is the measurement. Probe could already say whether two configurations of the same ROM diverge and AT WHICH FRAME, and could say nothing about where or why: a trial reduces each frame to one u64, the right shape for detecting a difference and the wrong shape for explaining one — a hash says frame 412 differs and has nothing to hand to Pixel Provenance, which is where an answer actually lives. divergence::localise re-runs both configurations to the detected frame, keeps the full output instead of its hash, and reports the shape of the difference — population count, first pixel in raster order, and the inclusive bounding box — which separates kinds of bug from each other (one pixel is a sprite or a palette entry, 256 in a row is a scanline, tens of thousands is a scroll or a mode change); is_single_scanline is offered rather than left to call sites because the inclusive comparison is easy to get wrong. It localises on the index framebuffer (256x240 u16s of (emphasis << 6) | colour, the PPU's own per-pixel output before the palette lookup) — half the bytes and at least as sensitive, since the RGBA buffer is a pure function of it given the same palette. Three answers, and the third is the point: Identical, Differs, and Inconclusive for an exhausted budget or two trials that cannot be compared — the Latency Oracle precedent applies directly, "I stopped looking" must not arrive wearing the same shape as "they agree" — and the budget is checked UP FRONT for all four trials, so spending two on detection and then finding the localisation pair unaffordable cannot consume the budget that would have answered the question. Beyond locating a difference the Lens explains it: trial-scoped provenance capture hands a located pixel to the machinery that already answers "what wrote this, and from which instruction", and an audio lens resolves a divergence to the CPU cycle. One defect was found and fixed inside the same work — the Lens left the emulator thirty frames ahead of where it started, because a trial restores the anchor on the way IN and not on the way OUT (deliberate — it is what lets the Lens read the trial's final frame off nes directly) and the outermost caller has to put the timeline back, and did not. Cut from its own boundary commit (#407's merge) rather than from main, so its artifacts contain exactly the Divergence Lens. Built on v2.3.7 "Overtone" (2026-08-19) — the audio-provenance release. The APU counterpart of Pixel Provenance: a per-register write attribution answering what wrote this, and from which instruction, and a per-CPU-cycle mix trace answering what were the channels actually doing — per CPU cycle rather than per output sample, because that is the cadence at which the mix is genuinely computed, and carrying raw pre-mix channel values so a record describes the chip rather than the user's mixer sliders. Surfaced at Tools → Audio → Audio Provenance; output-only, runtime-default-off, not serialized. Its subject is the trap it inherited. Pixel Provenance shipped non-functional for four releases because run-ahead's per-frame rollback cleared its store after the visible frame was harvested and before the frontend released the emulator lock, so the carry landed in the same change as the feature here rather than after a bug report. That enumeration was then found to be incomplete: rustynes-probe has three more same-timeline restores — Probe::run_uncounted (once per trial, and a latency measurement runs up to 21), latency::measure_in_place (the final restore, outside every per-trial guard), and the RAM Atlas panel's TimelineGuard — none of which used the stash, so running the Latency Oracle or the RAM Atlas emptied both provenance panels. Both stores are cumulative, so the records were not rebuilt by the next frame; they were gone for the session. The test named for the contract, measure_in_place_restores_the_live_timeline, compares nes.snapshot() and provenance is deliberately not in the snapshot — it asserted something strictly weaker than its own name and passed throughout. Closed by moving the stash into a shared TrialGuard, pinned by four independent mutations. $4014 and $4016 were documented as attributed and were not — the bus handles both without routing through Apu::write_register. Two defects were caught by measurement rather than reading: apu_throughput, built for this release, reshaped the plumbing three times on regressions invisible in the diff (the bench itself had to be corrected first — it omitted an end-of-cycle pair worth ~23% of true per-cycle cost), and a randomized sweep of the save-state parse boundary found four panics in VRC7's OPLL where hand-tracing found one, because the maximally-hostile all-0xFF payload set update_requests to all-ones and concealed an eg_shift panic. Also fixed: the browser demo applied no per-game header corrections, Rad Racer's roadside artifact (a hybrid address spliced from a stale v), VRC7 save states dropping the live FM synthesizer so rewind garbled the music, and no CI job carried a timeout — one hung job silently skipped a release for five hours. rustynes-apu and rustynes-core both change, so AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on v2.3.6 "Sounding" (2026-08-17) — about measuring, and what a measurement is allowed to claim. Two shipped features are found never to have worked. Pixel Provenance (the v2.3.2 marquee) returned an empty report for every user on the default run_ahead = 1: run-ahead's per-frame rollback is the LAST thing before the frontend releases the emulator lock, so the panel's first look was always after the wipe — and "click any pixel" was never implemented (two DragValue spinboxes; the only Sense in the file was hover() on a colour swatch). Two source comments and four doc claims asserted the opposite of their own code, which is why four releases passed unchecked. And Duck Hunt could never score: its protocol is "see NOTHING for one frame, then a bright spot in the next", and the light bit was sampled at end-of-frame, so a read during frame N returned frame N-1 — the probe exactly inverted (000000 -> 000500). Two new tools, both built to decline rather than guess: the Latency Oracle (replays one moment with a button held and without it; None and Some(0) are different answers never collapsed; START excluded because pausing is a reaction to a menu; recommends a run-ahead depth and never applies one) and the RAM Atlas (classifies all 2 KiB of work RAM, then VERIFIES a candidate by perturbing it — observation returns all 2048 labels as Untested so it is structurally incapable of claiming an effect; liveness is relative to its lens and every verdict names it; Inert is documented as NOT meaning unused). APU Workstream D is CLOSED — the 18.7%-of-frame figure stands, but it is not recoverable by gating per-cycle bookkeeping: one adoption, three measured rejections, one declined on inspection, two left unmeasured deliberately. Tools and Debug are regrouped by task (Tools had reached twenty flat entries). The core gains one const fn getter and nothing else, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED, not asserted. NOT fixed here: libretro/docs#1180 (the licence on the libretro docs site) is still open upstream. Built on v2.3.5 "Manifest" (2026-08-16) — about what the core declares about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms. It does: RetroArch reads dist/info/rustynes_libretro.info from libretro/libretro-super, a SEPARATE copy from this repo's that nothing syncs and nothing compared, so the v2.2.9 GPL relicense never reached it (both upstream PRs merged 2026-07-21, exactly two weeks BEFORE the 2026-08-04 relicense). The repo-side half is corrected here — GPLv3+, since libretro uses short tokens and marks "or later" with a trailing + (tallied across all 316 upstream cores) — plus a standing libretro_info_audit.rs that pins the local file against the workspace manifest AND the core's own retro_get_system_info, making the upstream sync a copy rather than a re-derivation. A licence change is now a mandatory upstream-sync trigger. Auditing the wrapper then found five further defects, every one with correct emulation behind it: a hardcoded 60.0988 fps for every cartridge plus retro_get_region unimplemented (PAL/Dendy ran 20.2% fast), retro_reset unimplemented so RetroArch's Reset did nothing, ever (the library default is a literal no-op), retro_unload_game unimplemented (Game Genie indices leaked across cartridges), aspect_ratio = 0.0 (square pixels, against the desktop frontend's 8:7), and no controller info so the Zapper was unreachable despite Nes::set_zapper being fully implemented. Review caught a use-after-free: RetroArch shallow-memcpys the outer retro_controller_info array but RETAINS each types pointer, so the description tables must be 'static (SET_INPUT_DESCRIPTORS is different and safe — never generalize between environment calls). The crate went from zero tests to eight. Separately the APU (18.7% of frame time, invisible to a symbol profile because fat LTO inlines it into cpu_clock) gained its first throughput bench and a default-configuration mix specialization, −3.3% to −4.2% on nes_run_frame_nestest, byte-identical by construction. Declared values are now DERIVED from rustynes_core constants (FRAME_DURATION_*, DEFAULT_SAMPLE_RATE) rather than transcribed. Audio stays 44,100 Hz — a matched-normalized-frequency SFDR comparison shows 44.1k and 48k are equivalent (81.6 vs 82.2 dB), so nothing is gained, and 44,100 is the only rate this project's audio is verified at. Shipped OUTPUT byte-identical, but the APU implementation did change (the mix specialization is a strict specialization, not a no-op), so AccuracyCoin 141/141 and nestest 0-diff were VERIFIED, not asserted. NOT fixed by that release, and since RESOLVED upstream: RetroArch showed the wrong licence until libretro-super#2069 merged (2026-08-16 — it now reads GPLv3+), and RustyNES did not appear on iOS/iPadOS/tvOS until RetroArch#19416 merged (2026-08-16, 76f60626984arustynes is now line 268 of pkg/apple/update-cores.sh, between reminiscence and sameboy). Being in the build list is not the same as being installable: it arrives with the next App Store RetroArch build, on libretro's cadence. Only libretro/docs#1180 remains open.

The prior release, v2.3.4 "Ledger" (2026-08-15), was the coverage release. Three boards land: mapper 176 submapper 2 (WAIXING-FS005 — the $A001 RAM Configuration Register with 32 KiB banked WRAM, the $5000-$5FFF register-window disable the Waixing copy-protection is built on, a mapper-195-like mixed CHR-ROM/CHR-RAM mode, two-bit $A000 mirroring, the $46/$47 bank-select swap that does NOT apply to $06/$07, PRG A21-A25, and the board's documented $E003 decode mask), 154 (NAMCOT-3453 — mapper 88 plus a one-screen nametable bit decoded across the WHOLE $8000-$FFFF range, not just the bank-select window) and 243 (Sachen SA-020A — mapper 150's ASIC on its own PCB, same three registers at INVERTED significance, which is why they need separate numbers). Breadth 172 → 174 families (51 Core + 95 Curated + 28 BestEffort). All three implemented from the NESdev wiki with no reference-emulator source consulted, unlike the FK23C transforms beside them which stay a disclosed Mesen2 derivation.

The release's subject is not the boards. The coverage harness loaded ROMs with a bare Nes::from_rom while the frontend rewrites the iNES header first from the per-game database — the regression net was testing a load path no user runs, so every fix delivered through that database was invisible to it (Seicross, fixed in PR #127, still reported blank). Moving the harness onto the real path — the database extracted into a shared rustynes-gamedb crate — immediately exposed a defect reaching users since v1.2.0: the vendored table uses 0 in its Mapper column as its unfilled-row default, apply_header_overrides read that as "force NROM", and 12 staged ROMs — every Sachen board in the corpus (133, 143, 145, 146, 147, 148, 149, 150) — had correct headers overwritten and then failed NROM's size check, refusing to load at all. Second time this table has force-applied a field it should not have (the first froze Wizards & Warriors, ADR 0031); fixed the same way — refuse an override indistinguishable from "no data". Also: a Bandai FCG EEPROM counter that panicked in debug builds ((addr + 1) & 0xFF on a u8 traps before the mask can wrap it), CLI launches that skipped every header override the File menu applied, mapper 15 PRG-RAM + CHR-RAM, save-state back-compat for mappers 15/88/176, and issue #360. Unlike the three releases before it this one touches the core (five mappers + the tier classifier), so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED, not true by construction. Workstream C — the APU at 18.7% of frame time — was NOT delivered and is carried to v2.3.5: its blocking step, an apu_throughput bench, was never built, so there is no measurement to adopt or reject. Built on v2.3.3 "Cadence" (2026-08-14) — the display-pacing release: the run-ahead throttle oscillation traced to a stale median (the gate counted 120 frames of a 600-sample ring, so a p50 at index 300 could not leave the previous depth; 6-7 transitions per 24 s → 1, spurious releases 2 → 0), a predictive engage arm (run_ahead = 3 converges in 2.8 s vs 12.1 s, 5/5 paired rounds, exact sign p = 0.0312), compositor refresh via wp_presentation, divisor display-sync, and a fail-closed validity gate — dropped frames 135-254 → 1-9. Frontend-only, AccuracyCoin 141/141. It does not claim the maintainer-reported shudder is resolved. On top of v2.3.2 "Lucid" (2026-08-11) — the pixel-provenance release. Click any pixel and get its full causal chain: the dot/scanline that emitted it, the layer that won priority, the nametable / attribute / pattern addresses of the tile actually on screen (v cannot answer this — by display time it has advanced two tiles past the pixel, so a ProvBgAddrs triple rides the same latchnextcur cascade as the pattern shifters, committed at PATTERN-fetch time because the dummy NT fetches at dots 337-340 clobber a pending tile), the palette entry, and the CPU instruction + cycle that last wrote each of those bytes (crates/rustynes-ppu/src/provenance.rs; the attribution split is bus-has-the-PC / PPU-has-the-destination, pushed down once per instruction from Nes::run_frame's existing debug block, so rustynes-cpu is untouched). Surfaced at Tools → Pixel Provenance. Also lands deterministic replay attestationrustynes verify <movie.rnm> --rom <rom> replays a .rnm and proves it reproduces its recorded run; the hash folds in the input applied AND the video it produced (an output-only hash confirmed a tampered input log as genuine on a ROM that ignores the controller — found by running it), with 64-frame checkpoints for divergence localization and NO container-version bump (the additive-trailing-field precedent rerecord_count set). All of it is debug-hooks-gated, output-only and default-off, so AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff — verified after the fact, not by construction. The egui 0.36 bump is deliberately NOT in this release: egui-winit 0.36.1 cannot compile for wasm32-unknown-unknown (upstream cfg mismatch on DroppedFile::bytes) and RustyNES ships a wasm demo; the complete migration (which also requires wgpu 29 → 30) is preserved native-green on chore/egui-0.36-wgpu-30-blocked. Spec: docs/pixel-provenance.md. Built on v2.3.1 "Plumb Line" (2026-08-06) — the measurement release: a harness-free frame probe, per-source-file subsystem attribution (recovering the APU at 18.7% of frame time, invisible in the symbol profile because fat LTO inlines it into cpu_clock), an adoption A/B with an A/B/A order-bias control, and a relative gate that declines to conclude on a contended host; ten core hot-path candidates measured and all ten rejected through six distinct mechanisms. On top of v2.3.0 "Datum II" (2026-08-05, the capstone that CLOSES the v2.2.6 → v2.3.0 NESdev-remediation line — true multi-viewport OS-window detach for every tool panel, the emulator-lock frame-pacing fix, a −5.1% byte-identical PPU optimization, both remaining forum-reported accuracy items verified already-correct, and the AccuracyCoin gate pinned to an exact 141/141. Built on v2.2.9 "Studio II" (2026-08-04, a frontend quality-of-life release that opened the second half of that line — TAStudio piano-roll edits now drive the emulator [handle_tas_requests re-seeks the Nes after a SetInput batch, matching the scripting path], .bk2 movies play back honoring their LogKey column order [bk2_interop parses the real column header instead of a fixed order, with parse errors surfaced on the status bar], and tool windows gain a detach / pop-out affordance (the shared detachable_window helper across 18 panels) [native-only, docked on wasm; honest scope caveat: the frontend is currently a single-viewport egui_winit integration, so show_viewport_immediate renders the panel embedded in the main window, NOT yet a separate OS window — so this does not yet fully resolve the Windows-10 trapped-window report; true OS-window detach needs multi-viewport render-loop wiring (set_embed_viewports(false) + per-viewport winit windows), tracked as follow-up]; frontend-only, so the deterministic core is untouched and AccuracyCoin holds 141/141 (100.00%) with nestest 0-diff), on top of v2.2.8 "Aperture II" (2026-08-04, a presentation-fidelity release — gamma-correct scanlines in linear light + a WebGL2 gamma fix + a sharper Gaussian scanline profile in the base CRT_WGSL; presentation-only, so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical and the shipped native default is unchanged [the native sRGB surface passes aux = 0, which selects the exact pre-v2.2.8 output; the new linear-light + sharpness path is keyed on a non-zero aux, set on the WebGL2 non-sRGB path and when the scanline knob is raised]; the shader/appearance changes await on-display + browser visual verification), on top of v2.2.7 "Timbre II" (2026-08-04) — an expansion-audio fidelity release (of the v2.2.6 → v2.3.0 NESdev-remediation line), driven by a measure-first cross-reference of VRC6 and Sunsoft 5B against 11 reference emulators + the NESdev wiki (Mesen2-only comparison hides where Mesen2 is the outlier). VRC6 recalibrated to ~1.0× a 2A03 pulse (VRC6_MIX_SCALE 979 → 650; the NESdev/field consensus — rustico/tetanes/BizHawk encode 1.0× exactly; Mesen2's louder ~1.506× mixer weighting was the outlier a reviewer flagged; db_vrc6a/b oracle 1.506 → 1.0), and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC (SUNSOFT5B_LOG_VOL32, matching nestopia/rustico, replacing the 4-bit 3 dB approximation). Expansion-only — base 2A03 byte-identical, so AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff; the base BLEP is a verified 81.6 dB-SFDR band-limited decimator. Built on v2.2.6 "Almanac" (2026-08-04) — a de-monetization + provenance release opening the v2.2.6 → v2.3.0 NESdev-remediation line. RustyNES is permanently open-source and income-free (ADR 0035): all planned monetization is removed (the rustynes-monetization crate, docs/monetization/, and the Android/iOS billing / ad / freemium / paywall layers deleted) and the native apps are kept as free FOSS apps (no ads, no tracking, no paid unlock; the free Google-Play services + foss/play split retained). It also discloses (ADR 0030) that the PPU hybrid-address timing was calibrated to TriCNES (reproducing the Rad Racer mis-render), flagged for a documentation-derived rework in v2.3.0. Zero emulation-core behavior changes, so AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff by construction. On top of v2.2.5 "Colophon" (2026-08-03) — a provenance, licensing, and documentation-integrity release with zero emulation-core behavior changes (so AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff by construction). It reworded in-source comments that had mischaracterized publicly-documented hardware-behavior implementations as "ports of" copyleft emulators (Mesen2 GPLv3, puNES GPLv2) into the accurate oracle framing; rewrote NOTICE to disclose the behavioral-oracle use of GPL emulators (Mesen2/MesenCE, higan, GeraNES, ares, FCEUX, Nestopia, puNES — no code incorporated) and to attribute the genuinely-incorporated permissive components (emu2413, TriCNES, rcheevos — all MIT), the bundled fonts and test ROMs, and the CRT-shader/NTSC-filter visual influences as independent reimplementations; disclosed GeraNES (GPL-3.0-only) as an oracle; added docs/originality-and-provenance.md; and added an AI-assistance disclosure to the README (removing a misleading comparison graphic and fixing a mislabeled screenshot caption). On top of v2.2.4 "Cartridge" (2026-07-24) — a libretro / RetroArch distribution cut whose purpose is that the RustyNES core builds and installs cleanly through the Libretro buildbot (https://git.libretro.com/libretro/RustyNES) for in-RetroArch use. Zero emulation-core changes — the deterministic #![no_std] chip stack, save-state / TAS / netplay formats, and every golden vector are byte-identical to v2.2.3, so AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff by construction. crates/rustynes-libretro wraps rustynes-core, so it inherits every v2.2.3 change automatically (the fast-dot-path default; the PPU_SNAPSHOT_VERSION 8 / APU v4 save-state schema, transparent because get_serialize_size / on_serialize size and emit the current snapshot via Nes::snapshot_core_into rather than a fixed layout; the Mapper::mix_audio i32 widening; the Zapper model; the mNNN_ mapper rename), and both buildbot cross-ABIs the CI early-warning gate models — x86_64-pc-windows-gnu and aarch64-linux-androidcargo check --release -p rustynes-libretro clean. The concrete change is a rustynes_libretro.info metadata correction: disk_control falsetrue (the real fix — the FDS multi-side Disk Control interface has been wired since the buildbot recipe landed but was advertised as absent, hiding multi-disk FDS swapping from RetroArch's Quick Menu), display_version v1.0.0v2.2.4, and the description mapper count 168172. Libretro core options (region / overscan / palette / accuracy toggles) remain unexposed — core_options = "false" is accurate, a documented future enhancement rather than a v2.2.4 gap. The Antigravity PR reviewer standardization onto the shared template rides along. On top of v2.2.3 "Datum" (2026-07-23) — a performance and accuracy-closure patch, the product of a measure-first appraisal that profiled the emulator and acted on what the profile showed rather than on intuition. Performance: the specialized PPU fast dot path is promoted to the default and exposed to users for the first time — Nes::set_fast_dotloop had no caller outside the core, so a −11.3% frame-time win (fresh clean-host Criterion, reproducing v2.1.8's +12.3% by a different method; differential-tested bit-identical every frame since v2.1.8) shipped switched off and unreachable; release builds now ship PGO-optimized Linux binaries when the existing >3%-and-byte-identical gate passes; and CI gained a same-runner relative frame-time regression gate, closing a hole where a 2.5x slowdown passed the deliberately-loose absolute ceiling. Two optimizations were measured and REJECTED and are documented with their numbers per docs/performance.md's convention — P3 (emit_pixel bounds-check elision) made the shipped default slower (+4.32% / +3.35% on the _fast workloads, p ≤ 0.02), and P4 (cpu_clock) found both textbook optimizations already implemented with the one remaining lever capped at ≤1.9%. Accuracy: the last two Holy Mapperel residuals are closed, so all 17 ROMs report detail=0000 (was 15/17) — MMC1's two software WRAM write-protect layers ($E000 bit 4 + SNROM's CHR-register layer, gated on chr_is_ram) and FME-7's open bus on the RAM-selected-but-disabled window, both routed through the trait's existing cpu_read_unmapped contract. MMC1 is the change Holy Mapperel's README calls a game-compatibility hazard (FCEUX / PowerPak omit it), so it was validated before landing: 60/60 commercial ROMs including seven battery-backed MMC1 saves, plus 138/138 extended. The Sunsoft 5B absolute level is calibrated against Mesen2, which required widening Mapper::mix_audio to i32 (the correct full-scale 5B tone 1882 * 18.471 = 34,761 does not fit i16). A save-state schema gap is fixed — PPU_SNAPSHOT_VERSION 8 carrying the sprite-eval FSM + OAM data-bus state, plus an APU v4 tail — which is what made AccuracyCoin report 141/141 through run-ahead as well as without it; a new standing field-vs-schema audit found it and the two APU gaps mechanically. A Zapper beam-relative light model lands opt-in / default-off (no pass-fail light-gun ROM exists to adjudicate it). AccuracyCoin holds 141/141 (100.00%), nestest 0-diff. Also: the eleven sprintN.rs mapper modules (27,631 lines, ~110 boards) are renamed for the boards they emulate with mNNN_ mapper-number prefixes, proven content-preserving by a byte-for-byte item comparison (930 items, 0 altered) and an identical 172-ID dispatch table.

The prior release, v2.2.2 "Conduit" (2026-07-21), was a build, distribution, and CI-integrity patch: the libretro buildbot recipe from 1 of 10 jobs green to all ten building (the last step before RustyNES lands in RetroArch's built-in core downloader), a GitHub Actions supply-chain hardening pass (persist-credentials: false on all 19 checkouts, a fail-closed release-tag check via git/matching-refs, dtolnay/rust-toolchain SHA-pinned off @master), and the toolchain collapsed to one pinned source of truth — no toolchain version literal anywhere under .github/ and no nightly on any build path. Zero emulation-core changes, so AccuracyCoin held 141/141 by construction. Its one behavioural improvement in a shipped artifact: the libretro tvOS core built with panic = "abort" like every other platform.

The prior release, v2.2.1 (2026-07-15), was a housekeeping patch on top of v2.2.0 "Capstone" (next paragraph): archives two batches of dev/research tooling (the Game Genie header-robust re-key's six research/verification scripts in scripts/gg/, and the 2A03-revision DMA-divergence probe in scripts/probes/), consolidates six open Dependabot PRs with zero source changes (pollster 0.4→1.0, wide 0.7→1.5, tungstenite/tokio-tungstenite 0.29→0.30, bytemuck/cc patch, actions/setup-python v5→v6), and wires four gitignored, RUSTYNES_FDS_BIOS-gated smoke tests against TakuikaNinja's FDS $4023 / mirroring / audio-register / DRAM-watchdog hardware-verification probes (regression insurance for behavior RustyNES already models correctly, not a fix — the $4030.D1 DRAM-refresh-watchdog probe tracks a known, honest residual RustyNES does not model, per docs/accuracy-ledger.md). Zero accuracy, feature, or core changes — the deterministic #![no_std] chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched; AccuracyCoin holds 141/141 (100.00%), unchanged from v2.2.0.

The prior release, v2.2.0 "Capstone" (2026-07-12), was the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run, landing its two remaining marquees — the netplay matchmaking / lobby stack and the FDS medium model — atop a peripherals + quality/security pass, all sitting on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. DualSystem dual-console support. Every v2.2.0 change is additive or default-off, so the deterministic core is untouched on the shipped default — AccuracyCoin holds 141/141 (100.00%), nestest is 0-diff, and blargg_apu_2005 / pal_apu_tests (10/10) / visual_regression / the 60-ROM commercial oracle / the #![no_std] chip stack are all unchanged, and save-state / TAS / netplay replay stay bit-identical. It lands (PRs #290, #291; cut #297): a netplay lobby + matchmaking stack over the existing room-code / TURN transport (ListRooms/RoomList browse-and-join room directory + a server-side QuickMatch/Matched quick-play path, both signaling only with the room-list frame parsed by a brace-depth walk bounded at MAX_ROOM_LIST = 256); delayed-stream spectators (SpectatorConfig.delay_frames, clamped to MAX_DELAY_FRAMES = 512 — a pure presentation hold that cannot perturb the match); a hardened desync + peer-liveness surface (a graded DesyncStatus with a hysteresis threshold of 3 ≈ 1.5 s + a graded PeerLink / terminal DisconnectReason::PeerTimeout on multi-second 2 s / 5 s RTT timeouts — telemetry only, the rollback/determinism contract untouched); the FDS medium model completion (F4.3, a marquee — a per-block CRC-16/KERMIT re-emitted on every BIOS write via resynth_block_crc over a synthesized gap/mark wire image, an opt-in default-OFF continuous analog head-seek / velocity model Fds::set_analog_head_seek replacing the flat HEAD_RESEEK_CYCLES, and a BIOS-free synthetic write-verify oracle Fds::medium_write_verify as the CI-verifiable half — the real-BIOS write-CRC path stays gitignored/local; an additive v4 FDS save-state tail, byte-identical with the model off); peripherals (a Famicom $4016-bit-2 microphone Nes::set_microphone$4016-only, never $4017 — and a 3×3-aperture Zapper light-timing model ZAPPER_APERTURE_*, both additive / default-off); and a quality / security pass (cargo-fuzz targets grown 3 → 8ppu_reg_io / apu_reg_io / netplay_message / save_state / movie — the movie target finding and fixing two real OOM-DoS paths in Movie::deserialize, byte-identical for valid input; a read-only Tools → ROM Info browser over &Nes that never mutates; and four new MkDocs handbook pages). The prior step — v2.1.10 "Fathom" ("Loom") (2026-07-12) — was the creator-tools & web-parity step (TAStudio greenzone depth + Lua API breadth + a browser RetroAchievements auth-proxy deploy stack (ADR 0015) + Vs. DualSystem presentation in the libretro core; the wasm desktop-style dual present remains deferred). The intervening steps: v2.1.6 "Timbre" (2026-07-11) expansion-audio fidelity (a decibel oracle asserting measured level ratios vs Mesen2 / hardware targets, hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical) → v2.1.7 "Stepping" (2026-07-12) hardware revisions & DMA frontier (opt-in PpuRevision / Cpu2A03Revision die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier proven a documented no-op on every oracle, ADR 0033 — honest, not faked) → v2.1.8 "Tempo" (2026-07-12) performance (a default-OFF specialized fast PPU dot path, Nes::set_fast_dotloop — ~+12.3% rendering-heavy, differential-tested bit-identical; a SIMD-validated software blitter where scalar stays default; a wasm 3.99 MiB-gzip size pass) → v2.1.9 "Aperture" (2026-07-12) presentation & signal (a raw NTSC composite rustynes-ppu::raw_signal core; a marquee CRT shader stack — CRT-Royale / CRT Guest Advanced / Sony Megatron — all naga-validated WGSL; GIF/WAV capture; a live generated-palette preview). Now on the shipped default the Fathom line keeps the deterministic core byte-identical and layers accuracy + display + quality-of-life work atop the v2.0.0 core: v2.1.0 accuracy remediation (PPU palette backdrop-override, the mapper-tier completion to 51 Core / 95 Curated / 26 BestEffort of 172, the MMC3 R1/R2 residual closed by-design-permanent) → v2.1.1 the Wizards & Warriors game-DB mirroring freeze fix → v2.1.2 "Prism" display fidelity (Vs. DualSystem desktop second-screen, the NTSC composite-shader ladder, an in-core generated NTSC palette, NSF non-60 Hz + NSFe) → v2.1.3 "Codex" quality-of-life (an APU audio filter-model selector, Game Genie code nomination + a full / header-robust code database, a Material-for-MkDocs handbook served at /docs/ on GitHub Pages) → v2.1.4 "Caliper" accuracy hardening (opt-in default-OFF OAM decay modeled on Mesen2, a CI boot-smoke sweep of all 26 BestEffort mapper families, a shared MMC3-clone A12/IRQ timing oracle) → v2.1.5 "Vernier" the regression-net & residual step → v2.1.6 "Timbre" expansion-audio fidelity → v2.1.7 "Stepping" hardware revisions & DMA frontier → v2.1.8 "Tempo" performance → v2.1.9 "Aperture" presentation & signal → v2.1.10 "Loom" creator tools & web parity → v2.2.0 "Capstone" the milestone cut closing the run → v2.2.1 (housekeeping) → v2.2.2 "Conduit" the build/distribution/CI-integrity patch. v2.0.0 "Timebase" remains RustyNES's designated MAJOR-boundary release — see "Timebase (v2.0.0)" below. RustyNES is now a multi-platform emulation suite, all on the one byte-identical cycle-accurate core — docs/STATUS.md is the authoritative current-state record. What ships beyond the desktop app:

  • Timebase (v2.0.0) — the scheduler substrate is rewritten from a five-counter dot-lockstep model to a single canonical cycle counter, every CPU cycle a real bus access, and a split-around-the-access start_cycle/end_cycle PPU catch-up (ADR 0002 / ADR 0029), now the only scheduler path. This is a MAJOR-boundary breaking change (ADR 0003): .rns save-state and .rnm movie format epochs bump (ADR 0028) — a pre-v2.0.0 .rns slot now fails to load with a clear error instead of silently misinterpreting stale bytes. Landed across five betas + rc.1 (PRs #217–223). Also new: core-level Vs. DualSystem dual-console support (Emu::Dual, crates/rustynes-core) for the four Vs. arcade cabinet boards — core-and-test-harness-only, frontend wiring deferred. The R1/R2 MMC3 IRQ-timing residual is by-design-deferred beyond this release with a mechanism-level finding recorded in ADR 0002 (not closed, not silently dropped). AccuracyCoin now measures 141/141 (100.00%): the v2.0.1 upstream AccuracyCoin re-sync grew the catalog to 146 rows / 141 assigned tests and briefly opened two new PPU gaps ("ALE + Read" $0491, "Hybrid Addresses" $0492), which v2.0.3 closed by promoting the 2-cycle-ALE PPU fetch model to the unconditional default (both experimental flags retired; additive PPU_SNAPSHOT_VERSION v5 tail). AccuracyCoin held 100% (139/139) throughout the v2.0.0 betas and final cut, dipped to 139/141 under the v2.0.1 re-sync, and is back to a full 141/141 from v2.0.3 onward.

  • Native Android app — the v1.8.0 → v1.8.9 "Android" train (crates/rustynes-mobile UniFFI bridge + crates/rustynes-android JNI/NDK host + a Jetpack Compose app, ADR 0024): full on-device emulation, multi-touch + P1–P4 hardware controllers, wgpu SurfaceView rendering + the shared WGSL shader stack, save-states / battery SRAM, Lua, RetroAchievements, direct-IP + CGNAT/TURN room-code netplay, a box-art ROM library, and platform polish (adaptive / foldable / TV, Material You, capture / PiP / home-screen widget). Distributed as GitHub-Releases sideload now; Google Play deferred to v2.3.0 (ADR 0025 foss/play split).

  • Native iOS / iPadOS app — the v1.9.0 → v1.9.9 "iOS" TestFlight train (crates/rustynes-ios Metal + CoreAudio shim reusing rustynes-mobile verbatim → UniFFI-generated Swift, ADR 0026): a native SwiftUI shell over wgpu→Metal, multi-touch + GameController, the shader stack, TAS / HD-pack / palettes / per-game DB, Lua + RetroAchievements, LAN + room-code netplay, CloudKit save-state sync, accessibility + EN/ES i18n + ReplayKit + Game Center, and the v1.9.9 creator tools (Cheats, a FOSS-gated read-only debugger, a touch TAStudio piano-roll, foreign movie import, a host audio-depth DSP). Ships to interim TestFlight now; App Store + AltStore PAL deferred to v2.3.0 (ADR 0027). Mobile ROM loading is iNES / NES 2.0 only (FDS / NSF a post-v2.0.0 carryover). Readiness record: docs/ios-v1.9.9-readiness.md.

  • Native Libretro corecrates/rustynes-libretro (builds the rustynes_libretro shared library — .so / .dylib / .dll by platform, per the crate Makefile) integrates RustyNES into RetroArch (RetroAchievements, dynamic audio sync, deterministic save-state / rollback). Docs in docs/libretro/, plan in to-dos/libretro/, reference in ref-docs/RustyNES-Libretro_Core.md; the crate Makefile cross-compiles natively and docs/libretro/UPSTREAM_SYNC.md covers the re-fork / upstream-info-file (libretro-super + libretro-docs) workflow.

  • Mapper breadth → 174 families (168 at the v1.7.x tag → 172 → 174 with v2.3.4's 176/2, 154 and 243), Core / Curated / BestEffort behind the CI accuracy-honesty gate.

  • Release automation.github/workflows/release-auto.yml: when a new version goes final-green on main, it auto-tags + publishes the GitHub Release (body from a maintainer-authored .github/release-notes/vX.Y.Z.md override, else the CHANGELOG [X.Y.Z] section; title codename parsed from the CHANGELOG header) and builds + attaches the desktop binaries by invoking release.yml via workflow_call (a tag pushed by GITHUB_TOKEN can't trigger on: push: tags, hence the direct call). The v1.8.0–v1.9.9 GitHub Releases are all published with comprehensive notes + Linux / macOS-aarch64 / Windows binaries.

Platform additions through v1.10.0 were host-only and additive: the deterministic #![no_std] chip stack was untouched and byte-identical on ARM. v2.0.0 "Timebase" is different by design — it rewrites the scheduler substrate itself (still #![no_std]-clean, AccuracyCoin now back at a full 141/141 (100%) from v2.0.3 — see above, but the save-state / movie format epochs deliberately bump per ADR 0028, so cross-version .rns/.rnm round-trip is a v1.x-only guarantee, not a v1.x⇄v2.x one). Forward path: the v2.0.x "Harbor" mobile-finalization re-port train onto the v2.0.0 core has fully shipped — v2.0.1 (first Android re-port + AccuracyCoin oracle re-sync), v2.0.2–v2.0.3 (the 2-cycle-ALE accuracy closure to 141/141), v2.0.4 (Android release candidate), v2.0.5–v2.0.8 (iOS finalization), and v2.0.9 (both-apps readiness) — followed by the v2.1.x "Fathom" accuracy line (v2.1.0 → v2.1.10) capped by the v2.2.0 "Capstone" milestone cut, then the v2.2.1 housekeeping patch, v2.2.2 "Conduit", v2.2.3 "Datum", and v2.2.4 "Cartridge" (the libretro/RetroArch distribution cut); see the "Current release" paragraph above for the actual current release. The v2.1.5 → v2.2.0 line is a "deepen the existing project" run (accuracy / performance / features / quality); v2.1.5 "Vernier" opened it (the tepples Holy Mapperel mapper bank-reachability / IRQ regression net, the first PAL-region APU oracle at pal_apu_tests 10/10, the MMC3 R1/R2 F5.0 A12-phase study, a measured fat-LTO A/B, and a real TURN NAT-traversal retransmit production fix — all NTSC-byte-identical), v2.1.6 "Timbre" continued it (the expansion-audio decibel oracle, the hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, and a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical), v2.1.7 "Stepping" added opt-in PPU / 2A03 die-revisions + power-on RAM/palette hardware models (the DMA "unexpected read" frontier proven a documented no-op on every oracle, ADR 0033 — honest, not faked), v2.1.8 "Tempo" the default-OFF specialized fast PPU dot path (~12% rendering-heavy, differential-tested bit-identical) + a SIMD-validated software blitter + a wasm size pass, v2.1.9 "Aperture" the marquee CRT shader stack + a raw NTSC composite signal-decode path + GIF/WAV capture + a palette editor, v2.1.10 "Loom" the TAStudio greenzone + Lua API breadth + the browser-RA auth-proxy deploy stack + Vs. DualSystem libretro presentation, and v2.2.0 "Capstone" the milestone cut closing the run (the netplay matchmaking / lobby stack + the FDS medium model + a peripherals & quality/security pass — fuzz targets 3 → 8, a Movie::deserialize OOM-DoS fix, a read-only Tools → ROM Info browser) — all NTSC-byte-identical, AccuracyCoin 141/141 throughout; the v2.1.5 → v2.2.0 run is now closed. A free-app store launch (Google Play / App Store / AltStore PAL / F-Droid — no ads, tracking, or paid unlock, per ADR 0035) remains a candidate, but it is no longer tied to v2.3.0: that slot was repurposed as the PPU-accuracy capstone and has shipped. Any store listing is a later, unversioned step — see to-dos/ROADMAP.md.


Release history → CHANGELOG.md. The full per-release detail — features, the mapper-count growth (51 → 172 families), ADRs, and PR trains for v1.0.0 → v2.0.0 (plus the documentary engine-lineage stages v0.9.0–v0.9.7) — lives in CHANGELOG.md (the single source of truth for user-visible change), the per-release GitHub Releases, and to-dos/plans/. Every release through v1.10.0 was additive / off-by-default, so with new features off those builds stayed byte-identical; v2.0.0 is RustyNES's one designated breaking release (ADR 0003) — the one-clock, every-cycle-bus-access scheduler (ADR 0002 / ADR 0029) is now the only path, and the old PPU-dot lockstep model is retired. AccuracyCoin holds 100% (139/139) on every release including v2.0.0. Workspace baseline: edition 2024, Rust 1.96, license GPL-3.0-or-later (RustyNES is a derivative work of GPL emulators — Mesen2 GPLv3, puNES/FCEUX/Nestopia GPLv2-or-later; relicensed in v2.2.9 per ADR 0036, credited in docs/originality-and-provenance.md + NOTICE), author DoubleGate; the WebAssembly / GitHub Pages build is live at https://doublegate.github.io/RustyNES/.

Engine-lineage versioning (read carefully). The core descends from an accuracy program whose internal "v1.x / v2.x" milestones are folded into RustyNES stages v0.9.0–v0.9.7 → the v1.0.0 production cut. Read deep-narrative "v2.0" anchors from before 2026-07-03 (the master-clock refactor, old ADRs / audit logs under docs/) as upstream engine lineage, never as RustyNES release versions — that engine-lineage v2.0 work shipped as the v1.0.0 production core (2026-06-13) and is a different thing from RustyNES's own v2.0.0 "Timebase" release (2026-07-03, the base of the current v2.0.x "Harbor" line), which replaces that same dot-lockstep scheduler with the one-clock model. docs/STATUS.md is the authoritative per-suite pass-count + mapper matrix.

Build / test / lint

# Build
cargo check --workspace
cargo build --workspace
cargo build --release --workspace

# Tests
cargo test --workspace                              # unit + integration
cargo test --workspace --features test-roms         # + AccuracyCoin / blargg / kevtris ROM suites
cargo test --workspace --features test-roms,commercial-roms  # + 60-ROM commercial oracle (needs local dumps)
cargo test -p rustynes-cpu                          # single crate
cargo test -p rustynes-cpu nestest                  # single test by name substring
cargo test --workspace -- --test-threads=1          # serial (for flake debugging)

# Run only the #[ignore]'d expected-fail probes
cargo test --workspace --features test-roms --no-fail-fast -- --ignored

# Quality gates (all run in CI; all must be green)
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy -p rustynes-frontend --all-targets --features scripting -- -D warnings  # v1.1.0 Lua engine
cargo clippy -p rustynes-frontend --all-targets --features scripting,hd-pack -- -D warnings  # v1.2.0 HD-pack
cargo clippy -p rustynes-frontend --all-targets --features retroachievements -- -D warnings  # RA FFI — DON'T skip this one
# After any `cargo clippy --fix`, re-run clippy for EVERY feature combo (incl. retroachievements):
# --fix compiles only the active feature set, so it can strip cfg-gated code that another feature
# needs (bit PR #80: an `elidable_lifetime_names` autofix removed a `<'a>` the `retroachievements`
# `ra` param uses, breaking that build). Wasm clippy commands are in the WebAssembly section below.
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
# NEVER `--all-features`: v1.2.0's `script-wasm` (piccolo/wasm) and `scripting` (mlua/native) are
# mutually-exclusive rustynes-script backends, so `--all-features` can't resolve. CI (and the
# pre-commit hook) use EXPLICIT features. Also: a rustdoc intra-doc link to a feature-only dep
# (e.g. [`piccolo`] / [`mlua`]) FAILS the default `cargo doc --workspace --no-deps` — use plain
# `code` spans for mutually-exclusive-feature crate names (bit PR #76).

# no_std cross-compile (the chip stack must compile against core + alloc only)
cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features

# Frontend (winit + wgpu + cpal + egui). Binary name is `rustynes`.
cargo run --release -p rustynes-frontend -- path/to/rom.nes
cargo run --release -p rustynes-frontend                 # opens with no ROM; menu / F12 to load
# Maximal NATIVE build — the "cargo --full equivalent" (#54): the `full` feature
# aggregates every native feature (retroachievements + scripting + script-ipc +
# hd-pack + debug-hooks + av-record, additive on top of the default set). It is
# purely opt-in (shipped/default build + core unchanged). Aliases in .cargo/config.toml:
cargo full-run path/to/rom.nes                           # run the maximal native binary (alias ends in `--`, so flags forward, e.g. `cargo full-run --fullscreen rom.nes`)
cargo full-build                                         # build it (= --release -p rustynes-frontend --features full)
# WASM-only features (script-wasm, browser-cheevos, wasm-canvas) are excluded by design.
# Default keys P1: arrows = D-pad, Z = A, X = B, Enter = Start, RShift = Select.
# Default keys P2: WASD = D-pad, Q = A, E = B, P = Start, L = Select.
# System: Esc = quit, F1 = save state, F4 = load state, F5 (held) = rewind,
# F2 = reset, F3 = power-cycle, F12 = open ROM, F9 = FDS disk-swap, ~ = toggle debugger.
# F6/F7/F8 = TAS movie record/play/branch. Drag-and-drop a .nes/.fds to load.
# USB gamepads auto-bind to P1 (Xbox-style: South=A, West=B, Start, Back/Select, DPad).

# WebAssembly frontend — needs `trunk` + the wasm32 target (auto-installed from
# rust-toolchain.toml). Run from crates/rustynes-frontend/web:
#   trunk serve                                          # dev server
#   trunk build --release                                # wasm-winit (default)
#   trunk build --release --no-default-features --features wasm-canvas  # lightweight embed
# wasm clippy gates (CI): cargo clippy -p rustynes-frontend --target wasm32-unknown-unknown
#   --lib --bins -- -D warnings   (and again with --no-default-features --features wasm-canvas)
# GOTCHA: web/Trunk.toml pins the wasm-bindgen CLI version, which MUST exactly match the
#   wasm-bindgen LIBRARY in Cargo.lock (grep -A1 'name = "wasm-bindgen"' Cargo.lock). A
#   mismatch fails `trunk build` (and the Pages deploy) at the wasm-bindgen step, but wasm
#   clippy still passes — so bump the pin whenever a resolve moves the library version.
# CI deploy: .github/workflows/web.yml ("Deploy Pages (demo + docs)") publishes BOTH the
#   playable demo (root) and the workspace rustdoc (/api/) to GitHub Pages from the
#   "GitHub Actions" source: https://doublegate.github.io/RustyNES/ + /api/.

# Benchmarks (criterion)
cargo bench -p rustynes-cpu
cargo bench -p rustynes-ppu
cargo bench -p rustynes-mappers
cargo bench -p rustynes-core

Toolchain is Rust 1.96 pinned in rust-toolchain.toml (bumped from 1.86 in v1.3.0 to unblock the edition-2024 + egui 0.34 / wgpu 29 / rfd 0.17 dependency tier). CI runs the test job on stable across Linux/macOS/Windows plus an MSRV pin at 1.96 on Linux.

On Linux, anything that pulls in rustynes-frontend (which cargo test --workspace does) needs the wgpu/winit/cpal system deps:

sudo apt-get install -y libxkbcommon-dev libwayland-dev libxkbcommon-x11-dev libasound2-dev libudev-dev
# CachyOS / Arch:
sudo pacman -S --needed libxkbcommon wayland alsa-lib systemd-libs

Architecture — load-bearing facts

These cross-cutting decisions span multiple files. Reading individual chip docs without them in mind will mislead.

The PPU is the master clock. The scheduler advances one PPU dot per tick_one_dot(); the CPU advances on every third dot (NTSC / Dendy; 3.2nd dot PAL); the APU advances every other CPU cycle. This is lockstep, not catch-up. It is the central architectural choice and the reason mid-instruction PPU events (sprite-zero hit at a precise dot, MMC3 IRQ at PPU dot 260, mid-scanline scroll writes) work without per-quirk patches. See docs/scheduler.md.

The Bus owns everything mutable. rustynes-core::Bus holds the PPU, APU, mapper-via-cart, WRAM, controllers, and open-bus latch. The CPU borrows &mut Bus during tick(). Per the TetaNES postmortem, this single choice avoids the borrow-checker fight that the alternative ("CPU holds PPU, but PPU also needs CPU bus") creates. The PPU and APU each see a smaller trait (PpuBus, ApuBus) for what they actually need — mapper-mediated CHR/nametable reads and DMC sample fetches respectively.

Workspace dependency graph is one-directional. rustynes-cpu has no PPU or APU dep. rustynes-ppu depends on rustynes-mappers only (CHR/nametable bus). rustynes-apu is independent. rustynes-core ties them together. Result: each chip is fuzzable and benchmarkable in isolation. Adding a cross-chip dependency breaks this invariant — don't.

Mapper IRQ logic lives in the mapper, not the PPU. The PPU calls Mapper::notify_a12(level) on every A12 transition; the mapper internally filters (MMC3's "3 falling edges of M2" lives in the MMC3 impl). MMC5 uses different scanline detection via notify_scanline_start / notify_vblank. VRC2/4/6, Sunsoft FME-7, and Namco 163 tick on notify_cpu_cycle(). All such hooks are default-no-op on Mapper. See docs/mappers.md for the per-mapper IRQ family table.

Determinism is a hard contract. Same seed + ROM + input sequence ⇒ bit-identical framebuffer and audio. CPU/PPU initial phase alignment is randomized at power-on from a seeded PRNG; reset preserves alignment. This is required for save-state round-trip, regression tests, TAS replay, and netplay rollback. Don't introduce hidden non-determinism (system time, thread scheduling, OS RNG) into the core. Netplay's dynamic rate control and run-ahead live in the frontend (a resampler stage / snapshot-restore orchestration), never in the core's synthesis — that is what keeps the contract intact.

The frontend is an always-on egui shell, not a bare window. rustynes-frontend is winit + wgpu + cpal + egui, and egui runs every frame: DebuggerOverlay::render_shell draws a persistent menu bar (File / Emulation / Tools / View / Debug / Help) + status bar + tabbed Settings window, with the toggleable (`) CPU/PPU/APU/memory debugger panels layered on top. The shell never holds the emu lock inside the egui closure — menu interactions return a MenuAction that App::dispatch_menu_action runs after the egui pass, and the hidden render branch copies the framebuffer under a brief lock, drops it, and renders/presents with nes = None (the locked branch is taken only when the overlay is visible or a nes-reading tool panel like Cheats is open). On native the emulator runs on a dedicated thread (emu-thread, default-on) communicating via the Arc<Mutex<EmuCore>> handle + lock-free SharedInput; the winit thread only does UI + present. Full spec in docs/frontend.md (this is just the primer).

Test ROMs are the spec. When the docs and a passing test ROM disagree, the ROM wins — the docs get updated. The blargg / kevtris / mmc3_test_2 / AccuracyCoin suites in tests/roms/ are the closed-form definition of "cycle-accurate." See docs/testing-strategy.md for the testing layers.

Where things live

  • crates/rustynes-{cpu,ppu,apu,mappers,core,netplay,cheevos,frontend,test-harness}/ — the core emulation stack; crate name = dir name. The binary is rustynes (in rustynes-frontend). Plus the supporting crates: rustynes-script (Lua), rustynes-ra (RetroAchievements session state), rustynes-gfx-shaders (shared WGSL), rustynes-hdpack (HD-pack loader/compositor + HD audio), and the platform crates rustynes-mobile (the UniFFI bridge — generates Kotlin and Swift), rustynes-android (JNI/NDK host), rustynes-ios (Metal + CoreAudio shim; only the #[cfg(target_os="ios")] glue is iOS-specific), and rustynes-libretro (the RetroArch core; builds the platform-appropriate rustynes_libretro shared library — .so / .dylib / .dll). The android/ and ios/ dirs hold the Compose / SwiftUI apps.
  • docs/ — implementation specs. These are the spec, not history: update them in the same PR as the code change. Per-subsystem files (cpu-6502.md, ppu-2c02.md, apu-2a03.md, mappers.md, cartridge-format.md, scheduler.md) + cross-cutting (architecture.md, testing-strategy.md, performance.md, frontend.md, compatibility.md). docs/STATUS.md is the single source of truth for per-suite pass counts, the mapper matrix, and version policy. docs/adr/ holds Michael-Nygard-format ADRs.
  • ref-docs/ — immutable hardware + emulation reference (60+ source research report). Updates go in dated supplemental files.
  • to-dos/ROADMAP.md → phase/sprint files — tickets with stable IDs T-PS-NNN. Reference in commits.
  • tests/roms/ — CC0 / public-domain test ROMs (committed). tests/roms/external/ — your own commercial dumps (gitignored).
  • tests/golden/ / screenshots/ — reference framebuffers, audio, and the visual baseline corpus (committed). tests/captures/ — current-run output (gitignored).

Never commit commercial Nintendo ROMs.

Workflow conventions

  • Branch names: <type>/<short-desc>.
  • A chip-behavior change touches both the chip code and the chip's docs/<subsystem>.md. They drift apart easily; don't let them.
  • For accuracy work: pin the failing test ROM expectation first, then implement until it passes.
  • Hot paths (Cpu::tick, Ppu::tick, mapper register access): no allocations, prefer fixed arrays, profile (cargo bench + perf record) before adding abstractions. On the frame-cost number: the ≤ 2 ms/frame headless figure in docs/performance.md is a design-phase aspiration (written before the cycle-accurate core existed, for 2018-era Skylake) — it is NOT a live gate. The implemented core measures ~3.9 ms (nes_run_frame_nestest_fast) / ~2.5 ms (flowing_palette), which docs/performance.md records as knowingly accepted for the master-clock design. That is ~23% of the 16.639 ms NTSC budget. The dominant costs are work the accuracy model requirescpu_clock is APU BLEP synthesis + the non-linear mixer (mixer ceiling measured ≤1.9%), and Ppu::tick is the per-dot lockstep loop — and the obvious levers were already measured and rejected (emit_pixel bounds-check elision was slower; the SIMD blitter was slower). Do not "optimize toward 2 ms" by trading away accuracy; the real-world multiplier on frame cost is run-ahead, not the per-frame core cost. Any optimization must clear the project's >3% same-runner A/B bar and stay byte-identical.
  • unsafe requires a // SAFETY: comment explaining the invariant. The chip stack is #![no_std] + extern crate alloc;; only rustynes-frontend and rustynes-cheevos (FFI) carry unsafe.
  • Comprehensive rustdoc + comments (project rule). Craft extensive //! crate/module preambles and /// / // inline comments matching the quantity, quality, and technical depth of the existing rustynes-* crates — explain the why alongside the architectural detail, the memory-safety guarantees, and the lockstep-timing considerations.
  • Comprehensive commit bodies (project rule). Commit message bodies are robust, comprehensive, and technically detailed: go beyond a summary to explain architectural impact, the mathematical implementation, memory constraints, and the deep technical specifics (the maintainer's house style; see docs/guidelines).
  • Code style: rustfmt defaults + the crate-level import grouping in rustfmt.toml; .editorconfig mandates UTF-8 / LF / a final newline and indentation of four spaces for Rust, two for Markdown / TOML / YAML. Justify any local #[allow].

Operating notes for Claude Code

  • docs/STATUS.md and the "Current release" summary above are the current-state source of truth; CHANGELOG.md and the docs/audit/ logs carry the deep engine-lineage history.

  • ref-docs/ is immutable. Research updates go in dated supplemental files.

  • ADRs go in docs/adr/ (Michael Nygard format).

  • rustynes-core re-exports the public types from the chip crates; downstream consumers (rustynes-frontend, rustynes-test-harness) should depend on rustynes-core rather than the chip crates directly.

  • When relabeling old engine "v2.x" narrative for users, present it as upstream lineage/history — never as a current RustyNES release version. The current release is v2.6.4 "Rubric" (OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged, on v2.6.3 "Mainspring" — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged, on v2.6.2 "Witness" — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged, on v2.6.1 "Interleave" — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged, on v2.6.0 "Assay" — the triangle, the noise channel and the sweep unit in the MiSTer co-simulation DUT — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged, on v2.5.9 "Overture" — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first, on v2.5.8 "Blanking" — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions, on v2.5.7 "Collimation" — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating, on v2.5.6 "Vestige" — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it, on v2.5.5 "Raster" — the first full frame, and three blind spots in the stimulus that fed it, on v2.5.4 "Escapement" — the background fetch pipeline, and an access two dots early that five gates could not see, on v2.5.3 "Hysteresis" — toggling rendering takes effect three dots after the write, and four instruments to prove it, on v2.5.2 "Dormant" — the 2C02 register file, and a gate that passed while testing nothing, on v2.5.1 "Retrace" — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned, on v2.5.0 "Rungwork" — the 6502 rung, and the two gates it cannot reach, on v2.4.9 "Plumbline II" — the bus half of rung 2, and what it found the day it existed, on v2.4.8 "Palimpsest" — read-modify-write, and a gate that cannot see its own subject, on v2.4.7 "Keystone" — the stack closes, and a dead line proves itself dead, on v2.4.6 "Abacus" — the core learns arithmetic, on v2.4.5 "Compass" — the core reaches memory, and chooses, on v2.4.4 "Ignition" — 2026-08-22, the first real RTL: the 6502's eight-cycle reset and the implied opcode group matching the oracle on all seven CPU fields, and the oracle settling a reset length our own prose gave two different answers for), on top of v2.4.3 "Touchstone" (2026-08-22, the two Fabric risks settled before any RTL: the Quartus 17.0.2 subset FITTED at 2 M10K blocks and 29 registers with zero synthesis warnings, and the sys/ licence audit finding ZERO GPL-2.0-only files, which inverts the plan's hedge and confirms GPL-3.0-or-later), on top of v2.4.2 "Cairn" (2026-08-22, the rung-0 compare surface: rolling per-cycle hash checkpoints measured at 15,263x smaller than the equivalent CSV, the acceptance gate made executable, and the partition between what RustyNES MODELS and what a device can OBSERVE), on top of v2.4.1 "Fabric" (2026-08-20, the oracle release opening the v2.4.1 → v2.5.0 "Fabric" line — a new NES core in SystemVerilog written from public hardware documentation in a sibling repository, with RustyNES as its VERIFICATION ORACLE; RustyNES is not being ported to FPGA and cannot be. crates/rustynes-cosim is the boundary (a narrow C ABI a Verilator testbench links, plus nes_golden_export), the firewall extends to HDL per ADR 0037 (NES_MiSTer and fpganes rtl/ are strict black boxes), and v2.5.0 is scoped to "the 6502 rung closes" rather than a finished core. Excluding the crate from the workspace is the load-bearing detail: cargo unifies features, irq-timing-trace selects a DIFFERENT per-dot loop in Bus::tick_one_cpu_cycle, and the accuracy battery was therefore validating a scheduler no user runs. Also found: the first run_frame() after power-on advances ZERO cycles, and two trace-gated core modules had never been linted. It CARRIES v2.4.0 "Concordance", which merged to main and was never tagged — atomic durable writes on every path that persists user data (four call sites, four further silent successes found in review), Nes::timeline_generation(), and the 15-anchor release audit. AccuracyCoin 141/141 and nestest 0-diff VERIFIED), on top of v2.3.9 "Crucible" (2026-08-20, the gates release — a crucible tests to destruction rather than inspects, and this one does that to the project's own checks. The docs-only CI skip HAD NEVER WORKED [predicate-quantifier defaults to some, so the code filter's '**' matched everything and all seven ! exclusions were dead from the day they were written]; fixed with TWO filter steps because the quantifier is step-level and accuracy is a list of alternatives that becomes unsatisfiable under every — the one-line fix would have silently disabled the accuracy battery. test-roms now runs at review time, path-filtered over the chip crates, the core, rustynes-gamedb, the harness and tests/ [11 of the last 40 merged PRs]. A freeze from one cartridge kept writing into the next — an active per-frame write into the wrong game — closed by a ROM-transition sweep under one rule: derived output discarded, user-authored input kept, and only input that actively WRITES neutralised. The config file is written atomically and durably [seven properties, five from review]. Movies record two ports while the Replay panel advertised "Four Score (P1..P4)", now disclosed at three levels. 257 lines of dead code removed, 25 of 29 #[allow(dead_code)] attributes suppressing nothing, undocumented_unsafe_blocks made a gate, and two cargo deny ignores retired on their own stated condition. rustynes-apu and rustynes-core both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of v2.3.8 "Parallax" (2026-08-20, the Divergence Lens — Probe could say two configurations diverge and AT WHICH FRAME and nothing about where or why, because a trial reduces each frame to one u64; divergence::localise keeps the full output and reports the SHAPE of the difference [population count, first pixel in raster order, inclusive bounding box], localises on the INDEX framebuffer so a palette difference cannot masquerade as a rendering one, hands the located pixel to Pixel Provenance, and answers Inconclusive rather than collapsing "I stopped looking" into "they agree". Cut from its own boundary commit, so its artifacts contain exactly the Lens), on top of v2.3.7 "Overtone" (2026-08-19, the audio-provenance release — the APU counterpart of Pixel Provenance: a per-register write attribution [what wrote this, and from which instruction] plus a per-CPU-cycle mix trace [what were the channels actually doing], at Tools → Audio → Audio Provenance, output-only and runtime-default-off. Its real subject is the trap it inherited: Pixel Provenance shipped non-functional for four releases because run-ahead's rollback cleared its store before any UI could read it, so the carry landed in the SAME change as the feature — and then the same defect turned up in THREE more places, every restore in rustynes-probe, so running the Latency Oracle or the RAM Atlas silently emptied both provenance panels [the v2.3.6 fix had enumerated one caller rather than the mechanism, and measure_in_place_restores_the_live_timeline could not see the breach because provenance is deliberately not in the snapshot]. Two defects found by measurement not reading: the new apu_throughput bench reshaped the plumbing three times on regressions invisible in the diff, and a randomized sweep of the save-state parse boundary found FOUR panics in VRC7's OPLL where hand-tracing found one — the all-0xFF payload CONCEALED one. Also fixed: $4014/$4016 documented as attributed and were not, the browser demo applied no per-game header corrections, Rad Racer's roadside artifact, VRC7 save states dropping the live FM synthesizer, and unbounded CI jobs. rustynes-apu and rustynes-core both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of v2.3.6 "Sounding" (2026-08-17, the measurement release — two shipped features found never to have worked [Pixel Provenance's record wiped by run-ahead before any UI could read it, its click never implemented; the Duck Hunt Zapper probe exactly inverted], the Latency Oracle and RAM Atlas both built to decline rather than guess, APU Workstream D closed on three measured rejections, and the Tools/Debug menus regrouped by task; core gains one const fn getter so AccuracyCoin 141/141 is VERIFIED), on top of v2.3.5 "Manifest" (2026-08-16, the declaration release — the libretro .info RetroArch reads is a SEPARATE upstream copy the GPL relicense never reached, corrected to GPLv3+ with a standing audit; five wrapper defects each with correct emulation behind them [PAL 20.2% fast, Reset inert, unload leaked cheat indices, square-pixel aspect, Zapper unreachable]; a use-after-free in the controller tables found in review; the APU's first throughput bench + a −3.3%/−4.2% default-mix specialization; AccuracyCoin 141/141 VERIFIED. The RetroArch licence display and iOS/iPadOS/tvOS availability both remain blocked on upstream PRs), on top of v2.3.4 "Ledger" (2026-08-15, the coverage release — mappers 176/2 (WAIXING-FS005), 154 (NAMCOT-3453) and 243 (Sachen SA-020A) taking breadth to 174 families; the coverage harness moved onto the frontend's real load path, exposing a per-game-database defect that had made every Sachen cartridge unloadable since v1.2.0; this one TOUCHES the core, so AccuracyCoin 141/141 is verified, not by construction; Workstream C — the APU at 18.7% — was NOT delivered and is carried to v2.3.5), on top of v2.3.3 "Cadence" (2026-08-14, the display-pacing release — the run-ahead throttle oscillation traced to a stale median, a predictive engage arm, and the wp_presentation apparatus; frontend-only, AccuracyCoin 141/141), on top of v2.3.2 "Lucid" (2026-08-11, the pixel-provenance release — per-byte write attribution + the per-pixel causal record + the Tools → Pixel Provenance panel + deterministic replay attestation via rustynes verify; all debug-hooks-gated and output-only, so AccuracyCoin holds 141/141 and nestest is 0-diff), on top of v2.3.1 "Plumb Line" (2026-08-06, the measurement release — ten hot-path candidates measured and all ten rejected), itself on v2.3.0 "Datum II" (2026-08-05, the capstone closing the v2.2.6 → v2.3.0 NESdev-remediation line — true multi-viewport OS-window detach for every tool panel (v2.2.9's affordance only embedded them, so the Windows-10 trapped-window report is now genuinely fixed); a frame-pacing fix predating that work (the render path held the emulator lock across the blocking swapchain acquire + present, stalling frame production whenever a debugger panel was open — now split so the lock covers only the egui UI build, plus pace_frames reading a lock-free has_rom atomic instead of locking every about_to_wait); a −5.13% / −3.51% byte-identical PPU optimization (v2.3.0 P1: #[inline] on the per-dot sprite eval + hoisting the tick_oam_bus early-out); both remaining forum-reported accuracy items (SMB left edge, Rad Racer hybrid-address) verified already-correct; and the AccuracyCoin gate pinned to an exact 141/141), on top of v2.2.9 "Studio II" (2026-08-04, a frontend quality-of-life release — TAStudio piano-roll edits wired to the emulator, .bk2 playback honoring the movie's LogKey column order, and a detach/pop-out affordance for tool windows (the shared detachable_window helper across 18 panels) [native-only; it embedded the panel on the single-viewport egui_winit integration rather than opening a separate OS window — resolved in v2.3.0 by the real multi-viewport implementation]; frontend-only so the deterministic core is untouched and AccuracyCoin holds 141/141, nestest 0-diff), on top of v2.2.8 "Aperture II" (2026-08-04, a presentation-fidelity release — gamma-correct scanlines + a WebGL2 gamma fix + a sharper scanline profile; presentation-only so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical, native default unchanged; visual verification pending), on top of v2.2.7 "Timbre II" (2026-08-04, an expansion-audio fidelity release — VRC6 recalibrated to ~1.0× a 2A03 pulse per the NESdev/field consensus [VRC6_MIX_SCALE 979→650; Mesen2's ~1.5× was the loud outlier], and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC; expansion-only, so the base 2A03 is byte-identical and AccuracyCoin holds 141/141), on top of v2.2.6 "Almanac" (2026-08-04, a de-monetization + provenance release — RustyNES is permanently open-source and income-free per ADR 0035; all planned monetization removed, native apps kept as free FOSS apps, and the TriCNES hybrid-address timing-calibration caveat disclosed per ADR 0030 for a v2.3.0 rework; zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction), on top of v2.2.5 "Colophon" (2026-08-03, a provenance/licensing/documentation-integrity release — zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction; NOTICE rewritten for full attribution + GPL-oracle disclosure + GeraNES, in-source "port" comments reworded to the oracle framing, the CRT-shader/NTSC provenance reworded to independent reimplementations, docs/originality-and-provenance.md added, README AI-assistance disclosure), on top of v2.2.4 "Cartridge" (2026-07-24, a libretro/RetroArch distribution cut — zero emulation-core changes so AccuracyCoin holds 141/141 by construction; the libretro core is confirmed up-to-date with all recent changes and builds for the buildbot ABIs [x86_64-pc-windows-gnu, aarch64-linux-android], and rustynes_libretro.info is corrected: disk_control false→true [the FDS Disk Control interface was wired but advertised absent], display_version v1.0.0→v2.2.4, mapper count 168→172; core options remain a documented future enhancement; the Antigravity reviewer standardization rides along), on top of v2.2.3 "Datum" (2026-07-23, a performance and accuracy-closure patch — the fast PPU dot path promoted to default and exposed, PGO binaries shipped on the release path, a same-runner relative frame-time CI gate, the last two Holy Mapperel residuals closed [MMC1 WRAM write-protect + FME-7 open bus, all 17 ROMs now detail=0000], the Sunsoft 5B level calibrated with Mapper::mix_audio widened to i32, a save-state schema gap fixed at PPU_SNAPSHOT_VERSION 8 + an APU v4 tail, an opt-in Zapper beam-relative light model, and the eleven sprintN.rs mapper modules renamed to mNNN_<board>.rs; two optimizations measured and REJECTED and documented as such; AccuracyCoin 141/141 — on top of v2.2.2 "Conduit" [2026-07-21, a build/distribution/CI-integrity patch — the libretro buildbot recipe taken from 1 of 10 jobs green to all ten building, a GitHub Actions supply-chain hardening pass, and the toolchain collapsed to one pinned source of truth with no nightly on any build path; zero emulation-core changes], itself on v2.2.1 [2026-07-15, a housekeeping patch: dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition], itself on v2.2.0 "Capstone" [2026-07-12], the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run — its two remaining marquees the netplay matchmaking / lobby stack and the FDS medium model, atop a peripherals + quality/security pass (Famicom $4016-bit-2 microphone + 3×3-aperture Zapper; cargo-fuzz targets 3 → 8 finding + fixing two Movie::deserialize OOM-DoS paths; a read-only Tools → ROM Info browser); every change additive or default-off, AccuracyCoin 141/141) on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. DualSystem dual-console support. The v2.0.x "Harbor" mobile-finalization train (v2.0.1→v2.0.9) and the entire v2.1.x "Fathom" line (v2.1.0→v2.1.10) plus the v2.2.0 "Capstone" milestone have all shipped — the run's steps being v2.1.5 "Vernier" (regression-net & residual) → v2.1.6 "Timbre" (expansion-audio fidelity) → v2.1.7 "Stepping" (opt-in PPU/2A03 die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier a documented no-op on every oracle, ADR 0033) → v2.1.8 "Tempo" (a default-OFF fast PPU dot path + SIMD blitter + wasm size pass) → v2.1.9 "Aperture" (a marquee CRT shader stack + raw NTSC composite signal-decode + GIF/WAV capture + palette editor) → v2.1.10 "Loom" (TAStudio greenzone + Lua API breadth + browser-RA auth-proxy deploy stack + Vs. DualSystem libretro presentation) → v2.2.0 "Capstone" (the milestone cut closing the run) → v2.2.1 (housekeeping) → v2.2.2 "Conduit" the build/distribution/CI-integrity patch — preceded by v1.10.0 "Arcade" the native Libretro / RetroArch core, the v1.9.0→v1.9.9 iOS TestFlight train, the v1.8.0→v1.8.9 "Android" train, and the desktop-feature lineage v1.1.0→v1.7.1, all on the v1.0.0 production core (see the top "Current release" block + docs/STATUS.md). Never claim any version later than v2.6.4 is released — the v2.2.6 → v2.3.0 line (de-monetization + NESdev remediation: audio [v2.2.7, shipped], video/gamma [v2.2.8, shipped], TAS/UX [v2.2.9, shipped], and the PPU left-edge + hybrid-address accuracy capstone at v2.3.0 "Datum II" [shipped]) is now complete. The freed v2.3.0 slot is repurposed as that accuracy capstone (NOT a store launch — RustyNES is now income-free per ADR 0035; any free mobile-app store listing is a later, unversioned step with no monetization — see to-dos/ROADMAP.md). Two distinct "v2.0"s exist and must not be conflated, both now shipped, at different times, for different reasons: the engine-lineage v2.0 master-clock work shipped as the v1.0.0 production core (2026-06-13) — it was the only scheduler through v1.10.0. RustyNES's own v2.0.0 "Timebase" release (2026-07-03) is a different milestone that replaces that same dot-lockstep scheduler outright: the one-clock + every-cycle-bus-access collapse (a single canonical cycle counter + a split-around-the-access start_cycle/end_cycle PPU catch-up, mirroring Mesen2's structure), full Vs. DualSystem dual-console emulation (core-and-harness-only; frontend wiring deferred), and the breaking save-state / cross-version changes it entailed (ADR 0002 / ADR 0028 / ADR 0029) — the one release that broke byte-identity / save-state compatibility, by design. The R1/R2 hard-tier MMC3 IRQ-timing residual was investigated under a bounded-effort campaign and is by-design-deferred beyond v2.0.0, not closed — see ADR 0002's decision-update section for the mechanism-level finding.

  • Forward plans + roadmap live in to-dos/. to-dos/ROADMAP.md (updated in #129) is the planning entry point and frames the release line + "the path to v2.0.0 and beyond"; to-dos/plans/ holds the per-release plan docs (through v1.7.0-forge-plan.md on main, plus the staged-forward v1.8.0-android-plan.md / v1.9.0-ios-plan.md / v2.0.0-master-clock-plan.md) + the to-dos/plans/engine-lineage/ history archive + a to-dos/plans/research/ reference-mining archive.

  • The v1.0.0 release + GitHub Pages/CI + post-release record is in docs/v1.0.0-synthesis-handoff-2026-06-13.md — read it before touching CI, Pages, or release tooling. Full per-release history is in CHANGELOG.md.

  • Markdownlint is a CI gate (pre-commit, pinned markdownlint-cli v0.49.1). The pin was v0.39.0 until the v2.6.3 dependency refresh, held because the newer local binary reported rules the pin lacked — chiefly MD060 (table-column-style), which was therefore NOT gated. That is now measured and resolved: MD060's inferred default reads this corpus as style compact and reports 1,936 findings across 122 files and nothing else, so .markdownlint.json pins MD060 to the style actually in use (leading_and_trailing), which measures zero and rewrites no document. It IS a gate now. Still verify with pre-commit run markdownlint --all-files rather than the bare binary — the pin and the local build can drift apart again. .markdownlint.json also keeps MD013/MD033/MD041 disabled by design (long technical tables, the README HTML banner/<img>, the HTML-led README). .markdownlintignore exempts ref-docs/, ref-proj/ (the reference-emulator clone, now removed from disk but kept in the ignore lists as a firewall guard so it can never re-enter the tree — see the MOST IMPORTANT RULE section above), the vendored tricnes/ + upstream READMEs, and the frozen docs/archive/ + to-dos/archive/ trees — don't lint or reformat those.

  • RetroAchievements client identity: the RA HTTP User-Agent (how RA authenticates/identifies/allowlists the client) is RustyNES/<crate version> rcheevos/<rcheevos version> — the RA_USER_AGENT const in crates/rustynes-cheevos/src/http.rs; the rcheevos version auto-syncs from the vendored rc_version.h via build.rs (RCHEEVOS_VERSION). Keep the leading RustyNES/ token (a regression test guards it).

  • Exhaustive Documentation Sweeps: When tasked with generating comprehensive project documentation or wikis, always recursively list and read the contents of docs/, ref-docs/, and to-dos/ to ensure no deep technical knowledge is missed.

  • GitHub Wiki Initialization: When assisting with GitHub Wiki deployments for the first time, instruct the user to click "Create the first page" in the GitHub UI to provision the .wiki.git repository. If the Wiki is cloned locally inside the main repository, ensure its folder (e.g., RustyNES.wiki/) is added to .gitignore.

  • Symlinked Agent Configs: Ensure symlinked agent files (like GEMINI.md -> AGENTS.md) are explicitly removed from .gitignore so they are correctly tracked by version control.

  • Libretro buildbot CI (issue #311) shipped in PR #312 (2026-07-19/20, b49dd1e0) — issue #311 stays OPEN by design. .gitlab-ci.yml + a [lib] name = "rustynes" naming-collision fix + [workspace] default-members + RA memory-maps + an FDS load-path fix/disk-control + native Game Genie cheats all shipped. TAS and Netplay needed no new libretro-side wiring — RetroArch's own rollback/movie machinery already rides the existing on_serialize/on_unserialize hooks. Companion upstream PRs libretro/docs#1164 and libretro/libretro-super#2021 are open but not yet merged by the libretro team; the remaining step (mirroring the repo onto git.libretro.com + enabling the buildbot) is entirely on their side. Never close #311 with a "Closes #311" commit-body keyword — it already auto-closed prematurely once this way and had to be reopened; only close it once the upstream team confirms the buildbot is live.

  • GENERAL RULE (learned twice, the hard way): never put a GitHub magic closing keyword — Closes #N / Fixes #N / Resolves #N — in a commit or PR body when the issue tracks ANY work the change does not finish. GitHub closes the issue the instant the PR merges, marks it stateReason: COMPLETED, and the tracker then reports finished work that was never done. This has now fired twice: #311 (PR #312), and again on #318, which auto-closed one second after PR #319 merged (02:42:15Z merge → 02:42:16Z close) even though #319's own body and review replies explicitly deferred two of its items. Write "addresses #N" / "partially addresses #N" in prose instead, and close the issue by hand only once its last item actually lands. When an issue looks completed, verify against the tree (git log, grep the code) rather than trusting the CLOSED/COMPLETED state — a keyword-closed issue is indistinguishable from a genuinely finished one in the UI.

    • It fired a THIRD time, in the subtlest way: even QUOTING the keyword triggers it. PR #321's commit body carefully said "Addresses #318" — but it also explained the earlier mistake with the sentence "#318 had auto-closed ... purely from a `Closes #318.` keyword in that commit body". GitHub's parser ignores backticks, quotation, and surrounding context: it saw Closes #318 and closed the issue on merge. Writing about the anti-pattern reproduced the anti-pattern. So the check is purely lexical — before pushing, grep the whole message for Closes|Fixes|Resolves followed by #, including inside code spans and narrative, and rewrite any such quotation (e.g. "a closing keyword" / Close&#8203;s #NNN) if the issue must stay open.
  • CI security hardening (PRs #319 + #320, 2026-07-21, merged 85ee20db / a69200ef). persist-credentials: false on all 19 actions/checkout sites (18 in #319; the last one, release-auto.yml, once its tag check stopped needing Git credentials — see the next bullet), because build scripts / proc macros / test binaries / Gradle scripts / MkDocs all execute unreviewed PR code that could read the token out of .git/config. Facts worth not re-deriving: .github/actions/rust-setup performs no checkout of its own (so call-site hardening is complete coverage); persist-credentials does not affect the gh CLI or API calls, only git network ops using the stored credential — which is why gh release create, softprops/action-gh-release, and fastlane match (a different repo, own MATCH_GIT_* secrets) all look like they need it and don't; and the highest-exposure job is web.yml's build, not any ci.yml job, because web.yml declares pages: write + id-token: write at workflow level. There are now no exceptions: release-auto.yml's prepare was the last holdout (it needed git ls-remote origin for the tag check), and that check is now a gh api call, so its checkout joined the sweep.

  • The release tag-existence check is FAIL-CLOSED — keep it that way. release-auto.yml's decide step queries gh api repos/$GITHUB_REPOSITORY/git/matching-refs/tags/<tag>, NOT git/ref/tags/<tag>: matching-refs answers "absent" with HTTP 200 + an empty array, so a genuine miss can never be confused with a lookup failure and no error-body parsing is needed. It matches by prefix, so the exact ref is compared in jq — this is load-bearing, not defensive: v2.2 prefix-matches two real tags (v2.2.0, v2.2.1) while exact-matching none. Under shell: bash + set -euo pipefail both a gh failure and a non-array body abort the job (verified: exit 1 and exit 5 respectively). The old git ls-remote ... >/dev/null 2>&1 read any non-zero exit as "tag absent", so a blip would try to re-release a shipped version. Never reintroduce a form where a failed lookup is indistinguishable from "absent." (Note when testing shell behavior locally: this harness's shell is zsh, whose set -e semantics for var="$(cmd)" differ from bash's — test with bash -c or you will get a false result.)

  • dtolnay/rust-toolchain is SHA-pinned in .github/actions/rust-setup — never put it back on a branch ref. It is @e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1; it used to be @master, a branch that advances on every upstream commit, so each run silently resolved to whatever HEAD was. That composite feeds 12 of the repo's 19 checkouts, including release.yml (contents: write, builds the shipped binaries) and web.yml (pages: write + id-token: write), and it is the action that installs the compiler — the hardest place to notice a compromise. The trailing # v1 is the form Dependabot's github-actions ecosystem (enabled in .github/dependabot.yml) reads to keep the pin current, so don't strip it. The other ~12 actions remain on @vN tags by choice: those are not expected to move, and blanket SHA-pinning is a maintenance-policy decision, not a drive-by fix.

  • pre-commit run --all-files REWRITES vendored/immutable trees — use --files <changed files> or a single named hook instead. trailing-whitespace / end-of-file-fixer / mixed-line-ending modify files and, before PR #320, had no exclude at all: one --all-files run silently reformatted 41 files across the vendored TriCNES C#, vendored rcheevos C, ref-docs/, an upstream font licence, and upstream test-ROM READMEs — destroying exactly the byte-identical-to-upstream property those trees exist for. .markdownlintignore covered them for markdownlint only. #320 added a shared exclude anchor across the three rewriting hooks, scoped deliberately narrower than .markdownlintignore: only content we did not author. Frozen-but-ours trees (docs/archive/, to-dos/plans/) stay in scope, since the invariant is "don't rewrite what we didn't write". If it happens anyway, revert only the unintended paths (never a blanket git checkout, and never including your own edits).

  • The libretro buildbot is a THIRD CI system with its own rules — and the pinned toolchain fights it. The first real run (pipeline #91899, 2026-07-20) passed 1 of 10 jobs. rust-toolchain.toml's channel = "1.96.0" makes rustup install a fresh toolchain inside libretro's build image, bypassing the image's pre-provisioned cross targets, so 8 jobs died on E0463: can't find crate for core; each job in .gitlab-ci.yml now runs rustup target add ${RUST_TARGET} (NOT added to rust-toolchain.toml's targets — that would cost every contributor and GH Actions job ~8 extra rust-std downloads). The Apple jobs must use !reference rather than extends for that, because GitLab's extends REPLACES array keys and would silently drop the templates' SDKROOT/STRIP/CC/CXX exports. tvOS: the upstream template's cargo +nightly build -Zbuild-std override is OBSOLETE — don't reinstate it. It dates from when aarch64-apple-tvos was tier 3 with no distributed rust-std; the target has since been promoted and rustup ships a complete prebuilt std including panic_abort (verified on the pinned 1.96.0: rustup target add aarch64-apple-tvos gives 26 rlibs and the crate cargo checks clean, bindgen included). Our job overrides script back to !reference [.libretro-rust-apple-base, script], putting tvOS on the same pinned stable as every other job. That one change dissolved THREE stacked workarounds the +nightly path had forced: a nightly-channel reinstall (+nightly outranks both rust-toolchain.toml and RUSTUP_TOOLCHAIN, so the job rode the image's stale 1.94.0-nightly, below our MSRV); CARGO_PROFILE_RELEASE_PANIC=unwind (bare -Zbuild-std omits panic_abort, and CARGO_UNSTABLE_BUILD_STD does NOT override the hardcoded crate list — the CLI -Z flag wins); and clearing the image's -C ar (see the next bullet). Worth reporting upstream: every Rust core's tvOS job could drop +nightly the same way. A green GitHub Actions run does not imply a green buildbot — the new libretro-cross CI job (one triple for each buildbot ABI family a Linux runner can model — MinGW-Windows and Android/NDK; the Apple families are deliberately excluded, as bindgen needs a real per-target sysroot and there is no Apple SDK on a Linux runner) is the early-warning gate; before touching anything libretro-related, cross-check cargo check --release -p rustynes-libretro --target <triple> locally.

  • The libretro build image injects -C ar into EVERY Apple job, and it is a hard error from Rust 1.97 — a bomb armed against the next MSRV bump. The image (not the rust-apple.yml template, which sets no RUSTFLAGS at all, and not our .cargo/config.toml) adds -Car=<path>,Clink-arg=-undefined,Clink-arg=dynamic_lookup,-rpath=<path> to osx-x64 / osx-arm64 / ios-arm64 / tvos-arm64. -C ar was a deprecated no-op for years and became a hard error in 1.97 (bisected locally: 1.93.0-nightly / 1.96.0 / 1.96.1 warn; 1.97.1 and 1.99.0-nightly error). No job trips it today — all four Apple jobs are on the pinned 1.96.0 and merely log the warning. The day rust-toolchain.toml moves to 1.97+, all four fail together — the warning lives in that file, at the line someone would edit. Discarding the flags is behaviour-preserving, not a gamble: rustc splits -C at the FIRST =, so the whole comma-joined string is swallowed as the ar value and those link args have never reached the linker for any core (cargo prints it as one argv token), and two upstream Rust cores have green tvOS jobs on the same image with the same dead token. The override works without knowing where the image sets it because cargo takes rustflags from exactly one source, first match wins: CARGO_ENCODED_RUSTFLAGSRUSTFLAGStarget.<triple>.rustflagsbuild.rustflags (verified locally against a global ~/.cargo/config.toml build.rustflags: RUSTFLAGS="" removes every injected -C, and empty means zero flags, not one empty argument).

  • There is ONE toolchain, rust-toolchain.toml's channel, and no version literal anywhere in .github/ — don't add one. .github/actions/rust-setup parses the channel out of that file and fails closed if it can't, so a toolchain bump is a one-line edit there. Pass the composite's toolchain: input only to install something deliberately different from the project pin. The resolver is table-scoped awk on purpose — do NOT "simplify" it back to a one-line sed. Matching the first channel = "..." anywhere in the file (the first implementation, caught in review on PR #322) resolves nightly if any other table carries a channel key ahead of [toolchain] — silently installing the very toolchain this setup exists to keep out, while the step still reports success. awk rather than tomllib because the step runs on Windows and macOS runners too and Python ≥3.11 is not a safe assumption there; only double-quoted TOML strings are accepted, and anything else (missing table, single-quoted value, empty file) aborts the job rather than being guessed at. The old stable default was misleading rather than wrong: rust-toolchain.toml is a directory override that outranks the rustup default the action performs, so every job was already compiling on 1.96.0 (rustup logs overridden by .../rust-toolchain.toml) — stable just downloaded a second toolchain nothing used and made the workflows read as though they tested latest stable, which they never did. Nightly is used in exactly one place, not a gate: cargo fuzz (hard requirement — libFuzzer's sanitizer flags are nightly-only). If you think a CI job needs nightly, it doesn't.

  • rust-libretro 0.3.2 is unmaintained (no commit since 2023-02) and has a MinGW bug we work around. It casts a keycode with cfg(target_family = "windows"), but C enum signedness follows the ABI: only MSVC gives plain enums int — under MinGW (x86_64-pc-windows-gnu, what the buildbot builds) bindgen emits c_uint and the crate fails E0308. .cargo/config.toml's [env] BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu = "--target=x86_64-pc-windows-msvc" fixes it; the generated-bindings diff is 28 lines, all enum signedness. Don't "clean up" that env var without rebuilding for x86_64-pc-windows-gnu.

  • CodeRabbit is configured as a 3rd PR review bot (.coderabbit.yaml, added 2026-07-20 in PR #316), alongside gemini-code-assist and copilot-pull-request-reviewer — the reply-and-resolve-every-thread ceremony applies to its threads too. It does NOT review automatically on this repository — see the next bullet. This one said "automated" and "applies before any merge" from #316 until v2.3.9, which read as a promise that its findings were arriving; they were not. Configured profile: assertive (not the "chill" default) and a tools{}/path_instructions/custom-checks set audited against this repo's actual file footprint, not guessed. tone_instructions has a hard 250-character schema limit that fails validation silently on the CodeRabbit side — after editing .coderabbit.yaml, verify with a @coderabbitai configuration PR comment and confirm every changed field shows Source: Repository YAML (base).

  • CodeRabbit does NOT auto-review this repository, and its check never resolves. Its own comment states the reason: "This repository does not receive automatic reviews because it has fewer than 10 stars." The configuration from #316 is loaded and correct (it reports Path: .coderabbit.yaml, Review profile: ASSERTIVE, Plan: Pro Plus) — only the automatic trigger is absent. Two consequences, both of which have been operated wrongly. First, the ceremony below silently covers two bots, not three. AGENTS.md credits CodeRabbit with catching a critical fast-forward defect (#358) and a use-after-free in the v2.3.5 libretro tables; none of that arrives unless a review is requested. Post @coderabbitai review on any PR whose diff warrants it. Second, waiting for zero pending checks never terminates: the CodeRabbit context sits with a null status and a null conclusion forever, so a healthy PR here reads as ~27 checks with exactly one permanently pending. The merge criterion is CI success = SUCCESS with every OTHER check complete — never "nothing pending", and never a bare check count either: a CONFLICTING PR also shows a short list, because GitHub cannot build a merge ref so CI never runs at all, and it is otherwise indistinguishable from a healthy PR whose jobs are still queuing. Gate on all three together — CI success green, every non-CodeRabbit check complete, and mergeable == MERGEABLE. Also note the check is not required and removing it is not the fix: main's ruleset requires exactly one context, CI success (verified via gh api repos/OWNER/REPO/rules/branches/main), so the pending CodeRabbit context blocks nothing — and suppressing its status reporting would also lose the check on the PRs where a review IS triggered, which is the one time it carries information. And CodeRabbit rate-limits manual triggers per developer, so a batch of @coderabbitai review comments can silently produce no reviews at all.

  • The bot-comment ceremony must read the review BODIES, not just the resolvable threads. CodeRabbit posts "Outside diff range" and other suppressed findings inside the review body, where they are invisible to a resolve-every-thread sweep — and Copilot does the same. This has now cost the project three times: issue #360 (an untested attestation path) reached main unaddressed; two findings of the same class on #357 were genuine defects, one critical (two threads producing frames during fast-forward under threaded display-sync, fixed in #358); and a use-after-free in the v2.3.5 libretro controller tables was caught only because the review body was read. A green "all threads resolved" is not evidence the review was addressed. Fetch the bodies explicitly — gh pr view <n> --json reviews --jq '.reviews[].body' — and triage every finding in them before merging.

  • lz4_flex 0.14+ requires the crate's own alloc feature explicitly for compress_prepend_size/decompress_size_prepended (used by rewind.rs/zwinder.rs) — it split real no_std support into an alloc-vs-std distinction that didn't exist in 0.13. A cargo build --workspace will NOT catch a missing alloc feature here because rustynes-core's own default-on std feature implies it via cargo's feature unification; only a standalone cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features (the exact CI no_std build job) will. Run that command locally before pushing any bump that touches this dependency.

  • The libretro .info RetroArch reads is a DIFFERENT FILE from this repo's, and it went stale for eleven days. RetroArch downloads dist/info/rustynes_libretro.info from libretro/libretro-super; crates/rustynes-libretro/rustynes_libretro.info is an unrelated copy that nothing syncs and nothing compared. So the v2.2.9 GPL relicense reached Cargo.toml, NOTICE, deny.toml, the SPDX headers and the local .info — and not the file users actually see, which went on advertising "MIT OR Apache-2.0" at display_version = v2.2.1. Both upstream PRs had merged exactly two weeks before the relicense, so no sync could have carried it. Upstream PRs are opened only on MINOR or MAJOR releases — a vX.Y.0 where X or Y changed. Patch releases do NOT trigger an upstream sync; the .info display_version is allowed to lag through a v2.4.1..v2.4.9 run and is brought current at the next vX.Y.0. Next scheduled upstream sync: DEFERRED past v2.5.0 — it now waits for the MiSTer core to be complete (maintainer decision, 2026-08-23, superseding the 2026-08-20 decision that named v2.5.0). The sync happens at whatever vX.Y.0 the MiSTer core is fully implemented at, not at the next vX.Y.0 on the calendar. So v2.5.0 opens no upstream PR, and neither does any release before the core is done. The reason is that the Fabric line's user-visible claims are about a core that does not exist yet, and an .info advertising a version whose headline feature is unfinished is the same class of stale declaration this bullet exists to prevent — just in the other direction. The one override is a licence change, which syncs immediately regardless of version — that is what this bullet's incident was about, and it stays on the same footing as a release. crates/rustynes-test-harness/tests/libretro_info_audit.rs pins the local file against the workspace manifest so the sync is a copy, never a re-derivation; it cannot see upstream, so the sync itself stays a human step. libretro .info uses short license tokens, not SPDX, and marks "or later" with a trailing + (tallied across all 316 upstream cores: GPLv2 x100, GPLv3 x64, GPLv2+ x19, GPLv3+ x5) — RustyNES is GPLv3+; a bare GPLv3 understates it as GPL-3.0-only. Full detail + the surface table: docs/libretro/UPSTREAM_SYNC.md.

  • iOS/iPadOS/tvOS availability is a THIRD repo and a HARDCODED list — being on the buildbot buys nothing there. RESOLVED 2026-08-16 by libretro/RetroArch#19416 (merged 76f60626984a; verified against master, not the PR state — rustynes sits at line 268 between reminiscence and sameboy). The mechanism below is retained because it recurs for any other core and for the sibling forges, and because "in the build list" is not "installable": it ships with the next App Store RetroArch build. iOS cannot download cores (Apple bans fetching executable code), so the App Store build bundles a fixed set chosen by pkg/apple/update-cores.sh in libretro/RetroArch. That script has two lists: allcores, fetched dynamically from the buildbot directory (RustyNES is in it automatically), and appstore_cores, a hardcoded array (RustyNES is absent). The iOS/tvOS build phases run rm -f ${SRCROOT}/<platform>/modules/*.dylib then ./update-cores.sh appstore, so only the hardcoded list survives. One entry covers iOS + tvOS + macOS App Store. Alphabetical order is mandatoryrustynes sorts between reminiscence and sameboy; re-check the neighbours at submission time rather than trusting a line number.

  • RetroArch retains SOME environment-callback pointers and copies others — the asymmetry is not documented in libretro.h, so check runloop.c. SET_CONTROLLER_INFO shallow-memcpys the outer retro_controller_info array but retains each entry's types pointer and dereferences it later when the Controls menu is built: the description arrays MUST be 'static (a stack local compiles cleanly and hands the frontend a use-after-free). SET_INPUT_DESCRIPTORS is different — RetroArch walks it during the call and retains only the description string pointers — so a stack array is fine there. Never generalize from one to the other; read the handler.

  • A fix that touches ONE call site of a shared code path may not fix the bug — and will report that it did. v2.3.6 hit this squarely. Review reported that a latency measurement destroyed the user's rewind history; the fix changed measure_in_place's FINAL restore to restore_quiet and stopped. Every trial still went through Probe::run_uncounted's loud nes.restore(..), and a measurement runs up to 21 trials against the live emulator, so the ring was still being cleared twenty-one times over behind a fix that closed the thread. Before declaring a fix complete, grep for every caller of the mechanism, not just the one the report named. The corollary is about tests: my test for that fix would have asserted "ring not empty", which passes while a second defect remains — the ring in fact GREW, polluted with replayed frames that never happened on the user's timeline. Assert the state comes back EXACTLY as it was; a weaker assertion is how an incomplete fix clears review.

  • Prose asserting an intent is how a defect survives releases. Pixel Provenance shipped broken for four releases with a comment two lines above the offending code claiming the clear "leaves exactly the visible frame's writes — which is the timeline the user is looking at", plus four matching doc claims. Nobody checked because the text said it was fine. When correcting this class, KEEP the wrong text quoted alongside the measurement that disproves it — deleting it loses the evidence that the prose was the mechanism.

  • ab_check.sh benchmarks the reference immediately after building it, and on this workspace that is a ~45-second fat-LTO compile across all cores. So the reference measures on a hot, frequency-throttled machine while the candidate runs once thermals have settled. v2.3.6 D1's run 1 read -3.81% at p = 0.00 on a shipped default workload and was entirely artifact: the order-bias control, benching the reference against ITSELF, drifted -3.73% on that same workload with no code change. Read the order-bias control before the candidate column. A cached reference build plus AB_MEASUREMENT_TIME=25 took the drift from ~4% to ~1%.

  • "Inert on almost every cycle" predicts an optimization win only if the work is actually EXECUTED. Under lto = "fat" with codegen-units = 1 the guarded code is already inlined into its caller, its repeated loads already merged by common-subexpression elimination, and always-not-taken branches are perfectly predicted — so swapping predictable not-taken branches for an equivalent count of loads plus a predicate is arithmetically a wash. This is why APU Workstream D produced three nulls (D1, D3, D6) and why D2 and D4 are left unmeasured: their prior is a null, not an unknown. Full numbers and the three conditions that would justify reopening: docs/performance.md.

  • The Antigravity reviewer USED to delete and replace its comment each round, destroying unread findings. Fixed in v2.4.0; the fix is INERT until it reaches each repo's main. Observed on PR #428: round 1 posted at 13:37:57Z and round 2 at 14:21:35Z, after which gh api repos/O/R/issues/N/comments returned exactly ONE bot comment with created_at == updated_at == 14:21:35Z. The first was gone — not edited (the timestamps would differ), not appended to. CodeRabbit and Copilot threads persist and can be resolved, so an unread finding stays visible; an Antigravity finding did not, and nothing on the PR indicated a round had happened. Both destroyed rounds on #428 were blocking and correct, one a data-loss defect, so the cost was not hypothetical. scripts/agy-review.sh now edits one comment per PR, folding earlier rounds into a collapsed <details> archive bounded by MAX_BODY_BYTES (oldest dropped first, and the count dropped is announced), and issues no DELETE at all — scripts/agy-review-selftest.sh asserts the absence of a DELETE, and the body format lives in the sourceable scripts/_agy_comment_body.sh so the selftest exercises the real implementation rather than a copy (its first version inlined the pipeline and a mutation came back NOT CAUGHT). Rounds are delimited by an HTML-comment sentinel, not by the <details> tag: a review body legitimately contains <details> blocks, and matching the tag cut inside a round — found in review, one commit after adopting markers for exactly that reason elsewhere in the same file. The workflow runs the script from a DEFAULT-BRANCH checkout, so the fix does nothing on any PR until it merges to main there — and note the corollary that bit once: the workflow YAML itself comes from the PR branch, so a change adding a script file breaks its own PR unless the workflow half tolerates both script sets. The four sibling repos (Rusty2600, RustyN64, RustySNES, SLAC) keep the old destructive behaviour until their own PRs land. Until then: read the comment before every push, and quote its findings into your reply, since the reply persists and the original may not.

  • The bot ceremony has a THIRD hiding place: plain issue comments. AGENTS.md already warned that CodeRabbit and Copilot post suppressed findings in review BODIES, invisible to a resolve-every-thread sweep. The Antigravity reviewer posts its entire review as an ordinary PR comment — not a review, not a thread — so gh pr view --json reviews misses it too. On PR #385 its blocking finding (an emoji in code, against a hard project rule) was caught only because the comment list was read. Fetch all three: reviewThreads, reviews[].body, AND comments[]. Also note Antigravity re-reviews on every push, so a green build plus zero unresolved threads can still sit under an unread blocking finding — check the newest comment's timestamp against your last push.

  • Panel state that outlives the Nes it describes is a recurring seam here. Three instances now: the Pixel Provenance panel edge-detected its arm against a mirror of core state that a fresh Nes had reset, so it never re-armed after a ROM load; the Latency Oracle kept a stale report AND its live Apply button across a ROM change (a depth measured on game A, one click from being applied to game B); and the RAM Atlas would have kept 2,048 labels that look like a map. There is now ONE hook — DebuggerOverlay::clear_rom_bound_analysis, called from all three ROM-transition sites in app.rs beside clear_tas_editor. Add new ROM-bound panels to it rather than adding a fourth per-panel clear.

  • The libretro wrapper is where the bugs live, not the core. A v2.3.5 audit found five defects and every one was a wrapper defect with correct emulation behind it: hardcoded 60.0988 fps for every cartridge (PAL ran 20.2% fast), retro_get_region unimplemented, retro_reset unimplemented (RetroArch's Reset did nothing, ever — the library default is a literal no-op), retro_unload_game unimplemented (Game Genie indices leaked across cartridges), aspect_ratio = 0.0 (square pixels; the desktop frontend applies 8:7), and no controller info (the Zapper was unreachable despite Nes::set_zapper being fully implemented). When something looks wrong in RetroArch, suspect what the wrapper advertises before suspecting the emulation. Prefer DERIVING declared values from rustynes_core constants (FRAME_DURATION_*, DEFAULT_SAMPLE_RATE) over transcribing them — the 60.0988 literal had lost all connection to the constant it was copied from.

  • --workspace --all-targets clippy covers DEFAULT features only — #[cfg(feature)] modules in the chip crates have never been linted. cpu_boot_trace.rs and irq_trace.rs sit behind cpu-boot-trace / irq-timing-trace, and no .github/workflows/ invocation enables either. Adding rustynes-cosim (which enables both mandatorily) surfaced six pre-existing -D warnings errors that had been sitting there. The clippy matrix in this file enumerates frontend feature combos exhaustively and core feature combos not at all, so treat any cfg-gated module in rustynes-{cpu,ppu,apu,mappers,core} as presumed-unlinted until checked. Same shape as the wasm32 gap: a gate that exists but does not reach the code in question.

  • Re-run mutations after a STIMULUS change, not only after a code change. v2.5.5 had one mutation — deleting the nametable toggle from inc_x — come back NOT CAUGHT three times, against a gate that was working correctly every time: horizontal arrangement made $2000 and $2400 alias the same kilobyte; a zero coarse-X scroll meant the only wrap happened at dot 256 and copy_x undid it at 257 before it reached a pixel; and the fill wrote one ascending 256-period ramp across two 1,024-byte nametables, so both came out byte-identical. Each fix looked like it had closed the hole, and only the next mutation run showed it had not. A NOT CAUGHT has now meant four different things in this project: the gate is blind, the stimulus is blind, the mutation was a no-op, or the mutant never built.

  • A fix that improves a symptom without curing it is evidence about the mechanism. v2.5.5's frame was shifted right by exactly one pixel. Moving the shift window alone took the first wrong pixel from x=9 to x=17 — one tile further in — and that partial improvement is what proved the reload was out of phase too, rather than the shift being wrong twice. The resolution then removed a register instead of adding a knob: the reload is the pattern-high fetch's own dot, so it takes chr_din directly and bg_hi_latch no longer exists. When a change helps but does not close, read the residual before reaching for the next constant.

  • A gate keyed to the CPU CYCLE cannot see a defect in when something happens INSIDE that cycle. v2.5.4's fetch gate found the co-simulation testbench presenting every CPU access on the second of the cycle's three PPU dots; a 6502 commits a write and samples a read at phi2, the third. Enabling rendering through $2001 therefore took effect two dots early, and so did disabling it — the DUT issued an extra nametable fetch at each rendering window's leading edge and dropped one at its trailing edge, one quantity wrong by one constant at both edges. Five gates stayed green across the move, in both directions: rung 1's registers on nine ROMs, rung 2's per-cycle bus, the interrupt sweep, and the v2.5.2 register and v2.5.3 scroll gates. That is not evidence the shift was harmless — every one of them reads state once per CPU cycle, so a uniform sub-cycle shift moves nothing they compare. When a defect is invisible to every existing gate, ask what RESOLUTION those gates sample at before concluding it does not matter. And note the corollary that made this findable at all: the fix is credible because the earlier rejected attempt (moving the access and shifting power-on together, two knobs for one quantity) stayed rejected — only the access moved.

  • A mutation that comes back NOT CAUGHT may be indicting the STIMULUS, not the gate. Two of v2.5.4's first eight mutations were no-ops because the ROM rendered from $2000 with PPUCTRL = 0, holding v[11] and ctrl[4] at zero for its whole duration. The gate was working correctly; the ROM never set the bits. Before weakening or excusing a gate, check whether the stimulus REACHES the mutated path — same shape as v2.5.1's BRK passing 186/186 while the interrupt return address was wrong. Extending the ROM (a second rendering window at $2800 with patterns at $1000) took the fetch count 3,099 → 6,247 and both mutations to CAUGHT.

  • The four trace features had never been linted, and now are — one CI step each. No workflow named cpu-boot-trace, irq-timing-trace, ppu-state-trace or ppu-fetch-trace; --workspace --all-targets reaches default features only, and the rustynes-cosim clippy step lints that package while compiling rustynes-core/rustynes-ppu as dependencies, where warnings are not denied. ppu-state-trace held six -D warnings errors, one of them a legitimate dead-code case (that feature compiles out the v2.2.3 fast dot path so the hook sees every dot, orphaning tick_visible_render_fast under that feature only — live by default, dead under one flag, the one shape that earns a scoped allow). Add a step when adding a feature; a cfg-gated module in a chip crate is presumed unlinted until a step names it.

  • The first Nes::run_frame() after construction advances ZERO cycles. run_frame opens with while !self.bus.take_frame_complete(), and the PPU is constructed at dot 340 of the pre-render line — so the 7-cycle reset sequence ticks past the frame wrap and leaves frame_complete set. The first call consumes that latch and returns having stepped nothing (measured: frame 0 = +0 cycles, frames 1..3 = ~29,780 each). Every existing caller runs thousands of frames so it is invisible to them; it stops being invisible the moment a count is recorded beside the output, where for _ in 0..n yields an (n−1)-frame result under a label claiming n. Gate on Nes::frame(), never the call count — and add a jam bail-out, or a JAM/KIL opcode spins the loop forever. Oracle::advance_frames in crates/rustynes-cosim is the reference implementation; the quirk is pinned by a test that names it, so a future core change removing it fails loudly rather than silently altering every golden's length.

  • The MiSTer / SuperStation One programme is a co-simulation ORACLE role, not a port (ADR 0037; docs/mister.md; to-dos/plans/v2.5.0-fabric-plan.md). RustyNES cannot become a bitstream — a MiSTer core is SystemVerilog compiled by Quartus 17.0.2 into a Cyclone V bitstream — so "Fabric" writes a new core from public documentation in a sibling RustyNES_MiSTer repo and verifies it against this emulator. Facts worth not re-deriving: v2.5.0 is scoped to "the 6502 rung closes" (7–13 months FTE for a full core against a 2–4 week window, so PPU/APU/MiSTer are v2.6–v2.9); the design is replay, not lockstep (Nes has no per-cycle step, and the determinism contract makes a pre-recorded trace exactly the lockstep trace); no DPI-C, because it puts `ifdef SIMULATION guards into RTL that must also pass Quartus; hash first, capture on divergence (4200 frames ≈ 7.5 GB of per-cycle CSV versus ~480 KB of 4096-cycle checkpoints — the plan's 244 KB assumed an 8-byte record, and ENCODED_LEN is 16: cycle AND hash); index_framebuffer pre-palette is the PPU gate so a palette difference cannot masquerade as a rendering difference; and ppu-state-trace plus mixed f32 audio are DIAGNOSTIC, never gates — they encode RustyNES's modelling choices, not hardware facts. Two risks are accepted in writing: NES_MiSTer scores 121/125 on AccuracyCoin and real Famicom AV hardware also scores ~121/125, so there is no published accuracy headroom and the core may be declined as a duplicate (Retro Remake is the planned fallback home, not a contingency); and the oracle can be wrong, since 141/141 is not "matches silicon", so every rung is labelled by whether it has an independent oracle. The sys/ licence audit is DONE (v2.4.3) and it INVERTED the hedge. The plan feared a GPL-2.0-only file would force the RTL down to GPL-2.0-or-later; across 57 files there are zero, and four are GPL-3.0-or-later (ddr_svc.sv, hps_io.sv, scandoubler.v, sd_card.sv). hps_io.sv is not optional — it is how a core receives a ROM from the HPS and how the OSD reaches it — so the combined bitstream must be GPL-3.0-or-later, which is already RustyNES's licence. No relicensing needed. The Quartus 17.0.2 subset is likewise FITTED, not documented (v2.4.3): a kitchen-sink module reached a placed-and-routed netlist on a 5CSEBA6U23I7 with 0 errors and 0 synthesis warnings, and its 2 KiB array inferred as 2 M10K blocks with 29 total registers — not 16,413 — from the source style alone, no ramstyle attribute; the initial block produced a real MIF and the enum was one-hot encoded. Nine constructs are fitted; plain case, priority case and $bits stay documented because the module does not exercise them, and extending the subset means extending that module and re-fitting, never adding a row on the strength of documentation. Quartus 17.0.2 specifically: Lite 25.1 still supports Cyclone V, so device support is not the constraint — Template_MiSTer pins v17.0.x in writing, sys/ carries Platform Designer IP that a newer Quartus compiles only after a one-way IP upgrade, and a newer Quartus is the more permissive tool, so a pass under it would report success for a property never tested. Its installer tarball is a two-stage bundle whose own setup.sh runs only the 17.0.0 base, so a naive install silently lands on 17.0.0 — assert the exact version, never a 17.0* pattern. Rung 1 has three opcode groups closed (v2.4.4 reset + implied, v2.4.5 addressing modes + loads/stores + branches, v2.4.6 indexed + ADC/SBC + compares): 573 records across three ROMs, seven mutations caught at each of the last two steps, and the entire rung has needed no oracle-side change because the DUT is the third writer of CpuBootTrace and --skip-fields already existed. Detail: docs/mister.md and RustyNES_MiSTer/docs/rung1-6502.md. Three tests read correctly and verified nothing, each found by mutation rather than by reading — a TXS whose wrong answer coincided with TSX's leftover flags, a store/load pair in the SAME addressing mode (self-consistent under any address mutation), and a read of RAM the program had not written (the oracle seeds work RAM; a flat-memory testbench zeroes it). The oracle also corrected our own spec: docs/cpu-6502.md said reset was 7 cycles in one section and 8 in another, and an independent implementation written FROM that document implemented seven and diverged on its first record — it is eight. The accuracy gate is NOT automated: the sibling's cpu-smoke step says in its own name that it is not the gate, because the goldens are not vendored there; make -C tb cpu-gate GOLDEN=… is the real comparison and golden fetching from a pinned commit is not built. Also: misterfpga.org returns HTTP 403 to automated fetching, so its Development-forum threads need reading by hand, and a DE10-Nano requires the SDRAM add-on for any NES core (the NES reads cartridge ROM directly; the onboard DDR3 is too slow) while the SuperStation One has 128 MB integrated.

  • A test ROM's own source is the SPECIFICATION, and checking whether it is reachable costs one command. Three AccuracyCoin entries resisted a full working session of tracing and hypothesis in v2.6.4. AccuracyCoin is MIT-licensed and its assembly source is one curl away; it carries a prose explanation of every assertion, written by the author who chose the stimulus, and it settled all three in minutes — naming a rule the nesdev pages do not state at all ($4015 reads are internal to the 2A03, so the data bus is not driven), giving the exact failing stimulus (LDX #$16 / LDA $40FF,X, which matched a trace divergence found independently), and distinguishing the neighbouring assertions a broad "fix" would break. The v2.6.4 plan's own note that the source "is not vendored" is true of this repository and had been read as unavailable; they are not the same statement. A test ROM is stimulus, not a reference implementation, so reading it raises no firewall question — check its licence, then read it before theorising about its verdict. The same holds for blargg's readme.txt, which states two APU rules the wiki does not. And decode its failure codes from the macro, never by inference: TEST_Fail reports (ErrorCode << 2) | 2 and the runner sets ErrorCode to 1 before every routine, so Fail(N) names test N one-based — read as a zero-based index it is off by one, and in v2.6.4 that made a regression (test 7 -> test 5) read as progress, a description that reached a code comment before the macro was read. It also retires v2.6.3's reading that six entries "sharing one failure code" implied one shared cause: the code indexes within one routine, so two entries sharing it share nothing.

  • A pass count is a claim about what RAN; measure that separately. accuracycoin_status reported "IDENTICAL entry for entry across all 146 entries" while 58 of those entries were NotRun on BOTH sides — the 600-frame window reaches the CPU catalog and stops partway through CPU Interrupts, asking nothing about the APU, PPU, sprite-evaluation or PPU-misc suites, i.e. the chips rungs 3 and 4 exist for. The comparator was correct and the stimulus window was short. 4500 frames executes all 146 (134,012,761 cycles) and is now the golden. Three standing rules fall out. First, break a result down by group and look for a group at zero — a whole subsystem missing is far easier to see than 58 rows scattered through a table. Second, write acceptance criteria a vacuous result cannot satisfy: this one said "including Skipped and NotRun" so a DUT could not pass by skipping, and needed one more clause — and no entry is NotRun on both sides. Third, coverage work is not bookkeeping — the first run of the widened window found a real RTL defect at cycle 20,636,325, 2.8 M cycles past where every previous run had stopped.

  • Comparing two consoles at a fixed cycle count assumes both reach the same PLACE, and that stops holding exactly when they disagree. At 17.9 M cycles the DUT and oracle agreed on 88 AccuracyCoin entries; at 134 M the oracle had all 146 and the DUT had five. Nothing was wrong with either dump. Measure the reference over the same axis or the subject's numbers mean nothing: the oracle climbs 88 -> 95 -> 117 -> 120 -> 146 while the DUT goes 88 -> 5 -> 5 -> 5, and only that pairing makes the shape the DUT's rather than the ROM's or the instrument's. Two points support any story — from 88-then-5 alone the conclusion was "completed a pass and restarted", and three more run lengths showed a flat line, which is a different defect. Then: catalog order is a plausible suspect, not evidence. The suite after the last one the DUT completed had never executed, so "it hangs there" was published in a commit body, a CHANGELOG, release notes and a PR. One PC probe refuted it — a three-cycle self-loop at $80DF, which disassembles to INC $EC / JMP $80DF, AccuracyCoin's menu idle loop, meaning the DUT had reset and returned to the menu. Reset and hang are different defects with different searches. When the question is "where did it stop", the answer is the program counter.

  • cargo test prints error: when a TEST FAILS, not only when the build breaks. A mutation classifier keyed on ^error reported BUILD-FAILED for three mutations that were all CAUGHT — the inverse of the usual trap, discarding evidence rather than manufacturing it. Classify from the runner's own vocabulary in order: could not compile/error[E -> BUILD-FAILED, test result: FAILED -> CAUGHT, test result: ok -> NOT CAUGHT, anything else -> investigate. And run the baseline through the same classifier: a baseline that does not land on the "tests pass" branch means the classifier is broken before any mutant has run.

  • An abort path does not flush the trace file. The co-simulation harness's cross-checks return 3, and --bus-out is written at exit, so a divergence they catch cannot be examined from the trace afterwards. The remedy is an env-gated stderr probe printing the relevant state per cycle around a target cycle — NMI_PROBE_CYC, and DMA_PROBE_CYC added in v2.6.4, which localised a defect in one run with four lines where a trace diff could not have shown it at all.

  • cargo test <filter> that matches nothing prints 0 passed and exits 0 — that is NOT a pass. Bit twice in one session: cargo test --workspace --features test-roms accuracycoin and cargo test -p rustynes-cpu nestest both matched zero tests and looked green. The real invocations are cargo test -p rustynes-test-harness --features test-roms --test accuracycoin (prints pass rate = 100.00% over 141 assigned tests; the RAM decoder is authoritative — the framebuffer decoder reports 120 and is known-buggy) and --test nestest. Always confirm a non-zero test count before reporting a gate as green.

  • A SKIPPED job satisfies CI success, and CI success is main's only required check. The ruleset Protect (Default) requires exactly one status context. That job runs if: always() and fails on contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')skipped is in neither list. So a gate that did not run reports a pass for a property it never tested. This is not theoretical: test-roms (the accuracy battery) was FULL-run only, so an accuracy regression could not be caught on the PR that caused it — it landed, turned main red, and needed a second PR (#396 introduced the vector move, #403 fixed it; both PRs were correct, the process was not). There is no merge queue — verified, no merge_group event appears in the run history — so nothing caught it later either. v2.3.9 A5 adds an accuracy paths-filter (chip crates, rustynes-core, rustynes-gamedb, rustynes-test-harness, tests/) so the battery runs on PRs that can actually break it; measured first, 11 of the last 40 merged PRs touch those paths, so ~72% still skip it. When judging whether a gate covers something, check three things separately: which contexts are required, what the aggregate treats as failure, and whether the job that tests the property actually ran.

  • Every SAME-TIMELINE restore must hand-carry the state that lives outside the save state. Nes::restore_inner clears the pixel- and audio-provenance stores — right for a real timeline change, wrong for a restore that puts back the state the user is still looking at. v2.3.6 wired take_provenance/put_provenance into RunAhead::finish, the one call site the report named; rustynes-probe had three more (Probe::run_uncounted per trial — a latency measurement runs up to 21 — latency::measure_in_place's final restore, and the atlas panel's TimelineGuard), so the Latency Oracle and RAM Atlas silently emptied both provenance panels. Both stores are cumulative, so nothing rebuilt them. crates/rustynes-test-harness/tests/snapshot_schema_audit.rs enumerates the whole excluded set and is the place to look — but its reasons are free text, and a keyword pass over them mis-sorted 17 of 26, so the durability question is not machine-checkable. Assert a caller's state returns byte-identical, never merely "armed": a store emptied while left armed passes an is_some() check (caught in review on #405).

  • timeout-minutes is NOT valid on a job that uses uses: — and actionlint is how you settle questions like that. GitHub's workflow-syntax and reuse-workflows pages state it neither way, and a #406 review asserted the restriction was lifted in 2022. It was not: actionlint reports the key unavailable and lists the seven allowed (name, uses, with, secrets, needs, if, permissions), so adding one is a hard syntax error, not an ignored key. release-auto.yml's build therefore cannot carry a timeout; its budget lives on the jobs inside release.yml. actionlint is installed and, from v2.3.9, a pinned pre-commit hook. .github/actionlint.yaml has existed since v2.2.3 (it declares the self-hosted agy label) — extend it, never Write over it; doing so lost its rationale and produced a false "adding the config activated a dormant check" finding that reached a commit body before being retracted.

  • Bound every workflow job, and every network fetch inside one. PR #400 bounded ci.yml and nothing else; v2.3.9 found six more unbounded workflows including release-auto.yml itself, after Clippy Security Lints hung two hours in a setup step and blocked the v2.3.7 release PR. Separately, apt provisioning hung four times across two PRs in one day, always in a setup/provisioning step and never in a compile or test step. A job timeout bounds the damage but cannot notice: a stalled fetch inside a 25-minute budget is indistinguishable from a slow job, and the run reports as cancelled, which reads as noise. .github/scripts/apt-install-retry.sh adds a per-command timeout plus three attempts, and warns on every attempt including ones that succeed — a run needing three and one needing one are identical in the conclusion, and that difference is the early warning.

  • Summing two percentiles is as invalid as differencing them. docs/performance.md records the subtraction case (a published table whose work p95 sat below its work p50). The addition case bit the v2.3.9 Latency Oracle design: an end-to-end figure needs render_work + render_lock (+ render_wait), and PerfView exposes those as three separate series, so the design was not implementable from existing data — found by trying to write it. The one valid case is adding a constant: internal lag is frames * frame_ms, so lag + render_work.p95 genuinely is a p95. That rescues exactly one series, which is why PerfPanelState::render_work deliberately exposes only that one. A true wall-clock figure needs a new single per-redraw series on RenderPerf.

  • grep -i on a short token matches more than you mean. Reading the AccuracyCoin result with grep -iE "RAM.*pass rate" matched the framebuffer line, because -i makes RAM match "f-ram-ebuffer" — and the framebuffer decoder is the known-buggy one reporting 120. The authoritative line is AccuracyCoin (RAM): pass rate = 100.00% over 141 assigned tests; match it case-sensitively, e.g. grep -E "AccuracyCoin \((RAM|framebuffer)\)" and read both. Same session, the same class of mistake produced two false negatives from patterns that could not match (full \*\*2x2 against **full 2x2). A pattern that cannot match looks exactly like content that is not there.

  • gh api repos/OWNER/REPO/issues/N RETURNS PULL REQUESTS, because GitHub's issues endpoint serves both. v2.4.0 item A nearly opened a duplicate upstream PR because of it: a pass ran exactly that against libretro/docs#1180, saw #1180 open — Correct the RustyNES core license, concluded "an open issue, so the fix still needs filing", and wrote that into a plan, a commit body and a user-facing summary. #1180 was a pull request filed 2026-08-16, MERGEABLE / CLEAN, +1/-1, awaiting review. When the question is "is this change already proposed?", the query is gh pr view N --repo O/R or gh pr list --author <you> --state all; the issues endpoint cannot distinguish them and its pull_request field is easy to miss.

  • Never write a commit body with git commit -m in this harness. The shell is zsh, so backticks are command substitution and <word> is an input redirect. A v2.4.0 message documenting three gh invocations lost all three to substitution and emitted no such file or directory: owner from a literal repos/<owner>/<repo>/... — the commit succeeded with a mangled body reading "recorded, because it is reusable: RETURNS THE PULL REQUEST". This project's house style puts command examples in commit bodies routinely, so -m is structurally the wrong tool: write the message to a file and use git commit -F. After amending, grep the message for each phrase that was supposed to survive.

  • A test that reimplements its subject is testing itself, and it will agree forever. v2.4.0 hit this in a test written for a review finding: it declared a local fn strip(t) { t.trim_start_matches([' ', '*']) } and asserted against that, so deleting the production stripping came back NOT CAUGHT. Only the mutation pass could see it — the test passed, read correctly, and covered nothing. The fix is the one this release needed three times over: extract the decision into a named item both the code and the test call. The other two were atomic_write's injectable rename predicate (with it hard-wired, the exhaustion branch is unreachable on Unix and a mutation returning Ok(()) for a save that never happened went uncaught) and TimelineWatch (DebuggerOverlay::new needs a window and a wgpu device, so nothing living only inside it is unit-testable). In all three the code read as testable beforehand.

  • Never byte-slice in a panic or format path. &text[at..(at + 24).min(text.len())] panics when the offset lands inside a multi-byte character, and these documents are full of em-dashes and arrows — so the audit would crash while formatting the diagnostic, replacing the message explaining the real failure with a char-boundary error about the reporting code. A diagnostic that can crash the diagnosis is worse than none, because the failure it exists to explain becomes harder to read than if the excerpt were omitted. Use s.chars().take(n).collect::<String>().

  • Two separately-verified facts do not verify the claim you assemble from them — and AGENTS.md is a notes file, not an oracle. v2.4.1's published release notes carried a section headed "a claim v2.3.9 made that v2.3.9 did not ship", asserting that v2.3.9's release body described release_anchor_audit.rs while the tag did not contain it. False, and it shipped to users. Both inputs were real and each was checked: the file genuinely is absent from the tag (git cat-file -e v2.3.9:crates/rustynes-test-harness/tests/release_anchor_audit.rs fails), and v2.3.9 genuinely is where the anchor drift was found. The conjunction was never checked, and it was one command awaygh release view v2.3.9 --json body. The body says the eight drifted documents "All now read v2.3.9", a MANUAL re-synchronisation which IS in its tag, and mentions no audit, no gate, no "15 anchors" anywhere. A hand fix followed by a later commit adding the mechanism is ordinary sequencing, not an overclaiming release note. Two propagation mechanisms matter more than the instance. First, the sentence was written into AGENTS.md during the v2.4.0 work and the v2.4.1 summary was then drafted FROM AGENTS.md — so an unverified claim entering this file is laundered into a verified one by the next release that quotes it; treat every factual assertion here as re-checkable, and re-check it before it reaches a user-facing surface. Second, the claim had the shape this project rewards — a recursive irony, "the same failure one level up" — and that shape is exactly what suppresses the check, because it reads as an insight rather than as an assertion needing evidence. The bar: before writing that a document, release, commit or person SAYS something, open it and grep for the words. Corrected in #431, and retracted in place rather than deleted, because the claim was an accusation against a shipped release and silently removing it would erase the record that it was made.

  • Verify a reviewer's claim before writing the fix, especially when their other findings were right. On #427 a reviewer stated starts_with("[workspace.package]") also matches [workspace.package.metadata]. Plausible, a real class of bug, and the fix plus a commit body describing "the regression I introduced" were written before it was tested. It is false: the literal ends with ] and the sub-table has . there, so the match is false; injecting such a sub-table and running the audit reads 2.3.9 under both forms. The same reviewer's two other findings that pass were both correct — which is exactly what makes the third easy to wave through. Adopt the change if it is better anyway (it was), but write down what is true, not a fix for a bug that never existed.

  • The libretro "let-chains are unstable" review claim is FALSE and has now been raised seven times. if let Some(x) = e && cond is stable in edition 2024, which this workspace uses on a pinned stable 1.96.0; the identical construct has been in libretro_info_audit.rs on main since v2.3.5; and CI's fmt + clippy + rustdoc job has compiled it green at five distinct SHAs. Do not "fix" it. Refute with the edition, the existing site, and the green lint job on the current SHA rather than by reference to earlier ones.

  • The workspace cannot carry a SemVer pre-release version. Setting [workspace.package] version = "2.3.9-rc.1" fails before any test runs: failed to select a version for the requirement rustynes-apu = "^2.0.0" / candidate versions found which didn't match: 2.3.9-rc.1. A caret requirement does not match a pre-release, so every intra-workspace dependency would have to be rewritten first. Relevant when reasoning about version parsing — release_anchor_audit.rs guards the case anyway, and its version_core is tested directly because the manifest route is unreachable.

  • release_anchor_audit.rs pins 15 release anchors across 10 documents against [workspace.package] version (README badge + Current Release, docs/STATUS.md, both AGENTS.md anchors + the never-claim-later guard, VERSION-PLAN.md header and its (current) table row, to-dos/ROADMAP.md, SUPPORT.md, SECURITY.md, root ROADMAP.md, OVERVIEW.md, ARCHITECTURE.md). It fails closed — a marker matching nothing panics rather than silently checking nothing — and it also asserts the CHANGELOG header parses the way release-auto.yml parses it (the - YYYY-MM-DD - prefix, a quoted codename, then a parenthesised theme), since a malformed header degrades the published release title. Rewording an anchor means updating ANCHORS in the same change; the test will tell you, by name.

<<< MC-PROJECT-END >>>