""" In-Line Lunge rubric scorer — pure function, no model calls. FMS In-Line Lunge Criteria (bilateral): - Score 3: dowel contacts maintained, no torso movement, knee touches behind heel. - Score 2: movement completed with compensation (trunk lean, loss of balance). - Score 1: loss of balance, inability to maintain foot contact or posture. - Score 0: PAIN — never auto-scored. """ from __future__ import annotations from formscout.types import BiomechFeatures, ScoreResult def score_inline_lunge(features: BiomechFeatures) -> ScoreResult: """Pure rubric scorer for in-line lunge.""" angles = features.angles alignments = features.alignments has_knee = "front_knee_flexion_deg" in angles if not has_knee: return ScoreResult( score=1, rationale="Insufficient data: knee flexion not measurable", confidence=0.3, notes="missing key measurements", ) knee_flex = angles.get("front_knee_flexion_deg", 180) trunk_upright = alignments.get("trunk_upright", False) knee_over_ankle = alignments.get("knee_over_ankle", False) rationale_parts = [] # Good lunge: knee flexion < 90° (deep), trunk upright, knee aligned deep_enough = knee_flex < 100 if deep_enough and trunk_upright and knee_over_ankle: score = 3 rationale_parts.append("Deep lunge with trunk upright and knee aligned") elif deep_enough or (trunk_upright and knee_over_ankle): score = 2 if not trunk_upright: rationale_parts.append(f"trunk lean {angles.get('trunk_lean_from_vertical_deg', '?')}°") if not knee_over_ankle: rationale_parts.append("knee drifts past ankle") if not deep_enough: rationale_parts.append(f"knee flexion {knee_flex:.0f}° (insufficient depth)") rationale_parts.insert(0, "Completed with compensation") else: score = 1 rationale_parts.append("Unable to complete lunge pattern") if not deep_enough: rationale_parts.append(f"knee flexion only {knee_flex:.0f}°") confidence = features.confidence * 0.85 return ScoreResult( score=score, rationale="; ".join(rationale_parts), confidence=confidence, notes="", )