Skip to content

Add LinkML schema for harmonized soil moisture data#3

Open
cmungall wants to merge 2 commits into
mainfrom
claude/linkml-schema-equivalent-xwqvjx
Open

Add LinkML schema for harmonized soil moisture data#3
cmungall wants to merge 2 commits into
mainfrom
claude/linkml-schema-equivalent-xwqvjx

Conversation

@cmungall

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a LinkML schema definition for the harmonized soil moisture dataset, providing a formal semantic representation of the canonical 9-column target schema that all harmonized datasets must conform to.

Key Changes

  • New file: src/schemas/target_schema.yaml - A complete LinkML schema defining the harmonized soil moisture data structure
    • Defines HarmonizedRecord class representing a single row with 9 slots (datetime_UTC, site_id, depth_m, replicate, is_timeseries, interval_min, and three moisture measurement variables)
    • Defines HarmonizedDataset class as a collection of harmonized records
    • Includes validation rule ensuring at least one of the three moisture variables (volumetric water content, gravimetric water content, or water potential) is present in each record
    • Specifies data types, units (UCUM codes), valid ranges, and rank ordering for all slots
    • Defines QCFlagVocabulary enumeration for quality control flag values
    • Mirrors constraints from the existing target_schema.py module (HARMONIZED_COLUMNS, EXPECTED_DTYPES, VALID_RANGES)

Notable Implementation Details

  • Uses standard LinkML imports and prefixes (linkml, QUDT for units)
  • All moisture measurement slots are optional but constrained by the postcondition rule
  • datetime_UTC and site_id are marked as required
  • Floating-point ranges accommodate domain-specific requirements (e.g., gravimetric water content up to 10.0 for organic-rich soils, water potential from -1,000,000 to 0 kPa)
  • Provides semantic documentation linking the LinkML schema to the Python implementation

https://claude.ai/code/session_01FUi8KYjn6jZ3QtznZi7yXE

claude added 2 commits June 30, 2026 18:17
Introduce src/schemas/target_schema.yaml, a LinkML representation of the
canonical 9-column harmonized dataset schema defined in target_schema.py.

- HarmonizedRecord class with one slot per harmonized column, in the same
  order as HARMONIZED_COLUMNS
- Slot ranges mirror EXPECTED_DTYPES (datetime, string, float, boolean)
- minimum_value/maximum_value constraints mirror VALID_RANGES, with UCUM
  units on the measurement slots
- datetime_UTC and site_id marked required; a class rule enforces that at
  least one moisture variable is present (matching validate_required_fields)
- QCFlagVocabulary enum mirrors the Python enum (d1, g1, g2)
- HarmonizedDataset wrapper class for a collection of records

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUi8KYjn6jZ3QtznZi7yXE
Build out src/schemas/target_schema.yaml into a conventional LinkML project
with examples, generated artifacts, and CI.

- examples/valid and examples/invalid: instance files that must pass / must
  fail validation, covering required fields, value ranges, types, and the
  at-least-one-moisture-variable rule
- project/pydantic/target_schema.py and project/jsonschema/target_schema.schema.json:
  generated, committed artifacts (gen-pydantic / gen-json-schema)
- Reworked the moisture rule with an always-true precondition so it emits a
  well-formed if/then conditional that JSON Schema validators enforce
- Dropped the unused qudt prefix; added .linkmllint.yaml disabling the naming
  rule (column names are a fixed external contract)
- Makefile with gen / lint / test-examples / test-pytest targets
- tests/test_target_schema_examples.py: pytest wrapper (skipped without linkml)
- .github/workflows/linkml.yml: lint, validate examples, run pytest, and fail
  if committed artifacts drift from the schema
- linkml optional-dependency group in pyproject.toml; README schema section;
  ignore examples/output working dir

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUi8KYjn6jZ3QtznZi7yXE
@cmungall

Copy link
Copy Markdown
Collaborator Author

@claude review this PR

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cmungall's task —— View job


PR Review

  • Gather context (read key files)
  • Review main LinkML schema
  • Review examples (valid/invalid)
  • Review tests
  • Review generated artifacts (jsonschema, pydantic)
  • Review CI/Makefile setup
  • Compare with existing target_schema.py
  • Post review feedback

