# Project export: LearnStep

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Upload. Learn. Ace it.
- Devpost: https://devpost.com/software/learnstep
- GitHub: https://github.com/Shradd7/LearnStep
- Video: https://www.youtube.com/embed/6RhLzU9o6ug?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Shraddhan (4 commits)

## Devpost submission (written by the team)

### Inspiration

Students often struggle not because they cannot learn, but because the explanation is too advanced, too generic, or disconnected from their own chapter. We wanted to create a study buddy that adapts to a student’s class, subject, and uploaded material—without making learning feel like another boring worksheet.

### What it does

LearnStep is an NLP-first learning companion for Classes 5–8 Mathematics and Science. Students upload a chapter PDF, notes, or worksheet. LearnStep extracts concepts, definitions, formulas, examples, and exercises; turns them into short lessons; asks class-appropriate questions; gives hints before solutions; and recommends what to revise next. Instead of calling students “weak” or “smart,” it tracks learning evidence such as: Introduced Developing with support Demonstrated independently Ready for revision

### How we built it

We built LearnStep using: React and TypeScript for the learning interface FastAPI for the backend APIs PostgreSQL and pgvector for structured data and semantic retrieval PyMuPDF for PDF extraction spaCy and rule-based NLP for detecting headings, definitions, formulas, examples, and questions MiniLM embeddings for retrieving relevant chapter content DistilBERT classifiers for question cognitive level and difficulty Docker for reproducible local development and deployment The retrieval system uses both semantic similarity and strict metadata filters such as student, class, subject, chapter, document, and concept. $$ \text{score}(q, c) = \cos(\mathbf{e}_q, \mathbf{e}_c) $$ where ( \mathbf{e}_q ) is the query embedding and ( \mathbf{e}_c ) is the learning-content chunk embedding.

### Challenges we ran into

The hardest challenge was making content truly class-appropriate. A shorter answer is not automatically easier—vocabulary, prerequisites, notation, numbers, and reasoning steps all need to match the student’s level. We also had to prevent the system from treating retrieval as teaching. Finding a relevant paragraph is not enough; the system needs to convert it into a structured flow: $$ \text{Learn} \rightarrow \text{Example} \rightarrow \text{Practice} \rightarrow \text{Hint} \rightarrow \text{Explain} \rightarrow \text{Revise} $$ Another major challenge was answer evaluation. Numerical answers need value, unit, and tolerance checks, while explanations need rubric-based feedback and should sometimes remain low-confidence instead of being force-marked correct or incorrect.

### Accomplishments we're proud of

Built a curriculum-grounded learning flow instead of a generic “chat with PDF” tool Designed an NLP pipeline that identifies educational content such as formulas, definitions, examples, and exercises Created class-, subject-, concept-, and document-filtered RAG retrieval Added staged hints so students can think before seeing the answer Separated curriculum coverage, learning evidence, and revision priority Designed the system without student ranking, behavioural profiling, or unnecessary personal-data collection Added a deep-learning evaluation plan that compares DistilBERT against a TF-IDF baseline instead of assuming the neural model is better

### What we learned

We learned that RAG alone is not enough for educational products. Good retrieval does not guarantee a good explanation for a Class 5 student. We also learned that AI feedback needs uncertainty. It is better for a system to say that an explanation needs another attempt than to confidently give incorrect feedback. Most importantly, we learned that building for children requires privacy, safety, and age-appropriate design from the first line of the product—not as an afterthought.

### What's next

We are starting with Classes 5–8 Mathematics and Science in English. Next, we plan to: Evaluate the NLP extraction pipeline on varied educational PDFs Train and benchmark the cognitive-level and difficulty classifiers Build a reviewed question bank with explanations and hint stages Add concept-level revision planning Deploy a safe demo using synthetic sample chapters Expand carefully to more subjects, languages, and curriculum mappings after evaluation

## README (from the GitHub repository)

# LearnStep

> **Upload. Learn. Ace it. 🚀**

[![CI](https://github.com/Shradd7/LearnStep/actions/workflows/ci.yml/badge.svg)](https://github.com/Shradd7/LearnStep/actions/workflows/ci.yml)

LearnStep is an NLP-first learning companion portfolio demo for Classes 5–8 Mathematics and Science. Students often struggle because an explanation is too advanced, too generic, or disconnected from their chapter. LearnStep explores a different flow: upload learning material, extract structured evidence, teach one clear concept, practise with staged hints, receive deterministic feedback, and decide what to revise next.

This repository is a **controlled synthetic demonstration**, not a public product for children. Demo accounts, chapters, PDFs, questions, labels, and evaluation corpora are synthetic. Nothing here is official, reviewed, CBSE/NCERT-endorsed, or evidence of educational improvement. Do not enter real child information.

## Try it out

| Resource | URL | Availability |
| --- | --- | --- |
| Public repository and evidence | <https://github.com/Shradd7/LearnStep> | Public |
| Local application | <http://localhost:5173> | After Docker setup below |
| Local controlled demo | <http://localhost:5173/demo> | After Docker setup below |
| Local API documentation | <http://localhost:8000/docs> | After Docker setup below |

There is currently **no public live application**. For a Devpost “Try it out” field, use the public repository URL and state that the executable demo is local-only.

## What works

- two synthetic demo identities with Argon2 password hashes and short-lived signed tokens;
- private temporary PDF storage with generated keys, validation, automatic expiry, and owner-authorized deletion;
- page-preserving PyMuPDF extraction with deterministic educational content labels;
- mandatory user, document, class, subject, and concept filters before pgvector retrieval;
- deterministic lessons with sources, evidence IDs, confidence wording, and limitations;
- hint 1 and hint 2 before solution reveal;
- exact multiple-choice and numeric-with-unit/tolerance evaluation;
- immutable attempt observations and non-ranking progress guidance;
- reproducible synthetic extraction, TF-IDF baseline, retrieval/isolation, and assessment evaluations;
- a complete synthetic journey: login → optional upload → lesson → hint → answer → feedback → progress → deletion.

## Architecture

```mermaid
flowchart LR
    UI["React + TypeScript<br/>accessible learning flow"] -->|"JSON + temporary PDF"| API["FastAPI modular monolith"]
    API --> NLP["PyMuPDF + deterministic<br/>educational NLP"]
    API --> TEACH["Deterministic lessons,<br/>hints and evaluators"]
    NLP --> DB["PostgreSQL 16 + pgvector"]
    TEACH --> DB
    API --> STORE["Private expiring<br/>file storage"]
```

The runtime has no external LLM, agents, queues, public file paths, student rankings, or cross-student comparisons. The controlled retrieval demo uses a clearly labeled deterministic 384-dimensional hash vector; it is not a claim about MiniLM semantic quality.

## Demo screenshots

All screenshots below were captured from the running synthetic demo at 1440×960 (3:2) and are under 5 MB.

| Screenshot | What it shows |
| --- | --- |
| ![LearnStep landing page](docs/media/devpost/01-landing.png) | Landing page, product boundary, and live service state |
| ![Synthetic learner selection](docs/media/devpost/02-synthetic-learner-selection.png) | Class 5 Mathematics and Class 6 Science synthetic identities |
| ![Synthetic chapter selection](docs/media/devpost/03-chapter-selection.png) | Class-filtered chapters with source and confidence wording |
| ![Source-grounded lesson](docs/media/devpost/04-source-grounded-lesson.png) | Structured lesson, source details, and practice question |
| ![Staged hint](docs/media/devpost/05-staged-hint.png) | Hint-first interaction before answer reveal |
| ![Deterministic feedback and progress](docs/media/devpost/06-feedback-and-progress.png) | Transparent answer feedback and non-ranking learning evidence |

## Run locally with Docker

Prerequisites: Docker Desktop with the Linux engine running. Git is optional for local testing.

```powershell
docker compose up -d db
docker compose run --rm backend alembic upgrade head
docker compose run --rm backend python -m classpath.scripts.seed_demo_curriculum
docker compose up -d --build backend frontend
docker compose ps
```

Open <http://localhost:5173/demo>. The UI enters either synthetic account without requiring manual credentials. Direct API testing may use:

| Account | Password | Scope |
| --- | --- | --- |
| `math-demo@example.invalid` | `Demo-Math-2026` | Class 5 Mathematics |
| `science-demo@example.invalid` | `Demo-Science-2026` | Class 6 Science |

These public credentials are intentional fixtures and must never be reused for a real deployment.

## Synthetic controlled-demo results

Measured locally on 2026-07-13. These results verify deterministic behavior on generator-controlled fixtures; they do not establish curriculum quality, educational effectiveness, or real-document generalization.

| Evaluation | Synthetic scope | Measured result |
| --- | --- | --- |
| PDF extraction | 60 generated PDFs, 8 concepts, 3 layouts | 60/60 extracted; content-type micro F1 1.00; concept micro F1 1.00 |
| Cognitive classifier | 384 unreviewed synthetic questions; grouped test n=64 | Majority macro F1 0.0556; TF-IDF macro F1 1.00 |
| Difficulty classifier | Same grouped test | Majority macro F1 0.1818; TF-IDF macro F1 1.00 |
| Retrieval and isolation | 120 synthetic queries, real PostgreSQL/pgvector, 2 users | Recall@5 1.00; MRR@10 1.00; 0 cross-user, wrong-class, or wrong-concept results |
| Assessment | 100 MCQ + 100 numeric synthetic cases | Rule-outcome accuracy 1.00 for each implemented answer type |

The perfect synthetic scores are expected because fixtures and labels are generator-controlled. DistilBERT was **not trained**: the repository does not yet have approved human-reviewed labels, a frozen real-world test set, inter-annotator review, or 100 reviewed classifier errors. No classifier is promoted.

## Built with Codex and GPT-5.6

Codex was used to plan, scaffold, implement, test, audit, and document LearnStep. GPT-5.6/Codex helped iterate on the modular architecture, deterministic test cases, child-safety boundaries, ownership isolation, and synthetic evaluation artifacts. The final system was verified using the documented controlled-demo test suite and real local PostgreSQL/pgvector integration checks.

Codex and GPT-5.6 did **not** independently validate curriculum quality, perform human educational review, run a student study, or create evidence for claims beyond the committed synthetic artifacts. Generated suggestions were treated as engineering input and checked against executable tests and recorded outputs.

## Verification commands

```powershell
cd backend
.\.venv\Scripts\ruff.exe check . ..\scripts
.\.venv\Scripts\ruff.exe format --check . ..\scripts
.\.venv\Scripts\mypy.exe src tests ..\scripts
.\.venv\Scripts\pytest.exe -m "not integration"
$env:APP_ENV="test"
$env:DATABASE_URL="postgresql+psycopg://classpath:classpath_dev@localhost:5432/classpath"
.\.venv\Scripts\pytest.exe -m integration
cd ..
pnpm --dir frontend verify
docker compose build backend frontend
```

Evaluation regeneration commands are documented in [docs/evaluation.md](docs/evaluation.md). Media regeneration is documented in [docs/media/devpost/README.md](docs/media/devpost/README.md).

## Safety, privacy, and limitations

- No real student data, private school PDFs, or external curriculum sources are included.
- Demo uploads expire after 60 minutes and can be deleted immediately by their synthetic owner.
- Learning evidence is not a mastery score and never ranks or compares learners.
- No DistilBERT training has been completed.
- No real-school document validation or qualified curriculum review has been completed.
- The 100-package human-review queue remains

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 103 recognized source files, 377 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- PyTorch (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 132)

```
.env.example
.github/workflows/ci.yml
.github/workflows/deploy-azure.yml
.gitignore
AGENTS.md
artifacts/evaluation/assessment_metrics.json
artifacts/evaluation/classifier_metrics.json
artifacts/evaluation/extraction_metrics.json
artifacts/evaluation/retrieval_metrics.json
artifacts/models/.gitkeep
artifacts/models/tfidf-synthetic-baseline/MODEL_CARD.md
artifacts/README.md
backend/.dockerignore
backend/alembic.ini
backend/alembic/env.py
backend/alembic/script.py.mako
backend/alembic/versions/20260713_0001_foundation.py
backend/alembic/versions/20260713_0002_controlled_demo.py
backend/Dockerfile
backend/pyproject.toml
backend/src/classpath/__init__.py
backend/src/classpath/api/__init__.py
backend/src/classpath/api/demo.py
backend/src/classpath/api/dependencies.py
backend/src/classpath/api/health.py
backend/src/classpath/core/__init__.py
backend/src/classpath/core/config.py
backend/src/classpath/db/__init__.py
backend/src/classpath/db/base.py
backend/src/classpath/db/session.py
backend/src/classpath/fixtures/__init__.py
backend/src/classpath/fixtures/demo_curriculum.json
backend/src/classpath/fixtures/demo_learning_packages.json
backend/src/classpath/main.py
backend/src/classpath/models/__init__.py
backend/src/classpath/models/common.py
backend/src/classpath/models/curriculum.py
backend/src/classpath/models/demo.py
backend/src/classpath/models/profile.py
backend/src/classpath/models/user.py
backend/src/classpath/repositories/__init__.py
backend/src/classpath/repositories/demo.py
backend/src/classpath/schemas/__init__.py
backend/src/classpath/schemas/demo.py
backend/src/classpath/schemas/health.py
backend/src/classpath/scripts/__init__.py
backend/src/classpath/scripts/seed_demo_curriculum.py
backend/src/classpath/services/__init__.py
backend/src/classpath/services/assessment/__init__.py
backend/src/classpath/services/assessment/evaluator.py
backend/src/classpath/services/auth.py
backend/src/classpath/services/demo_content.py
backend/src/classpath/services/health.py
backend/src/classpath/services/nlp/__init__.py
backend/src/classpath/services/nlp/extraction.py
backend/src/classpath/services/retrieval/__init__.py
backend/src/classpath/services/retrieval/hashing.py
backend/src/classpath/services/storage.py
backend/tests/conftest.py
backend/tests/integration/test_controlled_demo_journey.py
backend/tests/integration/test_foundation_database.py
backend/tests/unit/test_assessment.py
backend/tests/unit/test_config.py
backend/tests/unit/test_demo_fixture.py
backend/tests/unit/test_extraction.py
backend/tests/unit/test_health_api.py
backend/tests/unit/test_model_metadata.py
backend/tests/unit/test_storage.py
CODEX_INSTRUCTIONS.md
data/DATASET_CARD.md
data/interim/.gitkeep
data/processed/.gitkeep
data/raw/.gitkeep
data/README.md
data/source_manifest.csv
data/synthetic/evaluation/annotations.json
data/synthetic/human_review_queue.jsonl
data/synthetic/questions.jsonl
docker-compose.yml
docs/adr/0001-governing-product-scope.md
docs/api.md
docs/architecture.md
docs/child-safety-and-privacy.md
docs/content-review.md
docs/dependencies.md
docs/deployment.md
docs/development.md
docs/evaluation.md
docs/media/devpost/README.md
docs/portfolio-evidence.md
docs/reports/extraction_failure_analysis.md
docs/threat-model.md
frontend/.dockerignore
frontend/.prettierignore
frontend/Dockerfile
frontend/eslint.config.js
frontend/index.html
frontend/nginx.conf
frontend/package.json
frontend/pnpm-workspace.yaml
frontend/src/api/client.ts
frontend/src/api/demo.ts
frontend/src/api/health.ts
frontend/src/App.tsx
frontend/src/components/BackendHealthCard.tsx
frontend/src/features/health/useBackendHealth.ts
frontend/src/main.tsx
frontend/src/pages/AboutPage.tsx
frontend/src/pages/DemoPage.tsx
frontend/src/pages/HomePage.tsx
frontend/src/styles.css
frontend/src/test/App.test.tsx
frontend/src/test/setup.ts
frontend/src/types/demo.ts
frontend/src/types/health.ts
frontend/src/vite-env.d.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
[12 more files omitted for size]
```

### Dependencies

- backend/pyproject.toml: alembic@>=1.14.1,<1.17, argon2-cffi@>=23.1,<26, fastapi@>=0.115.8,<0.118, httpx@>=0.28.1,<0.29, mypy@>=1.14.1,<1.18, pgvector@>=0.3.6,<0.5, psycopg[binary]@>=3.2.4,<3.3, pydantic-settings@>=2.7.1,<3, PyJWT@>=2.10,<3, PyMuPDF@>=1.25,<1.27, pytest@>=8.3.4,<9, pytest-cov@>=6.0.0,<7, python-multipart@>=0.0.20,<0.1, ruff@>=0.9.4,<0.13, scikit-learn@>=1.6,<1.9, sqlalchemy@>=2.0.37,<2.1, uvicorn[standard]@>=0.34.0,<0.36
- frontend/package.json: @eslint/js@^9.19.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.1.0, @testing-library/user-event@^14.5.2, @types/node@^22.10.10, @types/react@^18.3.18, @types/react-dom@^18.3.5, @vitejs/plugin-react@^4.3.4, eslint@^9.19.0, eslint-plugin-react-hooks@^5.0.0, eslint-plugin-react-refresh@^0.4.18, globals@^15.14.0, jsdom@^26.0.0, prettier@^3.4.2, react@^18.3.1, react-dom@^18.3.1, react-router-dom@^7.1.5, typescript@~5.7.3, typescript-eslint@^8.22.0, vite@^6.1.0, vitest@^3.0.5

### Recent commits (newest first)

- docs: remove Devpost submission materials
- ci: run backend formatting checks from package root
- docs: package LearnStep Devpost submission
- feat: build LearnStep controlled portfolio demo

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# Repository Rules for Codex and Other Coding Agents

These rules apply to the entire repository. Nested `AGENTS.md` files may add stricter local requirements but may not weaken these rules.

## 1. Product boundary

- Build a curriculum-grounded learning companion for CBSE-aligned Classes 5–8 Mathematics and Science.
- The product is NLP-first: structured educational-content extraction and a trained question classifier are core; RAG supports evidence retrieval.
- V1 must function without an external LLM.
- Build a teaching sequence—learn, example, practise, hint, explain, revise—not a generic `chat with PDF` interface.
- Do not build student ranking, admissions, high-stakes grading, proctoring, surveillance or behavioural advertising.
- Do not infer intelligence, personality, attention, emotion, disability, mental state or future academic ability.
- Do not introduce agents, LangChain/LlamaIndex orchestration, microservices, Kubernetes, Kafka, Redis, Celery, a graph database or distributed training unless measured evidence and explicit user approval justify it.
- Do not train a large language model from scratch. DistilBERT-sized classifier fine-tuning is in scope.

## 2. Source-of-truth priority

When requirements conflict, use this order:

1. the user's latest explicit instruction;
2. this `AGENTS.md`;
3. `PRD.md` acceptance criteria and safety constraints;
4. `CODEX_INSTRUCTIONS.md` implementation guidance;
5. existing tests and documentation;
6. reasonable engineering judgment.

Pause when a decision would materially change product scope, child-data policy, curriculum claims, model objective or deployment cost.

## 3. Engineering conduct

- Inspect before editing and preserve unrelated/user changes.
- Make small coherent changes that keep the repository runnable.
- Prefer plain typed testable functions over framework magic.
- Reuse dependencies before adding new ones.
- Every dependency needs a direct use, compatible license, pinned range and maintenance justification.
- Do not leave commented-out implementations, fake TODO paths, dead flags or placeholder success responses.
- Do not suppress type, lint, security or test failures without a specific documented reason.
- Never claim a test, metric, download, review or deployment occurred unless it did.
- Never weaken tests merely to pass them; fix behaviour or document a genuine requirement change.
- Treat filenames, PDFs, extracted text, questions and student answers as untrusted input.

## 4. Scope and simplicity

- Use one modular monolith: React frontend, FastAPI backend, PostgreSQL/pgvector database and offline ML package.
- Use FastAPI background tasks or a small database-backed state flow in v1; preserve a boundary for a future queue.
- Start retrieval with metadata filters and pgvector cosine similarity.
- Use deterministic lesson templates, reviewed question banks and answer-type-specific evaluation.
- Add OCR, voice, generative models, hybrid search, reranking, more boards/languages/cla
[truncated — 14437 more characters]
```

### docs/content-review.md

```markdown
# Content and package review

Bundled lessons, questions, answers, hints, and evaluation corpora are original synthetic fixtures marked `synthetic_demo_not_educationally_reviewed` or `unreviewed_synthetic`. They are not verified curriculum and must not be relabeled without a documented reviewer decision.

## 100-package review queue

Regenerate `data/synthetic/human_review_queue.jsonl` with:

```powershell
.\backend\.venv\Scripts\python.exe scripts\generate_synthetic_evaluation_data.py
```

Each of the 100 rows starts with `pending_human_review`, null reviewer, null notes, and null decisions for:

- source support;
- class appropriateness;
- clarity and answerability;
- answer/rubric correctness;
- staged-hint quality;
- absence of invented claims.

A qualified human reviewer must record identity/role, date, decision, and notes. Codex output is not counted as human review. Current completed-review count: **0/100**.

Real content may be considered only after owner, URL, access date, license/terms, intended use, redistribution/training decision, and reviewer are recorded in `data/source_manifest.csv`.

```

### docker-compose.yml

```yaml
name: learnstep

services:
  db:
    image: pgvector/pgvector:0.8.0-pg16
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-classpath}
      POSTGRES_USER: ${POSTGRES_USER:-classpath}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-classpath_dev}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 3s
      timeout: 3s
      retries: 20
      start_period: 5s

  backend:
    build:
      context: ./backend
      target: runtime
    environment:
      APP_NAME: ${APP_NAME:-LearnStep}
      APP_ENV: ${APP_ENV:-development}
      APP_DEBUG: ${APP_DEBUG:-false}
      DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://classpath:classpath_dev@db:5432/classpath}
      EMBEDDING_MODEL: ${EMBEDDING_MODEL:-sentence-transformers/all-MiniLM-L6-v2}
      EMBEDDING_DIMENSION: ${EMBEDDING_DIMENSION:-384}
      CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:5173"]}
      DEMO_TOKEN_SECRET: ${DEMO_TOKEN_SECRET:-local-demo-secret-change-before-deployment}
      DEMO_TOKEN_MINUTES: ${DEMO_TOKEN_MINUTES:-60}
      PRIVATE_DATA_ROOT: ${PRIVATE_DATA_ROOT:-/app/private-data}
      UPLOAD_MAX_BYTES: ${UPLOAD_MAX_BYTES:-5000000}
      UPLOAD_MAX_PAGES: ${UPLOAD_MAX_PAGES:-10}
      UPLOAD_MIN_TEXT_CHARS: ${UPLOAD_MIN_TEXT_CHARS:-40}
      DEMO_UPLOAD_TTL_MINUTES: ${DEMO_UPLOAD_TTL_MINUTES:-60}
    ports:
      - "${BACKEND_PORT:-8000}:8000"
    volumes:
      - demo_uploads:/app/private-data
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/live', timeout=2)"]
      interval: 5s
      timeout: 3s
      retries: 12
      start_period: 5s

  frontend:
    build:
      context: ./frontend
      target: runtime
      args:
        VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:8000}
    ports:
      - "${FRONTEND_PORT:-5173}:80"
    depends_on:
      backend:
        condition: service_healthy

