U of I × MindRouter App Gallery

Rubric Studio

Education Built with Goose v1.50.0 Critic status: shipped

Rubric Studio

Build rubrics, grade a class, see the patterns

Design an analytic rubric, grade a whole roster criterion by criterion with keyboard speed, and get instant analytics on where the class struggled — then export grades for the gradebook.

Rubric Studio running on a desktop screen, build rubrics, grade a class, see the patterns Desktop · click to enlarge
Rubric Studio running on a phone, build rubrics, grade a class, see the patterns
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Rubric Studio lets an instructor build an analytic rubric, grade a class one student at a time with number keys and a reusable comment bank, and see where students struggled. It opens with a fictional 30-student chemistry lab-report class so the heatmap, score histogram, criterion difficulty chart and flagged-student lists have data to show, and it exports detailed or gradebook-ready CSVs plus one printable feedback sheet per student.

Try this

  1. Open Analytics and click Xena Xu in the Uneven Profiles card to jump to her weak Analysis score in the Grade tab.
  2. In Grade, click 'Next ungraded' to reach Lena Lopez's missing criterion, press 3, then check that Analytics now shows 28 of 30 fully graded.
  3. In Rubric, set Purpose & Hypothesis to weight 2 and watch totals, the difficulty chart and the outlier list recompute, then press Undo.

Highlights ✎ written by the AI critic

  • Rubric builder with templates, editable levels and points, per-criterion weights, drag or up/down reordering, and undo
  • Keyboard grading: keys 1-4 score the active criterion and move on to the next student; a comment bank adds feedback in one click
  • Analytics: class KPIs, score histogram with a mean line, criterion difficulty as % of max, and a clickable student x criterion heatmap
  • Flags students far below the class mean and students with one criterion well below their own average
  • Exports a detailed CSV, a gradebook-import CSV, a JSON backup, and printable per-student feedback sheets

Who it's for

Instructors and TAs, program assessment coordinators

Features

  • Rubric builder: criteria × performance levels grid, points or weights, level descriptors, drag to reorder, templates
  • Grading mode: one student at a time, click or number-key to score each criterion, per-criterion comments with a reusable comment bank
  • Roster import from CSV (name, id) and grade export to CSV compatible with an LMS gradebook import
  • Analytics: score distribution, criterion difficulty (mean and spread), heatmap of students × criteria, flagged outliers
  • Student feedback sheet generator (printable, one per student)
  • Autosave to localStorage, undo, and full JSON backup/restore

Data & sources

  • No external data (per SPEC). An example lab-report analytic rubric and a clearly fictional 30-student roster (DEMO-001..DEMO-030, fixed seed 20260913, deliberate outliers and 8 ungraded cells) are generated by the harness-written scripts/generate_example_data.py into data/embed/example_rubric.json and data/embed/example_roster.json. data/raw/SOURCES.md, verified by the data-prep workflow (verdict ready-after-repair), explains this and gives the suggested citation, which appears in the app's Data & Sources tab as 'Synthetic example data, no external source'.
Full data provenance ✎ written by the AI reviewer

Data sources: Rubric Studio

Retrieval date: 2026-09-13 (nothing was retrieved; see below)

Summary

This app uses no external data. Nothing was downloaded and there are no raw files in this directory apart from this note. No dataset, API, web page, or institutional record (University of Idaho or otherwise) goes into the app.

