diff --git a/README.md b/README.md index cff38ff..becfc30 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,28 @@ A dean enters their IPEDS UNITID and CIP codes (or auto-pulls from IPEDS Complet ## Status -Pre-implementation. The design spec is final (Pass 5, peer-respect & integrity). Build sequence is in flight under the Gas Town Mayor. +**v1 (cp-0on epic) shipped 2026-04-30.** Personas A/B/C live with 197/197 tests, CSULB UNITID 110583 reproducing dean memo v10 §1 exactly. + +**v1.1 (cp-j0gw extension) in PR review as of 2026-05-05.** Adds the senate-staff + public-commenter audience surfaces and folds in post-2026-04-30 corpus updates. See `docs/cp-j0gw-ext-changelog.md` for the full sub-bead ledger and 5-pass Ralph review record. Three open PRs (stacked): + +| PR | Title | Tests | +|---|---|---| +| [#1](https://github.com/AJBcoding/csu-deans-ep-tool/pull/1) | cp-j0gw: senate-staff + public-commenter audience extension; Mechanism Library + depth toggle + Sources | 203/203 | +| [#2](https://github.com/AJBcoding/csu-deans-ep-tool/pull/2) | cp-j0gw.11: interactive M01 cohort-floor demo widget | — | +| [#3](https://github.com/AJBcoding/csu-deans-ep-tool/pull/3) | cp-j0gw.8: systemwide 3-cut context — "this CIP fails at N CSU campuses" | 212/212 | + +## Routes + +| Route | Purpose | Audience | +|---|---|---| +| `/` | Landing — persona picker + ground rules | All | +| `/persona-a.html` | Check specific (CIP4, credlev) pairs | Dean / IR analyst | +| `/persona-b.html` | Auto-pull all programs at a UNITID | Dean / IR analyst | +| `/learn/` | Mechanism Library — every M-doc as standalone instructive content; reading-depth toggle (Brief / Standard / Detail) | Senate staff, legislators, public, students | +| `/sources.html` | Public Sources — every primary source the tool reads or cites, with verification path | All | +| `/comment-draft.html` | Generate DRAFT public-comment outline (markdown, copy/download) | Public commenters | +| `/widgets/cohort-floor-demo.html` | Interactive M01 cohort-floor demo (slider) | All | +| `/api/v1/...` | Public API surface (Persona C) | IR / programmatic consumers | ## Spec diff --git a/content/systemwide-cip-context.json b/content/systemwide-cip-context.json new file mode 100644 index 0000000..8939ead --- /dev/null +++ b/content/systemwide-cip-context.json @@ -0,0 +1,851 @@ +{ + "schema_version": "1.0.0", + "build_date": "2026-05-05", + "ppd_release": "2026", + "scope_label": "CSU systemwide (22 of 23 campuses, 196 exposure cells, 81 unique CIP-4 codes)", + "source_csv_path": "CIPcodes:analyses/csu-systemwide-exposure/output/3cut-2026-05-04/", + "reproducer_script": "CIPcodes:analyses/csu-systemwide-exposure/scripts/03_build_systemwide.py", + "failure_type_legend": { + "a_direct_fail": "AHEAD published per-cell fail flag = 1 (direct regulatory verdict)", + "c_invisible": "IPEDS-recorded completers but no AHEAD row (B16 invisibility)", + "d_knife_edge": "Within \u00b120% of lowest-of-three benchmark \u2014 sensitivity-flip candidate", + "e_pool_suppressed": "Below 30-completer floor at 4-digit; pool has failing siblings" + }, + "totals": { + "A_directly_failing_AHEAD": { + "n_cells": 21, + "n_campuses": 10, + "n_unique_cip4": 12 + }, + "C_invisible_no_AHEAD_row": { + "n_cells": 86, + "n_campuses": 20, + "n_unique_cip4": 52 + }, + "D_knife_edge_passing_within_20pct": { + "n_cells": 66, + "n_campuses": 17, + "n_unique_cip4": 31 + }, + "E_suppressed_pool_has_failing_siblings": { + "n_cells": 23, + "n_campuses": 9, + "n_unique_cip4": 15 + } + }, + "n_campuses_in_scope": 22, + "n_unique_cip4": 81, + "by_cip": { + "100": { + "cip4": "100", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "101": { + "cip4": "101", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "106": { + "cip4": "106", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "109": { + "cip4": "109", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "" + }, + "1101": { + "cip4": "1101", + "a_direct_fail": 0, + "c_invisible": 3, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "" + }, + "1109": { + "cip4": "1109", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1301": { + "cip4": "1301", + "a_direct_fail": 0, + "c_invisible": 5, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 5, + "n_unique_campuses": 5, + "cip4_title": "" + }, + "1311": { + "cip4": "1311", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1312": { + "cip4": "1312", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Teacher Education and Professional Development Specific Levels and Methods" + }, + "1313": { + "cip4": "1313", + "a_direct_fail": 0, + "c_invisible": 5, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 5, + "n_unique_campuses": 4, + "cip4_title": "" + }, + "1399": { + "cip4": "1399", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Education Other" + }, + "1401": { + "cip4": "1401", + "a_direct_fail": 0, + "c_invisible": 3, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "" + }, + "1409": { + "cip4": "1409", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1413": { + "cip4": "1413", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1436": { + "cip4": "1436", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1510": { + "cip4": "1510", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "1601": { + "cip4": "1601", + "a_direct_fail": 2, + "c_invisible": 1, + "d_knife_edge": 2, + "e_pool_suppressed": 0, + "total_cells": 5, + "n_unique_campuses": 5, + "cip4_title": "Linguistic Comparative and Related Language Studies and Services" + }, + "1603": { + "cip4": "1603", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 2, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "East Asian Languages Literatures and Linguistics" + }, + "1609": { + "cip4": "1609", + "a_direct_fail": 1, + "c_invisible": 0, + "d_knife_edge": 2, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Romance Languages Literatures and Linguistics" + }, + "1699": { + "cip4": "1699", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Foreign Languages Literatures and Linguistics Other" + }, + "1901": { + "cip4": "1901", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Family and Consumer Sciences/Human Sciences General" + }, + "1907": { + "cip4": "1907", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Human Development Family Studies and Related Services" + }, + "199": { + "cip4": "199", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "2301": { + "cip4": "2301", + "a_direct_fail": 2, + "c_invisible": 2, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 4, + "n_unique_campuses": 4, + "cip4_title": "English Language and Literature General" + }, + "2313": { + "cip4": "2313", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 3, + "e_pool_suppressed": 1, + "total_cells": 4, + "n_unique_campuses": 3, + "cip4_title": "Rhetoric and Composition/Writing Studies" + }, + "2401": { + "cip4": "2401", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Liberal Arts and Sciences General Studies and Humanities" + }, + "2603": { + "cip4": "2603", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Botany/Plant Biology" + }, + "2607": { + "cip4": "2607", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Zoology/Animal Biology" + }, + "2701": { + "cip4": "2701", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Mathematics" + }, + "2705": { + "cip4": "2705", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "3017": { + "cip4": "3017", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "3018": { + "cip4": "3018", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "" + }, + "3020": { + "cip4": "3020", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "305": { + "cip4": "305", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "3099": { + "cip4": "3099", + "a_direct_fail": 0, + "c_invisible": 7, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 7, + "n_unique_campuses": 6, + "cip4_title": "" + }, + "3101": { + "cip4": "3101", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Parks Recreation and Leisure Studies" + }, + "3105": { + "cip4": "3105", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Sports Kinesiology and Physical Education/Fitness" + }, + "3801": { + "cip4": "3801", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 2, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Philosophy" + }, + "399": { + "cip4": "399", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "4201": { + "cip4": "4201", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "4301": { + "cip4": "4301", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "4302": { + "cip4": "4302", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "4400": { + "cip4": "4400", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "" + }, + "4501": { + "cip4": "4501", + "a_direct_fail": 0, + "c_invisible": 4, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 4, + "n_unique_campuses": 4, + "cip4_title": "" + }, + "4502": { + "cip4": "4502", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 5, + "e_pool_suppressed": 1, + "total_cells": 8, + "n_unique_campuses": 8, + "cip4_title": "Anthropology" + }, + "4504": { + "cip4": "4504", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Criminology" + }, + "4506": { + "cip4": "4506", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Economics" + }, + "4507": { + "cip4": "4507", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "" + }, + "4509": { + "cip4": "4509", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "4510": { + "cip4": "4510", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 1, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Political Science and Government" + }, + "4511": { + "cip4": "4511", + "a_direct_fail": 1, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Sociology" + }, + "5003": { + "cip4": "5003", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 5, + "total_cells": 5, + "n_unique_campuses": 5, + "cip4_title": "Dance" + }, + "5004": { + "cip4": "5004", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 2, + "e_pool_suppressed": 1, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Design and Applied Arts" + }, + "5005": { + "cip4": "5005", + "a_direct_fail": 3, + "c_invisible": 2, + "d_knife_edge": 6, + "e_pool_suppressed": 1, + "total_cells": 12, + "n_unique_campuses": 9, + "cip4_title": "Drama/Theatre Arts and Stagecraft" + }, + "5006": { + "cip4": "5006", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Film/Video and Photographic Arts" + }, + "5007": { + "cip4": "5007", + "a_direct_fail": 5, + "c_invisible": 0, + "d_knife_edge": 6, + "e_pool_suppressed": 0, + "total_cells": 11, + "n_unique_campuses": 11, + "cip4_title": "Fine and Studio Arts" + }, + "5009": { + "cip4": "5009", + "a_direct_fail": 2, + "c_invisible": 0, + "d_knife_edge": 8, + "e_pool_suppressed": 3, + "total_cells": 13, + "n_unique_campuses": 12, + "cip4_title": "Music" + }, + "501": { + "cip4": "501", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Area Studies" + }, + "5010": { + "cip4": "5010", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 2, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Arts Entertainment and Media Management" + }, + "502": { + "cip4": "502", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 2, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Ethnic Cultural Minority Gender and Group Studies" + }, + "5099": { + "cip4": "5099", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Visual and Performing Arts Other" + }, + "5100": { + "cip4": "5100", + "a_direct_fail": 0, + "c_invisible": 4, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 4, + "n_unique_campuses": 3, + "cip4_title": "" + }, + "5107": { + "cip4": "5107", + "a_direct_fail": 1, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Health and Medical Administrative Services" + }, + "5108": { + "cip4": "5108", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5109": { + "cip4": "5109", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5115": { + "cip4": "5115", + "a_direct_fail": 0, + "c_invisible": 2, + "d_knife_edge": 3, + "e_pool_suppressed": 0, + "total_cells": 5, + "n_unique_campuses": 5, + "cip4_title": "Mental and Social Health Services and Allied Professions" + }, + "5122": { + "cip4": "5122", + "a_direct_fail": 1, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Public Health" + }, + "5123": { + "cip4": "5123", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Rehabilitation and Therapeutic Professions" + }, + "5138": { + "cip4": "5138", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Registered Nursing Nursing Administration Nursing Research and Clinical Nursing" + }, + "5202": { + "cip4": "5202", + "a_direct_fail": 1, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 2, + "n_unique_campuses": 2, + "cip4_title": "Business Administration Management and Operations" + }, + "5203": { + "cip4": "5203", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5209": { + "cip4": "5209", + "a_direct_fail": 1, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Hospitality Administration/Management" + }, + "5210": { + "cip4": "5210", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5213": { + "cip4": "5213", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Management Sciences and Quantitative Methods" + }, + "5216": { + "cip4": "5216", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5220": { + "cip4": "5220", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 0, + "e_pool_suppressed": 1, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Construction Management" + }, + "5299": { + "cip4": "5299", + "a_direct_fail": 0, + "c_invisible": 1, + "d_knife_edge": 0, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "" + }, + "5401": { + "cip4": "5401", + "a_direct_fail": 1, + "c_invisible": 0, + "d_knife_edge": 2, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "History" + }, + "901": { + "cip4": "901", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 3, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Communication and Media Studies" + }, + "904": { + "cip4": "904", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 1, + "e_pool_suppressed": 0, + "total_cells": 1, + "n_unique_campuses": 1, + "cip4_title": "Journalism" + }, + "907": { + "cip4": "907", + "a_direct_fail": 0, + "c_invisible": 0, + "d_knife_edge": 3, + "e_pool_suppressed": 0, + "total_cells": 3, + "n_unique_campuses": 3, + "cip4_title": "Radio Television and Digital Communication" + } + }, + "disclaimer": "CSU systemwide context derived from forward-simulated PPD:2026 negotiator analytic file. Not a prediction of which programs will fail in 2027 \u2014 a current snapshot of how the proposed rule maps onto the system as of the data freeze. Verify against the source CSVs (path above)." +} diff --git a/data/build/build_systemwide_context.py b/data/build/build_systemwide_context.py new file mode 100644 index 0000000..33c6e88 --- /dev/null +++ b/data/build/build_systemwide_context.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +build_systemwide_context.py — convert CSU systemwide-exposure 3-cut CSVs into +content/systemwide-cip-context.json (cp-j0gw.8). + +The output JSON gives the runtime engine a per-CIP context: how many CSU +campuses surface this CIP4 with each of the four failure types (A direct-fail, +C invisible, D knife-edge, E pool-suppressed). The verdict-card UI uses this +to surface "this CIP fails at N of K CSU campuses systemwide" alongside the +per-program verdict. + +Source CSVs live in the CIPcodes companion repo: + analyses/csu-systemwide-exposure/output/3cut-2026-05-04/ + cut1_by_campus.csv + cut2_by_cip.csv + cut3_by_failure_type.csv + unified_exposure.csv + +Reproducer for the source CSVs: + analyses/csu-systemwide-exposure/scripts/03_build_systemwide.py + +Usage: + python3 data/build/build_systemwide_context.py [--source ] [--out ] + +Defaults: + --source ~/gt/CIPcodes/crew/anthonybyrnes/analyses/csu-systemwide-exposure/output/3cut-2026-05-04 + --out content/systemwide-cip-context.json + +Determinism: output is sorted by CIP4. No randomness; no time-dependent fields +beyond the build_date constant. +""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +DEFAULT_SOURCE = Path.home() / "gt/CIPcodes/crew/anthonybyrnes/analyses/csu-systemwide-exposure/output/3cut-2026-05-04" +DEFAULT_OUT = Path(__file__).resolve().parent.parent.parent / "content/systemwide-cip-context.json" +BUILD_DATE = "2026-05-05" +PPD_RELEASE = "2026" + + +def load_by_cip(path: Path) -> dict[str, dict]: + out: dict[str, dict] = {} + with path.open() as f: + for row in csv.DictReader(f): + cip4 = row["cip4"] + out[cip4] = { + "cip4": cip4, + "a_direct_fail": int(row["A_directly_failing_AHEAD"]), + "c_invisible": int(row["C_invisible_no_AHEAD_row"]), + "d_knife_edge": int(row["D_knife_edge_passing_within_20pct"]), + "e_pool_suppressed": int(row["E_suppressed_pool_has_failing_siblings"]), + "total_cells": int(row["total_cells"]), + "n_unique_campuses": int(row["n_unique_campuses"]), + } + return out + + +def load_totals(path: Path) -> dict[str, dict]: + out: dict[str, dict] = {} + with path.open() as f: + for row in csv.DictReader(f): + out[row["failure_type"]] = { + "n_cells": int(row["n_cells"]), + "n_campuses": int(row["n_campuses"]), + "n_unique_cip4": int(row["n_unique_cip4"]), + } + return out + + +def load_campuses(path: Path) -> list[dict]: + out: list[dict] = [] + with path.open() as f: + for row in csv.DictReader(f): + out.append({ + "opeid6": row["opeid6"], + "instnm": row["instnm"].strip(), + "total_exposure_cells": int(row["total_exposure_cells"]), + }) + return out + + +def load_titles(path: Path) -> dict[str, str]: + """Read cip4_title from unified_exposure.csv, one per cip4. Source CSVs + arrive with whitespace-padded titles (``"... General."`` becomes + ``"... General"`` after rstrip-dot, leaving stray double spaces). Collapse + runs of whitespace so the rendered UI line reads cleanly.""" + import re + titles: dict[str, str] = {} + with path.open() as f: + for row in csv.DictReader(f): + cip4 = row["cip4"] + raw = row["cip4_title"].strip().rstrip(".") + title = re.sub(r"\s+", " ", raw).strip() + if cip4 not in titles and title: + titles[cip4] = title + return titles + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE, + help="Source directory (3cut CSVs).") + parser.add_argument("--out", type=Path, default=DEFAULT_OUT, + help="Output JSON path.") + args = parser.parse_args() + + src: Path = args.source + if not src.is_dir(): + raise SystemExit(f"Source directory not found: {src}") + + by_cip = load_by_cip(src / "cut2_by_cip.csv") + totals = load_totals(src / "cut3_by_failure_type.csv") + campuses = load_campuses(src / "cut1_by_campus.csv") + titles = load_titles(src / "unified_exposure.csv") + + # Empty string when no title was harvested. The UI omits the parenthetical + # entirely on empty/missing — see web/js/render.js:renderSystemwideContext. + for cip4 in by_cip: + by_cip[cip4]["cip4_title"] = titles.get(cip4, "") + + by_cip_sorted = dict(sorted(by_cip.items())) + + out = { + "schema_version": "1.0.0", + "build_date": BUILD_DATE, + "ppd_release": PPD_RELEASE, + "scope_label": f"CSU systemwide ({len(campuses)} of 23 campuses, " + f"{sum(t['n_cells'] for t in totals.values())} exposure cells, " + f"{len(by_cip)} unique CIP-4 codes)", + "source_csv_path": "CIPcodes:analyses/csu-systemwide-exposure/output/3cut-2026-05-04/", + "reproducer_script": "CIPcodes:analyses/csu-systemwide-exposure/scripts/03_build_systemwide.py", + "failure_type_legend": { + "a_direct_fail": + "AHEAD published per-cell fail flag = 1 (direct regulatory verdict)", + "c_invisible": + "IPEDS-recorded completers but no AHEAD row (B16 invisibility)", + "d_knife_edge": + "Within ±20% of lowest-of-three benchmark — sensitivity-flip candidate", + "e_pool_suppressed": + "Below 30-completer floor at 4-digit; pool has failing siblings", + }, + "totals": totals, + "n_campuses_in_scope": len(campuses), + "n_unique_cip4": len(by_cip), + "by_cip": by_cip_sorted, + "disclaimer": ( + "CSU systemwide context derived from forward-simulated PPD:2026 " + "negotiator analytic file. Not a prediction of which programs will " + "fail in 2027 — a current snapshot of how the proposed rule maps " + "onto the system as of the data freeze. Verify against the source " + "CSVs (path above)." + ), + } + + args.out.parent.mkdir(parents=True, exist_ok=True) + with args.out.open("w") as f: + json.dump(out, f, indent=2) + f.write("\n") + print(f"wrote {args.out}") + print(f" cip count: {len(by_cip)}") + print(f" campuses: {len(campuses)}") + + +if __name__ == "__main__": + main() diff --git a/tests/systemwide-context.test.ts b/tests/systemwide-context.test.ts new file mode 100644 index 0000000..ac1ed03 --- /dev/null +++ b/tests/systemwide-context.test.ts @@ -0,0 +1,105 @@ +// systemwide-context.test.ts — verify the cp-j0gw.8 systemwide-cip-context.json +// bundle is well-formed and that the renderer surfaces the per-CIP context line +// when the data is preloaded. + +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +interface SystemwideContext { + schema_version: string; + build_date: string; + ppd_release: string; + scope_label: string; + source_csv_path: string; + reproducer_script: string; + failure_type_legend: Record; + totals: Record; + n_campuses_in_scope: number; + n_unique_cip4: number; + by_cip: Record; + disclaimer: string; +} + +const ctx: SystemwideContext = JSON.parse( + readFileSync( + resolve(__dirname, '..', 'content', 'systemwide-cip-context.json'), + 'utf8', + ), +); + +describe('systemwide-cip-context.json (cp-j0gw.8)', () => { + it('reports 22 CSU campuses in scope', () => { + expect(ctx.n_campuses_in_scope).toBe(22); + }); + + it('reports 81 unique CIP-4 codes', () => { + expect(ctx.n_unique_cip4).toBe(81); + expect(Object.keys(ctx.by_cip).length).toBe(81); + }); + + it('totals match the senate-packet headline (196 cells, type breakdown)', () => { + expect(ctx.totals.A_directly_failing_AHEAD?.n_cells).toBe(21); + expect(ctx.totals.C_invisible_no_AHEAD_row?.n_cells).toBe(86); + expect(ctx.totals.D_knife_edge_passing_within_20pct?.n_cells).toBe(66); + expect(ctx.totals.E_suppressed_pool_has_failing_siblings?.n_cells).toBe(23); + const total = + ctx.totals.A_directly_failing_AHEAD!.n_cells + + ctx.totals.C_invisible_no_AHEAD_row!.n_cells + + ctx.totals.D_knife_edge_passing_within_20pct!.n_cells + + ctx.totals.E_suppressed_pool_has_failing_siblings!.n_cells; + expect(total).toBe(196); + }); + + it('CIP 50.09 (Music) shows multi-campus exposure (12 of 22 campuses)', () => { + const music = ctx.by_cip['5009']; + expect(music).toBeDefined(); + expect(music?.cip4_title).toMatch(/Music/i); + expect(music?.n_unique_campuses).toBe(12); + expect(music?.total_cells).toBe(13); + }); + + it('CIP 50.05 (Drama) shows multi-campus exposure (9 of 22 campuses)', () => { + const drama = ctx.by_cip['5005']; + expect(drama).toBeDefined(); + expect(drama?.cip4_title).toMatch(/Drama|Theatre/i); + expect(drama?.n_unique_campuses).toBe(9); + }); + + it('CIP 23.01 (English MA — REV-2 finding) is in the systemwide map', () => { + const english = ctx.by_cip['2301']; + expect(english).toBeDefined(); + expect(english?.cip4_title).toMatch(/English/i); + expect(english?.n_unique_campuses).toBeGreaterThan(0); + }); + + it('disclaimer marks the data as forward simulation, not observation', () => { + expect(ctx.disclaimer).toMatch(/forward[- ]simulated/i); + expect(ctx.disclaimer).toMatch(/Verify against the source/i); + }); + + it('includes reproducer-script pointer (CIPcodes-side path)', () => { + expect(ctx.reproducer_script).toMatch(/03_build_systemwide\.py/); + expect(ctx.source_csv_path).toMatch(/3cut-2026-05-04/); + }); + + it('every by_cip entry has the four failure-type counts that sum to total_cells', () => { + for (const [cip4, entry] of Object.entries(ctx.by_cip)) { + const sum = + entry.a_direct_fail + + entry.c_invisible + + entry.d_knife_edge + + entry.e_pool_suppressed; + expect(sum, `cip4=${cip4}`).toBe(entry.total_cells); + } + }); +}); diff --git a/web/css/main.css b/web/css/main.css index 2bf5fa9..4976115 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -616,6 +616,32 @@ button.ghost-btn { color: var(--color-text-muted); } +/* CSU systemwide context (cp-j0gw.8) */ + +.systemwide-context { + margin-top: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: rgba(31, 58, 138, 0.06); + border-left: 3px solid #1f3a8a; + border-radius: 4px; + font-size: 0.92rem; + line-height: 1.4; +} + +.systemwide-context .systemwide-source { + display: block; + margin-top: 0.25rem; + font-size: 0.82rem; + color: var(--color-text-muted); +} + +.systemwide-context code { + font-size: 0.82rem; + background: rgba(0, 0, 0, 0.06); + padding: 0.05rem 0.3rem; + border-radius: 3px; +} + /* ─── Panels ─────────────────────────────────────────────────────────── */ .panels-region { diff --git a/web/js/persona-a.js b/web/js/persona-a.js index b5abeff..18ce8e0 100644 --- a/web/js/persona-a.js +++ b/web/js/persona-a.js @@ -3,7 +3,8 @@ import { postAnalysis, ApiError } from './api.js'; import { loadCitations, hydrateCitations } from './citations.js'; import { loadPanel, autoTriggeredIds, learnMoreIds } from './panels.js'; -import { renderResult } from './render.js'; +import { renderResult, preloadSystemwideContext } from './render.js'; +preloadSystemwideContext(); import { mountPdfButton, defaultPrintFooter } from './pdf.js'; const CREDLEVS = [ diff --git a/web/js/persona-b.js b/web/js/persona-b.js index 89f4359..b95ed95 100644 --- a/web/js/persona-b.js +++ b/web/js/persona-b.js @@ -3,7 +3,11 @@ import { getAnalysisByUnitid, ApiError } from './api.js'; import { loadCitations, hydrateCitations } from './citations.js'; import { loadPanel, autoTriggeredIds, learnMoreIds } from './panels.js'; -import { renderResult } from './render.js'; +import { renderResult, preloadSystemwideContext } from './render.js'; + +// Kick off the systemwide-context fetch immediately so it's ready by the time +// the user submits a UNITID (cp-j0gw.8). Failures fall back to no-context. +preloadSystemwideContext(); import { mountPdfButton, defaultPrintFooter } from './pdf.js'; const form = document.getElementById('run-form'); diff --git a/web/js/render.js b/web/js/render.js index 75472b5..79e8cdc 100644 --- a/web/js/render.js +++ b/web/js/render.js @@ -141,6 +141,8 @@ export function renderVerdictCard(program) { ? renderVerificationRecipe(program.verification_recipe) : ''; + const systemwide = renderSystemwideContext(program.cip4, program.credlev); + return `
@@ -153,6 +155,7 @@ export function renderVerdictCard(program) { ${notMeasuredNote} ${xv} ${derivation} + ${systemwide} ${renderPanelTriggerList(program.panels_triggered)}
Why this verdict — rules fired @@ -163,6 +166,69 @@ export function renderVerdictCard(program) { `; } +/** + * Lazy-loaded CSU systemwide context (cp-j0gw.8). The JSON is fetched once on + * first call and cached. If the fetch fails (offline build / standalone test + * harness), the function returns an empty string and the verdict card renders + * without the context line — never blocks the per-program verdict. + */ +let SYSTEMWIDE_CONTEXT = null; +let SYSTEMWIDE_CONTEXT_LOADING = null; +async function loadSystemwideContext() { + if (SYSTEMWIDE_CONTEXT !== null) return SYSTEMWIDE_CONTEXT; + if (SYSTEMWIDE_CONTEXT_LOADING) return SYSTEMWIDE_CONTEXT_LOADING; + SYSTEMWIDE_CONTEXT_LOADING = fetch('./content/systemwide-cip-context.json') + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + SYSTEMWIDE_CONTEXT = data ?? { by_cip: {}, n_campuses_in_scope: 0 }; + return SYSTEMWIDE_CONTEXT; + }) + .catch(() => { + SYSTEMWIDE_CONTEXT = { by_cip: {}, n_campuses_in_scope: 0 }; + return SYSTEMWIDE_CONTEXT; + }); + return SYSTEMWIDE_CONTEXT_LOADING; +} + +/** Trigger preload from page init code (e.g. persona-b.js). */ +export function preloadSystemwideContext() { + return loadSystemwideContext(); +} + +/** + * Synchronous render — uses preloaded data if available, otherwise renders + * empty (and the page can re-render once the fetch resolves). + */ +function renderSystemwideContext(cip4, _credlev) { + if (SYSTEMWIDE_CONTEXT === null) return ''; + const ctx = SYSTEMWIDE_CONTEXT.by_cip?.[cip4]; + if (!ctx) return ''; + const k = SYSTEMWIDE_CONTEXT.n_campuses_in_scope ?? 22; + const total = ctx.total_cells ?? 0; + if (total === 0) return ''; + // Suppress when only one CSU campus surfaces this CIP — there is no real + // "systemwide context" to convey (just this one institution). + const nCampuses = ctx.n_unique_campuses ?? 0; + if (nCampuses < 2) return ''; + const direct = ctx.a_direct_fail ?? 0; + const invisible = ctx.c_invisible ?? 0; + const knife = ctx.d_knife_edge ?? 0; + const pool = ctx.e_pool_suppressed ?? 0; + const parts = []; + if (direct > 0) parts.push(`${direct} direct-fail`); + if (invisible > 0) parts.push(`${invisible} invisible`); + if (knife > 0) parts.push(`${knife} knife-edge`); + if (pool > 0) parts.push(`${pool} pool-suppressed`); + const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : ''; + // Title may be empty when the build pipeline could not harvest a CIP-title + // for this code; omit the parenthetical entirely rather than render "()". + const title = (ctx.cip4_title ?? '').trim(); + const titleFragment = title.length > 0 ? ` (${escapeHtml(title)})` : ''; + return `

CSU systemwide context: CIP ${escapeHtml( + cip4, + )}${titleFragment} shows ${total} exposure cell${total === 1 ? '' : 's'} across ${nCampuses} of ${k} CSU campuses${breakdown}. Source: public data sources · derived from CSU systemwide-exposure 3-cut analysis (2026-05-04).

`; +} + export function renderVerificationRecipe(recipe) { if (!recipe) return ''; const fields = (recipe.source_fields ?? [])