The capacity-tariff status row printed a peak read from SQLite next to a count
of banked quarters read from PROCESS MEMORY - `PeakTracker.recorded`, an `int`
set to 0 in `__init__` and counted up inside `sync()`. A 40-hour soak caught
what that means at a driven restart, on both instances independently:
before: 2026-09: peak 365 W, set by the quarter at 2026-09-11T18:45:00+00:00
- 10 % of the 3500 W soft limit; 120 quarter(s) banked
after : 2026-09: peak 365 W, set by the quarter at 2026-09-11T18:45:00+00:00
- 10 % of the 3500 W soft limit; 0 quarter(s) banked
Reproduced here before anything changed, through the real Controller and a real
restart - a second Controller object against the same file on disk - at 12 -> 0
with 12 rows standing in `quarter_blocks`.
The peak is durable and was never wrong. The COUNT is what says how much of the
billing month that peak covers, and 365 W over 120 quarters and 365 W over 0
quarters are different claims about the same number. Mid-month, after every
restart, the page made the weaker claim while displaying the stronger one's
value. That is TARIFF-01's own argument - an unreadable store must not read as a
zero peak, because a zero peak is disarming - one column over.
`PeakStore.banked_count(month)` asks the database, and `recorded` is a property
over it. Two more defects in the same variable fell out once it was driven, and
neither was in the ticket: it was never scoped to a MONTH (on 1 October the row
still showed September's tally beside October's empty month - measured), and it
missed the block `restore()` banks from the checkpoint, which never passes
through `sync()`.
⚠️ NOT 0, AND NOT None, WHEN THE STORE WILL NOT ANSWER. `banked_count()` returns
None and the row renders `an unreadable number of quarter(s) banked`.
`f"{None} quarter(s) banked"` was rejected because it reads as the same zero.
`0 quarter(s) banked` stays true and reachable - it is what a month nothing has
been written to says, and the two are asserted to be different text.
⚠️ THE DEDUPE IS NOT REMOVED WITH THE COUNTER. `_seen` still stops `cycle()`'s
once-a-second `sync()` re-banking a block `P1Ingest.blocks` keeps for a day. The
check that graded it asserted `recorded == 1` - the dedupe and the figure
reporting it were the same variable - and now grades `sync()`'s RETURN, because
counting rows cannot see a re-bank at all (PRIMARY KEY no-op).
THE TIE RULE, DECIDED RATHER THAN EMERGENT: at equal wattage the EARLIEST
quarter keeps the attribution. No bill and no guard moves - `peak_w` is still
only ever raised, the `>` term is untouched, SAFETY-07 reads the same watts.
The two readers DID disagree: the upsert's strict `>` kept whichever tied block
was WRITTEN first while `recompute_month()` has always kept the earliest, the
same only while blocks arrive in time order, and measured to diverge (09:45
against 09:00) fed newest-first.
⚠️ AND THE SOAK'S DRIFTING QUARTER WAS NOT A TIE. Six exactly equal 365.0 W
quarters re-attributed zero times on the shipped tree - `>` is strict. What
moved it was a real new maximum BELOW THE ROW'S OWN RESOLUTION: the row renders
whole watts, so 365.0000001 W is a new peak to the database and the same 365 W
to the reader. Left alone deliberately, and pinned so it is not re-diagnosed.
test_tariff.py parts 11 and 12. 12 mutations, all red, listed on the PR; M2 -
`banked_count()`'s sqlite3.Error arm returning 0 - was a SURVIVOR at -3 B on the
first battery, because a handle that never opened short-circuits before any SQL
runs and nothing reached the except arm. Closed with an open store whose table
is dropped under it, with a precondition that the same call answered 4 first.
main.py is NOT touched - TARIFF-11 holds its status rows. The row picks this up
without editing because `recorded` is an attribute access and is now a property.
The residual is stated in DECISIONS §3: `recorded` answers for now() while the
peak follows `checks(when=...)`, and the shipped install has exactly one call
site - `self.checks()` with no argument, main.py:4359 - where they are the same
month, so the live page is correct and durable. `recorded_in(when)` is shipped
and the row's move onto it is one token, for whoever holds main.py next; part 10
pins the present behaviour against a row count taken from `quarter_blocks` so
the gap cannot be quietly forgotten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
11 KiB
TARIFF-13 — the coverage of a peak, and which quarter gets the credit
Two decisions. §1 is the defect the ticket was raised on; §2 is the smaller question it asked to be settled deliberately rather than left as an accident. §3 is what this branch deliberately did not do, and why.
0. What was actually wrong, stated once
main.py's capacity-tariff status row prints a peak read from SQLite and a
coverage figure read from process memory:
_pk = self.peak.peak(when) # SQLite, durable
...
+ f"; {self.peak.recorded} quarter(s) banked" # an int on the tracker
PeakTracker.recorded was self.recorded = 0 in __init__, incremented once
per block inside sync(). A 40-hour soak caught what that means at a driven
restart, on both instances independently:
before: 2026-09: peak 365 W, set by the quarter at 2026-09-11T18:45:00+00:00
- 10 % of the 3500 W soft limit; 120 quarter(s) banked
after : 2026-09: peak 365 W, set by the quarter at 2026-09-11T18:45:00+00:00
- 10 % of the 3500 W soft limit; 0 quarter(s) banked
Reproduced here before anything was changed, through the real Controller, the
real P1Ingest, and a real restart — a second Controller object against
the same file on disk, never a re-read of the first:
before: 2026-09: peak 365 W, set by the quarter at 2026-09-10T09:00:00+00:00
- 10 % of the 3500 W soft limit; 12 quarter(s) banked
after : 2026-09: peak 365 W, set by the quarter at 2026-09-10T09:00:00+00:00
- 10 % of the 3500 W soft limit; 0 quarter(s) banked
peak survives the restart: True
count survives the restart: False (12 -> 0, store held 12 rows)
TARIFF-01's argument, one column over. That module's whole design is that an unreadable store must not read as a zero peak, because a zero peak is disarming. A coverage figure of zero is disarming in exactly the same way and about the same number: the count is what says how much of the billing month the watt figure actually covers, and 365 W over 120 quarters and 365 W over 0 quarters are different claims about the same 365 W. After a restart the page made the weaker claim while displaying the stronger one's value. It did not under-report the peak — it under-reported its own evidence.
Two further defects fell out of the same variable once it was driven, neither of them in the ticket:
- it was never scoped to a month. It counted every block the process had
banked, so on 1 October the row read September's tally beside October's "no
completed quarter recorded yet". Measured, on the shipped tree:
2026-10: no completed quarter recorded yet - 0 % of the 3500 W soft limit; 1 quarter(s) banked. - it missed the block
restore()banks. A stale block closed from the checkpoint goes intoquarter_blockswithout passing throughsync(), so the counter never saw it.
1. Decision: the count is READ FROM THE TIER THE PEAK IS READ FROM
The acceptance criterion allowed either route — make the count durable, or stop printing a number that silently resets. Durable, because the count is the useful half of the row and removing it would have cost the operator the one thing that says whether the peak means anything yet.
PeakStore.banked_count(month) asks the database SELECT COUNT(*) FROM quarter_blocks WHERE month=?, and PeakTracker.recorded is a property over it
rather than an attribute. Three consequences, and all three are the defects
above:
- a restart re-reads the same rows, so the figure survives it;
- the count is per month, so it agrees with the peak it is printed beside;
- a block banked by
restore()is a row like any other and is counted.
Three things this route deliberately does not do:
- it does not count
len(blocks_in(month)).blocks_in()returns[]on a read error — safe there only because no caller reads its emptiness as a peak, which its own docstring says — so taking its length would rebuild the disarming zero one call further out.banked_count()returnsNonewhen the database will not answer. - it does not count
monthly_peak. That tier holds one row per month and would answer1for every month ever measured — wrong by exactly the amount that makes it look plausible. The two tables are proven equal on the peak byrecompute_month(); they were never equal on the count. - it does not remove the dedupe.
_seenis still what stopscycle()'s once-a-secondsync()re-banking a block thatP1Ingest.blockskeeps for a day, andsync()'s return value is still the number newly banked. What moved is only where the status page reads its figure. ⚠️ The check that graded the dedupe used to assertrecorded == 1— the dedupe and the figure reporting it were the same variable. Counting rows inquarter_blockscannot see a re-bank at all (it is a PRIMARY KEY no-op), so that check is now graded onsync()'s return, with the store's tally as the second half.
⚠️ The unreadable case renders a WORD, not a number
banked_count() returns int | None; recorded_in() renders the None as
the module constant COUNT_UNREADABLE = "an unreadable number of", so the
sentence reads
... ; an unreadable number of quarter(s) banked
0 was never available. None was rejected too: f"{None} quarter(s) banked"
renders "None quarter(s) banked", which an operator reads as the same zero.
This is the PeakReading trade — no peak_w_or(default), ever — applied to the
coverage figure: a caller that formats it gets a true sentence in both states,
and a caller that does arithmetic on it gets a TypeError, which is loud.
"0 quarter(s) banked" remains a true and reachable sentence: it is what a month nothing has been written to says, and part 11 asserts that the two states are spelled differently.
2. Decision: at EQUAL wattage, the EARLIEST quarter keeps the attribution
First, not most recent. The reasoning, in the order it matters:
- It is not a money question. The utility bills the wattage; the timestamp is not part of the bill. So the decision is settled entirely on what the status row is for.
- The row answers "when did my monthly peak happen". An operator reading it goes and looks at one event — what was running, what the weather was. A stable answer is the one that can be investigated; "most recent wins" guarantees the answer moves every time the wattage recurs, and an operator who looked yesterday is sent somewhere else today for no reason.
- It is what one reader already did unconditionally.
recompute_month()isORDER BY offtake_avg_w DESC, quarter_start ASC LIMIT 1— earliest wins. Choosing "most recent" would have meant changing both readers; choosing "earliest" meant making the other one say out loud what it was already doing by accident.
⚠️ And the two readers DID diverge, which is why this is a code change
The monthly_peak upsert was WHERE excluded.peak_w > monthly_peak.peak_w —
strictly greater, so an exact tie never wrote and the row kept whichever tied
block was written first. That coincides with earliest only while blocks
arrive in time order. Fed four equal 365 W blocks newest-first, measured on the
shipped tree:
ascending arrival monthly_peak -> 09:00:00 recompute_month -> 09:00:00 agree=True
DESCENDING arrival monthly_peak -> 09:45:00 recompute_month -> 09:00:00 agree=False
That is a divergence in the derived record that part 8 of test_tariff.py
exists to prove cannot happen — latent, because nothing in the shipped path
banks out of order today. The upsert now carries the tie term explicitly:
WHERE excluded.peak_w > monthly_peak.peak_w
OR (excluded.peak_w = monthly_peak.peak_w
AND excluded.quarter_start < monthly_peak.quarter_start)
peak_w is untouched. The > term is exactly as it was, no tie can move
the billed number, and SAFETY-07's guard reads the same watt figure it read
before. The second term moves only the timestamp.
⚠️ AND THE SOAK'S DRIFT WAS NOT A TIE AT ALL
The ticket's reading — "ties at the same wattage are re-attributed to the most
recent matching quarter" — is false, and driving it is what showed that.
Six exactly-equal 365.0 W quarters on the shipped tree moved the attribution
zero times, because > is strict.
What the soak saw was a genuine new maximum smaller than the row's own
resolution. The row renders {peak_w:.0f} — whole watts — so 365.0000001 W is
a new peak to the database and the same 365 W to the reader:
q0 fed 365.0 -> renders 365 W, attributed 09:00:00
q1 fed 365.0000001 -> renders 365 W, attributed 09:15:00
q2 fed 365.0000002 -> renders 365 W, attributed 09:30:00
Decision: left alone, deliberately. It is the instrument behaving correctly — the peak really did rise — and the alternative, printing more decimal places, puts noise in the one row an operator reads at the moment the guard engages, to explain a movement that is real. Both cases are pinned in part 12 so that neither can be changed by accident, and the distinction is recorded here so the next reader does not re-diagnose it as a tie.
3. What this branch did NOT do, and who owns it
main.py's status row was not touched. TARIFF-11 holds main.py's status
rows on a concurrent branch, and the standing instruction is to stop rather than
race there. Everything above is in app/tariff.py, and the row picks the fix up
without editing, because self.peak.recorded is an attribute access and is now
a property.
The residual, stated with its scope. recorded answers for
datetime.now(). The peak beside it follows checks(when=...). The shipped
install has exactly one call site — self.checks() with no argument, at
main.py:4359 — where the two are the same month, so the live page is correct
and durable. They part company only under the when= affordance TEST-17 added
for the suite, and the mismatch there is strictly smaller than the one it
replaces (this month's tally, rather than a process-wide total across all
months).
PeakTracker.recorded_in(when) is the scoped form and is shipped. Moving the
row onto it is:
- + f"; {self.peak.recorded} quarter(s) banked"
+ + f"; {self.peak.recorded_in(when)} quarter(s) banked"
one token, on one line, inside the block TARIFF-11 owns. test_tariff.py
part 10 pins the present behaviour — graded against a row count taken
straight from quarter_blocks, not against the property that produces it — so
the gap goes red if it changes and cannot be quietly forgotten.