Skip to content

Latest commit

 

History

History
808 lines (648 loc) · 55 KB

File metadata and controls

808 lines (648 loc) · 55 KB

ta-course-match — PLAN

Software requirements and design plan for a TA-to-course assignment tool for the McMaster ECE department. Source meeting notes: doc/TA_Assignment_Meeting_May_5.pdf


1. Project Goal

Build a tool that helps ECE department admins (Brennan and team) generate optimized assignments of graduate students (TAs) to undergraduate courses for the 2026-27 cycle, replacing the manual CAS-based process.

Who runs this tool

The operator is an ECE office admin. They are fluent in Excel, email, and the CAS web UI; they are not software engineers. They will not edit Python, will not read tracebacks, and will not memorize long flag lists. V1 has no web UI, but the CLI it ships with must still feel like a piece of office software: helpful prompts, plain-English errors, ready-to-distribute CSV templates, and a clear stage-by-stage workflow for collecting preference data from instructors and students. See §8 for the UX contract.

Hard deadlines from the meeting

Date Event
2026-06-09 Platform ready for internal testing
2026-06-15 Launch (student-facing)
2026-06-16 → 06-30 Student application window
2026-07-01 → 07-15 Instructor preference window
Late July Assignment processing (algorithm run)
First week of August Assignments locked
Last week of August Hours of Work forms due to CUPE

Personal constraint

  • 1 week of development time for V1. V1 must be functional but minimal — it's the algorithm core plus structured I/O. UI and database layers come later.

2. Scope

V1 (this week) — in scope

  • Domain model for Course, Student (TA), Instructor, Assignment.
  • Constraint-based assignment engine (Timefold — see §4).
  • File-based I/O: read instructor data, course data, student data, student preferences, and instructor preferences from CSV (and/or JSON/YAML).
  • Export assignments + intermediate data in JSON, YAML, XML, CSV (human-readable).
  • Admin-friendly CLI with subcommands tcm init, tcm validate, tcm solve, tcm report, tcm demo — designed end-to-end for a non-developer office admin (see §8).
  • Input templates: tcm init <cycle> scaffolds a cycle directory with pre-filled, ready-to-distribute CSV templates that the admin emails to instructors and students for preference collection.
  • Validation step (tcm validate): a standalone read-and-check pass that emits a plain-English report — missing files, unknown MACIDs, capacity vs. demand, missing preferences — with file:line locations and recommended fixes.
  • Demo data generator (synthesizes realistic inputs from the 2025-26 cycle files for testing and admin onboarding).
  • Sanity check pass before solving (workforce capacity, vetos vs. seats, etc.).
  • Pre/post-solve reports (broken constraints, score breakdown, unassigned slots).

V1 — explicitly out of scope (but provisioned for)

  • Database: all I/O goes through a data-access abstraction (e.g. DataLoader / DataExporter) so V2 can swap CSV for a real DB without touching solver code.
  • Authentication / user accounts: no login. CLI assumes the operator is trusted. V2 will add a web UI with role-based auth (admin, instructor, student).
  • Email notifications: deferred to V2 (when the web layer exists).
  • Edit history / audit log: deferred. V1 keeps input files in git and treats each run as a fresh snapshot — that's the audit trail for now.
  • CV uploads, qualification text, co-op status: V1 supports these as opaque pass-through fields if present in the input, but the algorithm only uses preference rankings + previous-TA experience. Richer scoring comes later.
  • Student veto: explicitly out per the meeting notes ("may create complaints").
  • UI: optional stretch goal. If solver + I/O are stable by day 4, consider a thin Streamlit page for input review and output display. Otherwise, CLI only.

V2+ — future work

  • Web UI: student portal (preference submission), instructor portal (ranking + veto), admin portal (run solver, review, lock).
  • Postgres + SQLAlchemy persistence.
  • Auth (probably OAuth via McMaster SSO).
  • Notifications + edit history.
  • Multi-section handling (Case 2 from the PDF): instructors choose "team" vs. "individual" preferences, individual mode pro-rates TA allocation across instructors.

3. Functional Requirements (V1)

Numbered for later traceability into TODO.md.

FR-1: Input data ingestion

The system shall load the following from a configurable input directory:

File Content Format (V1)
courses.csv course code, section, level, term, # TAs required, instructor MACIDs CSV
instructors.csv MACID, first name, last name CSV
students.csv MACID, student number, name, # TA positions, program, level, supervisor MACID CSV
student_preferences.csv student MACID, course code, preference rank (1–5), prior_ta_for_course (bool) CSV
instructor_preferences.csv instructor MACID, course code, student MACID, rank, vetoed (bool) CSV
config.yaml constraint weights, solver time limit YAML

The 2025-26 CAS exports under doc/TA Assignment Files from CAS System - 2025-26 Cycle/ are the schema reference for courses/instructors/students.

FR-2: Multi-instructor / multi-section courses

  • One course code (e.g. ELECENG 3TQ3) may have multiple section rows (C01, C02) each with its own instructor.
  • V1 treats each section as a separate "course slot" for solver purposes but the export collapses sections into a single course for the student-facing view.

FR-3: Constraint-based assignment

The solver shall produce an assignment that:

  • Hard: satisfies course TA count, student TA-position count, and instructor vetos (see §5).
  • Soft: maximizes alignment with instructor and student preferences (instructor weighted higher than student, per the PDF).

FR-4: Output export

The system shall export the final assignment in all four of JSON / YAML / XML / CSV. The "master list" CSV format matches the existing manual Excel format from the 2025-26 cycle.

It shall also export:

  • Per-course assignment summary (which students assigned to which course).
  • Per-student assignment summary (which courses each student is teaching).
  • Unassigned students / understaffed courses (the manual-review queue — expected 5–10% per the PDF).
  • Score breakdown (which soft constraints scored how much).

FR-5: Sanity checks

Before solving, the system shall log warnings (with structured codes like [SOLVER.capacity]) if:

  • Sum of required TA positions across courses > sum of TA capacity across students.
  • All instructors vetoed a particular student.
  • A course has more vetos than feasible TAs available.

4. Timefold Evaluation

Recommendation: yes, use Timefold.

