Scriptorium · Rubric

The Rubric

The schema instructors author, the lifecycle it moves through, and the validation gates that decide whether it is fit to grade with.

Reference · Schema · Lifecycle · Validation

How Scriptorium works → Human roles & access → Genre manual · 117 genres →
§ 1

At a glance · three principles

A rubric in Scriptorium is a single JSON object — but it carries a strong contract behind that JSON. Every rubric the system grades against, however it was authored, must satisfy three principles. The rest of this page is the contract behind those three lines.

  1. Two tiers Every rubric is trait (main criterion) → sub-criterion. Three or more tiers explode combinatorially; one is too coarse to grade.
  2. Cumulative levels L1 is the floor. Every level above L1 must explicitly contain everything the previous level required, plus one observable increment. A paper that earns L4 has, by definition, already satisfied L3, L2, and L1.
  3. JSON, distributive A rubric is a single JSON object. Its traits are independent — any trait can be scored on a paper without reference to any other trait's score. This is what makes parallel AI grading possible.
scope

A "rubric" here is the artifact instructors and reviewers author and apply to a submitted paper, essay, code artifact, or peer-review response. It is not the AES scoring model, the calibration round, or the cohort report — those are downstream consumers.

§ 2

The schema

The shape has three layers: the persistence envelope (id, ownership, status, validation report), the gradable core (traits, sub-criteria, levels, descriptors), and the optional metadata (framework, course, references, anchor samples). The smallest object the grader will operate on is below — everything else adds audit, governance, and validation context, but does not change the gradable core.

The minimum gradable shape

{
  "rid":          "r_a1b2c3d4e5f6",
  "title":        "Argumentative Essay — Disciplinary Reasoning",
  "totalPoints":  100,
  "weight":       100,

  "levels": {
    "count":  5,
    "labels": ["Insufficient", "Developing", "Adequate", "Proficient", "Distinctive"]
  },

  "traits": [
    {
      "id":          "thesis",
      "name":        "Thesis & Position",
      "description": "Clarity, specificity, and arguability of the central claim.",
      "weight":      20,
      "subCriteria": [
        {
          "question":    "Is the central claim explicit, specific, and arguable?",
          "weight":      0.45,
          "description": "An evaluator looks for one sentence the reader could disagree with…",
          "descriptorPriors": {
            "L1": "No identifiable thesis…",
            "L2": "A claim is present but generic…",
            "L3": "In addition to L2's claim, the claim is locatable in one sentence, names a specific aspect, and is arguable.",
            "L4": "In addition to L3's specific claim, the claim distinguishes itself from rivals.",
            "L5": "In addition to L4's distinction, the claim identifies a non-obvious tension whose resolution drives the essay."
          }
        }
      ]
    }
  ],

  "descriptorPriors": {
    "thesis": { "L1": "…", "L2": "…", "L3": "…", "L4": "…", "L5": "…" }
  }
}

Sub-criterion question is always a yes-question — never a noun phrase, never a statement. This forces the grader (or AI) to commit to an answer. description names the test the evaluator applies. descriptorPriors is the per-level anchor; the cumulative rule lives here.

Two layers of descriptors

Both traits and sub-criteria carry their own per-level descriptors. They serve different scopes.

Trait-level

The holistic anchor

Read from rubric.descriptorPriors[traitId]. Answers: what does L3 of "evidence quality" look like as a whole?

This is the canonical client-visible location. The grader UI falls back to trait.descriptorPriors and curated trait.descriptors only when the top-level map is missing.

Sub-level

The diagnostic anchor

Read from trait.subCriteria[i].descriptorPriors. Scoped to one specific judgment: what does L3 of "are the sources credible?" look like?

When a grader scores at the sub-criterion level (the default in GradePanel), the trait-level descriptor is the reference the rationale should triangulate toward.

Distributive evaluation

The pipeline fires one LLM call per trait, in parallel. The holistic score is computed after per-trait scores by weighted average — it does not feed back. So a trait descriptor may reference the paper, the genre, the assignment context, or what other writers in this form do; it may not reference another trait's score. This is a hard requirement, not a preference.

completeness contract

A rubric is complete enough to grade with when it has ≥ 3 traits each with id / name / description / weight; 2–6 sub-criteria per trait, each with question / weight / descriptors; trait-level descriptors at the canonical location; rubric.levels declared; and sub-criterion weights within each trait summing to 1.0 ± 0.02. Errors block validation; weight-imbalance warnings auto-normalize. The single source of truth for the check is server/lib/rubricCompleteness.js.

