# Rubric Library: five rules and a five-check script (free sample)

Version 1.0, 2026-10-28. By PaperChaseWebb. For anyone who lets an agent score, grade or triage things, in Grok Bot, Claude Code, Codex, Hermes or a mix, and wants the scoring rules written down where the agent can load them. One Markdown file and one Python 3 script, standard library only. The script was tested on its worked example (5 findings, then 0) and on a missing file (exit 2). It was not run inside any agent harness or on a real scoring workflow.

Every "score these leads", "triage this inbox" or "rank these posts" prompt has a rubric in it. Usually it lives in one person's head or one chat thread, has no version, and changes a little every time someone rewrites the prompt. The scores look precise and mean whatever the agent guessed that day. The rubric is the product: write it down, give it an owner and a number, and say what each grade makes happen. These five rules cover the gaps that cost the most, and the script checks them in one small file.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('rubric_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 rubric_mini.py rubric-lite.json
```

## Five rules

1. **Every rubric library has a human owner.** A person decides what the grades mean. Not "TBD", and not the agent doing the scoring.
2. **Every rubric has a number.** An agent loads hook-pattern 1.2, not "the usual one". Change the wording, change the number.
3. **Every grade says what it means and what it triggers.** A grade with no definition gets applied by feel. A grade with no action is a number nobody uses.
4. **People, clients and money go to a person.** If a score feeds a decision about a person, a client or money, a person makes that decision every time, whatever the confidence.
5. **No profiling people who did not opt in.** Score material you own or were part of: your saved posts, your calls, your inbox. Not strangers.

## The five checks

| Id | Check |
|---|---|
| M1 | the library names a human owner |
| M2 | every rubric has a numbered version |
| M3 | every grade has a definition and an action |
| M4 | rubrics touching a person, a client or money have human_gate always |
| M5 | the refusal line forbids profiling non-consenting persons |

human_gate is always (a person decides every item), below_threshold (a person decides only low-confidence items) or never. Exit 0 clean, 1 findings, 2 missing or unreadable file. The script only reads.

## Worked example

A made-up coffee roaster with two rubrics: one grades hooks in posts the owner saved, one flags churn risk in customer calls the owner joined. Before (rubric-lite.json):

```json
{
 "owner": "TBD",
 "refusal": "We only score what helps the business.",
 "rubrics": [
  {
   "rubric_id": "hook-pattern",
   "version": "latest",
   "material": "posts and pages the owner saved",
   "decision": "which patterns to adapt into original drafts",
   "human_gate": "below_threshold",
   "grades": [
    {
     "grade": "strong",
     "definition": "names a concrete problem and a specific result in the first line",
     "action": "add to swipe notes with a one-line adaptation"
    },
    {
     "grade": "usable",
     "definition": "clear shape, vague result",
     "action": ""
    },
    {
     "grade": "noise",
     "definition": "no repeatable structure",
     "action": "skip"
    }
   ]
  },
  {
   "rubric_id": "churn-signal",
   "version": "1.0",
   "material": "transcripts of customer calls the owner joined",
   "decision": "which accounts get a check-in from a person",
   "human_gate": "below_threshold",
   "grades": [
    {
     "grade": "yes",
     "definition": "the customer states friction, a missed expectation or confusion about scope or price",
     "action": "flag for a person to call"
    },
    {
     "grade": "no",
     "definition": "no stated friction",
     "action": "keep in the monthly review list"
    }
   ]
  }
 ]
}
```

Output:

```
M1 the library has no human owner
M2 rubric hook-pattern has no numbered version (got 'latest'); agents cannot load it by name
M3 rubric hook-pattern grade usable is missing a definition or an action
M4 rubric churn-signal feeds a person, client or money decision but human_gate is below_threshold
M5 the refusal line does not forbid profiling non-consenting persons
5 checks, 5 findings
```

The fixes: Dana is named as the owner, "latest" becomes 1.2, the usable grade gets its action (note the shape only), the churn rubric goes to human_gate always so a person decides every flagged account, and the refusal line says plainly that non-consenting persons are not profiled. After:

```
5 checks, 0 findings
```

## One prompt to try

Paste into your agent: "Look through the last month of prompts where I asked you to score, rank, grade or triage anything. For each, write down the rubric you were actually using: the grades, what each meant and what it led to. Mark every place you guessed. Do not change anything; I will."

## What the full pack adds

The Rubric Library (pp-rubric-library, $19) turns these rules into a library your agents load before they score. Six small files: the library, rubrics, grades, evidence rules, examples with borderline cases, and an adversarial set for each rubric. rubric.py loads a named version or answers unverdicted, and checks every proposed score: ok, below threshold (a person decides), human (a person decides, always), or unverdicted. Nothing scores while the library is unratified. The owner ratifies the library and approves each scoring run at their own terminal, tied to a sha256 and refused inside an agent or on piped input. Fifteen read-only checks, 64 tests, a skill file for Claude Code, Codex and Hermes, an AGENTS.md snippet and a Grok Bot rules block. If you want it built with you inside a real workflow, that is the Install sprint on paperchasewebb.ai.

## The script

~~~python
#!/usr/bin/env python3
"""rubric_mini.py: five read-only checks on a one-file rubric library (rubric-lite.json).
Usage: python3 rubric_mini.py rubric-lite.json
Exit 0 clean, 1 findings, 2 missing or unreadable file. Standard library only. Writes nothing."""
import json, re, sys

PLACEHOLDERS = {"", "tbd", "todo", "owner", "someone", "agent", "ai", "team", "default"}
PERSON = ("person", "people", "client", "customer", "recruit", "candidate", "hire", "refund", "money", "pay", "spend", "contract", "legal")


def main(a):
    if len(a) < 2:
        print(__doc__); return 2
    try:
        m = json.load(open(a[1]))
    except Exception as e:
        print(f"cannot read {a[1]}: {e}", file=sys.stderr); return 2
    F = []
    if str(m.get("owner", "")).strip().lower() in PLACEHOLDERS:
        F.append(("M1", "the library has no human owner"))
    for r in m.get("rubrics", []):
        rid = r.get("rubric_id", "?")
        if not re.match(r"^\d+(\.\d+){0,2}$", str(r.get("version", ""))):
            F.append(("M2", f"rubric {rid} has no numbered version (got '{r.get('version', '')}'); agents cannot load it by name"))
        for g in r.get("grades", []):
            if not str(g.get("definition", "")).strip() or not str(g.get("action", "")).strip():
                F.append(("M3", f"rubric {rid} grade {g.get('grade', '?')} is missing a definition or an action"))
        text = (str(r.get("material", "")) + " " + str(r.get("decision", ""))).lower()
        if any(w in text for w in PERSON) and r.get("human_gate") != "always":
            F.append(("M4", f"rubric {rid} feeds a person, client or money decision but human_gate is {r.get('human_gate')}"))
    ref = str(m.get("refusal", "")).lower()
    if "non-consenting" not in ref or "profil" not in ref:
        F.append(("M5", "the refusal line does not forbid profiling non-consenting persons"))
    for c, msg in sorted(F):
        print(c, msg)
    print(f"5 checks, {len(F)} findings")
    return 1 if F else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
~~~

Made-up example; no real business or results are described.

PaperChaseWebb 💸
