TA-to-course assignment tool for the McMaster ECE department. Generates optimized assignments of graduate students to undergraduate courses for the annual TA cycle, replacing the manual CAS-based process.
Built on Timefold for constraint-based optimization. The CLI is the V1 surface — designed for a non-technical office admin. A V2 web UI (FastAPI + Next.js) ships alongside it for student/instructor self-service and admin review; see PLAN.md for the full plan and TODO.md for the sprint backlog.
- Loads instructors, courses, students, and their preferences from CSV files.
- Validates the inputs and reports problems in plain English (missing files, unknown MACIDs, capacity shortfalls, infeasible vetos, missing preferences).
- Solves a constraint optimization problem balancing instructor + student preferences, prior-TA experience, and (optionally) term spread.
- Exports results in four formats:
master_list.csv(matches the existing CAS Excel format),per_course.csv,per_student.csv,manual_review.csv, plus canonicalassignments.json/yaml/xml. - Explains the final score with Timefold's per-constraint breakdown: which constraint contributed what, and which exact
(assignment, ranking)pairs triggered each match.
Out of scope for V1 (the CLI): web forms, email integration, database persistence. The admin distributes / collects CSV templates via their normal email client. The V2 web stack adds web forms, a SQLite/Postgres backing store, and an admin import path that wraps the same loaders — production SSO is still pending (the dev build runs an X-MacID header shim).
- Python 3.12 (pinned in
.python-version; Timefold's Python translator currently caps at 3.12). - Java 17+ at runtime — Timefold needs a JDK. Easiest install on macOS/Linux: SDKMAN! →
sdk install java 17.0.14-tem. - uv for environment management.
uv syncThe CLI is one entry point (tcm) with five subcommands wrapping a full assignment cycle. See PLAN.md §8 for the workflow rationale.
# 1. Scaffold a new cycle's input directory with CSV templates
uv run tcm init 2026-27
# → data/2026-27/ now contains *_template.csv files + config.yaml + README.md
# 2. (Admin work) Pre-fill roster CSVs, email *_preferences templates,
# drop the returned files in as e.g. student_preferences.csv
# 3. Validate before solving — admin-friendly diagnostics
uv run tcm validate 2026-27
# 4. Run the solver (default time budget: 30s)
uv run tcm solve 2026-27 --time-limit-seconds 60
# → results/2026-27/run-<timestamp>/ contains master_list.csv +
# per_course / per_student / manual_review / assignments.{json,yaml,xml}
# 5. (Optional) Re-export a prior run without re-solving
uv run tcm report 2026-27
# Run the full pipeline against synthetic data (no input files needed)
uv run tcm demoSee uv run tcm <subcommand> --help for flags. Notable ones:
--constraint-version {default,with_term_spread,full}— pick a constraint set.--solving-method {blocking,solver_manager}—solver_managerstreams live best-score updates while solving.--time-limit-seconds N— solver budget (default 30 forsolve, 10 fordemo).--force—initoverwrites an existing cycle;solveproceeds despite validation errors.
After tcm solve, the run directory contains:
| File | Purpose |
|---|---|
master_list.csv |
One row per filled seat — matches the CAS "Master List" Excel format. |
per_course.csv |
One row per course; lists assigned students and staffing status. |
per_student.csv |
One row per student; lists assigned courses and load vs. capacity. |
manual_review.csv |
Understaffed courses, unassigned slots, under-loaded students. |
score_breakdown.txt |
Human-readable summary of total/filled/unfilled seats. |
assignments.json/yaml/xml |
Canonical structured form for downstream tooling. |
The terminal log itself is verbose — pre-solve problem summary, sanity check, streaming solver progress, post-solve per-constraint score breakdown (with the top indictments per constraint), and a per-course + per-student assignment dump.
The web stack adds a student portal (rank preferences), an instructor portal (rank candidates), and an admin dashboard (cycle wizard, validate, solve, review, lock) on top of the same solver and loaders. Local dev needs Node 20+ and the [web] Python extras.
uv sync --extra web
uv run alembic upgrade head
TCM_ADMIN_MACIDS=admin1 uv run tcm-web # backend on :8000 (Swagger at /docs)
# in another terminal:
cd frontend
npm install
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 npm run dev # frontend on :3000Auth is a shim during development: send X-MacID: <macid> on every request. Admins are the MacIDs listed in TCM_ADMIN_MACIDS; students and instructors are anyone whose MacID was imported into the current cycle. The shim refuses to boot in production — flip TCM_AUTH_MODE=sso when the SSO seam lands (see PLAN.md §12.4).
If installing Python 3.12 + a JDK on the admin's machine is a pain, use the
bundled Docker image instead. It ships the full Python + Java 17 runtime so
the only prerequisite on the host is Docker Desktop. The host's data/,
results/, and logs/ directories are bind-mounted into the container, so
CSVs you edit in your text editor and result files the solver writes are
visible on your filesystem immediately.
Build once:
docker compose build # ~2-3 min first time, ~10 s on rebuildsTwo ways to run the CLI:
(1) One-shot subcommands — fresh container per invocation, removed on exit:
docker compose run --rm tcm demo
docker compose run --rm tcm validate 2026-27
docker compose run --rm tcm solve 2026-27 --time-limit-seconds 60(2) Long-lived interactive shell — skip the per-command container startup, useful when you're iterating (validate → fix → re-validate → solve):
docker compose up -d tcm-shell # start the background container
docker compose exec tcm-shell bash # drop into a shell
> tcm validate 2026-27
> tcm solve 2026-27
> exit # leaves the container runningThe up -d step only needs to run once — until you call docker compose down, the tcm-shell container keeps running in the background, and you can
re-enter it as often as you like:
docker compose exec tcm-shell bash # re-enter — no rebuild, instantOr run a single subcommand without dropping into a shell:
docker compose exec tcm-shell tcm solve 2026-27When you're done with the session entirely:
docker compose down # stop + remove the containerThe shell ships with a colored prompt, common ls aliases, and a brief
cheatsheet banner (see docker/bashrc).
The image bundles data/example/ as offline reference data, so even a fresh
clone with no real cycle inputs can be exercised end-to-end:
docker compose run --rm tcm solve exampleOn Docker Desktop (macOS / Windows) file ownership is mapped transparently to your host user. On a native Linux engine, rebuild with your UID so bind-mounted files don't end up owned by container-internal UID 1000:
APP_UID=$(id -u) APP_GID=$(id -g) docker compose buildIf you'd rather not use compose, the raw equivalent is:
docker build -t ta-course-match .
docker run --rm -it \
-v "$PWD/data:/app/data" \
-v "$PWD/results:/app/results" \
-v "$PWD/logs:/app/logs" \
ta-course-match solve 2026-27The same compose file also runs the backend API and the Next.js dev server with hot-reload from your host source. Two images, both shipped from this repo:
docker compose build tcm-web tcm-frontend # ~3 min first time, fast on rebuilds
TCM_ADMIN_MACIDS=admin1 docker compose up -d tcm-web tcm-frontendEndpoints:
- Backend: http://localhost:8000/docs (Swagger). The container entrypoint runs
alembic upgrade headbefore launchingtcm-web, so the DB schema is always at head when the API is reachable. - Frontend: http://localhost:3000.
Source mounts: ./backend, ./src, and ./frontend are bind-mounted into the containers. Uvicorn auto-reloads (TCM_RELOAD=1) on Python changes; Next.js hot-reloads on TSX changes. Container-local node_modules and .next are anonymous volumes so the host's macOS/Apple-Silicon modules can't shadow the Linux ones.
Tail logs from one service:
docker compose logs -f tcm-web # or tcm-frontendStop everything:
docker compose downBy default the backend writes to bundled SQLite under /app/data/tcm.sqlite3 (visible on the host bind mount). To use the local Postgres service instead:
TCM_DB_URL=postgresql+psycopg://tcm:tcm@postgres:5432/tcm \
docker compose up -d postgres tcm-web tcm-frontendEnvironment variables the backend service honors (set in your shell or a .env file next to compose):
| Variable | Purpose | Default |
|---|---|---|
TCM_ADMIN_MACIDS |
comma-separated admin MacIDs the auth shim recognizes | admin1 |
TCM_AUTH_MODE |
shim (dev) or sso (production seam, not yet wired) |
shim |
TCM_CORS_ORIGINS |
comma-separated allowed origins for the browser | http://localhost:3000 |
TCM_DB_URL |
SQLAlchemy URL — opt into Postgres etc. | bundled SQLite |
NEXT_PUBLIC_API_BASE_URL |
API base used by the browser | http://localhost:8000 |
uv run pytest # default: fast tests
uv run pytest -m slow # include slow / end-to-end solver tests (needs Java 17+)See CLAUDE.md for the directory map, command cheatsheet, and non-obvious gotchas.