#!/usr/bin/env python3
from __future__ import annotations
import json, statistics
from pathlib import Path
from datetime import datetime, timezone

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT/'assets/data/image-admin'
DOCS = ROOT/'docs'; DOCS.mkdir(exist_ok=True)
QA = ROOT/'qa_screenshots'; QA.mkdir(exist_ok=True)

def now():
    return datetime.now(timezone.utc).isoformat(timespec='seconds')

MOVEMENT_STANDARDS = {
  'linear-pogos': dict(
    category='ankle stiffness / elastic pogo', priority='keep-but-tighten', risk='low-moderate',
    purpose='Snabb fot/ankelstyvhet, kort markkontakt och rytm utan djup knäböj.',
    keyframes=['tall ready stance','small ankle load','quick bilateral takeoff','brief vertical flight','soft bilateral landing','reset tall'],
    mustShow=['fötter under höft','nästan raka men inte låsta knän','bål lång/stabil','armar små/rytmstyrande','markkontakt på framfot/mittfot'],
    avoid=['djup squat som gör övningen till squat jump','knän som faller in','bakåtflytt i bild','stor armkastning'],
    anchors=['bilateral foot pair / footCenter on grounded phases','no foot swap during a single pogo cycle'],
    cues=['Stolt hållning','Studsa lågt och snabbt','Mjuka knän, snabba fötter'],
    appDecision='Can be activated selectively after visual QA; keep MediaPipe candidate as measurement, not sole truth.'),
  'lateral-line-hops': dict(
    category='lateral quickness / line hop', priority='high-priority-fix', risk='moderate',
    purpose='Snabb sidledsförflyttning över linje med bibehållen höft- och knäkontroll.',
    keyframes=['load left of line','push laterally','feet together in flight/transition','land right of line','rebound/reset'],
    mustShow=['båda fötter hör ihop som fotpar','knä pekar samma håll som tå','höft följer sidled, inte bakåt','bål stabil','huvud neutralt'],
    avoid=['skelett som tar första bästa fot','visuell backhoppning','en fot landar medan andra driver fel håll','korssteg om övningen är line hop'],
    anchors=['visual-left/visual-right foot-pair identity','takeoff pair and landing pair must be linked across image + skeleton'],
    cues=['Hoppa sida till sida','Land mjukt','Håll knä över tå'],
    appDecision='Needs explicit foot-pair sequence; this is the best test case for the new skeleton truth logic.'),
  'lateral-pogo-jumps': dict(
    category='lateral plyometric stiffness', priority='needs-definition', risk='moderate',
    purpose='Sidledes pogo med låg amplitude, snabb markkontakt och fotledsdominans.',
    keyframes=['ready one side','ankle-driven lateral push','short flight','quiet landing other side'],
    mustShow=['liten sidledsförflyttning','knä inte djupt böjt','fötter/fotpar tydligt','bål stilla','kort markkontakt'],
    avoid=['skater-hop-liknande lång sidledshopp om övningen ska vara pogo','djup landning','oklar enkel-/dubbelfot'],
    anchors=['clarify bilateral vs single-leg before activation'],
    cues=['Litet och snabbt','Tyst landning','Håll höften stabil'],
    appDecision='Hold runtime activation until bilateral/single-leg definition is chosen.'),
  'broad-jump': dict(
    category='horizontal power jump', priority='manual-review', risk='moderate',
    purpose='Horisontell explosivitet med arm-swing, höftdriv och kontrollerad stick-landing.',
    keyframes=['athletic ready','hip hinge/load','triple extension takeoff','flight with arm swing','two-foot landing','stick/reset'],
    mustShow=['höft bak i laddning','armar bak-fram','båda fötter lämnar mark samtidigt','landning med böjd höft/knä','bröst upp i stick'],
    avoid=['landning på raka ben','baklänges path','fötter som byter identitet','flight utan tydlig takeoff'],
    anchors=['footCenter on start/landing','path forward only, not visual flip backwards'],
    cues=['Ladda höften','Hoppa långt','Frys mjukt i landning'],
    appDecision='Good candidate for blueprint-driven re-pick; requires front-to-back direction QA.'),
  'carioca-shuffle': dict(
    category='coordination / crossover footwork', priority='manual-review', risk='low-moderate',
    purpose='Koordination med korssteg, höftrotation och rytm utan att tappa bålposition.',
    keyframes=['ready','front crossover','open/reset','back crossover','reset'],
    mustShow=['tydligt korsande ben','höftrotation synlig','fotidentitet behålls','bål kontrollerad','armar balanserande'],
    avoid=['vanlig sidshuffle som råkar heta carioca','random fotankare','för stor överkroppsrotation'],
    anchors=['dominantContactFoot per step + explicit crossing leg'],
    cues=['Korsa framför','Öppna höften','Korsa bakom'],
    appDecision='Needs phase labels for front/back crossover before activation.'),
  'lateral-power-shuffle': dict(
    category='lateral acceleration / deceleration', priority='manual-review', risk='moderate',
    purpose='Athletic lateral push, låg höft, bred bas och kontrollerad deceleration.',
    keyframes=['athletic stance','push from outside foot','shuffle step','plant/decelerate','return/reset'],
    mustShow=['låg atletisk position','fötter inte korsade','höft flyttar sidled','knä över tå','armar motrörelse'],
    avoid=['carioca-korssteg','för hög stance','fotankare som hoppar sida'],
    anchors=['outside push foot then landing/plant foot'],
    cues=['Låg höft','Tryck ifrån','Bromsa mjukt'],
    appDecision='Keep separate from carioca; candidate selection must penalize crossed feet.'),
  'plank': dict(
    category='anti-extension core', priority='keep', risk='low',
    purpose='Statisk bålspänning med neutral ryggrad och stabil skuldra.',
    keyframes=['top plank','hold micro-variation'],
    mustShow=['rak linje skuldra-höft-ankel','armbåge/händer under skuldra','huvud neutralt','höft varken saggar eller pikar'],
    avoid=['svank','höft för högt','nacke upp'],
    anchors=['supportCenter fixed; no movement needed except subtle breathing if animated'],
    cues=['Spänn magen','Rak kropp','Tryck golvet bort'],
    appDecision='Good for static lock; animation can be minimal.'),
  'push-up': dict(
    category='upper push / trunk control', priority='keep-with-QA', risk='moderate',
    purpose='Armpress med stabil planklinje och kontrollerat djup.',
    keyframes=['top plank','lower with elbows bent','bottom controlled','press back to top'],
    mustShow=['rak kropp genom hela rörelsen','händer under/strax utanför skuldra','armbågar ca 30-60 grader','bröst rör sig mot golv'],
    avoid=['höft faller före bröst','huvud framåt','armbågar rakt ut i 90 grader'],
    anchors=['supportCenter/hands+feet; body line must not slide vertically independent of hands'],
    cues=['Rak kropp','Bröst mot golv','Pressa upp som en planka'],
    appDecision='Can be candidate-activated after body-line QA.'),
  'bear-crawl': dict(
    category='quadruped crawl / coordination', priority='needs-phase-detail', risk='low-moderate',
    purpose='Krypkoordination med låg höft, motsatt hand/fot och stabil bål.',
    keyframes=['quadruped hover','opposite hand-foot advance','reset/other diagonal'],
    mustShow=['knän nära golv men inte i golv','motsatt hand/fot rör sig ihop','rygg relativt neutral','höft låg'],
    avoid=['vanlig plank/mountain climber','knän för högt','samma sida hand/fot utan intention'],
    anchors=['diagonal support pair, not generic supportCenter only'],
    cues=['Knä strax över golv','Motsatt hand och fot','Små steg'],
    appDecision='Needs diagonal-limb intent before runtime activation.'),
  'plank-walkouts': dict(
    category='hinge-to-plank / core transition', priority='manual-review', risk='moderate',
    purpose='Kontrollerad walkout från stående/höftfällning till plank och tillbaka.',
    keyframes=['stand hinge','hands to floor','walk hands out','full plank','walk hands back','return hinge/stand'],
    mustShow=['fötter låsta i början','höftfällning istället för rund kollaps','händer går framåt stegvis','planklinje i slutläge'],
    avoid=['fot som glider bakåt','för stor svank i plank','oklar handförflyttning'],
    anchors=['rearFoot/footCenter locked during hinge, hands anchor in plank'],
    cues=['Fäll i höften','Gå ut med händerna','Håll plankan'],
    appDecision='Special phase anchors needed; good review candidate.'),
  'sit-up': dict(
    category='trunk flexion', priority='review-for-youth', risk='moderate',
    purpose='Kontrollerad bålflexion från ryggliggande till uppresning.',
    keyframes=['supine start','curl trunk','top sit','return controlled'],
    mustShow=['fötter/ben stabila','bål rullar upp kontrollerat','nacke neutral','ingen kastande armkompensation'],
    avoid=['ryck i nacke','svank/höftdrag som tar över','för snabb nedgång'],
    anchors=['footCenter/hipCenter depending on variant; clarify fixed feet or not'],
    cues=['Rulla upp','Kontrollerat ner','Titta snett fram'],
    appDecision='Needs variant choice; consider replacing with dead bug/hollow if youth-core focus.'),
  'side-plank-hip-lift': dict(
    category='lateral core / hip control', priority='keep-with-QA', risk='low-moderate',
    purpose='Sidobål och höftkontroll med höftlyft från sidoplanka.',
    keyframes=['side plank low','lift hip','top hold','lower controlled'],
    mustShow=['axel över armbåge/hand','kropp i sidolinje','höft rör sig vertikalt','huvud i linje'],
    avoid=['rotation framåt/bakåt','axel hänger','höft vandrar framåt'],
    anchors=['lower arm/support and foot side line'],
    cues=['Tryck ner armen','Lyft höften','Lång kropp'],
    appDecision='Good if candidate pose clearly shows side orientation.'),
  'burpee': dict(
    category='full-body transition / conditioning', priority='manual-review', risk='moderate-high',
    purpose='Kontrollerad helkroppsövergång stående-golv-stående med tydliga faser.',
    keyframes=['stand','squat hands down','jump/step back plank','optional push-up/low','jump/step feet in','stand/jump finish'],
    mustShow=['händer till golv innan fötter bak','planklinje i bakläge','fötter tillbaka nära händer','landning mjuk'],
    avoid=['ryggkollaps','för snabb okontrollerad landning','oklar variant med/utan push-up'],
    anchors=['hands-floor then supportCenter then footCenter; explicit variant required'],
    cues=['Händer ner','Hoppa/kliv till plank','Fötter fram och res dig'],
    appDecision='Must choose youth-safe variant before animation truth.'),
  'med-ball-slam': dict(
    category='medicine ball power / hinge-slam', priority='special-handling', risk='moderate',
    purpose='Helkroppskraft från överhuvud till slam med höft/bål och stabil fotbas.',
    keyframes=['ball overhead','load through trunk','slam down','follow-through hinge','reset/lift'],
    mustShow=['boll finns och följer händer','fötter kvar stabila','bål/höft driver rörelsen','armar går över huvud till nedåt'],
    avoid=['osynlig boll','kast utan höft','rygg rundar okontrollerat','bilder där händer inte håller boll'],
    anchors=['footCenter locked, ball/hand object link required'],
    cues=['Upp med bollen','Slamma genom höften','Följ med ner'],
    appDecision='Requires ball-object binding; MediaPipe hands alone are insufficient.'),
  'goblet-squat': dict(
    category='squat pattern / loaded front hold', priority='keep-with-prop-QA', risk='moderate',
    purpose='Knäböj med vikt framför kroppen, bål upprätt och knäkontroll.',
    keyframes=['stand goblet hold','descend','bottom squat','ascend','stand'],
    mustShow=['vikt synlig vid bröst','fötter stabila','knä över tå','höft under/kring parallellt beroende nivå','bröst upp'],
    avoid=['häl släpper','knän faller in','vikt saknas eller flyter'],
    anchors=['footCenter fixed, hand/object position stable against chest'],
    cues=['Bröst upp','Knä över tå','Tryck genom hela foten'],
    appDecision='Good if dumbbell/weight is visible; otherwise downgrade to bodyweight squat.'),
  'single-arm-row': dict(
    category='upper pull / hinge support', priority='keep-with-side-QA', risk='low-moderate',
    purpose='Enarmsrodd med höftfällning, stabil bål och skulderbladskontroll.',
    keyframes=['hinge start arm long','pull elbow back','top squeeze','lower controlled'],
    mustShow=['rygg neutral','roddarm tydligt jobbar','stödhand/ben stabilt om variant kräver','höftfällning'],
    avoid=['rotation i bål','axel dras upp mot öra','oklar aktiv arm'],
    anchors=['support side fixed; active hand/elbow path defined'],
    cues=['Lång rygg','Dra armbågen bak','Kontrollerat ner'],
    appDecision='Needs active-side field; otherwise left/right can confuse animation.'),
  'db-farmer-carry': dict(
    category='carry / posture gait', priority='static-or-gait-decision', risk='low',
    purpose='Bål-/greppstyrka med neutral hållning under gång.',
    keyframes=['tall hold','step/carry posture'],
    mustShow=['vikter vid sidan','axlar jämna','huvud neutralt','gångfas om det ska vara carry'],
    avoid=['bara stående utan carry-intent om passet räknar distans','sned lutning'],
    anchors=['foot step sequence if animated; otherwise static posture card'],
    cues=['Stå lång','Axlar jämna','Gå kontrollerat'],
    appDecision='Decide whether this is static demonstration or animated walking loop.'),
  'pull-up': dict(
    category='vertical pull / bar', priority='special-handling', risk='moderate',
    purpose='Pull-up med överhand/neutral grepp, skulderkontroll och barankare.',
    keyframes=['dead hang','active hang','pull','chin/upper chest near bar','lower controlled'],
    mustShow=['händer låsta i baren','bar rör sig inte','skuldra aktiv innan drag','kropp under bar','kontrollerad nedgång'],
    avoid=['bar/händer vandrar','hel sprite åker istället för kroppen under fast bar','spark/kipping om strikt variant'],
    anchors=['barTop + both hands fixed; skeleton/body moves around locked hands'],
    cues=['Aktiva axlar','Dra bröstet upp','Sänk kontrollerat'],
    appDecision='Do exactly what you suggested: lock bar/hands and show key frames, not free animation.'),
  'chin-up': dict(
    category='vertical pull / bar', priority='special-handling', risk='moderate',
    purpose='Chin-up med underhand/armbågsdrag och fast barankare.',
    keyframes=['dead hang','active hang','pull','chin over/near bar','lower controlled'],
    mustShow=['händer låsta i bar','bar statisk','armbågar dras ner/fram','kropp rör sig upp', 'ingen fri bar-drift'],
    avoid=['samma visuella som pull-up utan greppskillnad om det spelar roll','kipping om strikt'],
    anchors=['barTop + hands fixed'],
    cues=['Lås händerna','Dra dig upp','Sänk lugnt'],
    appDecision='Use locked-bar sequence; minimal frames can be enough.'),
  'seated-box-jump': dict(
    category='seated explosive jump', priority='special-prop-review', risk='moderate-high',
    purpose='Explosiv uppresning/hopp från sittande med kontrollerad landning.',
    keyframes=['seated on box','lean/load','takeoff','land on target/ground'],
    mustShow=['sittande start tydlig','box/bänk synlig och låst','fötter under knä','landning kontrollerad'],
    avoid=['box flyter/saknas','för djup instabil start','barn hoppar från för hög låda utan nivåstyrning'],
    anchors=['box prop + footCenter/landing foot pair'],
    cues=['Sitt stolt','Tryck ifrån','Land mjukt'],
    appDecision='Requires box prop layer and level/safety note.'),
  'buddy-hamstring-curls': dict(
    category='Nordic hamstring / partner', priority='special-handling', risk='moderate-high',
    purpose='Eccentrisk hamstringstyrka där knän är pivot och partner håller fötter.',
    keyframes=['kneel tall','controlled forward lower','catch/support hands','push/return or assisted reset'],
    mustShow=['knäna låsta i golv som pivot','höft sträckt, inte fälld','partner/fotankare tydligt','bål-lår i linje'],
    avoid=['höften böjer och gör övningen lätt/fel','knäpunkt vandrar','fötter inte fixerade','för stor svank'],
    anchors=['kneeCenter locked; feet/partner anchor visual must exist'],
    cues=['Lång kropp','Bromsa framåt','Knän kvar i golvet'],
    appDecision='As you said: lock the knee pivot. Needs special animation path, not generic sprite translation.'),
  'bodyweight-squat': dict(
    category='squat pattern', priority='keep', risk='low-moderate',
    purpose='Grundläggande knäböj med kontroll i höft, knä, fot och bål.',
    keyframes=['stand','descend','bottom','ascend/reset'],
    mustShow=['fötter stabila','knän över tår','höft bak/ner','bröst relativt upp','huvud neutralt'],
    avoid=['knävalgus','häl släpper','rund rygg','för kort ROM om målet är squat'],
    anchors=['footCenter fixed'],
    cues=['Knä över tå','Hela foten i golvet','Upp med bröstet'],
    appDecision='Good base exercise for validating skeleton truth.'),
  'squat-jump': dict(
    category='vertical power jump', priority='keep-with-landing-QA', risk='moderate',
    purpose='Vertikal hoppkraft från squat-laddning till kontrollerad landning.',
    keyframes=['stand','load squat','takeoff','flight','land soft','reset'],
    mustShow=['djupare load än pogo','triple extension','armar driver om variant kräver','båda fötter landar ihop','mjuk knä/höftböj'],
    avoid=['pogo-liknande för liten ROM','raka ben i landing','fotidentitet släpper'],
    anchors=['footCenter on start/landing; airborne free'],
    cues=['Ladda','Explodera upp','Land mjukt'],
    appDecision='Good selective activation candidate after landing review.'),
  'box-jump': dict(
    category='jump to box', priority='special-prop-review', risk='moderate-high',
    purpose='Hopp upp på låda med mjuk, stabil tvåfotslandning.',
    keyframes=['ready/load','takeoff','flight','land on box','stand/reset'],
    mustShow=['box synlig och fast','landning ovanpå box','båda fötter landar stabilt','höft/knä absorberar'],
    avoid=['osynlig målbox','landning på mark om övningen heter box-jump','för hög box för ungdom'],
    anchors=['box prop + landing foot pair'],
    cues=['Hoppa upp','Land tyst','Ställ dig stabilt'],
    appDecision='Needs prop-aware animation; not just horizontal jump path.'),
  'mountain-climber': dict(
    category='plank knee drive', priority='needs-leg-phase-QA', risk='low-moderate',
    purpose='Plankposition med alternerande knädriv och stabil överkropp.',
    keyframes=['plank start','one knee drive','switch/other knee drive'],
    mustShow=['händer låsta','skuldra över händer','höft relativt låg','knä driver mot bröst','fotidentitet alternerar'],
    avoid=['bear-crawl-förväxling','höft för högt','händer glider'],
    anchors=['hands/supportCenter fixed; active knee/foot alternates'],
    cues=['Händer stilla','Knä fram','Håll höften låg'],
    appDecision='Needs explicit active leg; otherwise skeleton may swap feet.'),
  'hamstring-bridge': dict(
    category='posterior chain / bridge', priority='keep-with-QA', risk='low-moderate',
    purpose='Höftlyft med hamstring/glute kontroll och stabil fotkontakt.',
    keyframes=['supine feet planted','bridge up','top hold','lower controlled'],
    mustShow=['fötter kvar i mark','knän stabila','höft lyfts utan svank','revben/kropp kontrollerad'],
    avoid=['tryck från rygg/svank','fötter glider','knän faller ut/in okontrollerat'],
    anchors=['feet fixed; hipCenter vertical path'],
    cues=['Tryck hälarna ner','Lyft höften','Kontrollerat ner'],
    appDecision='Good candidate for foot-lock + hip path.'),
  'chest-pass': dict(
    category='medicine ball chest pass', priority='needs-new-sequence-or-prop-binding', risk='moderate',
    purpose='Explosiv framåtkast från brösthöjd med stabil atletisk bas.',
    keyframes=['ball at chest','load slightly','extend arms/pass','follow-through','reset'],
    mustShow=['boll vid bröst','armar pressar framåt','bröst/höft riktning framåt','fötter stabila eller step-pass om variant'],
    avoid=['slam/overhead-bild som återanvänds som chest-pass','boll saknas','armar över huvud'],
    anchors=['ball object locked to both hands'],
    cues=['Boll vid bröst','Tryck explosivt fram','Fånga/reset'],
    appDecision='Likely needs new pose sequence unless existing candidates show true chest pass.'),
  'rotational-throw': dict(
    category='medicine ball rotational throw', priority='needs-new-sequence-or-prop-binding', risk='moderate',
    purpose='Rotationell kraftöverföring från höft/bål till kast med boll.',
    keyframes=['side-loaded','hip/torso rotate','release/throw','follow-through','reset'],
    mustShow=['tydlig rotation i höft/bål','boll följer händerna','fötter/pivot synliga','huvud/bröst roterar med kontroll'],
    avoid=['vanlig slam-sekvens','ingen boll','bara armar utan höftrotation','fötter som random byter ankare'],
    anchors=['pivot foot + ball/hand object link'],
    cues=['Ladda sidan','Rotera från höften','Kasta genom väggen'],
    appDecision='Needs true rotational candidates; current medball-slam mappings should not be trusted blindly.')
}

