Continuity Guards
Continuity Guards
Section titled “Continuity Guards”Provenance. Extracted 2026-07-01 from
TanStack-migration.mdon 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
(
activeWorkersMap,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
beforeunloadguard 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 sendcredentials: 'include'(auth-context.tsx). - CONT-06 — Reference data reaches every store. Statistics + disease metadata reach
every per-tab store through
atomWithQueryover 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 servesindex.html— the repo ships no_redirectsfile). - CONT-10 — RETIRED.
VITE_ENABLE_PRICINGwas 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 theFeedbackWidget/Draggableprocess.envVitedefine.) - CONT-14 — Gates green. Pre-commit (Biome + related Vitest) and full
pnpm teststay 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.
2. Durable pitfalls
Section titled “2. Durable pitfalls”queryClientAtommust be hydrated in tests that exerciseatomWithQuery. An un-hydratedqueryClientAtomorphans 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()orcrypto.randomUUID()at import time — seed time/identity inside atoms or functions (thecurrentYearAtompattern, seeded fromnew 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-scopeduseState/useRef/empty-depsuseEffectfrom leaking across stores. Any restructure that changes provider mounting must preserve it — seedocs/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.randommocking 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).