SAFETY-25: pin the claim-source set, so the tripwire is one
checks / unit (pull_request) Successful in 3m53s
checks / unit (pull_request) Successful in 3m53s
The durability claim this PR shipped was FALSE, and the reviewer of #31 disproved it by building the fix. Part 10 said, in four committed places, that it "goes RED the day a loop-side floor is built, so the exposure cannot change size under a green suite". The reviewer implemented the memo's own recommended Option B in main.py - a `Claim.limit("reserve", P_LOOP, hi=0.0)` filed under a `loop_soc_floor` - and the ENTIRE SUITE STAYED GREEN. Part 10 green. Mutation 13 went on printing "NOTHING PREVENTS IT, ON PURPOSE" and the same 21 % -> 10.0 %, on a tree where something DID prevent it. Pushed to an absurd always-binding 95 % reserve it was caught only by SAFETY-24's pre-existing part 7 wiring checks; part 10 was still green. THE CAUSE IS STRUCTURAL, not a missing assertion. `replay_loop()` hand-builds main.py's claim list as a literal `[supervised, loop, peakguard.claim(...)]`, so it is blind to any claim main.py ADDS - which is exactly where AC 2 would be built. That is this repository's named failure mode: a check that observed something other than what it claimed to observe, shipped as a guarantee inside the document that recommends the change. THE FIX IS 10f: THE CLAIM-SOURCE SET, PINNED, two ways because the two fail differently. * STATICALLY, over `Controller.cycle()`'s own AST. Every `claims.append(...)` in it must be one the scan can read - an unreadable append FAILS rather than counting as nothing, which is how a scan like this goes quietly vacuous - and the sources it can file must be exactly {device, supervised, failsafe, safety, peak, arbitrage, maintenance, loop}. A ninth takes part 10 red on the line it is written, in every configuration, whether or not any fixture here reaches the state that files it. * DYNAMICALLY, through REAL `Controller.cycle()`s with main.py's OWN `resolve` spied - not a list this file built and hoped matched - swept over the WHOLE 0..100 % SoC range in the two states 10a-10c replay. Idle: exactly {device, supervised, loop}. Floor-limited: exactly {device, supervised, loop, peak}. A claim conditioned on a low pack, which is the shape a loop-side floor has, cannot hide from a sweep that visits 0 %. And the fold is checked rather than assumed: main.py's two P_SAFETY rating limits are both symmetric LIMITs, so replay_loop() modelling them as one `supervised` claim at the tighter bound is exact. MEASURED AGAINST THE COUNTEREXAMPLE. Reproduced first on the unfixed tree (green, as reported). On this tree the reviewer's Option B at its recommended default takes part 10 RED on 3 checks, and the 95 % variant on 6. Nine mutations were run in all, each against a freshly copied tree with no `__pycache__` present, mounted `:ro`; all nine red, no survivors, no `+0 B` delta. MUTATION.md records why a second round was needed: the first round's mutations were all aimed at DISARMING the guard, while the claim part 10 actually made was about the guard being FIXED. The four false sentences are replaced with what is now true, and each says WHICH half catches WHICH shape - 10f for an arbiter claim (Options B and C), 10a-10c for a clamp inside control.compute() or the guard (Option D), which the replay drives directly. AC 2 IS STILL OPEN AND NOTHING HERE BUILDS IT. The loop-side floor was a test mutant, reverted before commit. The memo still says "Status: OPEN. This is a memo, not a decision." Also, non-blocking, from the same review: the NOT READY banner had inherited the new scope sentence and become a ~550-character paragraph that the row three lines below repeated verbatim. A checks() row may now carry an optional short `banner` form; the floor-limited row does, and `status_payload()` prefers it. THE ROW IS UNCHANGED - shortening that would have been weakening AC 3 - and rows without the key are unaffected. Graded, and mutated: dropping the short form goes red. Merged origin/release/1.0 first (CI-02), keeping both tickets' CHANGELOG entries. Full suite green: 17/17 in goodwe-addon-ci (test_ci_gate.py needs `git`, which CI-02 added to .gitea/ci/Dockerfile but the local image lacks; run with it installed and it passes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3cb9d3b7d9
commit
e498dd4c1c
@@ -62,12 +62,33 @@ shape — a `hi` limit on the loop — crosses the peak guard's `lo` and resolve
|
||||
**CONTRADICTION at 0 W**, the battery parked in the quarter that sets the bill,
|
||||
unless `bms_soc_floor < loop_soc_floor <= peak_soc_floor` is enforced at startup.
|
||||
|
||||
**`test_peakguard.py` part 10 is a tripwire, not a guarantee.** It records the
|
||||
defect and goes **red** the day a loop-side floor is built, so the exposure cannot
|
||||
change size under a green suite. Eight mutations were run against it, each
|
||||
against a freshly copied tree with no `__pycache__` present at all, mounted
|
||||
`:ro` — the same structural immunity `MUTATION.md` records for CI. All eight go
|
||||
red on the named check.
|
||||
**`test_peakguard.py` part 10 is a tripwire, not a guarantee, and part 10f is
|
||||
what makes it one.** The replay half records the defect; on its own it could not
|
||||
see the defect being FIXED, because `replay_loop()` hand-builds main.py's claim
|
||||
list and is therefore blind to any claim main.py adds — which is exactly where a
|
||||
loop-side floor would be built. PR #31's reviewer proved that by building the
|
||||
memo's own Option B in `main.py` and watching the whole suite stay green. **10f
|
||||
pins the claim-source set:** every `claims.append(...)` in `Controller.cycle()`
|
||||
is read off its AST and must be one of eight named sources, and real
|
||||
`Controller.cycle()`s — spied at main.py's own `resolve`, swept over the whole
|
||||
0–100 % state-of-charge range in both the idle and the floor-limited state — must
|
||||
hand the arbiter exactly `{device, supervised, loop}` and
|
||||
`{device, supervised, loop, peak}`. A ninth source, at any state of charge, in
|
||||
any configuration, takes part 10 red on the line it is written. A floor built
|
||||
inside `control.compute()` or the guard instead moves 10a–10c's replayed numbers,
|
||||
which the replay drives directly. So the exposure cannot change size under a
|
||||
green suite.
|
||||
|
||||
**Nine mutations, re-run against the fixed tree**, each against a freshly copied
|
||||
tree with no `__pycache__` present at all, mounted `:ro` — the same structural
|
||||
immunity `MUTATION.md` records for CI. Six are the AC 3 and behaviour mutations
|
||||
already on this ticket's record (the status row's scope sentence, the
|
||||
notification's, `config.yaml`'s, the memo deleted, the loop's gain zeroed, the
|
||||
guard's claim given a `hi`); two are the reviewer's, and are the ones that
|
||||
motivated 10f — the memo's own Option B at its recommended default, **now three
|
||||
checks red where it was fully green**, and an absurd always-binding 95 % reserve,
|
||||
**six red**; one is new, against the banner short form below. All nine are red,
|
||||
no survivors, and no byte delta is `+0 B`.
|
||||
|
||||
**The gate has never once graded the content a pull request would land, and a
|
||||
run that never started reported as a test failure (CI-02).** Both halves were
|
||||
@@ -134,6 +155,16 @@ carries `pending / Waiting to run` within seconds, and a concurrency-held one
|
||||
API. `/actions/tasks`, which is what was polled originally, lists only tasks
|
||||
that started, and cannot answer the question at all.
|
||||
|
||||
**The NOT READY banner no longer repeats the row underneath it.**
|
||||
`status_payload()` renders the first hard-failing check as the page headline, so
|
||||
the scope sentence above turned the loudest element on the page into a
|
||||
550-character paragraph that the checks list then repeated verbatim. A row may
|
||||
now carry an optional short `banner` form; the floor-limited row does, and its
|
||||
headline is the guard's own line plus a 68-character scope tag. **The row itself
|
||||
is unchanged** — the checks list is where an operator reads what the floor does
|
||||
and does not bound, and shortening that would have been weakening AC 3. Rows
|
||||
without the key are unaffected, and the page still renders with `textContent`.
|
||||
|
||||
**A setpoint device smaller than `max_w` silently disarmed the frozen-meter
|
||||
detector, and the status page reported it as armed (SAFETY-20).** SAFETY-18's
|
||||
conjunction needs `abs(commanded_w) >= 0.95 * max_w`, but the emitted command is
|
||||
|
||||
Reference in New Issue
Block a user