DisMech Design Decisions
This document is the decision register for the Disorder Mechanisms Knowledge Base (DisMech). It records the deliberate design and scope choices that shape the project.
Both human contributors and AI agents need a single place that answers "why is it built this way, and what is in or out of scope?" rather than re-deriving the rationale from source each time.
How to use this document
- Agents: consult this before making structural, scope, ontology, or evidence decisions. When a choice here is relevant to a change, cite it. Do not silently contradict a recorded decision. If a decision looks wrong or stale, surface it.
- Humans: to change a recorded decision, open an issue describing the change and the
rationale, and tag a maintainer (e.g.
@cmungall). This document should be updated in the same PR that enacts a decision change. - This document describes decisions. It is not the authoritative source for the data
itself. Where a decision is enforced by a file (e.g. the ontology list in
conf/oak_config.yaml), that file remains canonical and is linked below.
A human-readable summary of the headline decisions is also published in the detailed docs.
How to suggest changes to these design decisions
- Open a GitHub issue describing the decision change and its rationale.
- Tag a maintainer (e.g.
@cmungall) for sign-off on scope/governance changes. - Update the canonical source (schema,
oak_config.yaml, exporter, etc.) and this document in the same PR. - Update the agent instructions in
CLAUDE.mdandAGENTS.md.
1. Project scope
Decision. DisMech is a mechanism-first knowledge base of disease pathophysiology. Each entry models the causal chain from etiology (genetic, environmental, infectious) through molecular and cellular dysfunction to clinical phenotypes, with curated, literature-grounded evidence.
In scope. Any disease or disorder with a mechanistic story worth modeling is in scope.
Mendelian, complex/common, infectious, environmental/exposure-related, neoplastic,
and psychiatric conditions are all represented. Rare and common diseases are both in
scope. Though DisMech is primarily intended as a resource for human diseases and disorders,
veterinary and animal-model observations are in scope as evidence
(evidence_source: MODEL_ORGANISM).
Out of scope. DisMech is not:
- a clinical-care guideline or treatment protocol authority,
- a diagnostic decision-support tool for individual patients,
- a store of patient-level / individual data (see individual data),
- a new ontology. We reuse existing ontologies rather than minting terms (see Ontology constraints below).
Disease selection and prioritization.
Which diseases get curated next is driven by:
- the needs of one or more specific research projects,
- the MONDO Prioritizer, and
- the compliance/priority dashboard. See
dashboard/priority.json.
2. Schema framework
DisMech is based on a data model represented in the LinkML data modeling language.
Decision. The data model is defined in
src/dismech/schema/dismech.yaml.
Rationale. As compared to other schema or data model representations, LinkML supports:
- Human-friendly authoring in YAML (curators and AI agents both edit plain YAML),
- Ontology binding via
meaningfields andreachable_fromdynamic enumerations, so enum values are validated against authoritative ontologies, - Multi-format generation (JSON Schema, SHACL, Pydantic, docs, etc.) from a single source of truth,
- Built-in tools for schema linting, data validation, and other parts of the data management lifecycle, and
- Alignment with the broader Monarch Initiative and OBO tooling ecosystem.
The internal representation uses LinkML with OBO ontology terms directly. Interoperable knowledge graph concerns (Biolink Model) are handled separately at the export layer (see Biolink reuse below).
3. Knowledge-representation principles
One Disease class, one file per disorder. Each disorder is a single YAML file in
kb/disorders/
that validates against the Disease class. Comorbidity and module files reuse the same
schema.
Subtypes vs. separate entries. Closely related variants of one diagnosis are modeled
as has_subtypes within a single file, using the subtype name as a foreign key that
other sections (phenotypes, genetic_basis, etc.) reference via their subtype slot
(enforced by check_subtype_foreign_keys). Create a separate disorder file only when
the condition has a distinct MONDO identity and a substantially independent mechanism;
otherwise prefer a subtype. See the subtype naming conventions in
CLAUDE.md.
Mechanism modules are conformance, not inheritance. Modules in
kb/modules/
capture conserved pathological processes (e.g. the fibrotic response) that recur across
disorders. A disorder node declares conforms_to: "module_name#Node Name". This is a
consistency check: conforming entries fully duplicate the relevant
content and substitute organ-specific cell types/genes. Modules deliberately do not act
as a base class that disorders inherit from.
Module collections are navigation, not disease groupings. Records in
kb/module_collections/ organize modules into published frameworks or broad
mechanistic families without claiming that the collection is itself a mechanism. They
use the dedicated ModuleCollection class and module filename stems as foreign keys.
Grouping remains reserved for explicit unions of diseases (and nested disease
groupings); it may refer to a module in membership criteria or a differentiating
mechanism, but a module cannot be one of its members. This separation prevents framework
navigation such as the Hallmarks of Aging from acquiring disease-membership semantics.
Causal graph / pathograph. Pathophysiology nodes connect via downstream causal
edges with a causal_link_type, forming a directed graph from etiology to phenotypes.
This graph backs the rendered pathographs and the computational-model integration
(see computational models).
3a. Cancer granularity ladder (2026-08-28)
Decision. Somatic cancer entries follow an explicit granularity ladder. The generic §3 rule ("separate file only for a distinct MONDO identity and a substantially independent mechanism") was written for Mendelian disease; cancer needs its own clause because the field's own taxonomy is layered — a histogenesis backbone with molecular alterations promoted into entity definitions case by case (WHO Classification of Tumours 5th ed. / cIMPACT-NOW integrated diagnosis, WHO-HAEM5 and ICC 2022, ICD-O, OncoTree) — and because most of the KB's precision-oncology entries sit below MONDO's granularity, which the Mendelian rule never contemplated.
| Level | Represent as | Rule |
|---|---|---|
| L1 Organ/system pool ("lung cancer", "lymphoma") | Lean umbrella Disease entry or a Grouping |
Never the curation target for mechanism content; every pathophysiology node must hold for all members |
| L2 Histologic entity (PDAC, SCLC, DLBCL, osteosarcoma) | Disease entry — the default level for a new cancer entry |
The WHO blue-book / ICD-O morphology level; anchor to the entity's MONDO term |
| L3 Molecularly defined entity (IDH-wildtype glioblastoma, APL with PML::RARA, NPM1-mutant AML, Ewing sarcoma) | Disease entry |
Create when WHO/ICC defines the entity molecularly — the field has already made the promotion call, and MONDO nearly always has the term |
| L4 Biomarker/therapy stratum (EGFR-mutant NSCLC, MSI-H CRC, TNBC, FLT3-mutant AML) | Default: has_subtypes on the L2/L3 entry. Promote to its own entry only when it passes the disjunction test (issue #7082): ≥2 pathophysiology nodes not true of sibling strata and a distinct first-line therapy or diagnostic pathway |
On promotion: (a) file a MONDO NTR; until granted, anchor disease_term to the parent term but record mapping_predicate: skos:narrowMatch in mappings.mondo_mappings with a justification — never a bare parent-term reuse presented as exact; (b) record non-disjointness with sibling strata (e.g. FLT3×NPM1 AML, MSI-H×BRAF-V600E CRC) in notes and grouping differentiating_mechanisms; (c) add or extend the covering Grouping (Molecularly_Defined_NSCLC_Subtypes is the worked precedent); (d) the parent's has_subtypes keeps a pointer subtype naming the split file rather than a divergent copy |
| L5 Variant tier (exon 19 del vs L858R vs T790M) | has_subtypes inside the L4 entry |
Never its own file. Exception: a variant is the L4 stratum when therapy is variant-specific (KRAS G12C is the stratum because sotorasib/adagrasib are G12C-covalent; "KRAS-mutant NSCLC" would be the wrong grain) |
| Stage / metastasis | stages: on the parent entry, progression records, and conforms_to on the invasion_and_metastasis module |
Never a Disease entry. TNM stage is orthogonal to taxonomy in every classification system |
| Etiologic stratum (HPV± oropharyngeal SCC, EBV-associated gastric cancer) | Case-by-case via the same L4 promotion test | HPV-positive oropharyngeal SCC passes (distinct mechanism, AJCC 8 stages it separately); EBV-GC is a TCGA molecular subgroup and sits closer to a subtype |
| Tissue-agnostic biomarker indication (NTRK fusion-positive cancer) | One MONDO-anchored entry per biomarker, categorized Tumor-Agnostic Indication |
Do not also stamp it as a subtype of every organ cancer; organ-specific entries reference it |
| Pathway / hallmark biology (MAPK, PI3K, the Hanahan–Weinberg hallmarks) | kb/modules/; use ModuleCollection when several modules form a named framework |
Never a Disease entry — pathways recur across entities and across the cancer/non-cancer boundary (cf. RASopathies) |
Germline cancer-predisposition syndromes (Li-Fraumeni, Lynch, FAP, VHL, HBOC…) are Mendelian diseases and stay under the plain §3 rules; this clause governs the somatic neoplasm entries only. The associated somatic cancer remains a separate entry (Lynch vs MSI-H CRC, Gorlin vs basal cell carcinoma).
Rationale. Neither variants, genes, nor pathways can serve as the primary axis: the
same lesion is a different disease in a different lineage (BRAF V600E melanoma responds
to BRAF inhibition; BRAF V600E CRC does not, via EGFR feedback — modeled in the KB's own
entry), yet a single lesion can define an entity when it is the initiating, universal,
lineage-bound driver (CML, APL, Ewing). The KB's biomarker-stratum entries are retained
— they carry exactly the distinct mechanism + therapy content that justifies a
mechanism-first split and that has_subtypes cannot scope (no subtype: discriminator
exists on Pathophysiology or Treatment) — but new ones must earn promotion rather
than default to it. This supersedes the informal "Molecular Subtypes as Discrete
Entities" policy in projects/CANCER.md and resolves the cancer buckets (A/B) of issue
5121 non-uniformly: driver strata stay split under the anchoring convention above;
"Metastatic X" stage entries are folded into their parents (enacted in the same PR as
this decision — the ten former Metastatic_* entries are now stages + merged content
on their histologic parents, with Cutaneous_Melanoma and Breast_Carcinoma created as
previously missing parent rungs).
Origin. Issues #306,
#3881,
#5121,
#7082; full review in
cancer-taxonomy-granularity-review-2026-08-28.
Enacted at maintainer direction (@cmungall, 2026-08-28 session). Still open: the
MONDO NTR list for promoted strata; creating remaining missing L2 parents (e.g.
Lung_Adenocarcinoma); structural overlap annotation between non-disjoint strata.
3b. Model-to-target scale gap is recorded as a fact and derived, not asserted (2026-09-02)
Status: ENACTED (2026-09-03, @cmungall-approved). See
model credibility for the reader-facing explanation.
Decision. ModelMechanismLink carries an optional model_scale
(BiologicalScaleEnum, the same enum as Pathophysiology.biological_scale) recording the
biological scale the model observes. The gap between a model and the node it is cited
for is then derived by comparing the two, never stored.
Problem it solves. A model linked to a node is not necessarily operating at that
node's scale. A Boolean signalling network whose output node is named "bone erosion"
observes molecular or cellular state and infers the tissue-level outcome; a
transcription-level model of dopamine synthesis is not a model of striatal dopamine
concentration. Before this slot the caveat survived only as prose in limitations —
unqueryable, inconsistently written, and easy to omit altogether. fidelity compresses
it into a coarse tier that also absorbs species divergence, expression level, and every
other translational concern, so a LOW tier does not say which problem it is.
Why derive rather than store. A stored scale_mismatch flag would duplicate
information already present in two slots and could drift out of sync with them. The
comparison is cheap (just model-scale-audit).
Why the comparison is directional. The two directions are different claims and must not be collapsed into a single "mismatch" boolean:
- Model below target — upward extrapolation. The model cannot observe the outcome it
is cited for. This is the reviewable state, and
check_upward_extrapolating_links_are_caveatedrequireslimitationson it, mirroring the existingFAILS_TO_RECAPITULATE-must-be-substantiated rule. - Model above target — the model contains the target's scale and is normally unremarkable: a whole animal can report a molecular readout.
Scope limits, deliberately. model_scale is orthogonal to fidelity and
relationship, not a replacement for either: a molecular model linked to a molecular node
shows no scale gap even when it is a poor model for an unrelated reason (pathway
activation standing in for recombination fidelity, say). An aligned result means "no
scale gap", never "good model". Both slots are optional, so a link with neither is
UNDETERMINED rather than defective — the state of most existing links.
Feasibility. 80.5% of the KB's 1,131 model→mechanism links already have a target
node carrying biological_scale, so the comparison is computable for the large majority
as soon as model_scale is populated. The initial pilot covers the ten Boolean-model
links; on those, the derived gap independently reproduced the hand-assigned fidelity
tiers (every 2-step upward extrapolation was graded LOW, every aligned link MODERATE),
which is the evidence that the slot captures something real rather than restating
curator intuition.
Not decided here. Whether ExperimentalReadout should carry its own scale, and
whether models themselves (rather than their links) should declare a scale span for
multiscale frameworks such as PhysiBoSS.
3c. Model divergence is typed and explained, not compressed into a fidelity tier (2026-09-02)
Status: ENACTED (2026-09-03, @cmungall-approved), alongside §3b. See
model credibility for the reader-facing explanation.
Decision. ModelMechanismLink carries divergences: a multivalued list of
ModelDivergence, each naming a kind from a closed ModelDivergenceTypeEnum, a required
curator description of why that kind applies to this link, and an optional
materiality recording whether it bears on this link's claim.
Problem. 831 of 1,131 model→mechanism links (73%) already carry a limitations
string, so curators reliably write the caveat. What is missing is structure. fidelity
compresses every translational concern into one coarse tier, so LOW does not say which
problem it is; and prose cannot answer "which models are limited by calibration
provenance rather than by species" — a question with a very different answer for
computational models than for animal ones.
The taxonomy is evidenced, not invented. All 50 computational-model limitations
strings were read and clustered, with the animal and NAM sets keyword-probed to establish
which kinds are shared. The modality profiles differ sharply: the animal set is dominated
by species divergence (276 hits) and allele mismatch (123), the computational set by
calibration provenance, absent dynamics, and proxy quantities, with species divergence
nearly absent. Full survey:
model-divergence taxonomy.
Relation to §3b. model_scale gives one machine-derivable divergence
(SCALE_EXTRAPOLATION); the taxonomy gives the ones that cannot be derived. The two
cross-check: a SCALE_EXTRAPOLATION divergence contradicted by the scale slots fails both
check_scale_extrapolation_divergence_agrees_with_scales and
just model-scale-audit --strict. The most important thing the taxonomy adds is
PROXY_QUANTITY — a model reporting a different quantity at the same scale, which no
scale comparison can see. The Fanconi anemia FA/BRCA link is the worked case: aligned on
scale, PARTIALLY_RECAPITULATES because pathway activation state stands in for
recombination fidelity.
Why per-divergence materiality rather than only per-link fidelity. A link can
carry several divergences of different weight, and IMMATERIAL is worth recording
precisely because it stops a reader inferring that a known limitation of the model
undermines this particular use of it. Consistent with §3b's derive-don't-store principle,
recording materiality per divergence is what could eventually let fidelity be derived
rather than authored — though that is not proposed here.
Coexistence with limitations, not replacement. The prose slot holds 831 existing
caveats and remains the summary form; a typed divergence now satisfies the caveat
requirement wherever limitations did. Migrating the existing prose is a separate
decision and is not proposed.
Scope. Populated on computational models only (10 links, 26 divergences). The value
set was chosen to extend to NAM and animal models unchanged; extending it would add
SUPRAPHYSIOLOGICAL_EXPRESSION and INCOMPLETE_PHENOTYPE, each already visible in the
animal set at 20-53 keyword hits and so evidenceable the same way.
Prior art, and the limits of the borrowing.
ASME V&V 40
(FDA-recognized consensus standard) and the FDA's
Assessing the Credibility of Computational Modeling and Simulation in Medical Device
Submissions
(final guidance, CDRH, 16 November 2023) anchor credibility in a stated context of use
and require an applicability analysis — the relevance of validation evidence to that
context. A ModelMechanismLink is a context-of-use statement; a typed divergence list is
its applicability analysis. PROXY_QUANTITY maps to a quantity-of-interest mismatch, and
materiality is the per-divergence analogue of that framework's risk grading
(model influence × decision consequence).
Both were written for regulatory submissions of physics-based device models, so only the
structural ideas are borrowed — assess a model for a stated use, argue applicability
separately from validity, grade evidence by how much the answer matters. The procedural
apparatus is not imitated: no V&V plan, no uncertainty quantification, no numeric
credibility goals, and no claim about numerical accuracy, since dismech annotates
published models from their papers rather than building them. The domain-general check is
the ten rules of credible practice in healthcare modeling
(Erdemir et al., J Transl Med 2020, PMID:32993675), which reach the same conclusions
with no device framing at all: rule 1 "define context clearly" is the link, rule 3
"evaluate within context" is materiality, and rule 4 "list limitations explicitly" is
divergences — typed rather than prose being the only change. All four references are
carried in the schema itself as see_also on ModelMechanismLink, ModelDivergence,
ModelDivergenceTypeEnum and ModelDivergenceMaterialityEnum.
3d. Cell of origin is derived from the pathograph, not stored in a slot (2026-09-02)
Decision. A neoplasm entry's cell of origin is derived, by finding the
pathophysiology node where the transforming lesion occurred and reading that node's
cell_types. dismech adds no cell_of_origin: slot, on Disease or on Subtype.
The origin node is identified by two rules, both reading a structured claim rather than a naming convention, with the rule that fired reported alongside the answer:
| Rule | Marker |
|---|---|
SOMATIC_LESION |
pathophysiology[].genetic_context.variant_origin: SOMATIC (or GERMLINE_AND_SOMATIC) — the transforming lesion. Not restricted to root nodes: a second-hit or transformation lesion is still a somatic event. allelic_hit_role: FIRST_HIT narrows it further |
ENVIRONMENTAL_TRIGGER |
an environmental[].influences_mechanisms link marking the node environmental_effect: TRIGGERS — non-mutational initiation (HPV, H. pylori, asbestos, UV), where there is no host lesion to mark. The same value the KGX exporter and compliance scoring already treat as causal |
The exposure rule applies only when no lesion is recorded, which is a statement about meaning and not confidence: the cell of origin is the cell the transforming event occurred in, so once the entry records that event the exposure is upstream context.
Rejected: fallback rules for unmarked entries. The first implementation also read an
initiating-sounding free-text role on a root node, and chained fallbacks so a stronger
rule could not discard a weaker rule's answer. Both existed to cover entries that had not
recorded their origin, and both mis-fired — the role rule derived macrophage and pancreatic
stellate cell as the cell of origin of pancreatic ductal adenocarcinoma, from a
chronic-inflammation node. A CONTEXT_NODE_MARKED finding was added to catch that, and
then false-positived on cancers that really are inflammation-initiated. The correct fix was
to mark the records (scripts/backfill_cancer_origin.py, 105 entries) and delete the
heuristics; an entry that does not say where it starts is now reported as not saying it
(@cmungall, 2026-09-02).
Rationale. A dedicated slot would restate what the pathograph already says, and the
two would drift. It would also sit at the disease level, unable to distinguish a
subtype's origin from its parent's, and it would invite filling in from the disease name
rather than from the mechanism. Deriving it keeps the claim attached to the node holding
its evidence, and makes an unmarked entry visible as unmarked rather than as an empty
field. This was raised as a possible cell_of_origin: slot and rejected in favour of the
marker on the node (@cmungall, 2026-09-02 session).
Consequence — the multi-origin finding is the lump/split signal. Deriving more than
one cell of origin is reported and never gated, because it means one of three
different things and only a curator can say which: (a) a pool wearing a Disease entry's
clothes (Non-Small_Cell_Lung_Cancer derives alveolar type 2 and bronchial epithelial
cell, which is exactly what NSCLC is) — remedy a Grouping or a split per §3a; (b) one
disease with genuine cell-of-origin subtypes (B-lymphoblastic leukemia across its 19
subtypes; GPR101-related_pituitary_adenoma_2's somatotroph and mammotroph) — remedy
has_subtypes; (c) an origin the literature has not settled
(Melanoma_in_Congenital_Melanocytic_Nevus names melanocyte and neural crest cell) —
remedy a note. Seven entries currently report it, which is short enough to work through.
A viral mechanism is never marked under rule 1 — HPV E6/E7 and HTLV-1 Tax leave no
host variant for variant_origin to describe — and a spurious mark there is doubly
harmful, since a recorded lesion suppresses the exposure rule that should answer for it
(caught in review of PR #10629).
NCIT is a cross-check, not a binding target. NCIT asserts cell of origin per disease
inside its owl:equivalentClass definitions — NCIT:R104 Disease_Has_Normal_Cell_Origin,
NCIT:R112 (weak form), and NCIT:R105 Disease_Has_Abnormal_Cell into the Abnormal Cell
branch (NCIT:C12913); DLBCL is Mature B-Lymphocyte → Neoplastic Large B-Lymphocyte.
All three are ingested by the manifest-driven OntologyEdgeSource as quotable
references_cache/NCIT_*.md rows. CellTypeTerm stays reachable_from: CL:0000000
alone, so cell_types remains CL-only and an NCIT code there fails term validation. There
is no NCIT-to-CL mapping in the repo, so agreement between our derived CL term and NCIT's
class is a curator's judgement, not a computed match.
Enacted. scripts/check_cancer_origin.py (just check-cancer-origin, advisory,
inside just qc), scripts/backfill_cancer_origin.py, tests/test_cancer_origin.py, and
105 entries marked. 124 of 252 assessed neoplasm entries now derive a cell of origin, and
ORIGIN_WITHOUT_CELL is at zero — every entry that marks an origin binds a cell there,
which makes that class ready to become a hard gate. Still open: the remaining 128
unmarked entries, whose pathographs name no genetic lesion, or name one with no cell type
anywhere in the entry — both need a curator rather than a script; and whether CL should
gain lineage-specific transformed-cell terms, or CellTypeTerm gain NCIT:C12913 as a
second source node, so an entry needing "H3 K27M-mutant glioma cell" stops binding the
generic CL:0001063. See cancer-cell-of-origin.md.
4. Ontology constraints
Decision. Term validation is restricted to an explicit, curated set of ontologies.
The canonical list is
conf/oak_config.yaml;
the table below mirrors it.
| Domain | Ontology | Prefix(es) |
|---|---|---|
| Disease | MONDO, ICD-10-CM, ICD-11 foundation | MONDO:, ICD10CM:, icd11f: |
| Phenotype | Human Phenotype Ontology | HP: |
| Cell types | Cell Ontology | CL: |
| Biological process / function / component | Gene Ontology | GO: |
| Anatomy | UBERON | UBERON: |
| Chemicals / drugs | ChEBI | CHEBI: |
| Genes | HGNC | hgnc: (canonical lowercase), HGNC: (legacy) |
| Inheritance / variant effects | Genotype Ontology | GENO: |
| Treatments / clinical interventions | NCI Thesaurus | NCIT: |
| Exposures | ECTO, ExO, XCO | ECTO:, ExO:, XCO: |
| Environment | ENVO | ENVO: |
| Food | FOODON | FOODON: |
| Parasite life cycle | OPL | OPL: |
| Taxonomy | NCBITaxon | NCBITaxon: |
Rationale. A constrained, OBO/Monarch-aligned set keeps terms interoperable, machine-validatable (offline SQLite adapters via OAK), and resistant to AI hallucination of fake identifiers.
Selection priority when several ontologies could apply:
- Treatments: use the most specific accurate NCIT clinical-intervention term
(all reachable from
NCIT:C25218). When NCIT has no suitable clinical-action term, omitterm:and keep a free-textpreferred_term— never invent an identifier. - Therapeutic agents: prefer CHEBI for specific small-molecule drugs; use NCIT for drug classes and for biologics/newer drugs lacking a CHEBI term.
- Disease-like phenotypes (phenotypes that are also diseases, e.g. osteoporosis,
glaucoma): dual-code with both an
HP:and aMONDO:identifier where appropriate.
Conventions.
- HGNC casing: gene CURIEs use lowercase
hgnc:(e.g.hgnc:746). This is the canonical form that passes validation; do not flag it as an error. preferred_termvsterm.label:term.labelmust exactly match the canonical ontology label (OAK-verified);preferred_termmay be more specific/clinical when the ontology term is too broad.
How to add an ontology. Add the prefix → OAK adapter mapping in
conf/oak_config.yaml, ensure the SQLite adapter is available, and re-run term
validation. Known gap: prefixes not listed there are silently skipped during
validation (only a warning), so an unconstrained prefix can pass unchecked — see Gaps below.
4a. MAXO removed in favour of NCIT (2026-07-31)
Decision. The Medical Action Ontology (MAXO) was removed from dismech entirely. All
4,300+ MAXO treatment_term / diagnosis_term bindings were remapped to NCI Thesaurus
clinical-intervention terms, MAXO:0000001 was dropped from TreatmentActionTerm's
source_nodes (leaving NCIT:C25218 as the sole root), and the MAXO adapter and term
cache were deleted. This reverses the earlier decision to treat MAXO and NCIT as co-equal
treatment vocabularies. See PR #7228 and the frozen crosswalk
docs/superpowers/maxo_ncit_final_map.tsv.
Rationale. One treatment vocabulary rather than two removes the recurring "which ontology has the better term?" judgement call, and NCIT covers the clinical-action space more completely and more specifically.
What was traded away. This is a genuine loss of specificity in places, recorded here so it is not rediscovered as a bug:
- Some MAXO terms have no exact NCIT counterpart and were mapped to a broader parent
(e.g. drug-class "X agent therapy" terms →
NCIT:C15986Pharmacotherapy). Where a drug class was lost from the action term it is recovered intherapeutic_agent; a tail of such bindings still carries no coded agent. - Seven MAXO terms have no NCIT equivalent at all (orthotic/hearing-aid/glasses usage,
airway management, emollient application, apoptosis assay, transepithelial nasal
potential difference). Those entries keep a free-text
preferred_termwith noterm:. - Mapping a route- or method-agnostic source term to a route- or method-specific NCIT term
would assert something the source never said, so defaults are deliberately neutral
(e.g. corticosteroid therapy →
NCIT:C15370Steroid Therapy, not the Systemic child).
Cache provenance. NCIT is served via ols:ncit (issue #5160) and the OLS adapter
cannot compute ancestors, so dynamic-enum membership cannot be re-derived from the
committed configuration. cache/enums/treatmentactionterm_*.csv was therefore generated
by temporarily pointing the NCIT: adapter at a local sqlite:obo:ncit build — the same
build the ncit-edges structured source already uses — and conf/oak_config.yaml was
then reverted to ols:ncit. Regenerating that cache requires repeating this; the
committed configuration alone is not sufficient.
5. Biolink reuse
Decision. BioLink
(biolink-model>=4.3.1) is used only at the export layer, in the KGX exporter
(src/dismech/export/kgx_export.py).
The internal DisMech schema does not use BioLink. Rather, it uses LinkML with OBO terms
directly.
Rationale. This deliberately separates the internal curation model (optimized for authoring and mechanism representation) from the interoperable exchange format (optimized for integration into knowledge graphs). Each can evolve without forcing churn on the other.
The KGX exporter emits typed, directed edges with the knowledge source identifier
infores:dismech. Mapping currently used:
Entity categories: biolink:Disease, biolink:PhenotypicFeature, biolink:Cell,
biolink:AnatomicalEntity, biolink:BiologicalProcess, biolink:Treatment,
biolink:Gene, biolink:ExposureEvent, biolink:OrganismTaxon,
biolink:GeneticInheritance, biolink:MolecularActivity, biolink:CellularComponent,
biolink:ChemicalEntity, biolink:Pathway, biolink:MacromolecularComplex,
biolink:MolecularEntity.
Predicates:
| Predicate | Edge (subject → object) |
|---|---|
biolink:has_phenotype |
Disease → PhenotypicFeature |
biolink:associated_with |
Disease → Disease |
biolink:has_participant |
Disease → Cell / CellularComponent / ChemicalEntity / MacromolecularComplex |
biolink:disease_has_location |
Disease → AnatomicalEntity |
biolink:affects |
Disease → BiologicalProcess / MolecularActivity / Pathway (with INCREASED/DECREASED direction qualifiers) |
biolink:treats_or_applied_or_studied_to_treat |
Treatment → Disease |
biolink:contributes_to |
Gene → Disease; ExposureEvent → Disease |
biolink:associated_with_decreased_likelihood_of |
ExposureEvent → Disease (protective) |
biolink:has_mode_of_inheritance |
Disease → GeneticInheritance |
biolink:causes |
OrganismTaxon → Disease |
biolink:has_biomarker |
Disease → MolecularEntity |
Known gap: differential_diagnoses and diagnosis sections are not yet exported. See Gaps below.
6. Evidence & provenance policy
Decision. Every evidence item must cite a real, resolvable reference and quote it exactly.
- Accepted reference types: PMID, DOI, NCT (ClinicalTrials.gov), and structured-source
IDs
ORPHA:(Orphanet),CGGV:/CGDS:(ClinGen),CIVIC_ASSERTION:/CIVIC_EID:(CIViC). - Exact-snippet rule:
snippetvalues must be exact substring quotes from the cited reference, enforced bylinkml-reference-validator. Paraphrase fails validation. - Cache files are tool-generated:
references_cache/*.mdare created exclusively byjust fetch-referenceor the validator. They are never hand-written or hand-edited. evidence_sourcedescribes the study type reported in the publication (HUMAN_CLINICAL, MODEL_ORGANISM, IN_VITRO, COMPUTATIONAL, OTHER), not how curation was performed. Model-organism evidence must not be the sole support for a human phenotype.- Deep-research outputs are leads, not ground truth. PMIDs, snippets, and ontology terms suggested by deep-research tools must be independently verified before commit.
- Frequency qualifiers need their own evidence: a phenotype
frequency:band is a separate quantitative claim from the association; when in doubt, omit it. See frequency-evidence-guidelines.
Rationale. The exact-quote-plus-validation pipeline is DisMech's primary defense against AI hallucination and is core to the project's scientific credibility.
6a. Superseded hypotheses are retained and marked, not deleted (2026-08-02)
Decision. When a disease-level mechanistic hypothesis has been overturned, it is
curated as a mechanistic_hypotheses entry with status: DEPRECATED — kept in the entry,
not removed from it — and the rendered page states explicitly that DisMech does not
assert it as the current mechanism.
- Retain rather than delete. An overturned model that is still circulating in reviews, textbooks or older diagnostic criteria is exactly the claim a reader arrives wanting to check. Deleting it leaves them with nothing to check against; recording it with a verdict and cited refutations is the more useful artifact.
- Citation volume does not decide standing. A deprecated hypothesis will often carry more supporting than refuting citations, because the supporting literature accumulated for decades before the refutation landed. Where evidence conflicts, DisMech follows the more recent and more direct evidence — a quantitative or orthogonal-method refutation outweighs any number of narrative citations asserting the older view.
- Both sides get cited. The hypothesis's own
evidencelist carries the founding supporting citations (supports: SUPPORT) alongside the refutations (supports: REFUTE), each with a verified snippet, so the assessment is auditable rather than editorial assertion. Renderers surface the SUPPORT/REFUTE/NO_EVIDENCE split as an evidence-balance row. - Disputed nodes are marked, not asserted. A pathophysiology node that exists only to
represent a deprecated model carries
mechanism_confidence: HYPOTHETICAL, and its causal edges opt into the deprecatedhypothesis_groupsso the disputed chain stays separable from the live models. Hypothesis chips on nodes and edges carry the deprecated status. - No conformance on a disputed claim. Do not add a
conforms_toedge to a mechanism module on the strength of a hypothesis curated as DEPRECATED — that would assert as fact precisely what the assessment rejects.
Rationale. Mechanism knowledge bases fail readers in two opposite ways: by omitting
retired models (so a reader meeting the claim in the literature has no recourse), and by
listing them undifferentiated alongside live ones (so retired models keep accruing
authority). Recording the model with an explicit verdict and a visible evidence balance
avoids both. The worked example is the beta-amyloid hypothesis of sporadic inclusion body
myositis (amyloid_beta_proteotoxicity in kb/disorders/Inclusion_Body_Myositis.yaml),
whose literature is itself the documented subject of a citation-distortion analysis
(PMID:19622839) — the clearest available case of citation weight outrunning data. See
the exploration report.
6b. supports is direction only; directness is a separate axis (2026-08-29)
Decision. EvidenceItemSupportEnum is narrowed to SUPPORT / REFUTE /
NO_EVIDENCE, and an optional directness slot (DIRECT / INDIRECT / UNKNOWN) is
added alongside it. PARTIAL and WRONG_STATEMENT are retired. This is the partial
adoption of the SEPIO evidence model proposed in
#7439.
Rationale. supports conflated several unrelated judgements. Reading the curators'
own explanation prose on the 11,620 PARTIAL items, the value was carrying at least
four: indirect support, an inverted model system, an item that supported one claim while
contradicting another, and plain irrelevance. WRONG_STATEMENT had a single use, which
recorded that an earlier version of an entry's text was inaccurate — provenance for a
history/ record, not a direction of evidence.
Directness rather than strength. #7439 proposed SEPIO's direction/strength split. We took direction/directness instead:
- Strength is not judgeable from the record. It needs study design, sample size and replication, none of which the evidence item holds. Directness needs the snippet and the claim, both of which are right there — so two curators who disagree about directness have a shared referent, and two who disagree about strength do not.
- The measured churn argues against it.
scripts/check_snippet_grading.pycounts one quoted sentence graded two ways in one file. Before this change: 8,285 divergences onsupportsagainst 715 onevidence_source. Strength would be worse than either, being a property of the evidence itself (so any divergence is a flat contradiction) and the most judgement-laden of the three. - This is consistent with the §12 position that strength should be derived from a typed, snippet-anchored experiment model rather than authored as a grade.
Migrated items are left unassessed. All 11,620 PARTIAL items became SUPPORT with
directness absent, not backfilled to a default. Absent means nobody has assessed the
item, which is true of essentially the whole KB; defaulting would have manufactured 11,620
appraisals no curator made. The population is enumerated in
docs/reports/data/partial-evidence-directness-worklist-2026-08-29.tsv.
Effect. Retiring PARTIAL cut supports divergences from 8,285 to 353 — a 96%
reduction — which indicates most of that signal was the value's ambiguity rather than the
legitimate claim-relativity that justifies leaving supports ungated. Gating it is worth
revisiting.
Not adopted: SEPIO's EvidenceLine / DataItem / Document classes. An
EvidenceItem is already an evidence line holding one data item and one document, and
linkml-reference-validator's ELLIPSIS_INSERTION support means a single snippet
already carries a discontinuous multi-span quote — which was most of what the nesting
bought. §5's export-layer-only rule for external models therefore stands: the SEPIO
alignment is expressed as exact_mappings on the dismech-native slots, and in
sepio_export.py. The known cost is that evidence about other evidence (a rebuttal of a
refutation, as in glymphatic_dysfunction.yaml) remains unrepresentable.
Consequence for the exporter. NO_EVIDENCE has no SEPIO direction — SEPIO's neutral
means the evidence bears on the claim without favouring a side, whereas NO_EVIDENCE
means it does not bear on the claim at all. The old mapping of NO_EVIDENCE to NEUTRAL
asserted something the curator did not and was removed; such lines now carry no
direction_of_evidence_provided and retain the raw value on dismech_supports.
6c. A dataset accession is a reference, cached one file per record (2026-08-27)
Decision. A Dataset.accession is verified by fetching its repository record into
references_cache/, the same mechanism that backs a PMID — not by a separate shared
verification cache. geo: is migrated; the remaining prefixes follow one at a time.
Dataset.accessionalready declaresimplements: linkml:authoritative_reference. It was always a reference slot;conf/reference_validator_config.yamlmerely lists the dataset prefixes underskip_prefixes.just verify-datasets <file>resolves ageo:accession through the reference fetcher, which writesreferences_cache/GEO_<ID>.md(GEO's own title and summary). The cache file is the verification — the fetcher writes one only after the repository returned a record — and it is committed with thedatasets:block.- All 919
geo:accessions inkb/were backfilled, so the check is offline in CI. geoandGEOhave been removed fromskip_prefixes, so the validator now checks GEO records like any other reference — the accession must resolve, aGEO:-cited snippet must be an exact quote from the cached summary, anddatasets[].titlemust be the repository's own title (a title slot adjacent to a reference field is compared with the fetched record).datasets[].titleis the repository's title, copied exactly — the curator's summary belongs indescription, and the title keeps the repository's typos, for the same reason a snippet never corrects its source. Turning this on was a curation pass, not a config change: it required fixing 32 records — 30 titles that paraphrased or replaced GEO's own (of 951; now 951/951 match) and 2 evidence snippets, one a reordered paraphrase of a sentence that was in the cache all along (GEO:GSE289185) and one quoting GEO's uncached "overall design" field (GEO:GSE316127). KB-wide, 133,610 of 138,867 snippets verify with zero GEO failures.cache/dataset_accessions.jsonis frozen. No script, module, test, workflow, or recipe reads or writes it, enforced bytest_no_automation_touches_the_frozen_dataset_cache. It remains in git only until the open PRs carrying edits to it have drained.
Rationale — the storage shape, not the checking, was the defect. The old cache was one
sorted JSON object rewritten in full on every run, including a run over a single
disorder file. So every curation PR that touched a datasets: block churned the same
1.8 MB file, and with 919 geo: keys sorted into one contiguous region, two PRs adding
neighbouring accessions landed inside each other's diff context and conflicted. It also
bought nothing that justified being committed: no CI job and no test read it, unlike
references_cache/ and cache/**/*.csv, which are committed precisely because
validation determinism requires them. Its 1,800 entries were all status OK (the
negative-cache machinery held nothing), and its payload — a title — duplicated the
datasets[].title already in the entry. Per-record files are add-only, so two PRs adding
datasets touch disjoint paths.
A datasets/ folder was considered and rejected. As a de-duplication layer it does
not pay: of 1,747 dataset records, 1,696 accessions are distinct and only 49 (2.9%)
appear in more than one entry, maximum fan-out 3 — and every one of those is a pair of
sibling entries (MED13/MED13L, Ehlers-Danlos/Hypermobile_EDS,
Schwannoma/Schwannomatosis), which is a lump/split question rather than a storage one.
Inline datasets: records stay inline. A genuine catalog layer — access tier, file
manifests, checksums, local mirror paths for a lakehouse — is a different argument and
still open; if it is built, its home is the existing structured-source framework
(src/dismech/structured_sources/ + data/<source>/MANIFEST.yaml), which is already
"one generated file per entity, checked into git", not a third mechanism.
Consequence for evidence. Bulk-generated dataset records carry no evidence: because
an evidence item needs an exact quote and there was nothing quotable. With the GEO summary
cached there now is, so a dataset record may carry evidence quoting it and citing
GEO:<ID> (worked example: Acne_Vulgaris). This does not license bulk evidence
generation; the §6 exact-quote rule is unchanged.
Follow-ups. (1) Give the remaining prefixes a fetcher, highest-volume first (ega
382, massive 120, metabolomics_workbench 81, dbgap 71), adding each to
REFERENCE_CACHED_PREFIXES and removing it from skip_prefixes; lrv ships a
BIOPROJECT source already, and its generic json_api source may cover others without
new code. Budget each migration as a backfill plus whatever the newly-enabled title and
snippet checks surface — it is a curation pass, not a config edit. (2) Delete the frozen
blob once open PRs have drained.
7. Curation process & governance
Decision. DisMech is agent-forward: most curation is performed by AI agents, initiated either by humans or by GitHub Actions.
- Humans initiate work; agents execute and may also author issue/PR comments.
- The default assumption is that issue/PR contents and comments are AI-generated; humans are not assumed to have verified every line their agent produced. Mark human-authored content explicitly if desired.
- Human-in-the-loop is the PR review gate. Every PR receives an automated review and must address its findings. Unresolved disagreements are escalated to a human maintainer.
See CONTRIBUTING.md
and the workflow definitions in .github/workflows/.
8. Prevalence representation
Decision. Disease occurrence is modeled with structured, separated slots on the
Prevalence class rather than the single overloaded percentage field that preceded
them. The strategy mirrors how phenotype frequency is banded (FrequencyEnum), but at
population scale and without discarding the underlying number.
Prevalence now carries:
population— cohort / geography only (e.g. "Worldwide", "Ashkenazi Jewish population"). Measure-type qualifiers that used to be jammed in here (e.g. "(Orphanet point prevalence)") belong inmeasure_type.measure_type(PrevalenceMeasureEnum) — which epidemiological measure the record reports:POINT_PREVALENCE,BIRTH_PREVALENCE,LIFETIME_PREVALENCE,PERIOD_PREVALENCE,ANNUAL_INCIDENCE,CARRIER_FREQUENCY,CASES_IN_LITERATURE,UNKNOWN. This prevents a point prevalence from being silently compared with an incidence rate or a literature head-count.prevalence_class(PrevalenceClassEnum) — the coarse, always-fillable, queryable band. Numeric tiers are the Orphanet prevalence classes (>1/1,000,1-5/10,000,1-9/100,000,1-9/1,000,000,<1/1,000,000,Not yet documented), so records that quote Orphanet (and theORPHA:structured source) map directly; qualitative tiers (COMMON,RARE,ULTRA_RARE,UNKNOWN) cover prose-only records with no numeric estimate. This is the population-rate analog of phenotypeFrequencyEnum.rate_per_100000(+rate_low/rate_highfor ranges) — one normalized, machine-comparable number in cases per 100,000. Every source notation (%,per 100,000,per million,1 in N, OrphanetN / M) converts losslessly into it.notesretains the verbatim source phrasing;evidenceis unchanged.percentageis deprecated (kept read-only during transition). It was anAny(float | int | string) field that, across the KB, conflated measure type, rate, unit, uncertainty, and a qualitative fallback in ~six mutually incompatible notations (audited: of 834 records, only ~5% were an actual percentage; ~18% were unit-ambiguous bare numbers, ~18% qualitative prose, the rest split acrossper N/1 in N/ Orphanet bands / explicit%). Do not populatepercentageon new records.
Migration. scripts/migrate_prevalence.py performs a non-destructive,
idempotent backfill: it parses the deterministic notations into the structured slots and
leaves genuinely ambiguous records (bare unit-less numbers, free-text head-counts)
unconverted, listing them for manual resolution in
research/prevalence_migration_report.md. Measure type is inferred only from the
percentage value and population label, never from prose notes (which routinely
mention "newborn screening" / "carrier frequency" / "incidence" as background and would
otherwise mislabel ordinary point-prevalence records); auto-defaulted measure types are
flagged for verification in the same report.
Rationale. One field cannot be both honest about imprecision and machine-queryable. Splitting the measure out, banding coarsely (Orphanet-aligned), and keeping one normalized rate gives a value that is always fillable (the band), precise when the source supports it (the rate), and never conflates incompatible epidemiological measures. Because prevalence is not yet rendered on disorder pages, the remodel carries no display-breakage risk.
Amendment (2026-09): prevalence vs. incidence — what a band means. The
measure_type discriminator above was necessary but not sufficient. Nothing below it
respected the distinction it drew: rate_per_100000 carried no dimension (a point
prevalence of 5.0 is a dimensionless proportion; an annual incidence of 5.0 is
5 per 100,000 per year), _band_from_rate() assigned Orphanet prevalence classes
purely as a function of the number, and no test or QC plugin read measure_type at all.
The KB drifted accordingly — 118 of 140 ANNUAL_INCIDENCE and 41 of 45
CARRIER_FREQUENCY records carry numeric prevalence bands (issue #8431).
Counts in this amendment are a snapshot taken on the branch that landed it
(c5496675ec, 2026-09-04), over kb/disorders/, kb/comorbidities/, and
kb/modules/. This repository merges fast enough that they drift within days —
they are recorded to show the scale of the problem the decision addresses, not
as figures to re-derive against a later tree. The audit is reproducible: walk
each prevalence: record and cross-tabulate measure_type against
prevalence_class.
Decision. A prevalence_class band reports magnitude only; measure_type
supplies the semantics. BAND_1_9_PER_100000 means "1-9 per 100,000 of whatever this
record measures" — people with the disease, new cases per year, or carriers — and is
meaningless read alone. Consumers must read measure_type alongside the band, and
should prefer the new rate_denominator slot (RateDenominatorEnum: POPULATION,
LIVE_BIRTHS, PERSON_YEARS, POPULATION_PER_YEAR) where present, which pins the
dimension explicitly instead of leaving it implied.
ANNUAL_INCIDENCE deliberately has no implied fallback. A published "annual incidence
per 100,000" is usually computed against a mid-year population, while person-year
denominators are standard in cohort studies, and the two are interchangeable only when the
population is stable. Neither is right often enough to assume, and assuming would assert a
dimension for all 138 existing incidence records, none of which were migrated with
denominator information. An unstated default costs a consumer one branch; a usually-wrong
default costs it correctness.
The qualitative tiers are the exception: COMMON, RARE, and ULTRA_RARE are each
defined by a prevalence threshold and each presuppose that the source gave no numeric
estimate. They are therefore invalid on the two measures that are definitely not
prevalence — ANNUAL_INCIDENCE and CARRIER_FREQUENCY — and should not sit alongside a
populated rate_per_100000. They remain correct on the prevalence measures, on
CASES_IN_LITERATURE, and on UNKNOWN, which is the ordinary prose-only case: a source
that says only "rare" without naming its measure. Scoping the rule this way matters — 82
of the 90 records pairing a qualitative tier with a non-prevalence measure are UNKNOWN
and legitimate; only 8 (all ANNUAL_INCIDENCE) are errors.
Rationale for this reading over the alternative. The alternative — treating bands as
strictly prevalence-typed and stripping them from incidence and carrier records — would
have required rewriting 159 KB records against a queue of 100+ open curation PRs, and
would have lost information: the only landing place for those records is a qualitative
tier, which is both less precise and itself prevalence-semantic, so the category error
would survive in a less detectable form. Worked counterexample:
2-Methylbutyryl-CoA_Dehydrogenase_Deficiency records a 21.8% Hmong carrier frequency
alongside a 1-in-227,571 Zhejiang birth prevalence; demoting the carrier record's band to
its nearest tier (COMMON) would make one file assert both that the disease is common and
that it is 1-9 per million.
The magnitude reading costs one thing: a band is no longer self-describing. There is
exactly one consumer to hold to that rule — src/dismech/templates/disorder.html.j2,
which has rendered measure_type, rate_per_100000, and prevalence_class on every
disorder page since "Render nine curated sections that no page showed" (#9510). It did not
honour the rule: it printed 1.2 per 100,000 for an annual incidence with no time
dimension, and mapped a band to a standalone self-describing label, so a carrier-frequency
record showed a tag reading "1–9 per 100,000" that a reader would take as disease
prevalence. This decision fixes that template alongside the schema — the band label is now
qualified by measure_type ("per year", "(carriers)") and rate_denominator is rendered
rather than silently dropped.
(An earlier draft of this amendment asserted the fields had no consumer. That was a stale-base error: the grep behind it predated #9510 by ~1,100 commits. Recorded here because the false premise was load-bearing for the decision, and the corrected fact — one existing consumer, already mis-rendering — argues more strongly against a 157-record rewrite, not less.)
7520 (render structured epidemiology on disorder pages) remains open and covers the
broader epidemiology sections; the prevalence part shipped ahead of it.
Accepted debt. The slot is still named prevalence_class and the class is still
Prevalence, so the names remain prevalence-flavored while the semantics are now
occurrence-general. Renaming would touch every KB record carrying a prevalence block and
was judged not worth the churn; the enum documentation carries the meaning instead.
Deferred to follow-ups. (a) A QC check gating qualitative tiers to prevalence-shaped
measure types, landing alongside the band-vs-rate check proposed in #7005 — 23
records to fix, not 8. The rule has two halves and only the first was counted initially:
8 records carry a qualitative tier on ANNUAL_INCIDENCE/CARRIER_FREQUENCY, and 20 carry
one alongside a populated numeric estimate (counting rate_low/rate_high, not just
rate_per_100000), overlapping in 5 (22 distinct files — Specific_Antibody_Deficiency
carries two such records). Five of the 18 are a distinct pre-existing data bug
— rate_per_100000: 0.0 on CASES_IN_LITERATURE records, where zero is not a rate. (b) Backfilling the 286 records that set no measure_type (a further 218 are
UNKNOWN), after which the slot can become required. (c) Whether CARRIER_FREQUENCY
belongs on Prevalence at all — it is a genotype frequency among unaffected people, and
GeneCaseFraction is structurally closer; moving it would remove 41 of the flagged
records from this class entirely.
(d) SCREEN_POSITIVE_RATE is still missing from PrevalenceMeasureEnum (#7003), the same
modeling gap one level up.
Per-gene case fractions (the genetic-spectrum analog). For a genetically
heterogeneous disease, "what share of cases does each gene explain?" is a different axis
from population occurrence — it is cohort/ancestry-dependent (e.g. BBS1 dominates European
Bardet-Biedl cohorts, BBS10 others) and needs its own population + evidence per estimate.
It was previously handled only by the overloaded free-text Genetic.frequency field
(qualitative prose such as "one of the most prevalent BBS genes") with the actual numbers
trapped inside evidence snippet: text. It now has a structured home: Genetic.case_fractions
(multivalued GeneCaseFraction), mirroring the Prevalence remodel — population (cohort)
+ case_fraction_percent (with case_fraction_low/case_fraction_high and optional
cohort_size) + evidence + notes, while frequency is retained as the coarse,
always-fillable qualitative band. This keeps the relative genetic spectrum distinct from
population occurrence (Prevalence) and from population allele frequency. Worked example:
Bardet-Biedl_Syndrome carries per-cohort case fractions across five genes — BBS1
(24.6% German / 27% metabolic / 7% Indian), BBS10 (32.8% / 30% / 10%), and the minor
genes ARL6/BBS3 (14%), MKKS/BBS6 (10%), and BBS9 (10%) in an Indian cohort — making the
ancestry-dependence of the genetic spectrum explicit (BBS1 falls from ~25% in Europeans
to 7% in the Indian cohort).
9. Imaging & detection modality representation
Decision. In-vivo imaging findings are modeled with a dedicated, ontology-bound
ImagingFinding class on the Disease entry (slot imaging_findings), the macroscopic
/ in-vivo counterpart of HistopathologyFinding. The modality (the test) and the
finding (what is seen) are represented separately, because they answer different
questions and bind to different vocabularies:
modality(ImagingModalityEnum) — a small closed set (MRI, functional MRI, CT, PET, SPECT, ultrasound, X-ray, mammography, angiography, OCT, other), withmeaning:values bound to the NCI Thesaurus Diagnostic Imaging branch (e.g.NCIT:C16809Magnetic Resonance Imaging,NCIT:C17204Computed Tomography,NCIT:C17007PET).imaging_finding_term(ImagingFindingDescriptor) — the imaging appearance, bound viaImagingFindingTermto the NCIT Imaging Finding branch (NCIT:C176708/NCIT:C199145) and/or the HP Phenotypic-abnormality branch (HP:0000118), since most radiologic observations coincide with a described phenotype (white-matter lesions, atrophy, hyperintensity). The term binding is RECOMMENDED, not REQUIRED: many specific radiologic appearances (e.g. "gadolinium-enhancing lesion") lack a dedicated NCIT/HP term and are carried onpreferred_termalone rather than being forced onto an ill-fitting code or fabricated.- Body site reuses the existing
located_in(UBERON) slot, pluslateralityandspatial_extent(focal/multifocal/diffuse), because imaging is inherently spatial. An optionalphenotype_termcross-links the finding to the HP phenotype it also maps to.
In scope. Imaging findings that are mechanistically- or diagnostically-meaningful
readouts — a lesion, an atrophy pattern, a contrast-enhancement behavior that reflects the
underlying pathophysiology or defines a diagnostic criterion (diagnostic: true).
Out of scope. Acquisition/protocol parameters, per-patient reads, and radiology
decision support — consistent with §1 (DisMech is not a diagnostic decision-support tool).
ImagingFinding is the in-vivo/macroscopic sibling of HistopathologyFinding (biopsy/
microscopy) and is distinct from the generic free-text diagnosis slot (which records that
a test was ordered / its presence-absence result) and from the molecular Biomarker* /
Biochemical machinery (lab analytes, unchanged).
Rationale. Before this decision, imaging leaked into four places — free-text
diagnosis entries (e.g. "MRI with Gadolinium Contrast"), free-text imaging_requirements
in criteria sets, HP phenotypes / UBERON anatomy, and notes/evidence prose — none of
it queryable by modality or finding. Splitting modality from finding makes "which diseases
show white-matter T2 lesions on MRI?" answerable while keeping the anti-hallucination
guarantee (every attached term must be a real NCIT/HP/UBERON term with a matching label).
Ontology choice. RadLex is the natural radiology vocabulary but is not hosted on EBI
OLS4 (it lives on BioPortal, which needs the bioportal: adapter + an API key), so it is
not wired into conf/oak_config.yaml today. The grounding therefore uses NCIT (already
OLS-served) + HP, which covers modality cleanly and findings adequately; a future
tightening to RadLex-grade finding granularity is a deferred follow-up (see §12). Because
the finding binding is RECOMMENDED, the ontology gap does not block curation.
Worked example. Multiple_Sclerosis carries two imaging_findings: multifocal
periventricular white-matter lesions on MRI (bound to HP:0007052, located_in
UBERON:0003544 brain white matter, spatial_extent: MULTIFOCAL, diagnostic: true) and
a gadolinium-enhancing lesion (modality MRI, preferred_term-only — the RECOMMENDED-no-code
case).
10. Electrophysiologic findings: phenotype post-composition, not a finding class
Decision. In-vivo electrophysiologic findings (EEG, and by extension EMG/EKG) are
modeled as ordinary phenotypes post-composed with an optional electrophysiology:
sidecar (ElectrophysiologyContext), not as a dedicated ElectrophysiologyFinding
class. This is a deliberate, principled asymmetry with imaging (§9), and it turns on a
single test:
If the finding term already lives in the phenotype ontology, it belongs in
phenotypesand needs no separate class. If it doesn't, it needs one.
Imaging findings bind to the NCIT Imaging Finding branch and histopathology to NCIT
morphology — vocabularies outside HP — so ImagingFinding / HistopathologyFinding
exist to give those terms a home, and their modality axis (MRI vs CT vs PET) is a real,
queryable dimension with its own NCIT branch. Electrophysiologic findings are different on
both counts: the terms are HP phenotypes (the EEG subtree HP:0002353, EMG HP:0003457,
EKG HP:0003115 all descend from HP:0000118), so they are already correctly typed as
phenotypes; and the "modality" is near-degenerate (almost always EEG, and implied by the
term itself). Both pillars that justified a dedicated imaging class collapse for EEG, so a
sibling class would only re-home terms that were already phenotypes and bolt on a
low-value modality axis.
The sidecar. A phenotype whose phenotype_term is an EEG/EMG/EKG finding may carry an
optional electrophysiology: block (ElectrophysiologyContext) with exactly the axes a
flat HP term cannot express:
electrophysiology_modality(ElectrophysiologyModalityEnum; EEG, video-EEG, ECG, EMG, NCS, evoked potential, PSG, MEG) —meaning:bound to the NCI Thesaurus diagnostic-procedure branch (NCIT:C38054EEG,NCIT:C38053ECG,NCIT:C38056EMG).ictal_state(IctalStateEnum: ICTAL / INTERICTAL / POSTICTAL).recording_state(EEGRecordingStateEnum: awake / asleep / drowsy / sleep-deprived / photic-stimulation / hyperventilation).
This is the same post-composition move dismech already uses for temporality,
clinical_course, severity, and onset on descriptors — the EEG-specific qualifiers just
travel in a named sidecar so they don't pollute the generic PhenotypeDescriptor.
Localization/laterality/extent reuse the descriptor slots already on phenotype_term; the
HP term (e.g. "EEG with focal epileptiform discharges") usually already carries them.
Preclinical / no-HP-term findings stay phenotypes too — a preferred_term-only
phenotype (no bound term:) carrying the sidecar, e.g. an animal-model electrographic
seizure (no HP term exists), tagged evidence_source: MODEL_ORGANISM. This keeps ictal
model-organism EEG alongside the interictal human findings instead of stranding it in prose.
Rationale / history. An ElectrophysiologyFinding sibling class was first built by
analogy to §9, then reverted once the analogy was checked and found not to hold (EEG
terms are HP phenotypes; imaging terms are not). Recording the reversal here so the register
reflects the corrected reasoning, not the false symmetry.
Category is already HP-derived — the sidecar does not touch it. The disorder-page
renderer does not group phenotypes by the free-text phenotypes.category string; it
derives the broad category from the phenotype_term's HPO ancestry, walking
rdfs:subClassOf to the 22 top-level children of HP:0000118 (HpoCategoryProvider /
HPO_TOP_LEVEL_CATEGORIES, already codified as the PhenotypeCategoryEnum in
schema/classifications/phenotype_category.yaml). EEG findings roll up to Nervous
System (HP:0000707), so that is the correct category value — not a novel
"Electrophysiologic" bucket. The EEG-ness is carried entirely by the electrophysiology:
sidecar, not by the category, which is exactly why the sidecar exists: it adds the
electrophysiologic axes without disturbing the organ-system categorization.
No category constraint is wanted here. A rule of the form category = X ⇒ phenotype_term
under X would be circular — the category is derived from the term's HP ancestry, so it
has no independent content to check — and the 22 top-level categories are too coarse to pick
out "EEG finding" anyway (EEG rolls up to the whole Nervous System). The sidecar is an
optional post-composition qualifier exactly like temporality / clinical_course /
severity / onset, none of which are category-gated or rule-enforced; this one follows
the same convention-over-constraint pattern. The only guardrail that would even type-check is
"sidecar present ⇒ term under HP:0002353/0003457/0003115 or term-less (the preclinical
preferred_term-only case)", and that is at most an advisory lint, not a schema rule.
Deferred (see §12). Independently of EEG, the PhenotypeCategoryEnum already exists but
is not yet wired to the phenotypes.category slot (still range: string); binding it, or
deprecating the hand-entered field in favour of the HP-derived value, is a separate cleanup.
Worked example. Dravet_syndrome carries five EEG phenotypes (category: Nervous
System) — the four interictal human patterns (multifocal / focal / generalized epileptiform
discharges and interictal epileptiform activity, HP-bound, sidecar ictal_state:
INTERICTAL), plus one preclinical preferred_term-only phenotype — ictal electrographic
seizures in the Scn1a+/- mouse model (sidecar electrophysiology_modality: EEG,
ictal_state: ICTAL, evidence_source: MODEL_ORGANISM).
Linking investigation-readout phenotypes into the pathograph (reports_on). Many HP
terms are investigation results rather than states of the organism — an abnormal
electroretinogram (HP:0000512), an abnormal EEG, an elevated circulating creatine kinase
concentration. They are legitimately HP phenotypes and stay in phenotypes (per the test
above), but functionally they are readouts of an underlying mechanism, not causal
participants in disease progression. As a result they tend to float as disconnected nodes in
the pathograph: nothing lists them as a downstream target and they carry no sequelae. The
tempting fix — adding a downstream edge mechanism → Abnormal ERG — is wrong, because a
downstream/causes edge asserts causal disease progression, whereas the test merely
measures the mechanism.
Decision. A phenotype may carry a reports_on: list (PhenotypeReadout) linking it to
the pathograph node whose underlying state it measures or reflects, exactly mirroring the
Biochemical.readouts (BiomarkerReadout) mechanism already used for molecular biomarkers.
It reuses the same BiomarkerReadoutRelationshipEnum (READOUT_OF / CORRELATES_WITH /
PREDICTS / PHARMACODYNAMIC_MARKER_OF), direction, and endpoint-context vocabularies, and
renders as the same dashed observational edge (mechanism -.-> readout, graph.py
predicate: readout) — not a solid causal arrow. PhenotypeReadout is deliberately the
lean counterpart of BiomarkerReadout: it omits the surrogate-endpoint/regulatory slots
(regulatory_endpoint_refs and the FDA source-table bridge) that belong only to molecular
biomarker readouts. This keeps the term where HPO places it (phenotypes), preserves the
"reports-on ≠ caused-by" distinction the schema already encodes for biomarkers, and makes the
otherwise-orphan test-result phenotype a first-class, evidenced pathograph edge.
Worked example. Bardet-Biedl_Syndrome's Abnormal electroretinogram phenotype now
reports_on the Photoreceptor outer-segment transport defect pathophysiology node
(relationship: READOUT_OF, direction: NEGATIVE, endpoint_context: DIAGNOSTIC),
replacing the previous — semantically incorrect — downstream causal edge from the
mechanism to the ERG. The ~200 Elevated/Decreased circulating … concentration lab-readout
phenotypes are candidate backfills (tracked in §12).
11. Reader-facing disclaimers (AI curation & not medical advice)
Decision. Every reader-facing DisMech page carries a disclaimer bar stating two
things: that the resource is AI-curated and AI-maintained, and that it is not medical advice.
A single canonical long-form statement lives in docs/disclaimer.md; the
bar links to it. The bar is shown by default on every page and may be dismissed for the
browsing session only — see the "Dismissible for the browsing session" design point below
for the scope and its rationale. (This paragraph originally read "persistent,
non-dismissible"; revised by
#7421.)
Why page-level rather than documentation-level. §7 already records that DisMech is agent-forward, and §6 records the evidence policy — but both are contributor-facing process documentation. The common way a reader encounters DisMech is by landing on a single disorder page from a search engine or an external link, never seeing the project documentation at all. Provenance and fitness-for-use therefore have to travel with the page.
Where it is implemented.
| Surface | Mechanism |
|---|---|
| Generated KB pages (disorder, module, grouping, comorbidity, classification, project, research, and their index pages) | src/dismech/templates/_disclaimer.html.j2 + _disclaimer.css.j2, {% include %}-ed into every full-page template (the same partial pattern already used for _palette.css.j2) |
Hand-maintained site pages (index.html, app/, details/) |
The same markup inlined, kept in step with the partial |
MkDocs documentation site (elements/) |
copyright: footer in mkdocs.yml, plus admonitions on docs/index.md and docs/about.md |
| Repository | Disclaimer section at the top of README.md |
Design points.
-
Dismissible for the browsing session. (Revised by #7421; this decision originally read "Not dismissible".) The bar carries a close button, and the closed state is recorded under the
dismech-disclaimer-dismissedsessionStorage key. sessionStorage is scoped to a single tab and cleared when that tab closes, so moving between DisMech pages in the same tab keeps the bar dismissed, while a fresh tab or a later visit shows it again.Deliberately not localStorage. Permanent dismissal would let a reader silence the statement once and never see it again. Session scope keeps the escape hatch — the bar is a banner, and a reader working through twenty disorder pages should not have to see it twenty times — without turning "I have read this" into "never tell me again". A test asserts the localStorage API is not used by the disclaimer script on any surface, so this cannot regress by accident. Deliberately scoped to that script rather than to whole files:
disorder.html.j2legitimately uses localStorage elsewhere, for its Q&A answer cache.One consequence worth knowing: sessionStorage follows the tab, not the page load. A reader who dismisses the bar and then types a DisMech URL into that same tab will not see it again; it takes a new tab (or a later visit) to bring it back.
Mechanics worth keeping: the control is a real focusable
<button>with an accessible label and tooltip, not the click-anywhere.notice-bannerbehaviour, which would fight the link inside the bar. It is CSS-hidden by default and revealed by the script, so a reader without JavaScript is never shown a control that cannot work. The script is inline and synchronous directly after the bar, so an already-dismissed bar is hidden before first paint instead of flashing. EverysessionStorageaccess is inside atry, because the property getter itself throws when storage is blocked; the bar then simply stays visible. Dismissing moves focus to the page'smain/h1rather than letting it fall back to<body>, so a keyboard reader keeps their place.Layout: the bar is a flex row and the button sits in flow, so it cannot overlap the text however the bar wraps. The button carries a 28px right margin to clear the Hypothes.is sidebar, which mounts a toolbar over the right edge of every page that embeds the annotation client (
disorder.html.j2,module.html.j2) and swallows clicks there. This is not cosmetic: measured in headless Chromium, a button atright: 8pxis unclickable on disorder pages at both 1280px and 375px wide. The clearance is therefore not dropped on narrow viewports. -
A dismissed page still carries the statement. Because the bar can be closed, generated disorder and module pages also carry
_disclaimer_footer.html.j2— a one-line, non-dismissible disclaimer in the page footer, worded to match the MkDocscopyright:footer. Without it, the reader §11 exists for (one disorder page, arrived from a search engine) could dismiss the bar and be left on a page with no disclaimer at all: the MkDocscopyright:footer renders only ondocs/pages, andrender.pypages have their own footer. Known gap: disorder and module are the only full-page templates with a<footer>, so grouping, comorbidity, classification, project and research pages have no footer line; on those, a dismissed bar does leave the page bare until the session ends. - Distinct from
.notice-banner. The pre-existing.notice-banner(pre-alpha content warning) is also dismissible, but only for the page view — it just removes itself and returns on the next page. The disclaimer is styled neutral grey rather than amber, and holds its dismissal for the session, precisely so the two read as different things when they appear together. - Top of page, not footer. A reader who leaves after the first screen must still have seen it. (The footer line above is an addition for the dismissed case, not a relocation.)
- One canonical wording.
docs/disclaimer.mdis the source of truth; the banner is its summary.tests/test_disclaimers.pygates that every full-page template and every hand-maintained site page still carries the disclaimer, so a new template cannot silently ship without one — and, since #7421, that each of them also carries the dismiss control, the shared sessionStorage key (and not the localStorage API), and the[hidden]rule that makes dismissal actually hide the bar (display: flexon the bar outranks the user-agent rule for thehiddenattribute).
Scope. Internal/derived QC surfaces are excluded — they present curation-completeness
metrics, not disease claims: dashboard/ (generated by just gen-dashboard) and
frontpage-candidates/ (a design-candidate gallery). pages/nih-topics/index.html is
included despite being a coverage report, because it sits under pages/ alongside the
disease surfaces and links out to disease and project pages; it is generated from an inline
template string in scripts/gen_nih_topics_summary.py rather than from
src/dismech/templates/, so it is covered by its own test rather than by the template glob.
Origin. #7182. Revised by #7421 (dismissible for the browsing session, plus the non-dismissible footer line).
12. Gaps
This section details decisions we have not yet made or formalized.
| Area | Status | Tracking |
|---|---|---|
Experiment-grounded evidence (experiment.design / inference.role) |
Design exploration, not yet a schema change. The EvidenceItem model is a validated citation-pointer (real reference + exact snippet + validator = citation integrity) with a thin appraisal layer — supports is polarity, evidence_source is a coarse organism bucket, and neither records what experiment produced a claim or how the mechanistic edge was inferred from it. Proposal: an optional experiment{design, system, perturbation, readout, result, inference} block plus two small closed enums — experiment.design (how it was shown) and inference.role (necessity / sufficiency / rescue / direct-physical / therapeutic-rescue, what the result licenses about the edge), mutually constraining so strength is derived, not authored and experiment.result.snippet stays substring-validated. Bespoke enum preferred over ECO (which types entity→term annotations, not causal-graph assertions); SEPIO reserved for the export layer. Worked on the FH PCSK9 sub-graph. |
The Evidence Model · FH worked example |
| Chromosomal-disorder curation guidelines | Not yet written; domain-specific extension of this register | #3756 |
Structural knowledge_gaps: schema slot |
Deferred; knowledge gaps currently modeled via discussions (kind: KNOWLEDGE_GAP) |
schema follow-up |
Measurement context on ExperimentalReadout (spatial position, sampling rate, named comparator) |
Open; narrower than first recorded. model_scale and divergences now cover the scale and shortfall axes on ModelMechanismLink, and a NAM curation pass populated both across eight links. What they do not cover is positive metadata about how a measurement was made: an internal spatial comparator (organoid interior vs. edge in the same construct — STRUCTURAL_IDEALIZATION is the wrong shape, since the spatial structure is the model's strength), a sampling rate (20 ms light-sheet calcium imaging, currently in free-text culture_system; TEMPORAL_SCOPE types a mismatch, not a rate), and the comparator arm that ModelReadoutDirectionEnum is explicitly defined against but which no slot names — a readout reading DECREASED against epicardium-free tissue rather than untreated tissue is nearly meaningless without its prose. The comparator is the narrowest and most tractable piece and every readout in the KB inherits it. Weigh against sparse population of the slots that already exist. assays is populated on several hundred readouts but is never ontology-bound: every entry carries a bare preferred_term and not one carries a term:, because OBI is absent from conf/oak_config.yaml and has no cache/enums/ membership cache, so a curator who tried could not validate it. Scale is near-universally UNDETERMINED. Both counts move with every curation pass and are deliberately not pinned in this register — read them from just model-scale-audit and from a term:-under-assays: scan of kb/. (An earlier revision of this row said assays was used on 0 of 289 readouts; the slot is used, it is the ontology binding that is at zero.) |
Spatially resolved in vitro models |
would_support / would_refute range |
ENACTED (#9224). These two Experiment slots hold entity references only — the [<file>:]<kind>#<name> grammar shared with attaches_to — and name what a result bears on. A prose statement of what would be observed goes in the sibling supporting_outcome / refuting_outcome slots. The alternative (widen the reference slots to accept both forms and split on whitespace at render time) was rejected: the two are different types, not two spellings of one. "No enrichment of these lesions in tissue would indicate that the dominant clinical resistance mechanism lies outside the bypass lesions currently modeled at this node" is a conditional inference with no referent, and a slot whose meaning turns on whether its value contains a space cannot be exported. The ~51 prose values that motivated the issue have been migrated (zero remain across kb/), and the anchors now resolve: render._build_semantic_ref_index is driven by entity_refs.SECTION_KEYS (#9193), so 562 of 564 references in these slots render as live in-page links rather than dead chips — the 2 exceptions name diagnosis and prevalence, sections the disorder page renders no card for, which is a page-coverage gap rather than a modeling one. Gated by check_entity_ref_foreign_keys, which now fails a prose value, an unknown <kind>, or a dangling anchor in these slots, with no baseline — the backlog is zero, so a finding is always newly introduced. Not precluded: if a structural knowledge_gaps: slot (#2617) later wants a ModelMechanismLink-shaped object carrying a target plus qualifying prose plus its own evidence, this decision is compatible with it — the prose lives in a named slot either way. |
#9224 · #9193 |
| Hypothesis-exploration report assessments and reconciliation | ENACTED (assessment: PR #7017; reconciliation: 2026-08-29; data/analysis provenance: 2026-08-29). A focused hypothesis report is a research lead, not disease-level curated evidence. One standalone LinkML-validated YAML sidecar is stored for each <provider>-assessment-by-<assessor> pair under kb/hypotheses/<Disease>/<hypothesis_id>/assessments/; optional Markdown/PDF files with the same stem are human-readable renderings. Each sidecar captures an overall qualitative verdict plus claim-level RETAINED / QUALIFIED / REJECTED / NEEDS_VERIFICATION dispositions, each optionally anchored to the raw report. It also inventories material data_sources and provider analyses: data access distinguishes accessed, negative-search, cited-only, and unverifiable sources; execution status distinguishes succeeded, partial, failed, skipped, and reported-only work; and claims link to the analyses to which the provider attributes them. Status and auditability determine whether execution actually supports the claim. SUCCEEDED is intentionally limited to artifact-backed, reproducible execution with recorded inputs, method, versioned software, code, environment, and outputs. Failed-tool and lower-fidelity fallback lineage is explicit, so prose or model knowledge cannot silently replace computation. Manifests, code, environment records, and small derived outputs are committed; large recoverable raw data, provider lakes, controlled data, and secrets remain external with stable identifiers and checksums. When comparison is useful, one authoritative reconciliation.yaml at the hypothesis root reconciles at least two separately assessed reports; it never replaces the source assessments (which may share an assessor, but must review each report on its own). Each provider input links both its report and selected assessment, and every reconciled claim records all providers' stances; every non-silent position also records source assessment claim IDs, a verbatim report quote, and claim lineage. Lineage distinguishes new-source discovery, new extraction from a seed-cited source, provider analysis, provider inference, seed-derived repetition, and prior-provider-derived repetition. A provider-reported but unauditable analysis remains visible as REPORTED_ONLY, but is not verified execution or independent computational support. Inherited claims, shared code, shared derived results, and shared upstream outputs are not independent convergence; use of the same public accession requires comparison of cohort, method, parameters, and outputs before calling analyses independent. The reconciliation assigns its own evidence-based claim dispositions and overall verdict; provider majority and citation count do not determine truth. Validation enforces layout, source and artifact containment, metadata and foreign-key consistency, data/analysis/claim links, the execution-auditability matrix, fallback and claim lineage, all-provider claim coverage, and report-quote anchoring. Literature identifiers and provider-derived datasets in either artifact remain review context, not disease-YAML evidence; promotion still requires normal reference-cache, dataset, and evidence validation. This hypothesis-local artifact is distinct from the broad disorder-level research/<Disease>-research-synthesis.yaml. |
src/dismech/schema/hypothesis_assessment.yaml; src/dismech/schema/hypothesis_reconciliation.yaml; docs/hypothesis-report-assessments.md |
| Hypothesis-based phenotype algorithms | ENACTED (2026-07-12, @cmungall-approved). definition_type: PHENOTYPE_ALGORITHM previously assumed established/validated grounding. Definition now carries an orthogonal derivation_basis (ESTABLISHED_CRITERIA / MECHANISTIC_HYPOTHESIS / MODEL_SYSTEM_EXTRAPOLATION); reuses the existing attaches_to slot to link the pathograph node(s)/edge(s) it is predicated on (so the hypothesis basis is inferred from those edges' hypothesis_groups → mechanistic_hypotheses[].status, not stored as a drift-prone duplicate ID); and a structured validation_status object (AlgorithmValidationStatus: status enum PROPOSED / UNVALIDATED / VALIDATED_AGAINST_GOLD_STANDARD + free-text rationale + optional evidence). Net effect: a mechanism-predicated EHR case-finding query (e.g. fever-triggered arrhythmia surfacing latent CACNA1C carriers) is not conflated with a consensus/OHDSI-validated phenotype. Gated by check_hypothesis_based_definition_attaches_to_foreign_keys (a MECHANISTIC_HYPOTHESIS definition must have resolving attaches_to refs). Worked examples spanning the spectrum: Timothy_Syndrome (fever_exacerbated_cav1.2; MECHANISTIC_HYPOTHESIS/PROPOSED, zebrafish), Brugada_Syndrome (fever-unmasking of the type-1 ECG; ESTABLISHED_CRITERIA/UNVALIDATED), Long_QT_Syndrome (QT-prolonging-drug unmasking of latent congenital LQTS; ESTABLISHED_CRITERIA/UNVALIDATED — a pharmacological rather than physiological trigger), and Malignant_Hyperthermia_of_Anesthesia (anesthetic trigger, skeletal-muscle RYR1/CACNA1S; ESTABLISHED_CRITERIA/UNVALIDATED — the first non-cardiac example, whose definition attaches_to the entry's existing trigger node). See hypothesis-based-phenotype-algorithms.md and the candidate register in reports/hypothesis-driven-ehr-case-finding. Remaining follow-ups: advisory declared-vs-inferred consistency lint; renderer badge; KGX/BioLink export treatment (suppress or specially mark). |
#6245 |
updated_date field |
Deprecated in favor of git history; legacy entries may retain it pending bulk cleanup | — |
Deprecated prevalence.percentage cleanup |
percentage superseded by structured prevalence slots (§8) and deprecated. The bare-number unit-ambiguity backlog is effectively resolved: of 199 records, 166 are converted via scripts/resolve_bare_prevalence.py plus reviewed batches — 91 low-value rare-disease prevalences, 47 high-percent population/cohort prevalences (conditional ones qualified by their population field), 9 hand-fixed DISAGREE, and 19 final records (12 uncorroborated-but-legit + 7 filter false-positives) using the rule decimal = percent, scientific-notation = proportion (e.g. CHIME 1e-06 = 1/million; Cockayne 4e-06 = 1/250,000; carrier/birth measures set where stated). All additive; percentage preserved. The 33 not converted are not a unit problem: 32 are records that are not population prevalence at all (MISPLACED_STAT in research/prevalence_bare_number_report.md — metastatic-cancer 5-year survival, staging fractions, complication rates, and fraction-of-category such as "X% of all lymphomas/leukemias/cancers"), which belong in a different slot and need relocation, not unit-fixing — a distinct data-quality task pending a schema home for survival/staging/subtype-share data; plus 1 genuinely-ambiguous record (Nephronophthisis 0.1-1.0, neither a clean percent nor proportion with no corroborating evidence). Plus ~8 free-prose head-counts. percentage field removal is deferred until the misplaced-data relocation lands. Post-migration correction (PR review): a systematic scan found 19 records across 16 files where a fraction-of-category or penetrance value (with the qualifier living in notes, so the percentage-only guard missed it) had been wrongly converted to a population rate_per_100000 — e.g. Osteogenesis_Imperfecta_Type_II 50% (half of prenatal-onset OI cases → 50,000/100k), HPAH/FXTAS carrier penetrance (~40% → 40,000/100k), Minimal_Change_Disease (70–90% of idiopathic NS), Cholesteatoma (419/1710 otitis-media patients). These had their measure_type/prevalence_class/rate_* slots stripped (bare percentage preserved). The migration guard was hardened accordingly: FRACTION_OF_CATEGORY_RE now also matches cohort head-counts (N of M) and % of <solved/idiopathic/sporadic/typhoidal/…> categories stated in the percentage, and a new PENETRANCE_RE (safe to run against notes) catches penetrance/lifetime-risk qualifiers. Bare-percentage cohort fractions whose qualifier is only in prose remain inherently ambiguous from the value alone and are corrected by hand rather than by an aggressive notes scan (which would false-positive on records like Lathyrism, whose notes cite a cohort count but whose percentage is a genuine population estimate). Second correction batch (PR review): a follow-up KB-wide scan surfaced a further class of measure-type/conditional errors on rate-bearing records — (a) genotype-conditional cumulative incidence / penetrance stated as "N% diagnosed by age X" (Hemochromatosis male C282Y homozygotes 56.4% by age 80) or "cumulative risk of new cases up to age N" (Oppositional_Defiant_Disorder), which were stripped like the penetrance records; (b) wrong measure_type where the type lived only in notes/snippet — lifetime prevalence tagged POINT (Anorexia_Nervosa, Migraine_with_Aura → LIFETIME_PREVALENCE) and 12-month prevalence tagged POINT (Obsessive-Compulsive_Disorder → PERIOD_PREVALENCE); (c) cohort-conditional risk-factor rates (Furunculosis S. aureus nasal-carriage 60%/36%, Acute_Hypotension 88% intraoperative-event rate in ASA 3–4 surgical patients), stripped; and (d) a two-figure percentage where the parser captured the incidence not the prevalence (Systemic_Lupus_Erythematosus North America "23.2/100k incidence; 241/100k prevalence"), split into separate POINT_PREVALENCE (241) and ANNUAL_INCIDENCE (23.2) records. PENETRANCE_RE was extended with cumulative incidence/risk and diagnosed by age (verified against the KB to add no false positives on legitimate rate-bearing records). Third correction batch (PR review): a further scan found cohort-conditional / diagnostic-procedure rates whose qualifier lives only in the population label (not percentage/notes), which the guards do not parse: e.g. FICUS_syndrome (PICS-F among ICU family members), Coronary_Vasospasm (spasm among ANOCA patients), Refeeding_Syndrome (event rate in hospitalized/PN patients), Aortitis (histology among aortic-surgery patients), Brucellosis (pooled prevalence among included study populations), Silent_Sinus_Syndrome (radiologic finding among head-CT patients), Laryngotracheoesophageal_Cleft (proportion among endoscopy referrals) — structured slots stripped. Plus three measure_type corrections to BIRTH_PREVALENCE (Klinefelter_Syndrome, Wolf-Hirschhorn_Syndrome, MECP2_Duplication_Syndrome) where the birth-prevalence language was in the snippet only. Population-label conditionality is deliberately not auto-guarded: the label alone cannot separate a selected referral cohort ("adults undergoing head CT") from a legitimate large-scale screening population that approximates the general rate ("Pregnant women undergoing genome-wide NIPS", 333,187 women → 6.9/100,000), so this class stays manual-review. |
migration follow-up + schema follow-up (destination for survival/staging/subtype-share) |
Per-gene case_fractions backfill |
New structured Genetic.case_fractions slot added (§8). Bardet-Biedl_Syndrome backfilled for five genes (BBS1, BBS10, ARL6/BBS3, MKKS/BBS6, BBS9) across European, metabolic, and Indian cohorts. Method/caveat: dominant-gene fractions (BBS1, BBS10) appear in citable abstracts; minor-gene fractions are recoverable only from open-access full-text cohort papers/reviews whose cache is full_text_xml (the Indian-cohort figures came from PMID:27853007), since abstracts and the GeneReviews table (NBK1363 T3) and the Niederlová meta-analysis abstract (PMID:31283077) do not carry them. Backfilling the remaining minor genes is gated on finding such full-text-cacheable sources — figures must not be filled from memory (anti-hallucination policy, §6). Whether to deprecate the overloaded frequency field is also outstanding; no automated extractor yet. |
schema follow-up |
KGX export of differential_diagnoses / diagnosis |
Not yet exported; candidate predicate biolink:disease_has_differential_diagnosis |
#2100 |
| RadLex-grade imaging-finding granularity | ImagingFinding (§9) grounds findings in NCIT + HP, which is patchy for specific radiologic appearances (e.g. contrast enhancement, T2 hyperintensity resolve to procedures or CTCAE grades). Tightening ImagingFindingTerm to a RadLex reachable_from (and finding_term to REQUIRED) is deferred: RadLex is not on EBI OLS4, so it needs a bioportal: adapter + API key in conf/oak_config.yaml. |
schema/ontology follow-up |
| Non-imaging detection modalities | Resolved for electrophysiology (§10) via phenotype post-composition (an electrophysiology: sidecar carrying modality + ictal_state + recording_state), not a finding class — because EEG/EMG/EKG terms are already HP phenotypes. Dravet_syndrome is the worked example. Still open: functional/provocation tests (e.g. tensilon, tilt-table) remain free-text diagnosis. |
schema follow-up |
Investigation-readout phenotype backfill (reports_on) |
New lean PhenotypeReadout slot added (§10): investigation-result phenotypes (abnormal ERG/EEG, Elevated circulating … concentration) attach to the mechanism they measure via a dashed observational readout edge instead of floating as orphan nodes or being mis-wired as causal downstream edges. Bardet-Biedl_Syndrome (Abnormal electroretinogram → Photoreceptor outer-segment transport defect) is the worked exemplar. First batch done (scripts/migrate_readout_phenotypes.py): 69 mis-wired causal edges across 60 files migrated to reports_on — restricted to pure lab/investigation readouts that are never themselves disease drivers (tissue-leakage enzymes: transaminases/CK/LDH/aldolase/ALP; acute-phase reactants; tumor markers AFP/β-hCG; newborn-screening acylcarnitines; the electroretinogram), HP-verified via descendants of HP:0032180/HP:0034684/HP:0010876/HP:0003111/HP:0030453. Deliberately NOT flipped: ~179 causally-active analytes where the downstream edge is correct — ammonia (→ encephalopathy), lactate (→ acidosis), vitamins (deficiency → neuropathy/retinopathy), cholesterol, hormones, ions, immunoglobulins — plus any readout carrying its own sequelae. Second batch done (floating pure readouts): 55 reports_on links added across 47 files by a parallel curation pass, each choosing the best-fit existing mechanism node (liver enzymes → hepatocyte-injury node, CK/aldolase/LDH → myofiber-necrosis node, ERG/EOG → photoreceptor-degeneration node, CRP/acute-phase → inflammation node, AFP/β-hCG/tryptase → tumor/mast-cell node, bone ALP → osteoblast node). ~14 deliberately left unlinked where the disease pathograph has no node the organ-injury lab measures (e.g. transaminases in Graves/Celiac/Stevens-Johnson, the Murine-typhus organ-injury labs) — these are genuine modeling gaps (the entry doesn't yet represent that organ's involvement), not readout-link gaps, and were skipped rather than invent a node. Open: the ~58 non-pure floating readouts (causally-active analytes) and the modeling-gap skips; causally-active analytes could also optionally gain a second reports_on link alongside their (correct) causal edge where the value is used diagnostically. |
KB migration (batches 1–2 done) |
Wire the existing PhenotypeCategoryEnum to phenotypes.category |
The renderer already derives each phenotype's organ-system category from its HPO ancestry (HpoCategoryProvider → the 22 top-levels, codified as PhenotypeCategoryEnum in schema/classifications/phenotype_category.yaml), so the hand-entered category (still range: string, ~200 inconsistent values, ~4k blank) is not what drives display. The cleanup is to bind that enum to the slot and/or deprecate the free-text field in favour of the derived value — not to invent new category values. (Note: category-gated rules are a non-goal — the category is derived from the term, so such a rule would be circular; see §10.) |
schema follow-up / KB migration |
| Histopathology (NCIT) vs phenotype (HP) boundary | Undecided — maintainer call outstanding. HistopathologyFindingTerm binds the NCIT Histopathology Result branch (NCIT:C83490) plus a narrow HP:0025461 (Abnormal cell morphology) carve-out; HP covers many organ-specific microscopic findings (foot-process effacement, ragged-red fibers) that fall outside both. Four questions are open: (1) should finding_term bind HP beyond HP:0025461, and what is the NCIT-vs-HP selection rule; (2) HP+NCIT dual-coding, mirroring the HP+MONDO disease-like-phenotype precedent (§4); (3) the authoritative phenotypes vs histopathology rule for a microscopic observation — §10's test ("if the term already lives in the phenotype ontology it belongs in phenotypes") answers the class-existence question but not the slot-choice one; (4) whether entity-level "findings" (Barrett esophagus, Castleman variants, the DNET glioneuronal element) should move to disease_term/subtype — independent of the vocabulary question. Re-census (2026-08-18) reframes the options: 325 of 707 findings (46%) across 188 files are unbound (up from 123/76 at the 2026-07-02 triage), the HP:0025461 carve-out carries almost no load (14 bound findings vs 368 NCIT), and the unbound tail is not a recurring-vocabulary gap — 324 distinct labels for 325 findings, 58% of them post-composed clauses vs 20% of bound ones. So broadening the HP root reaches at most the ~135 single-concept findings. Meanwhile 0 of 707 findings use any of the located_in/modifier/laterality/spatial_extent/severity slots HistopathologyFindingDescriptor already inherits from Descriptor — undocumented on that class, unlike its ImagingFindingDescriptor sibling — making "bind the head term, post-compose the rest" a fifth option needing no schema change. |
#5140 · re-census · 2026-07-02 triage |
| Heteroplasmy and genome of origin for mtDNA disease | Undecided — proposed, not enacted. Gene assignment binds HGNC CURIEs on GeneDescriptor.gene_term with no reachable_from constraint. That is complete for mitochondrial genetics at the identifier level — all 37 mtDNA-encoded genes resolve in HGNC with correct labels and SO types — but two things it cannot express. (1) Genome of origin. No HGNC gene group unites the 37 (hgnc.genegroup:1974 covers only the 13 protein-coding genes; tRNAs sit in 843, rRNAs in 1378), and in the OAK sqlite build every gene-group node is a label-less rdf:type stub, so a reachable_from enum on it would be unlabeled and incomplete. SO type does not separate genomes either (MT-ND1 and NDUFS4 are both SO:0001217). The only current signal is the MT- symbol prefix — a naming convention, not an assertion. (2) Heteroplasmy. ZygosityEnum has no homoplasmic/heteroplasmic values, and heteroplasmy is orthogonal to zygosity in any case; the fraction and its tissue-specific threshold are what determine penetrance and severity for an mtDNA disease. The concept appears in 20 files as free text only. Proposed shape: an optional Genetic.genome (GenomeEnum: NUCLEAR | MITOCHONDRIAL) plus an optional Genetic.heteroplasmy block (state: HOMOPLASMIC | HETEROPLASMIC | BOTH, threshold_percent, threshold_tissue, standard evidence). genome is mechanically backfillable over a closed 37-gene set; heteroplasmy needs per-entry curation from the 20 prose files. Both additive and optional, so legacy entries validate unchanged. |
report |
| Quantity kind: analyte amount vs catalytic activity | Deferred (2026-08-26). MP splits these into two disjoint upper-level branches — abnormal level terms under MP:0001764 abnormal homeostasis (e.g. MP:0005319 abnormal enzyme/coenzyme level), abnormal activity terms under MP:0005266 abnormal metabolism (e.g. MP:0005584 abnormal enzyme/coenzyme activity). HPO does not: both live under HP:0001939 (Abnormality of metabolism/homeostasis), and the merge is explicit in the labels — HP:0034684 Abnormal enzyme concentration or activity ("Concentration or activity of an enzyme is above or below the limits of normal"), with HP:0012379 its circulating child. HPO will even file an activity term under a level term: HP:0003282 Decreased circulating alkaline phosphatase activity has parent HP:0004379 Abnormality of alkaline phosphatase level. That is faithful to clinical chemistry (ALP is assayed as an activity in U/L and reported as a "level") but is not reasonable over. Why the axis matters mechanistically: amount and activity dissociate in all four combinations and the combination is the lesion — normal amount / low activity (catalytically dead missense, cofactor deficiency, inhibitor present); low amount / normal specific activity (nonsense, unstable protein); high amount / low activity (accumulated misfolded protein, compensatory upregulation). It bears directly on curated content: pseudodeficiency alleles (ARSA/HEXA — low in-vitro activity, no disease; Metachromatic_Leukodystrophy, Krabbe_Disease, Late-Onset_Pompe_Disease, Hurler_syndrome, Multiple_Sulfatase_Deficiency), pharmacological chaperones (migalastat) whose whole mechanism is restoring activity of protein that is already present, ERT raising activity without changing endogenous level, cofactor-responsive disorders, and the residual-enzyme-activity genotype-severity question raised in 173 disorder entries. What DisMech does today: draws the distinction by slot, not by subsumption — amount in biochemical: (presence, reference_ranges + unit), catalytic capability as a GO molecular function on a pathophysiology node carrying modifier: (PATO-bound PATO:0002300/PATO:0002301). That is the compositional form of what MP encodes as a branch split, and it is orthogonal to the quantitative-vs-qualitative axis (INCREASED vs GAIN_OF_FUNCTION, see CLAUDE.md). Residual gap: biochemical: is DisMech's own HP:0012379 — it holds enzyme activity assays alongside analyte concentrations, distinguished only by free-text name and reference_ranges.unit (U/L vs nmol/mg protein/hr vs mg/dL). Farber_Disease ("Reduced acid ceramidase activity") and Alpha_Mannosidosis ("Reduced acid alpha-mannosidase activity") each carry such a record (presence: DECREASED, no bound biomarker_term.term) and a same-named GO MF node with modifier: DECREASED; those are two different assertions — an assay result and a mechanism claim — and should not be collapsed into one. Deferred decision: do not add a level/activity enum or restructure anything. The candidate, if the axis ever needs to be queryable, is an optional quantity kind slot (amount / catalytic activity / flux) on Biochemical and ExperimentalReadout — the one thing MP gets from its branch split and HPO discards. Until then the distinction rides on unit discipline in reference_ranges and on binding a GO molecular function for every activity claim. Worked examples (2026-08-28). Mouse models were curated from the MGI genotype annotations that sit on each MP branch, chosen from genes annotated on both: Hypophosphatasia (the Alpl null on the level branch vs. the Alpl A116T knock-in on the activity branch, 50% residual plasma activity with a normal postcranial skeleton), Mucolipidosis_Type_II (Gnptab gene-trap — serum hydrolase levels up while M6P tagging is abolished, the two directions curated as readouts on one link), Menkes_Disease (Atp7a brindled — SOD3 specific activity down, partially restored by adding copper, with SOD1 unaffected), and Gaucher_Disease_Due_To_Saposin_C_Deficiency (activator loss with a FAILS_TO_RECAPITULATE link: no glucosylceramide accumulation, no organomegaly). |
schema follow-up (no issue yet) |
| Computed indices and composite endpoints in aging biology | Undecided — surfaced by curation, no schema change proposed yet. Curating biomarkers into the aging-hallmark modules (inflammaging, mitochondrial_dysfunction, telomere_attrition, epigenetic_alterations, deregulated_nutrient_sensing) hit the same wall five times: dismech represents mechanisms and analytes, and does not represent anything computed over them. On inspection this is two related gaps, not one, and conflating them would produce the wrong schema. (1) Computed indices over measurements. An epigenetic clock is a penalized regression over hundreds of CpG sites whose output is an age estimate in years; a composite biomarker panel is a fitted combination the field explicitly prefers over any single analyte (the 2025 Delphi consensus states "there is no existing consensus on the best combination of biomarkers to fully capture biological aging" and calls closing that a research priority); a deficit-accumulation frailty index is a ratio over a 36-item deficit list. None is an analyte, so none belongs in Biochemical — yet Biochemical.readouts is the only place a BiomarkerReadout link to a pathograph node can live, so DNAm PhenoAge is currently curated there under protest, with the reasoning recorded in the entry's own notes. (2) Composite clinical outcome endpoints. Disability-free survival (ASPREE: a time-to-event composite of death, dementia, and persistent physical disability) and multimorbidity (TAME: a count of incident age-related diseases) are outcomes, not measurements — a different type again, and the thing a candidate surrogate would be surrogate for. kb/surrogate_endpoints/fda_surrogate_endpoints.yaml holds 225 FDA rows and none is an aging endpoint, so BiomarkerReadout.regulatory_endpoint_refs has no target and every endpoint_context: CANDIDATE_SURROGATE in these modules currently points nowhere. The frailty index straddles the two, being a computed index used as an outcome. Nothing existing covers it: SeverityTier/severity_scale is a threshold-label pair on a model variable, not an estimator; computational_models: means mechanistic simulation (tellurium/SED-ML), not a fitted predictor. Candidate shapes, none worked through: (a) a CompositeMeasure/derived_index class carrying the input features, the fitting provenance, and reusing BiomarkerReadout for its mechanism links; (b) admit estimators to computational_models: and reach the pathograph via the existing ModelMechanismLink, accepting that "computational model" then means two different things; (c) extend SurrogateEndpoint beyond the FDA import to carry non-regulatory clinical outcome assessments, addressing gap (2) only. Interim state: five KNOWLEDGE_GAP discussions record the problem where a curator will meet it — knowledge_gap_composite_vs_single_inflammaging_markers and knowledge_gap_surrogate_for_which_geroscience_endpoint in inflammaging, knowledge_gap_no_ontology_term_for_epigenetic_clock in epigenetic_alterations, plus the marker-or-driver and telomere-reliability gaps. Ontology sub-gap (§4): NCIT has no term for an epigenetic clock or for biological age — "Epigenetic Clock" returns nothing and "Biological Age" returns only Biological Agent and descendants. NCIT:C17961 (DNA Methylation) and NCIT:C16269 (Aging) were considered and rejected as not-the-measurement; the interim binding names the assay (NCIT:C63328, DNA Methylation Analysis) and carries the clock identity in preferred_term, per the Ontology Term Contract. An NCIT term request is the real fix and is not yet filed. This blocks little today but compounds: five hallmark modules remain uncurated, and each will work around the same absence. |
report · schema follow-up |
| Obsolete ontology terms | Should fail validation but do not yet | #712 |
| Unlisted ontology prefixes | Silently skipped by term validation (only a warning) — an unconstrained prefix can pass unchecked | — |
| Schema docs vs. script docs separation | Schema element pages currently mix in script docs | #2737 |
| Abstract (non-disease) comorbidity/trajectory poles | Undecided. ComorbidityAssociation.disease_a/disease_b are ConditionDescriptors where slug is optional, so a pole need not resolve to a Disease entry — e.g. an exposure/state like "accelerated biological aging" expressed via preferred_term (optionally MONDO/HP-bound). Schema permits it; whether it is idiomatic (vs. requiring both poles to be bona fide conditions, and modeling the broad mechanism on a module instead) is not yet decided. Convention so far: keep the conserved mechanism on a kb/modules/ module and reserve trajectory entries for concrete condition pairs, with the module referenced via conforms_to from the trajectory's hypothesis nodes. |
schema/governance follow-up |
| Structured effect-modifier / life-stage on associations | Deferred. ComorbidityDirectionEnum encodes only temporal precedence (A_BEFORE_B, BIDIRECTIONAL, …), not the sign of an effect, and AssociationSignal.demographics.age_range is free text. There is no first-class way to represent a context-dependent sign reversal (antagonistic pleiotropy) — e.g. accelerated aging being risk-increasing for early-onset cancer but tumor-suppressive in later life. Today this is recorded only via two stratified association_signals (opposite-sign metrics + age_range strings) plus prose hypotheses, which is legible to humans but not to tooling. Candidate enhancement: an enum-backed life_stage/context and/or an effect_direction (RISK_INCREASING / PROTECTIVE) distinct from temporal directionality. Precedent for the modeling alternative: for the senescence case the antagonistic pleiotropy was modeled instead as two complementary precomposed modules — cellular_senescence (deleterious arm) and senescence_tumor_suppression (protective arm) — rather than a single effect-reversing edge. This sidesteps the missing construct and is the preferred pattern when the opposing effects are mechanistically separable; the structured effect-modifier remains a candidate only for genuinely single-edge sign reversals. |
schema follow-up |
| Xogenesis lesion anchoring (MPATH retirement) | PROPOSED, not enacted — see §13 for the full rationale. MPATH is the Mouse pathology ontology, is absent from conf/oak_config.yaml (so its ids have never been validated — an instance of the unlisted ontology prefixes row above), and lacks continuant classes for thrombus / atheroma / amyloid, which three of the five Xogenesis modules already flag as gaps. Proposal: drop MPATH entirely and re-ground the lesion-identity slot on NCIt via a bound XogenesisAnchor slot (output_kind + OGMS genus + UBERON site), with ICD-O-3 as a neoplasm-only mapping. Open: whether to reuse HistopathologyFindingTerm or mint a narrower enum, and how far the allow-list stretches for lesion terms NCIt files under Disease or Disorder. |
#7356 |
13. Xogenesis lesion anchoring: retire MPATH in favour of a clinical vocabulary
Status: PROPOSED, not enacted. No module file, schema file, or skill has been changed. This section records the rationale and the open questions so they can be signed off before any migration. Numbered 13 rather than inserted before Gaps so the existing
#12-gapsanchor keeps resolving.
Decision (proposed). MPATH is dropped from DisMech entirely. The lesion-identity
slot in the Xogenesis anchor convention is re-grounded on the NCI Thesaurus, with
ICD-O-3 available as a mapping for the neoplasm case only. The anchor moves out of
free-text notes: into a bound schema slot so it is covered by term validation.
What MPATH is doing today. Modules whose terminal output is the formation of a
pathological material entity ("Xogenesis" modules) carry a four-part anchor stanza in
notes: — process genus (OGMS:0000061 and its OGMS:0000080 / OGMS:0000081
sub-types), output continuant (OGMS:0000078 / OGMS:0000079), lesion identity
(MPATH), and site (UBERON). The convention lives in
.claude/skills/create-module/SKILL.md; five modules carry it — granuloma_formation
(MPATH:847), renal_cystogenesis (MPATH:62), thrombogenesis (MPATH:125),
atherogenesis (MPATH:28), amyloidogenesis (none).
Rationale for dropping MPATH.
- Mouse vocabulary, human KB. MPATH is the Mouse pathology ontology — per OBO
Foundry, "a structured controlled vocabulary of mutant and transgenic mouse pathology
phenotypes", built for Pathbase mouse histopathology annotation. It is not a clinical
terminology and has no clinical deployment. DisMech is a human-disease KB that is
careful enough to segregate
MODEL_ORGANISMevidence (§6); anchoring human lesion identity in a mouse-phenotype vocabulary contradicts that discipline. - Process/continuant conflation, running in both directions. The Xogenesis slot needs
a continuant — the lesion itself — and MPATH's labels do not reliably tell you which
branch a term is on. Ancestor checks against
sqlite:obo:mpathfor the ids the convention actually uses:
| id | label | MPATH branch |
|---|---|---|
MPATH:62 |
cyst | MPATH:603 pathological anatomical entity ✅ |
MPATH:125 |
thrombosis | MPATH:603 pathological anatomical entity |
MPATH:847 |
granuloma | MPATH:596 pathological process ❌ |
MPATH:28 |
atherosclerosis | MPATH:596 pathological process ❌ |
MPATH:181 |
fibrosis | MPATH:596 pathological process ❌ |
MPATH:125 carries a process word (thrombosis) but is filed under the continuant
branch; MPATH:847 carries a continuant word (granuloma) and is filed under
process. Label and BFO placement disagree in both directions, which is a worse
defect than a missing term — a curator reading the label cannot tell what they are
binding. Consequently four of five Xogenesis modules do not have a correctly-placed
continuant: renal_cystogenesis (MPATH:62) is the only one that does.
granuloma_formation is presented throughout the repo as the clean exemplar
(CLAUDE.md, kb/modules/granuloma_formation.yaml, and SKILL.md's claim that the
anchors are MPATH:603 subtree terms) and is not one.
3. Unvalidated — and adding a config line is not the fix. MPATH: is not in
conf/oak_config.yaml, so it falls squarely into the unlisted ontology prefixes gap
recorded in §12 — silently skipped, warning only. The anchors are additionally prose,
and term validation inspects bound term: objects in data, not CURIEs in free text. So
MPATH ids in DisMech have never been checked by anything except a curator running
runoak by hand. The cheap remedy is real and should be named rather than left hanging:
sqlite:obo:mpath exists, is ~689 KB, and works offline, so adding
MPATH: sqlite:obo:mpath would close this validation gap in one line. It is rejected
because it closes only this gap — the vocabulary would still be a mouse ontology
(#1) and its terms would still be on the wrong branches (#2), and binding a slot to it
would tie human lesion identity to that vocabulary harder than prose does. Note also
that moving the anchor out of notes: into a bound slot is separable from which
vocabulary is bound: the two halves of this proposal can be argued independently, and
rationale #3 supports only the first.
Why NCIt, and why not ICD-11 or SNOMED CT.
- NCIt is already a bound, OLS-served vocabulary in DisMech (§4), already carries the imaging and histopathology finding axes (§9), is openly licensed, is clinically deployed (CDISC/caDSR, cancer registries, regulatory submission vocabularies), and is UMLS-mapped to SNOMED CT — so it doubles as a SNOMED bridge without SNOMED's licensing constraints.
- ICD-11 Foundation is not a lesion vocabulary. Verified against
sqlite:obo:icd11f: searches for the eight lesion types enumerated in the skill's MPATH id list (SKILL.md:89-91) return diseases, not morphologic entities — granuloma → "Granulomatosis with polyangiitis"; thrombus → "Cerebral ischaemic stroke due to thrombus of extracranial large artery"; "amyloid deposition" exists only as a synonym onicd11f:2078467774Amyloidosis, the disease. ICD-11 remains what §4 already says it is: a disease-mapping axis for*_mappingsblocks. It is not a fallback for this slot. - ICD-O-3 applies only to
neoplasm, and DisMech already has the idiomatic pattern for it:src/dismech/schema/classifications/icdo_morphology.yamlbindsmeaning: NCIT:…and carriesICDO:8010/3-style codes inexact_mappings. Same shape applies here — NCIt binds, ICD-O rides along. - SNOMED CT is the terminology that actually has a coherent morphologically abnormal
structure axis (
49755003), and the create-module skill already names it as an external census/gap guide that is never bound. That stays true, on the grounds §4 and the skill actually rest on: SNOMED is neither OBO nor OLS-served, and its affiliate licensing is incompatible with redistributing a validation artifact. Note this is not an offline-validatability argument — §4 gives "offline SQLite adapters via OAK" as rationale, not as a constraint, andconf/oak_config.yamldeliberately departs from it for five prefixes includingNCIT: ols:ncit(resolved over the network, per #5160). An offline-validatability criterion, applied literally, would argue for keeping MPATH — a small offline SQLite — over the NCIt that replaces it.
Binding is RECOMMENDED, not REQUIRED — following the ImagingFindingTerm precedent in
§9. A lesion with no defensible NCIt term is carried on preferred_term alone with an
explicit gap note, exactly as "gadolinium-enhancing lesion" is in Multiple_Sclerosis.
Two lesions are known to need this treatment already: thrombus (NCIt's nearest is
NCIT:C27083 Blood Clot, which is arguably a different concept from an in-situ
thrombus) and concretion/calculus (no clean morphologic term surfaced).
NCIt has no single coherent lesion axis — but that is a routine situation here, not a
blocker. Ancestor checks against OLS4 place the ten candidate lesion concepts in four
unrelated NCIt branches. All eleven CURIEs below are OAK-verified against ols:ncit
(labels match exactly):
| Lesion | NCIt term | NCIt branch |
|---|---|---|
| granuloma | NCIT:C3064 |
Lesion (NCIT:C3824) → Finding |
| cyst | NCIT:C2978 |
Lesion (NCIT:C3824) → Finding |
| fibrosis | NCIT:C3044 |
Morphologic Finding (NCIT:C35867) → Histopathology Result |
| amyloid deposition | NCIT:C54018 |
Deposit → Morphologic Finding (NCIT:C35867) |
| aneurysm | NCIT:C26693 |
Cardiovascular System Finding → Finding by Site or System |
| abscess | NCIT:C26686 |
Disease or Disorder (Infectious / Inflammatory Disorder) |
| neoplasm | NCIT:C3262 |
Disease or Disorder |
| atherosclerotic plaque | NCIT:C78739 |
Biospecimen / Specimen / Material — semantically wrong |
| thrombus | — | no exact term (nearest NCIT:C27083 Blood Clot) |
| concretion | — | unresolved |
This scatter is the coherence SNOMED's 49755003 axis provides and NCIt does not.
Separately, on coverage: eight of these ten lesions carry an NCIt term, and
NCIT:C3824 Lesion plus NCIT:C35867 Morphologic Finding between them cover only four of
those eight.
A multi-root reachable_from is, however, already idiomatic in DisMech — this exact
problem has been solved twice before, both times because NCIt lacked a single root:
- the
HistopathologyFindingTermenum insrc/dismech/schema/dismech.yaml— 13 source nodes spanning two ontologies, includingNCIT:C35867Morphologic Finding and the cross-ontologyHP:0025461. - the
ImagingFindingTermenum in the same file — 3 source nodes includingHP:0000118.
(Both are cited by enum name rather than line number on purpose: these line numbers have
already gone stale twice as main moved beneath the branch.)
So the open decision is not "multi-root or not" — that is settled convention — but which roots, plus how far the allow-list stretches for the strays (abscess, aneurysm, neoplasm sit under Disease or Disorder, whose root cannot be used wholesale without admitting every disease in NCIt).
Reuse before minting. HistopathologyFindingTerm already contains two of the eight
lesion terms in its permissible set (NCIT:C3044 Fibrosis, NCIT:C54018 Amyloid
Deposition) and is rooted on the same NCIT:C35867 branch this decision wants — and a
Xogenesis lesion arguably is a morphologic finding. Reusing it, rather than minting a
new enum, is therefore the leading option and the enacting PR must either adopt it or
state explicitly why a separate binding is needed (§3 is reuse-first in the same spirit —
prefer a subtype over a new entry, share one schema across disorders and modules).
The likely argument for a separate binding is scope: HistopathologyFindingTerm
deliberately spans grading, immunophenotype, ultrastructure, and staining intensity, none
of which can be a Xogenesis output, so reuse would bind a far looser range than the slot
means. That trade-off is a decision for the enacting PR, not a settled matter here.
Proposed shape. A XogenesisAnchor class carrying output_kind (NCIt-bound,
RECOMMENDED), process_genus + derivation_type (OGMS), and site (UBERON), replacing
the prose stanza. OGMS: sqlite:obo:ogms would be added to conf/oak_config.yaml
(verified available: OGMS:0000078 resolves). NCIt and UBERON are already configured.
Note that §4's ontology table currently lists NCIT: only under Treatments / medical
actions, though §9 already uses it for imaging and histopathology findings; the enacting
PR should widen that row rather than leave it three-times understated.
Terminology note. The convention currently calls the lesion slot "species", in the
genus–differentia sense (the kind of lesion under the OGMS:0000078 genus). This reads
as biological species and has already caused a real misunderstanding — NCBITaxon was
proposed for it. Rename to output_kind. NCBITaxon's role in DisMech is unchanged and
correct: pathogen, vector, reservoir, host, and model-system organisms (§4), matching
Mondo's own use of NCBITaxon in its infectious_disease_by_agent, vectorBorneDisease,
and nonhuman_disease_taxon patterns.
Migration surface. Larger than a CLAUDE.md-driven grep suggests, so the enacting PR
should work from this list:
- The five module
notes:stanzas, but note thatrenal_cystogenesiscarriesMPATH:62in the module file while itsCLAUDE.mdregistry line never mentions MPATH — only four registry lines name it. .claude/skills/create-module/SKILL.md:88-91states the anchors areMPATH:603(pathological anatomical entity) subtree terms. Per rationale #2 that premise is false for three of the eight ids it lists (granuloma, fibrosis, and — in the registry — atherosclerosis are underMPATH:596pathological process), so the claim must be corrected or removed, not merely re-pointed at NCIt. The same false premise is repeated inkb/modules/granuloma_formation.yamland theCLAUDE.mdregistry line for it..claude/skills/create-module/SKILL.md:110-113, which declares eight worked Xogenesis modules while only five carry a stanza —nephrolithiasis_crystal_nucleation,cholelithiasis_biliary_supersaturation, andfibrotic_responsehave none. Those three are exactly where the unresolved concretion and fibrosis rows land, so the migration must decide whether to backfill them.docs/reports/mondo-anchoring-audit-2026-07-30.md:191,193mentions MPATH. It is a dated snapshot report, so the default is to leave it unedited rather than rewrite history.
Open questions (need sign-off before enacting).
- Does
output_kindreuseHistopathologyFindingTerm(whose range is looser than the slot's meaning) or mint a narrower enum? If minted: which roots beyondNCIT:C3824+NCIT:C35867, and how far does the allow-list stretch for the Disease or Disorder strays (abscess, aneurysm, neoplasm) before they are better leftpreferred_term-only with gap notes? (Multi-root binding itself is settled convention — see above.) - Is
NCIT:C78739Atherosclerotic Plaque acceptable despite sitting under Biospecimen, or is atheroma a gap plus an upstream NCIt term request? - Should the retired SNOMED "census guide only" line stay in the skill, given NCIt is UMLS-mapped to SNOMED and partly supersedes it?
- Where the disposition is "leave unanchored + flag the gap" (thrombus, concretion, and possibly atheroma), should the module's gap note carry a tracked upstream NCIt/OGMS term-request id rather than free prose? Otherwise the migration converts a silent MPATH gap into a silent NCIt gap, and the register gains nothing on that axis. Decide alongside Q2.