U of I × MindRouter App Gallery

Snowpack Watch

Scientific visualization Built with ForgeCode v2.13.21 Critic status: shipped

Snowpack Watch

SNOTEL snow-water equivalent tracker

Track this winter's snowpack at Idaho SNOTEL stations against the long-term median and the record years — the numbers that decide Idaho's summer water supply.

Snowpack Watch running on a desktop screen, snotel snow-water equivalent tracker Desktop · click to enlarge
Snowpack Watch running on a phone, snotel snow-water equivalent tracker
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Snowpack Watch tracks snow-water equivalent at six Idaho SNOTEL stations, from Moscow Mountain to Galena Summit, using real USDA NRCS daily data since 2001. Compare this winter against the 1991-2020 median and every past year, see when snowpack peaked and melted out, put two stations side by side, and replay a season day by day.

Try this

  1. Click the Mores Creek Summit card: WY2026 is the lowest peak since 2001 there, far below the median curve.
  2. On the Seasonal curve, click the WY2017 chip, then hover March to compare that big year with this winter and the record range.
  3. Open the Snow-depth scrubber, pick Mores Creek Summit and WY2006, and press Play at 4x to watch 99 inches of snow build and melt.

Highlights ✎ written by the AI critic

  • Real NRCS SNOTEL daily SWE and snow depth for six Idaho stations, WY2001-2026, with sources and access date shown
  • Seasonal chart with every past year, the 1991-2020 median, the min-max envelope, record years and a tap-or-hover tooltip
  • Peak-day and melt-out-day histograms in weekly bins, plus a per-year table linked to the bars
  • Compare mode with synchronized hover across two stations for any water year
  • Snow-depth scrubber that replays a water year at three speeds with a snow stake and SWE timeline

Who it's for

Water supply forecasting, forestry and ecology students, anyone who skis

Features

  • Station cards with current SWE, % of median and a tiny sparkline
  • Seasonal curve chart (Oct 1 → Sep 30): every past water year faint, median bold, min/max envelope shaded, current year highlighted
  • 'Peak day' and 'melt-out day' estimates per year with a histogram
  • Compare mode: two stations side by side
  • Animated 'snow depth' water-year scrubber
  • Source/citation panel

Data & sources

  • wcc.sc.egov.usda.gov — USDA NRCS National Water and Climate Center, AWDB REST API v1 /stations: metadata for the 6 Idaho SNOTEL stations (989, 623, 588, 370, 490, 637).
  • wcc.sc.egov.usda.gov — USDA NRCS NWCC, AWDB REST API v1 /data, one file per station: daily WTEQ (snow water equivalent) with the NRCS 1991-2020 daily median, plus daily SNWD (snow depth), 2000-10-01 to 2026-09-12.
  • wcc.sc.egov.usda.gov — USDA NRCS NWCC, AWDB REST API v1 /reference-data: element, duration and unit definitions.
Full data provenance ✎ written by the AI reviewer

Raw data sources: Snowpack Watch

