feat(marking): runtime-loaded rubric packs from priv/rubrics/#56
Open
hyperpolymath wants to merge 1 commit into
Open
feat(marking): runtime-loaded rubric packs from priv/rubrics/#56hyperpolymath wants to merge 1 commit into
hyperpolymath wants to merge 1 commit into
Conversation
Tutors can add a new exam rubric by dropping an .exs file into
priv/rubrics/ and clicking "Reload rubrics" in the composer UI — no
recompile, no restart.
RubricRegistry refactor
- Built-in @ou_3_part and @single_essay remain compile-time fallbacks
- all/0 returns disk-discovered packs ++ built-ins, with disk packs
shadowing a built-in of the same id (drop in ou_3_part.exs to
override the built-in OU pack without recompiling)
- First read triggers a lazy disk scan, cached in :persistent_term
- reload/0 re-scans and returns %{loaded: N, skipped: [...]}
- rubric_dir/0 is configurable via Application env, falling back to
:code.priv_dir(:etma_handler) <> "/rubrics" (nil when the app
isn't loaded — relevant for standalone module tests)
Robustness
- Files that fail to evaluate, return a non-RubricPack value, or have
an empty id are skipped with a reason (logged out, not raised). A
single broken file does not take the whole registry down.
LiveView
- "Reload rubrics" button next to the rubric selector
- Reload result surfaced via put_flash with the loaded count and any
per-file skip reasons
priv/rubrics/
- README.adoc documents the file shape and the shadowing rule
- example_extended.exs is a working three-component sample (method /
analysis / conclusion) that demonstrates the loading pipeline
Tests (50 total, 5 new runtime cases — async: false, isolated via per-
test tmp dir and persistent_term erase on exit)
- reload picks up a freshly written .exs file
- disk pack with the same id shadows the built-in
- malformed files are skipped with a reason, not raised (covers syntax
error, wrong shape, missing id)
- built-ins remain available when the rubric directory is empty
- reload with nil rubric_dir is a no-op (not a crash)
Verified locally via standalone elixirc + ExUnit (50/0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tutors can add a new exam rubric by dropping an `.exs` file into `priv/rubrics/` and clicking "Reload rubrics" in the composer UI — no recompile, no restart. Built-in rubrics stay as compile-time fallbacks; disk-discovered packs can shadow them on `id` collision (so `priv/rubrics/ou_3_part.exs` overrides the built-in OU pack without a code change).
What ships
Authoring a new rubric
Drop a file like:
```elixir
alias EtmaHandler.Marking.RubricPack
%RubricPack{
id: "my_course_t1",
title: "My course, TMA 1",
components: [%{id: "essay", code: "a", title: "Essay", short: "essay", good: [{"...", "..."}], improve: [{"...", "..."}]}],
default_questions: [%{"id" => "q1", "label" => "Essay", "mark" => "", "max" => "100", "component_id" => "essay"}]
}
```
into `priv/rubrics/`, click Reload rubrics, pick the new rubric in the selector. Done.
Tests
Test plan
🤖 Generated with Claude Code