265 lines
12 KiB
Markdown
265 lines
12 KiB
Markdown
# 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`](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/`](../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:
|
||
|
||
```python
|
||
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:
|
||
|
||
1. **Diff** the published table against the anchor module.
|
||
2. **Update the meters AND bump `RATE_CARD_SOURCE_DATE` in the same edit.**
|
||
3. **Update the test pins in the same commit.**
|
||
4. **Re-execute** the notebook; re-pin the gate's live-state numbers if the
|
||
defaults moved.
|
||
5. **Report the cost moves honestly** — say what got more expensive.
|
||
6. **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:
|
||
|
||
```python
|
||
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_cost` beside 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 `$0` line 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 `$/unit` figure.** 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_VERBATIM` to reflect a negotiated rate.** That is
|
||
the overlay, and it belongs in `engagement-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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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
|
||
|
||
1. Copy `calculators/Genesys_Token_Calculator/` and rename it
|
||
`Vendor_Subject_Calculator` (underscores — directories are Python packages).
|
||
2. Rename the engine package; keep `staging.py` byte-for-byte.
|
||
3. 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.
|
||
4. Rewrite the `topic-bank` catalogue and reset `engagement-data` to
|
||
placeholders.
|
||
5. Register the notebook in [`tests/nbcheck.py`](../tests/nbcheck.py)
|
||
(`NOTEBOOK_FIRST`) — the completeness test fails until you do.
|
||
6. Run the full verification: `pytest`, `mypy`, headless `nbconvert --execute`,
|
||
`python scripts/export_report.py`, a stage simulation, and an actual
|
||
`mercury --working-dir .` render.
|