Skip to content

Add ref doctor, and a generated reference data page - #865

Merged
lewisjared merged 8 commits into
mainfrom
feat/ref-doctor
Aug 13, 2026
Merged

Add ref doctor, and a generated reference data page#865
lewisjared merged 8 commits into
mainfrom
feat/ref-doctor

Conversation

@lewisjared

@lewisjared lewisjared commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Adds a ref doctor cli command.
This command provides useful information about potential configuration and data issues.

The current rules cover:

  • Missing reference data
  • Data ingested as obs4ref
  • Duplicate coverage

but it new rules can be added in the core or via each producer.

Reference data page

A side effect of collating with reference data are needed we now publish the required reference datasets to the docs.

Summary by CodeRabbit

  • New Features

    • Added the ref doctor command to identify deployment issues, including missing, unreachable, duplicated, or overlapping reference data.
    • Added text, Markdown, and JSON reports, optional environment details, strict mode, and check listing.
    • Added generated Reference data documentation showing required datasets and suppliers.
    • Added a deployment diagnosis guide.
  • Bug Fixes

    • Database connection logs now redact passwords and credentials from URLs.

Three problems in this area are silent. Reference data that is missing means its
diagnostics plan no executions, so a deployment looks healthy while producing
nothing for them. Data ingested as `obs4ref` is never selected, because the
solver matches a requirement against its own source type and no diagnostic
declares an obs4REF requirement. And a dataset ingested from two collections at
the same version merges on instance_id into one dataset holding both sets of
files, so a diagnostic reads the overlapping period twice.

`ref doctor` reports all three, plus the registry overlaps that cause the third.
It exits non-zero on an error, or on a warning with --strict, so it can gate a
run. Checks are plain functions over a DoctorContext; one that raises becomes a
finding rather than stopping the rest.

Which collection supplies a given dataset was not written down anywhere, so it
had to be reconstructed from the providers' data requirements every time the
question came up. `climate_ref_core.reference_data` works it out once, and both
the new generated Reference data page and doctor's missing-data check read from
it, so the published table and the local check cannot disagree.

Provenance is resolved per (source type, source_id). Per source_id alone would
credit the PMP registry's ERA-5 climatology for obs4MIPs ERA-5. Per
(source_id, variable_id) would be a claim the archive cannot support: ESGF
intersects its facets, so a requirement naming four sources and eight variables
does not mean all thirty-two combinations exist.

Verified against two real deployments: on one that ingested both the obs4REF
registry and the ESGF mirror it reports exactly the five datasets that hold
duplicate coverage, and on one that ingested obs4REF under the obs4ref source
type it reports the eight reference datasets whose ILAMB diagnostics are
consequently unreachable, plus the source type as the cause.

Constraint: a check must be able to run against catalogs alone, so DoctorContext.from_catalogs builds one with no database behind it
Rejected: Add an ingest-time guard that drops a file whose period is already covered | preferring the published obs4MIPs copy is the agreed fix and belongs with the obs4REF source-type migration; doctor reports the condition until then
Rejected: Report missing data per (source_id, variable_id) | ESGF facet intersection makes most such pairs fictitious, which is how an earlier hand analysis produced 25 phantom gaps
Confidence: high
Scope-risk: moderate
Directive: A new check goes in CHECKS and must tolerate an empty catalog for every source type; `from_catalogs` fills the ones it is not given
Not-tested: The duplicate-coverage check assumes start_time/end_time are populated, so a catalog ingested with the DRS parser and not yet finalised will not be checked
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e64bbad-8759-4bbb-acba-a170792d2a6f

📥 Commits

Reviewing files that changed from the base of the PR and between 752a031 and 46a2002.

📒 Files selected for processing (10)
  • docs/gen_reference_data_stubs.py
  • docs/how-to-guides/diagnose-a-deployment.md
  • packages/climate-ref/src/climate_ref/cli/__init__.py
  • packages/climate-ref/src/climate_ref/doctor/checks/data.py
  • packages/climate-ref/src/climate_ref/doctor/environment.py
  • packages/climate-ref/src/climate_ref/doctor/registry.py
  • packages/climate-ref/tests/unit/cli/test_root.py
  • packages/climate-ref/tests/unit/test_database.py
  • packages/climate-ref/tests/unit/test_doctor_environment.py
  • packages/climate-ref/tests/unit/test_doctor_registry.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/climate-ref/tests/unit/test_database.py
  • packages/climate-ref/tests/unit/cli/test_root.py
  • docs/how-to-guides/diagnose-a-deployment.md
  • docs/gen_reference_data_stubs.py
  • packages/climate-ref/tests/unit/test_doctor_registry.py
  • packages/climate-ref/tests/unit/test_doctor_environment.py
  • packages/climate-ref/src/climate_ref/doctor/environment.py
  • packages/climate-ref/src/climate_ref/doctor/registry.py
  • packages/climate-ref/src/climate_ref/doctor/checks/data.py

