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:
- bare class terms (
"lysosomotropic") — productive against SEC annual reports and broad literature. The RNAi planner deliberately never emits a bare modality becausesiRNAalone floods; a mechanism term does not. - class term × indication — the CT.gov / OpenAlex workhorse.
- 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_TARGET ≡ plan_round1_modality_x_target,
QUERY_STYLE_MODALITY_INDICATION ≡ plan_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_entitystrips 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_namefails 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 substringbiofires 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 corroboration —
target_pair_in_blobneeds 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
METbinding insidetrametinib, 990 of 1230 rows phantom. Ported from the run's own fix. cross_filingwithdrawn — 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 readsFalserather thanKeyError.
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 inRNAI.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 tosmall_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-001row declares["small molecule", "small-molecule", "autophagy inhibitor", "lysosomotropic"]and still resolves —_OUT_OF_SCOPE_TOKENSis 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.