U of I × MindRouter App Gallery

Palouse Growing Season Planner

Scientific visualization Built with Codex CLI v0.154.0 Critic status: shipped

Palouse Growing Season Planner

Growing degree days and frost risk for Moscow, Idaho

Turn a century of Moscow weather records into planting decisions: accumulate growing degree days for Palouse crops, see last-spring and first-fall frost odds, and compare any year with the normal.

Palouse Growing Season Planner running on a desktop screen, growing degree days and frost risk for moscow, idaho Desktop · click to enlarge
Palouse Growing Season Planner running on a phone, growing degree days and frost risk for moscow, idaho
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Explore 133 years of daily temperatures from the University of Idaho campus weather station in Moscow. Pick a Palouse crop and planting date to see growing degree days build up against the 1991–2020 normal, with the date each growth stage is reached. You can also check the odds of a late spring or early fall freeze, and browse a daily temperature-anomaly calendar and the warmest and coldest seasons on record.

Try this

  1. Choose Sweet corn, set Year to 2015 and Compare year to 2019, and see how much earlier a hot summer reaches silking and harvest.
  2. In the frost chart, switch to ≤ 28 °F and toggle between Full record and 1991–2020 to see how the median freeze dates move and how they compare with NOAA's official ◇ markers.
  3. In the Season leaderboard, open Cold season and click the harshest winter to load that year into the planner and the anomaly calendar.

Highlights ✎ written by the AI critic

  • Growing degree day planner for six Palouse crops plus a custom base/cap, drawn against the 1991–2020 median and 10–90% band, with growth-stage dates
  • Freeze-risk curves at ≤ 32 °F and ≤ 28 °F from the full 1893–2026 record or from 1991–2020, with NOAA's official freeze dates for comparison
  • Calendar heatmap of daily temperature anomalies for any year since 1894
  • Leaderboards of warmest and coldest years, growing seasons and winters; click a row to load that year
  • CSV export of season statistics and of the current GDD curve, with NOAA ACIS and NCEI sources cited in the app

Who it's for

Plant sciences, CALS Extension, community gardeners, farm managers

Features

  • Crop presets with their base temperatures (spring wheat, dry peas, lentils, canola, sweet corn, potatoes) and a custom base/cap
  • Planting-date picker → cumulative GDD curve for the chosen year vs the 30-year normal band, with crop growth-stage markers
  • Frost probability chart: probability of a ≤32°F / ≤28°F freeze after each spring date and before each fall date, from the full record
  • Calendar heatmap of daily temperature anomalies for a selected year
  • Warmest/coldest seasons leaderboard and a CSV export
  • Source/citation panel

