Four defects of one shape: the add-on's account of itself was wrong, always in the direction that flattered it. They shipped together because nothing in this repository graded the strings a human is shown — every tariff check inspects the TariffPlanobject, and all four were in the sentences built out of it.
Reproduced before the fix, driven through the shipped Controller against the committed energy-charts capture; the harness and its transcript are at /home/homelab/.cache/t11-mut/.
1. TariffPlan.words counted points and called them hours
Measured before: dynamic_dayahead from generic_dayahead, 192 h for 192 quarter-hourly points = 48 hours. Belgium moved to a 15-minute MTU on 2025-10-01, so on the real feed it was wrong every time it was printed — and not by a constant factor: across a 40-hour soak the overstatement ranged 1.79× to 5.33×, because §8.3's carried points are hourly while the firm ones are quarter-hourly.
After: dynamic_dayahead from generic_dayahead, 192 points over 48 h, 192 firm / 0 carried, 900 s spacing. N points over H h is FCST-04's Price history wording on purpose, so the two rows describing the same feed can be compared.
2. The §8.3 and flat-tariff refusals did the same — and they reach an operator
Before: §8.3: only 1 of 96 hours carry a published price about 96 points, which are 24 hours; the flat rung counted the same points and called them firm hours. ⚠️ This is exactly the point≠hour conflation TARIFF-14 removed from at(), left standing in the prose that describes it, and it reaches the status page through self.arb.explain().
After: §8.3: only 1 of 96 points carry a published price - 1 of 24 hours - and the rest are provisional … and the tariff is flat - every one of the 96 firm points, spanning 24 hours, costs ….
The same conflation in price_forecast.py's §8.3 carry-forward log line — %d of %d hours with a carried-hour count over a point count — is corrected in the same commit, because DOCS quotes that line verbatim.
3. The plan reached no HTTP surface at all
Before, driven: rows["Tariff"] == 'no static profile' on a site whose feed had just answered with a 192-point plan. origin, product, the horizon and the firm/carried split were in no row — the soak read 'no static profile' at all 802 samples.
After:
no static profile (recomputed this tick); provider plan fetched 0 s ago for a
48 h horizon: dynamic_dayahead from generic_dayahead, 192 points over 48 h,
192 firm / 0 carried, 900 s spacing
⚠️The two Tariff sources are kept distinguishable.static_status() runs on the 30 s I/O-free tick; the provider plan comes off the 1800 s refresh and is served for up to TARIFF_MAX_AGE_S (5400 s since TARIFF-12). So the plan half carries its own age, measured from tariff_at — which TARIFF-12 deliberately does not move on a kept plan — and the check drives the same plan object at 0 s and at 4000 s and asserts the two rows differ. A provider site with no usable plan says so, instead of falling silently back to the static sentence.
⚠️FCST-04's Tariff↔DOCS pin is honoured, not routed around.test_price_history.py pins the Tariff row and DOCS as a PAIR; wiring the plan in took it red, and DOCS.md now carries the sentence it had been overclaiming, with the row's five facts and the two-clock warning written out.
4. main.py:4486-4489's "if and only if" was false
cycle() returns at the input hold — inputs missing, bad_for < stale_input_s, 15 s by default — before it reaches self.arb = None. Driven, arbitrage_enabled: true plus a sensor answering None:
before: arbitrage.enabled(opts)=True self.arb=None row='off (arbitrage_enabled)'
after: row='ON (`arbitrage_enabled`), but NO DECISION has been reached yet - no
cycle has run as far as the arbitrage step. The loop's own reason:
inputs missing 0s'
and the second half, after a healthy cycle whose SoC then drops out:
before: row == the previous cycle's decision, rendered as current
after: 'no decision this cycle (inputs missing 0s); the last one was taken 0 s
ago: arbitrage idle - NOT COVERED: …'
The row reads the option instead of inferring it from a missing decision; cycle_at (stamped before cycle()'s first return) and arb_at are what make "this cycle reached no decision" an observation rather than a guess. The comment now states what is true.
The deliverable: a check that reads the RENDERED SURFACE
test_price.py section 12. It renders through the shipped Controller.status_payload() and grades rows["Tariff"] and rows["Arbitrage"].
Parsed by slot, never by presence."132 points" in row is satisfied by the number appearing in the other number's slot — MUTATION.md habit 14's zero-byte edit. Each number is pulled out of its own slot and compared against a figure computed from the fixture's two construction parameters, never from the property the row was built out of (habit 4).
The instrument is calibrated first (habit 1): the slot parser is driven against a correct row and against a row wrong in exactly the way the defect was, and a missing slot is a failed derivation rather than a silent pass.
The fixtures are a SET (habit 10) of four plan shapes, and the check computes and prints the table of field pairs no fixture separates — none. It deliberately contains both the degenerate hourly arrangement (points == hours, where the defect hid for three cards) and the quarter-hourly one where they differ 4×.
origin and product are graded by disagreement, not by substring: two plans over the identical point list differing in one field must render differently.
The Arbitrage row's three states are driven, with a sensor answering None and — for the stale case — by a real second cycle rather than a hand-set attribute, and graded by inequality: two facts that must not render identically.
End to end against scenarios/energy-charts-be-price-*.json, graded against the captured document's own length (habit 19).
Mutations
Every check driven. Harness /home/homelab/.cache/t11-mut/run.py: each mutation applied to a fresh cp -a of the clean worktree (never edit-and-revert — habit 18), __pycache__ purged on the host, tree bind-mounted :ro so no cache can be written into it, mutated line asserted on disk, byte delta recorded, full 29-file run each.
16 mutations, run rather than reasoned about, no survivors. M2 is +0 B
and is called out as such, per MUTATION.md's reporting rule. Harness /home/homelab/.cache/t11-mut/run.py: each mutation on a fresh cp -a of the
clean worktree rather than edit-and-revert (habit 18), __pycache__ purged,
tree bind-mounted :ro so no cache can be written into it, the mutated line
asserted on disk, full 29-file run each.
mutation
file
Δ
red
M1
the SHIPPED DEFECT restored: words prints the point count in the hour slot
static_tariff.py
+3 B
2
M2
firm and carried TRANSPOSED - habit 14's zero-byte edit, both numbers still in the sentence
static_tariff.py
+0 B
1
M3
hours counts POINTS - the defect one level down from words
static_tariff.py
-9 B
2
M4
_whole_hours stops truncating to the hour, so every quarter is its own 'hour'
static_tariff.py
-10 B
5
M5
the plan is dropped from the Tariff row - the shipped defect 3
main.py
-133 B
8
M6
the plan's age is a constant, so a 5400 s old plan reads as current
main.py
-10 B
1
M7
the Arbitrage row goes back to INFERRING 'off' from a missing decision - the shipped defect 4
main.py
+33 B
4
M8
a stale decision renders exactly like a fresh one
main.py
-144 B
2
M9
cycle_at is never stamped, so nothing can tell 'this cycle decided' from 'a previous one did'
main.py
-28 B
2
M10
arb_at is never stamped either - the row loses the age it reports
main.py
-18 B
4
M11
§8.3's refusal says 'hours' where it means points - the shipped defect 2, verbatim
static_tariff.py
-1 B
1
M12
the flat refusal drops the hour figure and goes back to calling points hours
static_tariff.py
-38 B
1
M13
a provider site with no plan falls silently back to the static sentence - the state the soak measured 802 times
main.py
-166 B
1
M14
only the FIRST spacing is reported, so a mixed 900/3600 plan reads as uniformly quarter-hourly
static_tariff.py
-23 B
1
M15
origin stops reaching the sentence: a §11.4 fallback reads as an observation of the feed
static_tariff.py
-190 B
1
M16
PAIRED: M1 and M3 together - the two rungs that answer 'how many hours' weakened at once
static_tariff.py
-6 B
2
⚠️TWO OF THEM WERE FOUND BY THE BATTERY IN THE CHECK ITSELF, which is why
the battery is not optional:
M15 was a SURVIVOR of the whole suite at −190 B on the first draft. The
origin pair gave the §11.4 plan provider="static_profile" as well as origin="fallback" — what a real one carries — so the two rows differed for two reasons and dropping origin from the sentence entirely changed
nothing the check could see. Habit 4, in the check written to close this
card. The provider string is now held equal and that equality is its own
precondition check.
M9 produced a TRACEBACK and NO NAMED RED.cycle_at unstamped is __init__'s None, and datetime < None raised inside the check's f-string operand — FCST-06's finding, in the operand rather than in the
condition, grading none of the checks after it. It goes through derived()
now and M9 is red on two named checks.
Not in this card
TARIFF-09's price_feed_url override warning still predicts the wrong degraded state (app/price.py:1574): on a cold store the unpublished hours are absent, not "carried forward and flagged provisional". That was item 3 of the original card and is not among the four this ticket names; absence is the worse state and the warning names the milder one. Left for its own card.
Nothing in app/tariff.py or the capacity-peak state (TARIFF-13's).
No behaviour changed: no threshold, refusal rung, claim or setpoint moved. Every edit is a sentence, a derived counter behind one, or where a sentence is rendered.
Four defects of one shape: **the add-on's account of itself was wrong, always in the direction that flattered it.** They shipped together because nothing in this repository graded the strings a human is shown — every tariff check inspects the `TariffPlan` *object*, and all four were in the *sentences* built out of it.
Reproduced before the fix, driven through the shipped `Controller` against the committed `energy-charts` capture; the harness and its transcript are at `/home/homelab/.cache/t11-mut/`.
## 1. `TariffPlan.words` counted points and called them hours
Measured before: `dynamic_dayahead from generic_dayahead, 192 h` for **192 quarter-hourly points = 48 hours**. Belgium moved to a 15-minute MTU on 2025-10-01, so on the real feed it was wrong every time it was printed — and **not by a constant factor**: across a 40-hour soak the overstatement ranged **1.79× to 5.33×**, because §8.3's carried points are hourly while the firm ones are quarter-hourly.
After: `dynamic_dayahead from generic_dayahead, 192 points over 48 h, 192 firm / 0 carried, 900 s spacing`. `N points over H h` is FCST-04's *Price history* wording on purpose, so the two rows describing the same feed can be compared.
## 2. The §8.3 and flat-tariff refusals did the same — and they reach an operator
Before: `§8.3: only 1 of 96 hours carry a published price` about 96 points, which are 24 hours; the flat rung counted the same points and called them `firm hours`. ⚠️ This is exactly the point≠hour conflation TARIFF-14 removed from `at()`, left standing in the prose that describes it, and it reaches the status page through `self.arb.explain()`.
After: `§8.3: only 1 of 96 points carry a published price - 1 of 24 hours - and the rest are provisional …` and `the tariff is flat - every one of the 96 firm points, spanning 24 hours, costs …`.
The same conflation in `price_forecast.py`'s §8.3 carry-forward log line — `%d of %d hours` with a carried-**hour** count over a **point** count — is corrected in the same commit, because DOCS quotes that line verbatim.
## 3. The plan reached no HTTP surface at all
Before, driven: `rows["Tariff"] == 'no static profile'` on a site whose feed had just answered with a 192-point plan. `origin`, `product`, the horizon and the firm/carried split were in **no** row — the soak read `'no static profile'` at all 802 samples.
After:
```
no static profile (recomputed this tick); provider plan fetched 0 s ago for a
48 h horizon: dynamic_dayahead from generic_dayahead, 192 points over 48 h,
192 firm / 0 carried, 900 s spacing
```
⚠️ **The two Tariff sources are kept distinguishable.** `static_status()` runs on the 30 s I/O-free tick; the provider plan comes off the 1800 s refresh and is served for up to `TARIFF_MAX_AGE_S` (5400 s since TARIFF-12). So the plan half carries **its own age**, measured from `tariff_at` — which TARIFF-12 deliberately does not move on a kept plan — and the check drives the same plan object at 0 s and at 4000 s and asserts the two rows differ. A provider site with no usable plan says so, instead of falling silently back to the static sentence.
⚠️ **FCST-04's Tariff↔DOCS pin is honoured, not routed around.** `test_price_history.py` pins the *Tariff* row and DOCS as a PAIR; wiring the plan in took it red, and DOCS.md now carries the sentence it had been overclaiming, with the row's five facts and the two-clock warning written out.
## 4. `main.py:4486-4489`'s "if and only if" was false
`cycle()` returns at the input hold — inputs missing, `bad_for < stale_input_s`, 15 s by default — **before** it reaches `self.arb = None`. Driven, `arbitrage_enabled: true` plus a sensor answering `None`:
```
before: arbitrage.enabled(opts)=True self.arb=None row='off (arbitrage_enabled)'
after: row='ON (`arbitrage_enabled`), but NO DECISION has been reached yet - no
cycle has run as far as the arbitrage step. The loop's own reason:
inputs missing 0s'
```
and the second half, after a healthy cycle whose SoC then drops out:
```
before: row == the previous cycle's decision, rendered as current
after: 'no decision this cycle (inputs missing 0s); the last one was taken 0 s
ago: arbitrage idle - NOT COVERED: …'
```
The row reads the **option** instead of inferring it from a missing decision; `cycle_at` (stamped before `cycle()`'s first return) and `arb_at` are what make "this cycle reached no decision" an observation rather than a guess. The comment now states what is true.
## The deliverable: a check that reads the RENDERED SURFACE
`test_price.py` section 12. It renders through the shipped `Controller.status_payload()` and grades `rows["Tariff"]` and `rows["Arbitrage"]`.
* **Parsed by slot, never by presence.** `"132 points" in row` is satisfied by the number appearing in the *other* number's slot — MUTATION.md habit 14's zero-byte edit. Each number is pulled out of its own slot and compared against a figure computed from the **fixture's two construction parameters**, never from the property the row was built out of (habit 4).
* **The instrument is calibrated first** (habit 1): the slot parser is driven against a correct row *and* against a row wrong in exactly the way the defect was, and a missing slot is a failed derivation rather than a silent pass.
* **The fixtures are a SET** (habit 10) of four plan shapes, and the check **computes and prints** the table of field pairs no fixture separates — `none`. It deliberately contains both the degenerate hourly arrangement (points == hours, where the defect hid for three cards) and the quarter-hourly one where they differ 4×.
* **`origin` and `product` are graded by disagreement**, not by substring: two plans over the *identical* point list differing in one field must render differently.
* **The *Arbitrage* row's three states are driven**, with a sensor answering `None` and — for the stale case — by a real second cycle rather than a hand-set attribute, and graded by **inequality**: two facts that must not render identically.
* **End to end** against `scenarios/energy-charts-be-price-*.json`, graded against the **captured document's own length** (habit 19).
## Mutations
Every check driven. Harness `/home/homelab/.cache/t11-mut/run.py`: each mutation applied to a fresh `cp -a` of the clean worktree (never edit-and-revert — habit 18), `__pycache__` purged on the host, tree bind-mounted `:ro` so no cache can be written into it, mutated line asserted on disk, byte delta recorded, full 29-file run each.
**16 mutations, run rather than reasoned about, no survivors.** M2 is `+0 B`
and is called out as such, per MUTATION.md's reporting rule. Harness
`/home/homelab/.cache/t11-mut/run.py`: each mutation on a fresh `cp -a` of the
clean worktree rather than edit-and-revert (habit 18), `__pycache__` purged,
tree bind-mounted `:ro` so no cache can be written into it, the mutated line
asserted on disk, full 29-file run each.
| | mutation | file | Δ | red |
|---|---|---|---|---|
| M1 | the SHIPPED DEFECT restored: `words` prints the point count in the hour slot | `static_tariff.py` | +3 B | **2** |
| M2 | firm and carried TRANSPOSED - habit 14's zero-byte edit, both numbers still in the sentence | `static_tariff.py` | +0 B | **1** |
| M3 | `hours` counts POINTS - the defect one level down from `words` | `static_tariff.py` | -9 B | **2** |
| M4 | `_whole_hours` stops truncating to the hour, so every quarter is its own 'hour' | `static_tariff.py` | -10 B | **5** |
| M5 | the plan is dropped from the *Tariff* row - the shipped defect 3 | `main.py` | -133 B | **8** |
| M6 | the plan's age is a constant, so a 5400 s old plan reads as current | `main.py` | -10 B | **1** |
| M7 | the *Arbitrage* row goes back to INFERRING 'off' from a missing decision - the shipped defect 4 | `main.py` | +33 B | **4** |
| M8 | a stale decision renders exactly like a fresh one | `main.py` | -144 B | **2** |
| M9 | `cycle_at` is never stamped, so nothing can tell 'this cycle decided' from 'a previous one did' | `main.py` | -28 B | **2** |
| M10 | `arb_at` is never stamped either - the row loses the age it reports | `main.py` | -18 B | **4** |
| M11 | §8.3's refusal says 'hours' where it means points - the shipped defect 2, verbatim | `static_tariff.py` | -1 B | **1** |
| M12 | the flat refusal drops the hour figure and goes back to calling points hours | `static_tariff.py` | -38 B | **1** |
| M13 | a provider site with no plan falls silently back to the static sentence - the state the soak measured 802 times | `main.py` | -166 B | **1** |
| M14 | only the FIRST spacing is reported, so a mixed 900/3600 plan reads as uniformly quarter-hourly | `static_tariff.py` | -23 B | **1** |
| M15 | `origin` stops reaching the sentence: a §11.4 fallback reads as an observation of the feed | `static_tariff.py` | -190 B | **1** |
| M16 | PAIRED: M1 and M3 together - the two rungs that answer 'how many hours' weakened at once | `static_tariff.py` | -6 B | **2** |
⚠️ **TWO OF THEM WERE FOUND BY THE BATTERY IN THE CHECK ITSELF**, which is why
the battery is not optional:
* **M15 was a SURVIVOR of the whole suite at −190 B on the first draft.** The
origin pair gave the §11.4 plan `provider="static_profile"` as well as
`origin="fallback"` — what a real one carries — so the two rows differed for
**two** reasons and dropping `origin` from the sentence entirely changed
nothing the check could see. Habit 4, in the check written to close this
card. The provider string is now held **equal** and that equality is its own
precondition check.
* **M9 produced a TRACEBACK and NO NAMED RED.** `cycle_at` unstamped is
`__init__`'s `None`, and `datetime < None` raised inside the check's
**f-string operand** — FCST-06's finding, in the operand rather than in the
condition, grading none of the checks after it. It goes through `derived()`
now and M9 is red on two named checks.
## Not in this card
* **TARIFF-09's `price_feed_url` override warning still predicts the wrong degraded state** (`app/price.py:1574`): on a cold store the unpublished hours are **absent**, not "carried forward and flagged provisional". That was item 3 of the original card and is not among the four this ticket names; absence is the worse state and the warning names the milder one. Left for its own card.
* Nothing in `app/tariff.py` or the capacity-peak state (TARIFF-13's).
* No behaviour changed: no threshold, refusal rung, claim or setpoint moved. Every edit is a sentence, a derived counter behind one, or where a sentence is rendered.
All 29 test files green.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Four defects of one shape - the add-on's report of its own tariff plan was
wrong, always in the direction that flattered it - and one check, which is
the deliverable.
1. `TariffPlan.words` counted POINTS and called them HOURS. The live add-on
logged `dynamic_dayahead from generic_dayahead, 132 h` for 132
quarter-hourly points = 33 hours. Belgium moved to a 15-minute MTU on
2025-10-01, so this was wrong every time it was printed, and NOT by a
constant factor: a 40-hour soak measured 1.79x .. 5.33x, because §8.3's
carried points are hourly while the firm ones are quarter-hourly.
`words` now reports both quantities, plus the firm/carried split and the
spacing, in FCST-04's *Price history* wording so the two rows can be
compared.
2. §8.3's firm-pair refusal and the flat-tariff refusal did the same, and
they reach an operator through `self.arb.explain()`. This is exactly the
point<->hour conflation TARIFF-14 removed from `at()`, left standing in
the prose. `price_forecast.py`'s §8.3 carry-forward log line carried the
third instance - a carried-HOUR count over a POINT count - and DOCS
quotes it verbatim, so it is corrected here too.
3. The plan reached no HTTP surface at all. `rows["Tariff"]` was
`static_status()`'s 30 s-tick sentence and never saw the provider plan -
it read 'no static profile' at all 802 soak samples on a site whose feed
was answering. It now carries the plan's shape. ⚠️ The two sources are
kept distinguishable: the plan half is stamped with its own age, measured
from `tariff_at`, because it can be up to TARIFF_MAX_AGE_S (5400 s) old.
FCST-04 pins that row to DOCS as a pair; DOCS moves in this commit.
4. `main.py`'s "if and only if" was false. `cycle()` returns at the input
hold before `self.arb = None`, so an armed site read
`off (arbitrage_enabled)` - naming an option that is on - and, after one
completed cycle, showed a decision up to 15 s stale as a current one. The
row now reads the OPTION and separates three facts; `cycle_at` and
`arb_at` make "this cycle reached no decision" an observation.
THE DELIVERABLE IS THE CHECK. All four survived 29 green suites because
nothing graded the strings a human is shown. `test_price.py` section 12
renders through the shipped `status_payload()`, parses each number out of
its OWN SLOT and compares it against a figure computed from the fixture's
construction parameters - not from the property the row was built out of
(habit 4), and not as a substring of a sentence the same file wrote (habit
14). The fixture set is four plan shapes and the check computes and prints
the field pairs no fixture separates. The *Arbitrage* row's states are
driven through real cycles with a sensor answering None and graded by
INEQUALITY.
No behaviour changed: no threshold, refusal rung, claim or setpoint moved.
MUTATIONS: 16, no survivors, one at +0 B (the risky delta) and red. Two were
found by the battery INSIDE THIS CARD'S OWN CHECK: a -190 B survivor whose
fixture varied two things at once (habit 4), and a mutation whose red was a
TRACEBACK in an f-string operand and therefore named nothing (FCST-06). Both
closed; the table is on the PR.
All 29 test files green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Four defects of one shape: the add-on's account of itself was wrong, always in the direction that flattered it. They shipped together because nothing in this repository graded the strings a human is shown — every tariff check inspects the
TariffPlanobject, and all four were in the sentences built out of it.Reproduced before the fix, driven through the shipped
Controlleragainst the committedenergy-chartscapture; the harness and its transcript are at/home/homelab/.cache/t11-mut/.1.
TariffPlan.wordscounted points and called them hoursMeasured before:
dynamic_dayahead from generic_dayahead, 192 hfor 192 quarter-hourly points = 48 hours. Belgium moved to a 15-minute MTU on 2025-10-01, so on the real feed it was wrong every time it was printed — and not by a constant factor: across a 40-hour soak the overstatement ranged 1.79× to 5.33×, because §8.3's carried points are hourly while the firm ones are quarter-hourly.After:
dynamic_dayahead from generic_dayahead, 192 points over 48 h, 192 firm / 0 carried, 900 s spacing.N points over H his FCST-04's Price history wording on purpose, so the two rows describing the same feed can be compared.2. The §8.3 and flat-tariff refusals did the same — and they reach an operator
Before:
§8.3: only 1 of 96 hours carry a published priceabout 96 points, which are 24 hours; the flat rung counted the same points and called themfirm hours. ⚠️ This is exactly the point≠hour conflation TARIFF-14 removed fromat(), left standing in the prose that describes it, and it reaches the status page throughself.arb.explain().After:
§8.3: only 1 of 96 points carry a published price - 1 of 24 hours - and the rest are provisional …andthe tariff is flat - every one of the 96 firm points, spanning 24 hours, costs ….The same conflation in
price_forecast.py's §8.3 carry-forward log line —%d of %d hourswith a carried-hour count over a point count — is corrected in the same commit, because DOCS quotes that line verbatim.3. The plan reached no HTTP surface at all
Before, driven:
rows["Tariff"] == 'no static profile'on a site whose feed had just answered with a 192-point plan.origin,product, the horizon and the firm/carried split were in no row — the soak read'no static profile'at all 802 samples.After:
⚠️ The two Tariff sources are kept distinguishable.
static_status()runs on the 30 s I/O-free tick; the provider plan comes off the 1800 s refresh and is served for up toTARIFF_MAX_AGE_S(5400 s since TARIFF-12). So the plan half carries its own age, measured fromtariff_at— which TARIFF-12 deliberately does not move on a kept plan — and the check drives the same plan object at 0 s and at 4000 s and asserts the two rows differ. A provider site with no usable plan says so, instead of falling silently back to the static sentence.⚠️ FCST-04's Tariff↔DOCS pin is honoured, not routed around.
test_price_history.pypins the Tariff row and DOCS as a PAIR; wiring the plan in took it red, and DOCS.md now carries the sentence it had been overclaiming, with the row's five facts and the two-clock warning written out.4.
main.py:4486-4489's "if and only if" was falsecycle()returns at the input hold — inputs missing,bad_for < stale_input_s, 15 s by default — before it reachesself.arb = None. Driven,arbitrage_enabled: trueplus a sensor answeringNone:and the second half, after a healthy cycle whose SoC then drops out:
The row reads the option instead of inferring it from a missing decision;
cycle_at(stamped beforecycle()'s first return) andarb_atare what make "this cycle reached no decision" an observation rather than a guess. The comment now states what is true.The deliverable: a check that reads the RENDERED SURFACE
test_price.pysection 12. It renders through the shippedController.status_payload()and gradesrows["Tariff"]androws["Arbitrage"]."132 points" in rowis satisfied by the number appearing in the other number's slot — MUTATION.md habit 14's zero-byte edit. Each number is pulled out of its own slot and compared against a figure computed from the fixture's two construction parameters, never from the property the row was built out of (habit 4).none. It deliberately contains both the degenerate hourly arrangement (points == hours, where the defect hid for three cards) and the quarter-hourly one where they differ 4×.originandproductare graded by disagreement, not by substring: two plans over the identical point list differing in one field must render differently.Noneand — for the stale case — by a real second cycle rather than a hand-set attribute, and graded by inequality: two facts that must not render identically.scenarios/energy-charts-be-price-*.json, graded against the captured document's own length (habit 19).Mutations
Every check driven. Harness
/home/homelab/.cache/t11-mut/run.py: each mutation applied to a freshcp -aof the clean worktree (never edit-and-revert — habit 18),__pycache__purged on the host, tree bind-mounted:roso no cache can be written into it, mutated line asserted on disk, byte delta recorded, full 29-file run each.16 mutations, run rather than reasoned about, no survivors. M2 is
+0 Band is called out as such, per MUTATION.md's reporting rule. Harness
/home/homelab/.cache/t11-mut/run.py: each mutation on a freshcp -aof theclean worktree rather than edit-and-revert (habit 18),
__pycache__purged,tree bind-mounted
:roso no cache can be written into it, the mutated lineasserted on disk, full 29-file run each.
wordsprints the point count in the hour slotstatic_tariff.pystatic_tariff.pyhourscounts POINTS - the defect one level down fromwordsstatic_tariff.py_whole_hoursstops truncating to the hour, so every quarter is its own 'hour'static_tariff.pymain.pymain.pymain.pymain.pycycle_atis never stamped, so nothing can tell 'this cycle decided' from 'a previous one did'main.pyarb_atis never stamped either - the row loses the age it reportsmain.pystatic_tariff.pystatic_tariff.pymain.pystatic_tariff.pyoriginstops reaching the sentence: a §11.4 fallback reads as an observation of the feedstatic_tariff.pystatic_tariff.py⚠️ TWO OF THEM WERE FOUND BY THE BATTERY IN THE CHECK ITSELF, which is why
the battery is not optional:
origin pair gave the §11.4 plan
provider="static_profile"as well asorigin="fallback"— what a real one carries — so the two rows differed fortwo reasons and dropping
originfrom the sentence entirely changednothing the check could see. Habit 4, in the check written to close this
card. The provider string is now held equal and that equality is its own
precondition check.
cycle_atunstamped is__init__'sNone, anddatetime < Noneraised inside the check'sf-string operand — FCST-06's finding, in the operand rather than in the
condition, grading none of the checks after it. It goes through
derived()now and M9 is red on two named checks.
Not in this card
price_feed_urloverride warning still predicts the wrong degraded state (app/price.py:1574): on a cold store the unpublished hours are absent, not "carried forward and flagged provisional". That was item 3 of the original card and is not among the four this ticket names; absence is the worse state and the warning names the milder one. Left for its own card.app/tariff.pyor the capacity-peak state (TARIFF-13's).All 29 test files green.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU