# Project export: ExamTwin

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: We’re two students, who got tired of re-explaining every exam to AI. ExamTwin turns your materials into realistic mock exams(AI workflows) and shows exactly where you need to improve(data analytics)
- Devpost: https://devpost.com/software/not-yet-u2kc0l
- GitHub: https://github.com/Rokki-Khazratov/OpenAI-buildweek
- Demo: https://examtwin-prep-rokkis-projects.vercel.app/
- Video: https://www.youtube.com/embed/dJqnmjUGrmU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Bek Khazratov (83 commits), Ilia Malkin (3 commits)

## Devpost submission (written by the team)

### Inspiration

We are students ourselves, and we built ExamTwin because we were looking for a tool like this and could not find one. Study materials are usually scattered across PDFs, notes, old exams, university platforms, group chats, and shared folders. Existing tools can generate quizzes or flashcards, but they rarely understand the real structure of a specific exam, track performance across multiple attempts, or help students prepare together. We wanted one place where students could upload their real course materials, recreate the format of their exam, generate realistic mock exams, understand their weak areas, and improve through repeated practice.

### What it does

ExamTwin is an adaptive exam preparation platform that turns real study materials into personalized mock exams. Students can: create a study space for a course or exam; upload notes, past exams, rubrics, syllabi, and other learning materials; generate realistic mock exams based on those materials; complete exams directly inside the platform; receive structured feedback and performance analytics; identify recurring mistakes and weak skills; generate follow-up exams focused on those weaknesses; track progress across multiple attempts; share study spaces with classmates; create groups and prepare together; publish reusable study spaces in a community library. Unlike a generic quiz generator, ExamTwin focuses on the full preparation cycle: materials → exam structure → mock exam → evaluation → analytics → adaptive next exam

### How we built it

We built ExamTwin as a modular full-stack platform. The frontend is built with Next.js, React, and TypeScript. It provides the study dashboard, material upload flows, mock exam interface, analytics views, shared study spaces, and community features. The backend is built with FastAPI, Pydantic, SQLAlchemy, and PostgreSQL. It manages authentication, users, study spaces, uploaded materials, exams, attempts, responses, analytics, and collaboration features. The AI layer uses OpenAI models to: analyze uploaded study materials; extract topics, skills, question types, and exam structure; generate grounded mock exams; evaluate open-ended responses; explain mistakes; map answers to skills; create personalized follow-up exams. The analytics layer uses Python and pandas to calculate scores, skill-level performance, trends, consistency, confidence, recurring mistakes, and readiness estimates. We designed the system to remain flexible across very different subjects, from IELTS and university entrance exams to mathematics, law, medicine, and quantum physics. Codex was used throughout the development process for implementation, refactoring, debugging, architecture decisions, API design, and testing.

### Challenges we ran into

The hardest challenge was making the system flexible enough to support very different kinds of exams. An IELTS writing task, a multiple-choice biology test, and a quantum physics problem set cannot be evaluated using the same fixed structure. We therefore avoided hardcoding the platform around one subject and instead created reusable abstractions for: exam sections; question types; skills; scoring rules; rubrics; difficulty levels; answer formats. Another challenge was keeping generated exams grounded in the uploaded materials. The system needs to create new questions without inventing unsupported facts or ignoring the original exam format. We also had to separate AI-generated insights from deterministic analytics. Language models are useful for evaluation and explanation, but scores, trends, attempt histories, and statistical metrics must remain structured, reproducible, and transparent. Finally, we had to control the scope. ExamTwin combines AI generation, analytics, collaboration, community features, and gamification, which made it very easy to attempt building an entire university ecosystem in one week. Humanity survives scope creep once again.

### Accomplishments we're proud of

We are proud that ExamTwin became more than a basic PDF-to-quiz generator. We built a complete preparation workflow where students can upload real materials, generate exams, complete them, receive feedback, understand their weak skills, and continue with personalized practice. We are especially proud of: the flexible exam model; adaptive mock exam generation; skill-level analytics across multiple attempts; support for both objective and open-ended questions; reusable study spaces; collaborative preparation; a clean and focused user experience; building a working product in a very short period of time. The project was created by students for a problem we experience personally, which helped us make product decisions based on real needs rather than imaginary user personas.