§ 3

Field reference

Top-level fields

FieldTypeReqPurpose
ridstringStable id, format r_<12-hex>. Never reused.
titlestringHuman-readable name.
ownerIdstringThe author's user.uid.
tenantIdstringThe tenant the rubric is filed under.
statusenumdraft · validated · production-validated · hs-validated · published · archived
visibilityenumprivate · tenant-public · curated. Orthogonal to status.
schemaVersionstringPinned (default "1.2").
genrestringoptGenre id from the 117-genre catalog.
constructobjectoptThe construct-alignment declaration: name · definition · framework · subConstructs[].
traitsarrayThe list of scoring traits.
levelsobject{ count, labels[] }. Default is 5 levels with the canonical labels.
descriptorPriorsobject{ [traitId]: { L1..Ln } } — canonical client-visible location.
weightnumberShare of the assignment grade (default 100).
constructTargetsobjectopt{ [tag]: weight } — protects against surface-fluency reward.
defaultAnchorsarrayoptAnchor seed from the genre template.
anchorSamplesarrayoptPromoted at /validate/save: [{ level, text, score, provenance }].
validationobjectoptSaved validation report. null until validated.
createdAt · updatedAtISO dateServer-stamped.

Trait fields · rubric.traits[i]

FieldTypeReqPurpose
idstringKebab-case, stable. Used by traitScores[id] and descriptorPriors[id].
namestringDisplay label — noun phrase ("Evidence & Sources").
descriptionstringOne-paragraph anchor: what the evaluator looks for at the trait scope. Not the level descriptor.
weightnumberShare of the holistic score (1–100). Auto-normalized at grading time.
subCriteriaarray2–6 sub-criteria. Not optional — a trait with zero subs is rejected.
tags · constructTagsstring[]optConstruct labels for analytics and IRR.
anchorSensitivebooleanoptWhether per-paper anchors materially shift scoring.
exemplarRefsobjectopt{ L<n>: ["exemplars/<trait>/L<n>/<id>.json"] }.

Sub-criterion fields · trait.subCriteria[i]

FieldTypeReqPurpose
questionstringA yes-question the grader answers. Not a noun phrase, not a statement.
weightnumberShare of the trait's score, in [0.0, 1.0]. Sub-weights within a trait must sum to 1.0 ± 0.02.
descriptionstringrec1–2 sentence anchor: the test the grader applies. Warning, not error, if missing.
descriptorPriorsobject{ L1..Ln } per-level descriptors scoped to this judgment.
anti-pattern

A sub-criterion with weight: 0 whose question starts with L<n>: is rejected outright as sub_leaked_descriptor — it is a known data-corruption shape from older imports where a level descriptor leaked into the sub-criterion array. Delete the sub; do not try to score it.

§ 4

Lifecycle & transitions

A rubric moves through a small set of states, each with different visibility and editability. The only path from draft to a gradable status runs through the validation wizard.

Rubric lifecycle Draft to Validated (with three tier sub-states) to Published, plus an Archived terminal state and a back-edge from Validated to Draft when the gradable core changes. Draft OWNER PRIVATE not gradable Validated CORE formative · low-stakes PRODUCTION routine deployment HIGH-STAKES grading, certification Published IMMUTABLE · v{n} gradable Archived READ-ONLY past scores valid VALIDATE PUBLISH ARCHIVE EDIT INVALIDATES

State definitions

StateVisibilityEditableGradableSet by
draftowner onlyownernocreateRubric() default
validatedtenant (per visibility)owner (limited)yesCore gates pass
production-validatedtenantowner (limited)yesProduction gates pass
hs-validatedtenantnobodyyesHigh-stakes gates pass
publishedtenant / curatednobodyyesadmin promotion
archivedrole-gatednobodyno — past scorings remain validowner / admin

READABLE_STATUSES in server/lib/rubrics.js is the allow-list: validated · production-validated · hs-validated · published. A rubric outside this set is owner-private regardless of visibility.

Storage

Per CLAUDE.md, durable rubric state lives in GCS keyed by tenant. Per-tenant rubrics under tenants/{tid}/rubrics/; cross-tenant reference data (genre templates, the curated library) under the _db/ prefix.

