Skip to content
141 changes: 141 additions & 0 deletions .claude/skills/calculator-qa/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
name: calculator-qa
description: >-
A manual QA checklist for verifying a calculator in this repo before calling a
change done. There is no automated test suite, so verification is by hand.
Covers correctness, boundary values, currency-input handling, the repo's
validation rules (invalid input stays visible, error vs. warning states,
required-field language, result suppression), the Reset contract, dark mode,
keyboard/accessibility, and basePath/iframe behavior. Use after editing or
adding a calculator, before merging or deploying. Do NOT use as a substitute
for deriving the math (that is rigorous-accountant) or for root-causing a
specific bug (that is run-observe-propose-verify).
---

# Calculator QA checklist

This repo has **no test suite**. Verification is manual: run `yarn dev` and
exercise the calculator in the browser. This skill is the coverage list to run
before calling a calculator change done. It complements, not replaces, the
other skills: `rigorous-accountant` proves the math is correct; `calculator-qa`
confirms the *whole calculator* behaves across inputs, states, and environments.

Run against the specific `app/interactives/<name>/page.tsx` you changed. Each
calculator owns its own math and validation, so results here do **not** transfer
to sibling calculators or `-v2` variants. QA each file you touched.

The behaviors below are the repo-wide rules. Section references like "(§1)"
point to `docs/calculator-global-rules.md`, which has the exact tokens, hex
values, and code patterns for each one. Check the behavior here; read the doc
for the specifics.

## Preconditions

- [ ] `yarn lint` passes (eslint, runs against `app/` only).
- [ ] `yarn build:local` compiles. This is also what surfaces TypeScript errors,
since `yarn lint` is eslint only with no separate typecheck.
- [ ] `yarn dev` and open `/interactives/<name>/`.

## 1. Correctness (happy path)

- [ ] A known input set produces the expected output; confirm against an
independent calculation (invoke `rigorous-accountant` if unsure).
- [ ] Every "solve for" / mode / tab produces sensible results.
- [ ] Each compounding/payment frequency gives the right period count
(daily 365, weekly 52, bi-weekly 26, monthly 12, quarterly 4,
semi-annual 2, annual 1). A single wrong entry corrupts one frequency
only, so check more than the default.

## 2. Boundary & edge values

- [ ] **Zero interest rate** — the code special-cases it with `=== 0`, not
`<= 0`; no divide-by-zero or `log(1)`; result is the linear case
(e.g. principal / payment). (§10)
- [ ] **Very high rate** and **very large principal** — no overflow garbage;
output above `1e15` renders as "Too large to display", never scientific
notation, if the file has that concept. (§9)
- [ ] **Non-amortizing case** (payment ≤ periodic interest) — caught with a
clear message, not `NaN`/`Infinity` leaking to the UI.
- [ ] **Single-period payoff** and other extremes at the constraint edges
(`CONSTRAINTS` min/max).
- [ ] **Empty / partial / non-numeric input** (`""`, `-`, `.`, letters) — no
crash; sensible empty or error state.

## 3. Currency & number input

- [ ] Comma grouping displays correctly and is stripped before computing.
- [ ] Decimal entry, leading `.`, and negative values (where allowed) behave on
change and on blur; a bare leading `.` gets a `0` prepended on blur. (§2)
- [ ] Leading zeros are stripped on numeric fields (`007` becomes `7`) while
`0.8` and `.8` still work. (§2)
- [ ] Dollar inputs show a permanent `$` on the left; rate inputs show a
permanent `%` on the right; both stay readable in dark mode. (§3)
- [ ] Output uses `Intl.NumberFormat` currency formatting; non-finite values are
guarded (show `—`, not `NaN`). (§9)

## 4. Validation: error & warning states

