1(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[4333],{85278:function(e,a,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/internal/heuristics",function(){return t(81613)}])},32849:function(e,a,t){"use strict";var s=t(85893),i=t(9008),n=t.n(i),r=t(11163);let o={title:"PoseTracker API",siteName:"PoseTracker",description:"PoseTracker offers the leading real-time pose estimation API, designed for effortless and swift integration into mobile apps, enabling developers to implement advanced motion tracking features with precision and ease",url:"https://app.posetracker.com",type:"website",robots:"noindex, nofollow",image:"https://app.posetracker.com/assets/card-image.png",author:"Fabrice Sepret"},l=[{rel:"apple-touch-icon",sizes:"57x57",href:"/favicon/apple-icon-57x57.png"},{rel:"apple-touch-icon",sizes:"60x60",href:"/favicon/apple-icon-60x60.png"},{rel:"apple-touch-icon",sizes:"72x72",href:"/favicon/apple-icon-72x72.png"},{rel:"apple-touch-icon",sizes:"76x76",href:"/favicon/apple-icon-76x76.png"},{rel:"apple-touch-icon",sizes:"114x114",href:"/favicon/apple-icon-114x114.png"},{rel:"apple-touch-icon",sizes:"120x120",href:"/favicon/apple-icon-120x120.png"},{rel:"apple-touch-icon",sizes:"144x144",href:"/favicon/apple-icon-144x144.png"},{rel:"apple-touch-icon",sizes:"152x152",href:"/favicon/apple-icon-152x152.png"},{rel:"apple-touch-icon",sizes:"180x180",href:"/favicon/apple-icon-180x180.png"},{rel:"mask-icon",href:"/favicon/safari-pinned-tab.svg",color:"#007bff"},{rel:"icon",href:"/favicon/favicon.ico"},{rel:"icon",type:"image/png",sizes:"16x16",href:"/favicon/favicon-16x16.png"},{rel:"icon",type:"image/png",sizes:"32x32",href:"/favicon/favicon-32x32.png"},{rel:"icon",type:"image/png",sizes:"96x96",href:"/favicon/favicon-96x96.png"},{rel:"icon",type:"image/png",sizes:"192x192",href:"/favicon/android-icon-192x192.png"},{rel:"manifest",href:"/site.webmanifest"}];a.Z=e=>{let a=(0,r.useRouter)(),t={...o,...e};return t.title=e.templateTitle?"".concat(e.templateTitle," | ").concat(t.siteName):t.title,(0,s.jsxs)(n(),{children:[(0,s.jsx)("title",{children:t.title}),(0,s.jsx)("meta",{name:"robots",content:t.robots}),(0,s.jsx)("meta",{content:t.description,name:"description"}),(0,s.jsx)("meta",{property:"og:url",content:"".concat(t.url).concat(a.asPath)}),(0,s.jsx)("link",{rel:"canonical",href:"".concat(t.url).concat(a.asPath)}),(0,s.jsx)("meta",{property:"og:type",content:t.type}),(0,s.jsx)("meta",{property:"og:site_name",content:t.siteName}),(0,s.jsx)("meta",{property:"og:description",content:t.description}),(0,s.jsx)("meta",{property:"og:title",content:t.title}),(0,s.jsx)("meta",{name:"image",property:"og:image",content:t.image}),(0,s.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,s.jsx)("meta",{name:"twitter:site",content:"@movelyticsAI"}),(0,s.jsx)("meta",{name:"twitter:title",content:t.title}),(0,s.jsx)("meta",{name:"twitter:description",content:t.description}),(0,s.jsx)("meta",{name:"twitter:image",content:t.image}),t.date&&(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("meta",{property:"article:published_time",content:t.date}),(0,s.jsx)("meta",{name:"publish_date",property:"og:publish_date",content:t.date}),(0,s.jsx)("meta",{name:"author",property:"article:author",content:t.author})]}),l.map(e=>(0,s.jsx)("link",{...e},e.href)),(0,s.jsx)("meta",{name:"msapplication-TileColor",content:"#F53838"}),(0,s.jsx)("meta",{name:"msapplication-TileImage",content:"/favicon/ms-icon-144x144.png"}),(0,s.jsx)("meta",{name:"theme-color",content:"#F53838"})]})}},83869:function(e,a,t){"use strict";t.d(a,{Z:function(){return h}});var s=t(85893),i=t(67294),n=t(74732);let r=["[email protected]"];var o=t(32849);function l(e){let{children:a,title:t}=e,{user:l,loading:c}=(0,n.a)(),[d,h]=(0,i.useState)(!1);(0,i.useEffect)(()=>{var e;h("localhost"===(e=window.location.hostname)||"127.0.0.1"===e)},[]);let p=d||function(e){if(!e)return!1;let a=String(e.email||"").toLowerCase().trim();return r.includes(a)}(l);return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(o.Z,{title:t||"PoseTracker internal",robots:"noindex, nofollow"}),c&&!d?(0,s.jsx)("div",{style:{minHeight:"100vh",background:"#111",color:"#aaa",display:"flex",alignItems:"center",justifyContent:"center"},children:"Loadingâ¦"}):p?a:(0,s.jsx)("div",{style:{minHeight:"100vh",background:"#111",color:"#666",display:"flex",alignItems:"center",justifyContent:"center"},children:"Not found"})]})}var c=t(94054),d=t.n(c);function h(e){let{title:a,children:t}=e;return(0,s.jsx)(l,{title:a,children:(0,s.jsx)("div",{className:d().root,children:(0,s.jsx)("div",{className:d().wrap,children:t})})})}},40724:function(e,a,t){"use strict";t.d(a,{Z:function(){return r}});var s=t(85893),i=t(94054),n=t.n(i);function r(e){let{review:a,compact:t=!1}=e,i=!!(null==a?void 0:a.skeleton),r=!!(null==a?void 0:a.live);return(0,s.jsxs)("div",{className:"".concat(n().reviewFlags," ").concat(t?n().reviewFlagsCompact:""),onClick:e=>e.preventDefault(),children:[(0,s.jsxs)("label",{className:i?n().reviewOn:n().reviewOff,title:"Authored lab skeleton and gates",children:[(0,s.jsx)("input",{type:"checkbox",checked:i,disabled:!0,readOnly:!0}),"Skeleton"]}),(0,s.jsxs)("label",{className:r?n().reviewOn:n().reviewOff,title:"Debug camera: guards refined and signed off",children:[(0,s.jsx)("input",{type:"checkbox",checked:r,disabled:!0,readOnly:!0}),"Live"]})]})}},81613:function(e,a,t){"use strict";t.r(a),t.d(a,{__N_SSP:function(){return D},default:function(){return R}});var s=t(85893),i=t(67294),n=t(11163),r=t(41664),o=t.n(r),l=t(83869);let c=[{id:"host",title:"Host iframe",tag:"Front"},{id:"movenet",title:"MoveNet COCO-17",tag:"2D"},{id:"session",title:"createEngine / session",tag:"Interpreter"},{id:"counter",title:"counter + dump",tag:"Output"}],d=[{id:"phases",title:"Phases + identity",tag:"repetition",example:"squat"},{id:"orbit",title:"Orbit",tag:"cycle",example:"shoulder_roll"},{id:"breath",title:"Breath",tag:"cycle",example:"shoulder_deep_breath"},{id:"side_reach",title:"Side reach",tag:"cycle",example:"chair_side_stretch"},{id:"hold",title:"Hold / duration",tag:"duration",example:"chair_forward_fold"}],h=[{id:"placement",title:"Placement + silhouette"},{id:"view",title:"Face / profile lock"},{id:"zoom",title:"Zoom / scale"},{id:"elevation",title:"Camera elevation"}],p=[{k:"type",v:"repetition | duration"},{k:"lock",v:"auto | require_face | require_profile"},{k:"views.face / .profile",v:"metrics[] + phase_order + phases.when"},{k:"cycle.kind",v:"orbit | breath | side_reach (+ scale_anchor: face)"},{k:"hold",v:"phase, enter_frames, exit_frames"},{k:"identity",v:"kind + reject_span_growth (squat)"},{k:"grade.mode",v:"depth_only | orbit_lift | breath_lift | reach_lift | â¦"},{k:"placement.silhouette",v:"upper | full | seated â mannequin face / profile / back, filled or outline"},{k:"camera_elevation.proxy",v:"standing | seated"},{k:"metrics.kind",v:"angle3, segment_ratio_y, tilt_from_vertical, norm_rel_y, norm_dx, â¦"}],m=[{id:"reco_count",title:"1. Rep counter",tag:"Did they finish?",example:"standing â squat â stand = +1"},{id:"reco_grade",title:"2. Letter grade",tag:"How good?",example:"AâF from depth, not from the cue text"},{id:"reco_cue",title:"3. Coaching text",tag:"What do we say now?",example:"âKnees caving in!â on this frame"}],u=[{id:"reco_polarity",title:"Polarity: TRUE fires",tag:"V3"},{id:"reco_context",title:"Context + units",tag:"V3"},{id:"reco_emit",title:"Emit + stale cue",tag:"SDK"},{id:"reco_v4gap",title:"V4 has no cues",tag:"Gap"}],f=[{id:"squat_knees",title:"knees_caving",tag:"pixel X",example:"warning"},{id:"squat_lean",title:"leaning_forward",tag:"pixel X",example:"warning"},{id:"squat_depth",title:"not_deep_enough",tag:"ratio",example:"info"}],g=[{id:"v4_predicates",title:"Phase predicates",tag:"V4 JSON"},{id:"v4_grade",title:"depthScore â AâF",tag:"V4"},{id:"v4_skips",title:"Skip reasons â reco",tag:"QA"}],x=[{k:"A",v:"score ⥠90"},{k:"B",v:"score ⥠80"},{k:"C",v:"score ⥠70"},{k:"D",v:"score ⥠60"},{k:"E",v:"V4 only (score ⥠40). V3 jumps D â F."},{k:"F",v:"below the last band"}
1],v=[{k:"squat (prod)",v:"form_score.depth_only = true â recos do NOT change the grade. Score = 100 â 40\xd7t, t = clamp((depth â ideal) / (worst â ideal)). Face: min_leg_ratio over the dip (ideal 0.05, worst 0.40). Profile fallback: min knee angle (90\xb0 / 150\xb0)."},{k:"most other V3",v:"start 100. Angle: Σ |current â ideal| / 10 \xd7 5, cap 40. Tempo vs tempo_range, cap 30. Reco: â10 per active cue this frame, cap 30. Then minGrade can drop the count entirely."},{k:"V4 reps",v:"depthScore(metric, ideal, worst) â 0â100 â gradeOf. minGrade gate before public count. Holds have no AâF."}],_=[{k:"{joint}_x / _y",v:"Denormalized onto a virtual 480\xd7640 portrait. Pixel deltas (20, 50, 100) are authored for that frame â not centimetres, not % of body."},{k:"min_leg_ratio",v:"min( (knee.yâhip.y) / (ankle.yâknee.y) ) left/right. Standing ~â¥0.68. Deep squat small. Scale-invariant."},{k:"angle / primary_angle",v:"3-point angle in degrees from angles_side_view when orientation = profile."},{k:"min_arm_ratio, hip_lift_ratio, â¦",v:"Per-exercise extras stuffed into the same context object. Each YAML invented its own names."}],y=[{k:"feedback.<id>",v:"Stable rule id (knees_caving). Not sent to the host â only message strings are."},{k:"condition",v:"JS boolean, safeEval, face view. TRUE â push the cue."},{k:"condition_side_view",v:"Same, profile. If missing, face condition is reused (or the rule is face-only)."},{k:"message",v:"English copy streamed in recommendations[]."},{k:"severity",v:"warning | info. Hosts often ignore it and only show the string."}],w=[{k:"metrics[].kind",v:"Closed library: angle3, segment_ratio_y, relative_y, alignment_*, tilt_from_vertical, norm_rel_y, norm_dx, min_of / max_of / abs_diff, ⦠No raw pixels, no safeEval."},{k:"phases.<name>.when",v:"all[] / any[] of { metric, op: gte|lte|gt|lt|exists, value }. First matching phase in phase_order wins."},{k:"identity.ranges",v:"Whole-rep envelope. A dip can match eccentric and still be rejected as walk / hinge / one-leg."},{k:"grade.ideal / worst",v:"Maps the tracked metric onto 0â100. Smaller-better when ideal < worst (ratios)."}];var b=t(94054),k=t.n(b);let j={host:{title:"Host iframe",files:["PoseTrackerFront/pages/pose_tracker/tracking.js","PoseTrackerFront/components/v3/CameraFeedV3.js"],what:"Same production tracking URL as V3. Default engine is V4 except unlabeled squat / face_squat (those keep the current V3 FSM). Explicit ?engine=v4 is required for the catalog squat. Optional engineDebug=true (HUD + Export dump), placementOverlay, timer, guide. Native COUNTER events fire when result.counted is true (including each new second on a hold).",how:"createEngine() from @posetracker/engine-v4. TrackingAppV3 calls resolveMovementEngine() (src/channel.js). Session is created once per exercise id. processPose gets normalized keypoints each frame. Unlabeled squat never takes this path.",challenge:["Is query-param switching enough for a future product cut-over, or do we need an EngineLoader channel / manifest like V3?","V4 counter emit is still âcounted this frameâ. Duration works because hold ticks counted=true on each new second â easy to miss if someone later batches time."]},movenet:{title:"MoveNet COCO-17",files:["17 names: nose, eyes, ears, shoulders, elbows, wrists, hips, knees, ankles"],what:"Only x, y, score. No Z. Image distances are not anatomical lengths. A walk-in scales the whole skeleton. A floor camera vs a desk camera changes foreshortening (different trap from zoom).",how:"Never treat raw image-Y as âthe joint went upâ. Use angles, ratios of same-depth segments, face-scale, or body span. See BIOMECHANICS_2D.md.",challenge:["Should we ever add a 3D / IMU channel, or is 2D-only a product constraint?","Desk crops invent hips. Face-locked movements already special-case isFaceUpper / isFacePlanted. That pattern will keep growing unless view detection is redesigned."]},session:{title:"Interpreter (session.js)",files:["engine/v4/src/session.js","engine/v4/src/index.js â createEngine()","engine/v4/dist/engine-v4.bundle.js (test-only build)"],what:"One session per movement. Stages: placement â countdown â measuring. After measuring it branches: cycle (orbit/breath) XOR type=duration (hold) XOR phase+identity (squat). EMA-smoothed keypoints. Skip reasons accumulate in skipHist for dumps.",how:"Catalog JSON is data. The interpreter is generic. Adding a movement should not fork session.js unless you need a new cycle.kind or a new identity.kind.",challenge:["Three counting paths in one file is already a smell. Splitting hold / cycle / dip into strategy objects would make the next exercise cheaper â or more abstract for little gain.","min_rep_duration is a throttle between reps, not âseconds in poseâ. Easy to confuse with duration holds."]},counter:{title:"Outputs",files:["engine_debug (~250 ms)","session_dump on end/Export","native COUNTER / form_score"],what:"Public count after minGrade (rep movements). Duration: count = floor(seconds in hold), session total (pause on leave, resume on return). Grades AâF from depthScore. lastSkipReason + skipHist for QA.",how:"Paste session_dump JSON in chat, not screenshots. HUD is test-only.",challenge:["Holds have no form grade. Is âseconds onlyâ enough for a product, or do we need tightness / stability scores?","Dump keeps the last 40 debug frames. Fine for QA, useless for a 10-minute hold â do we need a sampled timeline?"]},catalog:{title:"Catalog JSON + schema",files:["engine/v4/catalog/<id>.json (source of truth)","engine/v4/schema/movement.schema.json","scripts/generate-v4-catalog.mjs â *.movement.js + registry"],what:"Movements are JSON, not eval. generate writes JS modules the interpreter and the Front lab import. validate-v4-catalog checks views, metrics used in when, lock vs face/profile, counter vs duration.",how:"Do not hand-edit .movement.js. After JSON changes: npm run smoke:v4 && npm run build:v4. Gold examples: squat, shoulder_roll, shoulder_deep_breath, chair_forward_fold, chair_side_stretch.",challenge:["Is a closed metric library (angle3, ratios, â¦) enough, or will the next sport force a new kind every time?","Schema additionalProperties: false is strict on purpose. Painful for experiments, good for not shipping typos."]},isolation:{title:"Isolation (not production)",files:["engine/v4/ISOLATION.md"],what:"V4 is the iframe and npm default except unlabeled squat. Production V3 squat FSM is embedded in engine-v4.bundle.js. engine.bundle.js remains the explicit V3 channel. Never overwrite it.",how:"Never overwrite PoseTrackerStrAPI/private/sdk-engine/engine.bundle.js. Never change lib/v3 to âjust test V4â.",challenge:["When is the product switch? Dual engines forever is a tax. A written cut-over (feature flag, exercise-by-exercise) is missing from this board on purpose â it is a product decision.","Test-only means QA still lives on a hidden URL. That hides regressions from the rest of the company."]},phases:{title:"Phases + identity (squat family)",files:["src/metrics.js pickPhase","src/identity.js","catalog/squat.json"],what:"type=repetition, no cycle. Phases idle â eccentric â end_range (predicates on metrics). Count = return to counter.trigger_phase from from_phases AND identity.confirm. Identity rejects walk-in (span growth), hinge, one-leg, lost ankles.",how:"Face: leg ratios / relative Y. Profile: hipâkneeâankle angle. Camera elevation shifts the authored gates. A shallow dip still counts (low grade) unless minGrade.",challenge:["Identity kinds are per-movement. The next lunge/curl will copy-paste squat heuristics unless we invest in a real identity library.","Phase machines assume a dip. They are the wrong tool for orbits and holds â that is why cycle/hold exist. Keep it that way."]},orbit:{title:"Orbit (shoulder roll)",files:["src/orbit.js","catalog/shoulder_roll.json"],what:"Bilateral lift-then-drop. Internal vs external from opposite X on the way down. Same-direction skip. Arms may hang. Face only. Lift compared in face units when scale_anchor=face.",how:"Count is not âdrew a circleâ. From the camera it is a tall oval. Zoom is not a turn.",challenge:["Rotation from X drift is fragile if the user faces 20\xb0 off-axis. Do we need a stronger laterality signal, or is âface the cameraâ the product rule?"]},breath:{title:"Breath (deep inhale)",files:["src/breath.js","src/faceScale.js","catalog/shoulder_deep_breath.json"],what:"Bilateral gap shrink then grow: (sh
1oulder.y â nose.y) / faceScale. Count only after a real expire (return near rest). A hold at the top is one breath, not one count per second. Tiny F-grade jitters are not reps.",how:"Catalog min_inhale stays in frame units; interpreter divides by rest face size. Skip zoom if face size jumps ~10% mid-cycle.",challenge:["Face scale needs eyes/ears. A heavy crop or glasses-side profile can null the guard (scale=1) and we fall back to raw Y. Acceptable?","Breath vs shoulder shrug is a product call (small inhale). Grades will feel âtoo easyâ or âtoo strictâ per user."]},side_reach:{title:"Side reach (chair stretch)",files:["src/reach.js","catalog/chair_side_stretch.json","V4ReachGuide.js"],what:"Seated, face-only. Sit tall, arms along the body (reaching wrist optional; opposite elbow is enough). Lean in the frontal plane and take the wrist along a dashed path to a far zone. Count = wrist in the zone. Path quality grades the rep, not the count. Zone is sized from arm length and pulls in when a good stretch cannot reach the first guess. Rounded back lowers the grade.",how:"Arm length from hanging wrist, or 2\xd7 upper arm if the wrist is hidden. Ideal path is a quadratic (hip â over the head â far zone). Trail vs path is mean distance / arm. Opposite elbow raised â skip opp_arm.",challenge:["COCO wrists are noisy at the top of the frame. If live dumps show flicker
1counts, raise min_lift or require a few frames in-zone before the tick.","âStay tallâ from nose Y will miss a rounded back that keeps the head up. Is that acceptable for a gentle stretch class?"]},hold:{title:"Hold / duration (chair fold)",files:["src/hold.js","catalog/chair_forward_fold.json"],what:"type=duration. 1 public count per whole second in the hold phase. enter_frames before the clock starts; exit_frames hysteresis so a 200 ms dropout does not pause. Leave the pose â pause and bank time; come back â continue the session total. Head/arms ignored if not in required_points.",how:"Chair fold: shins near vertical (angle), shoulders ABOVE knees (signed Y / shin), lateral offset allowed (norm_dx). No pixel distances. Camera elevation uses a seated proxy (shoulderâknee / shin) sampled while sitting upright, not while folded.",challenge:["Session-total vs âthis bout onlyâ: sitting up and folding again continues the counter. Right for a stretch class; wrong if you wanted unique attempts.","Plank / wall sit should reuse hold.js + phases, not a new cycle.kind. If the next static exercise needs a new tracker, the abstraction failed."]},placement:{title:"Placement",files:["src/placement.js","V4PlacementOverlay.js","StartPoseFigure.js","src/labPose.js (startPosePoints)"],what:"required_points + inner box. box_width/box_height are % of the displayed camera; padding_* is the fallback. Silhouette: upper | full | seated | plank | supine. skipPlacement=true in lab Test live / smokes.",how:"Each required entry is a group; || means alternatives. Missing or out of box â stage stays placement. Start pose is the idle lab skeleton (t = 0) with a rounded shell â it reloads when heuristics / official poses change. Dots are indicative; the timer starts in-zone, not on an exact joint match.",challenge:["Keep startPosePoints() as the only silhouette source. A hand-drawn mannequin will drift from the animation the moment a pose is edited."]},view:{title:"View lock",files:["src/view.js"],what:"lock auto | require_face | require_profile. Detector uses shoulder/hip span. Desk hips can flip to profile; isFaceUpper / isFacePlanted keep face-locked upper-body / seated moves counting.",how:"unsupported_view: hint_only vs reject. Host direction: in-frame | face-camera | profile-camera.",challenge:["Global view from hips is the wrong feature for seated folds. We patched around it. A crop-aware view model might be cleaner than more isFace* helpers."]},zoom:{title:"Zoom / walk-in",files:["src/faceScale.js","identity.reject_span_growth"],what:"Uniform scale is not a movement. Upper body: face spans (eyes, noseâeye, ears). Full body: noseâankles span. Duration holds use angles/ratios so zoom is mostly invariant; still smoke it.",how:"Never count a raw Y-lift without one of those two guards.",challenge:["Face EMA restScale lags; a fast lean-in can skip zoom on a real breath. We chose a mild max_scale_delta (10%) plus gapNorm. Tune vs false skips."]},elevation:{title:"Camera elevation",files:["src/cameraElevation.js","CAMERA_ELEVATION.md"],what:"Not IMU pitch. Height relative to the body. Standing proxy: torso/leg Y ratio. Seated proxy: shoulderâknee / shin on upright sit. Freeze after N samples. Shift authored when.values (bias â1 floor ⦠+1 high).",how:'Keep keypoints as-is; move thresholds. Until freeze, use mid (catalog as written). Override: createSession({ cameraElevation: "ground"|"mid"|"high" }).',challenge:["Two proxies already. A third body shape will need a third. Is bias-on-thresholds enough, or do we need per-camera calibration?","Seated sampling ignores fold frames (ratio < 0.55). If someone never sits upright in frame, we stay mid forever."]},lab:{title:"Lab + smokes",files:["/internal/heuristics","engine/scripts/smoke-v4.mjs","NEW_EXERCISE.md"],what:"Admin catalog, 17-point skeletons, Test live (same iframe + overlay/timer). Smokes are the contract: happy path, zoom, one-side, tiny / hold-without-expire. Cursor rule engine-v4-exercises forces reading NEW_EXERCISE + BIOMECHANICS_2D before a new move.",how:"cd posetracker-rn-sdk/engine && npm run smoke:v4 && npm run build:v4",challenge:["Smokes are synthetic keypoints. They do not replace a live dump. A green CI with a bad live session is still a fail."]},reco_count:{title:"Count (not a reco)",files:["engine/src/v3/baseExercise.js â updateCounter()","engine/src/v3/exercises/<id>.js â counter.trigger_state / from_state"],what:"A public count is a FSM transition: current_state hits trigger_state, previous state is in from_state, min_rep_duration elapsed, and grade ⥠minGrade (if set). Reco strings are not an input to this gate on squat.",how:"Analyst cut: n_counts is not âhow many times the cue firedâ. Join on session + timestamp. A shallow squat still counts (low grade) unless the host passed minGrade.",polarity:"N/A â boolean state machine, not a cue.",formula:[{label:"Face squat count",value:"prev â {descent, bottom} â§ state = ascent â§ Ît ⥠0.8s â§ grade ⥠minGrade"}],units:"Discrete integer. Duration exercises: floor(seconds in hold).",impact:"Native event type: counter { count, form_score: { score, avg_score, grade } }.",challenge:["minGrade silently drops reps: the FSM fired, the user moved, analytics âlostâ a rep. Log skipped_min_grade if you care about funnel vs quality.","V3 squat counts any descent then stand-up. Depth is a grade, not a count filter (unless minGrade)."]},reco_grade:{title:"Grade AâF (form_score)",files:["engine/src/v3/baseExercise.js â calculateFormScore / getFormScoreGrade","engine/src/v3/exercises/squat.js â form_score.depth_only","engine/v4/src/grade.js"],what:"One 0â100 number per counted rep, then bands. V3 has no E (60â69 is D, below 60 is F). V4 adds E at â¥40. Squat production ignores recos when scoring. Other V3 moves subtract 10 per live cue (cap 30).",how:"depth_only: t = clamp((observed â ideal) / (worst â ideal)); score = 100 â round(t \xd7 40). So the worst authored depth still sits around 60 (D), not 0, unless you fall off the map.",polarity:"Higher score = closer to ideal (deeper squat, rounder orbit, bigger breath).",formula:[{label:"V3 squat face",value:"r = min_leg_ratio over descent+bottom\nscore = 100 â 40 \xd7 clamp((r â 0.05) / (0.40 â 0.05))"},{label:"V3 default",value:"100 â anglePenalty[â¤40] â tempoPenalty[â¤30] â 10\xd7n_cues[â¤30]"},{label:"V4",value:"depthScore(v, ideal, worst) = round(100 â 100 \xd7 clamp((vâideal)/(worstâideal)))"}],units:"Score 0â100. Grade ordinal A>B>C>D>E>F (V4). Host minGrade uses the same letters.",impact:"Does not emit a recommendation. Hosts that only chart grade will never see knees_caving.",challenge:["Squat depth_only vs âmost exercises penalise cuesâ is two products. Do not average grades across exercise families without a flag.","V3 depth penalty maxes at 40 points; V4 maps all the way to 0. Same letter bands, different dynamic range."]},reco_cue:{title:"Reco cues (recommendations[])",files:["engine/src/v3/baseExercise.js â checkFeedback()","engine/src/v3/v3Session.ts (emit)","PoseTrackerFront/components/v3/CameraFeedV3.js"],what:"Live coaching strings. Every measuring frame: for each named rule, safeEval(condition | condition_side_view) on the context. TRUE â include message. Feature flag recommendations=true (blocked on plan free). Output is an array of str
1ings, no rule id, no severity on the wire.",how:'Host/SDK join messages with "|" and emit only when that blob changes. Empty list is NOT emitted â last cue can stick on the client.',polarity:"V3 production: condition TRUE means âfault is presentâ â SHOW the cue. Opposite of the old StrAPI MovementHandler (match = good form, cue when match fails).",formula:[{label:"Frame",value:"cues = [ rule.message | rule in feedback, eval(rule.cond) === true ]"},{label:"Emit",value:"if flag && cues.length > 0 && join(cues) !== last â emit recommendations"}],units:"Unordered string set per frame. Duplicates collapsed by message text, not by rule id.",impact:"Does not increment counter. On squat it does not change form_score. On non-depth_only V3 it does (â10 each).",challenge:["No rule_id on the event: you cannot count âknees_caving rateâ from production telemetry, only raw English strings (and copy changes break the series).","Stale cue: good form after a warning never sends []. Session-level â% frames with recoâ is biased unless the host clears locally."]},reco_polarity:{title:"Polarity (read this first)",files:["engine/src/v3/baseExercise.js checkFeedback: if (safeEval(cond)) push","PoseTrackerFront/lib/MovementHandler.js: if (!hasMatch) push â legacy, inverted"],what:"Two reco engines exist in the repo. Production catalog = V3 JS configs, TRUE fires. CMS / old WebView MovementHandler = inverted (parameter âachievedâ hides the text). Custom jumps have no recos.",how:"When an analyst writes âreco firedâ, specify the engine. V3 squat not_deep_enough fires while 0.22 < min_leg_ratio < 0.5 â i.e. a medium squat, not a standing rest and not a full hole.",polarity:"V3: TRUE = show. Legacy StrAPI: TRUE = hide.",formula:[{label:"V3",value:"show if predicate(context) is true"},{label:"Legacy CMS",value:"show if none of the || options match the âgood formâ test"}],units:"Boolean per rule per frame.",impact:"Flipping polarity in a port doubles or zeros cue rates. V4 must pick one and document it.",challenge:["If V4 adds recos, copy V3 polarity (faultâshow) and attach a stable rule id. Do not revive CMS inversion."]},reco_context:{title:"Context the rules actually see",files:["engine/src/v3/baseExercise.js getContext()","engine/src/v3/v3Session.ts V3_FRAME_WIDTH=480 HEIGHT=640"],what:"safeEval sees a flat object: every keypoint as left_knee_x, left_knee_y, ⦠plus computed ratios and angles. Missing joints are undefined; rules must null-check or they never fire (eval throws are swallowed).",how:"x/y are NOT normalised 0â1 inside the predicate. Session denormalises 0â1 poses onto 480\xd7640 so a â20 pxâ knee cave matches the old WebView. A 1080p crop with a smaller person still has ~the same 480-wide skeleton â until they stand further away, then 20 px is a different anatomical error.",polarity:"N/A",formula:[{label:"Denorm",value:"x_px = x_norm \xd7 480 y_px = y_norm \xd7 640"},{label:"leg_ratio",value:"(knee.y â hip.y) / (ankle.y â knee.y)"}],units:"Pixels on a virtual canvas, or unitless ratios, or degrees. Mixed in the same string.",impact:"Pixel rules are camera-distance dependent. Ratio/angle rules are the ones you can trend across users.",challenge:["knees_caving uses 20 px on 480-wide. At 3 m from a laptop that is noise; at a tight crop it is huge. Do not A/B âcue rateâ without a size proxy (shoulder span).","safeEval allows Math.abs and mixed `and` / `&&`. Syntax errors fail closed (no cue)."]},reco_emit:{title:"What the host actually receives",files:["engine/src/v3/v3Session.ts","packages/pose-estimation-react-native/src/events/classicMessage.ts"],what:'Typed event { type: "recommendations", recommendations: string[] }. Classic/native payload often { type, data: string[] }. Dedup key is messages.join("|"). Severity and rule name are stripped.',how:"Free plan: SDK refuses the feature. Flag off: engine may still compute feedback for scoring on non-squat moves, but does not stream cues.",polarity:"N/A",formula:[{label:"Dedup",value:'"Knees cavingâ¦|Go deeper!" vs last blob'}],units:"Event stream, not a per-rep fact table.",impact:"You cannot reconstruct a clean panel without instrumenting rule ids yourself.",challenge:["No âreco clearedâ event. Time-to-correct is missing unless the client timestamps local UI hide.","Two messages in one frame are a set, not a priority queue. Hosts usually show the first string."]},reco_v4gap:{title:"V4: grades yes, recos no",files:["engine/v4/schema/movement.schema.json (no feedback key)","engine/v4/src/session.js (skipHist, form_score, no recommendations emit)"],what:"V4 will not show âKnees caving in!â today. It counts, grades depth/lift, and records skip reasons (zoom, min_grade, identity, view). Those skips are QA/HUD, not user copy, and they usually prevent a count rather than coach a counted rep.",how:"Closed predicates on named metrics. Adding a reco in V4 should be a new catalog field (id, metric, op, value, message, polarity) â not another safeEval string.",polarity:"Undef
1ined. Product decision.",formula:[{label:"Proposed (not shipped)",value:"cue if metric op value, emit { id, message, metric, value, threshold }"}],units:"N/A",impact:"Analysts comparing V3 squat cue rates to V4 lab sessions will see zero recos. That is expected.",challenge:["Should V4 recos affect the grade (old default) or stay orthogonal (squat depth_only)? Pick one per movement in JSON, do not hardcode.","Skip zoom vs cue âstep backâ: same 2D lie, different product. Mixing them in one dashboard will confuse coaches."]},squat_knees:{title:"Rule: squat.knees_caving",files:["engine/src/v3/exercises/squat.js â feedback.knees_caving"],what:"Cue âKnees caving in! Keep knees over ankles.â Face: left or right knee.x is more than 20 px inside the matching ankle.x (knee_x < ankle_x â 20). Profile: left side only.",how:"Image X, not a knee-valgus angle. In a mirrored selfie the anatomical âinsideâ depends on facing. Threshold is 20 virtual pixels.",polarity:"TRUE = cave suspected = SHOW (severity warning).",formula:[{label:"Face",value:"(L_knee_x < L_ankle_x â 20) OR (R_knee_x < R_ankle_x â 20)\nall joints required non-null"},{label:"Profile",value:"L_knee_x < L_ankle_x â 20"}],units:"Pixels on 480\xd7640. Not cm, not % of hip width.",impact:"Squat depth_only: does not change form_score. Still streamed if recommendations=true.",challenge:["Sign of X is view-dependent. A 180\xb0 camera flip or unsynced mirror flag inverts âcavingâ vs âbowingâ.","20 px at standing vs at the hole: the same anatomical valgus projects differently. Expect more cues at depth."]},squat_lean:{title:"Rule: squat.leaning_forward",files:["engine/src/v3/exercises/squat.js â feedback.leaning_forward"],what:"Cue âLeaning too forward! Keep chest up.â Face: shoulder.x > hip.x + 50 on left or right. Profile: left shoulder vs left hip, same +50 px.",how:"A chest-forward lean in profile is a real X offset. On face view, a user slightly turned or a wide stance shifts shoulderâhip X without a hinge. False positives expected on face.",polarity:"TRUE = lean = SHOW (warning).",formula:[{label:"Face",value:"(L_sh_x > L_hip_x + 50) OR (R_sh_x > R_hip_x + 50)"},{label:"Profile",value:"L_sh_x > L_hip_x + 50"}],units:"Pixels on 480\xd7640.",impact:"Orthogonal to squat grade.",challenge:["Face-view lean from X is the weakest of the three squat rules. Candidate to kill or replace with a torso angle in profile-only.","High camera (phone on desk looking up) foreshortens the torso and can fire this while the back is fine."]},squat_depth:{title:"Rule: squat.not_deep_enough",files:["engine/src/v3/exercises/squat.js â feedback.not_deep_enough"],what:"Cue âGo deeper!â. Face: 0.22 < min_leg_ratio < 0.5. Profile: 115\xb0 < angle < 140\xb0. Info severity. Does not fire when standing (â¥0.68) or when already deep (â¤0.22 / â¤115\xb0).",how:"This is a band in the middle of the dip, evaluated every frame â so a user who hits the hole still sees âGo deeper!â on the way down and up through that band. It is not âthis repâs min depth was shallowâ.",polarity:"TRUE = currently in the shallow-mid band = SHOW (info).",formula:[{label:"Face",value:"0.22 < min_leg_ratio < 0.5"},{label:"Profile",value:"115 < angle < 140"}],units:"Unitless ratio / degrees.",impact:"Does not change squat form_score (depth_only uses the min over the whole dip, different thresholds: ideal 0.05, worst 0.40). Cue and grade can disagree.",challenge:["Frame-band vs rep-min: a full-depth rep still generates many âGo deeper!â frames. If you chart cue-seconds you will punish good lifters. Chart min_leg_ratio per rep instead.","Grade worst is 0.40; cue upper is 0.5. A very shallow dip can score F and not show this cue (ratio ⥠0.5). Two different âshallowâ definitions."]},v4_predicates:{title:"V4 phase predicates (heuristics)",files:["engine/v4/src/metrics.js pickPhase","catalog/squat.json views.face.phases","schema/movement.schema.json $defs.predicate"],what:"Counting heuristics, not coaching copy. Each phase is AND/OR of metric ops. Example face idle: both legs exist, min_leg_ratio ⥠0.68, |LâR| ⤠0.22, shins vertical ⥠0.72, knees level ⥠0.55. Eccentric: both ratios < 0.68, hips dropping, not a walk (hip_knee_diff, knees_level).",how:"Metrics are declared once per view then referenced by id. Camera elevation may shift the authored numbers after freeze. Identity.ranges is a second gate on the whole interval.",polarity:"Predicate true â you are in that phase. No user string.",formula:[{label:"Idle (face, authored)",value:"min_leg_ratio ⥠0.68\nleg_ratio_diff ⤠0.22\nmin_shin_vertical ⥠0.72\nknees_level ⥠0.55"}],units:"Ratios and 0â1 alignments, not pixels.",impact:"Wrong thresholds = missed or extra counts. They never appear in recommendations[].",challenge:["V3 squat start used 0.68 + diff
10.18; V4 idle uses 0.68 + diff 0.22 + shin + knees. Same movement, different state machine â do not mix event logs.","No per-predicate telemetry in production dumps unless engineDebug. Hard for an analyst to see which clause failed."]},v4_grade:{title:"V4 grade mapping",files:["engine/v4/src/grade.js","catalog/squat.json grade","catalog/shoulder_*.json grade"],what:"Continuous metric â 0â100 â letter. Squat face: min_leg_ratio ideal 0.12 worst 0.55 (not the V3 0.05/0.40). Orbit/breath: lift or gap vs ideal_face/worst_face. Holds: no grade.",how:"If ideal < worst, smaller is better (ratios). Track only on track_phases so standing frames do not pollute the min.",polarity:"A is best.",formula:[{label:"Bands",value:"Aâ¥90 Bâ¥80 Câ¥70 Dâ¥60 Eâ¥40 else F"},{label:"Squat face",value:"depthScore(min_leg_ratio, 0.12, 0.55)"}],units:"Same letters as V3, different anchors â do not stitch grade histograms without an engine version dimension.",impact:"minGrade can hide F reps from counter. Dump still has skipHist.min_grade.",challenge:["V3 squat depth max penalty 40 pts; V4 can go to 0. A âDâ is not the same physical depth."]},v4_skips:{title:"Skip reasons are not recos",files:["engine/v4/src/session.js bumpSkip","DEBUG_DUMP.md"],what:"zoom, min_grade, min_duration, identity, view, placement, posture, anchor⦠These explain why a motion did not become a public count. They are not coaching. HUD shows a few as posture.hint (Stay the same distance⦠/ Face the camera).",how:"skipHist counts in the session dump. lastSkipReason is the latest. No string localisation pipeline.",polarity:"Skip = do not count.",formula:[{label:"Analyst",value:"counted_rep vs skipped_candidate â two facts, do not OR them into âqualityâ"}],units:"Counts of skip keys in a session.",impact:"Useful for QA funnels (how many walk-ins). Useless as in-app reco until someone authors messages.",challenge:["If product wants reco in V4, map a subset of skips to copy (zoomâdistance) and keep identity rejects silent (user is not doing the move)."]}};function S(e){let{node:a,onOpen:t}=e;return(0,s.jsxs)("button",{type:"button",className:k().archNode,onClick:()=>t(a.id),children:[a.tag?(0,s.jsx)("span",{className:k().archTag,children:a.tag}):null,(0,s.jsx)("span",{className:k().archNodeTitle,children:a.title}),a.example?(0,s.jsx)("span",{className:k().archExample,children:a.example}):null,(0,s.jsx)("span",{className:k().archHint,children:"Open â"})]})}function N(){return(0,s.jsx)("div",{className:k().archArrow,"aria-hidden":!0,children:"â"})}function F(e){let{rows:a}=e;return(0,s.jsx)("div",{className:k().archSchema,children:a.map(e=>(0,s.jsxs)("div",{className:k().archSchemaRow,children:[(0,s.jsx)("code",{children:e.k}),(0,s.jsx)("span",{children:e.v})]},e.k))})}function O(e){var a;let{selectedId:t,onSelect:n}=e,r=t?j[t]:null;return(0,i.useEffect)(()=>{if(!t)return;let e=e=>{"Escape"===e.key&&n(null)};window.addEventListener("keydown",e);let a=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{window.removeEventListener("keydown",e),document.body.style.overflow=a}},[t,n]),(0,s.jsxs)("div",{className:k().arch,children:[(0,s.jsxs)("p",{className:k().sub,style:{maxWidth:"72ch"},children:["Click any box for files, behaviour, and ",(0,s.jsx)("strong",{children:"what to challenge"}),". V4 is the iframe default except unlabeled production squat (V3 FSM). Recommendations are V3-only for now (V4 does not emit them â intentional). Progression 0â100 is on V4. SDK handshake default is V4; unlabeled squat stays on the V3 FSM inside that bundle. Catalog squat: ",(0,s.jsx)("code",{children:"?exercise=squat&engine=v4"})," / ",(0,s.jsx)("code",{children:"engine: 'v4'"})," only."]}),(0,s.jsx)("h2",{className:k().archH,children:"Runtime pipeline"}),(0,s.jsx)("div",{className:k().archRow,children:c.map((e,a)=>(0,s.jsxs)("span",{className:k().archRowItem,children:[a?(0,s.jsx)(N,{}):null,(0,s.jsx)(S,{node:e,onOpen:n})]},e.id))}),(0,s.jsx)("h2",{className:k().archH,children:"How a frame becomes a count"}),(0,s.jsxs)("div",{className:k().archFlow,children:[(0,s.jsxs)("svg",{viewBox:"0 0 920 220",className:k().archSvg,role:"img","aria-label":"Session flow",children:[(0,s.jsx)("defs",{children:(0,s.jsx)("marker",{id:"archHead",markerWidth:"8",markerHeight:"8",refX:"7",refY:"4",orient:"auto",children:(0,s.jsx)("path",{d:"M0,0 L8,4 L0,8 Z",fill:"#7cb4ff"})})}),(0,s.jsx)("rect",{x:"12",y:"78",width:"150",height:"64",rx:"10",fill:"#191c24",stroke:"#2c3140"}),(0,s.jsx)("text",{x:"87",y:"106",textAnchor:"middle",fill:"#eef0f4",fontSize:"13",fontWeight:"600",children:"keypoints"}),(0,s.jsx)("text",{x:"87",y:"124",textAnchor:"middle",fill:"#8b93a7",fontSize:"11",children:"EMA smooth"}),(0,s.jsx)("line",{x1:"162",y1:"110",x2:"214",y2:"110",stroke:"#7cb4ff",strokeWidth:"2",markerEnd:"url(#archHead)"}),(0,s.jsx)("rect",{x:"218",y:"78",width:"150",height:"64",rx:"10",fill:"#191c24",stroke:"#2c3140"}),(0,s.jsx)("text",{x:"293",y:"106",textAnchor:"middle",fill:"#eef0f4",fontSize:"13",fontWeight:"600",children:"placement"}),(0,s.jsx)("text",{x:"293",y:"124",textAnchor:"middle",fill:"#8b93a7",fontSize:"11",children:"box + required"}),(0,s.jsx)("line",{x1:"368",y1:"110",x2:"420",y2:"110",stroke:"#7cb4ff",strokeWidth:"2",markerEnd:"url(#archHead)"}),(0,s.jsx)("rect",{x:"424",y:"78",width:"150",height:"64",rx:"10",fill:"#191c24",stroke:"#2c3140"}
1),(0,s.jsx)("text",{x:"499",y:"106",textAnchor:"middle",fill:"#eef0f4",fontSize:"13",fontWeight:"600",children:"view lock"}),(0,s.jsx)("text",{x:"499",y:"124",textAnchor:"middle",fill:"#8b93a7",fontSize:"11",children:"face / profile"}),(0,s.jsx)("line",{x1:"574",y1:"110",x2:"626",y2:"110",stroke:"#7cb4ff",strokeWidth:"2",markerEnd:"url(#archHead)"}),(0,s.jsx)("rect",{x:"630",y:"40",width:"170",height:"48",rx:"10",fill:"#12141a",stroke:"#2563eb"}),(0,s.jsx)("text",{x:"715",y:"69",textAnchor:"middle",fill:"#eef0f4",fontSize:"12",children:"cycle orbit / breath"}),(0,s.jsx)("rect",{x:"630",y:"98",width:"170",height:"48",rx:"10",fill:"#12141a",stroke:"#2ee6c5"}),(0,s.jsx)("text",{x:"715",y:"127",textAnchor:"middle",fill:"#eef0f4",fontSize:"12",children:"duration hold"}),(0,s.jsx)("rect",{x:"630",y:"156",width:"170",height:"48",rx:"10",fill:"#12141a",stroke:"#fb923c"}),(0,s.jsx)("text",{x:"715",y:"185",textAnchor:"middle",fill:"#eef0f4",fontSize:"12",children:"phases + identity"}),(0,s.jsx)("line",{x1:"800",y1:"64",x2:"848",y2:"110",stroke:"#7cb4ff",strokeWidth:"1.5",markerEnd:"url(#archHead)"}),(0,s.jsx)("line",{x1:"800",y1:"122",x2:"848",y2:"110",stroke:"#7cb4ff",strokeWidth:"1.5",markerEnd:"url(#archHead)"}),(0,s.jsx)("line",{x1:"800",y1:"180",x2:"848",y2:"110",stroke:"#7cb4ff",strokeWidth:"1.5",markerEnd:"url(#archHead)"}),(0,s.jsx)("rect",{x:"852",y:"78",width:"56",height:"64",rx:"10",fill:"#1d4ed8",stroke:"#2563eb"}),(0,s.jsx)("text",{x:"880",y:"115",textAnchor:"middle",fill:"#fff",fontSize:"11",fontWeight:"700",children:"count"})]}),(0,s.jsx)("p",{className:k().archCaption,children:"Exclusive branches after placement. Click the matcher cards below â do not mix a dip machine with an orbit or a hold."})]}),(0,s.jsx)("h2",{className:k().archH,children:"Matchers (pick one per movement)"}),(0,s.jsx)("div",{className:k().archGrid4,children:d.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)("h2",{className:k().archH,children:"Guards (2D lies)"}),(0,s.jsx)("div",{className:k().archGrid4,children:h.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)("h2",{className:k().archH,children:"Authoring + isolation"}),(0,s.jsxs)("div",{className:k().archGrid3,children:[(0,s.jsx)(S,{node:{id:"catalog",title:"Catalog JSON + schema",tag:"Data"},onOpen:n}),(0,s.jsx)(S,{node:{id:"lab",title:"Lab + smokes",tag:"QA"},onOpen:n}),(0,s.jsx)(S,{node:{id:"isolation",title:"Not production",tag:"Hard rule"},onOpen:n})]}),(0,s.jsx)("h2",{className:k().archH,children:"What production sends today (plain language)"}),(0,s.jsxs)("p",{className:k().sub,style:{maxWidth:"78ch"},children:["PoseTracker watches a person on camera (~30 pictures per second). Each picture is a"," ",(0,s.jsx)("strong",{children:"frame"}),". From every frame the production engine (called ",(0,s.jsx)("strong",{children:"V3"})," â the one inside the SDK today) can emit up to ",(0,s.jsx)("strong",{children:"three different products"}),". They share the skeleton, but they answer different questions. Mixing them in one dashboard looks like bugs. This lab page (V4) does ",(0,s.jsx)("strong",{children:"not"})," send coaching text yet."]}),(0,s.jsxs)("div",{className:k().archStory,children:[(0,s.jsxs)("ol",{className:k().archStorySteps,children:[(0,s.jsxs)("li",{children:[(0,s.jsx)("span",{className:k().archStoryN,children:"1"}),(0,s.jsxs)("div",{children:[(0,s.jsx)("strong",{children:"Camera â 17 body points"}),(0,s.jsx)("p",{children:"MoveNet puts dots on nose, shoulders, hips, knees, ankles⦠Only 2D (x, y on the image). No true depth, no centimetres."})]})]}),(0,s.jsxs)("li",{children:[(0,s.jsx)("span",{className:k().archStoryN,children:"2"}),(0,s.jsxs)("div",{children:[(0,s.jsx)("strong",{children:"We turn dots into numbers"}),(0,s.jsxs)("p",{children:["Examples: how bent is the knee (degrees), how âfoldedâ is the leg (a ratio, not pixels), is the knee left of the ankle (pixels on a 480\xd7640 virtual photo). This bag of numbers is the ",(0,s.jsx)("em",{children:"context"})," the rules read."]})]})]}),(0,s.jsxs)("li",{children:[(0,s.jsx)("span",{className:k().archStoryN,children:"3"}
1),(0,s.jsxs)("div",{children:[(0,s.jsx)("strong",{children:"Then three independent answers"}),(0,s.jsx)("p",{children:"Same frame, three outputs. Click a card for the exact formula and what to challenge."})]})]})]}),(0,s.jsxs)("div",{className:k().archOutcomes,children:[(0,s.jsxs)("button",{type:"button",className:k().archOutcome,onClick:()=>n("reco_count"),children:[(0,s.jsx)("span",{className:k().archOutcomeTag,style:{color:"#2ee6c5"},children:"Product A \xb7 count"}),(0,s.jsx)("strong",{children:"Did they finish a rep?"}),(0,s.jsxs)("p",{children:["Squat example: stand â go down â stand back up = ",(0,s.jsx)("em",{children:"+1 squat"}),". This is a yes/no over time, not a quality score. An optional filter (",(0,s.jsx)("code",{children:"minGrade"}),") can refuse to count a very bad rep."]}),(0,s.jsx)("span",{className:k().archHint,children:"Open the spec â"})]}),(0,s.jsxs)("button",{type:"button",className:k().archOutcome,onClick:()=>n("reco_grade"),children:[(0,s.jsx)("span",{className:k().archOutcomeTag,style:{color:"#fb923c"},children:"Product B \xb7 grade"}),(0,s.jsx)("strong",{children:"How good was that rep? (AâF)"}),(0,s.jsxs)("p",{children:["One letter per counted squat, from how deep they went. On squat, the coaching sentences do ",(0,s.jsx)("em",{children:"not"})," change this letter. A shallow squat can still count as 1 with a D or F."]}),(0,s.jsx)("span",{className:k().archHint,children:"Open the spec â"})]}),(0,s.jsxs)("button",{type:"button",className:"".concat(k().archOutcome," ").concat(k().archOutcomeReco),onClick:()=>n("reco_cue"),children:[(0,s.jsx)("span",{className:k().archOutcomeTag,children:"Product C \xb7 reco (this is ârecoâ)"}),(0,s.jsx)("strong",{children:"What should we tell them right now?"}),(0,s.jsxs)("p",{children:["Live coaching, every frame: if knees look caved, send the English string âKnees caving in!â. If the rule is false, we send nothing (and the previous sentence can stick on the phone â a known quirk). This is ",(0,s.jsx)("em",{children:"not"})," the counter and, on squat, not the grade."]}),(0,s.jsx)("span",{className:k().archHint,children:"Open the spec â"})]})]}),(0,s.jsxs)("div",{className:k().archCallout,children:[(0,s.jsx)("strong",{children:"Worked example (one squat)."})," User stands, dips halfway, knees cave, stands up. Production typically sends: count ",(0,s.jsx)("code",{children:"+1"}),", grade maybe ",(0,s.jsx)("code",{children:"D"})," (not deep), and for many frames the reco ",(0,s.jsx)("code",{children:"âKnees caving in!â"})," plus"," ",(0,s.jsx)("code",{children:"âGo deeper!â"})," while they are in the mid-band. Three facts, three columns in a sheet â never âreco rate = qualityâ."]}),(0,s.jsx)("p",{className:k().archCaption,style:{margin:"12px 4px 0"}
1,children:"Rule of thumb: a reco rule is âif this fault is visible on this photo, show this sentenceâ. True â show. (An older CMS engine did the opposite; ignore it unless you are reading Strapi.) Click the cards for polarity, units, and the squat rules."})]}),(0,s.jsx)("div",{className:k().archGrid3,children:m.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)("div",{className:k().archGrid4,style:{marginTop:10},children:u.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)("h2",{className:k().archH,children:"Score formula (how a letter is born)"}),(0,s.jsx)(F,{rows:v}),(0,s.jsx)("h2",{className:k().archH,children:"Grade bands"}),(0,s.jsx)(F,{rows:x}),(0,s.jsx)("h2",{className:k().archH,children:"Rule context (what eval sees)"}),(0,s.jsx)(F,{rows:_}),(0,s.jsx)("h2",{className:k().archH,children:"feedback.<id> shape"}),(0,s.jsx)(F,{rows:y}),(0,s.jsx)("h2",{className:k().archH,children:"Gold example â squat rules (production V3)"}),(0,s.jsx)("p",{className:k().archCaption,style:{marginLeft:0},children:"Each rule is independent. Face vs profile pick different predicates. Pixel rules are not the same unit as the grade (leg ratio)."}),(0,s.jsx)("div",{className:k().archGrid3,children:f.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)("h2",{className:k().archH,children:"V4 heuristics by rule (no reco yet)"}),(0,s.jsxs)("p",{className:k().archCaption,style:{marginLeft:0},children:["Same 17 points, closed metric library, JSON predicates. Use this when you challenge a V4 port â not the V3 ",(0,s.jsx)("code",{children:"safeEval"})," strings."]}),(0,s.jsx)("div",{className:k().archGrid3,children:g.map(e=>(0,s.jsx)(S,{node:e,onOpen:n},e.id))}),(0,s.jsx)(F,{rows:w}),(0,s.jsx)("h2",{className:k().archH,children:"movement.schema.json (shape)"}),(0,s.jsx)(F,{rows:p}),(0,s.jsxs)("p",{className:k().archCaption,children:["Closed metric library â no s
1afeEval. Source of truth is catalog JSON, then"," ",(0,s.jsx)("code",{children:"npm run smoke:v4 && npm run build:v4"}),"."]}),r?(0,s.jsx)("div",{className:k().archModalBack,onClick:e=>{e.target===e.currentTarget&&n(null)},role:"presentation",children:(0,s.jsxs)("div",{className:k().archModal,role:"dialog","aria-modal":"true","aria-labelledby":"arch-modal-title",children:[(0,s.jsxs)("div",{className:k().archModalHead,children:[(0,s.jsx)("h2",{id:"arch-modal-title",children:r.title}),(0,s.jsx)("button",{type:"button",className:k().btn,onClick:()=>n(null),children:"Close"})]}),(0,s.jsxs)("div",{className:k().archModalBody,children:[r.polarity?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h3",{children:"Polarity"}),(0,s.jsx)("p",{children:r.polarity})]}):null,(0,s.jsx)("h3",{children:"What it does"}),(0,s.jsx)("p",{children:r.what}),(0,s.jsx)("h3",{children:"How"}),(0,s.jsx)("p",{children:r.how}),(null===(a=r.formula)||void 0===a?void 0:a.length)?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h3",{children:"Formula / predicate"}),r.formula.map(e=>(0,s.jsxs)("div",{className:k().archFormula,children:[(0,s.jsx)("div",{className:k().archFormulaK,children:e.label}),(0,s.jsx)("pre",{children:e.value})]},e.label))]}):null,r.units?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h3",{children:"Units"}),(0,s.jsx)("p",{children:r.units})]}):null,r.impact?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h3",{children:"Impact on count / grade / event"}),(0,s.jsx)("p",{children:r.impact})]}):null,(0,s.jsx)("h3",{children:"Files"}),(0,s.jsx)("ul",{children:r.files.map(e=>(0,s.jsx)("li",{children:(0,s.jsx)("code",{children:e})},e))}),(0,s.jsx)("h3",{children:"Challenge this"}),(0,s.jsx)("ul",{className:k().archChallenge,children:r.challenge.map(e=>(0,s.jsx)("li",{children:e},e))})]})]})}):null]})}var C=t(29503);let q=[{id:"profile",label:"Profile"},{id:"face",label:"Face"},{id:"back",label:"Back"}],E=[{id:"chair_forward_fold",kind:"seated",view:"face",variant:"filled",label:"Chair fold"},{id:"chair_side_stretch",kind:"upper",view:"face",variant:"filled",label:"Side stretch"},{id:"squat",kind:"full",view:"face",variant:"filled",label:"Squat"},{id:"shoulder_roll",kind:"upper",view:"face",variant:"filled",label:"Shoulder roll"},{id:"shoulder_deep_breath",kind:"upper",view:"face",variant:"filled",label:"Deep breath"}];function P(){return(0,s.jsxs)("div",{className:k().card,style:{marginTop:18},children:[(0,s.jsx)("h2",{style:{margin:0,fontSize:16},children:"Silhouette reference"}),(0,s.jsxs)("p",{style:{margin:"6px 0 0",color:"#8b93a7",fontSize:13,lineHeight:1.45},children:["Same mannequin as the reference SVG for every V4 overlay. Default colors: body"," ",(0,s.jsx)("code",{children:"#000000"}),", skin ",(0,s.jsx)("code",{children:"#E6B39D"}),", stroke ",(0,s.jsx)("code",{children:"#FFFFFF"}),". Outline drops the fills. Crop with ",(0,s.jsx)("code",{children:"kind"}),": upper \xb7 full \xb7 seated."]}),(0,s.jsx)("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, minmax(0, 1fr))",gap:12,marginTop:14},children:q.map(e=>(0,s.jsxs)("div",{children:[(0,s.jsx)("div",{style:{fontSize:11,color:"#8b93a7",letterSpacing:"0.08em",textTransform:"uppercase"},children:e.label}),(0,s.jsxs)("div",{style:{display:"flex",gap:8,marginTop:8},children:[(0,s.jsx)(T,{view:e.id,variant:"filled"}),(0,s.jsx)(T,{view:e.id,variant:"outline"})]})]},e.id))}),(0,s.jsx)("div",{style:{fontSize:11,color:"#8b93a7",letterSpacing:"0.08em",textTransform:"uppercase",marginTop:18},children:"Start pose"}),(0,s.jsx)("p",{style:{margin:"6px 0 0",color:"#8b93a7",fontSize:13},children:"Static overlay pose for each catalog exercise â sit, stand, or upper body, no motion."}),(0,s.jsx)("div",{style:{display:"grid",gridTemplateColumns:"repeat(5, minmax(0, 1fr))",gap:10,marginTop:10},children:E.map(e=>(0,s.jsxs)("div",{children:[(0,s.jsx)("div",{style:{height:200,borderRadius:10,padding:8,background:"#050505",border:"1px solid #2c3140",overflow:"hidden"},"aria-label":e.label,children:(0,s.jsx)(C.ZP,{kind:e.kind,view:e.view,variant:e.variant,preview:e.id})}),(0,s.jsx)("div",{style:{color:"#8b93a7",fontSize:11,textAlign:"center",marginTop:6},children:e.label})]},e.id))})]})}function T(e){let{view:a,variant:t}=e,i="filled"===t;return(0,s.jsx)("div",{style:{flex:1,height:168,borderRadius:10,padding:8,background:i?"#050505":"#161922",border:"1px solid #2c3140"},"aria-label":"".concat(a," ").concat(t),children:(0,s.jsx)(C.ZP,{kind:"full",view:a,variant:t,..."outline"===t?{color:"#FFFFFF",shadowColor:"#000000"}:{}})})}var A=t(22778),V=t(40724);function H(e){let{m:a}=e,t=Object.keys(a.views||{}),i=new Set;
1return t.forEach(e=>Object.keys(a.views[e].phases||{}).forEach(e=>i.add(e))),(0,s.jsxs)("div",{className:k().item,children:[(0,s.jsxs)(o(),{href:"/internal/heuristics/".concat(a.id),className:k().itemMain,children:[(0,s.jsxs)("div",{children:[(0,s.jsx)("div",{style:{fontSize:18,fontWeight:650},children:a.display_name||a.id}),(0,s.jsxs)("div",{style:{color:"#8b93a7",fontSize:13,marginTop:4},children:[a.type," \xb7 ",t.join(" + ")||"locked view"," \xb7 ",i.size," steps"]})]}),(0,s.jsx)("div",{style:{color:"#7cb4ff",fontSize:13},children:"Open â"})]}),(0,s.jsx)(V.Z,{review:a.review})]})}var D=!0;function R(e){let{movements:a}=e,t=(0,n.useRouter)(),r="diagram"===t.query.mode?"diagram":"catalog",o="string"==typeof t.query.node?t.query.node:null,[c,d]=(0,i.useState)(null);(0,i.useEffect)(()=>{t.isReady&&d("diagram"===r?o:null)},[t.isReady,r,o]);let h=e=>{let a={...t.query};"diagram"===e?a.mode="diagram":delete a.mode,"diagram"!==e&&delete a.node,t.replace({pathname:"/internal/heuristics",query:a},void 0,{scroll:!1})},p=a.filter(e=>(0,A.uy)(e)),m=a.filter(e=>!(0,A.uy)(e)),u=(0,i.useCallback)(e=>{d(e);let a={...t.query,mode:"diagram"};e?a.node=e:delete a.node,t.replace({pathname:"/internal/heuristics",query:a},void 0,{scroll:!1})},[t]);return(0,s.jsxs)(l.Z,{title:"V4 movement lab",children:[(0,s.jsx)("p",{className:k().kicker,children:"Internal \xb7 noindex \xb7 V4 catalog"}),(0,s.jsx)("h1",{className:k().title,children:"V4 movements"}),(0,s.jsxs)("p",{className:k().sub,children:["Heuristics for each movement, step by step. Face and profile skeletons, grades, and the JSON the engine actually runs. Allowlisted emails only. SDK: ",(0,s.jsx)("code",{children:"engine: 'v4'"})," at Provider; default remains V3 so ",(0,s.jsx)("code",{children:"startExercise('squat')"})," keeps the production FSM. Iframe default is V4 except unlabeled squat."]}),(0,s.jsxs)("div",{className:k().modeBar,children:[(0,s.jsx)("button",{type:"button",className:"".concat(k().chip," ").concat("catalog"===r?k().chipOn:""),onClick:()=>h("catalog"),children:"Catalog"}),(0,s.jsx)("button",{type:"button",className:"".concat(k().chip," ").concat("diagram"===r?k().chipOn:""),onClick:()=>h("diagram"),children:"Diagram & schema"})]}),"diagram"===r?(0,s.jsx)(O,{selectedId:c,onSelect:u}):(0,s.jsxs)("div",{className:k().list,children:[(0,s.jsxs)("p",{className:k().listSectionLead,children:["Two hard-coded sign-offs per movement â we flip them in this chat, they live on the catalog JSON.",(0,s.jsx)("strong",{children:" Skeleton"})," = authored poses and gates."," ",(0,s.jsx)("strong",{children:"Live"})," = debug camera, guards refined and signed off."]}),(0,s.jsxs)("button",{type:"button",className:k().item,onClick:()=>h("diagram"),children:[(0,s.jsxs)("div",{children:[(0,s.jsx)("div",{style:{fontSize:18,fontWeight:650},children:"Engine V4 \xb7 diagram & schema"}),(0,s.jsx)("div",{style:{color:"#8b93a7",fontSize:13,marginTop:4},children:"Pipeline, matchers, 2D guards, catalog shape â click a box to challenge it"})]}),(0,s.jsx)("div",{style:{color:"#7cb4ff",fontSize:13},children:"Open â"})]}),m.map(e=>(0,s.jsx)(H,{m:e},e.id)),p.length?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("h2",{className:k().listSection,children:"Flexibility"}),(0,s.jsx)("p",{className:k().listSectionLead,children:"13 static Flexifit holds â seconds + peak angle. Not the production Flexifit tool."}),p.map(e=>(0,s.jsx)(H,{m:e},e.id))]}):null]}),"diagram"!==r?(0,s.jsx)(P,{}):null]})}},94054:function(e){e.exports={root:"lab_root__ZCrPu",wrap:"lab_wrap__5FPkk",kicker:"lab_kicker__W_TtC",title:"lab_title__ruSuK",sub:"lab_sub__vtWgQ",row:"lab_row__3EWrz",btn:"lab_btn__36JMb",btnPrimary:"lab_btnPrimary__mtRqq",chip:"lab_chip__SQON5",chipOn:"lab_chipOn__3ny3R",grid:"lab_grid__VjmCR",views:"lab_views__nUMrM",card:"lab_card__UKR3c",list:"lab_list__PhOJu",listSection:"lab_listSection__wx_1f",listSectionLead:"lab_listSectionLead__A5vK0",item:"lab_item__tahE9",itemMain:"lab_itemMain___6Icm",reviewFlags:"lab_reviewFlags__NHSvf",reviewFlagsCompact:"lab_reviewFlagsCompact__iZDEp",reviewOn:"lab_reviewOn__LwNHM",reviewOff:"lab_reviewOff__3xV30",skeleton:"lab_skeleton___4A_b",previewPair:"lab_previewPair__CgrPy",previewPane:"lab_previewPane__0qIlV",previewPairStacked:"lab_previewPairStacked__WsqdK",previewPairFill:"lab_previewPairFill__bPzLt",previewPaneFill:"lab_previewPaneFill__RQBaW",metrics:"lab_metrics___PdG3",on:"lab_on__4n7gK",off:"lab_off__rRw_z",gauge:"lab_gauge__g1gF4",gaugeTrack:"lab_gaugeTrack__CWivc",gaugeSeg:"lab_gaugeSeg__vq9GU",gaugeSegActive:"lab_gaugeSegActive__Dc6HB",phases:"lab_phases__PIKt_",phaseOn:"lab_phaseOn__FKRp3",grades:"lab_grades__vXfiN",grade:"lab_grade__zFU9B",gradeOn:"lab_gradeOn__KoZO0",letter:"lab_letter__gF2xo",when:"lab_when__Gs_70",any:"lab_any__WOBKS",json:"lab_json__xtMYk",legend:"lab_legend__gRKcK",arch:"lab_arch__RRN5M",archStory:"lab_archStory__uNrZl",archStorySteps:"lab_archStorySteps__twzUv",archOutcome:"lab_archOutcome__0i7Vi",archCallout:"lab_archCallout___ghPB",archStoryN:"lab_archStoryN__2Fx5q",archOutcomes:"lab_archOutcomes__qsiL0",archOutcomeReco:"lab_archOutcomeReco__UwwBT",archOutcomeTag:"lab_archOutcomeTag__wmRJu",archH:"lab_archH__wj7DJ",archRow:"lab_archRow__IMacI",archRowItem:"lab_archRowItem__7AdE8",archArrow:"lab_archArrow__w_eVG",archNode:"lab_archNode__QQcRD",archTag:"lab_archTag__wAemo",archNodeTitle:"lab_archNodeTitle__4nMlg",archExample:"lab_archExample__xJ9k3",archHint:"lab_archHint__CdN94",archGrid4:"lab_archGrid4__2m0ub",archGrid3:"lab_archGrid3__s_P4Z",archFlow:"lab_archFlow__3FpXU",archSvg:"lab_archSvg__QTgqj",archCaption:"lab_archCaption__gcxOh",archModalBody:"lab_archModalBody__OxpB0",archSchemaRow:"lab_archSchemaRow__vUf_
1C",archSchema:"lab_archSchema__fpDMk",archModalBack:"lab_archModalBack__22jlI",archModal:"lab_archModal__KZCdC",archModalHead:"lab_archModalHead__F4n77",archChallenge:"lab_archChallenge__t_gtp",archFormula:"lab_archFormula__iHWzC",archFormulaK:"lab_archFormulaK__CD_qD",modeBar:"lab_modeBar__Hx509"}},9008:function(e,a,t){e.exports=t(23867)}},function(e){e.O(0,[1664,2778,9503,2888,9774,179],function(){return e(e.s=85278)}),_N_E=e.O()}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.