-
Notifications
You must be signed in to change notification settings - Fork 0
Development Guide
Python 3.14 and Node 22 — what CI pins, and what the dependency set is resolved against.
# Backend
cd backend
python -m venv .venv
.venv/Scripts/activate # Windows
source .venv/bin/activate # macOS / Linux
pip install -r requirements-dev.txt
uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload# Frontend
cd frontend
npm install
npm run dev| File | Contains | Use when |
|---|---|---|
requirements.txt |
Runtime dependencies | Running the app |
requirements-dev.txt |
The above plus the test runner | Developing |
requirements.lock |
Every version including transitives | Reproducing a known-good tree exactly |
CI installs requirements.lock, so a green run means the same tree that is green locally. If you change a dependency, regenerate the lock or CI is testing something else.
cd backend && python -m pytest -q
cd frontend && npm test && npm run typecheck && npm run lintRoughly 245 backend and 373 frontend tests. Run pytest --collect-only -q and vitest run for the current figures rather than trusting this line.
Order matters when reproducing CI locally: lint, then typecheck, then test. Both of the first two fail in seconds and catch breakage the test suite takes minutes to reach.
20 seconds, not the 5-second default. The heaviest tests drive a full MUI dialog through render → type → save → refetch, which measures around 7 seconds even running alone. Tests were failing intermittently with nothing wrong in the code under test — the classic way a suite loses its credibility.
Raised rather than papered over with retries, so a genuine hang still fails instead of being retried away.
Router → Service → Repository → Model. A layer talks only to the one beneath it.
- A router that builds a query is in the wrong layer.
- A service that returns an ORM object across the HTTP boundary should be returning a schema.
- A repository that makes a business decision belongs in a service.
There is no Alembic. Startup runs create_all plus apply_lightweight_migrations(), which adds missing columns and indexes. Adding a nullable column or an index works. Renaming, retyping, or dropping does not — that needs a hand-written path, and the helper will not do it for you.
See Architecture for why the trade was made.
Register literal paths before parameterised siblings. /analytics must come before /{prompt_id} or FastAPI matches the literal as an ID.
If something cannot be computed, say so. Empty state, None, —, "Not Graded". Not 0%, not a heuristic stand-in, not a plausible default. This is enforced by convention across every scoring path in the app, and it is the reason several endpoints return Optional[float] where a float would be more convenient.
- Schema in
app/schemas/— the request and response shapes. - Repository method in
app/repositories/if it needs a new query. - Service method in
app/services/for the logic. - Route in the relevant
app/api/v1/*.py, watching the ordering rule. - Test in
backend/tests/.
The router is registered already if the file exists; new router files need a line in app/api/v1/router.py.
In frontend/src/services/metrics/:
- Add the view to the union in
charts.tswith itsfamily,tier,primitive, andstage. -
TypeScript will now fail to compile —
buildChartPayloadswitches exhaustively over the union, so it demands a payload for the new view. This is intentional: you cannot add a chart and forget to feed it. - Add the payload case in
chartData.ts. - Declare any relationship it depends on in
couplings.ts, typedarithmetic | assumption | convention.
Important
A coupling that reaches no chart fails the completeness test. That is deliberate — an assumption the learner never sees is one they read as a fact.
You do not write a component. Four primitives cover all 27 views; pick the one that fits.
In frontend/src/services/learning/:
- Concept in
concepts.ts, with itsprerequisites. - Challenges in
challenges.tsreferencing it. - Scenario in
scenarios.tsif it needs a new parameter set.
The integrity suite will then check your work: vocabulary leaks (a challenge may only use terms licensed by its concept or a transitive prerequisite), relationship leaks (the card that asks a question must not answer it), and unbalanced options (a hedged distractor among confident ones is a giveaway).
Warning
If you add a regex-based guard, prove it can fail before trusting that it passes. Two guards here once matched nothing because `\b` inside a template literal is the JavaScript escape for backspace, not a word boundary — so new RegExp(`\b${x}\b`) was built from control characters. Write `\\b${x}\\b`. There is a test asserting those checks can fail; add yours to it.
No code needed for anything OpenAI-compatible. Add an entry to data/llm_profiles.custom.json — it merges over the built-ins and wins on key collision. See AI Providers.
These are load-bearing. Each has caused or nearly caused a real problem.
| Rule | Why |
|---|---|
| No endpoint returns an API key | Only has_api_key and the last four characters |
| The credential store stays out of git |
backend/data/.llm_secrets.json, .llm_secret_key
|
| PrepBench never downloads or launches a model | Enforced in local_setup.py
|
reveal payload is stripped server-side |
For any endpoint serving an unanswered question |
| No coefficient is presented as an industry constant | Calibration constants say what they are |
| PrepBench stays fully offline | No issue-tracker integration, now or later |