volumes:
  postgres_data:
  demo_uploads:

```

### frontend/Dockerfile

```
FROM node:22-alpine AS builder

ARG VITE_API_BASE_URL=http://localhost:8000
ENV CI=true \
    VITE_API_BASE_URL=$VITE_API_BASE_URL
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.7.0 --activate
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:1.27-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

```

### backend/Dockerfile

```
FROM python:3.12-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /build
COPY pyproject.toml ./
COPY src ./src
RUN python -m pip install --prefix=/install .

FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PYTHONPATH=/app/src

RUN addgroup --system classpath && adduser --system --ingroup classpath classpath
WORKDIR /app
RUN mkdir -p /app/private-data && chown classpath:classpath /app/private-data
COPY --from=builder /install /usr/local
COPY alembic.ini ./alembic.ini
COPY alembic ./alembic
COPY src ./src
USER classpath
EXPOSE 8000
CMD ["uvicorn", "classpath.main:app", "--host", "0.0.0.0", "--port", "8000"]

```

### frontend/package.json

```
{
  "name": "classpath-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "packageManager": "pnpm@11.7.0",
  "engines": {
    "node": ">=22"
  },
  "scripts": {
    "dev": "vite --host 0.0.0.0",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "lint": "eslint . --max-warnings=0",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "typecheck": "tsc -b --pretty false",
    "test": "vitest",
    "verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test -- --run && pnpm build"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^7.1.5"
  },
  "devDependencies": {
    "@eslint/js": "^9.19.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.1.0",
    "@testing-library/user-event": "^14.5.2",
    "@types/node": "^22.10.10",
    "@types/react": "^18.3.18",
    "@types/react-dom": "^18.3.5",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^9.19.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.18",
    "globals": "^15.14.0",
    "jsdom": "^26.0.0",
    "prettier": "^3.4.2",
    "typescript": "~5.7.3",
    "typescript-eslint": "^8.22.0",
    "vite": "^6.1.0",
    "vitest": "^3.0.5"
  }
}

