Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 13 additions & 45 deletions .github/release-notes/v2.3.1.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
RustyNES **v2.3.1 "Plumb Line"** is a measurement release. It makes the
performance apparatus trustworthy and then uses it — and what it found is that
**none of the ten hot-path candidates it measured yielded a shippable
improvement.** That is a claim about those ten, not about the core as a whole:
two core leads the campaign surfaced (the APU at 18.7% of frame, and `range.rs`
inlined inside `Ppu::tick` at 1.52%) remain **unmeasured** and are carried
forward.

**No emulation-core changes.** AccuracyCoin holds at **exactly 141/141** and
nestest is 0-diff, verified after every experimental probe was reverted rather
than merely asserted by construction: this release did land and remove real
edits.
RustyNES **v2.3.1 "Plumb Line"** is a measurement release. It makes the performance apparatus trustworthy and then uses it — and what it found is that **none of the ten hot-path candidates it measured yielded a shippable improvement.** That is a claim about those ten, not about the core as a whole: two core leads the campaign surfaced (the APU at 18.7% of frame, and `range.rs` inlined inside `Ppu::tick` at 1.52%) remain **unmeasured** and are carried forward.

**No emulation-core changes.** AccuracyCoin holds at **exactly 141/141** and nestest is 0-diff, verified after every experimental probe was reverted rather than merely asserted by construction: this release did land and remove real edits.

## Why a measurement release

Two failures in the preceding release motivated it.

- **v2.3.0's adopted PPU optimization measured `+2%` on a contended host and
`−5.13%` re-measured quiet** — the same commit, opposite sign. The project's
adopt/reject bar is only as good as the host it runs on, and nothing noticed
the host.
- **The profile the campaign was scoped from does not contain the APU.**
`perf report` shows zero `rustynes_apu::` symbols at any percent limit, because
fat LTO inlines the APU wholesale into `cpu_clock`. The working split
"PPU ~53%, CPU+bus ~39%" had folded roughly a fifth of the frame into the wrong
bucket.
- **v2.3.0's adopted PPU optimization measured `+2%` on a contended host and `−5.13%` re-measured quiet** — the same commit, opposite sign. The project's adopt/reject bar is only as good as the host it runs on, and nothing noticed the host.
- **The profile the campaign was scoped from does not contain the APU.** `perf report` shows zero `rustynes_apu::` symbols at any percent limit, because fat LTO inlines the APU wholesale into `cpu_clock`. The working split "PPU ~53%, CPU+bus ~39%" had folded roughly a fifth of the frame into the wrong bucket.

## New measurement tooling

Expand All @@ -33,13 +17,9 @@ Two failures in the preceding release motivated it.
| `frame_breakdown.sh` — attribution by source file | the **APU is 18.7% of frame time**; `perf report --inline` does *not* recover it |
| `ab_check.sh` — adoption A/B with an A/B/A order-bias control | the reference drifts up to **−1.17% from run position alone** |

Corrected subsystem split: **PPU 52.1% · APU 18.7% · CPU 10.1% · bus/scheduler
coupling 9.9% · std inlined at call sites 6.7% · mappers 2.5%.** The CPU proper
is about a third of what the symbol profile implied.
Corrected subsystem split: **PPU 52.1% · APU 18.7% · CPU 10.1% · bus/scheduler coupling 9.9% · std inlined at call sites 6.7% · mappers 2.5%.** The CPU proper is about a third of what the symbol profile implied.

`bench_relative_check.sh` additionally declines to emit a verdict when the host
was too noisy to resolve the effect under test, keyed on a robust MAD-based
coefficient of variation.
`bench_relative_check.sh` additionally declines to emit a verdict when the host was too noisy to resolve the effect under test, keyed on a robust MAD-based coefficient of variation.

## Ten candidates measured, ten rejected

Expand All @@ -52,36 +32,24 @@ coefficient of variation.
| the target is too small to matter | the `bg_split_state` capability gate (0.09% of frame) |
| forbidden by the ownership model | hoisting `PpuBusAdapter` (borrow checker, with no `unsafe` permitted) |

Six distinct mechanisms, which is what makes this a finding rather than one bad
assumption repeated: **the per-dot loop has no incidental overhead left to
reclaim.** Its ~3.78 ms is work the accuracy model requires. That corroborates
the existing record, where bounds-check elision and a SIMD blitter both measured
*slower*.
Six distinct mechanisms, which is what makes this a finding rather than one bad assumption repeated: **the per-dot loop has no incidental overhead left to reclaim.** Its ~3.78 ms is work the accuracy model requires. That corroborates the existing record, where bounds-check elision and a SIMD blitter both measured *slower*.

