How it works
The parts that would embarrass me if you found them yourself
Written for someone who will open the source. Everything below is a design decision with a stated reason, including the places this prototype is deliberately less than a product.
01
The grounding contract
The language model never produces a number. It selects typed analysis tools, and every figure it reports is recomputed by a deterministic verifier before it reaches the screen. That single constraint is the difference between a demo and something a club general manager can act on.
An analysis tool is a pure function of (params, dataset) → Evidence. Purity is the point: it makes each tool unit-testable in isolation, makes the verifier’s recomputation meaningful, and guarantees that the same question asked a month apart returns the same figure. No tool may call a model. The registry is also the assistant’s entire capability surface — a question that cannot be expressed as a call into it is a question the assistant declines rather than improvises.
02
Citation format
Every figure in a narrative must be written as [[e:<evidenceId>|<figure>]], where the evidence id is the sixteen hex characters the tool returned. The renderer turns those markers into the chips you can click; the verifier turns them into recomputation jobs.
The evidence id is itself deterministic: a SHA-256 of the tool name, its version and its canonicalised parameters. Two identical calls produce the same id, which is what lets a receipt be re-derived rather than merely retrieved.
Any figure left bare in the prose is reported as undeclared and the whole narrative fails. The exemption list is enumerated rather than heuristic — bare four-digit years and small ordinals such as “the top 3” — because a permissive allowlist here would quietly defeat the entire verifier.
03
The verifier recomputes, and fails closed
For each citation the verifier looks up the evidence record, re-executes the named tool from the parameters stored on that record, and compares the fresh result to what the narrative claims. Recomputation rather than trusting the evidence payload carried through the request is what closes the loop: a tampered or stale evidence object cannot pass.
Failure modes are named individually, not collapsed into one red flag: mismatch, unknown-evidence, recompute-failed, unsupported-shape, undeclared. Tool version drift is treated as a failure rather than compared across versions, because a figure computed by v1.0 and checked against v1.1 proves nothing either way.
What the verifier does not claim. It does not prove the tool logic is correct — that is what the tools’ own unit tests are for. It does not prove the model chose the right tool for the question — that is what the eval suite measures. Three different guarantees, three different mechanisms. Any system claiming one gate covers all three is overselling. Watch it block a fabricated figure.
04
Rounding-aware comparison, and why it exists
A naive verifier fails the moment a model writes “$1.2M” for 1,241,880 — technically a mismatch, journalistically correct. Rejecting that trains the model toward unreadable prose full of unrounded cents. Accepting any nearby number lets real fabrication through.
The resolution is to infer the precision the writer claimed from how the number is written, then accept the figure only if the true value lies inside the interval that rounds to it. “$1.2M” asserts a value in [1.15M, 1.25M). “1,241,880” asserts a value in [1,241,879.5, 1,241,880.5). Both are honest at their stated precision, and both are checkable.
The useful consequence: counts written at full precision demand an exact match. “47 members” must mean 47.
05
Churn scoring is arithmetic, not a model
A churn score decides where a club spends retention effort and eventually who gets a phone call from the general manager. It has to be reproducible on Tuesday and Thursday, defensible to a board committee, and decomposable when a member disputes it. Language models are none of those things.
So the score is a versioned, unit-tested weighted sum — currently v1.2.0 — where each signal contributes signed points carrying the plain-English reason that produced it. Baselines are per member rather than club-wide: a social member visiting twice a month has not disengaged, and comparing them to a full-golf cadence would generate noise that erodes trust in the whole feed.
Every tool that touches risk is evaluated as at 2026-08-31, the last date the data covers, rather than against the wall clock. Two reasons, and both matter: the verifier re-runs a tool minutes or days later, so a wall-clock window would drift and flag a true figure as fabricated; and asking “who is at risk today” of a closed historical record is a question with no honest answer.
The general lesson, and the most useful thing I have learned shipping AI features: decide what genuinely needs a model, and refuse to use one everywhere else. It is cheaper, faster, and it is the part that keeps working when the provider has an incident.
06
What replay mode is, and what it is not
No ANTHROPIC_API_KEY is configured in this deployment, so the assistant runs in replay mode.
Real on every request: the tool calls execute against the dataset, the evidence records — values, methods, row ids — are computed fresh, the figures rendered into the prose are the values those tools actually returned, and the groundedness verifier runs its full recomputation pass over the result. If a tool’s logic changed and a figure moved, the scripted narrative would be blocked exactly as a model’s would.
Not real: the wording. Which tools to call and how to phrase the answer are fixed in advance rather than chosen by a model. With a key set, the identical pipeline runs with Claude choosing the tools and writing the prose, and it must satisfy the same citation contract to render at all.
That boundary is stated on the assistant page itself rather than buried here, because a reviewer who discovers an undisclosed limitation stops believing the disclosed parts too. The same recording machinery is also what gives the eval suite byte-identical determinism, so it earns its place beyond the demo.
07
Every row of this data is synthetic
Windermere Hills Country Club does not exist. The dataset is generated from a fixed seed and covers 1 Sep 2024 to 31 Aug 2026: 420 members, 62,553 transactions, 47,451 facility visits, 104 events, 9,397 registrations and 1,244 free-text staff notes.
It is behaviourally realistic on purpose — genuine seasonality, real churn patterns, and a small number of planted anomalies so the insight engine has something true to find. Because the seed is pinned, every run of the demo, every recorded transcript and every eval baseline see byte-identical rows.
No real club, member or financial data is used anywhere in this application, and nothing here is confidential to anyone.
08
Limitations, stated rather than discovered
- The assistant’s wording is scripted. With no model API key configured, tool selection and phrasing come from recorded transcripts. The suggested-question chips are the honest surface for that: a free text box that silently understood only five sentences would be the most dishonest thing this prototype could ship.
- Action state is client-side only. Confirmations and rejections live in React state in your browser tab and vanish when you close it. Nothing is written to a server, no message is sent, and no member record is touched. The audit entries themselves are built by the same action definitions a real deployment would run — only the persistence is thrown away.
- Single tenant, no authentication. One club, no accounts, no roles, no row-level access control. A real deployment needs all of it; none of it would demonstrate anything about grounding, so none of it is here.
- The eval suite is a golden set, not a corpus. It covers the failure modes worth proving — fabrication, out-of-coverage refusal, rounding — at the scale a prototype justifies. Real coverage is measured in hundreds of cases with per-case cost and latency budgets.
- Forecasting is out of scope in this build. The seasonal projection described in the original spec is not implemented here. It is listed because an unimplemented feature quietly dropped from a spec is the same class of problem as an unverified figure.