fix(ui): spell one token quantity one way in the transactions view - #281
Conversation
) The summary card, the table row and the CSV export each carried their own rule for the same number: card fmtM Math.round(n / 1000) + "K" 1500 -> "2K" row fmtTokens (n / 1000).toFixed(1) trimmed 1500 -> "1.5K" export the cell's *display string* 1500 -> "1.5K" With one row in the result the card prints exactly the number the row below prints, spelled differently; and the export writes an abbreviated view string into a data column, where the affordance the screen has (the cell tooltip) does not exist at all. The K tier is the tier the deployment lives in (99.92% of tokens, max 494 795), so "the two rules agree in the M tier" is unreachable in practice. - the card now delegates to `fmtTokens` (`const fmtM = fmtTokens;`) - the export reads the raw numbers and converts with `fmtTokensExact`, the same helper the cell tooltip already used New gate `state_gate::the_transactions_view_states_one_token_quantity_one_way` (R1 the card carries no spelling literal of its own; R2 the card delegates; R3 all four export columns read <...>Raw through the tooltip's helper; R4 the reverse: the cells still print the abbreviated field), plus three self-proofs (`the_r158_roster_is_real`, `the_r158_scanners_have_teeth`, `the_r158_rules_separate_the_variants`). No i18n keys added or removed; no production Rust touched.
|
Self-review (own-approve is not available to this account on its own PRs, so the review is recorded here). Reviewed the diff against the run I actually performed.
One thing changed after the first green run, and it is worth naming because it is the trap Scope, stated as scope rather than as silence: the gate is lexical — it pins that the card |
Summary
One token quantity is spelled three ways in the transactions view, by three
different rules:
1500readsrenderTxSummary)fmtM:Math.round(n / 1000) + "K""2K"txsToView)fmtTokens:(n / 1000).toFixed(1)trimmed"1.5K"exportTxCsv)"1.5K"Two consequences, both reachable with a filter that leaves one row:
is the aggregate of exactly those rows, so the divergence is visible in one screen:
2Kabove,1.5Kbelow.exactness affordance — the cell tooltip (
title="Total: 1500", thefmtTokensExactlayer) — and the file has none:
1.5Kis a string, not a summable number.The K tier is the tier real data lives in (measured 2026-09 against the deployed database:
99.92% of the 47万 token values fall in the K tier,
max(tokens) = 494 795, and the M tierhas zero rows) — so "the two rules agree in the M tier" is the only defence of the old
shape, and it is unreachable in practice.
The fix makes the spelling single-source:
fmtTokens(const fmtM = fmtTokens;) instead of carrying itsown
Math.roundsibling implementation;<...>Rawin the view model) and converts withfmtTokensExact— the same helper the cell tooltip already used.Related Issue
None — the repository has no open issue for this; it was found during a UI audit of the
transactions view. No
Closes #N.Changes
ui/js/app.js— 2 lines:const fmtM = fmtTokens;(the card delegates) and the fourexport columns now read
fmtTokensExact(t.<...>Raw).ui/index.html— cache-bust bump forapp.js(20260922-3→20260922-4).src/state_gate.rs— new gatethe_transactions_view_states_one_token_quantity_one_wayplus three self-proofs(
the_r158_roster_is_real,the_r158_scanners_have_teeth,the_r158_rules_separate_the_variants). Test-only module:#[cfg(test)] mod state_gate, nothing compiles into release artifacts.ui/README.md— documents the single-source rule and the gate's scope.Tests
cargo test— 326 passed / 0 failed (baselinemain322; +4 = the new tests).cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean (CI's exact command).What the gate pins (shape)
Four rules, each with its own tooth, on a reader that classifies the source into
per-line verdicts:
const fmtM = fmtTokens;).<...>Rawand go through the tooltip's helper(none missed).
the tempting over-correction of making the file right by degrading the screen.
Three self-proofs keep the gate honest:
the_r158_roster_is_real(renaming a view-modelfield turns it red, i.e. the roster is read out of the code and not hard-coded),
the_r158_scanners_have_teeth(the extractors are exercised on synthetic input), andthe_r158_rules_separate_the_variants(five variants, each mutation flipping only therule it targets).
Evidence beyond the gate
The gate is lexical: it proves the card delegates to
fmtTokens, and it does notprove that
fmtTokensnumerically equals the exact value the tooltip names. That half wascovered by driving the real app (real
index.html+ the four real scripts, stubbed fetch)in jsdom, 6 variants × 12 legs = 72 legs, each leg printing its own declaration next to its
verdict:
2Kvs row1.5K, all four export token columns non-integer (["1.5K","1K","2.50M","1500"]);fmtM = String(Math.round(n))) — card1500vs row1.5K, and thecached/M-tier agreement legs red;
precondition red;
fmtMwhose values equalfmtTokens's: greenby declaration, not by accident — that mutation is exactly what a lexical check can
see and a DOM check cannot, which is the division of labour stated in the README.
The gate fragment was also compiled and executed against a materialised baseline
(
git archiveof the pre-fix commit): the base leg fails only on the axis test(35/36) and the patched leg is 36/36 green.
Checklist
fix/…).