Jev report · 17 Sep 2026 · jev-1.13.0

Skill selection, measured

I ship a tool that asks Jev, once per skill, whether a job posting calls for it. I had never tested that choice against anything. So: five arms, 743 skills, 12 postings, 8,916 decisions each, plus 240 of the hardest pairs adjudicated twice by hand. Two metrics, and they disagree about who wins — which turned out to be the most useful thing in the whole exercise.

Verdict

  • SpeedJev wins outright. 104s for 8,916 decisions — 3.4× faster than Haiku 4.5, 7.1× faster than GLM-5.3-flash.
  • ReliabilityJev wins structurally. Haiku silently lost 572 of 8,916 answers to parse failures. Jev lost none, and cannot — there is no text to parse.
  • AccuracyJev wins the decision that matters. On 215 adjudicated skill–posting pairs it ranks at AUC 0.793, against GLM's 0.732 and Haiku's 0.384. On the unambiguous subset, 0.937.
  • …butit loses the coarse one. On which broad region of the catalogue to look in, GLM leads on 11 of 12 postings, +0.067 mean AUC, p = 0.0063. The two metrics disagree, and the section below is about why.
  • CostUnproven, and it looks bad. TypeSafe publishes no price. Jev spent 941,194 tokens where GLM spent 319,672, so Jev is only cheaper below $0.046 per million.
  • vs freeNot separated. Jev's edge over a plain substring matcher is not statistically established at this sample size (p = 0.146).

The trap I walked into first

This is the most useful part of the exercise, so it goes before the results rather than in a footnote.

To score 8,916 skill–posting pairs you need labels, and nobody was going to hand-label them. My first idea was the obvious one: a skill is relevant to a posting if the posting names it verbatim. Cheap, objective, reproducible.

It is also exactly what the lexical arm computes. I had defined the ground truth to be one contender's output. Here is what that produced:

Metric 1 — positives are skills named verbatim. Worthless.
ArmAUC
lexical (substring)0.960
0.96
Jev0.927
0.93
GLM-5.3-flash0.876
0.88
Haiku 4.50.861
0.86
embeddings, per-sentence0.788
0.79
embeddings, whole doc0.637
0.64

Read carelessly that says a substring matcher beats every model, and Jev is second. Read correctly it says nothing at all: the top row is the label definition scoring itself, and every other row is being graded on how well it imitates str.find().

The fix has to break the tie between label and method. I relabelled using domain membership — a property of the skill, not of the posting's wording, so no arm can match it by string. Then one more constraint: only the 412 of 743 skills whose domains_source is master, meaning the membership was written by hand in my source file rather than inferred by Jev in an earlier pass. Scoring Jev against labels Jev wrote would have been a worse bug than the one it replaced.

What stays a judgement, stated so it can be argued with: I assigned the domains for each posting, from its title and stated responsibilities. That mapping is published in full in eval_clean_labels.py with the phrase from each posting that drove each line, so you can disagree with a specific call rather than with the conclusion.

The scoreboard

Labels no arm can game. 412 skills × 12 postings; between 36 and 239 positives per posting.

Metric 2 — AUC against master-authored domain labels
ArmAUC meanpooledper-posting range
GLM-5.3-flash0.681
0.68
0.6930.433 – 0.834
Jev (noul)0.614
0.61
0.6430.374 – 0.755
Haiku 4.50.607
0.61
0.6170.446 – 0.787
embeddings, whole doc0.575
0.58
0.5520.449 – 0.767
lexical (substring)0.571
0.57
0.5740.470 – 0.713
embeddings, per-sentence0.565
0.57
0.5470.364 – 0.723

Two things to take from this, and the second matters more than the first.

GLM beats Jev, and the margin survives testing

Ahead on 11 of 12 postings, mean difference +0.067, sign test two-sided p = 0.0063. Bootstrapped over 306,850 ranked pairs the pooled difference is +0.0597, 95% CI [+0.0581, +0.0612] — comfortably excluding zero. Haiku, by contrast, is indistinguishable from Jev (5 of 12, p = 0.77).

