From 71bf8fab57d5c62a0b804f8c913fe1004d2bd27c Mon Sep 17 00:00:00 2001 From: NINI Date: Sun, 11 Oct 2026 01:11:17 +0900 Subject: [PATCH] Keep native merge depth windows by reframing unsafe endpoints --- tools/android/exercise-final-progression.ps1 | 12 ++- tools/android/test-final-merge-framing.ps1 | 80 ++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tools/android/test-final-merge-framing.ps1 diff --git a/tools/android/exercise-final-progression.ps1 b/tools/android/exercise-final-progression.ps1 index 7aeb2c4..de61fbd 100644 --- a/tools/android/exercise-final-progression.ps1 +++ b/tools/android/exercise-final-progression.ps1 @@ -25,14 +25,19 @@ function Orbit([string]$Name,$CurrentBoard,[switch]$Pitch,[int]$YawPixels=120,[i throw 'No safe blank orbit start in the actual board view.' } function ZoomOut([int]$Stage,$CurrentBoard) { + $before=Save "stage-$Stage-zoom-out-before" + Check "stage-$Stage-current-pinch-projection" (Same $CurrentBoard (Board $before $Stage)) 'The pinch uses the exact current board and manual pose' $helper=Join-Path $PSScriptRoot 'send-composition-touch.ps1' foreach($contacts in @(@(30,400,999,1024),@(30,400,999,0),@(900,350,1,1024,1500,350,2,1024),@(1050,350,1,1024,1350,350,2,1024),@(1050,350,1,0,1350,350,2,0))){ & pwsh -NoProfile -Command "& '$helper' -Contacts $($contacts-join',')" $code=$LASTEXITCODE;$commands.Add(@{command='native sendTouch pinch-out';contacts=$contacts;exitCode=$code;utc=[DateTime]::UtcNow.ToString('o')});if($code-ne0){throw 'Native pinch failed'};Start-Sleep -Milliseconds 120 } Step "stage-$Stage-zoom-out" @() 300 - $after=Board (Save "stage-$Stage-zoom-out-state") $Stage + $state=Save "stage-$Stage-zoom-out-state";$after=Board $state $Stage Check "stage-$Stage-pinch-preserves-board" ((Same $CurrentBoard.balls $after.balls)-and(Same $CurrentBoard.groups $after.groups)-and(Same $CurrentBoard.countedBallIds $after.countedBallIds)-and$after.camera.distance-gt$CurrentBoard.camera.distance) 'Actual two-finger zoom-out; no ball, membership or count edits' + $expected=$state|ConvertTo-Json -Depth 20|ConvertFrom-Json + (Board $expected $Stage).camera=$CurrentBoard.camera + Check "stage-$Stage-pinch-preserves-whole-save" (Same $before $expected) 'Only this board manual camera may change; every independent record remains exact' return $after } function RevealBall([int]$Stage,[string]$Id,[string]$Name) { @@ -99,6 +104,11 @@ function MergeReal([int]$Stage,[string]$Name,[int]$SourceCount=0,[int]$TargetCou $targetPoint=BallPoint $board $candidate.tid if(WorldSafe @(($targetPoint[0]+$offset),$targetPoint[1])){$pair=$candidate;break} } + if(!$pair-and$projection-eq0-and$board.camera.distance-lt15){ + # Keep a visited contact depth plane when its endpoint is outside the UI-safe view. + $needsFrame=@($pairs|Where-Object {$point=BallPoint $board $_.tid;$_.depth-lt.9-and(BallClear $board $_.sid)-and!(WorldSafe @(($point[0]+$offset),$point[1]))}).Count-gt0 + if($needsFrame){$board=ZoomOut $Stage $board;continue} + } if($pair-or$attempt-ne0-or$projection-ne0){break} $routed=LiftMergeRoute $Stage "$Name-upward-route" $board $SourceCount $TargetCount if(!$routed){break} diff --git a/tools/android/test-final-merge-framing.ps1 b/tools/android/test-final-merge-framing.ps1 new file mode 100644 index 0000000..c584285 --- /dev/null +++ b/tools/android/test-final-merge-framing.ps1 @@ -0,0 +1,80 @@ +param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$FixtureDir,[string]$ProgressionSource=(Join-Path $PSScriptRoot 'exercise-final-progression.ps1')) +$ErrorActionPreference='Stop' +New-Item -ItemType Directory $EvidenceDir -Force|Out-Null +$checks=[Collections.Generic.List[object]]::new();$script:inputs=[Collections.Generic.List[object]]::new() +function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})} +function Load($Path,$Name){ + $ast=[Management.Automation.Language.Parser]::ParseFile($Path,[ref]$null,[ref]$null) + $f=$ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true) + if(!$f){throw "Missing function $Name"} + $expected=$f.Extent.Text.Replace("function $Name","function global:$Name").Replace('$PSScriptRoot',"'$PSScriptRoot'") + . ([scriptblock]::Create($expected)) + Record "$Name-production-body" ((Get-Command $Name).ScriptBlock.Ast.Extent.Text-ceq$expected) $Path +} +function Clone($Value){return $Value|ConvertTo-Json -Depth 30|ConvertFrom-Json} +function Adb {return 'Physical size: 1080x2400'} +function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}} +function Start-Sleep {} +# External input leaves are inert. Geometry below is counterfactual framing, never native proof. +function pwsh {$script:inputs.Add('pinch-leaf');$global:LASTEXITCODE=0} +function NativePointer {} +function Step {} +function global:LiftMergeRoute {return $null} +$commands=[Collections.Generic.List[object]]::new() +$script:current=$null;$script:zoomed=$false;$script:badZoom=$false;$script:caseState=$null +function Save([string]$Name){ + if($Name-match'-before-(\d+)$'){$script:current=if($script:caseState){Clone $script:caseState}else{Get-Content (Join-Path $FixtureDir "stage-16-merge-4-before-$($Matches[1]).json") -Raw|ConvertFrom-Json};$script:zoomed=$false} + if($Name-match'zoom-out-state'){ + $script:current=Clone $script:current + (Board $script:current 16).camera.distance=15 + if($script:badZoom){$script:current.completedStageIds+=16} + $script:zoomed=$true + } + return Clone $script:current +} +function global:NativeMoveAndHold($Name,$Board,$Id,$End){ + if(!(BallClear $Board $Id)-or!(WorldSafe $End)){throw 'unsafe-counterfactual-input'} + $script:inputs.Add(@{id=$Id;end=$End;distance=$Board.camera.distance;zoomed=$script:zoomed}) + throw 'safe-framed-drop-selected' +} +try{ + foreach($n in @('BallPoint','WorldSafe','BallClear','Same')){Load (Join-Path $PSScriptRoot 'final-common.ps1') $n} + foreach($n in @('Board','Orbit','ZoomOut','MergeReal')){Load $ProgressionSource $n} + $errorText=$null;try{MergeReal 16 'offline-merge16'|Out-Null}catch{$errorText=$_.Exception.Message} + $drops=@($script:inputs|Where-Object {$_-isnot[string]}) + Record 'actual-depth-window-reframes-and-reprojects-within-original-bound' ($errorText-eq'safe-framed-drop-selected'-and$drops.Count-eq1-and$drops[0].zoomed-and$drops[0].distance-eq15) @{error=$errorText;inputs=$script:inputs.ToArray();scope='Offline decision/input-leaf replay only; no merge success inferred'} + # A native pinch must not mask an independent completion record change. + $script:badZoom=$true;$script:inputs.Clear();$errorText=$null + try{MergeReal 16 'bad-zoom'|Out-Null}catch{$errorText=$_.Exception.Message} + Record 'pinch-independent-record-corruption-rejected-before-ball-input' ($errorText-match'pinch-preserves-whole-save'-and@($script:inputs|Where-Object {$_-isnot[string]}).Count-eq0) $errorText + $script:badZoom=$false + foreach($kind in @('already-safe-pair','no-eligible-depth','no-source-first-hit','maximum-distance','no-eligible-source-size','offset-only-unsafe')){ + $index=if($kind-eq'no-eligible-depth'){0}elseif($kind-eq'maximum-distance'){11}else{2} + $script:caseState=Get-Content (Join-Path $FixtureDir "stage-16-merge-4-before-$index.json") -Raw|ConvertFrom-Json + $b=Board $script:caseState 16;$sourceCount=1;$targetCount=5 + switch($kind){ + 'already-safe-pair'{$b.camera.distance=15} + 'no-source-first-hit'{foreach($ball in $b.balls){$ball.position=Clone $b.balls[0].position}} + 'maximum-distance'{foreach($ball in $b.balls){$ball.position.x*=15/8;$ball.position.y=1.25+($ball.position.y-1.25)*15/8;$ball.position.z*=15/8};$b.camera.distance=15} + 'no-eligible-source-size'{$sourceCount=2} + 'offset-only-unsafe'{ + $b.camera.rotation=[pscustomobject]@{x=0;y=0;z=0;w=1} + foreach($ball in $b.balls){$ball.position=[pscustomobject]@{x=-8;y=2.5;z=0}} + @($b.balls|Where-Object id -eq 's16-b04')[0].position=[pscustomobject]@{x=0;y=1.25;z=0} + @($b.balls|Where-Object id -eq 's16-b05')[0].position=[pscustomobject]@{x=-5.94;y=2.5;z=0} + $point=BallPoint $b 's16-b05' + Record 'offset-control-center-safe-endpoint-unsafe' ((WorldSafe $point)-and!(WorldSafe @(($point[0]-35),$point[1]))) $point + } + } + $script:inputs.Clear();$errorText=$null;try{MergeReal 16 "control-$kind" $sourceCount $targetCount|Out-Null}catch{$errorText=$_.Exception.Message} + $pinches=@($script:inputs|Where-Object {$_-is[string]}).Count + $drops=@($script:inputs|Where-Object {$_-isnot[string]}) + if($kind-eq'offset-only-unsafe'){$ok=$pinches-eq5-and$errorText-eq'safe-framed-drop-selected'-and$drops.Count-eq1-and$drops[0].zoomed} + elseif($kind-eq'already-safe-pair'){$ok=$pinches-eq0-and$errorText-eq'safe-framed-drop-selected'-and$drops.Count-eq1-and!$drops[0].zoomed} + else{$ok=$pinches-eq0-and$drops.Count-eq0-and$errorText-match'Native merge bound reached'} + Record "branch-control-$kind" $ok @{pinchLeaves=$pinches;drops=$drops;error=$errorText;scope='Explicitly counterfactual boundary control'} + } +}catch{Record 'evaluation' $false $_.Exception.Message} +$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0 +@{passed=$passed;scope='Offline frozen failed execution replay; mocked native leaves and explicitly counterfactual distance15; not Android completion';progressionSha256=(Get-FileHash $ProgressionSource).Hash;fixtures=@(0..15|ForEach-Object {$p=Join-Path $FixtureDir "stage-16-merge-4-before-$_.json";@{path=$p;sha256=(Get-FileHash $p).Hash}});checks=$checks.ToArray()}|ConvertTo-Json -Depth 12|Set-Content (Join-Path $EvidenceDir 'merge-framing-result.json') +if(!$passed){exit 1};exit 0