Skip to content

fix(ui): let the marketplace peak gate name the fields it displays - #295

Merged
argszero merged 1 commit into
mainfrom
fix/marketplace-peak-gate-names-the-displayed-fields
Sep 24, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/marketplace-peak-gate-names-the-displayed-fields

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

modelsToView() decided whether a marketplace row is peak-priced from one field:

const peak = (m.peak_input_per_m || 0) > 0;   // ui/js/app.js:3542

Every peak disclosure on the row (the price-cell badge, the capability tag, the detail row, the ×N ratio) is keyed on that single boolean. But the billing engine decides per field — billing::effective_prices() asks peak_input_per_m > 0.0, peak_cache_hit_input_per_m > 0.0 and peak_output_per_m > 0.0 independently, and keeps the off-peak price for whichever field is 0 (src/billing.rs:92-116). Its own unit test pins exactly that partial case ("高峰时段 + 部分缺省 → 缺省项沿用空闲价", src/billing.rs:350-353), and the three fields are independently optional by design (config #[serde(default)], three separate admin inputs, validate_common only checks >= 0, PATCH falls back per field).

So a model with only a peak output price is billed peak every output token in the peak window, while the marketplace shows no peak marker, no peak-price row and no peak-pricing tag; and a model with only a peak input price prints the output cell as 0 where the engine actually charges the off-peak output price. Reachable in two clicks from the admin model form, unsignalled, and it never self-heals.

The fix makes the trigger name the two fields the row displays (peakInOn || peakOutOn) and makes each price slot take its own field's peak-or-off-peak price, so every number printed is the price the engine actually charges in the peak window.

Related Issue

No linked issue — this repository's issue list is all CLOSED; the defect came from this task's own reconnaissance.

Changes

  • ui/js/app.js — modelsToView(): the trigger becomes peakInOn || peakOutOn; peakIn/peakOut take each field's own peak-or-off-peak price; peakMult is the ratio of the enabled field (input first, else output) so it is never 0 while peak pricing applies; the stale comment above the function is corrected to the per-field rule and points at billing::effective_prices.
  • ui/index.html — cache-bust js/app.js?v=20260924-2 → 20260924-3.
  • src/state_gate.rs — new gate the_marketplace_peak_trigger_names_the_fields_it_displays (R1 upper bound: the trigger names no field effective_prices ignores; R2 lower bound: every peak field the row displays is named by the trigger; R3 neither set empty; R4 positive control: mk.peak.badge still carries {n}), all four values derived from the sources, plus the_r171_rules_have_teeth (four synthetic mutants, each flipping exactly one rule).
  • No config / data-structure change (zero new i18n keys).

Deliberately out of scope, and why: the sandwich is displayed ⊆ trigger ⊆ engine, not trigger == engine. peak_cache_hit_input_per_m is an engine field, but the row displays no cache-hit column and mk.peak.badge always prints a {n} multiplier — pulling it into the trigger would make a cache-only-peak model read "Peak ×0", a worse lie than no marker. Registered as a separate open item.

Tests

  • cargo test — 378 passed / 0 failed (baseline 376, +2 new tests)
  • cargo fmt --check — clean
  • cargo clippy --all-targets — clean
  • New tests added (the gate above)

Evidence

Gate A/B (teeth). Appending the gate to src/state_gate.rs and compiling it against the pre-fix ui/js/app.js makes the axis test FAIL with r1=true r2=false r3=true r4=true | engine={"peak_cache_hit_input_per_m","peak_input_per_m","peak_output_per_m"} trigger={"peak_input_per_m"} displayed={"peak_input_per_m","peak_output_per_m"} — exactly the declared R2 violation. Against the fixed tree all four rules hold.

jsdom probe. r171_peak_gate_probe.js — the app's real four scripts, a real logged-in session, a fixture catalog holding the partial-peak shapes — reports RESULT: ALL LEGS AS DECLARED for 6 variants × 2 languages = 12/12 runs: base (the defect) red on D3,A1,A2,A3,A4; fix all green; and four competitors each rejected by their own legs — m_always (mark everything peak) by A2,C2,D3, m_gate_any (fix the trigger only) by A2, m_drop_caps by A4, m_bogus (trigger names an engine-ignored field) by D3.

Byte-exactness. The three code edits are byte-identical to the probe's own fix variant (the generator reads the patch constants out of the probe source): the edited tree and the probe's fix tree hash the same (md5 0d0869d3…, post-instrumentation). The fourth edit is comment-only — stripping // lines from the 4-edit tree reproduces the 3-edit tree exactly.

Checklist

  • Branch name follows the convention (fix/…)
  • Commit message follows Conventional Commits
  • Single responsibility, minimal change (production change: one captured boolean pair + three right-hand sides + one comment)

@argszero
argszero merged commit cc51f82 into main Sep 24, 2026
1 check passed
@argszero
argszero deleted the fix/marketplace-peak-gate-names-the-displayed-fields branch September 24, 2026 17:05
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
argszero added a commit that referenced this pull request Sep 30, 2026
Release v0.7.29 — a version bump plus the CHANGELOG entry for the 41
commits merged since v0.7.28 (#295–#335). No production logic changes.

The release PR now has to touch five files instead of three: #303 and #305
gave the copies in docker-compose.yml / Dockerfile / CHANGELOG.md an
executor in src/deploy_gate.rs, so a bump that misses any of them turns
`cargo test` red (this is the deliberate cost of those rules).

- Cargo.toml / Cargo.lock: 0.7.28 -> 0.7.29
- docker-compose.yml: header comment and `image: aitokenpool:<v>`
- Dockerfile: header comment, `docker build -t aitokenpool:<v>` and the
  `docker run` example
- CHANGELOG.md: the `## v0.7.29` entry
- src/deploy_gate.rs: the version-scanner self-check asserted against the
  literal "0.7.28"; it now uses the derived `declared_version()`, so the
  release number is not left behind as another stale copy in the test.

Verified: `cargo test` 462 passed / 0 failed, `cargo fmt --check` rc=0,
`cargo clippy --all-targets -- -D warnings` rc=0. A/B with the file bytes
restored afterwards: bumping the manifest alone reddens exactly the two
version rules and names all five copy sites; a stale CHANGELOG heading
reddens only the heading rule; a builder image below the declared
rust-version reddens only the >= rule.
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