### What we learned

We learned that generating questions is the easy part. Building a reliable learning system around those questions is much harder. A useful education product needs more than an AI prompt. It needs structured exam models, clear evaluation rules, historical data, confidence estimates, transparent analytics, and a feedback loop that actually changes what the student studies next. We also learned that educational analytics must be domain-independent. The platform cannot assume that every subject consists of vocabulary terms or multiple-choice questions. Most importantly, we learned that familiar ideas can become more valuable when they are combined around a clear user problem. ExamTwin uses known components such as document analysis, mock exams, progress tracking, and study communities, but connects them into one continuous exam preparation workflow.

### What's next

Our next steps are to: improve exam blueprint extraction; support more document formats; add stronger rubric-based evaluation; introduce richer skill graphs; improve readiness prediction; add spaced repetition and personalized study plans; expand group challenges and gamification; build a larger community library of reusable courses and exams; allow teachers and institutions to create verified study spaces; add deeper analytics for long-term learning progress; expand support to professional certifications and standardized exams. Our long-term goal is to make ExamTwin a shared preparation layer for any exam: students should be able to find or create a study space, practice under realistic conditions, understand exactly where they are weak, and improve together.

## README (from the GitHub repository)

# ExamTwin

> Build a faithful digital twin of a real exam, practise it under realistic conditions, and turn every attempt into a better next mock.

ExamTwin is an adaptive exam-preparation platform created for OpenAI Build Week. It helps students organise university exams, attach the material that defines each exam, describe its structure and rules, run a focused mock session, and retain results for future feedback and adaptation.

The product is deliberately exam-centred:

- a **Subject** is a lightweight category such as Quantum Physics;
- an **Exam** owns its context files, blueprint, scenario, timing, scoring rules, attempts, feedback and statistics;
- a **Class** shares either a whole Subject or selected Exams with a group;
- an **Attempt** is an archived mock run with answers, score, duration and feedback.

## Current status

The repository contains a working, API-backed P0 product, the complete P1 artifact pipeline, and an opt-in P2 grounded-AI loop. Demo mode remains available for visual review, while normal mode persists the complete core flow in PostgreSQL.

| Area | Status |
|---|---|
| Authentication and profile | Implemented in the API and frontend flows |
| Subject CRUD | Implemented |
| Nested Exam CRUD | Implemented |
| Exam data, blueprint, scenario and rules editor | Persisted through the API with optimistic configuration versions |
| Exam Run simulation | Backend-generated mock, durable attempt, autosave, reload/resume and immutable submit |
| Personal statistics and analytics | `analytics.v2`, canonical observations, versioned snapshots, data-quality gates, trends, readiness and `adaptive.v2` implemented |
| Library publishing and cloning | Implemented with safe public snapshots and independent private clones |
| Class CRUD, membership and progress | Implemented with owner-only aggregate analytics, small-group suppression, comparable exam scope and experiment events |
| PostgreSQL models and migrations | Implemented through D3, including observations, snapshots, taxonomy, shadow results and cohort events |
| Artifact ingestion and retrieval | Implemented: private upload, parsing, chunking, embeddings and owned-exam vector retrieval |
| Vertex AI generation and evaluation | Implemented with `gemini-3.5-flash` when `APP_VERTEX_PROJECT` is configured; deterministic fallback remains available |
| Background worker pipeline | Implemented with durable Dramatiq/Redis jobs and restart-safe retries |

The UI distinguishes the deterministic fallback from Vertex AI generation. AI questions are grounded in retrieved chunks, retain source citations, and use prior weak-topic evidence to adapt later mocks.

## Product preview

All product images below were captured from the running local application in the native browser at a laptop/desktop responsive breakpoint. Each screenshot includes the complete page width; no interface sections are cropped or reconstructed.

### Dashboard (updated local capture)

The home view keeps the next preparation action prominent and shows the student’s current exam context. This screenshot was freshly captured from the local demo at a wide desktop viewport.

