Files
palladium/docs/Calculator_Pattern_V1-00.md

12 KiB
Raw Blame History

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:

  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:

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.


  • 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 04, 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

  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 (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.