ARB-03: pin all eight actuator call sites statically
checks / unit (pull_request) Successful in 3m45s
checks / unit (pull_request) Successful in 3m45s
The independent review of PR #40 found AC 6 met at three of the eight claim construction sites while the check claimed to cover the class - this repository's named failure mode, a check observing something other than what it claimed, reproduced inside the check written to prevent it. WHAT WAS WRONG. Part 10g's only call-site check was a real two-inverter cycle. That cycle is auto, P1-healthy, no maintenance procedure, no tripped sign watchdog, the guard idle and arbitrage off, so it reaches `device`, `supervised` and `loop` and nothing else - three of the eight direct appends in `Controller.cycle()`. Its own check text nevertheless read "the line a mis-typed `actuator_of(...)` at a call site goes red on", and the PR body's mutation table presented it as covering the class. The reviewer proved the gap on the shipped tree: `actuator_of("maintenance")` -> `actuator_of("peak")` at the maintenance-shaping append left test_arbiter.py, test_peakguard.py and test_arbitrage.py ALL GREEN, while at a roster of two it built a site loop of -1500 W against the guard's site `lo` of 1500 W and the now-misrouted charge-only `hi = 0` - `lo > hi`, arbiter clause 3, 0 W IN THE BILLED QUARTER, SAFETY-25 §0a's fatal case from a one-word typo. The same mutation at the `failsafe` append survived too. The runtime detector does fire - `scope_violations()` names it - but it only logs; nothing graded it. THE FIX, over machinery already present. Part 10f's AST scan already reads each `claims.append(...)` in `Controller.cycle()`. All eight direct appends expose their actuator statically as `actuator=self.actuator_of('<literal>')`, so 10g now requires that literal to EQUAL the source the claim is filed under, read as SOURCE TEXT rather than evaluated - `actuator_of("peak")` at the maintenance append is the same VALUE at the shipped roster of one and a different string here. That pins all eight routing sites, on the line the typo is written, in every configuration and at every roster, exactly the way 10f pins the source set. Two further lines keep the scan from going vacuous: every direct append must be readable in that shape (a computed or hand-written actuator fails there), and the ten appends must still account for 10f's eight sources. The two INDIRECT appends carry no `actuator=` at the call site to mis-type. `peak_claim` and `arb_claim` are routed where their claim is built, and are pinned there - `peakguard.claim()` by the SWEEP_SITE sweep over every load x elapsed x battery x SoC x floor combination, `arbitrage.claim()` by test_arbitrage.py. 10g now says so at the line, and says which three of the eight its dynamic cycle actually sees, rather than rounding up. MEASURED, NOT ARGUED. Each of the eight direct appends mutated in turn to a wrong actuator - device->loop, supervised->peak, failsafe->safety, safety(stopped)->device, safety(inputs missing)->maintenance, maintenance(SET)->loop, maintenance(shaping)->peak, loop->supervised - each on a purged tree via tools/mutation_run.sh over all 17 files. All eight RED, every one of them on the new line. TWO SURVIVING MUTATIONS OF THE SAME CLASS, ALSO CLOSED. * `resolve()` did not have to honour the roster it was given: `if units is None:` -> `if True:` left the whole suite green. Ignoring the argument makes a unit that filed nothing this cycle cease to exist - a site of two reads as a site of one and stage 3 passes the whole grid target through to the inverter that spoke, which is ARCH-01's fault with its own evidence removed. test_arbiter.py part 7 now grades it on the one case that separates the two, a SILENT unit. Mutation RED in test_arbiter.py and test_peakguard.py. * main.py's N>=2 refusal was unreachable and ungraded: `if not resolution.allocated:` -> `if False:` left the whole suite green, so the right instinct was one refactor away from being deleted as dead code. The guard is KEPT - the alternative default is the ARCH-01 fault - and new part 10h grades it on the two-inverter cycle that already existed: the 0 W, the `failsafe` winner, the log line naming the roster size, and the write that follows. Mutation RED. AND THE WORDING CORRECTED. main.py does not "refuse to write" on `allocated=False`. It SUBSTITUTES a 0 W failsafe and then calls `write_setpoint(0.0)` like any other cycle. 0 W is the correct fail-toward-inaction direction and the log text is accurate about what it refuses - to BROADCAST the site target - but "refuses to write" overstates the code. Said as what it does, in the CHANGELOG, at the branch in main.py, and in 10h's own check text. `resolve()`'s docstring now says the roster argument has to be HONOURED, which is not the same as existing. 17/17 green on a purged tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
06f95e0a64
commit
38f6680639
@@ -39,8 +39,22 @@ numbers.**
|
||||
an allocator** - `ALLOC-01` builds it. With at most one unit it passes the
|
||||
target through (§6.2's N=1 identity); with two or more **it declines, no
|
||||
shares, `allocated=False`**, because the obvious placeholder - hand the target
|
||||
to each - IS the fault `ARCH-01` was filed about, and `main.py` refuses to
|
||||
write rather than broadcast.
|
||||
to each - IS the fault `ARCH-01` was filed about. **What `main.py` then does
|
||||
is SUBSTITUTE, not abstain**: it replaces the resolution with a 0 W `failsafe`,
|
||||
logs "refusing to broadcast the site target", and writes that 0 W through
|
||||
`write_setpoint()` like any other cycle. The refusal is to BROADCAST; the
|
||||
write still happens, in the fail-toward-inaction direction. Graded end to end
|
||||
on a real two-inverter cycle by new **part 10h** - the branch is unreachable
|
||||
on the shipped roster, and replacing it with `if False:` had left all 17
|
||||
suites green.
|
||||
* **`resolve()` HONOURS the roster it is handed, and that is now graded too.**
|
||||
Left `None` the roster is derived from the claims, which is the documented
|
||||
default; passed explicitly it is used. Deriving it *always* - ignoring the
|
||||
argument - makes a unit that filed nothing this cycle cease to exist, so a
|
||||
site of two reads as a site of one and the seam passes the whole grid target
|
||||
through to the inverter that spoke: `ARCH-01`'s fault with its own evidence
|
||||
removed. `test_arbiter.py` part 7 grades it on the one case that separates the
|
||||
two, a **silent unit**.
|
||||
* **The contradiction is no longer guarded against, it CANNOT BE WRITTEN DOWN.**
|
||||
The partition is by `actuator` and it is a partition: a unit's `hi` and the
|
||||
site's `lo` are never both terms of one `lo > hi` test. Constructed and swept -
|
||||
@@ -68,12 +82,34 @@ numbers.**
|
||||
reading it, which is why `actuator` is the LAST field of `Claim` and not the
|
||||
first. Its spy gained `**kw` so the roster passes through unread rather than
|
||||
being silently dropped. New **part 10g** pins `CLAIM_SCOPE` to the same
|
||||
eight-source set, so a ninth source now goes red twice; and it grades a **real
|
||||
two-inverter cycle**, which is the only place a mis-typed `actuator_of(...)`
|
||||
at a call site is observable - at N=1 every source legitimately routes to the
|
||||
site, so a swap there is invisible by construction. Verified by breaking it:
|
||||
building SAFETY-25's Option B as a ninth source still takes 10f red on the
|
||||
eight-source set, so a ninth source now goes red twice. Verified by breaking
|
||||
it: building SAFETY-25's Option B as a ninth source still takes 10f red on the
|
||||
line it is written.
|
||||
* **And the CALL SITES are pinned statically, at all eight of them.** 10g's
|
||||
first cut graded routing only through a **real two-inverter cycle** and called
|
||||
that "the line a mis-typed `actuator_of(...)` at a call site goes red on".
|
||||
**It was not.** That cycle reaches `device`, `supervised` and `loop` and
|
||||
nothing else - three of the eight direct appends - so a mis-type at the other
|
||||
five survived every suite in this repository. Proved on the shipped tree by PR
|
||||
#40's reviewer: `actuator_of("maintenance")` -> `actuator_of("peak")` at the
|
||||
maintenance-shaping append left `test_arbiter.py`, `test_peakguard.py` and
|
||||
`test_arbitrage.py` all green while, at a roster of two, it built a site loop
|
||||
of -1500 W against the guard's site `lo` of 1500 W and the now-misrouted
|
||||
charge-only `hi = 0` - `lo > hi`, arbiter clause 3, **0 W in the billed
|
||||
quarter**, SAFETY-25 §0a's fatal case from a one-word typo. That is this
|
||||
repository's named failure mode - **a check observing something other than
|
||||
what it claimed** - reproduced inside the check written to prevent it, so it
|
||||
is closed the way 10f closes it: over the **same AST scan**. All eight direct
|
||||
appends expose their actuator as a literal `actuator=self.actuator_of('...')`,
|
||||
so the literal is required to EQUAL the source the claim is filed under - all
|
||||
eight, on the line the typo is written, in every configuration and at every
|
||||
roster, whether or not any fixture reaches the state that files them.
|
||||
**Measured: each of the eight mutated to a wrong actuator in turn, each one
|
||||
red.** The two INDIRECT appends (`peak_claim`, `arb_claim`) carry no
|
||||
`actuator=` at the call site to mis-type, and are pinned where their routing
|
||||
is actually chosen - `peakguard.claim()` by the `SWEEP_SITE` sweep,
|
||||
`arbitrage.claim()` by `test_arbitrage.py`. 10g's dynamic cycle is kept and
|
||||
now says which three of the eight it sees instead of claiming the class.
|
||||
|
||||
**SAFETY-25's open question is ANSWERED: the BMS cut-out is the DELIBERATE and
|
||||
only backstop for demand-driven discharge, and a loop-side reserve floor is
|
||||
|
||||
Reference in New Issue
Block a user