trainlog/tools/validate_machine_exercises.py

84 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Validate MACHINE_EXERCISE_MODEL_V1 catalogs without inferring science."""
import json
import re
from pathlib import Path
from exercise_names import normalize_catalog_name
ROOT = Path(__file__).resolve().parents[1]
EXERCISE_ID = re.compile(r"^ex_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
PROFILE_ID = re.compile(r"^sp_[a-z0-9_]+_v1$")
LOADS = {"none", "external", "assistance", "bodyweight", "cardio"}
def load(name):
return json.loads((ROOT / "catalog" / name).read_text(encoding="utf-8"))
def main():
machine = load("machine-exercises-v1.json")
profiles = load("scientific-profiles-v1.json")
equipment = load("equipment-v1.json")
knowledge = load("exercise-knowledge-v1.json")
equipment_knowledge = load("equipment-knowledge-v1.json")
if set(machine) != {"format", "version", "exercises"} or machine["format"] != "trainlog-machine-exercises" or machine["version"] != 1:
raise ValueError("invalid machine exercise envelope")
if set(profiles) != {"format", "version", "profiles"} or profiles["format"] != "trainlog-scientific-profiles" or profiles["version"] != 1:
raise ValueError("invalid scientific profile envelope")
equipment_ids = {row["id"] for row in equipment["equipment"]}
exercise_knowledge = {row["exercise_id"] for row in knowledge["exercises"]}
capabilities = {(row["equipment_id"], cap["display_name"])
for row in equipment_knowledge["equipment"] for cap in row["capabilities"]}
profile_ids = set()
previous = ""
for row in profiles["profiles"]:
if set(row) != {"scientific_profile_id", "knowledge_source"}:
raise ValueError("invalid scientific profile record")
profile_id = row["scientific_profile_id"]
if not PROFILE_ID.fullmatch(profile_id) or profile_id <= previous or profile_id in profile_ids:
raise ValueError("scientific profiles must be unique and sorted")
previous = profile_id
profile_ids.add(profile_id)
source = row["knowledge_source"]
if source.get("kind") == "exercise":
if set(source) != {"kind", "exercise_id"} or source["exercise_id"] not in exercise_knowledge:
raise ValueError("unknown exercise knowledge source")
elif source.get("kind") == "equipment_capability":
if set(source) != {"kind", "equipment_id", "capability"} or (source["equipment_id"], source["capability"]) not in capabilities:
raise ValueError("unknown equipment capability source")
else:
raise ValueError("invalid profile source kind")
seen_ids = set()
seen_names = set()
previous = ""
for row in machine["exercises"]:
required = {"exercise_id", "display_name", "load_semantics", "machine_variant", "legacy_equipment_id", "scientific_profile_id", "science_state"}
if set(row) != required or not EXERCISE_ID.fullmatch(row["exercise_id"]):
raise ValueError("invalid machine exercise record")
if row["exercise_id"] <= previous or row["exercise_id"] in seen_ids:
raise ValueError("machine exercises must be unique and sorted")
previous = row["exercise_id"]
seen_ids.add(row["exercise_id"])
name = normalize_catalog_name(row["display_name"])
if name in seen_names:
raise ValueError("duplicate machine exercise name")
seen_names.add(name)
if row["load_semantics"] is not None and row["load_semantics"] not in LOADS:
raise ValueError("invalid load semantics")
if row["legacy_equipment_id"] is not None and row["legacy_equipment_id"] not in equipment_ids:
raise ValueError("unknown legacy equipment")
if row["scientific_profile_id"] is not None and row["scientific_profile_id"] not in profile_ids:
raise ValueError("unknown scientific profile")
resolved = row["science_state"] == "resolved"
if row["science_state"] not in {"resolved", "unresolved"} or resolved != (row["scientific_profile_id"] is not None):
raise ValueError("science state/profile mismatch")
seated_leg = next(row for row in machine["exercises"] if row["exercise_id"] == "ex_617007f9-7420-4408-91b9-8ffb77900f13")
if any(seated_leg[key] is not None for key in ("load_semantics", "legacy_equipment_id", "scientific_profile_id")):
raise ValueError("Seated Leg must remain unresolved")
print("MACHINE_EXERCISE_MODEL_V1 catalogs: OK")
if __name__ == "__main__":
main()