UX-04: nothing was hidden, the defaults pin was never built, and CFG-04 was closed by UX-02
checks / unit (pull_request) Successful in 6m20s
checks / unit (pull_request) Successful in 6m20s
UX-01's three unmet acceptance criteria, closed out. It shipped in 0.7.0 and is LIVE, so all three describe the running install. No behaviour, no schema and no default moves here: config.yaml, translations/ and every module under app/ are byte-identical to6e7b233. ⚠️ AC 3 - "the visible count drops to ~15" - IS WITHDRAWN AS UNSATISFIABLE, and the arithmetic is the reason rather than the effort. options: and schema: both declare all 71 keys, so the set HA's toggle can hide is EMPTY - it hides a key only when it is ?-optional AND absent from options:, and nothing is absent. But the deeper fact is that no manifest could have met it: Supervisor's own UiOptions marks 59 of the 71 REQUIRED, and the frontend's own filter - `schema.filter(e => e.name in opts || e.required)`, transcribed out of the bundle in the release image - shows a required field unconditionally. So 59 is the floor with options: EMPTIED ALTOGETHER. The target was ~4x below anything reachable. ⚠️ AND THE 12 HIDES THAT ARE EXPRESSIBLE WOULD BE BOUGHT WITH THE UPGRADE PATH. A key absent from options: has no default for the Supervisor to deep-merge into an existing box's stored blob. That is exactly the delivery_hold_s mechanism this release rests on: a REQUIRED key a 0.7.0 blob has never stored resolves to its options: default of 185 instead of becoming exit 78. Hiding a key is precisely the edit that removes that. It would also promote the 27 unpinned in-code literal fallbacks the new check ledgers into the ACTUAL source of the defaults on a live install - the CFG-03 defect the same card's AC 4 exists to police, made load-bearing by its AC 3. ⚠️ THE COUNT WAS PRINTED AND NEVER GRADED, WHICH IS HOW THIS PASSED REVIEW. supervisor_shape_probe.py section F has printed "the Configuration tab renders 71 fields" since the day it was written and exited 0 through UX-01's whole review while displaying the number that contradicted the criterion it was evidence for. Three assertions replace the bare print, every number computed from the manifest and from Supervisor's own `required` flag rather than typed, including an ISOLATED COUNTERFACTUAL that empties options: and nothing else. MEASURED RED: delete one key (price_feed_url) from options: and two of them fail - `Hideable: ['price_feed_url']`, `70 of 71 fields render` - exit 1. --- AC 4: the AST check, built -------------------------------------------- AC 4's first clause is VACUOUS over an empty set (see above) and its second - "the table changed goes red" - already works six times over. The third, the one with the content, was never written: the only literal scan in the repository is test_validate.py's regex over eight maintenance_* keys, which is a grep and predates the card. test_defaults_pin.py walks ast.Call nodes over app/*.py. It: - DISCOVERS the six *_OPTION_DEFAULTS mirrors from the tree rather than naming them. Every existing pin names its own table by hand, so a SEVENTH table added tomorrow is pinned by nobody and nothing says so; - grades all 30 mirrored keys against config.yaml, and requires that no module carries a literal fallback for any of them; - FREEZES the CFG-03 backlog at exactly today's 27 (module, key) pairs, by pair and not by count - a count goes green again the moment one site is deleted and another added. Line numbers are deliberately out of the key: a ledger that reddens on an unrelated reformat is one people re-bless without reading. It can only shrink; - grades every remaining literal AGAINST config.yaml, which is the half that catches CFG-04's actual failure - two copies of one number, one moves. ⭐ ONE PREVIOUSLY UNREPORTED DIVERGENCE, and it is what that last check is for: main.py:290 answers a missing target_grid_w with 0 where config.yaml ships -10. It cannot reach an operator - the deep-merge guarantees the key is present - and the only paths that hand over a partial dict are sim/inject.py and this repo's fixtures. Recorded as the single permitted divergence with its reason rather than corrected; changing it alters the control loop's target and belongs to a card that grades the loop. A second divergence goes red. ⭐ THE RED DIRECTION WAS RUN, TWICE OVER. Part 5 feeds the scanner a synthetic module containing a deliberate violation of each rule and REQUIRES it to be caught, in-process, through the same function the graded parts call - so a scanner that goes blind stops working there too - and the site total is graded against a floor. And a literal was planted in the REAL tree: main.py:290 float(opts.get("maintenance_charge_w", 2000)) -> FAIL no literal fallback survives for any of the 30 keys a mirror already answers for exit 1 main.py:290 opts.get("p1_host", "x"), opts.get("slew_w", 999) -> FAIL no NEW module/key pair has started carrying a literal fallback [('main.py', 'p1_host')] FAIL every remaining literal still AGREES with config.yaml main.py slew_w: code 999 vs config.yaml 0 exit 1 Both plants removed. --- AC 5: CFG-04 is closed, and not by this card's predecessor -------------fef6ef2touched config.yaml, translations/, test_translations.py and the probe - no defaults code at all - and on 0.7.0 the max_w 2000 -> 2500 triple mutation survives 21 files, 0 RED. CONFIRMED CLOSED on release/1.0 by re-running the same triple mutation on a throwaway copy of6e7b233: 26 files, RED at test_selfconfig.py, exit 1 - 1 FAILED: ⚠️ CLEARING max_w ON THE LIVE SHAPE IS REFUSED - rule 10 catches the rail collapsing to the 2000 default under an explicit 400 W slew git log -L 310,325:test_selfconfig.py attributes that check tobcee235, "UX-02: pin both halves of the max_w fix, because neither was graded". The AC is met BY UX-02 and ships with the pending 0.9.0 deploy. Nothing was re-done. --- the 8 per-entry translations: they stay -------------------------------- Measured negative and not a defect in them: UiOptions gives inverters type schema WITH multiple, the frontend sends that case to ha-selector-object, which labels fields `fields[name].label || name`, and the converter sets neither label nor translation_key. The Supervisor tab CANNOT be made to render them. They stay because UX-02's ingress panel is the surface that can, because they cost nothing, and because deleting them removes the only thing that would catch a per-entry key added without a translation - which is how SAFETY-32's REQUIRED bms_soc_floor would have joined the roster unlabelled. A written decision, not a change; translations/ is untouched. tools/supervisor_selfapi_probe.py stays red on its hardcoded len(_sub) == 7. That is TOOL-04, already filed, and it is not re-filed or fixed here. --- evidence --------------------------------------------------------------- positive control test_defaults_pin.py's mirror floor moved 6 -> 7 inside the harness container: "### RED: test_defaults_pin.py", exit 1 suite 27 files, ALL GREEN, exit 0 (goodwe-addon-checks:1, the checkout mounted read-only) shape probe all cases passed, exit 0, on ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0 Nothing here touched hardware, the live Home Assistant at 192.168.2.158, or sim/. Both probe runs were throwaway containers over a read-only mount. DECISIONS/UX-04-the-configuration-tab-and-the-defaults-pin.md carries all four decisions, the frontend transcription, and the two counterfactual transcripts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
@@ -315,6 +315,75 @@ print(f" the Configuration tab renders {len(ui)} fields, in `schema:` order "
|
||||
"(App.schema_ui is UiOptions(...)(data['schema']), so the ORDER IS THE "
|
||||
"SCHEMA BLOCK'S and not the options block's)")
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ⚠️ UX-04: THAT COUNT IS NOW GRADED, AND IT IS WHY UX-01's AC 3 IS WITHDRAWN.
|
||||
#
|
||||
# UX-01 AC 3 said the visible count would drop to ~15, with the rest behind
|
||||
# HA's "show unused optional configuration options" toggle. The line above has
|
||||
# printed the refutation since the day it was written - 71, not 15 - and PRINTED
|
||||
# IS NOT GRADED: this probe exited 0 through the whole of UX-01's review while
|
||||
# displaying the number that contradicted the criterion it was evidence for.
|
||||
# That is the defect, not the count.
|
||||
#
|
||||
# THE FRONTEND RULE, transcribed from the bundle in THIS image at the top of
|
||||
# this section and replayed rather than paraphrased:
|
||||
#
|
||||
# _filteredSchema = (opts, schema) => schema.filter(e => e.name in opts
|
||||
# || e.required)
|
||||
#
|
||||
# `opts` is the add-on's OPTIONS AS SUPERVISOR SERVES THEM - the operator's
|
||||
# stored blob after `options:` has been deep-merged into it - so a key is
|
||||
# hideable only when it is BOTH absent from `options:` AND not required.
|
||||
#
|
||||
# WHY THE CRITERION IS WITHDRAWN RATHER THAN IMPLEMENTED, in two measurements:
|
||||
#
|
||||
# 1. IT IS ARITHMETICALLY UNREACHABLE. Supervisor's own UiOptions marks
|
||||
# `required` on every non-`?` key, and the filter above shows a required
|
||||
# key UNCONDITIONALLY. So the floor - what the tab renders with `options:`
|
||||
# EMPTIED ALTOGETHER, which is the most hiding that is even expressible -
|
||||
# is the required count below. It is nowhere near 15. No arrangement of
|
||||
# `options:` can reach the target; the criterion was never satisfiable.
|
||||
#
|
||||
# 2. THE 12 KEYS THAT *COULD* BE HIDDEN COST THE DEEP-MERGE DEFAULT. A key
|
||||
# absent from `options:` has nothing for the Supervisor to merge into an
|
||||
# existing box's stored blob. That is exactly the `delivery_hold_s`
|
||||
# mechanism the 0.9.0 upgrade rests on: a key that 0.7.0's stored blob
|
||||
# cannot contain resolves to its `options:` default instead of failing
|
||||
# validation, and moving it out of `options:` is precisely how you get the
|
||||
# exit 78 that mechanism exists to prevent. Worse, it would promote the 27
|
||||
# unpinned in-code literal fallbacks that `test_defaults_pin.py` part 4
|
||||
# ledgers into the LOAD-BEARING source of those defaults on a live install
|
||||
# - 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
|
||||
# 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))
|
||||
_required = [n["name"] for n in ui if n.get("required")]
|
||||
|
||||
|
||||
def _filtered(opts):
|
||||
"""The frontend's own `_filteredSchema`, replayed on this manifest."""
|
||||
return [n for n in ui if n["name"] in opts or n.get("required")]
|
||||
|
||||
|
||||
show(f"every one of the {len(schema)} `schema:` keys is ALSO in `options:`, so "
|
||||
f"schema-only - the only set HA's toggle can hide - is EMPTY. UX-01 AC 3 "
|
||||
f"is not partially met, it is unstarted. Hideable: {_hideable}",
|
||||
not _hideable and len(schema) == len(defaults))
|
||||
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))
|
||||
# ⚠️ 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.
|
||||
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)
|
||||
|
||||
|
||||
def _entry(lang, name, path=()):
|
||||
node = TRANSLATIONS.get(lang, {}).get("configuration")
|
||||
|
||||
Reference in New Issue
Block a user