mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
main
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2cb460dfe3 | Release v1.5.1 | ||
|
|
8fc703fa56 |
Fill remaining skill gaps: DHS Program, EBI sequence tools, DeepSpotM, MolGlueDB
Second pass of the pre-release skill sweep, covering tool families the first batch (#592) didn't reach: - dataset-discovery, epidemiological-analysis: add DHS Program (DHSProgram_search_indicators/_get_data) as the international/LMIC equivalent of NHANES for fertility/maternal/child mortality/nutrition/ immunization/HIV survey indicators. - sequence-analysis: add EBI_scan_pfam_domains, EBI_profile_search, EBI_pairwise_align, EBI_translate_sequence, and EBI_predict_membrane_topology alongside the existing Pfam/BLAST/DNA- translation tools they complement (raw-sequence Pfam scan, remote-homology search, pairwise alignment, back-translation/6-frame, topology prediction for unannotated sequences). - spatial-transcriptomics: add DeepSpotM_predict_gene_expression as the virtual-readout option when only an H&E image is available and no real spatial assay exists. - binder-discovery: add MolGlueDB_search_compounds/_get_compound as a known- ligand-mining source specifically for molecular-glue-degrader neosubstrates (targets with no tractable pocket for conventional small-molecule binding). Also re-ran scripts/sync-codex-plugin-skills.sh, which caught up three plugins/tooluniverse/skills/ files (tooluniverse-biomedical-fact-lookup, tooluniverse-phylogenetics, tooluniverse/SKILL.md) that were stale relative to already-merged commits in skills/ and had never been propagated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0142e85518 |
Sweep skills for gaps left by ~48 new tool families since v1.4.1
Dispatched 6 parallel investigations covering all 41 tool families not already checked in prior rounds (structural/protein, genomic archives, plant/microbiology/ecology, funding/regulatory/ontology, rare-disease, and misc). Fixed every confirmed gap in one batch (16 skills touched): Clean additive fixes (tool exists, right skill exists, just not mentioned): - SKEMPI -> structural-proteomics (protein-protein binding mutations, BindingDB's missing counterpart) - REBASE -> molecular-cloning (Golden Gate hardcodes enzyme names with no lookup path for recognition sites/isoschizomers) - DANDI -> neuroscience (has a dedicated Neurophysiology Reasoning section, zero mention of the field's actual data archive) - BAR + Planteome -> plant-genomics (zero mentions; expression data and ontology annotation are gaps this skill's own description implies it should cover) - BOLD Systems -> ecology-biodiversity (DNA barcode species ID, direct fit for a skill already built around species identification) - Codon usage -> antibody-engineering + sequence-analysis: found a genuinely broken instruction -- antibody-engineering literally says "assess codon optimization for CHO" with no tool to do it, and sequence-analysis's only codon-usage source is a hardcoded human-only table - FDA Purple Book -> drug-regulatory (skill already has a worked example about finding a biosimilar; the tool for that was never named) - Folklore MCP -> variant-interpretation (direct match to the existing ClinVar/gnomAD/OMIM/ClinGen tool list) - Genomic Intelligence MCP -> regulatory-genomics (same tool class as the skill's existing AlphaGenome/Enformer/Borzoi/ChromBPNet/Evo2 comparison table) - EWAS Catalog -> epigenomics (methylation-trait associations, this skill's own domain) - GoaT -> comparative-genomics (genome-assembly-exists check before gene-level lookups on non-model species) - dbGaP/EGA/DDBJ/GSA -> dataset-discovery (frontmatter claimed dbGaP coverage the body never delivered; DDBJ is the strongest case, same INSDC tier as SRA/ENA which the body does list) - NIH RePORTER -> nih-funding-landscape (minor: simpler direct path alongside the existing OpenNIH bulk-corpus tools) Two duplicate-tool-family situations resolved as documentation clarifications, not removals (both families are live and each has some genuinely unique capability, unlike the earlier PR-vs-PR AlphaGenome duplicates which were cleanly supersede-and-close cases): - Orphadata vs. Orphanet (rare-disease-genomics): Orphanet is more complete (10 tools vs 4) and stays the default; noted Orphadata's MeSH/UMLS/MedDRA cross-refs as the one thing Orphanet's own ICD/OMIM/SNOMED mapping doesn't cover - OpenFDADevice vs. OpenFDA_device (product-safety-surveillance): 3 of 6 tools are genuinely new (UDI lookup, classification, PMA) and now listed; the other 3 overlap with an existing family, noted as interchangeable rather than duplicated guidance Also fixed while in here: tooluniverse-nih-funding-landscape and tooluniverse-antigravity-plugin existed only in skills/, never propagated to either plugin distribution since they were added -- this session's earlier plugin-sync runs were incorrectly treating their appearance as sync-script drift and discarding them each time. Both are legitimate (added via #354 and the OpenNIH PR respectively, neither excluded by the sync script's own deliberate exclusion list) and are now properly mirrored to plugin/skills/ and plugins/tooluniverse/skills/. Every tool name referenced (44 total) verified programmatically against the actual registered tool JSON before writing; two MCP env var names (FOLKLORE_MCP_URL, GENOMIC_INTELLIGENCE_MCP_URL) verified against their loader configs. Confirmed correctly out of scope, no action (~18 tool families): mcsa, membrane_topology/pdbtm (too niche for existing skills), bacdive, mediadive, ncats_translator, nci_evs, fhir_terminology, smartapi, scicrunch_rrid, nasa_osdr, mddb, biothings_gateway (flagged as a separate cross-cutting design question), deepspotm (plausible, not clear-cut), GIAB/CMS Open Payments/NIH DSLD/MolGlueDB (genuine gaps but no owning skill exists -- would need a new skill, out of scope for "is an existing skill stale"). |
||
|
|
656ffa22b8 |
Fix stale complex-prediction limitation in protein-structure-prediction skill
The skill flatly stated "complex prediction requires AlphaFold-Multimer (not available via these tools)" -- no longer true since the official Boltz API tools (72 Boltz_* tools, multi-chain + protein-ligand binding) landed. Added a dedicated section rather than folding it into the existing free-tool workflow: Boltz is paid (BOLTZ_API_KEY gated), asynchronous (submit/poll/retrieve), and requires an idempotency key to avoid double-billing -- explicitly instructs estimating cost and confirming with the user before submitting a paid job, and keeps ESMFold/AlphaFold as the default for anything that doesn't actually need a complex. Checked tooluniverse-single-cell for the other major addition since v1.4.1 (Single Cell Portal / UCSC Cell Browser / CellTypist catalog, ~8 dataset-discovery tools) -- correctly out of scope there, since that skill is about analyzing data the user already has (h5ad files), not discovering new public datasets. No fix needed. |
||
|
|
2a4700e55f |
Add Consensus, Noodle, and Exa to the literature-deep-research skill
Found while auditing skills for release readiness: three search tools landed since v1.4.1 (Consensus_search_papers, the opt-in Noodle MCP, the opt-in Exa MCP) with no mention in the skill built specifically around literature search and evidence grading. - Consensus_search_papers: added to the "Broad" domain row (2.2) and to Phase 3 (Evidence Grading) -- its study-design metadata (study type, sample size) is a fast first-pass triage signal, not a substitute for reading the source or the grade itself - Noodle (noodle_*, opt-in via NOODLE_MCP_URL): added to citation expansion (2.1) alongside the existing citation-graph tools, with the same "discovery signal, not evidence of validity" caveat its own tool description carries - Exa (exa_*, opt-in via EXA_MCP_URL): added as a last-resort fallback in 2.5 for when every structured index is empty -- flagged clearly as general web search, not a scientific database, not to be graded T1-T4 SerpBase (the new web_search backend) needed no mention -- it's a transparent swap under an existing tool's hood, invisible to this skill's guidance. Also updated TOOL_NAMES_REFERENCE.md with the same three additions. plugins/tooluniverse/skills/ regenerated for this skill only (unrelated pre-existing drift in other skills reverted, per the established pattern on this branch's predecessors). |
||
|
|
a6afe6e469 |
Cross-reference regulatory-variant-analysis from the rare-disease consequence filter
Found while checking whether the AlphaGenome/Atlas pipeline added to tooluniverse-regulatory-variant-analysis is actually discoverable: the router (tooluniverse/SKILL.md) sends "rare disease" queries to tooluniverse-rare-disease-genomics instead, and that skill's own consequence-hierarchy heuristic said "synonymous/intronic: usually benign unless at splice junction" with no cross-reference to the skill built to check exactly the exception -- a deep intronic variant creating a cryptic splice site far from the canonical boundary, i.e. the real GREGoR/DNM1 case the new pipeline is modeled on. Someone following this skill's own advice verbatim would dismiss precisely the variant type the new capability exists to catch, without ever learning it exists. Added one clause noting the exception and pointing to tooluniverse-regulatory-variant-analysis. |
||
|
|
3993097ed3 |
Cross-reference alternative sequence models when AlphaGenome isn't the fit
The skill's only sequence-prediction option was AlphaGenome -- if it's unavailable (no key) or the case specifically calls for a published, self-hostable model instead, there was no path forward mentioned. ToolUniverse already has that path (Enformer/Borzoi/ChromBPNet/Evo2, covered in tooluniverse-regulatory-genomics); this just points to it from Phase 4.5 and the missing-key fallback bullet, so the pipeline doesn't dead-end when AlphaGenome specifically isn't the right tool. |
||
|
|
341e8b6dd9 |
Fix wrong tool name and add robustness/generality gaps in the regulatory-variant-analysis pipeline update
Found on review: - atlas_scan_interval was referenced without its AlphaGenome_ prefix in two places -- the literal string doesn't match any registered tool, so an agent following the skill verbatim would fail to call it - No guidance for the case where ALPHA_GENOME_API_KEY isn't configured; Phase 0.5 read as a hard dependency when the rest of the skill (and its own pre-existing pipeline) works entirely without any gated key. Added an explicit skip-and-proceed instruction in Phase 0.5 and a matching Fallback Strategies bullet - AlphaGenome_atlas_scan_interval -- built for exactly the "many candidate SNPs at one locus" case this skill already treats as core (gwas_get_variants_for_trait, fine-mapping) -- had no real usage guidance, just two throwaway mentions in fallback/limitations text. Added proper Phase 0.5 coverage and a 4th worked example (triage a GWAS locus's candidate list by AVI_SCORE before running the expensive annotation phases on all of them) Verified every AlphaGenome_* tool name and parameter signature in the file against the actual registered tool schemas -- all others were already correct. |
||
|
|
862de585d2 |
Weave AlphaGenome/Atlas into the regulatory-variant-analysis pipeline
This skill was purely annotation-database-driven (GWAS, eQTL, ENCODE,
RegulomeDB, OpenTargets) with no sequence-based prediction at all --
a real gap, since AlphaGenome answers exactly the question these
databases can't: does this specific sequence change do anything,
independent of whether it's been annotated before.
Adds two pipeline touchpoints rather than one bolted-on section:
- Phase 0.5 (new, runs by default): an instant Atlas AVI_SCORE lookup
before the annotation phases, since it's a free precomputed read
- Phase 4.5 (new, conditional): escalates to the live model
(score_variant / score_ism_variants / predict_interval) specifically
when annotation came back silent or a mechanism is needed for the
report -- mirroring how the GREGoR Consortium actually solved an
unsolved rare-disease case via a deep intronic DNM1 variant that no
annotation database had anything to say about
Also: a 5th evidence question distinguishing model-based (causal,
mechanistic) from annotation-based (correlational) evidence; a new
Phase 5 synthesis tier ("sequence-model-supported, annotation-silent")
so a high AVI_SCORE with no database hits is no longer misclassified
as "no evidence"; a full worked example; and limitations covering
AlphaGenome's model-estimate caveat and the ISM/scan window caps.
plugins/tooluniverse/skills/ regenerated for this skill only via
scripts/sync-codex-plugin-skills.sh (unrelated pre-existing drift in
other skills reverted, as in the prior skills commit on this branch).
|
||
|
|
af05bdb3c3 |
Add AlphaGenome Atlas support; fix 7 bugs found via live-key testing (#579)
* Add AlphaGenome Atlas support and fix bugs found via live-key testing
Adds 7 new AlphaGenome operations exercising the full alphagenome SDK
surface (excluding pure batch convenience wrappers): predict_variant,
predict_sequence, score_interval, score_ism_variants, output_metadata,
atlas_scan_interval, and atlas_list_scorers.
Every operation (new and pre-existing) was verified against a real
ALPHA_GENOME_API_KEY rather than trusting SDK docs, which surfaced 7
real bugs now fixed:
- Wrong default Atlas scorer name ("AVI" -> "AVI_SCORE")
- score_variant/atlas_lookup_variant silently kept only the first gene
of a multi-gene AnnData and mislabeled tracks with meaningless numeric
indices instead of var["name"]
- predict_interval silently dropped 3 of 11 real output modalities
(PROCAP, SPLICE_JUNCTIONS, SPLICE_SITE_USAGE)
- atlas_scan_interval leaked non-JSON-serializable Variant objects
- atlas_list_scorers crashed on real pandas DataFrame truthiness
("x or []" on a DataFrame raises, doesn't fall back)
- top_n with a non-positive/invalid value returned nearly the entire
dataset via Python's negative-slice semantics instead of a summary
- Atlas scorers echoed back in a different case than the actual
response dict keys, causing lookups by the echoed name to fail
Also corrects two doc-only inaccuracies (incomplete output_type/
output_types value lists, wrong predict_sequence length constants).
Added 22 unit tests (0 previously existed for this tool), each mapped
to a real bug found live, using sys.modules stubbing for the alphagenome
SDK dependency (not installed in CI).
* Update regulatory-genomics and variant-interpretation skills for AlphaGenome Atlas
Both skill files (and their plugin/plugins mirrors) recommended only
score_variant/predict_interval, with no mention of the new Atlas lookup
operations added in this PR. Adds:
- AlphaGenome_atlas_lookup_variant/atlas_scan_interval to the tool
tables, with guidance to check Atlas first for known SNVs (precomputed,
cheaper than a live call) before falling back to the live operations
for indels/custom sequences/full per-track detail
- AVI_SCORE noted as a unified impact score (fuses AlphaGenome +
AlphaMissense), distinct from the Δ effect-size outputs of the other
tools, relevant to variant-interpretation's ACMG PS3/PP3/BP4 guidance
- AlphaGenome_score_ism_variants mentioned in regulatory-genomics for
"which base in this element matters" saturation-mutagenesis questions
plugins/tooluniverse/skills/ regenerated via
scripts/sync-codex-plugin-skills.sh, restricted to only the 3 changed
skills (the script also resyncs unrelated pre-existing drift in other
skills, which is out of scope here and was reverted).
|
||
|
|
780ed6b6c2 |
Cross-reference the Codex and Antigravity plugin skills from setup
setup-tooluniverse's "Path B" one-liner install only ever named tooluniverse-claude-code-plugin, even though tooluniverse-codex-plugin and tooluniverse-antigravity-plugin exist and give Codex/Antigravity users the same one-step "marketplace add + install" experience instead of the generic manual TOML config path this skill otherwise sends them through. Adds the parallel codex plugin marketplace add / codex plugin add one-liner alongside Claude Code's, and points Antigravity users at their own skill (agy plugin install, a different flow, so not worth reproducing inline). Applied to all three packaged copies (skills/, plugin/skills/, plugins/tooluniverse/skills/), matching how this skill is distributed. |
||
|
|
6e14a7ee68 |
Fix #559: retire CLUE.io tools (site shut down, no new API keys)
clue.io's own homepage carries an official retirement banner: "Effective January 31, 2026, the clue.io site, including its tools, will be retired due to financial constraints. As a result, subscriptions will no longer be offered." Confirmed live: the site still loads and the API still returns 401 for unauthenticated requests, but new CLUE_API_KEY registration is closed per that notice, so the 5 CLUE tools are unusable for anyone without a pre-existing key. Follows the same archival pattern used for OxO (#215) and other retired upstream APIs: moved clue_tools.json into broken_apis/ with a clue_rest.json entry documenting the failure and the GEO workaround (clue.io's own notice points there for the underlying public data), commented out the default_config.py entry, dropped the 5 generated tool wrappers and their tools/__init__.py + .tool_metadata.json + tool_page_index.json entries, and regenerated api_keys_catalog.json / .env.template via scripts/gen_api_key_catalog.py to drop the now-dead CLUE_API_KEY entry. Also updated the cell-line-profiling skill (all three packaged copies), which referenced CLUE_get_cell_lines as a live tool. clue_tool.py and its _lazy_registry_static.py entry are left in place, matching the OxO precedent -- harmless dead code once nothing loads its config. Verified live: with a dummy CLUE_API_KEY set, unmodified main still loads all 5 CLUE tools; this branch loads 0 and reports the category no longer exists. Reported in #559. |
||
|
|
bb314a29d5 |
Fix Round 41: drug-name lookups answering with a different drug's label (#517)
* Fix drug-name lookups answering with a different drug's label
FDA_get_boxed_warning_info_by_drug_name {"drug_name":"Albuminex"} returned
BEIZRAY/docetaxel's boxed warning -- toxic deaths, neutropenia, hypersensitivity
-- presented as the boxed warning of a plasma-derived human albumin product.
All 77 FDA_*_by_drug_name tools map drug_name onto the OR-group
(openfda.brand_name, openfda.generic_name, spl_product_data_elements). The third
is the raw product/ingredient blob, so the query matched any product merely
CONTAINING albumin; AND-ing _exists_:boxed_warning then deleted both genuine
Albuminex labels -- which correctly have no boxed warning -- and left only the
wrong drug's. Measured at api.fda.gov/drug/label.json:
spl_product_data_elements:"ALBUMINEX" 3
AND _exists_:boxed_warning 1 (BEIZRAY, docetaxel)
openfda.brand_name:"ALBUMINEX" 2
AND _exists_:boxed_warning 0 (the true answer)
AND _exists_:warnings_and_cautions 2
Drop a blob-matched row when the record's own resolved openFDA names exist and
none contains the queried name: that is an excipient match, not a product match.
Rows whose openfda block is empty are KEPT -- they have no resolved identity to
contradict, and that is how genuine unresolved products appear, which preserves
the denosumab 19->25 recall fix. Sampled live, the rule drops nothing for
denosumab, aspirin, metformin, warfarin, docetaxel, Prolia, Humira, Eliquis or
Gamunex-C, and drops exactly BEIZRAY for Albuminex.
When the filter empties a payload it is turned into openFDA's own NOT_FOUND
shape, so the existing section_exists_clause re-probe still engages: Albuminex
now answers with its own 2 labels, boxed_warning null, and the
warnings_and_cautions those labels do carry.
The disclosure note states only what was done. It does not claim the answer
describes the queried drug, because rows with an unresolved openfda block cannot
be checked -- a minocycline lookup whose surviving rows are all zinc gluconate
lozenges would make that claim false exactly where it matters. It instead
reports how many returned rows could not be checked. The caveat is gated on the
guard having actually dropped something, since an unresolved block is the normal
shape for 6 of the 25 real denosumab labels and warning on every clean lookup
would train the reader to skip the note.
* Fix four over-claims in the excipient-guard disclosure
An adversarial review of the previous commit found four assertions the response
does not support. The guard's filtering logic is unchanged -- it loses no recall
across 30+ drug names sampled live -- but what it TOLD the caller was wrong.
meta.total was page-local while the note asserted it had been corrected:
'albumin human' at limit=10 reported 37 when the true post-filter total is 16,
a 2.3x overstatement, and the page-local caveat existed only in a source comment.
The note now says "reduced accordingly, to N" only when the whole result set fits
on one page; otherwise it declares the number an UPPER BOUND and quotes the
upstream hit count and the page size so the reader can audit it.
dedup_note claimed meta.total was "openFDA's upstream hit count before local
processing" in the very responses where the guard had changed it -- two
contradictory statements in one payload. It now names the reduction and the raw
upstream count.
The note named spl_product_data_elements as the sole match route and told the
reader to inspect it. On broadened queries that is false: for minocycline the
term appears in no returned row's blob -- those rows matched
indications_and_usage/description. The note now names the fields actually
searched.
The unchecked-row count was taken before the user limit was applied, so
"N of the returned label(s)" could report 12 when 3 were returned. It is now
counted over the rows actually returned.
Seven tests cover the disclosure itself, which had none -- which is why it
regressed once already. A paged fake transport makes the page-local case
observable; the previous fake made the total correction exact by construction,
so no test could have caught it.
* Fix Round 42: FAERS counts that described a page the response no longer showed
FAERS_count_reactions_by_drug_event applies `reactionmeddraverse`
client-side: openFDA ranks every reaction term on the matching reports, then
the tool keeps only the row whose term equals the one asked for.
`stratified_report_count` -- documented as "what the counts in 'results' sum
to" -- was summed from the ranking page BEFORE that narrowing, so it
described neither the row shown nor the whole facet, and it moved with
`limit` while `results` did not.
Measured live on the anonymous tier. Three round-42 personas hit this
independently, on four drug/reaction pairs:
PROMETHAZINE + somnolence: `results` is the single row SOMNOLENCE 1,438 at
every limit, while stratified_report_count read 1,438 / 2,630 / 6,492 at
limit 1 / 5 / 50.
MODAFINIL + tachycardia, limit 20: the single row is 200 against 322
matching reports -- BELOW the total -- yet coverage_note read "946 --
293.8% of it, i.e. MORE than the number of matching reports", telling a
reader to discount a near-1:1 signal as threefold double-counted.
LISDEXAMFETAMINE + Suicidal ideation, patientagegroup Adolescent: the rows
sum to 8 against 8 matching reports, reported as 35 and "437.5% of it" --
a 35 standing next to an adolescent suicidality signal whose count is 8.
Sum `results` instead of the raw page, so the documented meaning holds by
construction, and give the narrowed regime its own prose: one kept term
cannot double-count against rows that are no longer present, so the
percentage is meaningful there -- a close upper bound on the share of
reports affected, since openFDA counts recorded values rather than reports.
When the narrowing keeps nothing but reports still matched, say so and name
both causes: outside the top-`limit` slice, or matched inside longer
preferred terms (measured: MODAFINIL + "arrest", 176 reports, no preferred
term IS "arrest" -- they are CARDIAC ARREST, RESPIRATORY ARREST).
CONTRACT CHANGE, narrowed path only: stratified_report_count's value moves
to what its own schema says it is. The unnarrowed path is untouched and is
pinned by a regression test that fails if the page sum is reinstated.
Also, additive: FAERS_count_drugs_by_drug_event was the only tool of the 18
in its family with no `medicinalproduct` filter, so it could only ever
return the database-wide top-N drug ranking -- 20,692,690 reports and HUMIRA
at the top whatever the caller wanted, and no way to ask the co-reported
medication question its name implies. With the filter, INTRALIPID returns
INTRALIPID 720, INTRALIPID 10% 119, TPN 82, TEDUGLUTIDE 77, HEPARIN 73
against 961 matching reports, matching openFDA queried directly; a drug that
does not exist returns no rows and a zero total instead of a global ranking.
* Fix Round 42: gene-disease concordance counted sources that never answered
gather_gene_disease_associations queries five databases and reports, on every
association row, `concordance` (how many named the entity) beside
`total_sources_queried: 5`. That denominator counts every source attempted,
including ones that failed and ones that structurally cannot contribute, so
agreement reads as disagreement. Measured for {"gene": "CFH"} with no API
keys configured: DisGeNET and OMIM fail on missing keys and ClinVar carries
no disease name in the gene->disease direction, leaving two sources able to
answer at all -- yet the top row reports "concordance 2, total_sources_
queried 5", and BRCA1's "breast cancer" reports 1 of 5.
Two caps were silent on top of that. `per_source_results` was sliced to ten
rows per source with no flag while `num_associations` counted the full table,
so the same CFH response showed "OpenTargets: 10" against "num_associations:
29" with nothing saying OpenTargets had actually returned 25; `associations`
was sliced to fifty the same way.
All additive -- no existing field changes value or meaning:
- each row gains `total_sources_with_data`, the number of sources that
contributed anything, which is the denominator to divide `concordance`
by; `total_sources_queried` keeps its value and its meaning and now says
in the schema that it is not that denominator
- `per_source_result_counts` gives the untruncated count per source
- `truncated` follows the repo-wide truncation convention for the 50-row
association cap
- `notes` gains a line per disclosure, and only when it applies: no note
when nothing was cut, none when every source contributed
One definition of "contributed data" (`_sources_with_data`) now feeds both
the per-row denominator and the note that explains it, so they cannot drift.
Corroborated across four rounds before this one.
* Fix Round 42: pin the gene-disease disclosures, and exercise the new FAERS filter
Validation of the two commits above found the gene-disease change shipped
without tests: nothing under tests/ mentioned total_sources_with_data,
per_source_result_counts or truncated for this tool, and none of its three
test_examples reaches the 50-row association cap (live: 29 / 41 / 30), so the
truncation branch was unreachable from anything shipped. The FAERS commit had
122 lines of regression tests; this one had none.
Eight tests, all pure functions -- no network, no ToolUniverse construction:
- the honest denominator rides on every row (5 queried, 2 able to answer for
the live CFH case) and concordance never exceeds it
- a source that answers nothing is not counted as disagreeing, and when all
five answer the denominator moves to 5
- each cap names what it cut ("10 of 25" for OpenTargets; 63 against the
50-row cap), and a source sitting exactly at the cap is not listed as cut
- the denominator caveat fires only when the denominator misleads, and a
response with nothing cut and nothing skewed carries no notes at all
- the two fields dropped during review (sources_with_data,
associations_returned) stay dropped, so neither creeps back as a second
way to say what the response already carries
Each was checked against a deliberately reverted build: restoring the
all-sources-queried denominator fails the row test, and silencing
_disclosure_notes fails the three note tests.
Also: FAERS_count_drugs_by_drug_event's only test_example omitted
medicinalproduct, so `tu test` exercised just the database-wide ranking the
new description tells callers not to rely on. Added the filtered example that
the description quotes; both examples pass.
* Fix Round 43: waits that stop early and then report the shortfall as a defect
Thread.join(timeout=...) returns the same thing whether the thread finished
or the wait expired. Four test sites joined with a timeout and then asserted
on the results they had collected, so a slow thread under CI load produced
"Expected 3 results, got 2" -- reporting broken concurrency when the truth
was that we stopped waiting. test_tool_finder_edge_cases::test_concurrent_calls
failed CI on #517 this way and passed on re-run with no code change.
Add tests/helpers/threads.py::join_all: one shared deadline across the set,
then an is_alive() sweep, so a timeout is always reported as a timeout and
names the threads still running. Converted all four sites
(test_tool_finder_edge_cases, test_critical_error_handling,
test_mcp_unit_functionality, test_performance_benchmarks). The budget lives
in one place and is chosen to stay under pytest.ini's per-test timeout = 60,
which the old per-thread budgets could exceed -- 5 x 30s let pytest-timeout
kill the test with its own opaque message first. Sweeping the rest of the
tree found no other bounded wait followed by an assertion; the src/ sites
(smolagent_tool, result_cache_manager, vcf_stats_tool) already check
is_alive() or catch TimeoutExpired.
pytest.ini gains `tests` on pythonpath so `helpers` is importable by
declaration rather than as a byproduct of conftest loading (it otherwise
breaks under --import-mode=importlib), and the four files keep an explicit
sys.path bootstrap so their __main__ blocks still run.
The same shape in tool code: ClinGen_search_actionability fans the Adult and
Pediatric contexts across a thread pool and swallowed a failed context with
`pass`, leaving its list empty. The tool then answered status "success" with
pediatric_count 0 -- indistinguishable from "ClinGen has curated no pediatric
actionability for this gene". Confirmed live: BRCA1 has 6 adult and 1
pediatric curation, but with the Pediatric fetch failing the tool still
answered success/0 with nothing marking the figure as unmeasured. Failed
contexts are now recorded in failed_contexts with a note saying the 0 means
"not retrieved"; when every context fails the tool returns an error rather
than two fabricated zeroes, matching proteins_api_tool's existing policy.
The healthy path is unchanged in keys and values.
* Fix Round 44: an indication inflating a comparator's adverse-event count
FAERS_compare_drugs matched reactions against openFDA's ANALYSED
`patient.reaction.reactionmeddrapt`, whose tokenizer splits on whitespace
and hyphens, so a search for one Preferred Term also matched every compound
term containing it. Measured live 2026-08-12 for "Thrombocytopenia":
heparin 6,805 -> 2,778 on `.exact` (2.45x), bivalirudin 111 -> 27 (4.11x).
The inflation is DIFFERENTIAL, which makes it a wrong answer rather than a
conservative one: `_compare_drugs` divides the two arms' RORs, so a factor
that differs per drug moves the verdict. The contaminating term is
HEPARIN-INDUCED THROMBOCYTOPENIA (3,968 reports) -- bivalirudin's
INDICATION, not its adverse effect -- so the comparator was inflated by
reports where the drug was the response to the event. "heparin and
bivalirudin show similar-strength detected signals" becomes "heparin's is
stronger" (ROR 7.393 vs 4.563) once each arm counts only the term it claims.
Every reaction filter in the module now routes through `_faers_search_query`
so no operation can search a different variant than its siblings. `.exact`
is case-insensitive at openFDA (verified across four capitalisations), so
callers keep working; a string that is not a Preferred Term now returns a
named error listing real terms to retry with, instead of silently counting a
union of terms.
Also in FAERS_compare_drugs: a three-drug list returned status "success", a
confident two-drug verdict and comparison_caveat null, with the third drug
absent from the response. It is now refused, since the analysis is pairwise
by construction.
openFDA label tools:
- A NOT_FOUND blamed `requested_return_fields[0]`, so a boxed-warning search
was explained as "This label section ('openfda.brand_name') is absent" --
naming a field never searched -- and recommended falling back to
`spl_product_data_elements`, the ingredient blob that answers "propylene
glycol" with rabeprazole, glipizide and urea PK. Both corrected; the real
cause, contiguous-phrase matching, is now stated.
- The `_exists_` guard defaults to a tool's RETURN fields, silently dropping
labels that match the search but lack a resolved openfda block
(adverse_reactions:"keratoacanthoma" 4 -> 2, the dropped one muromonab-CD3,
a different drug class). Now disclosed, gated so section tools stay quiet.
- Keyword trimming was skipped for three hard-coded keys and applied to
everything else, including identifiers: `id` and `set_id` came back as ""
because a UUID contains no matching sentence. Replaced with a named
NEVER_KEYWORD_TRIMMED set.
Regression introduced by this change, found and closed before shipping:
moving the three sibling operations to `.exact` turned a loose match into a
raw "404 Client Error ... for url: <full openFDA URL>". All four
reaction-filtered operations now distinguish "not a Preferred Term" from
"no reports for this pair".
That 404 exposed a PRE-EXISTING leak this round also closes, rather than
having caused it: all four RequestException handlers returned `str(e)`, and
because `_with_api_key` appends `&api_key=` to the same URL that requests
quotes back, a configured FDA_API_KEY appeared in the returned error string.
Reproduced with a key set, then closed by stripping at " for url:".
* Fix Round 45: searches answering with a different entity, and probes that under-measure
Three defects, each verified against the upstream API before and after.
proteins_api_search returned an unrelated human protein for a non-human gene
symbol. `{"query": "ben-1"}` returned BANP_HUMAN. ben-1 is the C. elegans
beta-tubulin whose loss confers benzimidazole resistance. Upstream:
gene=ben-1&taxid=9606 is empty, gene=ben-1 returns Q18817_CAEEL, and
protein=ben-1&taxid=9606 returns BANP_HUMAN because the free-text search
matches "BEN domain-containing protein 1". The empty human gene search fell
straight to the free-text field while keeping the human bias, so a different
entity's annotations came back as the requested protein, with no note. The
retry now tries the same field with only the tool's own human default dropped,
accepted when a hit's gene symbol is exactly the query -- which is what keeps
a genuine protein-name query ("insulin" -> "Insulin-like Peptide 1") from
being dragged off. blaKPC, blaCTX-M-15, insulin, BRCA1 and accession routing
are unchanged.
CIViC returned pages with no denominator. Every connection asked GraphQL for
nodes only, so a caller could not tell a complete answer from a first page.
civic_search_evidence_items for melanoma returned 100 records; the true count
is 218. totalCount and pageInfo are now selected across all 10 connection
tools. The count reported matches the tool's own status:ACCEPTED filter (218),
not the wider unfiltered 370. civic_get_variants_by_gene paginates in Python
and rebuilt the block without the count; it now carries totalCount through.
Monarch phenotype tools were nondeterministic, incomplete, and reported an
upstream rejection as success. The multi-phenotype intersection sliced an
unordered set, so three identical calls for HP:0002524 gave three different
differentials and Niemann-Pick type C appeared or vanished by hash seed; it
now intersects in upstream rank order. Each leg took one hard-coded page of
500 and intersected it as the phenotype's whole disease set -- HP:0001250
(Seizure) has 5331 associations, so Seizure + Cataplexy returned one disease
and omitted Niemann-Pick C1/C2, which carry both; legs are now paged, and a
leg still short of its total is disclosed instead of silently returned. The
x3 over-fetch in get_HPO_ID_by_phenotype pushed any limit above 166 past
Monarch's ceiling of 500, and the resulting HTTP 422 body was returned as
status:success quoting an "input" of 501 the caller never supplied; the
over-fetch is clamped and an upstream validation body is now an error.
Each test was confirmed to fail with its fix reverted.
* Fix Round 45: stop echoing an internal over-fetch value back to the caller
The Monarch validation-error path put the raw upstream body into the error
details. That body quotes the value actually sent, and this tool inflates the
caller's limit by 3 before sending it -- so a caller who asked for 167 was
handed back "input": "501", a number they never supplied, presented as their
own argument. Return only the upstream messages.
Found by running the adversarial leak check over the new error path rather
than by any query; no credential is exposed, but nothing the caller did not
supply should come back to them as though they had.
* Fix Round 45: close four defects the round's own changes introduced
A validation pass over the round's earlier commits found four regressions I
had shipped. Each is reproduced, fixed, and covered by a test that fails when
the fix is reverted.
The multi-phenotype intersection returned {diseases, total_matched} without a
'warning' whenever 'limit' truncated it, but the object branch of its own
return_schema listed 'warning' as required and never declared 'total_matched'
-- so the tool violated its contract on its own shipped test_example, in both
directions at once. 'total_matched' is now declared, 'warning' is required
only where it is actually emitted, and the live response validates.
'offset' became a silent no-op on that tool. The new per-leg pager assigns its
own offset into the leg query and overwrote the caller's, so offset=0 and
offset=1 returned byte-identical lists for a parameter still advertised as
"Number of initial entries to skip" and used by the shipped test_examples.
The caller's offset now pages the intersection, which is what it documents.
Two of the ten new CIViC totalCounts were denominators that ignore the query.
browseDiseases/browseTherapies report the whole browse table regardless of the
filter -- 653 therapies for "imatinib", for "zzzzznotatherapy", and for no
filter at all -- so a search matching nothing published 653 under a schema
description telling the caller to divide by it. That is worse than no
denominator, and it was newly introduced. Those two connections now select
pageInfo only, which is filter-aware; the eight filter-aware totals stay.
Clamping the over-fetch turned a loud failure into a quiet short answer:
get_HPO_ID_by_phenotype answered limit=600 with 490 items under
status:"success" and no note, while Monarch_search_gene -- same class, no
result_id_prefix, so no clamp -- errored on the same request. A limit this
tool cannot serve is now an error on both, negative and zero limits are
rejected before reaching the wire, and a page left short by namespace
filtering says so.
Also: civic_get_variant emitted molecularProfiles.totalCount without declaring
it; the widening gate's docstring claimed "gene symbol" while the code also
matches synonyms and locus names; the phenotype intersection kept paging legs
after it was already empty, costing up to 20 upstream requests each.
Two tests were true by construction and have been rewritten to assert the
query on the wire rather than a fixture that already contained the answer.
* Fix Round 46: filters ChEMBL never applied, returned as full-table successes
ChEMBLRESTTool sent every leftover argument to ChEMBL as a query parameter.
ChEMBL ignores parameters it does not recognise and answers with the whole
resource, so an unsupported filter came back as status:success over a table
answering a different question. Verified live against /target.json: no filter,
?species=Schistosoma+mansoni and ?bananaparam=x all return the identical
total_count of 18552 with Homo sapiens rows on top, while the real parameter
?organism=Schistosoma+mansoni returns 12. A search for schistosome drug targets
was answered with 18,552 mostly-human ones and told it was a success.
The check now consults ChEMBL's own published filter list per resource
(/<resource>/schema.json, key "filtering") rather than the tool config, because
the two disagree in both directions and each direction was live. assay_organism
is a real /assay.json filter that ChEMBL_search_assays does not declare -- a
config-only guard rejected 591 genuine Leishmania major assays, which a persona
caught mid-round. organism__icontains looks like ChEMBL filter syntax, but
organism is not an /assay.json field, so ChEMBL drops it exactly like
bananaparam and returns all 1,970,438 assays; trusting the __ suffix would have
let the defect back in through the escape hatch the error message itself
recommends.
The built query is checked rather than the caller's arguments, so rewrites are
covered too. That found five more instances of the same defect shipped in the
tools' own configs, each confirmed by comparing filtered and unfiltered
responses upstream:
/binding_site.json?target_chembl_id__exact=CHEMBL2074 same row as no filter
/protein_classification.json?target_chembl_id__exact=. total_count 905 either way
/activity.json?compound_record_id__exact=1 activity_id 31863, as unfiltered
/document.json?document_id=1 byte-identical to no filter
/compound_structural_alert.json?alert_name__contains=. alert_id 1, as unfiltered
compound_record_id is now sent as record_id, the column /activity.json actually
has (?record_id=1 returns activity_id 57025) -- the tool's own *required*
parameter had been returning the whole activity table. document_id becomes
document_chembl_id and alert_name__contains becomes alert__alert_name__icontains
(394,234 rows of 5,020,133, versus no filtering at all). binding_site and
protein_classification cannot be filtered by target at all, so the parameter
promising it is gone and the descriptions name the tool to use instead.
Round 45's fix in this area was incomplete: ChEMBL_get_drug_mechanisms mapped
drug_chembl_id__exact to the correct parent_molecule_chembl_id but left the
alias in the arguments it rebuilt from, so the request still carried a parameter
ChEMBL discards. The alias is now dropped.
CIViC's variant pager had no page budget. Its only exits were "hasNextPage is
false" and "no endCursor", both the server's to give, so a zero-node page with
hasNextPage true never grew the list and a repeated cursor never advanced --
either one spins forever inside a tool the harness itself calls. The budget is
derived from the caller's own limit, so it cannot truncate a request that is
progressing; BRAF still returns all 114 variants and TP53 all 273.
CKD-EPI called an impossible eGFR normal. creatinine 0.01 mg/dL in an
82-year-old returned "eGFR 240.2 -- CKD stage G1 (normal, >=90)"; 240 is above
any human value including pregnancy hyperfiltration, so that label is wrong
rather than unqualified. A negative creatinine answered with the Python
internals of the resulting complex number, naming neither the parameter nor the
constraint. And creatinine 0.3 at age 82 -- the textbook low-muscle-mass case
where creatinine-based eGFR overestimates GFR, the direction that overdoses
renally-cleared drugs -- came back "normal" with nothing said. The score is
unchanged in every case; values above 150 are now reported as not stageable, and
inputs outside the equation's validated range carry a declared caveats list.
Each test was confirmed to fail with its fix reverted, per assertion rather than
per file: 5 of 10 ChEMBL assertions fail with the guard and the record_id
mapping disabled, and the remaining 5 are controls that must pass either way.
Two pre-existing ChEMBL tests began making live requests because the schema
lookup bypasses the request layer they mock; both are now offline, verified with
a socket guard reporting 0 connection attempts.
* Fix Round 46: close what validation found in the round's own changes
A validation pass over the round-46 commit found three defects. One is the same
class the commit set out to close, left half-closed by it.
ChEMBL_get_drug_mechanisms reads two `__exact` aliases as the drug ID and the
commit dropped only one of them after mapping. `molecule_chembl_id__exact` IS a
real /mechanism filter, so the new guard cannot see it, and it survived as a
second AND filter beside the parent lookup -- silently removing the mechanisms
recorded against other salt forms. Same drug, same intent, both status:success:
CHEMBL20 drug_chembl_id -> 8 mechanisms
CHEMBL20 molecule_chembl_id__exact -> 4 mechanisms
Confirmed upstream: parent_molecule_chembl_id=CHEMBL1382 returns 2 rows, and
adding molecule_chembl_id__exact=CHEMBL1382 returns 0. All five accepted
spellings now agree, verified across CHEMBL20 (8/8/8/8/8), CHEMBL1201354 and
CHEMBL1382 (2/2/2/2/2) rather than on the one drug the report named.
CKD-EPI applied its own new principle to creatinine but not to age. An
impossible creatinine became an error and an impossible eGFR stopped being
called normal, yet age=-5 still returned "eGFR 96.6 -- CKD stage G1 (normal)",
qualified by a caveat about paediatric validity that is a category error for a
negative number. A non-positive age is now rejected the same way.
The creatinine caveats were one-sided. Below 0.2 mg/dL the response said the
value is under the assay's reporting limit; 1000 mg/dL returned "CKD stage G5
(kidney failure)" with nothing said at all, so the same silence remained in the
direction a unit mix-up produces. Values above 20 now say so and name the
umol/L conversion.
Also: `ruff format` on the files this round touched, which pre-commit would
otherwise have rewritten.
Both new assertions were confirmed to fail with their fix reverted. Full unit
suite: 5058 passed, 27 skipped, 0 failed; src/tooluniverse/tools/ and the lazy
registry stayed clean.
* Fix Round 47: failed calls reported as empty answers, and filters that matched the wrong disease
Five fixes, all reproduced against the live upstream APIs before being changed.
Fix-47-1 / Fix-47-5 (openfda_tool, 157 tools): search_openfda handled
NOT_FOUND richly and let every OTHER upstream error fall off the end of the
branch as a bare None, which the CLI renders as {"result": null}. Confirmed
live against api.fda.gov/drug/label.json: one undeclared query parameter is
answered by openFDA with HTTP 400 {"code":"BAD_REQUEST","message":"Invalid
parameter: section"}, and FDA_get_boxed_warning_info_by_drug_name
{"drug_name":"tramadol","section":"boxed"} returned {"result": null} --
indistinguishable from "tramadol has no boxed warning", for a drug whose
label carries one. openFDA reports rate limiting (its anonymous tier is
~40 req/min), server errors and invalid keys the same way. The upstream error
is now returned in the same envelope the NOT_FOUND branch already used, so no
consumer sees a new shape. The three requests.get calls also carried no
timeout and decoded with a bare .json(), so openFDA's HTML error pages
surfaced as "Validation error: Expecting value: line 1 column 1" with
retriable=false; they now share one helper that adds a timeout and reports
transport failures as transport failures.
Fix-47-4 (tool_discovery_tools, compose_tool): the same artifact reached
callers through the dispatch wrappers independently of openFDA -- any tool
returning None became {"result": null} with exit status 0. execute_tool
dispatches through ExecuteToolTool.run; ComposeTool._call_tool is the second
wrapper, used by composition scripts. Both now report a failure as a failure,
through one shared invariant in base_tool.
Fix-47-2 (panelapp_tool): _word_matches bounded only how much was trimmed off
the SHORTER word to reach the shared prefix, and never bounded what was left
dangling off the LONGER one, so any short-ish query became a prefix search
with three characters of slack. Measured against the live 433-panel list,
search="hernia" returned 15 panels, every one a "Hereditary ..." panel with no
hernia content, under a count of 15; search="myopia" returned 3 myopathy
panels. Also matched: sarcoma/sarcoidosis, anaemia/anaesthesia,
neuropathy/{neuronal,neuron,neural}, disease/distal. Bounding both ends
rejects all of them while leaving plurals and the medical adjectival and
-osis/-otic forms untouched. Tokenization now strips punctuation, so a term
followed by a comma or inside a hyphenated compound is still matched.
Fix-47-3 (ncbi_datasets_tool): the species parameter is named `taxon`, but the
tool's own description asks for an "organism" and the sibling species-scoped
tool calls it that. A caller following either had the value dropped and the
lookup fell back to human, then echoed query_taxon: "human" as if that filter
had been applied: {"symbol":"NR2F2","organism":"mouse"} returned human NR2F2
(gene_id 7026, tax_id 9606), and organism:"Vulcan" returned the same human
gene just as confidently. Both spellings are now accepted and declared in the
schema, and an applied default is disclosed as query_taxon_defaulted.
* Fix Round 47: close what validation found in the round's own changes
Three defects found by validating round 47's own changes, plus one piece of
dead code the validation exposed.
1. An overstated claim. _word_matches' docstring said the surviving matches
were "unchanged, verified against the live list". They are not. Measured
over 214 queries against the live 433-panel list, the new bound removes
~198 wrong panel-hits but also loses ~51 real ones, concentrated where
PanelApp's own disease_group vocabulary is a longer derived form:
cardiac/cardiology (15 panels, including Brugada, ARVC and the long/short-QT
panels), immune/immunology (11), muscle/muscular (12),
arrhythmia/arrhythmogenic (6), pigmentation/pigmentary (3),
retinopathy/retinal (1). No threshold separates these from the collisions
the round set out to remove -- cardiac/cardiology needs five dangling
characters and sarcoma/sarcoidosis has six -- so the bound stays and the
cost is now stated instead of denied. The zero-result note names the
remedy, so a caller who searches "cardiac" is told to retry "cardiology"
rather than being left with a bare empty result.
2. A test that passed either way. test_short_words_never_fuzzy_match claimed
to pin the _MIN_WORD_LEN floor while both its inputs were above the floor;
_MIN_WORD_LEN could be changed from 6 to 4 with the whole file still green.
Replaced with pairs that are the right SHAPE to match and are rejected
solely for being too short (renal/renin, colon/colitis), which does fail
when the floor is lowered. The upper end of the suffix bound was also
unpinned; a 3 -> 4 mutation now fails two collision cases.
3. A gained false positive, previously undocumented. Splitting hyphens means
"non-syndromic" yields "syndromic", so search="syndrome" now matches three
panels that are explicitly NON-syndromic. Word-level matching has no notion
of negation and the alternative is losing every hyphenated compound, so
this is recorded as a known cost rather than fixed.
4. Dead code. _MAX_STEM_DROP=3 could not affect any outcome given
_MAX_SUFFIX_LEFT=3: longer - prefix <= N and prefix <= shorter <= longer
together give prefix >= shorter - N, so the trim bound can never reject a
pair the suffix bound accepts. That is why validation could not pin its
upper end, and it means the previous commit's comment crediting it with
holding the -osis/-otic class was wrong -- those pairs pass on the suffix
bound alone. Removed, after checking 298,378 pairs over the live panel
vocabulary and 400,000 synthetic pairs for zero behavioural difference.
The unreachable apostrophe branch of the tokenizer went with it.
* Fix Round 48: restrictions accepted, ignored, and answered as though applied
Four tools returned a confident answer to a question they had not been asked.
FAERS analytics accepted any argument it did not read and returned the
unrestricted result as a success. Measured live 2026-08-13, byte-identical with
and without the filters:
FAERS_calculate_disproportionality {"drug_name": "warfarin",
"adverse_event": "Haemorrhage"} a_drug_and_event = 4313
... plus {"patientagegroup": "6", "concomitant_drug": "aspirin"} = 4313
FAERS_filter_serious_events {"drug_name": "warfarin",
"seriousness_type": "death"} total_serious_events = 17327
... plus {"patientsex": "1", "occurcountry": "US"} = 17327
So a whole-database pharmacovigilance signal was handed to a caller who asked
for an age-stratified or interaction-adjusted one. The spellings are not
invented: this family's own sibling configs teach them -- patientagegroup
appears 60 times in fda_drug_adverse_event_tools.json, whose descriptions read
"all other filters (patientsex, patientagegroup, occurcountry, serious,
seriousnessdeath) are optional". The guard refuses before spending a request
and names the tool that does stratify.
search_clinical_trials reported an upstream refusal as an empty answer.
ClinicalTrials.gov answers {"intervention": "[177Lu]Lu-PSMA-617"} with HTTP 400
"Error parsing query in Other terms: extraneous input '['"; the tool discarded
that and replied "No studies found for the given query parameters", going on to
blame its own phase filter. Dropping the brackets returns 46 studies. The
bracketed form is standard EANM/IUPAC radiopharmaceutical nomenclature and
appears in trial titles this tool itself returns. execute_RESTful_query
collapsed every failure into False and printed the reason to stdout where no
caller could reach it; execute_RESTful_query_detailed returns it instead. The
boolean wrapper is unchanged for its other callers, including the Monarch 422
path that needs a 4xx JSON body handed back parsed.
Clinical calculators scored physiologically impossible inputs. ASCVD with
total_cholesterol=0 answered the raw text "math domain error"; Child-Pugh with
albumin=-1 scored the albumin component in its worst band and returned "Class B
(score 7): significant functional compromise"; HAS-BLED and CURB-65 with
age=-70 returned confident low-risk scores. Fix-R46 closed this for CKD-EPI
from inside that one equation -- the constraint now lives on the shared
_req_number helper, and CKD-EPI's hand-written guards were migrated onto it so
the two cannot drift.
LOINC asked its index for a field name the index does not use. df=SHORT_NAME
returns "" for every code; the index spells it SHORTNAME and returns
"Potassium SerPl-sCnc" for 2823-3. Seven further fields (SYSTEM, SCALE_TYP,
CLASS, STATUS, TIME_ASPCT, METHOD_TYP, COMMON_TEST_RANK) were probed one at a
time and are absent from this index entirely, yet were emitted as empty strings
-- SYSTEM being the field that separates serum potassium from urine potassium.
They are now disclosed, at the shared parse point so all four operations get it.
Two regressions were introduced and closed during the round. Policing arguments
against the declared schema alone refused a valid stratify_by="country" behind
a partial test config, so acceptance is the union of the schema and the module
vocabulary. Moving the CTG search onto the new transport without moving its
own _fetch_total_count probe left that probe reaching the live network under a
test that mocked the search; both now use one seam.
tests/unit: 0 failures. Each new test was confirmed to fail with its fix
reverted -- 7 of 11 FAERS assertions bite, the other 4 are controls that must
pass either way.
* Fix Round 48: close what validation found in the round's own changes
Four defects in the Round 48 changes, found by validating them.
BLOCKER -- execute_RESTful_query stopped handing back a 4xx JSON body. The
round split the transport into a detail-returning function and a boolean
wrapper whose docstring claimed it was "kept exactly as it was". It was not:
the new code consulted status_code after parsing, so a 422 carrying a valid
FastAPI body returned False where it had returned the parsed dict. That killed
the Fix-R16A path in MonarchTool, whose _validation_error_detail turns that
body into "lower your limit, or page with 'offset'" -- instead run() reached
`if "facet_fields" in response` with False and raised
TypeError: argument of type 'bool' is not iterable
across all six shipped Monarch tools. A decodable body is now always returned
whatever the status, and the status code is consulted only when there is no
body to read -- which is the ClinicalTrials.gov case the round was fixing,
whose 400 carries plain text rather than JSON. Both properties were then
confirmed to hold at once. Network-level exceptions propagate again, as they
always had; the round had been swallowing them into a (False, reason) pair.
The two tests that gave false assurance are fixed too. The round's own
test_boolean_wrapper_contract_is_unchanged exercised only 400-with-non-JSON
and 200-dict -- precisely the two cases that had NOT changed -- so it passed
while the contract was broken. It now covers 4xx-with-JSON-body at the real
seam.
FAERS -- the refusal message made a false referral. It told callers "the
FAERS_count_* tools DO filter on concomitant_drug"; concomitant_drug and
reactionmeddraversepa appear in no config under data/ at all. Both are dropped
from the record-filter list and now get the generic refusal. The remaining
eleven names were each checked to appear in a shipped config, since that is
what the sentence claims.
FAERS -- the message also advertised the wrong accepted set. It listed the
union across all six operations, so a disproportionality rejection named
drug1, drugs, stratify_by and seriousness_type as accepted -- arguments that
operation accepts and ignores. The error was advertising the very failure mode
it exists to close. It now names the calling operation's own declared
parameters.
LOINC -- the SHORT_NAME to SHORTNAME rename had been applied to the code but
not to the return_schema, which still declared the field that is never
populated and did not declare fields_unavailable or fields_unavailable_note.
ruff format had drifted on five files; all nine changed files are clean again.
tests/unit: 0 failures.
* Fix Round 49: two LOINC operations that could not succeed but reported success
LOINC_search_forms sent sf=CLASS and then kept only rows whose CLASS contained
"survey"/"panel"/"form". Neither half could ever hold. sf selects which fields
are searched, and the API documents the choices as "text, COMPONENT,
CONSUMER_NAME, RELATEDNAMES2, METHOD_TYP, SHORTNAME, LONG_COMMON_NAME,
SURVEY_QUEST_TEXT, LOINC_NUM" -- CLASS is not among them, so the search matched
nothing:
search?terms=PHQ-9&sf=CLASS -> [0,[],null,[]]
search?terms=PHQ-9 -> 58 hits
and CLASS is separately empty for every code in this index, so the post-filter
would have discarded every row even had rows arrived. All three shipped
test_examples returned count 0 presented as success: the tool's own documented
invocations demonstrated only its failure mode. Restricting with the documented
type parameter instead returns the real instruments -- terms=PHQ-9&type=form
gives the two PHQ-9 panels. A search that now legitimately matches no
instrument says so and points at LOINC_search_tests, so "no instrument has this
name" is distinguishable from a failed query.
LOINC_get_answer_list had the same shape one step further on. It sent
type=answer, which is not an implemented value; unrecognised values are dropped
rather than rejected, so the call degraded into an ordinary keyword search whose
rows were returned as "answer-type codes". Totals were identical for
type=answer and for an invented type=zzzgarbage on every term tried (PHQ-9
58/58, potassium 145/145, hemoglobin 526/526), and the index holds no answer
codes at all (terms=LA6568-5 -> [0,[],null,[]]). Asked for the ABO group answers
it returned the ABO group question. Answer lists are published through
ef=AnswerLists, which does carry them: 883-9 now returns list LL2419-1 with
Group A / B / O / AB, and each row carries the datatype that says whether an
answer list should exist at all. answer_lists_found is reported separately from
count so that matched codes without a list cannot read as lists.
Both changes keep every key these operations already returned; the added keys
are AnswerLists, datatype, answer_lists_found and note. The schemas gain those,
plus the fields_unavailable pair that search_tests and search_forms already
emitted without declaring. Two test_examples that could only ever show the
failure mode are replaced with ones that exercise the working path and the
honest-empty path.
Tests: nine of the ten new assertions were run against the pre-fix class and
fail on it; the tenth is a labelled control that must pass on both. The mapping
of ef values is covered with two rows so that attributing one code's
permissible values to another cannot pass.
* Fix Round 49: impossible inputs scored confidently at the top of the range
Fix Round 48 closed the bottom of every calculator's input range and left the
top open, so the same class survived at the other end. Reproduced through the
CLI before this change:
ClinicalCalc_MELD_Na {"bilirubin":1.0,"creatinine":1.0,"inr":1.0,
"sodium":134000}
-> status success, "MELD-Na 6: low (~1.9% 90-day mortality)"
ClinicalCalc_Child_Pugh {"bilirubin":1.0,"albumin":1000,"inr":1.0}
-> status success, "Class A (score 5): well-compensated disease"
and sodium 5 mmol/L, which is not compatible with life, was clamped into the
MELD-Na formula and reported as a risk band rather than refused -- zero is not
a floor for a concentration, so Fix-R48's must_exceed=0 could not catch it.
The bound belongs to the quantity, not the call site: bilirubin means the same
thing in Child-Pugh and MELD-Na, four of the eight bounded quantities are read
by more than one calculator, and looking the range up by parameter name means a
calculator added later inherits it by naming its input. Each maximum sits an
order of magnitude beyond the most extreme value reported in a living patient
-- 1000 mg/dL for bilirubin against a highest-recorded 80, 200 years for age
against a verified 122 -- and a control case of a genuinely crashing patient
(creatinine 8.0, bilirubin 45, INR 6.5, sodium 118) is asserted to still score,
so the bounds are strict without refusing a real crisis.
Cost: a value beyond the bound now hard-errors instead of being scored. That is
the intent for a unit mix-up, but it is a refusal rather than a wrong answer,
and a future quantity whose real range exceeds these numbers would be refused
too.
creatinine is deliberately excluded. Refusal is the right protection only where
the calculator cannot disclose, and both calculators taking a creatinine
already do: Fix-R46 gave CKD-EPI a caveat naming units, and MELD-Na bounds
creatinine per UNOS and reports the bounded value. Adding a maximum silently
reversed that shipped decision -- round 46's own test is what caught it -- so
the entry was withdrawn and a test now pins the disclosure behaviour of both.
Separately, MELD-Na's components reported a sodium the formula had not used.
The sodium term applies only above a MELD of 11 and applies a value bounded to
[125, 137], but sodium_used named the raw argument, so a patient with sodium
118 was told the score used 118 when it used 125, and a patient below the
threshold was told a sodium was used when none was. Its three siblings already
reported the bounded values they used, and the breakdown exists to make the
score auditable. sodium_used now holds what entered the formula (null when the
term did not apply), sodium_reported holds the argument, and a note explains
any difference.
* Fix Round 49: a disclosure that contradicted the rows it was attached to
Three personas independently hit the same thing: LOINC_search_tests returns
rows reading METHOD_TYP "PhenX", "ISE", "LC/MS/MS" or "Confirm", directly above
a note asserting that METHOD_TYP is "returned empty for every code and their
emptiness carries no information about this code".
Fix-R48 built that list by probing field-by-field against a single code, 2823-3
(serum potassium), and generalised its empty answer to the whole index.
Re-measured over 570 rows drawn from 19 unrelated search terms:
METHOD_TYP 260/570 populated (45.6%)
SYSTEM 0/570
SCALE_TYP 0/570
CLASS 0/570
STATUS 0/570
TIME_ASPCT 0/570
COMMON_TEST_RANK 0/570
The other six survive the wider sample; METHOD_TYP was never absent. It is a
costly field to disclaim -- it is what separates a presumptive immunoassay
screen from a confirmatory LC/MS/MS quantitation -- and the note was telling
callers to disregard it. The disclosure is now also checked against the rows it
is attached to, so no field can be named unavailable while any returned row
carries a value for it. That is the cheap invariant that would have caught this
at runtime rather than three rounds later. And it is no longer emitted at all
on a zero-result response, where it described the fields of no codes.
Also, from review of this round's own two commits:
MELD-Na now discloses every bounded input, not just sodium. Reporting only the
post-clamp value is what let creatinine 900 mg/dL come back as creatinine_used
4.0, status success, with 900 appearing nowhere -- the same defect this round
had just called unacceptable for sodium, left in place for its three siblings.
A single bounded_inputs_note names every input whose supplied value differs
from the value used, so one breakdown no longer carries two disclosure
conventions. The comment excluding creatinine from the bounds table asserted
that MELD-Na "reports the bounded value"; that is now true of what the caller
is actually told, not just of a number in the payload.
The Clinical Tables response parser reads its ef fields off slot 2 rather than
being handed the field list a second time, since naming that list twice at one
call site is a way for ef values to vanish silently, and it adopts the null
hardening its sibling client clinical_tables_tool.py already had in place of a
length guard that could not fire. The sodium parameter description no longer
restates the numeric bound that the table holds. A comment claiming the API
recognises only question/form/panel is corrected: form_and_section is also
accepted, and panel is the inert one.
One assertion added earlier this round could never have failed -- the error
path returns no data key, so "score" not in result["data"] was true by
construction. It now asserts the absence of data itself. Of the five new
assertions here, all five fail against the pre-change class; two of those fail
on the changed signature rather than on behaviour, which is weaker evidence,
and the three that matter fail behaviourally.
* Fix Round 49: close what validation found in this round's own changes
Validation of the three commits above found two Medium defects, both in the
guard those commits introduced, and both invisible to the tests shipped with
them.
The runtime cross-check that refuses to name a field unavailable while a
returned row carries a value for it was enforced in one of two call sites.
`_get_code_details` returns the row rather than the envelope, so it rebuilds
the disclosure itself and passed only the field list. With `results` defaulting
to empty, `not any(...)` is vacuously true and the check never ran: a row
carrying SYSTEM "Ser/Plas", SCALE_TYP "Qn", CLASS "CHEM" and STATUS "ACTIVE"
came back in the same dict as a note asserting all four are empty for every
code. Latent live, since those four really are empty in this index -- but it is
the exact mechanism that produced the METHOD_TYP defect this round set out to
fix, and the comment above the shared parser claimed every operation was
covered.
Worse, the test whose docstring read "The bite: the claim is checked against
the rows, not just asserted" probed with METHOD_TYP -- which the same commit
had just removed from the static list, so the static list excluded it unaided
and the row argument was never consulted. Deleting the cross-check entirely
left all 50 tests green. It is the pattern this harness keeps rediscovering: a
test that covers the case that did not change. It now probes with SYSTEM, which
the static list does name, so only the row check can withhold it; a second test
proves one populated row among several is enough to retract an index-wide
claim; a third covers the details path directly with a row that must have one
field withheld and another still named. Deleting the cross-check, or reverting
the details call site, or making the datatype hint unconditional now each kill
tests -- verified by mutating all three.
Also from that review:
The refusal message rounded the offending value through %g, so sodium
49.999999999 was refused with "must be at least 50, got 50" -- a contradiction,
in the one message these bounds exist to make actionable. The bound is still
formatted that way, being a round number written by hand; the value is not.
The sodium floor was the one bound not an order of magnitude clear of reality.
Survivable hyponatremia is reported into the high 70s mmol/L, so 80 sat on top
of patients who exist. It is now 50, still far above the sodium of 5 that
motivated it.
The 45.6% populated rate quoted for METHOD_TYP did not reproduce; an
independent sample of the same size put it at 26.7%, because which codes a term
matches varies. Both are recorded rather than averaged. Only the zeroes are
load-bearing: these entries claim a field is never populated, and the evidence
for that is that no sample has ever found a value. The test now demands a
measurement exist for any field added to the list.
Finally, the no-answer-list note referred callers to `datatype` for the
explanation. When upstream sends no ef block at all, no row has a datatype, so
the note pointed at something absent. It is appended only when the rows carry
one, and two tests cover the ef shapes that were correct but unexercised: a
slot-2 list shorter than the code list, and a null slot 2.
* Fix Round 50: counts and filters that described a page instead of the query
ClinGen_get_variant_classifications reported `total: 25` for PAH, a gene with
817 expert-curated classifications -- a 32x understatement presented as a
complete success. Re-probing the Evidence Repository API showed two causes,
neither visible from the code alone:
* `classifications` pages at 25 when `matchLimit` is unset, and returns no
total, so `len(items)` counted one page and called it the answer
* `variant` is not an endpoint parameter at all. It is accepted and ignored
-- a deliberately bogus `variant=ZZZNOTAVARIANT` returns the same rows as
sending no filter -- so the request degraded to an unfiltered listing and
the tool narrowed it client-side over whatever page came back
The second cause was the worse one. CAID CA16020993 (PAH c.1315+1G>T) is row
401 of PAH's 817 and is classified Likely Pathogenic by the Phenylketonuria
VCEP, but sat outside the 25-row page, so the tool answered "No variant
classifications found ... Not all genes have active VCEPs" -- a false negative
delivered with a confident and incorrect explanation.
The endpoint does implement `caid`, `variationId` and `hgvs`, each an exact
server-side filter (verified live: all three resolve CA16020993 to one row,
and `hgvs` matches protein-change strings such as p.Arg408Trp). Variant
lookups now name whichever one fits the identifier supplied, so no client-side
narrowing remains. `total` is now the query's count, `returned` the published
count, and the gap between them is disclosed rather than left to be inferred.
PANTHER_ortholog had the same shape in miniature: matchortho returns a list
when several orthologs match, and `mapped[0]` discarded the rest even under
`ortholog_type='O'`, whose own description promises "all orthologs". Human
ABCB1 -> mouse returns Abcb1b and Abcb1a; only Abcb1b was published, losing
half of the P-glycoprotein pair that makes the mouse the standard model for
human ABCB1. `mappings` and `total_mappings` are additive; `mapping` still
holds the first match so existing callers are unaffected.
Verified live before and after against both upstream APIs. Each new assertion
was confirmed to fail with the fix reverted, except the two that deliberately
pin unchanged behaviour.
* Fix Round 50: state the row cap beside the total on every ClinGen listing
Sweeping the rest of the ClinGen family for the shape fixed in the previous
commit found it three more times in the same file. `_get_gene_validity`,
`_get_dosage_sensitivity` and the actionability listing each published
`data: curations[:100]` next to `total: len(curations)` with no flag, no
`returned` count and no way to page.
Confirmed live: `ClinGen_get_gene_validity {}` reported `total: 3659` directly
above exactly 100 rows, on a tool whose description promises a "comprehensive
list". The cap is deliberate -- the gene-validity CSV is thousands of rows --
but saying `3659` beside 100 rows and nothing else leaves the caller to detect
the gap by counting.
All four listings now go through one `_published` helper, so the classification
handler and its three siblings disclose truncation identically instead of three
copies of a slice drifting apart. Each carries a hint naming the parameter that
actually narrows that listing, since a truncation flag with no remedy only
relocates the dead end.
Verified live before and after. Every new assertion was confirmed to fail with
the fix reverted except the one pinning that a narrowed query stays unflagged.
* Fix Round 50: MeSH_get_descriptor answered for records that do not exist
NLM's linked-data server returns HTTP 200 with a bare `{ }` for any well-formed
but unassigned MeSH identifier; only malformed ones 404, and that path was
already handled. The tool mapped the empty body onto its full field layout and
published the result as a success, so `D999999` came back as a complete-looking
record with `label: ""`, `tree_numbers: []` and `active: null`. A descriptor
that does not exist was indistinguishable from one whose label is blank.
Separately, descriptor records carry the name in `label` while Term and Concept
records carry it in `prefLabel`. Reading only `label` discarded a name the API
plainly supplies: `T010724` returns `prefLabel: "Dysphagia"`, and the tool
published `label: ""` while at the same moment reporting `type: "Term"` -- it
had already determined the record was not a descriptor and still reported
success with the name thrown away.
Between them these made an empty `label` ambiguous across three unrelated
situations. Now an empty body is an error naming the identifier and pointing at
MeSH_search_descriptors, `prefLabel` is read as the fallback name, and a record
that resolves to something other than a topical descriptor says so and names
the tool that resolves the descriptor owning it.
Both bodies confirmed live at id.nlm.nih.gov before and after. Topical
descriptors are unchanged, with a test pinning that they gain no note.
* Fix Round 50: say that serious-event reaction rows are not a breakdown of the total
FAERS_filter_serious_events publishes total_serious_events beside
top_serious_reactions, and the two count different things: the total counts
reports, while each reaction row counts reports mentioning one term. A single
report lists several terms -- confirmed live that warfarin death report
10037626 carries four -- so the rows overlap and routinely sum past the total.
The numbers were right; nothing said so. PRUSSIAN BLUE + death publishes
total: 1 above four rows of count 1 with truncated: false, which reads as four
fatal cases rather than one report carrying four reactions. The stratification
operation in this same module already spells the hazard out at length, so the
family disclosed it in one place and not the other, and the arithmetic has been
filed as a defect in the count more than once -- including in the brief for
this round, which is what prompted re-deriving it at the API rather than
changing anything.
Additive: one coverage_note field, no number changes.
* Fix Round 50: close what the quality review found in this round's own changes
Four review passes over the round's diff. Three findings were substantive:
Truncation disclosure used a vocabulary unique to this file. The repo already
publishes `truncated` in 26 files and `truncation_note` in 15, with two shared
implementations (cbioportal_tool._truncation_fields, clinvar_tool.
_truncation_disclosure). The new keys `data_truncated` / `data_truncation_note`
/ `total_is_a_lower_bound` / `total_note` each greped to exactly one file, so a
caller reading truncation generically would have needed a spelling that existed
only here, and one response could carry six keys for one idea. Renamed to the
convention and folded the request-cap case into the same two keys, so
`_published` now reports `truncated` explicitly either way.
PANTHER_gene_info had the same collapse `_ortholog` was fixed for, 180 lines
above it in the same file: `gene_data[0]` under a comment that says "multiple
genes". Confirmed live that gene_id "TP53,BRCA1" echoed both names back while
publishing only BRCA1's PTHR13763 -- one gene's family and GO annotations
served as the answer for another. Publishing one instance of a two-instance
class makes the pattern read as fixed when it is half fixed. `genes` and
`total_genes` are additive; the three top-level fields still describe the first
match.
The empty-result notes pointed at `clinvar_search_variants`; the registered
name is `ClinVar_search_variants`, so the remedy offered to a caller who had
just hit a dead end was itself a dead end.
Also corrected a docstring claiming ~1s response times: measured live, asking
for every row costs 1.5 MB / ~5s for PAH and 8.1 MB / ~15s for RUNX1, the
repository's largest gene, against this tool's 120s timeout. That is the price
of `total` being a count, and the docstring now states it rather than quoting
the old page-sized figure.
Two findings were declined and are recorded in the round report: lifting
`_published` into shared infrastructure (the review measured 42 undisclosed
sites across 32 files -- real, but far past this round's scope), and declaring
`coverage_note` in FAERS_filter_serious_events' return_schema, which is
`{"type": "object"}` with no properties at all, where naming one field and
omitting eight would read as a contract it does not have.
* Fix Round 50: close what validation found in this round's own changes
Three substantive defects, all introduced by this round.
MeSH_get_descriptor's new note was factually false for three record classes.
It fired for anything that was not a TopicalDescriptor and asserted the record
"has no tree numbers or annotation of its own" -- true only of Term and
Concept. PublicationType, GeographicalDescriptor and Qualifier are indexed in
their own right: D016428 returns tree V02.600 and a full annotation, so the
note printed a denial directly above the populated fields contradicting it,
and offered an entry-term remedy to a record no descriptor owns. Worse, the
test file covered only Term -- the single class for which the claim happened
to hold -- so the falsehood was invisible to a green suite. The note is now
written per class, and PublicationType has a fixture.
A whitespace-only `variant` defeated the guard that exists to prevent an
unbounded request. `" "` is truthy, so it satisfied "at least one filter",
then stripped to an empty `hgvs=` and sent exactly the unfiltered listing the
docstring says must never be sent -- measured at a full 120s timeout, with the
cost paid upstream too. Arguments are now normalised before the guard rather
than after it.
PANTHER_gene_info published `gene_symbol`, a key `geneinfo` entries do not
have, so it was null on every row while being newly declared in the schema.
The identifier the caller typed lives in `mapped_id_list`, and PANTHER does
not return rows in input order -- "TP53,BRCA1" comes back BRCA1 first -- so
the previous commit's fix was only half done: it stopped dropping genes but
left rows the caller could not attribute without decoding an accession string.
Rows now carry `input_id`, plus `family_name`/`subfamily_name`, which the
original inline code had also discarded.
Also corrected: the no-result note called the lookup "exact" when `hgvs`
matches on substring; the `variant` parameter description never mentioned that
CAIDs and ClinVar VariationIDs are accepted, which is the headline fix and was
undiscoverable from the schema, and its test_examples exercised only `gene`,
so no example ever ran the identifier routing; PANTHER_ortholog's description
still promised a single ortholog; and MeSH's `note` was undeclared in its
return_schema while this same round declared PANTHER's new keys.
Each of the seven new assertions was confirmed to fail with its fix reverted.
* Fix Round 51: FAERS answering about a different product than the one asked for
Three fixes, all in the wrong-entity seam: a query that matches something other
than the drug named, and reports its data as that drug's.
1. FAERS drug-name counts now disclose how much of the answer is a different
product (additive; 18 count tools).
Every FAERS drug-name lookup searches the union of FAERS_DRUG_NAME_FIELDS,
and two of those three fields are openFDA's SPL annotation rather than the
text the reporter wrote. openFDA tags a drug entry with EVERY brand and
generic name registered for its active ingredient, so a brand-name query
returns every product sharing that ingredient. The union itself is correct
and stays (dropping a field undercounts SEROQUEL and XELJANZ); what was
missing was any statement of what it swept in.
Measured live 2026-08-12 against meta.results.total, "named" =
patient.drug.medicinalproduct alone, "matched" = the union:
IOSAT 6 / 1,395 99.6% named a different product
CYANOKIT 238 / 4,119 94.2%
SUBLOCADE 5,134 / 28,481 82.0%
ADVIL 67,159 / 264,100 74.6%
XGEVA 36,039 / 51,441 29.9%
OZEMPIC 66,161 / 66,161 0.0%
MAKENA 22,729 / 22,729 0.0%
CYANOKIT is the cyanide-poisoning antidote; hydroxocobalamin is also a
routine vitamin B12 supplement, and 3,881 of its 4,119 matched reports are
B12 reports (sampled: safetyreportid 10016092, medicinalproduct
HYDROXOCOBALAMIN, drugindication MICROCYTIC ANAEMIA). Before this,
FAERS_count_reactions_by_drug_event{"medicinalproduct": "CYANOKIT"} answered
ACUTE KIDNEY INJURY 211 against total_reports_matching_query 4,119 with
nothing saying the denominator was mostly vitamin recipients.
The OZEMPIC and MAKENA rows are why the share is measured per call rather
than warned about statically. Responses gain reports_naming_queried_drug,
reports_matched_by_name_resolution_only and, only when the query really did
widen, drug_name_scope_note. All three are declared in the return_schema of
the 18 tools that emit them, since agents plan off the schema.
Cost: one extra openFDA request per drug-name count call (three, not two),
skipped when no drug name was passed, when the name searches a single field,
when the facet is empty, or when the denominator is unavailable. Worst-case
latency rises from 55s to 80s; both probes fail soft.
2. OpenFDA_search_drug_events no longer discards the drug it was given
(contract change; 1 tool).
drug_name and reaction were honoured only when a raw `search` was absent, so
supplying both counted the whole of FAERS. Measured live, {"drug_name": D,
"search": "serious:1", "count": <reaction facet>} returned a byte-identical
payload topped by DEATH 846,360 for "levetiracetam", for "lacosamide" and
for the nonexistent "ZZZ_NOT_A_DRUG_XYZ" -- each "status": "success". They
are now AND-ed, which can only narrow: levetiracetam gives SEIZURE 14,934,
lacosamide 5,969, the nonexistent drug 0. The raw-search-only and
drug-name-only paths are byte-for-byte unchanged.
3. FAERS_search_serious_reports_by_drug can now actually ask for serious
reports (additive; 1 tool).
Despite its name the tool had no `serious` parameter and no seriousness
field in its search map, and returned byte-identical output to its
unfiltered sibling. Measured over the first 100 reports, non-serious was 12%
for acamprosate, 24% levetiracetam, 39% ibuprofen and 79% gadobutrol. The
parameter its five sibling tools already had is added, via the existing
HUMAN_TO_FDA_MAP["serious"] mapping; the default still returns both classes,
so the description, return_schema and skill reference now say so instead of
implying otherwise. The generated SDK wrapper is updated to match, so the
parameter is reachable from the Python API.
* Fix Round 51: close what validation found in this round's own changes
Validation of the round-51 commit found two defects in it and three inaccurate
claims. All five are closed here; the two defects now have regression tests that
fail when the fix alone is reverted.
1. The negative-difference guard published the figure it said it suppressed.
`reports_naming_queried_drug` was written immediately after the scope probe
and was never removed on the bail-out, so a probe disagreement shipped a
subset count LARGER than the superset it is drawn from -- simulated at
named=5,000 against matched=4,119, the envelope carried
reports_naming_queried_drug 5,000 beside total_reports_matching_query 4,119,
contradicting both the comment ("Publish nothing rather than a figure that
cannot be true") and the return_schema. Both keys are now written after the
check; a failed probe still publishes an explicit null, which is a different
statement from "not applicable".
2. `_drug_name_fields`' documented fallback was not real on the parent path.
The accessor promises that a config which forgets its name map still asks
the same question as every other FAERS tool, but only the multi-drug
subclass went through it: the parent read `self.search_fields` directly and
dropped the drug name entirely, while the scope probe -- which keys off
DRUG_PARAMETER rather than off the map -- still applied it. With the map
removed the facet searched `search=` (nothing at all) and the note then
blamed SPL annotation for 899,900 reports. The parent now uses the accessor,
so the promise holds where it is made.
3. `OpenFDADrugEventsTool`'s class docstring and the `OpenFDA_search_drug_events`
tool description both still said the tool accepts a raw `search` string OR
the convenience parameters, thirty lines above the code that AND-s them --
so nothing an agent reads said the combination now works, which was the
point of the fix.
4. Two wording corrections: the disclosure reuses the denominator as its
denominator, not "as its own numerator"; and the return_schema for
`reports_naming_queried_drug` listed two of the four conditions under which
the key is absent, where the runtime docstring lists all four.
tests/unit + tests/integration: 5,641 passed, 36 skipped, 0 failed.
* Fix Round 52: a safety inference computed over a cohort that is 94% a different drug
FAERS_calculate_disproportionality built its 2x2 contingency table from a
drug-name union that also searches openFDA's SPL annotation, which tags a
report with every brand and generic name registered for the active ingredient
of a product the report named. A brand query therefore collects every sibling
product sharing that ingredient, and the tool published an ROR labelled with
the queried brand.
Measured live 2026-08-12/13. CYANOKIT, a cyanide antidote, has 4,119 reports in
that union and 238 under the reported product name; the top names in the union
are HYDROXOCOBALAMIN 3,882, ATORVASTATIN 775, BISOPROLOL 538 -- vitamin B12
supplementation and its patients' polypharmacy. Against DEATH:
union cohort a=50 b=4,069 ROR 0.287 [0.217, 0.379]
reported-name cohort a=12 b=226 ROR 1.24 [0.694, 2.216]
The union interval lies entirely below 1, so the tool told a poison control
physician that death is reported disproportionately LESS often for a cyanide
antidote. Restricted to reports that named CYANOKIT the answer is inconclusive.
Not a shifted number: the opposite verdict, with a confidence interval on it.
The union stays the analysed cohort. The size of the gap cannot distinguish
brand contamination from a generic correctly collecting its own brands --
TOFACITINIB is 93.0% non-naming and CYANOKIT is 94.2%, and the extra reports
are genuine exposure in the first case and a different drug in the second -- so
no rule can pick a cohort automatically. What changes is that the split is now
measured per call and published as `cohort_scope`, with the ROR restricted to
reports that named the drug alongside it when the two differ. Additive: no
existing figure moves; one clause is appended to `note`. FAERS_compare_drugs
carries it on each arm, where it matters more, because the verdict there is a
ratio of two cohorts.
Also in this change:
- The probes are paid for. `_get_faers_count` now asks with `limit=0` instead
of `limit=1`, which returns the same `meta.results.total` without a report
body: one analysis moved 212,426 bytes before and 3,157 after, while going
from 4 requests to 6. It also shares the FAERS count tools' report-total
memo, so a second analysis of the same drug costs 3 requests, not 6.
- That memo is new, and closes a cost the count tools' own disclosure
introduced: a four-tool CYANOKIT workup sent 12 requests for 6 distinct
questions, each probe fetched four times. It now sends 6 and repeats nothing.
Built on the package's own LRUCache, keyed on (endpoint, search) rather than
on the built URL so the two modules' spellings of one question collapse to
one entry and no api_key ends up in a process-global dict key. Successes are
cached with a 300s TTL; failures never are, so one rate-limited moment cannot
stick. Not single-flight: concurrent misses still duplicate, and the 12->6
figure is measured for sequential calls.
- ClinicalTrials.gov reports totalCount only on a first page, and the fallback
`data.get("totalCount") or len(studies)` filled the gap with the page size.
query_cond "silicosis" at page_size 3 reported total_count 20 on page 1 and 3
on page 2; mesothelioma reported 64 then 5. Paging a query to the end left
the caller holding a denominator equal to their last page. Now null plus a
note saying where the real total is, matching the sibling tool. The guard is
`is None`, not falsiness, so a genuine totalCount of 0 survives.
27 tests added, 21 of which fail when the fix they cover is reverted; the other
6 assert that existing behaviour did not move.
* Fix Round 52: close what validation found in this round's own changes
The validator ran the full unit suite, which this round had not, and re-derived
every numeric claim in the previous commit against the live API. It found 20
regressions and one false claim. All are closed here.
20 unit-test regressions, all introduced by the previous commit:
- 13 came from the new report-total memo being a process global with no reset
hook. A test that let a probe succeed handed its answer to a later test that
stubbed the same probe to FAIL, and the assertion under test is usually "a
failed measurement reads as null, not as a number" -- so the leak supplied
exactly the number that hides the bug. test_faers_count_facet_coverage
asserted a null denominator and got 4741, cached by an earlier test in its own
file. The new test file had its own fixture; the six pre-existing FAERS files
had no reason to know the memo exists. The reset is now autouse in
tests/conftest.py.
- 7 came from the limit=1 -> limit=0 switch and the two extra probes breaking
mocks that keyed on the literal "&limit=1" or supplied a response sequence
sized for four probes. Updated in place; the behaviour they cover is unchanged.
tests/unit is now 5351 passed, 0 failed, 0 errors, 28 skipped.
tests/integration is unchanged from before this round: the same 3 failures in
test_mcp_protocol.py and 7 errors in test_smcp_http_server.py, both pre-existing
on the base commit.
The false claim: the previous commit said keying the memo on (endpoint, search)
lets faers_analytics_tool reuse entries the count tools put there. It does not,
for any single-word drug name -- which is every drug in the worked examples. The
two modules spell the same question differently: faers_drug_name_clause always
quotes the drug name, while the count tools' _render_clause quotes only when the
value contains a space. Verified: "SODIUM CHLORIDE" collapses to one entry,
CYANOKIT does not. Normalising the quoting in the key would be wrong rather than
cosmetic, because quoted and unquoted are genuinely different openFDA queries
for a hyphenated name -- a bare hyphen is a Lucene operator, which is why
faers_drug_name_clause quotes at all. So the claim is corrected in all four
places it was made, and what the memo actually buys is stated instead: repeat
calls within a module. A second analysis of the same drug still costs 3 requests
rather than 6, and the count tools' own 12-to-6 saving is unaffected. The
limit=0 change, which is the larger part of the payback, was verified exact:
212,426 bytes before, 3,157 after.
The test that claimed to prove cross-module sharing was hollow -- it seeded the
memo through the analytics module's own key builder, so it could only show the
tool reading back what it had written. Replaced with three tests that assert
what is true, including one that pins the non-sharing so the comments cannot
drift back.
Also from the validator:
- faers_analytics_tool._get_faers_count defaulted a missing meta.results.total
to 0. That was already wrong; memoising it would have made a fabricated zero
sticky for the TTL. It now returns None, routing into the same "count query
failed" path as a transport error.
- FAERS_calculate_disproportionality's return_schema was {"type": "object"}, so
the round's new output object was documented only in prose. cohort_scope and
its members are now declared.
Corrections to the previous commit message: the mesothelioma figure quoted there
as "64 then 5" is not reproducible -- that query returns total_count 604 today.
The silicosis case (20 then 3) is confirmed, as is the underlying behaviour that
ClinicalTrials.gov omits totalCount on a paged response. The test count should
have read 28 added, 22 of which fail on a reverted tree, not 27 and 21.
* Fix Round 53: totals invented from the rows, and disclosures that described a different measurement
Four fixes, all of the same shape: a number or a sentence that was not
measured from what it claimed to describe.
1. ClinicalTrials_get_field_values invented its own facet size.
`uniqueValuesCount` fell back to `len(all_values)` when ClinicalTrials.gov
did not send one, and four lines later `upstream_truncated` was derived
from that fallback -- so the test was `len(all_values) < len(all_values)`,
False for every input, and any field without an upstream count certified
itself complete.
Measured live over the whole /api/v2/stats/fieldValues payload
(2026-08-13): 149 of the 418 fields carry neither `topValues` nor
`uniqueValuesCount` -- 92 INTEGER, 29 DATE, 28 BOOLEAN. All 269 STRING and
ENUM fields carry the count, so the fallback fired only where it was wrong.
Those 149 returned `unique_values_count: 0`, `values: []`,
`truncated: false`: a positive claim that the field has no distinct values.
They also each carried a summary that was parsed past and dropped --
trueCount/falseCount for BOOLEAN, min/max/avg for INTEGER, min/max/formats
for DATE. HasResults is the case that matters: a caller asking how many
registered studies have posted results was handed an empty list, while
upstream held 79,677 true and 518,832 false, summing exactly to the
registry total.
BOOLEAN facets are now materialized from the two counts. INTEGER and DATE
report `unique_values_count: null`, `truncated: true` and a new
`value_summary` carrying the range. The coverage note for those fields
previously asserted, in one sentence, both "leaving 598,509 studies
represented" and "The rows sum to 0."
2. The FAERS cohort disclosure covered two operations out of six.
Round 52 added `cohort_scope` to the two inferential operations. Four
siblings build the identical union cohort from the identical `_drug_clause`
and published nothing: `_stratify_by_demographics`, `_filter_serious_events`,
`_analyze_temporal_trends`, `_rollup_meddra_hierarchy`.
`_analyze_temporal_trends` is the sharpest: it emits a verdict. Live,
CYANOKIT (a cyanide antidote, 4,119 reports of which 238 named it) returned
trend "Increasing", percent_change 19900.0 -- the trend in vitamin B12
supplement reporting, published under the antidote's name.
The split moved into a shared `_cohort_split`; the restricted-ROR arm did
not move with it, since there is no restricted form of a stratification.
Each call site supplies prose describing its own output. Cost: one extra
openFDA request per operation for stratify and rollup, two for serious
events and temporal trends, all `limit=0` and memoised per process.
3. PubMed read one of NCBI's two disclosure containers.
esearch reports dropped terms in `warninglist` when the search ends with
zero hits and in `errorlist` when terms were dropped but hits REMAIN. Only
`warninglist` was read, so the disclosure fired exactly when the caller
could not be misled and stayed silent exactly when they could. Measured
live: "benzene hematotoxicity NQO1 GSTT1 rs180056600" returns 4 articles
with errorlist.phrasesnotfound ["rs180056600"] and a null warninglist;
"zzzqqqxyz toluene diisocyanate nonexistentterm12345" returns 1,477 with
two thirds of the query discarded. Both now report `terms_not_found`; an
unrecognised [field] tag reports `search_fields_not_found`.
4. Two ClinicalTrials.gov disclosures described a different measurement.
`relaxed_match_check` counts a probe built from the original wording plus
every other filter, then published only the rewritten parameters as
`relaxed_query`. Live, query_intr="levetiracetam" +
query_term="subcutaneous palliative" published `{"query.intr":
"levetiracetam"}` beside a 0 and the words "a genuine absence of matching
trials" -- while that displayed query matches 253 studies on its own. It
now publishes the query that was counted.
The coverage note's "The rows sum to 481,189" is a statement about the
whole facet; with page_size=2 the reader sees two rows summing to 324,064.
The sentence now says which rows it means.
* Fix Round 53 (validation): correct a false cost claim, remove a redundant request, harden three null paths
Applied from this round's own quality review of its first commit.
A false claim in the round's own code. `_cohort_split`'s docstring said "ONE
extra openFDA request per operation". Only `_stratify_by_demographics` pays
one. `_rollup_meddra_hierarchy` and `_filter_serious_events` pay two, because
a `count=` response carries no meta.results.total and the serious-filtered
total measures a different cohort. The docstring now gives the count per
operation, with the reason each needs what it needs.
A second inverted claim, same commit. The comment at `_filter_serious_events`
said probing the serious subset "would cost a request to answer the same
question". It would cost ONE added request (the numerator; `total_serious` is
already its denominator) against the two spent here. The union pair is bought
for memo reuse across sibling operations, not because it is cheaper, and the
comment now says that.
A redundant request removed. `_analyze_temporal_trends` fetched a union total
it already held: the `count=receivedate` facet is unpaged and complete, so
`sum(yearly_counts.values())` is both free and the more faithful denominator --
it is exactly the reports the trend was computed over. Measured live
2026-08-13 that the two agree, because `receivedate` is populated on every
report: CYANOKIT 4,119/4,119, IVERMECTIN 6,367/6,367, RIFAPENTINE 521/521,
OZEMPIC 66,161/66,161, HYDROMORPHONE 130,429/130,429. Net cost of Fix-R53-2
is now +1 request for stratify and temporal, +2 for serious events and rollup.
A regression this round introduced, found by the full unit run rather than by
the targeted one. `test_faers_rollup_pt_truncation` pins the request budget of
`_filter_serious_events` at 2; the disclosure makes it 4. The assertion is
updated to 4 rather than deleted -- it is the guard that catches a fifth
request appearing by accident -- and now also checks that three of the four
are `limit=0` probes carrying no report bodies.
Three null paths hardened, none of them reachable today, all of them resting
on invariants that spanned two functions:
- the coverage note's no-ranking branch is guarded on `unique_values_count
is None` rather than on `value_summary`, so a BOOLEAN missing its
trueCount/falseCount cannot fall through into the completeness prose that
would tell the caller zero rows "partition the studies that record it";
- sections 3 and 4 of that note state their own `isinstance` precondition
instead of relying on the early return above them to have fired;
- the falsy-denominator guard moved from four call sites into
`_cohort_split`, which also removes a divergence the four had already
drifted into -- three emitted `cohort_scope: null`, one omitted the key.
`cohort_scope` is now declared in `return_schema` for the three operations
that emitted it with no contract entry at all, following the precedent set by
FAERS_calculate_disproportionality of declaring the one key that matters.
Also drops a redundant early return in `_search_warning_metadata` that the
existing empty check below it already subsumes.
* Fix Round 53 (validation 2): close four contradictions validation found, three of them this round's own
Behaviour is unchanged except where noted; these are corrections to claims.
A no-op rewrite, and a comment that was wrong about Python. This round changed
`truncated` from `page_truncated or upstream_truncated` to `True if
page_truncated else upstream_truncated`, justified by "`upstream_truncated` is
tri-state, so `or` would coerce an unknown facet size to False". Python's `or`
returns its second operand when the first is falsy, so `False or None` is
None: the two forms agree on all six inputs, the rewrite was a no-op, and no
test could have distinguished them. Reverted to the original expression, with
a comment stating why it needed no change.
A test whose name asserted something unreachable.
`test_page_size_truncation_still_wins_over_an_unknown_facet_size` used a
fixture carrying `uniqueValuesCount: 3` -- a known size -- and the case its
name described cannot occur at all, since an unknown size implies zero rows
and zero rows cannot be page-truncated. Renamed to what it tests.
The genuinely reachable unknown-size case now has a test, and it earns its
place: a BOOLEAN field arriving without trueCount/falseCount has no rows to
count, no upstream count and no range to fall back on. Reverting the coverage
note's guard to the `value_summary` form this round shipped first sends that
payload into the completeness prose -- "they partition the studies that record
it", "The rows sum to 0" -- which is what widening the guard to
`unique_values_count is None` was for. The widening was load-bearing, not
defensive.
An arithmetic error in the cost disclosure this round wrote to replace an
earlier wrong one. The per-operation table sums to six additions; the sentence
below it said seven. Corrected, and the memo saving is now stated as measured
rather than as a single number: across all six operations on CYANOKIT + DEATH
in one process, 11 requests against 11 unchanged -- the four descriptive
operations add three and disproportionality then drops from six to two on the
probes they already answered. A drug-only workup is +1. Three is the worst
case, not the typical one.
The inverted cost claim corrected in the previous commit was still live in the
test file that commit edited. `test_serious_events_says_which_cohort_it_
measured` still said probing the narrower cohort "would spend a request to
answer the same question", so source and test disagreed about the same
measurement. The test docstring now matches the source: the wider cohort is
chosen for memo reuse, not because it is cheaper.
A live figure quoted from the wrong query. The Fix-53A-1 comment said the old
`relaxed_query` value matches 253 studies on its own; sent verbatim to
/studies it returns 306. 253 is what that query returns after the tool's own
AREA rewrite -- the query the comment is contrasting it against.
* Fix Round 54: NCBI esearch answered a broader query than asked, silently
esearch drops search terms it cannot match and answers the REMAINING query,
returning a full result set for a question the caller never submitted. Round 53
taught PubMed to read NCBI's disclosure of this. Every other eutils database
behaves identically and eleven other modules parsed esearch responses without
reading either disclosure container.
Measured live across fourteen eutils databases, each probe compared against the
same query with the nonsense terms deleted: all fourteen reported both dropped
phrases in errorlist.phrasesnotfound and returned a count IDENTICAL to the
query with those phrases removed -- the dropped terms constrained nothing.
All fourteen stayed silent (errorlist: null) when nothing was dropped.
The parsing now lives in one place, ncbi_eutils_tool.esearch_query_disclosure,
which carries the measurements; pubmed_tool delegates to it. Wired into six
modules covering seven tools. Three of them were worse than silent: they named
a field for the query that ran and put the SUBMITTED query in it --
NCBI_search_nucleotide and NCBI_SRA_search_runs (data.search_term) and
SRA_search_experiments (data.query_used). Additive: the key appears only when
NCBI reported something, and costs no extra request, since the containers are
already in the response every caller parses.
The sweep is not complete and pubmed_tool now says so: clinvar, dbsnp, pmc, geo
and unified_guideline_tools remain unwired, pmc and icite because they return a
bare list with nowhere additive to put the disclosure.
Also, a hyphenated drug name reached openFDA unquoted. _render_clause quoted a
value only when it contained a SPACE, and a hyphen is a Lucene operator, so a
single-token hyphenated name was reparsed into a union of DIFFERENT drugs while
the response reported in prose how many reports "named" the product.
faers_drug_name_clause had always quoted and gave this exact case as its
reason, so the two paths disagreed on the one input each was written about, and
a third comment called the divergence deliberate.
medicinalproduct:sumatriptan-naproxen 124792 unquoted vs 13 quoted
medicinalproduct:CO-TRIMOXAZOLE 24806 unquoted vs 1416 quoted
FAERS_count_reactions_by_drug_event reported 219,048 reports for
sumatriptan-naproxen -- sumatriptan (51,295) OR naproxen (172,094) -- for a
product with 1,438 reports under its real brand name; 852,859 for the
non-existent MANNITOL-SULFATE; and 141 for an invented product, affirming that
141 reports named it. All three now return the correct count, 0 for the latter
two. Found independently by two personas.
Quoting is now the default with the Lucene range as the only carve-out. The
cost was measured over every other value shape these builders send -- drug
names, generic/brand name, single and multi-word reaction terms, all eight
coded HUMAN_TO_FDA_MAP values, occurcountry, numeric age, and the single-date
receivedate form: quoted and unquoted return identical totals for all of them,
so no query changes except the ones that were wrong. The range carve-out was
re-confirmed the same way (197,676 unquoted, HTTP 404 quoted).
faers_drug_name_clause now calls _render_clause instead of restating the rule,
so the drift cannot recur, and the memo comment that called the divergence
deliberate is corrected: both modules now spell the same question identically
and share one cache entry per drug instead of two.
Nine pre-existing assertions pinned the old unquoted query strings; each was
updated, not deleted, and every structural property they guard -- group
parenthesization, AND/OR nesting, encode-once, range-unquoted -- still holds.
* Fix Round 54 (validation): a zero-hit search is not a broadened query
Validation found that `outputmessages` must not gate the disclosure. NCBI
emits `warninglist.outputmessages: ["No items found."]` on EVERY zero-hit
search, including one it executed exactly as submitted. Measured live:
term "cystic fibrosis AND 1500[dp]"
-> count 0, errorlist null,
warninglist {"phrasesignored": [], "quotedphrasesnotfound": [],
"outputmessages": ["No items found."]}
which produced `query_not_executed_as_submitted: True` and the warning "No
items found." The query WAS run as asked; it simply matched nothing, so the
flag asserted the opposite of the truth, and it was loudest exactly where it
was most wrong. Round 53 shipped this for PubMed alone; this round would have
spread it to seven tools, where every empty MedGen, GEO, SRA and nuccore
search would have gained a block claiming its query was not run as submitted.
Only an actual alteration -- a dropped phrase, an ignored phrase, a dropped
term or an unrecognised field -- now creates the block. `outputmessages` is
still carried alongside one of those, so the zero-hit case that DID drop a
term keeps its disclosure; both directions are pinned by tests, each proven to
fail when the guard is reverted.
Also corrected, all found by the same validation, all authored by this round:
* the shared docstring claimed unaffected responses keep their shape, which
the `outputmessages` gate made untrue on every empty search;
* the unwired-module inventory said "eleven other modules" and omitted
`clinical_society_tools`, whose `_pubmed_search` takes a caller-influenced
term and discards both containers -- it is twelve, and both it and
`icite_tool` are now named;
* `_render_clause` said a range is "the only carve-out" while the code has
two, the second being non-string values, which a test four lines below the
one asserting "the ONLY exemption" already pinned. Both now say so;
* the nuccore test fixture was labelled verbatim but spliced the count from
the "BRCA1 human" probe (7359) onto the errorlist of the "BRCA1" probe
(66893). Corrected to the probe it claims to be;
* the taxonomy fixture cited a measurement absent from the table it defers
to. That fifteenth, zero-hit probe is now recorded there;
* the trimmed PubMed test header still narrated a probe whose fixture was
deleted.
tests/unit + tests/integration: 3 failed, 7 errors, all pre-existing on the
branch base (test_mcp_protocol, and one test_smcp_http_server fixture),
verified by stashing this round's changes and re-running.
* Fix Round 55: result sets truncated without saying so, and pointers to tools that do not exist
CPIC_list_drugs returned 200 of 324 drugs and said nothing. The missing
124 include efavirenz and clozapine, and the omission is not theoretical:
a persona in round 54 concluded from this tool that efavirenz has no CPIC
guidance. Upstream reports `content-range: 0-323/*`; the endpoint had a
paging value hard-coded in its URL template while its own schema also
declared one, so it was sending `limit=200&limit=5`.
Two class invariants now guard the general case rather than the instance:
no endpoint may hard-code a paging value its schema also declares, and no
GeneOntologyTool may declare a parameter absent from its endpoint
template, which `_build_url` can only ever ignore. Both pass repo-wide.
The FAERS_search_* detail family returned a bare array with no total, so
a caller could not distinguish ten reports from the first ten of
thousands -- `rifampin`+`methadone` returned 10 rows against an upstream
total of 35. They now return an envelope carrying `count` and
`total_available`, using the vocabulary `BaseRESTTool` and
`CBioPortalRESTTool` already share rather than inventing a fifth spelling
for the same idea.
Two dead pointers were found by replacing a heuristic with a rule. The
existing pointer test used difflib similarity, which measurably would
have missed 5 of the 9 names the file already pins; a registry-derived
rule (second segment must be a verb shared across at least ten vendor
prefixes) catches those and surfaced two nothing had caught:
ChEMBL_get_molecule_by_chembl_id, quoted in four --help epilog examples
where a user copy-pasting it would exclude nothing, and
UniProt_get_uniref_search in a tool description. Real names are
ChEMBL_get_molecule and UniProt_search_uniref.
FDADrugInteractionDetailTool held 318 lines byte-identical to
FDADrugAdverseEventDetailTool, and this change had been paying every edit
into both copies. It now subclasses and overrides only the query builder,
the same relationship FDACountAdditiveReactionsTool already has with its
parent: 366 lines removed, 56 added.
Verified: CPIC returns 324 with efavirenz and clozapine present, matching
upstream exactly; the six FAERS tools re-run live with identical totals;
46 new tests pass and each fails on revert.
* Fix Round 56: a search that never searched, and results that hid their own obsolescence
WHO_Guidelines_Search returned byte-identical results for every query.
The query was slugified into who.int/health-topics/<slug>; confirmed live
that this 404s for anything WHO does not treat as a health topic
("methadone", "clozapine" and the nonsense token "zzqqxxwubble" all 404,
while "tuberculosis"/"malaria"/"trachoma" return 200). On a 404 the tool
scraped WHO's generic recent-guidelines listing and returned those as the
answer, each tagged is_guideline: true, with nothing marking the query as
unmatched. A differential sweep over all ten guideline search tools found
this was the only one whose output did not vary with the query (SIGN and
CTFPHC return empty for these terms because those small national sets
genuinely lack them, which the sweep confirmed against terms they do
cover).
Topic misses now search WHO IRIS, WHO's official institutional
repository, over its DSpace REST API -- confirmed genuinely
query-responsive: methadone 1027 hits, clozapine 324, trachoma 3964,
"zzqqxxwubble" 0. IRIS has no "guideline" document type, so is_guideline
there reflects the record rather than being asserted for every hit, and
each result reports in matched_via which backend answered it.
Mondo_get_disease on an obsolete term returned success with a fully empty
record -- indistinguishable from a live disease with no annotations. The
only signal was the word "obsolete" inside the name. This is reachable,
not hypothetical: GenCC returns disease_curie MONDO:0014978 for PADI6,
which feeds straight in. The Monarch payload already answers it
(deprecated: True, versus None for the live term) and the tool discarded
the field; the same field was discarded on the /search rows, so a search
listed an obsolete term beside its replacement with nothing to tell them
apart. Obsolete terms now say so and, via OLS4's term_replaced_by, point
at the current term (MONDO:0014978 -> MONDO:1010200, which carries the
PADI6 gene). Costs one extra request on the deprecated path only; live
lookups are unchanged.
ClinVar published a url that described no request: the bare
esummary.fcgi endpoint, identical for every variant and an error when
replayed, because the query lived in params=. Now reports the URL as
sent. A repo-wide AST sweep found 11 sites across 7 files with this
shape; the remaining 10 are listed in the round report, unverified and
not touched here.
All 14 new tests fail when their fix is reverted, per assertion.
* Fix Round 56 (quality): one meaning for is_guideline across both WHO backends
Follow-up from the round's review pass, which found the previous commit
had left is_guideline meaning two different things in adjacent branches
of the same class: derived from the record on the new IRIS path, still
hardcoded True on the health-topic path. Both feed one result list, so a
consumer filtering on the field got an answer that depended on which
backend happened to answer.
Both backends now emit through one _result() shaper. Measured cost, live:
tuberculosis 5/10 rows keep is_guideline True (the consolidated
guidelines; the operational handbooks correctly drop to False), while
trachoma 0/2, malaria 0/5 and onchocerciasis 0/3 go to zero -- those
topic pages list epidemiological updates, procurement guidance and
surveillance toolkits, none of which are guidelines. Callers filtering on
is_guideline see a large drop in yield; the previous yield was asserted
rather than established. The flag is a title/document-type test, which
will miss WHO guidance published as handbooks or manuals, and the tool
description now says so.
Also from the review:
- _replaced_by is MONDO-only. The OBO PURL it builds is wrong by
construction for Orphanet (orpha.net/ORDO, OLS4 ontology "ordo"), EFO
and OMIM, all of which reach _normalize_curie, so those would have
spent a request to learn nothing.
- its timeout is capped at 10s independently of the Monarch timeout, so a
slow OLS4 cannot stall an answer that is already complete.
- the 0.5s sleep now spaces the second who.int request instead of
delaying the first request of every call.
- the per-item closure in _search_iris is a module-level _dc_value.
- corrected this round's own test docstring: chebi_tool and hpo_tool
disclose silent ID substitutions, they do not resolve replacements.
15 tests, all failing when their fix is reverted.
* Fix Round 56 (validation): declare the fields the round added, and correct two claims
From the round's validator pass.
- Monarch_get_entity and Monarch_search now emit `deprecated`, but only
mondo_tools.json declared it; monarch_new_tools.json listed every other
field and omitted this one, so the schema under-described the payload.
- WHO_Guidelines_Search declared `items: {type: object}` with no
properties at all, so none of matched_via, document_type, date_issued
or is_guideline was described anywhere a caller would look. Declared,
with is_guideline stating plainly what it does not detect.
- The WHO description claimed topic pages are used "when the query names
a WHO health topic". _topic_slug also retries on the query's first
significant word: live, "malaria vaccine" is answered by the whole
malaria topic page. matched_via already disclosed the broadening in the
payload; the description now does too.
- Corrected this round's own test docstring, which cited
api-v3.monarchinitiative.org while the tool calls
api.monarchinitiative.org. Re-checked both hosts: deprecated: True on
each, so the finding stands, but the citation now names the host under
test.
- Scoped the ClinVar test's claim to the success and rate-limited paths.
The two exception paths hold no response object and still report the
pre-request URL, which the previous framing did not admit.
* Fix Round 57: empty results that hid a dead identifier, and a URL that described no request
Round 56 fixed Mondo_get_disease to report deprecated/replaced_by, having
found Monarch's payload already carried the field. Its siblings answer the
same dead identifier with an empty success. Confirmed live on
api.monarchinitiative.org for three separate obsolete CURIEs, each with a
populated replacement one hop away: MONDO:0014978 -> MONDO:1010200 (2
phenotypes, PADI6), MONDO:0002906 -> MONDO:0019340 (190 phenotypes),
MONDO:0001111 -> MONDO:0018896 (83 phenotypes). Reachable, not
hypothetical: GenCC_search_gene {"gene_symbol": "PADI6"} hands back
MONDO:0014978, verified in this tree.
These endpoints return rows and nothing else, so there is no deprecated
field in the response to surface -- obsolescence has to be asked for
separately. Hence a hook on the empty path rather than a response shaper.
Mondo_get_disease_phenotypes, MonarchV3_get_histopheno,
MonarchV3_get_mappings and MonarchV3_get_associations now say the CURIE is
obsolete and name its replacement. Cost: one /entity request (measured 259
ms) only when the result set is empty, plus the OLS4 lookup (355 ms) only
when the term really is obsolete; non-empty results and non-MONDO CURIEs
are untouched.
The same shape one family over: HPO_get_diseases_by_phenotype and
HPO_get_genes_by_phenotype returned an empty success for a merged term
while HPO_get_term, in the same module, already detected the merge.
HP:0003114 -> 0 diseases; its replacement HP:0001626 -> 4962. The JAX
annotation endpoint answers a merged ID with HTTP 200 and empty arrays, so
upstream genuinely has nothing and the tool must disclose rather than
recover. The detection is the existing one: /api/hp/terms/<id> silently
serves the replacement's record, so a requested-vs-returned id mismatch is
the merge. The note text moved into a shared helper so both callers keep
one wording.
Round 56 fixed ClinVar's bare esummary.fcgi URL and listed ten more sites
unverified. Verifying them found the listed KEGG and HAL sites are not
instances -- KEGG builds its query into the path, so its published URL is
already complete (rest.kegg.jp/find/pathway/glycolysis), and HAL's is a
per-document link. The real instance is the shared NCBI E-utilities base,
reachable from four tools across three modules: geo_search_datasets,
geo_get_dataset_info, dbsnp_search_by_gene and NCBI_search_nucleotide all
published the bare endpoint, identical for every query, and replaying it
returns "Empty term and query_key - nothing todo". Now reports the URI as
sent; the replayed URL reproduces the request exactly. E-utilities takes no
credentials on these paths, so nothing secret is exposed.
PubMed_Guidelines_Search fetched the number of matching guidelines and
discarded it with a dead expression statement: "therapeutic plasma
exchange" returned 3 rows while esearch reported 94. It already returns a
dict envelope, so publishing metadata is additive here, unlike its four
sibling guideline tools whose bare-list returns keep them deferred.
retrieved is reported separately from returned because the rows are
filtered client-side against the query terms, so only total > retrieved is
upstream truncation.
All four fixes are additive; no contract changed. 31 new tests; each fix's
assertions were proven to fail against its own reverted source.
* Fix Round 57 (quality): one home for three duplicated idioms
Follow-up to the round-57 fixes, from the simplify pass. No behaviour
change except where noted.
The "resolved request URI, else the endpoint" helper existed three times
with two different coercion rules: tcia_tool's _requested_url, the new one
in ncbi_eutils_tool, and base_rest_tool's echo_request_url gate. The first
two were byte-equivalent, so they now share request_url() in base_tool.py.
base_rest_tool's gated variant is left alone -- defaulting it on is a
behaviour change across every BaseRESTTool, recorded as a follow-up rather
than smuggled into a cleanup.
The Mondo deprecation note was built twice, and the wording had already
drifted between the two copies in the same round. Both now call
_deprecation_metadata(), which takes the clause naming what is empty so one
sentence serves a stripped record and a stripped result set. This also
carries the OLS4 timeout cap, which _obsolescence_metadata had copied and
recomputed, to both callers.
The three row-returning Monarch endpoints had grown identical
build-metadata / probe-if-empty / envelope triples; they share _rows().
HPO's merge disclosure was factored down to the note string only, leaving
the three-key dict written twice. _merge_disclosure() now owns the keys and
the id-mismatch test, so _get_term and _merge_metadata cannot drift.
Two real improvements fell out of the review rather than the round:
_merge_metadata was passing the full 30s timeout where its Monarch twin
caps at 10s, and the JAX annotation payload carries genes, diseases and
assays together -- so a non-empty sibling array already proves the term
resolves, and the probe is now skipped in that case instead of spending a
round-trip to learn nothing.
PubMedGuidelinesTool._search_pubmed_guidelines returned a 2-tuple whose
first element was either a list or an error dict, so run() unpacked a value
it discarded on one branch and two error paths padded a {} nobody read. It
now returns the finished envelope and run() returns it directly.
That last change broke test_pubmed_guidelines_envelope.py, which stubbed
the private helper's old return shape -- found in the full suite, not in
the targeted re-run. The Fix-R9E-1 guard it encodes is still correct and
still wanted, so the three tests were rewritten to stub the HTTP session
instead of the helper; they now assert what run() hands a caller, which is
what R9E-1 was about, and no longer break on an internal refactor.
Skipped, with reasons: memoizing the deprecation probe (process-global
state across tests, against this repo's hermeticity rule); dropping
`truncated` as derivable (sibling search tools publish it, so consistency
wins); wiring esearch_query_disclosure into the guideline search (a
behaviour addition, not a cleanup); publishing totals for the four sibling
guideline tools (contract change, deferred).
* Fix Round 57 (validation): pin the shared note, and make the probe guard match its comment
From the round's validator pass. All three were reported as observations,
not gate failures; the suite was green before and after.
The Mondo deprecation note's tail changed wording when Fix-R57-4 moved the
sentence into a helper, and nothing caught it: the existing test asserted
only that "obsolete" and the replacement CURIE appeared in the string. HPO's
equivalent wording was already pinned, so the asymmetry meant Mondo's could
drift again silently. Both clauses are now pinned verbatim -- the one naming
what is empty is exactly the part that differs per caller, so it is the part
worth holding.
The HPO probe guard's comment claimed the annotation payload's four arrays
are all checked; the code checked three. Rather than narrow the comment, the
guard now checks medicalActions too, so a term annotated only with medical
actions no longer spends a round-trip to learn it is live.
Both PubMed error-path tests raised a bare OSError, which lands in the
blanket handler rather than the requests.RequestException branch the test
names describe -- so the documented branch was asserted only via the
catch-all. Parametrized over both.
Targeted suite: 229 passed, 0 failed.
* Fix Round 58: three values parsed and dropped, one of them naming the wrong species
BVBRC_search_specialty_genes selected `organism` -- BV-BRC's reference
annotation organism -- and not `genome_name`, the genome the hit was found
in. The two disagree routinely, so "which species carries blaKPC" was
answered with the wrong species: sp_gene row genome_id 550.1655 reported
organism "Klebsiella pneumoniae" for a genome named "Enterobacter cloacae
strain 1-RC-17-04409-1", contradicting its own adjacent taxon_id 550. The
sibling BVBRC_search_amr already selects genome_name; select it here too and
keep `organism` in place.
gnomad_get_gene_constraints asked only for gnomad_constraint and
exac_constraint, which gnomAD leaves null for mitochondrially-encoded genes
because it scores them under mitochondrial_constraint. MT-ND4 therefore
returned success with both null and no note, reading as "unconstrained" for
a gene gnomAD scores at oe_lof_upper 0.022. Request the field, both union
branches (protein and RNA), and say plainly when a gene has none of the
three rather than leaving nulls to be misread. The live query lives in the
shipped config, so the fix is there; the class default is kept in step.
UniProt_id_mapping hardcoded failed_ids to [] and reported the returned page
size as mapped_count, so Q9F663+NOTAREALACC99+P0DTC2 came back
"mapped_count 25, failed_ids []" with one valid accession and one typo'd one
silently absent, against an upstream x-total-results of 2236. Read failedIds
and the total, and stop treating the 303-redirected status page as final --
the resolution Fix-R20A-1 already applied to the sibling
UniProtIDMappingTool, which is a second copy of this logic. Q9F663 now
returns 83 of 83 rather than 25.
Return schemas updated to match, including gnomad_constraint's nullability,
which the old schema forbade.
* Fix Round 58: guideline searches reported a page of rows as the whole corpus
Six guideline backends publish a match count. Five parsed it and dropped it
with a bare expression statement -- Europe PMC `data.get("hitCount", 0)`,
TRIP `int(total.text) if total is not None else 0`, OpenAlex
`data.get("meta", {})` -- and NICE and WHO IRIS never read theirs. Each tool
then returned a bare list, so there was nowhere to put the figure even once
read, and `limit` rows were indistinguishable from everything that matched.
Measured live against the hosts the tools call: NICE "infection" is 1057
documents, of which the tool served 15; TRIP vancomycin 12638; WHO IRIS
tuberculosis 28435; OpenAlex 18266; Europe PMC 9972; PubMed 94.
Round 57 fixed PubMed_Guidelines_Search alone and deferred the rest rather
than leave the family half-converted. That left it worse than half: the new
envelope validated against neither branch of the tool's own declared
return_schema (`oneOf: [array, {required: [error]}]`), and
examples/workflows/clinical_guidelines_search.py, which unwraps `guidelines`
and `results` but not `data`, silently dropped every PubMed row.
So convert the whole set to the {status, data, metadata} envelope that
PubMed_Guidelines_Search and EuropePMC_search_articles already use, and fix
all six return_schemas -- including PubMed's, which was wrong before this
change. `retrieved` is separate from `returned` because several of these
filter rows client-side, so only total > retrieved is upstream truncation.
TRIP's <count> is deliberately not used: limit=3 answers <count>20</count>,
a page size, which is the defect being removed.
GIN, CMA, SIGN and CTFPHC keep their bare-list contract. Their sources
publish no match count, so an envelope could only report total ==
len(results) -- manufacturing the same defect in six more tools. The split
is by whether a real total exists, and is stated in the guide.
Also fixes a NICE test that asserts guidance ids start with ng/cg. NICE
serves in-development guidance under /guidance/indevelopment/<id> and ranks
one into "diabetes" today, so that assertion fails on unmodified main; this
was confirmed by stashing the change and re-running.
* Fix Round 58 (validation): restore the entry objects a wider page had flattened
The validator caught a regression in the round's own UniProt change. For
`to_db=UniProtKB` the job's 303 redirect lands on
/idmapping/uniprotkb/results/, which streams full entries; the generic
/idmapping/results/ returns bare accession strings. Re-fetching the generic
URL to get a bigger page therefore replaced every {accession, id, gene_name}
object with a plain string -- P04637 came back "P04637" instead of carrying
P53_HUMAN. Re-fetch through the URL the redirect actually landed on, which
keeps the shape while still asking for a full page, and skip the second
request entirely when the status response already carried every row (its
x-total-results and failedIds are on that response too). P04637 -> PDB still
returns 295 of 295, against 25 before the round.
`to` as a bare string was never admitted by the declared return_schema even
though the tool has always returned it for non-UniProtKB targets, so the
schema now carries both forms -- the same "output matching neither branch"
defect this round set out to fix, found one layer down.
Quality pass over the round's own diff:
* `_guideline_envelope` now pairs `truncated` with a `truncation_note`.
Every other truncation discloser in the repo says what to do about it;
a bare boolean states the fact and withholds the remedy.
* Dropped two empty-result early returns in Europe PMC and TRIP that
produced byte-identical output to the normal exit, and the PubMed
envelope closure, now a bare partial application over two call sites.
* `_note_absent_constraints`/`_CONSTRAINT_KEYS` moved to module level in
gnomad_tool.py beside `_missing_callset_note` and `describe_dataset`,
which handle the same genre of concern; also fixes a ruff-format
violation the validator flagged.
* Deduplicated a six-fold copy-pasted unwrap in test_guideline_tools.py.
* Removed test_both_organism_fields_reach_the_caller: it stubbed the
request helper with a hand-built row, so the select_fields list under
test was never consulted and it passed against pre-fix code.
* test_guideline_search_totals.py now asserts every search tool in the
config is classified, so a new one cannot land unguarded.
Declared BVBRC `genome_name` and gnomAD `note` in their return schemas,
which the validator found undeclared.
* Fix Round 59: CPIC_get_recommendations dropped rows without saying so
CPIC_get_recommendations applies a documented default `limit` of 50 but
reported only `count`, which is the size of the returned page. Confirmed live
that guideline 100416 (CYP2D6/OPRM1/COMT and opioids) has 66 recommendation
rows, so the default response was `count: 50` with no note and no total --
indistinguishable from the guideline having exactly 50, while 16 rows were
dropped. Codeine, tramadol and hydrocodone guidance all lives on that
guideline.
tu run CPIC_get_recommendations '{"guideline_id": 100416}'
before: count 50 (no total, no note)
after: count 50, total_count 66, offset 0,
note "Showing recommendations 1-50 of 66 ... offset=50 ..."
This is the same defect CPICGetAllelesTool in the same module already fixed;
its class docstring describes it. That tool's Content-Range helper is hoisted
to module level and reused, and the near-identical paging sentence both tools
build is now one `_paging_note` helper -- the alleles note comes out
byte-identical, verified live. The exact server-side COUNT is requested only
on the unfiltered path: when gene/phenotype filtering is applied the request
already pulls limit=1000 and the total that matters is the number of matching
rows, counted locally, so asking PostgREST for a count there would be work
thrown away. Purely additive -- no existing key changes value.
CPIC_search_gene_drug_pairs documented `limit` as "(default 50)". There is no
such default: confirmed live that an unfiltered search returns all 635 pairs
and a CYP2D6 search returns all 81. Returning everything is the better
behaviour, so the description is what was corrected.
Not done, deliberately: the nine CPIC return_schemas were NOT rewritten to
describe the {status, data, ...} envelope. That change was made, then reverted
after it was proven to break `tu test` 2/2 on CPIC_get_gene_info. Per issue
#246 the repo convention is that return_schema describes the inner `data`
payload -- cli.py validates `result["data"]` against it, and
tests/tools/test_return_schema_data_shapes.py asserts an envelope-shaped
schema must fail. 319 of the 328 envelope-returning configs already follow
the payload convention. The nine CPIC schemas were correct as shipped.
* Fix Round 60: Crossref searches reported a page of works as the whole corpus
CrossrefRESTTool._process_response extracted message["items"] and set
`count` to len(items), discarding message["total-results"] -- the only number
Crossref publishes for how many works actually match. With the documented
`limit` defaults (10 for works, 20 for funders/members) that made the page
size look like the size of the result set, on all four Crossref list
endpoints.
tu run Crossref_search_works '{"query": "CRISPR gene editing", "limit": 3}'
before: count 3 (no total, no note)
after: count 3, total_count 1037336,
note "Showing results 1-3 of 1037336 ... offset=3 ..."
tu run Crossref_search_members '{"query": "university", "limit": 3}'
before: count 3 after: count 3, total_count 4032
tu run Crossref_list_funders '{"query": "cancer", "limit": 3}'
before: count 3 after: count 3, total_count 746
Swept all eight CrossrefRESTTool configs. Four are list endpoints and all
four publish total-results upstream (works, funders, members, types); the
other four are detail lookups whose message carries no `items` and are left
byte-identical. /types is the one list endpoint that never diverges -- it
returns all 30 rows -- so it reports total_count 30 and correctly emits no
truncation note.
`offset` is added to the three searchable list tools so the note's advice is
actionable; verified live that Crossref pages on it (funders "cancer"
offset=0 -> Canceropôle PACA, Instituto Nacional de Cancerología; offset=2 ->
Cancerfonden, Syöpäsäätiö) and that the note counts from the caller's offset,
not from zero. The offset is read back off the URL requests actually
assembled, so it cannot drift from what was sent.
Purely additive: `data`, `count` and `url` keep the values they always had,
and total_count/note are omitted entirely when Crossref sends no usable
total. return_schema is untouched -- per issue #246 it describes the inner
`data` payload, and the new keys live in the envelope.
* Fix Round 60: a merged UniProt taxon was answered as the one that was asked for
UniProtTaxonomy_get_taxon fetched rest.uniprot.org/taxonomy/{id} and read
taxonId straight out of the body. UniProt answers a merged (retired) taxon
with an HTTP 303 to the node it was merged into, so requests followed the
redirect and the tool returned a different taxon than the caller asked for --
with the requested id appearing nowhere in the response, making the
substitution undetectable.
tu run UniProtTaxonomy_get_taxon '{"taxon_id": "46170"}'
before: taxon_id 1280, "Staphylococcus aureus" (46170 absent entirely)
after: query_taxon_id 46170, taxon_id 1280, merged_from 46170,
metadata.note "...merged it into 1280 ... the protein counts
are for the merged node, which may sit at a higher rank..."
Taxon 46170 is Staphylococcus aureus subsp. aureus, the subspecies node many
older BioSample/BioProject MRSA records are filed under; 1280 is the species.
Silently swapping one for the other inflates the protein-count denominator
any per-subspecies estimate is built on. UniProt publishes the merge plainly
-- `HTTP/2 303 ... location: /taxonomy/1280?from=46170`, and with redirects
disabled the body is
`{"taxonId":46170,"inactiveReason":{"inactiveReasonType":"MERGED","mergedTo":1280}}`
-- so the information was available and simply discarded.
Same shape as the obsolete-identifier disclosure already shipped for Mondo,
Monarch and HPO: answer with the current record, but say so. Detection
compares the requested id against the returned one, which needs no extra
request, so the fix costs nothing on the wire. String and integer ids compare
equal ('9606' and 9606 are one taxon) so an active lookup can never be
mislabelled a merge, and a non-numeric id degrades to no comparison rather
than an exception.
Purely additive: every field that was there before keeps its value, and
query_taxon_id is echoed on the happy path too so a caller can always join
request to response. The sibling UniProtTaxonomy_search is untouched.
* Fix Round 60: HGNC alias resolution note read 'a alias symbol'
The symbol-resolution note was templated as "a {relation}", and one of the
two relation labels starts with a vowel, so every alias hit -- the more
common of the two -- produced "a alias symbol".
tu run HGNC_fetch_gene_by_symbol '{"symbol": "CAH1"}'
before: "...listed as a alias symbol of 'CYP21A2'..."
after: "...listed as an alias symbol of 'CYP21A2'..."
tu run HGNC_fetch_gene_by_symbol '{"symbol": "CYP21"}'
before/after: "...listed as a previous symbol of 'CYP21A2'..." (unchanged)
Cosmetic only -- no key, value or control flow changes -- but this note is
the tool's disclosure that it answered with a different gene than the one
asked for, so it is worth having it read as written prose. The existing test
asserted only that 'alias symbol' appears in the note, which the defect
satisfied; it now pins the article, and a new case pins the consonant-initial
label so the helper cannot regress the other direction.
* Fix Round 60: OpenFoodFacts_search_products documented a default it never applied
page_size was described as "Default: 10". No default was ever applied: the
schema declares none, so BaseRESTTool._build_params has nothing to inject and
Open Food Facts' own default for /cgi/search.pl is what callers get.
tu run OpenFoodFacts_search_products '{"search_terms": "almond milk"}'
documented: 10 products
actual: page_size 50, 50 products
Swept both Open Food Facts tools that document a page_size default. The
sibling OpenFoodFacts_filter_products_by_tags runs against a different
endpoint and returns exactly the 20 it documents -- confirmed live -- so only
one of the two descriptions was wrong, and the correction is deliberately not
applied to both.
Returning 50 is the better behaviour (a caller who wants 10 passes 10), so
the description is what was corrected rather than the behaviour -- no
response changes value. The test pins the two descriptions against each other
so the wrong number cannot come back, and asserts this tool still declares no
schema default, since adding one would override upstream's and silently make
the description wrong again.
Not changed: this tool already reports Open Food Facts' true match count as
'count' (2577 for "almond milk" against a 50-row page), so it does not have
the page-size-as-total defect.
* Fix Round 60 (review): adopt the repo disclosure vocabulary; close a regression
Applied the findings from the simplify review and the tool-validator run.
Crossref:
- `note` -> `truncation_note`, plus a machine-readable `truncated` bool, so
a caller never has to string-match prose to learn it got a page. `note` was
also the key BaseRESTTool.run already uses for `fields.empty_result_note`,
i.e. the same key with two meanings in one class hierarchy.
- the sentence moves into a named module-level `_truncation_fields()`, so it
has one home rather than being an f-string buried in `_process_response`.
- the note no longer advises a page that cannot be fetched: Crossref caps
offset+rows at 10000 (measured: offset=9000 succeeds, offset=9999 returns
HTTP 400), so for a larger result set it now says only the first 10000 are
reachable by paging and points at 'filter'. Confirmed live that the caveat
appears for the 1037336-match query and not for the 746-match one.
- Crossref_search_works.limit gains the minimum/maximum its two siblings
already declare, since the note advertises "max 100".
Regression found, proven and closed: dropping `TypeError` from the offset
parser broke tests/unit/test_scientometrics_depth.py::test_parses_member_list.
That suite drives the tool through a MagicMock response whose `url` attribute
is a truthy Mock rather than a string, and urlparse raises TypeError on it --
turning the whole call into status: error. Both URL parsers now check
isinstance(url, str) explicitly, which is load-bearing rather than defensive
typing, and a new test pins the MagicMock path so it cannot recur.
UniProt taxonomy:
- the disclosure now reads the evidence UniProt publishes instead of
inferring it. The redirected URL carries ?from=<old> and the inactive stub
carries inactiveReason.mergedTo; when either is present the note states the
merge, and when only the ids differ it says UniProt did not state why. The
previous version asserted "UniProt has merged it into N" for any mismatch,
which is a claim it had not checked -- the same hedge ChEBI's redirect
disclosure already makes. Still zero extra requests.
- `query_taxon_id` is normalized, so it is directly comparable to
`taxon_id` without the caller coercing "9606" against 9606.
- `common_name` becomes ["null", "string"], matching the sibling
UniProtTaxonomy_search schema. Pre-existing, but this round promotes 46170
as the flagship example and 1280 has no common name, so the one example the
description advertises did not validate.
- `_as_taxon_int` is annotated Optional[int] rather than Any.
HGNC: the a/an choice becomes a two-entry table beside _RELATION_LABEL rather
than a general English article rule, since the input is those two fixed labels.
OpenFoodFacts: the schema now declares default: 50 rather than only describing
Open Food Facts' unversioned server default, so BaseRESTTool sends it
explicitly and the number is owned here. Verified live the response is
unchanged (page_size 50, count 2577) and an explicit page_size: 10 is still
honoured.
Long comments that merely restated the test docstrings are trimmed to the
mechanism and the decision; the live transcripts now live only in the tests.
All 8 Crossref tools, both UniProt taxonomy tools: built-in examples green.
* Fix Round 61: OMA paging parameters that were declared but not applied
OMA_get_orthologs forwarded `per_page` to /protein/{id}/orthologs/, which
accepts it, returns HTTP 200, and ignores it. Measured against P04637,
per_page of 3, 20 and 100 each returned all 157 orthologs, and the response
carries no Link, X-Total-Count or content-range header to page with. The
schema promised "default: 20, max: 100"; a caller asking for 3 received 157,
and the repo's own test_examples ask for 10.
The limit is now applied after fetching, since the full set is already in
hand, and the response says so: `count`, `total_count` and `truncated`, the
vocabulary round 60 settled on, plus a `truncation_note` naming a request
that actually works. `total_orthologs` is removed rather than redefined --
it meant "rows returned", the two coincided only while nothing was sliced,
and a key that silently changes meaning is worse than one that is absent.
No cap is applied here: with no server-side paging, capping at the
advertised 100 would strand orthologs 101-157 rather than merely defer them,
so the parameter description drops a "max" that was never enforced anyway.
Separately, all three nullable paging parameters crashed on their own
declared value. oma_tools.json types them ["integer", "null"], and each call
site then ran min(value, 100), raising TypeError: '<' not supported between
instances of 'int' and 'NoneType'. That reached the user as "Unexpected
error querying OMA" naming neither the parameter nor the cause. Confirmed
live on OMA_get_orthologs, OMA_resolve_xref and OMA_get_genome_pair_orthologs;
one `_page_size` helper now treats absent and null alike and tolerates the
strings CLI arguments arrive as.
Scope is one endpoint, not the module: /pairs/{g1}/{g2}/ does honour
per_page (measured: 3 -> 3 rows, 20 -> 20, each with a Link header), so
OMA_get_genome_pair_orthologs keeps forwarding it and gains only the null
fix, and OMA_resolve_xref already sliced client-side and disclosed its total.
Verified live after the change: per_page=3 -> 3 rows, default -> 20,
per_page=null -> 20, per_page=157 -> 157 with truncated false; pairs still
3 -> 3 and 20 -> 20; xref still 5 rows with total_matches 4357. 24 new tests
pass, 9 of them fail when the behaviour is reverted; full unit suite 5596
passed, 27 skipped.
* Fix Round 61 (correction): the null-argument fix is hardening, not a user bug
The previous commit claimed the TypeError on a null paging argument "reached
the user as Unexpected error querying OMA". That is wrong and this corrects
the record rather than quietly restating it.
execute_function.py:3115 strips None arguments before dispatch -- "None means
not provided -- simply omit such keys" -- so run_one_function never delivered
one. Verified against the pre-fix code through that path: all three OMA tools
returned success with a null paging argument. The crash is reachable only by
constructing the tool class directly, as this repo's own unit tests do. The
fix is kept, because a tool that accepts its own declared argument on only one
entry point is a trap for the next caller, but it is hardening and is now
labelled as such in both the helper and the test module.
Found while sweeping the rest of the repo for the same shape: 59 min(<arg>, N)
sites fed by arguments.get, 34 of them with no null guard, 14 (tool, param)
pairs where the parameter is also declared nullable. All 7 reachable ones were
live-tested through run_one_function and none crashed, which is what prompted
re-testing the OMA claim by the same route. That sweep is a negative result:
this class of defect is contained by the framework, and no further module
needs the change.
What the previous commit got right is unaffected and was re-confirmed through
run_one_function: on the pre-fix code OMA_get_orthologs returned all 157
orthologs for per_page=3 and for per_page=20, exactly as a user would hit it.
|
||
|
|
cf5566c3d6 |
Fix Round 33: tools that answered without the data they were asked for (#508)
* Fix Round 33: DNA translation ignored the genetic code the caller asked for
DNA_translate_reading_frames declared a genetic_code parameter with no enum
and never read it. Passing genetic_code=2 (NCBI vertebrate mitochondrial)
returned status=success with the standard-code translation, so ATGTGATTAAGATAA
came back as "M*LR*" when code 2 gives "MWL**" -- a spurious stop at codon 2
and a missing stop at codon 4.
That matters because the codes differ precisely at the codons a caller would
be checking: code 2 reads TGA as Trp rather than stop, and AGA as stop rather
than Arg. Someone translating a mitochondrial gene got a confidently wrong
protein with no indication that the requested code had been dropped.
Only the standard code is implemented, so reject anything else at input rather
than substituting a different table silently. The rejection is declared in the
schema (enum) so the constraint is visible before the call, and re-checked in
the handler so callers that construct the tool class directly, bypassing schema
validation, get the same actionable error.
* Fix Round 33: Reactome ID query accepted species/type filters it could never apply
Reactome_query_by_ids advertised species and types as optional filters. Neither
was ever transmitted: for /data/query/ids the POST branch sends the IDs as a
plain-text body with no query string, so both sat in query_params and were
dropped. A caller asking for R-HSA-73817 and R-MMU-73817 with
species="Homo sapiens" got the mouse pathway back alongside the human one, with
nothing to indicate the filter had been discarded.
Forwarding them would not have helped. A controlled probe against
reactome.org/ContentService/data/query/ids returned a byte-identical 32445-byte
payload for every variant -- no filter, species=Homo sapiens, species=9606,
species=Mus musculus, types=Reaction, and species+types combined. The negative
cases are the decisive ones: species=Mus musculus still returned the human
record, and types=Reaction still returned two Pathways. The endpoint performs no
server-side filtering at all, so the parameters are unimplementable by any
routing change.
Reject them at input rather than returning unfiltered results that look
filtered, and point the caller at what does work: each record carries
speciesName and schemaClass for client-side filtering, and
Reactome_list_top_pathways / Reactome_get_events_hierarchy take species as a
genuine path parameter. The guard is keyed on the endpoint template, so those
sibling tools are unaffected.
The properties stay in the config, with descriptions that now lead with the
non-support, so the generated Python wrapper keeps its signature and a caller
passing species gets the actionable error instead of a bare TypeError. The
target-research skill reference listed species as a working argument for this
tool and would have steered an agent straight into the error, so it is corrected
in all three copies.
* Fix Round 33: BindingDB returned no affinities for any target because of the query parameter name
Every BindingDB lookup sent plural query-parameter spellings where BindingDB's
REST spec documents singular ones, and joined multiple identifiers with a
semicolon where the spec uses a comma. The endpoints do not reject the wrong
names: getLigandsByUniprots answers HTTP 200 with an empty affinities list, and
getLigandsByPDBs answers HTTP 500. So the tools reported status=success with
zero records for targets that have tens of thousands.
Controlled probes, identical URLs except the parameter name:
?uniprots=O43613 -> HTTP 200, affinities: []
?uniprot=O43613 -> HTTP 200, 10,460 records
?uniprot=O43613;O43614 -> 0 records
?uniprot=O43613,O43614 -> 3,229 records at cutoff=10
?pdbs=4S0V -> HTTP 500
?pdb=4S0V -> HTTP 200 with data
After the fix, HCRTR1 (O43613) returns 10,460 affinities, HCRTR2 (O43614)
12,846, and EGFR (P00533) 29,169 - all previously empty.
The wrong parameter name also produced a false diagnosis that outlived it. The
module docstring, an inline comment, a user-facing error message and the
broken_apis registry all asserted that BindingDB's REST tier was down or that
the PDB route was "failing for all structures", and sent callers to substitute
databases for an outage that does not exist. Those claims are removed: only the
singular getLigandsByUniprot endpoint genuinely hangs, which is why every
operation still routes through the plural endpoint.
getTargetByCompound additionally namespaces its payload with a bdb. key prefix,
which the shared affinities parser did not recognise, so that endpoint returned
an empty list even when the request succeeded. The prefix is now normalised,
which also preserves bdb.species - hits are frequently non-human orthologs and
that distinction matters when reading an affinity.
The similarity parameter on getTargetByCompound is deliberately left as-is. The
spec names it cutoff, but the endpoint returns the same five records under
either spelling, at every value from 0.4 to 0.99, and with the parameter absent
entirely, so renaming it would change no behaviour while invalidating two
existing tests. That the endpoint ignores the threshold at all is noted for
follow-up rather than worked around here.
Empty results now carry a note distinguishing a genuine no-data answer from a
rejected query, so a future parameter defect cannot masquerade as absent data.
* Fix Round 33: Orange Book reported empty searches as a dead endpoint, and CTIS accepted status filters it ignored
Two tools that turned a caller's reasonable query into a confidently wrong
answer, in opposite directions.
FDA_OrangeBook_search_drug: openFDA answers HTTP 404 to mean "zero results",
not "endpoint missing". With no branch for that, every empty search surfaced as
a raw client error with the upstream URL leaked, so a caller could not tell "no
such product" from "the API is down". Separately, drug_name was aliased only to
the active-ingredient field, so a brand name passed to the most natural-sounding
parameter could never match: searching drug_name="Quviviq" failed with a 404
while brand_name="QUVIVIQ" returned NDA214985 from the same index. drug_name now
covers both name forms, and an empty result is reported as an empty result. The
404-as-empty handling follows the precedent already established in
faers_analytics_tool.py rather than inventing a second convention.
CTIS_search_trials_filtered: the status schema admitted any string, so an
invalid value reached the API unchecked and came back as status=success with
zero trials, echoing the bad value in search_criteria as though it had been
honoured. The trap was sharpened by the tool's own description, which describes
statuses in words ("ongoing/recruiting"), so the most natural guess produced a
confident "there are no such trials". Verified against the same query with a
valid code: status="bogus_nonsense_value" returned 0 records where status=[4]
returns 5. Status values are now validated at input and rejected with the list
of valid codes and their meanings.
Both changes are limited to inputs that previously produced a wrong or
misleading answer; valid queries are unaffected.
* Fix Round 33: Pharos target lookup omitted the diseases and ligands its description promised
Pharos_get_target's description states it "Returns Target Development Level
(Tdark/Tbio/Tchem/Tclin), protein family, diseases, ligands, and druggability
data". The GraphQL selection hard-coded eight scalar fields and never requested
the disease or ligand ones, so FOLH1 came back with name, sym, uniprot, tdl,
fam, novelty, description and publicationCount only. Target-to-disease was not
reachable from any Pharos tool, since the sibling runs the reverse direction.
The data was one field name away: querying diseaseCounts and ligandCounts for
the same target returns 240 ligands, 1 drug, and ~23 disease associations led by
prostate cancer at 1195. The selection now requests them, so the tool returns
what it advertises.
Separately, Pharos_get_ligand_targets reports activity values as pActivity
(-log10 molar), not nanomolar, and said nothing about it. A Ki printed as
9.46 is pKi 9.46 = 0.35 nM, so a reader taking it as nanomolar is wrong by ten
orders of magnitude; the value cross-checks exactly against the suvorexant FDA
label's stated 0.35 nM at OX2R. The sibling Pharos_get_target_ligands already
documents the convention, so its wording is carried across to the description
and the value field, with the conversion spelled out. The numbers are unchanged
-- this part is documentation only.
* Fix Round 33: add offline regression tests for the Orange Book, CTIS and Pharos fixes
Each pins the specific failure the fix removed, with the transport mocked so
they cannot flake on an external outage: that an openFDA 404 is read as an
empty result rather than a dead endpoint and that drug_name reaches both name
forms; that an invalid CTIS status is rejected at input instead of returning a
successful-looking empty trial list; and that Pharos_get_target requests the
disease and ligand fields its description promises.
* Fix Round 34: the framework still blamed the caller for the parameter it injects
Round 26 added a guard so the `operation` key ToolUniverse injects into the
caller's arguments is not reported back as an unrecognized caller
parameter. That guard was computed *after* the `jsonschema.validate` call,
so it never existed in the `except jsonschema.ValidationError` handler --
which is precisely the branch that runs when validation fails and the
blame message is built. The handler fell back to the raw
`filtered_arguments`, re-introducing the exact behaviour the guard exists
to prevent:
ARCHS4_get_gene_expression {"gene_symbol": "ATM"}
before: 'gene' is a required property
(unrecognized parameter(s): 'gene_symbol', 'operation')
after: 'gene' is a required property
(unrecognized parameter(s): 'gene_symbol')
`operation` is named there despite the caller never sending it and having
no way to remove it, which sends people looking for a parameter that is
not theirs. Two personas hit this independently in round 33.
The binding is moved ahead of `jsonschema.validate`, and the two blame
paths in the error handler use it. `jsonschema.validate` itself still
receives the full `filtered_arguments`, so no accept/reject outcome
changes -- only which keys the message names.
Blast radius measured rather than argued, since this is framework code on
the path of all ~2,600 tools: a differential sweep ran
`validate_parameters` over every `test_examples` entry in the repo before
and after -- 4,354 examples, **0 newly rejected, 0 newly accepted**. The
sweep does not exercise the improved message because valid examples do
not reach the failure path; the ARCHS4 reproduction above covers that.
* Fix Round 34: gnomAD structural variants answered in the wrong assembly
gnomad_get_sv_by_gene and gnomad_get_sv_by_region accept a reference_genome
parameter, but their GraphQL query hardcoded structural_variants(dataset:
gnomad_sv_r4). That callset is GRCh38-only, so reference_genome only moved the
gene/region selector: a GRCh37 request came back with a GRCh37 gene span and a
byte-identical GRCh38 SV list in the same payload. For RHD both assemblies
returned the same 162 variants, several of them whole-chromosome-spanning
complex calls at pos 1769047 that are not in the gene under either build.
gnomAD's API keeps the SV callset independent of the gene's reference genome,
and it does publish a GRCh37 callset: gnomad_sv_r2_1 returns 13 correctly placed
SVs for RHD. The dataset now follows the requested assembly (GRCh38 ->
gnomad_sv_r4, unchanged and still the default; GRCh37 -> gnomad_sv_r2_1) via a
generic fields.derived_variables lookup rather than a tool-name special case.
The two callsets use disjoint variant-id formats and neither resolves an id from
the other, so gnomad_get_sv_detail would have rejected every id a GRCh37 caller
now receives. It routes on the id shape as well, and keeps resolving r4 ids
exactly as before.
r2_1 records leave homozygote_count, major_consequence, hemizygote_count,
consequence, chrom2, ac_hom and ac_hemi null, and populate pos2/end2 on
breakends; the return schemas were widened to the shapes actually observed.
GRCh37 callers now get 13 SVs where they previously got 162 belonging to the
other assembly.
* Fix Round 34: BindingDB advertised a similarity threshold it does not apply
BindingDB_get_targets_by_compound documents similarity_cutoff as a 0-1
threshold and echoes the caller's value back as "similarity", so a tightened
cutoff reads as a filtered result set. BindingDB's getTargetByCompound endpoint
ignores it: four chemically diverse compounds, both parameter spellings, values
from 0.4 to 1.0 and omission all return byte-identical results (imatinib 395
rows every time, gefitinib 356, aspirin 30, caffeine 48).
The value is still accepted and forwarded, but the parameter description and a
note on every response now say plainly that it was not applied, so callers
filter the returned affinities themselves instead of trusting the echo.
* Fix Round 34: CPIC recommendations were filtered by the wrong drug identifier
CPIC_get_recommendations filtered the recommendation table with
eq.RxNorm:<rxnormid>, but CPIC keys that table on drugid, and 19 of its 324
drugs are not in the RxNorm namespace. Two opposite failures followed.
Gentamicin's drugid is ATC:D06AX07, so the RxNorm filter matched none of the 33
rows on the MT-RNR1 guideline and the tool answered that CPIC has no
recommendation for it -- for the archetypal aminoglycoside, on the guideline
about aminoglycoside-induced hearing loss. It now returns CPIC's three rows,
including the strong recommendation to avoid aminoglycosides in carriers.
Lumiracoxib and ramosetron have no rxnormid at all, so the filter was skipped
and no drug filter was sent. Asking for lumiracoxib returned all 42 rows of the
NSAID guideline, every one of them belonging to celecoxib, ibuprofen, meloxicam,
piroxicam, flurbiprofen, lornoxicam or tenoxicam, presented as lumiracoxib's own
prescribing guidance. Upstream returns nothing for its identifier, which is what
the tool now reports.
The filter uses the drugid CPIC publishes, the mechanism the working sibling
CPIC_search_gene_drug_pairs already used. Drugs whose drugid is RxNorm-namespaced
are byte-identical to before. The no-rows note now names the identifier the
absence was established against, and a separate branch covers the case where no
identifier resolves rather than asserting absence.
* Fix Round 34: dbSNP labelled an allele count as a sample count
dbSNP's global_mafs encodes each study as allele=frequency/count, where the
count is chromosomes carrying the allele. The frequency tools parsed that token
into a field called sample_count and documented it as 'Sample count'. For GJB2
c.35delG a clinician read 'freq 0.0024, sample_count 12' and concluded the
estimate rested on 12 people; it is 12 carrier chromosomes out of 5008.
Arithmetic settles it on both rsIDs tested: 1000 Genomes has 2504 samples and
5008 alleles, and 0.1505591 x 5008 = 754.0 exactly, the reported token, while
x 2504 gives 377. TOPMED freeze 8 on rs429358 is decisive at 132,345 samples:
0.1553138 x 264,690 = 41,110, the reported token.
sample_count keeps its value for compatibility and is documented as the legacy
misnomer; allele_count carries the same number under the right name. dbSNP does
not publish the denominator in global_mafs, so study size still cannot be
recovered from this response, and both descriptions say so rather than implying
otherwise.
dbsnp_get_variant_by_rsid returned an unlabelled position and discarded the
previous-assembly coordinate. It now labels the assembly and returns both
builds; for rs80338939 those differ by 574 kb.
The three dbSNP return schemas described a nested /beta/refsnp payload the tools
have never returned; they now describe the flat esummary shape actually emitted.
* Fix Round 34: a page of results reported as the total, and stats read from keys that do not exist
ReactomeContent_search computed total_results as the length of one unpaginated
upstream page, so a search matching 455 human pathways reported 10 -- exactly
Reactome's default page size. The schema already documented the field as 'Total
number of results', so this was a miscomputation of an existing field rather
than a change of meaning; it now carries upstream numberOfMatches. The response
also gained count, start, has_more and a truncation note, and the tool exposes
rows/start so the other 445 are reachable at all. Reactome's offset parameter is
spelled 'Start row', with a space: passing 'start' is accepted and silently
re-serves page 0, so the caller-facing name is mapped to the wire name and a
test pins the spelling.
ClinicalTrials_get_database_stats read totalStudiesCount and averageByteSize;
the API publishes totalStudies and averageSizeBytes. Both fields were null on
every call while largest_studies populated from the one correctly-named key, so
the tool looked healthy. They now return 597913 and 17275, matching the live
API. clinicaltrials_tool.py had the same wrong averageByteSize key with the
right totalStudies key, so the two implementations disagreed with each other;
they now agree. The percentiles and range buckets the description promises were
being dropped and are now returned.
A fixture in the schema-shape test asserted these two fields were 'legitimately
null upstream'. They never were -- that was this bug -- so the fixture now
carries the real live shape.
* Fix Round 34: FDA label deduplication destroyed distinct labels and the count did not match the list
The drug-label tools deduplicate on set_id or id or brand|generic. A guard meant
to add set_id/id checks the requested return fields, but for these tools the
openfda names arrive through search_fields instead, so it never fires. When a
label's openfda block is empty -- common for older SPLs -- every record hashed to
the literal key "|" and collapsed into one.
Asking for amifostine warnings returned a single document while meta.total said
4. Upstream has four labels with three distinct warnings texts, and the one
destroyed was the branded Ethyol label, which is the document a clinician
prescribing the radioprotectant actually wants. The dedupe key now falls back to
the record's content hash, so identical labels still collapse and different ones
survive: three records, one genuine duplicate removed.
meta.total is the upstream pre-deduplication hit count and was never reconciled
with the list, so a caller reading total 132 alongside 5 results could only
conclude 100 were returned and 32 remained. It keeps its value and meaning, and
the response now also carries result_count and duplicates_removed, which
reconcile against it.
Records whose openfda names are null carried a note telling the caller to check
exactly those fields to confirm the drug identity -- advice that could not be
followed. spl_product_data_elements, which upstream populates on precisely those
records, is now returned so the check is possible.
Pages are deduplicated independently and can overlap, which the disclosure note
now states rather than leaving a caller to sum counts across pages.
* Fix Round 34: three lists that were windows onto the data but read as the whole of it
PubMed_search_articles caps author lists at five and said nothing about it. A
paper with nine authors and a paper with five were byte-indistinguishable, so a
bibliography built from the search results was silently wrong. The full count is
in the same esummary payload the truncation is applied to, so each article now
carries author_count and authors_truncated: PMID 34202135 reports 9 and true,
PMID 41541570 reports 5 and false.
HPA_get_cancer_prognostics_by_gene returned only cancers where the association
reached significance and called the length prognostic_cancers_count, with no
denominator. For PRKDC that is 5 rows out of 31 cancers assessed. Head and neck
squamous cell carcinoma was assessed at p = 0.286 and simply absent, so a
researcher could not tell a citable negative result from missing data. The
assessed and non-significant counts are now reported, the non-significant rows
are returned in their own array with their p-values, and prognostic_summary is
unchanged.
XMLTool caps every nested list at 25 entries in raw document order. Labetalol
has 1855 interactions and nifedipine sits at index 71, so the interaction that
matters for a pregnant patient on both drugs was unreachable from either side
and the empty list read as no interaction. The cap is shared by every nested
list of every XMLTool and is unchanged; each list now reports its total, shown
count, offset and truncation, and the interactions tool takes optional
nested_contains and nested_offset so a specific pair can be looked up. Omitting
both reproduces the previous output exactly.
A test asserting exact equality on the echoed search_parameters was updated for
the two new inert echo keys, keeping its original three values pinned exactly.
* Fix Round 35: variant coordinates returned in an unnamed reference frame
MyVariant.info answers in hg19/GRCh37 by default and the tools never said so.
rs4244285 came back as chr10:g.96541616G>C while every neighbouring tool --
Ensembl, dbSNP, ClinVar, VariantValidator -- reports GRCh38 94781859 for the
same variant, a 1.76 Mb difference with nothing in the payload to say which
frame was meant. There was no way to ask for the other one either: `assembly`
appeared nowhere in mygene_tool.py or biothings_tools.json, though sixteen
other tool modules already handle it explicitly.
MyVariant.info does support assembly selection -- ?assembly=hg38 returns
94781859, matching Ensembl. All three tools now take an optional `assembly`
(hg19 default) and every response carries `coordinate_assembly`, naming both
the UCSC and GRC spelling because clinicians use both. The hg19 path is
byte-identical: the upstream parameter is omitted entirely rather than sent
explicitly. On the two id-based tools an HGVS variant_id must be expressed in
the assembly being requested -- MyVariant looks ids up verbatim and never lifts
coordinates over -- so the 404 that results from a mismatch now names that as
the likely cause instead of returning a bare request-failed string.
MyVariant also files more than one record under a single rsID, and resolving
rs267606617 (MT-RNR1 m.1555A>G, aminoglycoside ototoxicity) through /variant/
returns the metadata-only chrMT:m.1555A>G record. A tool whose entire purpose
is pathogenicity scores therefore answered "no scores" for a well-established
pathogenic variant while the sibling chrMT:g.1555A>G record carried CADD phred
9.793 and ClinVar Pathogenic assertions. The primary record is unchanged; when
an rsID resolves to a record carrying none of the requested annotation, the
sibling ids, their records and an explanatory note are added alongside it. The
extra request fires only on a path that was about to answer with nothing, and
this is not exotic -- of fifteen common pharmacogenomic rsIDs, seven resolve to
a record that under-reports what /query returns.
* Fix Round 35: a tool-computed category presented as source data, and a key the
source never populates
HPA's expression tools returned `expression_level` -- "Very high", "High",
"Medium" -- immediately beside `expression_value`, `expression_unit` and
`source_field`, where `source_field` literally names an HPA column. Every
neighbouring key was genuine HPA provenance; that one was ToolUniverse's own
banding of the nTPM number, with cut-offs at 50, 10, 1 and 0.1 that HPA does
not publish. Nothing marked the difference, so "Very high" read as an HPA
classification. The thresholds and the level strings are unchanged; each
response now carries `expression_level_basis` stating that the category is
computed by ToolUniverse and giving the exact cut-offs, so a reader can
reproduce the banding or discard it. It is placed once per response rather
than on every row, and omitted entirely when nothing was banded, so a query
that matched no tissue does not acquire phantom cut-offs. No claim is made
about HPA's own classification scheme, which was not verified.
A fifth banding site turned up during the work: HPAGetContextualBiologicalProcessTool
carried its own inline cut-offs with different vocabulary and a different
ladder, feeding both `functional_relevance` and the prose conclusion, which
made it the site where the invented bands travelled furthest. Its thresholds
and wording are preserved verbatim and it is now disclosed the same way. The
`expression_level` in HPA_get_comprehensive_gene_details_by_ensembl_id comes
from HPA's own XML level element and is deliberately left alone.
Separately, EnsemblVar_get_population_frequencies emitted `count` on every
population entry and it was null on every one of them, forever: Ensembl's
variation endpoint publishes no such field. For rs4244285 that is 148 entries,
146 keyed (allele, allele_count, frequency, population) and two keyed without
allele_count -- `count` appears zero times. A null sitting beside a populated
allele_count invites the reading "measured, sample size unknown", which is not
what the absence means. The dead key is removed from the response and the
return schema. The two TOPMed rows that genuinely lack `allele_count` keep
their null there, because that one is real missing data.
* Fix Round 35: a stratifiable subset reported as the total number of reports
FAERS_stratify_by_demographics summed the openFDA count facet and published
the result as `total_reports`. openFDA computes a facet only over records that
populate the counted field, so for ondansetron stratified by age that sum is
25,424 -- the reports where patient.patientagegroup is recorded -- against
121,838 reports actually matching the query. A clinician reading `total_reports`
concludes the drug has around 25,000 reports and understates it nearly
fivefold. Coverage is not a constant to correct for either: the same tool on
ibuprofen with gastrointestinal haemorrhage, stratified by sex, covers 3,543 of
4,008 reports, 88.4% against age's 20.9%.
The percentages are right as they stand -- a share of the stratifiable subset
is the correct denominator for a stratification -- so they are untouched, and
`total_reports` keeps its value for backward compatibility. The true total for
the same query is now fetched and reported as `total_reports_matching_query`,
the subset is repeated under the self-describing `stratified_report_count`, and
`coverage_note` names both figures, the coverage fraction and the field, and
states plainly that `total_reports` is not the drug's report count. If the
extra request fails the total reports null with an explanation rather than
turning a working call into an error.
The total is fetched through the existing _get_faers_count helper rather than a
second query builder, and the query construction both paths share is extracted
into _faers_search_query, so the facet query and the total query cannot drift
apart structurally. The first draft of this fix did duplicate the helper and
lost its 404-as-zero handling in the process, which would have reported a
genuine zero-match query as a failed lookup.
* Fix Round 35: sibling gnomAD tools answered from different callsets without
saying so, and CPIC's allele frequencies were discarded
gnomad_get_variant, gnomad_search_variants and gnomad_get_region default to
gnomad_r3 while gnomad_get_variant_populations and gnomad_get_constraint
default to gnomad_r4, and no response said which callset had answered. gnomAD
v3 is a genomes-only callset, so under the r3 default `exome` is structurally
null for every variant -- indistinguishable from "not observed". For GJB2
c.35delG, the commonest non-syndromic-deafness carrier variant, that null hides
10,423 exome allele observations out of 1,461,600 alleles, and a residual-risk
calculation run off the r3 genome AN of 151,764 instead is wrong by an order of
magnitude.
No default is changed. Every dataset-scoped response now reports the `dataset`
it used, the `reference_genome` that dataset implies, and, where the callset
has no exome or no genome component, a `dataset_note` saying the null means
"not present in this callset" rather than "not observed" and naming the dataset
that does carry the missing half. The assembly mapping was verified against
gnomAD's own reference_genome field across all fourteen dataset ids, which
showed the property holds for all six r3 subsets and that ExAC is the mirror
case, exomes only. Matching is by prefix so future subsets resolve without a
code change. gnomad_get_region sends its own reference_genome, and the value
sent now wins over anything inferred. The dataset-to-assembly table that
already existed privately in gnomad_constraint_tool.py is now shared rather
than duplicated, and gnomad_get_variant's return schema, which pinned exome to
type null as an artefact of the r3 default and would have rejected any r4
response, is widened.
CPIC_get_alleles selected four columns and dropped the per-ancestry allele
frequencies CPIC publishes -- present on 36 of 49 CYP2C19 alleles and 183 of
208 for CYP2D6 -- which is the one thing a pharmacogenomics counselling note
needs and which no other tool here provides. Frequency now passes through with
CPIC's own biogeographic-group labels verbatim, never remapped to gnomAD
population codes, alongside `strength`, `citations` and `inferredfrequency`.
CPIC distinguishes a published 0.0 from an absent value, sometimes within one
allele's frequency map, so absences pass through as null rather than zero.
`activityvalue` and `functionalstatus` are untouched: activityvalue is null
across CYP2C19 but populated on 183 of 208 CYP2D6 alleles, so it is real
gene-dependent data, and the null functionalstatus is already disclosed in the
tool description.
* Fix Round 35: a date range quoted into matching nothing, and a seriousness flag
labelled as patient status
The documented date-range syntax returned nothing. FAERS_count_reactions_by_drug_event
with receivedate "[20141001 TO 20141231]" -- verbatim the tool's own example --
came back as an empty list flagged truncated: false, meaning "no adverse events
were reported that quarter". openFDA returns 464 for that query.
The cause was a fix applied to one of two adjacent branches. The multi-field
branch already tested _is_range and left the range unquoted, carrying a comment
explaining exactly this hazard; the single-field branch three lines below saw a
string containing spaces and quoted it, which makes openFDA match the range as
a literal. receivedate maps to a single field, so every date-bounded query took
the branch the earlier fix had missed. Four clause-building sites across all
four classes in the file had the same asymmetry. Rather than adding the check
four times, the quoting rule and the OR-grouping rule each now live in one
helper used by every site, so the two paths cannot drift apart again -- the
drift was the whole defect. A negative control confirms the new tests fail
against the pre-fix code.
FAERS_count_death_related_by_drug labelled seriousnessdeath 2 as "alive".
openFDA's field reference documents only that the value is 1 if the adverse
event resulted in death and absent otherwise, and publishes no meaning for the
value 2 that its data nonetheless returns; the enumeration that does exist
belongs to the sibling `serious` field. It is a report-level seriousness
criterion, not patient status -- FAERS records outcome in
patient.reaction.reactionoutcome, where 5 is Fatal. The terms are now "death
criterion met" and "death criterion not met", and the note records that the
reading of 2 follows the ICH E2B(R2) convention rather than anything openFDA
publishes. This relabelling is the round's one contract change.
The counts also had no denominator: for acetaminophen they sum to 413,631 of
799,284 matching reports, so 48% of reports carry no flag and are absent rather
than bucketed. A reader computes a 30% case-fatality rate from them. The true
total, the flagged subset and a coverage note are now reported, using the same
key names as the equivalent disclosure in faers_analytics_tool.py, and the note
states that these counts do not support a case-fatality rate. Disclosure is
opt-in per tool, because a report has one seriousnessdeath but many reaction
terms and a coverage fraction over a reaction facet would be meaningless.
* Fix Round 35: the query ClinicalTrials.gov ran was not the query the caller
submitted
ClinicalTrials_search_by_intervention with "donor derived cell therapy"
returned zero studies with status success. ClinicalTrials.gov returns 251 for
that phrase. The tool rewrites a plain multi-word value into a quoted phrase
restricted to named AREA fields, and that rewritten query genuinely matches
nothing, so a transplant clinician searching for tolerance-induction trials is
told there are none. Nothing in the description, the parameter documentation or
the response indicated a rewrite had happened.
The rewrite is deliberate and load-bearing. Quoting exists because Essie treats
an unquoted multi-word value as implicit-OR keywords: "cervical cancer" matches
11,215 studies loose against 2,519 quoted, and the quoted set is the one
actually about cervical cancer. The AREA restriction exists because a bare
value matches the whole study record, which is how a vedolizumab search
surfaced an unrelated oncology trial, and InterventionOtherName was added
because brand names live there. None of that is changed here; the quoting and
the AREA restriction are byte-for-byte identical and all fifty-one guard tests
from those earlier fixes still pass. The failure mode is a silent false
negative, so the answer is disclosure, not removal, and the comments now say so
to stop a future reader deleting the quoting to "fix" this.
Every response now reports `executed_query` and `query_rewrites`, so what was
asked and what ran can never differ silently again. When a rewritten query
returns zero, and only then, a second probe establishes whether a relaxed
wording would have matched and reports `relaxed_match_check` naming the count
and how to retrieve those studies -- both helpers bail out on Essie syntax, so
passing the value parenthesised reaches the 251 directly. The returned studies
are not swapped: a zero stays zero, with an explanation attached. A query that
already works is unaffected and issues no probe, and the probe requests a
single field, cutting its payload from 62 KB to 167 bytes.
All four tools on the phrase-quoting path are covered. Three pre-existing tests
inspected the last mocked request and now inspect the first, since the probe is
a legitimate second call, and one asserted exact dictionary equality on the
response and now asserts its two original facts plus that no probe fires when
nothing was rewritten. No assertion was weakened.
* Fix Round 36: FAERS counts on a multi-word drug name were 8.6x too high
The two openFDA builders that take a `medicinalproducts` list percent-encoded
each drug name themselves, before the finished query went through the single
encode every openFDA query already gets. The name was therefore encoded twice.
Established live against api.fda.gov rather than inferred from the code: the
space in "SODIUM CHLORIDE" became %2520, openFDA decoded that once, and Lucene
received the literal term SODIUM%20CHLORIDE. Its analyzer split that on the
non-word "%" and the field stayed bound to the first token alone, so the query
silently widened from one product to every product whose name merely contains
"SODIUM" -- sodium bicarbonate, sodium valproate, docusate sodium.
medicinalproduct:%22SODIUM%20CHLORIDE%22 -> 74,079 + 16,715 = 90,794
medicinalproduct:SODIUM%2520CHLORIDE -> 605,620 + 173,560 = 779,180
medicinalproduct:SODIUM -> 779,180
The last two being identical is the proof the query collapsed to the first
token. The detail path had the same defect: ("SODIUM CHLORIDE", "ASPIRIN")
totalled 9,043 reports correctly encoded and 84,200 double-encoded.
This is the dangerous direction for a pharmacovigilance count. The result was
not empty and not malformed, it was richer than the truth, so nothing looked
broken -- and a disproportionality ratio built on it is inflated in exactly the
direction that manufactures a safety signal.
Drug names now go through the same `_render_clause` helper as every other
value, which Lucene-quotes a multi-word term and leaves percent-encoding to the
one place that does it. Single-token names are byte-identical to before
(ASPIRIN: 435,388 + 182,287, matching the API called directly).
Affects the six FAERS_count_additive_* tools and
FAERS_search_reports_by_drug_combination.
* Fix Round 36: ClinGen coordinates carried no frame and lost their only identifier
ClinGenAllele_lookup_hgvs returned genomic coordinates in two ways a caller
could not act on safely.
The coordinates are 0-based half-open (interbase) and nothing said so. For
NM_000059.3:c.1234C>G the tool reported start 32332711, while the same record's
own external_records.MyVariantInfo_hg38 is chr13:g.32332712C>G. Reading `start`
as a genomic position is a silent off-by-one, and there was no label anywhere in
the output or the description to warn against it.
Upstream also supplies a `referenceSequence` per allele, which the tool dropped.
That mattered most for the row that carries no assembly at all: upstream it has
no referenceGenome and no chromosome, so after the drop it rendered as
chromosome null, reference_genome null, coordinates 22232/22233 -- numbers in a
frame with nothing left to identify it. referenceSequence (RS002532, NG_012772.3
/ LRG_293) was that row's only identifier.
Both are now surfaced: `reference_sequence` on every genomic and transcript
allele, and an explicit `coordinate_system` label on every row that carries
coordinates. The descriptions and return_schema state the convention and the
start+1 rule.
Purely additive. No existing field is renamed or removed and no existing value
changes -- `start` is still interbase, it is now labelled rather than
renumbered. Verified against reg.genome.network directly.
* Fix Round 36: gnomAD gene/transcript coordinates named no assembly
Round 35 made gnomad_get_variant echo the callset it actually answered from.
The three sibling tools that return coordinates -- gnomad_get_gene,
gnomad_get_transcript, gnomad_search_genes -- were left out because they take
no `dataset` parameter. They still return `chrom`/`start`/`stop`, and still
never said which assembly those numbers were in.
The ambiguity is not academic. The same gene, same tool, same arguments:
gene(gene_symbol:"BRCA2", reference_genome: GRCh38) -> start 32315086
gene(gene_symbol:"BRCA2", reference_genome: GRCh37) -> start 32889611
574 kb apart, and nothing in the response distinguished them. A caller
comparing these against GRCh37 variant calls had no way to notice.
The assembly is now taken from gnomAD itself rather than assumed: schema
introspection confirmed `Gene` and `Transcript` both expose a
`reference_genome` field, so the query asks for it and the tool reports what
the API answers. `GeneSearchResult` has no such field, so gnomad_search_genes
echoes the assembly its request carried -- still the value in play rather than
a constant, and it matters there too (gene_search "BRCA2" returns
ensembl_version 17 on GRCh38 and 10 on GRCh37).
Purely additive: no field renamed or removed, no value or default changed, the
requests otherwise byte-identical apart from selecting one extra field.
One pre-existing test pinned the exact old response shape
(`data == {"gene": {"symbol": "GJB2"}}`) and could only pass while the assembly
stayed hidden. Its real intent -- that dataset-scoped keys are not injected
into queries that have no dataset -- is kept as a dedicated test.
* Fix Round 36: an advertised ClinicalTrials filter that could never work
ClinicalTrials_get_field_values documented `query_cond` as "Optional condition
filter to restrict value counts". The /stats/fieldValues endpoint accepts no
such filter, so every use of the parameter was a hard upstream 400 -- 100% of
the time, since the tool was released. Two personas hit it independently this
round, both while asking the same reasonable question: what age groups do
trials for this condition cover.
Probed live across 14 parameter spellings: only `fields` and `types` return
200. Every query.*, filter.* and postFilter.* form answers
"Invalid prefix in parameter name"; `cond`, `query`, `aggFilters`, `pageSize`
and `countTotal` answer "is unknown parameter". The /stats/field/values alias
behaves identically. The endpoint counts values across the whole registry and
cannot be scoped at all.
The parameter is now rejected at input, before any request is built, with an
error that names the working alternative (search_studies with query_cond, then
tally the per-study fields it does return) and is honest that StdAge is not
among them.
The parameter is deliberately KEPT in the schema rather than removed. This tool
silently drops unrecognized arguments, so deleting `query_cond` would have
turned a loud 400 into a silent registry-wide answer that the caller believes
is condition-scoped -- trading a visible failure for a wrong answer dressed as
success.
Input validation only. The unfiltered call is byte-identical before and after
(ADULT 551190, OLDER_ADULT 453760, CHILD 115847), pinned by a test that asserts
the outgoing params are exactly {"fields": "StdAge"}.
* Fix Round 36: FAERS facets that sum above their own total said nothing about it
openFDA's `count=` facet counts VALUES, not reports. A field nested under
patient.reaction[] or patient.drug[] is recorded once per reaction or per drug,
so one report lands in several buckets and the rows sum ABOVE the number of
matching reports. Measured live for vasopressin, 4,741 matching reports:
patient.reaction.reactionmeddrapt 29,070 = 613%
patient.drug.medicinalproduct 71,874 = 1516%
patient.drug.drugindication 21,895 = 462%
patient.drug.drugadministrationroute 9,273 = 196%
patient.drug.drugcharacterization 8,266 = 174%
patient.reaction.reactionoutcome 5,514 = 116%
occurcountry 3,949 = 83%
patient.patientagegroup 906 = 19%
Only one tool in this file disclosed a denominator. The rest returned the rows
bare, so the obvious next step -- case fatality = 1,477/4,741 = 31% for the
outcome facet -- silently divides a per-reaction numerator by a per-report
denominator. The distortion is invisible in the rows and, worse, runs in BOTH
directions: report-level fields sum below the total instead, because reports
missing the field are dropped from the facet rather than bucketed as unknown.
A caller could not tell which case they were in.
Eleven count tools now report `total_reports_matching_query`,
`stratified_report_count` and a `coverage_note`, reusing the disclosure and the
fail-soft `limit=0` denominator probe that already existed here. Each tool
declares its count field's cardinality, so the note names the actual field, the
actual direction, and whether the rows double-count -- rather than a generic
warning that would be wrong half the time.
The six FAERS_count_additive_* tools are deliberately excluded: their run()
builds its own query and never reaches this code path, so declaring the keys
there would be a claim the tools do not honour. A test pins that they carry
neither.
Purely additive -- every existing row, result_count, limit and truncated value
is byte-identical, pinned by a test that runs the same tool with disclosure
forced off and asserts the added key set is exactly the three disclosure keys.
One pre-existing test asserted the opposite contract (that reaction tools must
NOT disclose a denominator). It could only pass while the hazard was hidden, so
it is overturned with the reason recorded. Three other test files needed a stub
for the denominator probe, which goes through request_with_retry rather than
requests.get and so escaped their existing mocks.
* Fix Round 36: Open Targets reported a row count it had not returned
OpenTargets_get_drug_adverse_events_by_chemblId answered CHEMBL521 with
`count: 55` sitting immediately beside 25 rows, and nothing anywhere in the
response flagged the gap. Either number read alone is misleading: the caller
takes 55 as the size of what they were handed, or takes the 25 rows as the
complete set. The 30 dropped signals for ibuprofen include toxic epidermal
necrolysis, urticaria, systemic lupus erythematosus and hepatic enzyme
increased -- the rows a safety reviewer is there for.
The 25 is Open Targets' own default page size, not anything set here. Probed
against the API directly, bypassing ToolUniverse: page omitted -> 25 rows;
page explicitly null -> 25 rows; page size 60 -> 55 rows. The tool declares
`page` as an object, so the flat-`size` default injection in GraphQLTool never
applies and $page reaches the API as null.
Every `count` + `rows` container now carries `returned` and `truncated`, with a
`metadata.truncation_note` naming the real total, the page size, and the exact
argument to retrieve the rest. The note also says plainly that rows missing
here are absent from THIS RESPONSE only and are not evidence of absence from
Open Targets.
The disclosure lives in OpentargetTool.run, so it covers the whole family. Also
found silently truncating and now disclosed: publications by disease
(25/106,922), by target (25/347,754), by drug (25/41,523), target interactions
(25/8,626), diseases-phenotypes (25/5,638), associated targets (50/6,289),
target-disease evidence (25/153). Containers that were already complete get
`truncated: false` and no note. baselineExpression keeps its own page-index-aware
flag -- the generic pass skips containers that already carry `truncated`, so an
index-blind flag cannot overwrite a correct one.
The page-size hint clamps to 3,000: the API rejects larger
("you used size 8626 but the size must be between 0 and 3000"), so the note
never advertises an argument that would fail.
Purely additive. Rows, their order, count, criticalValue and the page size are
unchanged; the description edits keep the original text verbatim as a prefix.
* Fix Round 36: a label section absent by format read as "this drug has no warnings"
FDA_get_warnings_by_drug_name answered Giapreza with status success,
meta.total 1, and:
warnings: null boxed_warning: null
Read literally, angiotensin II has no warnings. It is a vasopressor whose
defining safety issue is thrombosis, and the label says so -- just under a
different key. Confirmed against openFDA directly: the record carries no
'warnings' and no 'boxed_warning' field at all, and does carry
warnings_and_cautions = "5. WARNINGS AND PRECAUTIONS There is a potential for
venous and arterial thrombotic and thromboembolic events in patients who
receive GIAPREZA. Use concurrent venous thromboembolism (VTE) prophylaxis."
The cause is a label-FORMAT split, not missing data. Modern PLR-format labels
(2006-) file this content under warnings_and_cautions and carry no 'warnings'
section; legacy and OTC labels do the reverse. A tool that reads only one of
the two returns a structurally-null answer for every drug of the other vintage
and reports it as success.
When a requested section is absent, the response now says so explicitly, names
which related sections the matched label does carry, surfaces that content
under its own section key, and points at the sibling tool. The note ends with
the sentence that matters: do not read a null section as "this drug has no
warnings".
Purely additive. `warnings` and `boxed_warning` keep their existing null
values; nothing is renamed or removed. A drug whose 'warnings' section IS
populated is byte-identical to before and gets no note -- verified live on
aspirin as a control.
* Fix Round 36: declare the Open Targets disclosure keys the code already emits
The row-truncation disclosure added earlier this round emits `returned`,
`truncated` and `metadata.truncation_note`, and the tool descriptions were
rewritten to promise them -- but the return_schemas were only partly updated.
`metadata.truncation_note` was declared in no OpenTargets schema at all. This
round introduced that gap; closing it here.
Four of the affected schemas turned out to describe ONLY an error payload
(`get_target_interactions_by_ensemblID` and the three publications tools), with
no success shape whatsoever, so declaring the disclosure keys meant declaring
the container they live in as well.
The disclosure helper is generic, so the audit covered the whole family rather
than the tools named in the original report: 11 tools now declare the keys.
Nothing hard-failed before -- no schema sets additionalProperties: false -- but
a description that promises a key the schema does not declare is precisely the
docs-versus-behaviour drift this harness exists to catch.
No description was edited: every promise checked out against what the code
actually emits. The keys are correctly absent when nothing is truncated, and
`target_disease_evidence` on a disease with no evidence rows emits neither,
because the empty list is pruned before the disclosure pass.
Strictly additive: 592 insertions, 0 deletions, no Python touched, no wrapper
needing a hand-edit (no description changed and every root schema type stayed
"object"). Live responses for all eight tools are byte-identical before and
after under sort_keys serialisation.
Also declared `metadata.datasource_rename_note` on get_evidence_by_datasource,
which was likewise emitted but undeclared.
Left unfixed and recorded: get_drug_indications_by_chemblId,
get_known_drugs_by_drug_chemblId, get_approved_indications_by_drug_chemblId and
get_associated_diseases_by_drug_chemblId emit at `drug.indications` while their
schemas name a different container, and get_associated_targets_by_drug_chemblId
declares a `linkedTargets` container its query never produces. Those are
pre-existing naming mismatches and correcting them means altering existing
properties rather than adding to them.
* Fix Round 37: the CLI build test rewrote a tracked source file
tests/tools/test_tu_cli.py::TestBuild called the real cmd_build, which runs
generate_lazy_registry.main() -- and that wrote straight into the tracked
src/tooluniverse/_lazy_registry_static.py, with no parameter to redirect it.
Step 2 also littered the caller's CWD with .tooluniverse/coding_api. Running
the suite therefore dirtied the working tree, the same class of defect fixed
for generate_tools in #497.
The orchestration test now stubs both generators and asserts what it is
actually about: that both steps ran, that step 2 received the resolved output
directory, that both labels reached stderr and that stdout stayed clean.
Stubbing alone would have left generate_lazy_registry with no coverage at all,
so main() gains an optional output_path whose default is byte-identical to the
previous hardcoded path, and a new test runs the real generator into tmp_path.
Verified: the build tests pass, and _lazy_registry_static.py, tools/ and the
working directory are all untouched afterwards.
* Fix Round 37: entities_ratio sat next to found/total but was neither
Reactome's per-pathway 'ratio' is the pathway's share of the entity universe
for its species -- entities_total / 16251 for human. Emitted as entities_ratio
directly beneath entities_found and entities_total, it read as found/total, the
enrichment coverage a reader of these results actually wants.
The error runs both ways, so it is not a bias a caller could mentally correct:
TP53 Regulates Transcription of DNA Repair reports 0.0052920 against a true
coverage of 0.08140, understating 15.4x, while Generic Transcription Pathway
reports 0.0968556 against 0.00572, overstating 16.9x. Because the divisor is
constant within a species, ranking pathways by entities_ratio ranks them by
size and carries no information about the submitted identifiers at all.
Additive: entities_ratio keeps its exact value as a deprecated alias, and the
two quantities it was confused for now have their own names -- entities_coverage
(found/total) beside the counts, pathway_size_fraction_of_reactome away from
them. p_value and fdr remain the enrichment statistics.
Also completes the item schema for the five tools sharing this formatter;
ReactomeAnalysis_token_result declared no items at all, so paging past the
first page returned rows with none of this documented.
* Fix Round 37: FAERS_compare_drugs judged two signals without showing either
The tool returned a verdict -- 'LORAZEPAM and DIAZEPAM show similar-strength
detected signals' -- with no case counts anywhere in the payload. An ROR of 23.5
built on 35 co-reported cases and one built on thousands are not comparable
evidence, and the verdict sentence flattened that distinction away. Recovering
the counts took a second and third call, and nothing said they were needed.
_compare_drugs already calls _calculate_disproportionality per drug, which
builds the 2x2 table and then dropped it, so this is a pass-through rather than
new arithmetic or an extra request. The table is forwarded verbatim, keeping the
key names identical to the sibling single-drug tool.
Adds comparison_caveat, which names any arm resting on fewer than 25 co-reported
cases. 25 is where an arm's own 95%% interval becomes narrower than the 1.5x band
the verdict uses to separate 'similar-strength' from 'stronger': with b, c and d
in the tens of thousands upward, exp(1.96/sqrt(a)) < 1.5 gives a > 23.4.
Additive: comparison, ROR/PRR/IC and both signal_detection blocks keep their
exact values. Verified the arms match the standalone tool cell for cell --
lorazepam a=6564, diazepam a=4310, ROR 2.394 and 2.31 unchanged.
* Fix Round 37: six FAERS multi-drug count tools never reached the coverage disclosure
FDACountAdditiveReactionsTool.run() returned _build_count_envelope directly, so
the six additive tools disclosed truncation but never the denominator their rows
should be read against. FAERS_count_additive_adverse_reactions for VASOPRESSIN
sums to 18,379 across its 100 rows while only 4,676 reports match the query --
393%%. Nothing in the payload said so, and dividing a row by that sum to get a
share is the reader's obvious next step.
The class had drifted into a near-copy of its parent: its own query building,
its own URL, and a requests.get with no timeout at all. It now overrides only
_build_search_query and delegates run() to the parent, inheriting the 30s
timeout, the 404-as-no-matches path, the truncation envelope and
_add_coverage_disclosure together. Net 89 lines removed.
The drug list is withheld from the dict handed to the parent rather than popped,
because run() passes the same dict to the facet and to the denominator probe;
popping would have left the probe counting reports for the filters alone.
Each config declares count_field_cardinality matching the sibling single-drug
tool for the same field, and the three disclosure keys are now declared in
return_schema -- reversing the earlier pinning test, which asserted these tools
must not claim a disclosure they could not make. They can now.
Also restores hermeticity in test_openfda_drug_name_encoded_once.py: it mocked
requests.get only, while the probe goes through request_with_retry ->
requests.request, so enabling disclosure made eight tests hit api.fda.gov for
real. Verified with a guard on requests.request that the whole unit suite now
makes no outbound call.
* Fix Round 37: a label lookup for a vaccine returned naproxen, reported as success
FDA_get_drug_label for 'yellow fever vaccine' returned Naproxen -- brand, generic,
boxed warning, contraindications, all NSAID -- with status success and metadata
echoing the vaccine name back. FDA_search_drug_labels answered 'typhoid vaccine'
with isoniazid and eculizumab. Nothing in either payload could be used to detect
the substitution.
_query_drug_fields tried each field quoted, then unquoted. openFDA binds the
field prefix to the FIRST token only; the rest become free text over the whole
document. Measured: generic_name:"yellow fever vaccine" is NOT_FOUND, while the
unquoted form returns 87,153 -- 33.3%% of the 261,639-label corpus -- although
every token bound to the field yields at most 15 + 4 + 0 = 19. The extra hits are
'fever' and 'vaccine' matching anywhere in the label text.
The fallback's stated purpose, catching salt forms, was never real: quoted and
unquoted return identical counts for single-token names (tofacitinib 29/29,
mefloquine 3/3), because openFDA analyses these fields, so the quoted form
already resolves TOFACITINIB CITRATE. Gating the fallback on a single token
would also have missed co-trimoxazole -- one whitespace token, two Lucene terms,
which previously returned a sunscreen.
Every term is now bound to the field: exact phrase first, then all tokens AND-ed
inside the field. This only fires after the phrase query returns nothing, so it
cannot dilute precision, and it adds the recall the old form pretended to --
'trimethoprim and sulfamethoxazole' returned SILICEA, a homeopathic remedy, and
now returns SULFAMETHOXAZOLE AND TRIMETHOPRIM.
The indication path built its query from raw user text the same way five lines
away; both now route through one phrase helper.
Contract change, narrowly: a multi-word name with no match returns not-found
with a suggestion instead of success with a wrong drug.
* Fix Round 37: ClinVar region search reported its own page size as a count
n_candidates was len(idlist) from an esearch pinned at retmax=500, so it read
500 for every busy window regardless of how many records matched. Measured
against Entrez directly: the 2 kb window has 942 matching records and the
default 4 Mb window 15,327, so the tool inspected 3.3%% of the candidates while
reporting a confident-looking 500.
The note then advised raising 'margin' to catch larger spanning variants, which
made recall strictly worse -- a wider window matches more records, the set is
still cut at 500 ordered by start position, so the far-upstream CNV the widening
was meant to find is pushed out. Measured 2 -> 1 -> 1 variants as margin grew.
Entrez indexes VLEN, the variant length. A record starting upstream can only
reach the region by spanning more than one base, so the upstream pad now
excludes single-base records. The default window's candidate set falls from
15,327 to 253 in the same single esearch -- no additional requests.
The filter is written as a negation rather than a positive length range because
2 records in that window carry no VLEN at all, and one of them is variation
1703527, the 1.5 Mb copy-number loss this tool exists to find. A positive range
would have dropped exactly the variant class it was meant to rescue.
Recall now rises with margin instead of collapsing: 8, 9, 11 variants, the last
including a 216 kb and a 1.8 Mb 11p11.2 deletion. Against an exhaustive scan of
all 942 records in the 2 kb window the fixed tool returns all 8 overlapping
variants, up from 2.
Reports total_available, truncated and truncation_note, matching the names
already used by cbioportal_tool for the same concept.
* Fix Round 37: the shipped skills still taught entities_ratio as found/total
tool_parameters.md stated it outright -- 'entities_ratio: Found/Total ratio' --
and an agent-facing instruction file outranks a return_schema description in
practice, so the skills were teaching the very misreading the tool fix removes.
The sample response disproved its own prose: entities_found 3, entities_total 47,
entities_ratio 0.00291, where 3/47 is 0.0638 and 47/16251 is 0.00289. The sample
values were consistent only with the size-fraction meaning, and were slightly off
under either reading, so they are recomputed rather than preserved.
Four unique files, each duplicated under skills/, plugin/skills/ and
plugins/tooluniverse/skills/. All twelve corrected together and verified
byte-identical across the three trees by checksum -- four distinct hashes, one
per unique file.
* Fix Round 37: an absent label section was blamed on the caller's spelling
FDA_get_pharmacogenomics_info_by_drug_name for codeine reported meta.total 533
with every returned row null, and a note reading 'No drug named codeine was
found; showing the closest-spelling candidate(s) (CODEINE, CODEINUM) instead'.
The whole openFDA corpus holds 524 labels with a pharmacogenomics section, so a
count of codeine labels having one cannot be 533; the true answer is 0. Same for
abacavir (66), carbamazepine (222) and azathioprine (43).
Stage C of the fallback chain built its query from spl_product_data_elements
without the _exists_ guard that stages A and B both apply, so it answered a
different question -- labels whose product blob mentions the drug -- and that
query's total became meta.total. Traced by capturing the outbound URLs: the
fourth request for abacavir is search=(spl_product_data_elements:"ABACAVIR")
with the guard gone.
The note was wrong twice over: it offered 'ABACAVIR' as the closest-spelling
candidate for 'abacavir', and abacavir plainly has labels -- 23 of them. It sent
readers to fix a spelling that was never wrong.
Codeine and abacavir are the textbook pharmacogenomics drugs, CYP2D6 and
HLA-B*57:01, both carrying FDA boxed warnings driven by genotype. That
information lives in the boxed warning, not in SPL section 12.5, so openFDA is
genuinely empty here -- and the old output read as 'no pharmacogenomic concern'.
All three stages now share one _guarded() helper so a future stage cannot omit
what C omitted. The suggestion states the name matched, names how many labels,
says the SECTION is absent, warns that this is not evidence of no concern, and
names the sections those labels do carry with the tools that return them.
Contract change: these drugs now yield the existing NOT_FOUND envelope instead
of success with null rows. Every row removed was null, and the rows also
violated the tool's own return_schema, which declares pharmacogenomics as an
array with no null. The legitimate misspelling path improves rather than
regresses -- contraindications for 'ibuprofin' went from 2,683 total with 4 of
14 rows populated to 697 with 20 of 20. Controls unchanged: warfarin 12 of 12,
ibuprofen clinical pharmacology 259/20.
* Fix Round 37: declare the suggestion key FDA_get_drug_label now returns
The not-found branch gained a suggestion in
|
||
|
|
be3ea656d1 | Fix self-review intent routing | ||
|
|
6115648636 |
Add gene liability evaluation skill (#404)
* Add gene liability evaluation skill * Keep gene liability skill host-agnostic |
||
|
|
089eb8e630 |
Fix: post-1.4.0 bug-hunt + onboarding-experience gaps (consolidates #391 + #392) (#393)
* Fix Round 140: honor HPO_search_terms max_results and Enrichr empty-libs default
HPO_search_terms: the JAX ontology search endpoint sizes result pages with the
`limit` query parameter, not `max`. The tool sent `max`, which the API silently
ignores, so every search was capped at the API default of 10 regardless of the
requested max_results, and `total_results` reported 10 even when 100 matches
existed. Send `limit`, coerce max_results robustly (handles null / 0 / negative
/ over-max without crashing), truncate defensively, and surface the true total
match count as `total_available`.
enrichr_gene_enrichment_analysis: an explicit empty `libs` list -- which is the
tool's own documented example -- queried zero libraries and returned an empty
enrichment wrapped in status:success. Empty or omitted `libs` now falls back to
the default library set. Also return `data` as a structured object instead of a
double-JSON-encoded string, fix `return_schema` to describe the real shape, and
add request timeouts so a stalled upstream cannot hang indefinitely.
Adds regression tests for both tools.
* Fix onboarding and install-experience gaps found in a 1.4.0 dogfood
Addresses papercuts a non-CS user hits between "pip install" and a first
working tool call.
Fix: the setup guide's first example used the wrong parameter.
`tu run FAERS_count_death_related_by_drug '{"drug_name": ...}'` fails
parameter validation; the tool requires `medicinalproduct`. Corrected in all
three skill trees (skills/, plugin/skills/, plugins/tooluniverse/skills/).
This is the scripted "it works!" moment, so it errored for every user who
followed the guide verbatim.
Feature: `tooluniverse-doctor` and `tu status` now distinguish config-loaded
from runtime-ready. Loading a tool registers its JSON config; it does not
install the tool's dependencies. On a base install both reported
"2599 available / 0 unavailable / All tools loaded successfully" while
ml/visualization/bioinformatics tools failed at call time. New
`tooluniverse.extras` module reports which optional dependency groups are
missing, with per-group install commands. The all-clear message now appears
only when nothing failed to load AND every group is installed. Tool counts
are presented as an upper bound, since a few tools use an optional package
only as an enhancement and degrade gracefully. `tu status --json` exposes a
`missing_extras` field.
Docs: make `uv` an explicit prerequisite for the SDK install path and warn
against system pip. On Homebrew Python 3.13/3.14, `pip install` fails with
PEP 668 `externally-managed-environment` and `python3 -m venv` fails at
`ensurepip`; both reproduce on a current macOS box. Added troubleshooting
entries for both errors plus the "loads but fails when run" case.
Docs: lead the chat-mode setup with the two genuinely low-friction paths
(ask an existing AI agent to read setup.md, or the Claude Code plugin
one-liner) and demote hand-editing JSON to a documented fallback with a
copy-paste-safe snippet and a validation command, since "JSON syntax error"
is a top reported failure.
Docs: note that `[all]` excludes singlecell, smolagents, client, and build;
reconcile the `--refresh` vs plain `uvx` args difference between README and
the setup skill by documenting the tradeoff; and record that PyTorch
Lightning warnings during ADMET calls are normal.
Document why the base install carries `markitdown[all]` rather than
narrowing it: `convert_to_markdown` accepts an arbitrary URI with no format
allowlist, so any dropped converter becomes a silent runtime failure, and
markitdown is imported unconditionally at module scope by
unified_guideline_tools.py.
Adds tests/unit/test_extras.py, which also keeps the extras mapping in sync
with pyproject.toml.
|
||
|
|
95ea136ff9 | Release tooluniverse 1.4.0 (#385) | ||
|
|
25517a665e |
Document tu build's undocumented side effect on the installed package (#361)
Continuing the "declared vs runtime reality" audit, this time on the CLI
surface: actually ran every documented example command from
setup-tooluniverse's CLI reference table (tu status/list/find/grep/info/
run) against the current source (not the stale shared venv) -- all worked
as documented, no discrepancies.
`tu build` was the one real gap. Its own top-level help ("Rebuild the
static tool registry") and its --output flag help ("Directory to write
coding-API wrapper files into") each describe only ONE of the two things
it actually does -- confirmed by reading cmd_build() and
generate_lazy_registry.py directly: every `tu build` call unconditionally
overwrites src/tooluniverse/_lazy_registry_static.py in place inside the
installed package, THEN writes coding-API wrappers to --output. Neither
help surface, nor the setup-tooluniverse skill's CLI table, mentioned the
registry side effect -- a user running `tu build --output ./my_tools`
expecting output scoped to that directory would not expect their installed
package's internal registry file to also get rewritten.
Updated the top-level help, the --output flag help, and the skill doc row
to state both effects. Verified via `tu --help` / `tu build --help` from
this fixed source that the descriptions now read correctly.
Also ran `tu test UniProt_get_entry_by_accession` (another documented
example) -- the call succeeds and returns real data, but fails a
return_schema check. Not fixed here: that's a tool-schema drift, squarely
the territory of the project's existing tool-fix harness (50+ rounds
already), not a "docs describe something nonexistent" bug like the CLI
gap above.
|
||
|
|
e9d8d8164b | Release v1.3.2 | ||
|
|
8228a7cd78 |
Fix plugin/skill sync drift: unreachable skills, stale content, sync gaps [release:patch] (#358)
* Fix plugin/skill sync drift: unreachable skills, stale content, sync gaps [release:patch] Auditing why tooluniverse-self-review was invisible (fixed separately via plugin auto-update) surfaced several related drift bugs in how skills reach the shipped Claude Code / Codex plugins: - 12 real skills (self-review, phewas, mendelian-randomization, fastq-qc, clinical-risk-scoring, product-safety-surveillance, natural-product-dereplication, and 5 protein-variant-mechanism skills) existed but were never added to the router's routing table, so natural- language queries could never dispatch to them. Added routing table entries for all 12. - tooluniverse-claude-code-plugin's "What you get" table was missing 3 of 8 slash commands (verify-references, self-review, setup-keys). - plugin/skills/ (shipped) had gone stale relative to skills/ (canonical) for 4 skills — missing LDSC heritability/genetic-correlation tools and sequence deep-learning variant-effect tools (AlphaGenome, Enformer, Borzoi, ChromBPNet, Evo2) that are already registered and real. Verified every referenced tool actually exists in the registry before syncing. - plugin/sync-skills.sh and scripts/sync-codex-plugin-skills.sh were both missing tooluniverse-cs-setup from their exclude lists — the next time either script ran (its documented, expected maintenance workflow), it would have shipped Claude-Science-only install instructions (conda env, host.skills.* API — meaningless outside that host) into both the Claude Code and Codex plugins. Fixed both exclude lists before that could happen; confirmed via an actual run that neither leaks it now. Re-ran plugin/sync-skills.sh from the fixed canonical source to regenerate both shipped mirrors cleanly. * Fix 5 stale tool-name references found in a full skill-vs-registry sweep Swept all ~150 skills' backtick-quoted tool-name-shaped tokens against the full tool registry (2,822 active tools). After filtering false positives (Enrichr gene-set-library names, env vars, code parameters — all verified by reading context, not just pattern-matched), 5 genuine stale references survived, each CLI-verified against the registry before fixing: - tooluniverse-precision-oncology: ChEMBL_get_drug_mechanisms_of_action_by_chemblId -> ChEMBL_get_drug_mechanisms (also fixed the param name shown) - tooluniverse-variant-interpretation: ClinGen_gene_validity -> ClinGen_get_gene_validity; ClinGen_dosage -> ClinGen_dosage_by_gene (matched on param signature) - tooluniverse-statistical-modeling: FAERS_count_patient_reaction -> FAERS_count_reactions_by_drug_event (matched on param + description) - tooluniverse-rare-disease-diagnosis: STRING_interactions -> STRING_get_interaction_partners - tooluniverse-protein-interactions: SASBDB list claimed "5 tools" but listed SASBDB_get_entry three times under three fictional purposes (metadata / structural models / scattering data) plus a nonexistent SASBDB_download_data. Collapsed to the real 2 tools; SASBDB_get_entry's actual description already covers all three claimed purposes in one call. Neighboring tools in each table were spot-checked and are real -- these were isolated bad entries, not whole-table corruption. Also swept the ~150-skill corpus for tool-name PREFIXES (source/database groupings, 3+ tools) with zero textual mention anywhere: 69 registered integrations (~340+ tools -- euhealthinfo, EBIProteins, HFInference, BMRB, NeuroMorpho, etc.) have no skill referencing them at all. Spot-verified two of the largest (euhealthinfo, EBIProteins) are real, substantial integrations with no alternate-phrasing mention either. Not fixed here -- writing curated guidance for 69 domains is a much larger scope decision, reported for follow-up rather than attempted here. These tools remain reachable via find_tools/grep_tools, just without curated skill guidance. |
||
|
|
701afa3ae0 |
Add tooluniverse-biomedical-fact-lookup skill (#279)
Grounds factual biomedical MCQ answering (gene/drug/variant/disease/pathway
lookups, "according to <database>" questions) in ToolUniverse database tools
instead of model memory. Covers routing (MSigDB C6/C3:MIR:MIRDB/C3:TFT:GTRD,
DisGeNET+UMLS, OMIM, MGI, Ensembl, ClinVar, PubTator3 fallback), MSigDB set-name
conventions, gene-disease differential ("in X but not Y") recipes, and
deterministic computational procedures (ORF counts forward-strand+nested,
restriction digest fragments, genetics segregation/gamete ratios) with an
explicit compute-then-match-the-option discipline.
Companion test_snippets.py exercises every embedded code block against synthetic
fixtures (no dataset-specific values).
|
||
|
|
9b7ff91ddb |
Release tooluniverse 1.3.1: OpenTargets migration follow-through + full-registry drift fixes (#268)
* Fix ThreeDBeacons_get_annotations: treat "no annotations of type" as empty, not error
The 3D Beacons annotations endpoint returns HTTP 404 when a valid protein has
no annotations of the requested type (e.g. P04637 has DOMAIN annotations but
no BINDING annotations). The tool routed that 404 through the shared handler
and reported "No structures found for protein ..." — a misleading error for a
legitimate empty result on an annotations query.
_get_annotations now treats a 404 as a successful empty result (annotation_count
0, annotations [], explanatory note) and only raises for genuine failures, so
both test_examples pass and callers can distinguish "no data of this type" from
a real error. Non-404 HTTP errors still surface as errors.
Surfaced by scripts/test_new_tools.py; unrelated to the OpenTargets 1.3.1 work.
* Fix stale return_schema in 6 tools surfaced during 1.3.1 validation
scripts/test_new_tools.py flagged six tools as schema-invalid because their
return_schema no longer matched the live API response (the tools return data
fine; only the declared contract was stale):
- PharmGKB_get_variant_annotations: 'significance' changed from string to an
object {id, resource, term, termId} -> allow object/string/null.
- OpenFDA_search_drug_shortages: 'therapeutic_category' is now a list -> allow
array/string/null.
- OpenFDA_search_food_adverse_events: 'date_started' can be null -> nullable.
- OMA_get_genome_pair_orthologs: 'oma_group' is '' when a protein has no OMA
group -> allow string.
- ols_search_ontologies: schema only described the error branch (never the
'results' success case) and lacked 'status', so the validator unwrapped to
data=None -> describe {status, results, error}.
- ols_find_similar_terms: schema described pre-v4 fields
(terms/total_items/showing) instead of the current output -> describe
{status, term_iri, source_label, ontology, similar_terms, total, note}.
All six now pass scripts/test_new_tools.py with 0 schema-invalid.
* Fix 8 more tools from the full-registry drift scan
A resumable full scan (2779 tools) surfaced further schema/example drift. All
fixed (tools returned data fine; the declared contract or example was stale):
- ZFIN_get_gene_expression: test_example was wrapped in {description, arguments}
instead of the flat arg form, so validation saw gene_id missing -> flatten.
- DepMap_get_cell_line: cell-line metadata (model_id, model_name, tissue,
cancer_type, msi_status, ploidy) can be null -> make nullable.
- Pharos_get_ligand_targets: target 'fam' can be null -> nullable.
- alphafold_get_annotations: region 'unit' can be null -> nullable.
- DailyMed_get_spl_by_setid: return_schema described only error fields (never
the {status, xml} success case) -> describe the real envelope.
- ols_get_efo_term_descendants: schema didn't account for the {status, data}
wrapper -> describe the real envelope.
- Crossref_search_members: schema branch was a bare array, but the tool returns
{status, data:[...]} -> describe the real envelope.
All pass scripts/test_new_tools.py with 0 schema-invalid.
* Fix 6 more tools from the completed full-registry scan
The full 2779-tool scan (now complete) surfaced a final batch of clean drift:
- FlyBase_get_gene_expression, RxTerms_search_drugs, HealthConditions_search,
DiseaseNames_search: test_examples wrapped args in {arguments: {...}} (the
same anti-pattern as ZFIN), so validation never saw the required param ->
flattened to the standard flat example form.
- DBAASP_get_peptide: DBAASP now returns objects/arrays for several fields
(nTerminus, cTerminus, synthesisType, complexity as {name,...}; smiles as a
list) -> widened those return_schema field types.
- VariantValidator_format_genomic_to_transcripts: the per-variant block's
additionalProperties required object, but the API puts errors: [] there ->
allow array/string/null.
All pass scripts/test_new_tools.py with 0 schema-invalid. (ADMETAI's 8 scan
"failures" were false positives from the parallel scanner hitting thread-unsafe
torch; the tools work single-threaded.)
* Fix miRBase_get_mirna: RNAcentral /rna/{id}/{taxid} now serves HTML
The RNAcentral /api/v1/rna/{URS}/{taxid} path now returns an HTML species view
instead of JSON, so _get_rna failed with "Expecting value: line 1 column 1".
- Fetch the JSON record from /rna/{URS}/?format=json (sequence, length, ...)
and derive the species-specific rna_type/species/description from
/rna/{URS}/xrefs/?format=json&page_size=100 (filtered by taxid), reading them
from each xref's `accession` sub-object.
- The record's `publications` field is a URL, not a count -> only populate
publications_count when the API returns an integer.
- distinct_databases is now a list, not a string -> widen the return_schema in
mirna_tools.json and lncrna_tools.json (same RNAcentral field).
miRBase_get_mirna/_publications/_xrefs pass with 0 schema-invalid. (The separate
miRBase_search_mirna is unaffected: EBI Search rnacentral is returning 500
upstream, which is not a client-side fix.)
* Add OpenAlex API-key support (keys required since 2026-02-13)
OpenAlex discontinued the polite-pool `mailto` mechanism and now returns HTTP
503 "anonymous search temporarily unavailable" without an API key. Add support
for the `api_key` query parameter (free key at openalex.org/settings/api),
read from the OPENALEX_API_KEY environment variable and injected into every
OpenAlex request (both OpenAlexTool and OpenAlexRESTTool paths).
- openalex_tool.py: `_with_api_key()` helper adds api_key to all request params.
- openalex_tools.json: declare optional_api_keys: ["OPENALEX_API_KEY"] on all
10 OpenAlex tools.
- .env.template: document OPENALEX_API_KEY.
With a key configured the tools work again; without one they degrade to the
upstream 503 (unchanged).
* Rewrite CellMarker tool to use the bulk download (JSP API removed)
CellMarker 2.0 restructured its site and removed the JSP search endpoints this
tool scraped (Marker_table.jsp / CONTROL now 404, and the server 403s the
default python-requests User-Agent). The only public interface left is the bulk
marker download.
The tool now downloads Cell_marker_All.xlsx once (primary domain, with the
published IP mirror as fallback), caches it on disk, loads it into a normalized
DataFrame, and answers all four operations locally:
- search_by_gene, search_by_cell_type, list_cell_types, search_cancer_markers
The record schema (species, tissue_class, tissue_type, cell_type, cell_name,
cell_marker, source, supports) is unchanged, so the return_schemas still
validate; `supports` is derived as the number of curated records backing each
marker/cell/tissue assignment. First call downloads ~10 MB (96k markers);
subsequent calls are served from cache.
All 4 CellMarker tools pass scripts/test_new_tools.py with 0 schema-invalid.
* Remove unused _RECORD_COLUMNS constant in cellmarker_tool
* Release tooluniverse 1.3.1
* Register OPENALEX_API_KEY in the api-key catalog
The Sync API key catalog check requires an api_key_info block for every key
referenced in optional_api_keys/required_api_keys. Add the OPENALEX_API_KEY
info block (define-once, on the first OpenAlex tool) and regenerate
api_keys_catalog.json and .env.template.
* Declare openpyxl dependency for .xlsx reading (CellMarker/dataset tools)
* Sync plugin manifests and docs version to 1.3.1
|
||
|
|
e2520a9610 |
Add /tooluniverse:self-review command + tooluniverse-self-review skill (#263)
Port the Qworld Recursive Expansion Tree (RET) criteria-generation method into the ToolUniverse plugin as a self-review capability for both Claude Code and Codex. - New canonical skill skills/tooluniverse-self-review/SKILL.md: given a task/goal/question, derive weighted YES/NO success criteria via the RET (scenarios -> perspectives -> criteria), then review supplied work against them (what is missing / done well), or emit a definition-of-done checklist when no work is supplied. Faithful port; no external tool calls. - New Claude slash command plugin/commands/self-review.md -> /tooluniverse:self-review. - Synced the skill into both plugin packagings (plugin/skills + Codex plugins/tooluniverse/skills, disable-model-invocation stripped for Codex). - Documented self-review (and previously-undocumented setup-keys) in plugin/README.md so every command is listed. Tests: Codex plugin suite 143/143; Claude plugin README command-documentation test now passes. |
||
|
|
373be25d76 |
Add 28 new tool families (67 tools): local-compute analysis, ML-model gateways, and knowledge-base APIs (#260)
* Add canonical VCF variant-statistics tool (bcftools-backed)
Adds VCFStatsTool with three deterministic, local-compute operations over a
user-supplied VCF/BCF:
- VCF_summary_stats: records/SNPs/indels/MNPs, multiallelic sites, ts/tv,
per-sample het/hom/missing
- VCF_count_variants: counts after PASS/QUAL/region/expression filters
- VCF_normalize: split/join multiallelics (+ optional left-align against a
reference FASTA), reporting counts before vs after
All 12 existing VCF-related tools are database lookups (query a variant by
ID/region); none compute statistics from a user's own file, so agents rolled
ad-hoc parsers that disagreed on multiallelic splitting and indel
left-alignment. Routing those questions through bcftools stats/norm makes the
SNP/indel counts reproducible. Registered in default_config.py and
_lazy_registry_static.py; flagged requires_local_input; variant-analysis skill
points at the tools as the structured one-call form of its bcftools recipe.
* Add ROC/AUC analysis tool + compute-tool roadmap
ROC_analysis (ROCAnalysisTool): deterministic, local-compute diagnostic-accuracy
analysis for any binary classifier or continuous biomarker. Takes scores + 0/1
labels (inline arrays or a CSV) and returns AUC with a bootstrap 95% CI, the
Youden-optimal cutoff and its sensitivity/specificity, optional metrics at a
fixed cutoff, and a downsampled ROC curve. scikit-learn under the hood; no API
key; never raises. Generic by construction — conventions (positive label,
cutoff) are parameters, output is the standard sklearn result, no task-specific
assumption baked in. Registered in default_config.py and _lazy_registry_static.py;
diagnostic-test-evaluation skill points at it. 9 unit tests + 2 runnable
test_examples pass.
docs/COMPUTE_TOOL_ROADMAP.md: evidence-based map of the remaining gap. ToolUniverse
covers knowledge/DB access comprehensively (568 APIs, ~2534 tools) but only a
handful of tools compute on the user's own data; ~86 analysis scripts across 33
skills are not callable tools. The roadmap prioritizes promoting the genuinely
general ones to tools and lists net-new gaps. Includes a "generality gate":
promote the method, not the script — engine-wrappers that expose conventions as
parameters qualify; benchmark answer-sweepers (emit-every-variant-to-match-GT)
and convention/format-hardcoded scripts are explicitly excluded.
* Fix ROC tool: drop scikit-learn dependency, use pure NumPy
scikit-learn is not a core ToolUniverse dependency (numpy is), so the
sklearn-backed version returned an error on a default install and failed CI.
Reimplemented AUC as the tie-aware Mann-Whitney rank sum (verified identical to
sklearn.metrics.roc_auc_score across random tie-heavy cases) and the ROC sweep
in pure NumPy. Tool now runs with no optional deps; 9 unit tests + 2
test_examples pass, and it works with sklearn import blocked.
* Roadmap: document the framework's optional-dependency design (lazy-loader graceful skip + required_packages + pyproject extras)
* Roadmap: correct compute-tool inventory (58 across 17 classes, not 5) and drop already-existing candidates
A find_tools/registry audit found the compute layer is far more populated than
the first draft claimed: 58 local-compute tools across 17 classes, not '5 of
2539' (that count only included requires_local_input file tools, missing every
inline-data compute tool). Survival, dose-response/IC50, enzyme kinetics, PK NCA,
population genetics, meta-analysis, DESeq2, sequence utils, and drug synergy all
already exist as tools and were removed from the build list. Added a
search-before-you-build audit table; narrowed Part A to find_tools-confirmed gaps
(edgeR/limma, expression PCA, FASTQ QC, single-cell compute, dN/dS, methylation
density, network proximity, GATK calling). ROC remains a genuine gap (shipped).
* Add Network_proximity tool (Guney/Barabasi 2016, networkx)
Network_proximity (NetworkProximityTool): deterministic closest-distance network
proximity between two node sets (e.g. drug targets vs a disease module) with a
degree-matched random Z-score and empirical p. Confirmed no existing tool via a
find_tools/registry audit before building.
Pure networkx + numpy (both core deps) — no network call, no API key, never
raises. General by construction: the caller supplies the graph (inline `edges`
or a 2-column `edgelist_path`) plus `targets`/`disease_genes`, so it works for
any interactome/metabolic/custom network and does not bake in a database or
species (unlike the skill's STRING-downloading CLI script). Registered in
default_config.py + _lazy_registry_static.py; network-pharmacology skill points
at it. 8 unit tests + 1 runnable test_example pass.
* Generalize Network_proximity: measure family + domain-neutral node sets
Make the tool cover its method family and decouple it from one use case:
- add `measure`: closest (Guney 2016, default), shortest (all-pairs mean), and
separation (Menche 2015 s_AB; <0 = overlapping modules) — instead of only the
closest measure.
- rename params to domain-neutral `set_a`/`set_b`, keeping `targets`/`disease_genes`
as aliases, so the tool works for any two node sets (two pathways, two marker
sets, …), not just drug-target vs disease.
Output keys generalized (value/measure, n_set_a_in_network, missing_set_a). Still
pure networkx+numpy, deterministic, never raises. 12 unit tests + 1 test_example
pass.
* network-pharmacology skill: wire STRING->Network_proximity (verified end-to-end) + full-interactome caveat
Smoke-tested STRING_get_network -> Network_proximity: edge rows' preferredName_A/B
(gene symbols) map straight to [A,B] pairs and line up with symbol-based gene
sets natively. Documented the chain, the new measure/set_a/set_b params, and the
gotcha the test surfaced: a small query-centered subnetwork yields an
uninformative z>0/p≈1 because the degree-matched null collapses — use a large
interactome (high limit or NDEx_get_network) for a meaningful Z.
* Add Sequence_dn_ds tool (Nei-Gojobori dN/dS, pure Python)
Promotes the comparative-genomics skill's validated Nei-Gojobori (1986) dN/dS
implementation to a callable tool (verified bit-identical). Pure Python, general
(any two coding sequences), no existing tool. 10 unit tests pass.
* Add RNAseq_edger_limma_de tool (edgeR / limma-voom DE)
edgeR/limma-voom DE complementing the DESeq2 tool, following the rnaseq-deseq2
skill + DESeq2Tool precedent; R pipeline ported from the skill's validated
wrapper. Recovers all planted DE genes end-to-end. 5 unit tests.
* Add 5 ML-model / API tools (9 wrappers), live-verified
Discovered + built via api-tool-builder agents, each confirmed against a LIVE
API and exercised through test_new_tools.py (22 example runs, 0 failures):
- gnomad_get_constraint (GnomADConstraintTool) — gnomAD GraphQL gene constraint:
pLI, LOEUF, mis_z, syn_z, obs/exp LoF. No key. Adds LOEUF/Z fields the existing
gnomad_get_gene_constraints lacks.
- HFInference_classify_text / _embed_text / _fill_mask (HuggingFaceInferenceTool)
— HuggingFace serverless inference (router.huggingface.co): classification,
embeddings, fill-mask (works for protein LMs e.g. ESM2). Optional HF_TOKEN.
- IUPred3_predict_disorder (IUPred3Tool) — IUPred3 protein disorder/ANCHOR
prediction from a UniProt accession (prediction, not a DB lookup). No key.
- ALOGPS_predict_logp_logs (ALOGPSTool) — ALOGPS 2.1 ASNN model: logP + aqueous
logS from SMILES. No key.
- VEP_predict_pathogenicity_by_rsid/_by_hgvs/_by_region (VEPPathogenicityTool) —
Ensembl VEP with AlphaMissense + SIFT + PolyPhen-2 per transcript. No key;
surfaces AlphaMissense scores no existing tool exposed.
Registered in default_config.py + _lazy_registry_static.py; API-key catalog
regenerated. All return {status,...} envelopes, never raise, 30s timeouts.
* Add 4 more ML-model / API tools (TDC, IBM RXN, Replicate, DTU)
TDC oracles (live), DTU DeepTMHMM/SignalP via biolib (live), IBM RXN + Replicate
(key-gated, built+no-key-path-verified). g:Profiler already existed. All modules
import without optional deps (CI-safe). Central registration + catalog regen.
* Add Cellpose / TDC-datasets / SCREEN-cCRE tools + 5 HuggingFace task wrappers
Third discovery batch: HF extended with 5 task wrappers (live), Cellpose
segmentation (live), TDC dataset retrieval (live), ENCODE SCREEN cCRE (live;
Enformer/Borzoi infeasible). All CI-safe. Central registration.
* Add jPOST / RNAcentral / LIPID MAPS-LMPD / InterPro-memberDB + HF vision wrappers
Fourth discovery batch: 4 novel data-API tools (each pivoted around already-wrapped
APIs) + HF image-classification/object-detection. All live-verified, CI-safe.
* Add Rhea / PanglaoDB / IDR-search / GBIF-taxonomy / UniBind tools
Fifth discovery batch: 5 novel data-API tools (each pivoted around already-wrapped
APIs), owning skills updated. All live-verified, CI-safe.
* Add ZOOMA / RGD-strains / SGD-protein / HuBMAP-biospecimen tools
Sixth discovery batch (EBI/NAR + model-organism + spatial omics): 4 novel tools,
owning skills updated. All live-verified, CI-safe.
* Fix dtu_protein test_examples: flatten + success-only (validation pass)
The DTU agent wrote test_examples as a {description,arguments} wrapper plus an
intentional error case; the harness reads top-level keys as args and counts
error-status returns as failures. Flattened to a single flat success example.
The tool itself was already correct (verified: DeepTMHMM returns real topology).
* Fix correctness issues found in pre-merge adversarial audit
Independent correctness/faithfulness audit of the new tools surfaced real
defects (beyond "test_examples don't error"); fixed:
Real bugs:
- rnacentral_genome: RNAcentral_get_sequence/_get_publications called with only
urs_id silently routed to genome_locations (operation default not injected;
hardcoded fallback). Now each wrapper fixes its op via fields.operation.
- idr_searchengine: IDR_search_images mapped `study` to non-existent keys
(study_title/study) -> always null. Now reads screen_name/project_name (+ adds
dataset_name).
Method fix:
- network_proximity: degree-matched Z-score null was an oversimplified Guney 2016
(exact-degree bins => hubs map only to themselves; with-replacement intra-set
sampling). Rewrote: degree bins merged to >= 100 nodes, sample WITHOUT
replacement, empirical p uses (k+1)/(n+1).
return_schema correctness:
- Normalized 14 tools whose oneOf success-branch described the data array
directly instead of the {status,data} envelope (panglaodb actually failed
harness schema validation; others passed only by luck). Now all are proper
envelope schemas.
Honesty/labels:
- TDC LogP oracle relabeled (it is TDC penalized-logP, not octanol-water logP).
- IUPred3 academic/non-commercial license note; PanglaoDB 2020-freeze note;
screen_ccre staging-endpoint reliability note.
* Trim redundancy from the audit: drop ALOGPS + RNAcentral_get_publications, collapse VEP 3->1
Acting on the value-audit's clearest redundancy findings:
- Removed ALOGPS (2005-era logP/logS model; ToolUniverse already has RDKit,
SwissADME, and ADMET-AI for lipophilicity/solubility). Deleted tool + config +
registration.
- Removed RNAcentral_get_publications (duplicated the existing
RNAcentral_get_xrefs_and_pubs). Kept the genuinely-novel
RNAcentral_get_genome_locations + _get_sequence.
- Collapsed VEP_predict_pathogenicity_{by_rsid,by_hgvs,by_region} into a single
VEP_predict_pathogenicity that auto-routes by whichever input is given
(rsid | hgvs_notation | chrom+pos+alt) — 3 tools -> 1.
Kept gnomad_get_constraint (verified the existing gnomad_get_gene_constraints
does NOT expose LOEUF/mis_z/syn_z/pli, so it adds real value) and jPOST
(jPOST-native metadata not surfaced via ProteomeXchange). Net -4 tools (2756->2752).
* Complete audit trim: remove ALOGPS registration, RNAcentral pubs wrapper, collapse VEP wrappers
Follow-up to
|
||
|
|
7d6a182fda |
Bring remaining peptide-deorphanization work to main: ConoServer tool + router wiring (#258)
* Add cross-biobank PheWAS coverage (BioBank Japan, UKB-TOPMed, TPMI, Genebass) + skill
Discovery round: ToolUniverse covered only FinnGen for phenome-wide association
(PheWAS). This adds the missing cross-ancestry PheWAS resources plus a
replication skill.
New tools (all public, no auth):
- BioBankJapan_phewas_by_variant (pheweb.jp, Japanese, GRCh37)
- UKBTOPMed_phewas_by_variant (pheweb.org/UKB-TOPMed, European, GRCh38)
- TPMI_phewas_by_variant (sinica TPMI, Taiwanese, GRCh38)
- Genebass_gene_burden_phewas (main.genebass.org, UKB exome gene burden)
Implementation notes:
- Shared PheWebPheWASTool for the three PheWeb biobanks (config-selected).
- rsID -> coordinates via Ensembl in each biobank's build; multi-allelic SNPs
try every alt and select the allele actually present (PheWeb returns HTTP 200
with a null body for absent variants, so selection is by presence of phenos).
- Genebass joins phenotype descriptions from /phenotypes (cached).
- TPMI serves a cert that fails OpenSSL 3.x strict verification; a per-host
adapter clears only VERIFY_X509_STRICT while keeping full CA-chain + hostname
verification (untrusted/self-signed certs still rejected; not verify=False).
New skill tooluniverse-phewas orchestrates the four tools + FinnGen for
cross-ancestry replication, with allele-frequency/power interpretation guidance.
Validated live: rs7903146/TCF7L2 -> T2D replicated across all ancestries;
PCSK9 pLoF -> LDL/cholesterol; rs671/ALDH2 -> alcohol disorders in East Asian
biobanks but monomorphic in UKB-TOPMed. 12 unit tests, ruff clean.
Both registration points updated (default_config + _lazy_registry_static).
* Add de novo MSA + phylogeny tools (EMBL-EBI Job Dispatcher)
Discovery gap: ToolUniverse could fetch pre-computed alignments/trees (Rfam,
Ensembl Compara) but had no way to align a user's own sequences or build a tree
from them. The phylogenetics skill explicitly disclaimed MSA generation.
New tools (public, no auth; EMBL-EBI Job Dispatcher submit -> poll -> result):
- EBI_msa_align MSA via Clustal Omega (default) / MUSCLE / MAFFT /
Kalign / T-Coffee; returns aligned FASTA, Clustal,
and guide tree
- EBI_build_phylogenetic_tree neighbour-joining / UPGMA tree (Newick) from an
alignment, optional Kimura correction + gap trim
Notes:
- Per-method param mapping (Clustal Omega uses outfmt+stype; MUSCLE uses format
and auto-detects type; MAFFT/Kalign use format+stype).
- Dynamic result-type selection: only fetch result types the job produced.
- 30s per-request timeout; polls up to ~2.5 min.
Updated the tooluniverse-phylogenetics skill to chain these for de novo
alignment + NJ/UPGMA trees (still defers ML/Bayesian to dedicated tooling).
Validated live: cytochrome C human/horse/yeast -> human+horse closest, yeast
outgroup; globin fragments align with correct guide tree. 11 unit tests, ruff
clean. Both registration points updated.
* Add validated clinical risk calculators (compute, no API/key)
Discovery gap: ToolUniverse had data-retrieval tools across many domains but no
bedside clinical risk scores. These are pure-compute, deterministic, fully
testable (no network, no key) implementations of standard validated formulas,
each returning the score, a risk interpretation, and an auditable component
breakdown.
Calculators (ClinicalCalculatorTool, selected by fields.calculator):
- ClinicalCalc_CHA2DS2_VASc AF stroke risk (Lip 2010)
- ClinicalCalc_HAS_BLED AF bleeding risk (Pisters 2010)
- ClinicalCalc_CURB_65 pneumonia severity (Lim 2003)
- ClinicalCalc_qSOFA bedside sepsis risk (Singer 2016)
- ClinicalCalc_Child_Pugh cirrhosis severity / class A-C (Pugh 1973)
- ClinicalCalc_Wells_DVT DVT pretest probability (Wells 2003)
- ClinicalCalc_Wells_PE PE pretest probability (Wells 2000)
- ClinicalCalc_MELD_Na liver mortality / transplant priority (OPTN 2016)
- ClinicalCalc_eGFR_CKD_EPI eGFR, CKD-EPI 2021 race-free (Inker 2021)
- ClinicalCalc_ASCVD_risk 10-yr ASCVD risk, Pooled Cohort Eqns (Goff 2013)
Validated against published reference cases: ASCVD White-male-55 = 5.4%,
White-female = 2.1%, African-American-male = 6.1% (official ACC/AHA examples);
CHA2DS2-VASc, Child-Pugh class, MELD-Na, CKD-EPI all match. 17 unit tests.
No orchestration skill: each calculator is a standalone deterministic call with
built-in interpretation, so a wrapper skill would add little. Both registration
points updated.
* Harden discovery-batch tools: Child-Pugh input validation, PheWAS per-candidate resilience, skill doc fixes
- ClinicalCalc_Child_Pugh: reject unrecognized ascites/encephalopathy values
instead of silently scoring the best case (1 pt); add 'absent'/'slight'
synonyms and an enum on the JSON schema so bad input is caught at validation.
- PheWebPheWAS: a transient 5xx on one multi-allelic candidate no longer aborts
the whole lookup; only surface an error when every candidate fails to fetch
(distinguishes 'variant absent' from 'all fetches failed').
- Genebass: clarify the phenotype-description join-key docstring.
- tooluniverse-phewas skill: document that FinnGen_get_variant_finemapping needs
an explicit GRCh38 chr:pos:ref:alt string (not an rsID).
- tooluniverse-phylogenetics skill: correct phykit saturation column semantics
and list the actual phykit_batch_analysis 'function' enum values.
- Add unit tests for all new behavior (34 pass).
- Bump plugin to 1.2.7 for the skill content changes.
* Discovery: +10 keyless tools (openFDA safety, FAVOR, OPSIN, NCBI genomes) + product-safety skill
Net-new tools from a 5-frontier discovery survey (all public, no API key, live-verified):
openFDA expansion (5, BaseRESTTool config — reuses existing query machinery):
- OpenFDA_search_drug_shortages (current/resolved U.S. drug shortages, added 2025)
- OpenFDA_search_device_adverse_events (MAUDE), OpenFDA_search_device_recalls
- OpenFDA_search_food_adverse_events (CAERS food/supplement/cosmetic)
- OpenFDA_search_animalvet_adverse_events (veterinary)
FAVOR (1): FAVOR_annotate_variant — comprehensive GRCh38 single-variant functional
annotation (BRAVO/gnomAD/1000G frequencies, GENCODE consequence, CADD/SIFT/PolyPhen/
AlphaMissense/MetaSVM scores, GERP/phyloP conservation, ClinVar, regulatory) in one call.
OPSIN (1): OPSIN_name_to_structure — deterministic IUPAC name -> SMILES/InChI/InChIKey
parser (EBI, MIT); resolves systematic names absent from compound databases.
NCBI Datasets v2 genome module (3, extends existing NCBIDatasetsTool):
- NCBIDatasets_get_genome_assembly, NCBIDatasets_list_genomes_by_taxon,
NCBIDatasets_get_sequence_reports (assembly metadata/discovery/sequence mapping).
Skill (1): tooluniverse-product-safety-surveillance — multi-product post-market safety
(devices/food/supplements/cosmetics/veterinary/shortages), orchestrating the new openFDA
tools. Explicit negative boundary vs the drug-AE signal skills (pharmacovigilance).
- 13 new unit tests (47 pass across changed tools). All tools live-verified via CLI.
- Bump plugin 1.2.7 -> 1.2.8 for the new skill content.
* Discovery (batch 2): +6 keyless tools — Ensembl Tark, MARRVEL, EU CTIS
More clean, public, no-API-key resource gaps from the discovery survey:
- Ensembl Tark (2): Tark_get_mane_transcripts (MANE Select/Plus Clinical ENST<->RefSeq
NM mapping by gene/transcript id) and Tark_get_transcript (archived transcript record
with versions/checksums/releases). Fills the MANE/transcript-equivalence gap.
- MARRVEL (2): MARRVEL_get_gene (aggregated OMIM/HGNC/Ensembl/Entrez/UniProt identity)
and MARRVEL_get_omim_phenotypes (OMIM disease associations + inheritance) for
rare-disease/Mendelian triage.
- EU CTIS (2): CTIS_search_trials and CTIS_get_trial — the EU clinical-trials register
(Clinical Trials Regulation, since 2022), complementing ClinicalTrials.gov.
All 18 test_examples live-verified (real retrievable IDs); 13 new unit tests (60 pass
across changed tools). Tools-only — no plugin/skill content change, no version bump.
* Discovery (batch 3): +1 ClassyFire tool + cross-feed new tools into skills
Tool:
- ClassyFire_classify_by_inchikey — ChemOnt chemical taxonomy (kingdom->superclass->
class->subclass) by InChIKey; fills the chemical-classification gap.
Skill gap fixes (wire the new resource tools into the skills that should use them):
- tooluniverse-clinical-trial-matching: add EU CTIS (CTIS_search_trials / CTIS_get_trial)
for European/EEA trials — previously ClinicalTrials.gov only; description updated to
trigger on EU-trial queries.
- tooluniverse-rare-disease-diagnosis: wire MARRVEL_get_gene + MARRVEL_get_omim_phenotypes
into Phase 3 (gene panel; inheritance-aware filtering) and FAVOR_annotate_variant into
Phase 4 (one-call variant annotation), plus fallback-chain entries.
All cross-fed tool calls live-verified; +3 unit tests (63 pass). Plugin 1.2.8 -> 1.2.9
(skill content changed).
* Discovery (batch 4): +4 keyless tools — NPAtlas, ISRCTN + ISRCTN trial-matching cross-feed
- NPAtlas (2): NPAtlas_search_compounds (microbial natural products by name/InChIKey/
formula/SMILES via basicSearch) and NPAtlas_get_compound (full record by NPAID incl.
source organism + originating reference). Fills the microbial-natural-products gap.
- ISRCTN (2): ISRCTN_search_trials and ISRCTN_get_trial — the ISRCTN registry (WHO-primary,
UK/international), parsing the XML query API into structured records. Third trial source.
- clinical-trial-matching skill: ISRCTN added alongside ClinicalTrials.gov + EU CTIS;
description now triggers on US/EU/UK trial queries; merge-and-dedupe guidance via cross-ref ids.
Fixed during build: ISRCTN pagination param is 'limit' not 'pageSize' (verified live).
All 12 test_examples live-verified (real retrievable IDs); +9 unit tests (38 pass).
Plugin 1.2.9 -> 1.2.10 (skill content changed).
* Discovery (batch 5): +2 new skills orchestrating this session's tools
The tools added this session were referenced by zero skills — these two close that gap:
- tooluniverse-microbial-genome-characterization: genome-assembly discovery/QC/replicon
mapping for any organism via the NCBI Datasets genome tools (list_genomes_by_taxon,
get_genome_assembly, get_sequence_reports + taxonomy). Negative boundary vs
comparative-genomics (gene orthology) and plant-genomics.
- tooluniverse-natural-product-dereplication: NP dereplication + chemotaxonomy via
NPAtlas (producing organism + reference) + ClassyFire (ChemOnt class) + OPSIN +
PubChem. Negative boundary vs chemical-compound-retrieval and metabolomics skills.
Both built with skill-creator; every documented tool call live-verified; evals/evals.json
with 3 verifiable prompts each. Plugin 1.2.10 -> 1.2.11.
* Discovery (batch 6): +4 keyless tools — EPA Envirofacts + USDA PLANTS (new frontiers)
Two entirely new coverage frontiers (TU had nothing for either):
- EPA Envirofacts (2): EPA_search_tri_facilities (Toxics Release Inventory) and
EPA_search_frs_facilities (Facility Registry Service) — US regulated-facility lookup.
- USDA PLANTS (2): USDA_plants_get_profile (taxonomy/habit/native status) and
USDA_plants_get_characteristics (morphology/physiology/growth traits) by PLANTS symbol.
All 12 test_examples live-verified; +7 unit tests (45 pass). Tools-only, no version bump.
* Discovery cross-feeds: wire this session's tools into 9 existing skills
The tools added this session were under-referenced by existing skills. Additive doc edits
(no content removed), every newly-referenced tool call live-verified:
- FAVOR_annotate_variant -> variant-functional-annotation, regulatory-variant-analysis,
variant-to-mechanism (one-call GRCh38 comprehensive annotation fast-pass + fallback).
- Tark_get_mane_transcripts / Tark_get_transcript -> variant-interpretation,
acmg-variant-classification (lightweight MANE/transcript-namespace cross-check).
- OPSIN_name_to_structure -> chemical-compound-retrieval, organic-chemistry
(systematic IUPAC name -> structure; trade names fall back to PubChem).
- NCBIDatasets genome tools -> infectious-disease, comparative-genomics (assembly QC
context; pointers to the new microbial-genome-characterization skill).
Plugin 1.2.11 -> 1.2.12.
* Discovery (batch 8): +3 keyless tools — Allen Cell Types + iDigBio
- Allen Cell Types (1): AllenCellTypes_search_specimens — single-neuron electrophysiology/
morphology specimens (human/mouse) via the Brain-Map RMA API; filter by species/structure.
Distinct from the existing gene-expression AllenBrain tools.
- iDigBio (2): iDigBio_search_records (130M+ biodiversity specimen records by taxon/locality)
and iDigBio_get_record (full Darwin Core record by UUID).
All 9 test_examples live-verified (real UUIDs); +7 unit tests (52 pass). Tools-only, no version bump.
* Discovery (batch 9): +2 Pathoplexus/LAPIS tools — pathogen genomic surveillance
Found by a double-check coverage sweep (a genuine gap missed in the first two survey rounds):
- Pathoplexus_count_sequences: aggregated open pathogen genome-sequence counts by
organism, filterable by country/lineage and groupable by metadata field.
- Pathoplexus_get_mutations: characteristic amino-acid/nucleotide mutations above a
proportion threshold, for mutation-prevalence surveillance.
LAPIS-backed (lapis.pathoplexus.org); organisms west-nile/ebola-zaire/ebola-sudan/cchf/
mpox. Distinct from Nextstrain (phylo builds) and BV-BRC. Caught + handled a LAPIS quirk:
the /aggregated endpoint rejects limit/offset (unordered output).
All 6 test_examples live-verified; +6 unit tests (58 pass). Tools-only, no version bump.
* Discovery (batches 10-12): Open Genes + FooDB + TogoID tools
Three gaps found by coverage-sweep double-checks (each had zero TU coverage):
- Open Genes (2): OpenGenes_get_gene / OpenGenes_search_genes — curated aging/longevity
gene DB (mechanisms, evidence counts per study type). Cross-fed into the
tooluniverse-aging-senescence skill Phase 2.
- FooDB (1): FooDB_get_compound — food chemical-constituent by FooDB id (structure +
HMDB/KEGG/PubChem/ChEBI cross-refs); complements USDA FoodData Central.
- TogoID (2): TogoID_convert / TogoID_list_datasets — universal biological ID conversion
across 117 databases (DBCLS relation graph). Cross-fed into the translate-id command.
All test_examples live-verified; +14 unit tests. Plugin 1.2.12 -> 1.2.14.
* Discovery (batch 13): +1 AlphaFill tool — ligands/cofactors on AlphaFold models
Found by coverage-sweep batch 8 (AlphaFill's transplant data is only mentioned, not
exposed, by existing 3D-Beacons/ChannelsDB tools):
- AlphaFill_get_transplants: for a UniProt, the ligands/cofactors/ions AlphaFill
transplants into its AlphaFold model (by homology to PDB), aggregated by compound
with occurrence count, best local-fit RMSD, and source PDB entries. Reveals likely
cofactor/metal/ligand/drug binding the bare AlphaFold (apo) model doesn't show.
All 3 test_examples live-verified (ABL1->imatinib/STI; CDK2->ATP/MG/staurosporine);
+4 unit tests. Tools-only, no version bump.
* Discovery: cross-feed session tools into 7 more skills (close skill-orphan gap)
8 tools added this session were referenced by zero skills. Wired each into its natural
home (additive, every tool call live-verified):
- Pathoplexus (count/mutations) -> infectious-disease (pathogen genomic surveillance)
- AlphaFill_get_transplants -> protein-structure-prediction (ligands the apo AlphaFold hides)
- AllenCellTypes_search_specimens -> neuroscience
- iDigBio (search/record) -> ecology-biodiversity
- FooDB_get_compound -> metabolomics
- EPA TRI/FRS facilities -> chemical-safety (environmental exposure screening)
- USDA PLANTS (profile/characteristics) -> plant-genomics
Plugin 1.2.14 -> 1.2.15.
* Discovery: +1 skill tooluniverse-clinical-risk-scoring (closes calculator orphan gap)
The 10 ClinicalCalc_* risk-calculator tools were referenced by zero skills. This skill
orchestrates them: maps a clinical scenario to the right score(s), computes, and
interprets against per-score risk-band tables.
- AF -> CHA2DS2-VASc + HAS-BLED (anticoagulation tradeoff)
- cirrhosis -> Child-Pugh + MELD-Na; pneumonia -> CURB-65; sepsis -> qSOFA
- VTE -> Wells DVT/PE; CVD prevention -> ASCVD; kidney -> eGFR (CKD-EPI)
Negative boundary vs polygenic-risk-score / epidemiological-analysis /
diagnostic-test-evaluation. All 10 tool calls live-verified; evals with 3 prompts.
Plugin 1.2.15 -> 1.2.16.
* Fix 2 tools surfaced by test-harness role-play (both HIGH-severity, CLI-confirmed)
Feature-KRAS-001 — annotate_variant_multi_source returned EMPTY from every source
(ClinVar/gnomAD/CIViC) and falsely listed all of them in sources_with_data:
- parsers read the wrong nested paths — gnomAD record is at data.gene.gene_id (not
data.gene_id), CIViC variants at data.gene.variants.nodes (not data.variants).
- queried ClinVar by a bare protein change ('V600E') which matches nothing; now query
by gene/rsid and match the specific change with 1-letter->HGVS-3-letter expansion
(V600E <-> Val600Glu), with honest exact_match flag + gene-level context fallback.
- sources_with_data now reflects actual non-empty results, not 'did not throw'.
Feature-ClinPharm-001 — FDA_get_drug_label_info_by_field_value returned a DIFFERENT
drug (asked imatinib boxed_warning, got sunitinib/regorafenib): return_fields (a
projection) were passed as exists=_exists_ MATCH filters, so requesting a field the
record lacks -> NOT_FOUND -> fallback dropped the exact field:value and matched other
drugs. Fix: exists=None (projection-only); missing fields come back null.
+6 unit regression tests. Tools-only, no version bump.
* Add NCBI Clinical Tables tools (RxTerms, conditions, disease_names)
Closes the one partial gap vs the openai/plugins life-science-research
reference set: ncbi-clinicaltables was only covered for ICD (ICDTool) and
LOINC (LOINCTool). This adds the remaining high-value Clinical Tables
endpoints as keyless tools:
- RxTerms_search_drugs - drug-name autocomplete + strengths/forms + RxCUIs
- HealthConditions_search - problem-list autocomplete + ICD-10-CM/ICD-9 crosswalk
- DiseaseNames_search - disease-name autocomplete + UMLS CUI
New ClinicalTablesTool class + clinical_tables_tools.json, registered in
default_config.py and _lazy_registry_static.py. 6 unit tests (mocked) +
live CLI-verified. Tools-only change, no version bump.
* Close 7 capability sub-gaps vs openai life-science-research reference skills
A capability-depth audit of all 50 skills in openai/plugins
life-science-research found 7 endpoints used by reference skills that
TU's tool for that database did not expose. All live-verified + unit-tested:
- gnomad_get_variant_populations - per-ancestry allele frequencies (was aggregate-only)
- IPD_search_hla_alleles / IPD_get_hla_allele / IPD_search_cells - IPD-IMGT/HLA (no HLA coverage before)
- ProteomeXchange_get_spectrum_by_usi - PROXI spectrum/USI access (was dataset-level only)
- ChEBI_get_ontology_parents - upward ontology traversal (had children only)
- ols_get_efo_term_descendants - transitive EFO subtree (had direct children only)
- PubChem_get_substance_by_SID - depositor substance records (was CID-only)
- EpiGraphDB_get_literature_evidence - SemMedDB literature triples
New IPDIMGTHLATool class (registered both points); all other tools extend
existing classes (no new registration). 56 unit tests, all live-verified.
Tools-only change, no version bump.
* Fix GTEx single-tissue eQTL crash on variant-only query
run() injected gencode_id=None when no gene symbol was supplied, and
_get_single_tissue_eqtls iterated it -> "'NoneType' object is not iterable".
Variant-only eQTL lookups are valid (GTEx filters by variantId alone).
Fix: only inject gencode_id when a symbol is present, and coerce None->[]
in the handler. Surfaced by the openai-reference capability audit.
3 regression tests; live-verified (variant-only now returns real eQTLs).
* Targeted NGS-analysis skill gaps: edgeR/limma routes, fastq-qc skill, scRNA QC
Closes the in-scope subset of the openai ngs-analysis reference plugin
(the parts that fit TU's plan + run-if-available skill model; raw-pipeline
skills needing local binaries are intentionally out of scope):
- rnaseq-deseq2: + edgeR (QL-F/exact) and limma-voom routes with a routing
table, references/edger_limma_voom.md, and r_edger_limma_wrapper.py
(preflights Rscript+Bioconductor; emits install plan if absent, never fabricates).
- NEW tooluniverse-fastq-qc: FastQC/MultiQC/fastp/Cutadapt/seqkit QC with a
module-by-module interpretation table and trim/don't-trim decisioning;
run_fastq_qc.py preflights tools, writes only to --workdir, never overwrites
raw FASTQs, emits install plan if tools missing.
- single-cell: + scRNA-seq QC gating section (mito%, doublets, ambient RNA,
empty droplets, MAD thresholds) + references/scrna_qc.md + run-if-available helper.
All three follow the openai preflight pattern (no vaporware). Tests: rnaseq
100/100, fastq-qc 5/5, single-cell unchanged-baseline. Plugin skills synced;
version 1.2.16 -> 1.2.17.
* Apply PR #248 verification fixes from persona round
Surfaced by post-ship researcher-persona verification:
- HIGH: ols_get_efo_term_descendants/children resolved only EFO: CURIEs, so
MONDO/HP/Orphanet terms (modern EFO disease nodes are MONDO) returned 0
silently with a malformed URL. Map non-EFO prefixes to their native OBO/ORDO
IRI (purl.obolibrary.org/obo/<P>_<n>, orpha.net/ORDO/Orphanet_<n>). MONDO:
0004993 now returns its 773 descendants. Also add a disambiguation note when
total=0 (leaf/obsolete/cross-ontology) so it isn't read as 'no subtypes'.
- MEDIUM: remove vendor provenance text from user-facing rnaseq-deseq2 skill
(script docstring + reference doc).
3 new EFO regression tests (MONDO/Orphanet IRI + zero-note). Plugin skills
synced; 1.2.17 -> 1.2.18.
* Register gnomADGetVariantPopulations in static lazy registry
The new gnomad_get_variant_populations class was missing from
_lazy_registry_static.py, so it would not resolve in frozen/lazy
environments where source files are absent (it only loaded because a
sibling gnomAD tool imports the same module). Add the class->module entry
for consistency with the other gnomAD types.
* Close validation-coverage gaps: test_examples + schemas + Alliance expression fix
From the schema/test-completeness audit (1+2):
- Fix Alliance gene-expression: GET /gene/{id}/expression-summary was retired
(404), breaking FlyBase_get_gene_expression / ZFIN_get_gene_expression. Switch
to POST /api/expression (per-annotation stage+location); both now return data
(dpp 317, shha 350). Updated descriptions/params/return_schema + 3 unit tests.
- Add live-verified test_examples to: alphafold_get_annotations (P00533),
HPA_get_rna_expression_in_specific_tissues, ArXiv_get_pdf_snippets (1706.03762),
SemanticScholar_get_pdf_snippets, expression_anova_per_gene, coding_variant_fraction.
- Add return_schema (oneOf) to HMDB_search / HMDB_get_metabolite / HMDB_get_diseases.
Tools whose upstreams are down/blocked (ZINC bot-wall, T3DB Cloudflare-403,
SynBioHub-401, BindingDB-500, SwissTargetPrediction broken) or are async
job/token fetchers (Foldseek/SwissDock/DynaMut2/ReactomeAnalysis) were left
without examples for cause rather than given placeholder IDs. Tools-only, no bump.
* Revive ZINC tools: migrate zinc15 (bot-wall) -> ZINC22/CartBlanche22
zinc15.docking.org now serves a bot-verification HTML wall on every
endpoint, so all 5 ZINC tools were dead. Migrate to the official successor
cartblanche22.docking.org (no bot-wall):
- ZINC_get_compound / ZINC_get_purchasable -> GET /substance/{id}.json
(record + vendor/price catalogs). Verified: ZINC000000000053 (aspirin),
mwt 180.159, 450 vendors.
- ZINC_search_by_smiles -> async /smiles.json submit + /search/result/{task}
poll. Verified: benzene similarity search returns hits.
- ZINC_search_compounds (name) and ZINC_search_by_properties (MW/LogP range):
ZINC22 is structure/ID-centric with no such endpoints -> clean {status:error}
pointing to search_by_smiles (honest, not fabricated).
Descriptions/return_schemas updated; 9 unit tests; live-verified test_examples
for the 3 working tools. Tools-only, no version bump.
* Add 7 capability-depth tools (drug/pharmacology + pathway/interaction)
Capability-depth audit of core clusters (vs each upstream API's own endpoints)
found 7 keyless, live-verified endpoints TU's tools did not expose:
- GtoPdb_get_ligand_properties (structure + molecularProperties)
- GtoPdb_get_disease_associations (diseaseTargets + diseaseLigands)
- PharmGKB_get_drug_label_annotations (/data/label FDA PGx biomarker labels)
- PharmGKB_get_pathway (/data/pathway curated PK/PD pathways)
- PharmGKB_get_variant_annotations (/data/variantAnnotation literature-level)
- SIGNOR_connect_proteins (getData.php?type=connect causal sub-network)
- PathwayCommons_paths_between (graph?kind=PATHSBETWEEN multi-gene paths)
All reuse existing tool classes (no registration change). 49 unit tests,
all live-verified. Tools-only, no bump.
* ultracode sweep: +31 capability-depth tools across 8 core clusters
Parallel capability-depth audit (8 clusters vs each upstream API's own
endpoints) + build of confirmed keyless gaps. All reuse existing tool
classes (no new registration). 119 unit tests, 27/27 live-smoke verified.
- variant-clinical (3): VariantValidator_format_genomic_to_transcripts,
GeneBe_classify_variants_batch, ClinGenAR_lookup_by_external_id
- expression (6): GTEx single-tissue sQTLs / median-transcript / single-nucleus /
finemapping+independent-eQTL, Harmonizome_get_gene_set_members, SCXA_get_cluster_marker_genes
- protein-structure (4): InterPro_get_residue_annotations, PDBeSIFTS_get_scop_mapping,
ELM_get_interaction_domains, ProteinsPlus_protonate_structure
- literature (5): PubTator3_GetEntityRelations, openalex_search_sources/get_source,
EuropePMC_get_article_datalinks, LitVar_get_variant_details
- sequence-genomics (4): EnsemblPheno_get_by_term, Ensembl_get_transcript_haplotypes,
RNAcentral_get_xrefs_and_pubs, UCSC_list_tracks
- chem-metabolomics (4): KEGG_find_compound, metabolights_get_reference_compound,
LipidMaps_get_compound_by_xref, SwissLipids_get_children
- disease-ontology (2): HPO_get_disease_annotations, MonarchV3_phenotype_profile_compare
- cancer-genomics (3): GDC_get_mutation_frequency_by_project,
cBioPortal_get_copy_number_alterations, Progenetix_get_cnv_frequencies
Deferred: ClinVar_get_submitted_records (needs a new class). Tools-only, no version bump.
* ultracode sweep 2: +33 capability-depth tools across 8 more clusters
Second parallel capability-depth sweep (8 clusters vs each upstream API).
All reuse existing classes (no new registration). 109 unit tests, 23/23
live-smoke verified, code-simplifier clean.
- immunology (4): SAbDab structure summary, TheraSAbDab sequences, IMGT germline
FASTA, IEDB antigen-processing prediction
- microbiology-pathogen (5): Pathoplexus details/FASTA, MGnify samples/downloads,
ENAPortal run FASTQ search
- gene-regulation (4): ReMap peaks-in-region, ChIPAtlas colocalization/target-genes/
experiment-metadata
- cheminformatics-admet (4): PubChem BioAssay concise activity, UniChem connectivity,
PubChem Tox eco/human toxicity
- systems-biology-enzymes (5): ReactomeAnalysis expression/species/found/not-found,
BioModels list-all-model-ids
- structural-biology-deep (5): BMRB sequence/shift search + validation, 3D-Beacons
annotations, PDB-REDO version
- gwas-population (2): PGS Catalog performance metrics, Ensembl LD region
- proteomics (4): PRIDE projects-for-protein, PDC quant matrix, ProteomicsDB peptides,
MassIVE protein identifications
Deferred: VEuPathDB (needs new POST class). Tools-only, no version bump.
* Add 3 new-class capability-depth tools (ClinVar SCV + VEuPathDB POST)
The deferred gaps that required a brand-new @register_tool class (the
reuse-only sweep could not build these):
- ClinVar_get_submitted_records (ClinVarSubmittedRecordsTool): efetch
rettype=vcv -> per-submitter SCV records. BRAF V600E returns 44 submissions
across germline/somatic_clinical_impact/oncogenicity axes (existing ClinVar
tools only return the aggregate classification).
- VEuPathDB_search_genes_by_organism + VEuPathDB_get_gene_record (VEuPathDBTool):
WDK POST API across the EuPathDB family (PlasmoDB/ToxoDB/FungiDB/...); the
shared BaseRESTTool is GET-only. PF3D7_0417200 -> DHFR-TS verified.
Registered both classes in default_config.py + _lazy_registry_static.py.
36 unit tests, all live-verified. Tools-only, no version bump.
* ultracode sweep 3: +48 capability-depth tools across 8 more clusters
Third parallel capability-depth sweep (8 clusters vs each upstream API).
All reuse existing classes (no new registration). 144 unit tests, 26/26
live-smoke verified, code-simplifier clean (usda_plants two-source merge confirmed coherent).
- nutrition-food (4): OpenFoodFacts tag filtering, USDA PLANTS wetland/invasive/wildlife
- environment-toxicology (2): EPA TRI facility chemical releases, AOP-Wiki key event
- neuroscience (6): AllenBrain structure expression, NeuroMorpho literature/persistence,
NeuroVault atlases
- plant-agriculture (3): USDA PLANTS search, Plant Reactome participants/species-tree
- model-organism (15): SGD regulation/sequence/disease, WormBase orthologs/interactions/
disease, RGD QTLs/resolve, Alliance orthologs/interactions/alleles, PomBase orthologs/
interactions/GO, InterMine pathquery
- scientometrics (6): ORCID fundings/peer-reviews, OpenAIRE projects, Crossref members,
ROR affiliation matching
- drug-safety-clinical (6): NCI Thesaurus maps/parents, RxNorm NDC status/properties,
DailyMed SPL media/history
- cell-imaging (6): IDR images/annotations, BioImage Archive files, HuBMAP provenance,
CryoET tiltseries/depositions
Skipped (already-covered or key-gated): USDA search dup, Gramene, NIH RePORTER.
Tools-only, no version bump.
* ultracode sweep 4: +41 capability-depth tools across 8 more clusters
Fourth parallel capability-depth sweep. All reuse existing classes (no new
registration). 131 unit tests + base_rest regression, 21/21 live-smoke,
code-simplifier clean. base_rest_tool.py got a backward-compatible opt-in
fields.parse_csv branch (CSV/TSV downloads -> dict rows); non-parse_csv tools
unchanged (regression-verified, only G2P uses it).
- rna-noncoding (7): ENCORI RBP-targets/ceRNA/RNA-RNA/degradome/RBP-disease/motif-scan,
RNAcentral region ncRNAs
- taxonomy-biodiversity (8): WoRMS classification/vernaculars/distribution/synonyms,
iDigBio summary-facets/media, GBIF name-match/occurrence-stats
- protein-ptm-motifs (2): iPTMnet proteoform PPI, ScanProsite motif->proteins
- variant-prediction-scores (4): MaveDB mapped/clinical/gnomad variants, GenomeNexus dbSNP
- molecular-interactions-deep (1): OmniPath general-protein annotation geneset
- drug-target-deep (12): Pharos target-ligands/ligand-targets/expression, OpenTargets
target expression/pathways/depmap/prioritisation/cancer-hallmarks + variant effect/
consequences/pharmacogenomics + credible-set colocalisation
- metabolic-pathways-deep (5): BiGG download-model, KEGG module/reaction/enzyme,
WikiPathways pathway-metabolites
- clinical-genetics-panels (2): Gene2Phenotype download-panel, HGNC gene-family members
Tools-only, no version bump.
* ultracode sweep 5 (part 1): +20 capability-depth tools across 6 clusters
Fifth sweep — 6 of 8 clusters built (2 hit transient Anthropic API rate-limit,
re-running separately). All reuse existing classes (no registration). 77 unit
tests, live-smoke verified, code-simplifier clean, no shared-file edits.
- clinical-trials-registries (3): CTIS filtered search, ISRCTN fielded search +
enriched get_trial (27 fields vs ~10)
- ontology-mapping-services (2): OLS term xrefs, Bioregistry prefix mappings
- proteins-api-features (5): EBI Proteins RNA-editing, variation by dbSNP/HGVS,
proteins by genomic location, HPP peptides
- chemical-safety-regulatory (3): FDA GSRS substance relationships, RxClass
class-hierarchy + disease-relations
- pharmacovigilance-faers (3): FAERS count indications/drug-characterization/
reporter-qualification (bare-list, matches existing FDADrugAdverseEventTool convention)
- sequence-archives (4): GEO supplementary files, SRA run-file locator,
BioSamples relationships + facets
Tools-only, no version bump.
* ultracode sweep 5 (part 2): +8 tools — functional-enrichment + metabolomics-spectra
Re-run of the 2 clusters that hit a transient Anthropic API rate-limit in part 1.
All reuse existing classes (no registration). 23 unit tests, 7/7 live-smoke,
code-simplifier clean, no shared-file edits.
- functional-enrichment (1): Enrichr gene->genesets reverse lookup (cluster near-saturated)
- metabolomics-spectra (7): MassBank spectral-similarity/get-record/advanced-search,
MetabolomicsWorkbench studies-by-phenotype + gene-protein, GNPS library-record +
NPClassifier-from-SMILES
Tools-only, no version bump.
* ultracode sweep 6 (part 1): +2 epidemiology tools (WHO GHO + CDC aggregate)
The only cluster of sweep 6 that completed before transient Anthropic API
rate-limiting cut off the other 7 (re-running separately):
- WHOGHO_list_dimension_values: enumerate dimension values (countries/regions/
sex/age) with code->title + country->region rollup (BaseRESTTool config-only)
- cdc_data_aggregate: server-side SoQL aggregation ($select/$group/$having/
$query) on Socrata /resource/{id}.json; CDCRESTTool error envelope hardened
(top-level error key). cdc_data_get_dataset unchanged (regression-verified).
Tools-only, no version bump.
* ultracode sweep 6 (part 2): +28 tools across 7 clusters
Re-run of the 7 clusters that hit transient Anthropic API rate-limiting in
part 1 (epidemiology already shipped in
|
||
|
|
e9bd73205c |
Add Codex plugin packaging for ToolUniverse (#249)
* Add Codex plugin packaging * Wire Codex plugin into skill-sync + release pipeline; bring to parity The Codex plugin packaging lived outside the Claude plugin's release flow, so its bundled skills and version drifted (134 skills @ 1.2.4 vs Claude's 136 @ 1.2.18). This puts both packagings on one pipeline: - plugin/sync-skills.sh now rebuilds BOTH plugin/skills/ (Claude) and plugins/tooluniverse/skills/ (Codex) from the canonical skills/ tree; Codex drops Claude-Code-specific skills (tooluniverse-claude-code-plugin). - scripts/release-plugin.sh now also bumps plugins/tooluniverse/.codex-plugin/ plugin.json on release, so Codex's version tracks Claude's. One-time parity: - Codex skills resynced (135 = 136 user-facing minus the Claude-only skill); adds tooluniverse-peptide-target-deorphanization, drops stale entries. - Codex plugin.json 1.2.4 -> 1.2.18 to match the current release. - Restored skills/tooluniverse-custom-tool/references/json-tool.md: PR #194 edited only the Claude plugin copy, leaving the source missing the API-key declaration docs, which any resync would otherwise drop. Codex auto-upgrades owner/repo Git marketplaces on startup, so this reaches Codex users on next launch; Claude users get it via the version bump. * Fix Codex skill sync: delegate to the Codex-specific normalizer The prior pipeline commit rebuilt plugins/tooluniverse/skills/ with the same filter as the Claude copy, which left in the disable-model-invocation marker (rejected by Codex validation; 133 skills) and descriptions over Codex's 1024-char limit (4 skills). Have plugin/sync-skills.sh delegate the Codex copy to scripts/sync-codex-plugin-skills.sh — which strips the marker and compacts long descriptions — instead of duplicating and diverging from that logic. Also add an evals/ exclude to the Codex sync so dev eval scaffolding does not ship, matching the Claude filter. * Run the Codex plugin packaging test in CI tests/test_codex_plugin.py lived at the tests/ top level, outside pytest.ini's testpaths (tests/unit, tests/integration, tests/tools), so CI never collected it — its per-skill assertions (description <= 1024 chars, disable-model-invocation not set) silently never ran, which is why the earlier broken Codex skill regen passed CI. Move it under tests/unit/, fix the repo-root parents[] depth, and mark it unit so the Codex skill set is validated on every run. * Rename Codex marketplace from tooluniverse-local to tooluniverse The catalog name/displayName carried a 'local' suffix left over from local-dir testing, but this is the published GitHub marketplace users add via `codex plugin marketplace add mims-harvard/ToolUniverse`. Drop the misleading 'local' so it lists as 'tooluniverse' / 'ToolUniverse'. * Update Codex plugin README: add install steps, point sync at the shared script - Add an Install section (codex plugin marketplace add mims-harvard/ToolUniverse + codex plugin add tooluniverse) — the README had no end-user install steps. - Point the Development sync at plugin/sync-skills.sh, the unified entry point that rebuilds BOTH the Claude and Codex skill copies; the old instruction ran only the Codex-specific sync, so following it left the Claude copy stale. * Version plugins as <package MAJOR.MINOR>.<plugin revision> Tie both plugin versions to the PyPI package's MAJOR.MINOR while letting them carry an independent PATCH (the plugin revision). release-plugin.sh now derives the next version from pyproject.toml's MAJOR.MINOR and bumps the plugin PATCH (resetting to .0 when the package's MAJOR.MINOR advances), instead of bumping the plugin's own semver freely. This keeps Claude + Codex plugin versions coherent with the package they serve while a skill-only change ships as a PATCH bump — without forcing an empty PyPI package release. * Update RELEASING.md for the package-tracked plugin version scheme Document the <package MAJOR.MINOR>.<plugin revision> scheme, the 4 bumped plugin manifests (Codex included), and that both Claude (version-driven) and Codex (git-driven) plugins auto-update — replacing the stale level-based / 3-manifest / Claude-only instructions. * Add tooluniverse-codex-plugin install skill (Codex-only) A one-step install + troubleshooting guide for the ToolUniverse plugin on OpenAI Codex, mirroring tooluniverse-claude-code-plugin but adapted to Codex's commands (codex plugin marketplace add / plugin add -m), startup auto-upgrade behavior, and MCP-via-uvx model — and noting Codex ships MCP tools + skills only (no slash commands). Symmetric to the Claude install skill: plugin/sync-skills.sh now excludes it from the Claude packaging, while the Codex sync includes it (and drops the Claude-only tooluniverse-claude-code-plugin). Description tuned via the skill-creator trigger optimizer (perfect precision; install-skill recall is inherently low and largely platform-bound). |