UX-08 phase 2a: short, plain notifications, and a ratchet for the rest
Every HA notification the add-on raises now has a title of at most 5 everyday words and a one-sentence body; a red alarm may use three so no action is lost. The texts live in app/messages.py, the rule in STYLE.md, and the detail that moved out in DOCS.md "Notifications", which each body links to. No notification_id, trigger or cadence changed. test_messages.py enforces the rule on every notification, fails any notification worded outside app/messages.py, and ratchets the kinds not yet rewritten (rows, banners, refusals, logs, options) through a shrinking allowlist of 200. Six tests re-pinned from prose to meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2503de124e
commit
50ccea3dd7
@@ -83,6 +83,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from named_absence import given, nth, present
|
||||
|
||||
from app import messages
|
||||
from app.clamp import (
|
||||
CLEAR_CYCLES, REASON_CODE, REASSERT_S, SUSTAINED_CYCLES, ClampWatchdog,
|
||||
clamp_to_rating,
|
||||
@@ -510,9 +511,9 @@ check("...one episode, re-asserted rather than one per cycle",
|
||||
ctl_r.clamp_wd.episodes == 1 and len(notes_r) < ctl_r.clamp_wd.engagements)
|
||||
# SAFETY-40: on a default install this runaway is the INTEGRATOR bound, so it
|
||||
# says so, and does not call anti-windup a bug (part 8 grades the wording).
|
||||
check("...and it names the integrator bound, not a BUG SIGNAL",
|
||||
bool(notes_r) and "anti-windup bound" in notes_r[0]["message"]
|
||||
and "BUG SIGNAL" not in notes_r[0]["message"])
|
||||
check("...and it is the integrator-bound notice, which calls nothing a fault",
|
||||
bool(notes_r) and notes_r[0]["title"] == messages.T_LOOP_BOUND
|
||||
and "fault" not in notes_r[0]["message"])
|
||||
rows_r = [e for e in ctl_r.events if REASON_CODE in e]
|
||||
check(f"the action log records every engagement, coalesced into one row per "
|
||||
f"episode ({len(rows_r)} row(s), {ctl_r.clamp_wd.engagements} "
|
||||
@@ -1002,7 +1003,9 @@ check("AC 3: the guard alarmed on write-path evidence alone",
|
||||
ctl_w.clamp_wd.engagements == 1 and len(notes_w) == 1)
|
||||
check("...and the operator is told WHICH bound bit, so a device-rating clamp "
|
||||
"is not reported as a max_w one",
|
||||
bool(notes_w) and "device bound" in notes_w[0]["message"])
|
||||
bool(notes_w)
|
||||
and messages.LIMIT_WORDS["device"] in notes_w[0]["message"]
|
||||
and messages.LIMIT_WORDS["max_w"] not in notes_w[0]["message"])
|
||||
check("...and the action log carries the row, written by write_setpoint()",
|
||||
any(REASON_CODE in e and "device bound" in e for e in ctl_w.events))
|
||||
|
||||
@@ -1094,19 +1097,24 @@ check("⚠️ THE ORDERED PAIR IN THE §15 ACTION-LOG ROW (main.py:1366). The "
|
||||
"reading backwards on the one line an operator is woken by",
|
||||
any(f"the computed setpoint {_asked_x:+.0f} W was clamped to "
|
||||
f"{_sent_x:+.0f} W" in e for e in _rows_x))
|
||||
check("⚠️ ...AND IN THE ClampWatchdog NOTIFICATION REASON (main.py:2452), "
|
||||
"which is a SECOND surface off the same sentence and has to be pinned "
|
||||
"separately - SAFETY-38 round 3's whole lesson was that pinning one call "
|
||||
"site leaves the other shipping",
|
||||
bool(notes_x)
|
||||
and f"the computed setpoint {_asked_x:+.0f} W was clamped to "
|
||||
f"{_sent_x:+.0f} W" in notes_x[0]["message"])
|
||||
# UX-08: the notification is a SECOND surface off the same numbers, in its own
|
||||
# words (messages.output_clamp): asked first, sent second.
|
||||
def _asked_then_sent(msg, asked, sent):
|
||||
return (f"asked for {messages.watts(asked)}," in msg
|
||||
and f"and sent {messages.watts(sent)} instead" in msg
|
||||
and msg.index(messages.watts(asked)) < msg.index("and sent"))
|
||||
|
||||
|
||||
check("⚠️ ...AND IN THE ClampWatchdog NOTIFICATION, which is a SECOND surface "
|
||||
"off the same numbers and has to be pinned separately - SAFETY-38 round "
|
||||
"3's whole lesson was that pinning one call site leaves the other "
|
||||
"shipping",
|
||||
bool(notes_x) and _asked_then_sent(notes_x[0]["message"], _asked_x, _sent_x))
|
||||
check("...and the TRANSPOSED sentence appears on NEITHER surface, so the two "
|
||||
"checks above discriminate rather than decorate",
|
||||
not any(f"the computed setpoint {_sent_x:+.0f} W was clamped to "
|
||||
f"{_asked_x:+.0f} W" in e for e in _rows_x)
|
||||
and f"the computed setpoint {_sent_x:+.0f} W was clamped to "
|
||||
f"{_asked_x:+.0f} W" not in nth(notes_x, 0)["message"]
|
||||
and not _asked_then_sent(nth(notes_x, 0)["message"], _sent_x, _asked_x)
|
||||
and present(nth(notes_x, 0)))
|
||||
# The DEVICE bound, off part 6c's controller: same sentence, other stage. The
|
||||
# unit of coverage is the call site, and `bound` is the only thing that differs
|
||||
@@ -1118,8 +1126,7 @@ check("...and the same ordered pair holds on the DEVICE-bound row, where the "
|
||||
f"{_asked_w:+.0f}" != f"{_sent_w:+.0f}"
|
||||
and any(f"the computed setpoint {_asked_w:+.0f} W was clamped to "
|
||||
f"{_sent_w:+.0f} W" in e for e in ctl_w.events)
|
||||
and f"the computed setpoint {_asked_w:+.0f} W was clamped to "
|
||||
f"{_sent_w:+.0f} W" in nth(notes_w, 0)["message"])
|
||||
and _asked_then_sent(nth(notes_w, 0)["message"], _asked_w, _sent_w))
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@@ -1973,8 +1980,8 @@ check("at the bound nothing is described as 'above' or 'over' the rating",
|
||||
for d, t, r in wind))
|
||||
notes_i = clamp_notes(hass_i)
|
||||
check("it still NOTIFIES (not suppressed), under a title that claims no bug",
|
||||
notes_i and all("integrator bound" in n["title"]
|
||||
and "exceeded" not in n["title"] for n in notes_i))
|
||||
notes_i and all(n["title"] == messages.T_LOOP_BOUND
|
||||
and "fault" not in n["message"] for n in notes_i))
|
||||
check("...under its OWN notification_id, so it cannot replace a BUG SIGNAL "
|
||||
"notice still on screen",
|
||||
notes_i and all(n["notification_id"] == BOUND_ID for n in notes_i))
|
||||
@@ -2008,10 +2015,14 @@ check("defect 3: 'clamped to' is the clamp's own result, not self.target",
|
||||
check("...and the 14 768 W rationale is still in its ERROR",
|
||||
any(x.levelno == logging.ERROR and "14 768 W" in x.getMessage()
|
||||
for x in log_o))
|
||||
check("...under the 'exceeded the rating' title, with a red status row",
|
||||
check("...under the over-rating title, quoting a tick's own asked and sent "
|
||||
"numbers in that order, with a red status row",
|
||||
clamp_notes(hass_o)
|
||||
and all("exceeded the rating" in n["title"]
|
||||
and n["notification_id"] == CLAMP_ID for n in clamp_notes(hass_o))
|
||||
and all(n["title"] == messages.T_OUTPUT_CLAMP
|
||||
and n["notification_id"] == CLAMP_ID
|
||||
and any(_asked_then_sent(n["message"], _f(d, 'requested_w'),
|
||||
_f(d, 'clamped_w')) for d, t, r in over)
|
||||
for n in clamp_notes(hass_o))
|
||||
and any(not c["warn"] and "OUTPUT CLAMP ENGAGED" in c["text"]
|
||||
for c in red_o))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user