![ExamTwin dashboard](materials/01-dashboard.jpg)

### Data-science analytics

The analytics views separate readiness from evidence confidence and make score movement inspectable over time.

![Exam statistics and readiness](materials/ds1.png)

![Score trajectory and attempt history](materials/ds2.png)

### Subjects

Subjects are intentionally simple categories. Exam-specific readiness, dates and content live on Exams instead.

![Subjects overview](materials/02-subjects.jpg)

### Subject workspace

Each Subject exposes its nested Exams and Classes without becoming a second exam-detail surface.

![Subject detail with nested exams](materials/03-subject-detail.jpg)

### Exams

The global Exams index provides a cross-subject view and the primary creation entry point.

![Exams overview](materials/04-exams.jpg)

### Exam workspace

Exam Detail is the control centre for data sources, blueprint, scenario, rules and attempt history.

![Exam detail workspace](materials/05-exam-detail.jpg)

### Five-step exam creation

The creation flow separates basics, data, blueprint, generation scenario/rules and final review.

![Exam creation wizard](materials/06-exam-create.jpg)

### Focused Exam Run

The simulation uses a distraction-free shell and prepares a mock from the configured exam structure.

![Exam Run preparation screen](materials/07-exam-run.jpg)

### Exam statistics

Statistics are scoped to an Exam and clearly label the confidence of insights while attempt coverage is still low.

![Exam statistics](materials/08-exam-statistics.jpg)

### Classes

Classes can be scoped to a complete Subject or a selected group of Exams.

![Classes overview](materials/09-classes.jpg)

## Demo video

