DECLARE · RUN · PROVE
invariant runs the checks that would actually catch it if any of those were false — and writes a proof bundle next to the report, so the answer isn't just an exit code someone has to trust.
'SELECT COUNT(*)...' = 0
the same config a person can read is the thing CI actually executes
invariants:
- name: no_negative_payments
check: sql
args: {dsn: prod.db,
query: "SELECT COUNT(*)
FROM payments
WHERE amount < 0",
must_equal: 0}
- name: latest_backup_restores
check: postgres_restore
args: {dump: backups/latest.dump}
- name: repo_controls_fire
check: security_scan
args: {repo: .}
[PASS] no_negative_payments 0.00s 'SELECT COUNT(*)...' = 0 [FAIL] latest_backup_restores 4.31s firedrill restored the archive and found problems [????] repo_controls_fire 0.00s carabiner is not installed (pip install carabiner-sec) 1 passed, 1 failed, 1 unverified evidence written to proof/
A check that could not run — the tool isn't installed, Docker is down, a credential is missing — is not the same fact as a check that ran and found a problem. Folding both into "fail" throws away the difference; folding the first into "pass" is worse, because now the report is lying. unverified is its own status everywhere, and it fails the build: an invariant nobody could check is not one you get to call satisfied.
The check ran, and what it found matches what you declared. This is the only status a green build should mean.
The check ran, in full, and found the invariant broken. You get the exact reason — a query result, a diff, a finding — not just a red X.
The check could not run at all. Reported as its own thing, loudly, because a claim nobody could test is not evidence of anything.
a check is args → (status, detail, evidence). nothing else changes.
STDLIB ONLY
One query, one expected scalar. Built straight into sqlite3 — not every invariant needs a whole tool behind it.
wraps: nothing
SHELLS OUT
Restores a Postgres dump into a disposable, version-matched container and reads back whether it actually worked.
wraps: firedrill
SHELLS OUT
Runs a repo through multiple security engines and reports only what's new since the last accepted baseline.
wraps: carabiner
Because the interesting part was never the runner — it's what backs each check. invariant doesn't reimplement backup verification or security scanning; it wraps tools that already do those honestly and reads their own reports as evidence, rather than trusting an exit code to summarize what happened.
Four repos that each individually prove one claim — this backup works, this repo is secure, these numbers reconcile — become one config file that proves all of it, with one report and one evidence directory next to it. A fifth check type is one new file under checks/, not a rewrite of the runner.
pip install invariant-verify # + firedrill / carabiner-sec
# for the checks that need them
python examples/make_demo_db.py
invariant run examples/invariant.yaml --evidence proof/