Two experiments, in that order. First we measured what TypeSafe AI's Jev judge model really does to our ranking. We include the day our own code scored 0.3822 nDCG@10 while plain BM25 scored 0.7750. That bug was ours. We found it, fixed it, and ran 1,271 judged queries on three public datasets. Then we flipped the question. A XERJ node now answers the System One wire itself. The pip-installed jev-reranker runs against it with no code changes. No request leaves your machine. Every number below comes from a run you can find in the repo. The losses sit next to the wins.
The rerank stage takes BM25's top hits, sends them to a hosted relevance judge, and reorders by the probability that comes back. It shipped with a caveat that said: quality not verified, no provider key. Then TypeSafe gave this project an evaluation key. The first measured run put a number on the caveat. The number was a disaster. The judge made ranking worse than not using it.
state blob. Then it asked each question an untargeted phrase: "Does this document contain information that answers the query?" The words "this document" pointed at nothing. So the model judged the whole pile once. It echoed roughly the same probability back for every question. Inside each 30-document window the probabilities were nearly flat. The spread was about 0.02. That is query-level noise dressed up as a ranking.
| Same 40 judged SciFact queries · same BM25 shortlists · same day | nDCG@10 | What differed |
|---|---|---|
| BM25 alone, no judge | 0.7750 | baseline |
V1, candidates in shared state, untargeted question |
0.3822 | what shipped. "This document" bound to nothing. |
| V2, one noul per document, the document inside its own question | 0.8389 | raw API, no stage code around it |
The fixed stage, V2 shape through the real rerank path |
0.8299 | the commit that landed. p50 399 ms, p95 515 ms per query. |
The pilot was for diagnosis. Forty queries were enough to see a 2× gap. The fix came from reading the installed jev-reranker wheel (MIT licence), not from memory: listwise mode, the document named by backtick path inside its own question.
Raw artifacts: benchmarks/beir-hybrid/results/2026-09-20-rerank-pilot
and …/2026-09-20-rerank-full ·
narrative in docs/research/typesafe-skill-2026-09/NOTES.md ·
model pinned jev-1.13.0 throughout.
Three BEIR datasets, all public, all from the standard distribution. SciFact has 300 judged queries. NFCorpus has 323. FiQA has 648. We fetched BM25 top-30 shortlists once. The shortlist step is deterministic. Then we sent the fixed request shape three times per dataset, with the query order shuffled each time, and took per-query medians. Zero failed requests. We repeated because the provider is not deterministic. That finding is below, and it sets the resolution of everything in this section.
Before calling any model we checked our harness on FiQA. Our local BM25 scored 0.2382 nDCG@10. BEIR's published BM25 figure for FiQA is 0.236. Close enough to trust the scorer.
nDCG@10, BEIR test split | SciFact · 300 q | NFCorpus · 323 q | FiQA · 648 q | Run by |
|---|---|---|---|---|
XERJ BM25 (multi_match, no embedder) |
0.6572 | 0.3016 | 0.2382 | us, any node |
XERJ hybrid RRF (--embed-mode neural), measured 2026-09-18, rc.74 |
0.6993 | 0.3448 | not run | us, that run |
XERJ BM25 top-30, reordered by Jev (jev-1.13.0, pinned) |
0.7410 | 0.3312 | 0.3638 | us, this run, median of 3 |
| Jev (TypeSafe AI), hosted reranker | 0.768 | 0.358 | no figure | not run by us, published in the hev/jev-rerank README |
| Voyage rerank-3, hosted reranker | 0.755 | 0.357 | no figure | not run by us, same README |
| Cohere rerank-v3.5, hosted reranker | 0.745 | 0.340 | no figure | not run by us, same README |
W / L / T counted per query against the same run's BM25 arm. All three datasets net positive. NFCorpus is the weakest, and its per-query losses are in the table above, not hidden.
| The bill, and the clock | SciFact | NFCorpus | FiQA |
|---|---|---|---|
| Judge input tokens (document text sent, 3 repeats) | 9,589,068 | 7,618,170 | 15,178,758 |
| Cost at $0.042 / Mtok, input only | $0.4027 | $0.3200 | $0.6375 |
| Wall clock per query, p50 | 1.49 s | 1.48 s | 1.23 s |
| Failed requests | 0 | 0 | 0 |
Mean per-document probability drift was 0.0077 on SciFact and 0.0096 on NFCorpus, with a max of 0.08. On FiQA the repeat means spanned 0.3622 to 0.3648. So: any comparison of Jev numbers closer than about ±0.01 nDCG@10 is measuring noise. We report medians for that reason. A single-shot benchmark of a hosted judge is a coin with a ±0.01 bias.
A judge probability is not the chance a document is relevant. On FiQA the expected calibration error is 0.3109. Documents the model rated near 0.93 confidence were relevant 34% of the time. The curve still rises to the right, so the model ranks well. But every bin above 0.2 promises far more than it delivers. SciFact: ECE 0.1023, bin 0.85 confidence, 42% relevant. NFCorpus: ECE 0.1332, bin 0.92 confidence, 55% relevant. Use the ordering. Never set min_score on the raw number.
19,440 (probability, relevance) pairs, 10 equal-width bins; the three highest are shown. The full curve, bin by bin, is in fiqa-full-run.json. The gap between the red bar and the gold bar is the whole finding: at the top of the scale the judge says "certain" and means "one in three".
Runner and raw JSON: …/2026-09-20-rerank-full (SciFact, NFCorpus) and
…/2026-09-20-rerank-full-fiqa (FiQA, with dataset provenance) ·
hybrid-row figures (0.6993 / 0.3448, 2026-09-18, rc.74): benchmarks/beir-hybrid (results/scifact.txt, results/nfcorpus.txt) ·
run 2026-09-20 on the wire-compat branch engine, scoring byte-identical to the hybrid benchmark's eval.py ·
FiQA fetched from the standard BEIR distribution ·
hosted-reranker figures: hev/jev-rerank (MIT). A third party published them. Not TypeSafe's numbers, and not ours.
System One is a documented interface for typed judgement. You POST a state, a model, and a map of questions. Each question comes back noul, a yes/no with a 0 to 1 probability, or choice, N named options with probabilities. XERJ already had a fast answer for exactly that shape of question. Index the labelled history you already have. Answer a new question by a rank-weighted vote over its k nearest neighbours. Report the winning label's vote share as the probability. No model. No tokens. So the node learned to speak the wire.
POST /v1/systemoneThe documented request and response shape, answered by the vote. A client written for TypeSafe's API works unmodified. state as string or object. instructions as string, object, or array. Backtick paths like `documents.doc_0` resolved against state. Answers keyed by exactly the ids sent. Everything strict clients validate is there.
POST /_decideThe same vote without the wire costume. It names its index per request, and it returns the evidence: each neighbour's id, label, the engine's own BM25 score, its 1/rank weight, and its text. Any answer can be checked against the history that produced it. Below decisions.min_confidence it abstains.
model is echoed truthfullyThe response says xerj-history-vote-1. Never a Jev model name. Echoing jev-1.13.0 would claim these probabilities are the hosted model's. The name you asked for rides at decisions.requested_model. Compatibility does not require lying about whose numbers they are.
A question whose vote finds no labelled neighbour carries no information. The endpoint refuses it. HTTP 422, naming the question ids. A silent 0.5 is exactly the silent-fake defect class this project treats as a bug. Wire compatibility does not change that.
No SDK of ours, no patches, no subclassing. pip install jev-reranker (0.1.0, MIT) in a clean venv, its own listwise template, its own splitting, its own strict response validation, pointed at a XERJ node holding 4,000 labelled SMS messages. Two environment variables, and one of them is a trap:
$ pip install jev-reranker $ # TYPESAFE_ENDPOINT carries the FULL path. TYPESAFE_BASE_URL is silently ignored $ # by this client (reranker.py:238), and the call still goes to the real host. $ export TYPESAFE_ENDPOINT=http://localhost:9441/v1/systemone $ export TYPESAFE_API_KEY=local-only # the node ignores it; localhost only $ python3 gate_run.py client resolved model: ['xerj-history-vote-1'] 1.0000 spam Please call our customer service representative on 0800 169 6031 betwe 1.0000 spam Great News! Call FREEFONE 08006344447 to claim your guaranteed £1000 C 1.0000 spam URGENT! Your Mobile number has been awarded with a £2000 prize GUARANT 0.0000 ham If you're not in my car in an hour and a half I'm going apeshit 0.0000 ham Carlos says we can pick up from him later so yeah we're set ... # 10 fixed docs, 5+5 mean noul spam=0.9014 ham=0.0956 gap=0.8058 GATE PASSED: unmodified jev-reranker ranked from the XERJ node
The client's own validation is the wire gate. Answers keyed by the sent ids. Every noul numeric in 0..1. Model a non-empty string. Usage non-negative integers. Surviving it unmodified is the compatibility claim. The spam/ham separation is the semantic one. Zero requests left the machine. The vote is an ordinary search of an ordinary index, and the module adds no outbound client.
state, and the vote text joins it, so query vocabulary takes part in retrieval. With a spam-bearing query like "winner claim prize", every document's vote retrieved spam neighbours. The vote measured the query, not the documents: spam 1.000 against ham 0.785. No separation. The fixed gate uses a query whose every term is absent from the corpus: "triage inbox unsolicited correspondence". Result, 0.9014 against 0.0956. This is inherent to a retrieval vote, not a defect. Pick the query for the history you have. Do not let a rerank habit leak vocabulary into a classification question.
A full news item against 120,000 labelled AG News examples is a heavy query. About 40 terms, and the match set is most of the index. This is the workload that exposed the waste in our own search loop. The old path built a hit list per term, folded them through hash maps, and sorted the whole match set. All that to return a size-10 page. We replaced it with a WAND-style pruned walk for the one shape every match query becomes. A document is now scored only when it can still make the page even at its best possible score. The method is the classic one from Broder et al., and the same idea tantivy uses, adapted to our postings format.
| AG News vote · 120,000 history docs · all 7,600 test items · identical inputs | ms / item | Accuracy | Decisions changed |
|---|---|---|---|
| Engine before the change | 68.7 | 0.9182 | — |
| WAND first cut | 51.2 | 0.9182 | none |
| WAND, allocation-free inner loop (shipped) | 34.2 | 0.9182 | none |
2.0× faster end to end, and the 7,600 decisions are bit-identical: same labels, same confidence bands. 83.1% of items decided at confidence 0.8 or higher, and 96.7% of those right. Zero items had no neighbour. Ranking quality did not move because scores are computed in the same order and to the same values. We proved that three ways. The three BEIR runs reproduce to four decimals on the new engine, 0.6572, 0.3016, 0.2382. A 648-query A/B over a byte-identical copy of the FiQA index found zero order changes and zero score-bit changes. The full ES wire-conformance suite stayed at 1371 passed, 0 failed.
The hosted bar is the Jev API's measured p50 on the FiQA rerank run in §02, 1.23 s per query. Its fastest of the three datasets, so the bar is generous to the hosted side; SciFact and NFCorpus measured 1.49 s and 1.48 s. The local bars are the same wire answered by the vote over 120k AG News history on this engine. Different tools: the hosted judge needs no history, the vote needs this index. This is a speed, cost and egress comparison, not a quality race. No Jev number exists for AG News and we claim none.
Measured earlier on the same engine (the hub's ranking section), on the vote's home ground: labelled history, in-domain classification. No judge model was run on these datasets by us, and none of these rows say anything about Jev.
| Decisions from labelled history · k = 10 · held-out items | Accuracy | Calibration error | Decided at confidence ≥ 0.8 | ms / item |
|---|---|---|---|---|
| Banking77, 77-way intent routing · 1,000 items — BM25, no model of any kind | 0.819 | 0.089 | 50.6% at 0.996 | 2.2 |
Banking77 — MiniLM neighbours (--embed-mode neural) |
0.933 | 0.012 | 86.9% at 0.979 | 8.6 |
| SMS spam, yes / no · 1,000 items — BM25, no model of any kind | 0.983 | 0.017 | 95.4% at 0.992 | 0.9 |
| AG News, 4-way topic · 7,600 items — BM25, no model of any kind | 0.9182 | 0.019 | 83.1% at 0.9669 | 34.2 |
$ # the audit surface — same vote, evidence attached $ curl -s localhost:9440/_decide -H 'content-type: application/json' \ -d '{"index":"sms","question":"URGENT! Your Mobile number has been awarded"}' {"index":"sms","k":10,"label":"spam","confidence":1.0,"abstain":false, "neighbours":[{"_id":"1805","label":"spam",...,"weight":1.0}, ...]}
score-type ordinal questions. It attaches the neighbours behind every answer. If you need judgement where you have no history, a new policy, an unseen category, this is the wrong tool and the hosted judge is the right one. The two ends of the wire are complements, not competitors.WAND engine change: cf6a9989, with the A/B logs · AG News artifacts and provenance: benchmarks/decisions-as-retrieval/results/2026-09-20-agnews · endpoint: engine/crates/xerj-api/src/systemone_api.rs · 14 end-to-end tests: tests/systemone_http.rs · operator doc: docs/DECISIONS.md · gate harness and transcript: benchmarks/systemone-gate.
The judge's bill on the 300-query SciFact run, next to the vote's numbers on its own measured workloads. Two tools, two workloads, priced side by side:
| The bills | Judge (hosted, SciFact run) | Vote (local, own workloads) |
|---|---|---|
| Cash cost | $0.4027 at $0.042/Mtok in | $0 |
| Latency per item, p50 | 1.49 s | 34.8 ms (measured on 120k AG News, its heaviest workload) |
| Document text that leaves the machine | 9.6 M tokens' worth (3 repeats) | none |
| Works with no labelled history | yes, that is its job | no, that is its limit |
| Calibration, on its own target | ECE 0.10 cross-domain BEIR | ECE 0.012 in-domain Banking77 |
TYPESAFE_ENDPOINT at a XERJ node when the questions have history and must stay home. Point it at the hosted API when they do not. Same client. Same wire. No code change. That is the whole bridge.$ git clone https://github.com/xerj-org/xerj && cd xerj/engine $ cargo build --release -p xerj-server $ cat > gate.toml <<'EOF' [decisions] index = "sms" k = 10 label_field = "label" text_field = "text" positive_label = "spam" min_confidence = 0.0 EOF $ ./target/release/xerj --insecure --port 9440 --config gate.toml # native REST on 9441 $ python3 benchmarks/systemone-gate/gate_load_sms.py # index 4,000 labelled SMS $ python3 -m venv ~/gate-venv && ~/gate-venv/bin/pip install jev-reranker $ TYPESAFE_ENDPOINT=http://localhost:9441/v1/systemone \ TYPESAFE_API_KEY=local-only ~/gate-venv/bin/python benchmarks/systemone-gate/gate_run.py mean noul spam=0.9014 ham=0.0956 gap=0.8058 GATE PASSED: unmodified jev-reranker ranked from the XERJ node
Everything on this page: the pilot · the full run · the FiQA run · the AG News vote · the gate · the research narrative · measured 2026-09-19..20 · engine at the wire-compat branch plus the WAND commit cf6a9989, ES-YAML conformance 1371 passed / 0 failed · related: the benchmarks hub · the rerank stage, documented · the ES scorecard.