def main():
    blueprints = json.load(open(DATA/'physio-movement-blueprints.json'))
    report = json.load(open(DATA/'rc44_physio_candidate_match_report.json'))
    sequences = json.load(open(DATA/'pose-atlas-exercise-sequences.json'))
    out = {
        'version':'v97-rc45-ai-physio-first-pass',
        'createdAt':now(),
        'status':'movement-qa-first-pass-not-medical-clearance-not-runtime-active',
        'importantNote':'This is a structured biomechanics/animation audit for app development. It is not a substitute for clearance by a licensed physiotherapist/medical professional.',
        'principle':'The exercise defines the skeleton truth. Pose/sheet names are not authoritative. MediaPipe is used as a geometry matcher only.',
        'runtimePolicy':{
            'activeRuntimeChanged':False,
            'mediaPipeRuntimeActivated':False,
            'candidateNamesIgnored':True,
            'keepRC43LiveMotionUntilSelectiveApproval':True
        },
        'summary':{},
        'exercises':{}
    }
    counts={}
    for key,bp in blueprints['blueprints'].items():
        std=MOVEMENT_STANDARDS.get(key, {})
        rep=report['exercises'].get(key,{})
        scores=[kf.get('topScore') for kf in rep.get('keyframes',[]) if isinstance(kf.get('topScore'),(int,float))]
        avg=round(statistics.mean(scores),2) if scores else None
        mn=round(min(scores),2) if scores else None
        keyframes=[]
        for i,kf in enumerate(bp.get('keyframes',[])):
            r=(rep.get('keyframes') or [{}])[i] if i < len(rep.get('keyframes',[])) else {}
            phase=kf.get('phase') or r.get('phase') or f'phase-{i+1}'
            intent = (std.get('keyframes') or [])
            phaseTarget = intent[i] if i < len(intent) else phase
            keyframes.append({
                'keyframeId':kf.get('keyframeId') or r.get('keyframeId'),
                'order':i+1,
                'currentPhase':phase,
                'physioTargetPhase':phaseTarget,
                'currentAnchor':kf.get('anchorIntent',{}).get('primaryAnchor') or kf.get('anchorLandmark') or r.get('anchorIntent'),
                'currentContact':kf.get('contact') or r.get('contact'),
                'topCandidate':r.get('topCandidate'),
                'topScoreAgainstSeed':r.get('topScore'),
                'firstPassDecision':'seed-match-only' if scores else 'no-candidate',
                'reviewInstruction':'Compare selected candidate against physioTargetPhase and mustShow/avoid rules; do not approve from score alone.'
            })
        priority=std.get('priority','needs-review')
        counts[priority]=counts.get(priority,0)+1
        out['exercises'][key]={
            'exerciseKey':key,
            'label':bp.get('label') or sequences['sequences'].get(key,{}).get('label') or key,
            'category':std.get('category','uncategorized'),
            'priority':priority,
            'riskTierForAnimationReview':std.get('risk','unknown'),
            'purpose':std.get('purpose','Needs movement definition.'),
            'movementStandard':{
                'mustShow':std.get('mustShow',[]),
                'avoid':std.get('avoid',[]),
                'anchors':std.get('anchors',[]),
                'coachingCues':std.get('cues',[])
            },
            'appDecision':std.get('appDecision','Review required before activation.'),
            'candidateScoreWarning':'Scores are high because RC44 targets were seeded from the current mappings. A high score is geometric consistency with the seed, not physiotherapy approval.',
            'currentCandidateStats':{
                'keyframes':len(rep.get('keyframes',[])),
                'averageTopScoreAgainstSeed':avg,
                'minTopScoreAgainstSeed':mn,
                'greenBands':sum(1 for k in rep.get('keyframes',[]) if k.get('reviewBand')=='green'),
                'yellowBands':sum(1 for k in rep.get('keyframes',[]) if k.get('reviewBand')=='yellow'),
                'redOrNoneBands':sum(1 for k in rep.get('keyframes',[]) if k.get('reviewBand') in ('red','none')),
            },
            'keyframes':keyframes,
            'nextAction':('candidate-selective-activation-after-visual-QA' if priority in ('keep','keep-with-QA','keep-with-landing-QA','keep-with-prop-QA','keep-with-side-QA') else 'manual-blueprint-review-before-runtime')
        }
    out['summary']={
        'exerciseCount':len(out['exercises']),
        'keyframeCount':sum(len(e['keyframes']) for e in out['exercises'].values()),
        'priorityCounts':dict(sorted(counts.items())),
        'mostImportantFlags':['lateral-line-hops foot-pair identity','bar exercises hand/bar lock','buddy-hamstring-curls knee pivot lock','medball/chest/rotational require object binding','box/seated-box require prop binding','carioca vs lateral shuffle must not share foot logic']
    }
    (DATA/'rc45_ai_physio_first_pass.json').write_text(json.dumps(out,ensure_ascii=False,indent=2),encoding='utf-8')

    md=[]
    md.append('# RC45 AI physio first-pass movement audit\n')
    md.append('Status: biomechanics/animation QA only, not medical clearance. Runtime is not changed.\n')
    md.append('Principle: exercise truth → skeleton keyframes → MediaPipe candidate matching → manual approval. Pose names are not truth.\n')
    md.append('## Priority summary\n')
    for k,v in out['summary']['priorityCounts'].items(): md.append(f'- {k}: {v}')
    md.append('\n## High-priority animation rules\n')
    for item in out['summary']['mostImportantFlags']: md.append(f'- {item}')
    md.append('\n## Exercise audit\n')
    for key,e in out['exercises'].items():
        md.append(f"\n### {e['label']} (`{key}`)\n")
        md.append(f"Priority: **{e['priority']}** · Risk tier: **{e['riskTierForAnimationReview']}**\n")
        md.append(f"Purpose: {e['purpose']}\n")
        md.append('Must show: ' + '; '.join(e['movementStandard']['mustShow']) + '\n')
        md.append('Avoid: ' + '; '.join(e['movementStandard']['avoid']) + '\n')
        md.append('Anchors: ' + '; '.join(e['movementStandard']['anchors']) + '\n')
        md.append('Cues: ' + '; '.join(e['movementStandard']['coachingCues']) + '\n')
        md.append(f"App decision: {e['appDecision']}\n")
    (DOCS/'RC45_AI_PHYSIO_FIRST_PASS.md').write_text('\n'.join(md),encoding='utf-8')

    qa={
        'createdAt':now(),
        'generatedFiles':['assets/data/image-admin/rc45_ai_physio_first_pass.json','docs/RC45_AI_PHYSIO_FIRST_PASS.md'],
        'runtimeChanged':False,
        'exerciseCount':out['summary']['exerciseCount'],
        'keyframeCount':out['summary']['keyframeCount'],
        'priorityCounts':out['summary']['priorityCounts']
    }
    (QA/'rc45_ai_physio_first_pass_report.json').write_text(json.dumps(qa,ensure_ascii=False,indent=2),encoding='utf-8')
    print(json.dumps(qa,ensure_ascii=False,indent=2))

if __name__ == '__main__': main()
