Skip to content

fix(i18n): write the sort-validation errors where the wordlist gate reads them - #307

Merged
argszero merged 1 commit into
mainfrom
fix/error-message-write-form-readable
Sep 27, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/error-message-write-form-readable

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

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 at each site was a bare identifier.

src/i18n_pack.rs::backend_error_literals is 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 gate every_backend_error_message_reaches_the_wordlist was vacuously true for those three messages, and an English UI displayed Chinese while cargo test stayed 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 in ERR_MAP (err.txSortDirMismatch, err.txSortKeyInvalid, err.txDirInvalid) and add both the zh and en pack keys.
  • src/i18n_pack.rs: new negative rule every_error_key_site_has_a_readable_write_form, plus its teeth test error_write_form_scanner_detects_injected_defects; ERR_MAP_ENTRY_COUNT 49 → 52, BACKEND_ERROR_CJK_COUNT 45 → 48, ZH/EN_KEY_COUNT 791 → 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).
  • No config or data-structure changes, so config/config.example.toml is 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?"

  • Corpus is derived, not hand-written: backend_source_candidates(root) reads src/*.rs + src/routes/*.rs off 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).
  • Same position, one implementation: 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 — not two conventions kept in sync by a comment.
  • Positive control first: per file, the scanner must see the same number of readable sites that the extractor reads out. Without it, "0 violations" could just as well mean "0 scanned" (the C2106 pitfall-245 shape).
  • Quantitative read-out: the two site counts (READABLE_ERROR_WRITE_SITES, STRUCTURED_ERROR_WRITE_SITES) catch a scanner that has gone blind in a way that still reports zero offenders.
  • Allowed forms: Readable (a literal, covered by the wordlist gate) and Structured (a {…} envelope — the gateway's {"error":{"message":…}}, whose message is supplied by the caller and by design is not part of the message corpus). Everything else fails.
  • Teeth: the sample in error_write_form_scanner_detects_injected_defects carries the three forms and three shapes that are not sites — an "error" in value position ("type": "error"), a match arm, and the fn 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.md as backend_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), and ui/README.md also 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.md rather 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.
  • New/updated unit tests added.

Two A/B legs were run against the pre-change tree, and both restore byte-identically:

Leg Change Result
1 Revert src/routes/wallet.rs to the closure form exactly the new rule turns red; everything else passes
2 Delete one new ERR_MAP pair two tests turn red — every_backend_error_message_reaches_the_wordlist (parsed 51 of 52) and every_pack_key_reaches_a_consumer (proving the new key is reachable, not an orphan)

Checklist

  • Branch naming follows the convention (fix/)
  • Commit message uses Conventional Commits
  • Single responsibility, minimal change

…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.
@argszero
argszero merged commit 4314851 into main Sep 27, 2026
2 checks passed
@argszero
argszero deleted the fix/error-message-write-form-readable branch September 27, 2026 04:57
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant