Cross-team contracts
When one team’s models read another team’s tables, a dropped or narrowed column breaks the reader. That break usually shows up at run time, in production, far from the change that caused it.
Cross-team contracts move the failure to compile time on the consumer’s side.
rocky compile fails when an upstream project changes a column the downstream
project actually reads.
The two projects stay independent. There is no shared repository, no shared state, and no coupling at run time. There is one vendored file and a diff. Vendored means the consumer keeps its own copy of the producer’s file, committed into the consumer’s repository.
How it works
Section titled “How it works” producer project consumer project ──────────────── ────────────────
rocky publish-ir │ writes the typed ProjectIr ▼ project-ir.json ── you vendor it ──► current.json (the snapshot) baseline.json (reviewed, accepted) │ │ rocky compile diffs │ baseline against snapshot ▼ E030 E031 E032 compile fails W031 warning W030 info │ │ it also checks the pin │ and the snapshot format ▼ E033 E034 compile fails-
The producer publishes a snapshot.
rocky publish-ircompiles the producer project. It writes the typedProjectIr— every model’s resolved columns and types — to a JSON file.Terminal window rocky publish-ir --with-seed --out project-ir.jsonPass
--with-seedfor a self-contained DuckDB producer, so leaf models resolve to concrete column types. A snapshot with no resolved types gives the contract nothing to check, sopublish-irrefuses to write a snapshot whose models are all empty. A snapshot that looks enforced but checks nothing is worse than none. -
The consumer vendors it and declares an
[imports.<name>]block:[imports.orders]path = "vendor/orders" # directory holding the vendored snapshotssnapshot = "current.json" # the producer's current published snapshotbaseline = "baseline.json" # the reviewed-and-accepted "before" imagepin = "*" # optional recipe-hash pin ("*" = trust any) -
The consumer’s
rocky compilechecks the contract. It links each consumer model to the producer table it reads, through the model’s[[sources]]entry. It diffsbaselineagainstsnapshot. It emits a diagnostic for any change that touches a column the consumer reads.
The diagnostics
Section titled “The diagnostics”| Code | Producer change | Severity | Fires when the consumer… |
|---|---|---|---|
E030 |
column dropped | error | references the column |
E031 |
column type narrowed | error | references the column |
E032 |
column went nullable → NOT NULL | error | references the column |
W031 |
column type widened | warning | references the column |
W030 |
column added | info | reads the producer via SELECT * |
E033 |
snapshot drifted from a concrete pin |
error | always (whole-project tripwire) |
E034 |
snapshot is a newer format than this build | error | always (fail closed) |
A consumer that selects columns by name is unaffected by changes to columns it does not read.
A consumer that uses SELECT * cannot list its columns. Rocky falls back to
flagging every relevant change. It over-reports rather than let a breaking change
through.
pin and baseline answer different questions
Section titled “pin and baseline answer different questions”They complement each other. Neither replaces the other.
baselineis the column-level before image. It is the only input that lets the diff emit the column codes:E030–E032,W030, andW031, for the columns the consumer reads.pinis a whole-project drift tripwire. Set it to a concrete recipe hash. Thenrocky compilefails withE033if the vendored snapshot differs at all, even for a change that touches no column you read. Leave it at"*", the recommended default, to fail only on changes to your reads.
Accepting a producer change
Section titled “Accepting a producer change”Nothing advances the baseline on its own. That is deliberate. The baseline is
the schema you have reviewed and accepted. When the producer ships a change
and you decide to take it, advance the baseline:
rocky imports update # advance every import's baseline to its snapshotrocky imports update --check # CI guard: fail if any baseline is behind or pin is stale--check writes nothing. It exits non-zero when an import is out of date. Put it
in CI to keep vendored contracts in sync.
Distributing the snapshot
Section titled “Distributing the snapshot”Rocky reads a file at the configured path. How the file gets there is up to you.
A git submodule is the simplest durable transport. Add the producer’s repository,
or a small artifact repository it publishes to, as a submodule under path. The
producer commits its publish-ir output there. The consumer then runs:
git submodule update --remote # pull the producer's latest snapshotrocky imports update # advance the baseline once you've reviewed itEvery snapshot carries a snapshot_version header. A consumer on an older build
fails closed with E034 against a newer format, rather than misreading it.
See the governance guide for how contracts sit alongside Rocky’s other trust controls.