11 Commits

Author SHA1 Message Date
github-actions[bot] 2cb460dfe3 Release v1.5.1 2026-09-17 03:24:55 +00:00
Shanghua Gao 95ea136ff9 Release tooluniverse 1.4.0 (#385) 2026-07-24 06:47:31 -04:00
github-actions[bot] e9d8d8164b Release v1.3.2 2026-07-21 05:32:01 +00:00
Shanghua Gao 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
2026-07-01 22:14:38 -04:00
Shanghua Gao 833f065225 Discovery batch: PheWAS + MSA/phylogeny + clinical calculators + 10 keyless resource tools + product-safety skill (#248) 2026-06-12 06:33:00 -07:00
Shanghua Gao ba98160c88 Fix Rounds 010-018: tool/skill robustness, GraphQL drift, ProtVar/Dfam APIs, +3 new tools (#245)
* Fix Round 010: CADD tool used broken bihealth mirror -> silent 'No score'

Found via CLI role-play harness. CADD_get_variant_score (and the position/range
ops) queried CADD_BASE_URL = cadd.bihealth.org, which returns an empty list
(HTTP 200) for GRCh38-v1.7 — so scored variants silently reported 'No CADD
score found'. e.g. BRAF V600E (7:140753336 A>T) -> data:null, while the
canonical cadd.gs.washington.edu returns PHRED 29.8.

The tool's own docstring already cites cadd.gs.washington.edu as the API host;
only the BASE_URL constant pointed at the dead mirror. Switched it. Now BRAF
V600E -> phred_score 29.8 'deleterious (top 1%)'. cadd sweep 5/5; +2 tests.

* Fix Round 011: MGnify search returned empty on biome/size params

MGnify_search_studies and MGnify_list_analyses targeted the api/latest
base, which 301-redirects to api/v2 and drops the query string, and they
sent biome=/size= where the API expects lineage=/page_size=. The net
effect was a silent empty result (data: []) even for well-populated
biomes such as the human gut -- the tool's own documented example
returned nothing.

- Point both tools at the stable api/v1 endpoint
- Map biome -> lineage and size -> page_size (accept lineage alias)
- Add mocked regression tests asserting the parameter translation

* Fix Round 012: OMA_get_hog retired-ID guidance + refresh stale HOG examples

OMA reassigns Hierarchical Orthologous Group IDs between releases. The
tool's test_examples and docs used the retired 'HOG:E0739094' (p53) and
'HOG:E0817124' (insulin) IDs, which now return HTTP 410 -- surfaced as a
bare 'OMA API HTTP error: 410'.

- Refresh examples/docs to the current IDs (HOG:F0782425 p53, HOG:F0798498 insulin)
- Catch 410 in _get_hog and return an actionable message pointing the
  caller at OMA_get_protein's oma_hog_id field
- Add mocked regression tests for both the 410 path and a valid HOG

* Fix Round 012: FourDN actionable message on 4DN expired-cert outage

The 4DN Data Portal (data.4dnucleome.org) has been serving an expired TLS
certificate (server-side lapse). All four FourDN operations leaked the raw
HTTPSConnectionPool/SSLCertVerificationError traceback as their error
string, which reads like a client problem.

- Add _format_request_error() classifying expired/invalid-cert SSL failures
  and returning an honest 'transient server-side 4DN issue' message
- Route all five except blocks through it
- Certificate verification is never disabled
- Mocked tests for the SSL path and the non-SSL passthrough

* Fix Round 013: Alliance_search_genes empty on descriptive name + misleading docs

The Alliance autocomplete matches gene symbols/synonyms, not descriptive
names: 'insulin' returns GO terms and diseases (no gene), while the symbol
'INS' returns the genes. The tool returned an indistinguishable empty list,
and its description/examples falsely claimed 'insulin' returns INS.

- Add a metadata 'note' when no gene matched but other entity types did,
  surfacing the matched categories and pointing at the gene symbol
- Correct the description and param doc to say symbol/synonym, not name
- Refresh test_examples to real symbols ('INS', 'shh')
- Add mocked tests for the note path and a symbol query

* Fix Round 013: RGD/Xenbase empty search, ProtVar null fields, handle_error guard

Three issues surfaced by the registry-wide test_examples sweep:

RGD_search_genes / Xenbase_search_genes returned empty for valid symbols
(e.g. 'Tp53') -- they queried Alliance with the stale 'category=gene' param
(no longer honoured -> 0 results) and read the gene id from 'primaryKey'
(now 'curie'). Fetch unfiltered, keep gene_search_result hits, read curie.

ProtVar_get_function crashed with TypeError on a UniProt payload carrying an
explicit null list (e.g. comment 'text': null) -> 'for t in None'. Treat null
lists as empty and guard a non-dict result.

_classify_exception called tool_instance.handle_error() unconditionally, but
plain (non-BaseTool) tools lack it, so any error from such a tool became a
cryptic 'has no attribute handle_error' that masked the real exception. Guard
with getattr and fall back to a generic ToolError. General fix for all
non-BaseTool tools.

Mocked regression tests for all three.

* Fix Round 013: refresh three stale/over-constrained test_examples

The registry-wide example sweep flagged three tools whose own test_example
returned empty -- the tools work, the examples were wrong:

- DailyMed_search_spls combined four mutually-inconsistent filters
  (drug_name + ndc + rxcui + setid), which the API ANDs -> no match. Keep
  just drug_name.
- MGnify_list_analyses used MGYS00000001, which has zero analyses. Use
  MGYS00002012 (verified to have analyses).
- HPA_generic_search searched 'machine learning optimization' (nonsense for
  a protein atlas). Use the gene 'EGFR'.

All three now return data.

* Fix Round 013: actionable error for ProtVar's removed /mappings endpoint

ProtVar restructured its API and removed the batch /mappings endpoint that
ProtVar_map_variant POSTs to, so every call returned a bare 'HTTP Error 404'.
The /function and /population endpoints still work. On a 404, return an
actionable message pointing the caller at ProtVar_get_function /
ProtVar_get_population instead. Mocked test for the 404 path.

* Fix Round 014: OpenTargets drug query used removed schema fields (HTTP 400)

OpenTargets_get_drug_description_by_chemblId requested 'yearOfFirstApproval'
(removed from the Drug type) and 'maximumClinicalTrialPhase' (renamed to
'maximumClinicalStage'), so every call returned HTTP 400 'Cannot query
field ...'. Update the GraphQL query and return_schema to current fields and
add a real test_example (CHEMBL25). Now returns drug data for aspirin.

* Fix Round 014: four more OpenTargets tools used removed GraphQL fields

A schema-drift sweep (dry-running every OpenTargets query against the live
GraphQL schema) found four more tools returning HTTP 400 on removed/renamed
fields:

- get_drug_blackbox_status_by_chembl_ID: blackBoxWarning -> drugWarnings{warningType,...}
- get_approved_indications_by_drug_chemblId: approvedIndications -> indications{count,rows{...}}
- get_gene_ontology_terms_by_goID: GeneOntologyTerm.name -> label
- get_target_gene_ontology_by_ensemblID: term.name -> label

Update each query + add real test_examples (had none). Parametrized
regression test asserts no removed field remains.

* Fix Round 015: skills referenced ClinVar tools with wrong case (clinvar_* -> ClinVar_*)

18 skills told agents to call 'clinvar_search_variants',
'clinvar_get_variant_details', and 'clinvar_get_clinical_significance', but
SDK tool lookup is case-sensitive and the registered tools are 'ClinVar_*'.
An agent following these skills got 'Tool not found'. Correct the case for
the three tool names only (50 refs across 18 source skills, synced to
plugin/skills); data-field names like 'clinvar_classification' are left
untouched.

* Fix Round 015: correct 20 more case-mismatched tool refs across skills

Skills referenced real tools with the wrong case, which case-sensitive SDK
lookup rejects as 'Tool not found'. Fixed (token -> real tool), e.g.:
- 8x OpenTargets_..._by_..._ensemblId -> ...ensemblID
- gnomAD_* -> gnomad_*, KEGG_get_pathway_genes -> KEGG (and kegg_find_genes),
  reactome_get_pathway -> Reactome_get_pathway, go_search_terms -> GO_search_terms,
  GEO_search_datasets -> geo_search_datasets, ENA_get_entry -> ena_get_entry,
  PDBe_get_entry_summary -> pdbe_get_entry_summary, enrichr_enrich -> Enrichr_enrich,
  monarch_search -> Monarch_search
Only exact multi-segment tool-name tokens changed (param names like the
'ensemblId' argument and field names are untouched); synced to plugin/skills.

* docs: skill audit of tool references not found in the registry (Round 015)

112 backtick-wrapped tool-shaped names appear in skills but match no
registered tool (any case) and aren't in any data config. Documented for
per-skill human review rather than mass-edited, since the list mixes
plausible renames, genuinely-absent tools, and illustrative pseudo-names.

* Fix Round 016: correct 23 renamed/format-variant tool refs in skills

Verified subset of the skill-reference audit: tool names that are unambiguous
format variants or clear renames of a real tool (target existence + semantics
confirmed). 80 refs across 28 source skills, synced to plugin/skills. Examples:
- ChEMBL_search_compounds -> ChEMBL_search_molecules
- DailyMed_get_spl_by_set_id / _sections_by_setid -> DailyMed_get_spl_by_setid
- ClinicalTrials_search -> ClinicalTrials_search_studies
- SemanticScholar_search -> SemanticScholar_search_papers
- UniProt_get_protein_by_accession -> UniProt_get_entry_by_accession
- OpenTargets_get_disease_associated_targets -> _get_associated_targets_by_disease_efoId
- DGIdb_get_interactions -> DGIdb_get_drug_gene_interactions, Orphanet doubled-prefix, etc.
Ambiguous/illustrative/genuinely-absent names left in the audit doc.

* docs: refresh skill tool-reference audit after Round 016 fixes

* Fix Round 016: restore ProtVar_map_variant on the restructured ProtVar API

ProtVar 2.x removed the batch POST /mappings endpoint (404). Reverse-engineered
the current contract from the ProtVar web app: GET /mapping?q=<input>&assembly=
<GRCh38|GRCh37>, returning content.inputs[].derivedGenomicVariants[].genes[].
isoforms[]. Rewrote the tool to call it and parse the new shape (AlphaMissense
amScore, merged popEveScore eve/esm1v, gene-level caddScore), added an 'assembly'
param and a test_example, dropped the now-unused _post_json helper.

Verified live for protein ('P04637 R175H' -> chr17:7675088, TP53 missense, AM
0.9857 PATHOGENIC), dbSNP ('rs1799966' -> BRCA1), and genomic VCF inputs.
Mocked tests updated for the new GET contract.

* Fix Round 016: actionable error for Dfam's broken genome-annotation endpoint

Dfam's /annotations endpoint returns HTTP 405 'Invalid Input - 101 - undefined'
for every well-formed region query (param validation passes; chrom/assembly/
start/end/family/nrph are all accepted), while Dfam's family endpoints still
work. This is a server-side Dfam issue. Detect the 405/'Invalid Input' response
and return an actionable message pointing at Dfam_search_families /
Dfam_get_family and the UCSC RepeatMasker track (UCSC_get_track) for genome TE
annotations, instead of a bare 'HTTP 405'. Mocked tests for the 405 path and
the missing-region validation guard.

* Fix Round 017: resolve 50 more renamed/moved tool refs in skills

Worked through the skill-reference audit instead of leaving it as a TODO.
Verified each target exists and matches semantics (checked tool descriptions),
then fixed 50 references across the skills (synced to plugin/skills), e.g.:
- clinical_trials_search -> ClinicalTrials_search_studies
- Reactome_search_pathway -> ReactomeContent_search
- gnomAD_get_variant_* -> gnomad_get_variant; gnomAD_search_gene_variants -> gnomad_search_variants
- NCBI_Taxonomy_search/get -> NCBIDatasets_suggest_taxonomy/get_taxonomy
- UniProt_get_protein_sequence -> UniProt_get_sequence_by_accession
- DrugBank_get_drug/targets -> drugbank_get_*_by_drug_name_or_id
- OpenFDA_get_drug_recalls/enforcement -> OpenFDA_search_drug_enforcement
- OpenTargets_get_associated_diseases_by_target -> _get_diseases_phenotypes_by_target_ensembl
- ExAC_get_constraint_metrics -> gnomad_get_gene_constraints (ExAC retired)
- emdb_search/get_entry -> EMDB_search_structures/get_structure, +~35 more
Only exact tool-name tokens changed (params/field names untouched).

* docs: recategorize skill-reference audit (94 fixed; remaining are non-tool/absent)

* Add HPO phenotype->genes and phenotype->diseases tools (fills audit gap)

Skills needed to go from an observed HPO phenotype to candidate genes and a
disease differential, but TU only exposed term lookup + hierarchy (the audit
flagged HPO_get_term_genes / HPO_get_term_diseases as absent capabilities).

Add two tools on the JAX network-annotation endpoint
(ontology.jax.org/api/network/annotation/{HP-id}):
- HPO_get_genes_by_phenotype: HP term -> NCBI genes (e.g. HP:0001250 -> RELN, SCN1A)
- HPO_get_diseases_by_phenotype: HP term -> diseases (ORPHA/OMIM) + MONDO id

Both with limit param, oneOf return_schema, real test_examples, never-raise.
Point the previously-broken skill references at the new tools. Mocked tests.

* Add GtoPdb_search_diseases tool (fills audit gap)

The audit flagged GtoPdb_list_diseases / GtoPdb_get_disease as absent. GtoPdb's
/services/diseases endpoint is public and config-driven by the existing
GtoPdbRESTTool, so add a search_diseases tool config (name/query -> diseaseId,
name, synonyms, external DB links) and point the skill references at it.
Verified live: 'epilepsy' returns GtoPdb disease entries.

* docs: refresh skill audit after building HPO/GtoPdb gap-filling tools

* Round 019: add OpenTargets target-info tool, fix Ensembl stable-ID lookup, +renames

- Build OpenTargets_get_target_info_by_ensemblID (GraphQL core target info);
  verified live for ENSG00000141510 (TP53).
- Fix ensembl_lookup_gene: bare stable ID (no species) failed with 'Missing path
  parameter species' (build_url ran before stable-ID routing). Add species default.
- Resolve more audit refs by rename to existing equivalents (Ensembl_get_gene_info,
  FDA_drug_search, FAERS_search_by_drug, OpenTargets_get_target/_associated_targets).
Mocked test for the new tool; skill refs updated and synced.

* docs: refresh skill audit after Round 019 (OpenTargets target-info + renames)

* Round 020: build UniProt features tool + resolve remaining addressable audit refs

- Build UniProt_get_features_by_accession (config-only: extract_path 'features'
  on the UniProtKB entry) -- returns all sequence features (domains, sites, PTMs,
  etc.). Verified live for P04637 (TP53).
- Renames to existing equivalents: ChEMBL_get_bioactivity_by_chemblid ->
  ChEMBL_search_activities (filters by molecule_chembl_id), ChEMBL_get_assays ->
  ChEMBL_search_assays, OpenTargets_diseases ->
  OpenTargets_get_diseases_phenotypes_by_target_ensembl, UniProt_get_protein_features
  -> UniProt_get_features_by_accession.
- Fix PubChem_get_drug_label_info_by_CID skill-logic error (PubChem has no FDA
  labels): rewrite example snippets to the correct CID -> compound synonyms ->
  FDA_get_drug_label(drug_name) chain; point tool-option tables at FDA_get_drug_label.

* Round 020: OpenTargets_pathways -> KEGG_get_gene_pathways (last clean audit rename)

The rare-disease TOOLS_REFERENCE listed OpenTargets_pathways as a gene->pathways
fallback, but no such tool exists; KEGG_get_gene_pathways is the right gene->pathway
lookup. DepMap_get_drug_response stays unresolved: GDSC/DepMap drug-sensitivity is
bulk-download data, not a queryable REST endpoint (all Sanger API sensitivity paths 404).

* docs+test: refresh audit (24 left, all non-actionable) + UniProt features tool test

* docs: remove skill-reference audit (all actionable items resolved)

Every fixable reference was resolved over Rounds 015-020 (105 renames/fixes + 5
new gap-filling tools). The only remaining entries were non-tool tokens (pipeline
function names, Enrichr gene-set library values, dev-SDK references, tutorial
illustratives) and DepMap drug sensitivity (bulk-download data, no REST API) --
none are actionable TODOs, so the tracking doc is no longer needed.

* Round 021: package a GDSC drug-sensitivity script for the no-API DepMap gap

DepMap/GDSC drug response is bulk-download data, not a REST API, so it can't be a
TU tool. Instead of leaving the gap, give the skill a real implementation:

- Add scripts/gdsc_drug_response.py to the precision-oncology skill: downloads the
  public GDSC2 fitted dose-response table once (cached), and queries drug
  sensitivity by drug / cell-line / target gene (LN_IC50, AUC, Z_SCORE, TCGA type).
  Verified: Trametinib in SKCM -> sensitive melanoma lines; target BRAF -> Dabrafenib.
- Replace the non-existent DepMap_get_drug_response references in 3 skills with a
  subprocess wrapper + tables pointing at the script, and document why (no API).

This is the 'skill provides a computational procedure when no tool can' pattern.

* Round 022: add IEDB_predict_bcell_epitopes (B-cell epitope prediction)

Skill audit (vaccine-design): TU predicted MHC-I/II epitopes but had no B-cell
(antibody) epitope prediction -- only search of known epitopes. The IEDB B-cell
API (BepiPred/Emini/etc.) is public and keyless. Add a predict_bcell endpoint to
IEDBPredictionTool + IEDB_predict_bcell_epitopes tool (collapses per-residue
assignment into contiguous epitope regions). Verified live; mocked test; wired
into the vaccine-design skill. (Audit's MHC-I/II 'gaps' were false positives.)

* Round 022: fix metabolomics-analysis code examples calling nonexistent tools

code_examples.md called hmdb_search_by_mass, kegg_find_compound, and
kegg_enrich_pathway -- none exist. Replace with the real tools:
- mass annotation -> MetabolomicsWorkbench_search_by_mz (mz_value/adduct/tolerance)
- metabolite-set pathway enrichment -> MetaboAnalyst_pathway_enrichment (takes
  metabolite names directly; drops the broken 2-step KEGG-ID lookup).

* Round 023: DepMap Chronos gene-dependency script (no-API bulk-data gap)

cell-line-profiling and functional-genomics-screens both need per-cell-line
CRISPR (Chronos) dependency scores, which DepMap_get_gene_dependencies can't
return (metadata only) -- the data is a bulk CSV, not a REST API.

Add scripts/depmap_gene_dependency.py: resolves the freshest DepMap Public
download URLs via the public index (signed URLs expire), caches
CRISPRGeneEffect.csv + Model.csv, and queries dependency by gene (most-dependent
cell lines, optional lineage filter) or by cell-line (most-essential genes).
Verified: KRAS -> pancreatic lines (ASPC1 -4.46); A375 -> ribosomal essentials.
Wired into both skills (functional-genomics points at the cell-line-profiling script).

* Round 023: gwas-study-explorer meta-analysis -- real pooling, no more fabricated I2

The skill advertised inverse-variance meta-analysis but the code FABRICATED I2
(variance of -log10(p) * 10, capped at 100), set combined_beta/se=None always,
and used min(p) as the 'combined p' -- presenting a non-statistical guess as a
real heterogeneity statistic.

Fix: parse per-study effect sizes (beta, or log(OR), + SE from the 95% CI 'range')
from GWAS Catalog associations. When >=2 studies have usable effect sizes, do a
REAL inverse-variance fixed-effect pooling + Cochran's-Q I2 (+ heterogeneity p via
scipy when available). When they don't (common), return method='descriptive',
i2=None, combined_beta=None, and combined_p = smallest reported p clearly labeled
as NOT a pooled p. Dataclass + interpretation updated to never invent an I2.
Verified effect-size parsing (beta/OR/CI incl. negative ranges) and both paths.

* Round 023: bundle full Hart CEGv2/NEGv1 reference gene sets for CRISPR screens

crispr-screen-analysis used a 5-gene/3-gene placeholder stub for BAGEL Bayes-Factor
scoring and for the screen-QC check ('are known essential genes recovered?') -- too
small for either. Bundle the published reference sets (CEGv2 core-essential ~684,
NEGv1 non-essential ~928) from BAGEL + a loader (reference_gene_sets.py with
core_essential()/nonessential()/recovery_rate()). Wire into the BAGEL function and
the QC step.

* Round 024: dN/dS (Ka/Ks) script for comparative-genomics selection analysis

The skill makes dN/dS its central tool to distinguish positive (>1) vs purifying
(<<1) vs relaxed (~1) selection, but TU has no tool that computes it. Add
scripts/dnds.py: a dependency-free Nei-Gojobori (1986) estimator with Jukes-Cantor
correction (counts syn/non-syn sites, averages over mutational pathways for
multi-diff codons). dN validated to match Biopython's NG86 exactly (0.0698);
returns null dN/dS honestly when dS is 0/uncorrectable. Documented the
homology -> CDS -> align -> dnds.py workflow in SKILL.md.

* Round 024: network proximity Z-score script (Guney/Barabasi) for network-pharmacology

The Network Pharmacology Score's largest component (35 pts) is the target-disease
network proximity Z-score, which the skill could only describe as prose pseudocode
(no tool: it needs the full interactome + a degree-matched random null). Add
scripts/network_proximity.py: downloads STRING v12 high-confidence human PPI once
(cached), builds a networkx graph, computes closest-distance d_c and the
degree-matched-null Z-score + empirical p. Verified: EGFR/ERBB2/MET vs a
lung-cancer module -> Z=-2.07 (proximal). Replaced the pseudocode with the script;
kept the count-based proxy clearly labeled as NOT the Z-score.

* Round 024: antibody developability script (AGGRESCAN/pI) + honest non-computable notes

The antibody developability phase called predict_tango_score / predict_aggrescan /
predict_binding_energy_change / predict_thermal_stability / predict_expression --
all undefined. Replace with what's legitimately sequence-computable, plus honest
notes for what isn't:
- scripts/developability.py: AGGRESCAN aggregation-prone regions (real Conchillo-Sole
  a3v propensity scale + windowing/hot-spots), isoelectric point (validated: polyK
  11.75, polyE 3.03), Kyte-Doolittle hydrophobic patches.
- Binding ddG -> FoldX/Rosetta on a modeled complex; Tm/titer -> ML predictors:
  documented as external, NOT fabricated.

* Round 025: add ENCORI_get_miRNA_targets tool (miRNA-target lookup gap)

noncoding-rna had no miRNA target-lookup tool -- skills fell back to bulk
TargetScan/miRTarBase downloads (miRTarBase Cloudflare-blocked). ENCORI (starBase)
exposes CLIP-supported + predicted miRNA-target interactions via a public REST API.
Add ENCORITool + ENCORI_get_miRNA_targets (registered in default_config):
mirna->targets or gene->miRNAs, ranked by CLIP-experiment support, predicting
programs listed. Verified miR-21-5p->CBX4 (103 CLIP); TP53->188 miRNAs. Mocked test.

* Round 025: add LDlink (LD proxies) and IUCN (conservation status) tools

Two key-gated gaps from the skill audit:
- LDlink_get_proxies (gwas-snp-interpretation): LD proxy variants for a SNP via
  NIH LDlink LDproxy, population-specific, R2-filtered (free LDLINK_TOKEN).
- IUCN_get_conservation_status (ecology-biodiversity): Red List category by
  scientific name (free IUCN_API_KEY, v4 API).
Both declare optional_api_keys/api_key_info, return a clear register-here error
without a token, parse the verified API response formats. Mocked tests. Wired
into both skills.

* Round 025: population-coverage script for vaccine HLA coverage (last queued gap)

Vaccine design needs HLA population-coverage, but TU has no HLA-frequency tool and
the skill just pointed at the external IEDB web tool. Package the coverage MATH
(scripts/population_coverage.py): per-locus Hardy-Weinberg coverage
1-(1-p)^2 combined across loci, with a small bundled common-HLA-A/B average
frequency table for a first-pass estimate and --freq-file to supply real
AFND/population-specific frequencies. Verified A*02:01 -> 27.1% (matches cited),
broad set -> 73.6%. Wired into Phase 3, with a clear caveat not to use the
average default for a specific ethnicity.

* Round 026: standard envelope for FDA label tools + RCSB entry_id alias

Feature-026C-1: FDALabelTool (FDA_search_drug_labels / FDA_get_drug_label /
FDA_list_drug_classes) returned bare lists/dicts on success (framework-wrapped
to {"result": [...]}), inconsistent with the project-wide
{status, data, metadata} success contract that the error paths already follow.
Added an _ok() helper; all three query types now return the standard envelope
and their return_schema oneOf success branch is updated to match.

Feature-026B-002: RCSBData_get_entry only accepted pdb_id; the RCSB-native term
is entry_id (endpoint is /core/entry/{id}), so callers reaching for it hit a
hard schema-validation failure. _query() now aliases entry_id/id -> pdb_id and
the schema accepts either (anyOf) while an empty call still returns a clear
error.

Tests: tests/unit/test_fda_label_envelope.py, test_rcsb_entry_id_alias.py (9).

* rnaseq-deseq2 skill: scan for precomputed DESeq results embedded in the data file

Supplementary RNA-seq spreadsheets frequently ship the authors' own DESeq
output (per-comparison Up/Down flags, log2FC/padj column blocks, extra sheets)
alongside the counts. Re-running DESeq2 — especially on the normalized counts
these files contain (DESeq2 needs raw integer counts) — gives a materially
different, wrong number. Added guidance to open every sheet, inspect all
columns, and count DE genes directly from the embedded flags when present;
defines DE across-all-comparisons (union) vs jointly/also-DE (intersection).

* Fix stale tool names in skill references (PR #245 review)

Review of PR #245 surfaced wrong/nonexistent tool names in skill docs:
- GtoPdb_get_target_interactions -> GtoPdb_get_interactions (does not exist;
  branch had renamed the sibling on the same line but missed this one)
- PDB_search -> PDB_search_similar_structures (fallback-cell hint)
- UniProt_features -> UniProt_get_features_by_accession
- UniProt_taxonomy -> UniProtTaxonomy_search
All four targets verified present in the registry; fixed across skills/ and the
plugin/skills/ mirror (kept byte-identical).

* Round 026 review: GtoPdb diseases schema shape + FDALabel envelope test update

Third review pass of PR #245 surfaced two genuine issues:
- GtoPdb_search_diseases return_schema declared synonyms as an array of strings,
  but the GtoPdb API (passthrough) returns synonyms as objects {"name": ...};
  live output failed schema validation. Widened items to object-or-string so the
  declared contract matches reality across epilepsy/asthma/diabetes queries.
- The standard-envelope change to FDALabelTool (commit 89ffda71) broke two
  pre-existing tests in TestFDALabelGenericNameFallback that asserted the old
  bare-list/dict shape. Updated their assertions to read result['data'] (the
  fallback logic they verify is unchanged). test_exact_match_preferred already
  passed (only checks absence of 'error' + call count).

* Round 026 deep review: make 4 stale return_schemas match real tool output

Deep validation (run every changed tool's test_example, validate live output
against its return_schema) surfaced 4 pre-existing schema-vs-reality mismatches
in tools this branch touched (test_example refreshes / sibling additions). All
are schema-metadata-only fixes (no behavioral change):

- GtoPdb_get_interactions: return_schema declared a bare array, but the tool
  returns the standard {status, data[], url, count, ...} envelope -> wrapped in
  oneOf [success, error].
- HPA_generic_search: return_schema was bare {type: array}; tool returns
  {status, data[]} -> oneOf envelope.
- MGnify_list_analyses: return_schema was a bare array (kept the detailed item
  schema under data); tool returns {status, source, endpoint, query, data[],
  success} -> oneOf envelope.
- DailyMed_search_spls: metadata.next_page/previous_page are page numbers
  (integers), not strings -> widened to [string, integer, null].

All 4 now validate against live output across multiple queries; full deep
validation of every body-changed tool reports 0 schema issues.

* Fix: FDA label tools wrongly required 'limit' (it has a default)

Edge-case review of PR #245 found FDA_search_drug_labels and
FDA_list_drug_classes listed 'limit' in parameter.required, but limit has
default=5/20 in both the code (arguments.get('limit', 5/20)) and the schema
property. The natural calls FDA_search_drug_labels(drug_name='aspirin') and
FDA_list_drug_classes() were therefore rejected with "'limit' is a required
property" — while the actually-needed drug_name/indication were not required.
Set required: [] for both (limit stays optional with its default; drug_name/
indication are validated in code). Added a regression test.

* Fix: drop contradictory species default on two required ensembl SV params

Same required-vs-default contradiction class as the FDA limit fix:
ensembl_get_structural_variants and ensembl_get_sv_detail listed species in
parameter.required AND gave it default='human'. species is a URL path
parameter the code substitutes directly (no code-side default for these
endpoints), and their region-based siblings (overlap_features, alignment,
vep_region) all require species with no default. Removed the spurious default
so the two SV tools match the family convention (species required). A
systematic scan of every changed config now reports 0 required-with-default
contradictions.

* ENCORI: declare the 'assembly' genome-build parameter (was code-only)

Parameter-surface audit (cross-check each new tool's schema vs the params its
code actually reads) found ENCORI_get_miRNA_targets sends an 'assembly' query
param to the API (arguments.get('assembly', 'hg38')) but never declared it, so
agents could not discover they can select the genome build. Added it as an
optional string (default hg38, hg19 for the older build) and noted the existing
'gene_symbol' alias in the gene description. Live call with assembly=hg38 works;
test_examples still validate against the updated schema.

* binder-discovery TOOLS_REFERENCE: add missing final newline

end-of-file-fixer compliance for the two mirror copies edited during the
stale-tool-name fix; the file was missing a trailing newline (pre-existing on
main). No content change.

* Release 1.2.6: bump version across pyproject, mcpb bundle, and server.json

Bumps the package version 1.2.4 -> 1.2.6 in all five canonical locations
(pyproject.toml, mcpb/pyproject.toml, mcpb/manifest.json, and both server.json
fields) so the pip package, MCPB bundle, and MCP registry stay in sync and the
mcpb-version-drift CI check passes. 1.2.5 is skipped because that git tag
already exists while the version files were never bumped past 1.2.4; 1.2.6 is
the next collision-free patch and carries this PR's tool/skill robustness fixes
and new tools (ENCORI, LDlink, IUCN, GtoPdb diseases, HPO, IEDB B-cell, etc.).

* Release 1.2.6: bump Claude Code plugin + marketplace manifests

The Claude Code plugin manifests were at 1.2.5 (ahead of the pip files, which
were stuck at 1.2.4 — the source of the version drift). Bump both to 1.2.6 so
plugin.json and marketplace.json match pyproject/mcpb/server.json. All seven
canonical version locations are now consistent at 1.2.6.

* chore: sync API key catalog for new IUCN/LDlink key-gated tools

The sync-api-keys CI guard failed: iucn_tools.json and ldlink_tools.json
declare api_key_info (IUCN_API_KEY, LDLINK_TOKEN) but the derived catalog
(api_keys_catalog.json) and .env.template were never regenerated. Ran
scripts/gen_api_key_catalog.py to add both keys to the catalog and template.
Generator output is idempotent; diff is purely additive (the two new keys).

* Fix: regenerate static lazy registry for new tool classes (DRIFT-001)

The ultracode generated-file-drift sweep caught that this branch added three
tool classes (ENCORITool/IUCNTool/LDlinkTool) without regenerating
src/tooluniverse/_lazy_registry_static.py, so the committed static registry was
missing all three. Runtime is unaffected in normal installs (build_lazy_registry
supplements the static map with live AST discovery), but a frozen/PyInstaller
bundle with stripped .py sources would not see the new tools. Regenerated via
generate_lazy_registry.py (adds the 3 + a pre-existing missing
StructureAnnotationTool, restores sort order); no entries removed.
2026-06-06 16:45:10 -07:00
github-actions[bot] 3ae1554a04 Release v1.2.5 2026-06-04 01:03:19 +00:00
Shanghua Gao 30829d734c Release v1.2.4 (unified: pip + MCP registry + plugin) (#220)
Align all release channels at 1.2.4 and ship the backlog accumulated on main
since 1.2.3:
- pyproject.toml 1.2.3 -> 1.2.4  → triggers publish-pypi (ships #215 #217 #219
  #213 #222 #223 src/ changes that never reached PyPI) + publish-mcp-registry
- server.json 1.2.3 -> 1.2.4 (top-level + packages[]) for the MCP registry
- plugin.json 1.2.1 -> 1.2.4 and marketplace.json 1.2.0 -> 1.2.4 → plugin
  auto-update (literature-search multi-source rewrite #216 etc.)

Push tag v1.2.4 after merge for the GitHub Release.
2026-06-03 15:32:13 -07:00
Shanghua Gao 577f7c7638 fix(plugin): correct install skill — skill count, .mcp.json args, version pin (#206)
* fix(plugin): correct skill count, .mcp.json args, and version-pin in install skill

* fix(plugin): use existing PyPI version (1.2.2) in install skill version-pin example
2026-05-29 11:37:55 -07:00
Shanghua Gao 44997b5853 feat: ESM-C SAE variant interpretation + DMS analysis suite (#192)
* Phase A: ESM SAE variant interpretation (tool + skill)

Adds the first piece of full ada-f/esmc_sae integration into ToolUniverse:
a new tool that runs a protein sequence through the ESMC-6B Sparse
Autoencoder (SAE) via the EvolutionaryScale Forge API, plus a skill that
guides agents through SAE-based missense variant interpretation.

Changes:

src/tooluniverse/esm_tool.py:
  + _get_esmc_client() helper — separate ESMCForgeInferenceClient (the
    existing _get_client returns ESM3ForgeInferenceClient which does not
    expose SAE outputs)
  + ESMTool._get_sae_features() — new operation 'get_sae_features'
    Calls client.logits(..., config=LogitsConfig(sae_config=SAEConfig(...))),
    parses the torch.sparse_coo_tensor (L+2, 16384), strips BOS/EOS,
    optionally filters to position +/- window, returns top-K features
    per residue as JSON-friendly COO triples.
  + Defensive ordering: input validation BEFORE SDK import check, so a
    caller with bad args sees the input error (not 'install esm').
  + License notice in output metadata (Cambrian non-commercial).

src/tooluniverse/data/esm_tools.json:
  + ESM_get_sae_features entry with full parameter schema, oneOf return
    schema, real test example using TP53 R175H N-terminal fragment.

tests/tools/test_esm_sae_tool.py (new, 8 tests):
  - Mocked SDK; all input + output paths covered including error cases.

plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md:
  + New skill (212 lines). 5-step workflow:
    1. gene -> UniProt accession
    2. Fetch canonical sequence
    3. Validate ref residue + build mutant
    4. Call ESM_get_sae_features for ref + mut
    5. Compute per-feature deltas, rank gained/lost, interpret
  + 6-category interpretation table
  + Cross-validation pattern
  + Provenance attribution to ada-f/esmc_sae

Verification:
  - Real end-to-end SAE call on TP53 (209-AA fragment, R175H at position
    175, +/- 8 window) returned 17 residues x 64 features = 1088 sparse
    activations.
  - 8 unit tests pass (mock SDK).
  - 13 total tests pass (8 SAE + 5 Gemini), no regressions.

Phase A scope only — Phase B (label tool + composite) and Phase C
(ThermoMPNN + LoF synthesis skill) pending.

* Phase B (1/2): ESM_score_variant_sae_disruption composite tool

Convenience layer over ESM_get_sae_features that runs ref + mutant SAE
inference in one call and returns ranked top-K features lost/gained at
the mutation site. This is the standard entry point for SAE-based
variant interpretation.

Tool: ESM_score_variant_sae_disruption
  Inputs: sequence, position (1-indexed), ref_aa, alt_aa,
          window=8 (default), top_k_features=10 (default)
  Output: ranked top_features_lost + top_features_gained with delta +
          ref_activation_sum + mut_activation_sum per entry
  Cost: 2 Forge credits. Latency ~3-6s.

Defensive validation:
  - sequence + position + ref_aa + alt_aa all required
  - sequence[position-1] must equal ref_aa (catches isoform mismatch
    before consuming Forge credits)

Tests (4 new, all mocked SDK):
  - happy path with correct shape + variant string
  - ref_aa mismatch returns clear error with actual residue
  - missing position / missing ref_aa each return distinct errors
  - metadata reports forge_calls_made=2 + non-commercial license

Integration test (live Forge, TP53 R175H):
  - 10.1s, 204 unique features touched, 2 Forge calls
  - feature 13270 went 0.19 -> 0 (fully deactivated by R175H)
  - feature 6404 weakened 0.83 -> 0.67
  - ref_aa='K' instead of 'R' returned correct validation error

Skill: tooluniverse-protein-sae-variant-interpretation/SKILL.md
  + Added 'Quick path (recommended)' section pointing at the composite
    tool; long path (manual ESM_get_sae_features + delta) preserved
    for cases needing raw per-residue features.

Total SAE tests: 12 pass (8 from Tool 1 + 4 for Tool 3).

* Phase B (2/2): ESM_describe_sae_feature on-demand labeling tool

Closes the SAE interpretability gap: agents can look up what a feature_id
MEANS biologically without depending on Ada Fang's pre-computed feature
label parquet. Runs SAE inference across a curated 10-protein panel,
cross-references SAE-activating residues with UniProt feature
annotations, infers a dominant high-level category.

Tool: ESM_describe_sae_feature
  Input: feature_id (0-16383), optional n_proteins / top_residues / use_cache
  Output: category (catalytic | ligand-binding | ptm | domain | motif |
          structural-stability | secondary-structure | transmembrane |
          signal-peptide | propeptide | uncategorized),
          confidence (0-1), category_vote_counts, supporting_evidence
  Cost: 10 Forge credits first call, FREE on cache hit.
        Cache: ~/.cache/tooluniverse/sae_labels/{sae_model}/feature_{id}.json
  Latency: ~30-60s first call, <1s cache hit.

Pipeline: per protein -> fetch UniProt JSON (urllib, no new dep) ->
SAE inference -> find top-K activating residues for target feature_id
-> check overlapping UniProt features -> map types to high-level
categories. Aggregate votes across panel.

Curated panel (10 diverse well-annotated human proteins): TP53, EGFR,
KRAS, thrombin, CYP3A4, insulin, hemoglobin-beta, tPA, serum albumin,
AKT1. Covers DNA-binding, kinase, GTPase, serine protease, P450 enzyme,
signal-peptide hormone, oxygen carrier, fibrinolytic protease,
transport protein.

Tests (5 new, mocked SDK + UniProt, 17 SAE tests total all pass):
  - Bad feature_id (out-of-range, wrong type, missing) errors clearly
  - Bad n_proteins (0, > panel size) errors clearly
  - 'uncategorized' when no UniProt overlap (honest 'don't know')
  - Cache hit avoids new UniProt fetches; from_cache=true
  - Voting: panel proteins with Modified residue at activating position
    -> category='ptm', confidence=1.0

Integration test (live Forge, feature 13270 with n_proteins=3):
  - 33.2s first call, 0.000s cache hit
  - Activated only at EGFR C-terminal tail; no informative UniProt
    annotations at those positions -> correctly returns 'uncategorized'

Phase B complete (Tools 2 + 3). Phase C remaining: ThermoMPNN tool +
tooluniverse-protein-lof-mechanism synthesis skill.

* Phase C: tooluniverse-protein-lof-mechanism skill (DynaMut2-based)

Final piece of the ada-f/esmc_sae TU integration: a synthesis skill that
combines 5 independent signals to propose a LoF mechanism for missense
variants and distinguishes 'structural stability LoF' (folding broken)
from 'direct functional disruption' (catalytic / binding / PTM site
damage).

5 signals:
  1. AlphaMissense pathogenicity score
  2. AlphaFold pLDDT (structural context at mutation position)
  3. ESMC ΔlogP (evolutionary plausibility of substitution)
  4. SAE feature disruption (which biological feature breaks) — from
     ESM_score_variant_sae_disruption + ESM_describe_sae_feature
  5. DynaMut2 ΔΔG (thermodynamic stability change)

Decision rule from ada's variant_lof_mechanism workflow:
  ddG > +1 AND ΔlogP < 0     → structural stability LoF
  ddG ≈ 0 AND SAE catalytic  → direct catalytic LoF
  ddG ≈ 0 AND SAE binding    → binding LoF
  ddG ≈ 0 AND SAE PTM        → PTM LoF
  ddG ≈ 0 AND SAE domain     → interface / domain LoF
  pathogenic but no SAE      → generic damaging, mechanism unclear

Why DynaMut2 instead of ThermoMPNN (which ada repo uses):
  - ThermoMPNN requires GPU + git clone + CUDA + conda; TU's design is
    pip-install + single env var
  - DynaMut2 is already in TU via hosted academic API (BioSig, UQ); no
    extra setup for the user
  - Both predict the same ΔΔG signal; for the binary 'ddG > +1 vs ≈ 0'
    decision driving this skill, either model gives the same answer
  - For users who need ThermoMPNN specifically (better accuracy near
    threshold, double-mutant support, indel support), the skill's
    'Optional: ThermoMPNN' section documents 1 local install path
    (MIT license, Kuhlman Lab) + 5 hosted SaaS alternatives (Tamarind,
    Neurosnap, BioLM, ProteinIQ, Levitate)

Provenance: Workflow adapted from variant_lof_mechanism.md in
ada-f/esmc_sae (Ada Fang, Marinka Zitnik lab). The 5-signal synthesis
+ mechanism decision rule + the structural-stability-vs-direct-
functional-disruption framework are from that work.

Cross-refs verified: all 9 tools referenced (UniProt_search,
UniProt_get_sequence_by_accession, UniProt_get_entry_by_accession,
AlphaMissense_get_variant_score, alphafold_get_prediction,
ESM_score_sequence, ESM_score_variant_sae_disruption,
ESM_describe_sae_feature, DynaMut2_predict_stability) exist in TU's
loaded tool registry.

No tests added — this is a methodology document; the tools it dispatches
have their own unit + integration tests in Phase A/B.

* Pre-push polish: skill stale wording fix + sequence length cap

Two issues found by pre-push audit:

1. plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md
   line 171 said 'Look up via ESM_describe_sae_feature (Phase B tool) —
   once available' — but Phase B is now done. Updated to describe the
   tool's actual cost / cache behaviour.

2. ESM_get_sae_features had no upper sequence-length cap. ESMC-6B Forge
   handles ~2,700 AA in practice (per ada repo + EvolutionaryScale docs);
   longer sequences fail with opaque server errors. Added explicit
   pre-flight check that returns a clear actionable error before
   wasting Forge credits.

Tests: +1 unit test (rejects overlong sequence with clear error).
Total 18 SAE tests, all pass.

* Phase D (1/2): Structure_annotate_per_residue tool

Per-residue structural annotation from a PDB structure:
  - binding interface: scHA distance to partner chain(s) < cutoff
  - ligand pocket: scHA distance to ligand heavy atoms < cutoff
  - core vs surface: relative SASA (freesasa, isolated target chain)
  - optional secondary structure from PDBe REST

Methodology adapted from ada-f/esmc_sae (Ada Fang, Marinka Zitnik lab) -
dms_analysis/scripts/04_compute_structural_annotations.py.

Verification: reproduces ada's kras_anno.csv with 168/168 region matches on
6VJJ (KRAS-RAF1 + GNP/MG). KRAS G12/G13/Q61/D119 oncogenic hotspots all
correctly classified as GTP-pocket residues; interface residues 21-41/67/71
match the known KRAS switch I/II region contacting RAF1-RBD.

New optional deps in [bioinformatics] extra: freesasa>=2.2.0.

Tests: 10 unit tests with synthetic PDB fixture, all pass. Real-PDB test
covered by the test_example in the JSON config (CLI smoke-test runnable).

* Phase D (2/2): port 6 DMS analysis skills from ada-f/esmc_sae

Adapts ada's dms_analysis/skills/ to the tooluniverse plugin layout:

  1. tooluniverse-protein-structural-annotation-pdb
     - Wraps Structure_annotate_per_residue (added in previous commit)
     - Cross-refs verified: PDBeSIFTS_get_best_structures,
       PDBeSIFTS_get_all_structures, RCSBAdvSearch_search_structures,
       UniProt_get_sequence_by_accession, pdbe_get_entry_secondary_structure

  2. tooluniverse-mavedb-dms-retrieval
     - Orchestrates existing MaveDB_* tools (search, get_score_set,
       get_variant_scores, search_experiments)
     - Adds HGVS parsing, single-mutant filtering, canonical-numbering
       verification step

  3. tooluniverse-sae-mutant-tensor-build
     - Library-scale orchestration of ESM_get_sae_features
     - (20 x n_positions x 16384) tensor + WT-diagonal NaN convention
     - Aggressive caching pattern documented

  4. tooluniverse-sae-dms-global-validation
     - Statistical workflow: Mann-Whitney U on topK_drop, neutral vs
       disruptive group, robustness sweep over K/neutral-band/quantile

  5. tooluniverse-sae-dms-hotspot-features
     - Per-cluster permutation test (mean of max_drop, BH-FDR within
       cluster) + descriptive top-5 ranking
     - Calls ESM_describe_sae_feature for labels

  6. tooluniverse-annotated-dms-heatmap
     - Visualization: heatmap + sequence + structural annotation track
     - Optional per-hotspot SAE feature callouts above heatmap

All skills:
  - disable-model-invocation: true (explicit invocation, like skills 5 + 6)
  - Provenance attribution to ada-f/esmc_sae throughout
  - Honest-limitations section + cross-references to other TU skills/tools
  - Tool names in code snippets verified against tu.all_tool_dict

Together with the SAE tools from Phase A+B+C and the
Structure_annotate_per_residue tool from Phase D (1/2), this PR delivers
the full TU implementation of ada-f/esmc_sae:

  Ada's plan                          TU implementation
  -----------------------------------------------------
  Tool 1 (SAE extraction)             ESM_get_sae_features
  Tool 2 (SAE interpretation)         ESM_describe_sae_feature
  Tool 3 (structural annotation)      Structure_annotate_per_residue
  Tool 4 (DMS heatmap plotting)       skill, no atomic tool needed

  Skill: structural annotations       tooluniverse-protein-structural-annotation-pdb
  Skill: build per-mutant SAE tensor  tooluniverse-sae-mutant-tensor-build
  Skill: SAE global drop vs DMS       tooluniverse-sae-dms-global-validation
  Skill: SAE hotspot enrichment       tooluniverse-sae-dms-hotspot-features
  Skill: retrieve DMS from MaveDB     tooluniverse-mavedb-dms-retrieval
  Skill: annotated DMS heatmap        tooluniverse-annotated-dms-heatmap

* Test+fix: execute every Python snippet in the 6 DMS skills

Process correction: shipped 6 DMS-analysis SKILL.md files with 37 Python
code blocks WITHOUT executing the load-bearing logic against synthetic data
first. This violated the harness rule I had just codified (devtu-create-tool
'Top 7 Mistakes #3: Fake test_examples — tests fail').

Built tests/tools/test_dms_skill_snippets.py:
  17 tests covering the verbatim Python logic from 5 of the 6 DMS skills:
    - skill 8 (MaveDB retrieval):       HGVS parser, matrix reshape, landmark check
    - skill 9 (SAE tensor build):       pooled features shape, WT-diagonal NaN, cache function
    - skill 10 (global validation):     drop computation, topk_drop, categorize, MWU
    - skill 11 (hotspot features):      max_drop, cluster chaining, permutation_pvalues, descriptive top-5
    - skill 12 (annotated heatmap):     landmark alignment, full matplotlib render to PNG
    - skill 7 (structural annotation):  field-extraction pattern (caught real bug)

Real bugs found and fixed:
  1. Skill 7 read 'annotations[X-1]["region"]' — assumes positions are
     1-indexed and contiguous, which PDB chains often violate. Changed to
     dict-by-position pattern: 'by_pos = {a["position"]: a for a in
     annotations}; by_pos[X]["region"]'. Tested both in the skill (verbatim)
     and the snippet test.

Also added live-PDBe test for Structure_annotate_per_residue's
include_secondary_structure=True code path — was previously untested.
Verifies KRAS β1 strand (residues 2-9) and α3 helix (residues 88-104) come
back labeled correctly. Skipped automatically if PDBe REST is unreachable.

Tests now: 28 (was 28) + 18 new (17 snippet + 1 live SS) = 46 total, all pass.

* Docs: bump skill count 115->121 in plugin.json + CHANGELOG 1.2.1 entry

- plugin/.claude-plugin/plugin.json: skill count 115 -> 121 (added 2 in
  Phase A+C: protein-sae-variant-interpretation + protein-lof-mechanism;
  added 6 in Phase D: protein-structural-annotation-pdb, mavedb-dms-retrieval,
  sae-mutant-tensor-build, sae-dms-global-validation, sae-dms-hotspot-features,
  annotated-dms-heatmap), version 1.2.0 -> 1.2.1 to match the actual release
  pyproject.toml ships.

- plugin/CHANGELOG.md: new [1.2.1] section documenting the 4 new tools +
  8 new skills, verification status (46 unit tests, 168/168 KRAS region
  match, TP53 R175H reproduction, PDBe SS live path), prerequisites, and
  the Cambrian Inference Clickthrough License attribution.

* E2E integration test: KRAS DMS pipeline end-to-end (real Forge API)

Validates the full 6-skill DMS analysis chain on real data, not synthetic:

  Step 1: UniProt_get_sequence_by_accession (P01116)        → KRAS 189 AA
  Step 2: MaveDB_get_variant_scores (urn:mavedb:00000115-a-7)
             folding ΔΔG abundancePCA                       → 2191 variants
  Step 3: HGVS parse + filter to positions 10-25            → 303 single missense
          numbering check passes (MaveDB pos 10 ref=G == UniProt KRAS[9])
  Step 4: ESM_get_sae_features per WT + per mutant          → 319 Forge calls
  Step 5: SAE drop + Mann-Whitney U: disruptive vs neutral
             ΔΔG range -0.49 to 2.92, neutral=173, disruptive=91
             disruptive median drop = 0.057 vs neutral = 0.041
             → p = 0.00706 (SAE drop correlates with folding disruption)
  Step 6: G12/G13 cluster hotspot → top 5 features labeled via
             ESM_describe_sae_feature:
             feature 6255  → ligand-binding   (conf 0.50)
             feature 271   → secondary-structure
             feature 11930 → secondary-structure
             feature 10721 → ligand-binding   (conf 0.50)
             feature 6615  → secondary-structure

Biology check passes: KRAS G12/G13 sit in the P-loop / Walker-A motif that
binds the β-phosphate of GTP. The top-ranked feature at this hotspot is
labeled 'ligand-binding' — the expected mechanism.

Integration bugs found: 0. All 6 skills (mavedb-dms-retrieval,
sae-mutant-tensor-build, sae-dms-global-validation, sae-dms-hotspot-features,
... ) chain together with consistent shapes / dtypes / orderings on real
data; no shape mismatches; HGVS parser handles MaveDB's actual hgvs_pro
format; numbering pins correctly to UniProt canonical.

Test is skipped by default (set RUN_DMS_E2E=1 to opt in). Reference output
from the successful run is committed at
tests/integration/dms_pipeline_e2e_kras_output.txt as evidence.

* Fix: MaveDB_get_variant_scores returns ALL variants by default

The /scores endpoint downloads the full CSV in one HTTP request; limit
was a client-side truncation that defaulted to 50 and was hard-capped at
500. For whole-protein DMS workflows (KRAS folding ΔΔG = 3553 variants,
TP53 = thousands) this silently truncated the data, forcing agents to do
per-position hgvs_pro substring pagination — which I had to write into the
DMS skill as a workaround.

Fix:
  - default limit changed to 0 (return all)
  - limit=null / 0 / negative all mean 'no truncation'
  - response now reports total_variants_in_set as a plain integer plus
    truncated:bool and limit_applied:int|null so callers can detect
    silent truncation explicitly
  - removed the 500 cap (the API streams it all anyway, capping client-side
    discarded data we already paid for downloading)

Files:
  src/tooluniverse/mavedb_tool.py:
    _get_variant_scores - limit logic + new response fields
  src/tooluniverse/data/mavedb_tools.json:
    schema: limit is now [integer, null], default 0; description rewritten;
    return_schema has new truncated + limit_applied fields
  tests/tools/test_mavedb_tool.py (NEW, 9 tests):
    default -> all 3000, limit=0 -> all, limit=100 -> truncated, limit>data -> all,
    limit=-5 -> all, hgvs_filter, missing urn, 404, empty CSV
  plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md:
    Step 3 simplified - single call returns everything, no pagination workaround
  tests/integration/test_dms_pipeline_e2e_kras.py:
    Step 2 simplified from 17 paginated calls to 1
  tests/integration/dms_pipeline_e2e_kras_output.txt:
    regenerated reference output (now shows 3553 variants from a single call)

Verification:
  9/9 new unit tests pass
  E2E pipeline still finds the G12/G13 hotspot top feature labeled
  'ligand-binding' (the expected KRAS P-loop / Walker-A biology)

* Reframe 3 DMS skills around user goals, not methodology

The original sae-* prefixed skills described what the methodology DID
(validate SAE method, find SAE features at hotspots). Users actually need
answers to broader questions:
  - 'Does my variant-effect predictor work on this DMS?' (not 'is SAE good?')
  - 'Why is this hotspot critical?' (not 'what SAE features dropped?')

Refactor (3 skills became 2, more general purpose):

  DELETED:
    tooluniverse-sae-mutant-tensor-build   (was just a pipeline step, not a goal;
                                            content folded into the two below)
    tooluniverse-sae-dms-global-validation (replaced by predictor-benchmarking)
    tooluniverse-sae-dms-hotspot-features  (replaced by hotspot-mechanism)

  NEW:
    tooluniverse-variant-predictor-dms-benchmarking
      Goal: validate ANY per-variant predictor against DMS data.
      Works for: AlphaMissense, ESM-C SAE drops, ESM logits, EVE,
                 conservation, DynaMut2, any custom score.
      Same statistical methodology (MWU + robustness sweep); SAE is shown
      as the worked example. Reviewer-quality benchmark output.

    tooluniverse-dms-hotspot-mechanism-interpretation
      Goal: explain WHY a DMS hotspot is functionally critical.
      Synthesizes 3-4 evidence layers:
        - Structural context (Structure_annotate_per_residue: interface /
          pocket / core / secondary structure)
        - UniProt features (active site / binding site / PTM / disulfide)
        - SAE feature drops (optional 4th layer, more rigorous via
          permutation test or fast via descriptive ranking)
      Returns a mechanism call: catalytic / ligand-binding / interface /
      structural-core / PTM / regulatory / mixed / unknown — synthesized
      from the evidence stack, with the reasoning shown.

  Updates:
    plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md: cross-refs
      now point to the 2 reframed skills (predictor-benchmarking + hotspot-
      mechanism), not the 3 deleted SAE skills.
    plugin/skills/tooluniverse-annotated-dms-heatmap/SKILL.md: callouts
      cross-ref updated to hotspot-mechanism-interpretation.
    plugin/CHANGELOG.md: 1.2.1 entry rewritten to describe the new
      user-goal-oriented skills.
    plugin/.claude-plugin/plugin.json: skill count 121 -> 120.
    tests/tools/test_dms_skill_snippets.py: header + section comments
      updated; statistical logic tests unchanged and all 17 still pass.

Verification: 55/55 unit tests pass (17 snippet + 9 MaveDB + 11 Structure
+ 18 ESM SAE).

* Merge annotated-dms-heatmap into dms-hotspot-mechanism-interpretation

By the user-goal principle: visualization is a deliverable of hotspot
interpretation, not a user goal on its own. Nobody comes to ToolUniverse
saying 'I want to draw a DMS heatmap' — they come saying 'I want to
understand these hotspots, ideally with a publication figure I can use.'

Changes:
  DELETED:
    plugin/skills/tooluniverse-annotated-dms-heatmap/
      Content folded into Step 7 of hotspot-mechanism-interpretation.

  EXPANDED:
    plugin/skills/tooluniverse-dms-hotspot-mechanism-interpretation/SKILL.md
      New Step 7: 'Visualize — annotated DMS heatmap with hotspot callouts'
      covering landmark alignment, heatmap + sequence + annotation track,
      WT-vs-not-measured cell distinction, callout overlay, long-protein
      panel splitting. The matplotlib code reads the structural annotation
      and mechanism call data already computed in Steps 2/5.

  CROSS-REFS UPDATED:
    mavedb-dms-retrieval, protein-structural-annotation-pdb,
    variant-predictor-dms-benchmarking: visualization line now points to
    'Step 7 of dms-hotspot-mechanism-interpretation' instead of the
    deleted standalone skill.

  Docs:
    plugin/.claude-plugin/plugin.json: skill count 120 -> 119.
    plugin/CHANGELOG.md: 1.2.1 entry updated to reflect the merge.
    tests/tools/test_dms_skill_snippets.py: header + section comments
      updated; matplotlib snippet tests unchanged (now exercise Step 7
      content of hotspot-mechanism instead of standalone heatmap skill).

Verification: 55/55 unit tests pass.

* Skill iter-2 improvements from eval feedback

Iteration-2 quality eval (4 user-question test prompts, 2 configs each, 8
subagent runs) exposed 3 actionable gaps in the refactored DMS skills.
All 3 fixed; iter-2 with_skill pass rate 100% (vs iter-1 91.7%).

variant-predictor-dms-benchmarking/SKILL.md:
  1. NEW Step 3.5 (mandatory NaN sanity gate)
     - Pre-MWU check that both groups have >=5 finite predictor scores;
       raises ValueError instead of silently running MWU on all-NaN inputs.
     - Sign-convention double-check via Spearman vs AlphaMissense.
     Motivation: iter-1 eval-1 had SAE matrix all-NaN, agent ran MWU,
     reported 'AM wins by default' — exactly the failure this gate prevents.
  2. Step 4 commentary: rank correlation + AUROC as valid complements to MWU
     Motivation: iter-1 eval-0 without_skill used Spearman+AUROC and gave a
     richer answer than the skill's MWU-only path.
  3. Predictor option B (AlphaMissense): rewrote with actual TU schema
     - Real call is AlphaMissense_get_variant_score(uniprot_id, variant).
     - Documents the hegelab proxy's categorical-bin response shape and
       provides bin-midpoint parsing code (BIN_MIDPOINTS dict).
     - Adds AlphaMissense_get_residue_scores + AlphaMissense_get_protein_scores
       as cheaper alternatives for saturation/full-protein analyses.
     - Documents the DeepMind bulk CSV as the higher-resolution alternative
       when true per-substitution numerics are needed.
     Motivation: iter-2 eval-0 with_skill agent flagged that the skill's
     call signature was wrong (used non-existent uniprot_accession +
     position + reference_amino_acid + alternate_amino_acid kwargs).

dms-hotspot-mechanism-interpretation/SKILL.md:
  4. NEW Step 0 (mandatory premise check)
     - When user names specific positions as a hotspot, verify they actually
       rank high in this DMS before proceeding.
     - Concrete decision rule: top 25% confirmed, top 50% noted, below 50%
       MUST be flagged as mismatch at the top of the answer.
     - Worked example: KRAS G12/G13 rank 105/187 + 124/187 by max ΔΔG in
       the AbundancePCA assay — famously oncogenic but NOT folding hotspots.
     Motivation: iter-1 eval-2 with_skill caught this mismatch by accident;
     iter-2 with_skill agent followed the new explicit Step 0 and surfaced
     it as the very first line of its report.

test_dms_skill_snippets.py:
  3 new tests for the new skill code blocks (per harness rule):
    - test_variant_predictor_alphamissense_bin_parsing: validates the
      categorical-bin -> bin-midpoint parsing logic (empty, single, full
      saturation cases)
    - test_variant_predictor_nan_sanity_gate: validates the ValueError
      raise on all-NaN predictor input, and the pass-through on sparse-
      but-adequate input
    - test_hotspot_premise_check_rank_logic: validates the ranking math
      that decides 'top 25% / top 50% / below 50%' bucket for the user-
      named positions
  20/20 snippet tests now pass.

* Generalize skill 6: residue-functional-mechanism-interpretation

Skill 6 rename + scope generalization: the previous name
'tooluniverse-dms-hotspot-mechanism-interpretation' framed the entry as
'DMS hotspot' specifically, which excluded valid user goals where the
residues come from other sources.

The core methodology (multi-evidence synthesis: structural context +
UniProt features + optional SAE) is already source-agnostic. Only the
entry was DMS-locked. This commit generalizes the entry.

Rename:
  tooluniverse-dms-hotspot-mechanism-interpretation
  -> tooluniverse-residue-functional-mechanism-interpretation

Two entry paths:
  Path A — user_provided_positions (NEW)
    Skips Step 1 hotspot detection entirely. Covers:
      - ClinVar recurrent variants (pull positions, pass directly)
      - Literature hot regions (paste positions from paper Fig 1)
      - Evolutionary conserved residues (filter by conservation, pass top-N)
      - Druggable-site residues (from a binding-site predictor)
      - Clinician's single-residue question ('why does R175 matter?')
  Path B — DMS hotspot detection (ORIGINAL use case, preserved)
    Same as before. DMS workflow runs unchanged.

Path A vs B evidence-layer contract documented:
  Structural + UniProt + DMS-effect-size: ✓ both paths
  SAE permutation test (needs DMS baseline): Path B only
  SAE descriptive labels: both paths (if SAE tensor supplied)

Cross-refs updated in:
  - plugin/skills/tooluniverse-mavedb-dms-retrieval/SKILL.md
  - plugin/skills/tooluniverse-protein-structural-annotation-pdb/SKILL.md
  - plugin/skills/tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
  - plugin/CHANGELOG.md (1.2.1 entry rewritten for the broader scope)
  - tests/tools/test_dms_skill_snippets.py (header + section comments)

Snippet test added: test_residue_mechanism_path_a_user_provided_positions
verifies the new Path A entry: deduplication, ordering, gap<=2 clustering
on a mixed-source residue list (KRAS G12/G13 + TP53 R175 + R273).

Verification: 21/21 snippet tests pass (was 20). Skill count unchanged
(rename, not delete) at 119 tooluniverse-* skills.

Why this matters: the eval-driven design principle ('user goals not
methodology') still had one specific entry framing left. Surfaced by
honest self-audit after iter-2 100% pass rate.

* Promote MaveDB DMS retrieval from skill to tool

New tool MaveDB_get_effect_matrix(urn, uniprot_id?, score_field?) returns
ready-to-analyze (20 AAs x n_positions) effect matrix in one call. Internally:
fetch all variants (limit=0), parse HGVS, filter to single missense, auto-
detect score field, optionally verify position numbering against UniProt
canonical, reshape to standard layout. Returns full audit metadata.

Tests: 7 new unit tests in test_mavedb_tool.py (HGVS filter, score-field
auto-detection + override, no-usable-variants error, missing urn error,
numbering match + offset-detected paths). 37/37 across mavedb + snippet
test files.

Deleted plugin/skills/tooluniverse-mavedb-dms-retrieval/. The boilerplate
that lived there (HGVS parser + numbering check + matrix reshape) is now
hidden inside the tool. Consumers updated:
  - tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
    Step 1 now calls the tool with one line instead of 30 lines of inline
    parsing.
  - tooluniverse-residue-functional-mechanism-interpretation/SKILL.md
    Cross-refs updated.

Docs: plugin.json skill count 119 -> 118. CHANGELOG 1.2.1 updated for
5 new tools (was 4) + 5 new skills (was 6). The end state has every skill
mapping to a real user question and every tool mapping to a self-contained
action; no more 'orchestration step' skills in the SAE/DMS bundle.

* Fix AlphaMissense_get_protein_scores: actually return all residues

Iter-3 eval surfaced that AlphaMissense_get_protein_scores returned only
a 'sample' (1 residue) despite name + description claiming whole-protein
coverage. Skill 5 (variant-predictor-dms-benchmarking) iterates
data['scores'] keyed by 'resi', so the tool's actual output was a
load-bearing contract violation.

Fix: tool now actually fetches every residue:
  1. Look up protein_length from UniProt FASTA
  2. Concurrent per-residue hegelab API calls (ThreadPoolExecutor, 20 workers)
  3. Aggregate into a 'scores' list, one record per successful position

New parameter: max_residues (default 0 = no cap). Set to a positive integer
to fetch only the first N residues for fast diagnostic calls on long proteins.

New response shape:
  data.scores: List[{position, uid, aa, resi, benign, ambiguous, pathogenic,
                     benign_all, ambiguous_all, pathogenic_all, mean, mean_all}]
  data.protein_length: int
  data.n_positions_returned + n_positions_attempted + max_residues_cap

Live verification:
  KRAS (max=20):   19/20 in 2.3s
  KRAS (full 189): 188/189 in 15.9s (pos 1 = Met start, no missense data)

Tests:
  - test_get_protein_scores_returns_per_residue_list pins the new contract
  - test_get_protein_scores_max_residues_cap verifies the cap parameter
  - existing test_get_protein_scores adjusted to use max_residues=5
  14/14 pass.

Skill 5 unchanged - it was already written against the post-fix contract;
the bug was the tool not honoring it.

* Scrub personal path from e2e reference output before merge

Pre-merge audit found one occurrence of '/Users/shgao/.../ToolUniverse-auto/'
in tests/integration/dms_pipeline_e2e_kras_output.txt — it came from the
TU 'Auto-loaded workspace profile.yaml' banner printed at startup. Replaced
with <REPO_ROOT> placeholder.

No secrets (ESM_API_KEY, ClawInstitute admin key, AWS/OpenAI keys) and no
other personal paths in any of the 24 PR-touched files.

* Remove upstream-collaboration attributions across SAE/DMS suite

Scrub explicit references to Ada Fang / ada-f/esmc_sae / Marinka Zitnik
lab from all PR-touched skills, tools, JSON descriptions, CHANGELOG, and
test assertions. Reference paths (dms_analysis/scripts, kras_anno.csv)
also removed in favor of generic wording ('an upstream research workflow',
'the original script', 'reference annotation CSV').

Files updated:
  plugin/CHANGELOG.md
  plugin/skills/tooluniverse-protein-lof-mechanism/SKILL.md
  plugin/skills/tooluniverse-protein-sae-variant-interpretation/SKILL.md
  plugin/skills/tooluniverse-protein-structural-annotation-pdb/SKILL.md
  plugin/skills/tooluniverse-residue-functional-mechanism-interpretation/SKILL.md
  plugin/skills/tooluniverse-variant-predictor-dms-benchmarking/SKILL.md
  src/tooluniverse/data/structure_annotation_tools.json
  src/tooluniverse/structure_annotation_tool.py
  tests/tools/test_structure_annotation_tool.py

Provenance language now uses neutral 'an upstream research workflow'.
The test_provenance_attribution_present assertion was updated to check
for the new phrasing.

Verification: 80/80 unit tests pass; 0 residual references across all
24 PR-touched files (Ada Fang, ada-f, Marinka Zitnik, esmc_sae, Mzitnik,
dms_analysis, kras_anno — all scrubbed).

* Strip residual attribution + reference-book language from skills

Second pass after initial scrub: skills are operational guidance, not
bibliographies. Removed:
  - all remaining 'ada' / 'Ada's' / 'ada repo' phrases
  - 'Provenance' paragraphs (5 skills) — they explain origin, not how to do anything
  - empty '## References' sections (5 skills)
  - script-path references ('05_validation1_global.py' etc.) leftover from scrub
  - 'from upstream research' attribution clauses
  - eval-debrief language ('The without-skill agent in the original eval missed this')
  - one stray comment in esm_tool.py ('per ada repo + EvolutionaryScale docs')

Final exhaustive sweep: 0 residual matches for any attribution pattern
(Ada Fang, ada-f, Marinka Zitnik, esmc_sae, dms_analysis, kras_anno,
Provenance, original eval, from upstream research, etc.).

Verification: 80/80 unit tests pass. Skills now read as pure operational
guidance — 'how to do X', not 'this rule is from Y'.

* Add 3 composite SAE tools: batch variant scoring, region aggregation, mechanism explanation

The first 3 SAE tools (get_sae_features, score_variant_sae_disruption,
describe_sae_feature) expose the raw capability but leave common
workflows on the caller. These 3 additions cover the most-frequent
caller-side composites:

- ESM_score_variant_sae_batch: score N variants against one reference
  with N+1 Forge calls instead of 2N (reference SAE computed once and
  reused). Enables saturation mutagenesis (all 19 alts at a position),
  DMS-style sweeps, and clinical variant panels at half the API cost.
  Capped at 100 variants per call to keep cost predictable.

- ESM_get_region_sae_features: aggregate SAE features over a contiguous
  residue range (a domain, epitope, binding pocket) and return features
  ranked by total |activation| with per-residue hit pattern. Feeds into
  ESM_describe_sae_feature on the top-K for category labels.

- ESM_explain_variant_mechanism: composite of disruption +
  describe_sae_feature on each top affected feature + 1-line category
  summary (e.g. "Disrupted feature categories (lost): catalytic=2,
  ligand-binding=1"). Set include_descriptions=false to skip labeling
  for cheaper calls. Used by variant-interpretation skills to get
  mechanism in one call rather than orchestrating 2 + N calls.

All 3 reuse existing _get_sae_features / _score_variant_sae_disruption /
_describe_sae_feature internally to keep validation and error handling
consistent. License note (EvolutionaryScale Cambrian Inference License,
non-commercial only) propagated to all 3.

8 new tests added to test_esm_sae_tool.py covering happy path, Forge
call counting, input validation, and category aggregation. Full suite
27 passed (was 19).

* Integrate ESMC-6B SAE mechanism tools into variant-interpretation + rare-disease-diagnosis skills

Both skills already use AlphaMissense to score variant pathogenicity but
had no mechanistic interpretation layer. Adds the new SAE composite
tools (ESM_explain_variant_mechanism, ESM_score_variant_sae_batch,
ESM_get_region_sae_features) as the "how does the variant disrupt
function" complement to "is the variant pathogenic":

- variant-interpretation/SKILL.md: new Phase 4.2 "Mechanism of Effect"
  between structural analysis and expression context. Recommends
  ESM_explain_variant_mechanism as the one-call entry point, with
  pointers to ESM_score_variant_sae_batch (saturation) and
  ESM_get_region_sae_features (domain-level) for advanced workflows.

- variant-interpretation/TOOLS_REFERENCE.md: new "ESMC-6B SAE" section
  with all 5 SAE tools, usage examples, and a table mapping SAE
  categories (catalytic/ligand-binding/ptm/etc.) to ACMG criteria.

- rare-disease-diagnosis/DIAGNOSTIC_WORKFLOW.md: new step "2b" added to
  the variant-interpretation pipeline that calls
  ESM_explain_variant_mechanism alongside AlphaMissense when WT
  sequence and aa-position are available.

- rare-disease-diagnosis/TOOLS_REFERENCE.md: new SAE section + table
  entry alongside the existing AlphaMissense entry.

All integrations explicitly note the ESM_API_KEY + esm-feature-branch
install requirements and the EvolutionaryScale Cambrian Inference
License (non-commercial only) so users aren't surprised at runtime.

No tool code changed; this is documentation/workflow integration of the
3 composite SAE tools added in the prior commit.

* Integrate composite SAE tools into 4 mechanism-focused skills

After the prior commit added the 3 composite SAE tools
(score_variant_sae_batch, get_region_sae_features,
explain_variant_mechanism), update the 4 skills whose workflow they
directly improve:

- protein-lof-mechanism: recommend ESM_explain_variant_mechanism as the
  one-call alternative to the existing ESM_score_variant_sae_disruption +
  ESM_describe_sae_feature chain in Step 4 (the unique signal). Lower-
  level pattern kept for callers that need raw feature_ids before
  labeling (e.g. filter by category first).

- variant-predictor-dms-benchmarking: replace the per-variant ESM_get_
  sae_features loop with ESM_score_variant_sae_batch as the preferred
  saturation-scoring path (1 + N Forge calls vs 2N). Loop pattern kept
  for callers that need the full per-residue x per-feature tensor for
  downstream PCA / clustering.

- protein-sae-variant-interpretation: promote ESM_explain_variant_
  mechanism as the fullest one-call entry point in the "Quick path"
  section; add a saturation example using ESM_score_variant_sae_batch.

- residue-functional-mechanism-interpretation: Step 4 cluster-feature
  ranking now has Path A (ESM_get_region_sae_features, 1 Forge call)
  for the common contiguous-cluster case and Path B (precomputed DMS
  tensor) for callers who already have one. Required-inputs table and
  cross-reference table updated accordingly.

No tool code changed. Total +88/-31 across 4 SKILL.md files.

* Light-touch SAE mechanism notes in ACMG + cancer-variant skills

Both skills already use computational pathogenicity predictors
(AlphaMissense, REVEL, CADD). Add a brief mention of
ESM_explain_variant_mechanism as the mechanism complement that turns
"PP3 satisfied" into "PP3 satisfied + here's why" — without changing
the underlying classification logic.

- acmg-variant-classification: Phase 2 (Computational Predictions /
  PP3, BP4) — note that PP3/BP4 is a vote count, not a mechanism.
  For VUS-resolution narratives, ESM_explain_variant_mechanism adds
  the lost/gained SAE feature categories (catalytic, ligand-binding,
  ptm, etc.) as a mechanistic explanation alongside the PP3 verdict.
  Does not strengthen PP3 above the predictor score alone.

- cancer-variant-interpretation: Driver vs Passenger Reasoning — for
  unique (non-hotspot) missense in known driver genes, recommend
  comparing the variant's SAE feature disruption to known hotspots in
  the same gene. A novel missense that disrupts the same SAE category
  as a known driver is more likely a driver than one disrupting
  unrelated features.

Both notes are 1-3 sentences each, no new phases or sections added.

* Fix dead-code bug in rare-disease-diagnosis SAE step: actually parse variant + fetch WT sequence

The prior commit added a Step 2b that called ESM_explain_variant_
mechanism but gated it on variant_info["wt_sequence"], ["aa_position"],
["aa_ref"], ["aa_alt"] — none of which the surrounding workflow ever
populates (it uses ["aa_change"] like "V600E" and ["uniprot_id"] only,
visible in Step 2 AlphaMissense). Result: the SAE step never ran in
practice.

Fix: parse aa_change with a regex (handles both "V600E" and "p.V600E"
forms), fetch the WT sequence via UniProt_get_entry_by_accession, and
gate on a WT-residue-matches-ref-aa check so isoform mismatches are
silently skipped rather than surfacing as the tool's ref_aa-mismatch
error inside a diagnostic report.

Verified UniProt_get_entry_by_accession exists in the registry with
the expected (accession) signature.

* Fix ESM tools' multi-op routing: bind operation via fields, not parameter

Pre-existing bug across all 10 ESM tool configs: operation was declared
as a required parameter with a JSON-schema default, but TU does not
honor JSON defaults during validation. Result: any caller using
tu.tools.X(...) or tu.run_one_function without explicitly passing
operation=<name> hit "Parameter validation failed: operation is a
required property" before reaching the tool's run() dispatch. Every
SAE skill code example in the repo (existing + my new ones) was
silently broken.

Root cause: ESM tools used the parameter.operation pattern. Other
multi-op tools (AlphaMissense, MaveDB, Orphanet, OMIM, ClinGen, etc.)
use the fields.operation pattern: the operation is bound to the tool
instance in __init__ from tool_config["fields"]["operation"], and
run() reads it from self.operation instead of from arguments. With
that pattern, operation isn't part of the user-facing schema at all.

Changes:

- ESMTool.__init__: read self.operation from fields.operation
- ESMTool.run: prefer self.operation, fall back to arguments["operation"]
  for backward compatibility with tests that pass it inline
- esm_tools.json (all 10 tools): add "fields": {"operation": "..."},
  remove operation from parameter.properties + required, drop the
  now-redundant operation field from test_examples

All 27 tests still pass (backward compatible). Verified via
tu.run_one_function on all 3 new composite tools: they now reach
their input-validation logic without the user passing operation.

* Fix ruff F841 in test_dms_skill_snippets — remove unused Path A locals

CI ruff check failed on:
  tests/tools/test_dms_skill_snippets.py:599:5: F841 Local variable
    `dms_matrix` is assigned to but never used
  tests/tools/test_dms_skill_snippets.py:600:5: F841 Local variable
    `disruptive_tail` is assigned to but never used

The Path A test (user_provided_positions, no DMS matrix) sets both
locals to None as documentation that they're irrelevant to this code
path, but then never references them. Remove the dead assignments —
the path-switch and the test comment above the if already document
that DMS-derived inputs aren't required here.

Verified locally:
  ruff check .  → All checks passed
  pytest tests/tools/test_esm_sae_tool.py tests/tools/test_dms_skill_snippets.py → 48 passed

* Unwrap 3 malformed test_examples in ESM SAE tool configs

Three original SAE tool configs (ESM_get_sae_features,
ESM_score_variant_sae_disruption, ESM_describe_sae_feature) had
test_examples wrapped in {description, arguments} dicts instead of
flat parameter dicts. TU's test_example runner expects flat dicts
(matching every other tool in the repo, e.g. AlphaMissense, MaveDB),
so these examples would have failed schema validation on first call.

Unwrapped each {description, arguments} → arguments dict and dropped
the now-redundant operation field (routing handled by fields.operation
since the previous commit). Pre-existing bug exposed by my schema audit
after the operation-routing refactor.
2026-05-24 22:54:56 -07:00
Shanghua Gao 16af425c05 Claude Code plugin: self-contained layout, skill-based routing, ML demo readiness (#161)
⏺ Introduces the Claude Code plugin for ToolUniverse as the recommended Claude Code
  integration. Replaces the previous MCP-only setup with a one-command install
  (`claude plugin install tooluniverse@tooluniverse`) that auto-configures the MCP
  server, slash commands, sub-agent, hooks, and 115 specialized research skills.

  Plugin structure
  - 115 skills routed via a single visible router (router auto-matches by question
    keywords + file extensions; sub-skills load on demand via Skill('name')).
  - 5 slash commands (research, compare, cross-validate, literature-sweep,
    translate-id) — each enforces a discipline the default agent doesn't apply.
  - 1 sub-agent (researcher) — same investigation as /tooluniverse:research but
    delegates to a forked-context subagent and returns one summary.
  - SessionStart hook (idempotent) that cleans up legacy globally-installed
    ToolUniverse skills so they don't shadow the plugin.

  Skill changes (general, not benchmark-specific)
  - Router gets a single percentage-vs-proportion units rule.
  - Sub-skills get top-of-mind discipline banners: long-format methylation
    ROWS-vs-unique-positions counting; Trimmomatic "reads completely discarded"
    = F + R + 2*D; DEG-count default reads the padj-only line; ClinVar benign-
    proportion 3-tier reporting; ortholog amino-acid single-representative sum;
    raw + log10 sensitivity for count-vs-length Pearson.

  Versions aligned at 1.2.0
  - Plugin manifest + marketplace.json + Python package (`pyproject.toml`) all at
    1.2.0. After merge, tagging v1.2.0 triggers the plugin-release workflow.

  Docs
  - New install + usage page (docs/guide/building_ai_scientists/claude_code.rst)
    covering the two-command install, version pinning, API-key setup, plugin
    troubleshooting, and the manual-MCP fallback.

  Quality measurement
  - BixBench closed-book official protocol (no reference notebook, hardened
    harness with strict leak audit and isolated workspace): 67.3% (LLM-graded).
    Compare to BixBench paper baseline 17% (Claude 3.5 Sonnet).
2026-05-21 13:12:38 -04:00