SAFETY-38 round 4: the enumeration covered one sink, and it was not the one this codebase uses
checks / unit (pull_request) Successful in 5m12s

The scanner's two censuses between them scored ONE syntactic sink and called it
the family. Every one of census B's 16 members was a sentence that is APPENDED,
ASSIGNED to a `reason`-shaped name, or passed as a LOG ARGUMENT. Not one was
RETURNED - and census A's return bucket only ever held a callee formatting its
own PARAMETERS, so a method formatting its own ATTRIBUTES and handing the string
back was invisible to both. That is the whole describe() / explain() /
reason-builder family, which is where this repository actually keeps its
operator sentences. Worse than the gap: it was in NONE of the five blind spots
the tool printed about itself.

THE SCANNER, WIDENED - and it rediscovered both members without being told:

  * CENSUS C, the return sink, resolved through the returning method's CALL
    SITES so a returned KEY (store.unit_key, mqtt.unit_state_key - MAINT-05's
    card) is not mistaken for a returned SENTENCE. 53 returned pairs, 39
    reaching an operator, 16 of those sharing a format spec.
  * A sentence handed to a constructor as `reason=` or placed under a "text"
    dict key is a MESSAGE. That one line of the sink walker was hiding SIX more
    caller-built members, census B 16 -> 22.
  * Every census B and C row now prints whether the two interpolations share a
    FORMAT SPEC - the difference between a transposition that is silent and one
    that merely looks broken.
  * The blind-spot list is now TWO lists: what it scans (printed from the
    constants, so they cannot drift) and WHAT IT DOES NOT SCAN AT ALL, largest
    hole first - `%s`-style lazy logging, MEASURED at 31 calls across 8 modules
    and each one listed, not asserted.

CLOSED, both zero-byte survivors of a green 21-file suite:

  * clamp.py:253 ClampResult.describe(), requested_w <-> applied_w. No
    test_*.py mentioned describe() at all. It is the SS15 action-log row
    (main.py:1366) AND the ClampWatchdog notification reason (main.py:2452) -
    the alarm SAFETY-40 and SAFETY-41 are about. Under the swap the guard
    reports a setpoint clamped UPWARD, to +9999 W by a bound whose rating the
    same sentence gives as 2000 W. Pinned on the ORDERED PAIR on the contiguous
    literal at BOTH consumers, operands read off ctl.target and
    hass.writes[-1] - what the loop asked and what the device was sent - not
    off the ClampResult the swap lives in. 4 checks red.
  * peakguard.py:493 PeakDecision.explain(), projected_w <-> ceiling_w. "peak
    guard idle" was in no test_*.py. The row then reads "projects 5013 W under
    a 2300 W ceiling" - 2713 W OVER - on a row whose own words say idle and
    whose own flag says ok. Pinned on the decision and on the shipped status
    row. 3 checks red.
  * test_arbiter.py's scope-violation check, whose CLAIM was wider than its
    ASSERTION. Measured this round rather than left at source level: 0 bytes,
    21/21 green before, red now. 2 checks.

DISCLOSED, NOT CLOSED - 11 measured zero-byte survivors, each with its file
fingerprint and its 21/21 verdict, in the decision doc SS7e-SS7g. Three in
allocator.py's UnitPlan.explain(), three across arbiter.py's two explain()s,
two in tariff.py, two in arbitrage.py's trade reasons, one in main.py's
SoC-floor notification. An enumerated list with honest gaps beats a fifth round
finding a fifth instance.

CORRECTIONS THIS ROUND OWES:

  * The idle sentence does NOT reach main.py:3299/3302 - those are the
    floor-limited branch, where explain() returns self.reason before the idle
    literal is built. It reaches main.py:3313 and, on the release edge, 1951.
    1951 is disclosed unpinned.
  * SS4's "Every one of the ten is red" sat under a twelve-row table: it is the
    ten site_standing entries.
  * SS6b said "weaker evidence than A1-A3"; A4 was measured, so SS7d's
    "A1-A4 and B1" is the right list and SS6b now says it.
  * AND THE PROBE THAT GRADES THESE MUTATIONS WAS THE DEFECT, ONCE. Its first
    cut numbered the operands BY ORDER OF APPEARANCE, so after a transposition
    the other leaf took the same number and all thirteen mutant renders came
    back byte-identical - which reads exactly like "the edit is inert". Keying
    the value to the NAME fixed it. Habit 4, in the measuring instrument:
    MUTATION.md habit 16 is the new entry, and the count there is corrected to
    eleven rather than inflated.

14 mutations measured: fresh full-repo copy each, mounted :ro, bytecode purged
and the purge COUNT printed, the edit re-read from disk, length and sha256
printed on both sides, and every one required to change the EMITTED SENTENCE
and not merely the bytes. 12 survived the pre-round suite, 1 was already
caught, 3 are caught now. Full 21-file suite green on the final tree.