Overall this is a well-structured, clean PR. The schema faithfully mirrors target_schema.py and the supporting toolchain (CI, Makefile, examples, tests) is solid. A few issues worth addressing:

Issues

1. QCFlagVocabulary is defined but never used in the schema (src/schemas/target_schema.yaml:176-188)

The enum exists but no slot in HarmonizedRecord or HarmonizedDataset references it. In target_schema.py it is also a standalone enum (no column uses it directly), but the disconnect between the two files is worth documenting. Either add a comment explaining it is reserved for future use, or link it to a concrete slot if one is planned.

2. The JSON Schema if/then rule does not reject null moisture values (project/jsonschema/target_schema.schema.json:24-133)

The generated JSON Schema uses "required" in the then branch to enforce "at least one moisture variable present". However, because all three moisture slots have "type": ["number", "null"], passing volumetric_water_content_m3_m3: null will satisfy "required" while the value is absent. The generated artifact reflects a genuine LinkML limitation here; worth noting in a comment or PR description so downstream consumers are aware.

3. Pydantic model does not enforce the any_of moisture rule at runtime (project/pydantic/target_schema.py:109-134)

The rules block is stored in linkml_meta metadata but no Pydantic model_validator is generated to actually enforce it. A record with all three moisture fields None would pass HarmonizedRecord.model_validate(...). This is a known LinkML limitation for complex rules in Pydantic generation; worth a comment so consumers don't assume the Pydantic model is the validation layer.

4. replicate has no range constraints (src/schemas/target_schema.yaml:107-113)

target_schema.py does not define a range for replicate either, so this is consistent, but a minimum of 0 or 1 might be appropriate to prevent negative replicate numbers. At minimum worth documenting why it is unconstrained.

5. Minor: interval_min upper bound is inconsistent between comment and value

target_schema.py:51 says # up to ~2 years but 1,000,000 minutes ≈ 1.9 years. The LinkML schema (src/schemas/target_schema.yaml:133) sets maximum_value: 1000000.0 correctly, but the comment in target_schema.py is slightly misleading. Not a schema bug, but could cause confusion.

Positives

  • Faithful mirroring: All 9 columns, dtypes, ranges, and the at-least-one-moisture rule are correctly translated.
  • Good use of rank: Slot ordering is explicitly encoded, which helps tooling maintain column order.
  • Sensible unit annotations: UCUM codes (m, min, m3/m3, g/g, kPa) are correct and will integrate well with ontology-aware tools.
  • CI check for artifact freshness (linkml.yml:55-63): The git diff --exit-code check on project/ is a clean way to prevent stale generated files.
  • Invalid examples are well-chosen: The 6 invalid examples cover the main constraint categories (range, required, type, rule).
  • Test skipping: Using pytest.importorskip (tests/test_target_schema_examples.py:20) is the right pattern so the main test suite doesn't break without the optional linkml dep.
  • .linkmllint.yaml: Disabling standard_naming with a clear comment is the correct call given the external naming contract.

Minor nits

  • src/schemas/target_schema.yaml has no version field; consider adding one (e.g., version: "0.1.0") to align with pyproject.toml.
  • The generated project/pydantic/target_schema.py embeds version = "None" (a string) on line 33 — this is a LinkML generator artifact from the missing version field above.
  • HarmonizedDataset.records is Optional[list[...]] in the generated Pydantic model (line 143), meaning an empty/missing dataset is valid. Whether that should be required is a design choice worth deciding explicitly.

cmungall added a commit that referenced this pull request Jun 30, 2026
- Fix repo_root default-arg bug: Option default None, resolve in body (was
  Path.cwd() evaluated at import time) [#6]
- Audit the LS tool (path) [#8]; check Grep/Glob 'glob'/'pattern' for
  path-bearing patterns [#7]
- Flag 'cd' into an answer location (answer key / real gold/processed), so a
  single-component read after it isn't missed [#1]
- Remove dead '&& ||' token guard [#3]
- --show-bash prints full multi-line commands instead of just the first line [#5]
- Tests: LS, Grep glob, cd-into-answer-key, multi-cd chain, ~ expansion

53 tests green; real fold_ds07 trace still CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants