feat: initial commit — Curio mastery tutor
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>
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
# CURIO — PRODUCT & BUILD SPECIFICATION
|
||||
|
||||
*An AI mastery tutor. Follow your curiosity.*
|
||||
|
||||
---
|
||||
|
||||
## 1. OVERVIEW
|
||||
|
||||
Curio is a curiosity-led learning platform where a learner states what they want to learn and receives a personalized **Journey** of short, bounded **Lessons**. Lessons are AI-generated, AI-verified, and — crucially — **AI-tutored**: the learner does not passively consume content, they *produce* (predict, explain, solve), and the AI diagnoses what they misunderstand and adapts.
|
||||
|
||||
The product is built on one idea: **a generative model can do what a course or a feed cannot — respond to the individual learner.** Curio is organized entirely around that capability.
|
||||
|
||||
---
|
||||
|
||||
## 2. CORE CONCEPT & DIFFERENTIATOR
|
||||
|
||||
Curio is not a course catalog and not a content feed. Learning is **generated, not selected**, and **diagnosed, not just delivered**.
|
||||
|
||||
The defensible capability — the moat — is **misconception diagnosis**: inferring *what* a learner misunderstands and *why* from what they actually say, and targeting exactly that. Generating explanations is now commodity; diagnosing a specific learner's wrong mental model and correcting it is not. **The product is built around the misconception model, not the content stream.**
|
||||
|
||||
---
|
||||
|
||||
## 3. THE LEARNING UNIT: THE LESSON
|
||||
|
||||
The atomic unit is a **Lesson**: a self-contained microlearning unit, completable in one sitting (~10 minutes), built from 2–4 **segments**.
|
||||
|
||||
```
|
||||
Lesson (~10 min, bounded, one sitting)
|
||||
├─ Segment 1: short readable explanation (~300–500 words, reads like an article)
|
||||
│ └─ Checkpoint: learner produces (predict / explain-back / solve)
|
||||
│ └─ AI diagnosis + targeted feedback (live)
|
||||
├─ Segment 2: explanation → Checkpoint → diagnosis
|
||||
└─ Segment 3: explanation → Checkpoint → diagnosis → Mastery gate
|
||||
```
|
||||
|
||||
Design rules:
|
||||
- **Reads like an article, not a quiz.** Explanation prose is continuous and calm; checkpoints *punctuate* (2–4 per Lesson) rather than interrupting every sentence.
|
||||
- **Bounded.** Clear start and end, finishable in ~10 minutes. The session cap bounds learner attention *and* live token cost.
|
||||
- **Active within the bound.** A passive and an active 10-minute session cost the same minutes; the active one retains far more. Microlearning brevity is preserved; passivity is removed.
|
||||
- **Open-ended where possible.** Checkpoints favor free-text production over multiple choice — free text is what makes diagnosis possible.
|
||||
|
||||
A **Journey** is an ordered sequence of Lessons for a topic, with adaptive branching driven by the memory engine.
|
||||
|
||||
---
|
||||
|
||||
## 4. THE TUTORING LOOP
|
||||
|
||||
Per checkpoint:
|
||||
|
||||
1. **Read** — a short AI-generated explanation segment.
|
||||
2. **Produce** — the learner predicts, explains in their own words, or solves a small problem.
|
||||
3. **Diagnose** — the AI evaluates the *actual response* against a pre-verified rubric, reference answer, and per-concept misconception library; it names the specific misunderstanding.
|
||||
4. **Adapt** — targeted feedback; re-explain differently if it didn't land; escalate difficulty if the learner is solid.
|
||||
5. **Gate** — advancement is earned by demonstrated production, not by clicks. The outcome feeds the memory engine (§11).
|
||||
|
||||
---
|
||||
|
||||
## 5. CURIOSITY RAILS
|
||||
|
||||
The entry point is open: *"What do you want to learn today?"* Learners may pull tangents mid-Lesson ("wait, why does that happen?"). A tangent spawns a short sub-explanation and then **returns to the checkpoint**, so exploration never decays into aimless dabbling. Curiosity is encouraged; it is always brought back onto the attempt→diagnose loop.
|
||||
|
||||
---
|
||||
|
||||
## 6. LEARNING MODES
|
||||
|
||||
- **Journey mode** — structured progression through a topic's Lessons with adaptive branching and mastery tracking.
|
||||
- **Daily Review** — surfaces concepts due for reinforcement from the spaced-repetition schedule. This is the substantive retention loop and the intended source of habit, in place of dopamine-driven gamification.
|
||||
|
||||
---
|
||||
|
||||
## 7. DESIGN LANGUAGE & EXPERIENCE
|
||||
|
||||
Curio should feel **modern, human, inspired, and beautiful** — not as decoration, but because the product is a reading-and-thinking environment, and the craft of that environment is part of the teaching. The interface should feel like a brilliant, warm tutor who has set your work down in front of them and is reading it carefully.
|
||||
|
||||
### 7.1 Thesis
|
||||
|
||||
The experience is **a calm reading surface that quietly comes alive when you respond.** Reading is unhurried and editorial; the moment of diagnosis is where the product breathes. Nothing competes with those two states. This is explicitly *not* a gamified, dashboard-dense, or chatbot-bubble app.
|
||||
|
||||
### 7.2 Signature: marginalia
|
||||
|
||||
The one memorable, bold element is the **diagnosis-as-marginalia** moment: when a learner submits a checkpoint response, the tutor's feedback arrives threaded against *their own words* — the way a great teacher annotates your answer in the margin, pointing at the exact phrase where the misconception lives (the `evidence_span` from §10.3). The feedback references what the learner actually wrote. Spend the design's boldness here; keep everything around it quiet.
|
||||
|
||||
### 7.3 Direction (a starting point, not a mandate)
|
||||
|
||||
Build by working the two-pass design process (plan a token system → critique it against this brief for genericness → then build). The following is the intended direction; refine it, don't default to it. Deliberately avoid the current AI-design clichés (cream + high-contrast serif + terracotta; near-black + acid accent; broadsheet hairline columns).
|
||||
|
||||
- **Palette — "ink & light."** A soft, slightly cool paper (not cream) as the reading ground; an inky near-black for text; a considered deep indigo as the focus/brand accent (depth, study); a warm ochre reserved *only* for the human tutor moments (marginalia, encouragement). Diagnosis states are calm and legible, never an alarm: mastered = a quiet sage/teal, partial = ochre, misconception = a soft clay/rose that says *let's look again*, never buzzer-red. A genuinely comfortable dark mode (warm charcoal, not pure black).
|
||||
- **Typography — reading is the product.** A humanist, low-contrast **reading serif** for body (e.g. Newsreader / Source Serif / Literata as candidates), set at a generous measure (~62–68ch) and line-height (~1.6); a characterful **display face used with restraint** for the prompt and headings; a humanist **mono** for mastery-map data and labels. Make the type a memorable part of the page, not a neutral delivery vehicle.
|
||||
- **Motion — meaning, not decoration.** No animation library. Use CSS transitions and the **View Transitions API**. Reserve the one orchestrated motion moment for diagnosis: a brief "considering" beat, then feedback easing into the margin — it should feel like a person responding, not a form validating. Everything else is still. `prefers-reduced-motion` is honored everywhere.
|
||||
- **Layout — one thing at a time.** Single reading column, generous margins, minimal chrome. The checkpoint is inset in the reading flow; feedback appears as marginalia, not a modal. The mastery map is a separate, richer view — it does not crowd the learn surface.
|
||||
|
||||
### 7.4 Voice (copy is design material)
|
||||
|
||||
Feedback copy *is* the tutor's character. Warm, specific, plain, and on the learner's side — names the exact thing that tripped them, never condescending, never gamified ("Not quite — the bit that slipped is *…*", never "❌ Wrong! −10 XP"). Active voice; actions say what they do. Empty and error states give direction in the interface's voice, not apologies.
|
||||
|
||||
### 7.5 Quality floor (non-negotiable)
|
||||
|
||||
Responsive to mobile; visible keyboard focus; reduced motion respected; WCAG-AA contrast on text and on every diagnosis-state color. Loading is part of the experience — the cold-start outline (§8.3) is a designed state, never a spinner.
|
||||
|
||||
### 7.6 Anti-patterns
|
||||
|
||||
No XP counters, badges, streak confetti, or leaderboards; no dense dashboard on the learn surface; no generic component-library default look; no chat-bubble UI; no autoplay or attention-grabbing motion.
|
||||
|
||||
---
|
||||
|
||||
## 8. CONTENT ARCHITECTURE
|
||||
|
||||
Curio is a **growing library of validated, generated content that gets personalized** — not a pure live-generation engine. The first learner to request a topic pays the generation cost; everyone after reuses validated content. This makes real-time UX, accuracy, and cost simultaneously feasible.
|
||||
|
||||
### 8.1 Three tiers
|
||||
|
||||
1. **Blueprint** — the canonical, fully verified Journey for a normalized intent: an ordered list of concepts with Lesson templates. Generated and verified **once**, reused by all learners.
|
||||
2. **Variants** — pre-generated explanation/checkpoint bodies per concept at 2–3 difficulty levels, generated and verified at blueprint time.
|
||||
3. **Live** — for long-tail intents with no blueprint hit: generated fresh through a lighter synchronous gate, then queued for full verification and **promoted into the blueprint cache** so the next learner gets a hit.
|
||||
|
||||
Two cost classes follow:
|
||||
|
||||
| Cached + verified once, reused by everyone | Live, per learner (pay tokens exactly here) |
|
||||
|---|---|
|
||||
| explanation segments | evaluation of the learner's free-text response |
|
||||
| checkpoints + reference answers + rubrics | misconception diagnosis + targeted feedback |
|
||||
| worked examples | adaptive re-explanation |
|
||||
| per-concept misconception libraries | tangent handling |
|
||||
|
||||
### 8.2 Intent normalization (drives cache hit rate)
|
||||
|
||||
"Learn Python", "teach me python basics", and "i wanna learn python" must collapse to one key:
|
||||
- **Structured key:** one cheap classification call → `{domain, subtopic, goal_type: skill|exam|concept, level_hint}` → canonical string.
|
||||
- **Embedding fallback:** embed the raw intent; pgvector nearest-neighbor search against existing blueprint embeddings; reuse if cosine > threshold.
|
||||
- Miss only when both fail. `goal_type` and `level_hint` are **parameters into** a blueprint, not separate blueprints.
|
||||
|
||||
### 8.3 Serve flow (skeleton-first + buffer)
|
||||
|
||||
Perceived latency is won by streaming *structure* before *content*: the Journey outline appears in under a second; the learner can't move faster than the first segment arrives.
|
||||
|
||||
```
|
||||
on submit(intent):
|
||||
key, ctx = normalize(intent) # ~100ms
|
||||
bp = blueprint_cache.lookup(key, ctx) # exact key, then embedding NN
|
||||
if bp: # WARM
|
||||
lesson = hydrate(bp, next_concept, user) # variant SELECTION, ~150ms
|
||||
buffer.push(session, lesson); stream(lesson)
|
||||
else: # COLD
|
||||
bp = gen_blueprint(intent, ctx) # structured call → concept list
|
||||
stream(bp.outline) # outline visible now (designed state)
|
||||
lesson = gen_lesson(bp, concept[0], rag) # light sync gate before serve
|
||||
buffer.push(session, lesson); stream(lesson)
|
||||
enqueue(promote_to_blueprint, bp) # full verification + cache, async
|
||||
|
||||
on advance / buffer_low(session):
|
||||
enqueue(generate_next, session, next_concept) # background job
|
||||
```
|
||||
|
||||
The serve path reads from a Redis buffer fronting Postgres; a background job refills it. Concurrent generations per session are capped for backpressure. Postgres remains the source of truth — a Redis outage degrades latency, not availability.
|
||||
|
||||
---
|
||||
|
||||
## 9. AI GENERATION PIPELINE
|
||||
|
||||
Generation is **RAG-grounded** to constrain hallucination at the source. Per concept the generator produces, all grounded in retrieved trusted sources and stored with provenance (`source_chunk_ids`):
|
||||
|
||||
- explanation segments
|
||||
- checkpoints (prompts) + **reference answers** + **grading rubrics**
|
||||
- worked examples
|
||||
- a **misconception library**: likely wrong mental models for the concept, each with a tag and a diagnostic signature
|
||||
|
||||
Rubrics and misconception libraries are first-class generated artifacts — they are what make live grading constrained and verifiable. All prompts live in a versioned registry; generation output is **structured (schema-constrained) JSON** via the model's native structured-output mode.
|
||||
|
||||
---
|
||||
|
||||
## 10. AI VERIFICATION SYSTEM
|
||||
|
||||
Curio verifies with AI on two surfaces. Both are made reliable by the same discipline: **ground the check against real artifacts, decorrelate from the generator, structure the output, and allow abstention.**
|
||||
|
||||
### 10.1 Content verification — the cascade
|
||||
|
||||
A naive "second LLM checks the card" shares the generator's blind spots and rubber-stamps confident-wrong content. Instead:
|
||||
|
||||
- **Grounded, not free-judged.** The verifier checks each atomic claim against the retrieved source chunks ("is this supported by these sources?"), not against its own priors.
|
||||
- **Independent solving.** The verifier never sees the marked answer; it **solves checkpoints/quizzes blind** (n-sample, majority vote) and compares to the reference answer. Mismatch → flag; sample disagreement → uncertainty.
|
||||
- **Decorrelation.** A different model/provider and an adversarial prompt, so failure modes don't overlap the generator's.
|
||||
|
||||
Run as a **cascade**, so expensive scrutiny is amortized:
|
||||
|
||||
| Tier | Runs | Cost | Checks |
|
||||
|---|---|---|---|
|
||||
| **T0 Structural** | every item, always | ~free | schema valid; exactly one correct option; reference answer references a real option; refs resolve; non-empty; length bounds |
|
||||
| **T1 Cheap-model** | every item at generation | low | claim grounding; blind solve; obvious safety. Clears the easy majority, else escalate |
|
||||
| **T2 Adversarial** | blueprint promotion + escalations + low confidence | medium, **amortized once per blueprint** | full rubric, claim-by-claim grounding, n-sample blind solve, pedagogical soundness, cross-item consistency |
|
||||
| **T3 Human** | top-traffic templates + persistent uncertainty | high, rare | spot review, golden-set labeling |
|
||||
|
||||
Because content is cached at the template level, T2 runs **once per blueprint** and is reused by thousands of learners — near-zero per-serve cost.
|
||||
|
||||
### 10.2 Response-grading verification
|
||||
|
||||
The AI also grades free-text learner responses. Making this trustworthy:
|
||||
|
||||
- **Grade against pre-verified artifacts.** At runtime the grader checks the response against the already-verified rubric + reference answer + misconception library — constrained, not freeform.
|
||||
- **Misconception matching.** The grader maps the response to a known misconception tag or flags `novel` (→ escalate). Closed-set classification beats open-ended critique.
|
||||
- **Two-sided error awareness.** A false-pass wastes learning; a **false-fail (telling a correct learner they're wrong) corrodes trust and is worse.** The grader is biased toward generous grading and always reveals its reasoning so the learner can contest.
|
||||
- **Abstention + fallback.** The grader may return `uncertain` → reveal the model answer for self-assessment rather than confidently mis-grading.
|
||||
- **Cascade.** A cheap model grades the rubric-matched majority; a strong model handles `novel`/`uncertain` only.
|
||||
|
||||
### 10.3 Output contracts
|
||||
|
||||
Content verifier:
|
||||
```json
|
||||
{
|
||||
"verdict": "pass | fail | uncertain",
|
||||
"claims": [{"text": "<atomic claim>", "status": "supported|unsupported|contradicted",
|
||||
"evidence_chunk_ids": ["..."]}],
|
||||
"solve_check": {"independent_answers": ["B","B","C"], "majority": "B",
|
||||
"reference": "B", "agreement": 0.67, "status": "match|mismatch|ambiguous"},
|
||||
"pedagogy": {"teaches_concept": true, "prereqs_respected": true},
|
||||
"safety": {"in_scope": true, "mode_compliant": true},
|
||||
"confidence": 0.0
|
||||
}
|
||||
```
|
||||
|
||||
Response grader:
|
||||
```json
|
||||
{
|
||||
"verdict": "mastered | partial | misconception | uncertain",
|
||||
"rubric_hits": ["criterion_id"],
|
||||
"misconception_tag": "tag | novel | none",
|
||||
"evidence_span": "<quote from learner response>",
|
||||
"feedback_directive": "advance | re-explain:<angle> | probe:<question>",
|
||||
"confidence": 0.0
|
||||
}
|
||||
```
|
||||
|
||||
Contracts are defined as **Zod schemas** shared between the LLM layer, the API, and the frontend.
|
||||
|
||||
### 10.4 Keeping verifiers honest
|
||||
|
||||
- **Golden sets** of human-labeled cases for both surfaces; measure precision/recall continuously, tracking **false-fail rate** separately for grading. A model upgrade triggers an offline re-sweep of the cached library.
|
||||
- **User-signal outer loop (free):** collective checkpoint failure (many learners miss the same item → suspect the reference answer), a report button, and dwell-time anomalies auto-trigger re-verification.
|
||||
- **Versioning:** content keyed by `(intent_key, content_version, model_version)`; invalidation is lazy; a flagged item regenerates just itself.
|
||||
|
||||
---
|
||||
|
||||
## 11. MEMORY ENGINE
|
||||
|
||||
A **mastery-tracking engine**, not a knowledge graph:
|
||||
- **Per-concept mastery score** updated from demonstrated production at checkpoints and from misconception tags, via Bayesian Knowledge Tracing or an Elo-style update.
|
||||
- **Spaced-repetition schedule** per concept (SM-2-like) driving Daily Review.
|
||||
|
||||
This drives difficulty scaling, review timing, reinforcement, and the mastery gate. A relational knowledge graph is deferred until there is data to justify it.
|
||||
|
||||
---
|
||||
|
||||
## 12. PEDAGOGY
|
||||
|
||||
The design implements the mechanics behind the **2-sigma** finding (1:1 tutoring + mastery learning outperforms classroom instruction by ~two standard deviations) — exactly what generative AI can deliver and a feed structurally cannot:
|
||||
- **Active production / retrieval practice** at every checkpoint.
|
||||
- **Mastery gating** before advancement.
|
||||
- **Spaced repetition** via Daily Review.
|
||||
- **Interleaving** of related concepts rather than blocking.
|
||||
|
||||
---
|
||||
|
||||
## 13. PERSONALIZATION
|
||||
|
||||
- **Variant selection** by mastery: pick the pre-generated difficulty variant matching the learner's score. Deterministic, sub-millisecond, zero live LLM cost.
|
||||
- **Live personalization** is the feedback and re-explanation themselves — which is the core value, not overhead, and is where live tokens are deliberately spent.
|
||||
|
||||
---
|
||||
|
||||
## 14. SAFETY & QUALITY CONTROL
|
||||
|
||||
Three strictness modes — **open**, **controlled** (default), **enterprise-safe** — applied as parameters to generation, content verification, and response grading (including how off-topic or unsafe free-text input is handled). Filtered domains are enforced at intent normalization and at T0/T1.
|
||||
|
||||
---
|
||||
|
||||
## 15. TELEMETRY & EVALUATION
|
||||
|
||||
Because the product *is* diagnosis, instrumentation is a first-class requirement, not an afterthought:
|
||||
- **Diagnosis telemetry:** every `response → grade` is logged; track false-fail rate, misconception-hit rate, and mastery gain per concept. You cannot improve a diagnosis engine you don't measure.
|
||||
- **LLM observability:** per-call tracing of latency, tokens, and cost (Langfuse or OpenTelemetry). Without it the false-fail and cost metrics are aspirational.
|
||||
- **Eval harness in CI:** golden sets for content verification and response grading run on every prompt change. Prompts are code — versioned and tested. The harness doubles as the build's definition of "good enough."
|
||||
- **Cost/latency guards:** per-session token cap (the Lesson bound enforced in code), a provider circuit breaker, an **edge rate limiter on the cold-start generation endpoint** (it spends money on demand), and graceful degradation to abstain→reveal.
|
||||
|
||||
---
|
||||
|
||||
## 16. FEATURE SET
|
||||
|
||||
**Core (in the product):** intent entry; Journey mode; bounded Lessons with checkpoints; live diagnosis with abstention; mastery + spaced repetition; Daily Review; mastery map (learner-facing view of mastered / shaky / due); source citations (surfacing the provenance already stored).
|
||||
|
||||
**Loop reinforcers (near-term):** confidence rating before reveal (catches *confidently wrong*); Socratic follow-up probe on a `partial` verdict.
|
||||
|
||||
**Deferred:** learn from your own material (PDF/notes → personalized Journey — strong differentiator, leverages existing RAG infra, but a large surface); voice.
|
||||
|
||||
**Explicitly out of scope:** XP / badges / leaderboards, infinite feed, social features, multi-provider fallback orchestration, knowledge graph. Microlearning + Daily Review + mastery map supply motivation intrinsically; bolt-on gamification risks reintroducing passive, low-retention "junk-food" learning.
|
||||
|
||||
---
|
||||
|
||||
## 17. DATA MODEL
|
||||
|
||||
```
|
||||
source_chunk(id, doc_ref, embedding, text)
|
||||
blueprint(id, intent_key, embedding, title, model_version, content_version,
|
||||
status[draft|verifying|published|flagged])
|
||||
concept(id, blueprint_id, ord, name, retrieval_ctx_ref)
|
||||
lesson(id, blueprint_id, ord, est_minutes)
|
||||
segment(id, lesson_id, ord, body_json, source_chunk_ids[], verify_status, content_version)
|
||||
checkpoint(id, segment_id, prompt, kind[predict|explain|solve],
|
||||
reference_answer, rubric_json, verify_status)
|
||||
misconception(id, concept_id, tag, signature, verify_status)
|
||||
verify_report(id, target_id, target_type, verdict, confidence, payload_json, model_version)
|
||||
|
||||
user(id, ...)
|
||||
journey_instance(id, user_id, blueprint_id, position)
|
||||
mastery(user_id, concept_id, score, last_seen, next_review)
|
||||
response(id, user_id, checkpoint_id, text, ts)
|
||||
grade(id, response_id, verdict, misconception_tag, confidence, payload_json)
|
||||
|
||||
job(id, type, idempotency_key, status, payload_json, attempts, created_at) -- durable ledger for paid background work
|
||||
|
||||
-- Redis: buffer:{session} -> [hydrated lesson/segment ids]; generation-concurrency locks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. TECH STACK
|
||||
|
||||
**Single language, end to end (TypeScript).** One type system, one toolchain, and request/response/LLM contracts shared across the boundary. Nothing here requires a Python ML runtime — the work is hosted-LLM calls, embeddings, and pgvector queries, all first-class in TypeScript.
|
||||
|
||||
- **App framework:** Next.js (App Router, TypeScript) full-stack — React Server Components for the reading surface, Route Handlers for the API, native streaming/SSE for skeleton-first delivery. Split out a thin service (e.g. Hono) only if the API outgrows Next.
|
||||
- **LLM orchestration:** Vercel AI SDK — provider-agnostic, native streaming and **schema-constrained structured output** (Zod). Generator and grader configured to **different providers/models** (decorrelation). Exploit two cost levers: native structured output for verifier/grader contracts, and **prompt caching** for the large, session-reused grounding context (sources + rubric + misconception library).
|
||||
- **Contracts/validation:** Zod, shared across API I/O, LLM outputs, and DB-derived types.
|
||||
- **Database:** PostgreSQL + pgvector (single store, source of truth).
|
||||
- **ORM & migrations:** Drizzle ORM + drizzle-kit (type-safe, SQL-first, clean pgvector support).
|
||||
- **Cache / buffer:** Redis — a cache and serve-buffer in *front* of Postgres, never load-bearing for availability.
|
||||
- **Background work:** for the vertical slice, Route Handlers + the `job` ledger table suffice. When scale work arrives: **BullMQ** (Redis-backed, TS), with **idempotency keys and durable Postgres job state** — these jobs spend money, so a retry must never regenerate-and-rebill.
|
||||
- **Styling & motion:** Tailwind CSS + CSS custom properties for design tokens; motion via CSS transitions and the **View Transitions API** (no animation library). `prefers-reduced-motion` respected.
|
||||
- **Observability:** Langfuse (or OpenTelemetry) for LLM call tracing; standard app metrics/logs.
|
||||
- **Rate limiting:** edge limiter (e.g. Upstash Ratelimit on Redis) on the cold-start generation endpoint.
|
||||
- **Testing:** Vitest (unit; LLM mocked) + a golden-eval runner (real-model, CI-gated) + Playwright for the core learn-flow e2e.
|
||||
- **Hosting:** Next on Vercel (or Node host); managed Postgres+pgvector (Neon / Supabase / RDS) and managed Redis (Upstash). Keep the cold-start generation path on a runtime with adequate timeout (Node serverless/long-running, not edge).
|
||||
|
||||
---
|
||||
|
||||
## 19. COST MODEL
|
||||
|
||||
- **Scaffolding** (explanations, checkpoints, rubrics, misconception libraries) is generated and T2-verified **once per blueprint**, amortized to near-zero per serve.
|
||||
- **Live cost** is grading + feedback turns: per Lesson ≈ 2–4 checkpoints × (grade + feedback) + occasional re-explanation/tangent. The ~10-minute Lesson bound **directly caps live turns per session** — microlearning bounds cost as well as attention.
|
||||
- **Levers:** a grading **cascade** (cheap model for the rubric-matched majority, strong model only on `novel`/`uncertain`) and **prompt caching** of the reused grounding context keep per-turn cost low.
|
||||
- Net: with a high cache-hit rate, live LLM spend concentrates on cold long-tail topics (throttleable) and on the irreplaceable act of responding to the learner. Model this against the chosen provider's pricing at assumed hit rates and checkpoint counts before building.
|
||||
|
||||
---
|
||||
|
||||
## 20. MVP SCOPE & BUILD ROADMAP
|
||||
|
||||
**The MVP answers one question:** *can an AI tutor diagnose what a learner misunderstands and fix it, well enough that they learn and return?*
|
||||
|
||||
**MVP loop:** prompt → one Lesson (2–3 segments) → read + open-text checkpoints → live grading against verified rubric + misconception library → mastery gate.
|
||||
|
||||
**Phases:**
|
||||
- **P0 — Scaffold & infra.** Next.js + TypeScript + Tailwind app; docker-compose (Postgres+pgvector, Redis); Drizzle schema + migrations; Vitest; provider-agnostic LLM layer (Vercel AI SDK) with a mockable client; design tokens stubbed.
|
||||
- **P1 — Vertical slice (single hardcoded topic).** Content schema + seeded corpus + pgvector retrieval → RAG-grounded Lesson generation (segments, checkpoints, reference answers, rubrics) → serve + render the reading surface and checkpoint → **response grading with abstention** → **golden eval harness gating false-fail rate in CI.** Build the marginalia feedback moment here — it is the core experience. This milestone validates diagnosis quality.
|
||||
- **P2 — Diagnosis depth.** Misconception-library quality + the full content-verification cascade.
|
||||
- **P3 — Adaptivity & scale.** Mastery + spaced repetition; blueprint cache, intent normalization, BullMQ workers with idempotent paid jobs.
|
||||
- **P4 — Roadmap features**, one at a time: confidence rating, Socratic probe, Daily Review, mastery map, source citations, variant personalization, accounts.
|
||||
|
||||
Anonymous-session-first; accounts arrive in P4.
|
||||
|
||||
---
|
||||
|
||||
## 21. OPEN DECISIONS
|
||||
|
||||
1. **Checkpoint count per Lesson** — fixed (e.g., 3) or adaptive to mastery? Sets the cost ceiling and the est-minutes contract.
|
||||
2. **False-fail tolerance** — how generous is the grader, and when does it abstain vs. reveal? Sets the trust/rigor balance; prototype with real responses. *Settle before writing the grader.*
|
||||
3. **Cold-start consistency** — may a cold long-tail Lesson be graded against a T1-only rubric (full T2 async), or does first serve block on T2?
|
||||
4. **Concept granularity** — one concept per Lesson, or per Lesson-cluster?
|
||||
5. **Source corpus policy** — define allowed, non-infringing sources before generation begins.
|
||||
|
||||
---
|
||||
|
||||
## 22. NORTH STAR
|
||||
|
||||
A learner types *"Teach me astrophysics."* A short, readable Lesson opens — finishable in ten minutes, set in calm, beautiful type. They read a few hundred words, then are asked to predict what happens to a collapsing star. They type a guess. The tutor reads it, pauses for a beat, and writes back in the margin — pointing at the exact phrase where their thinking went wrong, explaining the missing piece a different way, and asking again. They get it. Mastery updates; the next review is scheduled. Every explanation, problem, and rubric was generated and verified once and reused — only the part that read their actual answer ran live, which is the whole point.
|
||||
@@ -0,0 +1,180 @@
|
||||
# User Management Plan (T1 + T2 + T3)
|
||||
|
||||
## Context
|
||||
|
||||
Curio is anonymous-session-first (sessionStorage UUID). Adding Auth.js v5 (NextAuth) with email/password + OAuth (Google, GitHub), anonymous→auth migration, profile/settings, and admin panel. `users` table exists (id, createdAt) — needs column additions + 3 new Auth.js tables.
|
||||
|
||||
---
|
||||
|
||||
## Stack additions
|
||||
|
||||
```
|
||||
next-auth@beta # Auth.js v5
|
||||
@auth/drizzle-adapter # Drizzle adapter for Auth.js
|
||||
bcryptjs + @types/bcryptjs # Password hashing
|
||||
nodemailer + @types/nodemailer # Email verification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Schema migration
|
||||
|
||||
New migration via drizzle-kit generate → `drizzle/migrations/0004_auth.sql`
|
||||
|
||||
**Alter `user` table** — add columns:
|
||||
- `email` varchar(255) unique nullable (nullable: existing anonymous rows have none)
|
||||
- `email_verified` timestamp nullable
|
||||
- `name` varchar(255) nullable
|
||||
- `image` text nullable
|
||||
- `role` varchar(20) not null default `'learner'` — values: `learner | admin | suspended`
|
||||
- `password_hash` text nullable (null for OAuth users)
|
||||
|
||||
**New tables** (Auth.js Drizzle adapter required):
|
||||
- `account` — OAuth provider links (userId FK → user.id, provider, providerAccountId, etc.)
|
||||
- `session` — DB sessions (sessionToken PK, userId FK, expires)
|
||||
- `verification_token` — email verification (identifier + token composite PK, expires)
|
||||
|
||||
Schema file: `src/lib/db/schema.ts` — add 3 new tables, extend users inline.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Auth.js core config
|
||||
|
||||
**`src/auth.ts`** — single source of truth:
|
||||
- Adapter: `DrizzleAdapter(db, { usersTable: users, ... })`
|
||||
- Session strategy: `database` (allows session revocation for admin ban)
|
||||
- Providers: `Credentials` (email+bcrypt), `Google`, `GitHub`
|
||||
- Callbacks: `session` + `jwt` embed `user.id` + `user.role`
|
||||
- Pages: `signIn: '/auth/login'`, `error: '/auth/error'`
|
||||
- Exports: `{ handlers, auth, signIn, signOut }`
|
||||
|
||||
**`src/app/api/auth/[...nextauth]/route.ts`** — `export const { GET, POST } = handlers`
|
||||
|
||||
**`src/lib/auth-helpers.ts`** — server utilities:
|
||||
- `requireAuth()` — redirects to `/auth/login` if no session
|
||||
- `requireAdmin()` — requireAuth + role check
|
||||
- `getOptionalSession()` — returns session or null
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Middleware
|
||||
|
||||
**`src/middleware.ts`**:
|
||||
- Protected prefixes: `/profile`, `/settings`, `/admin`
|
||||
- Redirect unauthenticated → `/auth/login?callbackUrl=...`
|
||||
- `/admin` prefix: redirect non-admin → `/`
|
||||
- All other paths pass through
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Auth UI pages
|
||||
|
||||
`src/app/auth/login/page.tsx` — email+password form + Google/GitHub buttons + link to register
|
||||
|
||||
`src/app/auth/register/page.tsx` — email + name + password + confirm; POST `/api/auth/register`; then signIn + migrate anonymous session
|
||||
|
||||
`src/app/auth/verify-email/page.tsx` — "check inbox" + token handler
|
||||
|
||||
`src/app/auth/error/page.tsx` — maps Auth.js error codes to readable messages
|
||||
|
||||
Design: ink & light tokens, no card shadows, no rounded-xl. Labels above inputs.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Anonymous session migration
|
||||
|
||||
**`src/app/api/auth/migrate-session/route.ts`** — POST `{ anonUserId }`:
|
||||
- Auth-gated (requires valid session)
|
||||
- Transfers all rows: `responses`, `mastery`, `journey_instance` — UPDATE user_id = auth WHERE user_id = anon
|
||||
- Renames Redis budget key `budget:{anon}` → `budget:{auth}`
|
||||
- Idempotent
|
||||
|
||||
**Client side**: after `signIn` resolves, read `sessionStorage.getItem('curio_uid')`, POST migrate, clear key.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Update existing API routes
|
||||
|
||||
All 12 routes: prefer server session, fall back to client-supplied userId (keeps anonymous flow):
|
||||
|
||||
```typescript
|
||||
const session = await getOptionalSession();
|
||||
const userId = session?.user?.id ?? body.userId ?? searchParams.get('userId');
|
||||
```
|
||||
|
||||
Routes with body userId: `/api/grade`, `/api/advance`, `/api/explain`, `/api/tangent`, `/api/socratic`
|
||||
Routes with query userId: `/api/mastery`, `/api/review`
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Learner account pages (T2)
|
||||
|
||||
**`src/app/profile/page.tsx`** (RSC, protected) — name, email, joined date; mastery overview per blueprint
|
||||
|
||||
**`src/app/settings/page.tsx`** (RSC, protected) — Profile section (name, avatar), Security (change password, linked OAuth accounts), Danger (delete account)
|
||||
|
||||
New API routes (all auth-gated):
|
||||
- `PATCH /api/user/profile` — update name/image
|
||||
- `POST /api/user/change-password` — verify old bcrypt → hash new
|
||||
- `DELETE /api/user/account` — delete + cascade
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Admin panel (T3)
|
||||
|
||||
All protected by `requireAdmin()`.
|
||||
|
||||
**`src/app/admin/layout.tsx`** — sidebar: Users | Reports | Blueprints
|
||||
|
||||
**`src/app/admin/page.tsx`** — stats: total users, active today
|
||||
→ `GET /api/admin/stats`
|
||||
|
||||
**`src/app/admin/users/page.tsx`** — paginated user list; actions: promote to admin, suspend, delete
|
||||
→ `GET /api/admin/users`, `PATCH /api/admin/users/[id]`, `DELETE /api/admin/users/[id]`
|
||||
|
||||
**`src/app/admin/reports/page.tsx`** — content_report triage; actions: dismiss, flag for re-verify
|
||||
→ `GET /api/admin/reports`, `PATCH /api/admin/reports/[id]`
|
||||
|
||||
**`src/app/admin/blueprints/page.tsx`** — blueprint status management
|
||||
→ `GET /api/admin/blueprints`, `PATCH /api/admin/blueprints/[id]`
|
||||
|
||||
---
|
||||
|
||||
## New env vars
|
||||
|
||||
```
|
||||
AUTH_SECRET= # openssl rand -hex 32
|
||||
AUTH_URL=http://localhost:3000
|
||||
AUTH_GOOGLE_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
AUTH_GITHUB_ID=
|
||||
AUTH_GITHUB_SECRET=
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
EMAIL_FROM=noreply@yourdomain.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files created / modified (summary)
|
||||
|
||||
**New:** `src/auth.ts`, `src/middleware.ts`, `src/lib/auth-helpers.ts`, `src/app/api/auth/[...nextauth]/route.ts`, `src/app/api/auth/register/route.ts`, `src/app/api/auth/migrate-session/route.ts`, `src/app/auth/login|register|verify-email|error/page.tsx`, `src/app/profile/page.tsx`, `src/app/settings/page.tsx`, `src/app/api/user/profile|change-password|account/route.ts`, `src/app/admin/layout|page.tsx`, `src/app/admin/users|reports|blueprints/page.tsx`, `src/app/api/admin/stats|users/[id]|reports/[id]|blueprints/[id]/route.ts`
|
||||
|
||||
**Modified:** `src/lib/db/schema.ts` (extend users + 3 tables), all 12 existing API routes (userId fallback), `src/app/page.tsx` (nav link), `src/app/layout.tsx` (SessionProvider), `.env.example`
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
pnpm drizzle-kit generate && pnpm drizzle-kit migrate # 0004_auth applies clean
|
||||
pnpm dev
|
||||
curl http://localhost:3000/api/auth/providers # returns Google, GitHub, credentials
|
||||
# Register → lesson → grade works (anonymous flow intact)
|
||||
# Register → verify email → profile shows mastery
|
||||
# Set role='admin' in DB → /admin accessible
|
||||
# Non-admin → /admin redirects to /
|
||||
pnpm test # existing suite passes (userId fallback preserves behavior)
|
||||
```
|
||||
Reference in New Issue
Block a user