Skip to content

$ money-float: rates carried as exact nano-units end to end, converted once at the config intake; a non-whole-nano rate is refused, never rounded (#81, OWNER rule 21) - #870

Open
MattJackson wants to merge 5 commits into
predevfrom
lane-money-float-rates
Open

MattJackson wants to merge 5 commits into
predevfrom
lane-money-float-rates

Conversation

@MattJackson

@MattJackson MattJackson commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

What

Rates on the money path are carried as exact integer nano-units end to end. Each configured rate is converted once, at the config intake. A rate that is not a whole number of nano-units is REFUSED at config time and never rounded.

  • Intake. RateEntryCfg::raw_tier_rates (crates/busbar-kernel/src/config/sections.rs) converts each configured _utok rate once, through the ledger's representable_nano_rate. Nothing after it carries a float.
  • Shapes. RawTierRates (contract) and TierRates (ledger) hold Option<u64> nano-units per token. None means a rate the card cannot hold, and it refuses (Per-user provider credentials: using a seat subscription from your own apps #42).
    • Default is every tier at Some(0). That is the free tier 1.5.5's #[serde(default)] gave.
  • Card. RateCard::from_config and place_rate take integers. from_micro_rates converts only through representable_nano_rate. RateNanos::from_raw copies integers; it is no longer a second conversion outside the intake table.
  • nano_rate. It multiplies by 1000 exactly, on the configured decimal's own digits (the shortest round-trip text of the double, never a binary product). A rate with any digit past the third decimal of micro-units converts to 0 / None. Its ceiling is a checked integer multiply.
  • Boot validation. It refuses a None rate and names the model, class and value. The message now says that a non-zero rate is a whole number of nano-units (at most three decimals) and is never rounded.
  • Routing scalar. RawTierRates::blended_per_mtok is removed. rate_entry_per_mtok is a routing order (cheapest and the hook cost_per_mtok), not money. It reads the config figures exactly as 1.5.5 did (crates/busbar/src/config/mod.rs:2137 @v1.5.5).
  • Docs. docs/configuration.md states the rule for operators.

Why

  • Part 2 mcp: SSE on the POST, notifications/message, binary resources, resolving templates and typed prompt content #81 (BUSBAR-1.6.0.md l.2282): no f32/f64 on any money path.
  • OWNER rule 21 (2026-10-14): "money is bank auditor grade. its either right or its wrong. make it right." / "money - ledger * ratecard". A rate that cannot be represented exactly is a loud config refusal.
  • The row-159 xtask E audit found:
    • float carriers: RawTierRates, TierRates and RawRates, across cost.rs, rate_apply.rs, billing.rs, kernel-ledger cost/mod.rs and root/kernel.rs;
    • a float conversion at cost.rs:117, outside the intake table.

Against 1.5.5

  • Same: every rate that is a whole number of nano-units (at most three decimals of micro-units) converts to exactly 1.5.5's integer ((utok*1000.0).round(), crates/busbar/src/cost.rs:99 @v1.5.5). The busbar-oracle money fixtures configure only whole rates (0, 1, 1e5, 2e5, 1e6, 2e6, 1e7, 2e7), so every golden cell is unchanged.
  • Changed (rule 21): a rate 1.5.5 rounded (0.0015, 0.5005) is now refused at boot / --validate. It is never rounded.

REDs ($)

  • nano_rate_refuses_a_rate_that_is_not_a_whole_number_of_nano_units: 0.0015, 0.0014, 0.0005, 0.5005, 0.5015, 1.0001 and the double 0.1 + 0.2 are refused (0 / None). Whole-nano rates convert exactly.
  • rate_nanos_from_cfg_refuses_a_rate_finer_than_one_nano_unit: the intake refuses 0.0015 and 0.0014.
  • test_validate_rate_card_refuses_a_rate_the_card_cannot_hold: 0.0005, 0.0015 and 0.5005 refuse boot and name the model, class and value. 0.001 boots.
  • a_fractional_cent_rate_prices_exactly_through_the_live_intake: rates of 0.1 and 0.2 price one token of each at exactly 300 nano-units. The drifted double 0.30000000000000004 is refused.
  • a_large_count_times_a_rate_has_no_float_drift: 2^53+1 tokens × 300 nano-units, exact.
  • the_intake_prices_every_money_fixture_rate_at_its_1_5_5_figure: the oracle fixture rates plus the in-tree card fixtures.
  • from_raw_is_byte_identical_to_the_unclamped_projection_for_every_in_range_rate: over 10k generated rates there are two arms only. A whole-nano rate equals 1.5.5's integer; every other rate is None at the intake.
  • The cross-crate agreement test pins the exact integer ceiling (the float product was 480 nano-units short at 18_446_744_073_709_548). It also checks that a refused rate lands UNPRICED on the card, never as a priced zero.

Ledger rows touched

None. The no-float-money gate's MONEY_INTAKE rows (from_micro_rates, from_config, place_rate, TierRates, card_from_config) still name existing items. Those items no longer carry a decimal, so the gate is not weakened. The gate file is held by #853, #787 and #503.

Shared files (queue rule 20, disjoint hunks, clean merge with predev f70f567)

…once at the config intake

Part 2 #81 (BUSBAR-1.6.0.md l.2282): no f32/f64 on any money path. The four reserved
tier rates were carried as binary floats from the config struct through RawTierRates
(contract), the rate-apply seam (RawRates), the root relay (card_from_config) and
TierRates (ledger) into the card build, with a second float conversion in
RateNanos::from_raw (cost.rs:117) outside the intake table.

Now:
- RateEntryCfg::raw_tier_rates is THE intake: it converts each configured rate ONCE,
  through the ledger's representable_nano_rate, into Option<u64> nano-units.
- RawTierRates and TierRates hold Option<u64> nano-units (None = a configured rate
  the card cannot hold, item 22: placed unpriced, a hit refuses, #42). Default is
  every tier at Some(0), the free tier 1.5.5's #[serde(default)] gave.
- RateCard::from_config / place_rate take integers; from_micro_rates converts
  through representable_nano_rate and nothing else.
- RateNanos::from_raw copies integers; it no longer converts.
- RawTierRates::blended_per_mtok is gone; rate_entry_per_mtok (the routing order,
  not money) reads the config figures as 1.5.5 did.
- nano_rate does the x1000 and #44's half-away-from-zero rounding on the configured
  decimal's own digits (shortest round-trip text), never on a binary product, and
  its ceiling is a checked integer multiply.

1.5.5 parity: every rate with up to three decimals converts to 1.5.5's integer;
the busbar-oracle money fixtures configure only whole rates (0, 1, 1e5, 2e5, 1e6,
2e6, 1e7, 2e7). The one signed divergence (#81, #44) is an exact decimal half
nano-unit whose binary product fell below the half (0.5005 -> 500 in 1.5.5, 501
now), about 1% of x.xxx5 rates.

REDs: nano_rate(0.5005) == 501, nano_rate(0.1 + 0.2) == 300; a 0.1+0.2 rate prices
exactly through the live intake; a 2^53+1 token count prices with no drift; every
money-fixture rate prices at its 1.5.5 figure; the 1.5.5 projection equivalence over
10k generated rates allows only the signed half-nano class; the ceiling test pins the
exact integer boundary.
…ts is refused, never rounded (OWNER rule 21)

OWNER rule 21 (2026-10-14): "money is bank auditor grade. its either right or its
wrong. make it right." / "money - ledger * ratecard". The card holds integer
nano-units, so a configured *_utok rate either IS a whole number of them (at most
three decimal places of micro-units) or the card cannot hold it. nano_rate no longer
rounds: a rate with any digit past the third decimal converts to 0, and
representable_nano_rate answers None. Boot validation already refuses a None
naming the model, class and value.

Every rate the card can hold converts to exactly 1.5.5's (utok * 1000.0).round().
A rate 1.5.5 rounded (0.0015, 0.5005) is refused at config time.

REDs flipped to refusals: nano_rate(0.5005), (0.5015), (0.0015), (0.0014), (0.0005)
and the double 0.1 + 0.2 are refused (0 / None); RateNanos::from_cfg refuses
0.0015 and 0.0014; the 1.5.5 projection equivalence has two arms only (whole-nano
rates equal 1.5.5's integer, every other rate is None at the intake). The earlier
commit's "signed half-nano divergence" is withdrawn: nothing is rounded.
docs/configuration.md states the rule for operators.
…ses boot (0.0005, 0.0015, 0.5005); 0.001 is the boundary that boots; the refusal message says rates are never rounded (OWNER rule 21)
@MattJackson MattJackson changed the title $ money-float: rates carried as exact nano-units end to end, converted once at the config intake (#81) $ money-float: rates carried as exact nano-units end to end, converted once at the config intake; a non-whole-nano rate is refused, never rounded (#81, OWNER rule 21) Oct 14, 2026
@MattJackson
MattJackson marked this pull request as ready for review October 14, 2026 20:02
@MattJackson
MattJackson marked this pull request as draft October 14, 2026 20:20
@MattJackson MattJackson added the alone ModernCI: judge and land alone (never batched) label Oct 14, 2026
@MattJackson
MattJackson marked this pull request as ready for review October 14, 2026 20:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alone ModernCI: judge and land alone (never batched)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant