SAFETY-86: peak guard outranks the maintenance hold #161

Merged
admin merged 3 commits from safety-86-peak-over-hold into release/1.0 2026-10-01 02:27:37 +02:00
Owner

Owner decision 2026-09-30: the peak guard wins over the maintenance HOLD, as it does over the drain (SAFETY-83) and the charge (SAFETY-84). Record: DECISIONS/SAFETY-86-peak-over-hold.md. Tests: test_safety86.py.

The defect (reproduced on 4ad8298 in the test harness)

run 4ad8298 this PR
N=1, HOLD, house 6500, aim 3450 6500 W quarters, 'Load too high' 3450 W, no alarm
N=2, HOLD, house 6500 (B max 2500) 4000 W, 'Load too high' 3450 W (A 551 W, B 2500 W)
N=2, HOLD, house 6500, month peak 4000 (aim 3950) 4000 W 3950 W
house under the aim, N=1 and N=2 A 0 W A 0 W, every row identical

The fix (main.py, _peak_over_hold())

  • N=1: during a HOLD, a peak floor above 0 W is refiled at P_MAINTENANCE, the same move as _peak_over_drain(). The battery gives the floor. The Why line reuses "Battery maintenance: 3 051 W, raised for the peak limit". No new strings.
  • N>=2: the holding battery's pin is raised to the floor minus what the other live batteries may place (their hard hi, or their pin). The others give first and the holding battery gives only the shortfall. Why this split and not the allocator's normal shares: the hold is maintenance, which ranks above the others' modes and use in the owner's order. It is also the same rule the owner set for an Off battery ("gives way only for the shortfall"). So an Off battery (P_MODE, below maintenance) still gives before the holding one (test part 3).
  • Stops: a site stop (a SET above P_PEAK) decides alone. At N>=2 nothing is raised, and at N=1 the refiled floor stays below P_SAFETY. If another battery is pinned to charge (a SAFETY-73 probe), the pin is not raised, so the site is never split both ways.
  • After the peak: the hold resumes by itself. The rule runs every cycle, so when the guard lets go the command is 0 W again.
  • The level dips below the target: the hold finishes anyway. The HOLD does not read the level; it ends on its clock. A top-up would mean going back to CHARGE. CHARGE waits for sun or a cheap window (up to 36 h). Meanwhile the battery keeps the site's one maintenance slot, so the other battery's cycle waits, and then a new full hold starts that the guard can interrupt again. The guard only gives while the quarter is at risk, and the next monthly cycle balances again. Owner may prefer a top-up. That would be a change in maintenance.py's HOLD branch, not in this rule.

Tests (test_safety86.py, real Controller, safety84_identity harness)

  1. N=1, houses 5000/6500/7000: quarters on the aim, the phase stays hold, no alarm. This holds with 5 s spikes too (SAFETY-81 hold). Non-vacuous without the fix.
  2. Under the aim (N=1: 1200, 3000, −1500; N=2: 1500, 3000, 5000): A is at 0 W and every row equals the run without the fix.
  3. N=2, houses 6500/7000: on the aim, B at 2500 W, and A's pin = floor − 2500 on every cycle where A gives. The same holds with modes on and B Off. No refusal, no opposite split, no alarm.
  4. Banked month peak 4000 → aim 3950, at N=1 and N=2.
  5. Lost reading (site safety SET): every battery is at 0 W at N=1 and N=2, and the hold gives again after.
  6. The level falls from 99 % to 70 % during a 30 min hold: HOLD → IDLE, "cycle COMPLETE", never CHARGE, at N=1 and N=2.
  7. Boundaries on claim lists: a floor at, under, or 0.5 W over B's max; B not live, contradicting, refusing, or probing either way; a site vs a unit safety SET; DRAIN or idle not raised; N=1 floor 0 vs 0.5 W vs a ceiling-only claim.

Mutations (fresh copy per mutant, __pycache__ purged, python3 -B)