## Two near-misses

Worth recording, because each would have shipped on a single reading:

- One candidate produced a textbook **−1.84% … −2.75% at p = 0.00 on all four
workloads** — entirely an order-bias artifact. It measured as exactly zero on
re-run. This is what prompted the A/B/A control.
- Another measured **−0.51% at p = 0.00 on a shipped configuration** with a clean
control, then **+0.01% (p = 0.96)** on re-run.
- One candidate produced a textbook **−1.84% … −2.75% at p = 0.00 on all four workloads** — entirely an order-bias artifact. It measured as exactly zero on re-run. This is what prompted the A/B/A control.
- Another measured **−0.51% at p = 0.00 on a shipped configuration** with a clean control, then **+0.01% (p = 0.96)** on re-run.

Both were caught only by requiring an independent second run.

## Also in this release

- The PGO workflow's BOLT probe no longer reports success without BOLT. It ran
`apt-get install bolt` and trusted the exit status — but on Ubuntu that package
is the **Thunderbolt 3 device manager**, so the stage failed on the tool it had
just "confirmed" instead of skipping as its best-effort contract intends.
- Every rejected experiment is recorded in `docs/performance.md` with its
numbers, its order-bias control, and the mechanism behind the null result.
- The PGO workflow's BOLT probe no longer reports success without BOLT. It ran `apt-get install bolt` and trusted the exit status — but on Ubuntu that package is the **Thunderbolt 3 device manager**, so the stage failed on the tool it had just "confirmed" instead of skipping as its best-effort contract intends.
- Every rejected experiment is recorded in `docs/performance.md` with its numbers, its order-bias control, and the mechanism behind the null result.

## Verification

- `cargo test --workspace --features test-roms` green — AccuracyCoin **141/141**,
`visual_regression` 9/9, nestest 0-diff.
- `cargo test --workspace --features test-roms` green — AccuracyCoin **141/141**, `visual_regression` 9/9, nestest 0-diff.
- Workspace clippy clean at `-D warnings`; `cargo fmt --all --check` clean.
- `shellcheck` clean on every touched script.
28 changes: 28 additions & 0 deletions docs/SALVAGE_MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,31 @@ testing, and a built MkDocs static site — all spent/regenerable, none
salvaged. Only `scripts/probes/probe_rev.rs` was promoted from untracked
scratch to committed (see the row above) — everything else from the prior
pass stays as previously recorded.

## 2026-08-06 — pre-reboot salvage (manual, curated)

Run before rebooting for an NVIDIA driver/kernel update. The `tmp-salvage` dry
run offered **305 candidates / ~600 MB**; **3 files** were taken. The rest was
deliberately left to be wiped:

| left behind | why |
| --- | --- |
| `perf.data` (586 MB), `probe.perf`, `fp.data`, `di.data`, `bolt.log` | perf recordings and CI logs — regenerable with one command |
| ~100 `.md`/`.json` PR-review files | already posted to GitHub PR #348 |
| `fisk-*.png`, `v2p-*.png` (8 MB) | screenshots, possibly commercial-ROM framebuffers — not swept in unexamined |
| `/tmp/accuracycoin-build/` | third-party MIT source + a Windows `nesasm.exe`; vendoring is a deliberate provenance decision, not a salvage side effect |
| `agy-review.sh`, `agy-review-selftest.sh` | duplicates of files already in `scripts/` and `Local_Only-Projects/antigravity-pr-review/` |
| `p3.sh`, `p4.sh`, `probe*.sh`, `mi.rs` | extraction artifacts (YAML pulled out for `shellcheck`) and a temp probe already reverted on purpose |

Everything built during the session was **already committed** (`frame_probe.rs`,
`ab_check.sh`, `frame_breakdown.sh`, the `perf_log_check.py` p99 gate), so the
scratchpad held no unique source beyond the three below.

| file | → | note |
| --- | --- | --- |
| `reflow.py` | `scripts/release-automation/` | markdown full-width reflow; fixed the release-notes formatting. Existed nowhere else on disk. |
| `assemble.py` | `scripts/release-automation/` | HTML fragment → document (needs `bs4`). |
| `guardrails_assemble.py` | `scripts/release-automation/` | same, for the provenance-guardrails doc. |