📝 Walkthrough

Walkthrough

This change adds the ref doctor deployment diagnostic system, reference-data discovery and documentation, multiple report formats, plugin checks, resilient environment reporting, and database password redaction in connection logs.

Changes

Deployment diagnostics

Layer / File(s) Summary
Reference-data discovery and documentation
packages/climate-ref-core/src/climate_ref_core/..., packages/climate-ref-core/tests/unit/test_reference_data.py, docs/gen_reference_data_stubs.py, mkdocs.yml
Requirement summaries retain source_id values. New functions discover required datasets, resolve suppliers, render Markdown, and generate the Reference data page.
Diagnostic context, checks, and registry
packages/climate-ref/src/climate_ref/doctor/..., packages/climate-ref/tests/unit/test_doctor.py, packages/climate-ref/tests/unit/test_doctor_registry.py
The doctor package adds lazy catalogues, severity findings, built-in data checks, plugin loading, duplicate-slug validation, failure findings, and ordered check execution.
Reports, environment details, and CLI interface
packages/climate-ref/src/climate_ref/doctor/..., packages/climate-ref/src/climate_ref/cli/..., packages/climate-ref/src/climate_ref/text.py, packages/climate-ref/tests/unit/...
ref doctor supports text, Markdown, and JSON reports, environment details, check listing, strict warnings, and severity-based exit codes.
Deployment diagnosis guide
docs/how-to-guides/diagnose-a-deployment.md, changelog/865.feature.md, mkdocs.yml
The documentation describes doctor commands, report formats, APIs, check authoring, provider registration, and generated reference-data documentation.

Database URL redaction

Layer / File(s) Summary
Redacted database connection logging
packages/climate-ref/src/climate_ref/database.py, packages/climate-ref/tests/unit/test_database.py, changelog/865.fix.md
Database URLs are rendered without passwords. Unparseable URLs use a redacted placeholder. Connection logging uses the redacted URL.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant DoctorCLI
  participant DoctorContext
  participant Diagnose
  participant CheckRegistry
  Operator->>DoctorCLI: Run ref doctor
  DoctorCLI->>DoctorContext: Build deployment context
  DoctorCLI->>Diagnose: Request diagnosis
  Diagnose->>CheckRegistry: Run registered checks
  CheckRegistry-->>Diagnose: Return findings
  Diagnose-->>DoctorCLI: Return DoctorReport
  DoctorCLI-->>Operator: Render report and exit status
Loading

Possibly related PRs

Mergeability Score: ⚪ Minimal · up to 46a20

This PR adds a diagnostic command and generated reference-data documentation; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the two primary changes: the ref doctor command and generated reference data page.
Description check ✅ Passed The description explains both primary changes and their scope, but it omits the required checklist confirmations for tests, documentation and changelog entries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ref-doctor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.55672% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...te-ref-core/src/climate_ref_core/reference_data.py 93.51% 3 Missing and 4 partials ⚠️
packages/climate-ref/src/climate_ref/cli/doctor.py 97.24% 1 Missing and 2 partials ⚠️
.../climate-ref/src/climate_ref/doctor/checks/data.py 97.82% 1 Missing and 1 partial ⚠️
.../climate-ref/src/climate_ref/doctor/environment.py 97.33% 1 Missing and 1 partial ⚠️
Flag Coverage Δ
core 93.73% <97.55%> (+0.18%) ⬆️
providers 87.70% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/climate-ref-core/src/climate_ref_core/summary.py 98.80% <100.00%> (+<0.01%) ⬆️
...ckages/climate-ref/src/climate_ref/cli/__init__.py 92.75% <100.00%> (+0.50%) ⬆️
packages/climate-ref/src/climate_ref/database.py 96.92% <100.00%> (+0.13%) ⬆️
...ges/climate-ref/src/climate_ref/doctor/__init__.py 100.00% <100.00%> (ø)
...mate-ref/src/climate_ref/doctor/checks/__init__.py 100.00% <100.00%> (ø)
...ages/climate-ref/src/climate_ref/doctor/context.py 100.00% <100.00%> (ø)
...ges/climate-ref/src/climate_ref/doctor/findings.py 100.00% <100.00%> (ø)
...ges/climate-ref/src/climate_ref/doctor/registry.py 100.00% <100.00%> (ø)
...kages/climate-ref/src/climate_ref/doctor/report.py 100.00% <100.00%> (ø)
packages/climate-ref/src/climate_ref/text.py 100.00% <100.00%> (ø)
... and 4 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Two things were missing for doctor to grow the way it needs to. A check could only
be added by editing a tuple in `climate_ref`, so a provider package could not ship
one for its own setup problems. And the output named the finding but not the
deployment, which is most of what a maintainer has to ask for when a report arrives.

