fhir-diff 0.1.0

Semantic diff of FHIR packages and profiles with a breaking-change gate
Documentation
fhir-diff hl7.fhir.us.core@6.1.0 hl7.fhir.us.core@9.0.0
fhir-diff ./old-ig/output ./new-ig/output --fail-on breaking --format sarif -o fhir-diff.sarif

The arguments are either name@version, fetched from the FHIR package registry into the local package store along with the package's dependencies, or a folder holding an unpacked package (a package/ subfolder or the files directly).

The first report is US Core 6.1.0 → 9.0.0. It compares 71 profiles and extensions: 12 added, 1 removed, 47 changed, with 64 breaking changes.

How it works

  1. Every StructureDefinition of both packages is converted to FHIR Schema.
  2. Each profile is merged over its whole base chain, down to the core type, into an effective element tree. A change inherited from a base profile counts as much as one the profile makes itself. The diff never trusts a differential on its own.
  3. The two trees of each profile (matched by canonical URL) are compared element by element, slices included (Patient.identifier:mrn.system).
  4. Every change is given a rule and a severity.

Choice elements are compared as one element whatever form the profile uses to narrow them. effective[x] restricted to dateTime and a constraint on effectiveDateTime compare equal.

Reference targets that move from a core type to a profile of the same type (Practitioner → us-core-practitioner) are reported once as target-narrowed.

Rules

Severity Rules
breaking: an instance valid against the old profile may be invalid against the new one, or implementers gain an obligation profile-removed, min-increased (including a new required slice), max-decreased, type-removed, target-removed, must-support-added, binding-stronger (to extensible or required), binding-added (required or extensible), invariant-added (error), fixed-added, fixed-changed, slicing-closed
warning: may break, depending on content the diff does not see value-set-changed (under a required or extensible binding), invariant-changed, invariant-warning-added, target-narrowed, base-changed, slice-removed, modifier-changed
info: relaxations and additions profile-added, min-decreased, max-increased, type-added, target-added, must-support-removed, binding-stronger-advisory (example → preferred), binding-weaker, invariant-removed, fixed-removed, slicing-opened, slice-added

Options

Option
--format text|markdown|json|sarif Text for a terminal or CI log (the default), Markdown for people, JSON for tools, SARIF 2.1.0 for code scanning
--min-severity info|warning|breaking Report only changes at least this severe
--fail-on never|warning|breaking Exit 1 when a change at least this severe is found (exit 2 on errors)
--profile URL|NAME Compare only these profiles (repeatable)
-o FILE Write the report to a file

GitHub Actions

- run: fhir-diff hl7.fhir.us.core@9.0.0 ./output --fail-on breaking --format sarif -o fhir-diff.sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: fhir-diff.sarif

SARIF results point at package/StructureDefinition-*.json. Their logical location is <profile url>#<element path>.

Accuracy

cargo run --example selfcheck -- <package> compares each effective tree with the snapshot the package publishes, on min, max and mustSupport of every element:

Package Elements Agree
hl7.fhir.us.core@9.0.0 2,455 99.4%
hl7.fhir.us.core@6.1.0 2,116 99.1%

Known gaps:

  • When a package does not list a dependency that one of its profiles is based on (US Core 6.1.0 does not list SDC), the base is taken from the other package's dependencies when it has one with the same canonical URL, and over the core type otherwise. The report says which under Notes.
  • mustSupport set on a choice through a type slice (value[x]:valueCodeableConcept) shows on the slice, not on the choice.
  • Value set content is not compared. A different value set under a required binding is a warning, not an expansion diff.

Library

[dependencies]
fhir-diff = { version = "0.1", default-features = false }

Without default features the crate reads packages from folders or from definitions you hand it (Loaded), and needs neither a network stack nor a package store. Features:

  • registry: load("name@version") installs from the FHIR package registry into the local package store (octofhir-canonical-manager).
  • cli (default): the fhir-diff command; enables registry.
let (old, new) = (fhir_diff::load("./old")?, fhir_diff::load("./new")?);
let (old_pool, new_pool) = fhir_diff::Pool::pair(&old, &new);
let report = fhir_diff::diff("old", &old_pool, "new", &new_pool, &Default::default());
println!("{}", fhir_diff::report::text(&report, fhir_diff::Severity::Info));

Install

cargo install --git https://github.com/octofhir/fhir-diff

Building

cargo build --release
cargo test

License

MIT OR Apache-2.0.