Money Discipline
Money Discipline: every money bug in production is the same three bugs - floats for currency, arithmetic before validation, and rounding decided at the last minute by whoever's line of code got there first. This runs an invoicing plan twice: once the way demos do it (floats), once the way ledgers demand (integer cents, a Money value object, rounding policy declared at the boundary) - and lets a penny audit judge them both.
Observability & Ops
Round 13
Noel Rappin
exit 0
bundle exec ruby examples/money_discipline.rb
a real captured run
MONEY DISCIPLINE (same invoice, two arithmetics)
float version ledger version
subtotal 230.19999999999999 $230.20
tax 18.99150000000000 $18.99
total 249.19149999999999 $249.19
the ledger version signed its contract: integer cents, all
non-negative, and the adds_up rule verified to the penny.
now read the float column like an accountant: the subtotal ends
in ...999999, because 0.1 x 3 is not 0.3 in binary - IEEE 754
is already paying out interest. today it rounds to the
right penny (24919); at some other quantity or rate it won't,
and the discrepancy will surface in a reconciliation report
eleven months from now, assigned to whoever touched the code
last. the discipline is three sentences: money is integer
cents (a TYPE the contract can enforce - "integer" isn't
pedantry, it's a tripwire); rounding is a NAMED policy applied
at declared points (banker's, at multiplication), not an
accident of printf; and the books must balance BY RULE
(adds_up), not by hope. take my money - but count it in cents.
source
# frozen_string_literal: true # Money Discipline: every money bug in production is the same three # bugs - floats for currency, arithmetic before validation, and # rounding decided at the last minute by whoever's line of code got # there first. This runs an invoicing plan twice: once the way demos # do it (floats), once the way ledgers demand (integer cents, a Money # value object, rounding policy declared at the boundary) - and lets # a penny audit judge them both. # # bundle exec ruby examples/money_discipline.rb # # Runs offline; the discrepancy is real IEEE 754, not contrivance. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal LINE_ITEMS = [ {desc: class="s">"api calls", unit_price: 0.1, qty: 3}, {desc: class="s">"storage", unit_price: 29.99, qty: 3}, {desc: class="s">"seats", unit_price: 19.99, qty: 7} ].freeze TAX_RATE = 0.0825 # --- the demo version: floats all the way down ---------------------------------- def float_invoice(items) subtotal = items.sum { |i| i[class="y">:unit_price] * i[class="y">:qty] } tax = subtotal * TAX_RATE {subtotal: subtotal, tax: tax, total: subtotal + tax} end # --- the ledger version: integer cents + one rounding policy -------------------- # Money is a value object: cents inside, arithmetic closed, rounding # NAMED (banker's here) and applied exactly where policy says Money = Struct.new(class="y">:cents) do def +(other) = Money.new(cents + other.cents) def *(other) = Money.new((cents * other).round(half: class="y">:even)) def to_s = format(class="s">"$%d.%02d", cents / 100, cents % 100) end def cents(dollars) = Money.new((dollars * 100).round) CONTRACT = Agentic:class="y">:CapabilitySpecification.new( name: class="s">"invoice", description: class="s">"Price an invoice", version: class="s">"1.0.0", inputs: {items: {type: class="s">"array", required: true, non_empty: true}}, outputs: { subtotal_cents: {type: class="s">"integer", required: true, min: 0}, tax_cents: {type: class="s">"integer", required: true, min: 0}, total_cents: {type: class="s">"integer", required: true, min: 0} }, rules: { adds_up: {message: class="s">"total must equal subtotal + tax, to the penny", fields: [class="y">:subtotal_cents, class="y">:tax_cents, class="y">:total_cents], check: ->(o) { o[class="y">:total_cents] == o[class="y">:subtotal_cents] + o[class="y">:tax_cents] }} } ) def ledger_invoice(items) subtotal = items.map { |i| cents(i[class="y">:unit_price]) * i[class="y">:qty] }.sum(Money.new(0)) tax = subtotal * TAX_RATE {subtotal_cents: subtotal.cents, tax_cents: tax.cents, total_cents: (subtotal + tax).cents} end # Run both as plan tasks; validate only the ledger (floats can't even # SIGN the contract - integer cents is a type, and types are promises) orchestrator = Agentic:class="y">:PlanOrchestrator.new float_task = Agentic:class="y">:Task.new(description: class="s">"float invoice", agent_spec: {class="s">"name" => class="s">"f", class="s">"instructions" => class="s">"w"}, payload: LINE_ITEMS) ledger_task = Agentic:class="y">:Task.new(description: class="s">"ledger invoice", agent_spec: {class="s">"name" => class="s">"l", class="s">"instructions" => class="s">"w"}, payload: LINE_ITEMS) orchestrator.add_task(float_task, agent: ->(t) { float_invoice(t.payload) }) orchestrator.add_task(ledger_task, agent: ->(t) { ledger_invoice(t.payload) }) result = orchestrator.execute_plan floats = result.task_result(float_task.id).output ledger = result.task_result(ledger_task.id).output puts class="s">"MONEY DISCIPLINE (same invoice, two arithmetics)" puts puts format(class="s">" %-12s %-28s %s", class="s">"", class="s">"float version", class="s">"ledger version") puts format(class="s">" %-12s %-28.14f %s", class="s">"subtotal", floats[class="y">:subtotal], Money.new(ledger[class="y">:subtotal_cents])) puts format(class="s">" %-12s %-28.14f %s", class="s">"tax", floats[class="y">:tax], Money.new(ledger[class="y">:tax_cents])) puts format(class="s">" %-12s %-28.14f %s", class="s">"total", floats[class="y">:total], Money.new(ledger[class="y">:total_cents])) puts Agentic:class="y">:CapabilityValidator.new(CONTRACT).validate_outputs!(ledger) puts class="s">" the ledger version signed its contract: integer cents, all" puts class="s">" non-negative, and the adds_up rule verified to the penny." puts float_cents = (floats[class="y">:total] * 100).round puts class="s">" now read the float column like an accountant: the subtotal ends" puts format(class="s">" in ...%s, because 0.1 x 3 is not 0.3 in binary - IEEE 754", format(class="s">"%.14f", floats[class="y">:subtotal])[-6..]) puts class="s">" is already paying out interest. today it rounds to the" puts class="s">" right penny (#{float_cents}); at some other quantity or rate it won't," puts class="s">" and the discrepancy will surface in a reconciliation report" puts class="s">" eleven months from now, assigned to whoever touched the code" puts class="s">" last. the discipline is three sentences: money is integer" puts class="s">" cents (a TYPE the contract can enforce - \"integer\class="s">" isn't" puts class="s">" pedantry, it's a tripwire); rounding is a NAMED policy applied" puts class="s">" at declared points (banker's, at multiplication), not an" puts class="s">" accident of printf; and the books must balance BY RULE" puts class="s">" (adds_up), not by hope. take my money - but count it in cents."