Skip to content

The antibody landscape class

The second registered LandscapeClass adapter, after rnai. Its reference landscape is polygon-cd8-ami-001 — Polygon Therapeutics' PLG-101, a depleting anti-CD8 monoclonal antibody limiting cytotoxic-T-cell-driven myocardial damage in acute-MI ischemia-reperfusion.

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/antibody.py Matcher: ogur/engine/discovery_modality/matching_antibody.py Tests: tests/unit/engine/discovery_modality/test_antibody_class.py, tests/unit/engine/discovery_modality/test_matching_antibody.py

Why this class exists

base.py's own docstring named it in advance: "the antibody class adds a target-biology axis and coherence guards RNAi has no analogue for." Both turned out to be understatements — the run also inverted which diseases are on-thesis, and exposed a patent-retrieval gap no vocabulary change could close.

1. Oncology and immunology are the flood, not the field. For the RNAi decks a carcinoma mention is a decisive off-thesis disqualifier and cardiac disease is the field. That much carries over. What does not is the shape of the noise: a cold CD8/CD3 patent net is dominated by oncology — Her2×CD3 bispecifics, tumour-Treg depleters, anti-IL6 for cancer — because those are the patent classes CD8 lives in. Those entities carry real antibody + T-cell-target signal, so they cannot be dropped by modality or by target; only a decisive off-disease rule reaches them.

2. Non-cardiac autoimmune is deliberately NOT off-thesis. A T-cell-depleting antibody in myositis (Abcuro / anti-KLRG1) shares the PLG-101 mechanism and is a lead worth surfacing, so autoimmune reads unknown and survives. The off-vocab is metabolic + oncology only.

3. Checkpoint-inhibitor myocarditis needs an override, not an exception. It is a genuine adjacency (a cardiac toxicity of checkpoint blockade) that co-occurs with oncology terms by definition. The adjacency tier is therefore checked before the off gate — the reverse of the RNAi module's order, because this landscape's value is largely in its adjacencies.

The behavioural seams

Round-1 planning: modality × target — shared with RNAi, and that is the point

plan_round1_modality_x_target, unchanged. An antibody's antigen is named in the document ("anti-CD8 monoclonal antibody"), so a gene-anchored query is productive, exactly as it is for an oligo. This is worth stating because the sibling small-molecule class had to pivot the axis: GNS561 measured autophagy inhibitor PPT1 at zero CT.gov studies. Two classes sharing a planner while a third does not is evidence the seam varies on the dimension it claims to, rather than being a per-class copy.

What is not shared is the spelling. See the surface-form axis below.

Scope-fit: six axes, three orthogonal gates, one coherence guard

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

Axis Values
modality antibody / biologic / small_molecule / oligonucleotide / other
target_fit cd8_tcell / adjacent_immune / off / unknown
indication_fit in-thesis / adjacent / off / unknown
mechanism depleting / blocking / unknowninformational, never gates
provenance on-deck / novel
label core / honorable-mention / off-thesis

mechanism is surfaced so an analyst can tell a depleting anti-CD8 (a direct PLG-101 bet) from a blocking one, but it does not gate core: a trial or patent snippet frequently leaves it unstated, and requiring it would silently drop real competitors. Recall over precision on an informational field.

Three gates carry most of the precision:

The decisive off-disease rule (Pass 2). Round 1 first classified all 175 bound companies as honorable-mention and zero as off-thesis — a false-clean 0.0% off-share that makes the drift guard useless. Making a positive off-disease read decisive took the split to 1 core / 80 HM / 36 off, i.e. 30.8% honest. The classifier did not add off-thesis entities; it correctly labelled a pre-existing oncology tail.

Mechanism coherence (Passes 4 and 7). A big-pharma with a CD8-oncology patent, an antibody patent and a separate cardiac trial reads antibody + CD8 + in-thesis over the union of its snippets while no single record makes that claim. Pass 4 required antibody + CD8 in one hit and cut core from 4 to 1 (Polygon only). Pass 7 found that still insufficient — round-2 trial enrichment flipped a big-pharma's entity-level indication to in-thesis and re-admitted it (BMS gavo-cel, Roche Schnitzler's) — so the in-thesis indication must come from the same hit.

The indication must be evidenced by the record, and finding the right source for that took three corrections across three review rounds:

  1. Not the query. It read linked_indications, which _linkage derives from the concept query string concatenated with the record text — so an acute-MI-scoped query conferred the disease on every patent it surfaced. An anti-CD8 patent whose controlled text said nothing cardiac was core because of what we searched for. That is the pipeline citing its own search terms.
  2. Not the display snippet. The replacement read support_text, which is _support() = (summary or title)[:300] — summary preferred, then truncated. A patent titled "depleting anti-CD8 monoclonal antibody for acute myocardial infarction" with a short non-cardiac summary lost its title entirely.
  3. Not literal scope entries. record_targets / record_indications fixed the truncation but match only exact scope strings, so anti-CD8α did not evidence CD8A and STEMI did not evidence acute myocardial infarction.

The evidence is now record_evidence — the class's own vocabulary, applied to the full record at projection time. The seed does not need query-linkage either, since SeedEntity.support_text states acute MI explicitly.

Indication tiers stay separate (Codex, PR #211). ScopeSpec.indications flattens the acute-MI core window together with the deck's adjacencies (heart failure, atherosclerosis, stroke, myocarditis) so that a hit linking any of them is harvested. It is not a claim that they are the same tier, but both _indication_fit and _core_coherent tested membership in that combined list, which promoted every structured adjacency to in-thesis — a depleting anti-CD8 antibody linked only to heart failure came out core. Each linked value is now classified against the explicit tier vocabularies by _indication_tier, so an adjacency-only linkage enters at the adjacent tier and cannot reach the core window no matter what the scope flattens together.

The competitor gate (Pass 9). An entity with neither an antibody/biologic modality nor an immune target — even one with a cardiac indication — is a clinical-evidence programme, not an antibody competitor: small-molecule cardioprotection (Kai / KAI-9803), thrombectomy devices (Medtronic), stroke-imaging vendors, PI and funder rosters. Labelling them off-thesis took the honorable-mention tier to 262/262 competitor-shaped. Note this relabels evidence rather than dropping it — the off-share rose, which is the honest direction.

And one source rule (Pass 14). A CDE-sourced entity is a biologic by construction: the NMPA harvest queries the 生物制品 bucket. Its Chinese trial text rarely writes 抗体, so it read modality=other and the competitor gate dropped every China-domestic cardiac biologic. The promotion applies to other only, so it can never launder an off-platform asset into the landscape.

The three contract gaps

1. Mechanism-neighbourhood target expansion — already in the interface

Open Targets' disease→target axis answers "what has historically been drugged in this disease", which diverges hard from "the mechanism this asset acts through" for a novel-MoA antibody in an old disease. Probed live for the Polygon indications it returned 120 targets, ~110 of them cardiovascular pharmacology (PCSK9, LDLR, HMGCR, ACE, AGTR1, the adrenergic receptors), and missed the entire neighbourhood the thesis is about:

CD8A absent · CD8B absent · CD3E absent · GZMB absent · PRF1 absent
NLRP3 absent · TNF absent · CCR2 absent · C5 absent      (only IL6 survived)

TargetExpansionSpec.mode = "mechanism_neighbourhood" and its dispatch in target_expansion.expand_targets_for_class were already on main — this class is their first registered consumer, and supplies the ~32-gene payload (the textbook cytotoxic-T-lymphocyte effector programme plus the residual-inflammatory-risk cytokine axis). Standard immunology, deck-agnostic, so it cannot leak an answer key; and it is not a network call, so it cannot fail open on an OT outage.

TargetExpansionSpec refuses to construct in this mode with an empty gene set — expanding to nothing would report a configuration gap as an honest empty landscape.

Provenance travels with the mode (Codex, PR #211). The run's frozen discovered_targets_20260710.csv recorded all 23 expanded genes as opentargets-expanded. They never came from Open Targets — that is the whole point of this mode — so the label claimed an external, checkable derivation for a hand-supplied set, the worst direction for a provenance error to point: an analyst auditing the list would have gone looking for a query that was never made. target_expansion.expansion_provenance now derives the label from spec.mode (mechanism-expanded here, opentargets-expanded for RNAi), so a writer cannot stamp a constant that drifts from how the genes were actually obtained. The frozen artifact is corrected and a test asserts it stays that way.

Record evidence is a class-supplied judgement (LandscapeClass.record_evidence). Three sources of evidence were tried and each was wrong in its own direction: support_text is a display snippet — (summary or title)[:300], summary preferred — so a title-borne fact is invisible and a title/summary split can hide an off-disease title while keeping its endpoint list; linked_* mixes in the concept query, so an acute-MI search confers acute MI on everything it returns; and record_* excludes the query but matches only literal scope entries, so anti-CD8α did not evidence CD8A and STEMI did not evidence acute myocardial infarction. The class now judges the full record with its own vocabulary once, at projection, into a closed flag set (base.VALID_RECORD_EVIDENCE), and the classifier reads that. A class declaring no record_evidence (RNAi) produces no flags and every consumer falls back to its prior text scan.

Target roles are three-valued, not a binary. intracellular-marker / secreted / cell-surface. It was marker-vs-surface until the negative branch started being read as a positive claim: IL-1β, IL-6, TNF and C5 are secreted, and calling them cell-surface in a customer-facing table asserted biology nobody had checked. Only intracellular markers are held out of the fan-out — a secreted target is antibody-tractable (canakinumab binds IL-1β).

Intracellular markers are role-tagged, not fanned out. GZMB, GZMK, GZMA, PRF1, TBX21, EOMES, NFKB1, NLRP3 and CASP1 are granule cargo, transcription factors and cytosolic inflammasome components — real markers of the mechanism, none bindable by an antibody in vivo. A per-target patent or trial fan-out over them cannot return a competitor programme, and the hits it does return are mechanism papers being read as rival assets. TargetExpansionSpec.intracellular_markers names them and target_expansion.fanout_targets drops them from the fan-out list only. They stay in the expansion, and so in the scope and in per-hit linkage, so a record naming granzyme B still links the target. The tag is modality-specific rather than a fact about the genes: an siRNA can reach an intracellular target, which is why RNAi names none and the filter is a no-op there. The soluble/surface half of the same axis is deliberately untagged: IL-1β is the secreted product of that inflammasome and the canakinumab target, so marking it would delete the landscape's closest real comparator.

2. Seeding a known subject — on the run context, never the class

The run injected Polygon itself. That is legitimate, and it is not a shortcut: Polygon's 2026 anti-CD8α patents were indexed only by Lens's current corpus, and after that key started returning 401 they were absent from both the gpatents BigQuery snapshot and EPO OPS. The subject of the brief could no longer be re-found.

SeedEntity therefore lives on DiscoveryRunContext, beside landscape_id. A class is reusable across landscapes by construction; a brief's subject belongs to exactly one run, so putting it on ANTIBODY would inject Polygon into every future antibody landscape — Decision 1's failure mode.

Three properties make the seed honest rather than a report-layer fiction:

  • it carries its provenance. source="seed" reaches the source-attribution table like any other hit, and the support text states the corpus gap. The uniqueness claim then rests on the absence of any competitor, never on re-finding the subject.
  • it survives its own class's gates. The seeded entity classifies core through the normal cascade — it is not exempted from anything.
  • it is materialised fresh per call. DiscoveredEntity is mutable and merged in place during the loop, so a stored instance on an immutable context would carry one round's merge into the next run.

3. The patent surface-form axis — new

The by-target scan searches HGNC symbols. A patent does not write them:

seed patent title:  "Novel anti-CD8α antibodies and antigen-binding fragments"
symbol scanned:     CD8A                       →  no match

The gap is structural, not vocabulary: no quantity of extra symbols reaches a prose form. So it gets its own pass. LandscapeClass.patent_surface_forms declares the phrases, concept_queries.plan_patent_surface_form_scan(klass) returns them as a fan-out target list, and sources.harvest_patents_by_surface_form runs the scan with the run's own scope — so everything but the scanned terms is carried through unchanged, most importantly indications, and a surface-form hit still links the thesis's diseases and can reach the classifier's in-thesis test.

The planner returned a whole ScopeSpec with targets swapped, until Codex's review of PR #211 showed that shape could never execute: every public harvest boundary runs _enter_resolved_scope, which rejects a scope= diverging from ctx.scope because the cache namespace and source capabilities derive from the context. Handing the harvester the documented output raised ValueError, and nothing outside the tests called it, so the axis never ran. The harvester's targets= override — applied after the guard, precisely because it is a per-axis fan-out list rather than a different thesis — was the correct seam all along.

It is nearly free: a batched patent backend scans the publications table once per query at a cost that is term-count-independent, so a second scan carrying twenty phrases costs one scan. On the run it took patent-companies from 92 to 132 (Morphosys, Keymed, Capstan, Gyala, the China CD8 biotechs) — which is where "Google Patents covers the patents" was actually earned. On a sequential backend it would be twenty more rate-limited searches, which is the Pass-3 failure the by-target gate already handles for the primary scan.

() means the class declares no such axis and the scan is not planned — the function returns None, distinct from an empty scan that would read as "the patents were searched and there was nothing there".

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 shared normalizer is imported, and test_matching_antibody.py asserts the reuse by object identity, so a copy cannot pass review.

Four judgements differ:

# Difference Because
1 Single-antigen corroboration target_pair_in_blob needs two targets (the deck is dual-targeting); an antibody has one antigen, so that gate can never fire and every path-1 bind would fall through. Replaced by code-in-text or company and target.
2 Word-boundary target matching Antibody symbols are the shortest in the tree. C5 is two characters and sits inside C5AR1; TNF inside TNFRSF1A. The same phantom was measured on the GNS561 brief (MET inside trametinib, 990 of 1230 rows) — an antibody scope makes it strictly worse.
3 Antigen surface forms The binding half of gap 3: CD8A must bind through anti-CD8α, CD8 alpha, anti-CD8. Derived mechanically from the symbol's own shape (terminal Greek-chain letter, interleukin/chemokine hyphenation, the anti- prefix), never curated — a curated antigen list would carry landscape knowledge into a matcher.
4 cross_filing disabled With no deck, every discovered pair is a hypothesis, so pairing party A from one 8-K with party B from another lets the pipeline both propose and confirm a deal nobody claimed — the PR #118 phantom pairing, minus the curator.

Differences 1, 2 and 4 are the same three the small-molecule sibling makes, for the same reasons. Difference 3 is unique to this class.

Note the two surface-form lists are not the same object, on purpose: patent_surface_forms on the class is a scan list and includes mechanism idiom that names no antigen ("depleting antibody"); the matcher's target_surface_forms is a bind derivation and must be attributable to a specific target. Keeping them separate is what stops a phrase-level hit being reported as target attribution.

Source policy, and the one reverted adaptation

per_sponsor_sources = () — the only class in the tree that declares no per-sponsor fan-out at all. This is a measurement, not an omission, and it is the one adaptation here that was reverted before it shipped:

round-1 round-2
core 4 0
honorable-mention 230 45
off-thesis 92 274
off-thesis share 28% 86%

The Pass-6 anchor guard fixed the acute failure — 274 of 282 round-2 off-thesis entities were sec, all thin metadata (FORM 10-K, PRESS RELEASE, EX-99.1), because per-sponsor SEC runs an unscoped full-text search for the sponsor name and returns filings by unrelated companies that merely mention it. Off-thesis went 282 → 1. But adversarial verification of the guarded run showed the 36 remaining additions were 0 antibody competitors: ~15 real cardiac-trial companies of the wrong modality and ~21 research-org / PI / funder leaks. Per-sponsor CT surfaces trial rosters, not competitors.

A separate five-agent assessment found the deal/China channels that give the axis its point for an RNAi deck are the wrong lever here — a Western, nascent anti-CD8 cardiac thesis has ~no China-China deals or HKEX filings, and all four are filing-exempt per-sponsor sources that would add to the flood.

So default_rounds = 1 and the axis is (). has_thesis_anchor is kept and exported — it is correct, and it is the prerequisite for any future modality-gated round 2.

per_code_sources survives, and the distinction is principled rather than a hedge: Pass 6's root cause was full-text search on a company name. An asset code has no such failure mode — an unrelated filer naming PLG-101 is a real signal about that asset. It is a declaration by analogy, not a measured axis, and default_rounds = 1 means it does not fire unless a caller asks for round 2.

Other policy:

  • cpc_prefixes = ("A61", "C07K") — C07K16 is immunoglobulins. C12N15 (the RNAi nucleic-acid class) returns nothing for an antibody estate.
  • by_target_fanout_cap = 20, against RNAi's 40 — the mechanism neighbourhood is ~32 genes, not OT-expanded hundreds, and Pass 3 measured 40 sequential Lens searches on a 10 req/min tier hanging a round past ten minutes for zero unique recall.
  • cde_policy.drugs_type = "3" (生物制品), not RNAi's chemical "2". Querying the wrong partition returns a different landscape's sponsors — the R5 failure CdePolicy exists to prevent. The feasibility assessment rated CDE low-value and it was deferred; that was wrong. Wiring it was the single most novel channel of the run, surfacing an active China-domestic cardiac biologic landscape no patent, CT.gov or SEC search reached (SGC001 in anterior STEMI at PCI — the exact PLG-101 window — plus CSL300/clazakizumab, RLX030/serelaxin, JK07).
  • patents_backend = None. The run was a hybrid — concept axis on EPO, the two batched scans on gpatents — which one string cannot express, and pinning the paid one would bill every registrant (~230 GiB ≈ $1.40 per live scan). It stays a run/landscape decision.

What this adapter does NOT carry

  • sources.configure_class_vocab(). The run carried its class vocabulary by mutating shared module globals. That is the pattern DiscoveryRunContext exists to eliminate: two concurrent runs would overwrite each other's gate and shift each other's cache fingerprints mid-flight. The seam is retired, not re-created, and a test asserts it has not reappeared.
  • assemble. The deck-free exploratory assembler stays eval-side, exactly as the ground-truth deck-scorer does for RNAi — the engine must not import scripts/.
  • The landscape's scope. Targets and indications live on the ScopeSpec / Landscape row.

Routing tokens

_INFERENCE_TOKENS["antibody"] = ("antibod", "monoclonal", "mab", "nanobody").

This narrows the placeholder _KNOWN_UNREGISTERED entry rather than copying it: "antibody" + "antibodies" collapse to the stem "antibod", which also subsumes the table's "bispecific antibody" and "antibody-drug conjugate"; "adc" is dropped as too generic to select a prompt and a matcher.

Two directions are checked, and a test enforces both structurally over the tables themselves:

  • No RNAi thesis is captured. ["siRNA","RNAi"], ["microRNA inhibitor"], ["antimiR"], ["GalNAc-conjugated siRNA"] all still resolve to rnai alone. No antibody token occurs inside an RNAi token or vice versa, so nothing that resolved before becomes an ambiguity refusal.
  • A mechanism word alone does not resolve here. ["PD-1 inhibitor"] is an antibody and still refuses, because it declares no antibody platform token. Inferring a class from a mechanism word is the RNAi-default corruption in a new costume; the correct answer is the analyst declaring the class.

Known limitations, named rather than hidden

  • Off-thesis share is ~31%, above the ~20% ceiling. That is the true composition of a cold CD3/CD8 patent + trial net, whose patent classes are oncology-dominated. Those entities are already correctly labelled; lowering the ratio means surfacing fewer of them, which needs query-side oncology exclusions that risk recall. Recorded as a lever, not applied.
  • research_org_extra_tokens is wired, but the field name still under-describes it. The Pass-5 international academic supplement and the Pass-11 non-biomedical denylist are read by LandscapeClass.is_excluded_org, which every company-admission path routes through (corpus derivation and entity merge). Single-word entries match a token prefix, never a raw substring — as a substring the cea token fires inside "Oceania". The Pass-11 tokens are not research organisations; they share the field because they share the action (drop this entity), which the field name under-describes. The general fix is a positive "is a drug developer" test on the OpenAlex axis.
  • OT cannot separate a mAb from a small molecule. OT phrases both MoAs with the same verbs ("Programmed cell death protein 1 inhibitor" is an antibody). OT's own drugType facet would, and is not queried. Downstream, the modality gate is what keeps the net on-platform. The small-molecule class records the mirror image of this gap.
  • Only ~3 entities in the run were multi-source. Low cross-source corroboration is expected without the deal channels; the multi-source few are the strongest leads.
  • Bare "igg" was narrowed to " igg" / "igg1" / "igg4". The run's harvest gate used the bare token, which is a substring of "trigger", "bigger" and "digger" — the run's own scope-fit module already guarded this with a leading space and the gate did not. This is the one place the adapter does not reproduce the run verbatim.