Add claims to an existing codebase¶
This guide takes a package with functions, tests and no claims to a
passing mathema verify, without rewriting anything. It assumes the
quick start, so you have written one claim and seen a
record.
The running example is the fees module of a billing service, three functions and a small test file:
"""Fees and settlements for a billing service."""
def late_fee(days: int, base: float) -> float:
"""The fee for a payment that is `days` late: one percent of base a day."""
return base * 0.01 * days
def discounted(price: float, rate: float) -> float:
"""The price after applying a discount rate."""
return price * (1 - rate)
def settle(exposure: float) -> float:
"""The settlement amount for a signed exposure."""
return abs(exposure)
from billing.fees import discounted, settle
def test_discounted():
assert discounted(100.0, 0.25) == 75.0
def test_settle():
assert settle(-3.0) == 3.0
1. See where you stand¶
mathema audit gives every function under a package one line: where it
lives, whether it has claims, whether the derive route could prove
things about it, what state outside its parameters it touches, whether a
test report covers it, and how well its docstring states its intent.
mathema audit billing --root .
|| || derive route || typing || globals || || docs ||
key | span || claims || derives | cx | reason | code || typed | finite_domain || vars | mutates | funcs || tested || quality || docsync
billing.fees
.discounted | 6:8p || {11 | 0 | -} || yes | 1 | - | - || yes | - || - | - | - || no-report || 4/5 || 26%
.late_fee | 1:3p || {7 | 0 | -} || yes | 1 | - | - || yes | - || - | - | - || no-report || 4/5 || 26%
.settle | 11:13p || {9 | 0 | -} || yes | 1 | - | - || yes | - || - | - | - || no-report || 3/4 || 26%
0/3 claimed, 3/3 derivable, 3/3 derive reads with nothing supplied, 3/3 fully typed, 11/14 docstring quality criteria met, no coverage.json/.coverage report found, mean docsync 26%.
`derives` is what the derive route can do here, given the domain the signature, docstring and claims declare. The reason/code cells describe what derive reads with nothing supplied, so a branch:needs-domain row reads blocked there and derives all the same, once a claim declares the domain that prunes the branch. Neither is a ceiling: a probe claim can still be written and checked for every function here.
0/3 claimed, and all three could be proven. The claims cell reads
{floor | actual | expected}: the least a function of this shape gives
you to state (eleven for discounted, one per relevant built-in claim and
target), what it states (nothing yet), and what a function of this shape
typically carries (-: not known without a corpus). tested says
no-report: there is no coverage report yet for the tests to count.
2. Scaffold the store¶
mathema init writes the git files a tracked store wants and, for each
function with no claims, an empty placeholder in a claims file:
mathema init billing --root .
cat claims/billing.fees.claims.yaml
mathema init: scaffolded git files:
.gitattributes
.mathema/.gitignore
mathema init: wrote stub entries to:
claims/billing.fees.claims.yaml
# mathema init: bare declared stubs for billing.fees (fill in claims; an empty list means nothing declared yet)
billing.fees.discounted:
claims: []
billing.fees.late_fee:
claims: []
billing.fees.settle:
claims: []
A stub is a task, not a claim. init is additive: run it again as the
package grows and it appends keys it has not seen, never touching a
claim you wrote.
3. Start from a suggestion¶
For one function, mathema claims --suggest renders the standard claims
mathema would check, each with the route it would take, in three
sections: claims that stand alone, questions with candidate answers
(which way f moves or bends in each parameter), and claims likely to be
unknowable (none here):
mathema claims billing.fees.discounted --suggest --root .
billing.fees.discounted: 18 suggested claim(s) (adopt with: mathema claims KEY --adopt NAME)
individual claims:
- commutative: f(price, rate) == f(rate, price) [route best]
- associative: f(f(price, rate), c) == f(price, f(rate, c)) [route best]
- is_deterministic: f(price, rate) == f(price, rate) [route best]
- is_state_safe: f(price, rate) == f(price, rate) [route best]
- is_numerically_stable: g(f, price, rate) == 1 [route best]
- is_representation_safe[price]: is_representation_safe(price) [route examine]
- is_representation_safe[rate]: is_representation_safe(rate) [route examine]
questions with candidate answers (adopt every answer that holds):
monotonicity[price]:
- monotonic_increasing[price]: d(f(price, rate), price) >= 0 [route best]
- monotonic_decreasing[price]: d(f(price, rate), price) <= 0 [route best]
shape[price]:
- affine[price]: d(f(price, rate), price, price) == 0 [route best]
- convex[price]: d(f(price, rate), price, price) >= 0 [route best]
- concave[price]: d(f(price, rate), price, price) <= 0 [route best]
monotonicity[rate]:
- monotonic_increasing[rate]: d(f(price, rate), rate) >= 0 [route best]
- monotonic_decreasing[rate]: d(f(price, rate), rate) <= 0 [route best]
A suggestion is not verified and never gates until someone adopts it. A higher rate means a lower price, so adopt the one that says so:
mathema claims billing.fees.discounted --adopt "monotonic_decreasing[rate]" --root .
cat claims/adopted.claims.yaml
adopted monotonic_decreasing[rate] into ./claims/adopted.claims.yaml: d(f(price, rate), rate) <= 0
billing.fees.discounted:
claims:
- name: monotonic_decreasing[rate]
statement: d(f(price, rate), rate) <= 0
route: best
grammar: mathema
4. Try it before the sweep writes anything¶
mathema check adjudicates a function's claims and writes no record, so
it is the place to find out whether a claim is right before verify
keeps it:
mathema check billing.fees.discounted --root .
FAIL billing.fees.discounted: source, no side effects; claims 1/1 checked (0 proven, 0 holds, 1 falsified) <- 1 falsified claim(s)
Falsified. The witness says why:
import mathema
from billing.fees import discounted
(p,) = mathema.claims.check_conjectures(discounted, [
mathema.claim("d(f(price, rate), rate) <= 0", name="monotonic_decreasing[rate]")])
print(p.verdict, p.counterexample)
falsified rate = -2.7255878198803085 -> -32.66347642639832, rate = 4.922577492583454 -> 34.39055087522713 at price = -8.767334983247743 (not decreasing)
The derivative of price * (1 - rate) in rate is -price, which is
positive when the price is negative, and the claim said nothing about
prices. Both values were computed at the same price, the one the witness
prints after at. (d(f(price, rate), rate) is
the derivative in rate; the claim grammar lists
the calculus forms.) The suggestion was right about the function and silent about its
domain, which is the usual state of a suggestion. Say what the function
is for, a price that is never negative and a rate between none and all
of it:
billing.fees.discounted:
claims:
- name: monotonic_decreasing[rate]
statement: "for price in [0, 1e6], rate in [0, 1], d(f(price, rate), rate) <= 0"
route: best
grammar: mathema
5. Put a claim beside the code¶
A claim can also live in the function's docstring, where the next reader
of the code sees it. settle returns a magnitude:
def late_fee(days: int, base: float) -> float:
"""The fee for a payment that is `days` late: one percent of base a day."""
return base * 0.01 * days
def discounted(price: float, rate: float) -> float:
"""The price after applying a discount rate."""
return price * (1 - rate)
def settle(exposure: float) -> float:
"""The settlement amount for a signed exposure.
Claims:
nonneg: f(exposure) >= 0
"""
return abs(exposure)
The two places are equal: a docstring claim and a claims-file claim are adjudicated the same way, and Authoring claims says which wins when both name the same claim.
6. The first sweep¶
mathema verify --root .
mathema audit billing --root . --filter unclaimed --cols key,claims
ok billing.fees.discounted: no baseline record; 2 proven (1 claim, 1 built-in), 0 holds, 0 falsified
ok billing.fees.late_fee: no baseline record; 1 proven, 0 holds, 0 falsified
ok billing.fees.settle: no baseline record; 2 proven (1 claim, 1 built-in), 2 holds, 0 falsified
0 unchanged since the last run (not run again), 3 checked, 0 problem(s)
grammars detected: mathema; verified by this run: mathema
{"prefix":"billing.fees.","cols":["key","claims"],"rows":[["late_fee",0]]}
Both claims proved on the derive route, and every record also carries
dependencies_current, the claim verify adds that what the function
depends on has not moved; that is the second proven on the two claimed
functions and the only one on late_fee. The 2 holds on settle are
the proof's computation line, the same claim run through the real code
in floating point, and its policy line for a nan input. --filter unclaimed is the list of what is left, as
data, cut to two columns with --cols: one function. The records under
.mathema/verified/ are the evidence, and they are meant to be committed
with the code.
7. Let the tests you have count¶
The tests already exercise discounted and settle. Run them under
coverage once, and mathema coverage counts their lines beside the lines
mathema's own probes and proofs reached:
python -m coverage run -m pytest -q test_fees.py
python -m coverage json -q
mathema coverage billing --root .
2 passed in 0.05s
100% billing.fees.discounted [test+probe+derive]
100% billing.fees.late_fee [probe]
100% billing.fees.settle [test+probe+derive]
implementation coverage: 100%
late_fee has no claim of its own, so it reads probe alone:
coverage runs mathema's standard claims about a function while tracing
it, and the lines they ran count, but a proof counts the body as
modelled only for a claim included for the function and recorded by the
sweep, as discounted's and settle's are.
mathema coverage explains the three sources and
what happens to a test report when the code moves on. From here, Gate a
pipeline with mathema verify puts the sweep in CI,
and From a pytest test to a claim turns the
tests you already have into claims.