UX-04: the 59 was computed and then graded against "> 15"
checks / unit (pull_request) Successful in 6m22s

Review approved and found four things worth fixing. Three are cosmetic; the
fourth is the same defect this card is about, one level up.

⚠️ THE FLOOR WAS PRINTED AS 59 AND ASSERTED AS `> 15`. So a manifest change
dropping the required count to 20 would have passed while the sentence beside it
still said 59 - a number displayed but not graded, which is exactly what let
UX-01's AC 3 survive review in the first place, and exactly the shape
`test_translations.py` was already fixed for.

Now DERIVED and graded in two separate assertions, because they are two
different claims:

  1. `_EXPECT_REQ = len(schema) - len(_OPTIONAL)`, where optional is Supervisor's
     own rule - a `?` suffix, plus `inverters` because `UiOptions` marks a list
     optional - asserted EQUAL to Supervisor's required count. The printed
     number is computed, so it cannot go stale.
  2. ⚠️ AND THE WITHDRAWAL'S PREMISE, SEPARATELY. The equality alone is not
     enough and I checked why: both sides read the same `?` convention, so
     marking a key optional moves BOTH and it stays green - measured, by making
     `slew_w` optional and watching it pass. That grades Supervisor agreeing
     with us, NOT the thing AC 3 was withdrawn for. The second assertion grades
     the premise: the floor is still far above the ~15 AC 3 asked for, and if it
     ever is not, the withdrawal needs re-arguing rather than re-asserting.

Driven, both directions, against the real 2026.08.0 image:

    clean                         PASS  "floor is DERIVED: 71 schema keys minus
                                         12 optional ones is 59, and
                                         Supervisor's own required flag agrees"
    premise threshold 15 -> 40    FAIL  "the floor of 59 is still far above
                                         the ~40 AC 3 asked for"

Also: a dangling reference to `DECISIONS/UX-04-hiding-and-the-deepmerge.md`,
which was never the filename shipped, and `DOCS.md` still saying section F
"prints" what it now grades.

Not changed, and review agreed both are disclosed rather than defects: the
pair-keyed ledger cannot see a SECOND site for a pair it already holds (the
site count can grow while the pair set cannot), and the probe's assertions run
by hand rather than in CI because they need the Supervisor image - the same
status as before this card, not a regression.

Suite: 27 files, all pass. Probe: all cases passed on
ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0, --network none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-07 18:56:55 +02:00
co-authored by Claude Opus 5
parent 21f22b393a
commit 477911afa1
2 changed files with 43 additions and 3 deletions
+2 -1
View File
@@ -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.
| | |
@@ -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=()):