Skip to content

The small_molecule landscape class

The second registered LandscapeClass adapter, after rnai. Its reference landscape is gns561-cca-autophagy-001 — GENFIT's ezurpimtrostat (GNS-561), a lysosomotropic PPT1 inhibitor in intrahepatic cholangiocarcinoma.

The contract it implements is ADR-0006. Read that first for why the seam exists; this page covers what this particular adapter puts through it, and what it deliberately does not.

Code: ogur/engine/discovery_modality/classes/small_molecule.py Matcher: ogur/engine/discovery_modality/matching_small_molecule.py Tests: tests/unit/engine/discovery_modality/test_small_molecule_class.py, tests/unit/engine/discovery_modality/test_matching_small_molecule.py

Why this class exists

Two things about the RNAi adapter turned out to be RNAi facts rather than universal ones. Both were measured on a real run, not predicted.

1. Gene symbols are not universally productive full-text search terms. For an oligo the target gene is written into the trial record and the filing. For a mechanism-anchored small molecule the document names the drug and its mechanism; the gene lives only in the patent estate. Measured live on ClinicalTrials.gov:

Query Studies
autophagy 100
autophagy cholangiocarcinoma 2
chloroquine pancreatic 23
autophagy inhibitor PPT1 0
ULK1 inhibitor 0

A zero-yield query is worse than a bad one: its empty result reads as "no competitors exist" rather than "wrong query shape".

2. Oncology is on-thesis here. For the RNAi decks a carcinoma mention is a decisive off-thesis disqualifier. For this class it is decisive on-thesis, and the flood to drain is autophagy in non-oncology: PPT1/CLN1 is the infantile-Batten gene, autophagy dominates neurodegeneration, and the lysosomotropic parent class (chloroquine / hydroxychloroquine) is a malaria, lupus and rheumatoid drug.

The two behavioural seams

Round-1 planning: modality × indication

plan_round1_modality_x_indication in concept_queries.py, ported from PR #198. Three axes:

  1. bare class terms ("lysosomotropic") — productive against SEC annual reports and broad literature. The RNAi planner deliberately never emits a bare modality because siRNA alone floods; a mechanism term does not.
  2. class term × indication — the CT.gov / OpenAlex workhorse.
  3. a light modality × gene axis over the first few targets, for the gene forms that do index in full text. Genes otherwise reach the corpus through the by-target patent axis.

"Class terms" are scope.modalities + scope.modality_subclass — the thesis's own vocabulary, read off the ScopeSpec, never a literal in the adapter. A second small-molecule landscape on another mechanism therefore needs no code change to this planner.

Selection mechanism differs from PR #198

198 selected the axis with style=QUERY_STYLE_MODALITY_INDICATION on

generate_concept_queries, because that branch predates LandscapeClass. On main the seam is LandscapeClass.plan_round1 and validate_adapter requires it. Reintroducing style= would put two competing round-1 selectors in the tree and let a caller silently override the axis its class declared. Equivalence: QUERY_STYLE_MODALITY_TARGETplan_round1_modality_x_target, QUERY_STYLE_MODALITY_INDICATIONplan_round1_modality_x_indication.

Scope-fit: seven axes, and the company-grade gate

The shared ScopeFit carries four fields every class produces; this class adds three in details (fit.target_fit, fit.mechanism, fit.evidence still read like attributes).

Axis Values
modality autophagy / competitor / other
target_fit mechanism_axis / competitive / off / unknown
indication_fit in-thesis / adjacent / off / unknown
mechanism coherent / union / none
evidence company-grade / single-academic
provenance on-deck / novel
label core / honorable-mention / off-thesis

Two gates carry most of the precision:

Mechanism coherence. A big-pharma with a separate autophagy patent and a separate oncology trial reads mechanism + oncology over the union of its snippets while no single record makes that claim. core requires one hit carrying both.

Company-grade evidence. A trial sponsor or SEC filer is a company by registration, and so is an organisational patent assignee. OpenAlex projects author affiliations, so any number of OpenAlex hits is still "this affiliation co-authored papers". Frequency is deliberately not company-grade — a publishing university racks up hits exactly like a company would. OpenAlex and PubMed collapse to one literature family, so appearing in both is not multi-source corroboration. This gate is what cut the GNS561 core tier from 68 to 5.

The individual-assignee exception (PR #204 review). Patent registries harmonise every assignee, and individual inventors hold patents in their own names — the merged Genfit corpus carries "Ballabio Andrea" and "Egan Daniel F". Those arrived carrying the full weight of a company-grade signal and, on an otherwise coherent autophagy-oncology patent, were promoted to core: a person listed as a competitor. An entity whose only hard-source evidence is a patent filed under a personal name is therefore graded single-academic, which surfaces it as an honorable-mention lead rather than dropping it.

Two properties make that safe to apply automatically:

  • It reads the raw assignee strings, never canonical_name. normalise_entity strips legal suffixes, so "Eli Lilly and Company" canonicalises to "Eli Lilly" — two bare alphabetic tokens, structurally identical to a person. Only the raw form still carries the word that settles it.
  • aliases.is_personal_name fails toward organisation on every ambiguity. A missed person merely restores the old behaviour; a misjudged company would cost a real competitor its tier. Descriptors match as token prefixes rather than substrings, because as a substring bio fires inside the surname "Ballabio".

The sibling matcher

matching.py is canonical and is never forked, copied or edited — the closed-world coverage test and the open-world eval must share it byte-identically or their recall numbers stop being comparable. MatcherProtocol's docstring names the resolution: a new class supplies a sibling with its own tests. Every normalizer is reused by import (asserted by object identity in the tests).

Three judgements are re-made, because the siRNA deck schema decides them:

Canonical (matching.py) Sibling
Path-1 corroboration both deck targets in the blob code in text, or company and ≥1 target
Target scan substring word boundary
cross_filing allowed on SEC event filings always False
  • Single-target corroborationtarget_pair_in_blob needs two targets, a dual-targeting deck invariant that can never fire for a small molecule, so every path-1 bind would fall through.
  • Word boundaries — the GNS561 brief audit found MET binding inside trametinib, 990 of 1230 rows phantom. Ported from the run's own fix.
  • cross_filing withdrawn — pairing party A from one 8-K with party B from another is defensible when a curated deal list bounds what may be claimed. With no deck every pair is a hypothesis, so the same rule lets the pipeline both propose and confirm a deal nobody claimed. The key is kept, so a consumer reads False rather than KeyError.

Not covered. Unhyphenated asset codes (SAR405) — the shared code regex needs a hyphen; inherited, equally true for RNAi's AZD0780, and fixing it means editing matching.py. Deal financials — a coverage gap, not an extraction gap. CJK entity forms — available in the shared alias layer, but this class has no measured China footprint.

Deliberate non-declarations

Field Value Why
patents_backend None The run used live BigQuery gpatents (~230 GiB ≈ $1.40/scan). Pinning a paid backend as a class default would bill anyone who registers this class.
cde_policy None No localized NMPA terms measured for the biliary/hepatic cluster, so the CDE axis reports itself UNSUPPORTED rather than searching another class's disease scope (ADR-0006, R5).
ensembl_seed {} Never assembled for this class. An unseeded symbol resolves live against the OT search endpoint and is memoized — slower, never silently skipped. Inventing IDs would be unverified data.
assemble None The deck-free exploratory assembler stays eval-side, as the deck scorer does for RNAi.

Registry promotion

small_molecule moved from _KNOWN_UNREGISTERED to _INFERENCE_TOKENS, minus one token: bare "inhibitor".

The two tables are not interchangeable. A false positive in _KNOWN_UNREGISTERED only picks which "write the adapter" message an analyst reads. A false positive in _INFERENCE_TOKENS selects the decompose prompt, the modality gate and the matcher. "inhibitor" fails in both directions:

  • "microRNA inhibitor" is an RNAi modality (verbatim in RNAI.modality_tokens) — it would match both classes and turn a resolvable thesis into an ambiguity refusal.
  • "PD-1 inhibitor" / "checkpoint inhibitor" are antibodies — they would resolve to small_molecule.

Dropping it costs nothing real: a thesis whose only modality signal is the word "inhibitor" has not declared a class, and falls through to the normal fail-closed refusal.

The adapter is narrower than its name (PR #204 review)

The same argument excludes the generic small-molecule terms — "small molecule", "PROTAC", "degrader", "molecular glue", "kinase inhibitor" — which the promotion initially carried. small_molecule is not a generic small-molecule implementation: its decompose prompt, modality gate and scope-fit vocabulary are all the GNS561 autophagy-lysosome oncology thesis. Because POST /api/explore/decompose infers the class directly and does not rebind it, a PROTAC or kinase-inhibitor thesis came back 200 with a confident autophagy decomposition — the class-mis-selection the registry exists to prevent, arriving through the door marked "correct class".

Only the mechanism forms resolve now: "autophagy inhibitor", "lysosomal inhibitor", "lysosomotropic", "ppt1 inhibitor". The generic terms live in _OUT_OF_SCOPE_TOKENS, a third table whose refusal names the scope that is covered — distinct from _KNOWN_UNREGISTERED, where the class is missing entirely and the fix is "write the adapter". Without that distinction an analyst reads "no class matches ['PROTAC']" directly above "registered: ('rnai', 'small_molecule')" and reasonably concludes the registry is broken.

Two doors stay open, so this narrows inference without narrowing capability:

  • The real gns561-cca-autophagy-001 row declares ["small molecule", "small-molecule", "autophagy inhibitor", "lysosomotropic"] and still resolves — _OUT_OF_SCOPE_TOKENS is consulted only when no registered class matched, so generic terms sitting beside a covered mechanism are harmless.
  • Passing landscape_class="small_molecule" explicitly still selects the adapter. Inference is what got narrowed; a caller that says exactly what it wants is still never second-guessed.

Known limitation

modality_tokens and the scope-fit vocabularies ship the autophagy-lysosome mechanism set, because for a small molecule the productive harvest gate is the mechanism, not the phrase "small molecule". This is the same shape as rnai.py carrying cardiometabolic indication vocabulary for its two cardiometabolic decks.

A second small-molecule landscape on another mechanism rebinds both:

from dataclasses import replace
from ogur.engine.discovery_modality.classes.small_molecule import (
    SMALL_MOLECULE, make_classifier,
)

BTK = replace(
    SMALL_MOLECULE,
    modality_tokens=("btk", "bruton", "covalent inhibitor"),
    classify_scope_fit=make_classifier(
        mechanism_tokens=("btk", "bruton"), modality_label="btk"),
)

A third mechanism wanting a third adapter is the signal that this vocabulary should move onto ScopeSpec — the same rule ADR-0006 Decision 1 applies one level up.

One reported label was generalised from the ported classifier: its disease-competitor tier surfaced as cca_competitor. cca is cholangiocarcinoma, a landscape fact on a class named small_molecule, so the default is competitor, with competitor_label="cca_competitor" one builder argument away. Every classification rule is unchanged.

Benchmark evidence

ADR-0006 Decision 4 makes the cross-deck registry the merge gate. Both decks were replayed on copy DBs (never the production ogur.db), on this branch and on origin/main, with the identical scorer command:

Deck origin/main this branch
cardiometabolic-rnai-001 16/32 assets, 11/12 deals 16/32 assets, 11/12 deals
obesity-rnai-001 12/14 core assets, 11/11 companies, GATE PASS 12/14, 11/11, GATE PASS

The per-asset (545-line) and per-deal (13-line) coverage dumps are byte-identical between arms, and RNAI.config_digest() is 46776eaaf6e3 on both. The shared matcher, the RNAi adapter and both scorers are untouched (git diff --quiet origin/main), so the RNAi path is unchanged by construction as well as by measurement.