fix(ui): report the reset success with a toast level the sheet declares - #290
Conversation
The forgot-password success branch called
toast(T("forgot.done"), "ok");
reusing the API response's own status literal (`if (r.status === "ok")`, same
branch) as a toast *level*. `toast()` concatenates `type` into the element's
class with no validation:
el.className = "toast" + (type ? " " + type : ""); // ui/js/app.js:38
and `ui/css/style.css` declares `.toast.success` / `.toast.error` /
`.toast.info` only — `.toast.ok` exists solely in the design prototype
(`docs/prototype/aitokenpool-console.html`, whose own calls use `ok`/`warn`).
So the app's only password-reset success message silently fell back to the
base `.toast` style. `#40` introduced the product vocabulary and migrated the
call sites; `8ac018b` (forgot/reset password, three days later) wrote one new
call site back in the prototype's words.
Fix: use `"success"`, the word the running app already uses for a genuine
success on the same auth card, and say why the argument is a level.
The new static gate `state_gate::every_toast_level_is_a_level_the_sheet_declares`
pins the shape with three derived rules (no hand-written rosters):
1. every literal level at a `toast(...)` call site is in the sheet's
`.toast.<x>` rule set minus the state classes the function body applies
itself via `classList.add`;
2. that vocabulary equals, both ways, the contract line in `ui/README.md`;
3. conversely, every graded rule has a call site (no dead level).
`the_r92_rules_have_teeth` and `the_r92_rules_separate_the_variants` prove each
rule has its own tooth and declare each leg's verdict; the competitive fix
("leave the call site, add a `.toast.ok` rule") is rejected by rule 2, and the
jsdom probe rejects it too.
Self-reviewShape of the defect. One argument, one word, but it is the whole complaint: Why a static gate and not only the probe. CI has no JS runner; The three rules are all derived, deliberately. Rule 1 takes its vocabulary from The competitive fix was measured, not assumed. "Leave the call site alone, add A bug found while building the gate, fixed in the same commit. The gate's JS scanner walks the source to find Also fixed in the same commit: the "competitive fix" mutant was declared on the fixed tree, which made the added |
Summary
The forgot-password success branch reported its message with a toast level that no stylesheet declares:
"ok"is the criterion literal of the very sameif(the API response status), reused one line later as a level.toast()concatenates it into the element's class with no validation:and
ui/css/style.cssdeclares.toast.success/.toast.error/.toast.infoonly — plus the state class.toast.out, which the function body applies itself viaclassList.add("out")..toast.okexists only in the design prototype (docs/prototype/aitokenpool-console.html:321, whose own calls useok/warn); the product never had that rule. So the app's only password-reset success message silently fell back to the bare.toaststyle while every other graded toast on the same card carried its real level.Provenance is drift, not a trade-off: #40 (v1.16, "graded toasts") introduced the product vocabulary and migrated the existing call sites;
8ac018b(feat(auth): forgot/reset password, three days later) wrote this one new call site back in the prototype's words. The contract inui/README.mdhas saidtoast(msg, "success" | "error" | "info")all along.Related Issue
None — this came out of an internal audit of the toast vocabulary, not from an open issue. The linked-issue field is expected to stay empty by design; the PR is based on the default branch (
main).Changes
ui/js/app.js:"ok"→"success"(the word the running app already uses for a genuine success on the same auth card), with a comment saying why the second argument is a level.ui/index.html: cache-bustapp.js?v=20260922-12→-13.ui/README.md: extend the toast contract bullet — call sites must take their level from that one vocabulary — and record the new gate's scope.src/state_gate.rs: new static gateevery_toast_level_is_a_level_the_sheet_declareswith three derived rules (no hand-written rosters):toast(...)call site is a literal and lies in the sheet's.toast.<x>rule set minus the state classestoast()'s own body applies viaclassList.add;ui/README.md;Plus
the_r92_rules_have_teethandthe_r92_rules_separate_the_variants, which show each rule has its own tooth and declare and print each leg's verdict.- [x]No config/data-structure change, so no example file needed.Tests
cargo test— 369 passed / 0 failed (baseline onmain: 365; +4 = the three R92 tests above).cargo fmt --check— rc 0.cargo clippy --all-targets -- -D warnings— rc 0 (the mutant table needed a type alias forclippy::type_complexity).Instruments (two, each seeing what the other cannot)
Compiled gate, A/B. The same new
src/state_gate.rscompiled against two app trees materialized fromgit archive HEAD:ui/js/app.jsmd5every_toast_level_is_a_level_the_sheet_declaresbase(unfixed,"ok")c6883a9545988102823c4b068b60d7e6fix(this PR)908aeb05dbc0db9852ad40274e87b326jsdom probe (
r92_probe.js, reused verbatim, md57eeb1b99c92bb566c40c751cf97e9274): real boot ofui/index.html+ the four real scripts in jsdom, real#forgot-link→ real fields → real submit, reading the rendered#toast-wrapchildren'sclassName. Four variants × 13 checks, all as declared:base(unfixed)B1,B3,C3fix(this PR)m_css(competitive fix: leave the call site, add a.toast.okrule)B2,C3m_css2(same, body reordered so the duplicate-body detector goes quiet)C3Note the deliberate asymmetry: the competitive fix makes the screen green (
B1passes — the toast does get.toast.ok), and the gate rejects it anyway, because the stylesheet then declares a level the documented contract does not. The gate is stricter than the probe on that one point; the probe is stricter on the value (C3compares against the level the running app itself uses for a success, never a hard-coded spelling). Both are recorded, neither pretends the other is redundant.Checklist
fix/…).fix(ui): …).