Skip to content

feat(marking): tie grid rows to components; per-component % in prose#54

Open
hyperpolymath wants to merge 1 commit into
feature/composer-configurable-rubricfrom
feature/composer-grid-component-mapping
Open

feat(marking): tie grid rows to components; per-component % in prose#54
hyperpolymath wants to merge 1 commit into
feature/composer-configurable-rubricfrom
feature/composer-grid-component-mapping

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Stacked PR (3 of 4 follow-ups). Base = `feature/composer-configurable-rubric` (PR #53). Review/merge in order: PR #51 -> #52 -> #53 -> this. The diff shown is only the per-component layer on top of (1).

Summary

The per-question grid and the per-component ratings are no longer parallel tracks. Each grid row carries an optional `component_id` linking it back to a rubric component; the composer aggregates marks per component and surfaces the numeric performance inline in the `did_well` prose.

What ships in this PR

  • Rubric packs — default question rows now carry `component_id`:
    • `ou_3_part`: Q1 -> reflective, Q2 -> planning, Q3 -> essay
    • `single_essay`: q1 -> essay
    • Question labels updated for visibility (`"Question 1 (A reflective)"` etc.)
  • `aggregate_by_component/1` — new public API: returns `%{component_id => %{total, max, percentage}}`. Drops un-tagged rows; empty components are absent from the result.
  • `compose/1` — result map gains `:per_component`. `component_good_sentence` appends `" (scoring X / Y =~ Z%)"` when per-component data is available, keeping the existing rating language intact.
  • LiveView — hidden `component_id` input per row so the link survives form submit; a per-component breakdown card grid below the live total badge.

Sample output

Inputs: ou_3_part rubric, Q1=22/25 reflective+strong, Q2=18/25 planning+sound, Q3=40/50 essay+sound.

The reflective learning piece is a strong element (scoring 22 / 25 =~ 88%), particularly insightful reflection. The planning and practical SEN leadership piece is secure (scoring 18 / 25 =~ 72%). The academic essay is secure (scoring 40 / 50 =~ 80%). The overall mark reflects the balance of performance across the reflective learning piece, the planning and practical SEN leadership piece and the academic essay.

Out of scope (follow-ups)

  • Borderline detection — flagging when the tutor's selected rating disagrees with the derived per-component band (e.g. rating "strong" with numeric "fail"). Worth its own PR with a separate UI signal.
  • Per-component ranges in the grid — currently one component per row; supporting many-questions-per-component (e.g. 5-row essay rubric all tagged `essay`) works arithmetically but the UI doesn't make the grouping visible yet.
  • Editable component_id per row — the tutor cannot reassign a row to a different component from the UI; defined by rubric only.

Tests

  • 34 total, all passing (26 prior + 8 new)
  • `aggregate_by_component`: groupings, multi-row merge, drop-untagged, drop-empty-components, nil/non-list input
  • `compose/1` with per-component questions: `did_well` embeds percentages; components without numeric data omit suffix; `single_essay` rubric per-component matches overall

Test plan

  • 34/34 tests pass via standalone elixirc + ExUnit
  • LiveView syntax-checks
  • In a browser: enter per-question marks -> per-component cards appear below the total badge with correct breakdown
  • Generate feedback -> did_well includes "(scoring X / Y =~ Z%)" for components with marks; clean prose for those without
  • Switch rubrics -> per-component breakdown resets correctly

Generated with Claude Code.

Each per-question grid row gains an optional component_id linking it
back to a rubric component. The composer aggregates marks per component
and surfaces the numeric performance inline in the did_well prose.

Rubric pack changes
  - default_questions rows now carry "component_id":
    * ou_3_part: Q1 -> reflective, Q2 -> planning, Q3 -> essay
    * single_essay: q1 -> essay
  - Question labels for ou_3_part updated to show the mapping
    ("Question 1 (A reflective)", etc.) so the linkage is visible
    without inspecting source

Composer module
  - aggregate_by_component/1 public API: %{component_id => %{total, max, percentage}}
    * Groups rows by component_id; ignores rows with no/blank component_id
    * Components with no parseable rows are absent from the result map
    * Returns %{} for nil or non-list input
  - compose/1 result map gains :per_component (the aggregate map)
  - component_good_sentence appends " (scoring X / Y =~ Z%)" when the
    per-component aggregate is available, so the did_well prose surfaces
    the numeric performance without losing the existing rating language
  - Components without numeric data simply omit the suffix; no regression
    for the override-only path

LiveView
  - Hidden component_id input per row so the link survives form submit
  - When the live grid total is shown, also render a per-component
    breakdown card grid below it, one card per rubric component with
    parseable marks

Tests (34 total, all passing — 26 prior + 8 new)
  - aggregate_by_component: per-component groupings, multi-row merge,
    drop-untagged-rows, drop-empty-components, nil/non-list input
  - compose/1 with per-component questions: did_well embeds the
    percentages, components without numeric data omit suffix gracefully,
    single_essay rubric: per-component matches overall

Verified locally via standalone elixirc + ExUnit (34/0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant