checks / unit (pull_request) Successful in 7m27s
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
226 lines
11 KiB
Markdown
226 lines
11 KiB
Markdown
# 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**:
|
|
|
|
```python
|
|
_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 into `quarter_blocks` without passing through `sync()`, 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()` returns **`None`** when
|
|
the database will not answer.
|
|
* **it does not count `monthly_peak`.** That tier holds one row per *month* and
|
|
would answer `1` for every month ever measured — wrong by exactly the amount
|
|
that makes it look plausible. The two tables are proven equal on the **peak**
|
|
by `recompute_month()`; they were never equal on the **count**.
|
|
* **it does not remove the dedupe.** `_seen` is still what stops `cycle()`'s
|
|
once-a-second `sync()` re-banking a block that `P1Ingest.blocks` keeps for a
|
|
day, and `sync()`'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 assert `recorded == 1` — the dedupe and the figure reporting it
|
|
were the same variable. Counting rows in `quarter_blocks` cannot see a
|
|
re-bank at all (it is a PRIMARY KEY no-op), so that check is now graded on
|
|
`sync()`'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:
|
|
|
|
1. **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*.
|
|
2. **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.
|
|
3. **It is what one reader already did unconditionally.** `recompute_month()`
|
|
is `ORDER 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:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```python
|
|
- + 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.
|