U of I × MindRouter App Gallery

Cohort Flow

Administration Built with Pi v0.84.4 Critic status: shipped

Cohort Flow

Retention and graduation pathways dashboard

Follow an entering cohort semester by semester — who continues, changes major, stops out or graduates — and spot the moments where students are lost.

Cohort Flow running on a desktop screen, retention and graduation pathways dashboard Desktop · click to enlarge
Cohort Flow running on a phone, retention and graduation pathways dashboard
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Cohort Flow follows students who entered in a given term through every later semester and shows who continues, changes college, stops out or graduates. It ships with a clearly labeled fictional sample of 1,999 students, and you can import your own de-identified cohort CSV, which never leaves the browser. Published IPEDS retention and graduation rates for the University of Idaho sit alongside for context.

Try this

  1. Hover the top rows of the Leak finder and watch the matching stopped-out flow light up in the Sankey above.
  2. Set 'Compare by' on the retention curve to College, then hover a college name in the legend to see its line on its own.
  3. Click the Engineering card under 'Small multiples by college' to filter every chart to that college, then click it again to clear the filter.

Highlights ✎ written by the AI critic

  • Hand-drawn SVG Sankey of term-to-term flows with a draw-in animation, hover tooltips and PNG/CSV export
  • One filter bar (cohort, college, residency, first-gen) plus clickable college cards cross-filter the KPIs, Sankey, retention curve, leak finder and small multiples
  • Retention curve aligned by terms since entry, with 95% confidence bands and comparison by cohort, college, residency or first-gen; hover a legend entry to pull one group out of the bundle
  • Leak finder ranks the transitions, or college-by-transition pairs, that lose the most students, and hovering a row lights up that flow in the Sankey
  • In-browser CSV import with automatic column and status mapping, and a warning plus one-click drop for name- or email-looking columns

Who it's for

Institutional research, student success teams, college deans and chairs

Features

  • CSV import with column mapping and a built-in de-identification check (warns about name/email-looking columns)
  • Animated Sankey diagram of term-to-term flows (continuing, changed college, stopped out, graduated), drawn in SVG by hand
  • Retention curve by cohort and subgroup (college, residency, first-gen flag if present) with confidence bands
  • 'Leak finder' ranking of transitions with the largest losses
  • Small multiples by college and a filter bar that cross-filters every chart
  • Export charts as PNG and aggregated tables as CSV; nothing leaves the browser

Data & sources

Full data provenance ✎ written by the AI reviewer

Raw data sources: cohort-flow

Retrieval date for every file: 2026-09-13 (server Date headers between 18:01 and 18:06 GMT). All files are unmodified HTTP response bodies, saved byte-for-byte. Only the filenames were changed to stable names. Total size: about 253 KB.

The app's synthetic sample cohort (fictional, ~2,000 students) is not in this folder. It is generated deterministically by the app's own scripts and must be labeled synthetic in the UI. Only the files below are real, published University of Idaho figures.

Institution identity (confirmed)

  • University of Idaho, 875 Perimeter Drive MS 2282, Moscow, Idaho 83844-2282

  • IPEDS Unit ID 142285, OPE ID 00162600. Confirmed from three places: the College Navigator page header (IPEDS ID: 142285), the IPEDS profile page (window.unitId = 142285, title "Institution Data Profile - University of Idaho"), and the IPEDS xlsx exports (cell A3 142285 - University of Idaho).

Headline figures, exactly as printed on College Navigator (retrieved 2026-09-13)