Example content (synthetic, generated by the app's own scripts)

SPEC.md asks for two built-in examples so the grading and analytics screens have something to show before a user brings in their own data:

  1. Example rubric, e.g. a lab-report analytic rubric (criteria × performance levels, points, level descriptors). This is original illustrative content written for the app. It is not an official University of Idaho, departmental, or accreditation rubric and must not be described as one. If it is shipped as data rather than hard-coded in src/app.js, put it in data/embed/ as JSON (see the embedding note below).

  2. Example roster of 30 students with generated scores. It is fictional. The coding harness should generate it with a script in scripts/ (Python 3 standard library only) and write the result to data/embed/. It is not derived from any real class, gradebook, LMS export, or student record.

Embedding note: build.py embeds only data/embed/*.json (each file must be valid JSON or the build fails). Each file becomes <script type="application/json" id="data-<file stem>">, so a file named example_roster.json is read in the app with JSON.parse(document.getElementById('data-example_roster').textContent).

Requirements for the generated roster:

  • Label it in the UI. Wherever the example roster or its analytics appear, say something like "Example data: fictional students with randomly generated scores". Credit it the same way in the "Data & sources" panel. Never present it as real University of Idaho students, grades, or course results.

  • No external link exists. SPEC.md asks for credits "with links", but there is no external source URL for either example. Don't invent one. In the "Data & sources" panel, say "Synthetic example data, no external source" (the app must make no network requests, and scripts/ is not published with the page, so don't link to it either).

  • Use obviously fictional names and IDs. Don't use real people's names. Use placeholder IDs that can't be mistaken for real Vandal/student ID numbers (e.g. DEMO-001 … DEMO-030). Don't use real email addresses; if an email column is needed, use the reserved example.edu / example.com domains.

  • Make it reproducible. Use a fixed random seed so the committed data/embed/ output can be regenerated byte-for-byte. Document the seed and the score model (e.g. a per-student ability term plus a per-criterion difficulty term plus noise, clipped to the level range) in the script's header comment.

  • Keep it shaped like real grading data. Leave in a few deliberate outliers and some ungraded or missing cells so the "flagged outliers" and empty-state paths get exercised.

Files in data/raw/

FileSourceRecordsNotes
SOURCES.mdthis noten/aNo downloaded files exist.

License / terms

Not applicable, since no third-party data is used. The example rubric text and the generated roster are original to this app.

Suggested citation for the "Data & sources" panel

Example rubric and 30-student roster are fictional, illustrative content created for Rubric Studio; roster names, IDs and scores are randomly generated. No real student or University of Idaho data is included.

How it was built

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

19harness rounds
123.1minutes of harness work (2.1 h)
3,049lines of code
177KB, single-file app

Rounds by kind

build1
feedback8
continue7
critic3

Lines of code per file

body html186
app css597
app js2,077
scripts189

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

  • Avery Alvarez (DEMO-001) total 22 / 24 (91.7%) on Grade and Feedback sheet = raw scores 3+4+4+4+3+4 = 22
  • KPIs 74.1% mean / 79.2% median / SD 4.7 pts / 27 of 30 fully graded = independent Python over the 27 complete students (mean 17.78/24, median 19, population SD 4.70)
  • Criterion difficulty Purpose & Hypothesis 2.52 +/- 0.92 (63%), Clarity & Presentation 3.52 +/- 0.74 (88%) = Python per-criterion mean/SD
  • Flagged outliers Fiona Foster 8/24 and Sam Sato 6/24 (below mean - 1.5 SD = 10.73) = Python
  • Histogram bins 6-8:2, 9-11:2, 12-14:3, 15-17:3, 18-20:8, 21-23:7, 24:2 = counts from sorted raw totals
Show all 9 spot checks
  • With Purpose weight x2: 72.5% / 78.6% / SD 5.5, Avery 25/28, outliers Fiona 9, Sam 7, Aiden 12 = Python weighted recomputation
  • Uneven profiles Xena Xu -2.5 levels and Talia Turner -2.0 levels (Analysis) = Python gap from each student's mean level
  • CSV exports: Bella Barnes (DEMO-028) row 1,2 with empty cells and empty total; LMS export leaves Lena Lopez (conclusion null) total empty = raw nulls
  • scripts/generate_example_data.py (seed 20260913) regenerates data/embed JSON byte-for-byte; UI labels the roster as fictional/synthetic in the Analytics banner and Data & Sources
Build notes ✎ written by the AI reviewer

All app code (src/, scripts/, data/embed/) was written by Goose 1.50.0 on MindRouter default-agent (qwen/qwen3.8-27b) through the recorded round runner. I never edited project code, and no TAMPER GUARD was reported. My only project writes were FEEDBACK.md (removed and committed as 'driver: remove FEEDBACK.md') — data/raw needed no changes. There were no MindRouter outages (network_error_lines 0 in every round).

  • Harness behaviour was the main difficulty. Round 1 (build) produced a working, decent app in 7 minutes. After that, feedback rounds repeatedly fell into degenerate loops: after reading the large app.js/app.css, the model repeated one read-only command more than 100 times without editing. Round 2 hit max turns with no code changes.

  • Rounds 3, 4, 6, 9 and 14 were killed as stuck (exit 143) per the procedure and re-run as 'continue'.

  • What fixed it: From round 5 on, the prompt keeps the standard feedback sentence and adds a working-method paragraph: don't cat whole files, grep then view at most 3 small ranges then edit immediately, never repeat a command, and a grep with no match means the code doesn't exist yet so write it.

  • FEEDBACK.md was split into small batches of 2-5 precise items naming functions and CSS rules.

  • With this, rounds 5, 8, 10, 11, 12, 13, 15 and 16 finished in 10-600 seconds.

Round 14 (stuck, killed) left a syntax error (a missing ']' at app.js line 1392) that blanked the app. Continue round 15 fixed it in 10 seconds after FEEDBACK pointed at the line. Round 14's interrupted phone-levels change also caused a 208 px phone overflow on the Rubric tab plus redundant desktop labels; I treated finishing that stuck round's work as a continue re-run, and round 16 fixed both. This was a judgement call, since the 8 feedback rounds (2, 6, 8, 9, 11, 12, 13, 14) were already used.

In buildlog.json, the feedback_items count for round 15 (7) counts a 'features to keep working' list; the real fix request was 1 item.

I verified every SPEC feature with my own Playwright scripts (check_*.py, probe_*.py in runs/rubric-studio) at 1440 px and 390 px and looked at screenshots throughout. The final build has 0 errors, 0 external requests and 0 overflow on all tabs. The remaining flaws are minor phone cosmetics, listed in features_missing_or_broken. Total harness time is 7058 s across 16 rounds (1 build, 8 feedback, 7 continue).

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 6.7 min · 7 files changed, 3591 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: this app needs no downloaded data (data/raw/SOURCES.md explains why and what the example content must look like), so first write a Python script in scripts/ (standard library only, fixed random seed) that writes compact JSON to data/embed/: one example lab-report analytic rubric (criteria x performance levels, points, level descriptors) and a clearly fictional example roster of 30 students (IDs like DEMO-001) with generated scores, including a few deliberate outliers and some ungraded cells, and run it; 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, and label the example data as fictional in the UI and in a visible "Data & sources" section (use the suggested wording in data/raw/SOURCES.md). Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    3 continue 14.8 min · — Show prompt

    Prompt given to the harness:

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

    No feedback file was sent in this round.

    4 continue 16.0 min · 1 file changed, 11 insertions(+), 26 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: batch 1 of 2 (tested in Chromium at 1440 px and 390 px)

    No console errors and no external requests were found. Tabs, templates, add criterion, drag by the handle, comment bank, JSON backup/restore and autosave all work, so keep them working.

    This batch covers only the 6 most important fixes. Work through them one at a time, in order. For each item: open the relevant function in src/app.js or rule in src/app.css, make the change, run python3 build.py --check, then move on to the next item. Don't re-read files you have already read. If an edit doesn't apply, rewrite the whole function or CSS rule instead of retrying the same command.

    1. Keyboard grading sets the wrong criterion (handleGradeKeydown in src/app.js). In the Grade tab, pressing 2 gave criterion #2 a score of 2, and pressing 4 gave criterion #4 a score of 4. The number picks the criterion and the score, so you can never give criterion 1 a 4 from the keyboard. Expected: keep an "active criterion" index in state and highlight that criterion's block (gold left border). Number keys 1–N set the level for the active criterion, then the next criterion becomes active. ArrowUp/ArrowDown change the active criterion, ArrowLeft/ArrowRight change the student, and clicking a level button also makes that criterion active. Add a one-line keyboard legend under the grading card title, e.g. "1–4 score · ↑↓ criterion · ←→ student".

    2. CSV import breaks on quoted names (importRosterCSV). Importing

       name,id
       "Smith, Jane",T-001
       Alex Rivera,T-002

    produced a student named "Smith. Expected: a small CSV parser that handles quoted fields, commas inside quotes, doubled quotes, a UTF-8 BOM and CRLF line endings. Accept the header names name/student/student name and id/student id/sis user id, case-insensitively. Replace the alert(...) calls with an inline message shown in the roster bar.

    1. The grade export is not gradebook-compatible (exportGradesCSV). The Total column contains text like 20/24, which an LMS gradebook import rejects. Expected: numeric values only. Use the columns Student,ID, then one numeric column per criterion, then a numeric total named after the rubric title (e.g. Chemistry 101 — Lab Report Rubric (Example) (out of 24)), then Percent. Leave ungraded cells empty.

    2. Ungraded cells are counted as zeros in analytics (renderAnalytics, renderDistribution, renderOutliers). DEMO-028 Bella Barnes has only 2 of 6 criteria graded, but she is listed under "Flagged Outliers" as LOW 3/24 and drags the histogram down. Expected: the histogram and outlier detection use only students whose criteria are all graded. Show students with missing scores in a separate "Incomplete grading" list under the outliers, with their name and how many criteria are graded, e.g. "2 of 6".

    3. Analytics overflows sideways on a phone (src/app.css). At 390 px wide the Analytics tab scrolls sideways by 257 px, and the right half of the Score Distribution and Criterion Difficulty charts is cut off. Expected: no horizontal page scroll at 390 px. Cards and SVG charts shrink to fit the screen (min-width: 0 on grid children, SVG width:100%). The heatmap may scroll only inside its own card.

    4. Analytics shows broken charts instead of an empty state (renderAnalytics). After importing a new 3-student roster, the histogram showed one bar labelled "0 … 0", the difficulty chart was an empty grid, and the banner still said "Example data: fictional students…". Expected: until at least one student is fully graded, show a friendly empty state in the analytics area ("No complete grades yet — open the Grade tab to start"). Show the "Example data" banner only while the built-in example roster (id demo-roster-2026) is loaded.

    5 continue 3.0 min · 4 files changed, 336 insertions(+), 80 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.
    
    Working method (earlier attempts stalled by dumping whole files and then repeating the same command without editing anything):
    - Do NOT cat src/app.js or src/app.css in full; they are large.
    - For each FEEDBACK item, in order: run one grep -n to find the function or CSS rule it names, view only that range with sed -n 'A,Bp' (at most 80 lines), then immediately edit the file to make the fix.
    - Never run the same command twice in a row. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item.
    

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

    Reviewer feedback: batch 1 of 2 (tested in Chromium at 1440 px and 390 px)

    No console errors and no external requests were found. Tabs, templates, add criterion, drag by the handle, comment bank, JSON backup/restore and autosave all work, so keep them working.

    This batch covers only the 6 most important fixes. Work through them one at a time, in order. For each item: open the relevant function in src/app.js or rule in src/app.css, make the change, run python3 build.py --check, then move on to the next item. Don't re-read files you have already read. If an edit doesn't apply, rewrite the whole function or CSS rule instead of retrying the same command.

    1. Keyboard grading sets the wrong criterion (handleGradeKeydown in src/app.js). In the Grade tab, pressing 2 gave criterion #2 a score of 2, and pressing 4 gave criterion #4 a score of 4. The number picks the criterion and the score, so you can never give criterion 1 a 4 from the keyboard. Expected: keep an "active criterion" index in state and highlight that criterion's block (gold left border). Number keys 1–N set the level for the active criterion, then the next criterion becomes active. ArrowUp/ArrowDown change the active criterion, ArrowLeft/ArrowRight change the student, and clicking a level button also makes that criterion active. Add a one-line keyboard legend under the grading card title, e.g. "1–4 score · ↑↓ criterion · ←→ student".

    2. CSV import breaks on quoted names (importRosterCSV). Importing

       name,id
       "Smith, Jane",T-001
       Alex Rivera,T-002

    produced a student named "Smith. Expected: a small CSV parser that handles quoted fields, commas inside quotes, doubled quotes, a UTF-8 BOM and CRLF line endings. Accept the header names name/student/student name and id/student id/sis user id, case-insensitively. Replace the alert(...) calls with an inline message shown in the roster bar.

    1. The grade export is not gradebook-compatible (exportGradesCSV). The Total column contains text like 20/24, which an LMS gradebook import rejects. Expected: numeric values only. Use the columns Student,ID, then one numeric column per criterion, then a numeric total named after the rubric title (e.g. Chemistry 101 — Lab Report Rubric (Example) (out of 24)), then Percent. Leave ungraded cells empty.

    2. Ungraded cells are counted as zeros in analytics (renderAnalytics, renderDistribution, renderOutliers). DEMO-028 Bella Barnes has only 2 of 6 criteria graded, but she is listed under "Flagged Outliers" as LOW 3/24 and drags the histogram down. Expected: the histogram and outlier detection use only students whose criteria are all graded. Show students with missing scores in a separate "Incomplete grading" list under the outliers, with their name and how many criteria are graded, e.g. "2 of 6".

    3. Analytics overflows sideways on a phone (src/app.css). At 390 px wide the Analytics tab scrolls sideways by 257 px, and the right half of the Score Distribution and Criterion Difficulty charts is cut off. Expected: no horizontal page scroll at 390 px. Cards and SVG charts shrink to fit the screen (min-width: 0 on grid children, SVG width:100%). The heatmap may scroll only inside its own card.

    4. Analytics shows broken charts instead of an empty state (renderAnalytics). After importing a new 3-student roster, the histogram showed one bar labelled "0 … 0", the difficulty chart was an empty grid, and the banner still said "Example data: fictional students…". Expected: until at least one student is fully graded, show a friendly empty state in the analytics area ("No complete grades yet — open the Grade tab to start"). Show the "Example data" banner only while the built-in example roster (id demo-roster-2026) is loaded.

    7 continue 9.9 min · 3 files changed, 27 insertions(+), 40 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.
    
    Working method (earlier attempts stalled by dumping whole files and then repeating the same command without editing anything):
    - Do NOT cat src/app.js or src/app.css in full; they are large.
    - For each FEEDBACK item, in order: run one grep -n to find the function or CSS rule it names, view only that range with sed -n 'A,Bp' (at most 80 lines), then immediately edit the file to make the fix.
    - Never run the same command twice in a row. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item.
    

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

    Reviewer feedback: batch 2a (tested in Chromium at 1440 px)

    Verified fixed and working: keyboard grading with a highlighted active criterion, CSV parsing, numeric export, the incomplete-grading list, no horizontal scroll at 390 px, and the Analytics empty state after an import (it recovers after "Reset to Example"). Keep all of it. No console errors.

    This batch has only two items, both in the Rubric tab (renderRubric and bindRubricEvents in src/app.js, rubric styles in src/app.css). Do item 1, run python3 build.py --check, then do item 2.

    1. The rubric builder cannot edit levels, points or weights. SPEC requires a "criteria × performance levels grid, points or weights, level descriptors". In the Rubric tab the level headers ("1 — Beginning", "2 — Developing", …) are static text, every criterion shows a fixed "Max: 4 pts", and there is no way to add or remove a level. My test found 0 number inputs and 0 inputs in .rs-levels-head. Expected:

      • In .rs-levels-head, each level column has a text input for the level name (e.g. "Proficient") and a small number input for its points (e.g. 3), with aria-labels.

      • A "+ Add level" button next to the header adds a level with points = current highest + 1 and an empty descriptor textarea in every criterion row. Each level has a small ✕ remove button, and at least 2 levels must remain.

      • Each criterion row has a small number input labelled "Weight" (default 1, min 0.5, step 0.5). The row's "Max: N pts" text shows weight × highest level points.

      • The "Total: N points" footer updates live when points or weights change.

      • Store weight on each criterion in state (missing = 1). Save it to localStorage, include it in the JSON backup, and push undo before structural changes.

      • Everywhere a score is added up (the grading total, exportGradesCSV, the analytics totals and outliers, renderFeedback), use levelPoints(levelId) × criterion.weight instead of the raw level number. Put that in one helper function, e.g. critPoints(crit, levelId) and maxPoints(rubric), and call it from each place.

      • Since the level count can change, the grid columns must come from levels.length, not a fixed repeat(4, 1fr).

    2. Two criterion names are clipped (.rs-crit-name, first grid column 200px). At 1440 px, "Analysis & Calculations" and "Conclusion & Discussion" don't fit their input, so the text is cut off. Expected: the full name is always visible. Make the first column minmax(240px, 280px) and turn the name into a textarea that wraps to 2 lines, or reduce the input's font size slightly so it fits.

    10 continue 0.8 min · 4 files changed, 71 insertions(+), 56 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer feedback: batch 3a, Grade tab criterion cards (tested in Chromium at 1440 px and 390 px)

    Verified working: editable levels, points and weights (weight 2 gives 28 points and the grading total 25 / 28), add/remove level, unclipped criterion names, the active criterion's gold left border, keyboard grading, CSV import/export, the analytics empty state, no overflow at 390 px, and no console errors. Keep all of it.

    There are two items, both in renderGrade in src/app.js and the .rs-grade-crit… rules in src/app.css. Some of the class names below are new: grep will not find them, so just add them. Do item 1, run python3 build.py --check, then do item 2.

    1. Level buttons show only a bare number. Each criterion shows "1 2 3 4" in 44 px squares (.rs-grade-crit__level-btn), so a grader can't see what a level means without switching tabs. Expected:

      • Inside each level button, render three lines: <b> with the level number; <span class="lvl-name"> with the level name with any leading "3 — " removed (e.g. "Proficient"); and <span class="lvl-pts"> with points × weight (e.g. "3 pts").

      • CSS: the buttons are min-width: 96px; height: auto; padding: 8px 12px, flex-direction column. .lvl-name is .75rem and .lvl-pts is .7rem muted. Keep the gold .is-selected state.

      • Directly under the buttons, add <div class="rs-grade-crit__desc"> containing the descriptor text (from crit.descriptors) of the currently selected level of that criterion, in .8rem muted text. Show nothing if the criterion is ungraded.

      • At ≤640 px the buttons may wrap to two per row.

    2. Each criterion card wastes a lot of vertical space. Each card is about 200 px tall. Most of that is an empty 50 px comment textarea plus a row of four identical truncated chips ("Excellent work on this section…"), repeated under all six criteria. Expected:

      • In .rs-grade-crit__comment textarea, set min-height: 36px; rows=1, and grow it to about 80 px on :focus.

      • In renderGrade, output the .rs-grade-crit__comment-bank chips only for the criterion whose index equals state.activeCritIdx, and there show every bank comment, not just the first 4.

      • Reduce .rs-grade-crit padding to 10px 12px.

      • Goal: at 1440×900 the six criteria of the example rubric take less than about 900 px of height in total.

    15 continue 0.2 min · 3 files changed, 22 insertions(+), 26 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer feedback: urgent, the app no longer runs (tested in Chromium)

    The last round was stopped part-way through. python3 build.py --check now fails, and in the browser the page shows only the static shell: every tab is empty and nothing responds. The console shows one error:

    Uncaught SyntaxError: Unexpected token ')'

    node --check points at src/app.js line 1392, inside renderFeedback, in the new "does this student have any comments" loop:

          if ((s.comments && s.comments[criteria[hcj].id)) { hasAnyComment = true; break; }

    There is a missing closing square bracket: s.comments[criteria[hcj].id opens [ twice but closes it only once. The line should read, in words: if s.comments exists and s.comments at key criteria[hcj].id is truthy, set hasAnyComment and break. Do not hex-dump or re-print that line. Just rewrite that single line with balanced brackets, then run python3 build.py --check. It must print node --check: OK.

    Then run grep -n "Unexpected\|SyntaxError" on the build-check output, or re-run the check, to make sure no other syntax error remains.

    1. After the build passes, these features from the last round are already in the code and only need to keep working. Don't rewrite them; only fix one if you notice it's broken while looking at nearby code: The Undo button in the tab row, visible on every tab, disabled when there is nothing to undo.

    2. The phone-only "Performance levels" block, with name and points inputs per level, visible at ≤640 px.

    3. The "Criteria reordered." toast with Undo after dragging a criterion, plus toasts after add criterion and add level.

    4. Ctrl/Cmd+Z inside an input, textarea or select doing only native text undo, not the app undo.

    5. The narrow (<500 px) Criterion Difficulty layout with names above the bars, not clipped.

    6. Level labels "Beginning / Developing / Proficient / Exemplary" without the number prefix.

    7. The feedback-sheet FEEDBACK column hidden for students with no comments. This is the code at line 1392.

    When python3 build.py --check prints node --check: OK, stop.

    16 continue 0.7 min · 4 files changed, 36 insertions(+), 26 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer feedback: finish the interrupted "phone levels" change (tested in Chromium at 390 px and 1440 px)

    • The build passes again (node --check: OK) and almost everything works: The Undo button on every tab, the "Criteria reordered." and "Criterion added." toasts, and Ctrl+Z inside text fields leaving the app state alone.

    • The phone "Performance levels" block, with 4 name and 4 points inputs.

    • The unclipped phone difficulty chart, the level labels without number prefixes, the heatmap names in Public Sans, and the hidden empty FEEDBACK column.

    No console errors. Keep all of it.

    The interrupted change left two regressions in the Rubric tab. Fix only these two, in renderRubric in src/app.js and the rubric rules in src/app.css, then run python3 build.py --check and stop.

    1. Phone: the criterion rows no longer stack, and the page scrolls 208 px sideways. At a 390 px viewport, the Rubric tab (the first screen a visitor sees) shows each .rs-crit-row as a squeezed horizontal grid:

      • the name cell ("Purpose &" is cut off), "Max: 4 pts", "Weight ×", then all four descriptor boxes side by side;

      • each .rs-crit-desc-wrap / textarea.rs-crit-desc is only about 75 px wide, so the text shows one or two words per line;

      • the last two columns run off the right edge.

        document.documentElement.scrollWidth - innerWidth is 208. The widest elements are div.rs-crit-desc-wrap (right edges at 440, 518, 598 px).

        Likely cause: renderRubric writes grid-template-columns: minmax(240px,280px) repeat(N,1fr) as an inline style on every row, and inline styles beat the @media (max-width: 640px) { .rs-crit-row { grid-template-columns: 1fr } } rule.

        Expected: at ≤640 px each row stacks vertically, like this:

      • the name, ✕ and drag handle on the first line;

      • "Max: N pts" and the weight input on the second line;

      • then each level's descriptor as a full-width block with its small label above it.

        Fix either by setting the grid columns through a CSS custom property, e.g. style="--rs-cols: minmax(240px,280px) repeat(4,1fr)" with grid-template-columns: var(--rs-cols) on desktop and 1fr at ≤640 px, or by adding !important to the phone rule. Result: scrollWidth equals 390 on the Rubric tab.

    2. Desktop: the new per-descriptor labels clutter the grid. At 1440 px every descriptor textarea now has a label above it ("Level 1 — Beginning", "Level 2 — Developing", …) in large regular-weight text, about 16 px, repeated in every row. It duplicates the level header row and adds about 30 px to every row.

      Expected: hide .rs-crit-desc-label at desktop widths (display: none above 640 px). At ≤640 px, show it as a small uppercase muted label (.7rem, weight 700, letter-spacing .04em, colour var(--muted)) that reads just the level name, e.g. "BEGINNING · 1 PT".

    17 critic 2.0 min · 4 files changed, 206 insertions(+), 110 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer findings (critic round 1 of 3) — correctness bugs, all in src/app.js

    Fix these 5 items in order. Each one was reproduced in a real browser.

    1. Heatmap "click a cell to jump to grading" always opens the LAST student, then grading crashes
    • In renderHeatmap, the cell markup uses data-student-idx="' + si + '" inside the for (var ri = 0; ri < rows.length; ri++) loop. si is the counter of the EARLIER loop, so it is always students.length (30) for every cell. Clicking Evan Ellis opened Della Drummond, and then clicking a level button threw Cannot read properties of undefined (reading 'scores') (students[30] does not exist). Fix: In the loop that builds rows, store the roster index: rows.push({ student: s, idx: si, total: t, max: m, complete: complete });

    • In the cell markup use data-student-idx="' + r.idx + '".

    • Also, at the top of renderGrade after computing idx, write it back: state.gradingStudentIdx = idx; so a stale index can never reach bindGradeEvents or handleGradeKeydown.

    2. Loading a template needs many Undo presses (event listener leak)

    bindRubricEvents runs on every renderRubric and each time adds another change listener to the STATIC #rs-template-select (it lives in body.html, outside #rs-rubric-area). After visiting the Rubric tab 5 times, choosing a template loads it 5 times and Undo must be pressed 5 times to get the old rubric back. Fix: move the whole // Template select block out of bindRubricEvents into init() so it is bound exactly once (or guard it with if (!tplSelect.dataset.bound) { tplSelect.dataset.bound = "1"; ... }). One template load must equal one undo step.

    3. Analytics empty state shows stale KPI numbers from the previous data

    After importing a new roster (no grades yet), the Analytics tab shows "No complete grades yet" but the KPI row above it still says 74.1% class mean, 79.2% median, 27 / 30 fully graded, Purpose & Hypothesis hardest — numbers from the old roster. Fix in renderAnalytics: in the complete.length === 0 branch set document.getElementById("rs-kpi-row").innerHTML = "" (or hide the row), and restore it when data exists. Also the !roster || !roster.students || roster.students.length === 0 branch currently does area.innerHTML = ..., which deletes the chart cards permanently; use the same hide-cards + #rs-analytics-empty approach as the complete.length === 0 branch instead, and clear the KPI row there too.

    4. Number keys score the wrong level after a level is deleted
    • handleGradeKeydown does student.scores[crit.id] = num; — it stores the pressed digit as the level id. Level ids are not positions: after deleting "Developing" the levels are ids 1, 3, 4. Then key 2 stores a non-existent level (no button selected, criterion counted as graded with 0 pts), key 3 selects the 2nd button, and "Exemplary" can never be chosen by keyboard because 4 > levels.length. Fix: In handleGradeKeydown: var lvl = levels[num - 1]; student.scores[crit.id] = lvl.id;

    • In renderGrade, the big number on each level button ('<b>' + lv.id + '</b>') must be the position (lj + 1), so the key shown equals the key to press.

    • The aria-label should say "Score level N (label)".

    5. Criterion Difficulty chart contradicts the "Hardest criterion" KPI when weights differ

    renderCriterionDifficulty sorts and positions criteria by raw weighted points. With Purpose & Hypothesis at weight ×2 its mean is 5.04 of 8 (63%), so the chart draws it as the EASIEST criterion (bottom), while the KPI (percent of max) correctly says it is the hardest. Fix: compute each criterion's mean and SD as a percentage of critMax(crit) (0–100), sort by that percentage (hardest first), scale the x axis 0–100%, draw a thin axis with ticks at 0%, 50%, 100% under the rows, show the value label as e.g. 63%, and keep the tooltip detailed: Purpose & Hypothesis: mean 5.04 / 8 pts (63%) ± 1.84. Update the card subtitle in body.html to "Mean score as % of criterion max, with ±1 SD spread".

    18 critic 1.9 min · 5 files changed, 266 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer findings (critic round 2 of 3) — src/app.js and src/body.html

    The previous 5 fixes all work (heatmap click-to-grade, one-step template undo, empty KPI row, keyboard scoring by position, difficulty chart in %). Keep them. Fix these 5 items in order.

    1. Phone: Criterion Difficulty ±SD bars are stuck at the left edge

    In renderCriterionDifficulty, the if (narrow2) { branch still computes the spread line from raw points: var x1 = xOf(Math.max(0, row.mean - row.sd)); var x2 = xOf(row.mean + row.sd); Because the axis is now 0–100%, the bars render as tiny marks at x≈0 on phones (390 px). Fix: in the narrow branch use the same values as the wide branch: var x1 = xOf(Math.max(0, row.meanPct - row.sdPct)); var x2 = xOf(Math.min(100, row.meanPct + row.sdPct));

    2. Keyboard grading should flow to the next student
    • In handleGradeKeydown, after a number key scores the LAST criterion, state.activeCritIdx = (ai + 1) % criteria.length wraps back to criterion 1 of the SAME student. Fix: add a helper function firstUngradedIdx(student) (returns the index of the first criterion whose score is null, or 0). When ai === criteria.length - 1 and a next student exists: state.gradingStudentIdx++, state.activeCritIdx = firstUngradedIdx(nextStudent), and showToast("Next student: " + nextStudent.name).

    • Whenever the student changes — #rs-grade-prev and #rs-grade-next click handlers, ArrowLeft/ArrowRight in handleGradeKeydown, and the #rs-student-jump change handler — set state.activeCritIdx = firstUngradedIdx(newStudent).

    3. Score Distribution bin labels overlap ("6–8", "8–11", "11–13")
    • renderDistribution uses a float binWidth = range / numBins and Math.round labels, so neighbouring labels share boundaries. Totals are integers. Fix: var binWidth = Math.max(1, Math.ceil((max - min + 1) / 8)); var numBins = Math.ceil((max - min + 1) / binWidth);

    • bin index Math.floor((v - min) / binWidth); label lo + "–" + (lo + binWidth - 1) with lo = min + b * binWidth (just lo when binWidth is 1).

    • tooltip "Total " + label + ": " + count + " students".

    • Draw a dashed vertical line at the class mean (stroke="#191919" stroke-dasharray="4 3") from the top to the x axis, with a small text label "mean " + mean.toFixed(1) at the top.

    4. Comment Bank chips at the bottom of the Grade tab do nothing when clicked

    In renderGrade (// Comment bank management), each .rs-comment-bank__chip is a <button data-bank-idx> with no click handler, and its ✕ is a <span> inside the button (not keyboard reachable). Fix: render each chip as <span class="rs-comment-bank__chip"><button class="rs-comment-bank__use" data-bank-use="i">TEXT</button><button class="rs-comment-bank__chip__del" data-bank-del="i" aria-label="Remove this comment from the bank">✕</button></span>. In bindGradeEvents, clicking [data-bank-use] appends the text to the ACTIVE criterion's comment for the current student (same logic as the [data-bank-crit] chips), saves, re-renders, and shows showToast("Comment added to " + criterionName). Style .rs-comment-bank__use and .rs-comment-bank__chip__del in app.css as borderless transparent buttons that inherit font, with a visible :focus-visible outline.

    5. CSV export: header not escaped, garbled dash in Excel, no LMS-only format, fragile download
    • In exportGradesCSV: Criterion names in the header are pushed without escaping: use header.push(csvEsc(criteria[ci2].name)) (a criterion named "Style, voice" currently shifts every column).

    • Build the file as "" + rows.join("\r\n") so Excel shows the em dash in the title correctly.

    • Replace URL.revokeObjectURL(url); with setTimeout(function () { URL.revokeObjectURL(url); }, 1000); (immediate revoke can cancel the download in Firefox/Safari). Do the same in exportBackup.

    • Add an LMS-ready export: In body.html, next to #rs-export-grades, add <button class="rs-btn rs-btn--ghost" id="rs-export-lms" title="One total column per student, ready for a gradebook import">Export for LMS</button>; change the text of #rs-export-grades to "Export detailed CSV" and give it title="Per-criterion points, total and percent".

    • New function exportLmsCSV(): header Student,ID,SIS User ID,<csvEsc(rubric title + " (" + maxTotal + " pts)")>; one row per student: name, id, id, and total points only when every criterion is graded (empty otherwise). Download as gradebook-import-YYYY-MM-DD.csv with the BOM and delayed revoke. Bind it in init().

    19 critic 1.6 min · 5 files changed, 302 insertions(+), 46 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.
    
    Working method (earlier attempts stalled by reading the file over and over without editing):
    - Do NOT cat src/app.js or src/app.css in full, and do NOT page through app.js range by range; the files are large.
    - For each FEEDBACK item, in order: run one grep -n for the existing function or class name the item gives, view at most 3 short ranges (at most 60 lines each) with sed -n 'A,Bp', then immediately edit the file. Write the fix before looking at anything else.
    - If a grep finds no match, the thing does not exist yet: that is expected for new features. Write the new code now; do not search for it again.
    - Never run the same command twice. Once you have seen a command's output, act on it (edit) instead of looking again.
    - After each item run python3 build.py --check, then move on to the next item. When all items are done, stop.
    

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

    Reviewer findings (critic round 3 of 3) — src/app.js and src/app.css

    All earlier fixes work (keyboard flow to next student, integer histogram bins, bank chips, LMS export, phone difficulty bars, heatmap click-to-grade, template undo). Keep them. Fix these 6 items in order; items 1–4 are small.

    1. Histogram mean line is drawn half a bar too far right, last bin label exceeds the max

    In renderDistribution the mean line uses var meanX = pad.l + ((mean - min) / (max - min || 1)) * cw; but the bars span numBins * binWidth integer values. With min 6, mean 17.8 it is drawn in the middle of the "18–20" bar instead of just left of it. Fix: var meanX = pad.l + ((mean - min + 0.5) / (numBins * binWidth)) * cw; Also clamp the label of the last bin: var hi = Math.min(lo + binWidth - 1, max); and use just lo when hi === lo (the example shows "24–26" although no total can exceed 24).

    2. Weight and level-points edits cannot be undone

    In bindRubricEvents, the .rs-crit-weight change handler and the .rs-level-points change handler mutate state without pushUndo(). Pressing Undo afterwards reverts some OLDER action instead. Fix: call pushUndo(); at the start of both handlers (before changing state), and showToast("Weight updated.") / showToast("Level points updated.") so the toast offers Undo.

    3. Phone (≤640px): heatmap column headers break mid-word ("PURPO SE & HYPOT HESIS")

    Cause: the base rule .rs-heatmap th { ... overflow-wrap: anywhere; word-break: break-word; } (app.css around line 350) plus .rs-heatmap { table-layout: fixed; width: 100%; } squeezes 8 columns into 358 px. Fix: in app.css inside @media (max-width: 640px) add (these selectors are specific enough to win over .rs-heatmap th): .rs-heatmap { width: auto; min-width: 760px; } .rs-heatmap th.rs-heatmap__crit-head { width: 88px; overflow-wrap: normal; word-break: normal; font-size: .64rem; } and make the first column sticky so names stay visible while the table scrolls sideways inside .rs-heatmap-wrap: .rs-heatmap th.rs-heatmap__student, .rs-heatmap td.rs-heatmap__student-cell { position: sticky; left: 0; z-index: 1; background: var(--surface); } Desktop (wider than 640 px) must stay exactly as it is.

    4. Phone (≤640px): criterion names are truncated in the Rubric tab ("Purpose & Hypoth") and "Max: 4 pts / Weight ×" is cramped

    In app.css inside @media (max-width: 640px): .rs-crit-name-cell { flex-direction: column; align-items: stretch; gap: 6px; } .rs-crit-name-cell > div:first-child { width: 100%; } and .rs-crit-name { flex: 1 1 auto; min-width: 0; width: 100%; } .rs-crit-meta { display: flex; align-items: center; gap: 12px; white-space: nowrap; } so the name input gets the full card width on its own row and "Max: 4 pts Weight × [1]" sits on one line below it.

    5. Reordering criteria needs a keyboard / touch alternative

    Drag and drop (HTML5 draggable) does not work on phones or from the keyboard. In renderRubric, next to the .rs-crit-del ✕ button in each row, add two small buttons: <button class="rs-crit-move" data-move-crit="ci" data-dir="-1" aria-label="Move criterion up">↑</button> and the same with data-dir="1" and "Move criterion down" (↓). Disable ↑ on the first row and ↓ on the last. In bindRubricEvents, clicking one does pushUndo(), swaps state.rubric.criteria[ci] with [ci + dir], saveState(), renderRubric(), showToast("Criteria reordered."). Style .rs-crit-move like .rs-crit-del (small, borderless, visible :focus-visible outline). Keep drag and drop working. Update the card subtitle in body.html to "Criteria × performance levels. Drag rows or use ↑ ↓ to reorder. Use templates to start fast."

    6. Analytics: add an "Uneven profiles" card next to Flagged Outliers (the right half of that grid row is empty)
    • Total-score outliers miss students with one very weak criterion, e.g. Xena Xu: 4, 4, 4, 1, 4, 4 (Analysis 3 levels below her own average). In body.html, right after the Flagged Outliers card, add: <div class="rs-card"><h3 class="rs-card__title">Uneven Profiles</h3><p class="rs-card__sub">Fully graded students with a criterion at least 2 levels below their own average level</p><div id="rs-uneven"></div></div> New function renderUneven(complete, criteria) called from renderAnalytics after renderOutliers: For each fully graded student, compute the position of each score in state.rubric.levels (1-based index of the level id) and the student's mean position.

    • Flag every criterion whose position is at least 2 below that mean. One row per flag, sorted by gap (largest first), max 8 rows: student name, criterion name, level label, and the gap as e.g. "−2.5 levels".

    • Clicking a row sets state.gradingStudentIdx to that student's roster index and state.activeCritIdx to that criterion's index, then switchTab("grade").

    • Empty state text: "No uneven profiles — every student is consistent across criteria."

    Reuse the .rs-outlier / .rs-outlier-list classes so it matches the Flagged Outliers card visually.

What the agent was asked

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

Read the specification

Rubric Studio — Build rubrics, grade a class, see the patterns

Category: Education Who it is for: Instructors and TAs, program assessment coordinators

What it should do

Design an analytic rubric, grade a whole roster criterion by criterion with keyboard speed, and get instant analytics on where the class struggled — then export grades for the gradebook.

Data

No external data. Include one example rubric (e.g. a lab-report rubric) and a clearly fictional example roster of 30 students with generated scores so the analytics have something to show.

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

  • Rubric builder: criteria × performance levels grid, points or weights, level descriptors, drag to reorder, templates

  • Grading mode: one student at a time, click or number-key to score each criterion, per-criterion comments with a reusable comment bank

  • Roster import from CSV (name, id) and grade export to CSV compatible with an LMS gradebook import

  • Analytics: score distribution, criterion difficulty (mean and spread), heatmap of students × criteria, flagged outliers

  • Student feedback sheet generator (printable, one per student)

  • Autosave to localStorage, undo, and full JSON backup/restore

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

  • Heatmap cells show tiny white star-shaped marks where rounded cell corners meet (desktop and phone); on phone the sticky name column shows small colored slivers at row edges
  • Desktop grading cards are 160-230 px tall, so the six criteria need scrolling at 1440x900; the requested hover preview of another level's descriptor was never implemented
  • Wording slips: level buttons say '1 pts' and phone descriptor labels say '2 PT'
  • The Feedback tab renders all 30 printable sheets on one long page by default (about 28,800 px on a phone) until a student is picked
  • CSV roster import reads names and IDs only and replaces the roster; grades persist only in this browser's localStorage unless exported as JSON
  • With this data the total-score outlier list can only flag 'Low' students (mean + 1.5 SD exceeds the 24-point max); the Uneven Profiles card covers single-criterion dips