Coarse HPO Bindings Must Say Why
Status: enacted, 2026-09-05. This is the design record; the reference
documentation is docs/coarse-phenotype-bindings.md
and the decision is registered as §4b of the
decision register. Read this file for
why, that one for how.
Two things changed between this proposal and what was built, recorded here rather than edited away:
spectrum_termsdoes not exist. Part 2b below proposes it as the companion ofSPECTRUM_SUMMARY, and calls it "the recommended shape". It was built, then removed on review, and the value was renamedVARIABLE_SPECTRUMto stop the name implying enumerability. Two reasons. It inverts the meaning of the value — a spectrum is the case where the findings cannot be pinned down, so requiring a list demands what is unavailable; the maintainer's original framing was "really impossible to just pin down particular specific terms". And the worked example proved the opposite case is not a spectrum at all: where the source names findings and you have a quote, they are ordinaryphenotypesentries, and the slot's version of them was invisible to the phenotype table, facets, exports and entity references.VARIABLE_SPECTRUMis now a bare declaration likeSOURCE_UNSPECIFIED.- The
PATHOGRAPH_HUBrule inverted. Part 3 below requires a hub to have outgoingsequelaeinto its specific findings. That is wrong.sequelaeis aCausalEdge, and a coloboma is not caused by an eye abnormality — it is one, so the requirement would have had curators drawing an is-a hierarchy as a causal chain to satisfy a guard. As built, a hub is defined by its incoming edges (something in the entry must target it) plus the absence of afrequency.
Numbers below are the 2026-09-05 census taken before enactment: 168 unexplained
bindings, of which four became the worked examples, leaving the 164 now
grandfathered in tests/coarse_phenotype_baseline.txt. Regenerate with
just list-coarse-phenotypes rather than trusting them.
The problem, precisely
A phenotype like this validates, renders, exports, and derives the correct facet — and says almost nothing:
- category: Ophthalmologic
name: Eye Abnormalities
description: >-
Ocular findings are common in SYS and take the form of strabismus,
esotropia, or myopia.
phenotype_term:
preferred_term: Eye abnormality
term:
id: HP:0000478
label: Abnormality of the eye
frequency: FREQUENT
(Schaaf-Yang_Syndrome.) The description names three HPO-coded findings
(HP:0000486 Strabismus, HP:0000565 Esotropia, HP:0000545 Myopia). The
binding discards all three. Every tool downstream sees "eye".
The dismech-terms skill already says "choose the most specific term that
accurately represents the curated claim" and the term contract lets
preferred_term carry specificity the ontology lacks. Neither is checkable, so
neither is enforced, and the top-level bindings accumulate. The point of this
proposal is to make the legitimate reasons for a coarse binding explicit
and machine-readable, so that the unexplained coarse binding — the one that
means "didn't look" — becomes the thing a gate can see.
What the KB does today
Direct children of HP:0000118 bound as phenotype_term.term in
kb/disorders/ (the 23 terms in HPO_TOP_LEVEL_CATEGORIES /
PhenotypeCategoryEnum):
| Term | Label | Uses |
|---|---|---|
HP:0002664 |
Neoplasm | 47 |
HP:0000478 |
Abnormality of the eye | 36 |
HP:0000119 |
Abnormality of the genitourinary system | 19 |
HP:0033127 |
Abnormality of the musculoskeletal system | 12 |
HP:0000707 |
Abnormality of the nervous system | 9 |
HP:0001626 |
Abnormality of the cardiovascular system | 9 |
HP:0040064 |
Abnormality of limbs | 5 |
HP:0002086 |
Abnormality of the respiratory system | 5 |
HP:0001871, HP:0000598, HP:0000818, HP:0001939, HP:0025031 |
Blood / Ear / Endocrine / Metabolism / Digestive | 4 each |
HP:0001197 |
Abnormality of prenatal development or birth | 3 |
HP:0002715, HP:0001608, HP:0001507 |
Immune / Voice / Growth | 1 each |
| the other 5 | Breast, Head and neck, Integument, Thoracic cavity, Cellular, Constitutional | 0 |
About 190 bindings across roughly 130 files. Reading the eye cases, they sort cleanly into the three reasons plus the default:
- Spectrum summary (
Schaaf-Yang_Syndrome,Rubinstein-Taybi_Syndrome,FG_Syndrome_1, most of them): the description lists several specific findings; the coarse term stands in for a variable set. - Source says no more (
PAICS_Deficiency: "The specific ocular finding is not characterized in the available abstract, so the binding is deliberately at the general level.";Adams-Oliver_SyndromeDOCK6 subtype). The prose idiom already exists —PUS3-Related_Neurodevelopmental_Disordercarries a full paragraph arguing thatHP:0001627is "the right binding rather than a mere fallback parent" — but it lives innotes:anddescription:where nothing can read it. - Narrower concept, no HPO term: rare at this tier, because HPO's eye branch is deep. It is the common case one tier down and in newer disorders (a named dysmorphic gestalt, an EEG pattern, a retinal OCT appearance).
- Nothing recorded — the majority. Indistinguishable from the above.
The one thing the census cannot tell apart is what the proposal fixes.
Why not measure specificity
Three approaches are explicitly rejected, so that nobody re-proposes them:
- Information content / term depth. Depth is a property of how HPO
happens to be built, not of the claim.
HP:0004322Short stature is the most-used HP term in the KB (290 bindings) and sits only a few steps belowHP:0001507; it is exactly as specific as the literature ever gets. An IC threshold would penalise it and rewardHP:0008857"Neonatal short-trunk short stature" whether or not the paper said that. - Rewarding specificity in compliance scoring.
qc_pluginsscores coverage, not grain. A score gradient towards narrower terms is precisely the pressure that manufactures bindings the source does not support, which the term contract ("do not manufacture a narrower ontology match") forbids. - Category-gated rules. Design register §10 already records why category = X ⇒ term under X is circular: the category is derived from the term. The same holds here — nothing about the derived facet can tell you whether the coarse binding was deliberate.
What is left is a closed list: a small, reviewed set of terms that name an organ system and nothing else, declared in the schema, where binding one of them obliges the curator to say which of the legitimate reasons applies.
Part 1: the subset
1a. Which terms are "coarse"
Tier 0 (recommended starting point): the 23 direct children of HP:0000118.
These already exist as PhenotypeCategoryEnum in
schema/classifications/phenotype_category.yaml, each with a meaning: CURIE,
and as HPO_TOP_LEVEL_CATEGORIES in browser_export.py driving the
Phenotype Systems facet. Zero new vocabulary, one source of truth, and
just validate-terms-schema already verifies the meaning: labels. A term in
this set names a facet bucket; it cannot name a finding.
Tier 1 (defer; enumerate by hand if ever): HPO's organisational second
tier — the morphology/physiology split terms (HP:0012372 Abnormal eye
morphology, HP:0012373 Abnormal eye physiology, HP:0012638/HP:0012639
for the nervous system, HP:0011842/HP:0011843 for the musculoskeletal system) and
the system-part umbrellas (HP:0000924 Abnormality of the skeletal system,
18 uses; HP:0000079 Abnormality of the urinary system; HP:0000951
Abnormality of the skin; HP:0000271 Abnormality of the face). These also
name no clinical concept. But the tier is not uniform, which is why it must be
a curated list and never a depth rule:
| Tier-1-looking term | Uses | Verdict |
|---|---|---|
HP:0001627 Abnormal heart morphology |
150 | Not coarse. Carries "Congenital heart defect" as an EXACT synonym; it is the clinical concept when a paper says "CHD" (see the PUS3 note). |
HP:0001999 Abnormal facial shape |
178 | Not coarse. "Dysmorphic facies" is a real summary finding in dysmorphology. |
HP:0012443 Abnormal brain morphology |
28 | Borderline; the neuroimaging equivalent of the above. |
HP:0000924 Abnormality of the skeletal system |
18 | Coarse. |
The subset lives in the schema, is meaning:-bound, and is edited by PR with
a reason. That is the whole specificity model: membership in a list somebody
argued for.
1b. Where the subset is declared
Options, in order of preference:
- Reuse
PhenotypeCategoryEnumdirectly. The gate reads itsmeaning:values. Tier 1, if ever adopted, becomes a second enum (CoarsePhenotypeTermEnum) that is a superset, so tier 0 stays the facet vocabulary untouched. Advantage: the facet subset and the "coarse" subset are the same object until someone deliberately makes them differ. - A dedicated enum from the start,
CoarsePhenotypeTermEnum, whose tier-0 values are copied fromPhenotypeCategoryEnumand kept in sync by a test. Cleaner separation of concerns; one more thing to drift. - An HPO subset. HPO ships slim subsets (
hposlim_coreand the LOINC/ ORDO-related ones), but none is "organisational terms only", the OLS adapter does not surface subset membership through the cache layer, and an externally-maintained list would change under us. Rejected for now; noted because GO'sgoslim_*subsets are the natural analogue if this extends tobiological_processeslater.
Part 2: saying why — one slot, four values
2a. The slot
Add one optional enum slot to PhenotypeDescriptor (so it sits next to the
term: it qualifies, like modifier and severity do):
phenotype_term:
preferred_term: Eye abnormality
term:
id: HP:0000478
label: Abnormality of the eye
coarse_binding_basis: SPECTRUM_SUMMARY # or SOURCE_UNSPECIFIED / NO_HPO_TERM / PATHOGRAPH_HUB
Name candidates: coarse_binding_basis, term_granularity_basis,
binding_rationale. The name should make clear it is why the term is coarse,
not a grade of the term. Slot on Phenotype rather than the descriptor is the
alternative; the descriptor wins because ImagingFinding.phenotype_term and
ClinicalTrial.target_phenotypes reuse the same descriptor and inherit the
slot for free.
2b. The values
SPECTRUM_SUMMARY — case (a), pleiotropy and variability. The disease
produces many distinct findings in this system with variable expressivity,
and one summary phenotype is the honest grain. This is the value that needs
the most design, because "generalized" on its own is still lazy if the
spectrum is only in prose. Options, from cheapest to most structured:
- Prose only. The
descriptionmust name the findings. Not checkable. spectrum_terms:— a multivaluedPhenotypeDescriptorlist on the summary phenotype carrying the constituent HP terms with no frequency and no per-term evidence required. The evidence sits on the summary phenotype, as it does today. This keeps HPO specificity without demanding a fully-curated phenotype per finding, which is exactly the cost the curator was avoiding:
- name: Eye Abnormalities
phenotype_term:
preferred_term: Eye abnormality
term: {id: HP:0000478, label: Abnormality of the eye}
coarse_binding_basis: SPECTRUM_SUMMARY
spectrum_terms:
- preferred_term: strabismus
term: {id: HP:0000486, label: Strabismus}
- preferred_term: esotropia
term: {id: HP:0000565, label: Esotropia}
- preferred_term: myopia
term: {id: HP:0000545, label: Myopia}
frequency: FREQUENT
evidence: [...]
The gate can then require len(spectrum_terms) >= 2 for this value, and
term validation covers the constituent CURIEs because they bind the same
PhenotypeTerm enum. This is the recommended shape. It is also the shape
HPOA/OMIM cannot express, which is a small argument for dismech carrying it.
3. Bind HPO's own variability modifiers. HP:0003812 Phenotypic
variability and HP:0003828 Variable expressivity exist, and HPO's
clinical-modifier branch has HP:0012837 Generalized. But those are
modifiers of the disease or of spatial extent, not of a binding's
grain, and qualifiers is deprecated. Not recommended as the primary
mechanism; could ride along in spectrum_terms if a reader wants the HPO
term for "this varies".
SOURCE_UNSPECIFIED — case (b), it genuinely cannot be narrowed. The
cited source says "ocular involvement" and nothing more, or the finding is
heterogeneous at the individual level with no named lesion. The proof is the
evidence snippet, which already exists, so this value needs no companion
slot — it is a declaration that the curator looked and the source stopped
here. It is the value PAICS_Deficiency and the PUS3 paragraph are
reaching for in prose. Distinct from SPECTRUM_SUMMARY: there the specifics
are known and deliberately summarised; here they are unknown.
NO_HPO_TERM — case (c), a narrower concept HPO lacks. The description or
preferred_term names something more specific than any HP term, and the coarse
parent is the best available anchor. The term contract already licenses
preferred_term to be narrower than term.label; this value makes that
explicit and adds a place to record the gap:
phenotype_term:
preferred_term: peripapillary retinal pigment mottling
term: {id: HP:0000479, label: Abnormal retinal morphology} # tier-1 example
coarse_binding_basis: NO_HPO_TERM
term_gap: https://github.com/obophenotype/human-phenotype-ontology/issues/NNNN
term_gap (optional string, URL or free text) is the same idea as the
"searched, found nothing" record the environmental-evidence waiver requires:
it turns a permanent gap into a lead for an HPO term request. A gate rule
that suggests itself: for this value preferred_term must differ from
term.label — otherwise nothing narrower was claimed and the value is wrong.
PATHOGRAPH_HUB — the fourth case, deliberately unqualified. See Part 3.
2c. Cheaper alternatives, kept for the record
- A
review_notes:sentinel on the lines ofLeft deliberately uncited.(check-environmental-evidence): a phenotype whosereview_notesbeginsBound deliberately coarse.followed by ≥20 words is dispositioned. Works today with no schema change, and could be the migration bridge for the 190 existing bindings. It is prose, though, and the user's ask was for something explicit and queryable; the sentinel should be a stopgap at most. - Derive it.
preferred_term != term.label⇒ case (c); ≥2 HP-coded concepts recognisable indescription⇒ case (a). The first half is a reasonable lint hint; the second is NER over prose and is not. - Three separate slots (
spectrum_terms,source_limited: true,term_gap) with no enum. Loses the one thing the enum gives: a single place for the gate to look, and a single "none of these" state to flag.
Part 3: coarse terms in the pathograph, unqualified on purpose
The pathograph is where a coarse HP term is sometimes the right node, and where the current KB uses it wrong.
3a. What the KB does now: coarse terms as leaves
Rubinstein-Taybi_Syndrome has a pathophysiology node whose downstream
fans out to eleven targets with causal_link_type: INDIRECT_UNKNOWN_INTERMEDIATES,
two of which are Ocular abnormalities and Renal abnormalities —
phenotypes bound to HP:0000478 and HP:0000119 with no sequelae of their
own. Donnai-Barrow_syndrome and Basel-Vanagaite-Smirin-Yosef_Syndrome do
the same. No coarse-bound phenotype in the KB has outgoing sequelae.
So every coarse term in every pathograph today is a terminal node: the graph
ends at "eye", which is the graph-level version of the same laziness.
3b. The hub pattern
A coarse term earns its place in the pathograph when it is a convergence
point: one mechanism produces system-level disruption, which then resolves
into several specific findings. Model it as a phenotype that is a source of
sequelae, not a sink:
pathophysiology:
- name: Disrupted neural crest migration
downstream:
- target: Ocular developmental abnormality # the hub
causal_link_type: INDIRECT_UNKNOWN_INTERMEDIATES
phenotypes:
- name: Ocular developmental abnormality
phenotype_term:
preferred_term: ocular developmental abnormality
term: {id: HP:0000478, label: Abnormality of the eye}
coarse_binding_basis: PATHOGRAPH_HUB
sequelae: # bare names, per the pathograph rule
- target: Coloboma
- target: Microphthalmia
- target: Cataract
- name: Coloboma
phenotype_term: {term: {id: HP:0000589, label: Coloboma}}
frequency: FREQUENT
evidence: [...]
What "unqualified" means for a hub, and what the gate should require:
- It carries no clinical claim of its own. No
frequency(the children have theirs), andevidenceis optional — the edges carry the evidence. A hub with a frequency is aSPECTRUM_SUMMARY, not a hub. - It must have ≥1 outgoing
sequelaeto a non-coarse phenotype, and ideally every child should roll up to the hub's facet category — checkable offline againstapp/hpo_category_cache.json, which already maps every bound HP term to its top-level categories. - It is not a
SPECTRUM_SUMMARYwith edges. The difference is what the node asserts: a summary asserts "patients have eye findings, at this frequency, of these kinds"; a hub asserts "this mechanism's effect on the eye branches here". One entry may reasonably have both, and the hub should not be the one the disorder page's phenotype table shows with a frequency.
3c. Why this is not the "disruption of eye development" node
That node is pathophysiology, binds GO (GO:0001654 eye development with
modifier: DECREASED or ABNORMAL), and asserts a process. The hub binds
HP and asserts a system-level outcome. They can sit in sequence —
process → hub → findings — and they should not be merged. Two consequences:
- Do not add an HP slot to
Pathophysiologyto make room for hubs. The HP/GO split by section is a load-bearing convention (design register §4, §10) and the hub is a phenotype. - Watch the flat node namespace: a hub named
Abnormality of the eyeand a pathophysiology node with the same name collapse into one node (#9896). Hub names should be phrased as outcomes ("Ocular developmental abnormality"), not as processes.
3d. Rendering
A hub should look different from a finding, or readers will take it for one.
Cheap options: dashed outline plus the facet colour; label suffix "(system)";
or collapse hubs into the existing category grouping the renderer already
computes from HPO ancestry. The graph model does not need to change — the
node type is still phenotype, and coarse_binding_basis is one more
attribute graph.py already knows how to pass through for severity.
Neoplasm (HP:0002664, 47 uses, mostly cancer-predisposition syndromes) is
the best first candidate for hub conversion: the specific tumour types are
almost always already curated as sibling phenotypes, so the hub adds edges
rather than nodes.
Part 4: the gate
just check-coarse-phenotypes — offline, whole-KB, in just qc, ungated by
changed-path filtering (for the reason check-entity-refs is: a kb/-only PR
skips pytest). Rule:
A
phenotypes[]entry (or anyPhenotypeDescriptor) whoseterm.idis in the coarse subset must carrycoarse_binding_basis; the value's companion requirement must hold (SPECTRUM_SUMMARY⇒ ≥2spectrum_terms;NO_HPO_TERM⇒preferred_term ≠ label;PATHOGRAPH_HUB⇒ ≥1sequelae, nofrequency).SOURCE_UNSPECIFIEDhas no companion requirement.
The ~190 existing bindings go into tests/coarse_phenotype_baseline.txt,
which may only shrink, exactly like causal_target_baseline.txt. New coarse
bindings without a basis fail. Nothing scores anything; a coarse binding with
a basis is fully compliant, which is the point — the rule rewards saying why,
not narrowing.
Advisory only, not gated: a NO_HPO_TERM whose preferred_term has an exact
HP synonym (cache-first label match) is probably a missed term, and a
SOURCE_UNSPECIFIED whose own description names a finding with an HP label
is probably a SPECTRUM_SUMMARY.
What this deliberately does not do
- Touch
biological_processes/ GO. The same design applies (a closedCoarseBiologicalProcessTermEnum; GO's own slims as a lead), and nothing here is HP-specific except the subset. Out of scope now. - Extend to tier 1 before tier 0 has been lived with.
- Change
phenotypes.category. The open register item about wiringPhenotypeCategoryEnumto that slot is independent; this proposal only reads the enum'smeaning:values. - Bulk-migrate. The 190 rows are a worklist for the sentinel bridge or the slot, one entry at a time, by someone who can read the evidence.
Open questions for the maintainer
- Tier 0 only, or seed tier 1 with the handful of clear cases
(
HP:0000924,HP:0012372,HP:0012373,HP:0012638)? - Does
SPECTRUM_SUMMARYget the structuredspectrum_termslist, or is prose acceptable for a first version? - Slot on
PhenotypeDescriptor(inherited by imaging and trial phenotypes) or onPhenotypeonly? - Should a
PATHOGRAPH_HUBbe allowedevidenceat all, or is the edge evidence the only place it belongs? - Is
Neoplasma coarse term for this purpose, or is the neoplasm branch different enough (it is a disease-like-phenotype family, §4) to exempt?
Appendix: regenerating the census
# top-level bindings per term
for id in $(grep -o '"HP:[0-9]*"' src/dismech/export/browser_export.py | tr -d '"'); do
printf '%s %s\n' "$id" "$(grep -rh "id: $id\b" kb/disorders | wc -l)"
done
# every coarse-bound phenotype with its name/description/notes
grep -rn -B12 -A3 "id: HP:0000478" kb/disorders | grep -E "name:|description:|notes:|frequency:"
# do any coarse-bound phenotypes have sequelae? (none, as of 2026-09-05)
grep -rl "id: HP:0000478" kb/disorders | xargs -I{} python3 -c '
import sys,yaml; d=yaml.safe_load(open("{}"))
for p in d.get("phenotypes") or []:
t=((p.get("phenotype_term") or {}).get("term") or {}).get("id")
if t=="HP:0000478" and p.get("sequelae"): print("{}", p["name"])'