18 datasets × 3 runs: 7 of 9 LLM failures are one string (; RNA-Seq), and DeepSeek pro beats flash on every axis
#188
Replies: 1 comment
Closed out: both tiers green at three trials — and three things this review got wrongMeasured at This is the corpus's first multi-draw measurement. Every number in this thread, and every number in 1. The default-model finding stands. The mechanism findings stand. The case attribution never reproduced.Both mechanisms this review traced into source were real, and both are fixed:
But which cases each hit was a lottery, and this thread's per-case table did not survive re-measurement. The lesson is not that the analysis was sloppy — the mechanisms were traced correctly, into source. 2. "The benchmark is not measuring the code that ships" was right about the disease and wrong about the symptomThis thread identified the harness/product seam through one instance: It was a measured no-op. Recomputing both reductions over the run's own accepted assertions: they The class was real, and the large instance was somewhere else entirely. So: right that the instrument diverged, right to fix it first — but the instance this thread found was 3. Three of this thread's proposals were rejected, for reasons that generalise
4. A green draw is not a fixed bugWorth recording, because it nearly closed a live issue. After #192 the pro tier read 18/18, exit 0, 5. The secondary findings mostly shippedFour of five, in #195: the bare-array envelope (a valid batch under the wrong envelope no longer costs Batching is worth a number. Per trial, against the pre-#195 run: Three times fewer requests and roughly half the tokens, for the same 141 documents. 6. What is still not measured
Nothing carried over. #184, #189, #190, #194 and #196 are closed; the items that were argued but |
Uh oh!
There was an error while loading. Please reload this page.
A design review off three benchmark runs of the same 18-case corpus at
ac11b44— no-LLM, DeepSeekflash, DeepSeek pro. The question was "which model should be the default", and the answer turned out
to be the least interesting thing measured: 7 of the 9 non-green outcomes across both LLM runs
trace to a single string, and a second, smaller mechanism accounts for the other 2. Neither is a
model-quality problem. Both are places where code treats compatible information as contradictory.
Related: #184 (the
library_strategy-as-chemistry defect, with a no-LLM reproduction), #61 (thread 1,deeper metadata resolution), #185 / #186 (GSE234962's coin flip), #182 (
_without_short_readings),#50 (a deterministic GSM-title parser).
TL;DR
deepseek-v4-proshould be the default. It wins on every measured axis including the onesflash is supposed to win — 2.6× faster wall clock, 3.5× fewer output tokens, higher field
accuracy, half the questions, 1 failed document vs 6. The "cheap end of V4 is what a corpus-scale
harness should reach for" claim in five docstrings is falsified by its own benchmark.
library.chemistry = "RNA-Seq"— SRA'slibrary_strategy, read offthe
; <organism>; <strategy>suffix that GEO appends to every experiment title — appears in 7 ofthe 9 non-green outcomes. It is not wrong; it is a family term being coerced onto a leaf.
Spec.parent,node_kind: Literal["family","leaf"],children_decided_by(kb/schema.py:318-322) — and_match_techreturnsstr | None, so it structurally cannot say "this names a family".different words, compared with case-folded exact equality, and the attribute nulled. The eval
harness grades that null as
false_accept— "a confident wrong manifest" — which is the harnessdisagreeing with the resolver's own stated rationale.
manifest fillreduces chemistry claimsagreement-or-nothing;
evalsreduces them last-wins. Two callers, one stage, different answers.Method
Three runs of
eval runover the same 18-case benchmark corpus atac11b44, same prompt version(
2026.7.4), differing only in extractor:deepseek-v4-flashdeepseek-v4-proCaveat, and it is not a small one: both LLM runs are
trials: 1, sostabilityis unmeasured(reported
0.0throughout, against1.0for the deterministic run). GSE234962 is known to flip(#185/#186). Single-trial LLM grades are therefore claims about one sample, not about the extractor.
Everything below that rests on a mechanism traced into source is solid; anything resting on a grade
alone is one draw.
The default-model question, settled
Flash is not cheaper here in any currency this repo actually measures (there is no dollar pricing
anywhere in the codebase — cost is tokens and seconds). It burns 3.5× the output tokens, and a
large share of that output is discarded by construction: 38
field_not_permitted_for_docrejections on PRJNA1027859, 17 on GSE256266, 16 on GSE234962, where the model answers with fields
the prompt did not ask of that document's scope. Pro does this 0–2 times per case.
So: flip
DEEPSEEK_DEFAULT_MODEL(harvest/providers.py:50), and retire the motivating claim inproviders.py:49,cli/eval.py:158,cli/harvest.py:138,cli/run.py:299,evals/run.py:488.One live bug falls out: the pro run's stderr advises "re-run those cases with
--model deepseek-v4-pro" while already running pro. The remedy string does not know what model ran.Mechanism 1 — a family term coerced onto a leaf (7 of 9)
Every GEO experiment title carries a fixed suffix:
fields.py:143asksexperiment-scope documents forlibrary.chemistry. That document is thistitle. The model answers with the only technology-shaped token in it, and four independent, each
individually-defensible things line up (#184 traced the same chain):
verify.py:130-134already says so about itself:"Entailment is vacuous when value ⊆ quote." The value is copied out of the quote, so
span_verifiedandentailment_okboth pass trivially.verify.py:129claims the check's power "comesentirely from
valuebeing drawn from a controlled vocabulary: forlibrary.chemistrythe valueis a KB id." Nothing validates that.
"RNA-Seq"is not a KB id._match_tech(escalate.py:697-713) matches by symmetric substring. Pass 3 acceptsneedle in alias or alias in needleand returns the first hit in dict order."rna-seq"is asubstring of
bulk-rnaseq-pe's alias"bulk RNA-seq"._bulk_asserted_single_cell_observed(escalate.py:510)fires
conflict-bulk-asserted-single-cell-observed(escalate.py:540) against correctly-resolvedbarcoded bytes → exit 4.
The check is inverted on the one field it was built for
In GSE256266 and PRJNA1027859 the model also proposed
library.chemistry = "10x-3p-gex-v3.1"—the correct KB id — and it was rejected
not_entailed, while"RNA-Seq"was accepted withentailment_ok: true. The tripwire rejects the right answer (prose spells3'as30) and admitsthe wrong one (vacuity). That is worth saying plainly: on
library.chemistry, entailment currentlyhas negative value.
It is not only spurious questions
GSE317744 is the sharpest case. Its
expected.yamlpre-registers that10x-5p-gex-v2isbyte-identical to 3' v2 in every observable —
distinguishable_by: [metadata, alignment], no bytepath at any rung — so the case exists to grade the metadata-hypothesis branch. Under flash,
"RNA-Seq"occupied that channel and the run resolved10x-3p-gex-v2, i.e. the wrong chemistry,and would have emitted
soloStrand Forwardfor a Reverse library. So the same string produces both aspurious refusal and, where metadata is the only evidence, a wrong answer.
The hierarchy is already there
#184 notes the loose substring direction "is not a one-line deletion" because family claims like
"10x 5'"depend on it. That blocker dissolves, because the loose match is a workaround for amissing family node:
bulk-rnaseq-peis a leaf holding the generic EFO term, and there is norna-seqfamily nodeabove it. Meanwhile
confuse.same_family(confuse.py:203) already walks the tree, andengine.py:180-182deliberately handsescalatethe full KB "so id/confusable lookups resolve forunscored nodes". The machinery is in place and
_match_techis the one function that ignores it —legibly, in its type:
-> str | Nonecannot express "this is10x-3p-gex, decide the leaf frombytes".
same_familyis consulted downstream (escalate.py:417,:594) as a repair on a decisionthat should never have been forced.
Proposed:
rna-seqfamily node (node_kind: family, no backend,EFO:0008896, aliasesRNA-Seq/RNA sequencing/transcriptome) parenting the rna-modality nodes._match_techthenhits it at pass 2 (exact alias) and never reaches pass 3.
_match_techto return a node, and change the conflict predicate from equality tosubtree membership: an asserted node conflicts only when
observed ∉ subtree(asserted).10x-3p-gex-v3 ∈ subtree(rna-seq)→ consistent, no question. Prose claiming ATAC against 10x GEXbytes still blocks, which is the conflict worth having. A family term then narrows a candidate
set — strictly more useful than today, where a hypothesis can only break an exact tie.
library.chemistrydraft that resolves to no KB node, inverify_drafts— where thedocstring already assumes it happens.
<GSM>: <description>; <organism>; <strategy>before it reaches the model. Those last two fields are structured columns SRAappends, not prose, and rendering them as prose is what manufactures the error. (This is F5 (design): deterministic GSM-title covariate parser #50's
parser, one level up.)
Pass 3 also has a standing determinism hazard: it returns the first dict-order match, while
escalate.py:59-63goes to real trouble to make candidate ordering deterministic "orcandidates[0].technologyflips between runs of an unchanged input". The identical hazard isunguarded here, which means adding a KB spec can silently change an unrelated dataset's manifest
hash. That matters a lot if the corpus is about to grow.
Mechanism 2 — two wordings of one fact, and the fact is discarded (2 of 9)
GSE310378, one deposit, one sentence:
treatment = "Pro VSV GFP"treatment = "VSV GFP treated control"Both are contiguous word-runs of the same title. They overlap. Neither contains the other, so
_is_short_reading_of(records.py:600) fails both directions,_decide(records.py:528-533)calls it an equal-authority disagreement, and stores null. PRJNA1027859 loses
strain = "CQ757"on one sample the same way. In both cases the no-LLM run gets the field right: adding the LLM
destroyed a fact the archive had already supplied.
That is verbatim the failure
_without_short_readingswas written to prevent (#182, GSE282765,"Citrobacter rodentium infection"vs"Citrobacter rodentium"). It recurred in a new geometry dayslater. That is the signal worth acting on:
records.py:545-597is a 50-line docstring guarding a10-line predicate that handles exactly one near-miss shape, and there is no finite list of shapes.
The next ones are already visible —
_norm_value(records.py:615-621) folds case and whitespace andnothing else, so
"wild-type"vs"wild type","C57BL/6J"vs"C57BL/6","5 days"vs"5d"areall disagreements today.
The stated rationale does not survive contact with the harness
records.py:479argues "a wrong value is permanent and a missing one is not." But the manifest iswrite-once and content-hashed (R11 / ADR-0004), so a null is exactly as permanent — and the eval
harness grades the dropped
treatmentasfalse_accept: "a confident wrong manifest is the onefailure the corpus never recovers from." The harness and the resolver disagree about what null
means, and the harness is right: a manifest that silently omits a field it had evidence for, while
reporting that it decided, is a confident wrong manifest.
Proposed, cheapest first (R9 order):
_norm_value(-/_//→ space before splitting). One line, preservesevery safety property
_is_short_reading_ofrelies on (whole words, contiguous).BioSampleAttribute.synonyms— "what submitters write instead" — used today to harmonize keys(
io/attributes.py:121-127) and fed to the model as prompt text, but never to compare values.The one machine-readable vocabulary in the repo is applied to keys and withheld from values.
contained in the typed value" but "are the typed value and the reading both contiguous word-runs
of the same source document, overlapping?" — if so, the submitter's typed string wins byte-for-
byte. Same safety argument (nothing new reaches the manifest; you store exactly what a no-prose run
stores), still one-directional. This closes GSE310378 with no model call.
"VSV-GFP"vs"vesicular stomatitis virus expressing GFP").ArbitrationRequest/ArbitrationResponseare"modelled and unbuilt — there is no verb" (
docs/agents/models.md:164), andAGENTS.mdalreadygrants the LLM exactly this as job (b): "arbitrate ambiguity code has already flagged."
_decideis code having already flagged it. Building it completes the design rather than relaxing it —
and it stays inside R2 because arbitration is a selection over a code-supplied closed set, not
a generation: the response must be byte-equal to one of the inputs or
abstain, and code checksthat. The model cannot invent a value. Rungs 4–6 are empty; this is what belongs there.
EvidencedStralready carriesevidence; addvariantsso themanifest records
treatment: "Pro VSV GFP"and that the experiment title wrote it "VSV GFPtreated control". Strictly more information than either null or a silent pick, and it makes
step 4 auditable after the fact.
The harness is not measuring the code that ships
Two callers reduce the same harvest output to a chemistry hypothesis, differently:
Record documents come last in plan order, so under the eval path one line of GSM title overwrites a
good
10x-3p-gex-v3read off a supplementary table (docs/agents/eval-corpus.md:408-414says thisoutright). Under the product path that pair is
{"RNA-Seq", "10x-3p-gex-v3"}→ size 2 →None→ nohypothesis → no conflict.
Precisely what this does and does not explain: it does not account for pro's GSE229022 or
GSE256266, where
"RNA-Seq"was the only accepted chemistry claim (the correct10x-3p-gex-v3.1had already been rejected
not_entailed), so both reductions agree. It is the mechanism behindGSE234962's documented flip. Structurally it makes the harness more question-prone than the thing we
ship. Before the corpus grows, that seam should be one function — otherwise we tune against an
instrument that fails differently from production.
Secondary findings
over_askhides that the answer was right. Every over_ask case in both runs had 100% correctfields — the manifest was correct and the run still exited 4. Grading that identically to a wrong
answer means a question-gate regression and an extraction regression are indistinguishable. Suggest
splitting the axis: decided-correct / decided-wrong / asked-but-would-have-been-right /
asked-and-needed-to. The third bucket is currently the entire failure list.
extract.py:341-347raisesExtractUnavailableon a response whose top level is a bare JSON array, costing the entiredocument — even though every element would pass
AssertionDraft.model_validate, and a partlyinvalid
draftsarray is already accepted with rejects recorded (extract.py:349-355). ADR-0009's"half a batch is worse than none" is about a silent half-parse; this is the same batch under a
different envelope. Likely accounts for part of flash's 6 failed documents.
_pretrimmed_blockersblocks on a signal it can already localize. The gate isobs.read_length.n_distinct == 1(escalate.py:237), so one read a single base short in a2,000-read head blocks the dataset at exit 3, no appeal — while the Observation carries
mode,min_len,max_lenand percentiles that would tell a 0.3% ragged tail from a real offset shift.The over-length escape (
escalate.py:239-240) shows the shape a principled exemption takes.probe/__init__.py:38-56marks its ownoriginal justification FALSE ("A HEAD SLICE IS NOT A RANDOM SAMPLE", GSE305031's dark cycle:
91.35% N at R1 cycle 2 in the first 2,000 reads, 0.00% in the last 2,000) and correctly moves the
defence to
onlist_hit_rate's denominator. But composition, segmentation,n_rateandconsensus_match_rateare all still built from that head and have no equivalent coverage report.Nothing consumes
n_rateas a warning today.25). GSE310378's three documents are 45, 209 and 213 characters and cost 9,382 input tokens — >95%
prompt overhead, paid three times, three round trips. Batching same-scope documents is already safe
by construction: every draft carries
doc_sha256and code greps the quote into that document, socross-document contamination fails span verification automatically. The tripwire built for
hallucination is also what makes batching sound.
Proposed order of work
evals→_chemistry_hypothesis). It is the instrument; fixit before reading anything else off it.
library.chemistrydrafts inverify_drafts. Closes GSE229022 + GSE256266 and everylatent instance of the title suffix.
_without_short_readings. Closes GSE310378deterministically, no model call.
rna-seqfamily node + subtree-membership conflict +_match_techreturning a node. Not requiredfor green after (2), but it is what makes the next hundred datasets work, and it removes the
add-a-spec-changes-a-hash hazard.
over_askgrade, and run the LLM cases attrials > 1so stability is measured at all.Steps 2–4 are the concrete proposal for #184; steps 3–5 feed #61 thread 1.
All reactions