PolyDraftBack to app

Technical documentation

PolyDraft

A course & schedule planner for the Cal Poly SLO B.Arch program — map out all 10 semesters, pick real course sections, catch schedule and prerequisite conflicts, and account for incoming credit, before registration day.

Overview

PolyDraft was built for one family's real 5-year academic planning, starting with the B.Arch program. It replaces a spreadsheet with a data-backed planner: the 10-semester flowchart is sourced directly from the official Cal Poly catalog, course sections are scraped live from the university's public Class Search system, and every schedule change is checked against real prerequisite chains and time conflicts as it happens.

The project has three audiences in one: a student picking real sections each term, a parent reviewing the overall path to graduation, and — as of the most recent feature — an automated importer that reads an official Degree Progress Report and turns it directly into a plan.

Stack

  • Next.js 16 (App Router, Turbopack) + TypeScript
  • Tailwind CSS v4
  • Clerk for authentication (email/password)
  • Neon Postgres + Drizzle ORM (via the neon-http driver)
  • Vercel AI Gateway (AI SDK) for the Degree Progress Report import
  • Vitest for unit tests, deployed on Vercel

Architecture

The data model runs from a global catalog down to per-plan copies: programs and courses hold the canonical Cal Poly catalog data (subjects, units, prerequisites, typical offering term), curriculum_slots holds the official 10-semester flowchart for a program, and each plans row gets its own materialized copy — plan_slots / plan_slot_courses — that a student can freely edit without touching the shared template or anyone else's plan.

A recurring pattern across the codebase: Neon's neon-http driver has no db.transaction(), so every multi-statement mutation — materializing a roadmap, duplicating a plan, applying a batch of imported credits — pregenerates row IDs client-side with randomUUID() before building the insert/update statements, then commits them all in one db.batch([...]) call. Parent and child rows can reference each other's IDs before any of them exist in the database, and the whole operation succeeds or fails together.

Validation logic (prerequisite ordering, time/instructor conflicts, unit-load bounds, term-offering parity) is written as small, dependency-free pure functions with no database or Node imports — the same functions run identically whether they're called from a Server Action or, in principle, the client. That separation is also what makes them straightforward to unit test with plain fixture data instead of a database.

Curriculum & prerequisites

The 10-semester B.Arch flowchart wasn't summarized from a PDF or an AI pass over the catalog — it was parsed directly from the raw HTML of the official Cal Poly catalog pages, and every semester's unit total was cross-checked against the catalog's own displayed subtotal before being trusted. The seed script re-asserts those totals every time it runs, so an edit that accidentally breaks a semester's unit count fails loudly instead of silently drifting.

Prerequisite chains — including real AND/OR groups like “ARCH 2231 or ARCH 2232” — were pulled the same way, course by course, from the catalog's own prerequisite listings.

Live section scraper

Course sections for the current term are scraped from Cal Poly's public Class Search, a classic PeopleSoft interface. It turned out to be reachable over plain HTTP with no headless browser — a cookie-bootstrap redirect, a search POST, and a result page parsed for the meeting-time, room, instructor, and open/closed status fields PeopleSoft embeds in its HTML element IDs.

The scraper runs per-subject with independent error handling, so one subject failing to parse never loses previously-scraped data for the others — a stale-but-present dataset beats an empty one. It runs daily on a Vercel Cron job.

Conflict detection

Every semester's picks are checked for three kinds of conflict as they're made: overlapping meeting times, the same instructor double-booked across two sections, and (once real data exists for it) overlapping final exams. Prerequisite satisfaction is checked separately, across the entire planned arrangement — not just sections actually picked — so a course scheduled two semesters before its own prerequisite shows a warning immediately, before a section is ever selected.

All of these are non-blocking warnings, not hard stops — a plan can be saved in an imperfect state.

Incoming credit & editable roadmap

AP scores, dual-enrollment, and transfer credit are recorded as plan_credits rows and can be linked directly to a roadmap slot — either a specific course (unlocking anything that depends on it, via the prerequisite chain treating it as completed in a synthetic “semester 0”) or a general education requirement with no specific Cal Poly equivalent.

The roadmap itself is editable: slots can move between semesters or be deleted, and every edit is revalidated against the same unit-load, prerequisite-ordering, and term-offering-parity checks used everywhere else — consistently surfaced as warnings, never a block on saving.

Plan duplication

A plan can be cloned into an independent copy for “what if” exploration — a lighter senior year, a different elective mix — without disturbing the original. The clone copies the roadmap arrangement and incoming credits, remapping the internal ID cross-references between them, but deliberately leaves section selections behind: those are tied to a specific term's real sections and would only confuse a re-planned copy. Viewing a plan is enough to duplicate it — the clone is always owned by whoever makes it, regardless of who owns the original.

AI-ingested Degree Progress Report import

The newest feature turns an official Cal Poly Degree Progress Report — a dense, PeopleSoft-generated PDF — directly into a ready-to-use plan. The pipeline splits cleanly between what an LLM is good at and what deterministic code is good at:

Upload PDF
AI extraction
Slot matching
Credit + link
Rebalance
New plan

The PDF is sent directly to a multimodal model as a file, alongside a schema describing exactly what to pull out — which requirements the report marks satisfied, the course or transfer-credit details behind each, and its degree-requirement area. Reading the report's own status icons and requirement tables is exactly the kind of messy, context-dependent extraction an LLM handles well and brittle string parsing wouldn't.

{ role: "user", content: [
  { type: "text", text: instructions },
  { type: "file", mediaType: "application/pdf", data: pdfBytes },
]}

Everything after extraction is plain, deterministic, unit-tested code — matching each item to a real course or the right general-education slot, generating the credit rows, and linking them, is a pure function with no AI or database calls of its own.

Crediting out a semester's worth of general-education requirements can leave later semesters unevenly loaded, so a final pass rebalances the remaining flexible elective slots — never a required major course, so the prerequisite sequence is never touched — moving one only when doing so strictly reduces the plan's total number of validation warnings. That guarantee makes the rebalancer safe by construction: it can never produce a worse arrangement than leaving everything in its default position, and the loop is guaranteed to terminate because the warning count can only go down.

Status

Shipped: curriculum + prerequisite seeding, the roadmap and weekly schedule UI, conflict and prerequisite validation, the live section scraper, incoming credit with an editable roadmap, plan duplication, and the AI-ingested Degree Progress Report import described above.

Not yet built: professor ratings (PolyRatings integration), sharing a plan read-only with a parent, and a plan-wide progress summary page. Only the current term has live scraped section data — Cal Poly hasn't published sections for future terms yet.