```

### backend/pyproject.toml

```
[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"

[project]
name = "classpath-backend"
version = "0.1.0"
description = "Controlled synthetic portfolio demo for the LearnStep learning companion"
requires-python = ">=3.12,<3.13"
license = { text = "Proprietary" }
dependencies = [
  "alembic>=1.14.1,<1.17",
  "argon2-cffi>=23.1,<26",
  "fastapi>=0.115.8,<0.118",
  "pgvector>=0.3.6,<0.5",
  "psycopg[binary]>=3.2.4,<3.3",
  "PyJWT>=2.10,<3",
  "PyMuPDF>=1.25,<1.27",
  "pydantic-settings>=2.7.1,<3",
  "python-multipart>=0.0.20,<0.1",
  "sqlalchemy>=2.0.37,<2.1",
  "uvicorn[standard]>=0.34.0,<0.36",
]

[project.optional-dependencies]
dev = [
  "httpx>=0.28.1,<0.29",
  "mypy>=1.14.1,<1.18",
  "pytest>=8.3.4,<9",
  "pytest-cov>=6.0.0,<7",
  "ruff>=0.9.4,<0.13",
]
ml = [
  "scikit-learn>=1.6,<1.9",
]

[tool.hatch.build.targets.wheel]
packages = ["src/classpath"]

[tool.hatch.build]
include = ["src/classpath/**/*.py", "src/classpath/**/*.json"]

[tool.pytest.ini_options]
addopts = "--strict-config --strict-markers -ra"
testpaths = ["tests"]
markers = ["integration: requires a migrated PostgreSQL database with pgvector"]

[tool.ruff]
target-version = "py312"
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "ASYNC", "S", "RUF"]
ignore = ["S101"]