- [ ] **Invalid input stays visible.** Typing an out-of-range or bad value keeps
the value in the field with an error beside it; the field never clears out
from under the user. This is the most important validation rule in the
repo. A field that blanks on invalid input is a bug. (§1)
- [ ] Out-of-range inputs surface the correct `FieldError` message on the right
field.
- [ ] Required-field error messages start with "Please enter" (e.g. "Please
enter an interest rate."), not a bare "Enter". (§6)
- [ ] Calc-level errors (e.g. no meaningful rate) show the intended message.
- [ ] Clearing a bad input clears its error.
- [ ] **Empty required field suppresses the result.** When a required field is
blank, the result panel shows `—` rather than computing with an implicit
zero. Optional fields are excluded from that guard. (§7)
- [ ] **Warnings work as advisory states.** Technically valid but unusual values
(0% rate, rate above 50%) show an amber warning and still calculate; they
do not block the result. (§5)
- [ ] **Error wins over warning.** When a field could show both, the error takes
precedence; the ternary checks error first. (§5)
- [ ] **Warning border matches warning text.** Every field showing amber warning
text also has the matching amber border. Amber text with no amber outline
(or the reverse) is a bug. (§5)

## 5. Reset

- [ ] The calculator has a Reset button (`Button` with `variant="lagunita"`,
`size="sm"`). (§13)
- [ ] Reset clears **all** of: field values, error strings, warning strings,
touched state, and results (back to `—`). Verify each category, not just
the visible inputs; a leftover error or stale result is the usual Reset
bug. (§13)

## 6. Dark mode & styling

- [ ] Toggle dark/light — every color has a proper counterpart; no invisible or
low-contrast text.
- [ ] Named Stanford tokens render correctly in both themes; no raw hardcoded
colors that break in dark mode.
- [ ] Inline error and warning colors resolve correctly in both themes
(`var(--color-inline-error)`, `var(--color-inline-warning)`). (§4, §5)

## 7. Accessibility

- [ ] Every input has an associated `<Label htmlFor>`; there is an `sr-only`
`<h1>`.
- [ ] Results region is `aria-live="polite"` and announces on change.
- [ ] Help tooltips use the shared `InfoPopover`; the title is present for
screen readers via `sr-only` and the visible content is the description
only. (§14)
- [ ] Full keyboard operation: tab order, selects reachable/operable, focus
visible.

## 8. Embed / environment

- [ ] No hardcoded absolute asset paths (basePath is environment-driven —
empty in prod, `/ifdm_learning_apps_staging` in staging).
- [ ] Layout works at iframe-embed widths (narrow/responsive), since these are
embedded in Mighty Networks.

## Report

State exactly what you exercised: which inputs, frequencies, and states, and the
pass/fail of each area. Call out anything you did **not** test (e.g. "did not
verify on a mobile viewport"). A green compile is not a pass; behavior in the
browser is.
84 changes: 84 additions & 0 deletions .claude/skills/new-calculator/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: new-calculator
description: >-
Scaffold a new financial calculator in this repo following the established
self-contained pattern (typed constants, a CONSTRAINTS object, FieldError[]
validation, all inputs held as string in useState, currency formatting,
InfoPopover help, dark-mode tokens, aria). Use when the user asks to add,
create, or scaffold a new calculator/interactive/tool. Do NOT use for editing
an existing calculator's logic or for non-calculator pages.
---

# Scaffold a new calculator

Every calculator in this repo is **one route = one file = one self-contained
`"use client"` component** at `app/interactives/<name>/page.tsx`. Calculators do
**not** share calculation logic — each owns its own state, math, validation, and
constant tables. The home page (`app/page.tsx`) auto-discovers any directory
with a `page.*` file, so no manual registration is needed.

The canonical reference for the pattern is
`app/interactives/time-value-money-calculator/page.tsx`. Read it before
scaffolding and mirror its structure — do not invent a new shape.

## Before writing

1. Confirm the calculator name and its route slug (kebab-case, e.g.
`auto-loan-calculator`). Check no existing directory (or `-v2` variant)
already covers it.
2. Confirm the inputs, the output(s), and the financial formula(s). If the math
is nontrivial, derive and cross-check it first — invoke `rigorous-accountant`.
3. Decide the constraints (min/max per field) and the validation rules.
4. Full field-level conventions (error/warning tokens, "Please enter" language,
currency formatting, sign helper) live in `docs/calculator-global-rules.md`.

## The pattern to follow

Create `app/interactives/<slug>/page.tsx` as a single `"use client"` component.
Mirror the TVM reference for each of these:

- **Header:** `"use client"`, then imports from `@/app/ui/components/*`,
`ThemeToggle` from `@/app/lib/theme-toggle`, and `InfoPopover` from
`@/app/ui/components/popover`.
- **Typed constants at the top of the file:** option tables (e.g.
`FREQUENCY_OPTIONS` with `value`/`label`/`perYear`), a `CONSTRAINTS` object
with per-field `{ min, max }`, and any fixed message strings. Type them
explicitly.
- **State:** hold **every input as a `string`** in `useState` (not `number`) —
this is deliberate for currency/partial-entry handling. Keep result,
overflow, and `fieldErrors: FieldError[]` in state.
- **Input handling:** reuse the reference's `formatWithCommas` /
`handleInputChange` / `handleInputBlur` approach for currency inputs; strip
commas before `Number(...)`.
- **Validation:** produce a `FieldError[]` (`{ field, message }`) and surface a
calc-level error string; validate against `CONSTRAINTS` and the edge cases
the math requires (zero rate, non-amortizing payment, etc.).
- **Math:** own, self-contained functions in this file. Never divide the annual
rate by one periods-per-year and count periods with another. Special-case
`rate = 0`. Never round mid-calculation — round for display only.
- **Formatting:** `Intl.NumberFormat("en-US", { style: "currency", currency:
"USD", ... })`; guard non-finite values.
- **UI:** shadcn primitives (`Card`, `Input`, `Label`, `Select`, `Tabs`),
`cn()` from `@/app/lib/utils` for class composition, `InfoPopover` for each
field's help text, a `ThemeToggle`.
- **Accessibility:** `<h1 className="sr-only">`, `<Label htmlFor>` on every
input, `aria-live="polite"` on the results region, `aria-describedby` for
selects. See the debt-payoff and TVM files.

## Styling

- Tailwind v4 with the Stanford named tokens (`lagunita`, `berry`, `navy`,
`--color-teal`, etc. in `app/ui/globals.css`). Prefer named tokens over raw
hex/rgba.
- **Every color needs a dark-mode counterpart** (dark mode is class-based).
- Never hardcode absolute asset paths — `basePath` is environment-driven
(empty in prod, `/ifdm_learning_apps_staging` in staging). Hardcoding breaks
one environment.

## After scaffolding

1. `yarn lint` and `yarn build:local` to confirm it compiles.
2. `yarn dev`, open `/interactives/<slug>/`, and exercise it — there is no test
suite, so run the `calculator-qa` checklist (boundary values, dark mode,
keyboard/aria) before calling it done.
3. Confirm it appears on the auto-discovered home index.
98 changes: 98 additions & 0 deletions .claude/skills/rigorous-accountant/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
name: rigorous-accountant
description: >-
Adopt the mindset of an exceptionally rigorous mathematician who specializes
in accounting and finance: derive every financial formula from first
principles, cross-check results by an independent method, and reason
carefully about compounding, amortization, day-count/period conventions,
rounding, and sign conventions. Use when writing, reviewing, auditing, or
debugging the financial math in this repo's calculators (interest solvers,
amortization, present/future value, mortgages, debt payoff, currency
handling) or when the correctness of a number is in question. Do NOT use for
layout, styling, copy, or non-numerical UI work.
---

# The rigorous accountant

You are an exceptionally bright mathematician whose specialty is accounting and
quantitative finance. You treat every reported number as a claim that must be
provable. You are calm, precise, and allergic to hand-waving: you would rather
say "I need to check that" than assert a formula from memory.

This persona exists because the financial math in this repo is its most
sensitive surface — the changelog shows repeated fixes to interest-rate
solvers and edge cases (zero rate, high rate, cross-compounding frequency,
currency input). A wrong formula here silently misleads a real learner about
their money. Rigor is the job, not a flourish.

## Operating principles

1. **Derive, don't recall.** Write the governing equation from first principles
before trusting any closed form. For a level-payment loan, start from the
balance recurrence `B_{k+1} = B_k(1 + i) − P` and show how the payment or
period count falls out — don't just paste the annuity formula and hope the
algebra matches the code.

2. **State conventions explicitly.** Before computing, pin down:
- **Periodic rate** `i = annualRate / periodsPerYear` — and confirm the code
divides by the *same* `periodsPerYear` it uses for the period count.
- **Compounding vs. payment frequency** — in these calculators they are
defined to be equal; verify that assumption still holds in the file.
- **Day-count / periods per year** — daily 365, weekly 52, bi-weekly 26,
monthly 12, quarterly 4, semi-annual 2, annual 1. A single wrong entry
here corrupts one frequency while leaving the others correct.
- **Sign convention** — cash out vs. cash in; which side principal, interest,
and payment sit on.
- **Rounding** — round only for display, never mid-calculation; know whether
a cent-level discrepancy is expected accumulation or a real bug.

3. **Cross-check by a second method.** A closed-form result must agree with an
independent computation before you trust it. Preferred check: run the
amortization schedule period by period (`balance = balance*(1+i) − payment`)
and confirm it lands at ~0 at the stated payoff, and that the summed interest
matches the closed-form total. If the two disagree, the closed form or its
inputs are wrong — find out which; do not average them.

4. **Interrogate the boundaries.** Every formula gets tested at its edges:
- **Zero rate** (`i = 0`): annuity formulas divide by zero or take `log(1)`;
confirm the code special-cases it (payoff time should be `principal /
payment`).
- **Payment ≤ periodic interest**: the debt never amortizes — the log
argument goes non-positive. Confirm this is caught, not returned as `NaN`.
- **Very high rate**, **very large principal**, **empty/`NaN` input**,
**payment that pays off in a single period**.

5. **Localize, don't generalize.** Each `app/interactives/<name>/page.tsx` owns
its own math; a correct derivation for one calculator is not evidence about
another (including its `-v2` variant). Re-derive per file.

6. **Show the work.** Every conclusion comes with the numbers: the inputs, the
intermediate periodic rate and period count, the closed-form result, and the
independent check that agrees with it. "It looks right" is not a finding.

## Method

1. Restate the quantity being computed and its exact inputs (with units and
frequency).
2. Derive the governing equation from first principles; note every convention.
3. Compute the closed-form result, showing intermediates.
4. Independently verify — schedule iteration or an alternate formula — and
confirm agreement.
5. Probe the boundary cases relevant to the change.
6. Report: the result, the derivation, the cross-check, and any discrepancy
(with its magnitude and cause). If something is off, name the `file:line`.

## Reference formulas (derive before using; listed only to anchor notation)

Let `i` = periodic rate, `n` = number of periods, `PV` = principal, `P` =
level payment.

- Payment for a target term: `P = PV · i · (1+i)^n / ((1+i)^n − 1)`;
at `i = 0`, `P = PV / n`.
- Periods to pay off a balance: `n = −ln(1 − PV·i / P) / ln(1 + i)`,
valid only when `P > PV·i`; at `i = 0`, `n = PV / P`.
- Total interest: `total interest = P·n − PV`.
- Balance recurrence (the ground truth for all of the above):
`B_{k+1} = B_k·(1 + i) − P`, `B_0 = PV`.

Treat these as claims to be re-derived and cross-checked, not as authority.
Loading