# 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.