mathema check¶
One-off interactive verification of a single function, module, or
package against its built-in laws and any inline --claims, the way
you'd use mathema from a notebook. Lenient by default.
mathema check pkg.mod:fn [--claim "f(-x) == -f(x)"]
mathema check path/to/file.py:fn [--claim "..."]
Arguments¶
| Flag | Meaning |
|---|---|
target |
a dotted name (pkg.mod, pkg.mod.fn, pkg.mod:fn) or a file path (file.py, file.py:fn); files import with real package context, so relative imports inside them work, and report keys are always the canonical dotted module.qualname |
--root |
project root to import dotted targets relative to (default .) |
--claim LAW |
ad-hoc claim to adjudicate, e.g. "f(-x) == -f(x)" (repeatable) |
--domain name=lo:hi |
declared parameter range (repeatable) |
--strict / --lenient |
one strictness pair shared with verify; lenient is the default here (the authoring loop iterates while claims are still being written); strict additionally counts skipped (unverifiable) claims and accepted risk as failures, a reporting filter over already-computed verdicts, never an adjudication mode |
--trials-downscale FACTOR |
shrink the trial budget by FACTOR (FACTOR > 0) for a faster dev loop; a value above 1 is clamped to 1, so it never scales upward |
--format |
text (default), json, compact, junit, github, md |
--output FILE |
write the report to a file instead of stdout |
Report formats¶
text: human-readable, for a terminal.json: for artifacts; includestool/version/CDD_spec_version/functions/totalsfields.compact: the same adjudication asjson, minified onto one line with no envelope: a bare array of one object per function (key,passed,problems,claims). Each claim row carriesstance,verdict,route,source,gates,reason, and itsevidence.n, plus acounterexamplewhen there is one. Built for a program reading the result rather than a human or a CI widget, and the closest CLI equivalent of what the MCP tools return, so an agent that shells out and one that calls a tool see the same shape. Notesourceandgates: a suggested claim appears in the rows but hasgates: falseand never affects the exit code, so a client filtering ongatessees exactly what the gate saw.junit: for a GitLab test report widget.github: GitHub Actions annotation format.md: for a CI step summary.
The trial budget¶
The probe route's n isn't a flat constant. It's decided once per
call, before any law runs, from the target function's own structure:
- Higher, from a base of 128 up to a max of 256, for a
structurally riskier function, more branches, more loops, a wider
or unbounded domain (the claim's own binding, which replaces a
domain=passed tocheckfor the parameter it binds, or thatdomain=itself), or a language domain, which adds 32 for every 32 hazards its language brings (up to three steps), whether the language is written in the claim or inferred from an annotation. A language whose hazards still outnumber the budget raisesnto the number of hazards, so each is tried once, and the record's sampling line says it did. - Lower, down to 32, only when the derive route can prove the function is affine (a constant slope in every parameter) and the domain doesn't itself need the extra density, a wide or float-precision-risky domain, or a language, skips the reduction even for a provably affine function.
Every verdict reports the exact n it used, plus a
meta["mathema.confidence"] score (1-4 stars, capped below the derive
route's own 5; sampling evidence is never proof, however extensive)
built from the same structural factors, so a reader can see why a
verdict deserves more or less trust without re-deriving it. From the
library, mathema.check(fn, trials=N) takes an exact count, with no
adaptivity at all.
--trials-downscale FACTOR shrinks the whole budget by a flat factor
instead; 0.25 for a much faster dev loop, say, applied to
everything, an explicit trials=N included, not just the adaptive
default. It only ever shrinks (a value above 1 is accepted but has no
effect; there's no good reason to scale upward when a structurally
riskier function already gets more trials on its own), and it never
drops the budget below a floor that still means something: at least
16 trials, and at least enough to guarantee every special sampled
value (0, ±1, ±1e-9, ±1e6, ...) is actually exercised once.
--trials-downscale 0 or a negative value is a clean CLI error, not a
silent 0-trial holds.
The budget above is the probe route's. The computation line under a
proven claim runs on points of its own: every corner of the domain and
points sampled inside it, with the count in its note
(ran at 43 points: every corner and 40 interior points).
--trials-downscale leaves it as it is. A finite domain small enough
to run in about a second, an integer range or a small set, is swept
point by point. A claim can carry its own budget, the trials: field
of a claims-file entry or mathema.claim(..., trials=N); it sets the
count for that claim, and a finite domain whose points fit within it is
swept in full.
Exit code¶
1 if any row had a problem; 0 otherwise, suitable for a pre-commit
check on a single target. A falsified or unknown claim is a problem in
every mode; --strict additionally counts unverifiable (skipped)
claims and a silently-unenforced declared domain. Only claims you
actually stated gate the exit code: standard claims mathema itself
volunteers (see mathema claims --suggest) are shown for
adoption but never fail a run pre-adoption.
mathema check model.py --domain alpha=0:1 --claim 'excluded_outside_domain(alpha)'
mathema check model.py --format json --output claim-coverage.json
mathema check model.py --format junit --output claims.xml # GitLab test widget
mathema check model.py --format github # Actions annotations
Worked pipeline configs for GitHub Actions and GitLab are in
examples/ci/ in the repo.
Worked example: softmax, start to finish¶
softmax takes a list of float scores and refuses an empty list with
its own ValueError before max() can fail on it. mathema checks the
empty input on its own line under each claim: a deliberate error at
[] passes, while a crash the body merely stumbles into (max() of an
empty list) falsifies the claim.
# functions.py
import math
from typing import Annotated
from mathema.types import Shape
def softmax(scores: Annotated[list[float], Shape("n")]) -> Annotated[list, Shape("n")]:
"""Turn a vector of real-valued scores into a probability distribution.
Claims:
sums_to_one: sum(f(scores)) == 1
"""
if not scores:
raise ValueError("softmax needs at least one score")
m = max(scores)
exps = [math.exp(s - m) for s in scores]
total = sum(exps)
return [e / total for e in exps]
$ mathema check functions.py:softmax --claim "sum(f(scores)) == 1"
ok functions.softmax: source, no side effects; claims 6/6 checked (0 proven, 6 holds, 0 falsified)
Break it on purpose (drop the normalization, return exps instead of
dividing by the total):
# functions.py
import math
from typing import Annotated
from mathema.types import Shape
def softmax(scores: Annotated[list[float], Shape("n")]) -> Annotated[list, Shape("n")]:
"""Turn a vector of real-valued scores into a probability distribution.
Claims:
sums_to_one: sum(f(scores)) == 1
"""
if not scores:
raise ValueError("softmax needs at least one score")
m = max(scores)
exps = [math.exp(s - m) for s in scores]
total = sum(exps)
return exps
and sums_to_one correctly falsifies, the row fails and the exit code
is 1 (see CDD in one page: the
counterexample is kept as knowledge, and the run fails):
$ mathema check functions.py:softmax --claim "sum(f(scores)) == 1"; echo $?
FAIL functions.softmax: source, no side effects; claims 6/6 checked (0 proven, 4 holds, 2 falsified) <- 2 falsified claim(s)
1
See mathema verify for the same regression caught from the CI-sweep angle instead.