Skip to content

Commit ac8f731

Browse files
committed
feat(rubric): add human-calibrated V5.3-r3 candidate
1 parent 053eba9 commit ac8f731

15 files changed

Lines changed: 779 additions & 24 deletions

File tree

.agents/skills/grade-homework/SKILL.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,15 @@ unambiguous equivalent expressions satisfy an `equivalent_form_accepted`
249249
requirement. A required canonical simplification can be withheld only when the
250250
course rubric explicitly declares it required.
251251

252-
When a rubric declares a local-error or carry-forward rule, preserve the
253-
downstream final-result criterion for the direct consequence of that one local
254-
error. Treat a wrong formula, wrong governing relation, or invalid method as a
255-
method criterion, not as a local arithmetic error. Ignore extra work that is
256-
irrelevant to every declared criterion; evaluate extra work only when it
257-
directly contradicts a declared criterion.
252+
When a final-result criterion is separately declared, withhold it when the
253+
reported final result is wrong, even when an earlier local error caused that
254+
result. The no-double-count rule protects only dependent process criteria; it
255+
does not preserve an independently allocated final-result criterion. A course
256+
rubric may define a narrow exception only by explicitly declaring a conditional
257+
carry-forward rule. Treat a wrong formula, wrong governing relation, or invalid
258+
method as a method criterion, not as a local arithmetic error. Ignore extra
259+
work that is irrelevant to every declared criterion; evaluate extra work only
260+
when it directly contradicts a declared criterion.
258261

259262
Freeze the grading protocol before student grading starts:
260263

.claude/skills/grade-homework/SKILL.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,15 @@ unambiguous equivalent expressions satisfy an `equivalent_form_accepted`
249249
requirement. A required canonical simplification can be withheld only when the
250250
course rubric explicitly declares it required.
251251

252-
When a rubric declares a local-error or carry-forward rule, preserve the
253-
downstream final-result criterion for the direct consequence of that one local
254-
error. Treat a wrong formula, wrong governing relation, or invalid method as a
255-
method criterion, not as a local arithmetic error. Ignore extra work that is
256-
irrelevant to every declared criterion; evaluate extra work only when it
257-
directly contradicts a declared criterion.
252+
When a final-result criterion is separately declared, withhold it when the
253+
reported final result is wrong, even when an earlier local error caused that
254+
result. The no-double-count rule protects only dependent process criteria; it
255+
does not preserve an independently allocated final-result criterion. A course
256+
rubric may define a narrow exception only by explicitly declaring a conditional
257+
carry-forward rule. Treat a wrong formula, wrong governing relation, or invalid
258+
method as a method criterion, not as a local arithmetic error. Ignore extra
259+
work that is irrelevant to every declared criterion; evaluate extra work only
260+
when it directly contradicts a declared criterion.
258261

259262
Freeze the grading protocol before student grading starts:
260263

