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

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>
This commit is contained in:
glenn schrooyen
2026-10-01 02:00:20 +02:00
co-authored by Claude Opus 5.5
parent ba31e7c8b1
commit 1c41a1e412
7 changed files with 108 additions and 31 deletions
+8 -5
View File
@@ -149,7 +149,7 @@ check("...and `source` is still the first positional argument, which is what "
Claim.limit("device", P_SAFETY, -1, 1).source == "device")
print()
print("ARB-03 2. the split: fifteen sources, each classified where it is BUILT")
print("ARB-03 2. the split: sixteen sources, each classified where it is BUILT")
# The classification is graded against what each claim is ABOUT, restated here
# so that the table in arbiter.py and the expectation in a test are not the same
# sentence read twice. test_peakguard.py part 10f pins this set against main.py's
@@ -179,6 +179,9 @@ SITE_SOURCES = {
"charge": "the site target may not go above a cheap-window maintenance "
"charge, or a sibling would give against it. Filed at N>1 "
"only",
# SAFETY-86.
"hold": "the site target stays at the peak floor while a holding pack "
"gives the shortfall. Filed at N>1 only",
}
UNIT_SOURCES = {
"device": "one inverter's own reported min/max rating",
@@ -210,9 +213,9 @@ for _src, _why in sorted(SITE_SOURCES.items()):
print(f" site {_src:<11} {_why}")
for _src, _why in sorted(UNIT_SOURCES.items()):
print(f" unit {_src:<11} {_why}")
check("all FIFTEEN claim sources are classified, and only those fifteen",
check("all SIXTEEN claim sources are classified, and only those sixteen",
set(CLAIM_SCOPE) == set(SITE_SOURCES) | set(UNIT_SOURCES))
check("the eight that are about the GRID CONNECTION are site claims",
check("the nine that are about the GRID CONNECTION are site claims",
{s for s, v in CLAIM_SCOPE.items() if v == SCOPE_SITE} == set(SITE_SOURCES))
check("the seven that are about a PACK, a DEVICE or one write are per-inverter",
{s for s, v in CLAIM_SCOPE.items() if v == SCOPE_UNIT} == set(UNIT_SOURCES))
@@ -234,7 +237,7 @@ check("...and with none configured at all, likewise",
all(actuator_for(s, "inverter-a", ()) == ACTUATOR_SITE for s in CLAIM_SCOPE))
check("with TWO the identity breaks and the split becomes real: the seven "
"per-inverter sources route to the unit this controller drives, the "
"eight site sources stay on the grid connection",
"nine site sources stay on the grid connection",
{s: actuator_for(s, "inverter-a", TWO) for s in CLAIM_SCOPE}
== {**{s: "inverter-a" for s in UNIT_SOURCES},
**{s: ACTUATOR_SITE for s in SITE_SOURCES}})
@@ -515,7 +518,7 @@ check("...and at N=1, where every source legitimately routes to the site, the "
Claim.limit("maintenance", P_MAINT_SHAPE, hi=0.0)]) == [])
check("an unclassified ninth source is a violation wherever it is filed",
scope_violations([Claim.limit("reserve", P_LOOP, hi=0.0)]) != [])
check("and the eight, routed by actuator_for() at a two-inverter site, are "
check("and the sixteen, routed by actuator_for() at a two-inverter site, are "
"clean - which is what says the router and the checker agree",
scope_violations(
[Claim.limit(s, P_SAFETY, -1, 1,