Close #184 — mechanically enforce the fleet net_last_error registry - #222
Merged
Conversation
The ip65 ($40-$7F) and UCI ($80-$BF) error families are one namespace each, shared with c64-wireguard. #185 wrote their allocations into our two headers as prose. Prose does not fail a build, and the fleet has already had two collisions -- $88 (live four days) and wg#120's first commit minting $40-$44 over our $41-$45, caught only by a human reviewer. Two guards, one for each other's blind spot: src/net_err_registry_asserts.s -- picked up by the Makefile's $(wildcard src/*.s), so it assembles into every build, both backends, every profile, and emits no bytes (equates and .assert only; PRG sha256 unchanged on all five profiles). It carries the peer's codes as NET_ERR_PEER_* equates -- a namespace that is obviously not ours, so the #185 rule that an equate in our headers would read as "we emit this" still holds -- and asserts that no code of ours lands on one, that each is in family range, and that no published value has been reassigned. Scope `error`, not `lderror`: every operand is a local equate, so ca65 settles it at the offending object rather than at the link. tools/test_net_err_registry.py -- pure logic, milliseconds, no build. Parses the two headers rather than the asserts TU, so a code that never gets registered there is still caught, and diffs our snapshot against the live c64-wireguard/src/net_abi.inc when a checkout is present. Absent one it skips with a reason; pytest.ini already sets addopts = -ra, so that reason prints on every run. $8A UCI_ERR_LONG_READ is the one deliberate overlap -- their allocation, mirrored here reserved-never-emitted -- so it is asserted to EQUAL their value rather than differ from it, and is excluded from the collision sweep by name, never by value. Cross-repo state at c64-wireguard cf7b41e: no collision. All 16 codes we define are listed in their registry with the ownership we claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…view) Adversary review of the first commit. The zero-byte / every-profile / cross-product engineering held; these are the guard's advertised scope and one crash. F3 -- the standalone entry point died mid-suite. _require_peer() called pytest.skip(), which raises Skipped, a BaseException; main() caught only AssertionError. Five of eight checks never ran, no summary printed, and the exit 1 meant "a check FAILED" under this repo's contract. That is verbatim the failure _skip_policy.VoluntarySkip exists to prevent, and the `if root is None` fallbacks were dead code whenever pytest was importable. F4 -- and it re-decided the skip policy locally instead of using tools/_skip_policy.py, so it carried no coverage clause and no opt_out_env. Now require(..., executed=6, total=10, certifies=..., opt_out_env= "C64_ALLOW_SKIP"): a missing peer checkout is an INVOLUNTARY skip, so the four cross-repo checks FAIL rather than pass quietly. C64_ALLOW_SKIP=1 is the loud opt-out. The six structural checks run either way. F2 -- nothing checked our own set for pairwise distinctness. UCI_ERR_DUP = $88, duly registered, passed all eight checks including the peer one, because $88 IS in their registry under another name. The assembler cannot express this (its literal pins only cover codes that existed when they were written), so it is a new suite check. F1 -- the parser knew one spelling. Widened to `NAME = ddd` and `.define NAME $hh`; verified no existing decimal equate in either header falls in $40-$BF. Expression-valued equates are DECLARED OUT OF SCOPE in all four documentation sites: evaluating them needs an assembler, and a parser that silently mis-evaluates one would be worse than one that visibly does not try. F5 -- the $8A mirror compared values only, so a peer RENAME left every check green while our diagnostics carried a dead name. New check compares names too, for the whole snapshot; the commit message's "cannot silently drift off theirs" was true of the value and false of the name, and the TU comment now says which. F6 -- the macros' peer list is hand-written and ca65 cannot iterate a table, so a snapshot row with no matching .assert was silently unchecked. Now checked by the suite, and noted where someone editing the table sees it. F7 -- "adding a code takes all three edits and the suite is red until they agree" claimed a completeness F1 disproves. Corrected in both headers, the TU header and CLAUDE.md, each of which now carries what the pair does NOT cover. PRG sha256 unchanged on all five profiles; no assertion logic changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions (#184 review 2) N1 -- "the assembler cannot express this" was a FALSE MECHANISM CLAIM, and it foreclosed the better guard for a round. ca65 expresses it in one line: .macro NET_ERR_CLAIM_VALUE val .ident(.sprintf("NET_ERR_TAKEN_%02X", val)) = 1 .endmacro A second claim on a byte is a redefinition error naming the BYTE. O(n), no hand-maintained list, constant equate so still zero bytes. Called from both assert macros and once for the $8A mirror (which cannot go through a macro that asserts the value differs from every peer code). An intra-repo duplicate now fails the build on all five profiles instead of being caught only by a suite `make` never runs. The claim was wrong in four places and is corrected in all four; the narrower TRUE sentence it was over-generalised from -- the literal pins give distinctness only among codes that existed when they were written -- is kept. While verifying that, a SECOND over-pessimistic claim fell: ca65 evaluates whatever the macro is handed, so a REGISTERED expression-valued equate fires the collision assert exactly like a literal (measured). The residual gap is only an expression-valued code that is ALSO never registered. Corrected in the same four sites; the earlier "passes both halves while colliding" was wrong. N2 -- the decimal widening introduced a false-positive class: an innocent `UCI_HOST_BUF_MAX = 64` produced three red checks telling the author to allocate a buffer size in c64-wireguard's error registry. Gated on the `_ERR_` infix both headers use universally for codes and never for constants. DECISION and its cost: this lets a real code named without `_ERR_` (`UCI_STATUS_FOO = $8C`) past the suite. Accepted because the assemble-time half is name-blind and still catches it the moment it is registered, and because the false positive is a when rather than an if -- buffer sizes favour 64 and 128, which are $40 and $80, the first byte of each family. Named in every scope block. N3 -- moved the opt-out off the shared C64_ALLOW_SKIP (which also gates test_build_flags_stamp.py's toolchain prerequisite) to a dedicated C64_NO_PEER_REGISTRY. One hatch, one door. N4 -- the two headers' scope blocks are parallel again; ip65's kept a round-1 sentence that understated the missing-checkout behaviour. N5 -- inline immediates (`lda #$8C / sta net_last_error`) named as invisible to both halves. Latent: no such site exists. Every scope block now states over-coverage as well as under-coverage. Also: _our_codes()'s bare `assert` became a named RegistryParseError (it vanished under python -O and misattributed to whichever test ran first), and _peer_snapshot_expected_name() gained PEER_NAME_OVERRIDES for a future peer name fitting neither derivation. Keeping cannot_run per the coordinator: the check is applicable and unverified, and exit 0 would say otherwise. PRG sha256 unchanged on all five profiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ims (#184 review 3) Text only -- comments, docstrings and CLAUDE.md prose. No executable line in either guard changed; verified by filtering the .s and .py diffs, and by rebuilding all five profiles to unmoved PRG hashes. N6 -- the fourth untested mechanism claim in this area, and the one that mattered. The ERR_NAME_MARKER comment said a non-`_ERR_`-named unregistered code "is invisible either way, which is the pre-existing limit this does not widen". Measured on the two commits either side of the gate, with the same mutation (an unregistered, colliding UCI_STATUS_FOO = $8C): 3 failures at 53a35bb, 0 at 123fccb. The gate CREATED that class; an `_ERR_`-named unregistered code is caught at both revisions, so nothing was being restated. Corrected to say so. The decision two lines above is unchanged and still right -- only the blast radius was wrong. N7 -- CLAUDE.md listed under-coverage as two members while both headers and the suite docstring named three, and CLAUDE.md is what a future reader consults. It now enumerates all three, each with the "invisible to BOTH halves" qualifier that is what actually makes something a blind spot, and names (2) as a gap the gate created rather than inherited. N8 -- RegistryParseError's docstring claimed to have fixed the misattribution as well as the `python -O` erasure. Only the erasure and the naming are fixed: the exception is still raised from a helper and surfaces under every test that calls _our_codes(). Clause dropped, limitation stated instead. N9 -- kept NET_ERR_CLAIM_VALUE UCI_ERR_LONG_READ and recorded why, so the next reader does not re-derive that it changes no outcome: it does not, and was checked. It stays to keep the invariant total -- every code this repo defines claims its byte, no exception to carry in your head -- and to give the next macro-exempt code an obvious home. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #184.
#185 wrote c64-wireguard's
$8C-$8F/$46-$49allocations into our two headers as prose and closed the "presents them as free" half. The half left open is the one the issue ends on: whether a snapshot plus a pointer is enough. It isn't — this namespace has collided twice ($88, live four days; wg#120's first commit over our$41-$45) and prose caught one of the two.Two halves, each covering the other's blind spot
src/net_err_registry_asserts.s— picked up by$(wildcard src/*.s), so it assembles into every build on both backends and emits no bytes (present in all seven maps checked, claiming zero segments in every one). It holds the peer's codes asNET_ERR_PEER_*equates and asserts that no code of ours lands on one, that each is in family range, and that no published value is reassigned.NET_ERR_CLAIM_VALUEdefines.ident(.sprintf("NET_ERR_TAKEN_%02X", val)), so two of our own names on one byte is a ca65 redefinition error naming the byte — O(n), no list to maintain. Scopeerror, so ca65 settles everything at the offending object rather than at the link.tools/test_net_err_registry.py— 10 checks, pure logic, milliseconds. Parses the headers, so it catches codes that never reached the asserts TU (which is what makes the assembler half's coverage safe), and diffs our snapshot's values and names against the live peer registry.Scope of the guarantee, stated in all four documentation sites
Under-coverage. The suite reads
NAME = $hh,NAME = ddd,.define NAME $hhand nothing else. Three things are invisible to both halves: an expression-valued code that is also never registered; a code not named with_ERR_that is also never registered (this class is created by the name gate below — measured, 3 failures before it, 0 after); and a bare inlinelda #$8Cwith no equate (latent — no such site exists). Note the qualifier: neither the expression limit nor the registration limit is a gap on its own, since ca65 evaluates a registered expression-valued code fine.Over-coverage. The suite reads whole headers that also hold ordinary constants, so it gates on the
_ERR_infix. Without it a futureUCI_HOST_BUF_MAX = 64would be reported as an ip65-family error code needing allocation in a peer repo's registry — and buffer sizes favour 64 and 128, i.e.$40and$80, the first byte of each family. The gate's cost is bounded because the assemble-time half is name-blind and catches such a code the moment it is registered.A fresh clone with no c64-wireguard checkout is RED by design
A missing peer checkout is an involuntary skip under
tools/_skip_policy.py(#158/#165/#178): the four cross-repo checks fail rather than pass quietly, because the check is applicable and unverified and exit 0 would say otherwise.C64_NO_PEER_REGISTRY=1is the loud opt-out — its own variable, deliberately notC64_ALLOW_SKIP, which also gatestest_build_flags_stamp.py's toolchain prerequisite and would otherwise be silenced by anyone quieting this suite.Red-green
Sixteen injected cases across three review rounds. ca65 red on a peer collision inside a real
make, on a reassigned published value, and on an intra-repo duplicate under both backends (no PRG produced). The suite red on an unregistered code, an unregistered duplicate, the decimal and.definespellings, an unasserted snapshot row, and a simulated peer rename. Plus three expected-GREEN cases capturing the documented limits, so they are demonstrated rather than claimed — the reviewer confirmed the expression case emits no diagnostic at all rather than passing incidentally.The case worth reading twice: an unregistered
$8Cpasses ca65 and is caught by the suite; an intra-repo duplicate passes the suite's peer checks and is caught by ca65. That mutual coverage is why both halves exist rather than one.Cost
PRG sha256 byte-identical to master on all five profiles, re-verified after each of three review rounds:
Independently rebuilt by the reviewer across seven configurations, including the 14 B-margin wikipedia target and the armed
ENABLE_P384_VERIFY=1link..ident/.sprintfcollateral probed: out-of-family values report the range error rather than aNET_ERR_TAKENartefact,$1234and0-1assemble cleanly, and the namespace appears nowhere outside the TU.Root
pytest: 163 passed.Cross-repo
No collision at
c64-wireguard@cf7b41e; all 16 of our codes appear in their registry with the ownership we claim.Review
Three adversarial rounds. Round 1 blocked on a crash (
pytest.skip()raises aBaseExceptionthe standalonemain()did not catch, killing the suite mid-run and reporting "failed" when it meant "not verified") and on prose claiming a completeness the code lacked. Round 2 withdrew that block and found the guard covered exactly one spelling of an equate. Round 3 cleared it.Three of three rounds caught a false mechanism claim in this branch's prose — "the assembler cannot express this" (falsified by a four-line
.identexperiment, which then became the strongest guard here), "expressions pass both halves while colliding" (too pessimistic), and "the pre-existing limit this does not widen" (the gate created the class). All three were scope claims written in the same breath as a tested result and not given the same scrutiny; all three survived a green suite and an unmoved hash; each was falsifiable in about a minute. Recorded because it is the failure mode this repo keeps paying for.Follow-ups, not in this PR
libs/nistcurves' in-tree pin pluscheck_upstream_pins.py. That would make a fresh clone green and honest at once.src/net/net_families.inchas the identical unchecked cross-repo copy problem; noted in CLAUDE.md, needs the wireguard lane.🤖 Generated with Claude Code