SHA-256 verified identical to source after copy. See
`scripts/release-automation/README-doc-tools.md` for usage.
Comment on lines +131 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clarify the SHA-256 verification timeline.

The README states that reflow.py was fixed after recovery, while this line says the files were SHA-256 identical to the /tmp sources after copying. State whether the hashes were captured before later edits, and record per-file hashes if this is the release evidence. Otherwise, readers may treat the current files as byte-identical recovered artifacts.

As per coding guidelines, preserve provenance and clearly document recovered, discarded, or externally sourced artifacts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SALVAGE_MANIFEST.md` around lines 131 - 132, Clarify the SHA-256
verification statement in the recovery evidence near the referenced
release-automation documentation: state whether hashes were captured before
subsequent edits to reflow.py or other files, and record the per-file hashes if
they constitute release evidence. Ensure the wording does not imply that the
current files remain byte-identical unless that is verified, while preserving
provenance for recovered and externally sourced artifacts.

Source: Coding guidelines

52 changes: 52 additions & 0 deletions scripts/release-automation/README-doc-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Release-notes / doc rendering helpers

Three one-off tools rescued from `/tmp` before a reboot (2026-08-06). They were
written during the v2.2.5–v2.3.0 documentation work, existed **nowhere else on
disk**, and would have been lost. Recorded here so they are findable rather than
rediscovered.

| script | needs | what it does |
| --- | --- | --- |
| `reflow.py` | stdlib only | Unwraps hard-wrapped markdown into single full-width lines. |
| `assemble.py` | `bs4` | Assembles a rendered HTML fragment into a full document. |
| `guardrails_assemble.py` | `bs4` | Same, for the provenance-guardrails doc: injects a title block and reddens a curated set of hard takeaways. |

## `reflow.py` — the one you will want again

This is the tool that fixed the GitHub release-notes formatting complaint: notes
published from v2.2.5 onward had been hard-wrapped at ~80 columns, which GitHub
renders as artificially narrow text instead of using the full width available.

It unwraps paragraphs and list items to one line each while **preserving**
blank lines, ATX headings, horizontal rules, fenced code blocks, tables,
blockquotes, and raw HTML lines — the things that break if naively joined.

```bash
python3 scripts/release-automation/reflow.py < in.md > out.md
```

Worth running over any hand-wrapped `.github/release-notes/vX.Y.Z.md` before
publishing.

## The `bs4` pair

`assemble.py` and `guardrails_assemble.py` take a rendered HTML fragment and
produce a standalone document. They need BeautifulSoup, which is **not** a
project dependency — install it in a throwaway venv rather than adding it to the
repo:

```bash
python3 -m venv /tmp/venv && /tmp/venv/bin/pip install beautifulsoup4
/tmp/venv/bin/python scripts/release-automation/assemble.py frag.html out.html
```
Comment on lines +33 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Document the required assemble.py input structure.

Line 33 describes both tools as accepting any rendered HTML fragment. The supplied scripts/release-automation/assemble.py implementation searches for a top-level <table> and a top-level paragraph whose text starts with NOTE; missing either element raises StopIteration. Document these preconditions, or add explicit validation with an actionable error.

As per path instructions, flag documentation that drifts from the code it describes rather than just prose nits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-automation/README-doc-tools.md` around lines 33 - 41, Update
the documentation for assemble.py and guardrails_assemble.py to state that the
rendered HTML input must contain a top-level table and a top-level paragraph
whose text starts with “NOTE”. Keep the existing usage example, and either
document that missing elements cause failure or add explicit validation with an
actionable error in assemble.py.

Source: Path instructions


Both are specific to the one-time provenance/guardrails PDF build
(`ref-docs/`) and are kept for reproducing those artifacts, not for routine use.

**They are preserved verbatim and are not ruff-clean** (`SIM115` context
managers, `UP031` percent-format). That is deliberate: `bs4` is not installed
here and no sample fragment survives, so a lint rewrite could not be executed to
prove it still behaved. Rewriting code you cannot run is a worse trade than a
style nit. Clean them up the first time you actually need them, with a real
input to test against. `reflow.py` — which *is* testable, being stdlib-only — was
fixed and verified.
96 changes: 96 additions & 0 deletions scripts/release-automation/assemble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
import sys

from bs4 import BeautifulSoup, NavigableString

frag_path, out_path = sys.argv[1], sys.argv[2]
html = open(frag_path, encoding="utf-8").read()
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the two required command-line arguments.

