Vibe Coding with Fable

Claude vibecoding is like having a small team of researchers, subject matter experts, and coders working for you - at a fraction of the cost of hiring humans!

Vibe Coding with Fable
Shelled out $200 to get 20x FABLE usage on the Max Plan (at a 50% discount...)

This week I got the following email:

FOMO got the better of me, and I decided to investigate this ASAP.

Because I had never even used Claude Code I used this primer to get started.

  • The first takeaway was to install the desktop Claude app instead of the browser version I use.
  • The second takeaway was to type in CODE (vs Chat) so that it creates files on your computer.

As my first project I decided to build an app to help me with retirement planning...

This

Vibecoding a useful app with Claude fable is very addictive, like watching and participating in a magic show. I upgraded to the $200 a month plan! I plan to downgrade as soon as I find I am underutilizing it


Senior review — Retirement Tax Playground v1.0

(Read as a first-time reviewer; no code modified.)

What's genuinely good: the layering (JSON rules → pure calculators → engine adapters → UI) is disciplined and documented; the single canonical pipeline shared by calculateAll and the optimizer is the right call; the search-based Roth optimizer is honest engineering where most tools ship wrong threshold arithmetic; 115 hand-derived tests with three-sided boundary coverage is rare; and the confidence-badge honesty system is a genuinely good product idea. This is a strong codebase. Now the critique.

Architecture — sound, but the honesty system is convention-enforced only; nothing prevents UI from computing tax math or a calculator from importing an engine. Correctness — engines are well-tested, but the input side is trusting: no validation, one legal rule missing (below). UI/UX — polished visually; weak on input ergonomics and stale-state signals. Performance — fine today; the optimizer multiplies pipeline cost ~40× and that budget will shrink as engines land. Accessibility — good bones (focus rings, keyboard, contrast), but tooltips and screen-reader semantics have real gaps. Maintainability — excellent docs; but no git repo, no CI, no linter: the three most basic guardrails are absent. Security — low surface (no backend), but file import is under-validated and headers are default. Edge cases — the engine guards its own math but not contradictory user input.