[tool.ruff.lint.per-file-ignores]
"alembic/versions/*.py" = ["E501"]

[tool.mypy]
python_version = "3.12"
strict = true
plugins = ["pydantic.mypy"]
mypy_path = "src"

[[tool.mypy.overrides]]
module = ["alembic.*", "fitz.*", "pgvector.*", "sklearn.*", "PIL.*"]
ignore_missing_imports = true

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import { App } from "./App";
import "./styles.css";

const rootElement = document.getElementById("root");
if (rootElement === null) {
  throw new Error("Application root element is missing");
}

createRoot(rootElement).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

```

### frontend/src/App.tsx

```typescript
import { NavLink, Route, Routes } from "react-router-dom";

import { AboutPage } from "./pages/AboutPage";
import { HomePage } from "./pages/HomePage";
import { DemoPage } from "./pages/DemoPage";

export function App() {
  return (
    <div className="app-shell">
      <a className="skip-link" href="#main-content">
        Skip to main content
      </a>
      <header className="site-header">
        <NavLink className="brand" to="/" aria-label="LearnStep home">
          <span className="brand__mark" aria-hidden="true">
            L
          </span>
          <span>LearnStep</span>
        </NavLink>
        <nav aria-label="Primary navigation">
          <NavLink to="/" end>
            Home
          </NavLink>
          <NavLink to="/demo">Demo</NavLink>
          <NavLink to="/about">About</NavLink>
        </nav>
      </header>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/demo" element={<DemoPage />} />
        <Route path="*" element={<HomePage />} />
      </Routes>
      <footer>
        <p>
          LearnStep · Upload. Learn. Ace it. · synthetic portfolio demonstration
        </p>
      </footer>
    </div>
  );
}

