Skip to main content

control-assurance

Prove that a safety control can actually fail.

A guard that cannot go red is not a guard. This tool takes the controls you have declared, breaks each one on purpose in a throwaway copy of your project, and requires the test you named to fail. If the test still passes, the control was decoration — and you find that out from a command instead of from an incident.

max_position_cap          PROVED       1/1 mutations detected
admission_floor           NOT_PROVED   0/1 mutations detected

That second line is the product.

What this measures that other tools do not

Coverage tells you a line ran. Mutation testing (mutmut, cosmic-ray) tells you your suite notices changes to code the tool picked. Neither answers the question an auditor actually asks:

You wrote down that this system refuses orders above the cap. What test fails if that stops being true, and have you ever seen it fail?

control-assurance binds a declared claim to a named falsifier and refuses the claim when nothing carries it. The unit is not a line or a function — it is the sentence in your control register.

And it enforces one distinction that is the whole point: a scan proves a thing exists; it never proves that it works. A registry entry matching a string, a function name or an invocation line establishes presence, nothing more. Behaviour is established only by breaking the thing and watching a named test notice.

Install

pip install git+https://github.com/mrbestnaija/control-assurance

Python 3.10+. Requires pyyaml and pytest. There is no PyPI release yet, so pip install control-assurance will not resolve — use the line above until this note is gone.

Verified on a first run from a clean clone: Ubuntu 22.04 / Python 3.10.12, and Windows / Python 3.12.

If control-assurance is not found after installing, pip put the console script in a directory that is not on your PATH. This is the normal outcome of a --user install, which pip chooses automatically when site-packages is not writable — and it is easy to mistake for a failed install, because the package is in fact installed:

python -m control_assurance.cli prove --help   # always works, no PATH needed
~/.local/bin/control-assurance prove --help    # the script pip actually wrote
export PATH="$HOME/.local/bin:$PATH"           # or put it on PATH

The python -m form is the one to use in CI and in scripts, because it does not depend on PATH at all.

Hard limit, stated before you install rather than after: your falsifiers must be runnable by pytest. run_test shells python -m pytest <falsifier>. A project whose tests run under unittest alone, go test, jest or a shell script is out of scope until a runner adapter exists. There is no adapter and no date.

Start here if you have no control register

Most teams don't have one, and prove needs one. So don't write it — generate it:

control-assurance discover --root . --out controls.yml

One command, no configuration, no prerequisite. It reads your Python, proposes the functions that look like enforcement points, and tells you three things immediately:

  19 propose a BEHAVIOUR claim (a test names them and they raise on refusal)
 106 have NO test naming them at all
 152 have ZERO call sites outside their own file -- present, possibly never reached

  0 of 198 are PROVED.

That last line is the number this tool exists to move, and you get it on the first run against a codebase that had no register a minute earlier.

These are candidates, not findings. Detection is lexical — a function named reject_*, or one whose docstring's first line speaks of refusal and which actually raises. A guard named _h with no docstring is invisible to it; a helper called validate_email_format is proposed though it enforces nothing interesting. Both errors are cheap and neither is hidden. Delete what does not matter; the ones left are your register.

discover writes the mutations too, so the next command already works:

control-assurance prove --registry controls.yml --root . --all
enforce_max_position   PROVED       1/1 mutations detected
enforce_min_lot        NOT_PROVED   0/1 mutations detected
    guard_disabled_enforce_min_lot: UNDETECTED -- the falsifier passed while the control was broken

Two commands, no configuration, no register beforehand, nothing hand-edited — and the second line is a real finding: that control's test passes whether or not the control works.

It proposes a mutation by finding the if ...: guard that leads to a raise and turning it off. It only does so when the guard is a single line and appears exactly once in the file, because prove refuses an ambiguous anchor and a multi-line condition cannot be replaced without producing a syntax error. Where no such guard is found it writes a TODO stub instead and says so — a mutation you must write yourself, rather than a plausible-looking one that proves nothing.

Try it on the shipped example

The distribution ships a two-control example — one honest falsifier, one lazy one — so you see a PROVED and a NOT_PROVED together:

control-assurance prove --registry examples/demo/controls.yml --root examples/demo --all

A tool that only ever prints success teaches nothing about what it measures.

Use it on your own project

