No content change. The gate never created a run for 0942228d - the newest
run stayed on d31a12b7, the pre-rebase head - so `checks / unit` had
nothing to report and sat pending. Why no run was created is CI-02 and
belongs to CI-01's owner; this commit only asks for one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
Two conflicts, both predicted, both mine as the later merger.
app/main.py - #12 inserted `await self.check_output_clamp()` on the line
immediately above `await self.check_meter_frozen()`, so its hunk abuts my
comment correction. Resolved so EACH CALL CARRIES ITS OWN BOUNDARY,
because the two guards do not share one:
- check_output_clamp() reads `self.clamp_last`, which is None until
write_setpoint() has run, so its boundary IS the write;
- check_meter_frozen() grades `self.target`, assigned further up, so
its boundary is the assignment.
Letting my correction generalise across both would have been worse than
the vague comment it replaces, because it would read as deliberate. The
AST of app/main.py is identical to merged release/1.0 (+1251 bytes, all
comment).
test_freeze.py - #12 independently added `frozen_notes()` keyed on the
same id. `notes_of()` subsumes it: same id, plus the title fallback that
keeps the id's own check independently falsifiable, plus `all_notes_of()`
for claims about the whole surface.
THE THREE SURFACE-WIDE ASSERTIONS, DECIDED ONE AT A TIME. Measured on the
merged tree:
scenario surface ids freeze
frozen meter, railed command 2 frozen + clamp 1
TEL-05 converged, -14.2 W 0 - 0
moving meter, a real 8 kW load 1 clamp 0
uncovered transport, no unchanged_s 1 clamp 0
- the wiring scenario STAYS SURFACE-WIDE and now asserts exactly TWO
alarms with BOTH IDS NAMED. The answer to "is this still meant to
raise one alarm" is no, it raises two, and a third guard has to come
back to this line. That is the one assertion in the file that
accounts for the controller's entire alarm output;
- the CONVERGED case stays surface-wide and stays silent: at the
operating point the whole add-on should have nothing to say, that is
still true, and narrowing it would give away a real property;
- the other two NARROW to notes_of(). The clamp alarm there is correct
and is SAFETY-03's subject: a real 8 kW load does drive the loop past
the rating, and an uncovered transport does not stop it. Asserting
whole-surface silence would make this file fail whenever another
guard correctly does its job. Each still owns the load-bearing half -
THE FREEZE DETECTOR SAID NOTHING - and the surface is PRINTED so a
reader sees what else fired without this file grading it.
Re-verified on the merged tree: M1 RED 4, M2 RED 5, M3 RED 3 + RED 2,
N6 RED 1, N3 green (correctly), N8 RED 4, R1 RED 2, R2 RED 1. All nine
suites green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
1. notes_of() could not tell this guard's alarm from anybody else's. It
selected by SERVICE, so against #12's app/ (which adds
goodwe_output_clamp) eight assertions went red, AC1's core check among
them. Blanket id-keying is not the repair and was measured, not
guessed:
selector vs #12's app/ id renamed
by service (as committed) 8 red 1 red
by id alone 3 red 6 red
by id or title (shipped) 3 red 1 red
all_notes_of() is the whole surface; notes_of() is this guard's,
recognised by the id it should carry OR the title it writes - the `or`
is what keeps the one assertion whose subject IS the id able to fail on
its own. The three surface-wide claims stay surface-wide, each with a
comment naming which surface it is about and why the next guard's
author must decide rather than narrow them. They are the three that go
red against #12, which is the correct signal.
2. The ordering comment named the wrong boundary. N3 - moving the call to
immediately before write_setpoint() - is green and correctly so:
self.target is assigned two lines earlier, write_setpoint() clamps only
a local and can skip the write entirely. So the invariant is "after
`self.target = resolution.target_w`", and the detector grades pre-clamp
INTENT, not the wire. Corrected in test_freeze.py and in main.py's own
note above the call (comment-only; I merge second and own the
conflict). N8 - moving the call above the assignment - is RED 4.
3. The schema range's own bounds are now pinned. int(60,3600) ->
int(1,86400) survived green: an operator could set meter_freeze_s:
86400 and the guard is dead with nothing red. Both bounds are
re-derived rather than restated.
4. find_hwsim() crashed the whole suite on a checkout one level below the
root (HERE.parents[1] -> IndexError, reproduced at C:/goodwe_controller).
Guarded. And part 3 now names its transport in the summary: the rig
hwsim and the stub are not equivalent - the rig path failed 1 run in 3
- so a green log has to say which one backed it.
Also: the "revived transport re-rails" check is labelled a PRECONDITION
like its sibling; freeze_rows() records that flipping p1_enabled exercises
the row's content and never its gate; and the note about the deleted
assertion is corrected - it was REDUNDANT, not undecidable. The reviewer
falsified it in one line (move the hass.call above `if not fired: return`,
RED 9). "I could not construct a falsifier, therefore it cannot fail" was
exactly the inference this file exists to distrust.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
None of the three is a bug in the shipped code. They are gaps in what the
suite could detect, and the next person to touch freeze.py had less
protection than the passing count implied.
1. WHERE check_meter_frozen() runs in cycle(). Moving it to the top left
the whole file green. Pinned by CONSEQUENCE, not by source order, so a
refactor that keeps the semantics stays green:
- the alarm must land on the SAME cycle the command reaches the rail
(at the top it grades the previous command against this cycle's
meter and arrives a cycle late);
- the dead-poll-task composition: poll task dies, stale_input_s is
crossed, the arbiter commands 0 W, and on THAT cycle the alarm is
already clear - because the detector ran after the 0 W write.
wired() now returns a per-cycle trace, which is what makes "on which
cycle" an observation rather than a hope.
2. The NOT COVERED status row. The old check only asserted that an
uncovered transport is SILENT, which a page claiming `armed` on a guard
that cannot run also satisfies. The string is now asserted in both
directions: the uncovered row must say NOT COVERED, name the
meter_source and homewizard_local, and warn; the covered row must still
say armed, or "does not say armed" would be satisfied by deleting the
armed branch.
3. meter_freeze_s. Pinned in test_validate.py by MAINT-04's regex
technique (no PyYAML in the image), and in test_freeze.py by its
consequences: the shipped value drives the replays, must stay inside
the 900 s billing quarter, and 3600/hold_s must stay inside the shipped
deadband. A Controller built with the key ABSENT must land on the same
number, which pins main.py's fallback without touching CFG-03.
All three demonstrated red before being accepted green, and every new
check was mutated to confirm it can fail; one candidate assertion ("no new
notification while parked at 0 W") was DELETED because no mutation could
break it, and replaced by the exact notification count across a revived
transport, which can.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC