Tutorial: the CDD loop¶
The quick start showed one claim going from falsified to proven. This walks the whole loop on a small project: a real bug, a claim that was simply wrong, the human decisions in between, and what the record remembers about all of it. Every output below is from a real run you can reproduce.
The project¶
Two functions, in funcs.py. settle is meant to return a settlement
amount for a signed exposure, and it has a bug: it should take the
magnitude and instead returns its argument unchanged.
def settle(x: float) -> float:
"""Settlement amount for a signed exposure x."""
return x
def midpoint(a: float, b: float) -> float:
"""The midpoint of two values."""
return (a + b) / 2.0
And claims/demo.claims.yaml, stating what someone believed:
mkdir claims
funcs.settle:
claims:
- name: nonneg
statement: "for x in [-5, 5], f(x) >= 0"
route: probe
- name: symmetric_in_sign
statement: "for x in [-5, 5], f(x) == f(-x)"
route: probe
- name: negative_exposure_negative
statement: "for x in [-5, -1], f(x) <= 0"
route: probe
funcs.midpoint:
claims:
- name: commutative
statement: "f(a, b) == f(b, a)"
route: derive
Two of those claims are falsified by the bug. The third is falsified by being wrong, and keeping those two diagnoses apart is the thing this loop is for.
1. The first sweep¶
mathema verify --root .
note funcs.settle: nonneg, symmetric_in_sign initially falsified. A declared claim is kept until a human decides it:
(i) fix the code
(ii) to record it as a discovery, run: mathema accept funcs.settle <claim> --as discovery
(iii) to replace it with a corrected claim, edit it, then run: mathema accept funcs.settle <claim> --as superseded
(iv) to try a spelling first (it writes nothing), run: mathema check funcs.settle --claim "..."
ok funcs.midpoint: no baseline record; 2 proven (1 claim, 1 built-in), 3 holds, 0 falsified
FAIL funcs.settle: no baseline record; 1 proven, 2 holds, 2 falsified <- 2 falsified claim(s)
0 unchanged since the last run (not run again), 2 checked, 1 problem(s)
grammars detected: mathema; verified by this run: mathema
2 intents to accept (2 functions): mathema accept --intent
midpoint proves on the derive route. Each row also counts one
proven claim nobody wrote, dependencies_current, which verify
adds itself, and midpoint's holds is its proof's [float]
companion, the proven claim run through the real code in floating point. settle fails, and the record under
.mathema/verified/funcs.settle.yaml says precisely how (trimmed to
the three declared claims):
- name: "negative_exposure_negative"
statement: "for x in [-5.0, -1.0] : float|missing, f(x) <= 0"
verdict: "holds"
n: 34
route: "probe"
# ...
- name: "nonneg"
statement: "for x in [-5.0, 5.0] : float|missing, f(x) >= 0"
verdict: "falsified"
n: 1
counterexample: "(-5): -5.0 vs 0"
route: "probe"
# ...
mathema.counterexample_args:
- -5.0
# ...
- name: "symmetric_in_sign"
statement: "for x in [-5.0, 5.0] : float|missing, f(x) = f(-x)"
verdict: "falsified"
n: 1
counterexample: "(-5): -5.0 vs 5.0"
route: "probe"
# ...
mathema.counterexample_args:
- -5.0
Look at the third one. negative_exposure_negative holds, and it
holds for a bad reason: return x really is negative for negative x.
A wrong belief and a buggy implementation agreed with each other, which
is exactly how a bug survives review. The counterexamples are stored
structurally, not just as display text, which matters in a moment.
2. Fix the code¶
def settle(x: float) -> float:
"""Settlement amount for a signed exposure x."""
return abs(x)
def midpoint(a: float, b: float) -> float:
"""The midpoint of two values."""
return (a + b) / 2.0
mathema verify --root .
FAIL funcs.settle: form changed; 1 proven, 3 holds, 0 falsified, 1 invalidated <- 1 invalidated claim(s)
1 unchanged since the last run (not run again), 1 checked, 1 problem(s)
grammars detected: mathema; verified by this run: mathema
2 intents to accept (2 functions): mathema accept --intent
midpoint is the one unchanged function: its form hash has not moved,
so there is nothing to re-adjudicate, and only --verbose lists it (as
fresh). settle changed, so it is swept again,
and the record now keeps the arc (trimmed):
- name: "negative_exposure_negative"
statement: "for x in [-5.0, -1.0] : float|missing, f(x) <= 0"
verdict: "invalidated"
n: 1
counterexample: "(-5): 5.0 vs 0"
# ...
mathema.previous_verdict: "holds"
mathema.regressed_to: "falsified"
- name: "nonneg"
statement: "for x in [-5.0, 5.0] : float|missing, f(x) >= 0"
verdict: "holds"
n: 130
# ...
mathema.previous_verdict: "falsified"
- name: "symmetric_in_sign"
statement: "for x in [-5.0, 5.0] : float|missing, f(x) = f(-x)"
verdict: "holds"
n: 130
# ...
mathema.previous_verdict: "falsified"
Two things happened. The two real failures now hold, and they had to clear the exact points that falsified them before: a recorded counterexample replays on every later run, so a bug cannot be fixed by accident and un-fixed quietly.
And negative_exposure_negative went from holds to invalidated.
That is its own verdict, not an ordinary failure: a claim that used to
be supported and no longer is. Fixing the code exposed a belief that
was never true. mathema will not launder that into a plain
falsified, because "this used to pass" is information a reviewer
needs.
mathema.previous_verdict is one step: the verdict immediately before
this one, not a history. The record has no history field because the
record is in git. mathema review reports which verdicts flipped
between two refs, and git log .mathema/verified lists every commit
that changed a record.
3. Accept the evidence¶
nonneg holds empirically, over 130 seeded trials. Whether that is
enough is a human decision, so there is a verb for making it:
echo y | mathema accept funcs.settle nonneg --as evidence --by "Ada Lovelace"
accept funcs.settle :: nonneg as evidence, by Ada Lovelace
holds over 129 trials
the claim is accepted as evidence at n=129, bound to form py:3eb01e1d9919
write this acceptance? [y/N] written.
accept prints exactly what it will write and waits for a yes, here
piped in with echo y, which is why the answer does not show after the
prompt. The
acceptance binds to the form hash, so changing settle later drops the
acceptance and asks again: you accepted evidence about that code.
4. There is no accepting a bug¶
The invalidated claim is still sitting there. The obvious move is to wave it through as a known risk, and mathema refuses:
mathema accept funcs.settle negative_exposure_negative --as risk
cannot accept: negative_exposure_negative is invalidated: a falsification is never accepted as risk, diagnose it (--as discovery), or fix the code until it stops falsifying
This is the rule that makes the rest of it worth trusting. A falsification is either a bug you fix or a belief you correct, and neither of those is "acknowledged and ignored".
5. The discovery fork¶
Here the belief was wrong: settlements are magnitudes, so a negative exposure settles positive. That is a real discovery about the domain, and accepting it as one corrects the claim rather than deleting it:
echo y | mathema accept funcs.settle negative_exposure_negative --as discovery --by "Ada Lovelace"
accept funcs.settle :: negative_exposure_negative as a discovery, by Ada Lovelace
invalidated at x = -5: 5.0 vs 0
the claim moves to discoveries with that witness, out of claims/demo.claims.yaml
the corrected claim takes its place: negative_exposure_negative_corrected [probe]: for x in [-5.0, -1.0] : float|missing, f(x) > 0, checked now: holds over 127 trials
write this acceptance? [y/N] written.
The old claim is not erased. It moves to the record's discoveries
section, the counterexample that killed it is kept as the witness, and
claims/demo.claims.yaml is rewritten to declare the corrected claim
in its place.
The corrected claim is never guessed at: mathema offers its mechanical
inverse only when the shape is soundly invertible AND the candidate has
just been adjudicated against the live function and holds; anything
else (a chained comparison, a premise, an inverse that fails) records
the discovery with no replacement, and you state what is true yourself
with --corrected "<law>", which is adjudicated the same way before it
is written. A year later the record still says someone believed the
opposite, and why they stopped.
With the bug fixed, the evidence accepted and the wrong belief corrected, the sweep passes:
mathema verify --root .
2 unchanged since the last run (not run again), 0 checked, 0 problem(s)
grammars detected: mathema; verified by this run: mathema
2 intents to accept (2 functions): mathema accept --intent
funcs.settle is not checked again: each acceptance wrote its record
along with the claims file, so the sweep finds nothing changed.
6. Owning what nothing can settle¶
Some claims neither route can decide, and they stay unknown rather
than being guessed at. Accepting one as risk reclassifies it to
skipped:unknown_but_accepted:
mathema accept funcs.settle some_unknown_claim --as risk --note "monitored"
Lenient verification proceeds past it, named in the row as accepted risk. Strict mode still refuses it. Accepted risk stays visible either way, which is the difference between owning a gap and hiding one.
7. Where the loop ends¶
The objective is to drive unknowns and skips down until every claim is proven, holds with accepted evidence, or is an accepted discovery, with nothing silently dropped on the way. Two commands watch that:
mathema verify --root . # strict by default in CI
mathema verify --root . --lenient # accepted risks proceed
mathema audit mypkg # the population view: what is claimed at all
verify exits 1 when a claim is falsified or unknown or, in strict
mode, skipped or accepted as risk, and 2 when it could not run at all. audit answers the
question verify cannot: not "do the stated claims pass" but "which
functions have nobody stated anything about".
Where to go deeper¶
- Writing claims: the four authoring surfaces and their precedence.
- The claim grammar: every spelling, with a runnable example of each.
- mathema accept: every acceptance, including
--as supersededand--as historical. - mathema review: read the verified store by claim, the record shape, and the surface-versus-author provenance split.
- The derive route: why
midpointproved andsettlecould only be probed.