# controls.yml
controls:
  - name: max_position_cap
    asserts: behaviour
    falsified_by: tests/test_risk_limits.py
    mutations:
      - id: cap_removed
        file: src/risk_limits.py
        anchor: '    if size > cap:'
        replacement: '    if False:'
control-assurance prove --registry controls.yml --root . --all --json proof.json

Exit 0 when every control proved, 1 otherwise. --json writes a report carrying a sha256 of its own content, suitable for attaching to an audit trail.

Verdicts

Verdict Meaning
PROVED every declared mutation applied and the named falsifier failed on each
NOT_PROVED a mutation left the falsifier passing — the control did not notice being broken
BASELINE_FAILS the falsifier was already failing; it can prove nothing
NO_FALSIFIER the control names no test
NO_MUTATIONS the control names no way to break it

HARNESS_FAILURE appears per mutation when a mutation could not be honestly applied — the anchor matched zero or several times, or the mutated file does not parse. It is never counted as a detection and never silently skipped.

Three refusals, each one a real defect it was built from

A mutation that did not apply proves nothing. An anchor must occur exactly once. A stale anchor is the most common way a mutation suite rots, and treating it as a skip converts rot into a green light.

A mutation must compile. On 2026-09-21 a YAML plain scalar folded a three-line replacement into one line of invalid Python. The falsifier failed — on a SyntaxError, not on the planted defect — and the tool reported PROVED. That false proof reached the project's operator before anyone read what the YAML parsed to. The guard now compiles every mutated .py, because a mutation must be valid code that behaves wrongly; code that does not parse tests the interpreter, not your control. Two tests pin it, including one that a legitimate multi-line mutation is still applied normally.

Your source tree is never written to. Mutations land in a temp copy, and a test asserts the bytes of the mutated file are identical before and after a full run. That test exists because an in-place harness once rewrote every line ending and then hashed the text rather than the bytes, reporting the restore as clean.

Honest limits

  • Pytest only. See above.
  • You write the mutations. Deliberately: a generated mutation tests your code, a hand-written one tests the claim you made. Your coverage is exactly as good as your willingness to write down how each control could break, and that is the real work. The tool is the cheap part.
  • A proof decays. It is a fact about one tree at one moment. Run it in CI or it means nothing next month.
  • Minutes, not seconds. Each mutation copies the tree and runs pytest. Measured 1m35s for four controls on the originating project. CI-scale, not pre-commit-scale.
  • An existence claim is legitimate. Many controls honestly assert only that something is present, and mutating those is theatre. The tool restricts its demand to entries declaring asserts: behaviour.

The authors' own coverage

Published because a tool whose authors will not show their own number should not be trusted with yours. On the originating project: 4 of 22 active behaviour claims are mutation-proved. The other 18 each need a falsifying test written before they can be proved at all.

Licence

Apache License 2.0. See LICENSE. Copyright 2026 Bestman Ezekwu Enock.

You may use, modify and redistribute this, including commercially, provided you keep the notice and state your changes. The licence also grants a patent licence from contributors and asks that you not use the author's name or marks to endorse your derivative. If you find a control this proves is not actually falsifiable, a patch or an issue is worth more to the project than a private workaround.

Release files for control-assurance 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for control-assurance 0.1.0
File Size Uploaded
control_assurance-0.1.0.tar.gz 29.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for control-assurance 0.1.0
File Interpreter ABI Platform
control_assurance-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.5 kB

Release files / control_assurance-0.1.0.tar.gz

Download URL control_assurance-0.1.0.tar.gz
Size 29.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e63dca872e7d2fcd9a964d09eb136315ac2e485d09765f51e8566c6894243fda
BLAKE2b-256 checksum
How to use checksums
11b67910ef85c6757727ee10a8873d0ae40a553ac40f9a81e60d48414e3c1374
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release files / control_assurance-0.1.0-py3-none-any.whl

Download URL control_assurance-0.1.0-py3-none-any.whl
Size 25.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
58ff4f37fa6816453126c6a03c12ad819f2fcad14b44e2fd5cd83b3d89012aa4
BLAKE2b-256 checksum
How to use checksums
2d75fb3bf5245553ef7e10d1f22efc70f62a4b7e15b2b9333093e02694ce29f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page