Why it fits

  • Problem structure is a classic assignment problem with weighted preferences and hard constraints — Timefold's bread and butter.
  • The HardMediumSoftScore model maps cleanly onto our constraint tiers (counts/vetos = Hard, capacity bounds = Medium, preferences = Soft).
  • We already have an internal pattern for this type of problem in example/ta-scheduler-timefold/ — domain model, constraint generator class hierarchy, solver orchestration, CLI scaffolding. We can lift this structure directly and adapt the entities from Shift → Course and TA → Student.
  • The coding-style document at doc/coding_style.xml is already written around Timefold patterns (constraint provider dict, TimetableConstraintGenBase(ABC), sanity_check() on the planning solution). This is the path of least resistance.
  • Manual intervention (5–10%) is supported natively via @PlanningPin — we can lock in admin overrides and re-solve.

Risks / honest tradeoffs

  • Java dependency: Timefold requires Java 17+. Fine for a CLI tool on the admin's laptop; will need bundling thought for V2 deployment.
  • Overkill if requirements simplify: if the problem reduces to a pure 1:1 weighted bipartite match, the Hungarian algorithm (scipy.optimize.linear_sum_assignment) is much simpler and faster. But once we add instructor vetos, multi-TA-per-course, varying TA-position counts per student, multi-section handling, and prior-experience tie-breakers, Timefold's flexibility pays off.
  • Learning ramp: I've used Timefold before for the lab scheduler, so the ramp is acceptable inside a 1-week window.

Decision

Lift the ta-scheduler-timefold example's structure into this repo. Rename entities (Shift → Course, TA → Student), redefine constraints around course assignment rather than shift scheduling, and reuse the CLI + benchmark scaffolding.


5. Domain Model (V1 draft)

Mirrors the example's Shift / TA / ShiftAssignment / Timetable pattern.

Problem facts (@dataclass, no solver annotations)

@dataclass
class Instructor:
    macid: str                                  # primary key, e.g. "chenjun"
    first_name: str
    last_name: str

@dataclass
class Course:
    id: str                                     # synthetic, e.g. "ELECENG_3TQ3_C01"
    course_code: str                            # "ELECENG 3TQ3"
    section: str | None                         # "C01", "C02", or None
    level: int                                  # 2, 3, 4
    term: int                                   # 1 (fall) or 2 (winter)
    num_tas_required: int
    instructor_macids: list[str]                # 1+ instructors

@dataclass
class Student:
    macid: str
    student_number: str
    first_name: str
    last_name: str
    num_ta_positions: int                       # usually 2
    program: str                                # "ECEPhD" / "ECEMASc"
    level: int                                  # 1-4 (year of program)
    supervisor_macid: str
    # Preferences
    course_preferences: list[CoursePreference]  # ranked 1..5
    # Pass-through fields (not used by V1 solver, exported as-is)
    qualifications_summary: str = ""
    coop_status: str | None = None
    cv_path: str | None = None

@dataclass
class CoursePreference:
    course_code: str                            # student picks by course code, not section
    rank: int                                   # 1 = top choice
    prior_ta_for_course: bool = False

@dataclass
class InstructorRanking:
    instructor_macid: str
    course_id: str
    student_macid: str
    rank: int | None                            # 1, 2, 3, ... or None if not ranked
    vetoed: bool = False

Planning entity

@planning_entity
@dataclass
class CourseAssignment:
    id: Annotated[str, PlanningId]
    course: Course
    assigned_student: Annotated[Student | None,
                                PlanningVariable,
                                Field(default=None)]
    pinned: Annotated[bool, PlanningPin] = False  # manual override support

Planning solution

@planning_solution
@dataclass
class AssignmentSolution:
    id: Annotated[str, PlanningId]
    courses:               list[Course]               # ProblemFactCollectionProperty + ValueRangeProvider (for student via @PlanningVariable)
    students:              list[Student]              # ProblemFactCollectionProperty + ValueRangeProvider
    instructors:           list[Instructor]           # ProblemFactCollectionProperty
    instructor_rankings:   list[InstructorRanking]    # ProblemFactCollectionProperty (joined in constraints)
    constraint_parameters: ConstraintParameters       # ProblemFactProperty (weights)
    assignments:           list[CourseAssignment]     # PlanningEntityCollectionProperty
    score:                 HardMediumSoftScore
    solver_status:         SolverStatus | None = None

ConstraintParameters (tunable weights — @dataclass)

instructor_preference_reward_rank1: int = 100   # Soft
instructor_preference_reward_rank2: int = 60
instructor_preference_reward_rank3: int = 30
student_preference_reward_rank1:    int = 40    # Soft, weighted lower than instructor
student_preference_reward_rank2:    int = 24
student_preference_reward_rank3:    int = 12
student_preference_reward_rank4:    int = 6
student_preference_reward_rank5:    int = 3
prior_ta_experience_bonus:          int = 15    # Soft tie-breaker
supervisor_match_bonus:             int = 5     # Soft, very small

6. Constraint Design

Following the TimetableConstraintGenBase(ABC) + variant subclass pattern from the example.

Hard constraints (must hold)

  1. course_exact_ta_count — Each course shall have exactly num_tas_required students assigned.
  2. student_exact_position_count — Each student shall be assigned exactly num_ta_positions courses.
  3. no_duplicate_student_course — A student shall not be assigned to the same course twice.
  4. no_vetoed_assignment — A student vetoed by any instructor on a course shall not be assigned to that course.
  5. level_eligibility (optional, decide after consulting Brennan) — e.g. only PhD students teach 4xxx courses, etc. Open question — see §9.

Medium constraints

  1. student_course_term_spread — If a student has 2 TA positions, prefer (medium-penalize the inverse) one in each term (fall/winter), so they aren't slammed into a single semester.

Soft constraints (preferences — weighted)

  1. reward_instructor_preference — Reward proportional to the instructor's ranking of the assigned student (rank 1 > rank 2 > rank 3 …). Weighted highest per the PDF.
  2. reward_student_preference — Reward proportional to the student's ranking of the assigned course's course_code. Weighted second.
  3. reward_prior_ta_experience — Small reward if student.course_preferences[course].prior_ta_for_course == True. Used as tie-breaker per the PDF.
  4. reward_supervisor_match — Tiny reward if the course instructor is also the student's supervisor (anecdotally common — the 2025-26 data shows many students supervised by Emadi/Nahid-Mobarakeh/Chen, etc.). Sanity-check this against admin policy.

