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






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
- Enforce the QCD age rule in the engine.
income.jsapplies 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. - Type-validate scenario imports.
sanitizeInputsfilters unknown keys but not value types: importing{"iraWithdrawal": "abc"}propagatesNaNthrough every card with no error. Coerce/validate each field (number, finite, ≥ 0; enum for filingStatus) and reject bad files loudly. - 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."
- Kill the hidden
birthYearstate. 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. - 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.
- 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 runningnpm test+npm run buildon push is table stakes before any further work. - Validate the rules JSON at startup. The engines trust
federal-2026.jsonet 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. - 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 addaria-valuetext="$54,000"to the slider so it doesn't announce "54000". - 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.
- 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.
- 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). - 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.#/helpcosts little and fixes all three. - Decouple tests from copy.
insights.test.jsasserts exact sentences, so every wording tweak breaks tests (it already forced coordinated edits three stages running). HavebuildInsightsreturn structured facts ({ id: 'roth-room', binding: 'irmaa', amount: 3000 }) and test those; render copy from the structure. - 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-importzones would make violations build failures. - Remove the render-time global mutation in formatting.
setCurrencyStyle()is called insideApp'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 parameterizeusd. - Budget the optimizer before the pipeline grows.
calculateAllruns ~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. - Add security headers in
netlify.toml. A strict CSP (default-src 'self'),X-Content-Type-Options,Referrer-Policy, andPermissions-Policycost four lines and matter for a finance-adjacent site's trust posture (the inline theme script needs a hash or nonce). - Harden persistence.
localStorage.setItemcan throw (quota, private mode) — currently uncaught in save paths; saved scenarios lack theversionfield the export format has, so the promised forward-migration story only covers files, not the store itself. - 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. - Resolve the
npm auditfindings 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.md; VERIFY.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!