Everything is closer to a coin flip than anyone would like

The best arm scores 0.68 where 0.50 is guessing. Some of that is the label being coarse — a Security posting does not want every security skill, so perfect play does not score 1.0 here and the real ceiling is unknown. But it is a caution against the entire exercise, and it is why the one comparison people will want — Jev against a free substring matcher — is not statistically separated at this sample size (3 of 12 to lexical, p = 0.146). Jev is ahead. I cannot yet show that it is reliably ahead.

Speed and reliability, where Jev is not close

Every arm ran sequentially, in batches of 60, on the same machine and the same postings.

8,916 decisions per arm
ArmWall clockPer decisionTokens inTokens outCostAnswers lost
Jev103.9s11.7 ms781,642159,552unpublished0
Haiku 4.5352.8s39.6 ms284,62253,155$0.5504572
GLM-5.3-flash737.4s82.7 ms252,22467,448$0.04290
embeddings130.3s14.6 ms29,9890$0.00060
lexical3.0s0.3 ms00$00

The 572 is the number worth staring at. That is 6.4% of Haiku's answers that never arrived — lines that did not match the expected format, in a run where the format was one number per line at temperature 0. Nothing raised an error. Several postings came back with 518 or 636 of 743 skills scored and the pipeline carried on, and the only reason I know is that the harness counts them. A typed response has no equivalent failure: the number is a number or the call failed loudly.

And the trap on the other side

GLM nearly produced a false headline of my own making. It is a reasoning model, and at the token budget that is ample for 60 answer lines it spent every token thinking and returned empty content — 0 of 60 parsed on every batch, which reads exactly like a model that cannot follow a format. Measured, per batch of 60:

z-ai/glm-5.3-flash · one batch of 60 skills, same prompt
  budget   784, thinking on      0/60    6.4s     784 out
  budget  4000, thinking on     60/60   28.5s   3,895 out   $0.00129
  budget 12000, effort low      60/60    4.7s     361 out   $0.00023

Left on default, the model billed as one-eleventh the price of Haiku costs 5.6× and takes 6× as long as itself. Per-token price lists are misleading for reasoning models unless the effort is pinned. (enable_thinking: false was tried; the provider ignored it. reasoning_effort is what works.) Every GLM number above uses the pinned configuration.

The cost claim does not survive

I expected this to be the easy win. It is the weakest part of the case.

TypeSafe publishes no pricing I could find, so Jev's cost cannot be computed — only the price at which it would break even. Its token usage is recorded, and it is high: 941,194 tokens against GLM's 319,672 for identical work. Each noul carries its own instructions and criteria text, sixty per call, where the generative prompt lists sixty bare skill names once.

Break-even blended price per million tokens
AgainstIts cost, same workJev is cheaper only below
Haiku 4.5$0.5504$0.5848 / M
GLM-5.3-flash$0.0429$0.0456 / M

Beating Haiku is plausible. Beating GLM requires Jev to cost under five cents per million tokens blended, which would make it one of the cheapest endpoints in existence. On the evidence I have, "cheaper" is not a claim I can make — and if someone at TypeSafe wants to publish a number, this is the arithmetic it lands in.

The part no metric could grade

Every model arm spends most of its ranking on skills the posting never names:

top-40 picks per posting that the posting does not name, 12 postings
  jev      286/480   60%
  glm      289/480   60%
  haiku    297/480   62%

  lexical cannot rank a single one of them.

That 60% is the entire reason to pay for a model instead of running a substring match, and the domain metric cannot grade it — domain labels are too coarse to adjudicate an individual skill, and there is no automatic ground truth for “this posting implies Terraform without saying it.”

So I graded them. The harness writes the cases where the arms most disagree — one confident yes, another confident no, on a skill the posting never names — to eval-disagreements.json: 240 rows, twenty per posting. Each was then judged twice, independently, one pair at a time with the full posting in view and the arms' scores withheld.

Are the labels worth anything? Check before using them.
MeasureValueReading
Agreement between passes215 / 240 = 89.6%
Cohen's kappa0.791substantial
Yes-rate, pass A / pass B48% / 49%balanced, not a rubber stamp
Agreed rows either pass called arguable120 / 215the task really is hard