```

### backend/src/classpath/main.py

```python
import asyncio
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager, suppress
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from classpath import __version__
from classpath.api.demo import router as demo_router
from classpath.api.health import router as health_router
from classpath.core.config import Settings, get_settings
from classpath.db.session import get_session_factory
from classpath.repositories.demo import purge_expired_documents


def create_app(settings: Settings | None = None) -> FastAPI:
    active_settings = settings or get_settings()

    async def cleanup_worker() -> None:
        while True:

            def cleanup_once() -> None:
                with get_session_factory(active_settings.database_url)() as session:
                    purge_expired_documents(
                        session, storage_root=Path(active_settings.private_data_root)
                    )

            await asyncio.to_thread(cleanup_once)
            await asyncio.sleep(60)

    @asynccontextmanager
    async def lifespan(_: FastAPI) -> AsyncIterator[None]:
        task = asyncio.create_task(cleanup_worker()) if active_settings.app_env != "test" else None
        yield
        if task is not None:
            task.cancel()
            with suppress(asyncio.CancelledError):
                await task

    application = FastAPI(
        title=active_settings.app_name,
        version=__version__,
        debug=active_settings.app_debug,
        description=(
            "Controlled LearnStep portfolio demo. All examples and seeded concepts are "
            "explicitly synthetic."
        ),
        lifespan=lifespan,
    )
    application.state.settings = active_settings
    application.dependency_overrides[get_settings] = lambda: active_settings
    application.add_middleware(
        CORSMiddleware,
        allow_origins=active_settings.cors_origins,
        allow_credentials=False,
        allow_methods=["GET", "POST", "DELETE"],
        allow_headers=["Accept", "Authorization", "Content-Type"],
    )
    application.include_router(health_router)
    application.include_router(demo_router)
    return application


app = create_app()

```

[92 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]