Skip to content

Constrain the CultivationSetup unit slots, and make a new enum declare itself (#514, #518) - #517

Open
realmarcin wants to merge 2 commits into
enrich-cultivation-183gfrom
unit-enums-514
Open

Constrain the CultivationSetup unit slots, and make a new enum declare itself (#514, #518)#517
realmarcin wants to merge 2 commits into
enrich-cultivation-183gfrom
unit-enums-514

Conversation

@realmarcin

@realmarcin realmarcin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #515 (base enrich-cultivation-183g), because it corrects a value that PR introduces. Review #515 first.

What (#514)

Six CultivationSetup slots carry a unit or a kind — working_volume_unit, operating_temperature_unit, feed_or_dilution_rate_unit, retention_time_unit, applied_potential_unit, retention_time_type — and none had a range, so linkml-validate accepted any string. Six enums now constrain them.

Symbols, not SCREAMING_CASE. A unit is not a category. °C and 1/h are what every source and reader already writes, and the corpus was already using symbols (h, d, L/day, mL, mV). A slot that says PER_HOUR has to be translated back before it means anything.

The cost: these aren't valid Python identifiers. LinkML emits them via an _addvals/setattr classmethod rather than as class attributes, so dir(), a static import, and _defn.permissible_values all miss them. I checked this before building the rest — my first runtime assertion reported °C as absent when it was present. The test now queries the runtime enum the way that actually works.

What the gate caught on its first run

⚠️ One deliberate narrowing — a decision, not a fix

applied_potential_unit was documented as taking "mV vs SHE", and the smoke test pinned that shape. That makes one value both a unit and a reference electrode: not joinable across records, not convertible, and not a unit.

The enum is {V, mV}; the reference electrode moves to electrode_detail, where it can be stated in full rather than compressed into a suffix. No curated record used the old shape, so nothing real is lost — but this removes something the schema allowed and a test asserted. Reversible by adding mV vs SHE/V vs SHE as permissible values if you'd rather keep it.

What review then found (#518)

vocab/cultivation_terms.yaml states an invariant in its own header — vocab keys must equal enum permissible values — and enforcement was:

ENUMS = ("CultivationModeEnum", "CultivationSystemEnum")

A hardcoded tuple with no completeness check. Adding six enums here is the case that exposed it: none of them went red. That's the right outcome — units belong to UO/UCUM, not METPO — but the repo could not distinguish "deliberately exempt" from "nobody noticed". Same shape as #471, where nine tests stayed green because they pinned a constant's value and never its use.

Every one of the schema's 26 enums is now either staged or exempt with a written reason. The five that are genuine ontology candidates (FunctionalRoleEnum, InteractionTypeEnum, AtmosphereEnum, MediaRelationshipEnum, EcologicalStateEnum) say "candidate, not yet staged" rather than being lumped in with bookkeeping — the exemption is "not done", not "not applicable". Reasons are asserted non-empty so the dictionary can't decay into an allow-list.

Checks

  • just validate-all — exit 0 across the corpus
  • just lint, just validate-strict — exit 0
  • uv run pytest tests/ — 2387 passed, 16 skipped
  • Mutation-checked twice. Removing the six range: lines turns 7 of 11 new tests red, including test_a_wrong_unit_is_actually_rejected, which shells out to the real validator and requires a non-zero exit on operating_temperature_unit: CELSIUS. Adding a FakeNewEnum to the schema turns the The vocab-sync invariant applies to a hardcoded list of two enums and cannot notice a third #518 guard red. Both verified by reverting, running, and restoring.

Closes #514, closes #518.

🤖 Generated with Claude Code

Six slots carry a unit or a kind and none had a range, so linkml-validate
accepted any string. Six enums now: TimeUnitEnum, VolumeUnitEnum, RateUnitEnum,
TemperatureUnitEnum, PotentialUnitEnum, RetentionTimeTypeEnum.

Symbols, not SCREAMING_CASE. A unit is not a category — °C and 1/h are what
every source and reader already writes, and PER_HOUR has to be translated back
before it means anything. The cost is that the values are not Python
identifiers, so LinkML emits them through _addvals/setattr; the test checks the
runtime enum rather than dir(), which is the check that would otherwise pass
while a value was silently absent.

The gate found two things on its first run. `retention_time_type: HYDRAULIC`,
which I wrote in #512 against a slot whose description has always said "HRT" or
"SRT" — a description is not a constraint. And the repo's own smoke test used
`h^-1`, a third spelling of per-hour alongside `1/h` and the `PER_HOUR` I wrote
in #515.

One thing is deliberately narrowed rather than preserved: applied_potential_unit
was documented as taking "mV vs SHE", making the value a unit and a reference
electrode at once — not joinable across records and not convertible. The enum is
{V, mV} and the reference moves to electrode_detail, where it can be stated in
full. The smoke test exercised the old shape, so this changes what the schema
allows; called out in the PR since it is a modelling decision, not a fix.

Mutation-checked: removing the six ranges turns 7 of the 11 tests red, including
the one that shells out to the real validator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Network integrity findings

Warnings only — a member with no interaction yet, or a participant matched by ontology id rather than by name, or one on a community-level interaction that resolves to no member. Reported, but does not fail the build.

Network Integrity Audit Report
================================================================================

0 error, 55 warning across 27 records with findings
Only error-severity findings fail the build.

ANME_SRB_Anaerobic_Methanotrophic_Syntrophic_Consortia
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'ANME-1' matches no taxonomy entry by name; resolved to 'ANME-1 (anaerobic methanotrophic archaea, clade 1)' by source_id NCBITaxon:588814
  [warning] NAME_MISMATCH: Target 'Desulfofervidus' matches no taxonomy entry by name; resolved to 'Desulfofervidus (sulfate-reducing bacterial partner of ANME-1)' by target_id NCBITaxon:1902583
  [warning] NAME_MISMATCH: Source 'ANME-2a' matches no taxonomy entry by name; resolved to 'ANME-2a (anaerobic methanotrophic archaea, clade 2a)' by source_id NCBITaxon:588816
  [warning] NAME_MISMATCH: Target 'Seep-SRB1' matches no taxonomy entry by name; resolved to 'Seep-SRB1 (sulfate-reducing bacterial partner of ANME-2a)' by target_id NCBITaxon:213119
  [warning] NAME_MISMATCH: Source 'ANME-2c' matches no taxonomy entry by name; resolved to 'ANME-2c (anaerobic methanotrophic archaea, clade 2c)' by source_id NCBITaxon:3386252
  [warning] NAME_MISMATCH: Target 'Seep-SRB2' matches no taxonomy entry by name; resolved to 'Seep-SRB2 (additional sulfate-reducing bacterial partner)' by target_id NCBITaxon:213118

Total: 6 issues (0 error, 6 warning)

Aalborg_East_Full_Scale_EBPR_Community
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacteroidetes flanking community members' has no interactions
  [warning] DISCONNECTED: Taxon 'Tetrasphaera-related actinobacterial PAOs' has no interactions

Total: 2 issues (0 error, 2 warning)

Bacteroides_Methanobrevibacter_Gnotobiotic_Mouse_Mutualism
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Desulfovibrio piger comparator' has no interactions

Total: 1 issues (0 error, 1 warning)

BioModels_MODEL2204300001_Kefir_Community_Model
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Lactobacillus kefiri' has no interactions
  [warning] DISCONNECTED: Taxon 'Lactococcus lactis' has no interactions

Total: 2 issues (0 error, 2 warning)

BioModels_MODEL2405300001_Infant_Gut_HMO_SynCom
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacteroides ovatus' has no interactions
  [warning] DISCONNECTED: Taxon 'Bacteroides vulgatus' has no interactions
  [warning] DISCONNECTED: Taxon 'Bifidobacterium bifidum' has no interactions
  [warning] DISCONNECTED: Taxon 'Bifidobacterium breve' has no interactions
  [warning] DISCONNECTED: Taxon 'Blautia producta' has no interactions
  [warning] DISCONNECTED: Taxon 'Enterococcus faecalis' has no interactions
  [warning] DISCONNECTED: Taxon 'Escherichia coli K-12' has no interactions
  [warning] DISCONNECTED: Taxon 'Lacticaseibacillus rhamnosus' has no interactions
  [warning] DISCONNECTED: Taxon 'Ruminococcus gnavus' has no interactions
  [warning] DISCONNECTED: Taxon 'Streptococcus thermophilus' has no interactions

Total: 10 issues (0 error, 10 warning)

Crucian_Carp_Gut_Disease_Resistance_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Aeromonas hydrophila' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Drosophila_FiveSpecies_Gnotobiotic_Gut_Microbiota
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Drosophila five-species bacterial microbiota' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

East_River_Floodplain_Core_Microbiome
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'core floodplain bacteria' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_SOURCE: Source taxon 'East River floodplain bacteria' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Ensifer_YF2_Sphingobacterium_Y2_Polyethylene_Degrading_Consortium
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Chryseobacterium sp. MF1' has no interactions

Total: 1 issues (0 error, 1 warning)

GLBRC_UFMP_Fermentation_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Olsenella (Actinobacteriota)' not found in taxonomy section (community-level scope)
  [warning] NAME_MISMATCH: Target 'Clostridium (Firmicutes)' matches no taxonomy entry by name; resolved to 'Clostridium_B sp. (MAG CLOS1)' by target_id NCBITaxon:1485

Total: 2 issues (0 error, 2 warning)

Hanford_300_Area_Unconfined_Aquifer_Community
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'intrusion-associated Actinobacteria' matches no taxonomy entry by name; resolved to 'Actinobacteria/Actinomycetota aquifer bacteria' by source_id NCBITaxon:201174
  [warning] UNKNOWN_TARGET: Target taxon 'Hanford groundwater bacteria' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_SOURCE: Source taxon 'aquifer redox guild bacteria and archaea' not found in taxonomy section (community-level scope)

Total: 3 issues (0 error, 3 warning)

High_Solids_Switchgrass_Methanogenic_Microbiome
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacilli' has no interactions

Total: 1 issues (0 error, 1 warning)

KB1_Chlorinated_Ethene_Dechlorinating_Consortium
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Sporomusa spp. in KB-1' has no interactions

Total: 1 issues (0 error, 1 warning)

Legume_Rhizobia_Mars_Simulant_Symbiosis
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'Sinorhizobium spp. (rhizobial symbionts)' matches no taxonomy entry by name; resolved to 'Sinorhizobium meliloti' by source_id NCBITaxon:382
  [warning] UNKNOWN_TARGET: Target taxon 'Medicago truncatula (host legume)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Lunar_Martian_Simulant_PGPB_Lettuce_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Lactuca sativa (lettuce host)' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Lactuca sativa (lettuce host)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Lunar_Simulant_Phosphate_Solubilizing_Bacteria_Nicotiana
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Nicotiana benthamiana' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Model_Lignocellulose_Formaldehyde_Crossfeeding_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'model lignocellulose consortium members' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'model lignocellulose consortium members' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Moss_Microbe_Complex_Regolith_Biofertilizer
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Hypnum plumaeforme (moss host)' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Hordeum vulgare (barley model crop)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

ORNL_Clostridium_Desulfovibrio_Geobacter_Trophic_Model
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Desulfovibrio vulgaris Hildenborough and Geobacter sulfurreducens' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'three-species model community' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Oak_Ridge_FRC_Uranium_Nitrate_Groundwater_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'other groundwater bacteria' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

PET_Artificial_FourSpecies_Degradation_Consortium
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'engineered PETase/MHETase and TPA-utilization members' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

PMI_Variovorax_Thermotolerance_Collection
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Variovorax' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Arabidopsis thaliana' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Rice_Duckweed_Bacillus_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Bacillus SynCom' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Rhizoctonia solani' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Saanich_Inlet_OMZ_Redox_Gradient_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Saanich Inlet redox-gradient microorganisms' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Shewanella_Geobacter_Exoelectrogenic_Biofilm_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'anode-associated biofilm community' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'anode' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Sulfide_Spring_Autotrophic_CPR_Biofilm
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Beggiatoa' has no interactions

Total: 1 issues (0 error, 1 warning)

Thermophilic_Lignocellulose_Composting_SynCom_Biosanitization
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Pseudomonas aeruginosa' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

The full report is attached to the workflow run as an artifact.

vocab/cultivation_terms.yaml states an invariant in its own header — the keys
under each enum must equal that enum's permissible values — and enforcement was
`ENUMS = ("CultivationModeEnum", "CultivationSystemEnum")`, a hardcoded tuple
with no completeness check. The invariant was a property of two names someone
typed, not of the file.

Adding six enums in this PR is the case that exposed it: none went red. That is
the right outcome, since they are units and would map to UO or UCUM rather than
METPO — but the repo could not tell a deliberate exemption from an oversight.
Same shape as #471, where nine tests stayed green because they pinned a
constant's value and never its use.

Every schema enum is now either staged or exempt with a written reason, and the
five that are genuine ontology candidates say "not yet staged" rather than
being quietly lumped in with bookkeeping. Reasons are asserted non-empty so the
dictionary cannot decay into an allow-list.

Mutation-checked: adding a `FakeNewEnum` to the schema turns it red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@realmarcin realmarcin changed the title Constrain the CultivationSetup unit slots (#514) Constrain the CultivationSetup unit slots, and make a new enum declare itself (#514, #518) Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant