12 KiB
Calculator Pattern v1.0.0
Standardizes Calculators — Palladium masters that price a vendor's product from that vendor's own published rate card: a verbatim rate-card anchor, a client overlay in tagged cells, and a re-anchoring protocol for when the vendor republishes.
🐾 Red Panda Approval™
This pattern follows Red Panda Approval standards (see CLAUDE.md for the
rubric).
Audience note: written to be loaded whole as context by an LLM agent
building or modifying a Calculator. Rules are imperative (MUST/SHOULD/NEVER),
each with a one-line why. It specializes
Mercury_Notebook_Pattern_V1-00.md —
read that first; the shared mechanics (reactivity, widget-pairs, gate,
staging, appendix, packaging) are not restated here. CLAUDE.md is the
always-on contract and takes precedence where documents disagree.
Reference implementation:
calculators/Genesys_Token_Calculator/.
What a Calculator is — and why it's a third kind
| Study | Assessment | Calculator | |
|---|---|---|---|
| Anchored to | a dated publication | — (it is the instrument) | a living rate card |
| Naming | YYYYMM_… dated |
Instrument_Name undated |
Vendor_Subject_Calculator undated |
| Anchor lifetime | immutable forever | n/a | immutable until the vendor republishes |
| Its "numbers" | NPV / ROI / payback | status & progress | run-rate, $/unit, per-feature cost |
| Lives in | studies/ |
assessments/ |
calculators/ |
The distinguishing property is the anchor's lifetime. A Study's anchor is
frozen forever — the December 2025 Forrester PDF will never change, so a dated
directory name is honest. A Calculator's anchor is frozen until the vendor
republishes, at which point updating it is a deliberate, test-breaking,
dated act. A YYYYMM_ prefix would start lying the moment the vendor updated
their page, so Calculators are undated; the publication date lives inside
the anchor (RATE_CARD_SOURCE_DATE), pinned by a test and shown on stage.
Note. Mercury Pattern Variant 3 — Exploratory calculator describes the pre-master form of this: a what-if surface inside a study. This pattern promotes it to a first-class master type. A Variant-3 notebook that outlives its study should graduate into
calculators/.
Required Structure & Contracts
Everything the Mercury pattern requires, plus the four below.
1 · The rate card is a verbatim anchor, and it lives in .py
CLAUDE.md says content never lives in .py. A published vendor rate card is
not content, and the boundary is who authors and edits the thing:
In
.py(the engine): the vendor's published rate card — verbatim, with a source URL and a source date. A third-party record nobody in this repo authors. In tagged notebook cells: everything the consultant authors or the client supplies — volumes, headcount, licence model, the negotiated rate, feature enablement, adoption assumptions, scenario definitions, and the narrative.
Why: the content rule exists because "the notebook is the document the
consultant edits". Nobody edits the vendor's rate card — editing it is
forbidden, and changing it requires a republication plus the re-anchoring
protocol below. Putting an immutable anchor on the surface designated as
editable invites exactly the casual edit the anchor rule prevents, and makes
the most-likely-wrong numbers checkable only via a notebook parse. Precedent:
studies/202602_TEI_Amazon_Connect/teicalc/anchor.py holds Forrester's
published tables the same way.
Corollary that has teeth: the contracted overlay is client data and
belongs in the engagement-data cell, never in .py. A Calculator's engine
MUST ship no client volumes at all — no defaults.py full of real sites.
(studies/202607_CTM_GenesysCX/tokencalc/defaults.py does exactly that; it is
the anti-precedent, not the model.)
The anchor module MUST carry:
RATE_CARD_SOURCE: str # the URL
RATE_CARD_SOURCE_DATE: str # the publication's own "last updated" date
METERS_VERBATIM: tuple[Meter, ...] # feature wording AND rate string, exact
Each meter keeps the vendor's exact wording (feature) and exact rate
string (published_rate) beside the parsed number. Why: the string is
what makes a transcription slip catchable — a test pins the strings, and a
second test proves the float still agrees with the string beside it.
2 · The topic-bank cell holds the catalogue, not the rates
Since the rates are in the engine, the required topic-bank cell holds what a
consultant genuinely authors: the feature catalogue — per capability, a
client-facing title, a one-line description, and the sizing question to
ask. Each key MUST match a published meter key, pinned by a test. Why:
this keeps the tag taxonomy honest rather than tag-stuffed, and the cross-layer
pin catches a rename that would otherwise silently unprice a feature.
3 · Costs the vendor bills outside the primary meter are a separate line
A vendor usually bills something adjacent on a different basis (here: Enhanced TTS, per character rather than per token). Such a line MUST have its own source URL and date, its own confidence flag, and its own subtotal — and the grand total MUST show the components labelled, never one blended figure. Why: the headline is understated without it and wrong with it silently folded in; a reader must be able to see which meter produced which dollar.
4 · Every published rule that changes the arithmetic is modelled and reported
Vendors bury billing rules in prose. Each one that moves the number MUST be
enforced in the engine, pinned with hand-checked arithmetic, and surfaced in
the result (a warnings tuple), not applied silently. Why: a total that
shrank for a published reason nobody can see is indistinguishable from a bug.
In the reference implementation these are the per-call round-up, the free
monthly allowance, the highest-tier rule, and the Copilot exclusion.
The Re-anchoring Protocol
The section unique to this kind. A Calculator tracks a living rate card, so
updating the anchor is expected — but never casual. Re-anchoring is
Show-first (CLAUDE.md § Risk tier), unlike editing a Study anchor, which
is forbidden outright.
When the vendor republishes:
- Diff the published table against the anchor module.
- Update the meters AND bump
RATE_CARD_SOURCE_DATEin the same edit. - Update the test pins in the same commit.
- Re-execute the notebook; re-pin the gate's live-state numbers if the defaults moved.
- Report the cost moves honestly — say what got more expensive.
- Show the user the rate diff and the KPI move before it lands.
Never partially re-anchor. Never bump the date without the pins. A date that claims a rate card the numbers don't match is worse than a stale one, because it is stale and it lies about it.
Standard Choice Values
Beyond the Mercury pattern's confidence legend (🟢 confirmed / 🟡 estimated / 🔴 unknown), Calculators standardize:
class Confidence(Enum): # 🟢 requires source_url AND source_date — enforced
CONFIRMED = "confirmed"
ESTIMATED = "estimated"
UNKNOWN = "unknown"
class LicenceModel(Enum): # vendors price per-seat features by licence model
NAMED = "named"
CONCURRENT = "concurrent"
A 🟢 meter that carries no source URL and date MUST fail to construct. Why: it is the one invariant that stops an unsourced number wearing a green tick on a client-facing stage.
Recommended Practices
- The list → negotiated walk. Carry
list_costbeside the effective cost and show both. The client sees MSRP → their deal explicitly (the Mercury pattern's deck-frame column, applied to pricing). - Published zeroes are findings. A feature the vendor publishes as free
renders as an explicit
$0line with the quoted wording — not an omission. - Adoption as a range. Adoption is the largest uncertainty in any consumption estimate; ship conservative / base / aggressive rather than a single point.
- A
$/unitfigure. Cost per interaction is the number a client remembers and the one that survives a change of volume assumptions.
Anti-Patterns
- ❌ Don't build a business case in a Calculator. NPV, payback, ROI, a benefit model, a P&L — any of these means the work has become a Study. Move it, don't grow it here.
- ❌ Don't edit
RATE_CARD_VERBATIMto reflect a negotiated rate. That is the overlay, and it belongs inengagement-data. The anchor is what the vendor published, not what the client pays. - ❌ Don't bump the source date without updating the pins (see the protocol).
- ❌ Don't ship a rate without a source URL and a confidence flag — the schema should make this impossible.
- ❌ Don't put engagement volumes in the engine. Masters stay client-clean; an engine with real client sites in it cannot be shared.
- ❌ Don't fold a differently-metered cost into the headline meter's subtotal — separate line, separate source, labelled components.
- ❌ Don't model a per-unit round-up by averaging first. Round per billing event, then sum; averaging first understates short events, sometimes by multiples.
Testing
The Mercury pattern's layers 0–4, plus layer 1a — the rate-card tripwire, which is the highest-value test in a Calculator:
def test_published_meter_table_verbatim():
"""Exact feature wording AND exact rate string, every row.
A transcription slip or an un-noticed republication breaks the build
rather than quietly moving a client-facing number.
"""
assert {m.feature: m.published_rate for m in METERS_VERBATIM} == PUBLISHED
def test_source_pinned():
assert RATE_CARD_SOURCE_DATE == "2026-07-12"
def test_numeric_rates_match_their_published_strings():
"""Catches a float drifting away from the wording beside it."""
def test_every_confirmed_meter_carries_source_url_and_date(): ...
Then hand-check the arithmetic before pinning it — compute each published rule's effect by hand, write the working into the test's docstring, and pin the result:
def test_voice_bot_15_second_roundup():
"""1,000 calls at a 40s average.
ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month.
The naive 40/60 × 1,000 = 666.67 understates by 12.5%.
"""
assert voice_bot_billable_minutes(VoiceBotUsage(1_000, 40)) == pytest.approx(750.0)
The in-notebook gate additionally asserts structural ties that hold at any widget setting — the allowance identity, effective ≤ list, components summing to the grand total, and each published exclusion — so a stakeholder moving a slider cannot produce an incoherent number.
Adding a Calculator
- Copy
calculators/Genesys_Token_Calculator/and rename itVendor_Subject_Calculator(underscores — directories are Python packages). - Rename the engine package; keep
staging.pybyte-for-byte. - Replace the anchor module with the new vendor's published table — verbatim, with its own source URL and date — and rewrite the rate-card tripwire against it.
- Rewrite the
topic-bankcatalogue and resetengagement-datato placeholders. - Register the notebook in
tests/nbcheck.py(NOTEBOOK_FIRST) — the completeness test fails until you do. - Run the full verification:
pytest,mypy, headlessnbconvert --execute,python scripts/export_report.py, a stage simulation, and an actualmercury --working-dir .render.