Missing arguments raise IndexError. Extra arguments are ignored. Exit with a usage message unless exactly two paths are supplied.

  • scripts/release-automation/assemble.py#L6-L7: validate the argument count before reading the input and output paths.
  • scripts/release-automation/guardrails_assemble.py#L8-L9: apply the same validation and usage message.
Proposed fix
-frag_path, out_path = sys.argv[1], sys.argv[2]
+if len(sys.argv) != 3:
+    raise SystemExit(f"usage: {sys.argv[0]} INPUT_FRAGMENT OUTPUT_HTML")
+frag_path, out_path = sys.argv[1:]

As per path instructions, prioritize correctness and clear error messages over style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
frag_path, out_path = sys.argv[1], sys.argv[2]
html = open(frag_path, encoding="utf-8").read()
if len(sys.argv) != 3:
raise SystemExit(f"usage: {sys.argv[0]} INPUT_FRAGMENT OUTPUT_HTML")
frag_path, out_path = sys.argv[1:]
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 6-6: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(frag_path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.16.1)

[warning] 7-7: Use a context manager for opening files

(SIM115)

📍 Affects 2 files
  • scripts/release-automation/assemble.py#L6-L7 (this comment)
  • scripts/release-automation/guardrails_assemble.py#L8-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-automation/assemble.py` around lines 6 - 7, Validate that
exactly two command-line arguments are provided before assigning paths or
reading files in assemble.py at lines 6-7 and guardrails_assemble.py at lines
8-9; otherwise exit with a clear usage message. Apply the same argument-count
validation and message consistently in both scripts.

Source: Path instructions

soup = BeautifulSoup(html, "html.parser")
Comment on lines +6 to +8

# --- RED for curated genuine takeaways (exact <strong> text contains) ---
KEY = [
"more serious LLM error",
"did not follow it",
"AI self-attestation of license compliance is not trustworthy",
"single most important piece of evidence",
"Correct.",
"not written purely from hardware documentation",
]
for s in soup.find_all("strong"):
t = s.get_text()
if any(k in t for k in KEY):
cls = s.get("class", [])
s["class"] = cls + ["key"]

# --- top-level elements, in order ---
els = [c for c in soup.contents if getattr(c, "name", None)]
table_i = next(i for i, e in enumerate(els) if e.name == "table")
note_i = next(i for i, e in enumerate(els)
if e.name == "p" and e.get_text().lstrip().startswith("NOTE"))

before = "".join(str(e) for e in els[:table_i])
table = str(els[table_i])
middle = "".join(str(e) for e in els[table_i + 1:note_i])
Comment on lines +27 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the required fragment structure before slicing.

If the table or NOTE paragraph is absent, next() raises StopIteration. If the NOTE paragraph precedes the table, this code emits the NOTE twice. Reject fragments unless both elements exist and table_i < note_i.

Proposed fix
-table_i = next(i for i, e in enumerate(els) if e.name == "table")
-note_i = next(i for i, e in enumerate(els)
-              if e.name == "p" and e.get_text().lstrip().startswith("NOTE"))
+table_i = next((i for i, e in enumerate(els) if e.name == "table"), None)
+note_i = next(
+    (i for i, e in enumerate(els)
+     if e.name == "p" and e.get_text().lstrip().startswith("NOTE")),
+    None,
+)
+if table_i is None or note_i is None or table_i >= note_i:
+    raise ValueError("expected a top-level table followed by a NOTE paragraph")