Watch the ExamTwin product walkthrough on YouTube: [ExamTwin — OpenAI Build Week](https://www.youtube.com/watch?v=dJqnmjUGrmU).

## Why ExamTwin

Students usually prepare from fragmented notes, syllabi, rubrics, past papers and generic question banks. Conventional generators may cover the same topic, but they rarely reproduce the actual constraints that shape performance: section order, question types, point distribution, allowed materials, time pressure and grading rules.

ExamTwin treats those constraints as first-class data. Its target feedback loop is:

1. Create a Subject category.
2. Create an Exam inside that Subject.
3. Add past papers, rubrics, notes, learning targets and other context.
4. Review the extracted or manually entered blueprint.
5. Configure scenario and rules such as timing, points, pass mark and penalties.
6. Generate a grounded mock exam.
7. Complete it in the focused Exam Run interface.
8. Archive answers, result and feedback.
9. Use evidence from previous attempts to produce a more useful next mock.

## Domain model

```text
User
└── Subject (category)
    ├── Exam
    │   ├── Data sources / context files
    │   ├── Blueprint sections
    │   ├── Generation scenario
    │   ├── Rules and grading notes
    │   └── Attempts
    │       ├── Answers
    │       ├── Result
    │       └── Feedback
    └── Class
        └── All subject exams or selected exams
```

This boundary prevents Subject pages from accumulating exam-only responsibilities and allows multiple finals, midterms, oral exams or certification attempts to coexist under one course category.

## Architecture

```text
Browser
  │
  ├── Next.js 16 / React 19 frontend and BFF
  │     ├── App Router pages
  │     ├── API-backed product state (optional local demo mode)
  │     ├── HTTP-only session cookies and authenticated API proxy
  │     └── focused exam-session layout
  │
  └── FastAPI API
        ├── authentication and JWT lifecycle
        ├── ownership-protected CRUD and attempt lifecycle
        ├── SQLAlchemy async models
        ├── Alembic migrations
        └── PostgreSQL / pgvector

files → object storage → worker → parsing/chunking → embeddings/retrieval
      → Vertex AI grounded generation → mock → evaluation → mastery
```

### Technology stack

| Layer | Technology | Responsibility |
|---|---|---|
| Web | Next.js 16, React 19, TypeScript, Tailwind CSS 4 | Product interface and exam session |
| API | FastAPI, Pydantic, SQLAlchemy asyncio | Auth, ownership and domain APIs |
| Database | PostgreSQL 17 with pgvector | Relational records and future vector retrieval |
| Migrations | Alembic | Versioned database schema |
| Cache/jobs | Redis and Dramatiq worker | Durable artifact processing and retries |
| AI | Vertex AI / Google Gen AI SDK | Gemini 3.5 Flash generation, embeddings, evaluation and adaptation |
| Runtime | Docker Compose | Local PostgreSQL, Redis and API services |

## Repository layout

```text
.
├── backend/
│   ├── api/app/                 # FastAPI routes, services, models and configuration
│   ├── api/migrations/          # Alembic migrations
│   ├── api/tests/               # Uni

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 226 recognized source files, 989 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (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: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 258)

```
.dockerignore
.env.production.example
.github/workflows/ci.yml
.gitignore
APP_RELEASE_HARDENING_PLAN.md
backend/.env.example
backend/alembic.ini
backend/api/app/__init__.py
backend/api/app/ai/__init__.py
backend/api/app/ai/prompts.py
backend/api/app/ai/provider.py
backend/api/app/ai/schemas.py
backend/api/app/ai/validators.py
backend/api/app/ai/vertex.py
backend/api/app/api/__init__.py
backend/api/app/api/router.py
backend/api/app/api/routes/__init__.py
backend/api/app/api/routes/analytics.py
backend/api/app/api/routes/artifacts.py
backend/api/app/api/routes/attempts.py
backend/api/app/api/routes/auth.py
backend/api/app/api/routes/blueprints.py
backend/api/app/api/routes/classes.py
backend/api/app/api/routes/exams.py
backend/api/app/api/routes/health.py
backend/api/app/api/routes/library.py
backend/api/app/api/routes/subjects.py
backend/api/app/api/routes/users.py
backend/api/app/api/routes/workspaces.py
backend/api/app/api/schemas/__init__.py
backend/api/app/api/schemas/analytics.py
backend/api/app/api/schemas/artifact.py
backend/api/app/api/schemas/attempt.py
backend/api/app/api/schemas/auth.py
backend/api/app/api/schemas/blueprint.py
backend/api/app/api/schemas/classroom.py
backend/api/app/api/schemas/exam.py
backend/api/app/api/schemas/health.py
backend/api/app/api/schemas/library.py
backend/api/app/api/schemas/subject.py
backend/api/app/api/schemas/workspace.py
backend/api/app/core/__init__.py
backend/api/app/core/config.py
backend/api/app/core/dependencies.py
backend/api/app/core/logging.py
backend/api/app/core/middleware.py
backend/api/app/core/security.py
backend/api/app/db/__init__.py
backend/api/app/db/base.py
backend/api/app/db/dependencies.py
backend/api/app/db/models/__init__.py
backend/api/app/db/models/analytics.py
backend/api/app/db/models/artifact.py
backend/api/app/db/models/attempt.py
backend/api/app/db/models/audit.py
backend/api/app/db/models/auth.py
backend/api/app/db/models/blueprint.py
backend/api/app/db/models/classroom.py
backend/api/app/db/models/exam.py
backend/api/app/db/models/library.py
backend/api/app/db/models/user.py
backend/api/app/db/models/workspace.py
backend/api/app/db/session.py
backend/api/app/integrations/__init__.py
backend/api/app/integrations/storage.py
backend/api/app/integrations/vertex_ai.py
backend/api/app/main.py
backend/api/app/modules/__init__.py
backend/api/app/modules/analytics/__init__.py
backend/api/app/modules/analytics/model.py
backend/api/app/modules/analytics/service.py
backend/api/app/modules/artifacts/__init__.py
backend/api/app/modules/artifacts/parsing.py
backend/api/app/modules/artifacts/processor.py
backend/api/app/modules/artifacts/service.py
backend/api/app/modules/artifacts/tasks.py
backend/api/app/modules/auth/__init__.py
backend/api/app/modules/auth/dependencies.py
backend/api/app/modules/auth/service.py
backend/api/app/modules/blueprints/__init__.py
backend/api/app/modules/blueprints/service.py
backend/api/app/modules/classes/__init__.py
backend/api/app/modules/classes/service.py
backend/api/app/modules/exams/__init__.py
backend/api/app/modules/exams/attempt_service.py
backend/api/app/modules/exams/retrieval.py
backend/api/app/modules/exams/service.py
backend/api/app/modules/library/__init__.py
backend/api/app/modules/library/service.py
backend/api/app/modules/workspaces/__init__.py
backend/api/app/modules/workspaces/service.py
backend/api/Dockerfile
backend/api/migrations/env.py
backend/api/migrations/script.py.mako
backend/api/migrations/versions/20260714_0001_initial_foundation.py
backend/api/migrations/versions/20260714_0002_subject_exams_classes.py
backend/api/migrations/versions/20260716_0003_exam_configuration.py
backend/api/migrations/versions/20260716_0004_mocks_attempts.py
backend/api/migrations/versions/20260716_0005_artifact_ingestion.py
backend/api/migrations/versions/20260719_0006_ai_retrieval.py
backend/api/migrations/versions/20260720_0007_blueprints_evaluations.py
backend/api/migrations/versions/20260720_0008_library_classes.py
backend/api/migrations/versions/20260721_0009_ds_foundation.py
backend/api/migrations/versions/20260721_0010_class_entitlements.py
backend/api/README.md
backend/api/tests/__init__.py
backend/api/tests/conftest.py
backend/api/tests/fixtures/analytics_benchmark.v1.json
backend/api/tests/test_ai_contracts.py
backend/api/tests/test_analytics_benchmark.py
backend/api/tests/test_analytics_model.py
backend/api/tests/test_artifacts.py
backend/api/tests/test_auth_workspaces_integration.py
backend/api/tests/test_exam_drafts.py
backend/api/tests/test_health.py
backend/api/tests/test_m6_library_classes.py
backend/api/tests/test_metadata.py
backend/api/tests/test_openapi.py
backend/api/tests/test_security.py
backend/compose.yaml
[138 more files omitted for size]
```

### Dependencies

- backend/pyproject.toml: alembic@>=1.14,<2, asyncpg@>=0.30,<1, boto3@>=1.39,<2, dramatiq[redis]@>=1.18,<2, email-validator@>=2.2,<3, fastapi@>=0.128,<0.129, google-genai@>=1.33,<2, httpx@>=0.28,<1, mypy@>=1.14,<2, pgvector@>=0.4,<1, pwdlib[argon2]@>=0.3,<1, pydantic-settings@>=2.7,<3, pyjwt@>=2.10,<3, pymupdf@>=1.26,<2, pytest@>=8.3,<9, pytest-asyncio@>=0.25,<1, python-docx@>=1.2,<2, python-multipart@>=0.0.20,<1, ruff@>=0.9,<1, sqlalchemy[asyncio]@>=2.0.36,<3, uvicorn[standard]@>=0.34,<1
- frontend/package.json: @fontsource-variable/instrument-sans@^5.2.8, @tailwindcss/postcss@^4, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.2.10, jsdom@^29.1.1, lucide-react@^1.24.0, next@16.2.10, react@19.2.4, react-dom@19.2.4, tailwindcss@^4, typescript@^5, vitest@^4.1.10

### Recent commits (newest first)

- docs: refresh dashboard screenshot and analytics gallery
- fix: avoid public self-fetch during registration
- fix: avoid public self-fetch during registration
- fix: expose MinIO to the local reverse proxy
- fix: expose minio to local reverse proxy
- Pre-deploy: harden app and add production Docker deployment
- chore: add production docker deployment
- fix: scope class access and protect cohort analytics
- Merge exam drafts and workspace navigation
- feat(frontend): add workspace discovery controls
- feat(frontend): add exam draft save flow
- feat(exams): support explicit draft persistence
- Merge complete D0-D3 analytics stack
- docs(ds): mark D0-D3 implementation complete
- test(ds): add complete golden and Vertex proof flows
- feat(frontend): surface adaptive v2 and safe cohort signals
- feat(ds): add privacy-safe cohort intelligence
- feat(ds): ship versioned analytics foundation and policy v2
- Merge D0-D3 data science execution roadmap
- docs(ds): define D0-D3 execution roadmap

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

### DEPLOYMENT_GUIDE.md

```markdown
# Production deployment guide

This repository now contains a self-hosted production stack in `docker-compose.production.yml`:

- Next.js web app, bound only to `127.0.0.1:3000`;
- FastAPI API and Dramatiq worker;
- PostgreSQL with pgvector, Redis, and private MinIO storage;
- a one-shot migration service that must finish before API/worker start.

Use a reverse proxy such as Caddy or Nginx for public HTTPS. Do **not** expose PostgreSQL, Redis, MinIO, or the API directly to the Internet.

## Server prerequisites

1. Ubuntu 22.04+ (or equivalent), Docker Engine and Docker Compose v2.
2. A DNS record for the web domain, for example `app.example.com`.
3. Optional: a second HTTPS hostname such as `files.example.com` for MinIO uploads/downloads. It must proxy to `http://127.0.0.1:9000`; if you do not expose MinIO publicly, direct browser uploads cannot work.
4. Vertex AI credentials only if `APP_AI_ENABLED=true`.

## First deployment

```bash
git clone https://github.com/Rokki-Khazratov/OpenAI-buildweek.git /opt/examtwin
cd /opt/examtwin
cp .env.production.example .env.production
chmod 600 .env.production
mkdir -p secrets
```

Edit `.env.production`. Generate secrets locally on the server:

```bash
openssl rand -base64 48
```

Set `APP_CORS_ORIGINS` to the exact public web origin and `APP_STORAGE_PUBLIC_ENDPOINT_URL` to the exact MinIO public origin. When AI is enabled, copy the Google Application Default Credentials JSON to the absolute path set in `GOOGLE_APPLICATION_CREDENTIALS_HOST_PATH`; keep it mode `600`.

Validate configuration and start:

```bash
docker compose --env-file .env.production -f docker-compose.production.yml config --quiet
docker compose --env-file .env.production -f docker-compose.production.yml up -d --build
docker compose --env-file .env.production -f docker-compose.production.yml ps
curl -fsS http://127.0.0.1:3000/login > /dev/null
```

Check migration/API logs if a service is not healthy:

```bash
docker compose --env-file .env.production -f docker-compose.production.yml logs --tail=100 migrate api worker
```

## Caddy example

Install Caddy using its official package, then configure `/etc/caddy/Caddyfile`:

```caddyfile
app.example.com {
    reverse_proxy 127.0.0.1:3000
}

files.example.com {
    reverse_proxy 127.0.0.1:9000
}
```

Reload Caddy after validating the file. Caddy provisions TLS automatically when DNS points to the server and ports 80/443 are reachable.

## Update / rollback

```bash
cd /opt/examtwin
git fetch origin
git checkout main
git pull --ff-only origin main
docker compose --env-file .env.production -f docker-compose.production.yml up -d --build
```

The migration service runs on every deploy and Alembic applies only pending revisions. Before an irreversible database migration, take a PostgreSQL backup:

```bash
docker compose --env-file .env.production -f docker-compose.production.yml exec -T postgres \
  sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%F-%H%M).sql
```

To roll back ap
[truncated — 1016 more characters]
```

### APP_RELEASE_HARDENING_PLAN.md

```markdown
# ExamTwin application release hardening plan

## 1. Scope and release rule

This plan covers only the application code and its release proof. Landing-page and video
work are owned separately and are intentionally excluded.

The implementation order is fixed:

```text
R0 freeze and reproduce
→ R1 correct Class entitlements
→ R2 enforce cohort privacy
→ R3 prove the complete normal-mode runtime
→ R4 code-only release audit
→ deploy only after a PASS verdict
```

No deployment or Devpost submission may rely on demo-mode evidence. The release gate is a
fresh `NEXT_PUBLIC_DEMO_MODE=false` run against PostgreSQL, Redis, MinIO, the worker and the
FastAPI service.

## 2. Target access contract

Class membership is an explicit entitlement. It must not be converted into unrestricted
Subject workspace membership.

| Caller | Subject metadata | Exam access | Class visibility | Owner analytics |
|---|---|---|---|---|
| Subject owner | Full | Every Exam | Every owned Class | Yes |
| Subject-scope Class member | Read | Every Exam in that Subject | Only Classes they belong to | No |
| Selected-exams Class member | Read | Union of Exams selected by their Classes | Only Classes they belong to | No |
| Removed member | None unless another Class still grants it | None unless another Class still grants it | Removed Class hidden | No |
| Unrelated user | None | None | None | No |

If one user belongs to multiple Classes in the same Subject, effective access is the union
of those Class grants. Removing one membership must preserve access granted by another.

`WorkspaceMember` remains the direct workspace-membership model. Class membership must be
resolved from `ClassMember`, `Classroom.exam_scope` and `ClassExam`; it must not create or
delete unrelated direct grants.

---

# R0 — Freeze and reproduce

## Goal

Capture a failing regression for every audited defect before changing authorization code.

## Work

1. Create branch `codex/app-release-hardening` from the current `main`.
2. Keep the existing untracked landing/video work untouched.
3. Start PostgreSQL and run all 31 backend tests with `TEST_DATABASE_URL` configured.
4. Add failing integration cases to
   `backend/api/tests/test_m6_library_classes.py`:
   - a selected-exam member cannot list or read an unselected Exam;
   - a selected-exam member can read the selected Exam;
   - a subject-scope member can read every Exam;
   - a member sees only Classes they belong to;
   - removal immediately revokes the removed Class grant;
   - deletion of a Class revokes its grant;
   - another Class in the same Subject preserves its independent grant;
   - an unrelated user receives the same `404` response as a nonexistent resource.
5. Add cohort regression cases:
   - three registered members but one eligible learner stays suppressed;
   - three registered members but two eligible learners stays suppressed;
   - three eligible non-owner learners publishes aggregates;
   - the owner is not counted toward the privacy thresho
[truncated — 11296 more characters]
```

### frontend/Dockerfile

```
FROM node:22-alpine AS dependencies

WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci

FROM dependencies AS build

COPY frontend ./
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:22-alpine AS runtime

WORKDIR /app
ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1

COPY --from=build /app ./

EXPOSE 3000
CMD ["npm", "run", "start"]

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": "^20.19.0 || >=22.12.0"
  },
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build --webpack",
    "start": "next start",
    "lint": "eslint",
    "test": "vitest run"
  },
  "dependencies": {
    "@fontsource-variable/instrument-sans": "^5.2.8",
    "lucide-react": "^1.24.0",
    "next": "16.2.10",
    "react": "19.2.4",
    "react-dom": "19.2.4"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.10",
    "jsdom": "^29.1.1",
    "tailwindcss": "^4",
    "typescript": "^5",
    "vitest": "^4.1.10"
  }
}

