MAINT-20 part 2 r2: app/main.py comments, lines 3001-3500 (shadow .. cycle)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-08 18:06:00 +01:00
co-authored by Claude
parent 6f94944e44
commit 391c4b4411
+18 -23
View File
@@ -3023,25 +3023,15 @@ class Controller:
def write_refusal(self, value: float, inv=None) -> str | None:
"""Why `value` may NOT be written to `inv` right now, or None if it
may.
SAFETY-28: a number reaches the wire only if THE SINGLE ALLOCATOR
placed it, on this tick, for THIS unit (`Allocation.authorises()`).
specs 1.5 makes two writers on one actuator a non-goal;
`len(validate.ACTUATOR_WRITE_KEYS) == 1` is a statement about the
options schema, not the writes. A second writer must file a Claim and
go through `resolve()`.
0 W is ALWAYS authorised and the test for zero is first and reads
nothing: the failsafe writes run while the allocator has REFUSED, so a
gate asking its permission to command zero would deadlock the failsafe
behind the component whose failure it answers; and at S = 0 broadcast
and allocation coincide (no over-command). It tests `value == 0.0`, not
a `failsafe=True` flag (a flag is an assertion by the caller, which is
what a second writer would pass).
DEV-02: the unit is an ARGUMENT: the record holds one share per unit,
and the first inverter's name at a roster of two would authorise A's
share on B's wire. The default is for N=1 callers and REFUSES at N>=2
(`_only()`). DECISIONS/MAIN-orchestration.md."""
may. SAFETY-28: a number reaches the wire only if THE SINGLE ALLOCATOR
placed it, on this tick, for THIS unit (`Allocation.authorises()`); a
second writer must file a Claim. 0 W is ALWAYS authorised and tested
first, reading nothing: the failsafe writes run while the allocator has
REFUSED, so a gate asking permission to command zero would deadlock the
failsafe. It tests `value == 0.0`, not a flag the caller sets. DEV-02:
the unit is an ARGUMENT (the record holds one share per unit); the
default is for N=1 callers and REFUSES at N>=2.
DECISIONS/MAIN-orchestration.md."""
if value == 0.0:
return None
inv = self._only() if inv is None else inv
@@ -3311,8 +3301,10 @@ class Controller:
if n]
def _provider_gate(self, now: datetime) -> None:
"""PRICE-01: `cheap`, the gate and the gap from the standing plan.
(DECISIONS/MAIN-orchestration-archive.md)"""
"""PRICE-01: `cheap`, the gate and the gap from the standing plan. No
price for now is a closed gate, so the charge waits rather than run
unpriced; the gap flag stays False until the first refresh has
finished. (DECISIONS/MAIN-orchestration-archive.md)"""
plan = self.fresh_tariff_plan(now)
published = None if plan is None else plan.published
if published is not None:
@@ -3349,14 +3341,17 @@ class Controller:
def _charge_gate_open(self, inv) -> bool:
"""MAINT-13: may this pack charge now, for the maintenance timeout?
(DECISIONS/MAIN-orchestration-archive.md)"""
Closed only when a price gate exists, it is shut, and the pack is not
really taking charge. (DECISIONS/MAIN-orchestration-archive.md)"""
if not self._price_gated or self.cheap:
return True
batt = inv.batt
return batt is not None and float(batt) <= -MAINT_CHARGING_W
async def cycle(self) -> None:
"""Gather claims, resolve precedence, write the winner.
"""Gather claims, resolve precedence, write the winner. Nothing here
decides who wins (arbiter.py does, by one rule); this method only
states what each strategy wants.
(DECISIONS/MAIN-orchestration-archive.md)"""
now = datetime.now(timezone.utc)
# BEFORE the first `return` of this method on purpose (TARIFF-11): it