Constraint version registry

  • default: 1, 2, 3, 4, 7, 8, 9 — the safe baseline.
  • with_term_spread: default + 6.
  • with_supervisor_match: default + 10.
  • full: 1–10.

(Pattern matches constraints_provider_dict in example/ta-scheduler-timefold/src/hello_world/constraints.py.)


7. I/O Format Specifications

Input directory layout

data/2026-27/
  courses.csv
  instructors.csv
  students.csv
  student_preferences.csv
  instructor_preferences.csv
  config.yaml
  (optional: pinned_assignments.csv  — manual locks before solving)

Output directory layout

results/2026-27/<run_timestamp>/
  master_list.csv               # primary deliverable, matches CAS Excel "Master List" format
  assignments.json              # canonical structured form
  assignments.yaml              # human-friendly
  assignments.xml               # if a downstream tool wants XML
  per_course.csv                # one row per course, listing assigned students
  per_student.csv               # one row per student, listing assigned courses
  manual_review.csv             # unassigned / understaffed / flagged cases
  score_breakdown.json          # which soft constraints contributed what
  run_log.txt

Why all four formats?

The PDF doesn't pin a format, and Brennan called out that "downloading TA Assignment data into an Excel sheet" was the painful manual step last year. CSV is the operational primary; JSON/YAML/XML are for interoperability with whatever V2 / external tooling we wire in later. Implementing all four is cheap because they're all just serializations of the same AssignmentSolution dataclass tree — one to_dict() method on the solution + four short writer functions.


8. CLI UX & Admin Workflow

V1 has no web UI, but the CLI is still the surface a non-developer admin will interact with. This section defines the operator experience.

Workflow: one cycle, end-to-end

Each cycle (e.g. 2026-27) goes through four operator-facing stages. Each stage is one subcommand. Between stages the admin works in their existing tools (email, Excel).

Stage Subcommand What the admin does
1. Scaffold tcm init <cycle> Creates data/<cycle>/ with pre-filled CSV templates and a per-cycle README. Admin fills in the roster columns they already have, then emails the preference templates to students/instructors.
2. Ingest + validate tcm validate <cycle> Once filled templates come back and are dropped into data/<cycle>/, this command parses everything, cross-checks MACIDs across files, runs sanity checks, and prints a human-readable issue list. Admin iterates until it exits clean.
3. Solve tcm solve <cycle> Re-runs validate, refuses on hard errors (override with --force), runs Timefold with streaming progress + best-score updates, writes a timestamped result directory, prints a plain-English summary.
4. Report / re-export tcm report <cycle> [<run_id>] Re-exports an existing solve in alternate formats or with focused views (--per-instructor, --unassigned). Lets the admin generate per-stakeholder attachments without re-solving.

tcm demo (separate from a cycle) runs the full pipeline on synthetic data — useful for training a new admin and for solver smoke tests.

Templates emitted by tcm init

Each template is a plain CSV with (a) a comment header row describing each column, (b) one example row that the admin deletes before sending, and (c) any roster columns already populated from prior cycle / CAS exports where possible.

Template Distributed to Returned via Drop-in name
courses_template.csv (admin fills from catalog) courses.csv
instructors_template.csv (admin fills from roster) instructors.csv
students_template.csv (admin fills from CAS export) students.csv
student_preferences_template.csv Each student via email Filled CSV returned by email student_preferences.csv
instructor_preferences_template.csv Each instructor via email Filled CSV returned by email instructor_preferences.csv
config.yaml (admin only — defaults are sensible) config.yaml

The per-cycle README explains: who fills each file, what every column means, which CSV is the deadline-driver, and the exact filename to drop the returned CSV under.