Data & sources

  • data.rcc-acis.org — NOAA RCC-ACIS StnData web service (POST
  • NOAA RCC-ACIS StnData: same station, 1950-01-01 to 2026-09-12 subset, and the full record with flags and observation times.
  • data.rcc-acis.org — NOAA RCC-ACIS StnMeta (
  • www.ncei.noaa.gov — NOAA NCEI 1991–2020 U.S. Climate Normals, daily normals for USC00106152 (
  • www.ncei.noaa.gov — NOAA NCEI 1991–2020 U.S. Climate Normals, annual/seasonal normals including freeze-date probabilities (
  • Crop references cited in the app: NDAWN wheat and canola GDD help pages (NDSU), the University of Delaware Extension sweet-corn GDD note, and the ScienceDirect (2025) review of GDD base and upper temperature thresholds.
Full data provenance ✎ written by the AI reviewer

Raw data sources: palouse-growing-season

All files in this directory are byte-for-byte unmodified HTTP response bodies. They were renamed on disk and nothing else was changed (verified with cmp against the scratch downloads). Retrieval date for every file: 2026-09-13 (between 17:51 and 17:53 UTC). Client: curl with User-Agent UIdaho-gallery-data-prep/1.0, requests sent one at a time. No API keys or accounts were used. Every request is anonymous and public.

  • The station is MOSCOW U OF I, ID, a NWS Cooperative Observer (COOP) station on the University of Idaho campus in Moscow, Latah County (FIPS 16057), Idaho climate division ID02. Station ids: GHCN-Daily USC00106152, COOP 106152, NWSLI MUNI1, ACIS uid 351.

  • Location: 46.728 N, -116.9557 W. Elevation 2634 ft per ACIS, 810.8 m per NCEI.

Both ACIS and NCEI returned that exact station name for USC00106152, which confirms the station identity.

Which file should the app use?

NeedFile
Daily Tmax/Tmin for GDD curves, anomaly heatmap, season leaderboards, CSV export, and frost probabilities "from the full record"acis_stndata_USC00106152_maxt_mint_por.json (primary: 1893-11-07 to 2026-09-12)
Same data from 1950 only (the exact request named in SPEC.md)acis_stndata_USC00106152_maxt_mint_1950-2026.json (identical to the 1950+ slice of the POR file)
Observation-time metadata and the proof that no flags besides M existacis_stndata_USC00106152_maxt_mint_por_flags_obstime.json
Station metadata (county, climate division, uid)acis_stnmeta_USC00106152.json
Official NOAA 1991-2020 daily normals (Tmax/Tmin/Tavg normals and std devs, GDD normals), for cross-checking or labelling the normal bandncei_normals_daily_1991-2020_USC00106152.csv
Official NOAA 1991-2020 freeze-date probabilities, growing-season length, and seasonal/annual GDD, for cross-checking the frost chartncei_normals_annualseasonal_1991-2020_USC00106152.csv

The 30-year normal band and the frost probabilities requested in SPEC.md are not in any raw file as ready-made series. The app's scripts/ must compute them from the ACIS daily data. The NCEI normals files are an independent official reference computed by NOAA with a different method, which is explained below. Values computed from the ACIS data will not match them exactly.


1. acis_stndata_USC00106152_maxt_mint_por.json (PRIMARY)

  • Size: 1,260,007 bytes. SHA-256 9de98c5c20b7153e167ace7cb1ef698d52ab950a38cf8a6a5c8bd5616ab6f477

  • Publisher: NOAA Regional Climate Centers, Applied Climate Information System (RCC-ACIS). The response header x-rcc-host: nrcc 0 shows it was served by the Northeast RCC. The underlying observations come from the NWS Cooperative Observer Program via GHCN-Daily and other ACIS sources.

  • Request: POST https://data.rcc-acis.org/StnData with header Content-Type: application/json and body:

  {"sid":"USC00106152","sdate":"por","edate":"2026-09-12","elems":"maxt,mint","meta":"name,state,sids,ll,elev,valid_daterange"}
  • "por" means the start of the station's period of record. Contents: daily maximum and minimum air temperature for every calendar day from 1893-11-07 through 2026-09-12.

  • Record count: 48,522 daily rows. Dates are consecutive with no skipped dates; days without observations appear as rows of "M".

  • Structure:

  {
    "meta": {
      "name": "MOSCOW U OF I", "state": "ID",
      "sids": ["106152 2", "USC00106152 6", "MUNI1 7"],   // "<id> <type>": 2=COOP, 6=GHCN-Daily, 7=NWSLI
      "ll": [-116.9557, 46.728],                           // [longitude, latitude], decimal degrees
      "elev": 2634.0,                                      // feet
      "valid_daterange": [["1893-11-07","2026-09-07"],     // maxt
                          ["1893-11-07","2026-09-07"]]     // mint
    },
    "data": [ ["1893-11-07","50","45"], ["1893-11-08","50","M"], ... ]
  }
  • data[i][0] is the date string YYYY-MM-DD, local observer day.

  • data[i][1] is maxt, the daily maximum temperature in °F, as a string.

  • data[i][2] is mint, the daily minimum temperature in °F, as a string.

  • Every non-missing value is a whole-degree integer string such as "-11" or "83", verified for all values. Parse with parseInt.

  • Missing and flag codes (ACIS Web Services documentation, https://www.rcc-acis.org/docs_webservices.html, "Results" section):

    • "M" = missing. It is the only non-numeric value in this file.

    • "T" = trace, and a trailing "A" = accumulated value. ACIS documents both, but only for precipitation, snowfall and snow depth. Neither occurs in maxt/mint here: 0 occurrences, verified in file 3 with flags requested.

    • The app should treat "M" as null. Do not treat it as 0 and do not interpolate silently.

  • Missing-data summary (validation statistics computed by data-prep from this raw file with Python's json module; not a separate data product):

    • maxt: 690 of 48,522 days are "M". mint: 598 days are "M". 864 days have at least one missing; 424 days have both missing.

    • Only 37 of 134 calendar years have zero missing values.

    • Long gaps (both elements, 5 or more consecutive days):

      • 1899-06-25 to 1899-07-03 (9 days)

      • 1926-07-17 to 1927-03-18 (245 days), so fall 1926 and early spring 1927 are unusable

      • 1932-02-17 to 1932-02-22 (6 days)

      • 1950-05-25 to 1950-06-03 (10 days)

      • 2008-10-01 to 2008-10-31 (31 days), so the fall-2008 first-freeze date is unknowable

      • 2009-06-01 to 2009-06-30 (30 days)

      • 2026-09-08 to 2026-09-12 (5 days, not yet reported; see below)

    • mint only: 1908-12-01 to 1908-12-30 (30 days).

    • Scattered maxt-only gaps (mint complete, so frost analysis is unaffected but GDD for those years is affected): 1982 has 75 maxt missing, spread through every month including 58 Apr-Oct days; 1984 has 18 maxt missing, May-Oct.

    • Years with 5 or more Apr-Oct days missing either element: 1897 (44), 1899 (11), 1900 (13), 1918 (9), 1926 (107), 1950 (12), 1979 (5), 1982 (58), 1983 (5), 1984 (18), 2008 (32), 2009 (30), 2026 (5).

    • Sparse early record: 1896 has 20 days with maxt or mint missing (16 maxt, 18 mint), and 1897 has 66 days (50 maxt, 54 mint), scattered through the year. 1899 has 18 days and 1900 has 15. (Corrected by the verifier: an earlier draft said "16-54 missing days each", mixing per-element counts.)

    • 1991-2020 window: 97 maxt and 73 mint missing days. 1996-2025 window: 86 maxt and 74 mint missing days.

  • End of record: valid_daterange ends 2026-09-07. The last non-missing row is ["2026-09-07","68","44"]. Rows 2026-09-08 to 2026-09-12 are "M" because the observer's reports had not yet reached ACIS on the retrieval date; they are not a station failure. 2026 is a partial year, and its most recent values are preliminary (see the terms below).

  • Sanity checks (data-prep validation): no row has maxt < mint. POR extremes are a maxt of 109 °F on 1961-08-04 and a mint of -42 °F on 1968-12-30. The largest mint in the POR is 74 °F, before 1950; from 1950 on it is 72 °F. No further QC beyond ACIS/NCEI's own processing was applied.

  • Observation-time caveat: this is a COOP station, so each date's max and min cover the 24 hours ending at the observer's reading time, not midnight to midnight. See file 3 for obs-time details. This is standard for COOP data and fine for GDD and frost statistics.

2. acis_stndata_USC00106152_maxt_mint_1950-2026.json

  • Size: 727,963 bytes. SHA-256 e4f734f41ec6130e3dae503c5235a401bf90df12546c9f0fccf355dc8f352bff

  • Publisher: NOAA RCC-ACIS, as in file 1.

  • Request: POST https://data.rcc-acis.org/StnData with header Content-Type: application/json and body (the exact request named in the build guidance):

  {"sid":"USC00106152","sdate":"1950-01-01","edate":"2026-09-12","elems":"maxt,mint","meta":"name,state,sids,ll,elev,valid_daterange"}
  • Contents: daily maxt/mint in °F, 1950-01-01 through 2026-09-12, 28,014 consecutive daily rows.

    • maxt: 257 "M". mint: 123 "M". 303 days have at least one missing.

    • Long gaps (10 or more days): 1950-05-25 (10 days), 2008-10 (31 days), 2009-06 (30 days).

  • Format: identical to file 1 (same meta object, same data row layout, same codes).

  • Relationship to file 1: verified that the meta object is identical and that the data array equals file 1's rows with date >= 1950-01-01, element for element. Use file 1 unless only 1950+ is wanted.

3. acis_stndata_USC00106152_maxt_mint_por_flags_obstime.json

  • Size: 2,093,992 bytes. SHA-256 64992e8be8831cb937be23df52b52e1d454a9be439816d7149b1cebe88d1b5b3

  • Publisher: NOAA RCC-ACIS, as in file 1.

  • Request: POST https://data.rcc-acis.org/StnData with header Content-Type: application/json and body:

  {"sid":"USC00106152","sdate":"por","edate":"2026-09-12","elems":[{"name":"maxt","add":"f,t"},{"name":"mint","add":"f,t"}],"meta":"name,state,sids,ll,elev,valid_daterange"}
  • Contents: the same 48,522 days (1893-11-07 to 2026-09-12) with each value expanded to [value, flag, obs_time]. Values are verified identical to file 1 for every row.

  • Structure: the same meta as file 1, and data[i] = ["YYYY-MM-DD", [maxt, maxt_flag, maxt_obs_hour], [mint, mint_flag, mint_obs_hour]], for example ["1893-11-08", ["50"," ",0], ["M","M",0]].

    • flag (ACIS add code f): " " (one space) = normal value, "M" = missing. The only flags present are " " (47,832 maxt / 47,924 mint) and "M" (690 maxt / 598 mint). No T, A or other flags occur.

    • obs_hour (ACIS add code t, documented as "Time of observation (in Local Standard Time)") is an integer hour. Values present:

obs_hourmaxt daysmint daysWhere it occurs
17 (5 pm LST)28,40628,415the usual value since 1948-08-01
019,65319,758every reported day from 1893-11 to 1948-07 (except the 18 span below). From 1948-08 on, 0 appears on only 24 rows (22 maxt, 13 mint values), and every one of those values is "M": the 1950-05-25..06-03 gap, 1963-04-16..18, 1968-04-19, 1974-06-19, 1979-07-21..23, 1981-07-08, 1981-12-18, 2004-07-04/13/27 and 2022-11-29
-1311311only on "M" rows: the 1926-27 gap, 2008-10, 2009-06, and 2026-09-08..12; no report
18114none1908-12-01 to 1909-03-24
15 (3 pm LST)3838from 2026-08-01 onward; recent change in reading time
  • The meaning of 0 in the early record is not documented by ACIS. It most likely means the reading time was unrecorded rather than an actual midnight observation. This is an interpretation, not a documented fact. Use: optional. File 1 is sufficient for the app. This file documents the flags and the observation-time history.

4. acis_stnmeta_USC00106152.json

  • Size: 277 bytes. SHA-256 277e14c0ff661b4be0020437e9c71ffbc2ab1662b05006fabb6c0256e0f7be0f

  • Publisher: NOAA RCC-ACIS.

  • Request: POST https://data.rcc-acis.org/StnMeta with header Content-Type: application/json and body:

  {"sids":"USC00106152","meta":"name,state,sids,ll,elev,uid,county,climdiv,valid_daterange","elems":"maxt,mint"}
  • Contents: one station record at meta[0]:

    • name "MOSCOW U OF I", state "ID"

    • sids ["106152 2", "USC00106152 6", "MUNI1 7"]

    • ll [-116.9557, 46.728] (lon, lat)

    • elev 2634.0 ft

    • uid 351, county "16057" (Latah County FIPS), climdiv "ID02"

    • valid_daterange [["1893-11-07","2026-09-07"], ["1893-11-07","2026-09-07"]] (maxt, mint)

5. ncei_normals_daily_1991-2020_USC00106152.csv

  • Size: 665,110 bytes. SHA-256 061bd8fe344ea51aeabdfb26ea9d6188a9adf9bcc5157a8c477470ee842dd490

  • Publisher: NOAA National Centers for Environmental Information (NCEI), 1991-2020 U.S. Climate Normals (daily normals, by-station file).

  • Request: GET https://www.ncei.noaa.gov/data/normals-daily/1991-2020/access/USC00106152.csv

    • The normals-dailyv2 path returned HTTP 404. The normals-daily path is the live one.

  • Documentation:

    • https://www.ncei.noaa.gov/data/normals-daily/1991-2020/doc/Normals_DLY_Documentation_1991-2020.pdf

    • https://www.ncei.noaa.gov/data/normals-daily/1991-2020/doc/Readme_By-Variable_By-Station_Normals_Files.txt

  • Contents: 1 header row and 366 data rows, one per calendar day from 01-01 to 12-31 including 02-29. This is climatology for the 1991-2020 period, not individual years. There are 293 columns.

  • Format notes:

    • CSV with double-quoted header names. Numeric fields are space-padded (for example " 35.8"), so trim before parsing.

    • Leading columns: STATION ("USC00106152"), DATE (MM-DD), LATITUDE (46.7281), LONGITUDE (-116.9558), ELEVATION (810.8, metres), NAME ("MOSCOW U OF I, ID US"), month, day, hour (99 = not applicable).

    • After those, every variable occupies 4 columns: <VAR>, meas_flag_<VAR>, comp_flag_<VAR>, years_<VAR> (number of years used).

    • Temperature variables useful to this app:

      • DLY-TMAX-NORMAL, DLY-TMIN-NORMAL, DLY-TAVG-NORMAL: °F to 0.1

      • DLY-TMAX-STDDEV, DLY-TMIN-STDDEV, DLY-TAVG-STDDEV: °F

      • DLY-DUTR-NORMAL: diurnal range, °F

      • DLY-GRDD-BASE40|45|50|55|57|60|65|70|72: daily growing degree day normals, base in °F

      • DLY-GRDD-TB4886, DLY-GRDD-TB5086: "temperature bounded" GDD, base 48 or 50 °F with an 86 °F upper limit

      • DLY-HTDD-*, DLY-CLDD-*: heating and cooling degree days

    • The remaining columns (DLY-PRCP-*, MTD-/YTD-PRCP-NORMAL, DLY-SNOW-*, DLY-SNWD-*) are precipitation and snow, not needed here.

    • Missing value: -9999 (appears as -9999.0). None of the temperature or GDD columns listed above contain -9999.

    • Measurement flags (NCEI Table 1): blank = normal, M = missing, V = year-round frost-freeze risk ("too cold to compute"), W = not used, X = nonzero value rounded to zero, Y = insufficient values, Z = computed value logically inconsistent with other values. In the temperature/GDD columns only blank and X occur (for example 57 X days in DLY-GRDD-BASE50, which display 0.0).

    • Completeness flags: S = Standard (at least 24 years, gaps filled from neighbours), R = Representative, P = Provisional, E = Estimated. All temperature columns here are S. years_* is 27-30.

  • Method caveat (per NCEI documentation): daily temperature normals are smoothed and constrained to homogenized monthly normals. Degree-day statistics use 15-day sampling windows, and gaps are filled from neighbouring stations. The values will differ slightly from a simple 30-year average of the ACIS daily data.

  • Validation (data-prep): the 365 daily DLY-GRDD-BASE50 values (excluding 02-29) sum to 1647.6, consistent with ANN-GRDD-BASE50 = 1648.3 in file 6. The daily DLY-GRDD-TB5086 values sum to 2422.3, which does not match the seasonal/annual TB values in file 6 (see the caveat there).

6. ncei_normals_annualseasonal_1991-2020_USC00106152.csv

  • Size: 76,959 bytes. SHA-256 23003b8f721fdddebf54bc636947b52d136aeb874bd4df1221369eb21f12b95b

  • Publisher: NOAA NCEI, 1991-2020 U.S. Climate Normals (annual/seasonal normals, by-station file).

  • Request: GET https://www.ncei.noaa.gov/data/normals-annualseasonal/1991-2020/access/USC00106152.csv

  • Documentation:

    • https://www.ncei.noaa.gov/data/normals-annualseasonal/1991-2020/doc/Normals_ANN_Documentation_1991-2020.pdf

    • The same Readme as file 5.

  • Contents: 1 header row and 1 data row with 2,140 columns. The leading columns are the same as file 5 but without DATE; month, day and hour are all 99. Every variable is again a 4-column group (value, meas_flag, comp_flag, years).

  • Variables useful to this app (prefixes ANN, MAM, JJA, SON, DJF = annual, spring, summer, autumn, winter):

    • *-TAVG-NORMAL (°F). *-TMIN-AVGNDS-LSTHnnn, *-TMAX-AVGNDS-LSTHnnn, *-GRTHnnn: average number of days with Tmin or Tmax at or below / at or above nnn °F.

    • *-GRDD-BASEnn, *-GRDD-TB4886, *-GRDD-TB5086: seasonal/annual GDD.

    • Freeze probabilities:

      • ANN-TMIN-PRBLST-TnnFPmm: last spring occurrence of Tmin at or below nn °F. The value is a date string "MM/DD".

      • ANN-TMIN-PRBFST-TnnFPmm: first fall occurrence. The value is a date string "MM/DD".

      • ANN-TMIN-PRBGSL-TnnFPmm: growing-season length in days.

      • ANN-TMIN-PRBOCC-LSTHnnn: percent probability of at least one occurrence per year.

      • Thresholds nn are 16/20/24/28/32/36 °F. Probabilities mm are 10-90 in steps of 10.

    • How to read the FP columns: NCEI's doc text for PRBFST reads "mm% probability date of first nn F occurrence or earlier". For PRBLST it reads "mm% probability date of last nn F occurrence or earlier", which is ambiguous.

      • The values show the intended reading. For 28 °F, PRBLST FP10 = 06/08 and FP90 = 04/27. So FPmm for PRBLST is the date on or after which the last spring freeze occurs with mm% probability: a 10% chance of a 28 °F freeze on or after June 8.

      • For PRBFST, FP10 = 09/06 means a 10% chance the first fall 28 °F freeze occurs on or before Sept 6.

      • This reading is inferred from the value ordering (data-prep interpretation).

    • Example values:

      • PRBLST T28 FP50 = 05/14, PRBFST T28 FP50 = 09/23, PRBGSL T32 FP50 = 85 days

      • ANN-TMIN-AVGNDS-LSTH032 = 138.7 days/yr

      • ANN-GRDD-BASE50 = 1648.3, ANN-GRDD-BASE40 = 3553.4

    • Important: all PRBLST-T32*, PRBFST-T32*, PRBLST-T36* and PRBFST-T36* values are -9999.0 with measurement flag V ("year-round risk of frost-freeze; too cold to compute"). NOAA publishes no official 32 °F freeze dates for this station. The app's ≤32 °F freeze curve must come from the ACIS daily data, file 1. The 28 °F dates are available for comparison. years_* for the freeze statistics is 28.

  • Missing value: -9999.0. Flags are as described in file 5.

  • Caveat (data-prep observation, not documented by NCEI): the seasonal and annual GRDD-TB4886 and GRDD-TB5086 values look inconsistent. DJF-GRDD-TB5086 = 2655.8 is impossible as winter growing degree days above 50 °F, and ANN-GRDD-TB5086 = 5421.4 far exceeds both ANN-GRDD-BASE50 (1648.3) and the sum of daily DLY-GRDD-TB5086 (2422.3). Do not display the seasonal/annual TB values. The plain BASEnn GDD columns are internally consistent.


Terms of use and licence

  • RCC-ACIS (files 1-4): https://www.rcc-acis.org/disclaimer.html ("Terms of Use and Disclaimer").

    • The data are provided by the NOAA Regional Climate Centers. "Near-real-time data are preliminary in nature and sometimes contain errors... it is possible that some data will change after receipt of written records and final quality control steps. Users are... forewarned... [to] use preliminary data and information at their own risk." Use of the services implies agreement.

    • No redistribution restriction is stated. The underlying observations are U.S. Government (NOAA/NWS COOP) data, generally treated as public domain.

    • The app should state that recent (2026) values are preliminary.

  • NCEI Climate Normals (files 5-6): produced by NOAA NCEI, a U.S. Government work. Public domain in the U.S., with no restrictions on use; attribution requested.

    • Product page: https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals

    • A DOI for the 1991-2020 daily normals was not captured: NCEI's metadata landing pages (/access/metadata/landing-page/bin/iso?id=gov.noaa.ncdc:C01621 to C01624) returned HTTP 503 on the retrieval date. Cite the product page URL instead.

Suggested one-line citations (for the app's "Data & sources" panel)

  • Daily temperatures (files 1-3):

Daily max/min temperature, MOSCOW U OF I, ID (GHCN USC00106152 / COOP 106152), 1893–2026. NOAA Regional Climate Centers, Applied Climate Information System (ACIS) StnData web service, https://data.rcc-acis.org/ (retrieved 2026-09-13). Recent data are preliminary.
  • Station metadata (file 4):

Station metadata for MOSCOW U OF I, ID. NOAA RCC-ACIS StnMeta web service, https://data.rcc-acis.org/ (retrieved 2026-09-13).
  • Daily normals (file 5):

NOAA NCEI, 1991–2020 U.S. Climate Normals: Daily Normals, station USC00106152 (Moscow U of I, ID), https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals (retrieved 2026-09-13).
  • Annual/seasonal normals and freeze probabilities (file 6):

NOAA NCEI, 1991–2020 U.S. Climate Normals: Annual/Seasonal Normals (freeze probabilities, growing degree days), station USC00106152, https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals (retrieved 2026-09-13).

Not obtained / notes

  • https://www.ncei.noaa.gov/data/normals-dailyv2/1991-2020/access/USC00106152.csv returned HTTP 404 (the path does not exist). The working path normals-daily was used instead.

  • NCEI metadata landing pages (for a dataset DOI) returned HTTP 503. No DOI is recorded.

  • The ACIS request succeeded on the first try with the GHCN-style sid USC00106152. The COOP-style sid "106152 2" was not needed. ACIS lists it as an equivalent id in meta.sids.

  • Missing-value detection: use the value "M" (or flag "M") to detect missing data. Do not rely on obs_hour -1: some missing values carry obs_hour 0 instead (see the table in file 3).

  • Independent verification (2026-09-13): every file was re-parsed and the counts, gaps, extremes, flags, obs hours and NCEI values quoted above were recomputed. Live re-fetches matched the saved files:

    • ACIS StnData subsets 1893-11-07..20, 1961-07-30..08-08, 1968-12-25..1969-01-03, 2008-09-28..11-03 (with flags/obs time) and 2026-07-28..09-12 (with flags/obs time) matched value for value.

    • The StnMeta response and both NCEI CSVs were byte-identical (same SHA-256).

    • The NCEI flag tables quoted above match Readme_By-Variable_By-Station_Normals_Files.txt and Normals_DLY_Documentation_1991-2020.pdf.

    • The ACIS "M"/"T"/"A" and t ("Time of observation (in Local Standard Time)") definitions match https://www.rcc-acis.org/ACISWSdoc.html, the documentation fragment loaded by docs_webservices.html.

    • The PRBLST/PRBFST wording quoted for file 6 matches Normals_ANN_Documentation_1991-2020.pdf.

    • All cited documentation and terms URLs returned HTTP 200.

  • No derived or aggregated data files are included. All statistics quoted above (missing counts, gaps, extremes, sums) are validation notes computed by data-prep from the raw files with Python's standard json and csv modules.

How it was built

Written by Codex CLI 0.154.0 (github.com) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

11harness rounds
157.6minutes of harness work (2.6 h)
2,158lines of code
766KB, single-file app

Rounds by kind

build1
feedback5
continue3
critic2

Lines of code per file

body html198
app css325
app js1,244
scripts391

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

  • Calendar tooltip 2025 Jul 15: Tmax 89 F, Tmin 54 F, anomaly +5.4 F. Raw ACIS row [2025-07-15, 89, 54]; independent 7-day-smoothed 1991-2020 normal 66.09 F gives +5.4. MATCH
  • Frost KPIs and markers (<=32 F, full record): median last spring freeze May 19, first fall freeze Sep 22, frost-free 131 days, 90%/10% at Apr 27/Jun 12 (spring) and Sep 6/Oct 18 (fall), 133 spring / 131 fall years. Independent Python recompute from the raw file gives the same. MATCH (fall eligibility was fixed in round 10)
  • 1991-2020 <=28 F toggle: median May 4 / Sep 28, frost-free 149 days. Independent recompute May 4 / Sep 28 / 149. MATCH (the Data & sources text previously said May 5 / Sep 29; fixed)
  • Leaderboard warm season (Apr 1-Oct 31): 1958 60.4 F, 2015 59.8, 1936 59.7. Winters (Nov 1-Mar 31): mildest 1933-34 41.3 F, harshest 1916-17 27.7 F. Independent recompute is identical. MISMATCH before the critic rounds: the app used Apr 2-Sep 30 and same-calendar-year Jan-Mar+Oct-Dec, so it showed '1934-35 42.5 F' and '1985-86 28.9 F'. Fixed in round 10
  • Leaderboard annual GDD 32-86 (clipped formula): 1934 7,231; coldest 1916 5,386. Independent recompute 7,230.5 / 5,386. MATCH
Show all 10 spot checks
  • Normal-season GDD KPI for spring wheat planted Apr 20, through Sep 30 (1991-2020 median): 4,490. Independent recompute 4,490. MATCH
  • Sweet corn planted May 20: normal reaches silk 1,075 on Aug 6 and harvest 1,425 on Aug 26; 2025 reaches them Jul 29 and Aug 17. Independent recompute is identical. MATCH
  • 2025 anomaly summary: 217 warmer, 131 cooler, 17 even, mean +2.0 F. Independent recompute 217/131/17, +2.02. MATCH
  • 2024 Feb 28 calendar day now shows 39/27 F, which matches raw 2024-02-28. The build script used to overwrite it with Feb 29's 49/30; fixed in round 10
  • Crop stage citations checked against the source pages. NDAWN wheat table: 538 / 1,181 / 1,567 / 1,739 / 1,825, now used verbatim (previously mislabeled 'Jointing 970', 'Soft dough 1,825', 'Maturity 2,150'). NDAWN canola range starts 257 / 728 / 933 / 1,398 / 1,875, now used (previously range ends). The UDel page does not contain the sweet-corn 1,075/1,425 values, which are now marked approximate and no longer credited to UDel
Build notes ✎ written by the AI reviewer

The data-prep workflow reported READY (ready-after-repair). I re-validated every raw file before round 1: 48,522 daily rows from 1893 to 2026, NCEI files parse, and SOURCES.md was complete. No data changes of my own were needed.

8 harness rounds, all through run_round.sh, about 139 harness minutes. Codex wrote all application code; I only wrote FEEDBACK.md and my own test and probe scripts, which live in the runs directory. The tamper guard never fired.

  • Round history: Round 1 (build): full app with no errors, but several things were broken or wrong. The leaderboard rendered '[object HTMLHeadingElement]'. The calendar was an average, not per-year, and its cells were stacked in one block. The frost curves were truncated (spring stopped at May 31, fall started Sep 1). The GDD normal was accumulated from Jan 1 and drawn as a line, not a band. Crop stage values were implausible (wheat 'matured' in May). The page overflowed by 168 px on phone.

  • Round 2 (feedback, 13 items): fixed the frost math, KPIs and phone overflow. It claimed the leaderboard was fixed but it was not. It introduced new bugs: a black band fill, a blank month-only date input, an empty calendar showing a −45.9 °F anomaly, and a fabricated citation ('Rochdi et al. 2025, Sci. Total. Environ.').

  • Round 3 (feedback): spoiled. The model's output degenerated into garbage after 88 s with no code changes.

  • Round 4 (continue re-run of round 3): fixed the leaderboard, calendar, band, planting selects, NDAWN wheat stages and the citation.

  • Round 5 (feedback, 12 items): fixed the ahead/behind sign, crop stage timing, custom band, NOAA markers, leaderboard data-gap handling and winter labels.

  • Round 6 (feedback, 7 items): fixed several polish items and added a 1991–2020 frost toggle. That toggle drew its curves upside down, and the ahead/behind sign regressed.

  • Round 7 (feedback, 4 items): stalled with no file changes and a silent log for over 13 minutes; I killed it with kill_round.sh.

  • Round 8 (continue re-run of round 7): fixed all four items. Its log ended with degenerate '!!!!' output, but no junk reached the source files.

How I checked each round: verify_app.py, a Playwright feature test of every SPEC feature (crop presets, custom base/cap, planting date, year and compare year, hover tooltips, frost threshold and period toggles, calendar year and tooltips, leaderboard tabs and row clicks, both CSV downloads, 390 px overflow), and targeted probes. The probes covered the per-crop stage tables, 2026 handling, touch taps with event tracing, and bar widths. I also independently recomputed freeze probabilities and seasonal GDD targets from the raw file, which exposed three wrong frost calculations (the round-1 truncation, the round-2 readouts and the round-6 inverted toggle) and let me give the harness numbers to check against.

The harness could not run a browser inside its sandbox (headless Chrome aborted) and tested with Node DOM stubs instead. That is why several fixes it reported as verified were actually broken in a real browser, until I passed on concrete in-browser evidence.

Remaining minor issues are listed under features_missing_or_broken. MindRouter had no outages; network_error_lines was at most 2 per round, with no effect on progress.

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 18.7 min · 11 files changed, 2417 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 — the primary file is data/raw/acis_stndata_USC00106152_maxt_mint_por.json, daily max/min °F with "M" for missing; 2026 is a partial, preliminary year) 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.

    4 continue 31.3 min · 7 files changed, 410 insertions(+), 267 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×900 and 390×844)

    Real progress: the frost curves are now correct (they match my own recomputation from the raw file: ≤ 32 °F median last spring freeze May 19, first fall freeze Sep 22), the 10/50/90 % markers and chips are right, there is no phone overflow, KPI tiles appear, the GDD normal now starts from the planting date, the stage table is a good addition, and the seasons CSV has meaningful freeze columns. No console errors. The items below are still broken or wrong — most important first. Please test each fix by actually reading the rendered DOM/values, not only node --check.

    1. Season leaderboard is STILL broken (not fixed in round 2). The card still shows the literal text [object HTMLHeadingElement],[object HTMLTableElement] twice, for every tab. The two column containers are created with an array of elements passed where HTML text is expected, so the array gets stringified. Expected: the heading and table elements actually appended as children, so two real top-10 tables (Warmest / Coldest) with medals, values and mini bars render; clicking tabs visibly re-ranks; clicking a row loads that year.

    2. Anomaly calendar shows nothing, and its data/controls are broken.

      • The calendar area is an empty strip: #cal-grid contains 12 month blocks but has height 0, so no day cells are visible at 1440 px or 390 px. Only a short stray line "———" shows under the legend.

      • The Year dropdown contains only two options: "2026 (preliminary)" and "1893". Selecting any other year is impossible. It must list every year in the daily data, newest first, and default to 2025.

      • The dropdown shows 2026 while the summary chip says "2025: 0 days warmer, 365 days cooler than normal; mean anomaly −45.9 °F". A −45.9 °F mean anomaly is impossible (real yearly anomalies are within a few °F), so the anomaly is being computed against the wrong thing (e.g. a missing/zero or misaligned normal). Expected: anomaly = ((Tmax+Tmin)/2 for that date) − (1991–2020 smoothed normal mean temperature for that calendar day), values mostly within ±15 °F, with a sensible split such as "2025: 205 days warmer, 158 cooler; mean anomaly +1.6 °F".

      • Legend layout: at 1440 px it reads "−15 °F [bar] 0 +15 °F" with "0" at the right end of the bar; at 390 px "+15 °F" wraps onto its own line. Expected one bar with −15 at the left, 0 centred under the middle, +15 at the right. Expected overall: 12 visible mini month calendars (4 across desktop, 3 across tablet, 2 across phone), each day a coloured square in its correct weekday column, hatched squares for missing days, hover/tap tooltip with date, Tmax, Tmin, mean, normal and anomaly; selecting a year, or clicking a leaderboard row, redraws it.

    3. The GDD normal band is filled solid black. The 10–90 % band polygon renders with fill rgb(0,0,0) at full opacity, so a thick black ribbon covers the median line and most of the compare-year line (desktop and phone). The legend swatch says "Normal band (10–90%)" in light grey. Expected: a light, semi-transparent neutral or warm-grey band (e.g. steel/silver at ~15–20 % opacity) behind the lines, with the median as a clearly visible dashed dark line, the selected year as the bold gold line and the compare year in teal on top.

    4. Planting date picker is blank and cannot pick a day. It is now an <input type="month"> showing "--------- ----" with an empty value, so the user can't see or set Apr 20 (the note below says "if you plant Apr 20"). A month input also has no day. Expected: a visible planting-date control that shows the current date and lets the user choose month and day — e.g. a normal date input whose year is always kept equal to the selected Year, or two selects (Month, Day) side by side — changing it redraws the curve, stage table, readout and freeze-odds note.

    5. Stage thresholds for spring wheat, peas, lentils and potatoes are still implausible; the wheat values are wrongly credited to NDAWN. The stage table makes it obvious: spring wheat "Maturity 750 · reached May 28 (normal May 27)"; peas (650) mature in early June; lentils (550) in May; potatoes (900) in June. In the Palouse, spring wheat heads around late June and matures in late July; peas and lentils flower in mid/late June and mature late July–early August; potatoes planted in early May initiate tubers in mid/late June and mature in September. The Data & sources panel says "Spring wheat (NDAWN wheat table): tillering ≈150, jointing ≈400, heading ≈600, maturity ≈750" — NDAWN publishes no such numbers. NDAWN's rule is: ≈180 °F-GDD (base 32) from planting to emergence, +72.5 to Haun stage 1, then +143 per Haun stage, which gives Haun 3 (tillering) ≈ 540, Haun 6 (jointing) ≈ 970, Haun 10 (heading) ≈ 1,540, Haun 12 ≈ 1,825; physiological maturity is later (≈ 2,000+ ; not published by NDAWN). To sanity-check, here are the 1991–2020 median cumulative GDD at typical Palouse stage dates, computed from the raw station file:

      • Spring wheat, base 32, planted Apr 20: Jun 1 ≈ 900 · Jun 25 ≈ 1,520 · Jul 10 ≈ 2,030 · Jul 25 ≈ 2,520

      • Dry peas, base 40, planted Apr 15: Jun 15 ≈ 840 · Jul 1 ≈ 1,160 · Jul 20 ≈ 1,660 · Aug 1 ≈ 1,960

      • Lentils, base 34, planted Apr 20: Jun 20 ≈ 1,270 · Jul 5 ≈ 1,700 · Jul 25 ≈ 2,330 · Aug 5 ≈ 2,700

      • Potatoes, base 45, planted May 1: Jun 15 ≈ 480 · Jul 1 ≈ 710 · Aug 1 ≈ 1,370 · Sep 10 ≈ 2,140

      • Sweet corn, base 50 cap 86, planted May 15: Aug 15 ≈ 1,140 · Sep 1 ≈ 1,390 · Sep 15 ≈ 1,530 Expected: wheat stages from the NDAWN rule above (tillering, jointing, heading, soft dough, maturity ≈ 2,100–2,300); pea, lentil and potato stages chosen so they land at those typical dates, labelled in the UI as "approximate, calibrated to typical Palouse timing" (not as published figures). Sweet corn: harvest (milk stage) should come before "maturity", not after — use e.g. silk ≈ 1,000–1,150 and harvest ≈ 1,350–1,500 and drop the separate "Maturity 1450 / Harvest 1600" ordering. Canola's last marker should read "Ripe (> 1,874)". With realistic values the stage lines will spread up the chart — keep the label de-overlap (currently "Maturity · 750" and "Heading · 600" still overlap at the right edge on desktop and are unreadable on phone).

    6. Citation invented. Data & sources says "Rochdi et al. 2025, Sci. Total. Environ." for https://www.sciencedirect.com/science/article/pii/S037837742500469X. That author name and journal were not given anywhere and appear made up (the 0378-3774 ISSN in that URL is the journal Agricultural Water Management). Expected: cite it by its title — "Base and upper temperature thresholds to support the calculation of growing degree days aiming at their use with the FAO56rev crop coefficients curve: A review" (ScienceDirect, 2025) — with no invented authors. Also fix the wheat attribution (item 5). Never present a number as coming from a source unless it is in that source.

    7. Frost legend prints "null". After the two legend entries (Spring / Fall) the word null appears (desktop and phone) when the ≤ 32 °F threshold is selected. Expected: nothing there for 32 °F, and the "NOAA 1991–2020 ticks" legend entry only for 28 °F.

    8. Dates shown as "05-19" / "09-22". KPI tiles ("Last spring freeze (50/50) 05-19", "First fall freeze 09-22") and frost chips use MM-DD codes, while the chart labels say "May 20". Expected: human dates everywhere in the UI ("May 19", "Sep 22"); keep MM-DD only in CSV. Also the chip says May 19 but the chart's 50 % label says May 20 — make them agree.

    9. GDD readout wording. "2025 reaches 5,852 GDD by Dec 1 (from Apr 20) — +547 vs normal" compares totals on Dec 1, after the season. Expected: compare on a meaningful date — the last stage's normal date, or a user-visible "as of" date (the hovered date, or Sep 30), e.g. "By Aug 1: 2,950 GDD, +4 % vs normal (≈ 3 days ahead)". For 2026, use its last observed date (Sep 7) and say it is preliminary.

    10. Re-check everything on phone (390 px) after the fixes: the calendar must show all 12 months without the page scrolling sideways, the leaderboard tables must fit (stack the two tables), the planting control must be usable, and stage labels readable.

    8 continue 19.0 min · 3 files changed, 136 insertions(+), 74 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 6 build (tested in headless Chromium at 1440×900 and 390×844 with touch)

    Good: the intro tip is now a normal sentence on desktop and phone, leaderboard bars shrink monotonically with #1 longest, headings and the frost legend are clean, and the "Full record | 1991–2020" toggle is a great addition. No console errors, no overflow. Four problems remain — please verify each in the rendered page, not only in a DOM stub.

    1. The "1991–2020" frost view draws both curves upside down. Select "≤ 28 °F", then "1991–2020":

      • the spring curve starts at 0 % on Mar 1 and rises to 100 % by late May, staying at 100 % until Jul 1;

      • the fall curve is at 100 % from Jul 1 to mid-September and then falls to 0 % by November;

      • the markers read "90% · Mar 1", "10% · Mar 1", "90% · Jul 2", "10% · Jul 2" (nonsense), while the chips correctly say May 4 / Sep 28. Each year is being counted on the wrong side of its freeze date: spring should count a year for a date when that year's last spring freeze falls on or after the date; fall should count it when the first fall freeze falls on or before the date (years with no freeze in the window never count). The curves must have the same shapes as the full-record view (spring falling, fall rising). Reference values I computed from the raw file for 1991–2020 (years with ≤ 20 missing days per half-year):

      • ≤ 28 °F: spring 90 % ≈ Apr 2, 50 % ≈ May 4–6, 10 % ≈ May 16 · fall 10 % ≈ Sep 15, 50 % ≈ Sep 28, 90 % ≈ Oct 20

      • ≤ 32 °F: spring 90 % ≈ May 7, 50 % ≈ May 22, 10 % ≈ Jun 7 · fall 10 % ≈ Sep 6, 50 % ≈ Sep 17, 90 % ≈ Oct 3 Also the chip "Median frost-free season (≤ 28°F): 172 days" does not change when switching to 1991–2020; it should be recomputed for the period.

    2. "Days ahead / behind" is inverted again (regression) and gives nonsense values. With 2025 and default plantings: dry peas "+3 % vs normal (≈ 2 days behind)" although the stage table shows maturity Jul 24 vs normal Jul 26 (2 days ahead); potatoes "+16 % vs normal (≈ 53 days behind)" although maturity is Aug 31 vs normal Sep 14 (14 days ahead); sweet corn "+22 % vs normal (roughly even with the normal year)"; custom base 41 / cap 80 "+14 % vs normal (roughly even…)". Expected: for crops, use the last-stage dates already in the stage table — "≈ 14 days ahead" when the selected year reached the last stage 14 days before the normal, "behind" when later (this is what round 5 did, and it was right). For custom curves, compare the date the selected year reached the normal median's Sep 30 total with Sep 30 (earlier = ahead). Say "roughly even" only when the difference is 0–1 day; if it cannot be computed, leave the parenthetical out rather than saying "roughly even".

    3. Tapping the GDD or frost chart on a phone still never shows a tooltip. At 390 px with touch, tap inside the GDD chart: the tooltip text is filled in but its opacity is 0 immediately and stays 0. The tap on the GDD chart is also received by the frost chart's "tap outside hides the tooltip" handler (and by the same handlers left over from every earlier re-render of both charts, which keep piling up on the document), and since both charts share one tooltip element, those handlers hide it straight away. Expected: a tap on a chart shows its tooltip and guide line and they stay visible; only a tap that is outside every chart (and not on a calendar day) hides them; re-rendering a chart (changing crop, year, threshold, resizing) must not add more document-level handlers.

    4. NOAA explanation is duplicated and self-contradictory. At ≤ 28 °F the same explanation appears twice — once as a paragraph above the chips ("Why the ◇ dates sit weeks from the curve…") and again at the end of the footnote. The footnote also calls 1991–2020 "a warmer period" while saying its median last spring freeze is later (≈ May 5 vs Apr 27), which suggests colder springs, not warmer. Expected: keep one short note (e.g. under the chart), describe 1991–2020 simply as "a different, shorter period", and hide it at ≤ 32 °F where no NOAA dates are shown.

    9 critic 3.9 min · 1 file changed, 122 insertions(+) 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, with the exact expected values computed from data/raw. Fix every item (items 1-5 first; they are data-accuracy problems), keeping everything that already works. Re-run python3 scripts/build_data.py after changing it, then run python3 build.py --check.
    

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

    Reviewer feedback (critic pass) — tested in real Chromium at 1440x900 and 390x844

    Everything else works well: no console errors, no phone overflow, tooltips, toggles, CSV downloads and row clicks all work. Keep all of that. The items below are ordered by priority. The first five are data honesty problems: numbers or citations that do not match their labels or sources. The reviewer checked every expected value below independently against data/raw/acis_stndata_USC00106152_maxt_mint_por.json and the cited web pages (you have no network access, so the source content you need is quoted here).

    1. Leaderboard "Warm season" and "Cold season" are computed over the wrong days (and so is the CSV)
    • In scripts/build_data.py, warm uses day indexes 91..272 (Apr 2 – Sep 30). cold uses Jan–Mar plus Oct–Dec of the SAME calendar year. mwarm and mcold count missing days over different windows again. The UI, though, says "Apr–Oct" and "Winters run Nov 1 of the previous year through Mar 31". It labels 1934's value as "1934–35". Result: the ranks shown are wrong. The app currently shows "Mildest winter 1934–35 42.5 °F" and "Harshest 1985–86 28.9 °F", but no Nov–Mar winter has those values. Fix: warm season = Apr 1 – Oct 31 of year y (365-day index 90..303 inclusive), mean of daily (max+min)/2 over days where both are present; mwarm = days in that same window with max or min missing.

    • winter = Nov 1 of year y-1 through Mar 31 of year y, stored on year y, displayed as "(y-1)–yy" (e.g. "1933–34"). mcold = days in that same window with max or min missing. The winter ending in 2026 (Nov 2025–Mar 2026) is complete and must be allowed, even though the 2026 row is otherwise partial. Exclude the Apr–Oct 2026 warm season (it is incomplete).

    • Keep the "more than 5 missing days → left out" rule, applied to each window.

    • Rename the seasons-CSV columns to match the real windows, e.g. warm_season_apr1_oct31_f and winter_nov1_prev_to_mar31_f.

    • Expected (±0.1 °F): Warmest Apr–Oct: 1958 60.4, 2015 59.8, 1936 59.7, 1938 59.7, 1987 59.6.

    • Coolest Apr–Oct: 1919 52.9, 1895 54.2, 1916 54.3, 1912 54.6, 1903 54.8.

    • Mildest winters: 1933–34 41.3, 2025–26 39.6, 1991–92 39.1, then 1939–40 and 2014–15 at 38.8.

    • Harshest winters: 1916–17 27.7, 1948–49 27.7, 1978–79 27.8, 1984–85 28.8, 1921–22 29.5.

    2. Wheat stages are attributed to NDAWN but do not match NDAWN's table

    The app shows "Jointing 970", "Heading 1,540", "Soft dough 1,825" and "Maturity 2,150", described as the "NDAWN rule". NDAWN's wheat table (https://ndawn.ndsu.nodak.edu/help-wheat-growing-degree-days.html) gives, in °F GDD since planting: Emergence 180, Leaf 1 252, Leaf 2 395, Leaf 3 (tillers begin) 538, Leaf 4 681, Leaf 5 824, Leaf 6 967, Leaf 7 1,110, Flag leaf visible 1,181, Flag leaf emerged 1,255, Boot swelling begins 1,396, Boot completed 1,539, Heading begins 1,567, Headed 1,682, Flowering begins 1,739, Flowering completed 1,768, Kernel watery ripe 1,825. It gives NO GDD values for milk, dough, soft dough, hard dough or physiological maturity. Fix: use these wheat stages with NDAWN's names: Tillers begin (Leaf 3) 538, Flag leaf visible 1,181, Heading begins 1,567, Flowering begins 1,739, Kernel watery ripe 1,825. Remove Jointing 970, Soft dough and Maturity 2,150, or keep maturity only as a clearly marked "≈" approximate value that is NOT attributed to NDAWN. Update the readout text (for example "needs ≈1,825 GDD for kernel watery ripe") and the Data & sources paragraph.

    3. Canola stage thresholds use the END of each NDAWN range as if it were the start

    NDAWN canola table (https://ndawn.ndsu.nodak.edu/help-canola-growing-degree-days.html), °F GDD: Seedling 0–256, Rosette 3rd leaf 257–396, Rosette 4th leaf 397–727, Early bud 728–828, Late bud 829–932, Early flower 933–1,165, Late flower 1,166–1,397, Early ripening 1,398–1,634, Late ripening 1,635–1,874, Ripe >1,874. The app shows "Rosette 450", "Bud 932" and "Early flower 1,165". 450 is not in the table. 932 is the END of late bud, and 1,165 is the END of early flower. Fix: use each stage's START: Rosette (3rd leaf) 257, Early bud 728, Early flower 933, Early ripening 1,398, Ripe 1,875.

    4. Sweet-corn stage values are attributed to a page that does not contain them

    The UDel weekly crop update page (sites.udel.edu/weeklycropupdate/?p=17630) gives only the formula GDD = (Tmax + Tmin)/2 − 50 °F and a "1300 GDD corn" maturity example. It does NOT give silking 1,075 or harvest 1,425. Fix: mark the sweet-corn stages "≈" (approximate planning values, not published figures), in the table, footnote and Data & sources, the same way peas, lentils and potatoes are marked. Remove the claim that the 1,075/1,425 figures come from UDel.

    5. GDD formula: clip Tmax/Tmin before averaging, as NDAWN does (currently the cap is applied to the mean)
    • dayGdd() in src/app.js computes max(0, min(cap, (mx+mn)/2) − base). NDAWN's method adjusts each temperature first. Use this per day: hi = min(max(Tmax, base), cap), lo = min(max(Tmin, base), cap), GDD = (hi + lo)/2 − base. Wheat (NDAWN): base 32 °F; cap 70 °F until 395 GDD have accumulated since planting, 95 °F after that.

    • Canola (NDAWN): base 41 °F, no upper cap (floor only).

    • Peas, lentils, potatoes, sweet corn and custom: the preset or custom base and cap (86 °F default).

    • Apply the same clipping (base 32, cap 86) to gdd32 and gdd50 in scripts/build_data.py, so the "GDD 32–86" leaderboard and the seasons CSV agree with the planner, then re-run the script. Add one sentence to Data & sources stating the formula. Expected with this formula (normal = 1991–2020 median, ±2 days / ±1 %): Sweet corn planted May 20: normal reaches 1,075 ≈ Aug 6 (currently Aug 14) and 1,425 ≈ Aug 26.

    • Canola planted Apr 25 (stage starts from item 3): Rosette 257 ≈ May 14, Early bud 728 ≈ Jun 13, Early flower 933 ≈ Jun 24, Early ripening 1,398 ≈ Jul 15, Ripe 1,875 ≈ Aug 2.

    • Wheat planted Apr 20: 538 ≈ May 17, 1,181 ≈ Jun 13, 1,567 ≈ Jun 27, 1,739 ≈ Jul 2, 1,825 ≈ Jul 5. 2025 by Jul 15 ≈ 2,229 GDD.

    • Leaderboard "GDD 32–86" year totals: warmest 1934 ≈ 7,230, 1958 ≈ 7,102, 2015 ≈ 7,096; coldest 1916 ≈ 5,386, 1898 ≈ 5,454, 1913 ≈ 5,526.

    6. KPI tile "Normal season GDD" shows a stale planting date and value

    Repro: load the page, then choose Crop preset "Dry peas" (planting becomes Apr 15). The tile still says "Apr 20 → Sep 30 · Dry peas" and its value was computed from Apr 20. Change the planting month or day and the tile does not update at all. For example, with planting Oct 15 it still shows "3,990 · May 20 → Sep 30". Cause: applyCrop() calls renderKPIs() before applyPlantDefaults(), and the plant-date handlers never call renderKPIs(). Fix: re-render the KPIs after the planting date is set, and whenever the month, day, year, base or cap changes.

    7. Leap years are off by one day (and one day of data is lost in build_data.py)
    • The daily arrays are on a 365-day grid (index = CUM[m-1] + d − 1, with Feb 29 removed). But doyOf() and daysInYear() add a leap day. So in every leap year the curve starts one day late. Repro: Year 2024, wheat, planted Apr 20. Hovering Apr 20 shows "2024: — GDD", and the 2024 value by Jul 15 is 2,080 instead of ≈ 2,098 (Apr 20 alone is 19 GDD).

    • The default compare year 2020 and 8 of the 30 normal years are affected.

    • The GDD-curve CSV for 2024 has a duplicate trailing "2024-12-31,,,," row.

    Fix: index the data with the 365-day grid in every year (no leap offset). Export 365 CSV rows. Also, in scripts/build_data.py the leap-day "fold" OVERWRITES Feb 28 with Feb 29's values. Skip Feb 29 instead, so Feb 28 keeps its real observation.

    8. The GDD legend prints the word "null" when Compare year is "(off)"

    lg.replaceChildren(..., hasCmp ? el(...) : null) inserts the text "null". Filter out null items before calling replaceChildren.

    9. Frost curves count two half-years the record does not cover
    • The record starts 1893-11-07 and ends 2026-09-07. Fall 1893 counts as eligible with a "first fall freeze" on Nov 9 (an artifact: the freezes before Nov 7 were never observed). Fall 2026 counts as "no freeze", although its data stop on Sep 7. Fix: a half-year is eligible only if the record covers the whole window (exclude fall 1893 and fall 2026). Apply this to the stored curves and to the 1991–2020 toggle's computation. Show spring and fall counts separately. Expected, full record: spring n = 133, fall n = 131. ≤ 32 °F: median first fall freeze Sep 22, 10 % Sep 6, 90 % Oct 18.

    • ≤ 28 °F: median Oct 12, 10 % Sep 19, 90 % Nov 3.

    • Spring values and the frost-free medians are unchanged (131 / 172 days).

    Update "133 spring- and 133 fall-eligible years" in Data & sources.

    10. Data & sources hard-codes 1991–2020 freeze dates that disagree with the app's own toggle

    The text says the 1991–2020 median last spring freeze (≤ 28 °F) is "≈ May 5" and the first fall freeze "≈ Sep 29". The frost chart's 1991–2020 toggle shows May 4 and Sep 28, and the reviewer's independent computation agrees with May 4 / Sep 28. Make the text match (or fill it in from the computed values).

    11. Planting dates outside Mar 1 – Sep 30 look broken
    • Planting Feb 10: the "plant Feb 10" marker is drawn left of the plot area and its label is clipped ("nt Feb 10"). Clamp the marker to the plot's left edge and label it "← plant Feb 10".

    • Planting Oct 15: the readout says "By Sep 30: — GDD", the stage table says "Tillering … Nov 27 / not reached", and the KPI shows a value for another date. Show one clear message instead, such as "Planted after Sep 30, so there is too little season left to track crop stages". Either hide the stage table or restrict the planting month select to Feb–Aug.

    12. 2026 readout compares two different dates

    Sweet corn, Year 2026 shows "By Sep 9: 1,437 GDD — +0 % vs normal (≈ 4 days ahead)". 2026 data end Sep 7, so the 2026 value is from Sep 7 while the normal is from Sep 9. When the selected year's data end before the comparison date, compare both at the last observed day and say so, e.g. "By Sep 7 (latest data)".

    After fixing, run python3 scripts/build_data.py and then python3 build.py --check.

    10 continue 11.7 min · 10 files changed, 236 insertions(+), 112 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, with the exact expected values computed from data/raw. Fix every item (items 1-5 first; they are data-accuracy problems), keeping everything that already works. Re-run python3 scripts/build_data.py after changing it, then run python3 build.py --check.
    

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

    Reviewer feedback (critic pass) — tested in real Chromium at 1440x900 and 390x844

    Everything else works well: no console errors, no phone overflow, tooltips, toggles, CSV downloads and row clicks all work. Keep all of that. The items below are ordered by priority. The first five are data honesty problems: numbers or citations that do not match their labels or sources. The reviewer checked every expected value below independently against data/raw/acis_stndata_USC00106152_maxt_mint_por.json and the cited web pages (you have no network access, so the source content you need is quoted here).

    1. Leaderboard "Warm season" and "Cold season" are computed over the wrong days (and so is the CSV)
    • In scripts/build_data.py, warm uses day indexes 91..272 (Apr 2 – Sep 30). cold uses Jan–Mar plus Oct–Dec of the SAME calendar year. mwarm and mcold count missing days over different windows again. The UI, though, says "Apr–Oct" and "Winters run Nov 1 of the previous year through Mar 31". It labels 1934's value as "1934–35". Result: the ranks shown are wrong. The app currently shows "Mildest winter 1934–35 42.5 °F" and "Harshest 1985–86 28.9 °F", but no Nov–Mar winter has those values. Fix: warm season = Apr 1 – Oct 31 of year y (365-day index 90..303 inclusive), mean of daily (max+min)/2 over days where both are present; mwarm = days in that same window with max or min missing.

    • winter = Nov 1 of year y-1 through Mar 31 of year y, stored on year y, displayed as "(y-1)–yy" (e.g. "1933–34"). mcold = days in that same window with max or min missing. The winter ending in 2026 (Nov 2025–Mar 2026) is complete and must be allowed, even though the 2026 row is otherwise partial. Exclude the Apr–Oct 2026 warm season (it is incomplete).

    • Keep the "more than 5 missing days → left out" rule, applied to each window.

    • Rename the seasons-CSV columns to match the real windows, e.g. warm_season_apr1_oct31_f and winter_nov1_prev_to_mar31_f.

    • Expected (±0.1 °F): Warmest Apr–Oct: 1958 60.4, 2015 59.8, 1936 59.7, 1938 59.7, 1987 59.6.

    • Coolest Apr–Oct: 1919 52.9, 1895 54.2, 1916 54.3, 1912 54.6, 1903 54.8.

    • Mildest winters: 1933–34 41.3, 2025–26 39.6, 1991–92 39.1, then 1939–40 and 2014–15 at 38.8.

    • Harshest winters: 1916–17 27.7, 1948–49 27.7, 1978–79 27.8, 1984–85 28.8, 1921–22 29.5.

    2. Wheat stages are attributed to NDAWN but do not match NDAWN's table

    The app shows "Jointing 970", "Heading 1,540", "Soft dough 1,825" and "Maturity 2,150", described as the "NDAWN rule". NDAWN's wheat table (https://ndawn.ndsu.nodak.edu/help-wheat-growing-degree-days.html) gives, in °F GDD since planting: Emergence 180, Leaf 1 252, Leaf 2 395, Leaf 3 (tillers begin) 538, Leaf 4 681, Leaf 5 824, Leaf 6 967, Leaf 7 1,110, Flag leaf visible 1,181, Flag leaf emerged 1,255, Boot swelling begins 1,396, Boot completed 1,539, Heading begins 1,567, Headed 1,682, Flowering begins 1,739, Flowering completed 1,768, Kernel watery ripe 1,825. It gives NO GDD values for milk, dough, soft dough, hard dough or physiological maturity. Fix: use these wheat stages with NDAWN's names: Tillers begin (Leaf 3) 538, Flag leaf visible 1,181, Heading begins 1,567, Flowering begins 1,739, Kernel watery ripe 1,825. Remove Jointing 970, Soft dough and Maturity 2,150, or keep maturity only as a clearly marked "≈" approximate value that is NOT attributed to NDAWN. Update the readout text (for example "needs ≈1,825 GDD for kernel watery ripe") and the Data & sources paragraph.

    3. Canola stage thresholds use the END of each NDAWN range as if it were the start

    NDAWN canola table (https://ndawn.ndsu.nodak.edu/help-canola-growing-degree-days.html), °F GDD: Seedling 0–256, Rosette 3rd leaf 257–396, Rosette 4th leaf 397–727, Early bud 728–828, Late bud 829–932, Early flower 933–1,165, Late flower 1,166–1,397, Early ripening 1,398–1,634, Late ripening 1,635–1,874, Ripe >1,874. The app shows "Rosette 450", "Bud 932" and "Early flower 1,165". 450 is not in the table. 932 is the END of late bud, and 1,165 is the END of early flower. Fix: use each stage's START: Rosette (3rd leaf) 257, Early bud 728, Early flower 933, Early ripening 1,398, Ripe 1,875.

    4. Sweet-corn stage values are attributed to a page that does not contain them

    The UDel weekly crop update page (sites.udel.edu/weeklycropupdate/?p=17630) gives only the formula GDD = (Tmax + Tmin)/2 − 50 °F and a "1300 GDD corn" maturity example. It does NOT give silking 1,075 or harvest 1,425. Fix: mark the sweet-corn stages "≈" (approximate planning values, not published figures), in the table, footnote and Data & sources, the same way peas, lentils and potatoes are marked. Remove the claim that the 1,075/1,425 figures come from UDel.

    5. GDD formula: clip Tmax/Tmin before averaging, as NDAWN does (currently the cap is applied to the mean)
    • dayGdd() in src/app.js computes max(0, min(cap, (mx+mn)/2) − base). NDAWN's method adjusts each temperature first. Use this per day: hi = min(max(Tmax, base), cap), lo = min(max(Tmin, base), cap), GDD = (hi + lo)/2 − base. Wheat (NDAWN): base 32 °F; cap 70 °F until 395 GDD have accumulated since planting, 95 °F after that.

    • Canola (NDAWN): base 41 °F, no upper cap (floor only).

    • Peas, lentils, potatoes, sweet corn and custom: the preset or custom base and cap (86 °F default).

    • Apply the same clipping (base 32, cap 86) to gdd32 and gdd50 in scripts/build_data.py, so the "GDD 32–86" leaderboard and the seasons CSV agree with the planner, then re-run the script. Add one sentence to Data & sources stating the formula. Expected with this formula (normal = 1991–2020 median, ±2 days / ±1 %): Sweet corn planted May 20: normal reaches 1,075 ≈ Aug 6 (currently Aug 14) and 1,425 ≈ Aug 26.

    • Canola planted Apr 25 (stage starts from item 3): Rosette 257 ≈ May 14, Early bud 728 ≈ Jun 13, Early flower 933 ≈ Jun 24, Early ripening 1,398 ≈ Jul 15, Ripe 1,875 ≈ Aug 2.

    • Wheat planted Apr 20: 538 ≈ May 17, 1,181 ≈ Jun 13, 1,567 ≈ Jun 27, 1,739 ≈ Jul 2, 1,825 ≈ Jul 5. 2025 by Jul 15 ≈ 2,229 GDD.

    • Leaderboard "GDD 32–86" year totals: warmest 1934 ≈ 7,230, 1958 ≈ 7,102, 2015 ≈ 7,096; coldest 1916 ≈ 5,386, 1898 ≈ 5,454, 1913 ≈ 5,526.

    6. KPI tile "Normal season GDD" shows a stale planting date and value

    Repro: load the page, then choose Crop preset "Dry peas" (planting becomes Apr 15). The tile still says "Apr 20 → Sep 30 · Dry peas" and its value was computed from Apr 20. Change the planting month or day and the tile does not update at all. For example, with planting Oct 15 it still shows "3,990 · May 20 → Sep 30". Cause: applyCrop() calls renderKPIs() before applyPlantDefaults(), and the plant-date handlers never call renderKPIs(). Fix: re-render the KPIs after the planting date is set, and whenever the month, day, year, base or cap changes.

    7. Leap years are off by one day (and one day of data is lost in build_data.py)
    • The daily arrays are on a 365-day grid (index = CUM[m-1] + d − 1, with Feb 29 removed). But doyOf() and daysInYear() add a leap day. So in every leap year the curve starts one day late. Repro: Year 2024, wheat, planted Apr 20. Hovering Apr 20 shows "2024: — GDD", and the 2024 value by Jul 15 is 2,080 instead of ≈ 2,098 (Apr 20 alone is 19 GDD).

    • The default compare year 2020 and 8 of the 30 normal years are affected.

    • The GDD-curve CSV for 2024 has a duplicate trailing "2024-12-31,,,," row.

    Fix: index the data with the 365-day grid in every year (no leap offset). Export 365 CSV rows. Also, in scripts/build_data.py the leap-day "fold" OVERWRITES Feb 28 with Feb 29's values. Skip Feb 29 instead, so Feb 28 keeps its real observation.

    8. The GDD legend prints the word "null" when Compare year is "(off)"

    lg.replaceChildren(..., hasCmp ? el(...) : null) inserts the text "null". Filter out null items before calling replaceChildren.

    9. Frost curves count two half-years the record does not cover
    • The record starts 1893-11-07 and ends 2026-09-07. Fall 1893 counts as eligible with a "first fall freeze" on Nov 9 (an artifact: the freezes before Nov 7 were never observed). Fall 2026 counts as "no freeze", although its data stop on Sep 7. Fix: a half-year is eligible only if the record covers the whole window (exclude fall 1893 and fall 2026). Apply this to the stored curves and to the 1991–2020 toggle's computation. Show spring and fall counts separately. Expected, full record: spring n = 133, fall n = 131. ≤ 32 °F: median first fall freeze Sep 22, 10 % Sep 6, 90 % Oct 18.

    • ≤ 28 °F: median Oct 12, 10 % Sep 19, 90 % Nov 3.

    • Spring values and the frost-free medians are unchanged (131 / 172 days).

    Update "133 spring- and 133 fall-eligible years" in Data & sources.

    10. Data & sources hard-codes 1991–2020 freeze dates that disagree with the app's own toggle

    The text says the 1991–2020 median last spring freeze (≤ 28 °F) is "≈ May 5" and the first fall freeze "≈ Sep 29". The frost chart's 1991–2020 toggle shows May 4 and Sep 28, and the reviewer's independent computation agrees with May 4 / Sep 28. Make the text match (or fill it in from the computed values).

    11. Planting dates outside Mar 1 – Sep 30 look broken
    • Planting Feb 10: the "plant Feb 10" marker is drawn left of the plot area and its label is clipped ("nt Feb 10"). Clamp the marker to the plot's left edge and label it "← plant Feb 10".

    • Planting Oct 15: the readout says "By Sep 30: — GDD", the stage table says "Tillering … Nov 27 / not reached", and the KPI shows a value for another date. Show one clear message instead, such as "Planted after Sep 30, so there is too little season left to track crop stages". Either hide the stage table or restrict the planting month select to Feb–Aug.

    12. 2026 readout compares two different dates

    Sweet corn, Year 2026 shows "By Sep 9: 1,437 GDD — +0 % vs normal (≈ 4 days ahead)". 2026 data end Sep 7, so the 2026 value is from Sep 7 while the normal is from Sep 9. When the selected year's data end before the comparison date, compare both at the last observed day and say so, e.g. "By Sep 7 (latest data)".

    After fixing, run python3 scripts/build_data.py and then python3 build.py --check.

    11 critic 2.6 min · 7 files changed, 125 insertions(+), 174 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — a reviewer re-tested index.html in a real browser; three items remain (two of them were reported as done last round but the code was not changed). Fix all three, keeping everything that already works. Re-run python3 scripts/build_data.py after changing it, then run python3 build.py --check, then re-read dayGdd() in src/app.js and gdd() in scripts/build_data.py to confirm the new formula is really there.
    

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

    Reviewer feedback (critic pass 2), tested in real Chromium

    The last round fixed most items, and the reviewer re-verified them in the browser: leaderboard windows, NDAWN wheat and canola stages, KPI refresh, leap years, legend, frost eligibility, planting edge cases and the 2026 readout. Keep all of that. Three items remain.

    1. GDD formula still caps the daily MEAN. Not done last round, although the summary said it was
    • dayGdd() in src/app.js still returns max(0, min(cap, (mx+mn)/2) − base), and gdd() in scripts/build_data.py is the same. NDAWN's method adjusts each temperature BEFORE averaging: hi = Tmax raised to base if below it, then capped at cap

    • lo = Tmin raised to base if below it, then capped at cap

    • daily GDD = (hi + lo) / 2 − base

    • Crop caps: Canola: NDAWN uses NO upper cap. Give the canola preset no cap (floor at 41 °F only) and show "no cap" wherever the base and cap are displayed.

    • Every other preset keeps its current cap (86 °F). Custom uses its own base and cap.

    Apply the same formula to gdd32 (base 32, cap 86) and gdd50 (base 50, cap 86) in scripts/build_data.py, then re-run it. Add one sentence to Data & sources that states the formula.

    • Expected after the change. The normal is the 1991–2020 median, computed from data/embed/daily.json; allow ±1 day: Spring wheat, planted Apr 20. Normal reaches 538 May 17, 1,181 Jun 12, 1,567 Jun 27, 1,739 Jul 2, 1,825 Jul 5. The KPI "Normal season GDD" (Apr 20 → Sep 30) ≈ 4,490; it is 4,564 now.

    • Canola, planted Apr 25, no cap. Normal reaches 257 May 15, 728 Jun 13, 933 Jun 24, 1,398 Jul 15, 1,875 Aug 3. 2025 reaches 1,875 on Jul 28.

    • Sweet corn, planted May 20. Normal reaches 1,075 on Aug 6 (Aug 14 now) and 1,425 on Aug 26 (Sep 9 now). 2025 reaches them Jul 29 and Aug 17.

    • Potatoes, planted May 1. Normal reaches 600 Jun 17, 1,200 Jul 20, 2,200 Sep 8.

    • Leaderboard "GDD 32–86", allow ±10:

      • warmest: 1934 ≈ 7,230, 1958 ≈ 7,102, 2015 ≈ 7,096

      • coldest: 1916 ≈ 5,382, 1898 ≈ 5,454, 1913 ≈ 5,526

      • These are 7,212 and 5,253 now.

    2. Sweet-corn stage values are still attributed to UDel. Not done last round
    • src/body.html still says "Sweet corn silking ≈ 1,075 with harvest ≈ 1,425 GDD (base 50), per the UDel weekly crop update". That page does NOT contain those figures. It gives only GDD = (Tmax + Tmin)/2 − 50 °F and a "1300 GDD corn" example. Fix: Add approx: true to both sweet-corn stages in CROPS, so the table shows "≈" and the footnote appears.

    • In Data & sources, say the sweet-corn stage values are approximate planning values, not published figures. Keep the UDel link only as the source of the base-50 GDD idea.

    • Update the "≈ marks …" footnote and the #gdd-foot text so they name peas, lentils, potatoes AND sweet corn as approximate.

    3. Frost year counts: the fall curve now uses 131 years, but the UI still says 133
    • The frost readout chip says "133 years used". Change it to "133 spring / 131 fall years used". With the 1991–2020 toggle it should read "30 spring / 30 fall".

    • The KPI tile "First fall freeze (50/50)" says "133 yrs". It should show the fall count (131).

    After fixing, run python3 scripts/build_data.py and then python3 build.py --check. Then open src/app.js and confirm that dayGdd() now clips hi and lo before averaging.

What the agent was asked

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

Read the specification

Palouse Growing Season Planner — Growing degree days and frost risk for Moscow, Idaho

Category: Scientific visualization Who it is for: Plant sciences, CALS Extension, community gardeners, farm managers

What it should do

Turn a century of Moscow weather records into planting decisions: accumulate growing degree days for Palouse crops, see last-spring and first-fall frost odds, and compare any year with the normal.

Data

Real daily max/min temperature for the MOSCOW U OF I, ID COOP station (GHCN USC00106152), downloaded at build time from the NOAA Regional Climate Centers ACIS web service (data.rcc-acis.org/StnData, JSON) — ideally 1950–present. If the download is not possible inside the tool, the builder will be given the data file.

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

  • Crop presets with their base temperatures (spring wheat, dry peas, lentils, canola, sweet corn, potatoes) and a custom base/cap

  • Planting-date picker → cumulative GDD curve for the chosen year vs the 30-year normal band, with crop growth-stage markers

  • Frost probability chart: probability of a ≤32°F / ≤28°F freeze after each spring date and before each fall date, from the full record

  • Calendar heatmap of daily temperature anomalies for a selected year

  • Warmest/coldest seasons leaderboard and a CSV export

  • 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

  • Cosmetic: for the canola preset (no upper cap), the crop note reads 'cap none°F' and the readout and axis read 'cap no cap'.
  • Stage GDD values for dry peas, lentils, potatoes and sweet corn are approximate planning values, not published figures. The app marks them with '≈' and says so in Data & sources.
  • Wheat uses a flat 86 °F cap, not NDAWN's 70 °F cap before Haun stage 2 and 95 °F after. The independent check found stage dates differ by at most 1 day.
  • The ScienceDirect 2025 base-temperature review citation could not be verified (the page returns HTTP 403 to automated fetch).
  • Carried over from the build driver and not re-tested in this pass: on a phone, the first tap on a chart right after tapping a calendar day may show no tooltip.
  • chartPointer() adds a new window scroll listener on every chart re-render, so listeners pile up over a long session. No visible effect was found.