Skip to content

feat(marking): add deterministic exam feedback composer#51

Open
hyperpolymath wants to merge 2 commits into
mainfrom
feature/exam-feedback-composer
Open

feat(marking): add deterministic exam feedback composer#51
hyperpolymath wants to merge 2 commits into
mainfrom
feature/exam-feedback-composer

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Rescues an in-progress exam feedback composer (~1,360 LOC) from the bot-crash recovery branch (commit 29d5a0d on recovery/tma-mark2-after-bot-crash) into a clean, reviewable branch off main.
  • Pure-stdlib Elixir + a Phoenix LiveView — runs standalone: no CubDB, FHI parser or .docx generator needed. Usable for marking today at /exam-feedback-composer.
  • Tutor enters per-question subset marks (with arithmetic expressions like 10+5) → composer averages → derives band → emits deterministic, band-aware "did well" and "where to improve" prose for the two short-form feedback boxes, with copy buttons.

What's in this PR

Commit 1 — feat(marking): add deterministic exam feedback composer

  • lib/etma_handler/marking/exam_feedback_composer.ex (655 LOC) — pure functional composer
  • lib/etma_handler_web/live/exam_feedback_composer_live.ex (329 LOC) — LiveView UI
  • test/etma_handler/marking/exam_feedback_composer_test.exs (106 LOC, 5 cases)
  • assets/js/app.jsCopyText LiveView hook (additive only)
  • lib/etma_handler_web/router.ex — one-line additive route registration + missing copyright header

Commit 2 — feat(marking): wire per-question subset-mark grid + Calculator averaging

  • aggregate_questions/1 — sums per-question marks/maxes and computes a percentage. Each cell accepts arithmetic expressions (10+5) via Logic.Calculator.evaluate/1. Rows with blank/unparseable marks or non-positive max are dropped so partial entries don't poison the total.
  • compose/1 mark resolution: manual Overall mark % (override) wins → else aggregated grid percentage → else :unknown band.
  • LiveView: new "Per-question marks (optional)" section above the components with a live "Total: X / Y = Z%" badge that updates on phx-change. Override field relabelled with auto from grid placeholder.
  • 7 new test cases (12 total, all passing).

Provenance

The full WIP set (14 files, 1369 insertions) lives on recovery/tma-mark2-after-bot-crash from commit 29d5a0d "WIP: preserve bot crash state for tma-mark2". This PR cherry-picks only the composer-related changes onto a clean branch off main, with router edits limited to a single additive line (no plug :fooplug(:foo) reformat noise).

What this PR still excludes (follow-ups for separate PRs)

  • Cockpit link from MarkingLive to /exam-feedback-composer
  • bouncer.ex / audit.ex / calendar.ex / rubric.ex drive-by edits that were in the crash-recovery WIP but unrelated to the composer
  • Persisting the last-used form state (TODO in exam_feedback_composer_live.ex ~line 208)
  • Configurable question grid (currently a hardcoded 3-row default; matches the existing 3-component A/B/C structure)

Test plan

  • Composer + Calculator compile standalone via elixirc (no Phoenix deps required)
  • All 12 tests pass via standalone ExUnit run
  • Tutor manually walks through /exam-feedback-composer against a real exam script and confirms the generated prose is editable/usable
  • Verify the grid → total → band live-update behaviour in a real browser
  • Confirm CopyText hook fires (clipboard write + visual feedback) in a real browser

🤖 Generated with Claude Code

Rescues the in-progress exam feedback composer from the bot-crash
recovery branch (29d5a0d, recovery/tma-mark2-after-bot-crash).

The composer is pure-stdlib Elixir, runs standalone of the unbuilt
v1 data plane (no CubDB, FHI parser or .docx generator needed), and
provides an emergency tool for short-form exam feedback boxes today.

Inputs
  - 3 components (A reflective / B planning / C essay) with ordinal
    rating (strong | sound | adequate | weak | serious_issue | missing)
  - "good" and "improve" indicator checkboxes per component
  - 7 convention/completion toggles
  - 6 killer-issue overrides
  - overall mark percent (band selection only)

Outputs
  - did_well prose, >=120 words, band-aware
  - improve prose, >=120 words, band-aware
  - band metadata and warning flag for manual-override cases

Route: /exam-feedback-composer (LiveView)
Tests: 5 cases. Verified locally via standalone elixirc + ExUnit; all
pass.

Excluded from this PR (follow-ups):
  - per-question subset-mark grid + Logic.Calculator averaging
  - MarkingLive cockpit link to the composer
  - bouncer / audit / calendar / rubric drive-by edits from the
    crash-recovery WIP unrelated to the composer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 65 issues detected

Severity Count
🔴 Critical 3
🟠 High 12
🟡 Medium 50

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Issue in boj-build.yml",
    "type": "missing_timeout_minutes",
    "file": "boj-build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-policy.yml",
    "type": "missing_timeout_minutes",
    "file": "container-policy.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

Adds optional per-question marks alongside the existing single
"overall mark %" input. Marks support arithmetic expressions
(matching the legacy Java behaviour) via Logic.Calculator.evaluate/1.

Mark resolution in compose/1
  - Manual "Overall mark % (override)" wins if parseable
  - Else use the aggregated grid percentage (sum of marks / sum of maxes)
  - Else band is :unknown

Composer module
  - aggregate_questions/1 public API: %{total, max, percentage} | nil
  - Rows are dropped if mark is blank/unparseable or max is non-positive,
    so partial entries do not poison the total
  - effective_mark_for/1 (private) computes the band-driving percentage

LiveView
  - New "Per-question marks (optional)" section above the components
  - Live total/max/percentage badge updates on phx-change
  - "Overall mark % (override)" field relabelled with "auto from grid"
    placeholder
  - Robust handling of Phoenix form params shape (questions arrive as
    a string-indexed map, normalised back to an ordered list)

Tests (7 new cases, 12 total, all passing)
  - aggregate_questions: sum/percentage; expressions; partial-row drop;
    nil on empty input
  - compose with grid: grid-only path -> band; override wins; empty
    grid + empty override -> :unknown

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 65 issues detected

Severity Count
🔴 Critical 3
🟠 High 12
🟡 Medium 50

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Issue in boj-build.yml",
    "type": "missing_timeout_minutes",
    "file": "boj-build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-policy.yml",
    "type": "missing_timeout_minutes",
    "file": "container-policy.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

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