Skip to content

Compatibility checking

Protean's IR (Intermediate Representation) tooling helps you detect breaking changes to your domain model before they reach production. This guide walks you through setting up the .protean/ directory, configuring pre-commit hooks, and adding compatibility checks to CI.

For the classification rules that determine what counts as a breaking change, see the Compatibility Reference.


Step 1: Materialize the IR baseline

The .protean/ directory holds your materialized IR snapshot -- the baseline that changes are compared against. The easiest way to create it is with the pre-commit hook's --fix flag (see Step 3), which auto-creates the directory and generates the IR on every commit.

To generate the baseline manually:

protean ir show --domain myapp.domain > .protean/ir.json

Commit .protean/ir.json to version control. It serves as the baseline for detecting changes between releases.

Multi-domain projects

Projects with multiple bounded contexts use a subdirectory per domain:

.protean/
├── config.toml             # Shared configuration (includes [domains] table)
├── identity/
│   └── ir.json
├── catalogue/
│   └── ir.json
└── ordering/
    └── ir.json

Configure the [domains] table in .protean/config.toml:

[domains]
identity = "identity.domain"
catalogue = "catalogue.domain"
ordering = "ordering.domain"

Step 2: Configure strictness

Create .protean/config.toml to customize behavior. All settings are optional -- sensible defaults apply when the file is absent:

[compatibility]
strictness = "strict"  # "strict" | "warn" | "off"
exclude = ["myapp.internal.LegacyEvent"]

[compatibility.deprecation]
min_versions_before_removal = 3

[staleness]
enabled = true

For the full list of configuration keys, see the config reference.


Step 3: Add pre-commit hooks

Protean ships two pre-commit hooks. Add them to your project's .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: protean-check-staleness
        name: Check IR staleness
        entry: uv run protean-check-staleness --domain=myapp.domain
        language: system
        pass_filenames: false
        always_run: true
      - id: protean-check-compat
        name: Check IR compatibility
        entry: uv run protean-check-compat --domain=myapp.domain
        language: system
        pass_filenames: false
        always_run: true

Why repo: local?

Protean hooks call derive_domain() which imports your application's domain modules. A remote repo: installs hooks in an isolated virtualenv that does not have access to your source code, so the import will fail. Using repo: local with language: system runs the hook in the caller's environment. Prefix the entry with uv run (or activate your virtualenv) to ensure the hook executes inside your project's environment where your code is importable.

protean-check-staleness

Blocks the commit if .protean/ir.json is out of date.

Without --fix, a stale check prints the mismatch and suggests a manual regeneration command. With --fix, the hook regenerates the IR, stages the file with git add, and exits 0 -- allowing the commit to proceed.

# Auto-fix mode -- never blocks on stale IR
repos:
  - repo: local
    hooks:
      - id: protean-check-staleness
        name: Check IR staleness
        entry: uv run protean-check-staleness --domain=myapp.domain --fix
        language: system
        pass_filenames: false
        always_run: true

protean-check-compat

Blocks the commit if breaking IR changes are detected against the baseline in HEAD.

Multi-domain support

When your config.toml has a [domains] table, omit the --domain argument. Both hooks iterate over all configured domains automatically:

# No --domain needed -- reads [domains] from .protean/config.toml
repos:
  - repo: local
    hooks:
      - id: protean-check-staleness
        name: Check IR staleness
        entry: uv run protean-check-staleness --fix
        language: system
        pass_filenames: false
        always_run: true
      - id: protean-check-compat
        name: Check IR compatibility
        entry: uv run protean-check-compat
        language: system
        pass_filenames: false
        always_run: true

Each domain's IR is checked against its own subdirectory (.protean/<name>/ir.json). The hooks exit non-zero if any domain fails its check.


Step 4: Add CI checks

GitHub Actions

Add a compatibility check step to your CI workflow:

- name: Check IR compatibility
  run: |
    protean ir diff --domain myapp.domain --base origin/main

The command exits with code 1 on breaking changes, which fails the CI step.

pytest warning filters

Turn Protean deprecation warnings into test failures:

# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
    "error::DeprecationWarning:protean.*",
]

This catches deprecated API usage during development rather than after a breaking release.


Using the CLI

protean ir check

Compare the live domain against the materialized IR:

protean ir check --domain myapp.domain

Exit codes: 0 (fresh), 1 (stale), 2 (no IR found).

protean ir diff

Compare two IR snapshots with full breaking-change classification:

# Auto-baseline: compare live domain against .protean/ir.json
protean ir diff --domain myapp.domain

# Compare against a specific git commit
protean ir diff --domain myapp.domain --base HEAD

# Compare two explicit files
protean ir diff --left baseline.json --right current.json

Exit codes: 0 (no changes), 1 (breaking changes), 2 (non-breaking only).

When strictness = "warn", breaking changes are reported but the exit code is 0. When strictness = "off", the command exits 0 immediately.

For the full CLI reference, see protean ir.


See also