param( [string]$BaseUrl = "http://127.0.0.1:17317", [int[]]$ProbeIndices = @(1, 3), [int[]]$Limits = @(2, 5, 10, 20), [int]$PollIntervalSeconds = 2, [int]$TimeoutSeconds = 600, [string]$OutputRoot = (Join-Path (Resolve-Path -LiteralPath ".").Path "outputs\live-soak"), [switch]$GoalRun, [ValidateSet("current", "ik-traineddata", "compare")] [string]$ScanEngine = "current", [switch]$BenchmarkOcr, [int]$BenchmarkLimit = 5, [ValidateSet("current", "ik-traineddata", "compare")] [string]$BenchmarkEngine = "compare", [ValidateSet("fast", "full")] [string]$BenchmarkProfile = "fast", [switch]$SkipSmartCapture, [switch]$SaveFullReviewSamples, [switch]$AllowStaleBuild, [string]$ExpectedAppSignature, [switch]$ContinueAfterBlocked, [switch]$SelfTestAssessment ) $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($ExpectedAppSignature)) { $mainPath = Join-Path (Resolve-Path -LiteralPath ".").Path "electron\main.ts" if (Test-Path -LiteralPath $mainPath) { $mainSource = Get-Content -LiteralPath $mainPath -Raw $match = [regex]::Match($mainSource, 'APP_RUNTIME_SIGNATURE\s*=\s*"([^"]+)"') if ($match.Success) { $ExpectedAppSignature = $match.Groups[1].Value } } } function ConvertTo-SafeFilePart([string]$Value) { $safe = $Value -replace "[^A-Za-z0-9._-]+", "-" $safe = $safe.Trim("-") if ($safe.Length -eq 0) { return "item" } if ($safe.Length -gt 80) { return $safe.Substring(0, 80) } return $safe } function Invoke-DevJson([string]$Path) { $uri = if ($Path.StartsWith("http")) { $Path } else { "$BaseUrl$Path" } Invoke-RestMethod -Method Get -Uri $uri -TimeoutSec 60 } function Save-Json([string]$Name, [object]$Payload) { $path = Join-Path $RunDir "$(ConvertTo-SafeFilePart $Name).json" $Payload | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath $path -Encoding UTF8 return $path } function Assert-CurrentAppBuild([object]$HealthPayload) { if ($AllowStaleBuild) { return } if ($null -eq $HealthPayload.appBuild) { throw "Dev endpoint is stale: /health has no appBuild signature. Close the old elevated Genshin Artifact Assistant/Electron instance, restart with npm run dev:admin, then rerun this script. Use -AllowStaleBuild only for debugging old instances." } if ([string]::IsNullOrWhiteSpace([string]$HealthPayload.appBuild.signature)) { throw "Dev endpoint is stale: appBuild.signature is empty. Restart the elevated app before live scanner timing." } if (-not [string]::IsNullOrWhiteSpace($ExpectedAppSignature) -and [string]$HealthPayload.appBuild.signature -ne $ExpectedAppSignature) { throw "Dev endpoint is stale: appBuild.signature='$($HealthPayload.appBuild.signature)' but current source expects '$ExpectedAppSignature'. Close the old Electron instance, restart with npm run dev:admin, then rerun this script. Use -AllowStaleBuild only for debugging old instances." } } function Get-ScannerStatus { Invoke-DevJson "/scanner/status" } function Test-ProbeSucceeded([object]$ProbePayload) { if ($ProbePayload.ok) { return $true } if ($ProbePayload.changed) { return $true } return $false } function Get-CompletedScanSummary([object]$StatusPayload, [int]$Limit) { if ($null -ne $StatusPayload.status.summary) { return $StatusPayload.status.summary } $reviewStatus = [string]$StatusPayload.status.reviewStatus $status = "done" if ($reviewStatus -match "blockiert") { $status = "blocked" } elseif ($reviewStatus -match "gestoppt") { $status = "stopped" } $stats = $StatusPayload.status.stats return [pscustomobject]@{ mode = "Automatischer Scan" status = $status clicked = [int]$stats.clicked attempted = [int]$stats.attempted verified = [int]$stats.verified parsed = [int]$stats.parsed stored = [int]$stats.stored review = [int]$stats.review duplicates = [int]$stats.duplicates misses = [int]$stats.misses pages = [int]$stats.pages targetCount = $Limit gridLabel = $reviewStatus } } function Convert-ScanTimingSummary([object]$StatusPayload, [int]$Limit, [string]$Engine) { $summary = Get-CompletedScanSummary $StatusPayload $Limit $stats = $StatusPayload.status.stats return [pscustomobject]@{ engine = $Engine limit = $Limit status = $summary.status clicked = $summary.clicked attempted = $summary.attempted verified = $summary.verified parsed = $summary.parsed stored = $summary.stored review = $summary.review duplicates = $summary.duplicates misses = $summary.misses pages = $summary.pages elapsedMs = [int]$stats.elapsedMs activeScanMs = [int]$stats.activeScanMs writeFlushMs = [int]$stats.writeFlushMs averageMsPerParsed = [int]$stats.averageMsPerParsed activeAverageMsPerParsed = [int]$stats.activeAverageMsPerParsed artifactsPerMinute = [double]$stats.artifactsPerMinute activeArtifactsPerMinute = [double]$stats.activeArtifactsPerMinute projectedMsFor100 = [int]$stats.projectedMsFor100 activeProjectedMsFor100 = [int]$stats.activeProjectedMsFor100 averageCaptureMs = [int]$stats.averageCaptureMs averageOcrMs = [int]$stats.averageOcrMs captureP50Ms = [int]$stats.captureP50Ms captureP90Ms = [int]$stats.captureP90Ms ocrP50Ms = [int]$stats.ocrP50Ms ocrP90Ms = [int]$stats.ocrP90Ms cardReadyMs = [int]$stats.cardReadyMs cardReadyCount = [int]$stats.cardReadyCount averageCardReadyMs = [int]$stats.averageCardReadyMs scrollReadyMs = [int]$stats.scrollReadyMs scrollReadyCount = [int]$stats.scrollReadyCount averageScrollReadyMs = [int]$stats.averageScrollReadyMs captureMs = [int]$stats.captureMs ocrMs = [int]$stats.ocrMs reviewStatus = [string]$StatusPayload.status.reviewStatus } } function Write-ScanTimingLine([object]$Timing) { Write-Host ("engine={0} limit={1} status={2} parsed={3} review={4} miss={5} elapsed={6}ms active={7}ms flush={8}ms avg={9}ms activeAvg={10}ms captureAvg={11}ms captureP50={12}ms captureP90={13}ms ocrAvg={14}ms ocrP50={15}ms ocrP90={16}ms cardReadyAvg={17}ms scrollReadyAvg={18}ms ppm={19} activePpm={20} projected100={21}ms activeProjected100={22}ms" -f ` $Timing.engine, $Timing.limit, $Timing.status, $Timing.parsed, $Timing.review, $Timing.misses, $Timing.elapsedMs, $Timing.activeScanMs, $Timing.writeFlushMs, $Timing.averageMsPerParsed, $Timing.activeAverageMsPerParsed, $Timing.averageCaptureMs, $Timing.captureP50Ms, $Timing.captureP90Ms, $Timing.averageOcrMs, $Timing.ocrP50Ms, $Timing.ocrP90Ms, $Timing.averageCardReadyMs, $Timing.averageScrollReadyMs, $Timing.artifactsPerMinute, $Timing.activeArtifactsPerMinute, $Timing.projectedMsFor100, $Timing.activeProjectedMsFor100) } function Get-TimingBottleneck([object]$Timing) { $parts = @( [pscustomobject]@{ name = "ocr"; value = [int]$Timing.averageOcrMs }, [pscustomobject]@{ name = "capture"; value = [int]$Timing.averageCaptureMs }, [pscustomobject]@{ name = "card-ready"; value = [int]$Timing.averageCardReadyMs }, [pscustomobject]@{ name = "scroll-ready"; value = [int]$Timing.averageScrollReadyMs } ) | Sort-Object -Property value -Descending if ($parts.Count -eq 0 -or $parts[0].value -le 0) { return "unknown" } return $parts[0].name } function Get-TimingRecommendation([object]$Timing) { $bottleneck = Get-TimingBottleneck $Timing switch ($bottleneck) { "ocr" { return "OCR dominates; compare engine, crop count, worker pool, and parser-derived fields first." } "capture" { return "Capture dominates; reduce payloads/crops and avoid full-frame or Base64 work in the hot loop." } "card-ready" { return "Card-ready dominates; tune detail fingerprint polling against IK's 200ms item wait." } "scroll-ready" { return "Scroll-ready dominates; tune page fingerprint polling against IK's 100ms fast-scroll wait." } default { return "No dominant timing component detected; inspect misses/review/duplicates and raw diagnostic events." } } } function Get-ScanQuality([object]$Timing) { $parsed = [int]$Timing.parsed $misses = [int]$Timing.misses $review = [int]$Timing.review $target = [int]$Timing.limit $processed = [Math]::Max(1, $parsed + $misses) $parseCoverage = if ($target -gt 0) { [Math]::Round($parsed / $target, 4) } else { 0 } $missRate = [Math]::Round($misses / $processed, 4) $reviewRate = if ($parsed -gt 0) { [Math]::Round($review / $parsed, 4) } else { 1 } $statusOk = [string]$Timing.status -eq "done" $coverageOk = $parsed -ge $target $missOk = $missRate -le 0.02 $reviewOk = $reviewRate -le 0.15 $qualityPenalty = [Math]::Round(($missRate * 1000000) + ($reviewRate * 250000) + ((1 - $parseCoverage) * 1000000), 0) $qualityDecision = if (-not $statusOk) { "not-qualified: scan did not finish cleanly" } elseif (-not $coverageOk) { "not-qualified: parsed fewer artifacts than requested" } elseif (-not $missOk) { "not-qualified: miss rate above 2%" } elseif (-not $reviewOk) { "not-qualified: review rate above 15%" } else { "qualified" } return [pscustomobject]@{ parseCoverage = $parseCoverage missRate = $missRate reviewRate = $reviewRate qualityDecision = $qualityDecision qualityPenalty = $qualityPenalty qualified = ($statusOk -and $coverageOk -and $missOk -and $reviewOk) } } function New-PerformanceAssessment([object[]]$Summaries) { $limitReports = @() foreach ($group in ($Summaries | Group-Object -Property limit | Sort-Object { [int]$_.Name })) { $entries = @($group.Group | ForEach-Object { $quality = Get-ScanQuality $_ $_ | Add-Member -NotePropertyName parseCoverage -NotePropertyValue $quality.parseCoverage -Force $_ | Add-Member -NotePropertyName missRate -NotePropertyValue $quality.missRate -Force $_ | Add-Member -NotePropertyName reviewRate -NotePropertyValue $quality.reviewRate -Force $_ | Add-Member -NotePropertyName qualityDecision -NotePropertyValue $quality.qualityDecision -Force $_ | Add-Member -NotePropertyName qualityPenalty -NotePropertyValue $quality.qualityPenalty -Force $_ | Add-Member -NotePropertyName qualified -NotePropertyValue $quality.qualified -Force $_ } | Sort-Object -Property @{ Expression = "qualified"; Descending = $true }, qualityPenalty, activeAverageMsPerParsed, averageMsPerParsed) if ($entries.Count -eq 0) { continue } $winner = $entries[0] $engineReports = @() foreach ($entry in $entries) { $engineReports += [pscustomobject]@{ engine = $entry.engine status = $entry.status parsed = $entry.parsed review = $entry.review misses = $entry.misses parseCoverage = $entry.parseCoverage missRate = $entry.missRate reviewRate = $entry.reviewRate qualified = $entry.qualified qualityDecision = $entry.qualityDecision qualityPenalty = $entry.qualityPenalty activeAverageMsPerParsed = $entry.activeAverageMsPerParsed activeProjectedMsFor100 = $entry.activeProjectedMsFor100 averageOcrMs = $entry.averageOcrMs averageCaptureMs = $entry.averageCaptureMs averageCardReadyMs = $entry.averageCardReadyMs averageScrollReadyMs = $entry.averageScrollReadyMs bottleneck = Get-TimingBottleneck $entry recommendation = Get-TimingRecommendation $entry } } $limitReports += [pscustomobject]@{ limit = [int]$group.Name winnerEngine = $winner.engine winnerQualified = $winner.qualified winnerMissRate = $winner.missRate winnerReviewRate = $winner.reviewRate winnerActiveAverageMsPerParsed = $winner.activeAverageMsPerParsed winnerActiveProjectedMsFor100 = $winner.activeProjectedMsFor100 engines = $engineReports } } $goal100 = @($limitReports | Where-Object { $_.limit -eq 100 } | Select-Object -First 1) return [pscustomobject]@{ createdAt = (Get-Date).ToString("o") goal100 = if ($goal100.Count -gt 0) { $goal100[0] } else { $null } limits = $limitReports } } function Write-PerformanceAssessment([object]$Assessment) { foreach ($limit in @($Assessment.limits)) { Write-Host ("assessment limit={0}: winner={1} qualified={2} missRate={3:P1} reviewRate={4:P1} activeAvg={5}ms projected100={6}ms" -f ` $limit.limit, $limit.winnerEngine, $limit.winnerQualified, $limit.winnerMissRate, $limit.winnerReviewRate, $limit.winnerActiveAverageMsPerParsed, $limit.winnerActiveProjectedMsFor100) foreach ($engine in @($limit.engines)) { Write-Host (" {0}: qualified={1}, missRate={2:P1}, reviewRate={3:P1}, bottleneck={4}, activeAvg={5}ms, ocr={6}ms, capture={7}ms, cardReady={8}ms, scrollReady={9}ms, decision={10}" -f ` $engine.engine, $engine.qualified, $engine.missRate, $engine.reviewRate, $engine.bottleneck, $engine.activeAverageMsPerParsed, $engine.averageOcrMs, $engine.averageCaptureMs, $engine.averageCardReadyMs, $engine.averageScrollReadyMs, $engine.qualityDecision) } } } function Invoke-AssessmentSelfTest { $synthetic = @( [pscustomobject]@{ engine = "current" limit = 100 status = "done" parsed = 100 review = 22 misses = 0 activeAverageMsPerParsed = 700 averageMsPerParsed = 760 activeProjectedMsFor100 = 70000 averageOcrMs = 260 averageCaptureMs = 160 averageCardReadyMs = 190 averageScrollReadyMs = 60 }, [pscustomobject]@{ engine = "ik-traineddata" limit = 100 status = "done" parsed = 100 review = 4 misses = 0 activeAverageMsPerParsed = 820 averageMsPerParsed = 870 activeProjectedMsFor100 = 82000 averageOcrMs = 210 averageCaptureMs = 170 averageCardReadyMs = 205 averageScrollReadyMs = 80 }, [pscustomobject]@{ engine = "broken-fast" limit = 45 status = "done" parsed = 45 review = 0 misses = 3 activeAverageMsPerParsed = 300 averageMsPerParsed = 330 activeProjectedMsFor100 = 30000 averageOcrMs = 100 averageCaptureMs = 90 averageCardReadyMs = 40 averageScrollReadyMs = 20 }, [pscustomobject]@{ engine = "current" limit = 45 status = "done" parsed = 45 review = 1 misses = 0 activeAverageMsPerParsed = 600 averageMsPerParsed = 650 activeProjectedMsFor100 = 60000 averageOcrMs = 230 averageCaptureMs = 130 averageCardReadyMs = 160 averageScrollReadyMs = 50 } ) $assessment = New-PerformanceAssessment -Summaries $synthetic $goal100 = $assessment.goal100 $limit45 = @($assessment.limits | Where-Object { $_.limit -eq 45 } | Select-Object -First 1)[0] if ($goal100.winnerEngine -ne "ik-traineddata") { throw "Assessment self-test failed: expected ik-traineddata to win limit=100, got '$($goal100.winnerEngine)'." } if (-not $goal100.winnerQualified) { throw "Assessment self-test failed: expected limit=100 winner to be qualified." } if ($limit45.winnerEngine -ne "current") { throw "Assessment self-test failed: expected current to win limit=45, got '$($limit45.winnerEngine)'." } if (@($goal100.engines | Where-Object { $_.engine -eq "current" })[0].qualityDecision -ne "not-qualified: review rate above 15%") { throw "Assessment self-test failed: expected high-review current run to be rejected." } if (@($limit45.engines | Where-Object { $_.engine -eq "broken-fast" })[0].qualityDecision -ne "not-qualified: miss rate above 2%") { throw "Assessment self-test failed: expected broken-fast run to be rejected for miss rate." } Write-PerformanceAssessment $assessment Write-Host "Assessment self-test passed." -ForegroundColor Green return $assessment } function Invoke-OcrBenchmark { param( [int]$Limit, [string]$Engine, [string]$Profile ) Write-Host "Warming current OCR workers..." $warmCurrent = Invoke-DevJson "/scanner/ocr/warmup?engine=current" Save-Json "benchmark-warmup-current" $warmCurrent | Out-Null if ($Engine -eq "compare" -or $Engine -eq "ik-traineddata") { Write-Host "Warming IK-traineddata OCR workers..." $warmIk = Invoke-DevJson "/scanner/ocr/warmup?engine=ik-traineddata" Save-Json "benchmark-warmup-ik-traineddata" $warmIk | Out-Null } Write-Host "Running OCR benchmark engine=$Engine profile=$Profile limit=$Limit" $benchmark = Invoke-DevJson "/scanner/benchmark-ocr?limit=$Limit&engine=$Engine&profile=$Profile" Save-Json "benchmark-ocr-$Engine-$Profile-limit-$Limit" $benchmark | Out-Null if ($benchmark.summary.mode -eq "compare") { foreach ($engineSummary in @($benchmark.summary.engines)) { Write-Host ("benchmark {0}: avg={1}ms ocrAvg={2}ms p50={3}ms p90={4}ms projected100={5}ms skipped={6} pool={7}" -f ` $engineSummary.engine, $engineSummary.averageMs, $engineSummary.averageOcrMs, $engineSummary.p50Ms, $engineSummary.p90Ms, $engineSummary.projectedMs.artifacts100, $engineSummary.skippedOcrCaptures, $engineSummary.workerPoolSize) } } else { $summary = $benchmark.summary Write-Host ("benchmark {0}: avg={1}ms ocrAvg={2}ms p50={3}ms p90={4}ms projected100={5}ms skipped={6} pool={7}" -f ` $summary.engine, $summary.averageMs, $summary.averageOcrMs, $summary.p50Ms, $summary.p90Ms, $summary.projectedMs.artifacts100, $summary.skippedOcrCaptures, $summary.workerPoolSize) } return $benchmark } function Convert-ReviewSamplesSummary([object]$ReviewPayload) { $samples = @() foreach ($record in @($ReviewPayload.samples)) { $parsed = $record.sample.parsed $capture = $record.sample.capture $ocr = @() foreach ($entry in @($capture.ocr)) { $ocr += [pscustomobject]@{ id = $entry.id label = $entry.label text = $entry.text confidence = $entry.confidence } } $samples += [pscustomobject]@{ savedAt = $record.savedAt reason = $record.sample.reason parsed = [pscustomobject]@{ name = $parsed.name slot = $parsed.slot level = $parsed.level mainStat = $parsed.mainStat mainValue = $parsed.mainValue setName = $parsed.setName equipped = $parsed.equipped confidence = $parsed.confidence notes = $parsed.notes fields = $parsed.fields } capture = [pscustomobject]@{ name = $capture.name width = $capture.width height = $capture.height captureTarget = $capture.captureTarget capturedAt = $capture.capturedAt inventoryGrid = if ($capture.inventoryGrid) { [pscustomobject]@{ rows = $capture.inventoryGrid.rows cols = $capture.inventoryGrid.cols confidence = $capture.inventoryGrid.confidence source = $capture.inventoryGrid.source } } else { $null } inventoryCount = $capture.inventoryCount locked = $capture.locked ocr = $ocr } } } return [pscustomobject]@{ ok = $ReviewPayload.ok total = $ReviewPayload.total path = $ReviewPayload.path samples = $samples } } function Wait-ForScannerIdle([int]$Limit, [string]$Engine) { $startedAt = Get-Date $pollIndex = 0 $lastStatus = $null while ($true) { Start-Sleep -Seconds $PollIntervalSeconds $pollIndex += 1 $statusPayload = Get-ScannerStatus $lastStatus = $statusPayload Save-Json "scan-$Engine-limit-$Limit-poll-$pollIndex" $statusPayload | Out-Null $running = [bool]$statusPayload.status.running if (-not $running) { return $statusPayload } $elapsed = ((Get-Date) - $startedAt).TotalSeconds if ($elapsed -gt $TimeoutSeconds) { $stop = Invoke-DevJson "/scanner/stop" Save-Json "scan-$Engine-limit-$Limit-timeout-stop" $stop | Out-Null throw "Scanner timed out after $TimeoutSeconds seconds for limit=$Limit engine=$Engine. Stop command was sent." } } } if ($SelfTestAssessment) { Invoke-AssessmentSelfTest | Out-Null exit 0 } $stamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" $RunDir = Join-Path $OutputRoot $stamp New-Item -ItemType Directory -Force -Path $RunDir | Out-Null if ($GoalRun) { $Limits = @(2, 5, 20, 45, 100) $BenchmarkOcr = $true } $ScanEngines = if ($ScanEngine -eq "compare") { @("current", "ik-traineddata") } else { @($ScanEngine) } $RunSummaries = @() $transcriptPath = Join-Path $RunDir "transcript.log" try { Start-Transcript -Path $transcriptPath -Append | Out-Null } catch { Write-Warning "Could not start transcript: $($_.Exception.Message)" } try { Write-Host "Live soak output: $RunDir" Write-Host "Checking dev control server at $BaseUrl" $health = Invoke-DevJson "/health" Save-Json "00-health" $health | Out-Null Assert-CurrentAppBuild $health if ($health.appBuild) { Write-Host "App build: signature=$($health.appBuild.signature), pid=$($health.appBuild.pid), startedAt=$($health.appBuild.startedAt), ocrWorkers=$($health.appBuild.expectedOcrWorkerPoolSize)" if ($health.appBuild.expectedOcrWorkerPoolSize -lt 4) { Write-Host "WARNUNG: OCR worker pool is below 4. This is valid for constrained debugging, but not ideal for IK-speed comparison." -ForegroundColor Yellow } } $initialStatus = Get-ScannerStatus Save-Json "01-status-before" $initialStatus | Out-Null if ($initialStatus.status.runtimeInfo) { $runtime = $initialStatus.status.runtimeInfo Write-Host "Runtime: elevated=$($runtime.isElevated), genshinFound=$($runtime.genshinFound), target=$($runtime.targetProcess)" } else { Write-Host "Runtime info is not published yet. Open the app scanner view before running broad scans." -ForegroundColor Yellow } if ($BenchmarkOcr) { Invoke-OcrBenchmark -Limit $BenchmarkLimit -Engine $BenchmarkEngine -Profile $BenchmarkProfile | Out-Null } if (-not $SkipSmartCapture) { Write-Host "Capturing smart preflight snapshot without OCR..." $capture = Invoke-DevJson "/capture/smart?skipOcr=1" Save-Json "02-smart-capture-skip-ocr" $capture | Out-Null } foreach ($index in $ProbeIndices) { Write-Host "Running probe click index=$index" $probe = Invoke-DevJson "/automation/probe-click?index=$index" Save-Json "probe-index-$index" $probe | Out-Null if (-not (Test-ProbeSucceeded $probe)) { Write-Host "Probe index=$index did not fully pass. Review the saved JSON before broader scans." -ForegroundColor Yellow if (-not $ContinueAfterBlocked) { throw "Stopping after failed probe index=$index. Re-run with -ContinueAfterBlocked only if you deliberately want to continue." } } elseif (-not $probe.ok -and $probe.changed) { Write-Host "Probe index=$index changed the detail panel even though helper cursor/click readback was not clean; continuing." -ForegroundColor Yellow } } foreach ($engine in $ScanEngines) { foreach ($limit in $Limits) { if ($limit -lt 1) { continue } Write-Host "Starting bounded scanner run limit=$limit engine=$engine" $start = Invoke-DevJson "/scanner/start?limit=$limit&engine=$engine" Save-Json "scan-$engine-limit-$limit-start" $start | Out-Null $finalStatus = Wait-ForScannerIdle -Limit $limit -Engine $engine Save-Json "scan-$engine-limit-$limit-final" $finalStatus | Out-Null $summary = Get-CompletedScanSummary $finalStatus $limit Write-Host "engine=$engine limit=$limit summary: status=$($summary.status), attempted=$($summary.attempted), verified=$($summary.verified), parsed=$($summary.parsed), stored=$($summary.stored), review=$($summary.review), misses=$($summary.misses), pages=$($summary.pages)" $timing = Convert-ScanTimingSummary $finalStatus $limit $engine $RunSummaries += $timing Write-ScanTimingLine $timing if (($summary.status -eq "blocked" -or $summary.status -eq "stopped") -and -not $ContinueAfterBlocked) { throw "Stopping after scan status '$($summary.status)' for limit=$limit engine=$engine." } } } $review = Invoke-DevJson "/review/samples?limit=30" Save-Json "review-samples-tail-summary" (Convert-ReviewSamplesSummary $review) | Out-Null if ($SaveFullReviewSamples) { Save-Json "review-samples-tail-full" $review | Out-Null } $afterStatus = Get-ScannerStatus Save-Json "99-status-after" $afterStatus | Out-Null Save-Json "scan-run-summary" ([pscustomobject]@{ createdAt = (Get-Date).ToString("o") goalRun = [bool]$GoalRun scanEngine = $ScanEngine scanEngines = $ScanEngines benchmarkOcr = [bool]$BenchmarkOcr limits = $Limits summaries = $RunSummaries }) | Out-Null if ($RunSummaries.Count -gt 0) { $assessment = New-PerformanceAssessment -Summaries $RunSummaries Save-Json "scan-performance-assessment" $assessment | Out-Null Write-PerformanceAssessment $assessment $csvPath = Join-Path $RunDir "scan-run-summary.csv" $RunSummaries | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding UTF8 Write-Host "Timing summary CSV: $csvPath" } Write-Host "Live soak complete. Output: $RunDir" -ForegroundColor Green } catch { Write-Host "Live soak failed: $($_.Exception.Message)" -ForegroundColor Red Write-Host "Output so far: $RunDir" -ForegroundColor Yellow exit 1 } finally { try { Stop-Transcript | Out-Null } catch { } }