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:
@@ -231,15 +231,70 @@ 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:
|
||||
|
||||
| # | Δ 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 +305,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 +387,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 +539,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).
|
||||
|
||||
Reference in New Issue
Block a user