As per path instructions, prioritize correctness and clear error messages over style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
table_i = next(i for i, e in enumerate(els) if e.name == "table")
note_i = next(i for i, e in enumerate(els)
if e.name == "p" and e.get_text().lstrip().startswith("NOTE"))
before = "".join(str(e) for e in els[:table_i])
table = str(els[table_i])
middle = "".join(str(e) for e in els[table_i + 1:note_i])
table_i = next((i for i, e in enumerate(els) if e.name == "table"), None)
note_i = next(
(i for i, e in enumerate(els)
if e.name == "p" and e.get_text().lstrip().startswith("NOTE")),
None,
)
if table_i is None or note_i is None or table_i >= note_i:
raise ValueError("expected a top-level table followed by a NOTE paragraph")
before = "".join(str(e) for e in els[:table_i])
table = str(els[table_i])
middle = "".join(str(e) for e in els[table_i + 1:note_i])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-automation/assemble.py` around lines 27 - 33, Validate the
required table and NOTE paragraph structure before slicing in the surrounding
fragment assembly logic: handle missing matches with a clear rejection error,
and reject any fragment where the NOTE index is not after the table index.
Preserve the existing before, table, and middle extraction only after confirming
both elements exist and table_i < note_i.

Source: Path instructions


# --- italicize everything after "): " in the NOTE paragraph (PDF only) ---
note_p = els[note_i]
MARKER = "): "
kids = list(note_p.children)
head, tail_nodes, splitting = [], [], True
for k in kids:
if splitting and isinstance(k, NavigableString) and MARKER in k:
pre, post = str(k).split(MARKER, 1)
head.append(NavigableString(pre + MARKER))
if post:
tail_nodes.append(NavigableString(post))
splitting = False
elif splitting:
head.append(k)
else:
tail_nodes.append(k.extract() if hasattr(k, "extract") else k)
if tail_nodes: # only rebuild if the marker was found
note_p.clear()
for h in head:
note_p.append(h)
em = soup.new_tag("em")
for t in tail_nodes:
em.append(t)
# Bold + upright "Fiskbit" inside the italic note body (PDF styling only)
for tnode in list(em.find_all(string=True)):
if "Fiskbit" in tnode:
parts = str(tnode).split("Fiskbit")
repl = []
for i, seg in enumerate(parts):
if i > 0:
st = soup.new_tag("strong")
st["class"] = ["upright"]
st.string = "Fiskbit"
repl.append(st)
if seg:
repl.append(NavigableString(seg))
tnode.replace_with(*repl)
break
note_p.append(em)
note = str(note_p)

TITLE = """<div class="titlewrap">
<p class="eyebrow">RUSTYNES &#183; INCIDENT RECORD &#183; GPL PROVENANCE</p>
<h1 class="title">Provenance Failure Post-Mortem</h1>
<p class="subtitle">How GPL Emulator Code Was Lifted Despite a Black-Box Instruction</p>
<p class="docdate">Forensic Root-Cause Analysis &#183; 2026-08-04 &#183; RustyNES v2.2.9</p>
</div>"""

body = (TITLE
+ '<div class="cols">' + before + '</div>'
+ table
+ '<div class="cols">' + middle + '</div>'
+ '<div class="note-box">' + note + '</div>')

doc = ('<!doctype html><html lang="en"><head><meta charset="utf-8">'
'<title>Provenance Failure Post-Mortem</title></head><body>'
+ body + '</body></html>')

open(out_path, "w", encoding="utf-8").write(doc)
print("assembled ->", out_path,
"| before/middle/note split at table_i=%d note_i=%d" % (table_i, note_i),
"| reddened %d strong tags" % len(soup.select("strong.key")))
40 changes: 40 additions & 0 deletions scripts/release-automation/guardrails_assemble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Assemble the guardrails HTML: inject a title block, redden a curated
set of hard takeaways. Single-column theme handles the rest of the flow."""
import sys

from bs4 import BeautifulSoup

frag_path, out_path = sys.argv[1], sys.argv[2]
html = open(frag_path, encoding="utf-8").read()
soup = BeautifulSoup(html, "html.parser")
Comment on lines +8 to +10

# --- RED reserved for a few genuine, full-span takeaways ---
KEY = [
"capability plus availability plus an accuracy objective",
"C used as if it were A",
"source physically unavailable to the agent",
"Never launder",
]
reddened = 0
for s in soup.find_all("strong"):
t = s.get_text()
if any(k in t for k in KEY):
s["class"] = s.get("class", []) + ["key"]
reddened += 1

body = "".join(str(c) for c in soup.contents)

TITLE = """<div class="titlewrap">
<p class="eyebrow">COMMUNITY BEST-GUIDANCE &#183; AI-ASSISTED EMULATOR DEVELOPMENT</p>
<h1 class="title">Provenance &amp; License Guardrails</h1>
<p class="subtitle">A ready-to-ingest ruleset for Claude Code and other agentic / AI-assisted development tools</p>
<p class="docdate">Preventing the copyleft-source-lifting trap &#183; 2026-08-04</p>
</div>"""

doc = ('<!doctype html><html lang="en"><head><meta charset="utf-8">'
'<title>Provenance &amp; License Guardrails</title></head><body>'
+ TITLE + body + '</body></html>')

open(out_path, "w", encoding="utf-8").write(doc)
print("assembled ->", out_path, "| reddened %d strong tags" % reddened)
Loading