Source: college_navigator_142285.html, section "Retention and Graduation Rates" (anchor #retgrad).

Measure (page wording)ValueCohort / years as printed
First-to-Second Year Retention Rates, "Retention Rates for First-Time Students Pursuing Bachelor's Degrees": Full-time students76%"Percentage of Students Who Began Their Studies in Fall 2023 and Returned in Fall 2024"
Same chart: Part-time students18%Fall 2023 to Fall 2024 (same caption)
"Overall graduation rate" (150% of normal time, full-time first-time degree/certificate-seeking)57%"Overall Graduation and Transfer-Out Rates for Students Who Began Their Studies in Fall 2018"
Same chart: Transfer-out rate21%Fall 2018
"Bachelor's Degree Graduation Rates": 4-year41%"[Began in Fall 2016]"
Bachelor's Degree Graduation Rates: 6-year61%Began in Fall 2016
Bachelor's Degree Graduation Rates: 8-year63%Began in Fall 2016
Bachelor's Degree Graduation Rates: 4-year43%"[Began in Fall 2018]"
Bachelor's Degree Graduation Rates: 6-year57%Began in Fall 2018 (no 8-year value is shown for Fall 2018)

The bachelor's chart caption reads: "Percentage of Full-time, First-time Students Who Graduated in the Specified Amount of Time and Began in Fall 2016 or Fall 2018".

Note on wording: College Navigator calls 57% (Fall 2018, 150% of normal time) the "Overall graduation rate". The 4/6/8-year figures appear under a separate heading, "Bachelor's Degree Graduation Rates" (bachelor's-seeking students). The page prints no 4/6/8-year breakdown of the "overall" rate. If the app shows a "6-year graduation rate", the citable figure is 57% for the Fall 2018 cohort. Both the "overall" rate and the "bachelor's 6-year" rate give 57% for that cohort. For the Fall 2016 cohort the 6-year bachelor's rate is 61%.

  • Other figures printed in the same section: "At this institution, 52 percent of entering students were counted as "full-time, first-time" in 2024."

  • "6-Year Graduation Rate by Gender for Students Pursuing Bachelor's Degrees" (began Fall 2018): Male 54%, Female 61%.

  • "6-Year Graduation Rate by Race/Ethnicity for Students Pursuing Bachelor's Degrees" (began Fall 2018): American Indian or Alaska Native 14%, Asian 43%, Black or African American 24%, Hispanic/Latino 52%, Native Hawaiian or other Pacific Islander 56%, White 60%, Two or more races 50%, Race/ethnicity unknown 59%, U.S. Nonresident 64%.

  • The next section, "Outcome Measures" (anchor #outcome), covers students who entered in 2016-17 and reports status 8 years after entry: Full-time, first-time (All students): certificate 0%, Bachelor's 62%, still enrolled at same institution 1%, enrolled at different institution 21%

  • Part-time, first-time: 0% / 19% / 3% / 28%

  • Full-time, non-first-time: 0% / 68% / 0% / 18%

  • Part-time, non-first-time: 2% / 42% / 2% / 42%

  • Pell and Non-Pell breakdowns are also printed (see the file).

College Navigator prints no data-release date and no provisional flag. The IPEDS exports below give both. The underlying collections are marked provisional: retention from "Spring 2024-25, Fall Enrollment (provisional data)", released 2026-01-06; graduation rates from "Winter 2024-25, Graduation Rates (provisional data)", released 2025-12-09; Outcome Measures from "Winter 2024-25, Outcome Measures (provisional data)", released 2025-12-09.

Cross-check between College Navigator and the IPEDS profile (same retrieval day)
  • Every value matches except one: 6-year graduation rate for Asian students (2018 cohort): College Navigator prints 43%, but the IPEDS profile JSON and xlsx give 44 (RetGrad_Race_by_ICLEVEL.grad_rate_GRASIAT). The cause is unknown; it may be rounding or data vintage. Cite the source you actually display.

  • The College Navigator "Bachelor's" 4-year rate for the Fall 2018 cohort (43%) does not appear in the IPEDS profile exports. Those exports show only the 2016-cohort 4/6/8-year rates and the 2018-cohort 150% rates.

  • Outcome Measures, "Percent of adjusted cohort receiving an award" (IPEDS) = certificate + Bachelor's (College Navigator). Example: part-time non-first-time 44 = 2 + 42. Because each figure is rounded separately, the sum can be off by 1. For part-time non-first-time Non-Pell, IPEDS gives 40, but College Navigator prints certificate 2% + Bachelor's 37% = 39%. The one-decimal IPEDS values are 2.5 + 37.0 = 39.5. (This arithmetic was done by the verifier and is not a published figure.)


File 1: college_navigator_142285.html

  • URL: https://nces.ed.gov/collegenavigator/?id=142285 (HTTP GET, no redirect)

  • Request headers sent: User-Agent: UIdaho-gallery-data-prep/1.0, Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, Accept-Language: en-US,en;q=0.9

  • Publisher: U.S. Department of Education, National Center for Education Statistics (NCES), College Navigator

  • Retrieved: 2026-09-13 (server Date Sun, 13 Sep 2026 18:01:04 GMT), HTTP 200, text/html; charset=utf-8

  • Size: 169,346 bytes; sha256 9061eea6d6f0ca3a34704a7a92b56898f9a15ac2c884bc9dbd6fd460150c8ab2

  • Contents: The full College Navigator institution page for the University of Idaho. It has 14 collapsible sections: General Information, Tuition/Fees, Financial Aid, Net Price, Enrollment, Admissions, Retention and Graduation Rates, Outcome Measures, Programs/Majors, Servicemembers and Veterans, Varsity Athletic Teams, Accreditation, Campus Security, and Cohort Default Rates. The cohort-flow app only needs #retgrad and possibly #outcome.

  • Record count / date range: One institution. Retention covers the Fall 2023 to Fall 2024 cohort. Graduation rates cover the Fall 2016 and Fall 2018 cohorts. Outcome Measures cover the 2016-17 entering cohort. Other sections refer to Fall 2024 and 2024-2025.

  • Validation done: Parsed with Python html.parser (3,099 text nodes). It has a closing </html>. The <title> and header text name University of Idaho with IPEDS ID 142285. It is not an error, consent or throttle page. Note: a first request without the Accept header returned HTTP 200 with a 531-byte "NCES.ED.GOV Currently Experiencing High Load" placeholder. That response was discarded, not saved. A real page is 169 KB and contains IPEDS ID: 142285. Verifier note (2026-09-13 18:26 GMT): the placeholder is intermittent and not caused by a missing header. A verification re-fetch sent with the Accept/Accept-Language headers above first got the same 531-byte HTTP 200 placeholder. The immediate retry returned the real page, byte-identical to the saved file (same sha256). Any re-fetch script must check the size or the IPEDS ID: marker, not just the HTTP status.

  • Format / parsing notes:

    • UTF-8 ASP.NET page. Sections are <div class="collapsing" id="<anchor>"> followed by <div id="divctl00_cphCollegeNavBody_ucInstitutionMain_ctlNN" class="detailOff">. detailOff means collapsed via CSS, but the content is present in the HTML.

    • Retention & graduation = anchor retgrad, container divctl00_cphCollegeNavBody_ucInstitutionMain_ctl05. Outcome Measures = anchor outcome, container ..._ctl06.

    • Each chart is an <img src="Graph.aspx?...">. Values can be read two ways:

      1. From the alt attribute, which holds newline-separated Label: NN% lines. Series headers appear as [Began in Fall 2016].

      2. From the URL query: labels= (semicolon-separated, URL-encoded %3b), data= (first series), dataaux= / dataauxaux= (second/third series), slabels= (series names). Some data values carry an encoded percent sign (54%25%3b61%25). hide0=1 hides zero bars. For example, in the bachelor's chart the 2018 series has only two values (no 8-year).

    • The caption (cohort years) is in <strong> in the table row after the image. The chart title is in the preceding <thead><th>.

    • All values are whole percents. There is no missing-value code in this section. A missing bar is simply absent from data.

  • License / terms: Content produced by NCES, a U.S. federal agency. As a U.S. Government work it is not subject to U.S. copyright (17 U.S.C. §105), and NCES asks that it be credited. No separate terms page was fetched during this retrieval.

  • Suggested citation: U.S. Department of Education, National Center for Education Statistics, College Navigator: University of Idaho (IPEDS ID 142285), https://nces.ed.gov/collegenavigator/?id=142285, retrieved September 13, 2026.


Files 2-3: IPEDS Institution Data Profile JSON (Retention and Graduation; Outcome Measures)

How these were found: the legacy URL https://nces.ed.gov/ipeds/datacenter/InstitutionProfile.aspx?unitId=142285 redirects (5 hops, setting ASP.NET/F5 cookies) to https://nces.ed.gov/ipeds/institution-profile/142285. That page is a JavaScript shell with no data in its HTML, so it was not saved. Its bundle (/ipeds/clientapps/institutionprofile/assets/index.js) loads each subject from /ipeds/institution-profile/api/subject/{subjectId}?unitId={unitId} (subject 10 = Retention and Graduation, 11 = Outcome Measures). The API requests below were sent with the cookie jar from that redirect chain.

  • Cookies are required. The verifier tested this on 2026-09-13 at 18:24 GMT: Without a cookie jar, GET https://nces.ed.gov/ipeds/institution-profile/api/subject/10?unitId=142285 returned HTTP 404 with the 14-byte JSON body {"step1":true}, four times in a row.

  • With a jar filled by a plain curl -L -c jar -b jar GET of the legacy URL (5 redirects, final HTTP 200 at /ipeds/institution-profile/142285), the same API and xlsx URLs returned HTTP 200 and full bodies.

The jar holds DATACENTER_* ASP.NET session cookies and an F5 f5_cspm cookie. To reproduce, first GET the legacy URL with a cookie jar, then GET the API and xlsx URLs with the same jar. A 404 {"step1":true} means "no session", not "no data".

File 2: ipeds_profile_142285_subject10_retention_graduation.json
  • URL: https://nces.ed.gov/ipeds/institution-profile/api/subject/10?unitId=142285 (HTTP GET, Accept: application/json, User-Agent: UIdaho-gallery-data-prep/1.0)

  • Publisher: U.S. Department of Education, NCES, Integrated Postsecondary Education Data System (IPEDS)

  • Retrieved: 2026-09-13 (server Date Sun, 13 Sep 2026 18:05:51 GMT), HTTP 200, application/json; charset=utf-8

  • Size: 18,153 bytes; sha256 376b527cca096aac7c994de2001a28afa8111b5a664aaeee5c87002bae87e63d

  • Contents: 4 section groups for unit 142285:

    1. First-to-second-year retention, students who started Fall 2023: Full-time 76, Part-time 18 (ef_d.ret_pcf, ef_d.ret_pcp).

    2. Bachelor's degree graduation rates, 2016 cohort: 8-year 63 (gr200.BAGR200), 6-year 61 (gr200.BAGR150), 4-year 41 (gr200.BAGR100).

    3. Graduation rate within 150% of normal time, 2018 cohort, all degree/certificate-seeking full-time first-time students: Total 57, Men 54, Women 61, 9 race/ethnicity rates (AIAN 14, Asian 44, Black 24, Hispanic 52, NHPI 56, White 60, Two+ 50, Unknown 59, U.S. Nonresident 64), Transfer-out rate 21 (RetGrad_Race_by_ICLEVEL.*).

    4. 150% graduation rate by Title IV aid status, 2018 cohort: Pell recipients 49.3, Stafford Loan recipients not receiving Pell 56.7, Neither 64.8 (gr_pell_ssl).

  • Record count: 4 groups, 21 data fields in body[].series[].fields[] (2 + 3 + 13 + 3; header cohort-label fields not counted). Checked on retrieval: no field has a null, empty or - valueRaw.

  • Format / parsing notes:

    • Top level is a JSON array of groups: {id, header, footer, body[]} (footer is null here).

    • header.label is the full table title, including the cohort. header.series[0].fields[0].valueRaw is the cohort label alone ("Fall 2023", "2016 cohort", "2018 cohort").

    • Each body[] element is a section with sectionType ("table", or "subheader" in file 3), categories[] (categoryId, label, groupLabel) and series[] (label, fields[]).

    • Join fields[].categoryId to categories[].categoryId to get the column label. series[].label is the row label.

    • fields[].valueRaw is the value as a string in percent units, with no % sign. It is usually an integer; group 4 has one decimal.

    • fields[].numberOrZero is a numeric copy that turns missing values into 0. Prefer valueRaw so a genuine 0 is not confused with missing.

    • tableKey + columnName identify the IPEDS source table/variable. For group 4 (gr_pell_ssl), columnName is a SQL expression (e.g. case when PGADJCT = 0 ... PGCMTOT * 100.0 / ... end), not a plain variable name.

    • settings[] is a list of {sectionId, property, value, createdOn} objects, and value is always a string. Properties seen: isPercent = "true", toFixed = "1" (on the one-decimal tables), views = "bar,table" or "table", and chart.* layout hints.

    • In group 3 (the 150% rates), label-only rows are series with an empty fields[]: "Overall graduation rates" (serieType "static"), "Gender" and "Race/ethnicity" (serieType "header"). The rows under them have serieType "indent-1". Skip series with no fields. IPEDS labels differ slightly from College Navigator's: "Men"/"Women" (CN: Male/Female), "Hispanic or Latino" (CN: Hispanic/Latino), "Native Hawaiian or Other Pacific Islander" (CN: "other").

    • The top-level group id (a GUID) is regenerated on every request. In a verification re-fetch these 4 GUIDs were the only difference from the saved file. Do not use id as a key. sectionId, serieId, fieldId and categoryId stayed the same.

    • The sourceNotes[] and footnotes[] arrays are empty. Release dates and the provisional flag appear only in the xlsx export (file 4).

  • License / terms: U.S. Government work (NCES/IPEDS), not subject to U.S. copyright (17 U.S.C. §105). Credit NCES/IPEDS.

  • Suggested citation: U.S. Department of Education, National Center for Education Statistics, IPEDS Institution Data Profile: University of Idaho (142285), Retention and Graduation (Winter 2024-25 Graduation Rates and Spring 2024-25 Fall Enrollment, provisional), https://nces.ed.gov/ipeds/institution-profile/142285, retrieved September 13, 2026.

File 3: ipeds_profile_142285_subject11_outcome_measures.json
  • URL: https://nces.ed.gov/ipeds/institution-profile/api/subject/11?unitId=142285 (HTTP GET, same headers as file 2)

  • Publisher: U.S. Department of Education, NCES, IPEDS

  • Retrieved: 2026-09-13 (server Date Sun, 13 Sep 2026 18:05:54 GMT), HTTP 200, application/json; charset=utf-8

  • Size: 57,307 bytes; sha256 52e509ce0ff701f1823b8974d02d9fed0e61f93d3c4309d555e15c3f651dcf2f

  • Contents: 6 section groups, all for students who entered in 2016-17:

    1. Award/enrollment status 8 years after entry, by cohort type. Columns: Full-time first-time, Part-time first-time, Full-time non-first-time, Part-time non-first-time. Rows: % receiving an award (om.OMAWDP8), % still enrolled at your institution (om.OMENRYP), % enrolled subsequently at another institution (om.OMENRAP), % enrollment status unknown (om.OMENRUP). For full-time first-time these are 62 / 1 / 21 / 17.

    2. The same measures split by Pell status, as two subheader sections ("Students receiving Pell Grants", "Students not receiving Pell Grants") each followed by a table.

    3. to 6. For each of the four cohort types (FT first-time, PT first-time, FT non-first-time, PT non-first-time): % receiving an award at 4, 6 and 8 years after entry. Rows are Bachelors / Associates / Certificates. Columns are 9 categories, where categories[].label is "4 years after entry" / "6 Years after entry" / "8 Years after entry" and categories[].groupLabel is "Total" / "Pell" / "Non-Pell". The labels repeat, so you must use groupLabel to tell the columns apart. Values have one decimal (FT first-time Bachelors Total: 4y 47.0, 6y 60.7, 8y 61.8).

  • Record count: 6 groups, 156 data fields (16 + 32 + 4 × 27). Checked on retrieval: no field has a null, empty or - valueRaw.

  • Format / parsing notes: Same schema as file 2. Note that the header cohort label uses an en dash (U+2013, "2016–17") in some groups and an ASCII hyphen ("2016-17") in others. series[] order in the JSON is not always display order; use seq. In groups 3-6, the fields[] of a series are not in column order. They run 4y-Total, 4y-Pell, 4y-Non-Pell, 6y-Total, and so on, while categories[] run Total 4/6/8, Pell 4/6/8, Non-Pell 4/6/8. Always join fields[].categoryId to categories[], then order by categories[].seq if needed. Never zip them by position. The year labels are capitalized inconsistently ("4 years after entry" vs "6 Years after entry"), so match them case-insensitively. As in file 2, the group id GUIDs change on every request: all 6 differed on the verification re-fetch, and nothing else did.

  • License / terms: as file 2.

  • Suggested citation: U.S. Department of Education, National Center for Education Statistics, IPEDS Institution Data Profile: University of Idaho (142285), Outcome Measures (Winter 2024-25, provisional), https://nces.ed.gov/ipeds/institution-profile/142285, retrieved September 13, 2026.


Files 4-5: IPEDS Institution Data Profile xlsx exports (same data as files 2-3, plus release dates and source lines)

These are the files produced by the profile page's "Download" button for each subject. They are included because only these carry the NCES source line, the release date and the provisional flag.

File 4: ipeds_profile_142285_subject10_retention_graduation.xlsx
  • URL: https://nces.ed.gov/ipeds/institution-profile/download/subject?subjectId=10&unitid=142285 (HTTP GET, same cookie jar)

  • Publisher: U.S. Department of Education, NCES, IPEDS

  • Retrieved: 2026-09-13 (server Date Sun, 13 Sep 2026 18:05:22 GMT), HTTP 200. The server sent Content-Type: application/octet-steam (sic) and Content-Disposition: attachment; filename=retention-and-graduation-2026-09-13-14-05.xlsx.

  • Size: 3,875 bytes; sha256 41a12b5f471dd825b6b33473c6e27ab7e2d5d8955bf6159c8b939cc36c49516c

  • Contents: One sheet, "Retention and Graduation", holding the same 4 tables as file 2 in a human layout. Key cells:

    • A3 142285 - University of Idaho

    • Retention table: A6 title (started Fall 2023); B8:C8 Full-time/Part-time; B9:C9 76/18

    • A11 Date Release Date: 2026-01-06; A12 SOURCE: U.S. Department of Education, National Center for Education Statistics, IPEDS, Spring 2024-25, Fall Enrollment (provisional data)

    • Bachelor's table: A14 title (2016 cohort); B16:D16 8-year/6-year/4-year; B17:D17 63/61/41

    • 150% table: A19 title (2018 cohort); rows 22-37 (label in A, rate in B). A value of - in B marks a label-only group row (e.g. "Gender"), not missing data.

    • Pell/Stafford table: rows 39-42 (49.3, 56.7, 64.8)

    • A44 Date Release Date: 2025-12-09; A45 SOURCE: ... IPEDS, Winter 2024-25, Graduation Rates (provisional data)

    • A47 File created on 09/13/26 14:05, the local download time on the server, which changes with every download. A verification re-download at 14:27 differed from the saved file in this cell only. Every data cell was identical.

  • Format / parsing notes: Standard Office Open XML. Readable with Python's stdlib zipfile (xl/sharedStrings.xml + xl/worksheets/sheet1.xml) or openpyxl/SheetJS. Treat all cell values as text and coerce to numbers. Whole-number rates (e.g. B9 76) are numeric cells. One-decimal rates (e.g. 49.3; 47.0 in file 5) and the - placeholders are shared strings (t="s"), so a reader that keeps only numeric cells will miss them.

  • Validation done: Unzipped and parsed with Python stdlib. The sheet name, unit id and every value were checked against file 2 and all match.

  • License / terms: as file 2.

  • Suggested citation: same as file 2.

File 5: ipeds_profile_142285_subject11_outcome_measures.xlsx
  • URL: https://nces.ed.gov/ipeds/institution-profile/download/subject?subjectId=11&unitid=142285 (HTTP GET, same cookie jar)

  • Publisher: U.S. Department of Education, NCES, IPEDS

  • Retrieved: 2026-09-13 (server Date Sun, 13 Sep 2026 18:05:40 GMT), HTTP 200, server filename outcome-measures-2026-09-13-14-05.xlsx

  • Size: 4,512 bytes; sha256 cde17a0d540b6b57eb7b039e9b94c66571a8e91886686f8aacddcadab3d08144

  • Contents: One sheet, "Outcome Measures", with the same 6 groups as file 3:

    • rows 6-12: 8-year status by cohort type

    • rows 14-26: status by Pell status (sub-headings in A15 and A21)

    • rows 28-54: % receiving an award at 4/6/8 years for each cohort type. Header row columns B-J are Total, 4 years after entry … Non-Pell, 8 Years after entry.

    • A56 Date Release Date: 2025-12-09; A57 SOURCE: ... IPEDS, Winter 2024-25, Outcome Measures (provisional data); A59 File created on 09/13/26 14:05 (changes with every download)

  • Format / parsing notes / validation: As file 4. All values match file 3.

  • License / terms: as file 2.

  • Suggested citation: same as file 3.


Not obtained / not saved

  • The IPEDS profile HTML page (https://nces.ed.gov/ipeds/institution-profile/142285) was not saved. It is a JavaScript shell whose data comes from the API saved as files 2-3.

  • The College Navigator chart PNGs (Graph.aspx?...) were not downloaded. Their values are in the HTML alt text and query strings.

  • No IPEDS bulk data files (e.g. EF2024D, GR2024, GR200_24) were downloaded. The per-institution profile exports hold the same published values for unit 142285 and are far smaller.

  • No college/major-level or term-by-term University of Idaho data is published in these sources. Such data is not publicly available here, and the app's Sankey, retention curve and leak-finder must run on user-supplied or synthetic data.


Verification log (independent verifier, 2026-09-13)

  • Parsing and counts. All 5 data files parse. The JSON files have 4 groups / 21 fields and 6 groups / 156 fields, every valueRaw is a string, and none is null, empty or -. Both xlsx files open with stdlib zipfile, and their cell maps match the notes above. The HTML has 14 div.collapsing sections: #retgrad = ..._ctl05 with 5 Graph.aspx charts, and #outcome = ..._ctl06 with 4 charts. alt text is newline-separated, and data= / dataaux= / dataauxaux= / slabels= are as described.

  • sha256. All five sums listed above match the files on disk.

  • Live re-fetch (18:24-18:27 GMT).

    • College Navigator: byte-identical to the saved file (after one intermittent 531-byte placeholder).

    • API subject 10 and subject 11: all 21 and 156 valueRaw values identical; only group id GUIDs differed.

    • xlsx subject 10: every cell identical except the File created timestamp.

    • Values spot-checked live: retention 76/18; bachelor's 2016 cohort 41/61/63 and 2018 cohort 43/57; 150% total 57; Asian 44 (IPEDS) vs 43 (CN); transfer-out 21; OM full-time first-time award 62; release dates 2026-01-06 and 2025-12-09.

  • Key and email scan. A recursive grep for the MindRouter API-key prefix found nothing, and no file contains an email address.

How it was built

Written by Pi 0.84.4 (pi.dev) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

31harness rounds
313.2minutes of harness work (5.2 h)
2,599lines of code
1,667KB, single-file app

Rounds by kind

build1
continue13
feedback13
critic4

Lines of code per file

body html239
app css217
app js1,831
scripts312

Critic scores ✎ scored by the independent AI critic, 1–10

visual 8 out of 10
usefulness 8 out of 10
correctness 7 out of 10
interactivity 8 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases no empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • Benchmarks card, first-year retention Fall 2023 -> Fall 2024: full-time 76%, part-time 18%. Raw ipeds_profile_142285_subject10 JSON (table ef_d): ret_pcf valueRaw 76, ret_pcp valueRaw 18. MATCH
  • Bachelor's graduation, Fall 2016 cohort, 4/6/8-year: 41/61/63%. Raw subject10: BAGR100 41, BAGR150 61, BAGR200 63. MATCH
  • 150% graduation, Fall 2018 cohort: overall 57%, Men 54%, Women 61%, transfer-out 21%. Raw subject10: grad_rate_total 57, grad_rate_men 54, grad_rate_women 61, transfer_rate_total 21. MATCH
  • 150% graduation by aid, Fall 2018: Pell 49.3%, Stafford only 56.7%, neither 64.8%. Raw subject10 series valueRaw 49.3 / 56.7 / 64.8. MATCH
  • Sources text, 2016-17 entrants 8 years after entry: award 62%, still enrolled 1%, enrolled elsewhere 21%. Raw subject11: OMAWDP8 62, OMENRYP 1, OMENRAP 21. MATCH
Show all 12 spot checks
  • Computed KPIs, default view: 1,999 students, 1st-year retention 83.1%, graduated 723 (36.2%), stopped out 712 (35.6%), college changes 553. Independent Python (critic2_expected.py) on data/embed/sample_cohort.json: identical. MATCH
  • Computed KPIs under filters. Cohort Fall 2022: 287 / 82.2% / 31 / 99 / 69. Fall 2021 + Engineering: 310 / 87.7% / 132 / 92 / 107. Idaho + first-gen Yes: 451 / 81.8% / 151 / 176 / 113. Education card click: 161 / 80.1% / 60 / 64 / 34. Python gives identical values for all four. MATCH
  • Retention curve CSV (Overall): all 9 points and Wilson 95% bounds equal Python, e.g. +2 = 83.09% (1,661 of 1,999) [81.39, 84.67] and +8 = 64.19% (1,099 of 1,712) [61.89, 66.43]. Compare-by-College tooltip at +2: Arts and Letters 75.8% (350/462) ... Nursing and Health Sciences 87.5% (252/288), all equal Python. MATCH
  • Sankey column totals 1,712 / 1,591 / 1,773 / 1,696 / 1,610 / 1,512 / 1,438 / 1,169 / 840 and exit nodes (stopped out 121, 105, 77, 86, 98, 74, 73, 52; graduated 196, 277) equal Python. MATCH
  • Leak finder, 10 rows (e.g. 'Fall 2021 · first term 121, 7.1% of 1,712', 'Fall 2021 -> Spring 2022 105, 6.6% of 1,591', 'Spring 2025 -> Fall 2025 26, 3.1% of 840') equal Python. Hovering each row highlights a Sankey link of the same size, except 'Fall 2022 · first term 16', which highlights the merged Fall 2022 exit of 77. MOSTLY CONSISTENT
  • Small multiples: Arts and Letters 57%, Business 64%, Education 60%, Engineering 71%, Law 65%, Natural Sciences 65%, Nursing and Health Sciences 70%. CSV 57.36% ... 69.79%. Python identical. Rank by college: 56 pairs with n >= 30, top 'Arts and Letters Fall 2021 -> Spring 2022 10.9%, n = 340' (37/340). MATCH
  • Imported CSVs, after round 31. Spring-entrant file (50 Fall 2021 + 40 Spring 2022 entrants): 1st-year retention 67.8%; Python same-term-one-year-later gives 61/90 = 67.8%. MATCH. Registrar-code file (202180 ... 202380): term labels are now the codes in the right order, but 1st-year retention shows 0.0% where Python gives 61/80 = 76.2%. BUG
Build notes ✎ written by the AI reviewer

SUMMARY This follow-up ran rounds 14–26: 13 rounds, 2,558 s (about 43 min) of harness time. All app code was written by Pi 0.84.4 on MindRouter default-agent. Totals across all 26 rounds: 17,584 s (293.1 min). I wrote only FEEDBACK.md (deleted in commit ab02a80) and scratch files; SPEC.md and data/raw were unchanged. The tamper guard never fired. index.html md5 went from 75b51bd1… (round 4, the critic's build) to bb6ec457… (round 26).

  • HOW THE HARNESS BEHAVED One precisely located item, or up to 3 small ones with exact line ranges and expected numbers, landed fast with exit 0: rounds 14, 16, 17, 18, 19, 20, 22, 24, 25 and 26 took 13–76 s each. Two rounds stalled with no file changes and were killed under the stuck rule at about 18.5 min, while MindRouter answered 401 in under 0.6 s: Round 15: three function rewrites in one file.

  • Round 23: four mixed polish items.

Splitting their content into single-item continue rounds (16, 24, 25, 26) worked every time.

FEEDBACK LINT (honest disclosure) Round 21 never started. The runner's newly added FEEDBACK LINT refused it because FEEDBACK.md contained inline code. My FEEDBACK files for rounds 14–20 also contained short code-like hints: a return-shape description, a map expression, a Math.max width formula, CSS property values and a colour array. From round 22 on, all feedback was prose only (behaviour, location, target values) and passed the lint.

FEEDBACK ROUND COUNT Feedback-kind rounds that ran: 14, 15, 17, 18, 19, 20, 22, 23 (8). Round 21 was refused. Rounds 16, 24, 25 and 26 were continue re-runs of stalled content.

  • WHAT CHANGED, ROUND BY ROUND (each verified in Chromium at 1440 px and 390 px against an independent Python calculation, followup_expected.py) R14, retentionSeries: aligns students by terms since entry and keeps graduates retained. The x-axis now reads Entry, +1, …; the tooltip shows 'rate · retained of total'; the curve CSV has new columns; label CSS fixed the clipped y labels; group lines are solid.

  • R16, small multiples: aligned sparklines and an 'enrolled or graduated by last term' rate. Arts and Letters is now 57%, not 41%.

  • R17, leak rule: losses counted on the transition into the stopped-out term, matching the Sankey; first-term rows added; rank-by-college requires n of at least 30. Hovering 105 now highlights a link of 105.

  • R18, leak display: first-term highlight, two-colour bar with legend, 'n = 340', CSV columns.

  • R19: no-match state hides the charts; stuck tooltips and highlights cleared; Sankey tooltip term format fixed; imported terms sort chronologically; 'Imported Imported' wording fixed; 'Drop these columns' now works.

  • R20, Sankey desktop layout: thin light bars, label band, forward exit nodes, height fitted to content.

  • R22: phone Sankey at 120 px per column with a conditional scroll hint and scroll-aware tooltip; curve benchmark reduced to a short first-year marker with the 6-year line removed; 48 px margin; palette without Leather.

  • R24: benchmark drawn above the series, label moved below the marker with a halo; Sankey subtitle sentence added.

  • R25: label left-aligned on every width; 7% bands in compare mode.

  • R26: leak finder shows 8 rows with a Show all / Show fewer toggle.

RESULT Every item in the critic's remaining_issues is resolved and re-confirmed in a real browser. All cohort math (flows, curve points and denominators, Wilson bands, college rates, leak ranking) matches independent Python. The minor leftovers listed under features_missing_or_broken are cosmetic or untested edge cases.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 45.0 min · 5 files changed, 700 insertions(+) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write scripts in scripts/ (Python 3 standard library only) that (a) extract the real published University of Idaho headline figures (first-year retention, graduation rates, with cohort years and source citations) from the raw data in data/raw/ (see data/raw/SOURCES.md) into compact JSON in data/embed/, and (b) deterministically generate the clearly labeled synthetic sample cohort described in SPEC.md into compact JSON in data/embed/ — and run them; then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    2 continue 37.8 min · 4 files changed, 3529 insertions(+), 6 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. The data step is already done: scripts/extract_headlines.py and scripts/generate_sample_cohort.py exist and data/embed/headlines.json and data/embed/sample_cohort.json are generated and good enough — do NOT spend any more time tuning or re-running the synthetic data generator (its exact rates do not matter; it is fictional). Go straight to building the app: write src/body.html, src/app.css and src/app.js, run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    4 continue 10.8 min · 4 files changed, 1414 insertions(+), 614 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — round 2 build (tested in headless Chromium at 1440 px and 390 px)

    Most important first. Everything not mentioned here (sample auto-load, filter bar cross-filtering, leak list, PNG/CSV downloads, the de-identification warning text, Data & sources section) works — keep it.

    1. CSV import cannot be reached. The sample cohort loads automatically, so the empty state is hidden, and the only "Import CSV…" button (#empty-open-import) is inside it. No visible button anywhere opens the import dialog. Expected: a prominent, always-visible "Import CSV" button (in the filter bar next to "Load sample cohort", or in the dashboard header).

    2. CSV import maps and parses real files wrongly. Test file headers: StudentID, Full Name, Email Address, Entry Term, Term, Major College, Enrollment Status, Residency, FirstGen. Term values are like Fall 2021, Spring 2022. Status values are Continuing, Changed college, Stopped out.

      • The TERM field is auto-mapped to the "Entry Term" column instead of the "Term" column. An exact header match must win.

      • After "Use this data", the app shows 120 students but only 1 term. The Sankey is empty, 1st-year retention shows 0.0% and the leak finder says "No transitions to analyze".

      • The note "371 row(s) have a missing or duplicate student key" is wrong. In long format (one row per student per term) a key naturally repeats. Only flag empty keys or duplicate (key, term) pairs.

      • "Status values to review: changed college — these will be treated as 'enrolled'" is wrong: Changed college should count as changed college. Recognize common spellings (continuing/enrolled, changed college/major, stopped out/withdrawn/not enrolled, graduated/completed). Add a small status value-mapping step where each distinct status value can be assigned to Continuing / Changed college / Stopped out / Graduated.

      • Terms written Fall 2021 or 2021-Fall (or 202180-style codes) must both sort chronologically, with Spring before Fall in the same year.

      • The Data & sources text afterwards reads "Imported Imported CSV: 120 students, 1 terms" (duplicated word, "1 terms").

      • The de-identification warning itself is good. Also offer a one-click "drop these columns" option.

    3. Console errors. Choosing Cohort = "2022-Fall" logs Error: <polygon> attribute points: Expected number, "46,NaN 111.25,NaN 1…". The error comes from the retention-curve confidence band. For a later cohort the curve should start at that cohort's entry term, with no NaN points and no console errors for any filter combination.

    4. The Sankey diagram does not look polished yet (Term-to-term flow card, 1440 px):

      • The node bars are opaque black rectangles as tall as the whole flow. They dominate the chart and hide the flows. Use slim nodes in brand/series colours.

      • Term labels are drawn on top of the node bars: "2022-Spring" has the bar running through it. The first column's labels are clipped at the left edge ("21-Fall", "pped out", "1 students").

      • The last two columns ("2025-Spring", "2025-Fall") are squeezed together, so their labels and student counts overlap. The columns should be evenly spaced.

      • "Stopped out" / "Graduated" exit nodes are tiny squares whose labels overlap the bars. Stack exit flows neatly below each column with readable labels.

      • Continuing is one flat teal slab, and the changed-college flows are invisible hairlines. Make the four flow types clearly distinguishable, split continuing vs changed-college visibly, and give a minimum visible width to small flows.

      • Hovering a flow shows a tooltip (good) but does not highlight it. Expected: the hovered flow and its nodes are emphasised and the others dimmed.

      • The draw-in animation plays on Replay (good). When filters change, the flows should animate or morph to their new sizes instead of redrawing instantly.

      • There is about 100 px of empty space under the flows inside the SVG.

    5. The retention curve has no subgroup comparison (required: "by cohort and subgroup (college, residency, first-gen flag) with confidence bands"). It only draws one "Overall" line.

      • Expected: a "Compare by" control (None / Cohort / College / Residency / First-gen) that draws one coloured line plus a 95% CI band per group, with a legend.

      • The first point should be 100% at the entry term; it currently starts at ~93%.

      • The y-axis label "100%" is clipped at the left edge, and the rotated x-axis labels are clipped ("21-Fall", "022-Spring").

      • The benchmark labels "IPEDS 1st-yr retention 76%" and "IPEDS 6-yr grad 61%" sit directly on their dashed lines, so the lines strike through the text.

      • Hovering the chart shows nothing (#curve-tip stays empty). Expected: a vertical guide line with each series' value and n at that term.

      • The CI band is almost invisible; make it a clearly visible translucent band.

    6. The metrics contradict each other.

      • The KPI tile says "1ST-YEAR RETENTION 93.1% — returned next term", which is term-to-term, not first-year retention.

      • The retention curve ends near 66% at 2025-Fall, but every small multiple says "23–32% retention at last term".

      • Define each metric once and use it consistently in the KPIs, curve and multiples. For example: first-year retention = enrolled in the Fall one year after entry; retained = still enrolled or graduated. Add a short subtitle saying what is counted.

    7. No state for zero or tiny groups. Cohort 2022-Fall + College Law + Residency International gives "0 students". The KPIs, Sankey, retention curve, leak finder, small multiples and benchmarks all disappear, leaving the filter bar followed directly by Data & sources.

      • Expected: a friendly in-place message ("No students match these filters") with a Reset button.

      • Groups under ~30 students should show a small-sample caution.

      • Use "1 student", not "1 students".

    8. The leak finder is a static list. Clicking or hovering a row does nothing.

      • Expected: hovering or clicking a row highlights that transition in the Sankey.

      • Each row should show the split of the loss (stopped out vs changed college).

      • Add a toggle to rank transition × college pairs (e.g. "Engineering · Fall 2021 → Spring 2022 · 18% stopped out") so it really finds where students are lost.

    9. Small multiples: the sparklines have no shared scale reference.

      • Add a faint 50%/100% gridline and the start/end term labels.

      • Visually mark the card of the currently selected college.

      • Clicking the selected card again should clear the college filter. (Clicking a card to filter already works.)

    10. Published benchmarks card bugs.

      • "150% graduation by aid" shows "Pell undefined%"; the value should be 49.3%.

      • All benchmark bars are empty grey tracks with no fill (e.g. Full-time 76% has an empty bar). Fill them to the right width in gold/teal.

    11. The primary button has no fill. "Load sample cohort" (.btn--gold) in the filter bar renders as plain black text with no Pride Gold background until it is hovered or focused. Primary buttons must always show the gold fill.

    12. Phone (390 × 844) problems.

      • The sticky filter bar takes ~235 px of the 844 px screen and covers chart content while scrolling. On narrow screens make it non-sticky, or collapse it into a compact "Filters" button or drawer.

      • The Sankey is 640 px wide inside a 316 px scroll box. It is cut off mid-label with no hint that it scrolls. Make it fit the phone width, or show a clear scroll cue with sticky term labels.

      • The retention curve's y-axis and x-axis labels are clipped at 390 px.

    13. Exports. The PNG buttons and the leak CSV work. The spec asks for "aggregated tables as CSV", so also offer CSV for:

      • the Sankey flow table (from term → to term, status, count)

      • the retention curve points (group, term, n, rate, CI low/high)

      • the small-multiples table (college, n, retention) Label the export buttons clearly (e.g. "PNG" and "CSV" next to each other on every chart card).

    14. Term labels. Show terms as "Fall 2021" / "Spring 2022" everywhere (currently "2021-Fall").

    15. Motion. When filters change, the KPIs, bars and curves update instantly. Add smooth 200–300 ms transitions (numbers counting, bars and lines easing) so the dashboard feels alive, and respect prefers-reduced-motion.

    6 continue 19.3 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    8 continue 0.3 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    9 continue 45.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    11 critic 16.2 min · 1 file changed, 79 insertions(+) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory. It lists 9 problems a reviewer found when testing index.html in a real browser. Fix them in order, starting with item 1. For each item: read only the function or CSS rule it names (use grep -n and read with line ranges; src/app.js is about 1,700 lines, so never read it whole), then change it with small targeted edits. Never rewrite src/app.js, src/app.css or src/body.html as a whole, and keep everything that already works. Run python3 build.py --check after every two items and fix any error it reports. Keep going until all 9 items are fixed, then run python3 build.py --check a final time.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass (tested in headless Chromium at 1440 px and 390 px)

    How to work: src/app.js is large (about 1,700 lines). Do NOT read it whole. For each item, grep -n the function named, read only that line range, and make small targeted edits. Never rewrite a whole file. Run python3 build.py --check after every two items. Line numbers below are approximate.

    Keep all of this working: filters and cross-filtering, Compare by, rank-by-college, small-multiple click-to-filter, Import CSV with column mapping, status mapping and the de-identification warning, every PNG/CSV export, the phone filter drawer, the benchmarks card, the Data & sources section, and zero console errors.

    1. The retention curve and small multiples give wrong numbers (highest priority)
    • Code: retentionSeries (~line 639), renderCurve (~694), renderMultiples (~972), exportCurveCSV (~1232), exportMultiplesCSV (~1245). Graduates drop out of the numerator. A graduate has no rows after graduating, so later terms count them as lost. With "All cohorts" the curve ends at 40.7% at Fall 2025, and the multiples show Arts and Letters 41%. The correct "still enrolled or graduated" values are 64.2% and 57%.

    • Rule: once a student has a graduated row, count them as graduated in every later term too. A student counts as retained in a term if their row there is enrolled or changed_college, or if they graduated in or before that term.

    • Cohorts are mixed by calendar term. The line rises from Spring 2022 (74%) to Fall 2022 (85%). Instead, align every student by terms since their own entry term.

      • Label the x-axis "Entry", "+1", "+2", … ("+2 = one year").

      • The denominator at step k is the students whose entry term plus k is still inside the data.

    • Expected with the sample and All cohorts: Entry 93.1%, +1 87.2%, +2 83.1%, +3 78.9%, +4 74.3%, +5 70.5%, +6 67.2%, +7 65.2% (n=1,712), +8 64.2%. The line never rises.

      • Compare by = Cohort: the Fall 2022 cohort line stops at +6 (65.5%).

    • The curve tooltip currently shows "79.6% (n=1,591)", where 1,591 is the retained count. Show "93.1% · 1,862 of 1,999".

    • The curve CSV should have columns group, step, n_retained, n_total, rate, ci_low, ci_high.

    • Small multiples: use the same aligned rule. Label each card's rate "retained or graduated at last term".

    2. The Sankey desktop layout looks unfinished
    • Code: layoutSankey (~362), renderSankey (~428). Node bars: term nodes are 14 px near-black bars as tall as the whole flow, and they dominate the chart. Make them thin (6 px) and a light neutral colour.

    • Labels: term labels are drawn on top of the bars. The first column's label is clipped ("all 2021", "2 students"), and "Spring 2025" collides with "Fall 2025".

      • Put every term label and count in a label band above the columns.

      • Centre each label on its column, but left-align the first column and right-align the last.

      • Keep enough side padding that nothing is clipped.

    • Exit links run backwards. An exit link starts at the term node's right edge and ends at x = the same column's left edge. Place each exit node ("Stopped out", "Graduated") about 40% of the way toward the next column, below the flow, so the link runs left to right.

      • Stack exit labels with at least 26 px between them. Today "Stopped out 73" and "Graduated 196" overlap.

    • Height: there is about 100 px of empty space under the flows. Compute the SVG height from the tallest column stack plus the label band and padding.

    3. The leak finder disagrees with the Sankey it highlights
    • Code: computeLeaks (~842), computeLeakPairs (~870), renderLeaks (~901), highlightSankeyTransition (~595). Hovering the row "Fall 2021 → Spring 2022 · 121" highlights the Sankey link for that transition, which shows 105 students. The leak finder counts students whose stopped-out row is in the earlier term, while the Sankey puts the loss on the transition into the stopped-out term.

    • Use the Sankey's rule. A row (term t−1 → term t) counts students who were enrolled or changed college at t−1 and either have a stopped_out row at t or have no row at t (and did not graduate).

      • Changed college at t is a separate count, not a loss.

      • Expected top rows: Fall 2021 → Spring 2022 = 105 lost of 1,591 (78 changed college); Spring 2023 → Fall 2023 = 98 of 1,610; Fall 2022 → Spring 2023 = 86 of 1,696.

    • Add a "first term" row per entry term for students whose entry-term row is already stopped_out: "Fall 2021 · first term" = 121, and "Fall 2022 · first term" = 16. Hovering it highlights that column's "Stopped out" exit link.

    • Split each bar in two colours: raspberry for stopped out, lupine for changed college. Add a small legend above the list. The "changed college" count is currently always 0.

    4. The Sankey is unreadable on a phone
    • Code: renderSankey (~431 width logic); src/app.css .sankey-wrap / #sankey (~102–114). At 390 px all 9 columns are squeezed into 320 px. The labels merge into "20222023202320…", and the "← scroll for more terms →" hint shows but the box does not scroll (scrollWidth 320, clientWidth 316).

    • Below 700 px, draw the SVG at least 110 px per column wide (about 1,000 px) inside the horizontally scrolling .sankey-wrap. Keep the full "Fall 2022" labels.

    • Show the scroll hint only when the content really overflows.

    5. Retention curve visual problems
    • Code: renderCurve (~694), nextColor (~826). The left margin is too small: "100%" is clipped on desktop, and every y label is clipped on the phone. Use a left margin of at least 44 px.

    • With the "Entry, +1, …" axis from item 1, the x labels no longer need rotating. Remove the rotation.

    • Group lines are dashed like the benchmark lines. Make all data series solid; only benchmarks should be dashed.

    • Colours repeat: Engineering and the IPEDS benchmark lines are both #AB4700. Give the 7 colleges 7 distinct colours, none equal to the benchmark colour.

    • Benchmarks: the benchmark labels sit on top of the data lines.

      • Draw the IPEDS first-year retention benchmark (76%) as a short dashed marker only around step +2, with its label placed clear of the lines.

      • Remove the 6-year graduation line from this chart. It measures a different thing, and it is already in the benchmarks card.

    6. Imported terms are not sorted chronologically
    • Code: TERM_SEM_ORDER / parseTerm (~90–109), applyImport (~1546–1548). Importing a CSV with terms "Fall 2021, Spring 2022, Fall 2022, …" gives columns Fall 2021, Fall 2022, Spring 2022, Fall 2023, Spring 2023, Spring 2024.

      • TERM_SEM_ORDER ranks fall=0 and spring=1 within a year, but Spring 2022 comes before Fall 2022.

      • Within a calendar year the order should be Winter < Spring < Summer < Fall.

    • Afterwards the leak finder should show only consecutive, real transitions.

    • The Data & sources text says "Imported Imported CSV: 120 students…". Remove the duplicated word (~1561).

    • Drop these columns should visibly work: remove the dropped columns from the preview table and replace the warning with "Dropped: Full Name, Email Address".

    7. The no-match state leaves stale charts, and highlights get stuck
    • Code: renderAll (~1572), clearSankeyHighlight (~607). Cohort Fall 2022 + Business + International + First-gen No shows the "No students match these filters" card. Under it, the old Sankey, curve, leak rows and multiples from the previous selection stay visible (for example a Sankey of "1 student" columns).

      • While nothing matches, hide every chart card and show only the no-match card.

    • On every renderAll, and whenever data is loaded or imported, clear the leak highlight (sankeyHover, and the .is-active leak row) and hide both tooltips.

    8. Rank-by-college polish
    • Code: computeLeakPairs (~870), renderLeaks (~901). Only rank college groups with at least 30 students present. Add a one-line note: "Groups under 30 students are not ranked."

    • The right-hand text "of 132 in college" wraps onto three lines. Show it on one line as "n = 132".

    9. Mixed term formats
    • Code: attachLinkTip (~552), exportSankeyCSV (~1220). The Sankey tooltip says "Fall 2022 → 2023-Spring". Pass the to term through termLabel, so it reads "Fall 2022 → Spring 2023".

    • Use "Fall 2022"-style labels in every tooltip, label and leak row. The CSV exports may keep the raw term codes.

    12 continue 0.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory. It lists 9 problems a reviewer found when testing index.html in a real browser. Fix them in order, starting with item 1. For each item: read only the function or CSS rule it names (use grep -n and read with line ranges; src/app.js is about 1,700 lines, so never read it whole), then change it with small targeted edits. Never rewrite src/app.js, src/app.css or src/body.html as a whole, and keep everything that already works. Run python3 build.py --check after every two items and fix any error it reports. Keep going until all 9 items are fixed, then run python3 build.py --check a final time.
    

    No feedback file was sent in this round.

    13 continue 0.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory. It lists 9 problems a reviewer found when testing index.html in a real browser. Fix them in order, starting with item 1. For each item: read only the function or CSS rule it names (use grep -n and read with line ranges; src/app.js is about 1,700 lines, so never read it whole), then change it with small targeted edits. Never rewrite src/app.js, src/app.css or src/body.html as a whole, and keep everything that already works. Run python3 build.py --check after every two items and fix any error it reports. Keep going until all 9 items are fixed, then run python3 build.py --check a final time.
    

    No feedback file was sent in this round.

    16 continue 0.6 min · 3 files changed, 56 insertions(+), 75 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — college small multiples show wrong rates (tested in headless Chromium)

    How to work (important): this round has ONE item in two small functions. Do NOT read src/app.js whole (it is about 1,720 lines). Read only lines 982–1027 and 1255–1276, then make small targeted edit calls whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    Your earlier round fixed the retention curve: retentionSeries(ss) now aligns students by terms since entry and keeps graduates counted as retained, and every number matched the reviewer's own calculation. The college cards still use the old rule.

    1. renderMultiples(students) (src/app.js lines 982–1027) and exportMultiplesCSV() (lines 1255–1276)

    Today Arts and Letters shows 41%, because graduates count as lost after they graduate. The correct value is 57%.

    • In renderMultiples: Sparkline: replace the calendar-term loop (lines 1000–1008) with the aligned series. Set pts = retentionSeries(ss).pts.map(function (o) { return o.ci.p; }). Delete the now-unused e0 variable (line 996).

    • Headline rate: set last = the share of ss whose status at the last term index model.terms.length - 1 is enrolled or changed_college, or who have a graduated row at any term. studentStatuses(s) gives the statuses, and graduation always ends a student's rows.

    • Footer: plural(ss.length, "student") + " · enrolled or graduated by " + termLabel(T[lastIdx]). Keep the ⚠ small sample suffix.

    • In exportMultiplesCSV: Use the same headline rule.

    • Change the header to college,n,retained_or_graduated_at_last_term.

    Expected with All cohorts:

    CollegeRate
    Arts and Letters57%
    Business64%
    Education60%
    Engineering71%
    Law65%
    Natural Sciences65%
    Nursing and Health Sciences70%

    With Cohort Fall 2021 and College Engineering, Engineering shows 70% (310 students).

    24 continue 0.4 min · 3 files changed, 28 insertions(+), 20 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — benchmark label readability and one subtitle sentence (tested in headless Chromium, 1440 px and 390 px)

    How to work (important): this round has just 2 very small items. Do NOT read src/app.js whole (it is about 1,790 lines). Read only lines 765–777 of src/app.js and line 83 of src/body.html. Then make small targeted edits, copying the old text from what you just read. Do not change anything else. Run python3 build.py --check when done.

    1. The benchmark label is drawn over the data line

    Where: renderCurve, the benchmark block in src/app.js (lines 767–775).

    • What happens: With Compare by set to None, the Overall line passes straight through the words "IPEDS 1st-yr retention 76%", both at 1440 px and at 390 px.

    • With Compare by set to College, the college lines and bands are drawn after the marker, so they cover it.

    • Expected: Move the block. Put the benchmark block after the series loop, just before the comment "hover: vertical guide + all series values at that term". The marker and label are then drawn on top of the lines, and the hover overlay, which is added later, still receives the mouse.

    • Move the label. Place it below the dashed marker instead of above: right-aligned at the marker's right end (step +3), about 14 px under the dashed line.

    • Add a halo. Give the label a thin white outline painted behind the letters, so it stays readable where a line crosses it.

    2. The Sankey does not explain why a column grows

    Where: the Sankey card subtitle in src/body.html (line 83).

    What happens: the columns read Fall 2021 with 1,712 students, then Spring 2022 with 1,591, then Fall 2022 with 1,773. A first-time visitor cannot tell why a column can be larger than the one before it.

    Expected: add this sentence after the existing one: "Students who enter in a later term join the flow at their entry term, so a column can be larger than the one before it."

    25 continue 0.2 min · 2 files changed, 4 insertions(+), 4 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback: two small retention-curve fixes (tested in headless Chromium, 1440 px and 390 px)

    How to work (important): this round has just 2 very small items, both in renderCurve in src/app.js.

    • Do NOT read src/app.js whole (it is about 1,800 lines).

    • Read only lines 775–820, then make small targeted edits whose old text you copied from what you just read.

    • Do not change anything else.

    • Run python3 build.py --check when done.

    Your last round worked. The benchmark label is now drawn on top of the lines, below the marker and with a white halo, and it no longer crosses the Overall line on desktop.

    1. On a phone the benchmark label overlaps the "75%" axis label

    Where: the benchmark block just before the comment "hover: vertical guide + all series values at that term" (about lines 804–815).

    • What happens: At 390 px the label "IPEDS 1st-yr retention 76%" is right-aligned at step +3.

    • It is wider than the space between the y axis and step +3.

    • So it runs past the left edge of the plot and is printed over the "75%" tick label.

    • Expected: Keep the label below the dashed marker (about 14 px under it), but left-align it at the marker's left end (step +1) instead of right-aligning it at step +3.

    • Do this on every width. It then stays inside the plot on phones, and on desktop it still sits clear of the Overall line.

    2. With Compare by, the confidence bands merge into one muddy area

    Where: the series loop; the band opacity is set on line 784.

    • What happens: With Compare by set to College, seven bands at 18% opacity stack on top of each other.

    • Between 60% and 95% they form one grey-brown area, so no single college can be followed.

    • Expected: When more than one group is drawn, give each band about 7% opacity.

    • When only one group is drawn (Compare by set to None), keep 18%.

    26 continue 0.3 min · 2 files changed, 28 insertions(+), 2 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — leak finder length leaves a blank area beside the curve (tested in headless Chromium at 1440 px)

    How to work (important): this round has ONE small item, in renderLeaks in src/app.js. Do NOT read src/app.js whole (it is about 1,800 lines). Read only renderLeaks (lines 952–1023). Then make small targeted edits whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    Your last round worked. The benchmark label no longer overlaps the "75%" axis label, and the Compare-by bands are light enough to follow each group.

    1. Show at most 8 leak rows, with a "Show all" button

    Where: renderLeaks. Rows are limited to 12 on line 969, and the by-college note is added on line 1022.

    • What happens: At 1440 px the retention curve and the leak finder share a row.

    • The leak finder lists 10 rows, which makes both cards 854 px tall.

    • The curve card therefore shows about 340 px of blank space below its legend.

    • Expected: By default, show at most 8 rows.

    • When there are more rows than that, add a small ghost button below the list.

      • Its label uses the real row count: "Show all 10 transitions", or "Show all N pairs" in Rank by college mode.

      • Clicking it shows every row (up to the existing limit of 12) and changes the label to "Show fewer".

      • Clicking "Show fewer" returns to 8 rows.

    • Keep the choice in a variable outside the function, so it survives the re-render that happens when filters change.

    • The "Groups under 30 students are not ranked." note stays at the very end of the list, after the button.

    27 critic 0.3 min · 2 files changed, 6 insertions(+), 6 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — two small correctness bugs (tested in headless Chromium at 1440 px)

    How to work (important): this round has TWO small items in src/app.js. Do NOT read src/app.js whole (it is about 1,800 lines). Read only lines 950–1040 (renderLeaks) and lines 1400–1430 (guessMapping). Make small targeted edits whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    1. "Show all 56 pairs" only shows 12 rows

    Where: renderLeaks, around line 971, where the number of visible rows is chosen (8 when collapsed, 12 when expanded).

    • What happens: Tick "Rank by college" in the leak finder. The button reads "Show all 56 pairs".

    • Clicking it shows only 12 rows, and the label changes to "Show fewer". The other 44 pairs can never be seen.

    • Expected: When expanded, show every row. "Show all 56 pairs" must show 56 rows, and "Show all 10 transitions" must show 10.

    • Collapsed stays at 8 rows. Everything else in the function stays the same.

    2. CSV import maps the Student key to the Residency column

    Where: guessMapping, around line 1413, in the fuzzy pattern for the student field.

    • What happens: Import a CSV whose header row is: id, entry, term, program, status, residency, first_gen.

    • The Student key dropdown is pre-filled with "residency", because the student pattern matches the letters "sid" inside the word "residency".

    • The Entry term dropdown stays on "— choose —", because a header named "entry" is not recognized.

    • Expected, for that header row: Student key = id, Entry term = entry, Term = term, College/major = program, Status = status, Residency = residency, First-gen = first_gen.

    • The student field must never guess a header that contains "residen".

    • A header that is exactly "id", "key", "studentid", "studentkey", "personid" or "sid" (after the existing normalization) counts as the student key.

    • A header that is exactly "entry", or contains "entryterm", "startterm", "firstterm" or "cohort", counts as the entry term.

    • The existing sample file with headers StudentID, Full Name, Email Address, Entry Term, Term, Major College, Enrollment Status, Residency, FirstGen must still map exactly as it does now.

    28 critic 18.9 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    29 continue 0.1 min · 3 files changed, 4 insertions(+), 4 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — blank area in the retention curve card (tested in headless Chromium at 1440 px)

    How to work (important): this round has ONE small item. Do NOT read src/app.js whole (it is about 1,800 lines). Read only lines 729–740 of src/app.js (the start of renderCurve) and lines 20–26 of src/app.css. Make small targeted edits whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    1. Blank area in the retention curve card on desktop

    Where: renderCurve in src/app.js, line 733, where the chart height is fixed at 320; and the .grid-2 rule on line 23 of src/app.css.

    • What happens: At 1440 px the retention curve card and the leak finder card share one row and are stretched to the same height (758 px).

    • The curve card shows about 250 px of empty space below its legend.

    • In the leak finder, tick "Rank by college" and click "Show all 56 pairs". The leak card becomes several thousand pixels tall, and the curve card stretches with it into a very tall empty box.

    • Expected: When the browser window is wider than 1000 px (the two-column layout), draw the curve chart about 520 px tall. At 1000 px or narrower, keep it at 320 px.

    • The two cards in the grid row keep their own natural heights and align to the top of the row, instead of stretching to match each other.

    • Result at 1440 px with the default view: less than about 60 px of blank space below the curve legend. After "Show all 56 pairs", the curve card height does not change.

    30 continue 0.4 min · 3 files changed, 48 insertions(+), 4 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — highlight one group from the retention curve legend (tested in headless Chromium at 1440 px)

    How to work (important): this round has ONE item, in renderCurve in src/app.js. Do NOT read src/app.js whole (it is about 1,800 lines). Read only lines 729–860 (renderCurve). Make small targeted edits whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    1. Compare by College is hard to read — highlight one group from its legend entry

    Where: renderCurve, the series drawing loop (lines 776–802) and the legend (lines 849–855).

    • What happens: With Compare by = College, seven lines sit inside a narrow 57–97% band and cross each other, and the three dark colours are hard to tell apart.

    • Nothing happens when you hover a legend entry.

    • Expected: Hovering, or keyboard-focusing, a group's legend entry keeps that group's band, line and dots at full strength and fades every other group's band, line and dots to about 15% opacity.

    • Moving the pointer away, or blurring the entry, restores all groups.

    • The IPEDS legend entry does not trigger this. In "None" mode (a single Overall line) nothing changes.

    • Group legend entries show a pointer cursor and can be reached with the Tab key.

    • Everything else, including the hover tooltip that lists every group's value, keeps working.

    31 critic 0.5 min · 2 files changed, 16 insertions(+), 22 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — imported term codes and first-year retention (tested in headless Chromium at 1440 px)

    How to work (important): this round has TWO small items in src/app.js. Do NOT read src/app.js whole (it is about 1,850 lines). Read only lines 88–126 (parseTerm and termLabel) and lines 218–256 (computeKPIs and the KPI list in renderKPIs). Make small targeted edits whose old text you copied from what you just read. Do not change anything else. Run python3 build.py --check when done.

    1. Six-digit term codes get invented season names

    Where: parseTerm (lines 92–109) and termLabel (lines 111–124), in the branch for six-digit codes.

    • What happens: Import a CSV whose terms are registrar-style codes: 202180, 202210, 202280, 202310, 202380.

    • The Sankey columns are labelled "Winter 2021", "Winter 2022", "Winter 2022", "Winter 2023", "Winter 2023". Two different terms share one label, and none of these are winter terms.

    • Institutions use different suffix conventions, so the season cannot be guessed from the last two digits.

    • Expected: For a six-digit numeric code, do not guess a season. Order codes by their numeric value, so 202180 comes before 202210, which comes before 202280.

    • Label the term with the code itself, for example "202180", in the Sankey, the cohort filter, the leak finder and tooltips.

    • Named terms such as "Fall 2021" and "2021-Fall" keep working exactly as they do now.

    2. First-year retention only works when term names contain the word "Fall"

    Where: computeKPIs (lines 220–240), where the first-year term is found by looking for the next term whose name contains "fall" (line 230); and the KPI subtitle on line 250.

    • What happens: With the six-digit codes above, first-year retention shows 0.0%, because no term name contains "fall".

    • For a student who entered in Spring 2022, the KPI checks Fall 2022 (the very next term), which is not one year after entry.

    • Expected: A student's first-year term is the same term one year after entry: Fall 2021 becomes Fall 2022, Spring 2022 becomes Spring 2023, and code 202180 becomes 202280 (the code plus 100).

    • A student counts as retained if they are enrolled, changed college or graduated in that term.

    • If that term does not exist in the loaded data, leave that student out of both the count and the total for this KPI.

    • Change the subtitle to "still enrolled or graduated in the same term one year after entry".

    • The bundled sample must still show 83.1%. A file with 60 students entering in 202180 and 20 in 202210, of whom 61 are enrolled one year later, must show 76.2%, not 0.0%.

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

Cohort Flow — Retention and graduation pathways dashboard

Category: Administration Who it is for: Institutional research, student success teams, college deans and chairs

What it should do

Follow an entering cohort semester by semester — who continues, changes major, stops out or graduates — and spot the moments where students are lost.

Data

Works on the user's own de-identified cohort CSV (student key, entry term, term, college/major, status). Ship a clearly labeled synthetic sample cohort (fictional, ~2,000 students across U of I-style colleges) generated deterministically. Additionally show real published headline figures only if taken from a citable public source (e.g. IPEDS / College Navigator first-year retention and 6-year graduation rates for the University of Idaho) with the source and year.

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • CSV import with column mapping and a built-in de-identification check (warns about name/email-looking columns)

  • Animated Sankey diagram of term-to-term flows (continuing, changed college, stopped out, graduated), drawn in SVG by hand

  • Retention curve by cohort and subgroup (college, residency, first-gen flag if present) with confidence bands

  • 'Leak finder' ranking of transitions with the largest losses

  • Small multiples by college and a filter bar that cross-filters every chart

  • Export charts as PNG and aggregated tables as CSV; nothing leaves the browser

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • Registrar six-digit term codes (e.g. 202180): round 31 fixed the invented 'Winter 2021' labels and the ordering, but the 1st-year retention KPI still shows 0.0%. Python gives 76.2%. computeKPIs looks for a term whose parseTerm value is the entry value + 4, but codes now parse to the raw number, where one year later is + 100. The harness log claimed this case was verified; it was not. No critic rounds were left to fix it.
  • 1st-year retention still divides by every student in view. The requested rule was to leave out students whose one-year-later term is not in the loaded data, and it was not implemented. A cohort that entered in the final year of an imported file drags the KPI down. The bundled sample is unaffected: both of its cohorts have a one-year-later term.
  • Leak finder row 'Fall 2022 · first term 16' (visible after Show all) highlights the Sankey's Fall 2022 'Stopped out' exit of 77. The Sankey merges first-term stop-outs (16) and Spring 2022 -> Fall 2022 stop-outs (61) into one exit node.
  • The Sankey draws each stopped-out or graduating student twice: first as a coloured band into that term's column, then as an exit flow out of it. The counts are right, but the picture can read as extra flow.
  • Retention curve polish: the IPEDS '1st-yr retention 76%' label is crossed by the Overall line on a 390 px phone, and by college lines in Compare by College. With 7 colleges the hover tooltip wraps names and values onto two lines. When one group is highlighted from the legend, the other groups' confidence bands rise from 7% to 15% opacity instead of fading.
  • Small multiples: sparklines on a 0-100% scale look almost flat. 7 cards leave three empty grid slots on desktop, and 'Nursing and Health Scien…' is truncated.
  • CSV auto-mapping does not recognise a header such as 'TermCode' for the Term field, so the user must pick it by hand (Use this data stays disabled until then, which is at least safe).
  • Harness behaviour in this critic pass: round 27 (2 items) finished in 16 s. Round 28 (2 items in renderCurve) stalled 1,131 s with no file changes while MindRouter answered 401, and was killed under the stuck rule. Its content was split into continue rounds 29 (6 s) and 30 (24 s), which both worked. Round 31 finished in 29 s but only partly fixed item 2.