Skip to content

test(i18n): every pack key must reach a consumer - #266

Merged
argszero merged 1 commit into
mainfrom
fix/pack-key-reachability-gate
Sep 21, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/pack-key-reachability-gate

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The language packs are two files, and all three existing gates ask the same direction: is every key
that is referenced present in both packs?
Nothing asked the reverse question — so a key that nobody
references
is invisible to CI, and that is not harmless: share.toggle.relisted sat in both packs
unreachable, and that unreachable key was the fingerprint of a lost branch (the re-list outcome, fixed in
#265). A pack key with no consumer is either dead weight or evidence that a code path went missing.

This lands every_pack_key_reaches_a_consumer: the whole pack must be reachable, with today's residue
frozen as a sunset list rather than a registry exemption.

Provenance: the change was written in the worktree on 2026-09-15 (branch fix/pack-key-reachability-gate,
created 16:26) and left uncommitted when the cycle was cut short — since then every run of the evolution
task was forced read-only by the dirty-tree guard, so it could not be committed by the task itself. It is
landed here on the task's behalf; nothing in it has been altered except by the measurements below.

Related Issue

Changes

  • src/i18n_pack.rs — test-only module (main.rs compiles it as #[cfg(test)] mod i18n_pack, so no
    production code path and no binary-size change):
    • DATA_JS joins the consumer corpus: ui/js/data.js feeds marketRows() and friends, so a key
      referenced only there is still reachable (it was outside the corpus before).
    • the reachability predicate: a key is reachable iff it occurs at a key-token boundary (ASCII
      alphanumeric + _ . -, so a.b is not matched by a substring of a.bc) in thecomment-stripped
      consumer corpus — ui/js/app.js / api.js / data.js, ui/index.html (HTML comments stripped), and
      the parts of ui/js/i18n.js outside its pack sections — or begins with a dynamic prefix
      derived from the corpus itself (T("share.day." + …)), not from a hand-written roster.
    • UNREACHABLE_PACK_KEYS — the sunset list: 59 keys, each labelled with the class the census
      arrived at (old-design 30 · dup-sibling 11 · zero-mock 9 · neutral-literal 4 · composite 2 ·
      rename 1 · weak-should-wire 1 · host-decision 1). The list is a contract, not an exemption
      registry: it may only shrink.
    • every_pack_key_reaches_a_consumer — asserts the computed set equals the declared set
      exactly (not "⊆"). Three shapes turn it red: a newly unreferenced key; a list entry whose key has
      become reachable again (re-wire a sunset key and you must drop its entry in the same change); an entry
      naming a key that no longer exists.
    • pack_reachability_checker_detects_injected_defects — the predicate self-proof on synthetic inputs
      only (token boundary, comments are not consumers, an empty corpus makes everything unreachable).
  • ui/README.md — the rule, the CI coverage, and the measured scope: the gate pins the list
    (shrinking it — the 23 safely deletable keys — is a separate round), and the dynamic-prefix arm is
    redundant on today's real corpus (share.day.1..7 are statically bound in ui/index.html as well), so
    its tooth is proven by the synthetic inputs rather than by live drift.

No config or data-structure change, so no example file needs updating.

Tests

  • cargo test passes — 297 passed (was 295; +2 = the gate and its self-proof)
  • cargo fmt --check passes
  • cargo clippy --all-targets reports no warning
  • New tests added (the two above)

Killability — both directions of the equality, in place, restored byte-for-byte
(src/i18n_pack.rs md5 7a30a726df201bc733fa77d7c8d58517 before and after):

arm declared actual
drop one sunset entry (login.forgot) RED · the "new orphan" arm RED — 新增的不可达键 … : ["login.forgot"]
add a bogus entry (probe.bogus.key) RED · the "stale entry" arm RED — 清单里已可达 / 已不存在的键 … : ["probe.bogus.key"]
this branch GREEN GREEN — 297 passed

Checklist

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

The three existing pack gates all ask the same direction: is every key that IS
referenced present in both packs? Nothing asked the other question, so a key
nobody references is invisible to CI -- and an unreachable key is not harmless:
`share.toggle.relisted` sat in both packs unreachable, and it was the
fingerprint of a lost branch (the re-list outcome, fixed in #265).

This adds `every_pack_key_reaches_a_consumer`: the whole pack must be reachable,
with today's residue frozen as a sunset list rather than a registry exemption.

- `src/i18n_pack.rs` (test-only: `main.rs` compiles it as `#[cfg(test)] mod`):
  - `ui/js/data.js` joins the consumer corpus;
  - the predicate: a key is reachable iff it occurs at a key-token boundary in
    the comment-stripped corpus (`app.js` / `api.js` / `data.js`, `index.html`,
    and the code parts of `i18n.js` outside its pack sections) or begins with a
    dynamic prefix derived from the corpus itself (`share.day.`);
  - `UNREACHABLE_PACK_KEYS`: the sunset list, 59 keys, each labelled with the
    class the census arrived at;
  - the gate asserts the computed set == the declared set EXACTLY: a new
    unreferenced key, a stale entry, or a wired-up sunset key all turn it red;
  - the predicate self-proof runs on synthetic inputs.
- `ui/README.md`: the rule, the CI coverage and the scope (the gate pins the
  list, it deletes no key).

No production code path changes, no config or data-structure change.
@argszero
argszero merged commit a4cb622 into main Sep 21, 2026
1 check passed
@argszero
argszero deleted the fix/pack-key-reachability-gate branch September 21, 2026 08:35
@argszero argszero mentioned this pull request Sep 21, 2026
12 tasks
argszero added a commit that referenced this pull request Sep 21, 2026
Ships the 10 PRs merged since v0.7.25 (#261-#270). No schema change, no config
change, so the deployment-side config.toml needs no edit.

One fact, one source / display must equal what it consumes (frontend, 6 places)
- #261 read the spendable half of the wallet payload when refreshing your own
  balance; #262 the transactions payload signature covers the time range, with
  one reload trigger shared by the four controls; #263 the settings controls are
  either wired or explicitly inert; #265 the re-list outcome comes from the same
  entry as its action; #268 the sharing form shows a plan's label, not its
  config id; #269 the ops card stops reading a key's status count as a health
  verdict.

i18n reachability
- #266 every pack key must reach a consumer (the gate), and #270 drops the 23
  keys that gate proved unreachable: ZH/EN key count 811 -> 788, sunset list
  59 -> 36.

Gateway
- #267 applies the body limit where axum actually reads it (per-route
  DefaultBodyLimit, 8 MiB on the three gateway routes; unauthenticated
  endpoints keep the 2 MiB default). This is the application half of rant
  2026-09-18T09:14:18. It also corrects the false v0.7.10 "raised to 70MB"
  CHANGELOG line, which described installing a layer rather than raising a limit.

- Cargo.toml / Cargo.lock: 0.7.25 -> 0.7.26.
- CHANGELOG.md: v0.7.26 entry plus the v0.7.10 correction.
- ui/index.html cache-bust left as-is: this release touches no UI file; the live
  values are app.js 20260921-2 / i18n.js 20260921-2.

cargo test 302 passed; cargo fmt --check clean; clippy -D warnings clean.
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