Skip to content

test(control): record the early exits meeting a binding protection - #814

Merged
frahlg merged 2 commits into
masterfrom
agent/golden-early-exit-protections
Aug 5, 2026
Merged

test(control): record the early exits meeting a binding protection#814
frahlg merged 2 commits into
masterfrom
agent/golden-early-exit-protections

Conversation

@frahlg

@frahlg frahlg commented Aug 5, 2026

Copy link
Copy Markdown
Member

Test data only. No changeset — nothing ships.

The hole

Three branches of ComputeDispatch walk away from a cycle before the safety
pipeline: idle mode, the holdoff window, and the reactive deadband. The golden
corpus had no record of any of them meeting a protection that binds.

Of the 590 records, 33 returned no targets — 25 idle, 1 holdoff, 8 deadband —
and every one of the 8 deadband records ran with site_fuse_amps=0 and
peak_import_ceiling_w=0. Nothing was over any limit to defend. That is why
590 recorded ticks did not catch the deadband exit skipping every protection,
which is the bug #803 fixed. #803 left this gap open deliberately: recording
new records is a corpus change, not a fix.

What is added

early_exit_protections, 21 records, 611 in total.

Each shape appears twice — once where the protection binds, once where the same
site is configured identically and nothing is over its limit. The quiet half is
not filler. A corpus of only-firing records cannot tell a fix from an over-fire,
and a fuse-saver that discharges on every tick drains the pack as surely as one
that never fires trips the breaker.

exit binds quiet control
deadband tariff peak ceiling; fuse ceiling under a peak limit misconfigured above the breaker; 3φ phase over; 1φ phase over; safety-amp margin; two batteries split by headroom; stale-plan reactive fallback ceiling above the draw; limit under the fuse; balanced phases; 1φ under; pack below the 5 % floor; export-side phase 10 A over
idle peak ceiling; 3φ; 1φ both configured, neither over
holdoff peak ceiling; 3φ; 1φ both configured, neither over

The export-side record is the direction gate: forceFuseDischarge's only lever
is more discharge, which relieves an import-side phase and pushes an
export-side one further over. It must stay quiet, and now there is a record
that says so in watts.

Single-phase sites get their own records because the phase count is the
conversion #812 gave one owner. On one phase the aggregate meter and the phase
are the same wire, so relief is the overage once — 1610 W by both routes. Under
the bare * 3.0 #812 removed, these records would command 4830 W and drive the
meter through zero into an export-side violation.

This family records the law as fixed, not a bug

slew_limiter deliberately holds a known defect still so its fix reads as a
diff in watts. This one is the opposite: every record was taken after #803
landed and states what dispatch is supposed to do. A record here that moves is
a protection that stopped protecting. The family doc comment and the header of
golden_test.go both say so, since the corpus's standing disclaimer is that
nothing in it was reviewed as correct.

Verified against the un-fixed code

Reverting #803's dispatch.go hunk locally and replaying moves 7 of 611
records — all 7 deadband records where a protection binds, each reading as the
commanded discharge disappearing:

dispatch changed for early_exit/deadband_per_phase_3p_binds:
  per-driver targets:
    ferroamp: was -1380.00 W (clamped), now not dispatched
  site totals:
    battery_target_sum_w: was -1380.00 W, now 0.00 W (moved +1380.00 W)

The 6 quiet deadband records and all 8 idle and holdoff records hold still,
because idle and holdoff already ran the fuse-saver before #803. Restoring the
fix, all 611 replay clean.

Re-recording to silence the failure does not work either. TestGoldenDump
runs the coverage assertions over what it just wrote, and against the un-fixed
code they fail:

coverage gap: no deadband record where a protection binds — the exit is unguarded again
early_exit/deadband_per_phase_3p_binds: fleet total = 0 W, want -1380 W — 460 W over on the worst phase, across 3 phases

All 21 recorded values matched the prediction written down before recording.

Second commit: a provenance wart

slew_limiter.json carried ftw_commit: fe2c904f, a SHA that is not on master
and never was. It is the pre-squash tip of #809's branch — git cat-file finds
it locally with #809's subject, git merge-base --is-ancestor fe2c904f origin/master says it is not an ancestor, and git log -- slew_limiter.json
shows the squash c85bfe1 as the last commit to rewrite the file. So the
behaviour those 155 records hold is the behaviour at c85bfe1, which is what
the field now says. No record changes.

Why the diff adds only one corpus file

