Match native rounded-pixel grab rays and retry missing startup PID

This commit is contained in:
NINI
2026-10-10 23:55:54 +09:00
parent d4128d713d
commit bb32fdae9d
4 changed files with 144 additions and 12 deletions
+5 -3
View File
@@ -8,10 +8,11 @@ $checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generi
$result=[ordered]@{milestone='Content';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'} $result=[ordered]@{milestone='Content';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'}
$result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'} $result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'}
$networkBefore=$null; $musicBefore=$null $networkBefore=$null; $musicBefore=$null
function Adb([string[]]$Arguments) { function Adb([string[]]$Arguments,[switch]$AllowMissingPid) {
$started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE $started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
$commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")}) $commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() $missingPid=$AllowMissingPid -and $code-eq1 -and $Arguments.Count-eq3 -and $Arguments[0]-eq'shell' -and $Arguments[1]-eq'pidof' -and $Arguments[2]-eq$package -and [string]::IsNullOrWhiteSpace(($output -join "`n"))
if ($code -ne 0 -and !$missingPid) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim()
} }
function Check([string]$Id,[bool]$Passed,[string]$Actual) { function Check([string]$Id,[bool]$Passed,[string]$Actual) {
$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" } $checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" }
@@ -49,7 +50,7 @@ function StartHome([string]$Name) {
$ready=$false; $pauseRequested=$false $ready=$false; $pauseRequested=$false
Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Drawing
for($attempt=0;$attempt-lt30;$attempt++) { for($attempt=0;$attempt-lt30;$attempt++) {
$pidText=Adb @('shell','pidof',$package) $pidText=Adb @('shell','pidof',$package) -AllowMissingPid
$saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi')) $saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi'))
$validSave=$false $validSave=$false
try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { } try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { }
@@ -84,6 +85,7 @@ function StartHome([string]$Name) {
$pauseRequested=$true $pauseRequested=$true
} }
if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break} if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break}
if($pidText-notmatch'^\d+'){Adb @('shell','am','start','-W','-n',$activity)|Out-Null}
} }
$observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8 $observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8
Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review' Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review'
+19 -9
View File
@@ -9,10 +9,11 @@ $checks=[Collections.Generic.List[object]]::new(); $commands=[Collections.Generi
$result=[ordered]@{milestone='Final';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'} $result=[ordered]@{milestone='Final';sourceCommit=(& git rev-parse HEAD);sourceDirty=[bool](& git status --porcelain);serial=$Serial;apkSha256=(Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash;status='failed'}
$result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'} $result.rendererProjection=@{fieldOfView=62;fixedWorldPivot=@(0,.65,0);rawCamera='Saved Board.camera JSON remains canonical';viewport='Landscape wm size max/min; remaining UI and physical touch helper assume sole 2400x1080 AVD'}
$networkBefore=$null; $musicBefore=$null $networkBefore=$null; $musicBefore=$null
function Adb([string[]]$Arguments) { function Adb([string[]]$Arguments,[switch]$AllowMissingPid) {
$started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE $started=[DateTime]::UtcNow.ToString('o'); $output=& $adb -s $Serial @Arguments 2>&1; $code=$LASTEXITCODE
$commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")}) $commands.Add(@{startedUtc=$started;finishedUtc=[DateTime]::UtcNow.ToString('o');command=('adb -s '+$Serial+' '+($Arguments -join ' '));exitCode=$code;output=($output -join "`n")})
if ($code -ne 0) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim() $missingPid=$AllowMissingPid -and $code-eq1 -and $Arguments.Count-eq3 -and $Arguments[0]-eq'shell' -and $Arguments[1]-eq'pidof' -and $Arguments[2]-eq$package -and [string]::IsNullOrWhiteSpace(($output -join "`n"))
if ($code -ne 0 -and !$missingPid) { throw "ADB exit $code : $($Arguments -join ' ')" }; return ($output -join "`n").Trim()
} }
function Check([string]$Id,[bool]$Passed,[string]$Actual) { function Check([string]$Id,[bool]$Passed,[string]$Actual) {
$checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" } $checks.Add(@{id=$Id;passed=$Passed;actual=$Actual}); if (!$Passed) { throw "Content check failed: $Id ($Actual)" }
@@ -111,18 +112,26 @@ function BallClear($Board,[string]$Id) {
$camera=$Board.camera $camera=$Board.camera
$rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w) $rotation=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w)
$origin=[Numerics.Vector3]::new($camera.target.x,($camera.target.y+.65),$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation) $origin=[Numerics.Vector3]::new($camera.target.x,($camera.target.y+.65),$camera.target.z)+[Numerics.Vector3]::Transform([Numerics.Vector3]::new(0,0,-$camera.distance),$rotation)
$size=Adb @('shell','wm','size'); if ($size -notmatch '(\d+)x(\d+)') { throw "Unknown device resolution: $size" }
$width=[Math]::Max([int]$Matches[1],[int]$Matches[2]); $height=[Math]::Min([int]$Matches[1],[int]$Matches[2])
$scale=[Math]::Tan(31*[Math]::PI/180)/($height/2)
# Match the rounded native DOWN pixel, including its actual target entry distance.
$direction=[Numerics.Vector3]::Normalize([Numerics.Vector3]::Transform([Numerics.Vector3]::new(($point[0]-$width/2)*$scale,($height/2-$point[1])*$scale,1),$rotation))
$target=@($Board.balls|Where-Object id -eq $Id)[0].position $target=@($Board.balls|Where-Object id -eq $Id)[0].position
$direction=[Numerics.Vector3]::new($target.x,$target.y,$target.z)-$origin $delta=[Numerics.Vector3]::new($target.x,$target.y,$target.z)-$origin
$targetHit=$direction.Length()-.5 $along=[Numerics.Vector3]::Dot($direction,$delta)
$direction=[Numerics.Vector3]::Normalize($direction) $perpendicularSquared=($delta-$direction*$along).LengthSquared()
if($perpendicularSquared-gt.25){return $false}
$targetHit=$along-[Math]::Sqrt([Math]::Max(0.0,.25-$perpendicularSquared))
if($targetHit-lt0){return $false}
foreach($ball in $Board.balls){ foreach($ball in $Board.balls){
if($ball.id-eq$Id){continue} if($ball.id-eq$Id){continue}
$delta=[Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin $delta=[Numerics.Vector3]::new($ball.position.x,$ball.position.y,$ball.position.z)-$origin
$along=[Numerics.Vector3]::Dot($direction,$delta) $along=[Numerics.Vector3]::Dot($direction,$delta)
$perpendicularSquared=$delta.LengthSquared()-$along*$along $perpendicularSquared=($delta-$direction*$along).LengthSquared()
if($perpendicularSquared-le.25){ if($perpendicularSquared-le.25){
$near=$along-[Math]::Sqrt([Math]::Max(0,.25-$perpendicularSquared)) $near=$along-[Math]::Sqrt([Math]::Max(0.0,.25-$perpendicularSquared))
if($near-ge0-and$near-lt$targetHit){return $false} if($near-ge0-and$near-le$targetHit){return $false}
} }
} }
return $true return $true
@@ -160,7 +169,7 @@ function StartHome([string]$Name) {
$ready=$false; $pauseRequested=$false $ready=$false; $pauseRequested=$false
Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Drawing
for($attempt=0;$attempt-lt30;$attempt++) { for($attempt=0;$attempt-lt30;$attempt++) {
$pidText=Adb @('shell','pidof',$package) $pidText=Adb @('shell','pidof',$package) -AllowMissingPid
$saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi')) $saveText=Adb @('shell',('if test -s '+$remote+'; then cat '+$remote+'; fi'))
$validSave=$false $validSave=$false
try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { } try { $state=$saveText|ConvertFrom-Json -ErrorAction Stop; $validSave=$state.schemaVersion-eq1 -and $null-ne$state.completedStageIds -and $null-ne$state.puzzleStates } catch { }
@@ -195,6 +204,7 @@ function StartHome([string]$Name) {
$pauseRequested=$true $pauseRequested=$true
} }
if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break} if($pidText-match'^\d+'-and$validSave-and$ui){$ready=$true;$saveText|Set-Content (Join-Path $EvidenceDir "$Name-ready-save.json") -Encoding utf8;break}
if($pidText-notmatch'^\d+'){Adb @('shell','am','start','-W','-n',$activity)|Out-Null}
} }
$observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8 $observations.ToArray()|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir "$Name-readiness.json") -Encoding utf8
Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review' Check "$Name-home-ready" $ready 'Actual app PID, landscape home colors/button/title and app save JSON; bounded30 observations, black/Unity splash excluded, PNG retained for direct review'
+50
View File
@@ -0,0 +1,50 @@
param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Before,[Parameter(Mandatory)][string]$After,[Parameter(Mandatory)][string]$Baseline,[string]$Source=(Join-Path $PSScriptRoot 'final-common.ps1'))
$ErrorActionPreference='Stop'
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
$checks=[Collections.Generic.List[object]]::new()
function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})}
function Definition($Path,$Name){$ast=[Management.Automation.Language.Parser]::ParseFile($Path,[ref]$null,[ref]$null);return $ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true).Extent.Text}
foreach($name in @('BallPoint','BallClear','WorldSafe','Same','AssertNativeTranslation','AssertNativeMerge')){
$expected=(Definition $Source $name).Replace("function $name","function global:$name")
. ([scriptblock]::Create($expected))
Record "$name-production-body-identity" ((Get-Command $name).ScriptBlock.Ast.Extent.Text-ceq$expected) $Source
}
function HashText($Text){return [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($Text)))}
foreach($name in @('NativePointer','NativeMoveAndHold','AssertNativeTranslation','AssertNativeMerge')){Record "$name-contract-unchanged" ((Definition $Source $name)-ceq(Definition $Baseline $name)) @{baseline=$Baseline;beforeSha256=(HashText (Definition $Baseline $name));afterSha256=(HashText (Definition $Source $name))}}
$script:resolution='Physical size: 1080x2400';$script:adbCalls=0
function Adb($Arguments){$script:adbCalls++;return $script:resolution}
function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}}
function Ball($Id,$X,$Y,$Z){return [pscustomobject]@{id=$Id;position=[pscustomobject]@{x=$X;y=$Y;z=$Z}}}
function Board($Balls){return [pscustomobject]@{balls=@($Balls);groups=@([pscustomobject]@{id='same';ballIds=@($Balls.id)});camera=[pscustomobject]@{target=[pscustomobject]@{x=0;y=.6;z=0};rotation=[pscustomobject]@{x=0;y=0;z=0;w=1};distance=8}}}
try{
$beforeState=Get-Content -LiteralPath $Before -Raw|ConvertFrom-Json;$afterState=Get-Content -LiteralPath $After -Raw|ConvertFrom-Json
$b=@($beforeState.puzzleStates|Where-Object stageId -eq 16)[0];$a=@($afterState.puzzleStates|Where-Object stageId -eq 16)[0]
Record 'integer-max-overload-reproduced-and-double-preserves-radius' ([Math]::Max(0,.25-.09)-eq0-and[Math]::Abs([Math]::Max(0.0,.25-.09)-.16)-lt1e-12) @{integerMax=[Math]::Max(0,.25-.09);doubleMax=[Math]::Max(0.0,.25-.09)}
$point=BallPoint $b 's16-b02'
Record 'captured-wrong-source-pixel-rejected' (!(BallClear $b 's16-b02')-and$point[0]-eq733-and$point[1]-eq346) @{point=$point;requested='s16-b02';actualMoved='s16-b05'}
$errorText=$null;try{AssertNativeMerge 'captured' $b $a 's16-b02'}catch{$errorText=$_.Exception.Message}
Record 'captured-wrong-source-strict-merge-still-rejected' ($errorText-match'requested-source-actually-moved') $errorText
foreach($case in @(
@{id='single';balls=@((Ball 'target' 0 1.25 0));clear=$true},
@{id='front';balls=@((Ball 'target' 0 1.25 0),(Ball 'front' 0 1.25 -1));clear=$false},
@{id='behind';balls=@((Ball 'target' 0 1.25 0),(Ball 'behind' 0 1.25 1));clear=$true},
# Target projects to1200.45 and rounds1200. Center ray misses the left sphere,
# while the actual pixel ray intersects it. Geometry expectations are analytic.
@{id='rounded-pixel-front';balls=@((Ball 'target' .004 1.25 0),(Ball 'front' -.497 1.25 -1));clear=$false},
@{id='rounded-pixel-outside';balls=@((Ball 'target' .004 1.25 0),(Ball 'front' -.501 1.25 -1));clear=$true},
@{id='exact-tangent-front';balls=@((Ball 'target' 0 1.25 0),(Ball 'front' -.5 1.25 -1));clear=$false},
@{id='equal-entry-ambiguous';balls=@((Ball 'target' 0 1.25 0),(Ball 'same' 0 1.25 0));clear=$false},
@{id='behind-camera';balls=@((Ball 'target' 0 1.25 -9));clear=$false},
@{id='unsafe-ui';balls=@((Ball 'target' 20 1.25 0));clear=$false}
)){$board=Board $case.balls;$snapshot=$board|ConvertTo-Json -Depth 12;$actual=BallClear $board 'target';Record $case.id ($actual-eq$case.clear-and($board|ConvertTo-Json -Depth 12)-ceq$snapshot) @{expected=$case.clear;actual=$actual}}
foreach($kind in @('missing','duplicate')){
$board=Board @((Ball 'target' 0 1.25 0));if($kind-eq'duplicate'){$board.balls+=Ball 'target' 0 1.25 1}
$script:adbCalls=0;$rejected=$false;try{BallClear $board $(if($kind-eq'missing'){'absent'}else{'target'})|Out-Null}catch{$rejected=$_.Exception.Message-match'exactly one existing ball ID'}
Record "$kind-ID-rejected-before-projection-input" ($rejected-and$script:adbCalls-eq0) $kind
}
$script:resolution='unavailable';$rejected=$false;try{BallClear (Board @((Ball 'target' 0 1.25 0))) 'target'|Out-Null}catch{$rejected=$_.Exception.Message-match'Unknown device resolution'}
Record 'unknown-resolution-rejected' $rejected $script:resolution
}catch{Record 'production-evaluation' $false $_.Exception.Message}
$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0
@{passed=$passed;scope='Offline actual function bodies and captured wrong-source save. Analytic edge cases are counterfactual; no native raycast or product pass claim.';source=$Source;sourceSha256=(Get-FileHash $Source).Hash;before=$Before;beforeSha256=(Get-FileHash $Before).Hash;after=$After;afterSha256=(Get-FileHash $After).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 12|Set-Content (Join-Path $EvidenceDir 'pixel-ray-result.json')
if(!$passed){exit 1};exit 0
+70
View File
@@ -0,0 +1,70 @@
param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$HomePng,[string]$Source=(Join-Path $PSScriptRoot 'final-common.ps1'))
$ErrorActionPreference='Stop'
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
$EvidenceDir=(Resolve-Path $EvidenceDir).Path
$checks=[Collections.Generic.List[object]]::new();$commands=[Collections.Generic.List[object]]::new()
function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})}
foreach($name in @('Adb','StartHome')){
$ast=[Management.Automation.Language.Parser]::ParseFile($Source,[ref]$null,[ref]$null)
$definition=$ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true)
$expected=$definition.Extent.Text.Replace("function $name","function global:$name")
. ([scriptblock]::Create($expected))
Record "$name-production-body-identity" ((Get-Command $name).ScriptBlock.Ast.Extent.Text-ceq$expected) $Source
}
$adb='OfflineNative';$Serial='offline';$package='com.magnetismheroes.game';$activity="$package/activity";$remote='/offline/progress.json'
function OfflineNative {
$text=$args-join' ';$global:LASTEXITCODE=0
if($script:mode-eq'exit-control'){$global:LASTEXITCODE=$script:mockExit;return $script:mockOutput}
if($text-match'pidof'){
$script:pidCalls++
if($script:mode-eq'other-error'){$global:LASTEXITCODE=2;return ''}
if($script:mode-eq'absent'-or($script:mode-eq'eventually-ready'-and$script:pidCalls-le2)){$global:LASTEXITCODE=1;return ''}
if($script:mode-eq'empty-zero') {return ''}
if($script:mode-eq'malformed-pid') {return 'unavailable'}
return '77'
}
if($text-match'am start'){$script:starts++;if($script:mode-eq'start-error'){$global:LASTEXITCODE=1;return ''};return 'Status: ok; delivered-to-top-instance'}
if($text-match'cat '){if($script:mode-eq'save-error'){$global:LASTEXITCODE=1;return ''};if($script:mode-eq'invalid-save'){return '{}'};return '{"schemaVersion":1,"completedStageIds":[],"puzzleStates":[]}'}
return ''
}
function Step($Name,$Arguments=@(),$Delay=500){
$destination=Join-Path $EvidenceDir "$Name.png"
if($script:mode-eq'black-ui'){$image=[Drawing.Bitmap]::new(2400,1080);try{$image.Save($destination)}finally{$image.Dispose()}}else{Copy-Item -LiteralPath $HomePng -Destination $destination -Force}
}
function Start-Sleep {}
function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}}
try{
$script:mode='exit-control';$script:mockOutput='';$script:mockExit=1
$rejected=$false;try{Adb @('shell','pidof',$package)|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'}
Record 'default-adb-exit1-still-rejected' ($rejected-and$commands[-1].exitCode-eq1) $commands[-1]
$errorText=$null;try{$empty=Adb @('shell','pidof',$package) -AllowMissingPid}catch{$errorText=$_.Exception.Message}
Record 'allowed-pid-absence-retains-raw-exit1' (!$errorText-and$empty-eq''-and$commands[-1].exitCode-eq1) $commands[-1]
$script:mockExit=2;$rejected=$false;try{Adb @('shell','pidof',$package) -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 2'}
Record 'other-pid-error-still-rejected' ($rejected-and$commands[-1].exitCode-eq2) $commands[-1]
$script:mockExit=1
foreach($arguments in @(@('shell','wm','size'),@('shell','pidof','another.package'),@('shell','am','start','-W','-n',$activity))){
$rejected=$false;try{Adb $arguments -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'}
Record 'missing-pid-opt-in-cannot-hide-other-command-exit1' ($rejected-and$commands[-1].exitCode-eq1) $commands[-1]
}
$script:mockOutput='pidof: error';$rejected=$false;try{Adb @('shell','pidof',$package) -AllowMissingPid|Out-Null}catch{$rejected=$_.Exception.Message-match'ADB exit 1'}
Record 'pidof-exit1-with-error-output-still-rejected' ($rejected-and$commands[-1].output-eq'pidof: error') $commands[-1]
foreach($mode in @('eventually-ready','absent','empty-zero','malformed-pid','black-ui','invalid-save','other-error','start-error','save-error')){
$script:mode=$mode;$script:pidCalls=0;$script:starts=0;$commands.Clear();$errorText=$null
try{StartHome "offline-$mode"}catch{$errorText=$_.Exception.Message}
$observationPath=Join-Path $EvidenceDir "offline-$mode-readiness.json"
$observations=if(Test-Path $observationPath){@(Get-Content $observationPath -Raw|ConvertFrom-Json)}else{@()}
if($mode-eq'eventually-ready'){
Record 'absent-pid-relaunches-and-requires-real-home-save-pid' (!$errorText-and$observations.Count-eq3-and$script:starts-eq3-and$observations[0].pid-eq''-and$observations[-1].pid-eq'77'-and$observations[-1].validAppSave-and$observations[-1].readyUi) @{observations=$observations;starts=$script:starts}
Record 'absent-pid-raw-exits-remain-in-command-evidence' (@($commands|Where-Object {$_.command-match'pidof'-and$_.exitCode-eq1}).Count-eq2) $commands.ToArray()
}elseif($mode-eq'other-error'){
Record 'startup-does-not-swallow-other-adb-error' ($errorText-match'ADB exit 2'-and$script:pidCalls-eq1) $errorText
}elseif($mode-eq'start-error'-or$mode-eq'save-error'){
Record 'startup-does-not-swallow-other-exit1' ($errorText-match'ADB exit 1') @{kind=$mode;error=$errorText}
}else{
Record 'unready-home-cannot-pass-thirty-bound' ($errorText-match'home-ready'-and$script:pidCalls-eq30-and$observations.Count-eq30) @{kind=$mode;error=$errorText;observations=$observations.Count;starts=$script:starts}
}
}
}catch{Record 'production-evaluation' $false $_.Exception.Message}
$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0
@{passed=$passed;scope='Offline StartHome/Adb production bodies with leaf process/PNG mocks. Recorded Home PNG is real; process absence, invalid Save and black screen sequences are counterfactual, not native startup proof.';source=$Source;sourceSha256=(Get-FileHash $Source).Hash;homePng=$HomePng;homePngSha256=(Get-FileHash $HomePng).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 14|Set-Content (Join-Path $EvidenceDir 'startup-result.json')
if(!$passed){exit 1};exit 0