Stacked on TEL-10 (#92). Base is TEL-10, not release/1.0, so this diff carries only SAFETY-48's own commits. Retarget to release/1.0 after TEL-10 merges if the forge does not do it automatically.
The defect, in one line of arithmetic
compute() freezes the integrator when the inverter stops tracking, and the comment beside that freeze states the requirement outright: "the INTEGRATOR stop accumulating, not only the command". But frozen is computed from one signal — abs(prev_w - actual_w) > saturation_w. Slew-limiting never enters it.
the integrator moves gain * error per cycle
the command moves at most slew per cycle
so the integrator outruns the command whenever |error| > slew / gain
667 W at the live install's shape. The measured median grid error at the onset of a recorded episode is 1114 W. ⚠️ And SAFETY-39's retune made it more frequent, not less: a smaller slew_w makes that threshold lower. The unwind afterwards is integrator_max_w / slew_w cycles — 12.5, about a minute at this meter's rate.
Four decisions, each argued in §4 of the decision record: it is inside the branch; it is written one-sided (a clarity choice — the two-sided band is equivalent on 205 920 of 205 920 swept cases, measured, not reasoned); it moves i_w and not target (this cycle's command is already decided, what is bounded is what gets carried); and the cap is in steps, not watts, so it moves with the rail instead of outliving the slew_w it was derived against.
The measured effect — read the export row, not the peak row
Replayed over 16.00 days / 256 252 meter instants of the live install's own recorder (tools/replay_recorded_loop.py). The disturbance, the tick and the control law are real; the inverter and the pack are modelled.
The robust result is the export reduction: about an eighth of modelled export, and it falls at every plant and tuning variant tried.
release/1.0
SAFETY-48
export kWh
39.61
34.07 (−5.54)
wasted throughput kWh
4.67
3.19
battery sign reversals
2992
2770
recovery median s
232
199
unrecovered (> 900 s)
53
31
|i_w − prev_w| max W
9200
800
⚠️Do NOT read this as a capacity-tariff improvement. The worst quarter-hour average offtake — the billed quantity — reads 3739 → 3434 W only at the plant lag the replay happens to fit. At every lag ≤ 5.0 s, including the one that best reproduces the recorded export, release/1.0 is already at 3434 W and this card changes it by exactly zero:
plant lag
fit RMS W
replayed export kWh (rel)
worst quarter: rel / form A / SAFETY-48
0.0
392.3
23.5
3434 / 3434 / 3434
2.5
376.4
28.9 ← nearest the recorded 29.43
3434 / ⚠️ 3732 / 3434
5.0
378.0
32.1
3434 / ⚠️ 3711 / 3434
7.5
375.3 ← chosen
39.6
3739 / ⚠️ 3935 / 3434
10.0
384.2
75.4
3954 / ⚠️ 4326 / 3434
What the card can claim on the billed quantity is that it never makes it worse, at any lag. §3a carries this table.
Form A was built, measured and rejected
The card asked for the asymmetric rule already in service for saturation, on a slew flag carried across cycles the way sat_count is. It was built from release/1.0's own source, one substitution per change, and run through the same pipeline.
It wins on four rows and loses on the one that is billed. Export 34.43, import 110.62, sign reversals 2656, wasted 2.99, recovery median 184 — and the worst quarter-hour offtake 3739 → 3935 W, with the mean quarter +2.73 W over 1534 quarters, so it is systematic rather than one unlucky quarter.
⚠️And that rejection is the least lag-sensitive result in the card. Form A is worse on the billed quarter at every lag where release/1.0 produces a peak at all (3732 / 3711 / 3935 / 4326 against 3434 / 3434 / 3739 / 3954; a tie at lag 0.0 where both sit at 3434), and worse at every top-N cut of the quarters at the fitted plant — top-1 3934.6 vs 3738.7, top-5 3299.7 vs 3260.5, top-10 2890.6 vs 2871.0, top-50 1882.7 vs 1880.0, top-100 1481.7 vs 1479.0.
⚠️The card said "do not invent a second rule", and the measurement said to. That instruction is recorded in §3 rather than quietly dropped.
The honest limits, stated before the results are argued over
⚠️The replay is ~35 % pessimistic on export and prints it on every run. Replaying the before-law reproduces 39.6 kWh where the install actually exported 29.43 kWh. The delta is what the instrument is for — both laws run on the same ticks, the same disturbance, the same plant, the same seed — and no absolute figure here should be quoted as "what the install did".
⚠️The plant lag is not identified by this data.fit_plant() picks 7.5 s on battery-power RMS, which separates it from 2.5 s by 0.3 %; the tool's own printed fidelity measure prefers 2.5 s by the whole 35 % gap. Two objectives on the same recording disagree and nothing in the capture breaks the tie. A driven step response on the rig is what would settle it; that is a different card.
⚠️The cap DOES move a saturation-frozen integrator. It runs before the freeze block and is not gated on frozen. Of 79 160 frozen ticks it changed i_w on 1095, and on 679 of those it moved it further out on the side it was already on — putting back magnitude the freeze had just let fall, which is the one direction the freeze's rule ("may fall, never rise") exists to forbid. Worst case 2424 W, at prev_w −3400 W, grid +4697 W, actual −4144 W, sat_count 2 → 3: the freeze leaves the integrator at −176 W, the slew binds, and the cap drags it to −2600 W. It never overrides a same-cycle refusal (the freeze clipped on 75 512 ticks, the slew bound on none of them) — but it does not leave the frozen path alone either.
That is where SAFETY-13's committed settle values went, and it is stated rather than smoothed. Gating the cap on not frozen puts test_control.py part 5 back at release/1.0's −1190 W exactly. The gate is refused anyway: it leaves a worst observed lead of 4961 W, i.e. no bound at all on the quantity this card exists to bound.
Nothing here drives an inverter. The replay is a model with a 375 W residual and no arbiter in it. §8 lists the rest of what the card does not establish, including the brief's own cost figure, which does not survive (~14 kWh claimed, 5.5 kWh measured).
Two committed transcripts changed meaning, and neither is a silent edit
SAFETY-39's "the slew does not own the over-absorption" is now false — inverted, on two independent plant models.test_control.py's SAFETY-39 section (a first-order plant it writes itself, no recorded data in it) and tools/slew_sim.py (admin/goodwe's plantsim.Plant, a third model) agree:
slew_w
1000
800
700
600
556
500
400
300
250
release/1.0
958
1107
1133
1058
987
1029
1150
1143
1081
this branch
777
734
663
563
553
507
435
358
321
⚠️SAFETY-39 was not wrong. Its row is what release/1.0 still prints today — flat, non-monotone, 800 genuinely better than 556 — so its record gets a forward reference, not a retraction. slew_sim.py now reads SLEW_LEAD_STEPS off the law it imported and narrates from the column it just printed, with a third branch that refuses to narrate at all if the combination is one it has no sentence for. Both branches were driven.
Verified by breaking it
test_telemetry.py part 4 and test_control.py's SAFETY-39 section, with the full mutation table (L1–L6, byte deltas and red counts) in test_telemetry.py's header, and the instrument's own battery (I2–I4) in §7.
⚠️The placement was a SURVIVOR of all thirty files until a check was written for it. Moving the cap out of the branch is a −92 B edit that is bit-identical on the shipped configuration — 0 of 256 246 ticks differ over the recording. It is separable only where integrator_max_w is deliberately set abovemax_w; test_telemetry.py part 4 now drives exactly that, and the unconditional form is red on it.
⚠️L5 and L6 are both zero-byte edits — min/max and slewed/prev_w are each the same length — so no fingerprint and no byte delta can see either.
An independent review ran off-PR, and its findings are in a6af332
This branch was reviewed before a PR existed, so that history is not in this thread. Five of the seven findings landed here (the other two are in the stacked TEL-10 (#92) PR). All are in the second commit, so they can be re-checked rather than rediscovered. None of them is in executable code — app/control.py's diff in that commit is comment-only, verified mechanically; the one executable change anywhere is slew_sim.py's narration selector.
Every figure below was re-measured against the committed capture before it was written down.
R1 — a retracted justification was still in the shipped source, and it was false about the code.control.py claimed an unconditional placement "would also pull the integrator toward a command the SATURATION freeze is pinning", priced at 118 of 256 250 ticks. The 118 had been measured against a different pair; on the pair the sentence names the two forms are bit-identical (0 of 256 246, re-measured and confirmed). The comment now gives the real reason (the integrator_max_w > max_w configuration) and the uncomfortable half — the frozen-tick figures above, all of which reproduced exactly: 79 160 / 1095 / 679 / 2424 W, and the worked tick is the worst case. One wording disagreement was not adopted: "the direction the freeze had just refused" is not literally true, because on all 1095 ticks the freeze did not clip moved that cycle; the comment says what was actually measured instead.
R3 — the record misread its own sensitivity table. §7b read the lag 5.0 → 3434 → 3434 rows as "the direction does not move"; what they show is the improvement vanishing. New §3a carries the lag table (every cell reproduced), states the lag is not identified, and re-orders the record so the export reduction leads. The form-A rejection is strengthened, not weakened.
R2 — DOCS.md stated modelled watts to the owner as measurement, with no fidelity caveat. Rewritten: export reduction first, "these are simulated watt-hours" explicit, and no capacity-tariff promise.
R4 — slew_sim.py narrated the opposite of the table it printed (MUTATION.md habit 8). Fixed as described above; SAFETY-39's record gets a forward reference rather than a rewrite of history.
R5 — three mutation counts were decoration. Re-driven in the CI image on cp -a trees, purged, :ro, one file at a time: L5 is 5+5 with the min/max function names exchanged and 5+4 with the branch bodies exchanged — two different mutations wearing one name, now listed separately as L5a/L5b — and L6 is 6+3, not 4+3. The separable-state count is grid-dependent and three sweeps disagree (2050/257 400, 1220/246 064, and a third grid spelled out in full giving 776/40 320), so all three are quoted with their provenance and the load-bearing claim is where they separate, not how many.
R7 — one causal sentence was too big for the data."A large import answered too weakly" is not what form A does in aggregate: it cuts import by 5.45 kWh and export by 5.18 kWh, and paired per tick delivers 8.56 kWh less offtake energy on the ticks release/1.0 was importing. The harm is a tail redistribution inside quarters that already net to offtake. The conclusion stands; the sentence is narrowed to it.
Two things found while fixing that nobody asked for
⚠️control.py's form-A / lead-cap bullet carried stale figures that contradict the committed transcript beside it: "15.7 days", 3839 W as release/1.0's worst quarter (it is 3739), 39.8 kWh, 3050 → 2806 reversals, 237 → 214 s, 32.1 kWh, and "the meter's measured 4.84 s tick" (median 5.01 s). Corrected to the measured values while rewriting that block. Flagged because it was outside the review's scope and is easy to revert if you would rather it were its own change.
⚠️§5's claim that gating the cap on not frozen "leaves SAFETY-13 bit-identical" is false. It restores test_control.py part 5 (−1190 W) but not part 4(b), which stays at −1200 W against release/1.0's −2390 W — that one moved for the other reason, the winding before the freeze being bounded. §5's four figures for that variant also do not reproduce: export 34.71 (not 34.9), worst quarter 3434 (not 3461), worst lead 4961 W (not 4966); wasted 3.49 ≈ 3.5 ✓. Corrected, with the one-line gate spelling written into the record so the next reader can re-run it instead of re-deriving it.
What could not be reproduced
Neither published separable-state grid (2050 of 257 400, 1220 of 246 064) could be reproduced, because neither grid is recorded anywhere. A third grid — written out in full in §4 — gives 776 of 40 320. All three agree on the only part that is load-bearing: separation happens only in the integrator_max_w > max_w tuning and nowhere else.
Form A is not committed anywhere either, so it was reconstructed for this re-measurement. The reconstruction reproduces §3's column exactly (34.43 / 110.62 / 2656 / 2.99 / 3934.59 / +2.73 / 184.04), which is the evidence that it matches the form the implementing agent measured.
Suite state
Full suite in the CI image, purged tree, PYTHONDONTWRITEBYTECODE=1, :ro mount, one file at a time — the exact loop from .gitea/workflows/checks.yml — run on each branch separately:
branch
result
release/1.0 @ 68c5ede
29/29 PASS, exit 0
TEL-10 @ b6c6da3 (base of this PR)
30/30 PASS, exit 0
SAFETY-48 @ a6af332 (this PR)
30/30 PASS, exit 0
Nothing here reached the inverter. The live install still runs slew_w: 400 applied by hand on 2026-09-05 and release/1.0's control law. This branch does not deploy and does not bump the version. No ha addons update, no writes to /addons/, and no contact of any kind with the live box or the inverter.
> **Stacked on [TEL-10 (#92)](http://192.168.2.147:3000/admin/goodwe-addon/pulls/92).** Base is `TEL-10`, not `release/1.0`, so this diff carries only SAFETY-48's own commits. Retarget to `release/1.0` after TEL-10 merges if the forge does not do it automatically.
## The defect, in one line of arithmetic
`compute()` freezes the integrator when the inverter stops tracking, and the comment beside that freeze states the requirement outright: *"the INTEGRATOR stop accumulating, not only the command"*. But `frozen` is computed from **one** signal — `abs(prev_w - actual_w) > saturation_w`. **Slew-limiting never enters it.**
```
the integrator moves gain * error per cycle
the command moves at most slew per cycle
so the integrator outruns the command whenever |error| > slew / gain
```
667 W at the live install's shape. The measured median grid error at the onset of a recorded episode is **1114 W**. ⚠️ And SAFETY-39's retune made it *more* frequent, not less: a smaller `slew_w` makes that threshold **lower**. The unwind afterwards is `integrator_max_w / slew_w` cycles — 12.5, about a minute at this meter's rate.
## What ships
```python
slew = slew_limit_w(tuning)
slewed = max(prev_w - slew, min(prev_w + slew, target))
if slewed != target:
reason = "slew-limited"
_lead_cap = SLEW_LEAD_STEPS * slew
if target > slewed:
i_w = min(i_w, slewed + _lead_cap)
elif target < slewed:
i_w = max(i_w, slewed - _lead_cap)
target = slewed
```
Four decisions, each argued in §4 of the decision record: it is **inside the branch**; it is written **one-sided** (a clarity choice — the two-sided band is *equivalent* on 205 920 of 205 920 swept cases, measured, not reasoned); it moves **`i_w` and not `target`** (this cycle's command is already decided, what is bounded is what gets *carried*); and the cap is in **steps, not watts**, so it moves with the rail instead of outliving the `slew_w` it was derived against.
## The measured effect — read the export row, not the peak row
Replayed over **16.00 days / 256 252 meter instants** of the live install's own recorder (`tools/replay_recorded_loop.py`). The disturbance, the tick and the control law are real; the inverter and the pack are modelled.
**The robust result is the export reduction: about an eighth of modelled export, and it falls at every plant and tuning variant tried.**
| | release/1.0 | SAFETY-48 |
|---|---:|---:|
| **export kWh** | 39.61 | **34.07** (−5.54) |
| wasted throughput kWh | 4.67 | **3.19** |
| battery sign reversals | 2992 | **2770** |
| recovery median s | 232 | **199** |
| unrecovered (> 900 s) | 53 | **31** |
| \|i_w − prev_w\| max W | 9200 | **800** |
⚠️ **Do NOT read this as a capacity-tariff improvement.** The worst quarter-hour average offtake — the billed quantity — reads 3739 → 3434 W **only at the plant lag the replay happens to fit**. At every lag ≤ 5.0 s, *including the one that best reproduces the recorded export*, release/1.0 is **already at 3434 W** and this card changes it by **exactly zero**:
| plant lag | fit RMS W | replayed export kWh (rel) | worst quarter: rel / form A / SAFETY-48 |
|---:|---:|---:|---|
| 0.0 | 392.3 | 23.5 | 3434 / 3434 / **3434** |
| 2.5 | **376.4** | **28.9** ← nearest the recorded 29.43 | 3434 / ⚠️ 3732 / **3434** |
| 5.0 | 378.0 | 32.1 | 3434 / ⚠️ 3711 / **3434** |
| 7.5 | **375.3** ← chosen | 39.6 | 3739 / ⚠️ 3935 / **3434** |
| 10.0 | 384.2 | 75.4 | 3954 / ⚠️ 4326 / **3434** |
What the card *can* claim on the billed quantity is that it never makes it worse, at any lag. §3a carries this table.
## Form A was built, measured and rejected
The card asked for the asymmetric rule already in service for saturation, on a slew flag carried across cycles the way `sat_count` is. It was built from release/1.0's own source, one substitution per change, and run through the same pipeline.
**It wins on four rows and loses on the one that is billed.** Export 34.43, import 110.62, sign reversals 2656, wasted 2.99, recovery median 184 — and the worst quarter-hour **offtake 3739 → 3935 W**, with the **mean** quarter +2.73 W over 1534 quarters, so it is systematic rather than one unlucky quarter.
⚠️ **And that rejection is the least lag-sensitive result in the card.** Form A is worse on the billed quarter at **every lag where release/1.0 produces a peak at all** (3732 / 3711 / 3935 / 4326 against 3434 / 3434 / 3739 / 3954; a tie at lag 0.0 where both sit at 3434), and worse at **every top-N cut** of the quarters at the fitted plant — top-1 3934.6 vs 3738.7, top-5 3299.7 vs 3260.5, top-10 2890.6 vs 2871.0, top-50 1882.7 vs 1880.0, top-100 1481.7 vs 1479.0.
⚠️ **The card said "do not invent a second rule", and the measurement said to.** That instruction is recorded in §3 rather than quietly dropped.
## The honest limits, stated before the results are argued over
* ⚠️ **The replay is ~35 % pessimistic on export and prints it on every run.** Replaying the before-law reproduces 39.6 kWh where the install actually exported **29.43 kWh**. The delta is what the instrument is for — both laws run on the same ticks, the same disturbance, the same plant, the same seed — and no absolute figure here should be quoted as "what the install did".
* ⚠️ **The plant lag is not identified by this data.** `fit_plant()` picks 7.5 s on battery-power RMS, which separates it from 2.5 s by **0.3 %**; the tool's own printed fidelity measure prefers 2.5 s by the whole 35 % gap. Two objectives on the same recording disagree and nothing in the capture breaks the tie. A driven step response on the rig is what would settle it; that is a different card.
* ⚠️ **The cap DOES move a saturation-frozen integrator.** It runs before the freeze block and is not gated on `frozen`. Of **79 160** frozen ticks it changed `i_w` on **1095**, and on **679** of those it moved it further out on the side it was already on — putting back magnitude the freeze had just let fall, which is the one direction the freeze's rule ("may fall, never rise") exists to forbid. **Worst case 2424 W**, at `prev_w −3400 W, grid +4697 W, actual −4144 W, sat_count 2 → 3`: the freeze leaves the integrator at −176 W, the slew binds, and the cap drags it to −2600 W. It never overrides a *same-cycle* refusal (the freeze clipped on 75 512 ticks, the slew bound on none of them) — but it does not leave the frozen path alone either.
* **That is where SAFETY-13's committed settle values went**, and it is stated rather than smoothed. Gating the cap on `not frozen` puts `test_control.py` part 5 back at release/1.0's −1190 W exactly. The gate is refused anyway: it leaves a worst observed lead of **4961 W**, i.e. no bound at all on the quantity this card exists to bound.
* **Nothing here drives an inverter.** The replay is a model with a 375 W residual and no arbiter in it. §8 lists the rest of what the card does not establish, including the brief's own cost figure, which does **not** survive (~14 kWh claimed, 5.5 kWh measured).
## Two committed transcripts changed meaning, and neither is a silent edit
**SAFETY-39's *"the slew does not own the over-absorption"* is now false — inverted, on two independent plant models.** `test_control.py`'s SAFETY-39 section (a first-order plant it writes itself, no recorded data in it) and `tools/slew_sim.py` (admin/goodwe's `plantsim.Plant`, a third model) agree:
| `slew_w` | 1000 | 800 | 700 | 600 | 556 | 500 | 400 | 300 | 250 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| release/1.0 | 958 | 1107 | 1133 | 1058 | 987 | 1029 | 1150 | 1143 | 1081 |
| **this branch** | 777 | 734 | 663 | 563 | 553 | 507 | 435 | 358 | 321 |
⚠️ **SAFETY-39 was not wrong.** Its row is what release/1.0 still prints today — flat, non-monotone, 800 genuinely better than 556 — so its record gets a **forward reference**, not a retraction. `slew_sim.py` now reads `SLEW_LEAD_STEPS` off the law it imported and narrates from the column it just printed, with a third branch that refuses to narrate at all if the combination is one it has no sentence for. Both branches were driven.
## Verified by breaking it
`test_telemetry.py` part 4 and `test_control.py`'s SAFETY-39 section, with the full mutation table (L1–L6, byte deltas and red counts) in `test_telemetry.py`'s header, and the instrument's own battery (I2–I4) in §7.
⚠️ **The placement was a SURVIVOR of all thirty files until a check was written for it.** Moving the cap out of the branch is a −92 B edit that is **bit-identical on the shipped configuration** — 0 of 256 246 ticks differ over the recording. It is separable only where `integrator_max_w` is deliberately set **above** `max_w`; `test_telemetry.py` part 4 now drives exactly that, and the unconditional form is red on it.
⚠️ **L5 and L6 are both zero-byte edits** — `min`/`max` and `slewed`/`prev_w` are each the same length — so no fingerprint and no byte delta can see either.
---
## An independent review ran off-PR, and its findings are in `a6af332`
This branch was reviewed before a PR existed, so that history is not in this thread. Five of the seven findings landed here (the other two are in the stacked [TEL-10 (#92)](http://192.168.2.147:3000/admin/goodwe-addon/pulls/92) PR). All are in the second commit, so they can be re-checked rather than rediscovered. **None of them is in executable code** — `app/control.py`'s diff in that commit is comment-only, verified mechanically; the one executable change anywhere is `slew_sim.py`'s narration selector.
Every figure below was **re-measured against the committed capture** before it was written down.
* **R1 — a retracted justification was still in the shipped source, and it was false about the code.** `control.py` claimed an unconditional placement *"would also pull the integrator toward a command the SATURATION freeze is pinning"*, priced at 118 of 256 250 ticks. The 118 had been measured against a *different* pair; on the pair the sentence names the two forms are bit-identical (**0 of 256 246**, re-measured and confirmed). The comment now gives the real reason (the `integrator_max_w > max_w` configuration) **and** the uncomfortable half — the frozen-tick figures above, all of which reproduced exactly: 79 160 / 1095 / 679 / 2424 W, and the worked tick *is* the worst case. One wording disagreement was **not** adopted: "the direction the freeze had just refused" is not literally true, because on all 1095 ticks the freeze did not clip `moved` that cycle; the comment says what was actually measured instead.
* **R3 — the record misread its own sensitivity table.** §7b read the `lag 5.0 → 3434 → 3434` rows as *"the direction does not move"*; what they show is the improvement **vanishing**. New §3a carries the lag table (every cell reproduced), states the lag is not identified, and re-orders the record so the export reduction leads. The form-A rejection is **strengthened**, not weakened.
* **R2 — `DOCS.md` stated modelled watts to the owner as measurement**, with no fidelity caveat. Rewritten: export reduction first, "these are simulated watt-hours" explicit, and no capacity-tariff promise.
* **R4 — `slew_sim.py` narrated the opposite of the table it printed** (MUTATION.md habit 8). Fixed as described above; SAFETY-39's record gets a forward reference rather than a rewrite of history.
* **R5 — three mutation counts were decoration.** Re-driven in the CI image on `cp -a` trees, purged, `:ro`, one file at a time: **L5 is 5+5** with the `min`/`max` function names exchanged and **5+4** with the branch bodies exchanged — two different mutations wearing one name, now listed separately as L5a/L5b — and **L6 is 6+3**, not 4+3. The separable-state count is grid-dependent and three sweeps disagree (2050/257 400, 1220/246 064, and a third grid spelled out in full giving 776/40 320), so all three are quoted with their provenance and the load-bearing claim is **where** they separate, not how many.
* **R7 — one causal sentence was too big for the data.** *"A large import answered too weakly"* is not what form A does in aggregate: it cuts import by 5.45 kWh and export by 5.18 kWh, and paired per tick delivers **8.56 kWh less** offtake energy on the ticks release/1.0 was importing. The harm is a **tail redistribution** inside quarters that already net to offtake. The conclusion stands; the sentence is narrowed to it.
### Two things found while fixing that nobody asked for
* ⚠️ **`control.py`'s form-A / lead-cap bullet carried stale figures** that contradict the committed transcript beside it: "15.7 days", `3839 W` as release/1.0's worst quarter (it is 3739), 39.8 kWh, 3050 → 2806 reversals, 237 → 214 s, 32.1 kWh, and "the meter's measured 4.84 s tick" (median 5.01 s). Corrected to the measured values while rewriting that block. Flagged because it was outside the review's scope and is easy to revert if you would rather it were its own change.
* ⚠️ **§5's claim that gating the cap on `not frozen` "leaves SAFETY-13 bit-identical" is false.** It restores `test_control.py` part 5 (−1190 W) but **not** part 4(b), which stays at −1200 W against release/1.0's −2390 W — that one moved for the other reason, the winding *before* the freeze being bounded. §5's four figures for that variant also do not reproduce: export **34.71** (not 34.9), worst quarter **3434** (not 3461), worst lead **4961 W** (not 4966); wasted 3.49 ≈ 3.5 ✓. Corrected, with the one-line gate spelling written into the record so the next reader can re-run it instead of re-deriving it.
### What could not be reproduced
Neither published separable-state grid (2050 of 257 400, 1220 of 246 064) could be reproduced, because neither grid is recorded anywhere. A third grid — written out in full in §4 — gives 776 of 40 320. All three agree on the only part that is load-bearing: separation happens **only** in the `integrator_max_w > max_w` tuning and nowhere else.
Form A is not committed anywhere either, so it was reconstructed for this re-measurement. The reconstruction reproduces §3's column **exactly** (34.43 / 110.62 / 2656 / 2.99 / 3934.59 / +2.73 / 184.04), which is the evidence that it matches the form the implementing agent measured.
## Suite state
Full suite in the CI image, purged tree, `PYTHONDONTWRITEBYTECODE=1`, `:ro` mount, one file at a time — the exact loop from `.gitea/workflows/checks.yml` — run on each branch separately:
| branch | result |
|---|---|
| `release/1.0` @ `68c5ede` | **29/29 PASS, exit 0** |
| `TEL-10` @ `b6c6da3` (base of this PR) | **30/30 PASS, exit 0** |
| **`SAFETY-48` @ `a6af332` (this PR)** | **30/30 PASS, exit 0** |
**Nothing here reached the inverter.** The live install still runs `slew_w: 400` applied by hand on 2026-09-05 and release/1.0's control law. This branch does not deploy and does not bump the version. No `ha addons update`, no writes to `/addons/`, and no contact of any kind with the live box or the inverter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️The green on this PR's head is a SKIP, not a pass — do not read it as a verdict.
.gitea/workflows/checks.yml triggers on pull_request with branches: [release/1.0, master]. This PR is deliberately based on TEL-10, so the workflow does not run and Gitea resolves checks / unit (pull_request) as skipped, which its status API rolls up as success. Nothing has been checked by CI on this branch through this PR.
That is a consequence of stacking and it is the right trade here — it keeps this diff to SAFETY-48's own two commits instead of showing TEL-10's as well — but it means the CI verdict arrives only after this PR is retargeted to release/1.0, once #92 merges. Please do not merge this on the strength of the skip.
What does exist meanwhile is a local run of the exact loop from checks.yml — CI image, purged tree, PYTHONDONTWRITEBYTECODE=1, :ro mount, one file at a time — on each branch separately:
branch
result
release/1.0 @ 68c5ede
29/29 PASS, exit 0
TEL-10 @ b6c6da3
30/30 PASS, exit 0
SAFETY-48 @ a6af332
30/30 PASS, exit 0
That is the same command, not the same runner, and it graded the branch rather than the merge — so it is weaker than a real gate run on both counts. It is not a substitute for the post-retarget green.
⚠️ **The green on this PR's head is a SKIP, not a pass — do not read it as a verdict.**
`.gitea/workflows/checks.yml` triggers on `pull_request` with `branches: [release/1.0, master]`. This PR is deliberately based on **`TEL-10`**, so the workflow does not run and Gitea resolves `checks / unit (pull_request)` as **skipped**, which its status API rolls up as `success`. Nothing has been checked by CI on this branch through this PR.
That is a consequence of stacking and it is the right trade here — it keeps this diff to SAFETY-48's own two commits instead of showing TEL-10's as well — but it means **the CI verdict arrives only after this PR is retargeted to `release/1.0`**, once [#92](http://192.168.2.147:3000/admin/goodwe-addon/pulls/92) merges. Please do not merge this on the strength of the skip.
What does exist meanwhile is a local run of the exact loop from `checks.yml` — CI image, purged tree, `PYTHONDONTWRITEBYTECODE=1`, `:ro` mount, one file at a time — on each branch separately:
| branch | result |
|---|---|
| `release/1.0` @ `68c5ede` | 29/29 PASS, exit 0 |
| `TEL-10` @ `b6c6da3` | 30/30 PASS, exit 0 |
| `SAFETY-48` @ `a6af332` | 30/30 PASS, exit 0 |
That is the same command, not the same runner, and it graded the branch rather than the merge — so it is weaker than a real gate run on both counts. It is not a substitute for the post-retarget green.
admin
changed target branch from TEL-10 to release/1.02026-09-19 10:54:18 +02:00
`compute()` freezes the integrator with an asymmetric rule and the comment
beside it states the requirement outright - "the requirement is that the
INTEGRATOR stop accumulating, not only the command" - but `frozen` is computed
from ONE signal, `abs(prev_w - actual_w) > saturation_w`, and slew-limiting
never enters it. The integrator moves `gain * error` a cycle and the command
moves at most `slew_w`, so the integrator outruns the command whenever
`|error| > slew_w / gain`: 667 W at the live shape, against a measured median
grid error of 1114 W at the onset of a recorded episode. SAFETY-39's retune
made it MORE frequent, not less - a smaller step is a lower threshold.
⚠️ THE FORM THE CARD ASKED FOR RAISES THE BILLED QUANTITY, WHICH IS WHY IT IS
NOT WHAT SHIPPED. Reusing the asymmetric freeze on a carried slew flag was
built from release/1.0's own source and measured over 16.00 days / 256 252
meter instants of the live install's recorder: export 39.61 -> 34.43 kWh, and
the worst quarter-hour average OFFTAKE 3739 -> 3935 W with the mean quarter
+2.73 W, in five of six plant and tuning variants. A frozen integrator stops at
`prev + gain*error`, which undershoots by (1 - gain); for an export that is
free and for an import it is offtake, for as long as the ramp lasts. The card
said "do not invent a second rule"; the measurement says to, and both sentences
are recorded.
WHAT SHIPS BOUNDS THE LEAD AT ONE SLEW STEP, inside the branch where the slew
actually bound. Same 16 days, same ticks, same disturbance, same plant, same
seed: export 39.61 -> 34.07 kWh, wasted throughput 4.67 -> 3.19 kWh, battery
sign reversals 2992 -> 2770, median recovery 232 -> 199 s, p99 849 -> 850 s,
max 901 -> 898 s, unrecovered 53 -> 31, worst |i_w - prev_w| 9200 -> 800 W -
and the worst 15-minute average offtake 3739 -> 3434 W, i.e. the billed
quantity IMPROVES. `i_w = slewed` (a cap of zero) is better again at 32.02 kWh
and is refused: it is the velocity form this module's docstring says `i_w=None`
collapses to.
TWO COMMITTED NUMBERS MOVED, DELIBERATELY, WITH THE REASON BESIDE THEM.
SAFETY-13's released command settles at -800 W and -1200 W where release/1.0
settled at -1190 W and -2390 W, because there is less wound integrator to
settle onto; AC 4's property - it stops at the frozen integrator, not the rail
- is what is asserted and the second is now graded against `i_w` rather than a
literal. And SAFETY-39's "the slew does not own the over-absorption" is now
FALSE - INVERTED, on the independent first-order plant test_control.py writes
itself: 1005/1109/1285/1330 W rising with a slower slew becomes 729/550/424/326
falling. Two plant models that share nothing agree on the sign.
Evidence: `tools/replay_recorded_loop.py` plus the read-only capture it made
(scenarios/loop-2026-09-02_2026-09-17). The "before" law is the shipped file
with the block removed by a named substitution - never a transcription - and a
missing anchor is a REFUSAL carrying "NOTHING HAS BEEN MEASURED". The
instrument prints its own fidelity first: it reproduces 39.6 kWh of export
where the install exported 29.4, i.e. it is ~35 % pessimistic about the loop's
tracking, so only the delta is claimed.
30/30 suites green, including test_control.py's exhaustive i_w=None
equivalence sweep against the transcribed release/1.0 law, untouched: the cap
moves `i_w` and not `target`, and that sweep compares (target_w, sat_count,
frozen, reason).
Six law mutations, one instrument battery. L4 - the cap applied
unconditionally, -92 B - was a SURVIVOR of all thirty files: it is
bit-identical on the shipped configuration (0 of 256 246 ticks) and separable
on 2050 of 257 400 swept states, all at `integrator_max_w` above `max_w`, which
part 4 now drives. L5 and L6 are ZERO BYTES.
AND TWO COMMENTS IN THIS CARD WERE INVENTED AND ARE CORRECTED IN PLACE. The
one-sided spelling was justified by a two-sided form that would "drag a
trailing integrator" - it does not, and the two agree on 205 920 of 205 920
swept states; and a "118 of 256 252 ticks" figure had been measured against a
different pair. MUTATION.md gains habits 21 and 22 for both shapes.
NOT DEPLOYED, NOT VERSIONED. The live install still runs release/1.0's law.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Four review findings, none of them in executable code. The control law, the
tests and the instruments' arithmetic are untouched; every number below was
re-measured against the committed 16-day capture before it was written down.
R1 - app/control.py carried a justification the decision record and MUTATION.md
habit 22 had already retracted, and it was false about the code it sits in. It
said an unconditional placement "would also pull the integrator toward a
command the SATURATION freeze is pinning", priced at 118 of 256 250 ticks. The
118 was measured against a different pair; on the pair the sentence names the
two forms are bit-identical - 0 of 256 246 ticks, re-measured. And the narrow
form that shipped DOES move a saturation-frozen integrator: it runs before the
freeze block and is not gated on `frozen`. Of 79 160 frozen ticks the cap
changes `i_w` on 1095, and on 679 of those it moves it further out on the side
it was already on - putting back magnitude the freeze had just let fall, which
the freeze's own rule forbids. Worst case 2424 W, at prev_w -3400 W, grid
+4697 W, actual -4144 W, sat_count 2 -> 3: the freeze leaves the integrator at
-176 W and the cap drags it to -2600 W. It never overrides a same-cycle refusal
(the freeze clipped on 75 512 ticks, the slew bound on none of them), and that
is stated too. It is also where SAFETY-13's settle values went: gating the cap
on `not frozen` puts test_control.py part 5 back at -1190 W exactly, while part
4(b) does not come back - that one moved for the winding reason.
R3 - the record read its own sensitivity table backwards. `fit_plant()` picks
lag 7.5 s on battery-power RMS, which separates it from 2.5 s by 0.3 %, while
the tool's own printed fidelity measure (export against the recorded 29.43 kWh)
prefers 2.5 s by the whole 35 % gap. Swept properly: at every lag <= 5.0 s
release/1.0's worst quarter is ALREADY 3434 W and this card changes it by
exactly zero. The 3739 -> 3434 W headline exists at one plant. New section 3a
carries the table; the export reduction now leads every summary and the peak is
presented as lag-dependent. The form-A rejection is strengthened rather than
weakened: form A is worse on the billed quarter at every lag where release/1.0
produces a peak at all, and at every top-N cut of the quarters.
R2 - DOCS.md stated modelled watts to the owner as measurement. It now leads
with the export reduction, says the figures are simulated, and says plainly
that no capacity-tariff improvement is promised.
R4 - tools/slew_sim.py --slew narrated "the OVER-ABSORPTION does not move, and
at the slowest slews it is slightly WORSE" while printing a monotonically
falling column. That sentence is TRUE on release/1.0 (reproduced:
958/1107/1133/1058/987/1029/1150/1143/1081) and false under this branch's law
(777/734/663/563/553/507/435/358/321), so SAFETY-39 is not retracted: its
record gets a forward reference, and the script now reads SLEW_LEAD_STEPS out
of the law it imported and narrates from the column it just printed. Both
branches of that narration were driven.
R5 - three counts in the L table were decoration and are re-driven in the CI
image on a `cp -a`, purged, `:ro`, one file at a time: L5 is 5+5 with the
function names exchanged and 5+4 with the branch bodies exchanged (two
mutations wearing one name, now listed separately), L6 is 6+3. The separable-
state count is grid-dependent and three sweeps disagree, so all three are
quoted with their grids and the load-bearing claim is WHERE they separate.
R7 - "a large import answered too weakly" is not what the aggregate shows. Form
A cuts import by 5.45 kWh and export by 5.18 kWh, and paired per tick delivers
8.56 kWh less offtake energy on the ticks release/1.0 was importing. The harm
is a tail redistribution inside quarters that already net to offtake. The
conclusion stands; the sentence is narrowed to it.
Verified: the full suite in the CI image, purged tree, `:ro` mount, one file at
a time, exactly the command in .gitea/workflows/checks.yml - 30/30, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
The cap moving a saturation-frozen integrator — judged, not just confirmed
app/control.py, the third ⚠️ block inside the if slewed != target: branch. This is the one place the new law overrides an existing safety rule, so I re-measured it rather than reading the disclosure, and then formed a view on whether it is acceptable rather than whether it is stated.
The disclosed figures all reproduce, exactly
Driven over the committed 16-day capture with the shipped law and the before-law called on the same inputs at every tick (--network none, so nothing touched the live box):
ticks replayed 256251
frozen ticks 79160 claim 79160 ✓
cap changed i_w on 1095 claim 1095 ✓
... further out, same side 679 claim 679 ✓
worst move, FURTHER-OUT subset 2424 W claim 2424 W ✓
worst case (679 subset) prev_w -3400, grid +4697, actual -4144,
sat_count 2 -> 3, i_w -176 -> -2600
The worked tick in the comment is not merely a case, it is the worst of that subset, and it is present in the recording with exactly the numbers written down. That is the standard habit 22 asks for and it is met.
Verdict: acceptable — and the reason is stronger than the disclosure makes it sound
The bullet describes the individual move honestly ("putting back magnitude the freeze had just let fall, which is the one direction the freeze's own rule exists to forbid") but stops before the fact that decides it: where the integrator lands.
On the shipped configuration integrator_max_w is unset, so limit == max_w, so after the bounded step target == i_w identically. The cap therefore makes
|i_w - slewed| <= SLEW_LEAD_STEPS * slew
on every slew-limited cycle, frozen ones included. The freeze block then only pulls target back toward prev_w, and |slewed - prev_w| <= slew, so the carried integrator can never stand more than two slew steps from the command actually on the wire. Measured over the whole recording rather than argued:
release/1.0
SAFETY-48
max |i_w − prev_w| over 256 251 ticks, frozen ticks included
9200 W
800 W (= 2 × 400)
So on the frozen path this card replaces an integrator bounded only by integrator_max_w — up to ±5000 W away from the command — with one bounded at two slew steps. Every one of the 679 ticks lands inside that tighter envelope. The direction of the individual move is backwards with respect to the freeze's rule; the resulting state is strictly closer to the command than anything the freeze alone permits.
That is what makes it acceptable to me. The freeze exists so that an inverter which is not tracking cannot leave the loop wound up. After this card the loop is less wound up on those ticks, not more. And the same-cycle command cannot be affected at all — the cap moves i_w after slewed is already decided — so the only route to harm is the trajectory, which is measured: worst quarter 3739 → 3434 W, export 39.61 → 34.07 kWh.
I also confirmed the refusal of the obvious alternative is real rather than asserted. Gating the cap with if frozen: _lead_cap = float("inf"), the spelling §5 writes out:
release/1.0 export 39.61 wasted 4.67 worstQ 3738.7 worst lead 9200 W
SAFETY-48 shipped export 34.07 wasted 3.19 worstQ 3433.9 worst lead 800 W
gated on `not frozen` export 34.71 wasted 3.49 worstQ 3433.9 worst lead 4961 W
All four of §5's corrected figures land on the nose. The gate really does leave the bound with a hole big enough to make the card pointless.
One number I would add — non-blocking
"Worst case 2424 W" is correctly scoped to the 679 further-out ticks by the sentence it sits in. But the bullet's subject is how much this cap disturbs a frozen integrator, and a reader auditing that question will take 2424 W as the answer. Over all 1095 moved ticks the largest move is 4220 W:
I checked whether that case is worse in substance and it is not — it is the cap pulling a stale positive integrator down across zero to follow a command that is charging, magnitude falls 2420 → 1800, and it is the cap doing exactly its job. That is why it is not in the further-out set and why I am not blocking on it.
Still worth a half-sentence, because the two numbers answer two different questions and only one of them is currently written down: 1095 ticks, worst move 4220 W; of those, 679 moved further out, worst 2424 W. Given habit 22 governs this exact comment block, the distinction is cheap to state and expensive to rediscover.
## The cap moving a saturation-frozen integrator — judged, not just confirmed
`app/control.py`, the third ⚠️ block inside the `if slewed != target:` branch. This is the one place the new law overrides an existing safety rule, so I re-measured it rather than reading the disclosure, and then formed a view on whether it is *acceptable* rather than whether it is *stated*.
### The disclosed figures all reproduce, exactly
Driven over the committed 16-day capture with the shipped law and the before-law called on the same inputs at every tick (`--network none`, so nothing touched the live box):
```
ticks replayed 256251
frozen ticks 79160 claim 79160 ✓
cap changed i_w on 1095 claim 1095 ✓
... further out, same side 679 claim 679 ✓
worst move, FURTHER-OUT subset 2424 W claim 2424 W ✓
worst case (679 subset) prev_w -3400, grid +4697, actual -4144,
sat_count 2 -> 3, i_w -176 -> -2600
```
The worked tick in the comment is not merely *a* case, it is **the** worst of that subset, and it is present in the recording with exactly the numbers written down. That is the standard habit 22 asks for and it is met.
### Verdict: acceptable — and the reason is stronger than the disclosure makes it sound
The bullet describes the individual move honestly ("putting back magnitude the freeze had just let fall, which is the one direction the freeze's own rule exists to forbid") but stops before the fact that decides it: **where the integrator lands.**
On the shipped configuration `integrator_max_w` is unset, so `limit == max_w`, so after the `bounded` step `target == i_w` identically. The cap therefore makes
```
|i_w - slewed| <= SLEW_LEAD_STEPS * slew
```
on **every** slew-limited cycle, frozen ones included. The freeze block then only pulls `target` back toward `prev_w`, and `|slewed - prev_w| <= slew`, so the carried integrator can never stand more than two slew steps from the command actually on the wire. Measured over the whole recording rather than argued:
| | release/1.0 | SAFETY-48 |
|---|---:|---:|
| max \|i_w − prev_w\| over 256 251 ticks, frozen ticks included | 9200 W | **800 W** (= 2 × 400) |
So on the frozen path this card replaces an integrator bounded only by `integrator_max_w` — up to ±5000 W away from the command — with one bounded at two slew steps. Every one of the 679 ticks lands *inside* that tighter envelope. The direction of the individual move is backwards with respect to the freeze's rule; the resulting state is strictly closer to the command than anything the freeze alone permits.
That is what makes it acceptable to me. The freeze exists so that an inverter which is not tracking cannot leave the loop wound up. After this card the loop is **less** wound up on those ticks, not more. And the same-cycle command cannot be affected at all — the cap moves `i_w` after `slewed` is already decided — so the only route to harm is the trajectory, which is measured: worst quarter 3739 → 3434 W, export 39.61 → 34.07 kWh.
I also confirmed the refusal of the obvious alternative is real rather than asserted. Gating the cap with `if frozen: _lead_cap = float("inf")`, the spelling §5 writes out:
```
release/1.0 export 39.61 wasted 4.67 worstQ 3738.7 worst lead 9200 W
SAFETY-48 shipped export 34.07 wasted 3.19 worstQ 3433.9 worst lead 800 W
gated on `not frozen` export 34.71 wasted 3.49 worstQ 3433.9 worst lead 4961 W
```
All four of §5's corrected figures land on the nose. The gate really does leave the bound with a hole big enough to make the card pointless.
### One number I would add — non-blocking
"**Worst case 2424 W**" is correctly scoped to the 679 further-out ticks by the sentence it sits in. But the bullet's subject is *how much this cap disturbs a frozen integrator*, and a reader auditing that question will take 2424 W as the answer. Over all **1095** moved ticks the largest move is **4220 W**:
```
prev_w -2600, grid +7690, actual -3366, sat_count 4 -> 5
i_w +2420 -> -1800 (reason saturated-freeze)
```
I checked whether that case is worse in substance and it is not — it is the cap pulling a stale positive integrator down across zero to follow a command that is charging, magnitude *falls* 2420 → 1800, and it is the cap doing exactly its job. That is why it is not in the further-out set and why I am not blocking on it.
Still worth a half-sentence, because the two numbers answer two different questions and only one of them is currently written down: `1095 ticks, worst move 4220 W; of those, 679 moved further out, worst 2424 W`. Given habit 22 governs this exact comment block, the distinction is cheap to state and expensive to rediscover.
Second comment, covering everything outside the frozen-integrator question. Short version: I found nothing false. Every figure I checked reproduced, including the ones a previous round got wrong.
The honest-limits section does stop the misreading
I went looking specifically for a reader who could come away thinking this improves their capacity-tariff peak. I do not think the framing lets that through, and I checked the claim rather than the wording — my own lag sweep, --sensitivity's substitution convention, every cell:
lag
export rel
export S48
worst Q rel
worst Q S48
mean Q ΔW
0.0
23.5
21.6
3434
3434
+0.53
2.5
28.9
25.1
3434
3434
−0.59
5.0
32.1
27.2
3434
3434
−1.02
7.5
39.6
34.1
3739
3434
−2.39
10.0
75.4
68.3
3954
3434
−4.65
§3a's table is reproduced cell for cell. "The peak claim is a property of the plant model, not of the card" is the correct reading and it is the one written down, in the record, in the PR body, in DOCS.md ("treat the peak as unchanged") and in control.py's own bullet. Four places, consistent. That is enough.
I also chased the tail statistic, because §3 and §5 use per-quarter regression to condemn form A and I wanted to see whether the same standard was applied to the shipping form. It is — §3's table carries worst quarter-hour delta +532 W for SAFETY-48 beside form A's +992 W, and §5 states it again in prose. For completeness, the distribution at the fitted lag: 68 of 1534 quarters regress at all, 6 by more than 100 W, 1 by more than 300 W. The regressed quarters sit at 189–826 W absolute, nowhere near the 3434 W maximum, so none of them can set a billed peak. No finding.
The two unasked-for corrections are true
This is the one that matters most, because a corrected claim that is still wrong is worse than the original.
control.py's form-A / lead-cap figures. Every one of the (b) numbers is exact against the default run of the committed tool: export 39.61 → 34.07, wasted 4.67 → 3.19, reversals 2992 → 2770, worst quarter 3738.67 → 3433.94, recovery median 232.06 → 199.04. The tick figure now reads 5.0 s and the tool prints median 5.01 s. The old 3839 W / 4.84 s / 15.7 days set is indeed gone.
§5's four replacement figures for the not frozen gate — 34.71 kWh, 3434 W, 3.49 kWh, 4961 W — all four reproduce (see my other comment). The superseded set (34.9 / 3461 / 3.5 / 4966) does not. The correction was the right call and the new numbers are right.
Form A: not committed, but independently re-derivable
The body is straight about this — form A exists nowhere in the tree and was reconstructed for the re-measurement. That is the one claim in the card a third party cannot re-run from the repository, and since the form-A rejection is described as the least lag-sensitive result here, it is the claim carrying the most weight with the least committed evidence.
So I rebuilt it myself, from §3's prose alone ("the asymmetric rule already in service for saturation, on a flag carried across cycles the way sat_count is"), without looking at anyone's reconstruction:
Seven for seven from the description alone. The rejection is real and the prose is a sufficient specification. I am not asking for form A to be committed — but that it reconstructs this cleanly from §3 is worth having in the thread, because it is the evidence that §3 is a specification and not a summary.
Mutation evidence — re-driven, every count exact
Purged cp -a trees, CI image, :ro, one file at a time. Last round's counts were irreproducible; this round's are not.
#
byte delta
reds measured
claimed
L2 SLEW_LEAD_STEPS = 0
+0 B
4 telemetry + 3 control
4+3 ✓
L4 cap applied unconditionally
−75 B (my spelling)
1 telemetry, control GREEN
1, survivor of control ✓
L5a min/max names exchanged
+0 B
5 telemetry + 5 control
5+5 ✓
L6 lead against prev_w
+0 B
6 telemetry + 3 control
6+3 ✓
L4 is the interesting one and it behaves as advertised: test_control.py stays green on it and the only thing that catches it is test_telemetry.py part 4's integrator_max_w > max_w case. The survivor-of-all-thirty-files story is accurate.
I separately re-ran TEL-10's M1/M2/M7/M9 on #92 — all exact, details in that thread.
The two-sided equivalence (habit 22's own correction)
Swept 136 080 states across four tunings, every sign of prev_w/grid_w/actual_w/i_w, sat_count 0–13, and the i_w=None path, comparing the shipped one-sided spelling against max(slewed - cap, min(slewed + cap, i_w)) on (target_w, sat_count, frozen, reason, i_w):
0 differences in 136 080. Different grid from the card's 205 920, same answer. The retracted "it would be a bug" claim was correctly retracted.
Suite and CI
tree
result
release/1.0 @ 68c5ede
29/29 PASS, exit 0
TEL-10 merged into release/1.0
30/30 PASS, exit 0
SAFETY-48 merged into release/1.0
30/30 PASS, exit 0
SAFETY-48 merged into TEL-10
30/30 PASS, exit 0
Run on the merge, as CI-02 does, not on the branch tip.
⚠️On the skipped-CI problem in #1346 — confirmed, and worse than a stale status. I retargeted this PR to release/1.0 after #92 merged, and retargeting alone did not trigger a run: the skipped status from the TEL-10 base stayed attached to a6af332 and the combined status still read success. The required context was satisfied by a skip after the retarget, so the merge button was live on a green that had still never executed a test. I forced a genuine pull_request run by reopening the PR rather than merging on that.
That is worth a ticket independent of this card: on this Gitea a base change leaves the old conclusion in place, so any stacked PR retargeted into release/1.0 inherits a green that means nothing. The next person to stack will hit it.
No blocking findings from me. Holding the merge until run 799 reports on its own.
## Review — what I re-measured, and what I could not
Second comment, covering everything outside the frozen-integrator question. Short version: **I found nothing false.** Every figure I checked reproduced, including the ones a previous round got wrong.
### The honest-limits section does stop the misreading
I went looking specifically for a reader who could come away thinking this improves their capacity-tariff peak. I do not think the framing lets that through, and I checked the claim rather than the wording — my own lag sweep, `--sensitivity`'s substitution convention, every cell:
| lag | export rel | export S48 | worst Q rel | worst Q S48 | mean Q ΔW |
|---:|---:|---:|---:|---:|---:|
| 0.0 | 23.5 | 21.6 | 3434 | 3434 | +0.53 |
| 2.5 | 28.9 | 25.1 | 3434 | 3434 | −0.59 |
| 5.0 | 32.1 | 27.2 | 3434 | 3434 | −1.02 |
| 7.5 | 39.6 | 34.1 | 3739 | 3434 | −2.39 |
| 10.0 | 75.4 | 68.3 | 3954 | 3434 | −4.65 |
§3a's table is reproduced cell for cell. "The peak claim is a property of the plant model, not of the card" is the correct reading and it is the one written down, in the record, in the PR body, in `DOCS.md` ("treat the peak as unchanged") and in `control.py`'s own bullet. Four places, consistent. That is enough.
I also chased the tail statistic, because §3 and §5 use per-quarter regression to condemn form A and I wanted to see whether the same standard was applied to the shipping form. It is — §3's table carries `worst quarter-hour delta +532 W` for SAFETY-48 beside form A's +992 W, and §5 states it again in prose. For completeness, the distribution at the fitted lag: 68 of 1534 quarters regress at all, 6 by more than 100 W, 1 by more than 300 W. The regressed quarters sit at **189–826 W absolute**, nowhere near the 3434 W maximum, so none of them can set a billed peak. No finding.
### The two unasked-for corrections are true
This is the one that matters most, because a corrected claim that is still wrong is worse than the original.
* **`control.py`'s form-A / lead-cap figures.** Every one of the (b) numbers is exact against the default run of the committed tool: export 39.61 → 34.07, wasted 4.67 → 3.19, reversals 2992 → 2770, worst quarter 3738.67 → 3433.94, recovery median 232.06 → 199.04. The tick figure now reads 5.0 s and the tool prints `median 5.01 s`. The old `3839 W` / `4.84 s` / `15.7 days` set is indeed gone.
* **§5's four replacement figures** for the `not frozen` gate — 34.71 kWh, 3434 W, 3.49 kWh, 4961 W — all four reproduce (see my other comment). The superseded set (34.9 / 3461 / 3.5 / 4966) does not. The correction was the right call and the new numbers are right.
### Form A: not committed, but independently re-derivable
The body is straight about this — form A exists nowhere in the tree and was reconstructed for the re-measurement. That is the one claim in the card a third party cannot re-run from the repository, and since the form-A rejection is described as the least lag-sensitive result here, it is the claim carrying the most weight with the least committed evidence.
So I rebuilt it myself, from §3's prose alone ("the asymmetric rule already in service for saturation, on a flag carried across cycles the way `sat_count` is"), without looking at anyone's reconstruction:
```
export kWh 34.43 claim 34.43 ✓
import kWh 110.62 claim 110.62 ✓
sign reversals 2656 claim 2656 ✓
wasted kWh 2.99 claim 2.99 ✓
worst 15-min W 3934.59 claim 3934.59 ✓
mean quarter dW +2.73 claim +2.73 ✓
worst quarter dW +991.53 claim +992 ✓
```
Seven for seven from the description alone. The rejection is real and the prose is a sufficient specification. I am not asking for form A to be committed — but that it reconstructs this cleanly from §3 is worth having in the thread, because it is the evidence that §3 is a specification and not a summary.
### Mutation evidence — re-driven, every count exact
Purged `cp -a` trees, CI image, `:ro`, one file at a time. Last round's counts were irreproducible; this round's are not.
| # | byte delta | reds measured | claimed |
|---|---|---|---|
| L2 `SLEW_LEAD_STEPS = 0` | **+0 B** | 4 telemetry + 3 control | 4+3 ✓ |
| L4 cap applied unconditionally | −75 B (my spelling) | 1 telemetry, **control GREEN** | 1, survivor of control ✓ |
| L5a `min`/`max` names exchanged | **+0 B** | 5 telemetry + 5 control | 5+5 ✓ |
| L6 lead against `prev_w` | **+0 B** | 6 telemetry + 3 control | 6+3 ✓ |
L4 is the interesting one and it behaves as advertised: `test_control.py` stays **green** on it and the only thing that catches it is `test_telemetry.py` part 4's `integrator_max_w > max_w` case. The survivor-of-all-thirty-files story is accurate.
I separately re-ran TEL-10's M1/M2/M7/M9 on #92 — all exact, details in that thread.
### The two-sided equivalence (habit 22's own correction)
Swept 136 080 states across four tunings, every sign of `prev_w`/`grid_w`/`actual_w`/`i_w`, `sat_count` 0–13, and the `i_w=None` path, comparing the shipped one-sided spelling against `max(slewed - cap, min(slewed + cap, i_w))` on `(target_w, sat_count, frozen, reason, i_w)`:
**0 differences in 136 080.** Different grid from the card's 205 920, same answer. The retracted "it would be a bug" claim was correctly retracted.
### Suite and CI
| tree | result |
|---|---|
| `release/1.0` @ `68c5ede` | 29/29 PASS, exit 0 |
| `TEL-10` merged into `release/1.0` | 30/30 PASS, exit 0 |
| **`SAFETY-48` merged into `release/1.0`** | **30/30 PASS, exit 0** |
| `SAFETY-48` merged into `TEL-10` | 30/30 PASS, exit 0 |
Run on the **merge**, as CI-02 does, not on the branch tip.
⚠️ **On the skipped-CI problem in [#1346](http://192.168.2.147:3000/admin/goodwe-addon/pulls/93#issuecomment-1346) — confirmed, and worse than a stale status.** I retargeted this PR to `release/1.0` after #92 merged, and **retargeting alone did not trigger a run**: the `skipped` status from the `TEL-10` base stayed attached to `a6af332` and the combined status still read `success`. The required context was satisfied by a skip *after* the retarget, so the merge button was live on a green that had still never executed a test. I forced a genuine `pull_request` run by reopening the PR rather than merging on that.
That is worth a ticket independent of this card: on this Gitea a base change leaves the old conclusion in place, so any stacked PR retargeted into `release/1.0` inherits a green that means nothing. The next person to stack will hit it.
---
No blocking findings from me. Holding the merge until run 799 reports on its own.
admin
merged commit 186a044a3a into release/1.02026-09-19 11:05:26 +02:00
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.
The defect, in one line of arithmetic
compute()freezes the integrator when the inverter stops tracking, and the comment beside that freeze states the requirement outright: "the INTEGRATOR stop accumulating, not only the command". Butfrozenis computed from one signal —abs(prev_w - actual_w) > saturation_w. Slew-limiting never enters it.667 W at the live install's shape. The measured median grid error at the onset of a recorded episode is 1114 W. ⚠️ And SAFETY-39's retune made it more frequent, not less: a smaller
slew_wmakes that threshold lower. The unwind afterwards isintegrator_max_w / slew_wcycles — 12.5, about a minute at this meter's rate.What ships
Four decisions, each argued in §4 of the decision record: it is inside the branch; it is written one-sided (a clarity choice — the two-sided band is equivalent on 205 920 of 205 920 swept cases, measured, not reasoned); it moves
i_wand nottarget(this cycle's command is already decided, what is bounded is what gets carried); and the cap is in steps, not watts, so it moves with the rail instead of outliving theslew_wit was derived against.The measured effect — read the export row, not the peak row
Replayed over 16.00 days / 256 252 meter instants of the live install's own recorder (
tools/replay_recorded_loop.py). The disturbance, the tick and the control law are real; the inverter and the pack are modelled.The robust result is the export reduction: about an eighth of modelled export, and it falls at every plant and tuning variant tried.
⚠️ Do NOT read this as a capacity-tariff improvement. The worst quarter-hour average offtake — the billed quantity — reads 3739 → 3434 W only at the plant lag the replay happens to fit. At every lag ≤ 5.0 s, including the one that best reproduces the recorded export, release/1.0 is already at 3434 W and this card changes it by exactly zero:
What the card can claim on the billed quantity is that it never makes it worse, at any lag. §3a carries this table.
Form A was built, measured and rejected
The card asked for the asymmetric rule already in service for saturation, on a slew flag carried across cycles the way
sat_countis. It was built from release/1.0's own source, one substitution per change, and run through the same pipeline.It wins on four rows and loses on the one that is billed. Export 34.43, import 110.62, sign reversals 2656, wasted 2.99, recovery median 184 — and the worst quarter-hour offtake 3739 → 3935 W, with the mean quarter +2.73 W over 1534 quarters, so it is systematic rather than one unlucky quarter.
⚠️ And that rejection is the least lag-sensitive result in the card. Form A is worse on the billed quarter at every lag where release/1.0 produces a peak at all (3732 / 3711 / 3935 / 4326 against 3434 / 3434 / 3739 / 3954; a tie at lag 0.0 where both sit at 3434), and worse at every top-N cut of the quarters at the fitted plant — top-1 3934.6 vs 3738.7, top-5 3299.7 vs 3260.5, top-10 2890.6 vs 2871.0, top-50 1882.7 vs 1880.0, top-100 1481.7 vs 1479.0.
⚠️ The card said "do not invent a second rule", and the measurement said to. That instruction is recorded in §3 rather than quietly dropped.
The honest limits, stated before the results are argued over
fit_plant()picks 7.5 s on battery-power RMS, which separates it from 2.5 s by 0.3 %; the tool's own printed fidelity measure prefers 2.5 s by the whole 35 % gap. Two objectives on the same recording disagree and nothing in the capture breaks the tie. A driven step response on the rig is what would settle it; that is a different card.frozen. Of 79 160 frozen ticks it changedi_won 1095, and on 679 of those it moved it further out on the side it was already on — putting back magnitude the freeze had just let fall, which is the one direction the freeze's rule ("may fall, never rise") exists to forbid. Worst case 2424 W, atprev_w −3400 W, grid +4697 W, actual −4144 W, sat_count 2 → 3: the freeze leaves the integrator at −176 W, the slew binds, and the cap drags it to −2600 W. It never overrides a same-cycle refusal (the freeze clipped on 75 512 ticks, the slew bound on none of them) — but it does not leave the frozen path alone either.not frozenputstest_control.pypart 5 back at release/1.0's −1190 W exactly. The gate is refused anyway: it leaves a worst observed lead of 4961 W, i.e. no bound at all on the quantity this card exists to bound.Two committed transcripts changed meaning, and neither is a silent edit
SAFETY-39's "the slew does not own the over-absorption" is now false — inverted, on two independent plant models.
test_control.py's SAFETY-39 section (a first-order plant it writes itself, no recorded data in it) andtools/slew_sim.py(admin/goodwe'splantsim.Plant, a third model) agree:slew_w⚠️ SAFETY-39 was not wrong. Its row is what release/1.0 still prints today — flat, non-monotone, 800 genuinely better than 556 — so its record gets a forward reference, not a retraction.
slew_sim.pynow readsSLEW_LEAD_STEPSoff the law it imported and narrates from the column it just printed, with a third branch that refuses to narrate at all if the combination is one it has no sentence for. Both branches were driven.Verified by breaking it
test_telemetry.pypart 4 andtest_control.py's SAFETY-39 section, with the full mutation table (L1–L6, byte deltas and red counts) intest_telemetry.py's header, and the instrument's own battery (I2–I4) in §7.⚠️ The placement was a SURVIVOR of all thirty files until a check was written for it. Moving the cap out of the branch is a −92 B edit that is bit-identical on the shipped configuration — 0 of 256 246 ticks differ over the recording. It is separable only where
integrator_max_wis deliberately set abovemax_w;test_telemetry.pypart 4 now drives exactly that, and the unconditional form is red on it.⚠️ L5 and L6 are both zero-byte edits —
min/maxandslewed/prev_ware each the same length — so no fingerprint and no byte delta can see either.An independent review ran off-PR, and its findings are in
a6af332This branch was reviewed before a PR existed, so that history is not in this thread. Five of the seven findings landed here (the other two are in the stacked TEL-10 (#92) PR). All are in the second commit, so they can be re-checked rather than rediscovered. None of them is in executable code —
app/control.py's diff in that commit is comment-only, verified mechanically; the one executable change anywhere isslew_sim.py's narration selector.Every figure below was re-measured against the committed capture before it was written down.
control.pyclaimed an unconditional placement "would also pull the integrator toward a command the SATURATION freeze is pinning", priced at 118 of 256 250 ticks. The 118 had been measured against a different pair; on the pair the sentence names the two forms are bit-identical (0 of 256 246, re-measured and confirmed). The comment now gives the real reason (theintegrator_max_w > max_wconfiguration) and the uncomfortable half — the frozen-tick figures above, all of which reproduced exactly: 79 160 / 1095 / 679 / 2424 W, and the worked tick is the worst case. One wording disagreement was not adopted: "the direction the freeze had just refused" is not literally true, because on all 1095 ticks the freeze did not clipmovedthat cycle; the comment says what was actually measured instead.lag 5.0 → 3434 → 3434rows as "the direction does not move"; what they show is the improvement vanishing. New §3a carries the lag table (every cell reproduced), states the lag is not identified, and re-orders the record so the export reduction leads. The form-A rejection is strengthened, not weakened.DOCS.mdstated modelled watts to the owner as measurement, with no fidelity caveat. Rewritten: export reduction first, "these are simulated watt-hours" explicit, and no capacity-tariff promise.slew_sim.pynarrated the opposite of the table it printed (MUTATION.md habit 8). Fixed as described above; SAFETY-39's record gets a forward reference rather than a rewrite of history.cp -atrees, purged,:ro, one file at a time: L5 is 5+5 with themin/maxfunction names exchanged and 5+4 with the branch bodies exchanged — two different mutations wearing one name, now listed separately as L5a/L5b — and L6 is 6+3, not 4+3. The separable-state count is grid-dependent and three sweeps disagree (2050/257 400, 1220/246 064, and a third grid spelled out in full giving 776/40 320), so all three are quoted with their provenance and the load-bearing claim is where they separate, not how many.Two things found while fixing that nobody asked for
control.py's form-A / lead-cap bullet carried stale figures that contradict the committed transcript beside it: "15.7 days",3839 Was release/1.0's worst quarter (it is 3739), 39.8 kWh, 3050 → 2806 reversals, 237 → 214 s, 32.1 kWh, and "the meter's measured 4.84 s tick" (median 5.01 s). Corrected to the measured values while rewriting that block. Flagged because it was outside the review's scope and is easy to revert if you would rather it were its own change.not frozen"leaves SAFETY-13 bit-identical" is false. It restorestest_control.pypart 5 (−1190 W) but not part 4(b), which stays at −1200 W against release/1.0's −2390 W — that one moved for the other reason, the winding before the freeze being bounded. §5's four figures for that variant also do not reproduce: export 34.71 (not 34.9), worst quarter 3434 (not 3461), worst lead 4961 W (not 4966); wasted 3.49 ≈ 3.5 ✓. Corrected, with the one-line gate spelling written into the record so the next reader can re-run it instead of re-deriving it.What could not be reproduced
Neither published separable-state grid (2050 of 257 400, 1220 of 246 064) could be reproduced, because neither grid is recorded anywhere. A third grid — written out in full in §4 — gives 776 of 40 320. All three agree on the only part that is load-bearing: separation happens only in the
integrator_max_w > max_wtuning and nowhere else.Form A is not committed anywhere either, so it was reconstructed for this re-measurement. The reconstruction reproduces §3's column exactly (34.43 / 110.62 / 2656 / 2.99 / 3934.59 / +2.73 / 184.04), which is the evidence that it matches the form the implementing agent measured.
Suite state
Full suite in the CI image, purged tree,
PYTHONDONTWRITEBYTECODE=1,:romount, one file at a time — the exact loop from.gitea/workflows/checks.yml— run on each branch separately:release/1.0@68c5edeTEL-10@b6c6da3(base of this PR)SAFETY-48@a6af332(this PR)Nothing here reached the inverter. The live install still runs
slew_w: 400applied by hand on 2026-09-05 and release/1.0's control law. This branch does not deploy and does not bump the version. Noha addons update, no writes to/addons/, and no contact of any kind with the live box or the inverter.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
⚠️ The green on this PR's head is a SKIP, not a pass — do not read it as a verdict.
.gitea/workflows/checks.ymltriggers onpull_requestwithbranches: [release/1.0, master]. This PR is deliberately based onTEL-10, so the workflow does not run and Gitea resolveschecks / unit (pull_request)as skipped, which its status API rolls up assuccess. Nothing has been checked by CI on this branch through this PR.That is a consequence of stacking and it is the right trade here — it keeps this diff to SAFETY-48's own two commits instead of showing TEL-10's as well — but it means the CI verdict arrives only after this PR is retargeted to
release/1.0, once #92 merges. Please do not merge this on the strength of the skip.What does exist meanwhile is a local run of the exact loop from
checks.yml— CI image, purged tree,PYTHONDONTWRITEBYTECODE=1,:romount, one file at a time — on each branch separately:release/1.0@68c5edeTEL-10@b6c6da3SAFETY-48@a6af332That is the same command, not the same runner, and it graded the branch rather than the merge — so it is weaker than a real gate run on both counts. It is not a substitute for the post-retarget green.
The cap moving a saturation-frozen integrator — judged, not just confirmed
app/control.py, the third ⚠️ block inside theif slewed != target:branch. This is the one place the new law overrides an existing safety rule, so I re-measured it rather than reading the disclosure, and then formed a view on whether it is acceptable rather than whether it is stated.The disclosed figures all reproduce, exactly
Driven over the committed 16-day capture with the shipped law and the before-law called on the same inputs at every tick (
--network none, so nothing touched the live box):The worked tick in the comment is not merely a case, it is the worst of that subset, and it is present in the recording with exactly the numbers written down. That is the standard habit 22 asks for and it is met.
Verdict: acceptable — and the reason is stronger than the disclosure makes it sound
The bullet describes the individual move honestly ("putting back magnitude the freeze had just let fall, which is the one direction the freeze's own rule exists to forbid") but stops before the fact that decides it: where the integrator lands.
On the shipped configuration
integrator_max_wis unset, solimit == max_w, so after theboundedsteptarget == i_widentically. The cap therefore makeson every slew-limited cycle, frozen ones included. The freeze block then only pulls
targetback towardprev_w, and|slewed - prev_w| <= slew, so the carried integrator can never stand more than two slew steps from the command actually on the wire. Measured over the whole recording rather than argued:So on the frozen path this card replaces an integrator bounded only by
integrator_max_w— up to ±5000 W away from the command — with one bounded at two slew steps. Every one of the 679 ticks lands inside that tighter envelope. The direction of the individual move is backwards with respect to the freeze's rule; the resulting state is strictly closer to the command than anything the freeze alone permits.That is what makes it acceptable to me. The freeze exists so that an inverter which is not tracking cannot leave the loop wound up. After this card the loop is less wound up on those ticks, not more. And the same-cycle command cannot be affected at all — the cap moves
i_wafterslewedis already decided — so the only route to harm is the trajectory, which is measured: worst quarter 3739 → 3434 W, export 39.61 → 34.07 kWh.I also confirmed the refusal of the obvious alternative is real rather than asserted. Gating the cap with
if frozen: _lead_cap = float("inf"), the spelling §5 writes out:All four of §5's corrected figures land on the nose. The gate really does leave the bound with a hole big enough to make the card pointless.
One number I would add — non-blocking
"Worst case 2424 W" is correctly scoped to the 679 further-out ticks by the sentence it sits in. But the bullet's subject is how much this cap disturbs a frozen integrator, and a reader auditing that question will take 2424 W as the answer. Over all 1095 moved ticks the largest move is 4220 W:
I checked whether that case is worse in substance and it is not — it is the cap pulling a stale positive integrator down across zero to follow a command that is charging, magnitude falls 2420 → 1800, and it is the cap doing exactly its job. That is why it is not in the further-out set and why I am not blocking on it.
Still worth a half-sentence, because the two numbers answer two different questions and only one of them is currently written down:
1095 ticks, worst move 4220 W; of those, 679 moved further out, worst 2424 W. Given habit 22 governs this exact comment block, the distinction is cheap to state and expensive to rediscover.Review — what I re-measured, and what I could not
Second comment, covering everything outside the frozen-integrator question. Short version: I found nothing false. Every figure I checked reproduced, including the ones a previous round got wrong.
The honest-limits section does stop the misreading
I went looking specifically for a reader who could come away thinking this improves their capacity-tariff peak. I do not think the framing lets that through, and I checked the claim rather than the wording — my own lag sweep,
--sensitivity's substitution convention, every cell:§3a's table is reproduced cell for cell. "The peak claim is a property of the plant model, not of the card" is the correct reading and it is the one written down, in the record, in the PR body, in
DOCS.md("treat the peak as unchanged") and incontrol.py's own bullet. Four places, consistent. That is enough.I also chased the tail statistic, because §3 and §5 use per-quarter regression to condemn form A and I wanted to see whether the same standard was applied to the shipping form. It is — §3's table carries
worst quarter-hour delta +532 Wfor SAFETY-48 beside form A's +992 W, and §5 states it again in prose. For completeness, the distribution at the fitted lag: 68 of 1534 quarters regress at all, 6 by more than 100 W, 1 by more than 300 W. The regressed quarters sit at 189–826 W absolute, nowhere near the 3434 W maximum, so none of them can set a billed peak. No finding.The two unasked-for corrections are true
This is the one that matters most, because a corrected claim that is still wrong is worse than the original.
control.py's form-A / lead-cap figures. Every one of the (b) numbers is exact against the default run of the committed tool: export 39.61 → 34.07, wasted 4.67 → 3.19, reversals 2992 → 2770, worst quarter 3738.67 → 3433.94, recovery median 232.06 → 199.04. The tick figure now reads 5.0 s and the tool printsmedian 5.01 s. The old3839 W/4.84 s/15.7 daysset is indeed gone.not frozengate — 34.71 kWh, 3434 W, 3.49 kWh, 4961 W — all four reproduce (see my other comment). The superseded set (34.9 / 3461 / 3.5 / 4966) does not. The correction was the right call and the new numbers are right.Form A: not committed, but independently re-derivable
The body is straight about this — form A exists nowhere in the tree and was reconstructed for the re-measurement. That is the one claim in the card a third party cannot re-run from the repository, and since the form-A rejection is described as the least lag-sensitive result here, it is the claim carrying the most weight with the least committed evidence.
So I rebuilt it myself, from §3's prose alone ("the asymmetric rule already in service for saturation, on a flag carried across cycles the way
sat_countis"), without looking at anyone's reconstruction:Seven for seven from the description alone. The rejection is real and the prose is a sufficient specification. I am not asking for form A to be committed — but that it reconstructs this cleanly from §3 is worth having in the thread, because it is the evidence that §3 is a specification and not a summary.
Mutation evidence — re-driven, every count exact
Purged
cp -atrees, CI image,:ro, one file at a time. Last round's counts were irreproducible; this round's are not.SLEW_LEAD_STEPS = 0min/maxnames exchangedprev_wL4 is the interesting one and it behaves as advertised:
test_control.pystays green on it and the only thing that catches it istest_telemetry.pypart 4'sintegrator_max_w > max_wcase. The survivor-of-all-thirty-files story is accurate.I separately re-ran TEL-10's M1/M2/M7/M9 on #92 — all exact, details in that thread.
The two-sided equivalence (habit 22's own correction)
Swept 136 080 states across four tunings, every sign of
prev_w/grid_w/actual_w/i_w,sat_count0–13, and thei_w=Nonepath, comparing the shipped one-sided spelling againstmax(slewed - cap, min(slewed + cap, i_w))on(target_w, sat_count, frozen, reason, i_w):0 differences in 136 080. Different grid from the card's 205 920, same answer. The retracted "it would be a bug" claim was correctly retracted.
Suite and CI
release/1.0@68c5edeTEL-10merged intorelease/1.0SAFETY-48merged intorelease/1.0SAFETY-48merged intoTEL-10Run on the merge, as CI-02 does, not on the branch tip.
⚠️ On the skipped-CI problem in #1346 — confirmed, and worse than a stale status. I retargeted this PR to
release/1.0after #92 merged, and retargeting alone did not trigger a run: theskippedstatus from theTEL-10base stayed attached toa6af332and the combined status still readsuccess. The required context was satisfied by a skip after the retarget, so the merge button was live on a green that had still never executed a test. I forced a genuinepull_requestrun by reopening the PR rather than merging on that.That is worth a ticket independent of this card: on this Gitea a base change leaves the old conclusion in place, so any stacked PR retargeted into
release/1.0inherits a green that means nothing. The next person to stack will hit it.No blocking findings from me. Holding the merge until run 799 reports on its own.