```

### backend/pyproject.toml

```
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"

[project]
name = "openai-buildweek-backend"
version = "0.1.0"
description = "Backend for the adaptive exam-prep Build Week project"
readme = "api/README.md"
requires-python = ">=3.11,<3.14"
dependencies = [
    "alembic>=1.14,<2",
    "asyncpg>=0.30,<1",
    "boto3>=1.39,<2",
    "dramatiq[redis]>=1.18,<2",
    "email-validator>=2.2,<3",
    "fastapi>=0.128,<0.129",
    "google-genai>=1.33,<2",
    "pgvector>=0.4,<1",
    "pwdlib[argon2]>=0.3,<1",
    "pydantic-settings>=2.7,<3",
    "pymupdf>=1.26,<2",
    "pyjwt>=2.10,<3",
    "python-docx>=1.2,<2",
    "python-multipart>=0.0.20,<1",
    "sqlalchemy[asyncio]>=2.0.36,<3",
    "uvicorn[standard]>=0.34,<1",
]

[project.optional-dependencies]
dev = [
    "httpx>=0.28,<1",
    "mypy>=1.14,<2",
    "pytest>=8.3,<9",
    "pytest-asyncio>=0.25,<1",
    "ruff>=0.9,<1",
]

[tool.setuptools.packages.find]
where = ["api"]
include = ["app*"]