UX requirements (apply to all subcommands)

  • No tracebacks reach the admin. A top-level exception handler catches everything, prints a one-paragraph admin-facing message, and points at the full log file in logs/.
  • Friendly prompts when args are missing. If <cycle> is omitted, the CLI lists detected data/*/ directories and prompts the admin to pick one.
  • Plain-English validation errors. Instead of KeyError: 'macid', print: "Couldn't find a 'macid' column in students.csv (file expected at row 1). The first row should be: macid,student_number,first_name,.... Open the file in Excel and add the missing column." Each error includes file:line and a recommended fix.
  • No silent overwrites. tcm init refuses to overwrite an existing cycle directory without --force. tcm solve writes to a new timestamped subdirectory under results/<cycle>/ every run, so re-solves never clobber prior results.
  • Progress for long-running steps. Solve phase emits one line every N seconds with the current best score and elapsed time so the admin can see it's not hung.
  • Terminal niceties. Use color and symbols ( ) by default; honor --no-color and NO_COLOR env for piped output.
  • Distribution-ready outputs. master_list.csv opens cleanly in Excel (correct quoting, no BOM surprises); per_instructor.csv is structured so the admin can split + email per-instructor without post-processing.

Suggested entry-point structure

A single tcm script with subcommands (replacing the current tcm-solve / tcm-demo split):

[project.scripts]
tcm = "ta_course_match.main:cli"

main.cli() dispatches to cmd_init, cmd_validate, cmd_solve, cmd_report, cmd_demo. Keeps the admin's mental model small ("everything starts with tcm <verb>") and gives us one place to install the global exception handler, progress formatter, and colored output helper.

Out of scope for V1 (deferred to V2)

  • Web forms. V2 replaces emailed CSV templates with a student/instructor portal that writes directly to the DB. The subcommand structure above carries forward unchanged — only the data backing of init/validate/solve shifts from filesystem to database + HTTP.
  • Email integration. V1 admins still send templates and collect responses through their own email client; V2 wires McMaster SSO + mailer.
  • Interactive wizard / TUI. Subcommand + flags is the V1 contract. If admins struggle in practice, V2 adds a guided tcm wizard mode on top of the same building blocks.

9. Open Questions (for Brennan / domain experts)

  • Level eligibility rules: Are there hard rules like "only 4xxx courses get PhD students" or is it informal? Affects whether to encode as a hard constraint or just a soft preference.
  • Term distribution: Do students have a preferred term? Currently assumed not — the algorithm just distributes across terms.
  • Multi-instructor courses (Case 1 from PDF): In the rankings file, do all instructors on a course share one ranking, or each submits their own (and we average / max)?
  • Multi-section courses (Case 2 from PDF): For V1 we treat each section as independent. Confirm this is acceptable for the May-June test run.
  • TBA instructor rows: 4 courses in the 2025-26 file have TBA instructor — how should V1 handle these? Skip until assigned? Treat as wildcard with no preference?

10. Architecture & Future-Proofing

V1 is CLI + file I/O, but the structure is laid out so V2 can replace pieces without rewriting the solver:

[ CSV files ]                          [ Postgres ]   ← V2
       \                                /
        v                              v
       [ DataLoader (abstract) ]
                |
                v
   [ AssignmentSolution (domain) ]      ← stable boundary
                |
                v
       [ Solver (Timefold) ]            ← stable
                |
                v
       [ DataExporter (abstract) ]
        /          |          \
       v           v           v
   [ CSV/JSON ]  [ HTTP API ]  [ Email ]  ← V2 adds

Concretely for V1:

  • Define DataLoader and DataExporter as abstract base classes in utils.py (or io.py).
  • Concrete CsvDataLoader / JsonDataLoader etc. for V1.
  • V2 adds SqlDataLoader without changing solver.py or domain.py.

No login / auth in V1 — the CLI runs as the operator. V2's web layer is where auth lands; the domain model is auth-agnostic. See §12 for the V2 web architecture and auth provisioning plan.


11. Project Layout (target)

Per doc/coding_style.xml. V1 code lives under src/ta_course_match/; V2 adds the parallel backend/ (FastAPI) and frontend/ (Next.js) trees described in §12.

ta-course-match/
  src/ta_course_match/
    __init__.py
    domain.py
    constraints.py
    solver.py
    utils.py
    main.py
    demo_data.py
    io_loaders.py        # CsvDataLoader, JsonDataLoader, ...
    io_exporters.py      # CsvDataExporter, JsonDataExporter, ...
  backend/               # V2 — FastAPI service wrapping V1 modules (see §12.2)
    main.py
    deps.py
    auth.py              # AUTH SHIM — replaced by SSO in P5
    schemas.py
    db/
    routes/
  frontend/              # V2 — Next.js 14 App Router (see §12.2)
    src/app/
    src/components/
    src/lib/
    src/types/
  tests/
    conftest.py
    test_domain.py
    test_constraints.py
    test_io.py
    test_data/
  data/                  # gitignored
  results/               # gitignored
  configs/
    solver_config.xml
    default_weights.yaml
  logs/                  # gitignored
  doc/                   # already exists
  example/               # already exists (timefold reference)
  .python-version        # "3.13"
  pyproject.toml
  uv.lock
  CLAUDE.md
  PLAN.md                # this file
  TODO.md
  README.md

12. V2 — Web UI Architecture (in flight)

V2 puts a web frontend on top of the V1 solver. The design reference is doc/ui/design_handoff_ta_match_ui/README.md — that doc is the authoritative wireframe + token spec; this section describes how V2 is built incrementally on top of the existing V1 codebase.

12.1 Guiding principles

  • Incremental, not big-bang. Each phase ships a runnable slice (student portal alone, then instructor, then admin) backed by the existing CLI modules — no rewrites of solver.py / domain.py.
  • The CLI is the source of truth for cycle data. V2's first end-to-end story is: admin runs tcm init <cycle> and tcm validate <cycle> as today, then imports the resulting data/<cycle>/ directory into the web service. The DB is populated from existing CSV loaders. Nothing the admin already knows how to do gets thrown away.
  • Auth is provisioned, not built. A separate person finalizes SSO + session management; this codebase ships a clearly-marked auth shim so every page, route handler, and DB query already flows through Depends(current_user) / useSession(). Swapping the shim for real auth is a 1-file change.

12.2 Repository layout

ta-course-match/
  src/ta_course_match/        # V1 — unchanged. Solver, domain, loaders, CLI.
  backend/                    # V2 — FastAPI service wrapping the V1 modules.
    main.py                   # app factory + lifespan (DB engine)
    deps.py                   # current_user shim, get_session, get_loader
    auth.py                   # AUTH SHIM (X-MacID header) — replace with SSO
    schemas.py                # Pydantic DTOs mirroring src/ta_course_match/domain.py
    db/
      models.py               # SQLAlchemy ORM models
      session.py              # engine + sessionmaker; sqlite dev / postgres prod
      migrations/             # alembic
    routes/
      cycles.py               # /api/cycles*  — import, list, dashboard, lock
      preferences.py          # /api/preferences/student/me, /instructor/:course
      solve.py                # /api/cycles/:cycle/solve   (POST + SSE)
      runs.py                 # /api/cycles/:cycle/runs*
  frontend/                   # V2 — Next.js 14 (App Router) + TS + Tailwind.
    src/app/(student|instructor|admin)/...   # route groups (handoff §Routes)
    src/components/ui/        # shadcn-style primitives (hand-authored)
    src/lib/api.ts            # fetch wrapper + MacID header (auth shim)
    src/types/api.ts          # OpenAPI-generated; do not hand-edit

The backend starter in doc/ui/design_handoff_ta_match_ui/backend/ is the seed for backend/. The frontend in frontend/ has the student portal (S1 → S4) already standing.

12.3 The "admin imports a cycle" flow (Phase 1)

This is the simplest path to a usable V2 — it leans on the V1 admin workflow instead of building a separate web ingestion path.

Admin laptop                                Server
─────────────                               ──────
  tcm init 2026-27        ──fills CSVs──> data/2026-27/
  tcm validate 2026-27    (loops until clean)
  tar czf 2026-27.tar.gz data/2026-27/
                          ──upload──>     POST /api/cycles/import
                                              │
                                              ▼
                                          backend/routes/cycles.py
                                              │ uses CsvDataLoader from V1
                                              ▼
                                          DB tables populated:
                                            cycles, courses, instructors,
                                            students, instructor_rankings
                                              │
                                              ▼
                                          Students/instructors can now
                                          log in (via auth shim) and see
                                          their portal pre-populated.

What this means concretely:

  • The first deployable V2 does not require V2-native admin screens for data entry. A4/A5/A8 (solve, results, lock) can come in Phase 3; A1/A2/A3 can stay CLI-driven indefinitely if needed.
  • Students and instructors only ever interact with the web UI; the CLI never touches them.
  • Re-importing a cycle is idempotent (upsert by primary keys). Admins can iterate on CSVs and re-upload without resetting drafts.
  • Roster changes mid-cycle (a student drops out) are handled by editing the CSV and re-running validate + re-import. V2.1 can add a delta endpoint if this is painful.

12.4 Auth provisioning (separate owner finalizes)

A different team member owns the production auth stack. To keep them unblocked, this codebase ships explicit auth seams in fixed locations:

Layer Seam Shim behavior (today) Production behavior (separate PR)
Frontend route gate src/app/(role)/layout.tsx reads X-MacID cookie set by a dev /login page; redirects if missing NextAuth.js (SAML) session; redirects to McMaster SSO if no session
Frontend fetch src/lib/api.ts attaches X-MacID: <macid> header on every request attaches Authorization: Bearer <jwt>
Backend dependency backend/deps.py::current_user reads X-MacID header, looks up DB row, returns CurrentUser (role inferred from which table the MacID lives in) verifies JWT signature against McMaster's JWKS, extracts MacID + role claims
DB schema users.role column populated at cycle import (instructor MacIDs from instructors.csv, student MacIDs from students.csv, admin MacIDs from config.yaml) unchanged

Non-negotiables for the auth-finalizer:

  1. CurrentUser Pydantic shape (in backend/schemas.py) must not change — every route depends on it.
  2. The shim never ships to production: a TCM_AUTH_MODE=shim|sso env var gates which dependency function is registered, and shim mode refuses to start with ENVIRONMENT=production.
  3. All route handlers in backend/routes/* already call current_user = Depends(...) from day one. Adding SSO is changing the resolver, not adding new params across the codebase.

12.5 Component → backend wiring map

The handoff doc names every endpoint; this is which V1 module each one reaches into.

Frontend page Backend route V1 module it calls
S1 welcome GET /api/students/me reads from DB (populated by CsvDataLoader at import)
S2 rank, S3 review GET /api/preferences/student/me · PUT (autosave) · POST submit DB-only (no V1 module)
I1 dashboard GET /api/instructors/me/courses DB-only
I2 rank GET /api/courses/:id/candidates · PUT /api/preferences/instructor/:courseId DB-only
A1 dashboard GET /api/cycles/:cycle computes stage status from DB row counts
A2 init POST /api/cycles/import io_loaders.CsvDataLoader.load()
A3 validate GET /api/cycles/:cycle/validate domain.AssignmentSolution.sanity_check() + the validation pass from main.cmd_validate
A4 solve (SSE) POST /api/cycles/:cycle/solve + GET ...?runId= solver.TaCourseMatchSolverWithSolverManager with a progress callback that pushes SSE frames
A5 results GET /api/cycles/:cycle/runs/:runId reads persisted AssignmentSolution snapshot
A6 review queue GET /api/cycles/:cycle/runs/:runId/manual_review · POST ...resolve derived from manual_review.csv logic in io_exporters
A7 course detail same same
A8 lock POST /api/cycles/:cycle/lock sets DB flag + (V2.1) triggers mailer

The pattern: everything DB-only is new code; everything that says "V1 module" is wrapping existing functions through Depends(get_loader) / Depends(get_solver). The V1 code does not change.

12.6 Phasing

Phase Scope Exit criterion
P0 — student portal demo Frontend S1–S4 against mock-data.ts (done in frontend/) Walk the flow end-to-end in a browser; drafts persist locally.
P1 — backend foundation + import FastAPI app, SQLite dev DB, alembic, POST /api/cycles/import wrapping CsvDataLoader, GET /api/students/me, GET /api/preferences/student/me, PUT, POST submit. Frontend swaps mocks for fetch. Auth shim wired. Admin can import a cycle via curl; a student can rank + submit and the data lives in the DB.
P2 — instructor portal I1, I2.v1 (with @dnd-kit), I4. GET /api/instructors/me/courses, GET /api/courses/:id/candidates, PUT/POST per-course drafts. Instructor can rank candidates and submit. Co-taught (I3) deferred pending §9 answer.
P3 — admin solve + results A4 SSE wrapping TaCourseMatchSolverWithSolverManager, A5 results screen, A1 dashboard. A2/A3 stay CLI-only — admins keep using tcm init / tcm validate. Admin can kick off a solve from the web, watch progress, view results.
P4 — admin review + lock A6.v1 manual-review queue, A7 course detail, A8 lock flow. Pin/re-solve loop. Admin can resolve edge cases and lock the cycle.
P5 — production auth handoff Auth-finalizer replaces the shim with NextAuth (SAML) + JWT verification. No application code changes. TCM_AUTH_MODE=sso boots in staging; shim mode disabled in prod.
V2.1+ Notifications (mailer on lock), audit log UI, co-taught I3 (after §9 resolved), Postgres in prod, mobile graceful degradation.

12.7 What this section does not decide

  • Hosting / deployment topology. Likely a Docker compose stack (frontend, backend, postgres) but the exact target (McMaster on-prem? cloud VM?) is an ops decision.
  • CV upload + qualifications display. Resolved 2026-05-29 (dev/hand-off-v3): disk-backed PDF upload + viewer; the instructor "View CV" stub is now live and a dedicated swipe-mode viewer shows the CV beside rank controls.
  • Multi-instructor ranking semantics. Still open per §9 question #3. Blocks I3, not I2.
  • The "fit" indicator on instructor candidates. Drop from V2 MVP unless a heuristic is agreed (handoff §"Open decisions" #5).

12.8 Wireframe-fidelity gap (post-MVP polish)

P0–P3 (student portal · backend foundation · instructor portal · admin solve+results) ship a complete data path: students submit, instructors rank, admin imports a cycle + runs the solver + downloads the master-list CSV. The pages render with the McMaster tokens but visually diverge from the V1 wireframes in stat-card composition, list/row polish, and four entire admin screens that were cut from MVP scope (A3 validate · A6 manual review · A7 per-course detail · A8 lock, plus the A2 init UI that we intentionally keep CLI-driven).

The detailed inventory (per-screen status / what's missing / effort) and the suggested 5-branch closure phasing (feat/web-shared-primitivesinstructor-polishadmin-a3-a5admin-a4-a1admin-a6-a7-a8) live in TODO.md "V2 UI Polish — wireframe-fidelity gap closure". Cross-cutting infrastructure that several screens need (shared Stepper / Avatar / SeverityPill / HorizontalBar, a course.title column the schema is missing, and a few backend wraps for activity feed / per-constraint analysis / multi-file export) is called out in the same section.

Blocked items remain blocked: I3 (multi-instructor semantics — §9 #3), the I2 "fit" indicator (heuristic decision), "View CV" surfaces (CV scope), and A8 notifications (mailer ownership).

12.9 Post-merge bug-fix round

After the polish sprint shipped (PRs #3–#7), four functional gaps surfaced from real-world admin use. They're tracked as a separate iteration in TODO.md "Post-merge bug-fix round" with one short-lived feat/<area> branch per gap, smallest first:

  1. Save-on-manual-review — A6 commits pin / dismiss actions immediately on click. Switching to a "stage in local React state + commit on Save" pattern so the operator can try variations and discard.
  2. Activity log filter + download — the dashboard's Recent Activity card takes filter params (actor / action / since / until) and exposes a /activity.csv companion endpoint.
  3. Per-person submission views — two new admin pages (/cycles/[cycle]/students and /cycles/[cycle]/instructors) drill down from the existing aggregate-count tiles so the admin can see who hasn't submitted.
  4. Web setup wizard — full 5-step browser wizard for cycle creation (Name + windows → Roster CSVs → Preference CSVs (optional) → Weight overrides → Validate + finalize). Replaces the current tcm init + curl POST /import workflow for admins who don't want to use the CLI.

Non-negotiable CSV / IO compatibility rule for round 4 (and any future upload path): every CSV the wizard generates or accepts must round-trip cleanly through CsvDataLoader (src/ta_course_match/io_loaders.py). The wizard's template-download endpoint streams the canonical bodies from src/ta_course_match/templates.py _FILES rather than re-authoring the column lists in the frontend, so column shape cannot drift. The CLI's tcm init and the web wizard write equivalent data/<cycle>/ trees and tcm validate / tcm solve work against either interchangeably.


13. Critical evaluation & post-launch roadmap

Written 2026-05-25, after PRs #9–#13 merged and the admin CRUD / preferences / all-clear features shipped on the wizard branch. This section captures an honest assessment of where the codebase stands against the May-5 meeting commitments, where the algorithm has known soft spots, and what the next two quarters of work look like. The execution-ready bullet list lives in TODO.md "Post-launch backlog"; this section frames the why.

13.1 What's solid

  • End-to-end data path works for the full cycle: scaffold (CLI or wizard) → student submits → instructor ranks → admin solves → manual review → lock. Every transition is exercised by the demo cycle in data/example/.
  • Solver core is stable. The Timefold integration honors hard / medium / soft tiers correctly for the constraints that are wired; ConstraintVerifier-style coverage would be a polish item, not a fundamental gap.
  • CSV-CLI parity is real. Both tcm init and the web wizard write equivalent data/<cycle>/ trees; the wizard validates per-file with the same column rules CsvDataLoader uses at finalize. Round-trip tests live in tests/test_wizard_round_trip.py.
  • Auth seams are in place. Every route handler depends on current_user; replacing the shim is a one-file change. Boot-guard already refuses to start with shim mode in production.
  • Destructive actions are now safe. Type-to-confirm on unlock + delete, the delete-while-running guard from #10, the lock-aware roster edits from the latest CRUD work, and the validate-before-rename on uploads collectively close the foot-gun surface.
  • Docker stack runs both modes. The same image serves CLI subcommands and the FastAPI server; the frontend has its own Node container with hot-reload from host source.
  • Trust-pack landed on feat/ops-handoff-prep. Six items from §13.2 closed in one branch — config.yaml weights now live, solver concurrency is locked, the student_max_position_count slack and student-preference O(prefs) cost are gone, A5 metrics cache once terminal, and A4 progress streams via SSE. See HANDOFF.md for the DevOps-facing summary.

13.2 What's brittle

These are real risks, ordered by how visible they would be on Brennan's first serious use of the platform.

  1. config.yaml weights are a lie. Resolved 2026-05-26 on feat/ops-handoff-prepConstraintParameters is now joined as a ProblemFactProperty in every soft-reward stream; YAML edits move the score.
  2. Co-instructor preferences are siloed. The meeting PDF explicitly says co-instructors share a ranking view. Today each instructor has their own draft, filtered by instructor_macid. This will be the first complaint from any course with two professors.
  3. Multi-section Case 2 isn't modeled. No section_mode column, no proration. A two-section course with two profs choosing "individual" mode silently double-allocates today.
  4. No email notifications wired. The audit log captures the events; nothing sends mail. The PDF lists notifications as a hard requirement ("all instructors should be notified when preferences are submitted"); deferring it past launch is fine if it's named explicitly to the admin team.
  5. No instructor confirmation stage. The cycle goes solved → locked with no intermediate confirmation, which means the PDF's "Students notified after instructor review" can't be honored. This blocks the August timeline.
  6. Student-preference lookup is slow. Resolved 2026-05-26 on feat/ops-handoff-prepStudent.get_preference_rank / has_prior_ta_experience now hit a lazy dict cache invalidated on len(course_preferences) change.
  7. student_max_position_count tolerates +1 over capacity as Hard. Resolved 2026-05-26 on feat/ops-handoff-prep — slack stripped, count > num_ta_positions is now a Hard violation.
  8. CV path exists; no upload pipeline. Resolved 2026-05-29 on dev/hand-off-v3 — real multipart upload route (POST /api/students/me/cv, PDF-validated, 10 MiB cap), disk storage under TCM_DATA_ROOT/<cycle>/cvs/, and a viewer (GET /api/cycles/{cycle}/students/{macid}/cv, admin/in-cycle-instructor/self) wired into the student portal + instructor swipe-mode CV viewer. cv_path still accepts a legacy external URL (rendered as a link, not in the PDF iframe).
  9. A4 polling pulls full-table scans every 2s. Resolved 2026-05-26 on feat/ops-handoff-prep — A5 metrics now serve from _TERMINAL_METRICS_CACHE once a run is terminal, and the running path was migrated to SSE (GET /runs/{id}/events) so the FE stops re-pulling the full state every 2s.

13.3 V2.1 — closing stakeholder commitments

Goal: every requirement from the May-5 PDF is either implemented or explicitly deferred in writing with stakeholder sign-off. Target landing before the June-15 launch if possible, otherwise as the first post-launch sprint.

Branch Scope Driver
feat/shared-co-instructor-rankings Drop instructor_macid from InstructorRanking PK (or add a merged view), record per-row last_edited_by / _at, update I2 to surface co-editor avatars. PDF Case 1
feat/multi-section-mode Add section_mode to Course + wizard step + proration in import + solver-side allocation split. PDF Case 2
feat/instructor-confirmation-stage New awaiting_confirmation stage between solved and locked; per-instructor approval row; instructor portal screen for confirming assignments before notify. PDF timeline
feat/notifications Mailer integration (SMTP relay default, McMaster mail API if available). Triggers: instructor submission, admin "ready for confirmation", student "you've been assigned". Templates in backend/notifications/. PDF "send automatic emails"
feat/cv-upload Shipped on dev/hand-off-v3POST /api/students/me/cv multipart, GET /api/cycles/{cycle}/students/{macid}/cv (admin/in-cycle-instructor/self), storage under TCM_DATA_ROOT/<cycle>/cvs/. Plus an instructor swipe-mode CV viewer. PDF "CV upload"
feat/structured-coop-status coop_status enum (none / fall / winter / both), migration with best-effort string mapping, form widget. PDF "avoid vague questions"
feat/config-weights-actually-applied Make ConstraintParameters a ProblemFactProperty consulted by reward expressions. Algorithm correctness
feat/admin-course-crud Mirror the student/instructor CRUD for courses + the CourseInstructor link. Closes the "I forgot to add a section" papercut. Operational reality

Estimated total: ~12-14 working days. Not all need to land before launch — prioritize co-instructor + section-mode + confirmation-stage as the deadline-critical set; the rest can ship in early August.

13.4 V2.2 — quality, scale, and polish

Once stakeholder commitments are closed, this iteration tightens the edges that will be felt at department-wide scale and during admin debugging.

  • Solver correctness/perf: model StudentPreference as a Timefold problem fact (incremental scoring), drop the +1 hard slack, auto-scale time_limit_s by problem size, audit the I2 UI for the multi-Preference-1 case.
  • Operational concurrency: asyncio.Lock around solver runs, per-run logger instance (no global mutation), stale-run reaper on uvicorn restart, JVM heap docs.
  • SSE migration: A4 progress switches from polling to SSE. Frees up the A5 metrics path to cache aggressively.
  • CLI hygiene: implement cmd_report, stop leaking tracebacks via logger.exception, route tcm validate through the logger, honor NO_COLOR / --no-color, guard _resolve_cycle for non-TTY.
  • Frontend polish: per-constraint score breakdown card on A5, preference-distribution histogram, A4 stop/extend buttons, compare-two-runs view, audit-log page, shared Field form primitive.
  • Test coverage: route-level tests for the new admin CRUD (httpx + TestClient), ConstraintVerifier coverage for each constraint method, solver smoke against data/example/ in CI.

Estimated total: ~3 weeks.

13.5 V3 — long-range vision

These are deliberately not committed. They reflect where the platform could go if it lives past one cycle and earns continued investment.

  • Cycle clone & longitudinal data. Starting cycle N+1 from cycle N's roster + supervisor relationships, with anonymized cycle-over-cycle KPIs.
  • CUPE Hours-of-Work integration. Generate the CUPE form contents from the locked assignment so the admin's last-week-of-August task is one click.
  • ML-assisted matching. Train on historical assignment success (instructor-reported TA performance + student satisfaction) to add a predicted_fit soft constraint. Cold-start data is N=1 cycle, so realistically a year out.
  • Multi-department support. Partition the schema on department so ECE and CompSci can share the platform without forking.
  • Mobile-first portal. A11y + touch-target pass; not a redesign.
  • Real-time co-editing. Beyond shared rankings — live cursors / CRDT for two profs editing simultaneously.
  • iCal export. Once locked, each TA gets an .ics with their assigned course lab times.

13.6 Decision principles for the next sprint

  1. Stakeholder gaps beat algorithm cleanups beat polish. A co-instructor sharing a draft is more important than a 30% solver speedup.
  2. Silent wrongness beats visible bugs. The config.yaml-ignored case and student_max_position_count slack would each look "fine" right up until they suddenly didn't. Fix them before they cost trust.
  3. Defer-in-writing, not by omission. Any meeting-PDF item we choose not to ship for V2.1 must be in the deferred list above with a named alternative (e.g. "no mailer this sprint — admin sends notifications manually") so the stakeholder isn't surprised.
  4. CLI parity is a feature. Every new web endpoint should either wrap an existing CLI helper or be backfilled with a CLI subcommand. The admin's escape hatch when the web stack misbehaves at 2 AM is the CLI.

13.7 Security audit (2026-05-27)

Two read-only static security reviews of the FastAPI backend, core library, frontend, and deploy config (the standalone report files have been folded in here and removed). Execution items live in TODO.md "Security hardening"; this subsection frames the why.

Headline: the platform is well-architected for security — role checks are consistent across ~67 authenticated endpoints, all admin mutations are audit-logged, file/path operations are confined with relative_to guards + size caps, there is no SQL-injection / eval / unsafe-deserialization / XXE surface, and dependencies are current. For the documented trial (behind the auth boot-guard and a TLS-terminating reverse proxy, internal users) the posture is acceptable. There are three code-level bugs worth fixing now and a set of production-hardening items that must close before any public deployment — the largest of which (SSO) is already planned as P5 / §12.4.

Findings (absolute severity / deployment posture):

ID Finding Severity Posture Home
F-1 Auth shim trusts unverified X-MacID High (guarded) Prod blocker, guarded for trial by the boot-guard Already planned — P5 feat/web-auth-sso, §12.4
F-2 CSV / formula injection in 3 run-export writers (bypass the existing csv_safe()) Medium Trial-impacting TODO Security hardening
F-3 No rate limiting on expensive / write endpoints Medium Prod hardening TODO Security hardening
F-4 Cross-cycle read via ?cycle= on GET /api/courses (non-admins) Low/Med Trial-impacting TODO Security hardening
F-5 GET /api/system/schema unauthenticated Low Trial-acceptable TODO Security hardening
F-6 Permissive CORS (methods/headers=*, credentials) Low Prod hardening TODO Security hardening
F-7 Swagger /docs + /redoc exposed Low Prod hardening TODO Security hardening
F-8 Default tcm:tcm Postgres creds in committed compose Low Dev-only TODO Security hardening

Trial pre-flight: fix F-2 (small, self-contained), confirm ENVIRONMENT ∈ {dev,test,ci} + explicit TCM_ADMIN_MACIDS, deploy behind a reverse proxy that is the sole network path, pin TCM_CORS_ORIGINS. Production blockers: F-1 (land SSO), F-3, F-4, F-6/F-7, F-8.

Second review — verified-resolved criticals. A second review (originally against commit 33d913c, now also folded in) flagged two Critical issues that were re-checked against current code and are already fixed: a repo-root wipeout via a .. cycle name reaching shutil.rmtree (now blocked by _RESERVED_CYCLE_NAMES + the relative_to(DATA_ROOT) guard in _cycle_dir, wizard.py:99,211-221) and an unbounded, admin-spammable solve queue (now an asyncio.Lock + 409 "already running" + _prune_live_runs, runs.py:159,439-448). Its net-new open items — auth-probe MacID enumeration, credential-in-localStorage, the dev sign-in gate shipping in prod, a failed-solve error-message leak, a latent Content-Disposition header-injection sink, missing HTTP security headers, and a few low items — are enumerated in the TODO section.

13.8 Performance & latency audit (2026-05-27)

Static performance review (no profiling/benchmarks) across the web backend, the Timefold solver + IO layer, and the React frontend. The standalone report has been folded into this section and the TODO.md "Scalability & ops", "Algorithm correctness", and "Frontend polish" sections (the report file has been removed).

Headline: the single highest-impact issue is that a fresh SQLAlchemy Engine (and its connection pool) is created and disposed on every HTTP request — pooling is defeated process-wide, taxing every endpoint. After that: a second family of N+1 course↔instructor queries (distinct from the already-fixed list_instructor_courses), the A5 metrics recompute that still bites on the running-run poll path (the terminal-run cache is already shipped), and an unmemoized drag-and-drop ranking UI.

Two accuracy notes carried over from the report (so the next reader doesn't over-prioritize):

  • The JVM is not booted per request. Timefold/jpype boots once per process; only SolverFactory.create() (constraint compilation, ~1–2s) is paid per solve, which is minor against a 60–300s solve.
  • The terminal-metrics cache evicts FIFO, not LRU. _evict_metrics_cache pops insertion order; the shipped note in §13.1 / TODO calls it "LRU." Harmless at the 100-entry cap, but the wording is wrong.

Top priorities (highest impact first): singleton DB engine · eager-load the remaining N+1s · short-TTL cache for running-run A5 metrics · React.memo the ranking cards + memoize the manual-review suggestion maps · reuse solution_to_dict()/groupings across exporters · solver plateau-cutoff default + de-dup the reward_student_preference rank lookup.

13.9 V3 hand-off features (2026-05-29, branch dev/hand-off-v3)

Three stakeholder-facing features built on top of the trial-ready base, each threading through the existing auth/DB/route conventions (no solver rewrite, no auth-seam change). DevOps-facing summary in HANDOFF.md §9.

  1. Pin + capacity-safe re-solve. The A5 results page gained a per-row pin toggle and a "Re-solve with N pinned" CTA. The pin-add route now refuses (409) any pin that would exceed a course's num_tas_required or a student's num_ta_positions, so the pinned set always stays within capacity and the hard constraints (course_exact_ta_count / student_max_position_count) remain satisfiable. Pinned seats are pre-assigned + @PlanningPin-locked in _apply_pins, which is what makes the remaining demand the solver sees shrink correctly — pinned seats consume both course and student capacity, never double-counted. Unappliable pins (course/roster changed since pinning) are logged [PIN.skip], not silently dropped. Tests: tests/test_pin_resolve.py.

  2. CV upload + view + swipe-mode. Closes §13.2 #8 / the feat/cv-upload row in §13.3. Students upload a PDF (magic-byte + content-type validated, 10 MiB cap); admins / in-cycle instructors / the student themselves view it inline. Storage is disk-backed under TCM_DATA_ROOT/<cycle>/cvs/<macid>.pdf (env-overridable; back it up — not in git, not in the DB) with traversal-safe path validation. The instructor ranking page gained a "Swipe + CV" mode that shows one candidate's CV beside rank/skip/veto controls, reusing the same bucket state as the drag board. Tests: tests/test_cv.py.

  3. Per-cycle announcements. A net-new feature (not in the original PDF): admins publish messages targeted at students, instructors, or everyone, per cycle; the matching audience sees a read-only banner on their portal (incl. the post-submission pages). New announcements table (alembic 0007), role-filtered reads, admin-only audit-logged writes, cross-cycle read guard. Tests: tests/test_announcements.py.

Verified live end-to-end with chrome-devtools-mcp against the example cycle (announcement audience filtering, student CV upload, instructor swipe-mode PDF viewer, pin toggle → "Re-solve with N pinned"). An adversarial multi-agent review then surfaced 7 findings — 5 fixed on-branch (vetoed-pin guard so a pin can't lock an infeasible solve; instructor-CV access requires teaching the cycle; non-blank announcement title; CV object-URL revocation; blob-fetch error detail) and 2 deferred-in-writing (see TODO.md V3 section). 220 backend tests pass; the Next.js production build is clean.