benchmark/core/model_runner.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
from .model_policy import bind_model_release_policy
1212
from .packets import directory_digest, validate_packet_output_contract
1313
from .run_metadata import validate_run_metadata
14-
from .rubrics import execution_criterion_ids, execution_criterion_points
14+
from .rubrics import (
15+
execution_criterion_ids,
16+
execution_criterion_points,
17+
execution_scoring_gates,
18+
)
1519
from .schema import (
1620
CONFIDENCE_LEVELS,
1721
GRADING_OUTPUT_CONTRACT_DEDUCTION_TRACE_V1,
@@ -1036,6 +1040,7 @@ def _validate_grade_payload(
10361040
for record in records:
10371041
permitted_criteria = execution_criterion_ids(rubric, record.question_id)
10381042
criterion_points = execution_criterion_points(rubric, record.question_id)
1043+
scoring_gates = execution_scoring_gates(rubric, record.question_id)
10391044
if permitted_criteria is None:
10401045
continue
10411046
traced_criteria: set[str] = set()
@@ -1056,6 +1061,28 @@ def _validate_grade_payload(
10561061
raise ValueError(
10571062
f"{record.question_id} deduction_trace points_deducted must equal the declared criterion points"
10581063
)
1064+
declared_gate_ids = set(scoring_gates or {})
1065+
used_gate_ids = declared_gate_ids & traced_criteria
1066+
if used_gate_ids:
1067+
if len(used_gate_ids) != 1 or len(traced_criteria) != 1:
1068+
raise ValueError(
1069+
f"{record.question_id} scoring-gate deduction must be the only deduction_trace entry"
1070+
)
1071+
gate_id = next(iter(used_gate_ids))
1072+
gate = scoring_gates[gate_id]
1073+
if abs(float(record.score) - float(gate["score_cap"])) > 1e-9:
1074+
raise ValueError(
1075+
f"{record.question_id} scoring-gate deduction requires score_cap"
1076+
)
1077+
gate_trace = next(
1078+
trace
1079+
for trace in record.deduction_trace
1080+
if trace.rubric_criterion == gate_id
1081+
)
1082+
if gate_trace.deduction_type != gate["deduction_type"]:
1083+
raise ValueError(
1084+
f"{record.question_id} scoring-gate deduction_type must match the declared gate"
1085+
)
10591086
if "total" not in payload:
10601087
raise ValueError("total is required")
10611088
# Leaf score rows have already passed the course-specific coverage, range,

benchmark/core/rubrics.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
from typing import Any
77

8-
from .schema import CourseSpec, QuestionSpec
8+
from .schema import CourseSpec, DEDUCTION_TYPES, QuestionSpec
99

1010

1111
REQUIRED_LEVELS = (
@@ -193,6 +193,11 @@ def validate_execution_contract_rubric(
193193
question, question_id, course_question, criterion_ids
194194
)
195195
)
196+
findings.extend(
197+
_validate_execution_scoring_gates(
198+
question, question_id, course_question, criterion_ids
199+
)
200+
)
196201

197202
for question_id in _duplicates(question_ids):
198203
findings.append(f"duplicate rubric question ID: {question_id}")
@@ -223,11 +228,20 @@ def execution_criterion_ids(
223228
criteria = question.get("criteria")
224229
if not isinstance(criteria, list):
225230
return None
226-
return {
231+
criterion_ids = {
227232
criterion["id"]
228233
for criterion in criteria
229234
if isinstance(criterion, dict) and isinstance(criterion.get("id"), str)
230235
}
236+
scoring_gates = question.get("scoring_gates", [])
237+
if not isinstance(scoring_gates, list):
238+
return None
239+
criterion_ids.update(
240+
gate["id"]
241+
for gate in scoring_gates
242+
if isinstance(gate, dict) and isinstance(gate.get("id"), str)
243+
)
244+
return criterion_ids
231245
return None
232246

233247

@@ -247,13 +261,57 @@ def execution_criterion_points(
247261
criteria = question.get("criteria")
248262
if not isinstance(criteria, list):
249263
return None
250-
return {
264+
criterion_points = {
251265
criterion["id"]: float(criterion["points"])
252266
for criterion in criteria
253267
if isinstance(criterion, dict)
254268
and isinstance(criterion.get("id"), str)
255269
and _is_number(criterion.get("points"))
256270
}
271+
scoring_gates = question.get("scoring_gates", [])
272+
if not isinstance(scoring_gates, list):
273+
return None
274+
for gate in scoring_gates:
275+
if (
276+
isinstance(gate, dict)
277+
and isinstance(gate.get("id"), str)
278+
and _is_number(gate.get("score_cap"))
279+
and _is_number(question.get("max_score"))
280+
):
281+
criterion_points[gate["id"]] = float(question["max_score"]) - float(
282+
gate["score_cap"]
283+
)
284+
return criterion_points
285+
return None
286+
287+
288+
def execution_scoring_gates(
289+
rubric: dict[str, Any], question_id: str
290+
) -> dict[str, dict[str, Any]] | None:
291+
"""Return declared execution-contract score caps keyed by gate ID."""
292+
293+
if rubric.get("rubric_format") != EXECUTION_CONTRACT_FORMAT:
294+
return None
295+
questions = rubric.get("questions")
296+
if not isinstance(questions, list):
297+
return None
298+
for question in questions:
299+
if not isinstance(question, dict) or _question_id(question) != question_id:
300+
continue
301+
scoring_gates = question.get("scoring_gates", [])
302+
if not isinstance(scoring_gates, list):
303+
return None
304+
return {
305+
gate["id"]: {
306+
"score_cap": float(gate["score_cap"]),
307+
"deduction_type": gate["deduction_type"],
308+
}
309+
for gate in scoring_gates
310+
if isinstance(gate, dict)
311+
and isinstance(gate.get("id"), str)
312+
and _is_number(gate.get("score_cap"))
313+
and isinstance(gate.get("deduction_type"), str)
314+
}
257315
return None
258316

259317

@@ -326,6 +384,49 @@ def _validate_execution_criteria(
326384
return findings
327385

328386

387+
def _validate_execution_scoring_gates(
388+
question: dict[str, Any],
389+
question_id: str,
390+
course_question: QuestionSpec,
391+
criterion_ids: list[str],
392+
) -> list[str]:
393+
if "scoring_gates" not in question:
394+
return []
395+
scoring_gates = question["scoring_gates"]
396+
if not isinstance(scoring_gates, list) or not scoring_gates:
397+
return [f"{question_id} scoring_gates must be a non-empty list when present"]
398+
399+
findings: list[str] = []
400+
required = {"id", "score_cap", "trigger", "deduction_type"}
401+
for index, gate in enumerate(scoring_gates):
402+
label = f"{question_id} scoring_gates[{index}]"
403+
if not isinstance(gate, dict) or set(gate) != required:
404+
findings.append(
405+
f"{label} must contain exactly id, score_cap, trigger, deduction_type"
406+
)
407+
continue
408+
gate_id = gate["id"]
409+
if not isinstance(gate_id, str) or not gate_id.strip():
410+
findings.append(f"{label} id must be non-blank text")
411+
else:
412+
criterion_ids.append(gate_id)
413+
score_cap = gate["score_cap"]
414+
if not _allows_score(course_question, score_cap) or _same_number(
415+
score_cap, course_question.max_score
416+
):
417+
findings.append(
418+
f"{label} score_cap must be below the maximum and use the score step"
419+
)
420+
trigger = gate["trigger"]
421+
if not isinstance(trigger, str) or not trigger.strip():
422+
findings.append(f"{label} trigger must be non-blank text")
423+
elif AMBIGUOUS_RUBRIC_LANGUAGE.search(trigger):
424+
findings.append(f"{label} trigger must avoid unresolved discretionary language")
425+
if gate["deduction_type"] not in DEDUCTION_TYPES:
426+
findings.append(f"{label} deduction_type is invalid")
427+
return findings
428+
429+
329430
def _validate_concept_question(
330431
rubric_question: dict[str, Any],
331432
question_id: str,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Candidate Grading Prompt v5.3 r3
2+
3+
You are grading anonymous student work in a reproducible blind experiment.
4+
5+
Use only the files inside the current prompt packet:
6+
7+
- `INSTRUCTIONS.md`
8+
- `manifest.json`
9+
- `course.json`
10+
- `rubric.json`
11+
- `output.schema.json`
12+
- files under `inputs/`, including each available `inputs/<student_id>/submission.json`
13+
14+
For each anonymous student ID in `manifest.json`, grade only the supplied work
15+
against the frozen rubric. Produce exactly one JSON file per student under
16+
`outputs/`, named `<student_id>.json`. Do not inspect parent directories,
17+
other packets, prior outputs, identities, reference materials outside this
18+
packet, or other students' work.
19+
20+
## Submission assembly
21+
22+
The entire anonymous submission is the grading unit, never an individual page.
23+
Read every ordered page listed for the student in
24+
`inputs/<student_id>/submission.json`; that file is the authoritative page
25+
list. A page may contain evidence for multiple questions, and a question may
26+
continue across pages. Gather evidence for each declared leaf across the whole
27+
submission before assigning one score for that leaf.
28+
29+
Page position, source-page number, image filename, and input index identify
30+
only source/display order. They are never question numbers or a question-to-
31+
page mapping. Do not assume that the first page is Q1 or that different
32+
submissions use the same physical question order. If the supplied content
33+
cannot reliably establish a relevant page, flag `page_order_uncertain` rather
34+
than assigning credit by position.
35+
36+
## Scoring rules
37+
38+
1. Score only the declared leaf items, their declared criteria, permitted score
39+
increments, answer-only caps, and declared scoring gates. Do not merge,
40+
borrow, average, curve, or offset credit across leaves.
41+
2. Record visible evidence before assigning points. Do not guess hidden intent,
42+
repair an error, or infer unreadable work.
43+
3. Accept an unambiguous equivalent expression, notation variant, ordering, or
44+
viable alternative method when it satisfies the same declared criterion. A
45+
reference solution is an evidence anchor, not an exhaustive whitelist.
46+
4. Ignore extra work unless it directly contradicts a declared criterion. Do
47+
not require a standard presentation when an equivalent valid route is
48+
visible.
49+
5. For selected-response leaves, assess only the selected response unless the
50+
stem explicitly requires explanation or working.
51+
6. For calculation leaves, distinguish a local notation/arithmetic error, a
52+
wrong formula or governing relation, absent required work, failed required
53+
simplification, and an incorrect final result. Preserve only independently
54+
demonstrated process credit specified by the frozen rubric.
55+
7. A wrong final answer does not earn an independently allocated final-result
56+
criterion merely because an earlier error caused it. The no-double-count rule
57+
protects dependent process criteria, not a separately allocated final result.
58+
Do not infer any carry-forward exception.
59+
8. When symbolic equivalence is required, check factored, expanded, reordered,
60+
and otherwise unambiguous equivalent forms before withholding final-expression
61+
credit. When the rubric makes a simplification conditional on the student's
62+
preceding expression, evaluate that process criterion against that preceding
63+
expression separately from the final-expression criterion.
64+
9. If a declared scoring-gate trigger is visibly present, apply its score cap
65+
instead of the ordinary subtotal. Its deduction trace must contain only that
66+
gate ID and its declared deduction type.
67+
10. Apply a correct answer with no required visible work only up to the frozen
68+
answer-only cap.
69+
70+
## Deduction trace contract
71+
72+
For every non-full-credit leaf, emit a concise `deduction_trace` with one or
73+
more entries. Each entry has exactly:
74+
75+
- `rubric_criterion`
76+
- `observed_evidence_or_missing_or_incorrect_part`
77+
- `deduction_type`
78+
- `points_deducted`
79+
80+
Every trace must be checkable against visible work and the frozen rubric. Its
81+
point total must equal `max_score - score` for that leaf. Use the exact declared
82+
criterion or scoring-gate ID, and use its declared point value. This is a short
83+
audit statement, not hidden reasoning or a chain of thought.
84+
85+
Full-credit leaves omit `deduction_trace`. A zero score requires a specific
86+
missing or incorrect reason. A leaf with flags or `low` confidence requires a
87+
brief `attention_note`. Treat bonus leaves as independent: never use a base-leaf
88+
deduction to explain, offset, or replace a bonus decision.
89+
90+
Do not put a name, student identifier, email address, private path, raw file
91+
reference, or unneeded student answer transcription in a deduction trace or
92+
attention note.
93+
94+
## Final checks
95+
96+
Before writing output, revisit every unreadable or cropped region, blank or
97+
apparently missing answer, page-order concern, low-confidence score,
98+
high-impact deduction, total mismatch, and handwriting-dependent
99+
interpretation. Recheck semantic equivalence, score increments, arithmetic,
100+
alternative methods, scoring gates, and deduction-trace totals.
101+
102+
Use exactly the JSON schema in `output.schema.json`. Recompute `total` from
103+
the leaf scores and apply the course total cap only after scoring every leaf,
104+
including any earned bonus leaf. Do not change the rubric, packet, score policy,
105+
or prompt while grading. Record uncertainty for course-owner review.

0 commit comments

Comments
 (0)