The eight existing families are byte-identical. A full re-recording on this
machine rewrites 27 of the 590 old records with float noise up to 8.5 µW — a
thousandth of the 0.01 W tolerance, and nothing a reviewer should have to read.

make verify clean on both commits.

🤖 Generated with Claude Code

frahlg and others added 2 commits August 5, 2026 08:04
Three branches of ComputeDispatch walk away from a cycle before the
safety pipeline: idle mode, the holdoff window, and the reactive
deadband. The golden corpus had no record of any of them meeting a
protection that binds.

Of the 590 records, 33 returned no targets — 25 idle, 1 holdoff, 8
deadband — and every one of the 8 deadband records ran with
site_fuse_amps=0 and peak_import_ceiling_w=0. Nothing was over any
limit to defend, so 590 recorded ticks could not see the deadband exit
skipping every protection. That is the bug #803 fixed, and #803 left
this gap open on purpose: recording new records is a corpus change, not
a fix.

early_exit_protections adds 21 records, 611 in total. Each shape appears
twice — once where the protection binds, once where the same site is
configured identically and nothing is over its limit. The quiet half is
not filler: a corpus of only-firing records cannot tell a fix from an
over-fire, and a fuse-saver that discharges on every tick drains the
pack as surely as one that never fires trips the breaker.

Covered: deadband against a tariff peak ceiling, against a fuse ceiling
left above the breaker by a misconfigured peak limit, against a phase
over the breaker on three-phase and on single-phase services, with a
safety-amp margin, split across two batteries by discharge headroom,
denied by a pack under the 5 % floor, and reached through a stale plan's
reactive fallback; idle and holdoff against the peak ceiling and against
both phase counts. One record is an export-side phase 10 A over the
breaker that must stay quiet, because forceFuseDischarge's only lever is
more discharge and honouring per-phase relief there would push that
phase further over.

Single-phase sites get their own records because the phase count is the
conversion #812 gave one owner: on one phase the aggregate meter and the
phase are the same wire, so relief is the overage once. Under the bare
`* 3.0` those records would command three times the overage.

This family records the law as fixed, not a bug. slew_limiter
deliberately holds a known defect still so its fix reads as a diff in
watts; these records were taken after #803 landed and state what
dispatch is supposed to do, so a record that moves is a protection that
stopped protecting. The family doc comment says so.

Verified against the un-fixed code. Reverting #803's dispatch.go hunk
locally moves 7 of the 611 records, all of them deadband records where a
protection binds, each reading as the commanded discharge disappearing —
deadband_per_phase_3p_binds goes from -1380 W clamped to no targets. The
6 quiet deadband records and all 8 idle and holdoff records hold still,
because idle and holdoff already ran the fuse-saver before #803.
Re-recording to silence the failure does not work either: the coverage
assertions fail on what was just written, naming the deadband exit as
unguarded again.

The eight existing families are byte-identical; only the new file is
added. A full re-recording on this machine rewrites 27 of the 590 old
records with float noise up to 8.5 microwatts — a thousandth of the
0.01 W tolerance, and nothing a reviewer should have to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The family carried ftw_commit fe2c904, a SHA that is not on master and
never was. The other eight families name real commits — seven at
c7fe6c9, where #790 recorded them, and the new one at c85bfe1.

fe2c904 is the pre-squash tip of #809's branch: `git cat-file` finds it
locally with #809's subject line, and `git merge-base --is-ancestor
fe2c904 origin/master` says it is not an ancestor of master. GitHub
squash-merged that branch as c85bfe1, and `git log --
testdata/golden/slew_limiter.json` shows c85bfe1 as the last commit to
rewrite the file: #809's fix moved 94 lines of records, and they were
re-recorded in the branch worktree before the squash. So the behaviour
these 155 records hold is the behaviour at c85bfe1, which is what the
field now says.

No record changes; only the provenance line. It matters because the
field is the one thing telling a reader which dispatch produced these
watts, and a SHA that resolves nowhere on master sends anyone tracing
the file's history to a dead end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@frahlg frahlg added no-changeset PR intentionally exempt from the changeset requirement (dev tooling / non-shipping) and removed no-changeset PR intentionally exempt from the changeset requirement (dev tooling / non-shipping) labels Aug 5, 2026
@frahlg
frahlg merged commit 8040ca5 into master Aug 5, 2026
14 of 17 checks passed
@frahlg
frahlg deleted the agent/golden-early-exit-protections branch August 5, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changeset PR intentionally exempt from the changeset requirement (dev tooling / non-shipping)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant