""" Rotary Stability rubric scorer — pure function, no model calls. FMS Rotary Stability Criteria: - Score 3: unilateral (same-side) arm/leg extension with trunk stable, elbow/knee touch performed smoothly. - Score 2: contralateral (opposite) arm/leg extension performed with trunk stable. - Score 1: inability to maintain trunk stability during contralateral pattern. - Score 0: PAIN (spinal flexion clearing test) — never auto-scored. """ from __future__ import annotations from formscout.types import BiomechFeatures, ScoreResult def score_rotary_stability(features: BiomechFeatures) -> ScoreResult: """Pure rubric scorer for rotary stability.""" angles = features.angles alignments = features.alignments has_data = "trunk_stability_std_px" in angles or "shoulder_level_diff_px" in angles if not has_data: return ScoreResult( score=1, rationale="Insufficient data: trunk stability not measurable", confidence=0.3, notes="missing key measurements", ) trunk_stable = alignments.get("trunk_stable", False) shoulders_level = alignments.get("shoulders_level", False) hips_level = alignments.get("hips_level", False) rationale_parts = [] # Without video classification of ipsi vs contra, assume contralateral (safer) if trunk_stable and shoulders_level and hips_level: score = 2 # Assume contralateral unless classifier says ipsilateral rationale_parts.append("Trunk stable during extension, shoulders and hips level") rationale_parts.append("scored as contralateral pattern (default)") elif trunk_stable or (shoulders_level and hips_level): score = 2 if not trunk_stable: rationale_parts.append("minor trunk instability") rationale_parts.insert(0, "Contralateral pattern with minor compensation") else: score = 1 std = angles.get("trunk_stability_std_px", 0) rationale_parts.append(f"Trunk instability detected (std {std:.1f}px)") if not shoulders_level: rationale_parts.append("shoulder asymmetry during extension") confidence = features.confidence * 0.75 # Lower confidence — hard to assess from 2D return ScoreResult( score=score, rationale="; ".join(rationale_parts), confidence=confidence, notes="ipsi/contra distinction requires VLM classifier", )