[tool.pytest.ini_options]
addopts = "-ra --strict-config --strict-markers"
testpaths = ["api/tests"]
asyncio_mode = "auto"
markers = ["integration: tests that require PostgreSQL"]

[tool.ruff]
line-length = 100
target-version = "py311"
src = ["api", "worker"]

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

[tool.mypy]
python_version = "3.11"
strict = true
plugins = ["pydantic.mypy"]
files = ["api/app", "api/tests"]

[[tool.mypy.overrides]]
module = "alembic.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["boto3", "boto3.*", "botocore", "botocore.*", "docx", "docx.*", "dramatiq", "dramatiq.*", "pgvector", "pgvector.*"]
ignore_missing_imports = true
disallow_untyped_calls = false

```

### backend/api/Dockerfile

```
FROM python:3.11-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

COPY backend/pyproject.toml ./pyproject.toml
COPY backend/alembic.ini ./alembic.ini
COPY backend/api ./api
COPY backend/worker ./worker

RUN pip install --upgrade pip && pip install .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

```

### frontend/src/app/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function RootPage() {
  redirect("/home");
}

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import "@fontsource-variable/instrument-sans";
import "./globals.css";

export const metadata: Metadata = {
  title: {
    default: "ExamTwin",
    template: "%s · ExamTwin",
  },
  description: "Reconstruct the real exam. Practice with intent.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" data-theme="light" className="h-full antialiased" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(){try{var t=localStorage.getItem("examtwin.theme.v2")||"light";var d=t==="dark";document.documentElement.setAttribute("data-theme",d?"dark":"light");document.documentElement.style.colorScheme=d?"dark":"light"}catch(e){}})()`,
          }}
        />
      </head>
      <body className="min-h-full">{children}</body>
    </html>
  );
}

