MUSA Graderv0.1.0

Developer guide

Adding an assignment

A new assignment needs two things: a rubric YAML in rubrics/ and a grader class in assignments/ that defines a check_<rubric_id> method for each item. assignments.base takes care of dispatch, the execution check and error handling.

1. Write the rubric#

Start from the closest existing rubric. Decide first which situation the assignment is in, because that determines what the checks can compare against:

  • Every student uses the same distributed data (like Assignment 3): write the expected values into the rubric and match against them.
  • Students fetch their own data (like Assignment 1): check each step against the student's own data, never against a stored number.
  • The data is never handed in (like Assignment 2): grade the outputs saved in the notebook.
# rubrics/hw4.yaml
assignment:
  id: hw4
  name: "Assignment 4: Street Networks"
  version: HW4-v1
  total_points: 100

settings:
  score_rounding: 0.5

discovery:
  notebook_name_hints: ["assignment-4", "assignment4", "hw4"]
  ignore_name_patterns: ["checkpoint", "untitled", "scratch", "solution"]

data:
  required: true
  description: The OSM extract distributed with the assignment.

rubric:
  - id: notebook_execution
    name: Notebook executes successfully
    points: 10
    type: execution
    config:
      partial_credit_ratio: 0.4

  - id: graph_loaded
    name: Street graph loaded with osmnx
    points: 90
    type: deterministic
    config:
      min_nodes: 1000
      wrong_crs_credit_ratio: 0.5

Until a check exists, its item is returned as manual_review, so the rubric loads and runs from the start.

2. Write the grader class#

# assignments/hw4.py
"""Assignment 4 autograder — street networks."""

from __future__ import annotations

from grader import inspection
from grader.rubric import RubricItem
from grader.models import RubricItemResult

from .base import (
    STATUS_FAIL, STATUS_NOT_FOUND, STATUS_PARTIAL, STATUS_PASS,
    AssignmentGrader, GradingContext, register,
)


@register
class HW4Grader(AssignmentGrader):
    assignment_id = "hw4"          # must match assignment.id in the YAML

    # notebook_execution is inherited from AssignmentGrader.

    def check_graph_loaded(self, ctx: GradingContext, item: RubricItem) -> RubricItemResult:
        if not ctx.probe_ok:
            return self.no_probe_result(item, ctx)

        min_nodes = int(item.config.get("min_nodes", 1000))
        frames = [p for p in inspection.dataframes(ctx.probe)
                  if p.get("rows", 0) >= min_nodes]
        if not frames:
            return self.result(item, 0, STATUS_NOT_FOUND, 1.0,
                               "No node table of the expected size was found.")
        if len(frames) > 1:
            # Ambiguity lowers confidence, which sends the item to review.
            return self.result(item, item.points, STATUS_PASS, 0.75,
                               "Several plausible node tables were found.",
                               {"candidates": len(frames)})
        return self.result(item, item.points, STATUS_PASS, 0.98,
                           "Street graph loaded.", {"rows": frames[0]["rows"]})

Then register the module so importing the package loads it. Add it to the imports in assignments/__init__.py and to the import inside get_grader() and registered_assignments() in assignments/base.py:

from . import hw1, hw2, hw3, hw4  # noqa: F401  (registers the assignment graders)

The check contract#

RuleWhy
Return exactly one RubricItemResult, built with self.result(...)result() rounds to score_rounding and clamps to [0, points]
Put the reason in evidenceThe TA sees it on the student page. A deduction a TA can't explain has to be worked out again by hand.
Name the mistake in feedback"Wrong" isn't useful feedback. "Latitude and longitude are swapped" is.
Lower confidence when choosing between readingsAnything below the threshold goes to review. 1.0 means the result was matched exactly.
Read thresholds and ratios from item.configPolicy belongs in the YAML, so it can be fixed without touching Python
Don't worry about raisinggrade() turns an exception into a manual_review item, so a broken check never loses a submission
Never depend on variable namesSearch the probe payload by shape and content using grader.inspection

What a check can see#

Every check receives a GradingContext:

AttributeContents
ctx.analysisNotebookAnalysis: static analysis of the submitted source (imports, functions, read calls, paths, markdown cells)
ctx.executionExecutionRecord: success, timeout, error cell, traceback
ctx.probeThe probe's JSON description of the kernel namespace, or None if the kernel never reported
ctx.workdirThe submission's working directory, including the executed notebook
ctx.qualitative_graderThe LLM grader, or a disabled one. Call .grade(question, response, rubric).
ctx.cacheScratch space shared between checks on one submission

To grade from saved outputs instead of (or as well as) the re-run, use grader.answers for printed values and grader.charts for chart specs.

Hidden tests#

Declare the item as type: hidden_test with a selector and cases (see Hidden-test config). AssignmentGrader.hidden_test_specs() sends only the selector and the inputs to the probe. The probe calls the student's function in-kernel and reports what came back, and your check compares that against the expected values, which stay on the host.

3. Add tests and demo submissions#

Follow the pattern in tests/test_hw3.py and examples/make_hw3_submissions.py: one synthetic submission that does everything right, one that makes the near-misses students really make, and one that crashes. Assert that the correct one scores full marks (or close, on judgement items) and that each near-miss is named in the feedback.

Never commit student work

Reproduce the failure patterns you see in synthetic fixtures. No student file, name or identifier belongs in the repository.