Lab 0 — Build the scoreboard first
Time: 20 minutes · Prerequisites: Setup complete
Why this matters
In March 2026, before any of this existed, Northwind's platform team tried to improve their first classifier by editing its prompt. The change was small and obviously correct: they added three sentences clarifying what counted as "urgent," because too many merely-annoyed customers were landing in the urgent queue. Urgent volume dropped 40% the next day. Everyone agreed it worked.
Eleven days later a support lead noticed that safety reports had stopped arriving in the safety queue. The same three sentences that made the model stingier about "urgent" had also made it stingier about escalation, and nobody had a number that would have caught it. They had a before and an after, but the before was a feeling.
This is the ordinary failure mode of prompt work. A prompt change is not a code change: it has no type checker, no compiler, and no stack trace. It has one safety net, and the net has to exist before you start editing or it catches nothing. So you are going to build it before you write a single prompt.
The other reason to start here is more uncomfortable. You are about to hand-label three real tickets, and you will find that you and the person next to you disagree about at least one. That disagreement is not a warm-up exercise — it is the actual difficulty of this problem domain, and you should meet it in minute five rather than in Lab 6.
See the queue this scores against. The twenty tickets in
data/inbound-queue.jsonare real inputs to this service, and the queue playground shows them before and after triage.
Objectives
By the end you can:
- Run a scored eval against a hand-labelled gold set and read the result
- Explain what the confidence gap measures and why it beats raw accuracy
- Defend a scoring choice you disagree with
- Name what a twelve-case set can and cannot detect
Step 1 — read the cases before you run anything
Open evals/dataset.jsonl. Twelve lines, one JSON
object each: a message, the expected labels, and a notes field saying
which rule that case exists to test.
cat evals/dataset.jsonl | jq -r '"\(.id) \(.expected.category)/\(.expected.urgency) \(.notes)"'
Read the notes on eval-03 and eval-08. Neither case is here because it is
typical. Each is here because it sits on a line where two handbook rules touch.
Q1. Twelve cases is small. Argue for it — what does a hand-labelled set lose when it grows to two hundred?
Step 2 — label three tickets yourself, first
Before you run anything, open
data/inbound-queue.json and find
NW-T-1045, NW-T-1047, and NW-T-1060. For each one, write down on paper:
category, urgency, and whether it requires a human.
Do this alone, then compare with the person next to you. Do not skip the comparison — it is the point of the step.
jq -r '.[] | select(.id | IN("NW-T-1045","NW-T-1047","NW-T-1060")) | "\(.id)\n\(.subject)\n\(.message)\n"' data/inbound-queue.json
Q2. Which of the three did you disagree on, and was the disagreement about the ticket or about the definition? Keep your answer — Lab 6 comes back to it.
Step 3 — put a number on the board
npm run eval:quick
About a minute and roughly $0.09 on a warm cache (nearer $0.12 cold). You
get a PASS/FAIL line per case, an accuracy figure, and two confidence
numbers.
The first run reports no baseline yet. Record one:
npm run eval:quick -- --save
That writes evals/baseline.json, which is
checked into git on purpose. From here on, every prompt change you make in
this course produces a diff on that file. The diff is the evidence.
Q3. The runner reports mean confidence on passes and on failures separately, and calls the difference the "gap." Why is the gap more useful than either number alone?
Step 4 — argue about eval-11
One case is labelled deliberately ambiguous. Find it:
grep eval-11 evals/dataset.jsonl | jq .
It may pass or fail on any given run. What it does reliably is score around 0.45–0.48 confidence, against roughly 0.85 on the cases that pass.
That is not the model being unreliable. That is the model being unsure exactly
where a careful human would also be unsure — which is the property that makes
confidence useful for threshold routing rather than decorative.
Now find the other case that flips. Run the scoreboard twice more.
Q4. This repo scores between 10/12 and 12/12 across runs with nothing
changed. eval-11 flips at ~0.46 confidence; eval-03 flips at ~0.71. One of
those is caught by a confidence threshold at 0.6 and one is not. What does that
tell you about shipping confidence as your only safety control?
Q5. Given a two-case run-to-run spread, what is the smallest improvement this set could actually detect? What would you do if you needed to detect one smaller than that?
Your eval accuracy is 11/12. One case fails. What do you check first?
A prompt change moves your score from 10/12 to 11/12. What have you learned?
Step 5 — notice what is not scored
The scorer compares four fields: category, urgency, requires_human, and
entities.requested_remedy. Every response also carries sentiment and
summary, and neither is scored by anything.
That is a deliberate choice, recorded in
evals/lib/score.ts. Before you read the
reasoning there, form your own view.
Q6. Defend the choice not to score summary. Then argue against it. Which
side would you actually ship?
Q7. eval:quick is deterministic and skips the LLM judge. npm run eval
adds the judge for roughly double the cost. The two are closer in price than
you might expect, so cost is not the reason to prefer one. What is?
Checkpoint
You should be able to answer, without looking anything up:
- What does the confidence gap measure, and why not just accuracy?
- When an eval case fails, what do you check before the model?
- What can a 12-case set detect, and what can it not?
- Which flipping case does a 0.6 confidence threshold miss, and why?
- Why is
summaryunscored?
Extension
Write a thirteenth case and add it to dataset.jsonl with a notes field
saying which rule it tests. Make it one you expect the system to fail —
a case the current prompt gets wrong is worth ten that it already handles. Run
npm run eval:quick and watch your accuracy drop. Do not fix it yet; you will
come back to it in Lab 2.
Answers: ../solutions/lab-0.md