```

### backend/api/app/main.py

```python
"""FastAPI application entrypoint."""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

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

from app import __version__
from app.api.router import api_router
from app.core.config import Settings, get_settings
from app.core.logging import configure_logging
from app.core.middleware import RequestIdMiddleware
from app.db.session import Database, DatabaseProtocol
from app.integrations.storage import S3Storage, StorageProtocol


def create_app(
    settings: Settings | None = None,
    database: DatabaseProtocol | None = None,
    storage: StorageProtocol | None = None,
) -> FastAPI:
    """Build an application instance with explicit runtime dependencies."""
    resolved_settings = settings or get_settings()
    configure_logging(resolved_settings.log_level)
    resolved_database = database or Database(
        resolved_settings.database_url,
        echo=resolved_settings.database_echo,
    )
    resolved_storage = storage or S3Storage(resolved_settings)

    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
        app.state.settings = resolved_settings
        app.state.database = resolved_database
        app.state.storage = resolved_storage
        yield
        await resolved_database.dispose()

    application = FastAPI(
        title=resolved_settings.app_name,
        version=__version__,
        debug=resolved_settings.debug,
        docs_url="/docs" if resolved_settings.docs_enabled else None,
        redoc_url="/redoc" if resolved_settings.docs_enabled else None,
        openapi_url="/openapi.json" if resolved_settings.docs_enabled else None,
        lifespan=lifespan,
    )
    application.add_middleware(
        CORSMiddleware,
        allow_origins=resolved_settings.cors_origins,
        allow_credentials=True,
        allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
        allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
    )
    application.add_middleware(RequestIdMiddleware)
    application.include_router(api_router, prefix=resolved_settings.api_prefix)
    return application


app = create_app()

```

### frontend/src/app/(exam-session)/layout.tsx

```typescript
import type { ReactNode } from "react";

import { DemoProvider } from "@/features/demo/demo-provider";

export default function ExamSessionLayout({ children }: { children: ReactNode }) {
  return <DemoProvider>{children}</DemoProvider>;
}

```

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