A check is now declared with `@check(slug, description)` and found through the
`climate-ref.doctor-checks` entry point group, alongside `climate-ref.providers`.
The slug lives only in the registration: the runner stamps it onto every finding,
so the two cannot drift. A plugin module that fails to import becomes an error
finding rather than taking the command down, because a check that never ran must
not look like a check that passed. `ref doctor --list` shows what is registered
and where each check came from.

`--format markdown` and `--format json` add the versions, platform, configuration,
paths, providers, ingested counts and REF_/DASK_/ESMVALTOOL_ variables of the
deployment. Redaction happens where the values are collected rather than where
they are displayed, so no output format can leak a credential; SQLAlchemy parses
the database URL, since it is what parses it to build the engine.

The module became a package: findings, context, registry and environment separate
from the checks themselves, which now have somewhere to go other than one file.

Doctor also no longer configures or registers providers when it loads them. It
reads their metadata only, and the docstring already claimed it does not write to
the database it is inspecting; `configure` bootstraps conda as a side effect.

Constraint: a check must tolerate a context built from catalogs alone, with no database behind it
Rejected: Reuse cli/_utils.OutputFormat | its members are table/json and findings are not a DataFrame; per-command format enums are the existing convention
Rejected: Have `ref --version` render the doctor version block | a wider change to a stable output than this needs
Confidence: high
Scope-risk: moderate
Directive: Redact in environment.py, never in a renderer, so a new output format cannot leak a credential
Not-tested: A real plugin distribution; the entry point group is exercised through a stubbed `importlib.metadata.entry_points`
Base automatically changed from docs/obs4mips-fetch-requirements to main August 13, 2026 05:37
A Postgres deployment wrote its credentials into the log on every startup,
and into anything that log was pasted into.

- Adds `redact_url`, which hides the password and leaves the rest legible.
- The doctor environment report already did this, so it now shares the one implementation.
- An unparseable URL is redacted whole rather than printed on the hope it holds nothing.
The report repeated the whole remedy paragraph and the check slug once per finding,
so a deployment missing seventeen reference datasets printed the same instruction seventeen times.

- Splits `Finding.detail` into `detail`, `remedy` and `command`.
  Findings sharing a remedy are reported under it once, ahead of the list they apply to.
- `command` is printed alone and unwrapped, so it survives a narrow terminal and stays pasteable.
- Groups the findings by check, so the slug is named once per group rather than on every line.
- Moves the summary count above the findings, and gives wrapped lines the indent of the line they continue.
- Adds `pluralise`, so a count reads `1 diagnostic` rather than `1 diagnostic(s)`.

Also fixes `ref doctor` no longer being recognised as read only,
because the argument walk stopped matching a command that sits at the top level rather than in a group.
Displaying a report meant learning five calls and the order to make them in:
build a context, run the checks, count them, collect the environment, then rank what came back.

- Adds `diagnose`, which does all of that and returns a `DoctorReport`.
- Drops `register_check`, which had no callers, and `SEVERITY_ORDER`, which was `tuple(Severity)`.
- Collapses `EnvironmentReport` into the dict it wrapped, since it carried one field.
- Keeps `RegisteredCheck`, `run_checks` and `collect_environment` for the package's own use.
- Moves `pluralise` to `climate_ref.text`, because it is a text helper rather than a doctor concept.

This takes the exports from twelve to eight, split between the two audiences:
`diagnose`, `DoctorReport`, `Finding`, `Severity`, `worst_severity` and `iter_checks` for a caller,
`check` and `DoctorContext` for a package contributing its own checks.
Comment thread packages/climate-ref/tests/unit/test_database.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
packages/climate-ref/tests/unit/test_doctor.py (1)

200-204: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert that environment collection stays offline.

Add the no_network fixture and assert that no blocked socket attempt occurred. This test uses an in-memory DoctorContext, so a future network call should fail the test.

Based on learnings, offline tests should record blocked socket attempts and assert that no attempt occurred where resolution is expected to be fully offline.

Source: Learnings

packages/climate-ref-core/src/climate_ref_core/reference_data.py (1)

38-45: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the source-type value set out of the loop.

Line 195 rebuilds {t.value for t in REFERENCE_SOURCE_TYPES} for every requirement of every diagnostic. Compute the values once at module level.

♻️ Proposed refactor
 REFERENCE_SOURCE_TYPES = frozenset(
     {
         SourceDatasetType.obs4MIPs,
         SourceDatasetType.obs4REF,
         SourceDatasetType.PMPClimatology,
         SourceDatasetType.ESMValToolReference,
     }
 )
