SAFETY-38 round 3: pin the banner the SHIPPED install actually prints
checks / unit (pull_request) Failing after 3s
checks / unit (pull_request) Failing after 3s
Round 2 pinned the operand order of `SignWatchdog.evaluate()` at main.py:1620 -
the SITE detector. Part 9c asserts that detector is never armed at a roster of
one, so on every install this add-on has, the PER-UNIT banner is the only
sign-disagreement sentence that can ever be printed, and its call site is a
different line. main.py:1560 was left unpinned.
1. THE SAME ZERO-BYTE DEFECT ONE CALL SITE OVER.
`evaluate(stood, against, ...)` -> `evaluate(against, stood, ...)`:
app/main.py 234 542 bytes before and after, delta 0, fingerprint
61aa413f... -> e57e6026..., bytecode purged, tree mounted :ro, line re-read
from disk. On the round-2 tree the FULL 21-FILE SUITE PASSES.
Nothing behavioural could ever have caught it. `watchdog.disagrees()` is
SYMMETRIC in its first two arguments - `commanded_w * against_w < 0` with an
abs() gate on both - so the mutant takes identical actions on every input.
Not "no fixture happens to separate them": no fixture CAN.
What moves is the sentence. Measured by diffing the suite's own printed
banners between the round-3 tree and the same tree carrying only this edit:
9 emitted banner lines invert, in 3 distinct sentences.
shipped commanded -2000 W against a meter reading +2000 W
swapped commanded +2000 W against a meter reading -2000 W
shipped commanded +1000 W against its own measured power -1000 W
swapped commanded -1000 W against its own measured power +1000 W
shipped commanded -2000 W against a meter reading +4000 W (the N=1 arm)
swapped commanded +4000 W against a meter reading -2000 W
Persisted to `sign_reason` and re-read after the restart the latch exists to
survive; also the action log, `_LOG.error` and `status_payload()`'s headline.
CLOSED by the ORDERED PAIR on both per-unit banners, both operands read off
the OBSERVED CYCLE - `inv.last_write` and `inv.batt` off the objects the
plant moved, never off the arguments the detector was handed, which would
compare the swap against itself:
- part 3, the N>=2 trip: "commanded {stood:+.0f} W against its own
measured power {meas:+.0f} W", plus the persisted namespaced copy;
- part 7, the N=1 controller-driven trip: "commanded {stood:+.0f} W
against a meter reading {grid:+.0f} W", plus the namespaced key AND the
flat rollback mirror;
- a PRECONDITION on each that the two operands are different numbers of
opposite sign - without it a transposition produces the same string and
the check is decoration.
Measured after the fix: RED on four named checks in test_watchdog.py, GREEN
in the other twenty files. M11 (the site swap) re-measured on this tree:
still red. Round 1's and round 2's kills are intact.
2. A FALSE STATEMENT ROUND 2 INTRODUCED, AND IT WAS WORSE THAN REPORTED.
Section 4 said "three of these left the whole suite green - M1, M2 and M3 -
and the remaining seven were caught by SAFETY-30's own N>=2 checks". Round
1's vaguer wording was harmless; naming three made it specific and wrong.
Re-measured mutation by mutation on clean release/1.0 @ bbc903b, each on a
fresh copy, bytecode purged, byte delta and fingerprint printed, FULL 21-FILE
SUITE each time:
M1 +24 B fp 362374cf... 21/21 PASS - SURVIVOR
M2 +9 B fp 86573e74... 21/21 PASS - SURVIVOR
M3 +4 B fp 59fa8815... 21/21 PASS - SURVIVOR
M4 +9 B fp 43d852be... 21/21 PASS - SURVIVOR
M5 +16 B fp 58d26de8... RED test_watchdog.py, 3 checks
M6 +1 B fp 407ddb5f... RED test_watchdog.py, 3 checks
M7 -11 B fp 2a635a4e... 21/21 PASS - SURVIVOR
M8 +6 B fp b21eed89... 21/21 PASS - SURVIVOR
M9 +6 B fp 0973d3ca... 21/21 PASS - SURVIVOR
M10 +7 B fp 0e7b7a4f... 21/21 PASS - SURVIVOR
EIGHT survived, not three - and not the four a reviewer proposed either.
The corrected sentence is in DECISIONS section 4.
AND THE TWO THAT WERE CAUGHT MAKE THE CARD'S CASE. M5 (`sum(abs ...)`) and
M6 (`-sum`) are the only two entries that reverse the SIGN. Every other one
changes only the MAGNITUDE, and `_sign_site` - the suite's only N>=2 site
fixture before this card - runs shares of about -2500 W each against a 320 W
gate, so sum / mean / max / min / sorted[-1] / [0] / self.target / half /
double land on -2500, -2500, -2500, -2500, -2500, -2500, -2500, -1250 and
-5000 W: nine numbers, ALL ON THE SAME SIDE OF THE SAME THRESHOLD. The
pre-existing coverage could see a sign and nothing else.
3. THEN THE THING THAT WOULD HAVE CAUGHT BOTH: the SYMMETRIC-PREDICATE FAMILY,
enumerated by machine rather than by hand for a third time.
`tools/symmetric_message_scan.py` renames each parameter pair, erases every
formatted string, canonicalises the commutative operators and iterates to a
FIXPOINT - the symmetry is usually one call deep, and a single pass reports
an EMPTY family on a tree whose answer is already known. A second census
covers sentences the CALLER builds. Both print their own blind spots,
including the one way the scan OVER-reports.
It found one open survivor this card does not close and now discloses:
rail_unreachable()'s dev_min/dev_max at main.py:3208 - 0 bytes, 21/21 green,
a status-page NOT COVERED row - and twelve unpinned caller-built sentences,
including SAFETY-31's `circ_reason`, which is the same shape and is also
persisted and re-read after a restart. DECISIONS section 7 has the list, the
pinned status of every member, and what the instrument cannot see.
No app/ code changes. MUTATION.md gains habit 15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -111,6 +111,56 @@ shipped `sum` is correct and must not be "fixed".**
|
||||
unit's share and never the site target. A site asking for −500 W while −250 W
|
||||
stands would then take a **latching site-wide false trip** under its own
|
||||
300 W gate. Closed by part 9a's device-clamped counterfactual.
|
||||
* **⚠️ AND THE SAME ZERO-BYTE DEFECT ONE CALL SITE OVER — THE ONE A SHIPPED
|
||||
INSTALL CAN ACTUALLY SEE.** The ordered pair went onto `main.py:1620`, the
|
||||
**site** detector. That detector is armed only at N≥2, and part 9c asserts it
|
||||
is **absent at a roster of one** — so on every install this add-on has, the
|
||||
**per-unit** banner is the only sign-disagreement sentence that can ever be
|
||||
printed, and its call at **`main.py:1560`** was still unpinned. The identical
|
||||
swap there is **0 bytes** and left the tree **21/21 green**, and nothing
|
||||
behavioural could ever have caught it: `disagrees()` is **symmetric** in those
|
||||
two arguments (`commanded_w * against_w < 0`, `abs()` gate on both), so the
|
||||
mutant takes **identical actions on every input**. What moves is the sentence
|
||||
— measured, **9 emitted banner lines invert in 3 distinct sentences**,
|
||||
including the one persisted to `sign_reason` and read back after a restart:
|
||||
*"commanded +1000 W against its own measured power −1000 W"* becomes
|
||||
*"commanded −1000 W against its own measured power +1000 W"*. Closed by the
|
||||
ordered pair on **both** per-unit banners — the N≥2 `its own measured power`
|
||||
one and the **N=1** `a meter reading` one — with both operands read off the
|
||||
**observed cycle** rather than off the arguments the detector was handed, plus
|
||||
a precondition on each that the two operands really are different and opposite
|
||||
in sign. **Red on four named checks, green in the other twenty files. No
|
||||
`app/` change.**
|
||||
* **⭐ AND THE FAMILY IS NOW ENUMERATED BY MACHINE, because it had been
|
||||
enumerated by hand twice and was short both times.**
|
||||
`tools/symmetric_message_scan.py` finds every call whose argument order is
|
||||
load-bearing for a **message** rather than for a decision: it renames each
|
||||
parameter pair, erases every formatted string, canonicalises the commutative
|
||||
operators and iterates to a **fixpoint** (the symmetry is usually one call
|
||||
deep — `evaluate()` is symmetric only *through* `disagrees()`), then
|
||||
classifies each sentence by where it goes. A second census covers sentences
|
||||
the **caller** builds. It found **one open survivor this card does not close
|
||||
and now discloses** — `rail_unreachable()`'s `dev_min`/`dev_max` at
|
||||
`main.py:3208`, **0 bytes, 21/21 green**, a status-page row — and **twelve**
|
||||
unpinned caller-built sentences including SAFETY-31's `circ_reason`, which is
|
||||
the same shape and is also persisted. The list, the pinned status of every
|
||||
member and the instrument's own blind spots are in
|
||||
`DECISIONS/SAFETY-38-site-standing-pin.md` §7.
|
||||
* **⚠️ AND A LEDGER SENTENCE THIS CARD ITSELF GOT WRONG, CORRECTED WITH THE
|
||||
MEASUREMENT.** An earlier revision said *"before part 9 existed, **three** of
|
||||
these left the whole suite green — M1, M2 and M3 — and the remaining seven
|
||||
were caught by SAFETY-30's own N≥2 checks"*. **Eight** did. Re-run one at a
|
||||
time on clean `release/1.0` @ `bbc903b`, fresh copy each, bytecode purged,
|
||||
byte delta and `main.py` fingerprint printed, **full 21-file suite** every
|
||||
time: M1, M2, M3, M4, M7, M8, M9 and M10 are all **21/21 PASS**; only M5 and
|
||||
M6 go red. It is the same species of error this round exists to fix — **a
|
||||
survivor recorded as a kill** — and the file contradicted itself three times
|
||||
while saying it. **And the two that ARE caught make the card's case:** M5
|
||||
(`sum(abs …)`) and M6 (`-sum`) are the only two that reverse the **sign**.
|
||||
Every other entry changes only the **magnitude**, and the old `_sign_site`
|
||||
fixture's ~−2500 W shares put nine different magnitudes on the **same side of
|
||||
the same 320 W gate**. The pre-existing coverage could see a sign and nothing
|
||||
else.
|
||||
* **13 mutations — 11 of that line and its call site, plus 2 zero-byte edits to
|
||||
the arbiter claim wiring — each on a fresh full-repo copy mounted `:ro`,
|
||||
bytecode purged by `tools/mutation_run.sh`, the edit re-read from disk and the
|
||||
|
||||
Reference in New Issue
Block a user