Skip to content

fix(sharing): count the points spent through a key, not its tokens, in keys.used - #217

Merged
argszero merged 1 commit into
mainfrom
fix/sharing-used-points
Sep 13, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/sharing-used-points

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The keys row keeps a matched pair quota / used, and the sharing page
renders them in one unit — the 「已用量」 stat card, the 「已用 / 额度」 cell
with its progress bar, and the dashboard mini-list all print them under a 「点」
label. quota is points (the form that collects it is labelled
「声明额度(点数)」), but used was accumulated as a token count:
billing::settle ran UPDATE keys SET used = used + p.tokens.

So the page printed a token count as points, and the ratio
used / quota divided tokens by points — a progress bar that means nothing.

Measured through the real router (one real billing::settle with
tokens = 1_000_000, pts = 4.5, on a key whose quota = 5000):

before after
GET /api/sharings used = 1000000, quota = 5000 used = 4.5, quota = 5000
rendered 「已用 1000000 / 5000 点」, bar pinned at 100 % 「已用 4.5 点」, bar at 0 %
ledger, same call consume_pts = 4.5, earn_pts = 4.05 unchanged

The real deployment agrees with the defect: atp-data/aitokenpool.db key 1 has
quota = 5000 and used = 319883396, so its sharing card reads
「已用量 319883396 点」 under 「5000 点总额度」.

Why this is a defect rather than a chosen unit:

  1. The strings and the form label are the invariant — five display sites and both
    language packs say 点; the only voice on the other side is one line of module doc.
  2. The sibling column with the same semantics already uses a points quantity
    (ui/js/app.js: the departments table's 「已用」 is d.month_cost), so the
    project has one unit for 「已用 / 额度」 and the sharing side is the odd one out.
  3. The progress bar is a host-mandated feature, and a bar over used / quota is
    only meaningful within one unit — so 「relabel the number」 is not available.
  4. keys.used has exactly one reader (sharing.rs::sharing_row) and no
    enforcement path (keys.quota gates nothing; the author deferred that to P1),
    while the tokens stay recorded per call in transactions.tokens /
    usage_records.tokens — nothing depended on the column being tokens.

Related Issue

None (no issue exists for this; none was fabricated).

Changes

  • src/billing.rs — accumulate p.pts (the same unit as keys.quota)
    instead of p.tokens, so the value written is one every consumer already
    assumes; correct the module-doc claim (keys.used += tokens → points);
    fix the existing split test's expectation (that fixture: tokens = 150,
    pts = 2.0, so used must be 2.0) and add a test whose fixture makes
    tokens and points differ by five orders of magnitude.
  • src/db.rs — heal existing deployments once, from the ledger
    (used = SUM(transactions.pts WHERE type='consume'), which is exactly the
    invariant the fixed writer maintains, so re-running is a no-op), gated on
    schema_version < 13; SCHEMA_VERSION 12 → 13.
  • src/db.rs — read the version with MAX(version). The schema_version
    table has no unique constraint and INSERT OR REPLACE is a plain INSERT
    when nothing conflicts, so the previous single-row read returned the
    oldest row: v < SCHEMA_VERSION stayed true forever and every start-up
    appended a row (the live DB has accumulated 22: 7,8,8,8,9×13,10,10),
    which also kept every v < N migration gate permanently open.
  • src/routes/sharing.rs — document the used field's unit and provenance
    (comment only).
  • docs/architecture.md — drop the hardcoded schema version from the two
    rows that this bump invalidated. They already pointed at
    SCHEMA_VERSION but also re-typed the number, which is precisely how
    they went stale (they said v12); the number is removed rather than
    re-typed, so it cannot rot again.
  • No ui/ change and no i18n key change — the strings already say 点, which
    is the point of the fix. No config change, so config/config.example.toml
    needs no sync.

Not touched, on purpose: keys.quota enforcement (still deferred to P1 by the
author), and the usage_records.cost vs transactions.pts question (a separate,
host-referred topic). No money moves — the ledger, balances and settlement are
untouched; this changes a displayed number's unit and one dead-end column.

