Invariants (CI Gate)
September 5, 2026 · View on GitHub
These rules run on every push. They serve as the primary quality mechanism — catching the majority of AI hallucinations or invalid annotations automatically, so that human review focuses on what machines cannot decide.
Hard and Soft
The rules are not all equal, and treating them as such previously caused harm.
HARD (1, 2, 3, 3b, 4, 5, 6, 7, 8, 9, 10, 17, 21, 22, 23, 24, 25) are structural and referential: an ID points to nothing, a vocabulary value does not exist, a muscle is listed twice. There are no legitimate exceptions for these, and any violation blocks the build.
SOFT (11, 12, 13, 14, 15, 20) are plausibility heuristics. They state correlations, not laws. "Cardio is not logged in reps" is usually true — but burpees exist. "A compound movement has at least two muscles" is usually true — but edge cases exist.
The danger of an overly strict rule is not the false alarm. A false alarm is visible. The danger is the silent bending of annotations to pass the gate: the result would be green CI with incorrect data — precisely the type of defect these rules are designed to prevent.
A soft invariant can therefore be excepted on a per-exercise basis:
exceptions:
invariant_12: "Squat Thrust / Burpee is a cardio / full-body exercise, but logged by repetitions."
The following policies apply:
- An exception on a hard invariant is itself an error.
- An exception without a justification text is an error.
- An exception that does not trigger at all is a warning. Otherwise stale exceptions accumulate that no one can associate anymore.
build/validate.pysummarizes at the end how often each soft invariant fired and how often it was excepted. A high frequency is a signal that the rule is flawed, not the data.
Structure
- Every file under
data/exercises/validates againstexercise.schema.json. - Every vocabulary value exists in the corresponding
vocab/*.yaml. slugis globally unique;idis globally unique. 3b. IDs withinvocab/muscles.yamlare unique across all hierarchy levels. Group, muscle, and head share a single table with a primary key in the build — therefore, a grouphamstringsand a musclehamstringscannot both exist. The same rule applies inequipment.yamlacrossprimary_equipmentandsetup.- Every language marked as
curatedinvocab/languages.yamlhas a file underdata/i18n/<lang>/for every exercise withstatus: active. - Every
data/i18n/<lang>/<id>.yamlhas a correspondingdata/exercises/<id>.yaml. status: merged⇒merged_intois set, target exists, target isactive.- No alias points to an ID that is itself an alias (no chains).
Content Plausibility
Everything here is soft unless stated otherwise: excusable via exceptions
with justification.
- (hard) At least one muscle with
role: primary. - (hard) No duplicate muscle; no muscle simultaneously primary and secondary.
- (hard) No muscle node alongside one of its ancestors or descendants
(
latissimus_dorsi+backis redundant,trapezius+traps_upperas well). primary_equipment: bodyweight⇒ no heavy barbell/cable setup insetup(squat_rack,power_rack,cable_tower,landmineprohibited).modality: cardio⇒tracking_type∈ {time,distance_time,distance_only}.modality: strength⇒tracking_type∉ {distance_only}.mechanic: isolation⇒ at most 2 primary muscles.mechanic: compound⇒ at least 2 participating muscles in total.- (deprecated/deleted) The rule used to be
supports_assistance: true ⇒ primary_equipment: bodyweight— which caused the exact defect it was meant to prevent. It restricted "is assisted" exclusively to bodyweight exercises, forcing an incorrect answer for assisted weight machines.supports_assistancewas removed without replacement; the intended concept is now represented byload_mode: assisted(Invariant 25). - (hard)
supports_added_weight: true⇒tracking_type∈ {bodyweight_reps,time}. - (deprecated/deleted) The rule used to be
movement_pattern anti_* ⇒ tracking_type ∈ {time, time_weight}. Becauseanti_*describes what is being resisted (not whether the movement is static or dynamic), predicting the logging type is fundamentally impossible (rollouts, Pallof press, walkouts etc. are dynamic). The information is already captured intracking_type. - (structurally satisfied) The rule used to be "
movement_patternmatchesforce_vector".force_vectoris no longer annotated; it is derived by the build fromforce_vector_by_patterninvocab/classification.yaml— a violation can no longer be formulated. The rule was not abolished, but moved into the data structure itself — a superior kind of invariant: one that cannot be broken, rather than one checked after the fact. - Primary muscle group must match
movement_pattern— table invocab/pattern_muscle_expectations.yaml. Every primary muscle group of an exercise is checked against expected groups for that pattern.movement_pattern: otheris exempt from this check. A violation is a warning, not an error: exceptions exist, but each warrants review.
Load Mode Invariants
- (hard)
supports_added_weight: true⇒load_mode: bodyweight. Adding external load presumes the base movement is bodyweight. - (hard)
load_mode: assisted⇒primary_equipment∈ {machine,resistance_band}. Assistance is provided by either a machine or a band.
Nothing more. The remaining conceivable rules around load_mode are mere
correlations, and turning correlations into laws was the mistake that made this
separation necessary.
Regression
- No
status: activeexercise disappears between two releases without an alias ormerged_intopointing to a successor. - Total count of active exercises never drops by more than 5% compared to the
previous release (threshold analogous to
WGER_FAIL_ON_REMOVED_THRESHOLD). - The golden set (
test/golden/*.yaml, ~50 hand-reviewed exercises) matches field-for-field. Runs before every AI batch as an evaluation for prompt changes.