Skip to content

fix(ui): express the marketplace count in the unit of its rows - #276

Merged
argszero merged 1 commit into
mainfrom
fix/marketplace-count-unit
Sep 21, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/marketplace-count-unit

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The marketplace toolbar count (#mk-count) is filled with T("cnt.on", { n: list.length }),
and cnt.on renders "{n} keys on sale" / "{n} 个在售 key" — a key count.

But list is the filtered model-row list (marketRows()/api/models), so the number is a
row count. The same table's own header is mk.col.providerModel ("Provider / model" /
"厂商 / 模型"), and each row carries its own key pill ("Available · 3 keys" / "No key").
The widget counts one thing and names another, and one screen says it twice.

With the probe fixture (4 model rows, 6 keys in total) the label reads "4 keys on sale" — a number
that is not the key count (4 ≠ 6) and a unit that contradicts the "No key" pill two lines below
it.

This change renames the pack key cnt.oncnt.models, updates its two values, and updates the two
call sites, so the count speaks the unit of the rows it actually counts. After the change the toolbar
reads "4 models" / "4 个模型".

Root cause and provenance — drift, not a deliberate trade-off

  • The design baseline (the author's own prototype, docs/prototype/aitokenpool-console.html)
    writes $("#mk-count").textContent = "共 " + list.length + " 个模型"; — the count was models
    from the start. The word "在售" (on sale) appears nowhere in that file.
  • d70e032 (feat(ui): AITokenPool static HTML UI prototype (marketplace + enterprise + login) #7) is where the live page diverged; 68f9f70 (feat(ui): i18n — zh/en language packs with switching (v1.21) #86, the i18n extraction) carried the
    divergent literal verbatim into the packs and named it cnt.on (on = "on sale" — a name that
    describes keys).
  • The 2026-09-11 site-wide UI redraw rebuilt the whole marketplace table against the new baseline and
    missed this cell.
  • The row source is one row per model (dao::list_models_with_availability), and marketRows()
    funnels every row through modelsToView(Live.models) — so "rows" and "models" are the same set
    here; only the label disagrees.

Because the name cnt.on also lies once the value becomes a model count, this renames the key
rather than only retyping the value. A rename keeps the pack key count identical (one name changed,
none added or removed) and the T("…") literal count identical, so no i18n gate constant needs
recalibration.

Changes

# File Change
E1 ui/js/i18n.js (zh pack) "cnt.on": "{n} 个在售 key""cnt.models": "{n} 个模型"
E2 ui/js/i18n.js (en pack) "cnt.on": "{n} keys on sale""cnt.models": "{n} models"
E3 ui/js/app.js (empty/failure fill) T("cnt.on", { n: 0 })T("cnt.models", { n: 0 })
E4 ui/js/app.js (list fill) T("cnt.on", { n: list.length })T("cnt.models", { n: list.length })
E5 src/i18n_pack.rs new gate the_marketplace_count_is_expressed_in_the_unit_of_its_rows (+ its self-check test)
E6 ui/README.md new section: the toolbar count counts rows (models), so its wording must use the same unit as the table's row-identity column; plus the gate's stated scope
E7 ui/index.html cache-bust both changed scripts: js/i18n.js and js/app.js (procedure: read the live ?v= value and write a strictly newer one — never copy a literal out of a plan)

The gate (E5) — five rules, each with its own tooth

The gate derives both of its rulers instead of hard-coding a wording:

  1. Count key is derived from who fills the element: every site that writes #mk-count's
    content in app.js must take its value through T("<literal>"), and all sites must name the
    same key. If a site names more than one key, every key is judged by rules 1/3/4.
  2. Row-identity key is derived from the same screen's table header (the first data-i18n in the
    <thead> of the table containing #mk-body), and its value must be of the form A / B
    (otherwise the noun cannot be isolated → the rule fails loudly).
  3. Both packs' count wording must contain the row-identity unit noun (模型 / model).
  4. Neither pack's count wording may contain the app's noun for a single key
    (ruler = the share.col.key value).
  5. That ruler must stay the same word as the marketplace row pills' mk.avail. family, so the
    ruler cannot be silently swapped.

Ruler 4 is deliberately taken from the sharing view's column header, not from the market row's own
pill: if the ruler were read off the very element under suspicion, the test would be circular.

Evidence — A/B

Gate A/B (authoritative; 11 trees, every leg's expectation declared next to its verdict)

Tree Declared red rules Actual red rules
base (the defect) ③ row unit zh, ③ row unit en, ④ key unit zh, ④ key unit en same
fix (this PR) (none) (none)
m_zh_only (only zh fixed) ③ row unit en, ④ key unit en same
m_en_only (only en fixed) ③ row unit zh, ④ key unit zh same
m_cosmetic (append the row word, keep the key word) ④ key unit zh, ④ key unit en same
m_pill_weakened (fix + strip the key word from the market pills) ⑤ cross-view wording zh/en same
m_ruler_renamed (fix + rename the ruler's zh value) ⑤ cross-view wording zh same
m_header_shape (fix + row header loses its A / B shape) ② header shape zh same
m_no_fill (fix + delete one fill site) (none) — the gate is blind here, declared (none); the probe's P0b/F1/T1 legs reject it
m_diff_keys (fix, then the 2nd site names another key) ① not-same-source, ③ en, ③ zh same
m_hardcoded (fix, then the 2nd site writes a literal) ① no-pack-key same

Result: ALL LEGS AS DECLARED. clippy control on the assembled tree: without the new block
rc=0, no diagnostics; with the block rc=0, no diagnostics — so no diagnostic can be
attributed to the new block.

The base tree is built by reverse-applying the same byte-exact rename to the live pack (and the
harness refuses to run unless the live tree is in the post-fix state), so the baseline is explicit
rather than implied by whatever happens to be checked out.

DOM probe (c2172_probe.js, jsdom, 14 legs)

Two independent facts are printed per leg — the declared expectation and the measured verdict — never
collapsed into one column.

  • pre-fix tree14/14 as declared; the axis legs {A1,A2,A3,A4} fail exactly:
    • en: header noun "model", count "4 keys on sale"
    • zh: header noun "模型", count "4 个在售 key"
      while F1/F2 show the number equals the row count (4) and is not the key total (6).
  • this PR's tree14/14 as declared; count "4 models" / "4 个模型".
  • Controls: R1 (the sharing page does count keys and its rows are keys — anti over-correction:
    the rule is "say what you count", not "never say key") and T1 (filtering narrows the rows and the
    count follows them).

Five competing fixes were re-derived from the pre-fix baseline and each is rejected by an independent
leg (a competitor that only changes the wording is not accepted):

Competing fix Legs that fail
only the zh label fixed A1, A2
key unit kept, row noun bolted on ("cosmetic") A2, A4
the number changed to a key total F1, T1 (and the label legs)
the count deleted entirely P0b, F1, T1
row pill loses its noun so a naive word-check would go green K2

Tests

  • cargo test314 passed, 0 failed on this branch (baseline main = 312; +2 = the new
    gate test and its self-check).
  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.
  • New gate (E5) with its own self-check test and synthetic inputs, so a checker that stopped
    discriminating would fail rather than pass silently.
  • Cache-bust read-back: exactly one js/i18n.js?v=… and one js/app.js?v=… tag remain, and each
    carries the newly written token.

What this PR deliberately does NOT do

  • It does not add or remove any pack key (cnt.oncnt.models is a rename), so the i18n
    positive-control constants (ZH/EN_KEY_COUNT, T_LITERAL_COUNT) are unchanged and
    UNREACHABLE_PACK_KEYS is untouched.
  • It does not touch mk.count, the value-identical orphan that used to be cnt.on's twin: that
    key was already dropped by the earlier pack-key-shrink change (PR chore(i18n): drop 23 keys that no consumer can reach #270), which is why the anchor
    for this rename carries the key name and not only the old value.
  • It does not change the count's arithmetic, its filter behaviour, or any other view — R1/T1
    above pin both halves of that.

Related Issue

(No upstream issue — internal consistency fix found by the project's own maintenance pass.)

Checklist

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

The toolbar count on the marketplace view was filled with
T("cnt.on", { n: list.length }), and `cnt.on` renders "{n} keys on
sale" / "{n} 个在售 key" — a key count. But `list` is the filtered
model-row list, so the number is a row count: with the probe fixture
(4 model rows, 6 keys) the label reads "4 keys on sale", contradicting
both the "No key" pill on one of its own rows and the key total.

The author's own prototype counts models ("共 N 个模型"), so this is
drift rather than a trade-off. Rename the pack key cnt.on -> cnt.models
and update its two values plus the two call sites, so the count speaks
the unit of the rows it counts.

Add src/i18n_pack.rs::the_marketplace_count_is_expressed_in_the_unit_of_its_rows,
which derives both of its rulers instead of hard-coding a wording: the
count key comes from whoever fills #mk-count, and the row-identity
noun comes from the same table's header. It also pins that the "single
key" ruler stays the same word as the marketplace row pills.

Rename only: no pack key is added or removed, so the i18n gate
constants are unchanged. Cache-bust both changed scripts.
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (author is also the merging committer; gh pr review --approve on one's own PR always fails, so this is a plain comment).

Verification performed on this exact branch head (91ccf65):

Local gates

  • cargo test314 passed, 0 failed (baseline main = 312; +2 = the new gate test and its synthetic self-check).
  • cargo fmt --check — clean. cargo clippy --all-targets -- -D warnings — clean.

Gate A/B (11 trees, live src/i18n_pack.rs assembled against each tree, rustc --test, both new tests run): ALL LEGS AS DECLARED — the defective tree fails rules ③/④ in both packs, the fixed tree passes all five rules, and seven competing shapes are each rejected by an independent rule. clippy control on the same assembly without the new block: rc=0, no diagnostics, so nothing in the block is responsible for a diagnostic.

DOM probe (jsdom on the real index.html + the four real scripts, fixture = 4 model rows / 6 keys, count derived from the rendered DOM): pre-fix tree 14/14 as declared with exactly {A1,A2,A3,A4} red; this branch 14/14 as declared with nothing red. Five competing fixes re-derived from the pre-fix baseline are each rejected: zh-only label (A1,A2), cosmetic word-append (A2,A4), number switched to a key total (F1,T1), count deleted (P0b,F1,T1), row pill stripped of its noun (K2).

Scope honesty

  • The gate pins the unit, not the number: deleting one fill site passes it (the site count drops with the site). That case is caught by the probe's P0b/F1/T1 legs, and the gate's scope is stated in ui/README.md as part of this PR.
  • No pack key is added or removed (rename only) — ZH/EN_KEY_COUNT and UNREACHABLE_PACK_KEYS are untouched, which is why the i18n positive-control constants did not need recalibration.
  • The old value's value-identical twin mk.count was already removed by the earlier pack-key-shrink change (chore(i18n): drop 23 keys that no consumer can reach #270), so the rename anchors carry the key name rather than only the old value.

@argszero
argszero merged commit bd715f1 into main Sep 21, 2026
1 check passed
@argszero
argszero deleted the fix/marketplace-count-unit branch September 21, 2026 14:49
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