Imported from Vinay-ops/LearNova (
AGENTS.md). Install upstream withnpx skills add Vinay-ops/LearNova. Copyright stays with the author.
AGENTS.md — Learnova
Database / test lifecycle (footgun)
- pytest's autouse
setup_databasefixture inbackend/tests/conftest.pydrops all tables on the shared dev DB (backend/test_casepilot.db). After running tests, any running dev backend breaks (tables gone, andalembic_versionis gone too, soalembic upgrade headis a silent no-op). - Rebuild recipe: kill uvicorn on :8000 →
cd backend && rm -f test_casepilot.db && python -m alembic upgrade head→ restart uvicorn. alembic checkonly reports clean when the DB was built by migrations; a pytestcreate_allDB (no alembic_version stamp) makes it report every table as "new upgrade operations". That is expected noise, not drift.- There is no SQLite separate from dev: the app and tests share
backend/test_casepilot.db(gitignored;backend/supabase_schema.sqlis the manual Supabase bootstrap).
Answer-key contract (assessment/quiz security)
GET /api/assessments/{id}/questionsis deliberately sanitized:correct_option_index,correct_answer,explanationare null even though the keys exist in the JSON schema. It feeds quiz taking.- The key is revealed ONLY by
GET /api/assessments/attempts/{id}/review, gated on ownership +status == completed(422 while in progress, 403 for other users). - The FE must never compute or send
is_correct/points_earned— the server gradesselected_option_indexagainst the stored key and overrides any client claim (MCQs only). Results page must use the review endpoint, not questions+answers reconstruction.
Auth API shape (non-obvious)
POST /api/auth/logintakes JSON{email, password}(not OAuth2 form data). Signup requiresfull_nametoo.- Frontend stores the JWT in
localStorage["access_token"];AuthContextrehydrates via/api/auth/meand exposes camelCase fields (user.name,profile.experienceLevel,targetFirms,interviewDate) as getters over snake_case backend fields — do not look for those keys in raw API responses. - Rotating
JWT_SECRETinvalidates all existing sessions — expect users to re-login.
FastAPI route-order shadowing
- In
backend/app/api/{cases,assessments,drills}.py, static GET routes (e.g./attempts) MUST be declared before parameterizedGET /{id}, or the static path matches/{id}and 404s. The list functions under shadowed routes were never executed until the reorder — they had missing model imports that only surfaced then. Check the same pattern when adding routes.
Error-code mapping (custom AppError)
ValidationError→ 422,AuthorizationError→ 403,NotFoundError→ 404,AuthenticationError→ 401,AIError/AIValidationError→ 502. Tests assert these exact codes.
Quiz + learning architecture (reused tables, no migration)
- AI-generated quizzes persist as
assessmentsrows (title"<topic> Quiz · <difficulty>",category= topic,skill_tagper question = subtopic) — the existing attempts/answers/scoring stack is reused;alembic checkstays clean. - Tutor sessions reuse
ai_sessions/ai_messageswithsession_type='learning'; topic/learner_level live inmetadata_. Chat history passed to the LLM is the last 12 messages, each truncated (user 800 chars, tutor 1500). POST /api/quizzes/generate: question_count 3–10 (UI offers 3/5/10; pydantic and service both enforce), difficulties Easy/Medium/Hard. Invalid LLM questions regenerate in bounded rounds (max 2) instead of failing.
Frontend prod-only rendering bug class
- Route pages mount inside a framer-motion wrapper; on minified production builds the enter animation can never fire, leaving content in the DOM at
opacity: 0(blank screen). Dev server masks it. Fix already applied:initial={false}on the wrapper insrc/main.tsx; keep it. - To reproduce prod-only issues:
npm run buildthen servedist/(e.g. a scratch node server on :5198 that proxies/api/*→ localhost:8000) and test in a real browser. Plain static serving ofdist/is the closest local proxy to the Vercel single-project setup.
Vercel/deployment topology
VITE_API_URLmust stay empty/unset in Vercel — the api-client uses same-origin/api/*andvercel.jsonrewrites to the FastAPI service. Never set localhost.- Supabase
DATABASE_URLmust be the Session pooler host (aws-0-<region>.pooler.supabase.com:6543, IPv4) — Vercel Functions are IPv4-only and can't reach Supabase's IPv6 direct-connection host (2406:/db.<ref>.supabase.co:5432), failing with "Cannot assign requested address". Add?sslmode=require; plainpostgresql://URLs are normalized to psycopg v3 inapp/db/database.py. - New tables on Supabase are applied manually via
backend/supabase_schema.sql(stamps alembic to0002_remaining_tables) oralembic upgrade headagainst the pooler URL.
Windows / Git Bash quirks (dev machine)
kill <pid>does not stop Windows processes from Git Bash — usetaskkill //PID <pid> //F(double slashes). Find listeners withnetstat -ano | grep ":<port>" | grep LISTEN.- Windows
pythonin heredocs writes/tmp/...to a literalC:\tmp\..., which differs from Git Bash's/tmp— keep temp files and tokens in the project dir or bash-written/tmp. - Never use
nulredirects; POSIX syntax only.
Verification commands
- Backend:
cd backend && python -m pytest -q(70 tests; quiz/learning tests inbackend/tests/test_learning_quiz.pyuse a fake LLM — no network). - Frontend:
npx tsc -bthennpm run buildfrom repo root. - LLM features return stub content locally until
GROQ_API_KEYis set — that is the expected fallback behavior (get_llm_client→StubLLMClient), not a bug. - Env is consolidated to 6 vars: backend
DATABASE_URL,GROQ_API_KEY,JWT_SECRET,FRONTEND_URL,ENVIRONMENT; frontendVITE_API_URL. Everything else is a code constant — Groq endpoint/model inbackend/app/ai/client.py(GROQ_BASE_URL/GROQ_MODEL), log level inapp/core/logging.py(LOG_LEVEL), JWT alg/expiry +LLM_TEMPERATUREasSettingsdefaults. There are no OpenRouter vars or client code (it was removed; only a stale pytest-cache nodeid remains). - Groq model availability changes often:
llama-3.3-70b-versatilewas decommissioned 2026-08-16. Verify ids at console.groq.com/docs/models before editing theGROQ_MODELconstant. - AI provider is Groq via its OpenAI-compatible endpoint, called through the
openaiSDK pointed atGROQ_BASE_URL(there is no Groq SDK dependency).GROQ_BASE_URL/GROQ_MODELare code constants inbackend/app/ai/client.py;models/prompt.py,db/seed.py,BaseLLMClient.DEFAULT_MODEL, andGroqLLMClient.DEFAULT_MODEL_FALLBACKall import them, so a model swap is a one-line edit. A per-promptmodelfield (prompt registry /promptstable) still overrides the constant per request, so individual prompts can be re-pointed without a redeploy. Voice/TTS/STT never use this model (browser-native SpeechRecognition).