+
+_REFERENCE_SOURCE_TYPE_VALUES = frozenset(t.value for t in REFERENCE_SOURCE_TYPES)
-                    if requirement.source_type not in {t.value for t in REFERENCE_SOURCE_TYPES}:
+                    if requirement.source_type not in _REFERENCE_SOURCE_TYPE_VALUES:
                         continue

Also applies to: 193-200

packages/climate-ref/src/climate_ref/doctor/context.py (1)

16-17: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Do not share one mutable DataFrame between source types and contexts.

EMPTY_CATALOG is a single mutable object. from_catalogs assigns that same instance to every absent source type, and catalog() returns it to callers. If any check or caller mutates the returned frame in place, the mutation becomes visible for all other source types and all other contexts. Return a new empty frame instead.

♻️ Proposed refactor
-EMPTY_CATALOG = pd.DataFrame()
-"""Stands in for a source type with nothing ingested."""
+def empty_catalog() -> pd.DataFrame:
+    """Stand in for a source type with nothing ingested."""
+    return pd.DataFrame()
         complete = {
-            source_type: catalogs.get(source_type, EMPTY_CATALOG) for source_type in SourceDatasetType
+            source_type: catalogs.get(source_type, empty_catalog()) for source_type in SourceDatasetType
         }

Also applies to: 58-61

packages/climate-ref/src/climate_ref/doctor/checks/data.py (1)

62-89: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The reported overlapping pair can name the wrong partner file.

The sweep tracks running_end across all earlier rows, but line 71 always records paths[index - 1] as the partner. If row index overlaps an earlier row rather than its immediate predecessor, the detail names a file that does not overlap. The count and the finding stay correct, so this only affects the reported collection roots.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6b0d370-57ba-4d5b-be94-c329d55459f7

📥 Commits

Reviewing files that changed from the base of the PR and between 14c0ea9 and 752a031.

📒 Files selected for processing (27)
  • changelog/865.feature.md
  • changelog/865.fix.md
  • docs/gen_reference_data_stubs.py
  • docs/how-to-guides/diagnose-a-deployment.md
  • mkdocs.yml
  • packages/climate-ref-core/src/climate_ref_core/reference_data.py
  • packages/climate-ref-core/src/climate_ref_core/summary.py
  • packages/climate-ref-core/tests/unit/test_reference_data.py
  • packages/climate-ref/src/climate_ref/cli/__init__.py
  • packages/climate-ref/src/climate_ref/cli/doctor.py
  • packages/climate-ref/src/climate_ref/database.py
  • packages/climate-ref/src/climate_ref/doctor/__init__.py
  • packages/climate-ref/src/climate_ref/doctor/checks/__init__.py
  • packages/climate-ref/src/climate_ref/doctor/checks/data.py
  • packages/climate-ref/src/climate_ref/doctor/context.py
  • packages/climate-ref/src/climate_ref/doctor/environment.py
  • packages/climate-ref/src/climate_ref/doctor/findings.py
  • packages/climate-ref/src/climate_ref/doctor/registry.py
  • packages/climate-ref/src/climate_ref/doctor/report.py
  • packages/climate-ref/src/climate_ref/text.py
  • packages/climate-ref/tests/unit/cli/test_doctor.py
  • packages/climate-ref/tests/unit/cli/test_root.py
  • packages/climate-ref/tests/unit/test_database.py
  • packages/climate-ref/tests/unit/test_doctor.py
  • packages/climate-ref/tests/unit/test_doctor_environment.py
  • packages/climate-ref/tests/unit/test_doctor_registry.py
  • packages/climate-ref/tests/unit/test_text.py

Comment thread docs/gen_reference_data_stubs.py Outdated
Comment thread docs/how-to-guides/diagnose-a-deployment.md Outdated
Comment thread packages/climate-ref/src/climate_ref/cli/__init__.py Outdated
Comment thread packages/climate-ref/src/climate_ref/doctor/checks/data.py
Comment thread packages/climate-ref/src/climate_ref/doctor/environment.py Outdated
Comment thread packages/climate-ref/src/climate_ref/doctor/registry.py
- Redacts a password held inside a URL-valued environment variable, not just a
  variable whose name marks it as a secret.
- Matches a read-only top-level command in the command path only, so a path or an
  option value reading as a command name no longer skips the pre-migration backup.
- Drops the checks a plugin registered before its import failed, so a half-loaded
  plugin contributes nothing.
- Guards the catalog columns both data checks read, so an adapter with a different
  schema does not stop the run.
- Skips only an unavailable provider when generating the reference data page, and
  reports why.
- Fixes two missing words in the how-to guide.
@lewisjared
lewisjared merged commit 11a4fea into main Aug 13, 2026
28 checks passed
@lewisjared
lewisjared deleted the feat/ref-doctor branch August 13, 2026 10:12
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