Validate NameRes ES: group its differences from Solr, and let a target declare it has no blocklist - #137
Open
gaurav wants to merge 6 commits into
Open
Validate NameRes ES: group its differences from Solr, and let a target declare it has no blocklist#137gaurav wants to merge 6 commits into
gaurav wants to merge 6 commits into
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
namelookup-es implements no blocklist at all. Every `negative`-flagged sheet row and all 313 blocklist-sheet entries therefore failed against --target ci-es, which is ten to three hundred assertions a day all saying the same known thing and drowning the findings that matter. targets.ini already exists to record this kind of per-target capability rather than infer it from the hostname, so add NameResHasBlocklist there, defaulting to true in [DEFAULT] and false in [ci-es]. A target that forgets to declare it keeps its blocklist coverage rather than silently losing it. Filter on the `negative` flag rather than on the "Blocklist Nov 2023" category: only 16 rows carry the flag, 5 of them live in "Unit Tests", and row 175 (hypersexuality) is an ordinary positive row that happens to sit in the Blocklist category. Category would have been wrong in both directions. test_blocklist.py skips in the test body rather than in pytest_generate_tests, because target_info is parametrized by the root conftest and does not exist at generation time. --target ci-es tests/nameres/: 164 failed -> 58 failed, 329 skipped. --target ci is unchanged: 196 passed, 117 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_label calls pytest.xfail() imperatively when the expected CURIE is in the top NameResXFailIfInTop (5) but not first, so a demotion from rank 1 to rank 2 is reported as an xfail and never appears in the failure count. Against namelookup-es that hides the single largest regression there is: 46 rows moved from rank 1 into ranks 2-6, against 32 that fail outright. Recording the rank as a user_property puts it in --report-jsonl, where a cross-environment comparison can see it. Raising NameResXFailIfInTop instead would have made every other environment noisier to fix one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The assertion vocabulary could not express either of the two things the NameRes ES validation most needs to say. SearchByName is top-N, with N fixed at 5 by NameResXFailIfInTop in targets.ini, so it cannot distinguish "the concept wins" from "the concept is somewhere in the top five". That is exactly the largest regression against namelookup-es: 46 rows moved from rank 1 into ranks 2-6, and a SearchByName assertion passes on both services for every one of them. SearchByNameTopResult passes only on rank 1, and it still asks for the top N so a failure can say whether the CURIE was at rank 4 (a ranking problem) or absent (a retrieval one) — different symptoms with different owners. DoesNotSearchByName is the first negative NameRes assertion of any kind; until now only NodeNorm had DoesNotResolve/DoesNotResolveWith. It makes a blocklisted term expressible: the term stays searchable, but the concept must not come back. A CURIE it cannot normalize is a failure there, not a pass. "We could not look it up" and "it was not returned" both end with the CURIE absent from the results, so the other choice would let a typo'd blocklist assertion succeed forever while testing nothing. DoesNotResolve takes the opposite line (VALIDATE_CURIES = False) because there the CURIE failing to resolve is the assertion. Both format untrusted params with !r, per the rule in CLAUDE.md — the neighbouring SearchByNameHandler predates it and still uses plain quotes. Neither is used by any filed issue yet: an issue's assertions run against every --target with no way to scope them, so one that fails on ES and passes on Solr would report a strict XPASS failure on every other environment daily. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The NameResHasBlocklist skips had no test, which is the wrong way round: a capability that reads false when it should read true does not fail, it *skips*, and the run stays green while testing nothing. So assert the checked-in targets.ini directly (every section parses, only ci-es opts out, an absent key keeps its coverage) and drive both skips against a synthetic target. The last two tests catch BaseException rather than Exception, deliberately. pytest's outcome exceptions derive from BaseException, so `pytest.raises(Exception)` misses them entirely; and catching Failed specifically would be worse than useless, because a regressed skip would then propagate and mark the test itself skipped, which reads as passing. Also pins expected_rank, added in b065aa1, by faking the lookup: the rank-2-6 branch calls pytest.xfail(), so recording the rank before it is the only reason a comparison can see a demotion from rank 1. CLAUDE.md gets the two things this session cost real time to work out: that a NameRes gsheet run's failure count understates regressions, with the classification needed to read --report-jsonl properly, and that an -es target must be compared against ci rather than dev or exp, which are newer Babel releases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md requires untrusted text to be formatted with %r/!r, and the search query is untrusted: it comes from a GitHub issue body and is the one param curie_params() excludes from CURIE validation, because it is free text bound for a URL query string. Not a live hole — AssertionHandler._rejection() rejects a non-printable param before any handler sees it, and it is deliberately the one check that runs over every param for exactly this reason. But it is the only thing standing between an issue body and these messages, repr is the belt to its braces, and having SearchByName quote its query one way while SearchByNameTopResult and DoesNotSearchByName quote it another invites the next handler to copy the wrong one. Message punctuation changes; no test asserts on it, and none is added — an assertion that a message contains a particular kind of quote restates the implementation, and the escaping this guards against is unreachable past _rejection(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Validating the Elasticsearch-backed NameRes (
namelookup-es.ci.transltr.io) turned up alarge, undifferentiated pile of failures. This branch turns it into named failure classes
with reproducers — filed as #131-#136 under the
NameRes ES validation, September 2026
milestone — and makes the changes to this repo that the investigation needed.
The findings
Running the same 2076 sheet rows against
ci-esand againstci— the Solr-backed NameReson the same Babel data, which is what separates a backend difference from a data one:
xfail)Three mechanisms account for nearly all of it — #131 (alphanumeric tokens are split, so
?string=BRCA1and?string=BRCAreturn byte-identical lists andNCBIGene:672isunreachable), #132 (longer names containing the query outrank the concept), #133 (candidates
tie on score and the canonical concept falls out of the window). #134 records the eight rows
where ES is already ahead of Solr, so they do not get "fixed" back.
What changed here
NameResHasBlocklist, a new per-target capability. namelookup-es implements noblocklist, so all 313 blocklist-sheet entries and every
negative-flagged sheet row failedby construction — the same known thing, daily, drowning the findings that matter.
targets.inialready exists to record this kind of difference rather than infer it from thehostname.
--target ci-es tests/nameres/goes from 164 failures to 58;--target ciisunchanged.
Filtering is on the
negativeflag, not the "Blocklist Nov 2023" category: only 16 rowscarry the flag, 5 of them live in "Unit Tests", and row 175 (
hypersexuality) is an ordinarypositive row sitting in the Blocklist category. Category would have been wrong both ways.
expected_rankis now recorded.test_labelcallspytest.xfail()imperatively whenthe expected CURIE is inside
NameResXFailIfInTopbut not first, so a demotion from rank 1to rank 2 never reaches the failure count. That hid the single largest regression: 46 rows
demoted, against 32 failing outright. Recording the rank as a user property puts it in
--report-jsonlwhere a comparison can see it.NameResXFailIfInTopis deliberately notraised — that would make every other environment noisier to fix one.
Two new assertion handlers. The vocabulary could not express either thing this
validation most needs to say.
SearchByNameis top-N with N fixed at 5 bytargets.ini, soit passes on both services for all 46 demoted rows;
SearchByNameTopResultpasses only onrank 1.
DoesNotSearchByNameis the first negative NameRes assertion of any kind, and makesa blocklisted term expressible.
Gotchas worth keeping
curlonly, deliberately. An issue's assertions run againstevery
--targetwith no way to scope them, and an open issue whose assertions all pass isa strict XPASS failure. An assertion that fails on ES and passes on Solr would turn
prod, test, ci, dev and exp red daily. Scope an issue's assertions to particular targets #136 tracks the scoping that unblocks this.
DoesNotSearchByNametreats a CURIE it cannot normalize as a failure, not a pass."We could not look it up" and "it was not returned" both end with the CURIE absent, so the
other choice lets a typo'd blocklist assertion succeed forever while testing nothing.
DoesNotResolvetakes the opposite line because there the CURIE failing to resolve isthe assertion.
BaseExceptionrather thanException. pytest's outcomeexceptions derive from it, and catching
Failedspecifically would be worse than useless:a regressed skip would propagate and mark the test itself skipped, which reads as
passing.
target_infois parametrized by the rootconftest and does not exist in
pytest_generate_tests. That is test_blocklist.py makes an HTTP request at collection time (before any target is selected) #91, now on the milestone.blackwas not run on the two nameres test files; it wanted to rewrite 236 and 101 linesand would have buried the change.
Before merging
Settle the final form of the two new assertions.
SearchByNameTopResultandDoesNotSearchByNameare unit-tested against fakes only and have never run against alive service, because no issue can carry them until Scope an issue's assertions to particular targets #136 lands. Worth deciding now
whether the shapes are right — in particular whether top-1 should instead be a
parameter on
SearchByName([query, curie, rank]) rather than a separate assertionname, and whether
DoesNotSearchByNameshould honourNameResXFailIfInTopas its Nor take its own.
Decide when
NameResHasBlocklist = falsecomes back out, and schedule it. Theflag is a stopgap, not the answer: it stops
ci-esreporting the same 10 knownfailures daily, but it also means that target has no blocklist coverage, and nothing
enforces its removal. The real fix is
NCATSTranslator/Babel#738 —
filter blocked synonyms in Babel so they are never written out, rather than having each
consumer delete them from its own index — now decided and on Babel v1.19. If #738 lands
well after this PR, this flag is the thing most likely to be forgotten, and
ci-essilently keeps its exemption long after it has stopped needing one. Worth agreeing the
trigger for removing it (Babel v1.19 shipping? the ci-es index rebuilt from it?) before
this merges. Tracked in Decide the trigger for removing NameResHasBlocklist = false from [ci-es] #144, which lists the candidate triggers and what "done" means;
note that the most accurate of them is itself blocked on NameRes ES: /status reports numDocs 0 and no babel_version #135, since NameRes ES
/statuscannot currently say which Babel release an index was built from.Testing
pytest -m unit— 217 passed (was 192 at the branch point).tests/unit/test_target_capabilities.py: the checked-intargets.iniparses, onlyci-esopts out, an absent key keeps its coverage, both skips fire, andexpected_rankis recorded before the xfail. Each mutation-checked.
pytest --target ci-es -n auto tests/nameres/— 59 failed, 56 passed, 329 skipped. Thefailures are NameRes ES: alphanumeric tokens are split, so gene symbols never match #131-NameRes ES: candidates tie on score, pushing the canonical concept out of the results #133 and are the point.
pytest --target ci -n auto tests/nameres/test_blocklist.py— 196 passed, 117 skipped,unchanged.