## What & why
Keys the enrollment report's offering-kind axis on the SIS program_type enum instead of the free-text program name.
- int_school_year_offering.sql: WHERE po.program_name = 'School Year' → WHERE po.program_type = 'MAIN'. Header rewritten to describe selection by the source enum and to note SIS expresses MAIN under two naming conventions (the shared School Year program and per-campus <Campus> Main Program); both now reach the axis. program_type is carried in the CTE and published as a documented constant column (mirroring int_campus.delivery_mode); its domain is guarded by the source-side test rather than a tautological model-level accepted_values.
- int_program_offering.sql: carries program_type through from stg_sis_program (already selected there, previously unused downstream). Header + _int_enrollment__models.yml updated to document it.
- tests/assert_sis_program_type_domain.sql: new source-side value-domain test mirroring assert_sis_campus_delivery_mode_domain.sql. Reads source('sis','programs'), non-deleted rows, fails when program_type IS NULL or NOT IN (MAIN, SUMMER_CAMP) — a future EXTENDED_DAY/AFTER_SCHOOL surfaces for review instead of being silently dropped.
- seeds/sis_campus_program_map.csv: eight Alpha School campuses added (alphabetical by campus_name) so assert_sis_campus_program_map_covers_axis stays green.
- Stale name = 'School Year' scope references in stg_sis_program.sql and _sis__sources.yml updated to the current program_type = 'MAIN' shape (rule 22).
Why: program_type is SIS's own enum for the MAIN-vs-SUMMER_CAMP distinction. The name predicate only expressed "the year-long main program" as a side effect of the old one-shared-School Year-program convention; when SIS began provisioning per-campus <Campus> Main Program records, the label changed and eight active, PHYSICAL Alpha School campuses silently fell off the axis. The #1268 staging campus filter (not the name) is what keeps the 11 out-of-scope Main Program campuses out — the name predicate's only live effect was dropping these eight.
Closes #1279.
## Verification — real Redshift build (green)
Full dbt build --select path:models path:seeds --vars '{pr_number: 1279}' against sandbox_education: PASS=284, ERROR=0, SKIP=0, one pre-existing unrelated WARN (assert_admissions_pipeline_tenant_coverage, admissions-pipeline domain, present on main).
Acceptance tests passing: assert_sis_program_type_domain, assert_sis_campus_program_map_covers_axis, assert_sis_campus_program_map_hubspot_resolves, unique_int_school_year_offering_id, dbt_utils_unique_combination_of_columns_mart_enrollment_dtl_campus_id__cohort_id__session_school_year (has_fact grid uniqueness), assert_sis_enrollment_has_fact_consistency, assert_sis_enrollment_qualified_is_fact (qualified → has_fact completeness).
- The eight campuses now produce 10 campus×year axis rows (Anywhere Center - Founders & Greenwich - Armonk: 2026+2027; the other six: 2026). None were on the axis before.
- Axis grain unchanged: campus×year has no group > 1 before or after; unique_int_school_year_offering_id passes.
- SELECT DISTINCT program_type FROM int_school_year_offering → only MAIN; Summer Camp contributes zero axis rows (Port Chester's Summer Camp 2026 offering is correctly excluded).
- The four zero-enrollment campuses (Bethesda, Miami Beach (Biarritz), San Juan, Virtual) each form 13 factless grid rows (0 facts).
## Before/after cohort counts — SY2024–SY2027 (post-#1258 spine)
Measured on the same source snapshot: main's code and this branch both built fresh (pr9000_ baseline vs pr1279_), so the delta reflects only this change, not the hourly-refresh drift in the production tables. Counts are on the #1258-filtered spine (test records dropped), so they differ from the issue's raw-spine figures by design.
Distinct fact enrollments:
| School year | Before | After | Δ |
|---|---|---|---|
| SY2024 | 284 | 284 | 0 |
| SY2025 | 757 | 757 | 0 |
| SY2026 | 1621 | 1667 | +46 |
| SY2027 | 22 | 23 | +1 |
| Total | 2752 | 2799 | +47 |
Fact rows (cohort fan-out): SY2026 3735→3818, SY2027 25→26. Total mart rows incl. factless grid: SY2026 4255→4429, SY2027 542→569.
Delta attribution — confined to the eight new campuses + one explained re-enrollment. The +46 SY2026/SY2027 distinct fact enrollments are the four enrolled new campuses: Alpha Greenwich - Armonk 24, Alpha Anywhere Center - Founders 17, Alpha Austin - Founders 3, Alpha Lexington 2. (Bethesda, Biarritz, San Juan, Virtual add 0.)
The +1 at the existing campus Alpha Greenwich - Port Chester (SY2027, 10→11) is a correct second-order effect, not a predicate re-resolution — Port Chester's axis offering IDs are byte-identical before/after. One student transferred from the newly-admitted Alpha Greenwich - Armonk (2026) and holds an ON_HOLD 2027 enrollment at Port Chester. With Armonk now visible on the axis, that prior-year enrollment is seen, so the 2027 enrollment's is_returning flips FALSE→TRUE and it joins the re-enrollment-other cohort. The report is now correctly recognizing a returning student whose prior enrollment was previously invisible. No source drift (staging stg_sis_enrollment identical at 18,169 rows across both builds).
## Founders SIS-vs-HubSpot on-campus parity (#1216 restated)
Alpha Anywhere Center - Founders — the reported symptom (14 on-campus students in HubSpot, zero rows in SIS before this change) — now resolves. SIS cohort split (SY2026): on-campus 16, first-day 16, re-enrolled 1, re-enrollment-other 1 (17 distinct enrollments; the 16 PENDING_REVIEW land on-campus, plus one re-enrollment). SIS on-campus 16 vs HubSpot's reported 14 — this change closes the parity gap (SIS was showing 0) rather than opening a new one; the small SIS/HubSpot residual is the pending-review timing the issue describes. Full #1216 cross-system parity reconciliation (HubSpot-side models) is out of scope for this dbt change.
## Alpha Virtual — explicit decision
Alpha Virtual is admitted despite its name because SIS records it as delivery_mode = 'PHYSICAL', so the #1268 mode filter does not hold it back and the type-keyed axis includes it. It has zero enrollments today (factless grid rows only), so it costs nothing numerically. Correcting the SIS delivery_mode at source is Out of Scope — worth raising with the SIS team, but the report is not special-casing one campus while the source disagrees (same resolution as the Alpha World School case in #1268: SIS is the spine).
## Seed mappings — two reviewed decisions + build-driven corrections
- Alpha Miami Beach (Biarritz) → has_hubspot_program = false. The one HubSpot Alpha Miami Beach program is already mapped to the distinct SIS campus Alpha Miami Beach (a34e72b4); pointing Biarritz at it too would double-count it in any SIS↔HubSpot parity comparison. (Whether Biarritz is a genuinely separate site vs a rename is an admissions data question — Out of Scope.)
Two corrections vs the issue's verbatim "Seed rows required" block, both required to pass existing seed tests and both following the seed's own documented conventions (caught by the mandatory local build):
1. program_name populated for Biarritz and Virtual with their identity name (= program_code: Alpha Miami Beach Biarritz, Alpha Virtual) instead of an empty field. The seed's not_null test on program_name (unchanged by this PR) rejects an empty CSV cell, which dbt loads as NULL. This matches the established SIS-only-campus convention — campuses HubSpot has no program for publish their own campus name as an identity program (e.g. Alpha Carrollton, Alpha Anywhere Center Port Chester). The identity name is distinct from Alpha Miami Beach, so the Biarritz no-double-count intent is preserved.
2. Alpha Austin - Founders and Alpha San Juan set has_hubspot_program = false (not true). Their HubSpot programs exist in EduCRM (26 and 39 rows) but carry zero real enrollment facts yet (has_fact = false throughout) — assert_sis_campus_program_map_hubspot_resolves requires a true mapping to resolve to a real fact. This is exactly the seed's documented "valid campuses whose HubSpot program carries no enrollment facts yet (SIS leads HubSpot for newly onboarded campuses)" case. The four campuses that do resolve to real facts (Anywhere Center - Founders, Bethesda, Greenwich - Armonk, Lexington) stay true.
The seed yml description counts are updated to the current shape (rule 22): has_hubspot_program TRUE 48→52, FALSE 10→14 (6 SIS-only + 8 no-facts-yet).
## Rule notes
Rule 8 preserved — the offering-kind filter stays in exactly one place (int_school_year_offering); only the column it reads changed. No new rule-5 exception taken. All descriptions/comments state the current shape (rule 22); no name = 'School Year' scope reference remains.