invariant

DECLARE · RUN · PROVE

A backup that passed. A repo that's secure. A number that adds up.
How do you know?

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.

VERIFIED · NOT ASSUMED · VERIFIED · NOT ASSUMED · PASS no_negative_payments

'SELECT COUNT(*)...' = 0

You declare it once.
invariant runs it every time.

the same config a person can read is the thing CI actually executes

invariant.yaml
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: .}
$ invariant run invariant.yaml --evidence proof/
  [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/

Three statuses, not two

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.

PASS

The check ran, and what it found matches what you declared. This is the only status a green build should mean.

FAIL

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.

UNVERIFIED

The check could not run at all. Reported as its own thing, loudly, because a claim nobody could test is not evidence of anything.

Three check types today.
A fourth is one file.

a check is args → (status, detail, evidence). nothing else changes.

STDLIB ONLY

sql

One query, one expected scalar. Built straight into sqlite3 — not every invariant needs a whole tool behind it.

wraps: nothing

SHELLS OUT

postgres_restore

Restores a Postgres dump into a disposable, version-matched container and reads back whether it actually worked.

wraps: firedrill

SHELLS OUT

security_scan

Runs a repo through multiple security engines and reports only what's new since the last accepted baseline.

wraps: carabiner

Why another one

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.

Install

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/