Skip to content

fix(rigs): device-state prep — configure, do not refuse; and do not degrade toward the risky write - #225

Open
JC-000 wants to merge 4 commits into
masterfrom
fix/rig-device-state-prep
Open

fix(rigs): device-state prep — configure, do not refuse; and do not degrade toward the risky write#225
JC-000 wants to merge 4 commits into
masterfrom
fix/rig-device-state-prep

Conversation

@JC-000

@JC-000 JC-000 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #197, closes #187, closes the second half of #212.

Three defects of one shape. Device config on an Ultimate is runtime-only, so REU-Disabled at 1 MHz is the factory default, not another lane's mess: a run restores baseline and then sets what it needs. Four of five preflight_reu callers had nothing for the preflight to back up (#197); all four HTTPS rigs degraded a failed turbo probe into writing anyway — the exact $88-causing write the probe exists to skip (#187); and no run recorded the device state it ran against (#212).

New tools/uci/_device_prep.py::prepare_device(), called under the DeviceLock after enable_uci and before preflight_reu at all five crypto-path sites. rig_https_wiki.py's ensure_reu_16mb was generalised into it, not deleted. The record lands as device_state.json in the run's own artifact dir.

The failure policy is per item, deliberately asymmetric

An unreadable REU state writes anyway — the write is the safe action. An unreadable turbo state aborts after one retry — the write is the hazard, and skipping it runs the rig at the previous lane's clock, invalidating every wall-clock number and reproducing #212's misdiagnosed comb boot. Overrides: C64_FORCE_TURBO_WRITE=1, C64_SKIP_DEVICE_PREP=1.

Both directions are mutation-pinned: reinstating the old turbo degrade fails 3 cases, and extending fail-closed to the REU fails 3 different ones. A uniform policy either way fails half the suite, which is what makes the asymmetry load-bearing rather than a rationalisation.

Review sharpened the first half. "Write anyway is safe" only holds because the write is now wrapped, and the code comment says so at the wrap site. With REST refusing, the harness's Cartridge preset probe is also inconclusive, so set_reu PUTs Cartridge: "REU", a C64U answers HTTP 400 — and unwrapped that escaped every call site, downgrading master's exit-4-plus-wedge-ladder into exit 1 and a traceback, on the path this project most often misreads as firmware corruption. The condition that triggers "write anyway" is the same condition that makes the write fail.

Rig coverage is discovered, not listed

