Coarse phenotype bindings
A phenotype bound to HP:0000478 Abnormality of the eye validates, renders,
exports, and lands in the right browser facet — while saying almost nothing.
Schaaf-Yang_Syndrome used to name strabismus, esotropia and myopia in its
description and then discard all three in the binding; they are now three
ordinary phenotypes, which is what this page will tell you to do.
Usually that is a curator who stopped early. But three legitimate reasons for a coarse binding exist, and the knowledge base already carried all three written as prose that nothing could read. This page describes the slot that records which one applies, and the guard that requires it.
The rule is not "prefer narrow terms". Manufacturing a specificity the source does not support is a worse defect than a coarse binding, and the ontology term contract forbids it outright. The rule is that a coarse binding must say why it is coarse, which leaves the unexplained one as the only thing that fails.
What counts as coarse
Two hand-reviewed schema enums, which the guard treats as one list of 56 terms.
Tier 0 — the organ-system roots. The 23 direct children of HP:0000118,
taken from the meaning: values of PhenotypeCategoryEnum in
src/dismech/schema/classifications/phenotype_category.yaml. That is the same
list the browser's Phenotype Systems facet is built from, so there is one
vocabulary rather than two. A term in that set names a facet bucket; it cannot
name a finding.
Tier 1 — buckets below the roots. CoarsePhenotypeTermEnum in
src/dismech/schema/classifications/coarse_phenotype_terms.yaml: 33 terms that
name a body system, a whole organ, or a gross body region and assert nothing
about what is wrong with it — HP:0000077 Abnormality of the kidney,
HP:0000924 Abnormality of the skeletal system, HP:0011024 Abnormality of
the gastrointestinal tract. The morphology/physiology split terms directly under
such a root come with it, since they divide the bucket without narrowing it.
Both enums are meaning:-bound, so just validate-terms-schema verifies every
label in them against HPO.
Why it is a list and not a rule
Tier 1 was curated in one pass over all 360 distinct Abnormal* HP terms bound
in the knowledge base. 33 are buckets; the rest are findings. The two most-used
of all are findings:
| Term | Uses | Coarse? |
|---|---|---|
HP:0001999 Abnormal facial shape |
177 | No. "Dysmorphic facies" is a real summary finding. |
HP:0001627 Abnormal heart morphology |
149 | No. It carries "Congenital heart defect" as an EXACT synonym, and is the concept when a paper says CHD. |
HP:0002500 Abnormal cerebral white matter morphology |
48 | No. What a radiologist reports off an MRI. |
HP:0012332 Abnormal autonomic nervous system physiology |
30 | No. Dysautonomia. |
HP:0000077 Abnormality of the kidney |
26 | Yes. |
HP:0000924 Abnormality of the skeletal system |
17 | Yes. |
HP:0000077 and HP:0001627 sit one step below the same kind of root. No rule
over depth, information content, or the shape of the label separates them —
every such rule flags the two most-used terms in the KB and pushes curators into
asserting lesions their sources never named. Membership in a reviewed list is
the whole model; adding a term is a pull request with an argument attached.
Four terms were left out as genuinely undecided rather than judged:
HP:0000504 Abnormality of vision, HP:0000925 Abnormality of the vertebral
column, HP:0002926 Abnormality of thyroid physiology, HP:0002270 Abnormality
of the autonomic nervous system. Each is arguably a bucket and arguably a
finding. The enum's own description records these and the excluded findings
above, so the next person inherits the reasoning rather than redoing it.
The slot
coarse_binding_basis lives on PhenotypeDescriptor, so it sits next to the
term: it qualifies and is inherited by imaging findings and trial targets:
phenotype_term:
preferred_term: Eye abnormality
term:
id: HP:0000478
label: Abnormality of the eye
coarse_binding_basis: VARIABLE_SPECTRUM
Two of the four values are bare declarations with nothing further to supply; the other two carry a requirement, checked wherever the value appears.
VARIABLE_SPECTRUM
Involvement of the system is real and recurrent, but its form varies between patients with no characteristic finding to bind — pleiotropy and variable expressivity, where picking terms to list would be arbitrary. The node's content is the organ-system involvement itself, usually with an aggregate frequency the source measured at that level.
- category: Ophthalmologic
name: Eye Abnormalities
phenotype_term:
preferred_term: Eye abnormality
term:
id: HP:0000478
label: Abnormality of the eye
coarse_binding_basis: VARIABLE_SPECTRUM
frequency: FREQUENT
evidence: [...]
It takes no companion slot. Stating the reason is the whole obligation, and that follows from what the value means: a spectrum is the case where the findings cannot be pinned down, so a rule requiring them to be listed would demand exactly what is unavailable.
If you can list the findings, they are not a spectrum — they are phenotypes.
Where the source names specific findings and you have a quote for them, curate
each as an ordinary phenotypes entry with its own term and evidence. That is
strictly better than recording them inside the coarse binding: they appear in the
phenotype table, count toward the browser facets, reach the exports, and can be
targeted by a phenotypes# entity reference or a pathograph edge.
Worked example: Schaaf-Yang_Syndrome. Its cited sentence reads "Eleven of 14
patients manifested eye abnormalities in the form of strabismus, esotropia, or
myopia." Strabismus, esotropia and myopia are each curated as their own
phenotype, citing that sentence and carrying no frequency, because the source
gives no per-finding counts. The coarse node keeps the one thing only it can say:
the 11/14 aggregate rate, and the fact that which form a patient has varies.
A slot named spectrum_terms, for listing constituents inside the binding, was
built and removed before this shipped. It produced second-class annotations no
downstream consumer could see, and it inverted the value's meaning. Do not
reintroduce it; test_the_schema_has_no_slot_for_listing_a_spectrum says so.
SOURCE_UNSPECIFIED
The cited source characterizes the finding no further, so a narrower term would assert something the evidence does not. No companion slot: the evidence snippet is the proof.
Worked example: PAICS_Deficiency, whose description already said "the specific
ocular finding is not characterized in the available abstract, so the binding is
deliberately at the general level".
This is the value for the case PUS3-Related_Neurodevelopmental_Disorder argues
at length in prose — a source saying only "congenital heart defect" with no named
lesion. Note that entry does not need the slot, because HP:0001627 is not in
the coarse set; it is the pattern, not an instance.
NO_HPO_TERM
The claim is narrower than any available HPO term and the coarse parent is the
best honest anchor. preferred_term must differ from the bound label — otherwise
nothing narrower was actually claimed and the value is wrong. Record what you
searched in term_gap, so a permanent gap becomes a term request rather than
folklore.
phenotype_term:
preferred_term: Multiple primary malignant neoplasms
term:
id: HP:0002664
label: Neoplasm
coarse_binding_basis: NO_HPO_TERM
term_gap: >-
HPO has no term for neoplasm multiplicity as such. Searches of "multiple
primary", "metachronous" and "second primary malignancy" return nothing;
the closest descendants are anatomically restricted (see notes).
Worked example: Li-Fraumeni_Syndrome. Multiplicity of primaries is the
characteristic feature of the syndrome and HPO has no term for it; the closest
descendants (HP:0007606 Multiple cutaneous malignancies, HP:0033714 Multiple
meningiomas) are anatomically restricted.
PATHOGRAPH_HUB
A deliberately unqualified convergence point in the causal graph: a mechanism
disrupts a system, and the system-level disruption is where several specific
findings converge. At least one causal edge in the same entry must target it,
and it takes no frequency.
The reason for that second rule is narrower than "a hub makes no claim". A hub
does assert that the system is involved, and carries evidence for that
assertion like any other phenotype — the Noonan exemplar below has a SUPPORT
item. What it cannot carry is a rate: an unqualified system-level node
names no finding whose occurrence could be counted, so the frequency belongs
to each specific finding beside it. A coarse node that does carry a
frequency is describing something countable, which makes it a
VARIABLE_SPECTRUM, not a hub.
pathophysiology:
- name: RASopathy Neoplastic Predisposition
downstream:
- target: Tumor Predisposition # bare name, per the pathograph rule
phenotypes:
- name: Tumor Predisposition
phenotype_term:
preferred_term: Neoplasm
term:
id: HP:0002664
label: Neoplasm
coarse_binding_basis: PATHOGRAPH_HUB
Worked example: Noonan_Syndrome.
An arriving edge proves nothing on its own. The guard can ask whether an
edge reaches the node; it cannot ask whether that edge means anything, and a
node reached by one arm of a wide fan-out is not a convergence point. The
counter-example is real: Rubinstein-Taybi_Syndrome declared Ocular
abnormalities a hub on one incoming edge that turned out to be one of twenty
INDIRECT_UNKNOWN_INTERMEDIATES targets of a single upstream node — Short
stature and Broad thumb satisfied the guard identically. Nothing converged,
the entry held no specific eye findings, and the snippet was a GeneReviews list
sentence. It is SOURCE_UNSPECIFIED.
Noonan is the contrast: RASopathy Neoplastic Predisposition targets that node
and nothing else, and the coarseness is a property of the biology rather than of
the source — germline RAS/MAPK hyperactivation raises risk across lineages
instead of producing one characteristic tumour.
just list-coarse-phenotypes prints incoming-edge count and the widest fan-out
among the sources for every declared hub, and flags a source fanning out past 3.
Advisory only: a real hub can be reached from a node that also points elsewhere,
and no threshold separates the two cases.
Two things about hubs are easy to get wrong.
A hub is defined by its incoming edges, not its outgoing ones. An early draft
of this design required outgoing sequelae into the specific findings. That is
wrong: sequelae is a CausalEdge, and a coloboma is not caused by an eye
abnormality — it is one. Requiring those edges would have had curators drawing
an is-a hierarchy as a causal chain to satisfy a guard, corrupting the graph. A
hub reached by a mechanism is complete on its own; its constituent findings,
where known, are ordinary phenotype entries beside it. Outgoing sequelae remain
fine where they are genuinely causal.
A hub is not a "disruption of eye development" node. That node belongs in
pathophysiology, binds GO (GO:0001654 with a modifier:), and asserts a
process. A hub is a phenotype, binds HP, and asserts a system-level outcome.
The two can sit in sequence, and should not be merged. Do not add an HP slot to
Pathophysiology to accommodate hubs. Watch the flat node namespace too: a hub
and a pathophysiology node sharing one name collapse into a single graph node
(#9896), so phrase
hub names as outcomes rather than processes.
A hub with a frequency is a VARIABLE_SPECTRUM. Frequency is a claim about
patients, and a hub makes none.
The guard
just check-coarse-phenotypes # gate, whole KB
just check-coarse-phenotypes kb/disorders/MyDisease.yaml
just list-coarse-phenotypes # census, exit 0
just update-coarse-phenotype-baseline # only ever to shrink
Offline, ungated by changed paths, and part of just qc — for the same reason
check-entity-refs and check-causal-targets are: CI selects pytest by changed
path, and a curation PR touches only kb/, matching neither the python nor the
schema filter. The checks written to protect knowledge-base content are exactly
the ones a content-only PR skips.
The 396 bindings that predate the slot are grandfathered in
tests/coarse_phenotype_baseline.txt. That file may only shrink, with two
exceptions, both argued in the diff that takes them: deliberately widening the
coarse set grows it once (adding tier 1 took it from 164 to 341 in a reviewed
pass), and so does refreshing the branch onto a moved main, because the
baseline is a snapshot of the pre-existing backlog taken at the moment the guard
lands (331 to 386 to 397, from entries curated in the meantime; 396 after review promoted one to a hub). Clearing a row means
a curator decided between the four values, or bound a specific term instead. A
companion-rule violation is never grandfathered, because a declared basis can
only come from content written after the slot existed.
Companion rules apply wherever a basis is declared, including on terms outside
the coarse set. That is deliberate: it lets a curator declare a basis on a
second-tier term such as HP:0000924 Abnormality of the skeletal system
before anyone decides whether to widen the subset, without the declaration going
unchecked.
Burning down the backlog
396 bindings across 279 files, of which 203 files carry exactly one. The census sorts them by term:
just list-coarse-phenotypes | head -40
HP:0002664 Neoplasm (51), HP:0000478 Abnormality of the eye (41),
HP:0000077 Abnormality of the kidney (29) and HP:0011024 Abnormality of the
gastrointestinal tract (21) are a third of it. Two shapes are worth separating
before starting:
- 94 are hub candidates — already reached by a causal edge and carrying no
frequency.
PATHOGRAPH_HUBannotates what the node is already doing. - 189 make a clinical claim, carrying a frequency, so they need a curator to
read the evidence and choose between
VARIABLE_SPECTRUMandSOURCE_UNSPECIFIED. Half of those are also reached by an edge, so the two shapes overlap rather than partition.
Neoplasm is the best place to start on the rest: in cancer-predisposition entries the specific tumour types are usually already curated as sibling phenotypes.
Do not clear a row by picking a narrower term the source does not support. If none of the four values fits and no specific term is defensible, leave the row and say so in the pull request.
Deliberately out of scope
- GO and
biological_processes. The same design would work — a closed coarse set, one basis slot — and GO ships its owngoslim_*subsets as a starting list. Nothing here is HP-specific except the vocabulary. Not now. phenotypes.category. The open register item about bindingPhenotypeCategoryEnumto that free-text slot is independent; this guard only reads the enum'smeaning:values.
Background: the design brainstorm.