Tests

  • cargo test — 220 passed (was 217; +2 net after fixture edits).
  • cargo fmt --check — clean.
  • cargo clippy --all-targets -- -D warnings — clean.
  • New test billing::tests::keys_used_records_consumed_points_not_tokens:
    drives a real settle whose tokens and pts differ by five orders of
    magnitude and asserts keys.used == SUM(consume pts) and that only
    consume rows count (the same call's earn row and a topup row must not
    contribute).
  • New test db::tests::keys_used_is_healed_from_the_ledger_on_upgrade:
    seeds a legacy row (used = 319883396 with a matching consume ledger
    totalling 12.5), a never-called key whose column is stale (42 → must
    become 0), and asserts the heal runs once (a second migrate must
    not re-run it, so appending a ledger row must leave used alone).
  • New test db::tests::schema_version_gate_uses_the_highest_recorded_version:
    seeds a multi-row version history and asserts the gate appends nothing on
    a second run.
  • A/B — each mutation applied alone, tree restored byte-identically
    (baseline 220 passed / 0 failed):
    - M1 writer used += pts → tokens → 2 red:
    keys_used_records_consumed_points_not_tokens,
    settle_splits_90_10_and_writes_ledger.
    - M2 heal gate if v < 13 → if false → 1 red:
    keys_used_is_healed_from_the_ledger_on_upgrade.
    - M3 version read back to the oldest row → 2 red:
    keys_used_is_healed_from_the_ledger_on_upgrade,
    schema_version_gate_uses_the_highest_recorded_version.
    Three mutations, three distinct red sets.
  • End-to-end display probe through the real router (temporary test,
    reverted byte-identically): POST /api/sharings → 200, one real settle
    (tokens = 1_000_000 / pts = 4.5, quota = 5000) → GET /api/sharings
    returns used = 4.5, quota = 5000, progress bar no longer saturated.

Checklist

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

…n keys.used

`keys.quota` is point-denominated (the form that collects it is labelled
「声明额度(点数)」), but `keys.used` was accumulated as a **token** count
(`billing::settle` ran `UPDATE keys SET used = used + p.tokens`). The sharing
page divides the two to draw its progress bar and prints both under a 「点」
label, so it rendered a token count as points.

Measured through the real router (one `billing::settle` with
`tokens = 1_000_000` / `pts = 4.5` on a key whose `quota = 5000`):

  before: GET /api/sharings -> used = 1000000, quota = 5000
          => 「已用 1000000 / 5000 点」, bar pinned at 100 %
          (the ledger for the same call holds consume_pts = 4.5)
  after:  GET /api/sharings -> used = 4.5, quota = 5000

The project has one unit for 「已用 / 额度」 elsewhere (the departments table
uses `d.month_cost`), and `keys.used` has exactly one reader
(`sharing.rs::sharing_row`) and no enforcement path, so nothing depended on
the column being tokens. Tokens stay recorded per call in
`transactions.tokens` / `usage_records.tokens`.

Changes:
- `billing.rs`: accumulate `p.pts` (same unit as `keys.quota`) instead of
  `p.tokens`; correct the module-doc claim; fix the existing split test's
  expectation and add a test whose fixture makes tokens and points differ by
  five orders of magnitude, asserting `used == SUM(consume pts)` and that
  only consume rows count.
- `db.rs`: heal existing deployments once, from the ledger
  (`used = SUM(transactions.pts WHERE type='consume')`), gated on
  `schema_version < 13`; `SCHEMA_VERSION` 12 -> 13. Also read the version with
  `MAX(version)`: the table has no unique constraint, so the previous
  single-row read made every start-up append a row (the live DB has 22 rows)
  and kept every `v < N` gate permanently true.
- `sharing.rs`: document the `used` field's unit and provenance.
- `docs/architecture.md`: drop the hardcoded schema version so the doc
  cannot rot again (it referenced `SCHEMA_VERSION` but also re-typed the
  number, which is exactly how it went stale).

No money moves: the ledger, balances and settlement are untouched. This
changes a displayed number's unit and one dead-end column.

Test suite: 217 -> 220 passed. fmt/clippy clean.
@argszero
argszero merged commit 164b8d3 into main Sep 13, 2026
1 check passed
@argszero
argszero deleted the fix/sharing-used-points branch September 13, 2026 18:16
@argszero argszero mentioned this pull request Sep 14, 2026
11 tasks
argszero added a commit that referenced this pull request Sep 14, 2026
Ships the 76 PRs merged since v0.7.22 (#161-#237), the largest release so far.
Database schema moves 12 -> 14:

- v13 (#217): `keys.used` changes unit from tokens to points, and the live
  values are healed from the ledger (`used = SUM(transactions.pts WHERE
  type='consume')`), gated on schema_version < 13.
- v14 (#234): four indexes for the monthly aggregates —
  `transactions(user_id, time, type, pts)`, `transactions(time, type, pts)`,
  `usage_records(time)`, `usage_records(user_id, time)`.

Deployments must apply the DeepSeek flash rename (#233) to their own
config.toml: `seed_models` is a full sync, so a model absent from the config is
deleted at startup; the retired names are gone from config.example.toml.

- Cargo.toml / Cargo.lock: 0.7.22 -> 0.7.23.
- ui/index.html: asset cache-bust 20260912-4 / 20260912-5 / 20260914-1 /
  20260914-4 / 20260914-9 -> 20260914-10 (all five refs).
- CHANGELOG.md: v0.7.23 entry, grouped by area with the PR and hash of each fix.

Gates: `cargo test` 249 passed / 0 failed, `cargo fmt --check` clean,
`node --check` on the four ui/js files OK.
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