How course and program data is sourced from the Loyola catalog (catalog.luc.edu) and kept correct.
Every course number maps to exactly one title. A course title is always looked up from the
catalog by its number — never copied from a sibling program file or typed by hand. The Audit tab
de-duplicates shared courses across programs by code + title (src/utils/progress.ts), so a
divergent title for one number silently breaks credit-sharing. The pipeline and the course integrity test exist to enforce this.
| Path | What it is | Generated by |
|---|---|---|
src/data/dept-courses/<DEPT>.json |
Per-department course list (topics variants consolidated). Used for the UI and elective-pool population. | scripts/fetch-dept-courses.mjs |
src/data/course-index.json |
Flat, unconsolidated code → {title, credits} master map. The authoritative number→name lookup. |
scripts/fetch-dept-courses.mjs |
src/data/course-exceptions.json |
Documented exceptions to the integrity check (page-less prefixes, known-missing codes). Keep small and shrinking. | hand-maintained, tracked in LIKELY_WRONG.md |
src/data/<program>.json |
Major/minor/grad program requirements + roadmaps. | hand/AI-authored, titles normalized from the index |
Always ingest courses first, then make program data conform to them.
# 1. Scrape the master course list for every known department (writes
# src/data/dept-courses/*.json and rebuilds src/data/course-index.json)
node scripts/fetch-dept-courses.mjs
# …or refresh specific departments (merges into the index):
node scripts/fetch-dept-courses.mjs PHIL MATH
# 2. Rewrite every program's course TITLES from the index (one name per number).
# Reports any code not found in the catalog. Add --dry-run to preview.
node scripts/normalize-program-courses.mjs
# 3. Verify nothing drifted. The "course integrity" test fails if any program
# course code is missing from the index or its title differs.
npm test
# 4. Full gate before committing.
npm run typecheck && npm run lint && npm run buildWhole program files (src/data/<id>.json) are reproducible from committed inputs, not hand-typed.
Three layers, merged by one generator (each owns disjoint fields):
| Input | Path | Owns |
|---|---|---|
| Manifest (seed) | scripts/program-manifest.mjs |
id, name, degree, school, department, kind, catalogUrl, credit-total overrides |
| Extract (deterministic scrape) | src/data/program-extracts/<id>.json |
courses (code/title/credits), elective pools, roadmap |
| Overlay (AI, fuzzy semantics) | src/data/program-refine/<id>.json |
choose-N / credit caps: creditsRequired, note, requirementGroup, choiceNote |
| Supplement (non-catalog) | src/data/program-supplements/<id>.json |
checklist (AP/transfer/milestones), program note, hasCompletionEstimate, credit pins |
Pipeline:
node scripts/fetch-program.mjs # scrape program pages -> program-extracts/<id>.json (+ contentHash)
# author/refresh program-refine/<id>.json + program-supplements/<id>.json as needed (only fuzzy/non-catalog bits)
node scripts/build-programs.mjs # merge -> src/data/<id>.json (+ programs.ts at full rollout)
node scripts/normalize-program-courses.mjs
npm test && npm run check:programs # programIntegrity + byte-reproducibility drift guard- Titles/credits in the extract are reconciled against
course-index.json, so the catalog stays the source of truth for names. The build is deterministic and offline (it never calls a model — the AI overlay is committed JSON). - Migration is incremental. A manifest entry with
generated: trueis adopted: itssrc/data/<id>.jsonmust byte-matchbuild-programs.mjsoutput (npm run check:programsfails otherwise), so hand-editing an adopted file is forbidden — change the manifest/extract/overlay/ supplement instead. Entries without the flag are legacy hand files awaiting their migration wave. - Run
node scripts/build-programs.mjs --out /tmp/regen <id>and diff against the current file to vet a program before flippinggenerated: true.
When LUC publishes a new catalog year (and preserves the current page structure):
- Bump
CATALOG_YEARinscripts/fetch-dept-courses.mjs. - Run the pipeline above. Re-run #1 with no arguments to rebuild the whole index.
- Run
node scripts/normalize-program-courses.mjs— it reports codes not in course-index. For each: it's either a renumbered course (fix the code in the program JSON), a new page-less prefix (add toprefixesWithoutCatalogPage), or a genuinely missing code (add toknownMissingCodesand note it inLIKELY_WRONG.md). npm testuntil thecourse integritytest is green, thennpm run build.
Adding a new department: add a { code, name } entry to DEPARTMENTS in
scripts/fetch-dept-courses.mjs (override slug only if the catalog URL differs from the lowercase
code), then run the pipeline.
- Titles only. The normalizer does not change credit hours; program credit totals depend on
them. Catalog-vs-data credit mismatches are reported in
CREDITS_DISCREPANCIES.md. - Page-less subjects. A few prefixes (
FORS,FRSC,HSRV) have no/course-descriptions/<dept>/page; their titles live only in program JSON and are exempt from the integrity check. - Wrong course numbers. Where a program used the wrong number for a course, normalization makes
the displayed name match the (wrong) number — these are tracked in
LIKELY_WRONG.mdfor a code fix, not silently "corrected" by changing the title. - Roadmaps reference course IDs (not titles), so they inherit normalized titles automatically; no separate step is needed.