ADR-002: Explicit mapping files replace runtime department inference
- Status: Accepted 2026-10-03. Decided: a unit is the source system’s organisation (Banner’s department) by default, keeping CEDAR’s existing splits of a Banner department as recorded splits. Stages 0, 1 and 2 are done.
- Date: 2026-10-03
- Supersedes: the code-parsing department chain (
generate_program_map(),program_map.qs, and the overrides inR/lists/program_code_maps.R), and the program-to-department review list as the way to fix mappings. - Complements: ADR-001, whose unit dimension this defines; institution-boundary-audit.md, which found ten of twelve
R/lists/files are institution configuration. - Resolves when done: ISSUES I7 (phantom departments), I9 (pre-major lists disagree), I11 (concentrations mapped by name), and the ROADMAP item “Externalize department/program/subject mappings to YAML or CSV data files”.
Context
Every CEDAR department report rests on one question: which unit does this course, program, or student belong to? CEDAR answers it at transform time, by inference:
- A program’s department is rebuilt from its code through five tiers:
major_college_to_dept,subj_to_dept,major_to_dept(derived from a generatedprogram_map.qs),extra_p2d, and finally the program code itself. - A course’s department is its subject code looked up in
subj_dept_map, falling back to the subject code itself. - Thirteen lists feed this, totalling about 1,060 lines, plus a 180-line generator that parses Banner program codes into degree, major and college.
The October 2026 pipeline audit measured what that costs:
| Failure | Measured |
|---|---|
| Programs reported under a department named after their own code | 77 codes |
| Courses reported under a department named after their subject | 21 subjects, 10,221 enrollments |
| Overrides written down but silently ignored | FPMD="PHRM" and three more, until #106 |
| Concentrations with no department, or the wrong one by name | 23,304 rows since Fall 2024; Public Policy counted under PADM for Political Science students |
| Programs mapped to the wrong department | East Asian Studies, Comparative Literature (#100) |
Each was found one at a time, after someone noticed a number was wrong. The inference is invisible: a wrong answer looks exactly like a right one.
Meanwhile Banner already records the owning department. Department is native to the Academic Studies, degrees and class-list exports, 99.9% filled, and was identical on all 12,786 rows re-pulled a median 153 days apart. It names the right owner for every major fixed by hand this year: East Asian Studies, Comparative Literature, Doctor of Pharmacy, Radiologic Sciences and Health Administration. CEDAR read it until March 2026 and replaced it with code parsing because the name-to-code crosswalk covered only 29% of rows.
Two facts shape the replacement:
- Native fields are good evidence but not a contract. Banner’s department is too coarse in places (one “Provost Branch Campuses” for 38 programs), is a bucket rather than an owner in others (
*Interdisciplinary: A.S.holds Museum Studies, Biochemistry and the MPP), and covers only the primary major. - CEDAR has to run outside UNM. Another institution has different programs, departments, export columns, and its own messy data. Logic tied to UNM’s MyReports fields cannot be adopted; a reviewed text file can.
Decision
The pipeline reads explicit mapping files and nothing else to assign units. Native fields and data patterns are used only to propose rows for those files, by a separate script, for a person to confirm.
Three roles, kept apart:
| Role | Who | Reads | Writes |
|---|---|---|---|
| Evidence | Source exports | Native fields such as Banner Department | |
| Proposal | scripts/propose-mappings.R | Exports + existing mapping files | New rows marked proposed, with their evidence |
| Contract | Transform | Mapping files only | Units on every CEDAR table |
The transform never parses a code, never falls back to a self-named unit, and never reads a source department field to decide anything.
The mapping files
One directory per institution, institution/<id>/, chosen by the CEDAR_INSTITUTION environment variable (default unm). Not a config.R setting, because config.R is not committed and CI, the demo stack and the transform run without one. UNM is institution/unm/; the synthetic demo gets institution/demo/. Plain CSV, UTF-8, one row per key, so every change is a one-line diff.
units.csv: what a department report is about
| Column | Meaning |
|---|---|
unit_code | Short stable code, e.g. HIST, THDA, MSST |
unit_name | Display name |
college_code | The unit’s home college (decided 2026-10-04; see “Colleges are mapped, not read”) |
kind | department, program (a standalone interdisciplinary unit), or college (a college-wide unit owning courses no department does, coded college + CW: ASCW). Added 2026-10-04 with its first use; replaces the draft’s school |
status (deferred) | active or retired; retired units keep their history |
status was planned for Stage 2 and deferred: nothing reads it yet. Add it with its first reader.
The first draft gave units no college, because 11 units appear under two colleges in subjects.csv (PADM under Arts & Sciences and the Provost, for example). That is superseded: each unit has one home college, and the 11 are decided once. See below.
subjects.csv: course subject to unit
subject_code, in_college, in_level, unit_code, college_code, status, evidence, notes. Replaces subj_dept_map.R.
in_collegeandin_levelnarrow which courses a row covers: branch campuses reuse subject codes (HLED,PH,SUST) for units that differ from the main campus’s, and a subject’s courses can belong to different colleges by level. The most specific confirmed row wins (subject + college + level, subject + college, subject + level, subject).college_codenames the college credited with the courses when it is not the unit’s home college (decided 2026-10-04): Global & National Security is one unit, its undergraduate courses University College’s and its graduate courses Graduate Studies’.statusandevidencework as inprograms.csv: the assistant proposes a row for every subject the data uses with no row, and only confirmed rows map.- From Stage 3 the transform takes the most specific confirmed row, after translating the section’s college through
colleges.csv. The runtime lookups built fromsubj_dept_mapstill take the first confirmed row for a code until Stage 4, so row order still matters to them.
programs.csv: program code to unit
| Column | Meaning |
|---|---|
program_code | The major, minor or second-major code as it appears in the data |
college_code | Blank for every college, or a college where this code has a different unit. A college row wins there. Only two UNM codes need one (BADM, CRIM at the branches) |
program_name | Display name |
unit_code | Owning unit |
is_pre_major | TRUE / FALSE, stated rather than inferred |
leads_to | For a pre-major, the program code of the degree it leads to (replaces premaj_canon). A program code, never a department code; blank when that degree has no code. It translates pre-major to major and records no history: a code that replaced another is lineage (institution/unm/README.md) |
basis | Why this unit: source_department, subject_code, inherited, name_match, course_taking, decided, override, no_unit (nothing owns it), unresolved (no evidence; always proposed) |
status | confirmed or proposed |
evidence | What the assistant saw, e.g. Banner Department "Radiology" on 236 rows |
notes | Free text, e.g. the date and who decided |
colleges.csv
college_code, college_name, source_names (every spelling the exports use, |-separated, replacing college_name_to_code).
source_departments.csv: evidence aid, not read by the transform
source_name, unit_code, kind, notes. Records what each source-system department means, so the assistant can propose program rows from it. kind is department (one unit), split (several units, |-separated; the assistant places each program among them), bucket (no unit: every program in it needs its own evidence) or non_degree (its programs belong to no unit). The source_code column first proposed was dropped: UNM’s exports carry only names. Every spelling needs a row, because Banner renames departments.
settings.csv
setting, value. Holds mapping_files_url, the GitHub location of the institution’s directory, so Admin > Mappings can link each proposed program row to its line. Mapping decisions are edits to these files in the repository, never in the app: the running container’s copy is replaced by the next deploy.
Rules that need no file
- Concentrations take the unit of the student’s primary major. Measured on every concentration in the Academic Studies file: none fit only a second major. Should an institution need exceptions, an optional
concentrations.csvcan be added later. - Second majors and minors use their own code’s row in
programs.csv, never the student’s primary-major department.
Validation
validate_mapping_files() runs at startup and in the test suite, and stops with every problem listed: missing columns, duplicate keys, a unit_code absent from units.csv, a leads_to that is not a program, an unknown basis or status. Invalid files are an error, never a warning.
Colleges are mapped, not read (decided 2026-10-04)
The mapping files state how the institution is organised, so that no reported relationship depends on which fields an export happens to carry. That covers colleges as much as units:
- A program’s college is its unit’s college: program → unit (
programs.csv) → college (units.csv). An optional per-program college, for a program that sits in a different college from its owning unit, overrides it. - Reports use the mapped college. The college the source recorded on each row is kept beside it as
source_college, for audit only, and the mapping audit lists every row where the two disagree. That is how a rename is seen: UNM’s College of Education appears asEDand “College of Education” before 2021 and asEHand “College of Educ & Human Sci” after (ISSUES I12). - History is restated to today’s organisation, as it already is for units: a program that moved colleges reports under its current college in every year.
source_collegekeeps the as-recorded view. - Every spelling and former code a source uses for a college is listed in
colleges.csv(source_names), so source values are translated explicitly and an unknown one is listed, never silently dropped. - One source college stays an input: a course section’s own college (the DESR
COLLEGE), becausesubjects.csvkeys on it to tell a branch campus’sHLEDfrom the main campus’s. It chooses the unit; it is not reported as the course’s college.
Two changes follow in the files. units.csv gains college_code, and the assistant proposes each unit’s from the data for review. In programs.csv, the column that today means “this row applies only in that college” is renamed in_college, so that no column called college_code means two different things.
Decided with it, 2026-10-04:
- Graduate students report under their academic unit’s college. Banner’s Actual College files 79% of graduate major rows under Graduate Programs (
GP), an administrative home; academic units keep their graduate students. - Pre-majors report under the college they lead to, through
leads_to: a pre-Nursing student counts under Nursing, not University College. - Banner’s Translated College is the evidence and the check, not the reported value. It is Banner’s own translation of each student to an academic college: graduate students to their colleges, most pre-majors to their target college, and the I12 renames already applied. The chain program → unit → college agrees with it on 96.9% of 393,143 UNM major rows. The assistant proposes unit home colleges and per-program overrides from it (Biochemistry’s courses are taught by Medicine but its majors are Arts & Sciences), and the audit compares the mapped college against it, so the remaining disagreements are each a reviewed decision. An institution whose exports carry nothing like it gets proposals from weaker evidence, and the same mapping.
- “Undergrad Certificate Program” is
AD, with the branch campuses’ other programs; “University Studies” isUC;EDand “College of Education” areEH(ISSUES I12).
Why not read the college from each row, as CEDAR did: it is implicit (nobody can point to where “Nursing is in the College of Nursing” is stated), it inherits every source quirk (a rename splits one college’s history in two), and an institution whose exports carry no college gets nothing. Reading per row is right for events – what a student declared, which course they took – and those stay in the data.
The pipeline contract
- Every subject and program code in the data is looked up in its file. There is one tier.
- A code with no row gets no unit (
NA). It is listed on Admin > Data & Usage > Mappings and in the refresh report. It is never given a self-named unit, and it never blocks the refresh, which the whole app depends on. - Only
confirmedrows assign units.proposedrows are visible on the Admin page but assign nothing until someone confirms them. - The unit a row resolved through is kept, so any number can be traced to the line in the file that produced it.
The mapping assistant
scripts/propose-mappings.R --institution <id> [--write]
- Reads the parsed exports and collects every distinct subject code, program code (primary, second major, minors), and source department name.
- For each code with no row, proposes a unit, strongest evidence first:
- the source department, through
source_departments.csv(primary majors); - inheritance: a minor or second-major code that is someone’s primary major takes that program’s unit;
- for a pre-major, the unit of the program it leads to;
- a name match against
units.csv; - course-taking: which unit’s courses the program’s students take most.
- the source department, through
- Writes the evidence beside each proposal and marks it
proposed. - Never edits a
confirmedrow. Dry run by default;--writeappends. - Prints a short review list: new codes, proposals, and codes it could not propose for.
Run it at setup, and whenever the Admin page shows unmapped codes. The daily refresh may run it in dry-run mode so new codes appear on the Admin page the day they arrive.
For UNM its first run reproduces most of today’s departments from Banner’s Department, leaving the cases Banner cannot settle, already drafted in docs/developers/drafts/unit-decisions-draft.csv (branch drafts/unit-decisions): 20 programs in interdisciplinary buckets, 38 branch-campus programs, and 44 codes that only ever appear as minors or second majors.
Migration for UNM
Staged, each stage compared against the previous output before it ships.
| Stage | Work | Accepted when |
|---|---|---|
| 0 | Snapshot today’s unit for every program and subject (scripts/unit-mapping-baseline.R snapshot; compare diffs any later build against it) | Done. 741 program groups, 263 course groups |
| 1 | Write institution/unm/units.csv, subjects.csv, colleges.csv from subj_dept_map.R; add the reader, validator and tests | Done. Built table identical to the old one; rebuilt sections, students and programs differ from the baseline in 0 groups |
| 2 | Build the assistant; generate programs.csv and source_departments.csv | Done. See Stage 2 results |
| 2b | Colleges mapped (units.csv and per-program college_code, colleges.csv source_names); the mapping audit at the end of every transform and on Admin | Done. Mapped college agrees with Banner’s Translated College on 98.98% of major rows; the rest are decided pre-major differences and two to review. Changes no number |
| 3 | Transform reads the files for units; both self-naming fallbacks removed | Done 2026-10-09. Rebuilt from the same exports, programs, degrees, sections and class lists differ from Stage 1 only as decided; no real unit loses a row. See Stage 3 results |
| 3b | Colleges reported through program → unit → college, and through subject rows for courses | Done 2026-10-10. Units unchanged from Stage 3; colleges differ only as decided. See Stage 3b results |
| 4 | Retire generate_program_map(), program_map.qs, and the lists it fed | Done 2026-10-10. Tests pass with the lists gone; units and colleges identical to Stage 3b; pre-major flags differ only as reviewed. See Stage 4 results |
| 5 | Give the synthetic demo its own institution/demo/ files | Done 2026-10-10. The demo builds with institution/unm/ absent; its course units and colleges are identical to the UNM-files build. See Stage 5 results |
| 6 | Move the remaining UNM R lists to configuration files under institution/<id>/ (CEDAR_CONFIG_FILE_SPECS), one list at a time | Done 2026-10-16. campuses.csv, course_levels.csv, gen_ed_courses.csv, excluded_courses.csv, population_groups.csv, projection_groups.csv, projection_courses.csv, program_names.csv, program_name_aliases.csv, source_organizations.csv; every R/lists/ file is now platform, and the constants they build are unchanged, so no number moves |
Stage 5 results (2026-10-10)
institution/demo/ holds the demo’s own colleges, units, subjects and programs, covering every code the fixtures use; dev/generate-demo.R and the demo app’s config set CEDAR_INSTITUTION=demo. With institution/unm/ moved away, the demo builds. Against the demo built on UNM’s files: sections and class lists identical in unit and college; programs differ only where UNM’s files had no row for a fixture code (GES, PSYC, PSYC-MIN and the business codes now have units) and in BUSA’s few fixture pre-major rows, since a flag is stated per code. test-demo-data.R fails if the generator reads UNM’s files.
Stage 6 (2026-10-16): every remaining UNM R list moved to institution/<id>/ as a configuration file – campuses, course levels, Gen Ed areas, the excluded list, the population groups, the projection scopes and the three text maps – and the demo institution carries its own copy of each. A configuration file differs from a mapping file in who reads it: the R/lists file that builds its constant reads and validates it at load (CEDAR_CONFIG_FILE_SPECS), and the platform code that reads the constant does not change.
Stage 4 results (2026-10-10)
program_map.qs, generate_program_map(), scripts/build-program-map.R and R/lists/program_code_maps.R are gone. catalog_lookups.R builds the runtime vectors from the files (major_to_dept from confirmed every-college programs.csv rows; premajor_leads_to from leads_to, which replaced premaj_canon in the population groups); the transform reads each code’s is_pre_major from programs.csv, keeping the PHRD undergraduate rule (row-level, in the data semantics) and falling back to Banner’s “Pre-“ name, labelled pre_major_basis = name_prefix, only for a code with no row. A concentration has no code and never takes it: until 2026-10-14 it did, which made the “Pre-Law” concentration a pre-major (ISSUES.md I21).
Rebuilt from the same exports and compared with Stage 3b: units and colleges identical except 5 program rows, undecided-major students whose blank Translated College now falls back to an Actual College that translates (“Undergrad Certificate Program” → AD). Pre-major flags changed on 4,138 rows, exactly as reviewed:
- 13 F and XF codes became pre-majors (FES, FFDA, XFBA, XFPY, FLAI, FNE, FIDA, FPE, XFNA, XFCH, XFJM, XFCC, XFIT). Banner’s own
Programfield names every one “Pre-“ on every row and none has awarded a degree; the old rule exempted five and could not see XF codes. - 5 branch “Pre-“ programs stopped being pre-majors (ASPE, PBA, PRSC, APHS, PHSC): associate degrees and a certificate students are admitted to, decided with the user. The “no degrees awarded” evidence was void for them: the degrees export holds no associate degrees (ISSUES I18).
The Dept Trends scope box lists a unit’s codes from programs.csv; Admin’s “Reviewed Exceptions” tab and its “old program_map checks” are gone; a dead Pathways warning about identity fallbacks is removed.
Stage 3b results (2026-10-10)
Decided with the user before building: a student counts under their primary major’s college on every row (Headcount’s college filter keeps meaning “students in this college”); a program nothing owns may name a college (Undecided → UC); the four School of Medicine programs Banner’s Actual College files under UC (Dental Hygiene, Radiologic Sciences, EMS, Medical Laboratory Sciences) report under ME by the rule; a code not yet decided reports Banner’s college, labelled college_basis = banner; and branch-campus sections of main-campus units stay in the branch college (their in_college AD rows set college_code AD).
Rebuilt from the 2026-10-08 exports and compared with Stage 3, row by row: units identical in all four tables, one college per student-term, and
| Table | What changed |
|---|---|
cedar_programs, Fall 2025 | 1,041 of 33,352 rows against Banner’s Translated College, the column the app read: pre-majors to their target’s college (pre-Nursing 811 to NU, pre-Pharmacy 139 to PH, pre-Population Health 32 to PO), and about 60 branch students whose Translated College was blank gaining one (21 of them labelled banner). Graduate students and the four Medicine programs did not move: Translated College already placed them so |
cedar_sections | 11,712 of 242,413: 10,423 Education sections ED → EH (I12); 1,092 Provost-filed sections (PA) to their units’ colleges (AS 616, UC 476); about 200 restated to today’s organisation (Native American Studies’ 2018–19 sections UC → AS, BCIS 2019 EN → MG, and a handful of cross-listed one-offs) |
Stage 3 results (2026-10-09)
The transform resolves every unit through the files: resolve_program_units() for programs and degrees, concentrations taking their primary major’s unit, and resolve_course_units() for sections and class lists. Rebuilt from the 2026-10-08 exports and compared row by row with the tables Stage 1 code built from the same exports:
| Table | Rows changed | What changed |
|---|---|---|
cedar_programs | 118,779 | 82,018 concentration rows gain their primary major’s unit; 33,505 rows move from a phantom unit to none (Non-Degree and Undecided, by decision, and proposed codes); 3,255 gain a confirmed unit. One concentration row loses LCL: its student’s primary major is Non-Degree |
cedar_degrees | 144 | All gain a confirmed unit |
cedar_sections | 1,101 | 592 move from a phantom unit to none (subjects still proposed or with no row); 422 gain FACW; 87 branch-campus sections of HLED, PH, BUSA, SUST move to the branch units |
cedar_students | 9,045 | The same, in class-list rows (814 branch-campus rows) |
No non-concentration row anywhere loses or changes a real unit except the branch-campus sections, which is the college-keyed lookup this stage exists for. Two things made that true and are worth knowing:
- A literal college match would have dropped 10,370 Education sections. Subject rows are keyed on
EH; sections before 2021 carryED. The course lookup now translates the section college throughcolleges.csvfirst. - 588 more sections were taught under a college their subject had no row for (
ECEDunderEH,NATVunderUC,WRunderGP, and seven one-offs). Each got a row carrying the unit it had, so this stage moved none of them.
Two codes that had a real unit were still proposed and would have lost it: ENVS (EPS) and ASPE (the branch Pre-Engineering unit). Both were decided first. Colleges are unchanged; they are Stage 3b.
Stage 2 results (2026-10-03)
scripts/propose-mappings.R proposed a row for each of 420 program codes in the exports. scripts/unit-mapping-baseline.R reconcile then compared each proposal with the unit CEDAR reported at Stage 0:
| Outcome | Codes |
|---|---|
| Assistant agreed with Stage 0 independently; confirmed | 307 |
Branch-campus college rows (BADM, CRIM in AD), beside their codes; confirmed | (2 rows) |
| Stage 0 unit was a hand decision the assistant disagreed with; kept (decided by class) | 30 |
Non-Degree, Undecided: no_unit (decided in department_less_major_codes) | 2 |
Self-named phantom unit, or none, at Stage 0; left proposed (decided) | 79 |
Stage 0 unit derived by code parsing, assistant disagrees; left proposed | 2 |
Pre-majors: where a code’s rows all agreed at Stage 0, the flag was carried over. 16 codes were pre-majors on only some rows; the decision was that the F code decides (F and XF are pre-majors), overruling pre_major_exempt_codes for FES, FPE, FNE, FFDA and FLAI (ISSUES I9).
unit-mapping-baseline.R files previews Stage 3 against the baseline: no confirmed program changes unit. What changes is exactly what was decided: concentrations take their primary major’s unit (83,138 rows, 82,024 of which had none), Non-Degree and Undecided lose their phantom units (25,523), and the 81 proposed programs report under no unit until confirmed (11,618).
Two lessons recorded so they are not relearned:
- Read the exports the transform reads. The first run used the repository’s
data/academic_studies.qs, a June copy, and missed 12 codes including FRAD (I7’s Radiologic Sciences) plus two renamed Banner departments. - Compare row by row. A first reconcile tested each proposal against the whole column of Stage 0 units, confirming 65 wrong rows (FBAD as ACCT among them). The Stage 3 preview caught it: a confirmed row should never move.
Found while mapping colleges, for Stage 3. The course transforms look a subject up by its code alone (subj_to_dept), so a branch campus’s HLED, PH, SUST and BUSA sections land in the main campus’s units (HED, HSCI, GES, MGMT), and the 18 branch-only units in subjects.csv receive no sections at all. Stage 3’s lookup must key on subject and the section’s college.
Stage 5 is the adopter test: if the demo institution works from its own files alone, so can a real one.
What this retires
generate_program_map() and program_map.qs; xvar_explicit, known_suffixes, ad_major_to_dept, allowed_unmapped_program_codes, real_F_progs, pre_major_exempt_codes; most of extra_p2d; the department half of catalog_lookups.R; the program-name department lookup used by headcount; and the identity fallbacks in transform_programs() and the course transforms. subj_dept_map.R, mappings.R and premaj_canon move into the CSV files rather than disappearing.
Consequences
Gains. A wrong unit is a visible line in a file, not an emergent result of five tiers. Adopting CEDAR means writing files, helped by the assistant, not changing code. Every mapping change is reviewable in a diff. Unmapped codes are seen the day they appear.
Costs, stated honestly.
- Someone must own the files. New programs arrive every term, and a proposed row assigns nothing until it is confirmed.
- The first UNM
programs.csvneeds a review pass, mostly the 102 drafted rows. - Until Stage 4 retires them,
program_map.qsand its lists still feed the runtime lookup vectors beside the files, though no stored unit depends on them after Stage 3.
Open questions
What a unit is.Decided 2026-10-03: the source organisation by default (Theatre & Dance is one unit, with each major broken out inside its report), with explicit splits only where a subject area is reported on separately.- Branch campuses. One Banner department covers 38 programs; each needs a unit decision, or a rule such as “branch programs report to their campus”.
- Bucket programs. Whether each interdisciplinary program (Museum Studies, Latin American Studies, International Studies) is its own unit or belongs to a home department. The MPP was mapped to PADM until 2026-10-12, when it moved to ASCW with its PUBP courses.
- Concentration rule. Inferred from the data, not confirmed against Banner’s own concentration-to-major link, which the export flattens away.
- CSV or YAML. CSV is proposed for diffability and spreadsheet editing.
- The 81 proposed programs. Mostly branch-campus certificates and minor-only codes. Many carry a sensible proposal (Art → ARTS, Liberal Arts → LAIS); each needs someone who knows the program, because some do not survive review (Military Studies was proposed for NVSC and belongs to ROTC). From Stage 3 they appear under no unit, on Admin > Mappings, until confirmed.
- Hand decisions Banner disagrees with. Kept at Stage 2, but Banner names a different owner for ABA and ASD (Special Education, CEDAR says PSYC), CTS (Biomedical Sciences, CEDAR says CHEM), ELNG (LLSS, CEDAR says LING) and CHBI (Chemistry, CEDAR says BIOL). Worth confirming with IR.
- Accelerated online (X-prefix) programs. X marks an accelerated online program. CEDAR maps each to its base program’s unit and has never defined whether they should be reported apart.
Out of scope
- Input adapters. Mapping files solve “my programs and departments are different”, not “my exports have different columns”. A defined input format with a small adapter per source is separate work.
- Faculty data (stops at Spring 2025; audit finding 3) and the HR merge.
- ADR-001’s move to domain-shaped tables. This ADR defines the unit dimension that move will need.