Skip to content

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

  1. Open a GitHub issue describing the decision change and its rationale.
  2. Tag a maintainer (e.g. @cmungall) for sign-off on scope/governance changes.
  3. Update the canonical source (schema, oak_config.yaml, exporter, etc.) and this document in the same PR.
  4. Update the agent instructions in CLAUDE.md and AGENTS.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 meaning fields and reachable_from dynamic 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_caveated requires limitations on it, mirroring the existing FAILS_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-LymphocyteNeoplastic 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, omit term: and keep a free-text preferred_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 a MONDO: 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_term vs term.label: term.label must exactly match the canonical ontology label (OAK-verified); preferred_term may 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:C15986 Pharmacotherapy). Where a drug class was lost from the action term it is recovered in therapeutic_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_term with no term:.
  • 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:C15370 Steroid 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.

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: snippet values must be exact substring quotes from the cited reference, enforced by linkml-reference-validator. Paraphrase fails validation.
  • Cache files are tool-generated: references_cache/*.md are created exclusively by just fetch-reference or the validator. They are never hand-written or hand-edited.
  • evidence_source describes 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 evidence list 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 deprecated hypothesis_groups so 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_to edge 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.py counts one quoted sentence graded two ways in one file. Before this change: 8,285 divergences on supports against 715 on evidence_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.accession already declares implements: linkml:authoritative_reference. It was always a reference slot; conf/reference_validator_config.yaml merely lists the dataset prefixes under skip_prefixes.
  • just verify-datasets <file> resolves a geo: accession through the reference fetcher, which writes references_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 the datasets: block.
  • All 919 geo: accessions in kb/ were backfilled, so the check is offline in CI.
  • geo and GEO have been removed from skip_prefixes, so the validator now checks GEO records like any other reference — the accession must resolve, a GEO:-cited snippet must be an exact quote from the cached summary, and datasets[].title must be the repository's own title (a title slot adjacent to a reference field is compared with the fetched record). datasets[].title is the repository's title, copied exactly — the curator's summary belongs in description, 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.json is frozen. No script, module, test, workflow, or recipe reads or writes it, enforced by test_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 in measure_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 the ORPHA: 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 phenotype FrequencyEnum.
  • rate_per_100000 (+ rate_low / rate_high for ranges) — one normalized, machine-comparable number in cases per 100,000. Every source notation (%, per 100,000, per million, 1 in N, Orphanet N / M) converts losslessly into it.
  • notes retains the verbatim source phrasing; evidence is unchanged.
  • percentage is deprecated (kept read-only during transition). It was an Any (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 across per N / 1 in N / Orphanet bands / explicit %). Do not populate percentage on 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), with meaning: values bound to the NCI Thesaurus Diagnostic Imaging branch (e.g. NCIT:C16809 Magnetic Resonance Imaging, NCIT:C17204 Computed Tomography, NCIT:C17007 PET).
  • imaging_finding_term (ImagingFindingDescriptor) — the imaging appearance, bound via ImagingFindingTerm to 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 on preferred_term alone rather than being forced onto an ill-fitting code or fabricated.
  • Body site reuses the existing located_in (UBERON) slot, plus laterality and spatial_extent (focal/multifocal/diffuse), because imaging is inherently spatial. An optional phenotype_term cross-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 phenotypes and 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:C38054 EEG, NCIT:C38053 ECG, NCIT:C38056 EMG).
  • 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-dismissed sessionStorage 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.j2 legitimately 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-banner behaviour, 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. Every sessionStorage access is inside a try, because the property getter itself throws when storage is blocked; the bar then simply stays visible. Dismissing moves focus to the page's main/h1 rather 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 at right: 8px is 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 MkDocs copyright: 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 MkDocs copyright: footer renders only on docs/ pages, and render.py pages 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.md is the source of truth; the banner is its summary. tests/test_disclaimers.py gates 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: flex on the bar outranks the user-agent rule for the hidden attribute).

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_groupsmechanistic_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 modulescellular_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-gaps anchor 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.

  1. 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_ORGANISM evidence (§6); anchoring human lesion identity in a mouse-phenotype vocabulary contradicts that discipline.
  2. 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:mpath for 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 on icd11f:2078467774 Amyloidosis, the disease. ICD-11 remains what §4 already says it is: a disease-mapping axis for *_mappings blocks. 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.yaml binds meaning: NCIT:… and carries ICDO:8010/3-style codes in exact_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, and conf/oak_config.yaml deliberately departs from it for five prefixes including NCIT: 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 HistopathologyFindingTerm enum in src/dismech/schema/dismech.yaml13 source nodes spanning two ontologies, including NCIT:C35867 Morphologic Finding and the cross-ontology HP:0025461.
  • the ImagingFindingTerm enum in the same file — 3 source nodes including HP: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 that renal_cystogenesis carries MPATH:62 in the module file while its CLAUDE.md registry line never mentions MPATH — only four registry lines name it.
  • .claude/skills/create-module/SKILL.md:88-91 states the anchors are MPATH: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 under MPATH:596 pathological process), so the claim must be corrected or removed, not merely re-pointed at NCIt. The same false premise is repeated in kb/modules/granuloma_formation.yaml and the CLAUDE.md registry 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, and fibrotic_response have 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,193 mentions 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).

  1. Does output_kind reuse HistopathologyFindingTerm (whose range is looser than the slot's meaning) or mint a narrower enum? If minted: which roots beyond NCIT:C3824 + NCIT:C35867, and how far does the allow-list stretch for the Disease or Disorder strays (abscess, aneurysm, neoplasm) before they are better left preferred_term-only with gap notes? (Multi-root binding itself is settled convention — see above.)
  2. Is NCIT:C78739 Atherosclerotic Plaque acceptable despite sitting under Biospecimen, or is atheroma a gap plus an upstream NCIt term request?
  3. Should the retired SNOMED "census guide only" line stay in the skill, given NCIt is UMLS-mapped to SNOMED and partly supersedes it?
  4. 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.