And the ranking inverts

The 215 rows both passes agreed on — 103 yes, 112 no. Answering “yes” to everything scores 48%.

Metric 3 — adjudicated skill–posting pairs
ArmAccuracyCaught the yesesRejected the noesAUCSays yes
Jev73.0%78.6%67.9%0.793
0.79
54.4%
GLM-5.3-flash60.0%22.3%94.6%0.732
0.73
13.5%
Haiku 4.536.7%45.6%28.6%0.384
0.38
59.1%

GLM's earlier win is explained by that last column. It says yes to 13.5% of these pairs and consequently rejects 94.6% of the true noes — excellent specificity, and it catches barely a fifth of the skills a posting genuinely calls for. For a metric built on hard negatives that caution reads as skill. For actually selecting what goes on a résumé, a recall of 22.3% is not usable. Jev is the only arm anywhere near balanced.

Haiku ranks below chance at 0.384 — worse than shuffling. It is also the arm that shares a lineage with its judge, which should have flattered it. It did not.

Does it survive dropping the hard cases?

Restricting to the 95 rows neither pass flagged arguable, the gap widens rather than closing, which is the opposite of what an artefact does:

Unambiguous rows only — 39 yes, 56 no
ArmAccuracyCaught the yesesRejected the noesAUC
Jev86.3%94.9%80.4%0.937
0.94
GLM-5.3-flash66.3%25.6%94.6%0.735
0.74
Haiku 4.532.6%48.7%21.4%0.411
0.41

How to hold the two results together. They are not in conflict. The domain metric asks which region of a catalogue should I search, and GLM is better at that. The adjudicated metric asks does this posting call for this specific skill, which is the question the tool actually has to answer, and Jev is better at that by a wide margin. An arm can be good at the first and useless at the second by simply being cautious — which is exactly what GLM's 13.5% yes-rate is.

Read these numbers with three things in mind. The adjudicators are models, not me — careful ones, judging one pair at a time with the whole posting, which is a stronger setting than any scored arm was given, but not human ground truth. They are Claude-family, so Haiku's row shares a lineage with its judge and should be the most suspect; it is also the worst, which cuts against the bias rather than for it. And these 240 pairs are where the arms disagreed most — deliberately the hardest in the set, so treat them as a floor on difficult cases rather than typical performance.

What I changed in the pipeline on this evidence: nothing. Jev stays. It is the fastest arm, the only one that structurally cannot drop an answer, and — on the judgement the tool actually makes — the most accurate by a clear margin. I went into this expecting to be talked out of it and was not.

Haiku 4.5 is out. It is the most expensive arm, it silently lost 572 answers, and it ranks below chance on the adjudicated pairs. GLM-5.3-flash with reasoning_effort pinned is worth keeping in the drawer as a cheap second opinion where a false positive costs more than a miss — it rejects 94.6% of true noes — but at 22.3% recall it cannot be the thing that picks what goes on a résumé.

Reproducing it

Four scripts, all in the repository that builds this site's CV data. Every figure on this page comes out of the results file, not off a terminal.

ScriptWhat it does
eval_skill_selection.pyRuns the five arms, writes every score to eval-skill-selection.json, checkpoints per arm
eval_clean_labels.pyThe domain-label metric, with the posting→domain mapping published in full
eval_board.pyScores any checkpoint, so a partial run is still readable
eval_significance.pySign tests, the bootstrap, break-even pricing, the beyond-text counts
eval_disagreements.pySelects the 240 hardest pairs and writes the labelling sheet
eval_adjudicated.pyAgreement and kappa first, then scores the arms on the rows both passes agreed

Two caveats I would raise if I were reviewing this rather than writing it. Twelve postings is a small sample for per-posting tests, which is why the sign test is reported alongside a pooled bootstrap rather than instead of it. And the postings are all from one candidate's search in one month, skewed toward pre-sales and MSP work — the domain mix is not representative of hiring generally, only of what I was actually applying to.