param( [Parameter(Mandatory = $true)] [string]$Path ) $ErrorActionPreference = 'Stop' $resolved = Resolve-Path -LiteralPath $Path $item = Get-Item -LiteralPath $resolved $file = if ($item.PSIsContainer) { Get-ChildItem -LiteralPath $item.FullName -Filter '*.jsonl' -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } else { $item } if (-not $file) { throw "No JSONL file found: $Path" } $records = New-Object System.Collections.Generic.List[object] $lineNumber = 0 Get-Content -LiteralPath $file.FullName | ForEach-Object { $lineNumber++ try { $records.Add(($_ | ConvertFrom-Json)) } catch { throw "Invalid JSONL at line $lineNumber in $($file.FullName): $($_.Exception.Message)" } } $sequenceStrict = $true for ($index = 1; $index -lt $records.Count; $index++) { if ([uint64]$records[$index].sequence -le [uint64]$records[$index - 1].sequence) { $sequenceStrict = $false break } } $immQueries = @($records | Where-Object recordType -eq 'imm_query') $successfulResults = @($immQueries | Where-Object immQueryStatus -eq 'success') $echoSummaries = @($records | Where-Object recordType -eq 'echo_correlation') $textLeaks = @($records | Where-Object { $_.messageCategory -eq 'text' -and $_.wParamHex -ne $null }) $lost = @($records | Where-Object recordType -eq 'records_lost') $lostTotal = 0 foreach ($lostRecord in $lost) { if ($null -ne $lostRecord.lostCount) { $lostTotal += [int]$lostRecord.lostCount } else { $lostTotal++ } } $attach = $records | Where-Object recordType -eq 'window_attach' | Select-Object -First 1 $closeObserved = @($records | Where-Object messageName -eq 'WM_CLOSE').Count -gt 0 $sessionEndObserved = @($records | Where-Object recordType -eq 'session_end').Count -gt 0 $experimentConfigs = @($records | Where-Object recordType -eq 'experiment_config') if ($experimentConfigs.Count -gt 1) { throw "More than one experiment_config record was found: $($experimentConfigs.Count)" } $experimentConfig = $experimentConfigs | Select-Object -First 1 $experimentTargetEnters = @() $experimentTargetLeaves = @() $experimentEnters = @() $experimentLeaves = @() $matchingEasyX = @() $easyxVisibility = $null $experimentValidity = 'not_configured' $imePositionAttempts = @() $imePositionSuccesses = @() $imePositionResultRecords = @() $imePositionOrphans = @() $runtimeFailOpenEnters = @() $runtimeFailOpenLeaves = @() $runtimeFailOpenReason = $null if ($experimentConfig) { $schemaVersions = @($records | Select-Object -ExpandProperty schemaVersion -Unique) $experimentTargetEnters = @($records | Where-Object { $_.recordType -eq 'wndproc_enter' -and $_.experimentId -eq $experimentConfig.experimentId -and $_.ruleId -eq $experimentConfig.ruleId }) $targetCallIds = @($experimentTargetEnters | Select-Object -ExpandProperty wndProcCallId) $experimentTargetLeaves = @($records | Where-Object { $_.recordType -eq 'wndproc_leave' -and $targetCallIds -contains $_.wndProcCallId }) $runtimeFailOpenEnters = @($experimentTargetEnters | Where-Object failOpenApplied -eq $true) $runtimeFailOpenCallIds = @($runtimeFailOpenEnters | Select-Object -ExpandProperty wndProcCallId) $runtimeFailOpenLeaves = @($experimentTargetLeaves | Where-Object { $runtimeFailOpenCallIds -contains $_.wndProcCallId -and $_.failOpenApplied -eq $true }) $runtimeFailOpenRecords = @($runtimeFailOpenEnters) + @($runtimeFailOpenLeaves) $runtimeFailOpenReasons = @($runtimeFailOpenRecords | Select-Object -ExpandProperty failOpenReason -Unique) if ($runtimeFailOpenReasons.Count -eq 1) { $runtimeFailOpenReason = [string]$runtimeFailOpenReasons[0] } $experimentEnters = @($experimentTargetEnters | Where-Object ruleMatched -eq $true) $matchedCallIds = @($experimentEnters | Select-Object -ExpandProperty wndProcCallId) $experimentLeaves = @($records | Where-Object { $_.recordType -eq 'wndproc_leave' -and $matchedCallIds -contains $_.wndProcCallId }) $easyXMessageType = if ([string]$experimentConfig.targetMessage -match 'UP$') { 'WM_KEYUP' } else { 'WM_KEYDOWN' } $targetScanSpecified = $null -ne $experimentConfig.targetScanCode $matchingEasyX = @($records | Where-Object { $_.recordType -eq 'easyx_key' -and $_.easyxMessageType -eq $easyXMessageType -and [int]$_.easyxVkCode -eq [int]$experimentConfig.targetVirtualKey -and (-not $targetScanSpecified -or [int]$_.easyxScanCode -eq [int]$experimentConfig.targetScanCode) }) $imePositionResultRecords = @($records | Where-Object recordType -eq 'ime_position') # 兼容本修复前把定位结果直接挂在 wndproc_enter 上的 schema v2 日志。 $legacyImePositionAttempts = @($records | Where-Object { $_.recordType -eq 'wndproc_enter' -and $_.imePositionAttempted -eq $true }) $imePositionAttempts = @($imePositionResultRecords) + @($legacyImePositionAttempts) $imePositionSuccesses = @($imePositionAttempts | Where-Object { $_.imePositionContextAvailable -eq $true -and $_.compositionPositionSucceeded -eq $true -and $_.candidatePositionSucceeded -eq $true }) $sequenceLookup = @{} foreach ($entry in $records) { $sequenceLookup[[string]$entry.sequence] = $true } $imePositionOrphans = @($imePositionResultRecords | Where-Object { $null -eq $_.parentSequence -or -not $sequenceLookup.ContainsKey([string]$_.parentSequence) -or [uint64]$_.parentSequence -ge [uint64]$_.sequence }) $easyxVisibility = if ($matchingEasyX.Count -eq 0) { 'absent' } elseif ($matchingEasyX.Count -eq 1) { 'present_once' } else { 'present_multiple' } if ($schemaVersions.Count -ne 1 -or [int]$schemaVersions[0] -ne 2) { $experimentValidity = 'invalid_schema' } elseif (-not $sequenceStrict) { $experimentValidity = 'inconclusive_sequence' } elseif ($textLeaks.Count -gt 0) { $experimentValidity = 'inconclusive_text_leak' } elseif ($lostTotal -gt 0) { $experimentValidity = 'inconclusive_capture_loss' } elseif ($imePositionOrphans.Count -gt 0) { $experimentValidity = 'inconclusive_ime_position_link' } elseif (-not $closeObserved) { $experimentValidity = 'inconclusive_close_missing' } elseif (-not $sessionEndObserved) { $experimentValidity = 'inconclusive_session_end_missing' } elseif (-not $experimentConfig.configurationValid) { $experimentValidity = 'invalid_configuration' } elseif (-not $experimentConfig.ruleEligible) { $experimentValidity = 'ineligible_fail_open' } elseif ([string]$experimentConfig.inputSource -eq 'Manual' -and (-not $experimentConfig.imePositionConfigured -or $imePositionSuccesses.Count -eq 0)) { $experimentValidity = 'inconclusive_ime_position' } elseif ($experimentTargetEnters.Count -ne [int]$experimentConfig.maxMatches) { $experimentValidity = 'unexpected_target_count' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndConsume' -and $experimentEnters.Count -eq 0 -and $runtimeFailOpenEnters.Count -eq [int]$experimentConfig.maxMatches -and $runtimeFailOpenLeaves.Count -eq $runtimeFailOpenEnters.Count -and $runtimeFailOpenReasons.Count -eq 1 -and -not [string]::IsNullOrWhiteSpace($runtimeFailOpenReason) -and @($runtimeFailOpenLeaves | Where-Object { $_.actualAction -ne 'ObserveAndForward' -or $_.forwarded -ne $true }).Count -eq 0) { $experimentValidity = 'runtime_fail_open' } elseif ($experimentEnters.Count -ne [int]$experimentConfig.maxMatches) { $experimentValidity = 'unexpected_match_count' } elseif ($experimentLeaves.Count -ne $experimentEnters.Count) { $experimentValidity = 'missing_leave_record' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndConsume' -and @($experimentLeaves | Where-Object actualAction -ne 'ObserveAndConsume').Count -gt 0) { $experimentValidity = 'consume_action_mismatch' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndConsume' -and @($experimentLeaves | Where-Object forwarded -eq $true).Count -gt 0) { $experimentValidity = 'consume_was_forwarded' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndConsume' -and @($experimentLeaves | Where-Object forwardResultHex -ne '0x0000000000000000').Count -gt 0) { $experimentValidity = 'consume_result_nonzero' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndForward' -and @($experimentLeaves | Where-Object actualAction -ne 'ObserveAndForward').Count -gt 0) { $experimentValidity = 'forward_action_mismatch' } elseif ([string]$experimentConfig.requestedAction -eq 'ObserveAndForward' -and @($experimentLeaves | Where-Object forwarded -ne $true).Count -gt 0) { $experimentValidity = 'forward_was_not_forwarded' } else { $experimentValidity = 'valid' } } $correlationCounts = [ordered]@{} foreach ($entry in $echoSummaries) { $key = "$($entry.echoChannel):$($entry.correlationStatus)" if (-not $correlationCounts.Contains($key)) { $correlationCounts[$key] = 0 } $correlationCounts[$key]++ } [pscustomobject]@{ file = $file.FullName recordCount = $records.Count schemaVersions = @($records | Select-Object -ExpandProperty schemaVersion -Unique) sequenceStrict = $sequenceStrict recordsLost = $lostTotal lossRecordCount = $lost.Count textRawWParamLeaks = $textLeaks.Count isWindowUnicode = $attach.isWindowUnicode ansiCodePage = $attach.ansiCodePage wndProcThreadIds = @($records | Where-Object sourceBackend -eq 'Win32WndProc' | Select-Object -ExpandProperty threadId -Unique) hostThreadIds = @($records | Where-Object sourceBackend -eq 'HostEventLoop' | Select-Object -ExpandProperty threadId -Unique) imeStartCount = @($records | Where-Object messageName -eq 'WM_IME_STARTCOMPOSITION').Count imeCompositionCount = @($records | Where-Object messageName -eq 'WM_IME_COMPOSITION').Count imeEndCount = @($records | Where-Object messageName -eq 'WM_IME_ENDCOMPOSITION').Count inputLanguageChangeRequestCount = @($records | Where-Object messageName -eq 'WM_INPUTLANGCHANGEREQUEST').Count inputLanguageChangeCount = @($records | Where-Object messageName -eq 'WM_INPUTLANGCHANGE').Count immQueryCount = $immQueries.Count successfulResultCount = $successfulResults.Count successfulUtf16Lengths = @($successfulResults | Select-Object -ExpandProperty utf16Length) successfulScalarCounts = @($successfulResults | Select-Object -ExpandProperty scalarCount) digestPresentCount = @($successfulResults | Where-Object sessionDigest -ne $null).Count correlationCounts = [pscustomobject]$correlationCounts closeObserved = $closeObserved sessionEndObserved = $sessionEndObserved experimentConfigured = $null -ne $experimentConfig experimentId = if ($experimentConfig) { $experimentConfig.experimentId } else { $null } ruleId = if ($experimentConfig) { $experimentConfig.ruleId } else { $null } ruleEligible = if ($experimentConfig) { [bool]$experimentConfig.ruleEligible } else { $null } experimentFailOpenReason = if ($experimentConfig) { $experimentConfig.failOpenReason } else { $null } runtimeFailOpenReason = $runtimeFailOpenReason requestedAction = if ($experimentConfig) { $experimentConfig.requestedAction } else { $null } targetMessage = if ($experimentConfig) { $experimentConfig.targetMessage } else { $null } targetVirtualKey = if ($experimentConfig) { $experimentConfig.targetVirtualKey } else { $null } targetScanCode = if ($experimentConfig) { $experimentConfig.targetScanCode } else { $null } baselineId = if ($experimentConfig) { $experimentConfig.baselineId } else { $null } targetObservationCount = $experimentTargetEnters.Count targetLeaveCount = $experimentTargetLeaves.Count failOpenObservationCount = @($experimentTargetEnters | Where-Object failOpenApplied -eq $true).Count failOpenForwardedCount = @($experimentTargetLeaves | Where-Object { $_.failOpenApplied -eq $true -and $_.forwarded -eq $true }).Count ruleMatchCount = $experimentEnters.Count consumedMatchCount = @($experimentEnters | Where-Object actualAction -eq 'ObserveAndConsume').Count forwardedMatchCount = @($experimentLeaves | Where-Object forwarded -eq $true).Count matchingEasyXKeyCount = $matchingEasyX.Count easyxVisibility = $easyxVisibility imePositionAttemptCount = $imePositionAttempts.Count imePositionSuccessCount = $imePositionSuccesses.Count imePositionReady = $imePositionSuccesses.Count -gt 0 imePositionResultRecordCount = $imePositionResultRecords.Count imePositionOrphanCount = $imePositionOrphans.Count experimentValidity = $experimentValidity }