--- /mnt/data/nex_v297_rehydrate/NEXT11_V297_REHYDRATED_V286_BASE_ROOT/assets/nexprod/nexprod01-runtime-candidate.mjs 2026-08-19 17:39:58.296671751 +0000 +++ /mnt/data/nex_v300_grip_entry_contract/NEXT11_V300_GRIP_ENTRY_CONTRACT_REHYDRATED_ROOT/assets/nexprod/nexprod01-runtime-candidate.mjs 2026-08-19 18:23:13.928579977 +0000 @@ -21,7 +21,7 @@ sourceDerivedCanonicalGateForSpec } from './nexprod01-core.mjs'; -export const NEXPROD01_RUNTIME_VERSION = 'NEXPROD01_RUNTIME_V297_REHYDRATED_V286_BASE_2026_08_19'; +export const NEXPROD01_RUNTIME_VERSION = 'NEXPROD01_RUNTIME_V300_GRIP_ENTRY_CONTRACT_REHYDRATED_2026_08_19'; export const UNIT_SCALE = 0.010162; export const FOCUS_CASES = [ @@ -2956,6 +2956,84 @@ return {schema:'NEXPROD01_V297_PREFINGER_CHILD_LAYER_GATE',active:false,side,blocked:true,reason:'grip_solver_v1_palm_contract_not_passed',gripPass:!!gripV1?.audit?.pass,gripReason:gripV1?.audit?.reason||null,entryFrameGate:gripV1?.audit?.stages?.v297EntryFrameGate||null,policy:'V297 enforces the V267/V286/V291 contract: fingers are child-layer only after palm/hand/object entry passes. While blocked, there are no finger, thumb, hand, wrist, arm, snap-point, leg, foot or toe writes.'}; } + + + function gripV300CloneActiveGripTargets(out,b){ + const keys=[b.U,b.E,b.H,b.M,b.I,b.P,b.T,b.K,b.F,b.Toe].filter(k=>out?.[k]); + const snap={}; for(const k of keys) snap[k]=clonePoint(out[k]); + return snap; + } + + function gripV300RestoreActiveGripTargets(out,snap){ + for(const [k,v] of Object.entries(snap||{})) out[k]=clonePoint(v); + } + + function gripV300CapsuleSurfaceMetrics(p,limb){ + const required=(Number(limb?.radius)||0)+(Number(limb?.clearance)||0); + const rel=sub(p,limb.axisPoint); + const axial=scaleP(limb.axis,dot(rel,limb.axis)); + const radialVec=sub(rel,axial); + const radialM=len(radialVec); + const signedOutsideM=dot(radialVec,limb.outward); + const outsideScore=dot(safeNorm(radialVec,limb.outward),limb.outward); + const distanceToSurfaceM=Math.abs(radialM-required); + const penetrationM=Math.max(0,required-radialM); + return {radialM:+radialM.toFixed(5),requiredRadialM:+required.toFixed(5),signedOutsideM:+signedOutsideM.toFixed(5),outsideScore:+outsideScore.toFixed(5),distanceToSurfaceM:+distanceToSurfaceM.toFixed(5),penetrationM:+penetrationM.toFixed(5),surfaceOk:distanceToSurfaceM<=0.012 && outsideScore>0.25 && penetrationM<=0.006,sideOk:outsideScore>0.25}; + } + + function gripV300ObjectMeetGate(armShift){ + const meetM=Number(armShift?.legDeltaM)||0; + const maxObjectMeetM=0.080; + return {schema:'NEXPROD01_V300_OBJECT_LEG_MEET_SHIFTED_SURFACE_GATE',active:true,applied:meetM>0,meetM:+meetM.toFixed(5),maxObjectMeetM,pass:meetM<=maxObjectMeetM,legDelta:armShift?.legDelta||{x:0,y:0,z:0},clamped:!!armShift?.clamped,policy:'V300 keeps neutralization in the approach/contact layer. If arm reach cannot place first palm contact, the lower-leg/object target surface may meet the hand up to 80 mm before palm-frame validation. Surface checks must then use the shifted surface, not the original one.'}; + } + + function gripV300ForearmRollSecondPalm(out,rawTargets,b,limb,cfg,opts={}){ + const snap=gripV300CloneActiveGripTargets(out,b); + const pAAnchor=gripV1PalmPoint(out,b,cfg.pointA); + const pBStart=gripV1PalmPoint(out,b,cfg.pointB); + const startFirst=gripV300CapsuleSurfaceMetrics(pAAnchor,limb); + const startSecond=gripV300CapsuleSurfaceMetrics(pBStart,limb); + const forearmAxis=safeNorm(sub(out[b.H],out[b.E]), limb.axis||{x:0,y:-1,z:0}); + const maxRollDeg=125; + let best=null; + for(let deg=-maxRollDeg; deg<=maxRollDeg+1e-9; deg+=1){ + gripV300RestoreActiveGripTargets(out,snap); + gripV1RotateRigidHand(out,b,pAAnchor,forearmAxis,deg,true); + const arm=gripV1SolveElbowForWrist(out,rawTargets,b,out[b.H]); + if(!arm?.ok) continue; + const pA=gripV1PalmPoint(out,b,cfg.pointA), pB=gripV1PalmPoint(out,b,cfg.pointB); + const firstErr=len(sub(pA,pAAnchor)); + const first=gripV300CapsuleSurfaceMetrics(pA,limb), second=gripV300CapsuleSurfaceMetrics(pB,limb); + const basis=gripV1HandBasis(out,b); + const palmFacesObject=dot(basis.z,scaleP(limb.outward,-1)); + const thumbDownProxy=dot(basis.x,{x:0,y:-1,z:0}); + const grossPen=first.penetrationM>0.018 || second.penetrationM>0.018; + const score=(firstErr*5000)+(second.distanceToSurfaceM*120)+(second.sideOk?0:50)+(grossPen?100:0)+(palmFacesObject>0?0:10)+Math.abs(deg)*0.002; + const cand={deg,score,arm,firstErrM:+firstErr.toFixed(5),first,second,palmFacesObject:+palmFacesObject.toFixed(5),thumbDownProxy:+thumbDownProxy.toFixed(5),grossPenetration:grossPen,pointA:roundObj(pA),pointB:roundObj(pB)}; + if(!best || cand.score0); + // Thumb-down is a real contract gate, but V300 does not write thumb bones and does not yet have an approved actual thumb-axis probe. + // Therefore the contact geometry can be ready while finger release remains blocked until thumb-down is visually/probe verified. + const thumbDownVerified=false; + const preFingerReady=!!(palmContactReady && thumbDownVerified); + return {schema:'NEXPROD01_V300_PALM_FRAME_GATE',active:true,pass:preFingerReady,palmContactReady,preFingerReady,status:preFingerReady?'GRIP_ENTRY_CONTRACT_PREFINGER_READY':(palmContactReady?'PALM_CONTACT_READY_THUMB_DOWN_GATE_PENDING':'GRIP_ENTRY_CONTRACT_HOLD_BEFORE_FINGERS'),checks:{handPalmTowardObject:roll?.after?.palmFacesObject!==undefined ? roll.after.palmFacesObject>0 : null,thumbDownVerified,thumbDownGate:'UNVERIFIED_THUMB_AXIS_NO_THUMB_WRITE_IN_V300',thumbDownProxyTelemetry:roll?.after?.thumbDownProxy??null,firstPalmOnShiftedSurface:first?.surfaceOk??null,secondPalmOnShiftedSurface:second?.surfaceOk??null,outsideContact:!!(first?.sideOk && second?.sideOk),noGrossPenetration:roll?.after?.grossPenetration===false,objectMeetWithinLimit:objectMeet?.pass??null},policy:'V300 implements the recovered five-step grip-entry contract. Palm contact may be solved, but the thumb-down gate is explicitly pending because no approved thumb-axis writer/probe is active. Fingers remain blocked.'}; + } + function gripV1PalmContactConfig(side){ // These are not target-world coordinates. They are anatomical hand-surface coordinates // inside the hand branch frame, learned from the right-hand work and mapped to left via @@ -2977,8 +3055,8 @@ const b=gripV1Branch(side); const out={...(targets||{})}; const rule=gripV1ChooseRule(sourceContact,side); - const audit={schema:'NEXPROD01_GRIP_SOLVER_V1',version:'V267',side,applied:false,implemented:true, - policy:'Clean active grip solver. No nearest-point authority, no historical V189/V198/V199 active calls, no new guessed anchors. Uses two inside-palm hand points plus one outside-limb snap point.'}; + const audit={schema:'NEXPROD01_GRIP_SOLVER_V1',version:'V300_GRIP_ENTRY_CONTRACT_REHYDRATED_FROM_V267',side,applied:false,implemented:true, + policy:'V300 rehydrated clean grip solver: V267 palm-contract owner plus explicit five-step grip-entry contract. Neutralization is an approach/contact gate; object/leg meet uses shifted-surface validation; fingers remain blocked until separate visual approval.'}; if(!rule) return {targets:out,audit:{...audit,implemented:false,reason:'no_nonfloor_grip_rule_for_side'}}; const required=[b.U,b.E,b.H,b.M,b.I,b.P,b.F,b.K,b.T]; if(!required.every(k=>out[k])) return {targets:out,audit:{...audit,reason:'missing_required_targets',missing:required.filter(k=>!out[k])}}; @@ -2998,8 +3076,16 @@ const limbAfterSnap=gripV1OutsideLimbPoint(out,rawTargets,b,rule) || limb; const pA1=gripV1PalmPoint(out,b,cfg.pointA), pB1=gripV1PalmPoint(out,b,cfg.pointB); stages.pointASnap={applied:true,pointA0:roundObj(pA0),pointB0:roundObj(pB0),limbTargetBefore:roundObj(limb.target),limbTargetAfter:roundObj(limbAfterSnap.target),shift:roundObj(shift),shiftM:+len(shift).toFixed(5),pointAAfter:roundObj(pA1),pointBAfter:roundObj(pB1),arm:armShift, - policy:'Translate the rigid hand/palm plate so inside-palm point A meets the one outside-limb snap point. If the arm cannot reach, move the target limb contact body by the residual. Do not choose nearest visible point.'}; + policy:'Step 1-2 of V300 contract: arm reach / approach places first inside-palm contact on the outside limb snap. If the arm cannot reach, object/leg-meet is the only allowed compensation; fingers/hand-only patches are not owners.'}; Object.assign(limb, limbAfterSnap); + stages.v300ObjectMeetGate=gripV300ObjectMeetGate(armShift); + if(!stages.v300ObjectMeetGate.pass) return {targets:out,audit:{...audit,reason:'object_meet_exceeds_v300_limit',stages,nextPhase:'stop_before_fingers_object_meet_limit'}}; + stages.v300ForearmAxisSecondPalmRoll=gripV300ForearmRollSecondPalm(out,rawTargets,b,limb,cfg,{caseId:opts.caseId,phase:opts.phase}); + stages.v300PalmFrameGate=gripV300PalmFrameGate(stages); + if(stages.v300PalmFrameGate.palmContactReady){ + const afterV300=v93Basis(out,side); + return {targets:out,audit:{...audit,applied:true,pass:false,palmContactReady:!!stages.v300PalmFrameGate.palmContactReady,preFingerReady:false,status:stages.v300PalmFrameGate.status,ruleType:rule.type||null,targetLimb:rule.targetLimb||null,targetSide:'outside_shifted_surface',palmPoints:{pointA:cfg.pointA,pointB:cfg.pointB},limb:{axisA:limb.axisA,axisB:limb.axisB,axisPoint:roundObj(limb.axisPoint),outside:roundObj(limb.outward),target:roundObj(limb.target),radiusM:+limb.radius.toFixed(5),clearanceM:+limb.clearance.toFixed(5),t:+limb.t.toFixed(5)},beforeBasis:null,afterAngles:afterV300?.angles||null,stages,nextPhase:'thumb_down_gate_pending_finger_child_layer_blocked',notes:['V300 palm contact is solved, but thumb-down gate is pending; pass remains false so v68/V200 finger layer stays blocked','first palm contact was held while wrist/forearm-axis roll searched the second palm point','shifted-surface gate is evaluated after object/leg meet']}}; + } // Align the line between the two inside-palm points to the target limb axis. There is still one // limb point; point B is validated by collision/contact after rotation, not assigned a second target.