gs://{bucket}/
├─ tenants/{tid}/rubrics/
│  ├─ {rid}.json                                      // canonical rubric
│  ├─ {rid}/draft-validation.json                     // in-flight validation work
│  └─ {rid}/exemplars/{trait}/L{level}/{sub_id}.json  // anchored exemplars
│
├─ _db/genre_rubrics/                                 // 117-genre template library
│  ├─ {id}.json                                       // HEAD
│  └─ {id}/v{templateVersion}.json                    // immutable snapshots
│
└─ _db/rubric_library/                                // curated cross-tenant rubrics
   └─ r-{n}.json
§ 5

Validation tiers

The validation wizard walks a rubric through a tiered set of quality gates and promotes it to one of three validated statuses. Tiering is FDA-analogous — regulatory burden scales with consequence. Forcing every author through high-stakes rigor would deter use; allowing every author to ship at Core would harm students.

Core

Formative

Formative feedback, exploratory authoring, low-stakes practice.

Gates run
Structural · Independence · Discrimination · Test-Retest · Sensitivity
Typical cost
~80 LLM calls
Promotes to
validated
Production

Routine deployment

Routine deployment in courses, dashboards, large-N feedback.

Gates run
Core + Perturbation Fidelity + Cross-Rater + Anchor Provenance
Typical cost
~140 LLM calls
Promotes to
production-validated
High-stakes

Consequential

Grading, admissions, certification, anything consequential.

Gates run
Production + Fairness/DIF + human-rated anchors required
Typical cost
~220 calls + ≥ 6 human-rated anchors
Promotes to
hs-validated
misconception flag

A validated (Core) rubric is not ready for high-stakes use. The wizard's green checkmark does not imply otherwise — the Report Card surfaces tier prominently, and the Library badges show tier alongside status. Tier upgrades require re-running the additional gates; you cannot promote by editing the status field.

§ 6

The gates

Each gate answers one psychometric question. Pure-function gates run on cached scores (no LLM cost, idempotent); LLM-gates fire calls. Filled dots show which tiers run each gate.

# Gate Question it answers Core · Prod · HS LLM cost
1 Structural Integrityv1: Schema Is the rubric structurally well-formed? 0 (pure)
2 Trait-Descriptor Disjointnessv1: Independence Do trait descriptors describe orthogonal axes? 1
3 Known-Groups Discriminationv1: Discriminability Do good/ord/bad anchors separate and use the scale's range? 18
4 Intra-Rater Test-Retest Reliabilityv1: Robustness Does the same rater produce the same scores on re-runs? 18
5 Causal Sensitivity to Targeted Editsv1: Sensitivity Do targeted perturbations move the targeted trait's score — and only that one? 42
5a Perturbation Fidelity Auditnew in v2 Do the generated perturbations actually damage the trait they were meant to? ~12
6 Cross-Rater Agreementnew in v2 Do two different raters (LLMs or human) agree on the same papers? ~30
7 Anchor Provenancenew in v2 Where do the anchor labels come from — and is the source defensible for the stakes? 0
8 Fairness / DIFnew in v2 Does the rubric score equivalent-content papers differently based on surface markers? ~30
CI Negative-Control Sanitybuild-time fixture Does the pipeline fail when given a deliberately broken rubric?

The Anchor Provenance gate requires LLM-generated anchors at Production tier (with cross-model separation to avoid circularity), but requires human-rated anchors with κ ≥ 0.61 at High-stakes. The CI gate is a fixture that runs against tests/fixtures/broken-rubric.json and asserts at least 3 of 5 Core gates fail — it catches gate-leniency regressions over time.

What the headline gates actually do

Structural Integrity
The completeness contract from §2 — missing trait id, no levels, sub weights don't sum to 1.0, leaked-descriptor subs. Zero LLM calls.
Disjointness
One LLM call returns a pairwise overlap matrix across trait descriptors; pairs above independenceThreshold are flagged. Measures overlap among the rubric text, not among scored data.
Discrimination
Score three anchors (good · ord · bad) × N traits, then check both gap (adjacent levels separate) and range coverage (good ≥ 4.0, bad ≤ 2.0 on a 1–5 scale). Reports point estimate ± SE and 95% CI per trait.
Test-Retest
Score one sample R times with the same rater; compute per-trait SD; gate on threshold. Reports per-trait ICC(2,1) with 95% CI — informational at Core, gating at High-stakes (robustnessICCMin = 0.75).
Sensitivity
For each (sample, trait), generate a perturbation that damages that trait specifically; rescore; verify the targeted drop, that other traits don't drift, and that neutral edits don't move scores.
Cross-Rater
Re-score with a second rater (default Anthropic + OpenAI pairing). Compute per-trait ICC and Krippendorff's α with ordinal weights. Default pass thresholds: ICC ≥ 0.60, α ≥ 0.667.
Fairness / DIF
Generate matched-content variants differing only in surface markers (L2 phrasing, register shifts, dialectal markers); rescore; check mean drift per (variant family × trait) is below threshold. A lite DIF check — real DIF on cohort data is a separate workstream.
§ 7