# mutation in _peak_over_hold() test_safety86 first red check
M1 N=1 refile removed RED part 1 quarters on the aim
M2 N>=2 pin not raised RED part 3 on the aim / shortfall
M3 site-stop check removed RED part 7 site safety SET
M4 others not subtracted (A gives the whole floor = allocator-style share) RED part 2 rows identical, part 3
M5 live check dropped RED part 7 B not live
M6 contradicting unit counted RED part 7 B contradicting
M7 others' pin ignored (hard hi used) RED part 7 B probing / unit SET
M8 charging-pin guard removed RED part 7 B probing to charge
M9 rule also applied to a DRAIN RED part 7 "only a HOLD" (test_safety83 red too)
M10 floor of 0 W counts (lo >= 0) RED part 7 N=1 floor 0
M11 negative give not dropped (A would charge) RED part 2, part 7 floor under B
M12 call site removed RED part 1

No survivors. owns_setpoint in the HOLD test is not mutated: a HOLD always owns the setpoint, so that mutant would be equivalent.

Pins and other suites

  • No digest re-pinned. The SAFETY-78/83/84, OPT-11, peak-margin and other identity suites pass unedited against their existing pins.
  • test_safety83.py: its last check asserted "HOLD not changed at N=1" (SAFETY-83's "Not changed" list). It now asserts the battery gives, bound by the peak. That change is intentional and is this ticket.
  • Separate commit 513183a (not SAFETY-86): test_peak_margin.py part 3 read the simulated September's month peak on the wall clock, so from 2026-10-01 it crashed on every tree, 4ad8298 included (I ran it on 4ad8298: same TypeError). It now reads at the runs' clock (BASE). This is test-only and was needed for CI to go green.
  • Full suite, run CI-style in goodwe-addon-checks:1: 55 of 56 passed. test_named_absence flagged two unguarded reads in test_safety86.py. After fixing those (test only), test_named_absence and test_safety86 pass.

Out of scope, and whether this changes them

  • SAFETY-87 (the guard grades the emptiest battery): unchanged, and now more visible. At N=2 with B under peak_soc_floor, the guard is floor-limited, its floor is 0 W, and a full A still holds at 0 W. I checked this: A 0 W, the quarter over the limit, PEAK_GUARD_FLOOR_LIMITED.
  • SAFETY-85 ("Battery is not keeping up"): not fixed. In a 45 min N=2 HOLD run at 6500 W, the loop's integrator is clamped at +5000 W in one episode both before and after, but the OUTPUT_CLAMP episode log lines went from 6 to 20. At N=1 the count went from 0 to 1. Neither easier nor fixed. A reviewer may want to look at the extra lines.
  • SAFETY-88 and the N=1 charge headroom sizing: untouched.

Docs

DOCS.md got one short paragraph under maintenance. The DECISIONS entries for SAFETY-83 and SAFETY-84 got a pointer to SAFETY-86, and there is a new SAFETY-86 entry. The arbiter order comment now names the HOLD.

🤖 Generated with Claude Code

https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr

Owner decision 2026-09-30: the peak guard wins over the maintenance HOLD, as it does over the drain (SAFETY-83) and the charge (SAFETY-84). Record: `DECISIONS/SAFETY-86-peak-over-hold.md`. Tests: `test_safety86.py`. ## The defect (reproduced on 4ad8298 in the test harness) | run | 4ad8298 | this PR | |---|---|---| | N=1, HOLD, house 6500, aim 3450 | 6500 W quarters, 'Load too high' | 3450 W, no alarm | | N=2, HOLD, house 6500 (B max 2500) | 4000 W, 'Load too high' | 3450 W (A 551 W, B 2500 W) | | N=2, HOLD, house 6500, month peak 4000 (aim 3950) | 4000 W | 3950 W | | house under the aim, N=1 and N=2 | A 0 W | A 0 W, every row identical | ## The fix (`main.py`, `_peak_over_hold()`) - **N=1:** during a HOLD, a peak floor above 0 W is refiled at `P_MAINTENANCE`, the same move as `_peak_over_drain()`. The battery gives the floor. The Why line reuses "Battery maintenance: 3 051 W, raised for the peak limit". No new strings. - **N>=2:** the holding battery's pin is raised to the floor minus what the other live batteries may place (their hard `hi`, or their pin). **The others give first and the holding battery gives only the shortfall.** Why this split and not the allocator's normal shares: the hold is maintenance, which ranks above the others' modes and use in the owner's order. It is also the same rule the owner set for an Off battery ("gives way only for the shortfall"). So an Off battery (P_MODE, below maintenance) still gives before the holding one (test part 3). - **Stops:** a site stop (a SET above `P_PEAK`) decides alone. At N>=2 nothing is raised, and at N=1 the refiled floor stays below `P_SAFETY`. If another battery is pinned to charge (a SAFETY-73 probe), the pin is not raised, so the site is never split both ways. - **After the peak:** the hold resumes by itself. The rule runs every cycle, so when the guard lets go the command is 0 W again. - **The level dips below the target: the hold finishes anyway.** The HOLD does not read the level; it ends on its clock. A top-up would mean going back to CHARGE. CHARGE waits for sun or a cheap window (up to 36 h). Meanwhile the battery keeps the site's one maintenance slot, so the other battery's cycle waits, and then a new full hold starts that the guard can interrupt again. The guard only gives while the quarter is at risk, and the next monthly cycle balances again. **Owner may prefer a top-up.** That would be a change in `maintenance.py`'s HOLD branch, not in this rule. ## Tests (`test_safety86.py`, real Controller, safety84_identity harness) 1. N=1, houses 5000/6500/7000: quarters on the aim, the phase stays hold, no alarm. This holds with 5 s spikes too (SAFETY-81 hold). Non-vacuous without the fix. 2. Under the aim (N=1: 1200, 3000, −1500; N=2: 1500, 3000, 5000): A is at 0 W and every row equals the run without the fix. 3. N=2, houses 6500/7000: on the aim, B at 2500 W, and A's pin = floor − 2500 on every cycle where A gives. The same holds with modes on and B Off. No refusal, no opposite split, no alarm. 4. Banked month peak 4000 → aim 3950, at N=1 and N=2. 5. Lost reading (site safety SET): every battery is at 0 W at N=1 and N=2, and the hold gives again after. 6. The level falls from 99 % to 70 % during a 30 min hold: HOLD → IDLE, "cycle COMPLETE", never CHARGE, at N=1 and N=2. 7. Boundaries on claim lists: a floor at, under, or 0.5 W over B's max; B not live, contradicting, refusing, or probing either way; a site vs a unit safety SET; DRAIN or idle not raised; N=1 floor 0 vs 0.5 W vs a ceiling-only claim. ## Mutations (fresh copy per mutant, `__pycache__` purged, `python3 -B`) | # | mutation in `_peak_over_hold()` | test_safety86 | first red check | |---|---|---|---| | M1 | N=1 refile removed | RED | part 1 quarters on the aim | | M2 | N>=2 pin not raised | RED | part 3 on the aim / shortfall | | M3 | site-stop check removed | RED | part 7 site safety SET | | M4 | others not subtracted (A gives the whole floor = allocator-style share) | RED | part 2 rows identical, part 3 | | M5 | `live` check dropped | RED | part 7 B not live | | M6 | contradicting unit counted | RED | part 7 B contradicting | | M7 | others' pin ignored (hard hi used) | RED | part 7 B probing / unit SET | | M8 | charging-pin guard removed | RED | part 7 B probing to charge | | M9 | rule also applied to a DRAIN | RED | part 7 "only a HOLD" (test_safety83 red too) | | M10 | floor of 0 W counts (`lo >= 0`) | RED | part 7 N=1 floor 0 | | M11 | negative give not dropped (A would charge) | RED | part 2, part 7 floor under B | | M12 | call site removed | RED | part 1 | No survivors. `owns_setpoint` in the HOLD test is not mutated: a HOLD always owns the setpoint, so that mutant would be equivalent. ## Pins and other suites - **No digest re-pinned.** The SAFETY-78/83/84, OPT-11, peak-margin and other identity suites pass unedited against their existing pins. - `test_safety83.py`: its last check asserted "HOLD not changed at N=1" (SAFETY-83's "Not changed" list). It now asserts the battery gives, bound by the peak. That change is intentional and is this ticket. - **Separate commit 513183a (not SAFETY-86):** `test_peak_margin.py` part 3 read the simulated September's month peak on the wall clock, so from 2026-10-01 it crashed on every tree, 4ad8298 included (I ran it on 4ad8298: same TypeError). It now reads at the runs' clock (`BASE`). This is test-only and was needed for CI to go green. - Full suite, run CI-style in `goodwe-addon-checks:1`: 55 of 56 passed. test_named_absence flagged two unguarded reads in test_safety86.py. After fixing those (test only), test_named_absence and test_safety86 pass. ## Out of scope, and whether this changes them - **SAFETY-87** (the guard grades the emptiest battery): unchanged, and now more visible. At N=2 with B under `peak_soc_floor`, the guard is floor-limited, its floor is 0 W, and a full A still holds at 0 W. I checked this: A 0 W, the quarter over the limit, PEAK_GUARD_FLOOR_LIMITED. - **SAFETY-85** ("Battery is not keeping up"): not fixed. In a 45 min N=2 HOLD run at 6500 W, the loop's integrator is clamped at +5000 W in one episode both before and after, but the OUTPUT_CLAMP episode log lines went from 6 to 20. At N=1 the count went from 0 to 1. Neither easier nor fixed. A reviewer may want to look at the extra lines. - **SAFETY-88** and the N=1 charge headroom sizing: untouched. ## Docs DOCS.md got one short paragraph under maintenance. The DECISIONS entries for SAFETY-83 and SAFETY-84 got a pointer to SAFETY-86, and there is a new SAFETY-86 entry. The arbiter order comment now names the HOLD. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr
admin added 2 commits 2026-10-01 00:57:12 +02:00
Part 3 read the simulated September's peak with the wall clock, so from
2026-10-01 it read no peak and crashed on every tree (4ad8298 too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SAFETY-86: peak guard outranks the maintenance hold
checks / unit (pull_request) Successful in 24m17s
infra-pin / fresh (pull_request) Successful in 1s
ba31e7c8b1
During a HOLD the holding battery gives what keeps the quarter at the peak
aim. N=1: the peak floor is refiled at P_MAINTENANCE, as for a drain
(SAFETY-83). N>=2: the hold's pin is raised to the floor minus what the
other live batteries may place, so they give first. A site stop still
decides alone. The hold ends on its clock even if the level dipped.
Below the aim nothing changes (rows identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Review round 1 (SAFETY-86)

Checked at ba31e7c. Full suite in goodwe-addon-checks:1: 55 of 56 pass. The one failure is test_freeze on my machine, because it needs the ../goodwe hwsim sibling; CI has that and passed.

Findings

R1-1 (fixing, pushed below): at N=2 the moving pin fills the action log. While A gives, A's pin follows the guard's floor. The loop still asks for its full +5000 W, but only pin + 2500 W can be placed. The ALLOCATOR "could not be placed" row triggers on every 1 W change in the unmet amount, and each such row splits the merged OUTPUT_CLAMP row. With a 6500 W house that varies by ±150 W, the 40-row log held only the last ~2 minutes (20 ALLOCATOR + 20 clamp rows in 45 min). Before this PR it held 16 rows, because A sat at 0 W and the unmet amount did not move. Fix: while the holding pack gives at N≥2, cycle() adds a site hold ceiling at the floor (P_MAINTENANCE). This is the same pattern as SAFETY-84's charge ceiling. The site target is then exactly the floor, which N=1 already does. Result: 0 ALLOCATOR rows and 1 clamp row. It is filed only when A gives, so part 2 (under the aim) is unchanged. It can never clash with the floor (hi == lo), and no other site lo exists during a hold (drain needs the one maintenance slot). It reuses the existing Why words "battery maintenance", so there is no new text.

R1-2 (answered, SAFETY-85): does the 6→20 clamp row rise mean a new episode? No. Every row is episode 1; each quarter-end burst is the same episode, re-inserted under an ALLOCATOR row (see R1-1). R1-1 brings it back to 1 row.

R1-3 (DECISIONS): top-up. The owner decided on 2026-10-01 that the hold ends on its clock, with no top-up. The DECISIONS entry now states this as the owner's decision.

R1-4 (test gap, fixed): a raise in the CHARGE phase (mutant R2) was killed only by test_safety84's digest; test_safety86 stayed green. Part 7 now checks that a CHARGE is not refiled at N=1.

Checked and fine

  • N=1 refile: safety SET still wins. Margin 0: quarters bank 3500 W at N=1 and N=2. Banked peak 4000 → 3950 W. Restart mid-hold (hold started 110 of 120 min earlier): it ends at 600 s and hands back to the loop, never both ways.
  • N≥2 split: the others give first, and an Off battery's shortfall rule is respected. A SAFETY-73 charging probe blocks the raise. A giving probe is counted. Nothing pushes power both ways in any run.
  • P1 gap during a hold (N=1, 120 s): the guard goes blind (coverage) for the rest of the quarter, so its floor is 0 and the hold rests at 0 W. That quarter banked 3701 W. This is the guard's designed blind action, not this rule, but the rig test should know about it.
  • 513183a: test only, and correct. peak(when) reads the runs' BASE (2026-09-20), not the wall clock.
  • test_safety83 flip: flipped only for this ticket (HOLD at N=1 now gives).
  • Strings: none new. The DOCS paragraph is short and plain.

Mutations (fresh copy per mutant, __pycache__ purged, python3 -B)

# mutation result
M2 N≥2 pin not raised RED test_safety86 p3
M3 site stop ignored RED p7
M4 others not subtracted RED p2, p3
M8 charging-probe guard removed RED p7
M11 negative give kept RED p2, p7
R1 holding pack gives first (≤2500 W), others the rest RED p2, p3
R2 raise also in CHARGE RED test_safety84 only (now also p7)
R3 N=1 refile at P_SAFETY RED p7
R4 top-up: HOLD → CHARGE under target RED p6, test_safety83
R5 aim ignores banked peak RED p4, test_safety83
R6 aim ignores margin RED p1, p3
R7 holding pack gives shortfall + 50 W RED p3

SAFETY-87 (next ticket), measured on this branch

N=2, A in HOLD at 99 %, B at 15 % (below peak_soc_floor 20), house 6500 W. The guard grades the emptiest battery (B), so it is PEAK_GUARD_FLOOR_LIMITED. The peak claim is lo=0, hi=inf (reason "SoC floor"), so _peak_over_hold sees no floor and A stays at 0 W. B gives 2500 W through the loop. The first quarter banks 4051 W (the month peak rises), and the next aims under that new peak (4000 W). There is no 'Load too high'; the notice fires on FLOOR_LIMITED and names "15 % against 20 %", which is B's level, while a full A sits idle.

## Review round 1 (SAFETY-86) Checked at ba31e7c. Full suite in `goodwe-addon-checks:1`: 55 of 56 pass. The one failure is `test_freeze` on my machine, because it needs the `../goodwe` hwsim sibling; CI has that and passed. ### Findings **R1-1 (fixing, pushed below): at N=2 the moving pin fills the action log.** While A gives, A's pin follows the guard's floor. The loop still asks for its full +5000 W, but only pin + 2500 W can be placed. The ALLOCATOR "could not be placed" row triggers on every 1 W change in the unmet amount, and each such row splits the merged OUTPUT_CLAMP row. With a 6500 W house that varies by ±150 W, the 40-row log held only the last ~2 minutes (20 ALLOCATOR + 20 clamp rows in 45 min). Before this PR it held 16 rows, because A sat at 0 W and the unmet amount did not move. **Fix:** while the holding pack gives at N≥2, `cycle()` adds a site `hold` ceiling at the floor (P_MAINTENANCE). This is the same pattern as SAFETY-84's `charge` ceiling. The site target is then exactly the floor, which N=1 already does. Result: 0 ALLOCATOR rows and 1 clamp row. It is filed only when A gives, so part 2 (under the aim) is unchanged. It can never clash with the floor (hi == lo), and no other site `lo` exists during a hold (`drain` needs the one maintenance slot). It reuses the existing Why words "battery maintenance", so there is no new text. **R1-2 (answered, SAFETY-85): does the 6→20 clamp row rise mean a new episode?** No. Every row is `episode 1`; each quarter-end burst is the same episode, re-inserted under an ALLOCATOR row (see R1-1). R1-1 brings it back to 1 row. **R1-3 (DECISIONS): top-up.** The owner decided on 2026-10-01 that the hold ends on its clock, with no top-up. The DECISIONS entry now states this as the owner's decision. **R1-4 (test gap, fixed): a raise in the CHARGE phase** (mutant R2) was killed only by test_safety84's digest; test_safety86 stayed green. Part 7 now checks that a CHARGE is not refiled at N=1. ### Checked and fine - N=1 refile: safety SET still wins. Margin 0: quarters bank 3500 W at N=1 and N=2. Banked peak 4000 → 3950 W. Restart mid-hold (hold started 110 of 120 min earlier): it ends at 600 s and hands back to the loop, never both ways. - N≥2 split: the others give first, and an Off battery's shortfall rule is respected. A SAFETY-73 charging probe blocks the raise. A giving probe is counted. Nothing pushes power both ways in any run. - P1 gap during a hold (N=1, 120 s): the guard goes blind (coverage) for the rest of the quarter, so its floor is 0 and the hold rests at 0 W. That quarter banked 3701 W. This is the guard's designed blind action, not this rule, but the rig test should know about it. - 513183a: test only, and correct. `peak(when)` reads the runs' `BASE` (2026-09-20), not the wall clock. - test_safety83 flip: flipped only for this ticket (HOLD at N=1 now gives). - Strings: none new. The DOCS paragraph is short and plain. ### Mutations (fresh copy per mutant, `__pycache__` purged, `python3 -B`) | # | mutation | result | |---|---|---| | M2 | N≥2 pin not raised | RED test_safety86 p3 | | M3 | site stop ignored | RED p7 | | M4 | others not subtracted | RED p2, p3 | | M8 | charging-probe guard removed | RED p7 | | M11 | negative give kept | RED p2, p7 | | R1 | holding pack gives first (≤2500 W), others the rest | RED p2, p3 | | R2 | raise also in CHARGE | RED test_safety84 only (now also p7) | | R3 | N=1 refile at P_SAFETY | RED p7 | | R4 | top-up: HOLD → CHARGE under target | RED p6, test_safety83 | | R5 | aim ignores banked peak | RED p4, test_safety83 | | R6 | aim ignores margin | RED p1, p3 | | R7 | holding pack gives shortfall + 50 W | RED p3 | ### SAFETY-87 (next ticket), measured on this branch N=2, A in HOLD at 99 %, B at 15 % (below `peak_soc_floor` 20), house 6500 W. The guard grades the emptiest battery (B), so it is PEAK_GUARD_FLOOR_LIMITED. The peak claim is `lo=0, hi=inf` (reason "SoC floor"), so `_peak_over_hold` sees no floor and A stays at 0 W. B gives 2500 W through the loop. The first quarter banks **4051 W** (the month peak rises), and the next aims under that new peak (4000 W). There is no 'Load too high'; the notice fires on FLOOR_LIMITED and names "15 % against 20 %", which is B's level, while a full A sits idle.
admin added 1 commit 2026-10-01 02:02:20 +02:00
SAFETY-86 review: hold the site at the peak floor while a HOLD gives
checks / unit (pull_request) Successful in 24m23s
infra-pin / fresh (pull_request) Successful in 1s
1c41a1e412
At N>=2 the holding pack's pin follows the guard's floor while the loop
still asks for +5000 W, so the ALLOCATOR "could not be placed" row was
re-logged on every 1 W change and filled the 40-row action log. While the
pin is raised, cycle() now files a site `hold` ceiling at the floor
(P_MAINTENANCE), as SAFETY-84 does for a charge. Filed only when the
holding pack gives; hi == the floor, so it never clashes with it.

Tests: a noisy N=2 house logs no ALLOCATOR row and at most one clamp
row (20 and 20 without the ceiling); the ceiling is the floor itself;
a CHARGE is not refiled at N=1. The DECISIONS entry records the owner's
2026-10-01 decision: the HOLD ends on its clock, no top-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Pushed 1c41a1e for R1-1, R1-3 and R1-4 (verified with git ls-remote).

  • main.py: _peak_over_hold() returns the floor when the holding pack gives at N≥2. cycle() then files Claim.limit("hold", P_MAINTENANCE, hi=floor) on the site. arbiter.CLAIM_SCOPE adds hold as a site source, and WHY_LIMIT maps it to the existing "battery maintenance".
  • test_arbiter and test_peakguard part 10f: source counts 15 → 16 and the direct appends 16 → 17.
  • test_safety86: part 3 adds a noisy-house check (0 ALLOCATOR rows and ≤1 clamp row, against 20/20 without the ceiling, which is non-vacuous). Part 7 adds a check that the ceiling is the floor itself, only when A gives, and that a 5000 W loop ask resolves to it. It also checks that a CHARGE is not refiled at N=1.
  • DECISIONS: the ceiling bullet; and the owner decision of 2026-10-01: the HOLD ends on its clock, no top-up.

Locally: test_safety86, 83, arbiter, named_absence and messages pass. Waiting for CI, the full suite and the mutants on the new head.

Pushed 1c41a1e for R1-1, R1-3 and R1-4 (verified with `git ls-remote`). - `main.py`: `_peak_over_hold()` returns the floor when the holding pack gives at N≥2. `cycle()` then files `Claim.limit("hold", P_MAINTENANCE, hi=floor)` on the site. `arbiter.CLAIM_SCOPE` adds `hold` as a site source, and `WHY_LIMIT` maps it to the existing "battery maintenance". - test_arbiter and test_peakguard part 10f: source counts 15 → 16 and the direct appends 16 → 17. - test_safety86: part 3 adds a noisy-house check (0 ALLOCATOR rows and ≤1 clamp row, against 20/20 without the ceiling, which is non-vacuous). Part 7 adds a check that the ceiling is the floor itself, only when A gives, and that a 5000 W loop ask resolves to it. It also checks that a CHARGE is not refiled at N=1. - DECISIONS: the ceiling bullet; and the owner decision of 2026-10-01: the HOLD ends on its clock, no top-up. Locally: test_safety86, 83, arbiter, named_absence and messages pass. Waiting for CI, the full suite and the mutants on the new head.
Author
Owner

Review round 2: verdict, merge

At head 1c41a1e:

  • CI: checks / unit (pull_request) and infra-pin / fresh both success.
  • Local full suite: 55 of 55 pass (test_freeze skipped locally; it needs the hwsim sibling, and CI runs it).
  • Mutants on the new head are all RED in test_safety86: M2, M4, R2 (CHARGE refile, now caught by p7), R8 (no hold ceiling) and R9 (ceiling 50 W above the floor).
finding state
R1-1 action-log flood at N=2 fixed in 1c41a1e
R1-2 clamp rows 6→20 refuted: same single episode, rows re-inserted under ALLOCATOR rows; now 1 row
R1-3 top-up owner decision 2026-10-01 recorded in DECISIONS
R1-4 CHARGE refile caught only by a digest fixed (p7)

Open, outside this PR: SAFETY-87 (described in round 1). A P1 gap blinds the guard for the rest of the quarter, so the hold rests at 0 W; this is the guard's designed blind action.

Merging with head_commit_id pinned to 1c41a1e.

## Review round 2: verdict, merge At head 1c41a1e: - CI: `checks / unit (pull_request)` and `infra-pin / fresh` both success. - Local full suite: 55 of 55 pass (test_freeze skipped locally; it needs the hwsim sibling, and CI runs it). - Mutants on the new head are all RED in test_safety86: M2, M4, R2 (CHARGE refile, now caught by p7), R8 (no `hold` ceiling) and R9 (ceiling 50 W above the floor). | finding | state | |---|---| | R1-1 action-log flood at N=2 | fixed in 1c41a1e | | R1-2 clamp rows 6→20 | refuted: same single episode, rows re-inserted under ALLOCATOR rows; now 1 row | | R1-3 top-up | owner decision 2026-10-01 recorded in DECISIONS | | R1-4 CHARGE refile caught only by a digest | fixed (p7) | Open, outside this PR: SAFETY-87 (described in round 1). A P1 gap blinds the guard for the rest of the quarter, so the hold rests at 0 W; this is the guard's designed blind action. Merging with `head_commit_id` pinned to 1c41a1e.
admin merged commit b8ddfd787f into release/1.0 2026-10-01 02:27:37 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#161