diff --git a/goodwe_controller/DOCS.md b/goodwe_controller/DOCS.md index 0ed60bf..e6e3aa7 100644 --- a/goodwe_controller/DOCS.md +++ b/goodwe_controller/DOCS.md @@ -110,7 +110,8 @@ so Home Assistant's *Configuration* tab renders human labels and a description for every option instead of the raw option keys. What that does and does not buy you is worth stating plainly, because it was **measured against the Supervisor image this site runs** (`ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0`) -rather than assumed — `tools/supervisor_shape_probe.py` section F prints the +rather than assumed — `tools/supervisor_shape_probe.py` section F GRADES (it +used only to print) the whole rendered list, in both languages. | | | diff --git a/goodwe_controller/tools/supervisor_shape_probe.py b/goodwe_controller/tools/supervisor_shape_probe.py index 3b6718e..e1fa976 100644 --- a/goodwe_controller/tools/supervisor_shape_probe.py +++ b/goodwe_controller/tools/supervisor_shape_probe.py @@ -356,7 +356,8 @@ print(f" the Configuration tab renders {len(ui)} fields, in `schema:` order " # - which is the CFG-03 defect the same card's AC 4 exists to police. # # So: four fewer clicks, bought with the upgrade path and with the defaults -# pin. The decision is written up in DECISIONS/UX-04-hiding-and-the-deepmerge.md +# pin. The decision is written up in +# DECISIONS/UX-04-the-configuration-tab-and-the-defaults-pin.md # and the three assertions below are what keeps it a decision rather than an # omission - if anyone empties `options:` to shorten the form, this goes red. _hideable = sorted(set(schema) - set(defaults)) @@ -375,6 +376,41 @@ show(f"every one of the {len(schema)} `schema:` keys is ALSO in `options:`, so " show(f"...so replaying the frontend's own _filteredSchema on the served options " f"hides NOTHING: {len(_filtered(defaults))} of {len(ui)} fields render", len(_filtered(defaults)) == len(ui)) +# ⚠️ DERIVED, NOT TYPED, AND NOT A FLOOR. The assertion above read +# `len(_required) > 15` while the prose said 59 - so a manifest change dropping +# the required count to 20 would have passed while the sentence beside it stayed +# wrong. That is the same shape as the count this repo already fixed in +# `test_translations.py`: a number displayed but not graded. +# +# So the expected value is computed from config.yaml by Supervisor's OWN rule - +# a key is optional iff its schema string ends in `?`, plus `inverters`, which +# `UiOptions` marks optional because it is a list - and the assertion is an +# EQUALITY against that. Typing 59 here would rot the first time a key is added. +_OPTIONAL = {k for k, v in schema.items() + if isinstance(v, str) and v.rstrip().endswith("?")} | {"inverters"} +_EXPECT_REQ = len(schema) - len(_OPTIONAL) +show(f"...and that floor is DERIVED: {len(schema)} schema keys minus " + f"{len(_OPTIONAL)} optional ones (the `?` suffix, plus `inverters`) is " + f"{_EXPECT_REQ}, and Supervisor's own required flag agrees", + _EXPECT_REQ == len(_required)) + +# ⚠️ AND THE WITHDRAWAL'S OWN PREMISE, GRADED SEPARATELY. The equality above is +# deliberately NOT enough on its own: both sides read the same `?` convention, +# so marking a key optional moves BOTH and it stays green. That is correct - it +# makes the printed number impossible to leave stale - but it grades Supervisor +# agreeing with us, not the thing AC 3 was withdrawn for. +# +# AC 3 was withdrawn because the floor is FAR above the ~15 it asked for. If +# enough keys ever became optional that the floor approached 15, the withdrawal +# stops being true and this file should say so rather than keep printing a +# sentence that has quietly become wrong. +_AC3_ASKED = 15 +show(f"...and the floor of {_EXPECT_REQ} is still far above the ~{_AC3_ASKED} " + f"AC 3 asked for, which is the whole reason it was withdrawn. If this ever " + f"goes red the withdrawal needs re-arguing, not re-asserting", + _EXPECT_REQ > 2 * _AC3_ASKED) + + # ⚠️ AN ISOLATED COUNTERFACTUAL, and the number is Supervisor's own `required` # flag rather than a count typed here. `options:` is emptied and NOTHING else # varies, so the answer cannot be about anything but the merge. @@ -382,7 +418,10 @@ show(f"...and with `options:` EMPTIED ENTIRELY - the maximum hiding that is " f"expressible - the tab still renders {len(_filtered({}))} required " f"fields. THAT is the floor, and AC 3 asked for ~15. The criterion is " f"withdrawn as unsatisfiable, not deferred.", - len(_filtered({})) == len(_required) and len(_required) > 15) + len(_filtered({})) == len(_required) and len(_required) == _EXPECT_REQ) + + + def _entry(lang, name, path=()):