Wizard & architecture

The validation wizard is a tier-dependent flow (7 steps at Core, 9 at Production, 11 at High-stakes) opened from Rubric Studio. The architecture is forced by one binding constraint: codespace and reverse-proxy gateways enforce a 30–60 second per-request timeout, and Claude Opus on a long writing routinely takes 15–45 s per scoring call. Three crosscutting design decisions follow.

A · Per-item fan-out

One LLM call, one HTTP request

Every step that does serious LLM work is split into per-item calls driven sequentially from the client — per (sample, trait), per (repeat, trait), per perturbation job. Each call lands in < 30 s. Retries fire only failed items; successful items are durable. Non-negotiable — do not reintroduce a bulk endpoint.

B · GCS-backed drafts

Closed tab loses nothing

Per-trait reviews are LLM-expensive. They land at tenants/{tid}/rubrics/{rid}/draft-validation.json as they complete. The fingerprint covers rubric content, scoring model id + version, prompt hash, and tier; any change invalidates the draft on hydration. Catches model bumps, not just rubric edits.

C · Pure-function gates

Recompute without re-calling

Gate verdicts (assess*) are pure functions of cached data, served at dedicated */assess endpoints — zero LLM cost, idempotent. The wizard can recompute a gate without re-firing scoring calls. All v2 gates follow the same pattern.

D · Stat isolation

Psychometrics is library code

ICC, Krippendorff's α, t-CIs, Mantel-Haenszel-style DIF — all live in server/lib/psychometrics.js as pure functions, dependency-light, testable. A research mode can run them on archived score data without touching the LLM stack.

Wizard step rail

                              Core   Production   High-stakes
1 · Configure                  ✓        ✓             ✓     // tier, model, thresholds
2 · Independence               ✓        ✓             ✓     // pairwise overlap matrix
3 · Anchor Provenance          —        ✓             ✓     // LLM gen / upload / hybrid
4 · Samples                    ✓        ✓             ✓     // generate or load anchors
5 · Discrimination             ✓        ✓             ✓     // score baseline + range coverage
6 · Test-Retest Reliability    ✓        ✓             ✓     // R-repeat scoring + ICC
7 · Cross-Rater Agreement      —        ✓             ✓     // re-score with alt rater
8 · Causal Sensitivity         ✓        ✓             ✓     // perturb + score
9 · Perturbation Fidelity      —        ✓             ✓     // audit subsample
10 · Fairness / DIF            —        —             ✓     // matched-content variants
11 · Save                      ✓        ✓             ✓     // persist report; promote status

Past steps are revisitable (rail shows ✓); forward navigation without a gate pass is blocked. Tier downgrade at save is silent (a Production-attempt that only completed Core gates saves at Core); tier upgrade reopens the wizard and runs only the new gates against the previously-saved data.

§ 8

Reporting & freshness

Every saved validation produces a Report Card surfaced in Rubric Studio and the Library. It is the artifact downstream consumers (Calibration Studio, the Bias & Fairness Audit, Cohort Analytics) actually read. The Report Card always carries:

The freshness indicator

A validated rubric is not fresh forever. Anthropic, OpenAI, and Google all rev models; yesterday's validation does not hold for free. The Library shows a freshness state on every validated rubric:

StateTriggerAction
FreshAll version fields match current production model + promptNone
Stale-modelScoring model upgraded since last validationSoft warning; "Re-validate" CTA
Stale-promptSystem prompt changedSoft warning
Stale-rubricRubric edited since last validationHard warning; status reverts to draft
why this matters

The silent-failure mode this catches is the one where a validated rubric quietly becomes invalid because the underlying scoring model shipped a new version overnight. The fingerprint covers scoringModelVersion and promptHash — when either drifts, the rubric is flagged as stale and authors are given a one-click path to re-validate. The gradable core itself is invalidated entirely on edit; that one is a hard revert to draft.