PREP_CALL_SITES is gone; rigs are found by client.run_prg( plus a mention of the comb profile. That found rig_https_banner.py, which an allowlist never could — it is #212's failure verbatim (menu boot, 75 s budget, "comb boot precompute", no turbo or REU write) and sits in KNOWN_UNPREPPED with a reason and an owner, never edited here. Emptying that list goes red naming it.

The rule is a text match, and its limits are documented rather than claimed away. Review wrote two rigs with #197's exact defect that pass 30/0 — one naming its client u64, one never containing the word "comb". Under-selection is silent; over-selection is loud, because a module the rule misses never enters the compared set. The guard's docstring says so, and says what to do about a false positive: add an exemption, never weaken the rule, since that trades a loud false positive for a silent false negative.

Also fixed in passing

  • cpu_speed_enum(1) returns ' 1' with a leading space, and master's unstripped comparison meant the turbo skip could never fire at TURBO_MHZ=1 — every 1 MHz run performed a redundant, $88-risking turbo write.
  • C64_SKIP_REU_PREFLIGHT=false used to skip the guard while C64_SKIP_DEVICE_PREP=false did not. One parser now, fail-closed: measured across 13 values, =0 and =1 unchanged, and every behaviour change moves a guard from OFF to ON. No value exists where a previously-ON guard is now OFF. Scoped honestly — C64_SKIP_TEMP_GC and KEEP_DEBUG_ON_PASS are not unified, and the comment says so and says the work is worth doing.

Red-green

tools/test_device_prep.py, 30 cases, faked client, no hardware. Call-site contract red on 4 rigs + bench pre-fix. The five amendment cases red against the first commit's sources — two arriving as raw uncaught _HarnessError rather than FAIL, which is the blocker manifesting, confirmed by review as genuine escapes rather than test bugs.

Clean-tree pytest: master 153 passed / 0 failed, branch 183 passed / 0 failed, failure sets diff-identical, delta exactly +30.

Harness audited at 1219fe1

Verified claim by claim by the reviewer against the sibling tree, not against docs, since it is an editable install and their merges are our regressions: get_config_value (raises rather than returning None), get_config_item, get_config_category (unchanged, still bare values), set_reu (C64U Cartridge preset probe), set_turbo_mhz, cpu_speed_enum, apply_factory_baseline + BASELINE_NEVER_TOUCH. No never-touch store is read or written; apply_factory_baseline is deliberately never called, since it resets U64 Specific Settings and would perform exactly the unconditional turbo write #187 is about.

Hardware

Independent review run, U64E fw 3.15 / commit 4011c97c / fpga 125, via acquire_device_lock(). Device found in exactly #212's leftover state (Turbo Control='Manual', CPU Speed='48', RAM Expansion Unit='Disabled'). The turbo write was correctly skipped; set_reu(True, "16 MB") succeeded and read back, with the Cartridge write correctly omitted on the 3.15 shape; a second prepare_device gave wrote=[] skipped_write=['reu','turbo'] — idempotent, zero PUTs. State restored, lock released.

That evidence still describes the tip: the success path is byte-for-byte unchanged since, the write only having gained a try/except. Round 2 did not re-run, because the device was held by a live, progressing holder — PID 73138 running rig_https_banner.py at FETCH_TIMEOUT=900, i.e. the #210 lane that KNOWN_UNPREPPED names as banner's owner. The exemption's stated ownership is corroborated by the device queue.

Still hardware-unproven, and stated as such: the REU-write-refusal path (now a Python-level wrapper, low residual risk) and the C64U Cartridge HTTP 400 itself, which is unprovable on a U64E at all and rests on the harness's live-verified claim.

Accepted side effects

Four more rigs leave the device REU-Enabled and REU Size at 16 MB, up from the flash default. We never call save_config_to_flash, so the device reports 16 MB while flash holds the smaller default, and a reboot silently restores it mid-session — per get_reu_config's own docstring, that RAM/flash divergence is the pair that confused a previous reporter. An on-chip build is never given an REU, though its four state reads still happen, because the record is the point and reads are not the hazard.

A later on-chip run on the same device is therefore no longer free evidence of REU-less operation; that now needs a deliberately REU-disabled device or C64_VICE_NO_REU=1.

Follow-up owed

rig_https_banner.py needs prepare_device(client, LABELS_PATH, turbo_mhz=..., artifact_dir=run_dir) after enable_uci. It has no TURBO_MHZ concept today, so its owner has to decide the clock it should assert. Sequenced after #210.

🤖 Generated with Claude Code

JC-000 and others added 4 commits September 7, 2026 23:18
…ing toward the risky write (#197, #187, #212)

Three defects of one shape. Device config on an Ultimate is runtime-only:
a re-flash restores factory defaults and we never write flash, so
`RAM Expansion Unit: Disabled` and a 1 MHz clock are the DEFAULT state,
not another lane's leftovers. The working pattern is that a run restores
baseline and then sets what it needs.

#197 -- four of the five `preflight_reu` callers had nothing for it to
back up. `bench_ecdsa_u64e.py`, `rig_https_local.py`,
`rig_https_bad_finished.py` and `rig_https_live.py` read the REU state
and exited 4 on a device that was simply at its default (observed on a
U64E at fw 3.15 while validating #181). Only `rig_https_wiki.py`
configured it. The preflight is the backstop for a rig that forgot; it
is not a substitute for setup.

#187 -- all four HTTPS rigs probed `CPU Speed` / `Turbo Control` in
order to SKIP a redundant write, because the write itself glitches the
UCI bridge and loses the next pushed command ($88). On a failed probe
they printed "writing anyway" -- performing the exact action the probe
exists to avoid, with an intermittent $88 as the symptom on a project
with a documented history of blaming that on firmware.

#212 (second half) -- no run recorded the device state it ran against.

New `tools/uci/_device_prep.py::prepare_device()`, called under the
DeviceLock after `enable_uci` and BEFORE `preflight_reu` at all five
sites. It configures the REU the linked profile needs, sets turbo before
the reset, and prints the before- and after-state (also written to
`$UCI_DEBUG_DIR/device_state.json`).

The failure policy is per item, not per module, because both degrades
are wrong somewhere:

  * REU probe unreadable -> WRITE ANYWAY. The write is the configuration
    the run needs; a wasted one costs a PUT. This is `ensure_reu_16mb`'s
    shape, generalised rather than deleted -- #197 names it as the
    correct asymmetry.
  * Turbo probe unreadable -> ABORT, after one retry. Writing risks the
    $88 the probe was added to avoid; skipping runs the rig at the
    previous lane's clock, which invalidates every wall-clock number and
    is how a 1 MHz comb boot got read as a code defect. Two unsafe
    degrades means stopping is the honest answer, with
    `C64_FORCE_TURBO_WRITE=1` as the named way back.

`preflight_reu` is unchanged in behaviour and keeps its #179 fail-closed
guarantee; only its superseded "it never enables the REU for you" policy
prose is corrected to say which half now does. Every device read still
goes through its `_read_config_value`, so the harness-shape tolerance
has one home to audit, not two.

Harness APIs audited against the sibling editable tree
(c64-test-harness @ 1219fe1): `get_config_value` (raises rather than
returning None -- the ambiguity #187 describes disappears at the read),
`get_config_item`, `get_config_category` (unchanged, still bare values
per item), `set_reu` (probes the Cartridge preset; C64U-safe),
`set_turbo_mhz`, `cpu_speed_enum`, and `apply_factory_baseline` /
`BASELINE_NEVER_TOUCH` -- neither REU store is in the five never-touch
stores, and this module resets nothing regardless: it PUTs exactly the
four items it names.

Red-green, hardware-free, `tools/test_device_prep.py` (21 cases, faked
client, on `test_reu_preflight.py`'s model):

  * before, against the unmodified rigs: 18 passed, 3 failed --
    `test_every_crypto_rig_prepares_before_it_checks`,
    `test_no_rig_still_degrades_toward_the_turbo_write` (4 rigs) and
    `test_rigs_do_not_hand_roll_the_turbo_probe` (4 rigs).
  * after: 21 passed, 0 failed.

Both halves of the asymmetry are mutation-checked, because a uniform
policy in either direction passes half the suite:

  * MUTANT A (reinstate the "writing anyway" turbo degrade): 18 passed,
    3 failed -- the two abort cases plus the None-is-not-a-mismatch case,
    each reporting `turbo writes: [48]`.
  * MUTANT B (extend the fail-closed to the REU): 19 passed, 2 failed --
    `test_unreadable_reu_writes_anyway` and
    `test_unreadable_items_are_named_in_the_report`.

`tools/test_reu_preflight.py` stays green (PASS). Bare `pytest` has the
same 16 pre-existing failures as master (build-dependent and pin
suites), and one fewer: `test_pytest_boundary` goes green with the new
suite listed in `testpaths`.

NOT covered: no hardware run. The U64E was actively held by a peer lane
throughout, and queuing behind it would have blocked the session's other
hardware lanes for a plumbing change that is fully covered
hardware-free. The prep's own REST calls are therefore proven against a
faked client only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md's rig section said crypto-path rigs "call preflight_reu(); it
never writes device config", which was the whole of the story and is now
half of it. It names the prep, the call order (prep first, preflight as
the backstop -- asserted at all five sites by
`tools/test_device_prep.py`), the two opposite degrade directions and
their overrides. `preflight_reu` itself still writes nothing, and that
sentence survives unchanged.

`tools/uci/README.md` gets the same in table form, plus `_device_prep.py`
in the helper-module list.

`docs/engineering-notes.md` closes the gap its #197 entry opened, and
keeps the part worth carrying forward: the right degrade is per item,
against a named cost, and a uniform policy in either direction is wrong
in one of the two places -- with the mutation counts that show it.

No memory-margin figure is touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d close four review findings

Adversarial review of the previous commit blocked on two findings; this
addresses both, plus four smaller ones.

BLOCKER 1 -- the REU write was unwrapped, so `prepare_device` broke its
own documented "never raises for the REU" contract in the worst possible
direction. The vicious part is the correlation: the condition that makes
the probe unreadable is frequently the same one that makes the write
fail. With REST refusing, the harness's `_cartridge_preset_supported`
probe is also inconclusive, `set_reu` therefore INCLUDES a
`Cartridge: "REU"` PUT (ultimate64_helpers.py:483-485) which a C64
Ultimate rejects HTTP 400, and `set_config_items` catches nothing per
item. All five call sites catch only `DevicePrepError`, so that escaped
`main()`: master reported such a device as exit 4 with the
writemem-wedge ladder, and this branch would have reported exit 1 and a
raw traceback with `outcome=UNKNOWN` -- on the one path this project has
a documented habit of misdiagnosing as firmware corruption. The write is
now wrapped into a `DevicePrepError` carrying its own ladder (distinct
from the preflight's "your device has no REU" text: nothing here says the
REU is missing), and it aborts before the turbo write -- a device
refusing config is not a device to keep writing to. The docstring is
corrected too: `set_reu` can PUT a FIFTH item, so "this module PUTs
exactly the four items it names" was wrong.

BLOCKER 2 -- `device_state.json` was written on zero default runs. No
call site passed `artifact_dir`, and every rig defaults `UCI_DEBUG_DIR`
in Python, so the env fallback was normally unset; when set it resolved
to the rigs' BASE dir, not the per-run timestamped dir, so it would be
overwritten by the next run. `bench_ecdsa_u64e.py` has no `UCI_DEBUG_DIR`
concept at all. All five now pass the run's own dir; `rig_https_local.py`
and `bench_ecdsa_u64e.py` create one when DEBUG_CAPTURE left none, since
the run that most needs the record is the one nobody enabled capture for.
The test passed for the wrong reason (it passed `artifact_dir` explicitly
while `_run` popped the env var), so both directions are now covered.

3 -- `C64_SKIP_DEVICE_PREP=1` is the dangerous hatch, not
`C64_FORCE_TURBO_WRITE=1`. It skips the turbo write entirely, so the run
inherits the previous lane's clock: the exact outcome the turbo policy
argues is worth aborting over. Its banner said only that the preflight
still fails closed -- true, and irrelevant to the clock. It now warns.

4 -- `PREP_CALL_SITES` was a hardcoded five-tuple claiming to be "every
rig that runs a crypto-path PRG", which by construction could not find a
sixth. Replaced by discovery: a module that calls `client.run_prg` AND
knows about the comb profile. That selects the five plus
`rig_https_banner.py` -- which boots the PRG through the menu on a 75 s
budget while printing "comb boot precompute", writes neither turbo nor
REU, and is therefore #212's documented failure verbatim (a comb boot at
1 MHz needs ~36 min; `tests/rig_ip65_rrnet_hw.py` writes turbo at stock
1 MHz, so "RR-Net run, then banner run" reaches it live). That file is
owned by another lane and is NOT edited here: it is listed in
`KNOWN_UNPREPPED` with the reason, and a second test fails if the rig is
fixed and the entry is left behind.

5 -- the two skip flags parsed their values differently:
`C64_SKIP_REU_PREFLIGHT=false` SKIPPED while `C64_SKIP_DEVICE_PREP=false`
did not, with both documented on one line. Unified on one parser
(`_reu_preflight.env_flag_enabled`) in the fail-closed direction, so
`=false` now leaves every guard ON.

6 -- correcting the previous commit message rather than amending it: it
claimed "one fewer failure: test_pytest_boundary goes green". Measured,
the failure SET is identical on master and this branch and
`test_pytest_boundary` passes on both. Master is 16 failed / 137 passed
(reviewer-measured); this branch is 16 failed / 167 passed -- the delta
is 30 new passing cases, not a fixed failure.

Also: the retry delay comment no longer implies a measurement behind the
1.0 s (there is none), and the notes record the accepted side effect that
four more rigs leave the device REU-Enabled after a REU-profile run, so a
later on-chip run on the same device is no longer free evidence of
REU-less operation.

Red-green, hardware-free. New cases against the PREVIOUS COMMIT's
sources (`git checkout 110e36f -- tools/uci/*.py`), 25 passed / 5 failed:

  ERROR test_a_refused_reu_write_does_not_reach_the_turbo_write:
        _HarnessError: HTTP 500
  ERROR test_a_refused_reu_write_is_exit_4_with_a_ladder_not_a_traceback:
        _HarnessError: HTTP 400: 'REU' is not a valid choice for Cartridge
  FAIL  test_every_prepping_rig_hands_over_its_run_artifact_dir:
        bench_ecdsa_u64e.py:471 calls prepare_device without artifact_dir ...
  FAIL  test_skipping_prep_warns_that_the_clock_is_unmanaged:
        skipping prep must warn about the unmanaged clock
  ERROR test_the_two_skip_flags_parse_env_values_identically:
        AttributeError: module '_reu_preflight_parity' has no attribute
        'env_flag_enabled'

Note the first two arrive as raw `_HarnessError`, which IS the finding:
they escape as uncaught exceptions rather than as `DevicePrepError`.

After: 30 passed, 0 failed. The discovery rule has teeth -- emptying
`KNOWN_UNPREPPED` gives 29 passed / 1 failed, naming `rig_https_banner.py`.
Both asymmetry mutants still fail on the amended module (A: 27/3, B: 27/3;
B now also fails the refused-write case, since a uniform fail-closed
swallows that distinction). `tools/test_reu_preflight.py` PASS,
`test_pytest_boundary` PASS, all five rigs import and compile.

Hardware, from the independent review run (not mine), U64E fw 3.15 commit
`4011c97c` / fpga 125, via `acquire_device_lock()`: device found in
exactly #212's leftover state (`Manual` / `48` / REU `Disabled`);
`_turbo_matches` -> True so the turbo write was correctly SKIPPED on real
firmware; `set_reu(True, size="16 MB")` succeeded and read back with the
`Cartridge` write correctly omitted on the 3.15 shape; a second
`prepare_device` gave `wrote=[] skipped_write=['reu','turbo']`, i.e.
idempotent with zero PUTs. State restored, lock released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prose only. No behaviour change; `tools/test_device_prep.py` stays 30/0,
`test_reu_preflight.py` PASS, `test_pytest_boundary` PASS, all five rigs
import.

A (must fix) -- "so a sixth rig cannot slip past" was a false mechanism
claim. The discovery rule is a text match (`client.run_prg(` plus a
mention of the comb profile), so it finds a sixth rig written to the same
SHAPE and misses one that is not: the reviewer built two rigs that boot
the real PRG and hold `preflight_reu` with no prep -- #197's exact defect
-- one naming its client `u64`, the other never saying "comb", and both
pass the suite 30/0. `test_the_discovery_rule_finds_the_known_rigs`
cannot see them either, because a module the rule misses never enters the
compared set. **Under-selection is the silent direction; over-selection
is loud.** Both are now stated where the rule lives, together with the
instruction that follows from it: on a false positive, add a
KNOWN_UNPREPPED exemption -- do not weaken the rule, which trades a loud
false positive for a silent false negative. The discovery is still what
found `rig_https_banner.py`, which an allowlist could not; only the
absoluteness was wrong.

C (must fix) -- "one parser for every `C64_*` switch these rigs read" was
an overclaim. Three flags were unified (`C64_SKIP_REU_PREFLIGHT`,
`C64_SKIP_DEVICE_PREP`, `C64_FORCE_TURBO_WRITE`). `C64_SKIP_TEMP_GC`
still tests `== "1"` / `!= "1"`, inconsistently between its two call
sites, and `KEEP_DEBUG_ON_PASS` is spelled three ways across the rigs.
The `_ENV_FALSE` comment now scopes itself to the three and names the two
it does NOT cover, so nobody reads it as already done.

E -- "an on-chip build makes no REU call at all" is true of
`_reu_preflight` (literally zero REST calls) and false of
`prepare_device`, which still issues the four state reads for the #212
record. Same words, two meanings, one module apart; the prep's docstring
now says what it actually means -- no REU is ever configured for an
on-chip image, reads excepted, because reads are not the hazard.

F -- the side-effect paragraph named REU-Enabled but not REU Size, which
these rigs also force to 16 MB from the flash default. Per the harness's
own `get_reu_config` docstring that RAM-vs-flash divergence is the exact
pair that confused a previous reporter: we never call
`save_config_to_flash`, so the device reports 16 MB while flash holds the
default, and a reboot silently restores the smaller one mid-session.

G -- the refused-write ladder labelled the pre-attempt snapshot "state".
`set_config_items` is one PUT per item and explicitly non-atomic
("earlier writes are left in place"), so on a firmware that writes
`Cartridge` first, a rejected `RAM Expansion Unit` leaves the device
half-configured while the message implies otherwise. Relabelled "state
before the attempted write", with the non-atomicity and a re-read
instruction spelled out.

H -- "fifth item" (module docstring) versus "third item" (CLAUDE.md,
README, ladder) read as a contradiction. Both frames are now named in one
place: `Cartridge` makes it a THREE-item PUT batch and the FIFTH distinct
config item this module is responsible for. Also documented at the call
sites: `rig_https_local` and `bench_ecdsa_u64e` now prune on capture-OFF
runs where they previously pruned nothing, so such a run can rotate away
the oldest of the kept 5 capture dirs; and a PASS with capture off leaves
the prep dir behind holding only `device_state.json`, since the cleanup
block is nested under `if run_dir is not None`. Both accepted --
unbounded prep dirs would be worse -- and the printed line now says the
directory holds the record only.

B was raised as a note and needs no code: over-selection is brittle (a
helper containing `client.run_prg(` and the word "combines" earns two
reds, one demanding that a helper call `prepare_device`) but fails loud.
Captured in the guard's docstring so the next person under time pressure
adds an exemption instead of loosening the rule.

Correcting my own numbers again: the "16 failed / 137 passed vs 16 failed
/ 167 passed" figures in commit 12527fb were measured on an UNBUILT tree,
where build-dependent suites fail for reasons unrelated to this branch.
On a clean built tree the reviewer measured master 153 passed / 0 failed
and this branch 183 passed / 0 failed -- delta exactly +30, failure sets
diff-identical (both empty). The +30 and identical-failure-set claims
hold; the framing did not.

No hardware this round, deliberately: the U64E was held by a live,
progressing holder (PID 73138 running `rig_https_banner.py` at
FETCH_TIMEOUT=900, queue depth 0), checked read-only, not queued. Round
1's hardware evidence still carries because the success path is
byte-for-byte unchanged since -- the write only gained a try/except.
Fittingly, the lane holding the device is the #210 lane that
KNOWN_UNPREPPED names as banner's owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant