Add Gene2Phenotype (G2P) terminology value sets - #79
Conversation
Adds src/valuesets/schema/clinical/gene2phenotype.yaml covering the controlled vocabularies documented at https://www.ebi.ac.uk/gene2phenotype/about/terminology, with terms and definitions taken from the G2P API (/attribs/description/, /molecular_mechanisms/, /ontology_terms/variant_types/). 15 enums: - G2PConfidenceCategory (6) - GenCC classification terms; GenCC IDs recorded as annotations since the GENCC prefix is not in bioregistry or OLS - G2PAllelicRequirement (9) - mapped to HPO mode-of-inheritance terms; every G2P genotype string is an exact synonym of the HPO term it maps to - G2PCrossCuttingModifier (7) - 5 mapped to HPO inheritance qualifiers, 2 G2P-specific with no HPO equivalent - G2PMolecularMechanism (5) and G2PMolecularMechanismSynopsis (10) - Backwell and Marsh definitions (PMID:35395171); SO variant terms recorded as close_mappings because G2P mechanisms describe a gene-disease association rather than a variant - G2PMolecularMechanismSupport (2) - G2PMechanismEvidenceCategory (4) plus G2PFunctionEvidence, G2PFunctionalAlterationEvidence, G2PModelsEvidence and G2PRescueEvidence - ClinGen gene-disease validity SOP evidence matrix - G2PVariantConsequence (6) and G2PVariantType (31) - Sequence Ontology terms, hierarchy expressed with is_a; where G2P's label differs from the current SO label the G2P label is kept as an alias - G2PPanel (7) All 37 SO accessions and 14 HPO accessions were checked against OLS; descriptions use the SO text definitions where SO provides one. Verified with `just site` (gen-project and gen-doc; gen-slides fails only because marp is not installed in this environment), `just validate-schema` on the new file, and `just test` (63 passed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Claude finished @cmungall's task in 3m 45s —— View job Review: Gene2Phenotype (G2P) terminology value sets
Overall this is a well-researched, carefully sourced addition. Terms are grouped by domain, every enum carries Correctness1. Three
2.
Post-zygotic events are somatic — that is exactly what 3. Is The G2P allelic-requirement list I'm aware of distinguishes 4. MONOALLELIC_AUTOSOMAL:
title: Autosomal dominant inheritance # <- HPO's label
aliases: [monoallelic_autosomal] # <- G2P's own labelElsewhere in the repo ( Modeling & consistency5. GenCC identifiers are in annotations rather than mappings —
6. Cross-references between enums use display strings, not keys —
7.
8. Titles mix G2P labels and SO labels within one enum —
9. Redundant aliases —
10. Slots cover 8 of 14 enums — No slot is defined for the four evidence enums, 11. Panels get added over time. Consider noting a retrieval date in the description or CopyeditingSeveral typos are inherited verbatim from the source and are worth fixing (or worth an explicit "definitions quoted verbatim" note so future readers don't try to fix them):
AttributionAll descriptions are verbatim from G2P / GenCC / SO and get redistributed into the generated Python, OWL and docs artifacts. The per-enum Test coverageNo tests or example data added. Security / performanceNothing of concern — this is declarative schema data with no executable content. The +1109 lines add ~14 enums to an already large merged schema; generation time impact is negligible. Note on verificationI could not run |
Changes from the automated review on PR #79: - Record the SO subclass hierarchy on the eight NMD-qualified variant types (e.g. STOP_GAINED_NMD_TRIGGERING is_a STOP_GAINED with mixin NMD_TRIGGERING). Each term has two parents in SO, verified via the OLS parents endpoint, so the base variant is the is_a and the NMD qualifier is a mixin. - Point the parent_mechanism and variant_type_group annotations at permissible value keys instead of display titles, so they resolve programmatically and survive title edits. - Add slots for the seven enums that had none (evidence categories, variant type group, panel); none of the new slot names collide with existing slots in the merged schema. - Flag the internal inconsistency in G2P's verbatim TYPICALLY_DE_NOVO wording, which describes a post-zygotic (somatic) event where the mapped HP:0025352 means pre-zygotic de novo, rather than silently rewriting the quotation. - Note at schema level that descriptions are quoted verbatim from the source vocabularies including their spelling errors, so the retained "casual role" and "segregration" are not mistaken for typos to fix. - Explain in G2PConfidenceCategory why GenCC identifiers are annotations rather than meanings: the GENCC prefix is absent from bioregistry, OLS and the OBO PURL system, rechecked at review time. - Drop the 5'/3' UTR aliases that merely duplicated their titles. - Add a retrieval date and drift note to G2PPanel, which EBI revises. Verified with `just validate-schema` on the file, `just gen-project`, and `just test` (63 passed, 1 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Claude encountered an error after 3m 21s —— View job I'll analyze this and get back to you. |
|
Thanks — worked through all of this. Acted on in d89776c: 6 (annotations now use PV keys), 7 (SO hierarchy added), 9 (duplicate UTR aliases dropped), 10 (slots added for the seven uncovered enums), 11 (retrieval date on Several findings don't hold up, and since they'd change the data if applied I want to record why. 3 —
8 — the titles are not mixed. All six 1 — those three CURIEs were verified. 4 — keeping 5 — GenCC. Rechecked at review time: Typos. Test coverage. Two corrections on the environment notes. Generated by Claude Code |
|
Heads up on the red X: the From the job log, the review action's own SDK run errored out: One turn and under a cent before failing means the review agent aborted at the start and never produced a review — no findings were lost. The same workflow succeeded on the previous commit with the same credentials, so it looks transient rather than a config problem.
Merge state is otherwise clean — no conflicts against Generated by Claude Code |
|
Claude finished @cmungall's task in 3m 47s —— View job Re-review after
|
| # | Finding | Resolution |
|---|---|---|
| 6 | Cross-refs used display strings | Now PV keys (LOSS_OF_FUNCTION, NMD_VARIANTS, …) throughout |
| 7 | is_a missing on G2PVariantType |
Added on all 8 NMD-qualified types, with the second SO parent as mixins |
| 10 | Slots covered only 8/14 enums | Seven slots added; all 14 enums now have one |
| 11 | G2PPanel snapshot drift |
retrieved: '2026-08-18' + drift note (:1100) |
| 9 | Redundant UTR aliases | Dropped |
| 2 | TYPICALLY_DE_NOVO self-contradiction |
Not rewritten, but the inconsistency is now called out inline with the HPO sense — the right call for verbatim quotations |
| 5 | GenCC IDs as annotations | Justified in the enum description (GENCC unresolvable in bioregistry/OLS/OBO PURL). Reasonable; the recorded rationale is the valuable part |
| Copyedit | "casual role", "segregration" | Covered by the new schema-level verbatim-quotation policy |
Two judgement calls I'd have made the same way. The verbatim policy in particular is worth having in writing.
Still open
1. close_mappings CURIEs remain outside the validated set — gene2phenotype.yaml:379,392,404
SO:0002054, SO:0002053, SO:0002052 are still absent from cache/so/terms.csv, and I now know why that matters concretely: enum_evaluator.py:429-441 only reads pv.meaning — close_mappings are never label-checked against the ontology at all. So these three IDs are the only ontology references in the file that no part of the toolchain will ever catch if wrong. They look right by label correspondence to the aliases (loss_of_function_variant / gain_of_function_variant / dominant_negative_variant), but "looks right" is exactly the state CLAUDE.md's always check IDs rule exists to avoid. Cheapest fix: verify via OLS and add the three rows to the cache. (Broader gap — the validator ignoring close_mappings/exact_mappings — is pre-existing, not this PR's to fix.)
2. MONOALLELIC_X is unchanged and unremarked — :211
This was finding #3 last time and the commit message doesn't mention it. G2P's published allelic-requirement list distinguishes monoallelic_X_hemizygous and monoallelic_X_heterozygous; a bare monoallelic_X looks like an introduced parent carrying HP:0001417. If it isn't in G2P's list, a curator can emit a code G2P won't accept. Either confirm it's real or drop it — a one-line answer either way.
3. The new allelic-requirement description makes a stronger claim than before — :179-182
HPO itself records each of these G2P genotype strings as an exact synonym of the term mapped below (for example monoallelic_autosomal on HP:0000006), which is what makes these mappings unambiguous.
That's a sharper, more falsifiable claim than the "exact synonym" phrasing it replaced, and nothing in the repo can check it: cache/hp/terms.csv stores curie,label,retrieved_at only — no synonyms. If the assertion holds for all ten values it's a nice piece of provenance; if it holds for only some (I'd particularly expect monoallelic_X and the PAR terms to be the weak ones), the sentence overstates. Worth spot-checking the ones you're least sure of, or softening to "HPO records several of these strings as exact synonyms".
4. title: is still the HPO label, not the G2P label — :195-196 and throughout G2PAllelicRequirement
Untouched from last round. MONOALLELIC_AUTOSOMAL renders as "Autosomal dominant inheritance" while its own G2P label sits in aliases. Elsewhere in the repo (clinical/genetics.yaml:33-38) title is the label of the thing the key denotes. This is a legitimate design disagreement rather than a defect — if you prefer HPO labels for display, say so once in the enum description and I'll stop raising it.
5. Title inconsistency inside G2PVariantConsequence — :746, :755
ALTERED_GENE_PRODUCT_STRUCTURE is titled altered_gene_product_sequence (SO's label) and UNCERTAIN is titled function_uncertain_variant, while their siblings carry G2P labels. Cosmetic, but it's two values out of six going the other way.
On the new mixins: usage — :852,863,873,884,895,906,917,928
Modelling each NMD-qualified type as is_a: <base variant> + mixins: [NMD_TRIGGERING|NMD_ESCAPING] is faithful to SO's two-parent structure, and LinkML only gives you one is_a, so this is the right shape. Two notes:
- This is the first use of
mixinson permissible values anywhere in this repo — 14 other schemas use PVis_a, none use PVmixins.gen-projectpassing in CI proves it round-trips, but it's worth a quick eyeball of the generated OWL to confirm the mixin parent surfaces as asubClassOfand isn't silently dropped. If it is dropped, the NMD axis becomes annotation-only in the OWL product while looking modelled in the source — the subtler of the two failure modes. - Both parents are genuine SO
is_aedges, so which one becomes the mixin is arbitrary. Choosing the base variant asis_ais the more useful default for consumers; no change needed, just noting the asymmetry isn't ontologically meaningful.
Minor
Slot naming is now mixed: g2p_confidence and g2p_panel are prefixed, while variant_type, variant_consequence, molecular_mechanism and allelic_requirement claim generic names in a namespace merged across 100+ schemas. No collision exists today (I checked all 15), but the generic ones are the likely future flashpoint if another clinical-genetics schema lands. Prefixing consistently now is cheaper than renaming later.
Test coverage is unchanged — still no example instance under tests/data/valid/ exercising any of the new slots. Low priority for a pure value-set addition, but it's the one thing that would turn "CI is green" into "the enums are actually reachable from a data model".
Verification note
Same limitation as last round: Bash beyond git/grep was not approved in this environment, so I could not run just test, just validate-schema, or reach OLS to check the three SO IDs myself. What I can confirm is that the repo's own "Build and test" workflow completed successfully on d89776c, which covers _test-schema (gen-project), pytest and the examples. Correcting my earlier note: a validation recipe does exist — validate and validate-schema are defined in project.justfile:81-89, not the root justfile, so CLAUDE.md's instruction is followable after all.
· branch claude/gene2phenotype-terminology-bq51mi
Follow-up to the re-review on PR #79. The substantive finding was the OWL one. Generating OWL for this schema confirmed that gen-owl emits only the is_a parent as rdfs:subClassOf and silently drops the mixin, so the NMD axis was present in the LinkML source but absent from the OWL product. The NMD status is now also recorded in an nmd_status annotation, which does survive (valuesets:nmd_status "TRIGGERING"), and the limitation is documented in the enum description. mixins is retained: it is correct at the source level and is preserved in the merged LinkML output. Also: - Add the three close_mappings CURIEs to cache/so/terms.csv. They were verified before use and are re-verified here against the same SO release the validator reads; enum_evaluator only inspects pv.meaning, so nothing in the toolchain would have written them. - Record in G2PAllelicRequirement that the nine values are G2P's published genotype list from the attribs API, which settles whether the bare monoallelic_X is a real G2P value without relying on a PR comment. - Qualify the exact-synonym claim with how it was checked. All nine strings are oio:hasExactSynonym in the HPO release; the OLS obo_synonym field lists only synonyms carrying an xref, which makes three of them look unscoped through that API. - State the title convention once in G2PAllelicRequirement and G2PVariantConsequence: title is the ontology label, the G2P label is an alias. In G2PVariantConsequence the SO and G2P labels coincide for four of six values, so only two carry an alias. - Prefix every slot with g2p_ for consistency and to keep generic names out of the merged namespace. No collisions. Verified with `just validate-schema`, `just test` (63 passed, 1 skipped) and a gen-owl inspection of the emitted axioms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Claude finished @cmungall's task in 2m 52s —— View job Re-review after
|
| Prior finding | Resolution |
|---|---|
close_mappings outside the cache |
Three rows added to cache/so/terms.csv |
MONOALLELIC_X unconfirmed |
Recorded in the enum description as G2P's published genotype list from GET /api/attribs/, including the bare monoallelic_X (:181-184) |
| Exact-synonym claim unfalsifiable | Now states the check method and why OLS under-reports it (:186-189) |
title: = ontology label |
Convention stated explicitly (:191-193, :711-714) — settled, I won't raise it again |
Title inconsistency in G2PVariantConsequence |
Explained: SO and G2P labels coincide for 4 of 6, so only 2 carry an alias |
| Generic slot names | All 15 slots now g2p_-prefixed |
mixins may be dropped in OWL |
Confirmed dropped; worked around via nmd_status and documented at :820-825 |
On the OWL workaround
Confirming the mixin is dropped from rdfs:subClassOf and adding a surviving annotation is the right response, and documenting the generator limitation inline means the workaround can be removed when gen-owl improves. Three follow-ups, all small:
1. nmd_status values are bare strings, not PV keys — :851,866,878,890,901,913,925,937,949,961
mixins:
- NMD_TRIGGERING # resolves to a PV in this enum
annotations:
variant_type_group: NMD_VARIANTS # PV key — follows the convention
nmd_status: TRIGGERING # resolves to nothingTRIGGERING/ESCAPING aren't keys of anything. The sibling annotation on the same PV uses a G2PVariantTypeGroup key, and that was the point of the earlier fix — cross-references should be resolvable. Using NMD_TRIGGERING/NMD_ESCAPING costs nothing, makes the annotation obviously a mirror of the mixins: entry directly above it, and keeps the whole file on one convention.
2. Nothing keeps mixins and nmd_status in agreement. They're now two encodings of one fact, and the annotation exists precisely because the authoritative one doesn't survive generation. Add an NMD-qualified variant type later, forget the annotation, and the OWL silently loses the axis again with nothing failing — the same failure mode this commit just fixed. This is the one test that would pay for itself, and it doesn't need the instance data / carrier class that blocked the earlier test suggestion: load the YAML, assert that every PV whose mixins contains NMD_TRIGGERING/NMD_ESCAPING carries the matching nmd_status, and vice versa. (It has to be a YAML-level test rather than one in tests/test_rich_enums.py, since the generated valuesets.enums.clinical.gene2phenotype module doesn't exist until regen-derived runs on main.)
3. NMD_TRIGGERING and NMD_ESCAPING annotate themselves — :851,866
The two abstract qualifiers carry their own nmd_status, so an annotation-based query for triggering types returns 5 values while a mixin-based traversal returns 4 — the qualifier isn't its own child. Defensible either way, but the two access paths giving different answers is exactly the drift risk in (2). Worth a word in the description if intended.
Minor
Convention note covers 2 of 4 enums. G2PCrossCuttingModifier (:280) and G2PVariantType (:812) follow the same "title is the ontology label, G2P string is an alias" rule without stating it. Since it's now a file-wide convention, stating it once at schema level alongside the verbatim-quotation policy would cover all four and not go stale as enums are added.
Hand-stamped cache rows — cache/so/terms.csv:112-114. The three new rows carry 2026-08-18T22:40:00.000000; every other row has microsecond precision from the fetcher. Harmless — enum_evaluator.py never reads these rows — but the round-second timestamp is the only thing distinguishing hand-authored entries from fetched ones, and a wholesale cache refresh would drop them again since nothing requests them. The underlying gap (the evaluator ignoring close_mappings/exact_mappings entirely) is pre-existing and worth its own issue; these three IDs stay outside the validated set regardless of the cache rows.
Verification note
Weaker than last round: just isn't on PATH in this environment and network fetches weren't approved, so I could not run validate-schema, gen-owl, or reach OLS. Specifically, I did not independently confirm the three SO labels or the HPO exact-synonym claim — the cache rows match the aliases in the schema, but both were written from the same source, so that's self-consistency, not verification. The commit reports just validate-schema passing, just test at 63 passed / 1 skipped, and a gen-owl axiom inspection; the "Build and test" run for 332aa0e (32194117511) was still in progress when I checked and should be confirmed green before merge.
Nothing here blocks merge. Finding 1 is a one-line consistency fix; finding 2 is the one I'd actually push for, since it protects the fix this commit just made.
· branch claude/gene2phenotype-terminology-bq51mi
|
Good re-review — the OWL hunch was right, and it was the one thing here that was actually broken. Pushed 332aa0e. The SO:0002321 a owl:Class ;
rdfs:label "STOP_GAINED_NMD_TRIGGERING" ;
rdfs:subClassOf SO:0001587,
valuesets:G2PVariantType ;The On #3, you were right to push and the answer is better than a spot-check. OLS made three of the nine look unscoped, which is presumably what your instinct was picking up. That's an OLS artifact: its Nine for nine, PAR terms included. The claim stands, and the description now says how it was checked and warns about the OLS discrepancy. #1 — the three CURIEs are now in #2 — #4 and #5 — taking you up on the offer: the title convention is now stated once in each enum. Title is the ontology label, G2P label as alias. On #5 specifically, all six Slot naming — took the suggestion; all fifteen are Test coverage — still leaving this. Generated by Claude Code |
Follow-up to the third review round on PR #79. The substantive request was a test keeping mixins and nmd_status in agreement. They are two encodings of one fact, and the annotation exists only because the authoritative encoding does not survive OWL generation, so drift would silently reintroduce the bug the previous commit fixed. tests/test_g2p_nmd_axis.py asserts the biconditional in both directions, that nmd_status values resolve to real permissible values, that the NMD-qualified types keep a base variant as is_a so the parent that does reach OWL is the useful one, and that the set stays at 8 values split 4/4. It reads the schema YAML rather than the generated enums module, which does not exist until regen-derived runs on main. The tests were mutation-checked rather than just observed to pass: removing one nmd_status fails 2 of them, and flipping one to the wrong axis fails 3. Also: - nmd_status values are now the permissible value keys NMD_TRIGGERING and NMD_ESCAPING rather than bare TRIGGERING/ESCAPING, matching the mixins entry directly above and the variant_type_group convention. - NMD_TRIGGERING and NMD_ESCAPING no longer annotate themselves. They are the axis, not members of it, so annotating them made an annotation query return 5 values where a mixin traversal returns 4. Removing the self-annotation makes the two access paths agree, which the tests now enforce. - Hoist the title convention to schema level so it covers every enum rather than the two that stated it. Verified with `just validate-schema`, `just test` (69 passed, 1 skipped) and a gen-owl re-inspection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Claude finished @cmungall's task in 2m 43s —— View job Re-review after
|
| Prior finding | Resolution |
|---|---|
nmd_status used bare strings |
Now NMD_TRIGGERING/NMD_ESCAPING PV keys (:880–:963) |
Nothing kept mixins and nmd_status in agreement |
tests/test_g2p_nmd_axis.py, six tests |
| Qualifiers annotated themselves | Both nmd_status lines removed (:854, :868) |
Reading raw YAML rather than the generated valuesets.enums.* module is the right call and the docstring says why. The best test in the file is test_nmd_qualified_types_keep_a_base_variant_parent (:109) — it encodes the reason the is_a/mixins split is the way round it is ("the is_a is what reaches OWL"), which is the piece of knowledge that would otherwise evaporate.
One real gap in the guard
Mixins outside the hardcoded set are invisible to every test — tests/test_g2p_nmd_axis.py:41-42
def _nmd_mixins(pv):
return NMD_QUALIFIERS.intersection(pv.get("mixins") or [])Every test funnels through this intersection, so a mixin value that isn't literally NMD_TRIGGERING/NMD_ESCAPING returns the empty set and the value is skipped rather than flagged. Concretely: add STOP_LOST_NMD_ESCAPING with mixins: [NMD_ESCAPPING] (typo) and no annotation, and all six tests pass —
- test 1 skips it (no recognised mixin),
- test 2 skips it (no
nmd_status), - test 5 still counts exactly 8, because the missing annotation is the thing being counted,
- test 6 skips it.
That is precisely the "new NMD type added, axis silently lost in OWL" scenario the suite was written to catch. Two cheap closures, either of which is sufficient:
def test_mixins_resolve_to_permissible_values(variant_types):
for name, pv in variant_types.items():
for mixin in pv.get("mixins") or []:
assert mixin in variant_types, f"{name} has unknown mixin {mixin!r}"
def test_nmd_named_types_carry_the_axis(variant_types):
for name, pv in variant_types.items():
for qualifier in NMD_QUALIFIERS:
if name != qualifier and name.endswith(f"_{qualifier}"):
assert qualifier in (pv.get("mixins") or []), f"{name} is missing mixin {qualifier}"
assert _nmd_status(pv) == qualifier, f"{name} is missing nmd_status {qualifier}"The first catches the typo; the second catches an NMD-named value that forgot the axis entirely. All 8 current keys end in _NMD_TRIGGERING/_NMD_ESCAPING and the two qualifiers don't, so the suffix test is exact today.
Smaller
1. The 8 / 4 / 4 literals — :99-106
The count tripwire earns its place (it's the only thing that catches the workaround being deleted wholesale, which test 1 can't see). The 4 == 4 parity assertion doesn't: SO gives no guarantee the two arms stay balanced, and G2P adding one more escaping type without a triggering counterpart would fail a test that isn't actually about correctness. Also, when the count does legitimately change, expected 8 NMD-qualified types, found 10 reads like a defect rather than "bump this number deliberately" — worth saying so in the message.
2. The same dangling-reference risk exists for the other two cross-reference annotations. variant_type_group (31 values) and parent_mechanism (10 values) came from the earlier "use PV keys, not display strings" fix and carry the identical failure mode: a typo resolves to nothing and nothing complains. I checked all 41 and they resolve correctly today, so this is purely preventive — but the fixture already has the whole schema loaded, and generalising test_nmd_status_values_are_permissible_value_keys (:74) into "every annotation naming a PV key resolves in its target enum" is a handful of lines and covers the convention rather than one instance of it.
3. Nothing pins the premise. The suite protects the mixins ↔ nmd_status mirror, but not the reason the mirror exists. If gen-owl gains mixin support, the annotation becomes dead weight that still passes every test and still looks load-bearing. An end-to-end OWL assertion is too heavy for this repo, but recording the linkml version the gen-owl behaviour was observed on — in the module docstring or the enum description at :820-825 — gives a future reader something to re-check against.
Nit: _nmd_status (:45-46) assumes the compact annotation form. LinkML also accepts nmd_status: {tag: ..., value: ...}; that form would compare a dict against a string and fail with a confusing message rather than a clear one. Fails safe, so only worth a line if you touch the helper anyway.
Verification note
Bash beyond git was not approved in this environment, so I did not run pytest, just test, or gen-owl — the pass/fail claim above is from hand-checking the schema against each assertion, not from execution. import yaml matches existing practice (tests/test_data.py does the same) and testpaths = ["tests"] will collect the new file. Both CI runs for bca25b9 (Build and test, Claude Code Review) were still in progress when I checked; the build one is the meaningful confirmation.
Nothing here blocks merge. The mixin-typo hole is the only finding I'd actually push for — it's the difference between the suite catching the drift it was written for and appearing to.
· branch claude/gene2phenotype-terminology-bq51mi
|
All three taken, in bca25b9. Finding 2 was the right one to push on — it defeated my earlier "no test is possible here" reasoning, which was about instance data and didn't apply to a YAML-level invariant. 2 — I mutation-checked it rather than just watching it pass:
3 — removed rather than documented. You framed it as "defensible either way, worth a word if intended". On reflection it wasn't intended, and removing is strictly better: 1 — Convention note — hoisted to schema level alongside the verbatim-quotation policy, so it covers all four enums and won't go stale. On your verification note — a fair call-out, and you're right that cache rows matching the schema aliases is self-consistency. The independent check was against the OAK-built SQLite releases the validator itself reads, not against the schema text: That's what surfaced the OLS discrepancy in the first place: OLS showed three of the nine as unscoped, CI — "Build and test" on 332aa0e finished green (32194117511), so that's confirmed. bca25b9 is running now; locally it's 69 passed / 1 skipped, up from 63 with the six new tests. Happy to open a separate issue for the evaluator ignoring Generated by Claude Code |
The previous round's guard had a hole, confirmed by mutation rather than
by reading: every test funnelled through an intersection with the known
qualifier names, so a mixin that was not literally NMD_TRIGGERING or
NMD_ESCAPING yielded an empty set and the value was skipped instead of
flagged. Adding a type with mixins: [NMD_ESCAPPING] and no nmd_status
passed all six tests -- exactly the drift the suite was written to catch.
Two tests close it:
- test_mixins_resolve_to_permissible_values asserts every mixin names a
real permissible value, catching the typo.
- test_nmd_named_types_carry_the_axis asserts a value named for a
qualifier carries both the mixin and the matching annotation, catching
an NMD type added without either.
Also:
- Generalise the permissible-value-key check into
test_cross_reference_annotations_resolve, covering variant_type_group
and parent_mechanism as well as nmd_status. All three came from the
same "use keys, not display strings" change and share the failure mode
where a typo resolves to nothing silently.
- Replace the triggering/escaping parity assertion with the suffix test.
Parity is not a correctness property, since nothing guarantees SO keeps
the two arms balanced, and the suffix test catches the real defect
parity was standing in for. Keep the count as a tripwire against the
annotations being dropped wholesale, with a message saying to update it
deliberately when the count legitimately changes.
- Record that the dropped-mixin gen-owl behaviour was observed on linkml
1.9.5, so a future reader can re-check whether the workaround is still
needed.
- Read annotations through a helper tolerating both the compact and
{tag, value} forms.
All five mutation scenarios now fail the suite: typo'd mixin, missing
annotation, flipped axis, and a typo in either cross-reference
annotation. Verified with `just validate-schema` and `just test`
(73 passed, 1 skipped).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
You were right, and I confirmed it by running your scenario rather than reasoning about it. Injecting Both closures taken. Smaller 2 — generalised. Smaller 1 — parity dropped, count kept. Your argument holds: nothing guarantees SO keeps the arms balanced, so Smaller 3 and the nit — the docstring records that the dropped-mixin behaviour was observed on linkml 1.9.5, so the workaround can be re-checked rather than assumed. Annotations now go through a helper handling both the compact and Full mutation matrix, all against the real suite:
Schema is byte-identical after the mutation runs; this commit touches only the test file. 73 passed / 1 skipped locally, and CI on bca25b9 went green before this push. Generated by Claude Code |
|
Claude finished @cmungall's task in 2m 36s —— View job Re-review after
|
| Assertion | Schema state |
|---|---|
| 8 NMD-qualified types | 8 mixins blocks, 8 nmd_status annotations (:876–:963), 4 triggering / 4 escaping |
| Qualifiers don't self-annotate | NMD_TRIGGERING :841, NMD_ESCAPING :855 carry only variant_type_group |
| Suffix rule is exact today | all 8 keys end in _NMD_TRIGGERING/_NMD_ESCAPING; neither qualifier does |
is_a parents are base variants |
STOP_GAINED, FRAMESHIFT_VARIANT, SPLICE_DONOR_VARIANT, SPLICE_ACCEPTOR_VARIANT, all real PVs |
| Cross-refs resolve | 31 variant_type_group → 5 G2PVariantTypeGroup keys; 10 parent_mechanism → LOSS_OF_FUNCTION/GAIN_OF_FUNCTION/DOMINANT_NEGATIVE; all resolve |
| Only mixins in the file | the 8 in G2PVariantType — nothing else the test could miss today |
All eight tests should pass on current content. The linkml 1.9.5 / linkml-runtime 1.9.5 provenance in the docstring (:15-18) checks out against uv.lock:1442,1493 — worth noting because it makes the workaround's expiry condition mechanically re-checkable rather than a memory.
Remaining, all minor
1. The mixin/is_a resolution check is scoped to one enum while the annotation check isn't — :79, :194
test_mixins_resolve_to_permissible_values takes the variant_types fixture, so it only sees G2PVariantType. test_cross_reference_annotations_resolve walks schema["enums"] and covers everything — the same reasoning applies to structural references. Concretely, G2PVariantConsequence has three PV is_a references (gene2phenotype.yaml:748,755,762) that no test touches; they resolve correctly today, and I don't know whether gen-project would fail on a dangling PV is_a, so a typo there may well be as silent as the annotation case was. One loop over all enums checking both is_a and every mixins entry against that enum's own keys subsumes the current test and closes both surfaces.
2. The suffix rule is exact today but not total. An NMD-qualified type added under a name that doesn't end in the qualifier (say NMD_TRIGGERING_STOP_LOST, or an SO label that reads the other way round) and carrying neither encoding is still invisible: test 2 skips it on the name, tests 3/4 skip it on the absent encodings, and test_nmd_axis_is_not_dropped_wholesale counts annotated values so it stays at 8. Not worth chasing with a "key contains NMD" rule — that would false-positive the day a legitimate NMD_TRANSCRIPT_VARIANT (SO:0001621) lands in the enum, which is a plausible addition. The current shape is the right trade-off; it's just worth not reading the guard as total.
3. Nit — _annotation tolerates two forms, the callers tolerate one. _annotation (:63-68) handles both the compact and {tag, value} annotation forms, but _nmd_mixins/_nmd_status are called with pv directly, so a bodyless permissible value (SOME_KEY: with no children, parsed as None) raises AttributeError in tests 2–4 and 6–8, while test_cross_reference_annotations_resolve handles it via pv or {} (:153). Same for the list-of-{tag, value} annotation form LinkML also accepts, which would AttributeError in _annotation itself. Both fail loudly rather than passing wrongly, so this is cosmetic — the fixture could just normalise pv or {} once and be done.
Verification note
Bash beyond git was not approved in this environment — my one attempt to load the YAML in Python was denied — so I did not run pytest, just test, or gen-owl. The table above is from reading the schema with grep/Read and checking each assertion by hand, not from execution. Both CI runs for dfb75ce were still in progress when I checked (Build and test — the meaningful one, since just test covers gen-project plus the now-73 tests). The commit reports 73 passed / 1 skipped locally, up 4 from the 69 at bca25b9, which matches the two added tests plus the three parametrised cases replacing one.
Nothing here blocks merge, and nothing I'd push for. Finding 1 is the only one with a real (if currently hypothetical) failure mode behind it.
· branch claude/gene2phenotype-terminology-bq51mi
The last review asked whether a dangling permissible-value is_a would be as silent as the annotation case, and proposed generalising the mixin resolution check across every enum on that basis. Measured rather than assumed, the premise does not hold: dangling PV is_a -> `just test` FAILS at _test-schema dangling PV mixins -> `just test` FAILS at _test-schema typo'd annotation value-> `_test-schema` exits 0, no diagnostic omitted nmd_status -> `_test-schema` exits 0, no diagnostic Structural references are resolved during OWL generation, which gen-project runs, so a typo in either fails the build with `ValueError: Cannot find permissible value`. That holds for mixins even though gen-owl discards them afterwards: it resolves first and drops after. The three unguarded is_a references in G2PVariantConsequence are therefore already covered, and generalising the check would add no coverage, so it is not done here. Annotation values are the surface nothing guards, which is what this suite is actually for. The module docstring now records the division of labour with the evidence, so the next person does not have to re-derive it, and states plainly that the suffix rule is not total, along with why the looser "key contains NMD" alternative was rejected: it would false-positive on a legitimate NMD_TRANSCRIPT_VARIANT (SO:0001621). This also corrects something I claimed in the previous round. I reported the typo'd-mixin mutation as previously invisible; it was invisible to this suite but would have failed CI. The genuinely silent modes are the annotation ones. Also normalise permissible value bodies to a dict in the fixture, so a value written without one does not raise AttributeError in the helpers. Verified with `just test` (73 passed, 1 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Finding 1 rests on an open question — "I don't know whether
Notably that holds for A correction to my last comment. I reported the typo'd-mixin mutation as previously invisible. It was invisible to this suite, but it would have failed CI — I'd only run 2 — agreed, and now stated in the file. The docstring records that the suffix rule isn't total and why the looser "key contains NMD" alternative was rejected: 3 — fixed. The fixture normalises bodies to a dict once, so the helpers can't The docstring now carries the whole division of labour with the evidence behind it, so nobody has to re-derive which surface the toolchain covers. 73 passed / 1 skipped; this commit touches only the test file. Generated by Claude Code |
|
Claude finished @cmungall's task in 3m 8s —— View job Re-review after
|
The review noted that the structural-reference claim added last commit is
now load-bearing, since it is the stated reason a test is absent, but was
recorded without the expiry condition the mixin-drop claim got. Both are
now pinned.
The structural-reference guarantee is recorded as measured on linkml
1.9.5 and holding only while `owl` is absent from `excludes` in
config.yaml. Adding it there would stop _test-schema resolving
permissible-value is_a/mixins at all, and nothing would report that the
docstring had become false.
The review also asked whether config.yaml's `mixins_as_expressions: true`
might surface the mixin parent, in which case the nmd_status workaround
could be dropped entirely. Measured: it does not. Running gen-owl with
the full config argument set, including --mixins-as-expressions, still
emits only the is_a parent; that flag governs class mixins, not
permissible-value mixins. The docstring now records that the drop was
measured on both OWL paths -- gen-owl bare, which is what `just gen-owl`
ships, and gen-owl under config.yaml args, which is what gen-project
runs -- so the workaround is justified on both rather than on the one
path I happened to test first.
Also corrected the stated benefit of test_mixins_resolve_to_permissible_
values. The clearer failure message only appears under a direct pytest
run; under `just test` the reader sees the generator traceback, because
_test-schema runs first and aborts. The test is still worth keeping for
the direct-pytest workflow, but the justification was a half-step off.
Nits from the same review: _annotation now handles the list-of-{tag,
value} annotation form as well as the two mapping forms, and the enum
body is normalised so a bodyless enum cannot AttributeError.
Verified with `just test` (73 passed, 1 skipped).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
All three taken in 2294e62, and finding 3 was worth running — thanks for spotting the second invocation path. 3 — SO:0002321 a owl:Class ;
rdfs:label "STOP_GAINED_NMD_TRIGGERING" ;
rdfs:subClassOf SO:0001587,
valuesets:G2PVariantType ;Still only the 1 — pinned. The structural-reference claim now carries the same expiry condition as the mixin-drop one: measured on linkml 1.9.5, holding only while 2 — justification corrected. You're right that Nits — Thanks also for checking 73 passed / 1 skipped; this commit touches only the test file, schema byte-identical since 332aa0e. Agreed the PR has converged — I'll leave it here unless something new comes up. Generated by Claude Code |
|
Claude finished @cmungall's task in 1m 59s —— View job Re-review after
|
| Docstring claim | Evidence |
|---|---|
owl not in excludes, so _test-schema resolves PV refs |
config.yaml:7-11 lists markdown, excel, python, pydantic only |
The config path includes mixins_as_expressions: true |
config.yaml:22 |
just gen-owl invokes it bare and writes the shipped artifact |
project.justfile:63,73 |
| linkml 1.9.5 / linkml-runtime 1.9.5 | uv.lock:1441-1442 |
_test-schema runs before pytest, so the traceback wins under just test |
justfile:88 (test: _test-schema _test-python _test-examples), justfile:192-193 |
| Schema unchanged | 8 mixins blocks, 8 nmd_status annotations, qualifiers at :841/:855 carry none |
The corrected justification on test_mixins_resolve_to_permissible_values (:140-143) is the right shape now — "its value is under a direct pytest run" is exactly the residual benefit, and it's stated rather than implied.
One thing left, small
The same generator claim exists a third time, in the schema, still unpinned — src/valuesets/schema/clinical/gene2phenotype.yaml:824-828
LinkML preserves both, but the OWL generator currently emits only the
is_aparent asrdfs:subClassOfand drops the mixin, so the NMD axis is additionally recorded in thenmd_statusannotation
Both copies in the test module now carry an expiry condition; this one doesn't — and it's the copy with the widest reach, since enum descriptions ship into the generated docs and OWL while the test docstring stays in the repo. "currently" is the word doing the work, with nothing saying when. A reader of the docs a year from now has no way to know whether to re-check. Since the commit's whole point was making these claims re-checkable, adding "as of linkml 1.9.5" to that sentence finishes the job at the cost of four words.
Notes, not findings
linkml is pinned in the lock but not in pyproject. uv.lock:1442 gives 1.9.5, uv.lock:3931 gives the constraint >=1.9.5. So a routine uv lock --upgrade moves the version the docstring names, silently. The docstring's "re-check both if either changes" covers this in spirit, and pinning a floor tighter isn't this PR's call — worth knowing that the version half of the expiry condition drifts more easily than the config.yaml half, which at least requires someone to edit a list deliberately.
_annotation's two extra branches are unexercised. :114-123 now handles the list-of-{tag, value} and {tag, value} mapping forms, but every annotation in the schema is the compact form, so only the last two lines ever run. The branches are correct as written (I traced both), just untested — and if one were wrong, the failure mode is value is None → continue in test_cross_reference_annotations_resolve (:212-213), i.e. a silent skip rather than a failure. Not worth a test for a helper this size; noting it because silent-skip is the exact shape of the blind spot two rounds ago. If the schema ever adopts a non-compact form, that's the moment to check the branch fires.
Verification note
Same as the last two rounds: just, uv and python3 are not runnable in this environment — Bash beyond git/grep was not approved, and my one attempt to load the YAML in Python was denied. So I did not run pytest, just test, or gen-owl, and I did not reproduce the mixins_as_expressions measurement. What I checked is that every file the docstring cites says what it's claimed to say, which establishes the claims are internally consistent with the repo, not that the generator behaves as measured.
CI: both runs for 2294e62 were still in progress when I checked (Build and test is the meaningful one). dfb75ce was green across 3.10–3.13, and since a3d4144 and 2294e62 are both test-only with the schema byte-identical, the only new question is whether the eight tests still pass.
This PR is done as far as I can take it. The remaining item is a four-word docstring edit that doesn't affect the artifacts.
· branch claude/gene2phenotype-terminology-bq51mi
The third copy of the mixin-drop claim lives in the G2PVariantType description, where "currently" was doing the work with nothing saying when. Both copies in the test module carry an expiry condition; this one did not, and it is the copy with the widest reach, since enum descriptions ship into the generated docs and OWL while the test docstring stays in the repo. Now says "as of linkml 1.9.5", and states that the annotation is redundant once a later linkml emits permissible-value mixins, so a reader of the generated docs knows what to re-check rather than having to rediscover it. Verified with `just validate-schema` and `just test` (73 passed, 1 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7
|
Fair catch — I pinned the two copies I'd been looking at and missed the one that actually ships. Done in 45db6ce: the On the two notes, both worth having on record: The lockfile point is the sharper of the two. The unexercised That's everything actionable. Schema is otherwise unchanged since 332aa0e, 73 passed / 1 skipped locally. Calling it converged here. Generated by Claude Code |
|
Claude finished @cmungall's task in 1m 18s —— View job Re-review after
|
| Check | State |
|---|---|
| All three copies of the mixin-drop claim pinned | schema :826, test docstring :15 (both OWL paths + mixins_as_expressions), test docstring :40 (the structural-reference claim, pinned to 1.9.5 and to owl being absent from excludes) |
| Bare "currently" left anywhere | only tests/test_g2p_nmd_axis.py:6, which the same docstring pins nine lines later at :15 — same reader, same scroll, fine |
| NMD encodings still balance | 8 mixins: blocks, 8 nmd_status: annotations |
| Schema drift since the last schema change | none — 45db6ce is the only schema commit after bca25b9 |
Nothing outstanding
I have no findings this round. For completeness, the two things I flagged last time as notes-not-findings are unchanged and still don't warrant action:
linkmlis>=1.9.5in the dependency constraint and 1.9.5 inuv.lock, so a routineuv lock --upgrademoves the version the three pins name without touching them. The docstrings' "re-check if either changes" covers the intent; pinning a ceiling isn't this PR's call._annotation's non-compact branches (tests/test_g2p_nmd_axis.py:114-123) are correct but unexercised, since every annotation in the schema uses the compact form.
One trivial correction to the record, no action needed: the previous commit message says the schema has been "byte-identical since 332aa0e". It's been identical since bca25b9 — bca25b9 changed 22 lines of the schema (the nmd_status PV-key fix, dropping the self-annotations, hoisting the convention note). Doesn't affect anything; noting it only because the last several rounds have been about claims being re-checkable, and that one isn't.
Verification note
Same as the last three rounds: just, uv and pytest are not runnable here — Bash beyond git/grep was not approved — so I did not run pytest, just test, just validate-schema, or gen-owl. The table above is git show plus grep over the working tree, not execution. The commit reports just validate-schema clean and just test at 73 passed / 1 skipped, consistent with the last three commits.
CI: both runs for 45db6ce were queued when I checked (Build and test is the meaningful one). dfb75ce was green across 3.10–3.13. Since this commit changes only prose inside a description: block, the build is a formality — but a YAML folded-scalar edit is exactly the kind of change worth letting go green before merge rather than eyeballing.
This PR is done. Nothing blocks merge; I'd merge on the green build.
· branch claude/gene2phenotype-terminology-bq51mi
Summary
This PR adds comprehensive value set definitions for Gene2Phenotype (G2P), the EBI resource of curated gene-disease associations. The implementation includes controlled vocabularies for all components of G2P's locus-genotype-mechanism-disease-evidence (LGMDE) framework.
Key Changes
New schema file:
src/valuesets/schema/clinical/gene2phenotype.yaml(1066 lines)Enum definitions:
G2PConfidenceCategory: Gene-disease association confidence levels (DEFINITIVE, STRONG, MODERATE, LIMITED, DISPUTED, REFUTED) with GenCC mappingsG2PAllelicRequirement: Genotype requirements using HPO mode-of-inheritance terms (autosomal dominant/recessive, X-linked, Y-linked, PAR, mitochondrial)G2PCrossCuttingModifier: Inheritance qualifiers (anticipation, imprinting, de novo, mosaic, incomplete penetrance, secondary findings, restricted mutations)G2PMolecularMechanism: Disease mechanisms per Backwell & Marsh framework (loss-of-function, gain-of-function, dominant-negative, undetermined)G2PMolecularMechanismSynopsis: Refined mechanism descriptions (destabilizing LOF, assembly-mediated GOF, aggregation, etc.)G2PMolecularMechanismSupport: Evidence vs. inferred mechanism classificationG2PMechanismEvidenceCategoryand related enums: ClinGen-based evidence categories (function, functional alteration, models, rescue)G2PVariantConsequence: Sequence Ontology terms for variant consequences (altered/decreased/increased/absent gene product level, altered structure, uncertain)G2PVariantType: Comprehensive SO variant types organized by group (NMD variants, splice variants, regulatory, protein-changing, other)G2PPanel: Disease-area panels for organizing curated associationsUpdated main schema: Added import of
clinical/gene2phenotypetosrc/valuesets/schema/valuesets.yamlUpdated ontology cache: Added 40 Sequence Ontology terms and 5 HPO terms to support G2P variant consequence and allelic requirement mappings
Implementation Details
meaning:to ontology terms using OBO CURIEshttps://claude.ai/code/session_01De8cxj98MtmCDAW3uuUZQ7