jevbooks

← All design patterns

Scorecard

Composite scoring · 记分卡

The model scores, code adds up

Split one vague judgment into a few separate questions, let each come back with its own number, and do the adding up yourself.

.81.12.55min · max · Σ.12

The problem

You are building a risk check for shell commands. Before anything runs, you want to know how dangerous it is, and anything dangerous goes to a human. So you ask one scoring question: how risky is this command, 0 to 2?

Two weeks in, almost everything comes back 1. A command that wipes a directory but does it inside a sandbox averages out to the middle; so does a harmless command with a scary-looking name. Worse, the 1 tells you nothing about where the risk is, so a human ends up reading the whole thing anyway.

The obvious fix is to write a better question, so you stuff the scoring rules into it: if it deletes data score 2, unless it is in a temporary directory, in which case… The scores get vaguer, not sharper. You have asked one question to both find evidence and do arithmetic, and arithmetic is the one part your code was never going to get wrong.

The solution

The scorecard pattern says: ask about one visible fact at a time, and keep the total in your own code. Break the vague judgment into a few dimensions that do not overlap — does it delete data, does it send local data outward, does it download and run code from elsewhere — plus, where a yes-or-no is too blunt, one ordered level: if this goes wrong, is the damage none, recoverable, or irreversible?

Then write the combining rule as a plain line of code. All of them must hold: take the smallest. Any one is enough: take the largest. They trade off against each other: use weights. Here, any of the three hazards at 0.8 denies the command outright, and a hazard that only reaches 0.45 escalates only if the damage level is at least 1.5.

Finally, print every dimension next to the verdict. The explanation is that row of numbers, not the total: "the downloads-and-runs-code question came back 0.91" is something a person can act on, while "risk: 1.7" is something they have to re-derive.

Ask · Answer · Act

  1. AskPut the command and the scripts it would run — the material Jev is shown, which the docs call the state — in front of it and ask five things at once: three about what it does, one about whether it weakens security, one about how bad the damage would be.
  2. AnswerDeletes data 0.12, sends data out 0.08, runs downloaded code 0.91, weakens security 0.20; damage level 1.7.
  3. ActThe largest of the hazards is 0.91, which is over the deny line, so the command never runs — and the whole row of numbers goes into the log, so the next person can see it was the downloaded-code question that stopped it.

Where the name comes from

Gymnastics scoring. The judges do not announce what the routine was worth; they score difficulty, execution and artistry separately, and the total comes out of a published formula. Let one judge call the total instead and you lose both things that matter: you cannot see where the points went, and you cannot re-score last season when the rules change.

Use it when

The vague judgment breaks into three to five dimensions that do not restate each other, each one a visible fact, and you want to know which of them decided.

Not when

The dimensions turn out to be the same thing in different clothes — "is it done", "are the requirements met", "can we ship" is one number asked three times, not three safeguards. And if there is only one dimension, you have a line to draw, not a scorecard to build.

Projects that do this

  • huncho — Decisions as code on System One models: typed questions, hysteresis, journal, calibration.
  • jev-axi — CLI for TypeSafe's Jev: fast calibrated judgments from the shell.
  • abide — Make coding agents abide by project rules via Jev checks.

All projects with this pattern →

Read this code
  • hunchosrc/compose.ts:65-97

    "All of them" takes the smallest number, "any of them" takes the largest, and weights are applied here: every bit of arithmetic lives in the code, none of it in the model.

  • jev-axisrc/safety.ts:238-261

    Four separate "does it do this" questions plus one "how bad if it goes wrong", combined in code: any of the four at 0.8 denies the command.

  • abidepackages/cli/src/lib/band.ts:11-46

    For a pick-one question, the code adds up the probability of every option it considers a violation, and puts that sum against the lines.

Show me the code
const { answers } = await jev(state, {
  deletes:  noul("Does the command delete, overwrite or reset data?"),
  sends:    noul("Does the command send local data to a remote host?"),
  runs:     noul("Does the command download and execute code from elsewhere?"),
  damage:   score("If this goes wrong, how bad is it?", ["none", "recoverable", "irreversible"]),
});
const hazard = Math.max(answers.deletes.p, answers.sends.p, answers.runs.p);   // any one is enough
const verdict = hazard >= 0.8 ? "deny"
  : hazard >= 0.45 && answers.damage.score >= 1.5 ? "ask" : "allow";
log({ ...answers, hazard, verdict });          // the row explains it; the total would not
How jevbooks recognises it

How do we know a project does this? We put one question about its README to Jev and it returns a percentage; the higher, the more it looks like it. If you copy the question, copy one thing: ask whether the text mentions this one concrete thing, never whether the project is good.

questions.json · sent for every README
{
  "composite-scoring": {
    "type": "noul",
    "instructions": {
      "what": "Does the text describe combining several Jev probabilities or scores in code (min, max, weighted sum, probability mass over a subset of options, or two answers mapped to three outcomes) into one decision?",
      "not_for": "A single question thresholded on its own, or per-question thresholds with no combination."
    },
    "criteria": {
      "true": "The text explicitly describes this. One sentence is enough; other content does not cancel it.",
      "false": "The text does not describe this."
    }
  }
}

Build this with Jev →