All files in this directory are byte-for-byte, unmodified HTTP response bodies from the USDA NRCS Air and Water Database (AWDB) REST API v1. Nothing was filtered, reshaped or aggregated after download. Server-side query parameters were the only way the size was limited.

  • Publisher: United States Department of Agriculture (USDA), Natural Resources Conservation Service (NRCS), National Water and Climate Center (NWCC), Snow Survey and Water Supply Forecasting Program.

  • API base: https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/ (Swagger/OpenAPI doc: https://wcc.sc.egov.usda.gov/awdbRestApi/v3/api-docs)

  • Retrieval date: 2026-09-13, between 17:52 and 17:58 UTC. All requests were HTTP GET with no request body and User-Agent UIdaho-gallery-data-prep/1.0. Every response was HTTP 200 application/json.

  • Total size: ~6.2 MB (8 data files + this file).

Stations (all six SPEC suggestions verified as correct, active Idaho SNOTEL sites)

Each station ID in SPEC.md was looked up with /stations and matched its name. All six are active (endDate = 2100-01-01 00:00, meaning still in service) and have daily SWE for every water year from 2001 onward.

TripletNameCountyRegion (for the app)Elev. (ft)LatLonStation beginWTEQ element beginSNWD element begin
989:ID:SNTLMoscow MountainLatahPalouse, near UI Moscow campus470046.80500-116.853502000-10-012000-10-012000-10-19
623:ID:SNTLMica CreekShoshoneNorth Idaho (St. Joe basin)448047.15045-116.266431989-10-011990-10-012002-08-08
588:ID:SNTLLolo PassIdahoClearwater / ID-MT border528046.63448-114.580721982-10-011983-05-231999-07-28
370:ID:SNTLBrundage ReservoirAdamsWest-central mountains (McCall)628045.04315-116.132531985-10-011986-10-012004-09-08
490:ID:SNTLGalena SummitBlaineCentral mountains (Big Wood / Salmon headwaters)879043.87497-114.713631978-10-011978-10-121996-12-11
637:ID:SNTLMores Creek SummitBoiseBoise basin, central mountains609043.93200-115.665881978-09-301978-10-011998-10-01

(The "Region" column is a descriptive label written by the data-prep step. It is not a field in the API.)


File: awdb_stations_ID_SNTL.json

  • URL (GET): https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/stations?stationTriplets=989:ID:SNTL,623:ID:SNTL,588:ID:SNTL,370:ID:SNTL,490:ID:SNTL,637:ID:SNTL&elements=WTEQ,SNWD&durations=DAILY&returnForecastPointMetadata=false&returnReservoirMetadata=false&returnStationElements=true&activeOnly=true

  • Publisher: USDA NRCS National Water and Climate Center (AWDB REST API).

  • Retrieved: 2026-09-13 (17:52:45 UTC). Size: 5,290 bytes.

  • Contents: station metadata for the 6 stations, including their daily WTEQ and SNWD station elements.

  • Record count: 6 station objects, each with 2 stationElements entries (SNWD and WTEQ, DAILY).

  • Format / parsing notes:

    • Top level is a JSON array of station objects. The array order is the order the server returned them (989, 623, 588, 370, 490, 637).

    • Fields: stationTriplet ("id:state:network"), stationId, stateCode, networkCode (SNTL = SNOTEL), name, dcoCode, countyName, huc (12-digit hydrologic unit code), elevation (feet), latitude / longitude (decimal degrees; the API does not state a datum), dataTimeZone (hours offset from GMT; -8.0 = Pacific Standard Time, no daylight saving), pedonCode (absent for some stations), shefId, operator, beginDate / endDate ("YYYY-MM-DD HH:mm"; endDate 2100-01-01 00:00 means active), associatedHucs (HUCs within 5000 m).

    • stationElements[]: elementCode (WTEQ or SNWD), ordinal, durationName (DAILY), dataPrecision (decimal places: WTEQ 1, SNWD 0), storedUnitCode / originalUnitCode (in), beginDate, endDate, derivedData.

  • License / terms: U.S. federal government data. USDA NRCS publications and data are public domain in the U.S. (17 U.S.C. §105), with no restrictions on reuse. NRCS disclaimer (verbatim from the NWCC Report Generator header): "The data you have obtained from this automated Natural Resources Conservation Service database are subject to revision regardless of indicated Quality Assurance level. Data are released on condition that neither the NRCS nor the United States Government may be held liable for any damages resulting from its use."

  • Suggested citation: USDA NRCS National Water and Climate Center, SNOTEL station metadata, Air and Water Database REST API (https://wcc.sc.egov.usda.gov/awdbRestApi/), accessed 2026-09-13.


Files: awdb_data_<id>_ID_SNTL_daily_WTEQ_SNWD.json (6 files, one per station)

  • URL template (GET). Only the station id changes between the six requests: https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/data?stationTriplets=<id>:ID:SNTL&elements=WTEQ,SNWD&duration=DAILY&beginDate=2000-10-01&endDate=2026-09-12&periodRef=END&centralTendencyType=MEDIAN&returnFlags=true&returnOriginalValues=false&returnSuspectData=false

    • awdb_data_989_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=989:ID:SNTL (retrieved 17:53:30 UTC)

    • awdb_data_623_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=623:ID:SNTL (17:54:01 UTC)

    • awdb_data_588_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=588:ID:SNTL (17:54:32 UTC)

    • awdb_data_370_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=370:ID:SNTL (17:55:37 UTC)

    • awdb_data_490_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=490:ID:SNTL (17:57:10 UTC)

    • awdb_data_637_ID_SNTL_daily_WTEQ_SNWD.json: stationTriplets=637:ID:SNTL (17:57:49 UTC)

  • Publisher: USDA NRCS National Water and Climate Center (AWDB REST API), SNOTEL network.

  • Retrieved: 2026-09-13.

  • Contents: daily snow water equivalent (WTEQ, inches) with the NRCS daily 30-year median (1991–2020 normal) and flags, plus daily snow depth (SNWD, inches) with flags. Coverage is water year (WY) 2001 (starting 2000-10-01) through 2026-09-12.

  • Water year note: a water year runs Oct 1 → Sep 30 and is named for the calendar year it ends in (WY2026 = 2025-10-01 … 2026-09-30). WY2026 is still in progress: it ends 2026-09-30, but these downloads stop at 2026-09-12, so the last 18 days of WY2026 are not in the files. (On 2026-09-12 SWE is 0.0–0.1 in at every station, so melt-out has already happened.) Complete water years are WY2001–WY2025.

Record counts and date ranges (checked by parsing every file)
File (station)BytesWTEQ rowsWTEQ first → lastWTEQ missing daysSNWD rowsSNWD first → lastSNWD missing days
989 Moscow Mountain1,033,3619,4782000-10-01 → 2026-09-1209,4572000-10-19 → 2026-09-123
623 Mica Creek998,5579,4422000-10-01 → 2026-09-12368,7092002-09-30 → 2026-09-1240
588 Lolo Pass1,035,3119,4782000-10-01 → 2026-09-1209,4322000-10-01 → 2026-09-1246
370 Brundage Reservoir980,3269,4782000-10-01 → 2026-09-1208,0402004-09-08 → 2026-09-120
490 Galena Summit1,045,2099,4782000-10-01 → 2026-09-1209,4712000-10-01 → 2026-09-127
637 Mores Creek Summit1,043,2429,4642000-10-01 → 2026-09-12149,4632000-10-01 → 2026-09-1215

(9,478 = every calendar day from 2000-10-01 to 2026-09-12 inclusive.) "Missing days" counts dates inside first → last that have no row. See "Gaps" below for the exact dates.

Value ranges across WY2001–2026: WTEQ 0.0–42.6 in (max 42.6 in, Mores Creek Summit, 2006-04-05). SNWD 0–123 in. No negative values, no nulls and no sentinel codes appear in value.

JSON structure
[                                   // array with exactly 1 element (one station per file)
  {
    "stationTriplet": "989:ID:SNTL",
    "data": [                       // 2 elements; order as returned is SNWD first, then WTEQ.
                                    // Select by stationElement.elementCode, NOT by index.
      {
        "stationElement": { "elementCode": "SNWD", "ordinal": 1, "durationName": "DAILY",
                            "dataPrecision": 0, "storedUnitCode": "in", "originalUnitCode": "in",
                            "beginDate": "2000-10-19 11:00", "endDate": "2100-01-01 00:00", "derivedData": false },
        "values": [ { "date": "2026-02-01", "value": 14, "qcFlag": "V", "qaFlag": "P" }, ... ]
      },
      {
        "stationElement": { "elementCode": "WTEQ", ..., "dataPrecision": 1, "storedUnitCode": "in", ... },
        "timingCentralTendencies": {                 // WTEQ only
          "medianPeak":    { "month": 3, "day": 14, "value": 20.2 },   // median date and SWE (in) of peak SWE
          "medianOnset":   { "month": 11, "day": 10 },                 // median snowpack onset date
          "medianMeltout": { "month": 5,  "day": 5 }                   // median melt-out date
        },
        "values": [ { "date": "2026-02-01", "value": 4.5, "qcFlag": "V", "qaFlag": "P", "median": 14.4 }, ... ]
      }
    ]
  }
]
  • values[].date: "YYYY-MM-DD" (no time part). Rows are in ascending date order with no duplicates.

  • values[].value: number, inches. WTEQ has 1 decimal place and SNWD is an integer.

  • values[].median (WTEQ rows only): daily median SWE in inches. The OpenAPI schema calls it "The 30-year median". The NWCC Report Generator labels the same series "Median (1991-2020)", and its values match the API's (allowing for the date convention below). SNWD has no median. The API returns none for snow depth, so any depth "normal" would have to be computed by the app from these rows and labelled as derived. Every WTEQ row has a non-null median, including Feb 29 in leap years.

  • timingCentralTendencies (WTEQ only) per station, exactly as returned:

StationmedianPeak (date, SWE in)medianOnsetmedianMeltout
989 Moscow MountainMar 14, 20.2Nov 10May 5
623 Mica CreekMar 29, 23.0Nov 5May 17
588 Lolo PassApr 5, 28.8Nov 1May 26
370 Brundage ReservoirApr 12, 32.0Oct 30Jun 1
490 Galena SummitApr 21, 20.8Oct 22Jun 5
637 Mores Creek SummitApr 7, 31.2Nov 5May 27
  • Missing data convention: days with no data are omitted from values. There are no nulls and no sentinel codes (such as -99), so treat any date absent from the array as missing. Every row in the saved files has a numeric value. Requests used returnSuspectData=false (the API default). The OpenAPI doc says true means "data marked as suspect will not be filtered out". Leaving it false drops rows NRCS flagged Suspect (qcFlag S).

    • Verifier re-check (2026-09-13, every gap window re-requested with returnSuspectData=true): two gaps are suspect-flagged days, not absent days. The rows come back with qcFlag: "S" and no value key at all, so there is still no usable number:

      • 588 Lolo Pass SNWD 2016-02-16 → 2016-03-30 (44 days)

      • 623 Mica Creek SNWD 2002-08-08 → 2002-09-29 (53 days), which is why valued depth there starts on 2002-09-30 even though the element begin date is 2002-08-08.

        Every other gap returned no row even with suspect data included. That covers 623 WTEQ/SNWD WY2019, 637 WTEQ/SNWD 2023-09-14→27, 637 SNWD 2004-09-29, the 989, 623 (2003), 588 (2004, 2021) and 490 single-day SNWD gaps, and 370 SNWD before 2004-09-08. Those are genuinely absent from AWDB. Either way the saved files and the app treat both kinds of gap the same: no value for that day.

    • Parser warning if anyone re-downloads with returnSuspectData=true: values[] rows can then lack the value key. Guard with "value" in row rather than assuming it is present.

  • Gaps (exact inclusive date runs with no row):

StationWTEQ gapsSNWD gaps
989 Moscow Mountainnone2002-08-04; 2003-10-19; 2004-07-30
623 Mica Creek2019-01-27 → 2019-02-27 (32 d); 2019-03-01 → 2019-03-04 (4 d) (WY2019 peak season)2003-03-03; 2003-03-17; 2003-06-30; 2019-01-27 → 2019-03-04 (37 d)
588 Lolo Passnone2004-01-25; 2016-02-16 → 2016-03-30 (44 d); 2021-07-21
370 Brundage Reservoirnonenone (depth record starts 2004-09-08)
490 Galena Summitnone2002-01-19; 2002-01-30 → 2002-02-02 (4 d); 2004-01-05; 2004-01-25
637 Mores Creek Summit2023-09-14 → 2023-09-27 (14 d, snow-free season)2004-09-29; 2023-09-14 → 2023-09-27 (14 d)
  • The WY2019 gap at Mica Creek falls in peak season, so any WY2019 peak or melt-out estimate there is uncertain. Show it as incomplete rather than interpolating silently. Median depends only on calendar day: within each file, every WTEQ row with the same MM-DD has the same median (checked for all 366 MM-DD keys, including 02-29, at all 6 stations). A 366-point daily median curve can be read from any one full year of rows plus a leap-day row.

  • Flags (returnFlags=true). The legends below are verbatim from the NWCC Report Generator CSV header for the same database:

    • qcFlag (Quality Control): V Valid (validated data). N No Profile (no profile for automated validation). E Edit (minor adjustment for sensor noise). K Estimate. C Correction (a different equation has been applied to the original sensor voltage). B Back Estimate (regression-based estimate for homogenizing collocated snow course and snow pillow data sets). X External Estimate. S Suspect. M Missing. Codes actually present in these files: V, E, K (K appears on ≤30 rows per element).

    • qaFlag (Quality Assurance): U Unknown. R Raw (no human review). P Provisional (preliminary human review). A Approved (processing and final review completed). The qaFlag key is absent on every row before 2022-09-30 at all six stations. Treat an absent key as "not assigned", not as an error. From 2022-09-30 on, the flags run in water-year blocks. 370, 490 and 637: A 2022-09-30→2023-09-29, P 2023-09-30→2024-09-29, A 2024-09-30→2025-09-29, P 2025-09-30→2026-09-07/10, then R for the last 2–5 days. 623: the same, except that rows 2023-09-30→2024-07-30 have no qaFlag and P starts 2024-07-31. 588 and 989: A 2022-09-30→2023-09-30, no qaFlag 2023-10-01→2024-07-30, P 2024-07-31→2026-09-10, R 2026-09-11→12. WY2026 values are provisional (P) or raw (R) and may still be revised by NRCS.

  • Date convention (important for exact day alignment): requests used periodRef=END (the API default). SNOTEL daily SWE and depth are instantaneous midnight readings. With periodRef=END, a row dated D holds the reading taken at the midnight ending day D. The NRCS Report Generator's default "Start of Day" report labels that same reading D+1. A check on station 989 for 2026-02-01…05 confirmed this: the periodRef=START value (and median) on D+1 equals the periodRef=END value (and median) on D, for both WTEQ and SNWD. Medians use the same dating as the values. For an Oct 1 → Sep 30 seasonal chart the one-day difference is negligible. Just don't mix these files with Report Generator CSV values without shifting by one day.

  • Station caveats:

    • Moscow Mountain (989) began in WY2001, so fewer than 30 of its own years fall inside 1991–2020 (WY2001–2020 = 20 years). NRCS still publishes a daily 1991–2020 median for it, but the NRCS normals methodology pages (nrcs.usda.gov) timed out on every attempt during retrieval, so how this short-record median was produced could not be confirmed. Present it as "NRCS 1991–2020 median" without describing its method.

    • Snow depth starts later than SWE at 989 (2000-10-19), 623 (2002-09-30; the element's beginDate is 2002-08-08, but 2002-08-08→09-29 is flagged Suspect with no value) and 370 (2004-09-08). For the depth scrubber: at 623, WY2001 has no depth and WY2002 has only one day (2002-09-30). At 370, WY2001–WY2003 have none and WY2004 has only 2004-09-08→09-30.

    • "Record" or min/max envelopes computed from these files cover WY2001–WY2026 only, not each station's full period of record (several stations go back to 1978–1990). Label them "since 2001" in the UI.

  • License / terms: U.S. federal government data (USDA NRCS), public domain in the U.S. Same NRCS disclaimer as above: data are subject to revision regardless of QA level, and the NRCS / U.S. Government accept no liability.

  • Suggested citation (one per station, same pattern):

    • USDA NRCS National Water and Climate Center, SNOTEL daily snow water equivalent and snow depth, Moscow Mountain (989:ID:SNTL), WY2001–2026, Air and Water Database REST API (https://wcc.sc.egov.usda.gov/awdbRestApi/), accessed 2026-09-13.

    • … Mica Creek (623:ID:SNTL) …; … Lolo Pass (588:ID:SNTL) …; … Brundage Reservoir (370:ID:SNTL) …; … Galena Summit (490:ID:SNTL) …; … Mores Creek Summit (637:ID:SNTL) …

    • Combined, for the "Data & sources" panel: Snow data: USDA Natural Resources Conservation Service, National Water and Climate Center, SNOTEL network. Daily SWE, snow depth and 1991–2020 medians via the Air and Water Database REST API (https://wcc.sc.egov.usda.gov/awdbRestApi/), accessed 2026-09-13. Provisional data subject to revision.


File: awdb_reference_elements_durations_units.json

  • URL (GET): https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/reference-data?referenceLists=elements,durations,units

  • Publisher: USDA NRCS National Water and Climate Center (AWDB REST API).

  • Retrieved: 2026-09-13. Size: 35,098 bytes.

  • Contents: AWDB reference (lookup) lists: 116 element definitions, 13 duration codes, 68 unit codes. It is not time series data and has no date range. It is included so the app can show official element names and descriptions.

  • Format / parsing notes: a JSON object with keys elements, durations, units (each an array).

    • elements[]: code, name, physicalElementName, functionCode, dataPrecision, description, storedUnitCode, englishUnitCode, metricUnitCode. The relevant entries are: WTEQ = "SNOW WATER EQUIVALENT", "Depth of water that would theoretically result if the entire snowpack were melted instantaneously", stored in in (metric mm). SNWD = "SNOW DEPTH", "Total Snow Depth", stored in in (metric cm).

    • durations[]: code, name, durationMinutes (a string). units[]: code, singularName, pluralName.

  • License / terms: U.S. federal government data, public domain.

  • Suggested citation: USDA NRCS National Water and Climate Center, AWDB reference data (elements, durations, units), https://wcc.sc.egov.usda.gov/awdbRestApi/, accessed 2026-09-13.


Independent verification (2026-09-13, 18:21–18:27 UTC)

  • A separate verifier re-parsed every file and re-fetched from the live API. Requests were GET, one at a time, with the same User-Agent, and every response was HTTP 200. awdb_stations_ID_SNTL.json, awdb_reference_elements_durations_units.json and awdb_data_989_ID_SNTL_daily_WTEQ_SNWD.json: each exact URL above was re-fetched, and the new responses are byte-identical to the saved files (SHA-256 match).

  • The other five data files were checked with multi-station re-fetches (same parameters) for 2006-03-25→04-10, 2019-01-15→03-15 and 2025-12-01→2026-09-12. All 4,283 returned rows equal the saved rows exactly (value, median, qcFlag, qaFlag), with no extra or missing dates. timingCentralTendencies also match.

  • Record counts, date ranges, gaps, qaFlag blocks, qcFlag codes, the 366-key medians, value ranges and file sizes in this document were re-derived from the files and found correct.

  • periodRef convention re-confirmed on 989 (2026-01-30→02-07): the START value on D+1 equals the END value on D for WTEQ, its median and SNWD. The Report Generator's "Start of Day" CSV (URL below) matches START, and labels the median "Median (1991-2020)".

  • Correction made by the verifier: the suspect-data findings in the "Missing data convention" bullet above (the 588 and 623 SNWD suspect windows).

Supporting lookups (not saved here; listed for provenance)

  • https://wcc.sc.egov.usda.gov/awdbRestApi/v3/api-docs (OpenAPI 3 doc): parameter and field definitions quoted above.

  • https://wcc.sc.egov.usda.gov/reportGenerator/view_csv/customSingleStationReport/daily/989:ID:SNTL%7Cid=%22%22%7Cname/2026-02-01,2026-02-05/WTEQ::value,WTEQ::qcFlag,WTEQ::qaFlag,WTEQ::median_1991,SNWD::value (5-day NWCC Report Generator CSV): source of the flag legends, the "Median (1991-2020)" label, the disclaimer text and the Start-of-Day date comparison.

  • NRCS guidance says the official NRCS normal for snowpack comparisons is the 1991–2020 median (https://www.nrcs.usda.gov/resources/data-and-reports/climatic-and-hydrologic-normals, seen via search-result excerpts; the page itself timed out). NRCS's citation guidance (https://www.nrcs.usda.gov/programs-initiatives/sswsf-snow-survey-and-water-supply-forecasting-program/citing-data-from-the, also only reachable via search excerpts) asks users to cite the Air and Water Database with the actual access date and the URL used. Its APA example reads: "United States, US Department of Agriculture, Natural Resource Conservation Service, National Water and Climate Center. (2024, February 29). Air and Water Database. Water and Climate Information System. https://nwcc-apps.sc.egov.usda.gov/".

How it was built

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

14harness rounds
61.2minutes of harness work (1.0 h)
2,491lines of code
558KB, single-file app

Rounds by kind

build1
feedback7
continue3
critic3

Lines of code per file

body html193
app css279
app js1,803
scripts216

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

visual 8 out of 10
usefulness 8 out of 10
correctness 8 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

  • Moscow Mountain card: WY2026 peak 10.1 in on Mar 13, 50% of median peak. Raw WTEQ 2026-03-13 = 10.1 (max of WY2026); NRCS medianPeak 20.2; 10.1/20.2 = 50%. Match.
  • Galena Summit scrubber, Mar 20 2017: depth 87 in, SWE 31.3 in. Raw SNWD 2017-03-20 = 87, WTEQ = 31.3. Compare tooltip the same day: median 18.2 in, 172% of median. Raw median 18.2; 31.3/18.2 = 172%. Match.
  • Mores Creek Summit scrubber, Apr 5 2006: depth 99 in, SWE 42.6 in. Raw 99 / 42.6 (the dataset maximum SWE). Match.
  • Melt-out vs median (computed): Moscow Mountain melted out Apr 8 vs NRCS median May 5, so 27 days early; Galena Summit May 15 vs Jun 5, 21 days early. Independent Python gives 27 and 21, matching the app after round 12. Before the critic rounds every station read one day short (26, 20).
  • Hero summary average peak 64% (computed): per-station 50, 53, 80, 70, 88, 42, mean 63.8, displayed 64%. Match.
Show all 9 spot checks
  • Record low (computed): Mores Creek Summit WY2026 peak 13.0 in is the lowest WY2001-2026 peak in the raw file. The UI now labels it 'Lowest peak since 2001'; before, it showed a broken raspberry legend entry and a chip that highlighted nothing.
  • Feb 29 median: raw 2024-02-29 medians are Mica Creek 19.8, Lolo Pass 23.4, Brundage Reservoir 23.1. Embed was interpolated (19.9 / 23.6 / 23.3) and is now exact after round 12; a full JSON diff showed no other value changed.
  • Galena Summit per-year table WY2017: peak 36.2 in on May 2, melt-out Jun 15. Independent peak/melt-out recompute from raw WTEQ gives the same values.
  • Sources: Data & sources panel credits USDA NRCS NWCC SNOTEL via the AWDB REST API with a link and access date, marks provisional data, and labels peak/melt-out as app-derived since 2001. No synthetic data.
Build notes ✎ written by the AI reviewer

Data: the data-prep workflow reported READY (ready-after-repair). I parsed every raw file myself (record counts, date ranges, value ranges) and reused them unchanged. All raw data (~6 MB) and SOURCES.md were already committed by data prep.

  • Harness behaviour (ForgeCode 2.13.21, default-agent qwen/qwen3.8-27b): Round 1 (build, 6 min) produced a complete app with every feature, but with a mislabeled month axis, wrong melt-out logic and leap-year misalignment.

  • The model repeatedly stalled in exploratory loops. Round 2 (feedback, 61 s) and round 3 (continue, 252 s) edited nothing: they read files, then ran near-identical python data probes. Round 5 (feedback) spent 99 of ForgeCode's 100 requests per turn printing single array values and never edited a file. Round 4 (continue) also hit the 100-request cap, but did most of the round-2 feedback.

  • From round 6 on, prompts that forbade exploratory shell commands (only build.py --check allowed) made it edit reliably.

  • Hard problems and regressions: The month-axis bug took four reports (rounds 2, 5, 7). It was fixed in round 8 only after FEEDBACK gave the exact tick positions and described the month-number root cause.

  • Round 7 introduced a load-time crash: it changed seasonSummary's signature without updating two callers. Round 8 fixed it.

  • Round 9 broke non-leap-year date alignment. The root cause was that scripts/build_data.py and app.js used different Feb 29 slot conventions (61 vs 152). The harness fixed both in round 10 and re-ran the data script.

  • The phone stake-label size needed three reports.

Round counts: 7 feedback rounds used (2, 5, 7, 8, 9, 10, 11; rounds 2 and 5 produced no edits) plus 3 continue rounds (3, 4, 6). I stopped after round 11 with one feedback round unused, because the remaining defect is a ~2 px line break and rounds 7 and 9 had shown real regression risk.

Rules: no network outages (network_error_lines 0 in every round) and no TAMPER GUARD. I never edited src/, scripts/, data/embed/ or index.html. I only wrote FEEDBACK.md (removed at the end, commit d5174f7) plus prompts and test scripts in the runs directory. FEEDBACK files sometimes named root causes in words (field names, slot convention) but contained no code.

Verification used verify_app.py plus my own Playwright and python checks in the runs directory (test_features.py, probe.py, probe2.py, probe_dates.py, probe_feb29.py, probe_r11.py, stack_probe.py, check_embed.py). A headless Chrome from another app's driver (meeting-finder) and an unrelated http.server on port 8765 were running when I finished; neither is mine, and I did not touch them.

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 5.8 min · 6 files changed, 2687 insertions(+), 3 deletions(-) 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/ that turn the raw data in data/raw/ (see data/raw/SOURCES.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.

    3 continue 4.2 min · — Show prompt

    Prompt given to the harness:

    Continue where you left off: re-read SPEC.md, AGENTS.md and FEEDBACK.md in this directory. FEEDBACK.md lists 13 problems a reviewer found when testing index.html in a real browser; none of them has been fixed yet (src/ and scripts/ are unchanged). Fix every item in order — edit scripts/build_data.py and re-run it where the data logic is wrong, then update src/body.html, src/app.css and src/app.js — keeping everything that already works. Do not stop until every item is fixed and python3 build.py --check succeeds.
    

    No feedback file was sent in this round.

    4 continue 14.2 min · 6 files changed, 1776 insertions(+), 388 deletions(-) Show prompt

    Prompt given to the harness:

    Continue where you left off. FEEDBACK.md in this directory lists 13 problems a reviewer found in index.html; none are fixed yet. The analysis is already done: FEEDBACK.md states the facts (you already confirmed the melt-out examples in the last session), so do NOT run more exploratory python checks on the data. Start editing files right away and work through FEEDBACK.md in this order, running python3 build.py --check after each step:
    Step 1 — scripts/build_data.py: melt-out = first day after the season peak with SWE <= 0.5 in (item 2); keep leap-year arrays consistent (item 3); mark only years with real snow-season gaps as incomplete and treat WY2026 as a normal current year (item 9). Re-run the script.
    Step 2 — src/app.js: fix the month labels and tick positions on every date axis and make all charts align by calendar date including Feb 29 (items 1 and 3).
    Step 3 — station cards season summary, sparkline median line, balanced card grid, list/button semantics (items 4, 10, 13).
    Step 4 — responsive charts drawn at container width with readable phone text, no horizontal scroll at 390 px, single-line tabs (item 5).
    Step 5 — scrubber scene, snow stake, timeline with playhead, faster playback (item 6).
    Step 6 — compare mode shared scale, stats table, synchronized hover (item 7).
    Step 7 — seasonal chart record years, year highlighting, richer tooltip, last-data marker (item 8); peak & melt-out titles, markers, per-year view (item 9).
    Step 8 — shared selection across views and the hero summary (items 11 and 12).
    Keep everything that already works. Do not stop until every item is fixed and python3 build.py --check succeeds.
    

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

    Reviewer feedback — round 1 build (tested in headless Chromium at 1440×900 and 390×844)

    Good start: no console errors, no external requests, every tab renders, the embedded SWE values match the raw AWDB files exactly, and peak-day values are correct. The problems below are listed most important first.

    1. Month labels are wrong on every date axis (seasonal chart, both compare charts, both histograms). The seasonal chart's x-axis reads Oct Nov Dec Feb Mar Apr May Jun Jul Aug Sep Oct: January is missing, and every label from January on is one month late (the tick at Jan 1 says "Feb", and the tick at Sep 1 says "Oct"). The tick positions are also slightly off: October is laid out as 30 days. Hovering the seasonal chart where the axis says "Apr" gives a tooltip of "Mar 6, 2026". The histograms start at "Dec" where they should say "Nov", and the histogram range (meant to be Nov 1 → Jul 31) actually starts on Dec 1. Expected: 12 labels Oct Nov Dec Jan Feb Mar Apr May Jun Jul Aug Sep, each at the first day of its month, agreeing with the tooltip date under the cursor, on every chart.

    2. Melt-out day is computed wrongly. It is currently "the last day of the water year with SWE > 0.5 in", so early-autumn snow counts as the melt-out. Examples from data/embed: Mica Creek WY2017 melt-out = day 354 (Sep 19, 2017), but the snowpack actually melted out on 2017-05-24. Galena Summit WY2017 = day 363 (Sep 28) instead of 2017-06-15. Days outside the histogram range are silently clamped into the edge bins (Brundage's melt-out histogram has bars in late August). Expected: melt-out = the first day after the season's peak on which SWE falls to ≤ 0.5 in (no September values). Say the rule in the Data & sources text.

    3. Leap water years are misaligned. years[wy].swe/depth have 366 entries in leap water years (WY2004, 2008, 2012, 2016, 2020, 2024). The median array has 366 entries in leap-year indexing. The charts and the scrubber still assume 365 days (n = 365, "day 1 / 365"), so after Feb 28 the median is read one day early, and the last day (Sep 30) of the median and of leap years is dropped. Expected: every series lines up by calendar date (Feb 29 handled), and the scrubber shows "/ 366" in leap years.

    4. Station cards never show "% of median" for most of the year. It is September, so all six cards say 0.0 in SWE snow-free, and the card's headline number (% of median) is absent. That makes the most important row of the page uninformative. Expected: whenever the pack is gone (or whenever the median for today is ~0), the card should switch to a season summary. Show WY2026 peak SWE, its peak date, peak as % of the NRCS median peak (the station's medianPeak value, colour-coded above/below), and melt-out date vs the median melt-out date (e.g. "melted out 23 days early"). Keep "current SWE + % of median today" for snowy dates. The sparkline has no context: add a faint median line behind the WY2026 line and a dot at the latest value.

    5. Phone (390 px): the page scrolls horizontally, and the charts are unreadable.

      • The two-column card grid is 18 px wider than the screen. The right-hand card buttons end at x = 408, so the page scrolls sideways. Station names wrap onto three lines, and "WY2026 sparkline / peak 22.3 in" wraps. Expected: no horizontal scroll at 390 px. Use one card per row, or a compact horizontal scroll-snap strip that is clearly scrollable.

      • Every chart uses a fixed wide viewBox (960 px) scaled down, so axis labels render ~4 px tall on the seasonal chart and ~6 px on the histograms and compare charts at 390 px. Expected: draw charts for the actual container width (re-render on resize) so tick labels stay ≥ 11 px on a phone. Use fewer x ticks when narrow (e.g. every other month), and give the seasonal chart a sensible phone height.

      • The four tabs are squeezed into one row. "Seasonal curve" wraps to two lines and "Snow-depth scrubber" to three. Expected: single-line tab labels, via a horizontally scrollable tab strip or a tidy 2×2 grid.

    6. The snow-depth scrubber scene looks broken. At deep snow (Galena Summit WY2017, day 176, 89 in), the "snow" is a white copy of the ridge shape shifted upward. It fills most of the sky and hides the sun, and it sits above the mountain instead of on it (screenshot: white band over the top half of the scene, grey rock below). Expected: a snowpack that visibly builds on the slopes and ground, and a snow stake / depth ruler with inch marks so the depth reads at a glance (SNOTEL sites use a snow depth sensor over a pillow, and a stake works well). Also:

      • Playback is very slow. At the default 2× it advances 2 days per second, so a full year takes 3 minutes. Expected: a year plays in ~20–30 s by default, with faster/slower options and smooth (not jumpy) motion.

      • It opens on WY2026 at Oct 1 with 0 in and an empty scene. Open on a snowy moment instead, or make it obvious to press Play.

      • Add a small SWE/depth timeline for the chosen year under the scene, with a moving playhead that you can click or drag to jump. It should show where in the season you are, including the peak and melt-out markers.

      • On desktop the right column has a large empty area below the controls.

    7. Compare mode says "on a shared scale" but the scales differ. Brundage Reservoir's chart runs 0–40 in and Moscow Mountain's runs 0–20 in, so the two lines look similar when Brundage has twice the snow. Expected: both charts use the same y-axis maximum. Add a side-by-side stats table under the charts (elevation, WY2026 peak SWE and date, peak % of median peak, melt-out date vs median, today's SWE) and a synchronized hover crosshair: hovering one chart shows the same date and values on both. It would also help to be able to pick the water year to compare (default WY2026).

    8. Seasonal chart: add the record years and let people explore past years. SPEC asks to compare this winter "against the long-term median and the record years". Right now the faint past years cannot be identified. Expected:

      • Label the record-high and record-low peak years since 2001 (e.g. highlighted lines with "WY2011 — record high since 2001" annotations or legend entries).

      • Let the user highlight any past water year, by hovering or clicking a faint line or via a year picker or chips. The highlighted year gets a distinct colour and is named in the legend.

      • The hover tooltip should also show the min–max envelope for that day (with the years that set them) and the highlighted year's value.

      • Mark the latest data point of WY2026 ("Sep 12") with a dot and label. The gold line currently runs visually to the end of the axis.

      • The y-axis has only 3 labels (0 / 20 / 40) for Lolo Pass. Use 5–8 gridlines.

    9. Peak & melt-out panel needs per-year estimates and labelling. SPEC: "'Peak day' and 'melt-out day' estimates per year with a histogram". The histograms have no titles or axis titles; you cannot tell the teal one is "peak day" and the rust one is "melt-out day" without hovering. Expected:

      • Chart titles and axis titles ("Date of peak SWE", "Number of water years").

      • A marker for the NRCS median peak / median melt-out date and for WY2026 on each histogram.

      • Bar tooltips that list the water years in that bin.

      • A per-year view of the estimates (e.g. a compact table or a dumbbell/timeline chart: one row per water year from peak date to melt-out date, with peak SWE). Hovering a year should highlight it in the histograms.

      • WY2026 is drawn as "incomplete" (rust) in the scatter, but its snow season is fully recorded. Treat it as a normal (current) year. Only years with real data gaps in the snow season (Mica Creek WY2019) should be marked incomplete, with a legend entry explaining the rust colour.

    10. Card grid layout on desktop. At 1440 px the six cards sit in a 5-column grid, leaving Mores Creek Summit alone on a second row. Expected: a balanced layout (6 across, or 3 × 2).

    11. Selections don't carry across views. Clicking a station card updates the seasonal and peak views, but the Compare tab's Station A and the scrubber's Station stay on their old values. Expected: selecting a card also sets Compare's Station A and the scrubber station. Each panel should name the selected station clearly in its heading.

    12. Desktop hero has a large empty area under the intro paragraph (left column, beside the How-to-use card). Use it for a short live summary of WY2026 across the six stations, e.g. average peak as % of median, the earliest and latest melt-out, and which station fared best/worst. Keep it computed from the embedded data.

    13. Accessibility: the station cards are <button role="listitem">, which removes their button semantics for screen readers. Expected: a list whose items contain real buttons (keep aria-pressed). Also make the chart tooltips work with tap on touch screens. The How-to text says "Hover", so mention "tap" too.

    6 continue 5.1 min · 4 files changed, 244 insertions(+), 102 deletions(-) Show prompt

    Prompt given to the harness:

    Continue where you left off. FEEDBACK.md in this directory lists 8 problems a reviewer found in index.html. None of them is fixed yet: in the last session you read the files and then spent every request printing single values from data/embed/snowpack.json with python, and changed nothing. FEEDBACK.md already contains every data fact you need, so in this session do NOT run python, cat, grep or any other shell command to inspect data — the only shell command you may run is python3 build.py --check. Use your file read and edit tools only.
    Work through FEEDBACK.md strictly in order and make a real edit for each item before moving on:
    1. Item 2 first (quickest win): in src/app.js every use of medianPeak and medianMeltout must read the keys that actually exist in the embedded data — month, day and value — instead of m, d and v. That removes the NaN/null text and the console errors. Run python3 build.py --check.
    2. Item 1: fix the month-name lookup for every date axis (seasonal chart, compare charts, histograms, scrubber timeline) so the labels read Oct Nov Dec Jan Feb Mar Apr May Jun Jul Aug Sep at the first day of each month. Run python3 build.py --check.
    3. Then items 3, 4, 5, 6, 7 and 8 in that order, editing src/app.js, src/app.css and src/body.html, with python3 build.py --check after each item.
    Keep everything that already works. Do not stop until all 8 items are fixed and python3 build.py --check succeeds.
    

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

    Reviewer feedback — round 4 build (tested in headless Chromium at 1440×900 and 390×844)

    • Big improvement. These now work well and must be kept: melt-out fixed

    • leap years

    • envelope tooltip with record years

    • year chips

    • "WY2026 · Sep 12" marker

    • 6-across cards with median sparkline

    • compare shared scale + stats table + synchronized hover

    • scrubber timeline + faster playback

    • per-year peak table

    • no horizontal page scroll on phone

    • readable chart text on phone

    Remaining problems, most important first.

    1. Month labels are STILL wrong on every date axis. This is item 1 from last time, and it was not fixed.

      • The seasonal chart reads Nov Dec Feb Mar Apr May Jun Jul Aug Sep Oct. The Oct 1 tick at the left edge has no label, there is no "Jan", and the label "Oct" is printed at the Sep 1 tick.

      • Both compare charts and the scrubber timeline show the same wrong sequence.

      • The histograms read Dec Feb Mar Apr May Jun Jul Aug.

      • Proof: in Compare, hovering exactly on the gridline labelled "Apr" gives a tooltip for "Mar 2, 2026".

        Every label from January on is one month too late, because the month name for the Jan–Sep ticks is looked up one position too far along the month-name list. Expected, for a non-leap year with water-year day 1 = Oct 1:

    DayLabel
    1Oct
    32Nov
    62Dec
    93Jan
    124Feb
    152Mar
    183Apr
    213May
    244Jun
    274Jul
    305Aug
    336Sep

    That is exactly 12 labels, and it applies to every chart, including the Nov→Aug histogram axis. After the fix, hovering on a month gridline must give the 1st of that month in the tooltip.

    1. NaN / null / "—" everywhere because the median peak and melt-out fields are read with the wrong names. In data/embed/snowpack.json each station has "medianPeak": {"month": 4, "day": 21, "value": 20.8} and "medianMeltout": {"month": 6, "day": 5}, but the app reads them as if the keys were m, d and v. Symptoms:

      • Console errors, 1 set on page load and 8 more after clicking the cards and tabs: Error: <line> attribute y1: Expected length, "NaN"., Error: <line> attribute y2: Expected length, "NaN". and Error: <text> attribute y: Expected length, "NaN".

      • Every station card says "melted out NaN days late".

      • Hero summary: "Average peak: 0% of each station's median peak". "Best vs median: Moscow Mountain (null% of median peak)" and "Worst vs median: Moscow Mountain (null% of median peak)" name the same station for best and worst.

      • Compare table: "Peak % of median peak: —" and "Melt-out vs median: May 14 (NaNd late)".

      • Peak scatter: the label says "median peak — in", and the dashed median line is drawn above the plot, outside the axes.

      • The NRCS median peak / median melt-out markers are missing from the histograms.

        Expected: real numbers everywhere. For example, Moscow Mountain WY2026 peak 10.1 in vs median peak 20.2 in gives "50% of median peak". Melt-out Apr 8 vs median May 5 gives "melted out 27 days early".

    2. Station cards: wrong badge and cramped layout.

      • Next to the peak value every card shows a bold "no peak yet" badge, even though the peak exists ("peak Mar 13"). Expected: the colour-coded "% of median peak" badge.

      • At 1440 px the cards are ~200 px wide, so names wrap to two or three lines ("Mores / Creek / Summit"). The footer "WY2026 · grey = median / peak 10.1 in" wraps into two ragged lines, and the peak value is repeated in the footer. Expected: tidy cards. A 3 × 2 grid with wider cards at desktop is fine, and so are shorter footers (e.g. a small legend once above the cards instead of in every card).

      • The section subtitle still says "Current SWE as of Sep 12, 2026 · % of the 1991–2020 median for that day" while the cards show season peaks. Make the subtitle match what the cards show.

    3. The scrubber scene still looks broken.

      • Galena Summit WY2017 at Feb 27 (96 in): the white "snow" is still a copy of the ridge shifted upward. It fills the top of the scene, hides half the sun, and sits above the rock instead of on it.

      • The snow stake floats in the middle of the grey rock face, not planted on the ground.

      • The red depth pointer sits beside the stake at ~90, but no snow reaches the stake, so the stake does not show the depth.

      • On the phone at 33 in, the snow is a thin white band floating above the mountain outline.

        Expected: the snow surface builds up from the ground and slopes. The stake stands in the foreground on the ground, and the snow surface visibly reaches the stake mark equal to the depth (96 in reaches just below the 100 mark). Sky and sun stay visible.

    4. Scrubber day count and timeline details.

      • Non-leap years offer a day 366: WY2001 at "366 / 366" shows "Oct 1, 2001 · WY2001", which belongs to WY2002. The phone shows "164 / 365" for WY2026, while desktop shows "150 / 366" for WY2017; both are non-leap years. Expected: 365 days in non-leap years and 366 in leap years, never a date outside the water year.

      • Timeline y-axis labels overlap into an unreadable stack ("20 17.5 15 12.5 10 7.5 5 2.5 0" in ~60 px). Expected: 2–3 ticks.

      • On the phone the "peak" and "melt-out" labels overprint as "peakelt-out" when the dates are close. Expected: offset them.

      • On the phone the snow-stake numbers are ~6 px tall. Expected: ≥ 11 px.

    5. Record years are not visually distinct on the seasonal chart. The legend lists "WY2002 — record high since 2001" and "WY2005 — record low since 2001" for Moscow Mountain. Both use the same grey swatch as the other past years, and in the chart they cannot be told apart from the faint lines. Expected: distinct colours from the secondary palette, e.g. Clearwater for the record high and Raspberry for the record low. Give each a slightly thicker line and a small year label at its peak, with legend swatches that match.

    6. Peak & melt-out panel polish.

      • The "WY2026" marker label on the melt-out histogram overprints the tallest bar (Lolo Pass). Expected: a label with a white halo, or placed above the plot.

      • The chart titles "Peak day" / "Melt-out day" collide with the top y-axis label "15".

      • The per-year table has 26 rows and makes the panel ~1,600 px tall. Expected: show the most recent ~8 years plus a "Show all 26 years" toggle, or a max-height scroll area with a sticky header.

    7. Phone tab strip gives no hint that more tabs exist. At 390 px the strip scrolls horizontally, but "Snow-depth scrubber" starts at x = 406, entirely off-screen, with no cue. Expected: a visible affordance (fade or arrow at the edge, or scrolling the active tab into view), or a 2 × 2 grid of tabs.

    12 critic 3.4 min · 5 files changed, 248 insertions(+), 69 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists 8 bugs a reviewer found when testing index.html in a real browser, each with its exact cause and fix. Fix every item strictly in order, keeping everything that already works; change only what each item requires and do not rewrite or reorganize anything else. Item 5 is an edit to scripts/build_data.py (then run python3 scripts/build_data.py); all other items are edits to src/app.js (touch src/app.css or src/body.html only if an item needs it). FEEDBACK.md already contains every fact you need, so do NOT run python, node, cat or grep shell commands to inspect data or code — the only shell commands you may run are python3 scripts/build_data.py and python3 build.py --check. Use your file read, search and edit tools. Make a real edit for each item before moving to the next, and run python3 build.py --check after each item. Whenever you change a function's parameters, find every call of that function and update each one. Do not stop until all 8 items are fixed and python3 build.py --check succeeds.
    

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

    Reviewer feedback: 8 correctness bugs (tested in a real browser)

    Fix them in this order. Every fact and number below was already checked against data/raw, so no data inspection is needed.

    1. Snow-depth scrubber: the Speed menu does nothing
    • Measured: at 1×, 2× and 4× the animation advances exactly 61 days in 2 seconds, one day per 33 ms tick. Cause, in src/app.js section 7: (a) startScrub computes the next day as Math.ceil(state.scrubDoy + daysPerTick). daysPerTick is below 1, so that always adds exactly one day.

    • (b) state.speed starts at 18, but the speed menu's change handler sets it to 1, 2 or 4.

    • Fix: Keep a fractional play position (a float) in state, separate from the integer day that is displayed.

    • On every tick, add (days per second ÷ 30) to it and display Math.floor of it.

    • Define the menu values as 1× = 15 days/s, 2× = 30 days/s, 4× = 60 days/s. Derive the speed from the selected menu value everywhere, including the initial 2×.

    • Dragging the slider or clicking the timeline must reset the fractional position to that day.

    Expected: a full water year plays in about 24 s at 1×, 12 s at 2× and 6 s at 4×.

    2. "Show all 26 years" under the per-year table needs two clicks

    On the Peak & melt-out tab, the first click only turns the chip gold. Still 8 rows, and the label still says "Show all 26 years". Cause: in renderPerYear, the click handler sets open to the negation of the return value of classList.toggle(...). But toggle returns true exactly when the class was just added, which is when the table should open. Fix: remove the negation. The first click shows all 26 rows and relabels the button "Show 8 most recent". The second click collapses it again.

    3. Lines break at Feb 29 in every non-leap year (visible white slit near "Mar")
    • In the Seasonal chart, these lines all have a 2–3 px gap at the Feb 29 slot, just left of the Mar gridline: the gold WY2026 line

    • the record-high and record-low lines

    • every grey past-year line

    • About 20 grey lines break at the same x, so every station shows a white vertical slit through the envelope. Both Compare charts have the same gap. Cause: the path loops reset their "started" flag whenever a slot is null, and slot 152 (0-based index 151) is null by design in non-leap water years. The affected loops are the current year and past years in drawSeasonal, and the selected year in drawCompare. Fix: In those loops, when the index is 151 and the water year is not a leap year (isLeapWy), skip that slot without resetting the started flag.

    • Keep breaking the line on every other null (real data gaps such as Mica Creek WY2019).

    Check: the d attribute of the gold WY2026 path must contain exactly one "M" command.

    4. "Melted out N days early" is one day too small everywhere

    This affects the station cards and the Compare table's "Melt-out vs median" row. Correct WY2026 values, from the raw NRCS data:

    StationMelt-outNRCS medianCorrectApp shows
    Moscow MountainApr 8May 527 days early26
    Mica CreekApr 21May 1726 days early25
    Lolo PassMay 10May 2616 days early15
    Brundage ReservoirMay 14Jun 118 days early17
    Galena SummitMay 15Jun 521 days early20
    Mores Creek SummitMay 4May 2723 days early22
    • Cause: the MD2DOY lookup at the top of src/app.js skips Feb 29 while counting, so it gives Mar 1 = 152. But the per-year arrays and the peak.doy / meltout.doy fields use leap-year slots, where Feb 29 = 152 and Mar 1 = 153. Fix: Make MD2DOY count Feb 29 as slot 152, so Oct 1 = 1, Feb 28 = 151, Feb 29 = 152, Mar 1 = 153 and Sep 30 = 366. This is exactly the convention in scripts/build_data.py.

    • The same fix moves the Mar–Sep month gridlines and labels, and the dashed median markers in the histograms, onto the correct day.

    • Keep MONTH_STARTS, HIST_LO/HIST_HI and seasonSummary using MD2DOY consistently.

    5. The Feb 29 median is interpolated instead of the NRCS value

    The raw files do contain Feb 29 rows (2004-02-29, 2008-02-29 … 2024-02-29), and each carries the NRCS published median:

    StationNRCS Feb 29 medianCurrent data/embed value
    Moscow Mountain18.8(matches)
    Mica Creek19.819.9
    Lolo Pass23.423.6
    Brundage Reservoir23.123.3
    Galena Summit15.7(matches)
    Mores Creek Summit24.6(matches)
    • Three stations are wrong because scripts/build_data.py skips slot 152 and averages Feb 28 and Mar 1. Its comment says no leap water year exists in the record, which is false. Fix, in the median loop of scripts/build_data.py: Map every row with doy_to_leap_idx(doy_of(d), wy_of(d)), the same mapping the per-year arrays use, so slot 152 is filled from a Feb 29 row.

    • Delete the interpolation line and the wrong comment.

    • Run python3 scripts/build_data.py, then python3 build.py --check. No other value in data/embed may change.

    6. Mores Creek Summit: the current year is the record low, and the UI mislabels it
    • WY2026's 13.0 in is the lowest peak since 2001 at Mores Creek Summit. Right now: the legend shows a separate raspberry entry "WY2026 — record low since 2001", but no raspberry line exists

    • the first chip is "WY2026 (record low)", and clicking it highlights nothing

    • the phone "Highlight a year" select lists that entry too

    • Fix (in renderSeasonal, buildYearChips and drawSeasonal): When rec.hi or rec.lo belongs to the current water year, do not add a record legend entry, chip or select option for it.

    • Instead, change the gold current-year legend label to "WY2026 (current) — lowest peak since 2001" (or "highest peak since 2001").

    • Add a short "lowest peak since 2001" note to that station's card, after "42% of median peak".

    • When the current year is not a record, nothing changes. Moscow Mountain must still show WY2002 as record high and WY2005 as record low.

    7. Highlighting a year with a data gap draws nothing
    • Select Mica Creek, then click the "WY2019" chip. The legend says "WY2019 (highlighted)", but no purple line appears. Cause: drawSeasonal skips every incomplete year before drawing, including the highlighted one. Fix: Always draw the highlighted year (purple, width 2.5), even when it is incomplete, and leave its real data gap as a break in the line.

    • Keep excluding incomplete years from the faint grey lines and from the envelope.

    • When the highlighted year is incomplete, label its legend entry "WY2019 (highlighted, data gap)".

    8. Phone: tapping a chart shows no tooltip

    "How to use" says "Hover (or tap)", but at 390×844 a touch tap on the Seasonal chart leaves the tooltip hidden. Cause: the chart overlays in drawSeasonal and drawCompare only listen for pointermove, and a tap fires pointerdown/pointerup without any pointermove. Fix: register the same handler for pointerdown as well, and keep pointermove for dragging. A tap should show the crosshair and tooltip for the tapped day.

    13 critic 4.5 min · 4 files changed, 416 insertions(+), 198 deletions(-) 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 after your last round, each with its cause and the exact fix. Fix every item strictly in order, keeping everything that already works (the last round's fixes are correct — do not undo them); change only what each item requires and do not rewrite or reorganize anything else. All edits are in src/app.js, src/app.css and src/body.html; do not edit scripts/ or data/embed/. FEEDBACK.md already contains every fact you need, so do NOT run python, node, cat or grep shell commands to inspect data or code — the only shell command you may run is python3 build.py --check. Use your file read, search and edit tools. Make a real edit for each item before moving to the next, and run python3 build.py --check after each item. Whenever you change a function's parameters or what it returns, find every call of that function and update each one. Do not stop until all 9 items are fixed and python3 build.py --check succeeds.
    

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

    Reviewer feedback, round 2: 9 items (tested in a real browser after your last fixes)

    The last round fixed items 2–7 correctly: melt-out days, Feb 29 medians, record labels, Show all, the continuous gold line and incomplete-year highlighting. Keep all of that. Fix the items below in order. Every fact was already checked, so no data inspection is needed.

    1. Regression: the scrubber at 1× speed is frozen
    • Measured: at 1× the day does not advance at all (0 days in 2 s). 2× (30 days/s) and 4× (60 days/s) are correct. Cause: setScrubDay now sets state.scrubPos = doy.

    • The play timer calls setScrubDay(Math.floor(state.scrubPos)) on every tick. At 1× each tick adds 0.5, so scrubPos is floored back to the same day before the next half-day can accumulate.

    • Fix: Remove the state.scrubPos = doy line from setScrubDay.

    • Set state.scrubPos explicitly wherever the user picks a day or a new year:

      • the slider input handler

      • the timeline jump function in buildScrubTimeline

      • the scrubber station and year change handlers

      • selectStation

    • In startScrub, set state.scrubPos = state.scrubDoy before starting the timer.

    Expected: from day 30, 2 s of play advances about 30 days at 1×, 60 at 2× and 120 at 4×.

    2. Phone: the chart tooltip vanishes as soon as the finger lifts
    • After your pointerdown change, a tap fills the tooltip, but it is hidden immediately. Tested at 390×844 with a touch tap: the tooltip text is set, but the is-visible class is gone. A touch pointer always fires pointerleave right after pointerup. Fix, in the overlay pointerleave handlers of drawSeasonal and drawCompare: When ev.pointerType === "touch", return without hiding, so the tooltip and crosshair stay visible.

    • Hide them instead when the user taps anywhere outside that chart: one pointerdown listener on document, added once, that hides the seasonal and compare tooltips and crosshairs when the target is not inside their chart.

    3. There is still a white vertical slit at Mar 1, now inside the min–max envelope

    The lines are continuous now. But the shaded envelope has a deep V-notch at the Feb 29 slot at every station, because only the 6 leap years have a value there:

    StationEnvelope top (Feb 28 / Feb 29 / Mar 1), inEnvelope bottom (Feb 28 / Feb 29 / Mar 1), in
    Galena Summit28.4 / 19.8 / 28.510.5 / 12.4 / 10.5
    Mores Creek Summit35.0 / 30.1 / 35.213.0 / 21.4 / 13.0
    Moscow Mountain30.0 / 27.6 / 30.05.4 / 13.3 / 5.4

    Fix: in drawSeasonal, where the envelope path is built, skip index 151 in both the hi pass and the lo pass. The envelope then bridges Feb 28 directly to Mar 1.

    4. The seasonal tooltip's "Range since 2001" row overflows the tooltip box
    • Hovering Galena Summit on Apr 2 renders "12.4 (WY2001) – 33.3 (WY2017)" past the right edge of the black tooltip: .tooltip has max-width: 260px and white-space: nowrap. Fix: Replace that one row with two rows: "Lowest since 2001" → "12.4 in (WY2001)", and "Highest since 2001" → "33.3 in (WY2017)".

    • Raise .tooltip max-width to 300px.

    5. The Mores Creek Summit card's record note looks broken
    • "· lowest peak since 2001" wraps onto its own line, starting with a stray bold "·". Fix: Render the note as a small pill badge on its own line, with no leading dot. Text "Lowest peak since 2001". Raspberry text (var(--ui-raspberry)) on a light raspberry background (rgba(215,0,63,.08)), font-size .7rem, weight 700, padding 2px 8px, border-radius 999px.

    • Use clearwater colours for "Highest peak since 2001".

    6. The peak-day and melt-out histograms are too coarse
    • Twelve bins of about 23 days spread over Nov 1 – Jul 31, so each station's data sits in 3 or 4 bars with large empty areas. The tooltip's "± 2 wk" is also wrong (a bin is ±11 days). Fix, in histBins and drawDayHist: Use 7-day bins.

    • Give each histogram its own range: from the 1st of the month containing its earliest value (or its median or current-year marker, if earlier) to the last day of the month containing its latest value or marker.

    • Draw month labels only inside that range.

    • Tooltip title: the exact bin dates, e.g. "Apr 9 – Apr 15", followed by the count and the list of water years.

    • highlightHistYear must compute the bin index with the same range and bin width as the drawn bars, for example by storing them on the SVG element when drawing.

    7. Snow-depth scrubber scene
    • a. The snow caps never appear. In capPath, the L list and the R list walk the same ridge points in opposite directions, so the polygon has zero area. Rebuild each cap as the part of the mountain above a snowline:

      1. Start at the summit, (200,95) or (330,70).

      2. Go down the left slope to the x where the ridge reaches y = summit_y + capHeight (ridgeYAt gives the slope heights).

      3. Cross to the matching x on the right slope with a jagged edge of 4 small zigzags (±5 px).

      4. Go back up to the summit.

        capHeight = 10 + depth_in × 0.4 px, capped at 55 px. At zero depth, draw no cap.

    • b. The stake labels overlap. The "in" unit label at the top of the stake overlaps the "120" label. Move "in" to the left of the stake top (text-anchor end, x = STAKE_X − 8, same y as the 120 tick).

    • c. Phone layout (≤ 640px). The three readout boxes (Snow depth, SWE, Day of year) wrap to 2 + 1, and the Speed menu drops onto its own line. Keep the three boxes in one row (grid-template-columns: repeat(3, 1fr), value font-size 1rem, label .62rem). Keep Play, the slider and Speed on one row.

    8. The Compare table's "Today's SWE" is unclear in September

    It shows "0.0 in (— of median)". When the median for that day is ≤ 0.05 in, show "0.0 in · snow-free (median 0.0 in)". Otherwise keep "X in (NN% of median)".

    9. Phone: the per-year table cells wrap to two lines
    • At 390px the cells break ("10.1 / in", "Mar / 13", "MELT- / OUT DATE"). Fix, at ≤ 560px: Use short headers: "WY", "Peak (in)", "Peak date", "Melt-out", "Days".

    • Show plain numbers in the Peak and Days cells (no " in" or " days" suffix). Keep the full header text and units on wider screens.

    • Set the table to font-size .76rem, cell padding 5px 6px, white-space: nowrap, so every row fits on one line.

    14 critic 1.1 min · 4 files changed, 70 insertions(+), 91 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists 2 small problems a reviewer found when testing index.html in a real browser, each with its cause and the exact fix. Fix both, keeping everything that already works (every other fix from the previous rounds is verified correct — do not undo or reorganize anything). Item 1 is in the snow-cap functions (capPath / capsPath) in section 7 of src/app.js; item 2 is one CSS rule in src/app.css. Do not edit scripts/ or data/embed/. FEEDBACK.md already contains every fact you need, so do NOT run python, node, cat or grep shell commands to inspect data or code — the only shell command you may run is python3 build.py --check. Use your file read, search and edit tools. Whenever you change a function's parameters, find every call of that function and update each one. Do not stop until both items are fixed and python3 build.py --check succeeds.
    

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

    Reviewer feedback, round 3: 2 items (everything else from the last round is verified correct; keep it)

    1. At high snow depth the snow caps become large white wedges floating in the sky

    Tested: Mores Creek Summit, WY2011, Apr 29 (depth 88 in, so capHeight = 45).

    • What goes wrong for the left peak (200,95): The snowline is at y = 140.

    • capPath walks right from the summit until ridgeYAt(x) >= 140.

    • The saddle at (260,130) never gets that low, so the walk continues past the 330 peak and stops near x = 500.

    • The path then closes with a straight line from x = 500 back to the summit.

    The result is a white triangle across the sky over the whole range. At depth 33 in the caps look correct.

    • Fix, in capPath / capsPath: Each cap must stay on its own peak. Give capPath the x-limits of that peak's two neighbouring ridge vertices:

    SummitLeft vertexRight vertex
    (200,95)(130,150)the saddle (260,130)
    (330,70)the saddle (260,130)(400,140)
    • Clamp capHeight per peak so the snowline is never lower (larger y) than the higher of those two vertices, minus 4. For example, the snowline is at most y = 126 for both peaks, because the saddle at y = 130 is the higher vertex in both cases.

    • Stop each slope search at that vertex's x.

    • Close the path back up along the ridge itself: add the ridge points (x, ridgeYAt(x)) every 4 px from the right snowline x back to the summit. Do not use a straight line.

    The cap is then exactly the part of the mountain above the snowline, with a jagged lower edge, and never covers sky.

    2. Phone: the "Day of year" readout wraps onto two lines

    At 390px, "164 / 365" breaks into "164 /" and "365". Fix: in the ≤ 640px media rule, add white-space: nowrap to .scrub-stat and reduce its font-size to .95rem, so all three readout values stay on one line.

What the agent was asked

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

Read the specification

Snowpack Watch — SNOTEL snow-water equivalent tracker

Category: Scientific visualization Who it is for: Water supply forecasting, forestry and ecology students, anyone who skis

What it should do

Track this winter's snowpack at Idaho SNOTEL stations against the long-term median and the record years — the numbers that decide Idaho's summer water supply.

Data

Real NRCS SNOTEL daily snow-water-equivalent (SWE) for 5–8 Idaho stations, downloaded at build time from the NRCS AWDB Report Generator CSV (wcc.sc.egov.usda.gov/reportGenerator) or the AWDB REST API. Suggested: Moscow Mountain (989), Mica Creek (623), Lolo Pass (588), Brundage Reservoir (370), Galena Summit (490), Mores Creek Summit (637). Several complete water years back to ~2000.

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

  • Station cards with current SWE, % of median and a tiny sparkline

  • Seasonal curve chart (Oct 1 → Sep 30): every past water year faint, median bold, min/max envelope shaded, current year highlighted

  • 'Peak day' and 'melt-out day' estimates per year with a histogram

  • Compare mode: two stations side by side

  • Animated 'snow depth' water-year scrubber

  • Source/citation panel

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

  • Snow-depth scrubber scene is still simple: at high depth a flat white drift covers the lower slopes and the conifers, and the snow caps are angular.
  • Melt-out is the first day after peak with SWE at or below 0.5 in, while the NRCS median melt-out date is compared against it; the definitions may differ by a few days (the threshold is stated in Data & sources).
  • Histogram marker legend text (median / WY2026 date) is small, about 10 px.
  • Moscow Mountain's NRCS median covers a station that only began in WY2001; the method could not be confirmed (stated in the UI).