fhir-diff 0.1.0

Semantic diff of FHIR packages and profiles with a breaking-change gate
Documentation
<p align="center"><img src=".github/banner.png" alt="octo/diff — FHIR profiles compared, breaking changes caught in CI" width="100%"></p>

<p align="center"><b>Semantic diff of FHIR packages and profiles, with a breaking-change gate for CI.</b></p>

<p align="center">
  <a href="https://github.com/octofhir/fhir-diff/actions/workflows/ci.yml"><img src="https://github.com/octofhir/fhir-diff/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="#license"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg" alt="License: MIT OR Apache-2.0"></a>
</p>

```sh
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](reports/us-core-6.1.0-to-9.0.0.md). 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]https://github.com/octofhir/fhirschema.
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

```yaml
- 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

```toml
[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`.

```rust
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

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

## Building

```sh
cargo build --release
cargo test
```

## License

MIT OR Apache-2.0.