fix(i18n): write the sort-validation errors where the wordlist gate reads them - #307
Merged
Merged
Conversation
…eads them
`src/routes/wallet.rs::tx_order_by` produced its three 400 messages through a
helper closure (`let bad = |msg: String| … json!({ "error": msg })`), so the
`"error"` value was a bare identifier. `src/i18n_pack.rs::backend_error_literals`
is a lexical extractor: it only reads a literal at that position, so those three
messages were neither counted nor checked — `every_backend_error_message_reaches
_the_wordlist` was vacuously true for them, and an English UI showed Chinese.
They arrived in #284, 35 PRs after the gate that exists to catch exactly this.
Fold the envelope back into the three call sites — the shape the other 60-odd
error sites in this crate already use — and register the three messages in
`ERR_MAP` plus both language packs (`err.txSortDirMismatch`,
`err.txSortKeyInvalid`, `err.txDirInvalid`).
Then close the blind spot structurally rather than one message at a time:
`every_error_key_site_has_a_readable_write_form` inverts the default from
"everything read is registered" to "nothing unreadable exists". Its corpus is
`src/*.rs` + `src/routes/*.rs`, derived from the filesystem by
`backend_source_candidates`, so a new file joins the scan on its own. The rule
shares `error_value_pos` / `err_json_value_pos` with the extractor, so the
position it judges is by construction the position the extractor reads. It opens
with a positive control — per file, the scanner must see the same number of
readable sites the extractor reads out — before asserting zero opaque sites, and
finally pins the two site counts so a stale scanner cannot look like a clean
corpus. Teeth live in `error_write_form_scanner_detects_injected_defects`, whose
sample carries the three forms and three shapes that are *not* sites (an
`"error"` in value position, a match arm, the `fn err_json(` definition).
Two counts in that gate's roster were stale in a way nothing could see: the
route-file roster was documented (and referenced in `ui/README.md` and the source
comment) as `backend_error_sources_cover_the_routes_directory`, a test name that
never existed in this repository — it was born wrong in #249 and both carriers
kept pointing at it. Both now name the test that is actually there.
Scope, honestly: the gate is lexical. It proves the write form, not the text on
screen — and there is deliberately no runtime instrument, because the frontend
cannot send an invalid `sort` / `dir` (the #284 gate pins the server whitelist to
the UI column list in one build); only a direct API client reaches those three
400s. So this is an API-contract / consistency fix plus a structural fix to a
gate blind spot, not a user-visible UI defect.
cargo test: 405 -> 407. fmt and clippy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/routes/wallet.rs::tx_order_byproduced its three 400 messages through a helper closure —let bad = |msg: String| … json!({ "error": msg })— so the"error"value at each site was a bare identifier.src/i18n_pack.rs::backend_error_literalsis a lexical extractor: it reads a string literal at that position, and nothing else. A bare identifier therefore makes the site invisible — neither counted nor checked — so the existing gateevery_backend_error_message_reaches_the_wordlistwas vacuously true for those three messages, and an English UI displayed Chinese whilecargo teststayed green. They arrived in #284, thirty-five PRs after the gate that exists to catch exactly this class of drift.This PR does two things: it fixes the three sites, and it closes the blind spot structurally rather than one message at a time.
Related Issue
None.
Changes
src/routes/wallet.rs: fold the error envelope back into the three call sites (json!({ "error": format!(…) })) — the shape the other 60-odd error sites in this crate already use — and leave a comment saying why it must not be collapsed back into a helper.ui/js/i18n.js: register the three messages inERR_MAP(err.txSortDirMismatch,err.txSortKeyInvalid,err.txDirInvalid) and add both thezhandenpack keys.src/i18n_pack.rs: new negative ruleevery_error_key_site_has_a_readable_write_form, plus its teeth testerror_write_form_scanner_detects_injected_defects;ERR_MAP_ENTRY_COUNT49 → 52,BACKEND_ERROR_CJK_COUNT45 → 48,ZH/EN_KEY_COUNT791 → 794.ui/index.html: cache-bust the one asset that changed (js/i18n.js→?v=20260927-1), leaving the others alone.ui/README.md: document the write-form contract, the gate, and its scope (see Scope below).config/config.example.tomlis untouched.The new gate, and why it is shaped this way
The old gate asks "is everything read registered?" — the new one inverts the default: "does anything unreadable exist?"
backend_source_candidates(root)readssrc/*.rs+src/routes/*.rsoff the filesystem, so a new file joins the scan on its own (the hand-written roster in this repository has silently lagged before — C2072 / C2127).error_value_pos/err_json_value_poswith the extractor, so the position it judges is by construction the position the extractor reads — not two conventions kept in sync by a comment.READABLE_ERROR_WRITE_SITES,STRUCTURED_ERROR_WRITE_SITES) catch a scanner that has gone blind in a way that still reports zero offenders.Readable(a literal, covered by the wordlist gate) andStructured(a{…}envelope — the gateway's{"error":{"message":…}}, whosemessageis supplied by the caller and by design is not part of the message corpus). Everything else fails.error_write_form_scanner_detects_injected_defectscarries the three forms and three shapes that are not sites — an"error"in value position ("type": "error"), a match arm, and thefn err_json(definition — so the rule cannot start shouting at things that are not messages.A stale reference fixed in passing
The route-file roster's backing test was named in the source comment and in
ui/README.mdasbackend_error_sources_cover_the_routes_directory— a test name that has never existed in this repository. It was born wrong in #249 and both carriers kept pointing at it. Both now name the test that is actually there (backend_error_sources_cover_every_file_that_emits_an_error_literal), andui/README.mdalso cross-links the new rule.Scope, honestly
The gate is lexical: it proves the write form of the value, not the text on screen or in the response body. There is deliberately no runtime instrument for the other half, because the frontend cannot send an invalid
sort/dir— the #284 gate pins the server-side whitelist to the UI's column list within a single build. Only a client talking to the API directly can reach those three 400s.So this is an API-contract / consistency fix plus a structural fix to a gate blind spot — not a user-visible UI defect. That boundary is recorded in
ui/README.mdrather than left implicit.Tests
cargo test— 407 passed, 0 failed (baseline 405; the two new tests are the rule and its teeth).cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean.Two A/B legs were run against the pre-change tree, and both restore byte-identically:
src/routes/wallet.rsto the closure formERR_MAPpairevery_backend_error_message_reaches_the_wordlist(parsed 51 of 52) andevery_pack_key_reaches_a_consumer(proving the new key is reachable, not an orphan)Checklist
fix/)