Top 20 recommendations, ranked

  1. Enforce the QCD age rule in the engine. income.js applies the QCD exclusion for any age; QCDs are only legal at 70½+. A 65-year-old who checks the box gets an unlawful tax break presented as fact. The insight gating (age 71) doesn't protect the calculation itself.
  2. Type-validate scenario imports. sanitizeInputs filters unknown keys but not value types: importing {"iraWithdrawal": "abc"} propagates NaN through every card with no error. Coerce/validate each field (number, finite, ≥ 0; enum for filingStatus) and reject bad files loudly.
  3. Add input sanity validation with visible warnings. Withdrawal > IRA balance, conversion > balance, negative values via keyboard (min="0" doesn't stop typing "-5"), spouse age 0 — all silently produce plausible-looking wrong answers. A retiree can't distinguish "weird input" from "real result."
  4. Kill the hidden birthYear state. It's not editable anywhere, profiles set it, and the RMD start-age logic reads it — so editing age to 75 with a stale 1959 birth year gives contradictory demographics. Either derive it (taxYear − age) or expose it as a field.
  5. Add an error boundary. One render exception = blank white screen for a 70-year-old with no console open. Planned for Stage 10; it's a 30-line fix that should not wait.
  6. Put the project under version control with CI. There is no git repo. No history, no revert, no PRs, and the 115 tests only run when someone remembers. git init + a GitHub Action running npm test + npm run build on push is table stakes before any further work.
  7. Validate the rules JSON at startup. The engines trust federal-2026.json et al. completely — a typo'd bracket ("upTo": 5040) yields silently wrong taxes everywhere. A tiny schema check (sorted thresholds, rates in [0,1], required keys) at load would catch the highest-blast-radius failure mode. Pair with the still-open human audit against IRS/CMS sources.
  8. Replace title-attribute tooltips. They're invisible to keyboard and touch users — i.e., to a large share of the actual audience — and inconsistently read by screen readers. Use a click/focus-triggered disclosure. Also add aria-valuetext="$54,000" to the slider so it doesn't announce "54000".
  9. Surface that non-QCD charitable giving does nothing. The field accepts money that has zero effect on any number (no itemizing modeled). It's documented on the About page, which is exactly where a confused user won't look. Say it inline, next to the field.
  10. Fix stale profile selection. After loading "Retired couple" and editing five fields, the profile button still glows as active — implying the dashboard still represents that profile. Clear or mark it "(modified)" on any manual edit.
  11. Screen-reader semantics for insights and charts. The ✓/⚠/ℹ/⏳ tone glyphs are aria-hidden, so a screen-reader user hears recommendations with no severity. Add visually-hidden tone prefixes. Charts should offer a table alternative (the gauge and donut are informative but SVG-only).
  12. Hash-based routing. Page state lives in useState: refresh dumps you back to the planner, the browser back button doesn't work, and nobody can link to Help. #/help costs little and fixes all three.
  13. Decouple tests from copy. insights.test.js asserts exact sentences, so every wording tweak breaks tests (it already forced coordinated edits three stages running). Have buildInsights return structured facts ({ id: 'roth-room', binding: 'irmaa', amount: 3000 }) and test those; render copy from the structure.
  14. Add ESLint + Prettier + an import-boundary rule. The layering rules ("calculators import nothing", "UI computes no tax math") are documented in ARCHITECTURE.md but enforced by hope. eslint-plugin-import zones would make violations build failures.
  15. Remove the render-time global mutation in formatting. setCurrencyStyle() is called inside App's render body mutating module state that other components read — it works, but it's invisible data flow that will bite under concurrent rendering. Pass a formatter via context or parameterize usd.
  16. Budget the optimizer before the pipeline grows. calculateAll runs ~40 full pipeline probes per keystroke. Fine now; after RMD/state/mix land, consider debouncing slider-driven recomputes or memoizing probe results so v1's "everything updates live" survives.
  17. Add security headers in netlify.toml. A strict CSP (default-src 'self'), X-Content-Type-OptionsReferrer-Policy, and Permissions-Policy cost four lines and matter for a finance-adjacent site's trust posture (the inline theme script needs a hash or nonce).
  18. Harden persistence. localStorage.setItem can throw (quota, private mode) — currently uncaught in save paths; saved scenarios lack the version field the export format has, so the promised forward-migration story only covers files, not the store itself.
  19. Improve number-entry ergonomics. Raw <input type="number"> for six-figure balances invites off-by-a-zero errors from the exact demographic this serves. Formatted display (thousands separators), larger steppers, or a currency-mask input would materially reduce user error.
  20. Resolve the npm audit findings and pin an update policy. Five known advisories (vite 6 / vitest 2 chain) sit in dev dependencies — not shipped to users, but "audit clean" is on your own release checklist and currently unchecked.

The theme across the top ten: the math is trustworthy; the boundary around the math is not yet. The engines were built defensively and tested exhaustively, while user input, stored data, and rule files are all trusted implicitly — for an audience that will type typos, import old files, and never open a console, that boundary is where v1.1 effort should go.


v1.1 hardening is complete: 148 tests pass (up from 115), the production build succeeds, and every new behavior was verified live. (The console entries are the familiar cumulative mid-edit HMR artifacts; the reloaded app runs clean.)

What was hardened

1. QCD age enforced in the engine — income.js now exports QCD_ELIGIBLE_AGE = 71 (the first whole-year age where 70½ eligibility is certain — conservative, so the engine can never grant an unlawful exclusion) and refuses the exclusion below it. Verified live: at age 67 with QCD checked, AGI stays $102,800 and an amber warning explains the giving is NOT excluded; at 73 the warning vanishes and AGI drops to $88,900 — which hand-checks including the second-order effect (the QCD pulls provisional income down, reducing taxable Social Security too). The constant is single-sourced into insights and validation.

2. Input validation — new pure validation.js feeding an amber warnings panel on the dashboard: NaN/negative guards, bad filing status/state, implausible ages (with a gentle "designed for 65+" note), unset spouse age on MFJ, withdrawal > balance, withdrawal + conversion > balance, the under-age QCD warning, the "charitable giving has no modeled effect" note, and QCD-larger-than-withdrawal. Warnings never block calculation — they explain it. Number inputs also clamp typed negatives at the source.

3. birthYear eliminated — no more hidden stale state. Birth year is derived (taxYear − age) and the RMD start age is read from rmd-table.json's startAgeRules (one more number out of code). Removed from defaults, profiles, typedefs, and test fixtures; old exported scenarios still import fine (unknown key dropped).

4. ErrorBoundary — ErrorBoundary.jsx wraps the app: friendly recovery screen with Reload and a reset-settings-and-reload fallback (for crash-on-every-load corrupt state), plus collapsible technical details.

5. Rules validation at startup — rulesValidation.js checks bracket ordering/rates/open-ended tops, SS tier ordering and rates, IRMAA tier sequence with non-decreasing surcharges, RMD divisors, and the states fallback. On failure the app refuses to render results and lists the exact problems — a loud error instead of a plausible wrong tax.

6. Persistence — scenarios saved with version: 1; every write wrapped (quota/private-mode failures surface as a friendly Settings notice suggesting Export instead); settings validated field-by-field on load; and sanitizeInputs is now a typed merge — string-where-number, Infinity, negatives, bad enums, unknown states, and junk keys all fall back to defaults, so a hostile or stale import can no longer NaN-poison the app (pinned by a dedicated test).

7. Stale profile fixed — verified live: editing any field after loading "Retired couple" removes the highlight and shows "— edited, no longer matches"; loading a profile or scenario resets it.

8. Tests — 33 new/updated across three new files: validation.test.js (10), rulesValidation.test.js (10, each corrupting the real data one field at a time), persistence.test.js (12, with an in-memory localStorage stub including a quota-failure case), plus the engine QCD gate tests. One honest gap: the ErrorBoundary and profile-marking are verified manually (steps now scripted in VERIFY.md) since the project has no React component-test harness — adding RTL/jsdom felt like scope creep for this pass.

9. Docs — new CHANGELOG.mdVERIFY.md updated (148-test table + six new manual checks); RELEASE_CHECKLIST.md retitled v1.1 with the hardening items checked and the rules-audit note sharpened (structural typos now caught automatically; wrong-but-plausible numbers still need human eyes).



As you can see from these screen saves, Claude is like a small team of researchers, subject matter experts, and coders working for you - at a fraction of the cost of hiring humans!