param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$SessionId, [Parameter(Mandatory = $true)] [string]$StartedAtUtc, [int]$ProbeExitCode = 0 ) $ErrorActionPreference = 'Stop' function Get-FailureCategory { param([object]$Case) if (-not $Case.evidenceComplete) { return 'evidence_missing' } if (-not $Case.targetChanged) { return 'surface_unreachable' } if (-not $Case.nonTargetUnchanged) { return 'surface_cross_write' } if (-not $Case.gdiStateRestored) { return 'gdi_state_not_restored' } if (-not $Case.wideContentPreservedAfterEasyXFollowUp) { return 'measurement_relation_invalid' } return $null } function New-WideTextCases { param([object]$Raw, [object[]]$EvidenceFiles) $items = New-Object System.Collections.Generic.List[object] foreach ($case in @($Raw.result.cases)) { $caseFailure = Get-FailureCategory -Case $case $caseEvidence = @($EvidenceFiles | Where-Object { $_.path -like "evidence/$($case.testCaseId)_*" }) $phases = @('after_draw') if ($case.batchState -eq 'inside') { $phases += @('after_flush', 'after_end') } foreach ($text in @($case.textTests)) { $textFailure = $caseFailure if (-not $textFailure -and (-not $text.isValidUtf16 -or -not $text.getTextExtentPoint32W -or -not $text.textOutW -or [int]$text.drawTextWHeight -le 0)) { $textFailure = 'measurement_relation_invalid' } foreach ($phase in $phases) { $fingerprint = switch ($phase) { 'after_draw' { $case.observations.afterDraw; break } 'after_flush' { $case.observations.afterFlush; break } 'after_end' { $case.observations.afterEnd; break } } $status = if ($textFailure) { 'fail' } else { 'pass' } $items.Add([pscustomobject][ordered]@{ testCaseId = "$($case.testCaseId)/$($text.testStringId)/$phase" testStringId = $text.testStringId codePoints = @($text.codePoints) utf16Units = @($text.utf16Units) surfaceId = $case.surfaceId batchState = $case.batchState observationPhase = $phase hdc = $fingerprint.hdc hbitmap = $fingerprint.hbitmap hfont = $fingerprint.hfont bitmapWidth = $fingerprint.bitmapWidth bitmapHeight = $fingerprint.bitmapHeight planes = $fingerprint.planes bitsPerPixel = $fingerprint.bitsPerPixel stride = $fingerprint.stride dibSection = $fingerprint.dibSection logFont = $fingerprint.logFont gdiStateBefore = $case.before.gdiState gdiStateAfter = $fingerprint.gdiState apiResults = [pscustomobject][ordered]@{ getTextExtentPoint32W = $text.getTextExtentPoint32W textOutW = $text.textOutW drawTextWHeight = $text.drawTextWHeight } logicalExtent = $text.logicalExtent textOutInkBounds = $text.textOutInkBounds drawTextInkBounds = $text.drawTextInkBounds targetPixelsBefore = $case.before.pixelDigest targetPixelsAfter = $fingerprint.pixelDigest controlPixelsBefore = $case.nonTargetBefore.pixelDigest controlPixelsAfter = $case.nonTargetAfterDraw.pixelDigest easyXFollowUp = [pscustomobject][ordered]@{ wideContentPreserved = $case.wideContentPreservedAfterEasyXFollowUp surfaceFingerprint = $case.observations.afterEasyXFollowUp } automatedStatus = $status manualStatus = 'not_reviewed' failureCategory = $textFailure evidenceFiles = $caseEvidence }) } } } return $items.ToArray() } function New-DpiCoordinateCases { param([object]$Raw, [object[]]$EvidenceFiles) $failure = $null if ($EvidenceFiles.Count -lt @($Raw.evidenceFiles).Count) { $failure = 'evidence_missing' } elseif (-not $Raw.result.gdiStateRestored) { $failure = 'gdi_state_not_restored' } elseif (-not $Raw.result.roundTripsExact) { $failure = 'coordinate_transform_invalid' } elseif (-not $Raw.result.gdiExtentSuccess -or -not $Raw.result.gdiTextSuccess) { $failure = 'measurement_relation_invalid' } $status = if ($failure) { 'fail' } else { 'pass' } $decision = if ($Raw.result.implicitScalingObserved) { 'requires_explicit_transform_validation' } else { 'direct_hwnd_relative_coordinates_supported_at_observed_dpi' } return @([pscustomobject][ordered]@{ testCaseId = 'dpi-coordinate-current-environment' surfaceId = 'main' batchState = $Raw.result.batchState observationPhase = 'after_end' dpiAwareness = [pscustomobject][ordered]@{ process = $Raw.environment.processAwareness thread = $Raw.environment.threadAwareness window = $Raw.environment.windowAwareness } windowDpi = $Raw.environment.dpiForWindow clientRect = $Raw.environment.clientRect windowScreenRect = $Raw.environment.windowRect surfaceSize = [pscustomobject]@{ width = 960; height = 720 } surfaceFingerprint = $Raw.result.surfaceFingerprintAfterDraw surfaceBounds = $Raw.result.surfaceBounds clientBounds = $Raw.result.clientBounds coordinateRoundTrips = @($Raw.result.coordinateRoundTrips) surfaceMarkerSizesAgree = $Raw.result.surfaceMarkerSizesAgree clientMatchesSurface = $Raw.result.clientMatchesSurface implicitScalingObserved = $Raw.result.implicitScalingObserved coordinateDecision = $decision automatedStatus = $status manualStatus = 'not_reviewed' failureCategory = $failure evidenceFiles = $EvidenceFiles }) } function New-ImePositionCases { param([object]$Raw, [object[]]$EvidenceFiles) $items = New-Object System.Collections.Generic.List[object] foreach ($anchor in @($Raw.result.anchors)) { $events = @($Raw.result.imeEvents | Where-Object { $_.syntheticAnchorId -eq $anchor.syntheticAnchorId -and $_.apiAttempted }) $successful = @($events | Where-Object { $_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult }) $preconfigureEvents = @($events | Where-Object { $_.positionTrigger -eq 'anchor_change' -or $_.stage -eq 'anchor_preconfigure' }) $successfulPreconfigure = @($preconfigureEvents | Where-Object { $_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult }) $lifecycleEvents = @($events | Where-Object { $_.positionTrigger -eq 'ime_lifecycle' -or $_.stage -eq 'ime_start' -or $_.stage -eq 'ime_composition' }) $successfulLifecycle = @($lifecycleEvents | Where-Object { $_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult }) $latest = $events | Select-Object -Last 1 $failure = if ($successful.Count -gt 0) { $null } else { 'ime_position_invalid' } $status = if ($failure) { 'fail' } else { 'pass' } $anchorEvidence = @($EvidenceFiles | Where-Object { $_.path -like "evidence/$($anchor.syntheticAnchorId)_*" -or $_.path -like "manual-evidence/$($anchor.syntheticAnchorId)_*" }) $items.Add([pscustomobject][ordered]@{ testCaseId = "ime-position/$($anchor.syntheticAnchorId)" syntheticAnchorId = $anchor.syntheticAnchorId anchorCoordinates = [pscustomobject][ordered]@{ controlOrigin = $anchor.controlOrigin controlLocalCaretTopLeft = $anchor.controlLocalCaretTopLeft controlLocalCaretBottom = $anchor.controlLocalCaretBottom controlLocalLineRect = $anchor.controlLocalLineRect easyxSurfaceCaretTopLeft = $anchor.easyxSurfaceCaretTopLeft easyxSurfaceCaretBottom = $anchor.easyxSurfaceCaretBottom easyxSurfaceLineRect = $anchor.easyxSurfaceLineRect targetHwndRelativePoint = $anchor.targetHwndRelativePoint diagnosticScreenPoint = if ($latest) { $latest.diagnosticScreenPoint } else { $null } } compositionForm = if ($latest) { $latest.compositionForm } else { $null } candidateForm = if ($latest) { $latest.candidateForm } else { $null } imeId = if ($latest) { $latest.imeDescriptionUtf16 } else { @() } imeFileName = if ($latest) { $latest.imeFileNameUtf16 } else { @() } hkl = if ($latest) { $latest.hkl } else { $null } apiAttemptCount = $events.Count successfulApiAttemptCount = $successful.Count preconfigureApiAttemptCount = $preconfigureEvents.Count successfulPreconfigureApiAttemptCount = $successfulPreconfigure.Count lifecycleApiAttemptCount = $lifecycleEvents.Count successfulLifecycleApiAttemptCount = $successfulLifecycle.Count positionDisposition = 'not_observable' automatedStatus = $status manualStatus = 'not_reviewed' failureCategory = $failure evidenceFiles = $anchorEvidence }) } return $items.ToArray() } $resolved = Resolve-Path -LiteralPath $Path $item = Get-Item -LiteralPath $resolved $directory = if ($item.PSIsContainer) { $item.FullName } else { $item.DirectoryName } $rawPath = if ($item.PSIsContainer) { Join-Path $directory 'raw-result.json' } else { $item.FullName } if (-not (Test-Path -LiteralPath $rawPath)) { throw "Raw result not found: $rawPath" } $raw = Get-Content -Raw -LiteralPath $rawPath | ConvertFrom-Json if ([int]$raw.rawSchemaVersion -ne 1) { throw "Unsupported raw schema: $($raw.rawSchemaVersion)" } if ($raw.mode -notin @('wide-text', 'dpi-coordinate', 'ime-position')) { throw "Unsupported probe mode: $($raw.mode)" } $evidence = New-Object System.Collections.Generic.List[object] foreach ($entry in @($raw.evidenceFiles)) { $relativePath = [string]$entry.path $nativeRelative = $relativePath.Replace('/', [IO.Path]::DirectorySeparatorChar) $fullPath = Join-Path $directory $nativeRelative if (-not (Test-Path -LiteralPath $fullPath)) { continue } $file = Get-Item -LiteralPath $fullPath $evidence.Add([pscustomobject][ordered]@{ path = $relativePath purpose = $entry.purpose captureBackend = $entry.captureBackend sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash byteLength = $file.Length crop = $null }) } $rawEvidencePresent = $evidence.Count $manualEvidenceDirectory = Join-Path $directory 'manual-evidence' if (Test-Path -LiteralPath $manualEvidenceDirectory) { Get-ChildItem -LiteralPath $manualEvidenceDirectory -Filter '*.png' -File | ForEach-Object { $evidence.Add([pscustomobject][ordered]@{ path = 'manual-evidence/' + $_.Name purpose = 'manually cropped IME position evidence' captureBackend = 'manual_crop' sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash byteLength = $_.Length crop = 'user_supplied' }) } } $evidenceArray = $evidence.ToArray() $testCases = if ($raw.mode -eq 'wide-text') { New-WideTextCases -Raw $raw -EvidenceFiles $evidenceArray } elseif ($raw.mode -eq 'dpi-coordinate') { New-DpiCoordinateCases -Raw $raw -EvidenceFiles $evidenceArray } else { New-ImePositionCases -Raw $raw -EvidenceFiles $evidenceArray } $overallManualStatus = 'not_reviewed' $sourceManualReview = $null $manualReviewSummary = $null $manualReviewPath = Join-Path $directory 'manual-review-v1.json' if (Test-Path -LiteralPath $manualReviewPath) { $manualReview = Get-Content -Raw -LiteralPath $manualReviewPath | ConvertFrom-Json if ([int]$manualReview.manualReviewSchemaVersion -ne 1) { throw "Unsupported manual review schema: $($manualReview.manualReviewSchemaVersion)" } if ([string]$manualReview.mode -ne [string]$raw.mode) { throw "Manual review mode does not match raw result: $($manualReview.mode)" } $allowedManualStatuses = @('pass', 'fail', 'inconclusive', 'not_reviewed', 'not_available') $allowedPositionDispositions = @('as_requested', 'system_adjusted', 'not_observable') $reviewedIds = New-Object System.Collections.Generic.List[string] foreach ($reviewCase in @($manualReview.testCases)) { $testCaseId = [string]$reviewCase.testCaseId if ($reviewedIds.Contains($testCaseId)) { throw "Duplicate manual review case: $testCaseId" } $reviewedIds.Add($testCaseId) $matchingCases = @($testCases | Where-Object testCaseId -eq $testCaseId) if ($matchingCases.Count -ne 1) { throw "Unknown manual review case: $testCaseId" } $manualStatus = [string]$reviewCase.manualStatus $positionDisposition = [string]$reviewCase.positionDisposition if ($manualStatus -notin $allowedManualStatuses) { throw "Unsupported manual status for ${testCaseId}: $manualStatus" } if ($positionDisposition -notin $allowedPositionDispositions) { throw "Unsupported position disposition for ${testCaseId}: $positionDisposition" } $targetCase = $matchingCases[0] if ($manualStatus -eq 'pass' -and $targetCase.automatedStatus -ne 'pass') { throw "Manual pass cannot override automated failure: $testCaseId" } $reviewEvidence = @($reviewCase.evidenceFiles) if ($manualStatus -eq 'pass' -and $reviewEvidence.Count -eq 0) { throw "Manual pass requires explicit evidence files: $testCaseId" } foreach ($relativePath in $reviewEvidence) { $relative = [string]$relativePath if ($relative -notlike 'manual-evidence/*') { throw "Manual review evidence must be under manual-evidence: $relative" } $matchingEvidence = @($targetCase.evidenceFiles | Where-Object path -eq $relative) if ($matchingEvidence.Count -ne 1) { throw "Manual review evidence is missing or belongs to another case: $relative" } } if ($manualStatus -eq 'pass' -and $positionDisposition -eq 'not_observable') { throw "Manual pass requires an observable position disposition: $testCaseId" } $targetCase.manualStatus = $manualStatus $targetCase.positionDisposition = $positionDisposition $targetCase | Add-Member -NotePropertyName manualReviewNotes ` -NotePropertyValue ([string]$reviewCase.notes) -Force } if ($reviewedIds.Count -ne @($testCases).Count) { throw 'Manual review must cover every manifest test case' } $caseManualStatuses = @($testCases | ForEach-Object { $_.manualStatus }) $derivedManualStatus = if ($caseManualStatuses -contains 'fail') { 'fail' } elseif ($caseManualStatuses -contains 'inconclusive') { 'inconclusive' } elseif ($caseManualStatuses -contains 'not_reviewed') { 'not_reviewed' } elseif ($caseManualStatuses -contains 'not_available') { if (@($caseManualStatuses | Where-Object { $_ -ne 'not_available' }).Count -eq 0) { 'not_available' } else { 'inconclusive' } } else { 'pass' } if ([string]$manualReview.overallStatus -ne $derivedManualStatus) { throw "Manual review overall status mismatch: expected $derivedManualStatus" } $overallManualStatus = $derivedManualStatus $sourceManualReview = [pscustomobject][ordered]@{ path = 'manual-review-v1.json' sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $manualReviewPath).Hash } $manualReviewSummary = [pscustomobject][ordered]@{ reviewedAtUtc = $manualReview.reviewedAtUtc reviewer = $manualReview.reviewer expectedImeName = $manualReview.expectedImeName actualImeName = $manualReview.actualImeName identitySource = $manualReview.identitySource notes = $manualReview.notes } } $failedCases = @($testCases | Where-Object automatedStatus -eq 'fail') $allEvidencePresent = $rawEvidencePresent -eq @($raw.evidenceFiles).Count $overallFailure = $null if (-not $allEvidencePresent) { $overallFailure = 'evidence_missing' } elseif ($failedCases.Count -gt 0) { $overallFailure = $failedCases[0].failureCategory } elseif (-not $raw.completed -or $ProbeExitCode -ne 0) { $overallFailure = 'inconclusive_probe_exit' } $overallStatus = if ($overallFailure) { 'fail' } else { 'pass' } $manifest = [pscustomobject][ordered]@{ visualProbeSchemaVersion = 1 sessionId = $SessionId startedAtUtc = $StartedAtUtc completedAtUtc = [DateTime]::UtcNow.ToString('o') mode = $raw.mode processId = $raw.environment.processId threadId = $raw.environment.threadId osBuild = $raw.environment.osBuild easyxVersion = $raw.environment.easyxVersion probeExecutableExitCode = $ProbeExitCode dpiConfiguration = $raw.dpiConfiguration environment = $raw.environment automatedStatus = $overallStatus manualStatus = $overallManualStatus failureCategory = $overallFailure sourceRawResult = [pscustomobject][ordered]@{ path = 'raw-result.json' sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $rawPath).Hash } sourceManualReview = $sourceManualReview manualReview = $manualReviewSummary evidenceFiles = $evidenceArray testCases = @($testCases) } $manifestPath = Join-Path $directory 'manifest-v1.json' $manifestJson = $manifest | ConvertTo-Json -Depth 100 $utf8Bom = New-Object System.Text.UTF8Encoding($true) [IO.File]::WriteAllText($manifestPath, $manifestJson + [Environment]::NewLine, $utf8Bom) $manifestHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $manifestPath).Hash $sidecarPath = $manifestPath + '.sha256' $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [IO.File]::WriteAllText($sidecarPath, "$manifestHash *manifest-v1.json`r`n", $utf8NoBom) [pscustomobject][ordered]@{ mode = $raw.mode manifestPath = $manifestPath manifestSha256 = $manifestHash automatedStatus = $overallStatus manualStatus = $overallManualStatus testCaseCount = @($testCases).Count evidenceCount = $evidence.Count dpiForWindow = $raw.environment.dpiForWindow windowAwareness = $raw.environment.windowAwareness }