Full Next.js App Router application with: - AI-graded lesson checkpoints (BKT/Elo mastery tracking) - Auth.js v5: credentials, Google, GitHub, generic OIDC - Anonymous-session-first with migrate-on-signin - Admin panel: users, blueprints, reports, site settings - Password reset + email verification (nodemailer/SMTP) - Site config: require_auth + signups_enabled flags - server-only guards on all DB/generation/verification modules - PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM - 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.7 KiB
CLAUDE.md — Curio
Repo context for Claude Code. Read this every session. Keep it current: if a convention changes, update this file in the same commit. Full product/architecture/design reference: docs/curio-specification.md.
What Curio is (one paragraph)
Curio is a curiosity-led mastery tutor. A learner states intent → gets a Journey of bounded ~10-minute Lessons → each Lesson is short readable AI-generated explanation segments punctuated by production checkpoints (predict / explain-back / solve) → the AI diagnoses the learner's free-text response against pre-verified rubrics and a per-concept misconception library, gives targeted feedback, and gates advancement on demonstrated mastery. The moat is diagnosis (inferring what a learner misunderstands and why), not content generation.
Decided stack — single language (TypeScript), do not substitute without asking
- App: Next.js (App Router) + TypeScript — RSC reading surface + Route Handlers (API) + native streaming/SSE
- LLM: Vercel AI SDK (provider-agnostic, streaming, schema-constrained structured output). Generator and grader configured to different models/providers.
- Contracts: Zod — shared across API I/O, LLM outputs, DB-derived types
- DB: PostgreSQL 16 + pgvector (single store, source of truth)
- ORM/migrations: Drizzle ORM + drizzle-kit
- Cache/buffer: Redis — a cache/buffer in front of Postgres, never load-bearing for availability
- Background jobs: Route Handlers + the
jobledger table for the slice; BullMQ (Redis) when scale arrives — jobs spend money, so they must be idempotent with durable Postgres state - Styling/motion: Tailwind + CSS custom properties (design tokens); motion via CSS transitions + View Transitions API (no animation library); honor
prefers-reduced-motion - Observability: Langfuse (or OpenTelemetry) for LLM call tracing
- Rate limiting: edge limiter (e.g. Upstash) on the cold-start generation endpoint
- Tests: Vitest (unit, LLM mocked) + golden-eval runner (real-model, CI) + Playwright (e2e)
- Local infra: docker-compose (postgres+pgvector, redis)
Repo structure
curio/
CLAUDE.md
docker-compose.yml
docs/curio-specification.md
src/
app/ # Next.js App Router: reading surface + api/ route handlers
components/
lib/
llm/ # provider-agnostic client (Vercel AI SDK) + versioned prompt registry
generation/ # RAG-grounded content generation
verification/ # content cascade (T0–T2) + response grading
memory/ # mastery (BKT/Elo) + spaced repetition
intent/ # intent normalization + blueprint lookup
db/ # drizzle schema, queries, migrations
schemas/ # zod contracts (shared)
styles/ # design tokens
jobs/ # BullMQ workers (deferred until P3)
tests/golden/ # frozen eval sets: content verification + response grading
e2e/ # playwright
Architectural invariants — violating these is a bug
- All LLM access goes through
src/lib/llm/client.ts. Never call a provider/AI-SDK model directly elsewhere. Generator and grader pull their model from config; they must differ. - Prompts live in the versioned registry (
src/lib/llm/prompts/), never inline. Each has an id + version. - Contracts are Zod schemas in
src/schemas/, shared by the LLM layer, the API, and the UI. LLM structured output is validated against them. - The serve path reads from the Redis buffer; jobs generate. No synchronous content generation in request handlers except the documented cold-start gate. Postgres is the source of truth — a Redis outage degrades latency, not availability.
- Generated content stores provenance (
source_chunk_ids) so verification and grading are grounded against sources, not free-judged. - Grading is constrained. The grader evaluates against a pre-verified rubric + reference answer + misconception library. Verdict ∈ {
mastered,partial,misconception,uncertain}. It must supportuncertain(abstain → reveal). Bias against false-fail — telling a correct learner they're wrong is the worst error. - Verification is a cascade. T0 structural (free) → T1 cheap-model → T2 strong-model (adversarial, at blueprint promotion). Expensive checks amortized once per blueprint, never per serve.
- Paid background jobs are idempotent (idempotency key + durable
jobstate). A retry must never regenerate-and-rebill. - Per-session token budget is enforced in code. The ~10-min Lesson bound is a real cap.
- Content is versioned by
(intent_key, content_version, model_version); invalidation is lazy.
Design invariants (see spec §7)
- Thesis: a calm, editorial reading surface that comes alive only when the learner responds. Not gamified, not a dashboard, not a chat-bubble app.
- Signature: diagnosis-as-marginalia — feedback threaded against the learner's own words, pointing at the
evidence_spanthat tripped them. Spend the design's boldness here; keep everything else quiet. - Motion is meaning, not decoration: reserved for the diagnosis moment (a brief "considering" beat → feedback eases in). CSS/View Transitions only.
- Reading is the product: humanist reading serif, generous measure (~62–68ch) and leading; display face used with restraint; tokens centralized in
src/styles/. - Quality floor (non-negotiable): responsive to mobile, visible keyboard focus, reduced-motion respected, WCAG-AA contrast (including every diagnosis-state color).
- Before building any UI, run the frontend-design two-pass process (plan tokens → critique for genericness → build). Avoid AI-default looks (cream+serif+terracotta, near-black+acid accent, broadsheet hairlines).
- Copy is design material: warm, specific, plain, on the learner's side; never gamified or condescending.
Commands
docker compose up -d # postgres+pgvector, redis
pnpm install # or npm
pnpm db:generate && pnpm db:migrate
pnpm dev # Next.js
pnpm test # vitest (unit; LLM mocked)
pnpm test:golden # real-model eval harness (CI-gated)
pnpm test:e2e # playwright
pnpm lint && pnpm format # eslint + prettier
Testing rules
- Write/extend tests with each change; run the suite before declaring a task done.
- Mock the LLM in unit tests (deterministic fixtures). Real-model calls only in the golden-eval suite.
- Golden evals (
tests/golden/) are the bar for the moat: response-grading accuracy (track false-fail rate) + content-verification accuracy. Don't regress them; add cases on new failure modes. - Migrations: never edit an applied drizzle migration; add a new one.
Out of scope — do NOT build unless explicitly asked
Gamification (XP/badges/leaderboards), infinite feed / social, multi-provider fallback orchestration, knowledge graph, voice, PDF ingestion, accounts (MVP is anonymous-session-first). If a task seems to require one of these, stop and ask.
Working agreement
- Plan before coding for anything non-trivial; surface architecture- or design-affecting choices before implementing.
- Small, focused commits per task. One ticket = one logical change.
- Keep Route Handlers thin; logic lives in
src/lib/. - Run the frontend-design planning pass before UI tickets.
- If you change a convention here, update this file in the same commit.
- When unsure between two reasonable designs, ask rather than guess — especially on the grading/verification path and the diagnosis UI.