Skip to content

Continuity Guards

Provenance. Extracted 2026-07-01 from TanStack-migration.md on its deletion. The TanStack (Router/Start) migration is cancelled permanently — the app stays React 19 + Vite. What supersedes it as direction: a strictly behavior-preserving four-layer restructure (shared → core → per-section features → app) whose north star is that the forecasting core stays headlessly drivable — a future agent/MCP adapter must be able to create a store → load a snapshot → set values → read outputs with no UI. This file keeps only the framework-independent survivors of that plan: the continuity checklist, durable pitfalls, and locked decisions that still bind. All TanStack vocabulary is intentionally gone.

The regression net is the existing test suite (run pnpm test for the current count; it was ~496 at extraction) plus the guards below. A restructure that keeps the suite green and every guard below true has, by definition, changed no behavior. Verify all guards on a preview deploy before declaring any structural change complete.

1. Continuity guards (each = a behavior that must keep working)

Section titled “1. Continuity guards (each = a behavior that must keep working)”

IDs are retained from the source for traceability. Each is a verifiable behavior, not an implementation.

  • CONT-01 — DOCX export is structure-stable. Per-scenario and Comparison DOCX exports produce the same document structure for identical input. Pipeline: collectReportData → captureChartPngs → buildDocxReport → download (docx-helpers.ts, exportComparisonReport).
  • CONT-02 — Monte Carlo worker contract. >1K iterations run off the main thread; each scenario store owns exactly one worker that terminates on tab close (activeWorkers Map, terminateWorkerForStore); ≤1K runs on the main thread.
  • CONT-03 — Multi-scenario isolation. Each tab owns an isolated store; dirty tracking, snapshot create/load, close-with-undo, and the beforeunload guard all stay per-scenario (ScenarioProvider, isDirtyAtom, useBeforeUnloadGuard).
  • CONT-04 — Comparison view + export. With 2+ scenarios the Comparison view renders sales / MC / model totals / tornado, each with per-section Image + CSV export (ComparisonPage, collectComparisonData).
  • CONT-05 — Auth flow. JWT-cookie auth unchanged: session via /api/check_session, unauthenticated redirect to ${VITE_BIOLOUPE_URL}/login, all requests send credentials: 'include' (auth-context.tsx).
  • CONT-06 — Reference data reaches every store. Statistics + disease metadata reach every per-tab store through atomWithQuery over one shared QueryClient (queryClientAtom, statisticsQueryAtom, referenceRowsAtom).
  • CONT-07 — Snapshot round-trip. Rails-persisted snapshots round-trip; currentModelAtom / isDirtyAtom / create-load semantics unchanged (model-atoms.ts, FullSnapshot).
  • CONT-08 — Sentry survives build reshape. Error + tracing instrumentation and hidden-source-map upload with correct release tagging survive any change to build output shape (instrument.ts, @sentry/vite-plugin).
  • CONT-09 — Deploy + previews. Cloudflare Pages deploy and per-branch previews keep working; deep-link refresh serves the SPA (deploy.yml; Cloudflare Pages’ automatic SPA fallback serves index.html — the repo ships no _redirects file).
  • CONT-10 — RETIRED. VITE_ENABLE_PRICING was removed; Pricing is unconditional. Kept only so cross-references to the deleted plan resolve.
  • CONT-11 — Theming. Tailwind theming, Radix/shadcn primitives, dark mode, bioloupe.gold, and media-query variants unchanged.
  • CONT-12 — CSV ingestion. CSV import + statistics ingestion unchanged (lib/export-csv.ts).
  • CONT-13 — App-root shells stay mounted. Sonner toast, the draggable FeedbackWidget, and dev-only jotai-devtools remain mounted at the app root. (See the CLAUDE.md gotcha on the FeedbackWidget/Draggable process.env Vite define.)
  • CONT-14 — Gates green. Pre-commit (Biome + related Vitest) and full pnpm test stay green. (Covered in depth by CLAUDE.md — listed here for completeness.)
  • CONT-15 — No perf regression. No >10% regression in initial paint, view switch, or section render, measured on a preview deploy.
  • queryClientAtom must be hydrated in tests that exercise atomWithQuery. An un-hydrated queryClientAtom orphans the query and the derived atom never resolves. This is also the seam a headless adapter uses to warm the cache before reading derived atoms.
  • Keep module scope side-effect-free and deterministic. No new Date() or crypto.randomUUID() at import time — seed time/identity inside atoms or functions (the currentYearAtom pattern, seeded from new Date().getFullYear() and overridable in tests). Import-time side effects break deterministic/headless testing of the core.
  • Multi-store state-leak bug class. The key={activeTabId} remount on the scenario <Provider> is what prevents component-scoped useState/useRef/empty-deps useEffect from leaking across stores. Any restructure that changes provider mounting must preserve it — see docs/ARCHITECTURE.md §4.

3. Inherited locked decisions & anti-goals (still binding)

Section titled “3. Inherited locked decisions & anti-goals (still binding)”

Locked decisions (LD1–LD4) — moved to their canonical home. The four locked decisions — LD1 (one-way layering; UI-free compute core), LD2 (one query definition per server resource), LD3 (the core stays headlessly drivable), LD4 (client SPA; Rails owns server logic) — now live in docs/ARCHITECTURE.md §3 “Layering Contract (LD1–LD4)”, which holds the normative text. This file no longer restates them. Machine enforcement: .dependency-cruiser.cjs via pnpm lint:boundaries (CI), mirrored by biome.json overrides as an editor-time signal.

Anti-goals (out of scope even when they seem natural)

  • No E2E (Playwright/Cypress), no visual-regression/screenshot tests, no Storybook.
  • No property-based testing (fast-check) unless example tests miss real bugs.
  • No global Math.random mocking in tests — Monte Carlo uses seeded RNG; a global mock shifts consumption order and invalidates year-N fixtures.
  • No DOCX/PPTX pipeline rewrite — adapt file paths only.
  • No speculative math changes — only bug fixes found under test (see the CLAUDE.md math-change invariant).
  • Land large restructures on a feature branch; merge only when the full suite is green.

Supply-chain posture (pointer, not re-documented). Exact pins + pnpm-workspace.yaml hardening (release-age cooldown, strict dep builds, empty savePrefix, allowBuilds allowlist, CVE floor-pins) + scan-before-regenerate discipline. Canonical detail lives in docs/SECURITY.md and the check-package-policy.mjs pre-commit gate (CLAUDE.md).