TEL-14 N1: trim DECISIONS (drop probe transcript, §1 to the reason), move the mutation ledger there with M2/M5
checks / unit (pull_request) Successful in 8m44s

test_pv.py part 11 now prints a one-line pointer instead of the 12-row table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
2026-09-25 01:11:40 +02:00
co-authored by Claude Opus 5.5
parent 93f349a6af
commit 7fa045af85
2 changed files with 37 additions and 57 deletions
+35 -37
View File
@@ -7,49 +7,24 @@ Owner report on 0.12.0: `sensor.espressif_vermogen` (ESPHome clamp) and
## 1. This reverses TEL-11 §2, and why
TEL-11 §2 refused a `pv_invert` because (1) PV is read by nothing in the control
path, and (2) a reversed sensor "announces itself as −4 kW at noon" and should be
"fixed where it is produced". (1) still holds: the flip is applied inside
`pv.total()`, which only feeds the MQTT `pv` row and the status page;
`test_pv.py` part 12 re-runs part 6's 40-cycle identity with a flip set, and
`tools/tel11_pv_trace.py` on release/1.0 vs this tree gives identical `control`
documents for `unset`, `one` and `multi`; the only telemetry change is the new
caution in the `negative` (−4200 W) cells. Its `--compare` verdict itself now
REFUSES on any honest run, because both trees know `pv_entities` — TEL-12 §3's
discriminator has aged out, as TEL-11's did; not fixed here. (2) did
not survive contact with a real site: these sensors are not *wrong*, they
follow a different convention; at night they read a plausible small positive
number, so nothing announces anything; and "fix it where it is produced" means a
template sensor, which TEL-12 §2 already names as the place an `unavailable`
source goes quietly wrong.
TEL-11 §2 refused a `pv_invert` on two grounds. "PV is read by nothing in the
control path" still holds: the flip lives in `pv.total()`, and `test_pv.py`
part 12 re-runs part 6's 40-cycle identity with a flip set. "A reversed sensor
announces itself" did not survive a real site: these sensors follow a different
convention, read a plausible small positive number at night, and the only other
fix is a template sensor, the place TEL-12 §2 says goes quietly wrong.
## 2. The shape: a parallel list, `pv_invert_entities` — measured
## 2. The shape: a parallel list, `pv_invert_entities`
`tools/supervisor_shape_probe.py` §H, Supervisor 2026.08.0, against a 0.12.0
blob (`pv_entities` set, no new key):
```
== H. TEL-14: the SHAPE of a per-entity PV sign option ==
PASS (a) PREFIX: `-sensor.pv_meter` validates under the shipped `- str` - Supervisor does not look inside the string, ...
PASS (b) ⭐ PARALLEL LIST, SHIPPED: the 0.12.0 blob (no `pv_invert_entities` key) validates unchanged and the new key defaults to []
PASS ...and one entity marked inverted validates
PASS ⭐ ...and the shipped `options:` block with ONLY `pv_invert_entities` removed is REFUSED BY NAME ...
Missing option 'pv_invert_entities' in root in GoodWe RS485 Controller (local_goodwe_controller)
PASS ...and the Configuration tab renders it as a REPEATABLE string field with its own top-level (translated) label
[{"name": "pv_invert_entities", "multiple": true, "required": true, "type": "string"}]
PASS (c) LIST OF DICTS ON `pv_entities`: the STRINGS a 0.12.0 install stored are REFUSED ...
Invalid dict for option 'pv_entities' in GoodWe RS485 Controller (local_goodwe_controller)
PASS (c2) ...as a THIRD key it validates, but renders as a type-schema repeatable section whose per-entry keys reach the screen RAW ...
PASS (d) GLOBAL `pv_invert: bool` validates too - it loses on SEMANTICS (one sign for a mixed roster), not on the schema
== all cases passed ==
```
Measured by `tools/supervisor_shape_probe.py` §H (Supervisor 2026.08.0, against
a 0.12.0 blob); re-run it for the transcript.
| shape | schema | why it lost / won |
|---|---|---|
| (a) `-sensor.x` prefix | accepted | invisible: the sign is a syntax only the description explains, in a field an installer fills with the entity picker's text |
| (b) parallel list | accepted, 0.12.0 blob unchanged | **ships**: its own labelled, translated field ("PV entities whose sign is reversed"); an installer lists the odd ones out |
| (c) dicts on `pv_entities` | **refuses 0.12.0's stored strings** | bricks the upgrade — TEL-12's trap again |
| (c2) dicts as a third key | accepted | per-entry labels render raw (`entity`, `invert`) — §F; and a third PV surface |
| (c2) dicts as a third key | accepted | per-entry labels render raw (`entity`, `invert`) — probe §F; and a third PV surface |
| (d) global bool | accepted | one sign cannot describe inverter + meter in one roster |
(b)'s known cost is a typo in the second list: `pv.stray_inverts()` turns it into
@@ -67,12 +42,35 @@ three correct meters at −20 W sum to −60 W and must stay quiet, while a reve
a wrong threshold costs a spurious or missed caution, never data. The text says
ADD (unflipped) or REMOVE (already flipped).
## 4. History
## 4. Mutations
Fresh `cp -a` each, `__pycache__` purged, CI image, `:ro`, `--network none`;
`test_pv.py` only unless stated. T1-T12 at 7559d21; M2/M5 are review #1628's,
red since B1 (merged into release/1.0 @ 43b2740).
| # | mutation | bytes | reds |
|---|---|---|---|
| T1 | flip ignored in `total()` | −45 | 9 |
| T2 | flip applied to every member | −7 | 6 |
| T3 | `0.0 - w` → `-w` | −5 | 1 |
| T4 | flip written as `abs()` | −1 | 2 |
| T5 | `_read_pv()` stops passing the flip | −26 | 7 |
| T6 | caution threshold −50 → 0 W | −17 | 3 |
| T7 | `[sign inverted]` marker dropped | −39 | 3 |
| T8 | negative caution made `bad` | +1 | 1 |
| T9 | `stray_inverts()` always empty | −50 | 1 |
| T10 | `inverted()` stops stripping | −8 | 1 |
| T11 | ADD/REMOVE advice swapped | +4 | 2 |
| T12 | `pv_invert_entities: []` deleted from `options:` | −25 | 2 test_translations + 17 probe |
| M2 | `stray_inverts()` roster ignores `pv_entity` | +15 | 2 (REPORTED CASE, PADDING) |
| M5 | `_read_pv()` flip set not stripped | +25 | 1 (PADDING) |
## 5. History
OPT-10's `sensor.goodwe_pv_power` recorded since 2026-09-24 on an install with
such sensors carries the old sign; nothing rewrites it.
## 5. CHANGELOG entry for the next PKG cut
## 6. CHANGELOG entry for the next PKG cut
Not written into `CHANGELOG.md`: `test_version_gate.py` 11b/11d refuse an
`## Unreleased` heading (measured on this branch: 4 reds). For PKG to paste:
+2 -20
View File
@@ -1343,27 +1343,9 @@ print(" time - see DECISIONS/TEL-11-pv-production.md §5 for the transcript."
print(" ⚠️ A RED COUNT IS A PROPERTY OF THE SUITE AS MUCH AS OF THE MUTATION:")
print(" these name THIS tree. Re-measure rather than inherit.")
# --- TEL-14's battery: fresh copy each, no __pycache__, CI image, :ro, --------
# --network none. Every red is a NAMED check in part 12; no traceback.
print()
print(" TEL-14 mutation bytes reds")
print(" ------------------------------------------------ ----- --------")
for row in [
("T1 the flip ignored in total()", "-45 B", "9 here"),
("T2 the flip applied to EVERY member", "-7 B", "6 here"),
("T3 `0.0 - w` -> `-w` (a -0.0 leaks)", "-5 B", "1 here"),
("T4 the flip written as abs()", "-1 B", "2 here"),
("T5 _read_pv() stops passing the flip", "-26 B", "7 here"),
("T6 caution threshold -50 W -> 0 W", "-17 B", "3 here"),
("T7 the [sign inverted] marker dropped", "-39 B", "3 here"),
("T8 the negative caution made `bad`", "+1 B", "1 here"),
("T9 stray_inverts() always empty", "-50 B", "1 here"),
("T10 inverted() stops stripping", "-8 B", "1 here"),
("T11 ADD/REMOVE advice swapped", "+4 B", "2 here"),
("T12 `pv_invert_entities: []` deleted from options:", "-25 B",
"2 test_translations + 17 probe"),
]:
print(f" {row[0]:<48} {row[1]:>5} {row[2]}")
print(" TEL-14: 14 mutations, all red, no survivors - bytes and named reds in")
print(" DECISIONS/TEL-14-pv-sign.md §4.")
print()
if fails: