A good signal should not erase a failed requirement
Shipped
I changed the training-plan verdict in local-fitness so completing the distance no longer proves that a prescribed tempo session happened. The v0.63.0 tag includes a volume verdict followed by a pace cap: the second check can lower the result but cannot raise it. It also distinguishes unavailable pace evidence from running credit that was never verified.
That combination is useful well outside fitness. A deployment can finish copying files while failing its health check. A migration can process every row while producing invalid records. One successful dimension should not cancel a failed requirement.
Start with an order you can explain
Use Python 3.11 or newer. No packages or access to fitness data are needed. The example below is a small deployment classifier, not the production workout implementation.
Save this as verdict.py. Python’s IntEnum gives the states readable names while preserving numeric comparison. Higher means closer to release readiness; these values are ranks, not percentages.
from enum import IntEnum
class Verdict(IntEnum):
BLOCKED = 0
PARTIAL = 1
READY = 2
def cap(base: Verdict, evidence: Verdict | None) -> Verdict:
if evidence is None:
return base
return min(base, evidence)
def classify(copied: Verdict, healthy: Verdict | None,
*, copy_verified: bool = True) -> Verdict:
if not copy_verified:
return Verdict.BLOCKED
return cap(copied, healthy)
The important line uses min, which returns the least item under the supplied ordering. If copying is partial and health looks ready, the result remains partial. A healthy fragment is not evidence that all files arrived.
Keep the order in one place. Alphabetical ordering of labels would put PARTIAL after BLOCKED, but there is no reason your next product label would preserve that accident. An explicit enum makes the comparison reviewable.
Notice that None is not another grade. Here it means this optional check supplied no usable evidence. That is a policy decision, and it is not appropriate for every gate. If production health is mandatory, missing health must block deployment in your caller.
Check the rule across every pair
Add this second block to the same file. It checks the entire small state space, not just one appealing example.
def verify() -> None:
for base in Verdict:
assert cap(base, None) == base
assert cap(base, base) == base
for evidence in Verdict:
result = cap(base, evidence)
assert result <= base
assert result <= evidence
assert result == cap(evidence, base)
assert cap(result, evidence) == result
assert classify(Verdict.PARTIAL, Verdict.READY) == Verdict.PARTIAL
assert classify(Verdict.READY, Verdict.BLOCKED) == Verdict.BLOCKED
assert classify(Verdict.READY, None) == Verdict.READY
assert classify(Verdict.READY, None, copy_verified=False) == Verdict.BLOCKED
if __name__ == "__main__":
verify()
print("partial copy + healthy =", classify(Verdict.PARTIAL, Verdict.READY).name)
print("complete copy + failed health =", classify(Verdict.READY, Verdict.BLOCKED).name)
print("cap properties passed")
These assertions say more than the names of the examples. A cap cannot promote either input. Repeating a cap does not change the answer. Applying the two measured requirements in the opposite order gives the same answer.
This is the sort of invariant that property-based testing lets you exercise with generated inputs. For this enum, exhaustive loops are simpler. If your inputs become durations, timestamps, or nested records, generated strategies become more useful than growing a hand-written list.
Run the file normally, without Python’s optimization flag, which removes assertions:
python3 verdict.py
The expected output is:
partial copy + healthy = PARTIAL
complete copy + failed health = BLOCKED
cap properties passed
As a quick check of the test itself, temporarily replace min with max. The assertion that the result never exceeds its base must fail. Restore min before integrating the classifier.
Decide what missing evidence means before adding thresholds
The production fix needed more than a better threshold. Quality workouts could prescribe distance and pace without duration. The old duration-only path treated that shape like a targetless session. The new volume path checks the prescribed distance when duration is absent.
After that, pace caps the volume result. At the tag, missing rep-sized pace normally leaves the volume verdict alone. There is an explicit exception for unverified running credit, which becomes missed. The tutorial’s copy_verified flag makes that distinction visible without bringing workout heuristics into the example.
Keep that exception outside the cap primitive. Otherwise the same None value gradually acquires incompatible meanings, and callers cannot tell whether they are abstaining or rejecting.
Gotchas
Fixing the first requirement may leave the reported bug untouched. The release’s distance fallback closes the short-session hole, but the documented misgraded sessions had already covered enough distance. They still needed the pace cap. Keep separate tests for incomplete volume and complete volume at the wrong pace.
Missing data can hide an unearned success. Treating absent pace as automatic failure would penalize historical sessions without splits. Treating every absence as permission to keep done would retain unverified running credit. The tagged implementation checks that exceptional case before abstaining; define an equally explicit policy for your own inputs.
A stricter cap can be calibrated against the wrong measurement. The release history records revisions after fixed-distance auto-laps mixed fast reps with recovery. The symptom was an overly punitive verdict. Compare like-for-like measurements before tightening a cutoff; the ordering tests above prove cap behavior, not measurement quality.
Sources
- Python IntEnum — named ordered states.
- Python min — least-value selection.
- Hypothesis introduction — testing properties over generated inputs.
Changelog
- Promote dev → main: 0.62.0 + 0.63.0 (#247, #248) (#249) (54e7c58)