UX-08 phase 2b: short, plain status rows and page banners
Every Commissioning row and every page banner is now worded in app/messages.py (row_*, banner_*): one sentence of at most 15 words, a red row up to three sentences so a latch still says how it clears, banners at most 12 words. No row names a Home Assistant entity: sensors by their setting, solar sources by their place in the list. Reason codes and limits stay on the log line and event list; rows keep the numbers a reader acts on. UX-07 item 1: ClampWatchdog counts over-rating and integrator-bound engagements separately, and after an episode each count shows under its own alarm's title instead of both under "output clamp". No row colour, visibility or banner order changed. Rows carry an "id" so the tests pin meaning rather than wording. The row and banner kinds leave the allowlist (200 -> 168, cap equal). DOCS.md gains "Status page"; choices are in DECISIONS/UX-08-short-notifications.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
@@ -7,8 +7,11 @@
|
||||
2. No notification is worded anywhere else. An AST scan of `app/` fails any
|
||||
`persistent_notification` call whose payload is not built by
|
||||
`app/messages.py`.
|
||||
3. THE RATCHET for the kinds not rewritten yet. Every status-row and banner
|
||||
literal, every validator refusal, every WARNING+ log line and every option
|
||||
3. The status page. Every row and banner in `app/messages.py` obeys the rule
|
||||
for its colour: a row one sentence of at most 15 words, a red row up to 3
|
||||
sentences and 30 words, a banner at most 12 words.
|
||||
THE RATCHET for the kinds not rewritten yet. Every status-row and banner
|
||||
literal left in `app/`, every validator refusal, every WARNING+ log line and every option
|
||||
name and description in `translations/en.yaml` must either obey its limit
|
||||
or appear in `ux08_allowlist.txt`. A new string that breaks the rule and is
|
||||
not listed goes red. So does a listed entry that no longer matches a
|
||||
@@ -302,7 +305,7 @@ ALLOW_FILE = HERE / "ux08_allowlist.txt"
|
||||
# The allowlist may never grow. The test demands the list be exactly this long,
|
||||
# so deleting lines means lowering this number in the same change, and adding
|
||||
# one means raising it where a reviewer sees it.
|
||||
ALLOWLIST_MAX = 200
|
||||
ALLOWLIST_MAX = 168
|
||||
|
||||
|
||||
def read_allowlist(path):
|
||||
@@ -326,10 +329,57 @@ if "--violations" in sys.argv:
|
||||
print(f"{k} # {where} {'; '.join(p)} :: {text[:70]!r}")
|
||||
sys.exit(0)
|
||||
|
||||
STATUS = messages.status_samples()
|
||||
|
||||
|
||||
def row_problems(colour, text):
|
||||
"""STYLE.md section 1 for a status row (by colour) or a banner (None)."""
|
||||
n_s, n_w = sentences(text), len(words(text))
|
||||
out = []
|
||||
if colour is None:
|
||||
if n_w > 12:
|
||||
out.append(f"banner over 12 words ({n_w})")
|
||||
elif colour == messages.RED:
|
||||
if n_s > 3:
|
||||
out.append(f"red row over 3 sentences ({n_s})")
|
||||
if n_w > 30:
|
||||
out.append(f"red row over 30 words ({n_w})")
|
||||
else:
|
||||
if n_s > 1:
|
||||
out.append(f"{colour} row over 1 sentence ({n_s})")
|
||||
if n_w > 15:
|
||||
out.append(f"{colour} row over 15 words ({n_w})")
|
||||
if ";" in text:
|
||||
out.append("semicolon: split it into sentences or drop a clause")
|
||||
return out + text_problems(text, keys=False, entities=False)
|
||||
|
||||
|
||||
for fn, colour, text in STATUS:
|
||||
p = row_problems(colour, text)
|
||||
check(f"{colour or 'banner':6} {text!r}{' - ' + '; '.join(p) if p else ''}",
|
||||
not p)
|
||||
_catalogue = {k for k, v in vars(messages).items()
|
||||
if callable(v) and k.startswith(("row_", "banner_"))}
|
||||
_missing = sorted(_catalogue - {fn for fn, _c, _t in STATUS})
|
||||
check(f"the samples cover every row and banner in the catalogue "
|
||||
f"(missing: {_missing})", not _missing)
|
||||
# A red row with no banner of its own heads the page as "Not ready - <its first
|
||||
# sentence>". The three trips have their own branch in status_payload(), and
|
||||
# main.py gives the other two a "banner" key (both are sampled above).
|
||||
_own_banner = {"row_direction_trip", "row_site_direction_trip",
|
||||
"row_circulating_trip", "row_peak_reserve", "row_power_too_high"}
|
||||
_long = [t for fn, c, t in STATUS if c == messages.RED and fn not in _own_banner
|
||||
and len(words(messages.banner_not_ready({"text": t}))) > 12]
|
||||
check(f"every other red row makes a 'Not ready' banner of at most 12 words "
|
||||
f"(too long: {_long})", not _long)
|
||||
|
||||
allow = read_allowlist(ALLOW_FILE)
|
||||
by_kind = {}
|
||||
for kind, *_ in found:
|
||||
by_kind[kind] = by_kind.get(kind, 0) + 1
|
||||
for _fn, colour, _t in STATUS:
|
||||
kind = "row" if colour else "banner"
|
||||
by_kind[kind] = by_kind.get(kind, 0) + 1
|
||||
check(f"the scan found every ratcheted kind ({by_kind})",
|
||||
all(by_kind.get(k, 0) >= 5 for k in LIMITS))
|
||||
new = sorted(k for k in violating if k not in allow)
|
||||
@@ -361,6 +411,8 @@ section = part[-1].split("\n## ", 1)[0] if len(part) == 2 else ""
|
||||
heads = set(re.findall(r"^### (.+)$", section, re.M))
|
||||
missing = sorted({n.docs for n in SAMPLES} - heads)
|
||||
check(f"...with a heading for every Details line (missing: {missing})", not missing)
|
||||
check("DOCS.md explains the status page rows under "
|
||||
f"'## {messages.DOCS_STATUS}'", f"\n## {messages.DOCS_STATUS}\n" in DOCS)
|
||||
|
||||
|
||||
# ============================================================================== #
|
||||
@@ -394,6 +446,18 @@ check("a message that lost its Details line is refused",
|
||||
_fake = " x = {'text': 'THE METER IS FROZEN AND NOTHING WORKS AT ALL'}\n"
|
||||
_n = [t for node in ast.walk(ast.parse(_fake.strip()))
|
||||
if isinstance(node, ast.Dict) for _, t in strings_in(node.values[0])]
|
||||
check("a second sentence in an amber row is refused",
|
||||
bool(row_problems(messages.AMBER, "Meter stuck. Check it.")))
|
||||
check("a fourth sentence in a red row is refused",
|
||||
bool(row_problems(messages.RED, "One. Two. Three. Four.")))
|
||||
check("a 16-word green row is refused",
|
||||
bool(row_problems(messages.GREEN, " ".join(["word"] * 16))))
|
||||
check("a 13-word banner is refused",
|
||||
bool(row_problems(None, " ".join(["word"] * 13))))
|
||||
check("an entity id in a status row is refused",
|
||||
bool(row_problems(messages.GREEN, "Battery level: sensor.soc = 54")))
|
||||
check("a semicolon in a status row is refused",
|
||||
bool(row_problems(messages.RED, "It stopped; check it.")))
|
||||
check("a new shouting status row is seen by the scan and is not listed",
|
||||
_n and problems_of("row", _n[0])
|
||||
and key_of("row", _n[0]) not in allow)
|
||||
|
||||
Reference in New Issue
Block a user