SAFETY-38 round 3: pin the banner the SHIPPED install actually prints
checks / unit (pull_request) Successful in 5m12s

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:
2026-09-05 20:39:58 +02:00
co-authored by Claude Opus 5
parent 564ba11c11
commit 357cb4f07f
5 changed files with 1095 additions and 8 deletions
@@ -231,15 +231,73 @@ fingerprint, and verifies no cache appears during the run). The mutated line was
| M8 | `sum(standing.values()) / 2.0` | +6 | **RED** |
| M9 | `sum(standing.values()) * 2.0` | +6 | **RED** |
| M10 | `sorted(standing.values())[-1]` — largest signed | +7 | **RED** |
| **M11** | **argument SWAP** at the call site: `evaluate(site_standing, self.grid, …)` → `evaluate(self.grid, site_standing, …)` (main.py:1620) | **0** | **was a SURVIVOR in round 1 — now RED**, see §4a |
| **M11** | **argument SWAP** at the SITE call site: `evaluate(site_standing, self.grid, …)` → `evaluate(self.grid, site_standing, …)` (main.py:1620) | **0** | **was a SURVIVOR in round 1 — now RED**, see §4a |
| **M12** | **argument SWAP** at the PER-UNIT call site: `evaluate(stood, against, …)` → `evaluate(against, stood, …)` (**main.py:1560**) | **0** | **was a SURVIVOR in round 2 — now RED**, see §4c |
**Every one of the ten is red in `test_watchdog.py`.** Re-measured in round 2
on `release/1.0` @ `86eaef2` (where `app/main.py` is 234 542 bytes, 2 022 more
than the `f72c48d` the round-1 figures were taken on): all ten still red, at the
byte deltas in the table, each on a fresh copy with the edit re-read from disk.
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,
none of which grade the *quantity*.
Adding assertions cannot turn a red green, so those verdicts carry forward to
the round-3 tree unchanged.
### ⚠️ ROUND 2 SAID THREE OF THESE SURVIVED. **EIGHT DID.**
Round 2 wrote: *"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."* Round 1's wording was vaguer and harmless; naming three made it
**specific and wrong**, and it is the same species of error the round was
written to fix — **a survivor recorded as a kill**. This file contradicted it
three times on its own pages: the ledger row calls M7 *"a SURVIVOR — now RED"*,
§5 says M7 survived the whole suite, and `CHANGELOG.md` says so too.
**Re-measured in round 3, one mutation at a time, on clean `release/1.0` @
`bbc903b`** — a fresh copy each, bytecode purged (`0` remaining, printed), the
line re-read from disk, byte delta **and** `app/main.py` fingerprint printed,
mounted `:ro`, and the **FULL 21-FILE SUITE** run each time rather than
`test_watchdog.py` alone. (`release/1.0` has since moved on to `9e42cec`, which
merged [[CI-06]]; that commit touches `test_peakguard.py` and `CHANGELOG.md`
only, neither of which grades `site_standing`, so the ten verdicts below stand
on the tree they were taken on and this branch is rebased onto the newer tip.)
| # | Δ bytes | fingerprint of `app/main.py` | suite before part 9 |
|---|---|---|---|
| M1 | +24 | `362374cfb6486378` | **21/21 PASS — SURVIVOR** |
| M2 | +9 | `86573e7494f1c933` | **21/21 PASS — SURVIVOR** |
| M3 | +4 | `59fa8815e6fda4cf` | **21/21 PASS — SURVIVOR** |
| M4 | +9 | `43d852be3037f861` | **21/21 PASS — SURVIVOR** |
| M5 | +16 | `58d26de82af29a1a` | **RED** — `test_watchdog.py`, 3 checks |
| M6 | +1 | `407ddb5f375cc217` | **RED** — `test_watchdog.py`, 3 checks |
| M7 | **−11** | `2a635a4eca0a790d` | **21/21 PASS — SURVIVOR** |
| M8 | +6 | `b21eed89ff4c9240` | **21/21 PASS — SURVIVOR** |
| M9 | +6 | `0973d3caf680db65` | **21/21 PASS — SURVIVOR** |
| M10 | +7 | `0e7b7a4fed7787d0` | **21/21 PASS — SURVIVOR** |
**So: EIGHT of the ten left the whole suite green, and TWO were caught** — not
three and seven, and not the four and six a reviewer proposed either. The
correct sentence is: *before part 9 existed, eight of these ten left the whole
21-file suite green, and the only two SAFETY-30's own N≥2 checks caught were M5
and M6.*
**⚠️ AND THE TWO THAT WERE CAUGHT ARE CAUGHT FOR A REASON THAT MAKES THE CARD'S
CASE RATHER THAN WEAKENING IT.** M5 (`sum(abs(v) …)`) and M6 (`-sum(…)`) are
the only two entries in the table that **reverse the SIGN** of `site_standing`.
Every other entry changes only its **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. `sum`, the mean, `max(key=abs)`, `min(key=abs)`,
`sorted(...)[-1]`, `[0]`, `self.target`, half the sum and twice the sum all land
on **−2500, −2500, −2500, −2500, −2500, −2500, −2500, −1250 and −5000 W**: nine
different numbers, all on the **same side of the same threshold**. The three
checks that fired on M5 and M6 grade *"the site trips and no unit does"*, the
claim it trips through, and where the latch is written — none of them grades the
**quantity**, and the sign flip reaches them only because it stops the trip
happening at all.
That is the whole thesis of this card measured rather than argued: **the
pre-existing coverage could see a sign, and nothing else.** Part 9's fixture —
per-unit shares each **below** the gate whose **sum** clears it — is the only
arrangement in which the magnitude is load-bearing, and it is what takes the
other eight red.
⚠️ **ONE FALSE RED, AND IT IS [[CI-06]]'s.** In the batch run M10 also reported
`FAIL test_peakguard.py`. Re-run in isolation **three times** and once more
@@ -250,6 +308,17 @@ and that **no claim in this card rests on it**. `test_peakguard.py` is the file
CI-06 records as flaky, and this is the behaviour it records. Believe no red
from it without three isolated re-runs.
⚠️ **ROUND 3 CAN NAME ONE, AND IT IS THE SAME FILE.** Round 3's own batch — the
ten full-suite runs above, plus §7's sibling swaps — produced **no** red from
`test_peakguard.py` on M10 or on any other ledger entry. It produced exactly one
elsewhere, on a **`static_tariff.py`-only** edit (§7b, A4), and this time the
failing check *was* captured: *"one cycle of the intent form is exactly the
delivered form plus the command standing on the wire"* — which is CI-06's
**mechanism 1 by name**, the cross-time projection. `test_peakguard.py` then
passed **four for four** in isolation on that same tree. That is the first time
this card has been able to put a name to one of these reds, and it is the name
CI-06 predicts.
⚠️ **M3 IS NOT "FOR COMPLETENESS", AND ROUND 1 SAID IT WAS.** Round 1 recorded
M3 as already closed structurally by SAFETY-32 and disclaimed it. §1 above has
the correction and the mechanism: `[0]` **survives** a full green 21-file suite
@@ -321,6 +390,73 @@ coverage rather than merely inheriting it. **W2 is caught entirely by
pre-existing checks** — SAFETY-38 adds nothing to circulation, and does not
claim to.
### 4c. ⚠️ The SAME zero-byte defect one call site over — and it is the one the shipped install can see
Round 2 closed §4a by asserting the ordered pair on the **site** banner at
`main.py:1620`. It did not touch `main.py:1560`, the **per-unit** call, and the
identical swap there —
```python
if inv.sign_wd.evaluate(stood, against, self.following, now) # shipped
if inv.sign_wd.evaluate(against, stood, self.following, now) # M12
```
— left the round-2 tree **21/21 green**. Measured: `app/main.py` 234 542 bytes
before and after, **Δ 0 bytes**, fingerprint `61aa413f…` → `e57e6026…`, bytecode
purged, tree mounted `:ro`, the line re-read from disk.
**Nothing behavioural can catch it, and that is structural rather than
unlucky.** `watchdog.disagrees()` is *symmetric* in its first two arguments —
`commanded_w * against_w < 0`, with an `abs()` gate on **both** — so the swap
cannot change a single trip decision on any input. What it changes is the
**sentence**. Measured by diffing the suite's own printed banners between the
round-3 tree and the same tree carrying only M12: **9 emitted banner lines
invert, in 3 distinct sentences.**
```
shipped: SIGN_DISAGREEMENT: commanded -2000 W against a meter reading +2000 W …
M12: SIGN_DISAGREEMENT: commanded +2000 W against a meter reading -2000 W …
shipped: … commanded +1000 W against its own measured power -1000 W …
M12: … commanded -1000 W against its own measured power +1000 W …
shipped: … commanded -2000 W against a meter reading +4000 W … (the N=1 arm)
M12: … commanded +4000 W against a meter reading -2000 W …
```
**⚠️ AND IT IS WORSE THAN THE ONE ROUND 2 CLOSED.** Part 9c asserts the SITE
detector is **not armed at a roster of one**, and inverter B does not physically
exist. So on the shipped install the **per-unit banner is the only
sign-disagreement sentence that can ever be printed** — in the action log, in
`_LOG.error`, in `status_payload()`'s headline banner, and in `sign_reason`,
which is persisted and read back after the restart the latch is designed to
survive. Round 2 pinned the N≥2-only call site and left the shipping one open.
**Closed by the ORDERED PAIR on both per-unit banners**, with 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 be
comparing the swap against itself:
* `test_watchdog.py` **part 3**, the N≥2 per-unit trip:
`f"commanded {stood:+.0f} W against its own measured power {meas:+.0f} W"`,
plus the persisted `unit/inverter-b/sign_reason` copy, plus a **precondition**
that the two operands are different numbers of opposite sign — without which
a transposition would produce the same string and the check would be
decoration;
* `test_watchdog.py` **part 7**, the **N=1** controller-driven trip:
`f"commanded {stood:+.0f} W against a meter reading {grid:+.0f} W"`, plus the
namespaced key **and** the flat rollback mirror, plus the same precondition.
**Measured after the fix: M12 is RED on four named checks in
`test_watchdog.py` and GREEN in the other twenty files** — one file, four
checks. M11 re-measured on the same tree: still red.
**⚠️ Neither §6a nor MUTATION.md disclosed it.** §6a's "what was NOT closed"
enumerated `circ_wd`, the eight other `CLAIM_SCOPE` entries, `actuator_of()`'s
body and the `n_units > 1` guard — not this. MUTATION.md's own closing rule, in
the file round 2 edited, is *"a variant is not covered because a near-miss of it
is."* §7 is the enumeration that would have caught both instances at once.
## 5. The survivor this card went looking for, and closed
M7 is not one of the card's three. It was found by asking the question the
@@ -406,6 +542,184 @@ disk, byte delta printed):**
re-verified on `86eaef2` for **M3** and **M11** (both full 21-file runs) and is
otherwise carried forward from round 1's measurement on `f72c48d`. It is a
weaker statement than round 1 made and is written down as such.
⚠️ **Round 3 closed the other half of this**, but only in the *before*
direction: all ten were re-run on clean `release/1.0` @ `bbc903b` under the
**full 21-file suite** to establish which of them the pre-existing coverage
could see — see §4's ⚠️ paragraph, and the answer was **eight survivors, not
three**. The *after* direction — "each of the ten is red in
`test_watchdog.py` and green in the other twenty **on the round-3 tree**" —
is still measured end-to-end only for **M3, M11 and M12**. For the other
seven, "red" carries forward from round 2 (which adding assertions cannot
undo) and "green elsewhere" carries forward from round 1.
* **`test_peakguard.py`'s CI-06 flake is not fixed by this card** and is not
this card's subject. It is *characterised* here (five isolated runs on the M3
tree, all PASS) only far enough to overturn the false kill it produced.
### 6b. Round 3: what was closed, and what is still open
**Closed in round 3:**
1. **M12**, the zero-byte argument swap at the **per-unit** call site
`main.py:1560` — §4c. It was a survivor of the round-2 tree at **21/21
green**, and it is the **only** sign-disagreement banner a shipped
one-inverter install can print. Red on four named checks, green in the other
twenty files.
2. **The ledger arithmetic in §4**, which round 2 made specific and wrong. See
§4's own ⚠️ paragraph: re-measured mutation by mutation on clean
`release/1.0` @ `bbc903b`, full 21-file suite each time.
3. **The family itself is now enumerated by machine** —
`tools/symmetric_message_scan.py`, §7 — rather than by hand for a third time.
**Open, named, and NOT claimed:**
* **A3 and B1** — the only two status-page rows in which the freeze detector
ever names a device range: the **NOT COVERED** row and the **armed** row's
"below max_w because …" clause. `rail_unreachable()`'s `dev_min`/`dev_max` at `main.py:3208` and the
literal `f"only {inv.dev_min}...{inv.dev_max} W"` at `main.py:3231`. Both
measured: **0 bytes each, 21/21 green each.** Real, open members of the
family. They are **status-page rows rather than latching alarms**, and closing
them wants a fixture with a device whose two bounds are **asymmetric** — every
fixture in this repository uses `±d`, so the transposition is currently
invisible by construction — which belongs to `test_freeze.py`'s card, not to
this one.
* **Twelve census-B sites** (§7c, B2–B12) determined unpinned at the **source
level** — the distinguishing literal appears in no `test_*.py` — and **not**
mutated. Weaker evidence than A1–A3, and written down as such.
* **B6**, SAFETY-31's `circ_reason`, is the same shape as the defect this card
closed for SAFETY-02/30 and is *also* persisted and re-read after a restart.
§6a already declines to touch `circ_wd`; §7d says what that costs.
* Everything §6a lists stays open: `circ_wd`'s own thresholds, the eight other
`CLAIM_SCOPE` entries, `actuator_of()`'s body, and the `n_units > 1` guard.
## 7. The symmetric-predicate family — every call whose argument order is load-bearing for a MESSAGE
**Why this section exists, and it is the most useful thing in the card.** Round 2
pinned the operand order of `SignWatchdog.evaluate()` at `main.py:1620`. Round 3
found the **identical zero-byte defect at `main.py:1560`**, one call site over —
and that one is worse, because `test_watchdog.py` part 9c asserts the site
detector is **never armed at
a roster of one**, so on the shipped install the per-unit banner is the **only**
sign-disagreement sentence that can ever be printed. Twice in one card the
enumeration was written by hand and twice it was short. MUTATION.md habit 12: *a
hand-written scope is the same defect as a hand-written fixture.* So the family
is now **computed**, by `tools/symmetric_message_scan.py`.
### 7a. What makes this family invisible, and why it is not a fixture problem
Every other defect in this document is a fixture problem: *no fixture happens to
separate the mutant from the shipped code.* Build the right fixture and it goes
red. This one is not that.
Where a callee is **symmetric** in two arguments, a swap at a call site cannot
change **any** action on **any** input. `watchdog.disagrees()` is
`commanded_w * against_w < 0` with an `abs()` gate on **both** sides: the
product commutes and the two gates are the same gate. There is nothing for a
behavioural check to observe, ever — **not "no fixture separates them" but no
fixture *can*.** The only surface that moves is the **sentence**, and the only
check that can see it is one asserting the **ordered pair** (MUTATION.md habit
14) at **that** call site.
Two consequences the earlier rounds got wrong:
* **The unit of coverage is the CALL SITE, not the function.** `evaluate()`'s
operand order was pinned and the defect still shipped, because the pin was on
the *other* call.
* **A behavioural suite is not weak evidence here, it is NO evidence.** "21/21
green" is what this mutation is *guaranteed* to produce. Measured, twice: M11
at `main.py:1620` on the round-1 tree, and M12 at `main.py:1560` on the
round-2 tree.
### 7b. Census A — the callee formats its own parameters
`tools/symmetric_message_scan.py` renames each parameter pair, erases every
formatted string, canonicalises the commutative operators (`*`, `+`, `and`,
`or`, `==`, `min`/`max`, iterated tuple literals) and compares the **bodies**;
it iterates to a **fixpoint**, because the symmetry is usually one call deep —
`evaluate()` is symmetric only *through* `disagrees()`, and a single pass
reports an empty family on a tree whose answer is already known. It then
classifies each sentence by **where it goes**.
Fixpoint on this tree: **3 rounds, 17 interchangeable positional and 5 keyword
pairs across 14 functions**. 45 parameter pairs reach a sentence; **8** are
symmetric — **5** whose sentence goes only to an operator, and **3** whose
sentence is the **return value** and therefore has to be classified by hand,
because a returned string is sometimes a KEY and sometimes a SENTENCE.
| # | scan row | pair | called at | swap costs | reaches an operator via | pinned? |
|---|---|---|---|---|---|---|
| **A1** | `watchdog.py:613` `SignWatchdog.evaluate()` | `(commanded_w, against_w)` | **`main.py:1560`** — per-unit, **and the N=1 detector** | **0 B** | `sign_reason` (persisted), the action log, `_LOG.error`, `status_payload()`'s headline banner | **YES — round 3.** `test_watchdog.py` part 3 (`its own measured power`) and part 7 (`a meter reading`), on the contiguous literal with both operands read off the observed cycle. **Measured: 0 B, red on 4 named checks, green in the other 20 files.** |
| **A2** | same row, second call site | `(commanded_w, against_w)` | **`main.py:1620`** — site | **0 B** | `site_sign_reason`, the action log, the status page | **YES — round 2**, part 9a. Re-measured on the round-3 tree: still red. |
| **A3** | `freeze.py:405` `FreezeWatchdog.evaluate()` and `freeze.py:311` `rail_unreachable()` — both symmetric through `rail_ceiling_w()`'s `for bound in (dev_min, dev_max)` | `(dev_min, dev_max)` | **`main.py:3208`** (and 1928, 2755, 3196, 3219) | **0 B** | the status page's `freeze detector: NOT COVERED — the setpoint device reports a range of …` row | **NO.** Measured: swapped at `main.py:3208-3209`, **0 bytes**, `main.py` fingerprint `61aa413f…` → `2305b560…`, **21/21 green**. Disclosed in §7d, not closed. ⚠️ `rail_unreachable()` is the row the scan puts in its **RETURN** bucket: its returned string is a **sentence**, not a key, so it is a family member after all — which is exactly why that bucket says *read by hand*. |
| **A4** | `static_tariff.py:522` `_raise_gap()` | `(start, end)` | `static_tariff.py:509`, `:513` | **0 B** | the `ProfileInvalid` refusal an operator reads at startup | **YES — pre-existing.** Measured: both call sites swapped, **0 bytes**, `static_tariff.py` fingerprint `caef7fde…` → `50e9a768…`, **red in `test_static_tariff.py` on two named checks** (*"…and names the uncovered range so the operator can fix it"*, *"…and the refusal names the uncovered range"*), because `_hhmm(start)}-{_hhmm(end)` is asserted as the contiguous literal `"22:00-23:00"`. ⚠️ That batch run also reported `FAIL test_peakguard.py` on a `static_tariff.py`-only edit; the failing check is *"one cycle of the intent form is exactly the delivered form plus the command standing on the wire"*, which is [[CI-06]]'s **mechanism 1 by name**, and `test_peakguard.py` passes **four for four** in isolation on that same tree. No claim here rests on it. |
| A5–A6 | `static_tariff.py:522` `_raise_gap()` | `(start, weekday)`, `(end, weekday)` | — | — | — | **not real — the scan's one over-report.** It erases the sentence, so it cannot see that `DAY_NAMES[start]` with `start = 1320` raises `IndexError`. A mixed-type pair fails loudly, which is the opposite of this family. |
| A7–A8 | `store.py:177` `unit_key()`, `mqtt.py:222` `unit_state_key()` | `(unit, key)`, `(slug, key)` | many | 0 B | — | **not this family.** The formatted string is the **return value and it is a KEY**: a swap renames a store key and an MQTT topic, which is behaviour every ordinary check can see. MAINT-05 is the card about it. |
### 7c. Census B — the caller builds the sentence
Census A only sees a callee formatting its **own** parameters, which was gap 2
of its own honest-gap list. The other half never crosses a function boundary:
an operator sentence built where it is printed, interpolating two values that
are **interchangeable by shape**. No predicate is involved at all, so census A's
symmetry test has nothing to test. The scan pairs interpolated expressions that
are **one leaf apart** — `inv.dev_min` / `inv.dev_max`, `cfg.min_w` /
`cfg.hold_s` — which is exactly what a transposition swaps, and it keeps only
sentences whose sink is a **message**: an f-string that becomes a URL or a store
key is data, and a swap there is an ordinary behavioural change. **16
candidates**, every one of them a **zero-byte** edit:
| # | site | pair | reaches an operator as | pinned? |
|---|---|---|---|---|
| **B1** | `main.py:3231` | `inv.dev_min` / `inv.dev_max` | status page, `… accepts only −120…130 W` | **NO — MEASURED.** Transposed in place, **0 bytes**, `main.py` fingerprint `61aa413f…` → `ac36a97b…`, purged, `:ro`, **21/21 green**. The row then tells the operator the device accepts `130…−120 W`. |
| **B2** | `main.py:3362` | `inv.dev_min` / `inv.dev_max` | status page, `setpoint … (range −120…130 W)` | **NO** — the literal `setpoint … (range` is in no `test_*.py`. |
| **B3** | `main.py:3435` | `sign_wd.cfg.hold_s` / `.cfg.min_w` | status page, `sign watchdog: armed (trips after 180 s opposed, both over 300 W)` | **NO** — `s opposed` is in no `test_*.py`. |
| **B4** | `main.py:3460` | `site_sign_wd.cfg.hold_s` / `.min_w` | status page, the site sign row | **NO** — `armed against the sum` is in no `test_*.py`. |
| **B5** | `main.py:3491` | `circ_wd.cfg.hold_s` / `.min_w` | status page, the circulation row | **NO** — `circulation watchdog: armed` is in no `test_*.py`. |
| **B6** | `circulation.py:317` | `cfg.min_w` / `cfg.hold_s` | **`circ_reason` — persisted, and re-read after a restart** | **NO** — `test_watchdog.py` asserts only `"CIRCULATING_POWER" in reason`. **This is SAFETY-31's twin of the exact defect this card just closed for SAFETY-02/30**, on a string with the same persistence. |
| **B7** | `main.py:2454` | `self.site_max_w` / `self.target` | the clamp action-log line | **NO** — `was clamped to` is in no `test_*.py`. |
| **B8** | `main.py:3386` | `clamp_wd.engagements` / `.episodes` | status page, `engaged N time(s) in M episode(s)` | **NO** — `time(s) in` is in no `test_*.py`. |
| **B9–B11** | `main.py:3169` (three pairs) | `p1.net_w` / `.samples` / `.parse_errors` | status page, the P1 row | **NO** — `P1 meter (` is in no `test_*.py`. |
| **B12** | `arbiter.py:264` | `c.source` / `c.actuator` | `scope_errors`, the arbiter's mis-routing refusal | **NO, and this one is instructive.** `test_arbiter.py:452` says it *"names the source and the actuator it does not belong on"* and then asserts `"peak" in r_mis.scope_errors[0]`. Under the swap the sentence becomes `"inverter-b: a SITE claim filed on unit 'peak'"` — and `"peak" in …` is **still true**. A claim wider than its assertion, which is habit 14's own shape. |
| **B13** | `validate.py:723` | `meter_phases` / `meter_max_age_s` | the startup refusal | **NO** for the ordered pair. |
| **B14** | `validate.py:1211` | `arbitrage_soc_floor` / `arbitrage_soc_ceiling` | the startup refusal | **NO** for the ordered pair. |
| **B15** | `watchdog.py:667` | `cfg.min_w` / `cfg.hold_s` | the `SIGN_DISAGREEMENT` sentence's `(limits: both over 300 W, held over 180 s)` clause | **YES**, and it is worth naming because it shows the habit works. `test_watchdog.py`'s SAFETY-30 section asserts the site instance's **whole sentence with `==` against a literal**, the `(limits: …)` clause included. Full-string equality is habit 14 taken to its limit. |
| B16 | `main.py:237` | `i.name` / `i.max_w` | the roster log line | **not real** — a `str` beside a `:.0f` format. The same over-report as A5–A6: a mixed-type transposition raises rather than lying. |
### 7d. What this enumeration does NOT close, with sizes
* **A3 is a measured, open survivor.** `dev_min`/`dev_max` transposed in
`rail_unreachable()`'s NOT COVERED row: **0 bytes, 21/21 green.** It is
disclosed here rather than fixed, because closing it wants a `test_freeze.py`
or status-page fixture with a device whose two bounds are **asymmetric** —
every fixture in the repository uses `±d` — and that is a different file's
card. It is a **status-page row**, not a latching alarm.
* **B1 is a second measured, open survivor.** `inv.dev_min` and `inv.dev_max`
transposed in `main.py:3231`'s status row: **0 bytes**, fingerprint
`61aa413f…` → `ac36a97b…`, **21/21 green**. The page then tells the operator
the device accepts `130…−120 W`. Same disposition as A3: **A3 is the row the
freeze detector prints when the rail is UNREACHABLE, B1 the clause the ARMED
row prints when the device is what set the rail** — the two rows in which that
detector ever names a device range, and neither one's operand order is
asserted anywhere.
* **Thirteen further census-B sites are unpinned and UNMEASURED.** B2–B14 above
are determined at the **source level** — the distinguishing literal appears in
no `test_*.py`, or the check that exists asserts a substring wider than its
own claim — and were **not** mutated. That is a weaker statement than A1–A4
and B1, and is written down as one.
* **B12 is a check whose CLAIM is wider than its ASSERTION**, which is habit
14's own shape one module over. `test_arbiter.py:452` says
`scope_violations` *"names the source and the actuator it does not belong
on"* and then asserts `"peak" in r_mis.scope_errors[0]`. Swap `c.source` and
`c.actuator` in `arbiter.py:264` and the sentence becomes
*"inverter-b: a SITE claim filed on unit 'peak'"* — and the substring is still
found. Source-level, not mutated, and not fixed here.
* **B6 is the closest thing to a repeat.** SAFETY-31's `circ_reason` is
persisted and re-read after a restart, exactly like `sign_reason`, and its
two limits are graded by a bare `"CIRCULATING_POWER" in reason`. This card
does not fix it; §6a already declines to touch `circ_wd`, and this is the
precise shape of what that declining costs.
* **The instrument's own blind spots** are printed by the instrument, every
run: semantic symmetry it cannot spell (it under-reports), census B pairs
that are the same *kind* but not the same *shape*, `%s`-style logging and
non-f-string payloads, anything outside `app/`, and the one way it
**over-reports** — it erases the sentence, so a swap that would make the
formatting itself raise is scored as changing no decision (A5–A6 above).