Mapping pipeline audit: what is CEDAR, and what is UNM
Audited 2026-09-09, after ISSUES.md I7 showed a mapping defect hiding 85% of a program’s students. The question this answers: is UNM-specific configuration cleanly separable from CEDAR as a platform?
Short answer: mostly, by accident rather than by design. The boundary is real in practice but declared nowhere, one institution-specific list is hardcoded inside platform code, and the duplicate copies of it have drifted apart.
The pipeline
academic_studies.qs (institutional export)
│
▼
generate_program_map() transform-to-cedar.R ← platform mechanism
uses: subj_dept_map, premaj_canon, xvar_explicit, ← institution data
extra_p2d, known_suffixes, real_F_progs,
ad_major_to_dept, allowed_unmapped_program_codes
│
▼
program_map.qs → catalog_lookups.R → major_college_to_dept ┐
subj_to_dept ├─ 4-tier
major_to_dept ┘ dept_code chain
│
▼
cedar_programs$dept_code transform_programs() ← platform mechanism
+ inline F-code list ← INSTITUTION DATA (defect)
Classification of R/lists/
Measured by institution-shaped content, not by intent:
| File | Lines | Kind |
|---|---|---|
grades.R | 41 | Platform — grade vocabulary and the DFW policy |
status_codes.R | 32 | Platform — Banner registration status codes |
catalog_lookups.R | 244 | Mixed — derivation mechanism with institution rules embedded |
data_semantics.R | 155 | Mixed — registry mechanism plus UNM entries |
subj_dept_map.R | 294 | Institution — subject → department |
program_code_maps.R | 223 | Institution — Banner code conventions |
mappings.R | 218 | Institution — name → code, HR org → dept (moved to institution/<id>/program_names.csv, program_name_aliases.csv, source_organizations.csv 2026-10-16) |
excluded_courses.R | 226 | Institution (moved to institution/<id>/excluded_courses.csv 2026-10-16) |
gen_ed_courses.R | 151 | Institution (moved to institution/<id>/gen_ed_courses.csv 2026-10-16) |
population-presets.R | 174 | Institution — named program groups (moved to institution/<id>/population_groups.csv 2026-10-16) |
enrollment_projection_groups.R | 133 | Institution — monitored courses (moved to institution/<id>/projection_groups.csv and projection_courses.csv 2026-10-16; the file keeps the platform model registry) |
campuses.R | 101 | Institution (moved to institution/<id>/campuses.csv 2026-10-16; the file is now platform mechanism) |
Ten of twelve files are institution configuration. Nothing says so. AGENTS.md describes the directory as “static constants, domain lookups”, and first-hour.md covers the synthetic demo rather than adoption, so an adopter has no list of what to replace.
Findings
1. Institution data hardcoded in platform code
transform-to-cedar.R carried a sixteen-code UNM F-prefix list inline while generate_program_map() read real_F_progs from R/lists/program_code_maps.R. Platform code containing institution configuration is the violation, regardless of whether the values are right — and it is why the two drifted without anyone noticing.
Fixed: the inline list is now pre_major_exempt_codes in R/lists/program_code_maps.R, identical in membership, and an architecture test fails if institution codes reappear as literals outside R/lists/.
A first pass at this audit claimed the two lists “answer the same question and disagree” across 23,272 rows. That was wrong, and the correction is instructive about auditing from counts rather than mechanism. They serve different consumers:
| Constant | Consumer | Question |
|---|---|---|
real_F_progs | generate_program_map() | is this a real program for prog_type and department routing? |
pre_major_exempt_codes | transform_programs() | should the NAME decide pre-major status here, rather than the F-prefix? |
pre_major_exempt_codes is load-bearing: without it, 4,664 declared Film and Digital Arts majors would be flagged pre-majors. What remains is narrower and real — real_F_progs lists four codes the transform treats as pre-majors in every row, one of which (FCS) has a demonstrated consequence. ISSUES.md I9.
2. The flag recorded its answer and destroyed its evidence
is_pre_major is set by two signals — a Pre- program-name prefix and the F-code convention — and the prefix is stripped from program_name immediately after. So 13,646 rows, 11% of all pre-major flags, carried no evidence of why they were flagged. Any disagreement between the signals was unauditable, which is what made finding 1 look like a contradiction rather than two rules doing different jobs.
Fixed: cedar_programs$pre_major_basis records which signal decided each row — name_prefix, code_convention, name+code, phrd_undergraduate, or absent when the row is not a pre-major. Stripping the prefix remains correct: it is what lets a pre-major and its declared program share a program_name, which is how population building resolves FRAD and RADS as one program. The flag is the answer; the basis is the evidence; both are needed.
3. A semantic break coded as a special case
# PHRD used for UG pre-pharmacy students before 202580 (switched to FPHS)
(major_code == "PHRD" & student_level %in% c("UG", "NG"))
A code whose meaning changed at a known term is exactly a CEDAR_DATA_SEMANTICS entry. Here it is an inline condition in the transform, invisible to every consumer and to the reader of any number it affects.
4. Branch-campus knowledge hardcoded
branch_campus_suffixes <- c("GA", "LA", "TA", "VA") and progs$col[branch_mask] <- "AD" are UNM’s campus and college codes written into the transform. An adopter with different branch codes must edit platform code.
5. Cosmetic: examples are UNM-flavoured
Roxygen blocks across branches, cones and features illustrate with "AS", "HIST", "MATH". Harmless — no executable path depends on them — but it reinforces the impression that the platform assumes one institution.
What the boundary should be
Two categories, declared:
- Platform — mechanism, vocabulary, and policy that any adopter inherits: the dept_code tier chain, the anomaly screens, the registry accessors, grade and status vocabulary, the DFW policy.
- Institution — every code, name, course, campus, college and program list that describes one university.
The rule that follows: platform code may read institution data through a named constant, and may never contain one. Finding 1 is the only executable violation; findings 3 and 4 are the same violation in weaker form.
Recommended sequence
- Unify the two F-code lists into one institution constant, and decide which membership is correct. This changes
is_pre_majorfor real students, so it needs a decision and a measured before/after, not a quiet edit. - Move findings 3 and 4 into institution constants — the PHRD term rule into
CEDAR_DATA_SEMANTICS, the branch suffixes and branch college intoR/lists/campuses.R. - Declare the boundary: a header convention in each
R/lists/file stating Platform or Institution, this table inAGENTS.md, and an adoption section infirst-hour.mdnaming the ten files to replace. - Enforce it with an architecture test that fails when a campus, college, department or course literal appears in executable code outside the institution files — the same shape as the existing
CAMPUS_ROLLUPtest.
Step 4 is what keeps this from re-eroding. Steps 1 and 2 are defects; step 3 is documentation; only step 4 makes the boundary hold.
What is already clean
The four-tier dept_code chain, generate_program_map(), catalog_lookups.R’s derivations, the anomaly screens, and the semantics registry are all mechanism that takes institution data as input. mappings.R explicitly records which lookups were superseded and warns against re-adding them. The separation was being maintained by care; it now needs to be maintained by structure.