NOT ONE BYTE OF app/ MOVES. This card changes coverage only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
Claude Opus 5
2026-09-05 22:38:40 +02:00
parent 357cb4f07f
commit e2efc765af
7 changed files with 901 additions and 78 deletions
+67
View File
@@ -222,6 +222,73 @@ shipped `sum` is correct and must not be "fixed".**
it is recorded rather than quietly dropped — unlike the one that *was*
believed, two bullets up. Ledger, and an **enumerated list of what was not
mutated**, in `DECISIONS/SAFETY-38-site-standing-pin.md` §4a, §4b and §6a.
* **⭐ ROUND 4: THE SCANNER COVERED ONE SYNTACTIC SINK AND CALLED IT THE
FAMILY, AND TWO LIVE MEMBERS WERE SITTING IN THE HOLE.** Every one of census
B's 16 members was a sentence that is **appended, assigned, or logged**. Not
one was **RETURNED** — and census A's return bucket only ever held a callee
formatting its own *parameters*, so a method formatting its own **attributes**
and handing the string back was invisible to both. That is the whole
`describe()` / `explain()` family, which is where this codebase keeps its
operator sentences. **Worse than the gap: it was in none of the five blind
spots the tool printed about itself.** The tool now scans the return sink
(census C), resolving it through the returning method's **call sites** so that
a returned *key* — `store.unit_key()`, `mqtt.unit_state_key()` — is not
mistaken for a returned *sentence*; it also scores a sentence handed to a
constructor as `reason=` or placed under a `"text"` dict key, which added
**six more caller-built members** it had been stepping past. Re-run, it
**rediscovered both of the members this round was sent to fix without being
told where to look**.
* **⭐ `ClampResult.describe()` — the clamp alarm the owner has been complaining
about all day — had a SECOND independent way to lie, and no `test_*.py`
mentioned `describe()` at all.** Transposing `requested_w` and `applied_w` in
`clamp.py:253` is a **zero-byte** edit that leaves the guard reporting a
setpoint clamped **UPWARD**, in the measured case to +9999 W by a bound whose
rating the same sentence gives as 2000 W. It is the §15 action-log row
(`main.py:1366`) **and** the ClampWatchdog notification reason
(`main.py:2452`). Measured **21/21 green** before; now **red on 4 checks** in
`test_clamp.py` part 6d, pinned on the **ordered pair** on the contiguous
literal at **both** consumers, with operands read off `ctl.target` and
`hass.writes[-1]` — what the loop asked and what the device was sent — rather
than off the `ClampResult` the swap lives in.
* **⭐ `PeakDecision.explain()` — the status-page banner — the same shape.**
`projected_w` ↔ `ceiling_w` at `peakguard.py:493`, **zero bytes**, and the
row then reads *"quarter projects 5013 W under a 2300 W ceiling"*: a quarter
**2713 W over** its ceiling, on a row whose own words say *idle* and whose own
flag says **ok**. `"peak guard idle"` appeared in no `test_*.py`. Measured
**21/21 green** before; now **red on 3 checks** in `test_peakguard.py` part 13,
pinned on the decision and on the shipped status row at `main.py:3313`.
⚠️ **A correction the ticket is owed:** the idle sentence does not reach
`main.py:3299/3302` — those are the *floor-limited* branch, where `explain()`
returns `self.reason` before the idle literal is built. It reaches
**`main.py:3313`** and, on the release edge, `main.py:1951`.
* **`test_arbiter.py`'s scope-violation check made a claim WIDER than its
assertion** — it said `scope_violations` *"names the source and the actuator
it does not belong on"* and asserted `"peak" in scope_errors[0]`, which is
still true after the two swap roles. Measured this round: **0 bytes, 21/21
green**; now red on 2 checks, pinned on the ordered pair.
* **14 more zero-byte mutations measured, and 11 SURVIVORS ARE DISCLOSED RATHER
THAN CLOSED**, each with its file fingerprint and its 21/21 verdict, in
`DECISIONS/SAFETY-38-site-standing-pin.md` §7e–§7g: three in
`allocator.py`'s `UnitPlan.explain()`, three across `arbiter.py`'s two
`explain()`s, two in `tariff.py`, two in `arbitrage.py`'s trade reasons and
one in `main.py`'s SoC-floor notification. **An enumerated list with honest
gaps beats a fifth round finding a fifth instance.**
⚠️ **And the probe that grades those mutations was itself the defect, once.**
Its first cut numbered the operands **by order of appearance**, so after a
transposition the other leaf took the same number and all thirteen renders came
back **byte-identical** — which reads exactly like *the edit is inert*. Keying
the value to the **name** fixed it. A measurement comparing the swap against
itself, in the instrument this time.
* **The tool now states what it does NOT scan**, largest hole first:
`%s`-style lazy logging (`_LOG.error("… %s … %s", a, b)` puts the operands in
*arguments*, so no census here looks at them and the transposition is the same
zero-byte edit — **31 such calls across 8 modules, counted and listed by the
tool rather than asserted**), `.format()`/`%`/`+`/`.join()` sentences, a sentence that
leaves through a dict or dataclass and is rendered elsewhere, `yield`ed
sentences, message attributes outside the `reason`-shaped set, and everything
outside `app/`. Its five over/under-report gaps stay, and every census row now
prints whether the two interpolations **share a format spec** — the difference
between a transposition that is silent and one that merely looks broken.
**Inverter B gets its own MQTT device, and inverter A keeps the one it has
(TEL-07).** MQTT discovery announced **one** device, and every entity hung off