diff --git a/Tools/windows/Authorize-FocusedUiCapture.ps1 b/Tools/windows/Authorize-FocusedUiCapture.ps1 new file mode 100644 index 00000000..bd0184ca --- /dev/null +++ b/Tools/windows/Authorize-FocusedUiCapture.ps1 @@ -0,0 +1,144 @@ +param( + [Parameter(Mandatory)][string]$Repository, + [Parameter(Mandatory)][string]$OwnedRoot, + [Parameter(Mandatory)][string]$FocusedPlanPath, + [Parameter(Mandatory)][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$FocusedPlanSha256, + [ValidateRange(3,30)][long]$AuthorizationMinutes = 20 +) +$ErrorActionPreference='Stop' +$root=[IO.Path]::GetFullPath($OwnedRoot).TrimEnd('\') +$repositoryRoot=[IO.Path]::GetFullPath($Repository).TrimEnd('\') +$planPath=[IO.Path]::GetFullPath($FocusedPlanPath) +if (-not $planPath.StartsWith($root + '\',[StringComparison]::OrdinalIgnoreCase) -or + -not (Test-Path -LiteralPath $planPath -PathType Leaf)) { + throw 'Focused plan must be an existing file below the exact owned root.' +} +if ((Get-FileHash -LiteralPath $planPath -Algorithm SHA256).Hash -ne + $FocusedPlanSha256.ToUpperInvariant()) { + throw 'Focused plan hash mismatch.' +} +$plan=Get-Content -LiteralPath $planPath -Raw | ConvertFrom-Json -DateKind String +if ($plan.schemaVersion -ne 1 -or $plan.status -cne 'PREPARED_NO_AUTHORITY' -or + $plan.ownedRoot -ine $root -or $plan.repository -ine $repositoryRoot -or + $plan.officialParityEvidence -cne $false -or $plan.phases.Count -ne 1 -or + $plan.phases[0] -cne 'after' -or $plan.sceneCount -ne $plan.sceneNames.Count -or + $plan.sceneCount -le 0 -or $plan.pngArtifacts -ne 0 -or $plan.processLaunches -ne 0) { + throw 'Focused plan shape or no-authority boundary differs.' +} +if (@(Get-ChildItem -LiteralPath $root -Recurse -File -Filter '*.png').Count -ne 0) { + throw 'Focused authorization refuses a prepared root containing PNG artifacts.' +} +$modulePath=Join-Path $repositoryRoot 'Tools\windows\UiCaptureDriver.psm1' +$workerPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCaptureWorker.ps1' +$supervisorPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCapture.ps1' +Import-Module $modulePath -Force -WarningAction SilentlyContinue +$jobPath=[IO.Path]::GetFullPath([string]$plan.jobPath) +$preflightPath=[IO.Path]::GetFullPath([string]$plan.preflightReceiptPath) +foreach ($binding in @( + @($modulePath,$plan.modulePath,$plan.moduleSha256), + @($workerPath,$plan.workerPath,$plan.workerSha256), + @($supervisorPath,$plan.supervisorPath,$plan.supervisorSha256), + @($jobPath,$plan.jobPath,$plan.jobSha256), + @($preflightPath,$plan.preflightReceiptPath,$plan.preflightReceiptSha256), + @($plan.fixture.mappingPath,$plan.fixture.mappingPath,$plan.fixture.mappingSha256), + @($plan.fixture.canonicalSourcePath,$plan.fixture.canonicalSourcePath,$plan.fixture.canonicalSourceSha256), + @($plan.fixture.rewrittenPath,$plan.fixture.rewrittenPath,$plan.fixture.rewrittenSha256), + @($plan.binaries.graphCodePath,$plan.binaries.graphCodePath,$plan.binaries.graphCodeSha256), + @($plan.binaries.zmxPath,$plan.binaries.zmxPath,$plan.binaries.zmxSha256), + @($plan.binaries.daemonPath,$plan.binaries.daemonPath,$plan.binaries.daemonSha256), + @($plan.binaries.backendFixturePath,$plan.binaries.backendFixturePath,$plan.binaries.backendFixtureSha256), + @($plan.binaries.offlinePeerPath,$plan.binaries.offlinePeerPath,$plan.binaries.offlinePeerSha256) +)) { + $actual=[IO.Path]::GetFullPath([string]$binding[0]) + $declared=[IO.Path]::GetFullPath([string]$binding[1]) + if ($actual -ine $declared -or -not (Test-Path -LiteralPath $actual -PathType Leaf) -or + (Get-FileHash -LiteralPath $actual -Algorithm SHA256).Hash -ne [string]$binding[2]) { + throw "Focused prepared input changed: $declared" + } +} +$head=(& git -C $repositoryRoot rev-parse HEAD).Trim() +$tree=(& git -C $repositoryRoot rev-parse 'HEAD^{tree}').Trim() +if ($head -cne $plan.sourceHead -or $tree -cne $plan.sourceTree -or + @(& git -C $repositoryRoot status --porcelain --untracked-files=all).Count -ne 0) { + throw 'Focused authorization requires the unchanged clean prepared source head and tree.' +} +$job=Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json -DateKind String +Assert-CapSceneContract -Job $job +$capturePlan=Get-CapMatchedCapturePlan -Job $job +if ($capturePlan.profile -cne $plan.profile -or + $capturePlan.evidenceClassification -cne $plan.evidenceClassification -or + ($capturePlan.sceneNames -join ',') -cne ($plan.sceneNames -join ',') -or + ($capturePlan.phases -join ',') -cne ($plan.phases -join ',') -or + $plan.authorizationBounds.requestedMaxMinutes -ne $AuthorizationMinutes -or + $plan.authorizationBounds.cleanupReserveSeconds -ne 90 -or + $plan.cleanupExpectations.kernelExitAndEofRequired -cne $true -or + $plan.cleanupExpectations.pendingLaunchesExpected -ne 0 -or + $plan.cleanupExpectations.ownedProcessSurvivorsExpected -ne 0 -or + $plan.cleanupExpectations.cleanupFailuresExpected -ne 0) { + throw 'Focused authorization plan, geometry, deadline or cleanup expectations differ.' +} +$preflight=Get-Content -LiteralPath $preflightPath -Raw | ConvertFrom-Json -DateKind String +if ($preflight.executed -ne 185 -or $preflight.passed -ne 185 -or + $preflight.failed -ne 0 -or $preflight.skipped -ne 0 -or + $preflight.gui -cne 'NotExecuted' -or $preflight.images -ne 0 -or + @($preflight.cases | Where-Object {$_.passed -cne $true}).Count -ne 0 -or + $preflight.controls.moduleSha256 -ne $plan.moduleSha256 -or + $preflight.controls.workerSha256 -ne $plan.workerSha256 -or + $preflight.controls.supervisorSha256 -ne $plan.supervisorSha256) { + throw 'Focused authorization preflight no longer proves the prepared controls.' +} +$now=[DateTimeOffset]::UtcNow +$granted=$now.AddSeconds(-1) +$authorization=[ordered]@{ + schemaVersion=1L + authorizationId=[guid]::NewGuid().ToString() + status='AUTHORIZED' + authorizedAction='focused-matched-capture' + evidenceClassification=$plan.evidenceClassification + officialParityEvidence=$false + desktopAuthorized=$true + captureEvidenceAuthorized=$true + pngAuthorized=$true + lane='S' + head=$head + sourceTree=$tree + jobManifestSha256=$plan.jobSha256 + moduleSha256=$plan.moduleSha256 + workerSha256=$plan.workerSha256 + supervisorSha256=$plan.supervisorSha256 + preflightReceiptSha256=$plan.preflightReceiptSha256 + fixtureMappingSha256=$plan.fixture.mappingSha256 + beforeBinarySha256=$plan.binaries.graphCodeSha256 + afterBinarySha256=$plan.binaries.graphCodeSha256 + profile=$plan.profile + sceneNames=@($plan.sceneNames) + phases=@($plan.phases) + focusedPlanSha256=$FocusedPlanSha256.ToUpperInvariant() + grantedUtc=$granted.ToString('o') + expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString('o') + maxMinutes=$AuthorizationMinutes +} +$authorizationPath=Join-Path $root 'focused-authorization.json' +if (Test-Path -LiteralPath $authorizationPath) { + throw 'Focused authorization has already been granted for this prepared root.' +} +[IO.File]::WriteAllText($authorizationPath,($authorization | ConvertTo-Json -Depth 20), + [Text.UTF8Encoding]::new($false)) +[ordered]@{ + ownedRoot=$root + focusedAuthorizationPath=$authorizationPath + focusedAuthorizationSha256=(Get-FileHash -LiteralPath $authorizationPath -Algorithm SHA256).Hash + focusedPlanPath=$planPath + focusedPlanSha256=$FocusedPlanSha256.ToUpperInvariant() + jobPath=$jobPath + jobSha256=$plan.jobSha256 + fixtureMappingSha256=$plan.fixture.mappingSha256 + preflightReceiptPath=$preflightPath + preflightReceiptSha256=$plan.preflightReceiptSha256 + outputDirectory=Join-Path $root 'outputs' + sourceHead=$head + sourceTree=$tree + profile=$plan.profile + sceneNames=@($plan.sceneNames) + evidenceClassification=$plan.evidenceClassification +} | ConvertTo-Json -Depth 10 diff --git a/Tools/windows/Invoke-UiCapture.ps1 b/Tools/windows/Invoke-UiCapture.ps1 index 8071c462..7557aae6 100644 --- a/Tools/windows/Invoke-UiCapture.ps1 +++ b/Tools/windows/Invoke-UiCapture.ps1 @@ -61,6 +61,7 @@ $authorizationPath = $null $authorizationHash = $null $authorityId = $null $authorityHash = $null +$focusedPlan = $null $authorityPath = $null $deadlineUtc = [DateTime]::MinValue $jobHash = $null @@ -1242,8 +1243,9 @@ try { $fixtureMappingHashExpected = $job.fixture.mappingSha256 } if ($focusedCapture) { + $focusedPlan=Get-CapMatchedCapturePlan -Job $job if ($job.executionMode -cne $ExecutionMode -or $job.desktopAuthorized -cne $true -or - $job.evidenceClassification -cne 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2') { + $job.evidenceClassification -cne $focusedPlan.evidenceClassification) { throw 'Expected an explicitly authorized focused matched capture job.' } $preflightHash=(Get-FileHash -Algorithm SHA256 -LiteralPath $preflightPath).Hash @@ -1599,7 +1601,7 @@ try { evidenceClassification = if ($transitionRehearsal) { 'LIVE_TRANSITION_REHEARSAL_NOT_CAPTURE_EVIDENCE' } elseif ($focusedCapture) { - 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + $focusedPlan.evidenceClassification } else { $null } officialParityEvidence = if ($focusedCapture) {$false} else {$null} authorityId = $authorityId diff --git a/Tools/windows/Invoke-UiCaptureWorker.ps1 b/Tools/windows/Invoke-UiCaptureWorker.ps1 index ec708e47..309f0d0c 100644 --- a/Tools/windows/Invoke-UiCaptureWorker.ps1 +++ b/Tools/windows/Invoke-UiCaptureWorker.ps1 @@ -2161,6 +2161,9 @@ function Invoke-CapMatchedScenes { $app.SObservation=[pscustomobject]@{readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY' fixtureState=$state[0];card=$card[0];wireState='HASH_BOUND_STATIC_PEER_NOT_LIVE_BACKEND'} $recovery=$null + $cycleRoute=$null + $postFixCardFocused=$focusedCapture -and + $matchedCapturePlan.profile -ceq 'PostFixCards' if ($scene -cmatch '^s-goal-unwired-') { $wireIdentity="wire-node:$($fixture.graphs[0].project.path):$($state[0].nodeId)" $markIdentity="mark-entry:$($fixture.graphs[0].project.path):$($state[0].nodeId)" @@ -2235,41 +2238,87 @@ function Invoke-CapMatchedScenes { controlType='Text';invokable=$false;bounds=@($reasonBounds.Left,$reasonBounds.Top, $reasonBounds.Right,$reasonBounds.Bottom)} selected=[bool]$state[0].selected;cardIdentity=$card[0].AutomationId}) + $cycleRoute=[pscustomobject]@{actionIdentity=$actionIdentity;actionAutomationId=$actionId + reasonAutomationId=$reasonId;cardAutomationId=$cardId;cardName=$card[0].Name} } Save-CapSceneImage -AppRun $app -SceneName $scene -ZoomEvidence $zoom -Phase $Phase if ($null -ne $recovery) { - $null=Invoke-CapSceneAction -AppRun $app -Action 'wire-node' -SubjectId $recovery.wireIdentity - $wireStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` - -ExpectedProcessId $app.Run.Identity.Pid -Name 'Choose a target loop to wire this entry' ` - -AutomationId 'status' - if ($null -eq $wireStatus) {throw "Wire it up did not expose its armed result for $scene."} - if (-not (Send-CapMatchedEscape -AppRun $app)) { - throw "Wire it up could not be cancelled after matched evidence for $scene." + $runWire=-not $postFixCardFocused -or $scene -ceq 's-goal-unwired-unselected' + $runMark=-not $postFixCardFocused -or $scene -ceq 's-goal-unwired-selected' + $wireReceipt=$null + $markReceipt=$null + if ($runWire) { + $null=Invoke-CapSceneAction -AppRun $app -Action 'wire-node' -SubjectId $recovery.wireIdentity + $wireStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` + -ExpectedProcessId $app.Run.Identity.Pid -Name 'Choose a target loop to wire this entry' ` + -AutomationId 'status' + if ($null -eq $wireStatus) {throw "Wire it up did not expose its armed result for $scene."} + if (-not (Send-CapMatchedEscape -AppRun $app)) { + throw "Wire it up could not be cancelled after matched evidence for $scene." + } + Start-Sleep -Milliseconds 100 + $graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` + -ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph' + $wireAfter=@(Get-CapDirectChildren -Parent $graph) + if (@($wireAfter | Where-Object {$_.Current.AutomationId -ceq $recovery.wireAutomationId}).Count -ne 1) { + throw "Wire it up cancellation did not preserve the unwired control for $scene." + } + $wireReceipt=[pscustomobject]@{automationId=$recovery.wireAutomationId + result='Choose a target loop to wire this entry';cancelledWith='Escape' + mutation='NONE_ARMED_THEN_CANCELLED';controlPreserved=$true} + } + if ($runMark) { + $null=Invoke-CapSceneAction -AppRun $app -Action 'mark-entry' -SubjectId $recovery.markIdentity + $entryStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` + -ExpectedProcessId $app.Run.Identity.Pid -Name 'Marked as an entry for this session' ` + -AutomationId 'status' + if ($null -eq $entryStatus) {throw "Mark as entry did not expose its result for $scene."} + $graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` + -ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph' + $after=@(Get-CapDirectChildren -Parent $graph) + if (@($after | Where-Object {$_.Current.AutomationId -cin @( + $recovery.wireAutomationId,$recovery.markAutomationId)}).Count -ne 0) { + throw "Mark as entry left stale unwired controls for $scene." + } + $cardAfter=@($after | Where-Object {$_.Current.AutomationId -ceq $cardId -and + $_.Current.Name -ceq $card[0].Name}) + if ($cardAfter.Count -ne 1 -or $cardAfter[0].Current.ProcessId -ne $app.Run.Identity.Pid) { + throw "Mark as entry did not preserve the owned Goal card for $scene." + } + $markReceipt=[pscustomobject]@{automationId=$recovery.markAutomationId + result='Marked as an entry for this session';mutation='DECLARED_ENTRY_ADDED' + controlsRemoved=$true;cardPreserved=$cardAfter[0].Current.AutomationId + graphReadback='UNWIRED_CONTROLS_ABSENT_SAME_CARD_IDENTITY_PRESENT'} } - Start-Sleep -Milliseconds 100 - $null=Invoke-CapSceneAction -AppRun $app -Action 'mark-entry' -SubjectId $recovery.markIdentity - $entryStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` - -ExpectedProcessId $app.Run.Identity.Pid -Name 'Marked as an entry for this session' ` - -AutomationId 'status' - if ($null -eq $entryStatus) {throw "Mark as entry did not expose its result for $scene."} + $app.SObservation | Add-Member -NotePropertyName recoveryRoutes -NotePropertyValue ` + ([pscustomobject]@{fixtureIsolation='ONE_SCENE_ONE_DISPOSABLE_PHASE_ROOT' + wire=$wireReceipt;markEntry=$markReceipt}) + } + if ($postFixCardFocused -and $scene -ceq 's-main-cycle-selected') { + $null=Invoke-CapSceneAction -AppRun $app -Action 'attention-action' ` + -SubjectId $cycleRoute.actionIdentity + $null=Invoke-CapSceneAction -AppRun $app -Action 'workspace-show-graph' + $rowsAfter=@(Read-CapMatchedRows -AppRun $app) + $cardAfter=@($rowsAfter | Where-Object {$_.AutomationId -ceq $cycleRoute.cardAutomationId -and + $_.Name -ceq $cycleRoute.cardName}) $graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity ` -ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph' - $after=@(Get-CapDirectChildren -Parent $graph) - if (@($after | Where-Object {$_.Current.AutomationId -cin @( - $recovery.wireAutomationId,$recovery.markAutomationId)}).Count -ne 0) { - throw "Mark as entry left stale unwired controls for $scene." - } - $cardAfter=@($after | Where-Object {$_.Current.AutomationId -ceq $cardId -and - $_.Current.Name -ceq $card[0].Name}) - if ($cardAfter.Count -ne 1 -or $cardAfter[0].Current.ProcessId -ne $app.Run.Identity.Pid) { - throw "Mark as entry did not preserve the owned Goal card for $scene." + $childrenAfter=@(Get-CapDirectChildren -Parent $graph) + $inspectAfter=@($childrenAfter | Where-Object { + $_.Current.AutomationId -ceq $cycleRoute.actionAutomationId -and + $_.Current.Name -ceq 'Inspect'}) + $reasonAfter=@($childrenAfter | Where-Object { + $_.Current.AutomationId -ceq $cycleRoute.reasonAutomationId -and + $_.Current.Name -ceq 'cycle · no entry point'}) + if ($cardAfter.Count -ne 1 -or $cardAfter[0].Selected -cne $true -or + $inspectAfter.Count -ne 1 -or $reasonAfter.Count -ne 1) { + throw 'Selected cycle Inspect/Show in Graph route did not preserve stable card, action and reason identities.' } - $app.SObservation | Add-Member -NotePropertyName recoveryRoutes -NotePropertyValue ` - ([pscustomobject]@{wire=[pscustomobject]@{automationId=$recovery.wireAutomationId - result='Choose a target loop to wire this entry';cancelledWith='Escape'} - markEntry=[pscustomobject]@{automationId=$recovery.markAutomationId - result='Marked as an entry for this session';controlsRemoved=$true - cardPreserved=$cardAfter[0].Current.AutomationId}}) + $app.SObservation | Add-Member -NotePropertyName cycleRoute -NotePropertyValue ` + ([pscustomobject]@{inspectAutomationId=$cycleRoute.actionAutomationId + returnAction='Show in Graph';cardAutomationId=$cycleRoute.cardAutomationId + reasonAutomationId=$cycleRoute.reasonAutomationId;selectedAfterReturn=$true + identitiesStable=$true}) } } else { $null=Invoke-CapSceneAction -AppRun $app -Action 'open-loop' -Name $node.title diff --git a/Tools/windows/Prepare-FocusedUiCapture.ps1 b/Tools/windows/Prepare-FocusedUiCapture.ps1 index 0e437731..d694697e 100644 --- a/Tools/windows/Prepare-FocusedUiCapture.ps1 +++ b/Tools/windows/Prepare-FocusedUiCapture.ps1 @@ -7,6 +7,8 @@ param( [Parameter(Mandatory)][string]$BackendFixtureBinary, [Parameter(Mandatory)][string]$OfflinePeerSource, [Parameter(Mandatory)][string]$PreflightReport, + [ValidateSet('TerminalChrome','PostFixCards')][string]$FocusedProfile = 'TerminalChrome', + [switch]$PlanOnly, [ValidateRange(3,30)][long]$AuthorizationMinutes = 20 ) $ErrorActionPreference='Stop' @@ -32,6 +34,33 @@ foreach ($path in @($modulePath,$workerPath,$supervisorPath,$GraphCodeBinary,$Zm if (-not (Test-Path -LiteralPath $path)) {throw "Focused capture input is missing: $path"} } Import-Module $modulePath -Force -WarningAction SilentlyContinue +$focusedScenes=@(Get-CapMatchedFocusedSceneNames -Profile $FocusedProfile) +$focusedClassification=Get-CapMatchedFocusedClassification -Profile $FocusedProfile +$moduleHash=(Get-FileHash -LiteralPath $modulePath -Algorithm SHA256).Hash +$workerHash=(Get-FileHash -LiteralPath $workerPath -Algorithm SHA256).Hash +$supervisorHash=(Get-FileHash -LiteralPath $supervisorPath -Algorithm SHA256).Hash +$preflight=Get-Content -LiteralPath $PreflightReport -Raw | ConvertFrom-Json -DateKind String +$requiredRealZmxCases=@( + 'real zmx job lifecycle through actual command with target normal', + 'real zmx job lifecycle through actual command with target nonzero', + 'real zmx job lifecycle through actual command with target timeout' +) +if ($preflight.suite -cne 'UiCaptureDriver' -or $preflight.executed -ne 185 -or + $preflight.passed -ne 185 -or $preflight.failed -ne 0 -or $preflight.skipped -ne 0 -or + $preflight.mode -cne 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL' -or + $preflight.gui -cne 'NotExecuted' -or $preflight.images -ne 0 -or + $preflight.backendTurns -ne 0 -or $preflight.cases.Count -ne 185 -or + @($preflight.cases | Where-Object {$_.passed -cne $true}).Count -ne 0 -or + $preflight.controls.moduleSha256 -ne $moduleHash -or + $preflight.controls.workerSha256 -ne $workerHash -or + $preflight.controls.supervisorSha256 -ne $supervisorHash) { + throw 'Focused capture preparation requires the exact green 185-case no-desktop preflight for the current controls.' +} +foreach ($caseName in $requiredRealZmxCases) { + if (@($preflight.cases | Where-Object name -CEQ $caseName).Count -ne 1) { + throw "Focused capture preflight omitted or duplicated required case: $caseName" + } +} $inputs=Join-Path $root 'inputs' $runtime=Join-Path $root 'runtime' @@ -83,6 +112,14 @@ $daemonHash=(Get-FileHash -LiteralPath (Join-Path $runtime 'graphcoded.exe') -Al $backendHash=(Get-FileHash -LiteralPath (Join-Path $runtime 'copilot.exe') -Algorithm SHA256).Hash $backendSourceHash=(Get-FileHash -LiteralPath $backendSourcePath -Algorithm SHA256).Hash $peerHash=(Get-FileHash -LiteralPath (Join-Path $inputs 'offline-peer-source.ps1') -Algorithm SHA256).Hash +foreach ($caseName in $requiredRealZmxCases) { + $case=@($preflight.cases | Where-Object name -CEQ $caseName)[0] + if ($case.detail.evidenceMode -cne 'EXECUTED_CURRENT_REAL_ZMX' -or + $case.detail.providerCommit -cne '785b3fd15dcafd1882b495c831a10f98c201b908' -or + $case.detail.zmxSha256 -ne $zmxHash) { + throw "Focused capture preflight real-zmx evidence differs for: $caseName" + } +} $scenes=@(Get-CapMatchedSceneNames | ForEach-Object { [ordered]@{name=$_;naturalWidthBeforeAfter=@(250L,250L)} @@ -99,7 +136,7 @@ $identity=[ordered]@{ $job=[ordered]@{ executionMode='FOCUSED_LIVE_CAPTURE' desktopAuthorized=$true - evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + evidenceClassification=$focusedClassification lane='S' sceneAdapter='S_MATCHED_V12_DPI_V1' head=$head @@ -109,9 +146,10 @@ $job=[ordered]@{ authority=[ordered]@{centralCaptureLeaseGrantedByThisArtifact=$false;sWorkerMayLaunchGui=$false} focusedCapture=[ordered]@{ schemaVersion=1L + profile=$FocusedProfile phases=@('after') - sceneNames=@(Get-CapMatchedFocusedSceneNames) - evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + sceneNames=$focusedScenes + evidenceClassification=$focusedClassification officialParityEvidence=$false } captureContract=[ordered]@{ @@ -148,6 +186,98 @@ $job=[ordered]@{ $jobPath=Join-Path $root 'job.json' [IO.File]::WriteAllText($jobPath,($job | ConvertTo-Json -Depth 40),[Text.UTF8Encoding]::new($false)) $jobHash=(Get-FileHash -LiteralPath $jobPath -Algorithm SHA256).Hash +$planPath=Join-Path $root 'focused-plan.json' +$plan=[ordered]@{ + schemaVersion=1L + status='PREPARED_NO_AUTHORITY' + preparedUtc=[DateTimeOffset]::UtcNow.ToString('o') + ownedRoot=$root + repository=$repositoryRoot + sourceHead=$head + sourceTree=$tree + profile=$FocusedProfile + evidenceClassification=$focusedClassification + officialParityEvidence=$false + phases=@('after') + sceneNames=$focusedScenes + sceneCount=[long]$focusedScenes.Count + jobPath=$jobPath + jobSha256=$jobHash + modulePath=$modulePath + moduleSha256=$moduleHash + workerPath=$workerPath + workerSha256=$workerHash + supervisorPath=$supervisorPath + supervisorSha256=$supervisorHash + preflightReceiptPath=$preflightPath + preflightReceiptSha256=(Get-FileHash -LiteralPath $preflightPath -Algorithm SHA256).Hash + preflight=[ordered]@{ + executed=185L;passed=185L;failed=0L;skipped=0L + gui='NotExecuted';images=0L;backendTurns=0L + requiredRealZmxCases=$requiredRealZmxCases + } + fixture=[ordered]@{ + canonicalSourcePath=$copiedFixtureSource + canonicalSourceSha256=$fixtureSourceHash + rewrittenPath=$fixturePath + rewrittenSha256=$fixtureHash + mappingPath=$mappingPath + mappingSha256=$mappingHash + graphs=1L + topLevelNodes=8L + syntheticMatchedData=$true + } + binaries=[ordered]@{ + graphCodePath=(Join-Path $inputs 'graphcode-after.exe');graphCodeSha256=$binaryHash + zmxPath=(Join-Path $runtime 'zmx.exe');zmxSha256=$zmxHash + daemonPath=(Join-Path $runtime 'graphcoded.exe');daemonSha256=$daemonHash + backendFixturePath=(Join-Path $runtime 'copilot.exe');backendFixtureSha256=$backendHash + offlinePeerPath=(Join-Path $inputs 'offline-peer-source.ps1');offlinePeerSha256=$peerHash + } + geometry=$job.captureContract + authorizationBounds=[ordered]@{ + status='NOT_GRANTED' + requestedMaxMinutes=$AuthorizationMinutes + cleanupReserveSeconds=90L + minimumRemainingMinutesAtLaunch=1L + } + cleanupExpectations=[ordered]@{ + kernelExitAndEofRequired=$true + pendingLaunchesExpected=0L + ownedProcessSurvivorsExpected=0L + cleanupFailuresExpected=0L + independentZeroProcessCheckRequired=$true + } + pngArtifacts=0L + processLaunches=0L +} +[IO.File]::WriteAllText($planPath,($plan | ConvertTo-Json -Depth 40),[Text.UTF8Encoding]::new($false)) +$planHash=(Get-FileHash -LiteralPath $planPath -Algorithm SHA256).Hash +if (@(Get-ChildItem -LiteralPath $root -Recurse -File -Filter '*.png').Count -ne 0) { + throw 'Focused plan preparation created an unauthorized PNG.' +} +if ($PlanOnly) { + [ordered]@{ + ownedRoot=$root + status='PREPARED_NO_AUTHORITY' + focusedPlanPath=$planPath + focusedPlanSha256=$planHash + jobPath=$jobPath + jobSha256=$jobHash + fixtureMappingSha256=$mappingHash + preflightReceiptPath=$preflightPath + preflightReceiptSha256=$plan.preflightReceiptSha256 + outputDirectory=Join-Path $root 'outputs' + sourceHead=$head + sourceTree=$tree + profile=$FocusedProfile + sceneNames=$focusedScenes + evidenceClassification=$focusedClassification + pngArtifacts=0L + processLaunches=0L + } | ConvertTo-Json -Depth 10 + return +} $now=[DateTimeOffset]::UtcNow $granted=$now.AddSeconds(-1) $authorization=[ordered]@{ @@ -155,7 +285,7 @@ $authorization=[ordered]@{ authorizationId=[guid]::NewGuid().ToString() status='AUTHORIZED' authorizedAction='focused-matched-capture' - evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + evidenceClassification=$focusedClassification officialParityEvidence=$false desktopAuthorized=$true captureEvidenceAuthorized=$true @@ -164,14 +294,15 @@ $authorization=[ordered]@{ head=$head sourceTree=$tree jobManifestSha256=$jobHash - moduleSha256=(Get-FileHash -LiteralPath $modulePath -Algorithm SHA256).Hash - workerSha256=(Get-FileHash -LiteralPath $workerPath -Algorithm SHA256).Hash - supervisorSha256=(Get-FileHash -LiteralPath $supervisorPath -Algorithm SHA256).Hash + moduleSha256=$moduleHash + workerSha256=$workerHash + supervisorSha256=$supervisorHash preflightReceiptSha256=(Get-FileHash -LiteralPath $preflightPath -Algorithm SHA256).Hash fixtureMappingSha256=$mappingHash beforeBinarySha256=$binaryHash afterBinarySha256=$binaryHash - sceneNames=@(Get-CapMatchedFocusedSceneNames) + profile=$FocusedProfile + sceneNames=$focusedScenes phases=@('after') grantedUtc=$granted.ToString('o') expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString('o') @@ -188,10 +319,15 @@ $result=[ordered]@{ preflightReceiptSha256=$authorization.preflightReceiptSha256 focusedAuthorizationPath=$authorizationPath focusedAuthorizationSha256=(Get-FileHash -LiteralPath $authorizationPath -Algorithm SHA256).Hash + focusedPlanPath=$planPath + focusedPlanSha256=$planHash outputDirectory=Join-Path $root 'outputs' sourceHead=$head sourceTree=$tree binarySha256=$binaryHash zmxSha256=$zmxHash + profile=$FocusedProfile + sceneNames=$focusedScenes + evidenceClassification=$focusedClassification } $result | ConvertTo-Json -Depth 10 diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index 94dcc542..606dfecd 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -2977,11 +2977,25 @@ function Invoke-CapMatchedContracts { $plan=Get-CapMatchedCapturePlan -Job $focused Assert-CapTest ($plan.phases.Count -eq 1 -and $plan.phases[0] -ceq 'after' -and $plan.sceneNames.Count -eq 11 -and $plan.includeRestart -eq $false -and + $plan.profile -ceq 'TerminalChrome' -and $plan.evidenceClassification -ceq 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2') ` 'Focused matched plan did not preserve the exact after-only F2 inventory.' + $postFix=$focused | ConvertTo-Json -Depth 30 | ConvertFrom-Json -DateKind String + $postFix.focusedCapture | Add-Member -NotePropertyName profile -NotePropertyValue 'PostFixCards' + $postFix.focusedCapture.sceneNames=@(Get-CapMatchedFocusedSceneNames -Profile PostFixCards) + $postFix.focusedCapture.evidenceClassification= + Get-CapMatchedFocusedClassification -Profile PostFixCards + $postFixPlan=Get-CapMatchedCapturePlan -Job $postFix + Assert-CapTest ($postFixPlan.profile -ceq 'PostFixCards' -and + $postFixPlan.sceneNames.Count -eq 4 -and + ($postFixPlan.sceneNames -join ',') -ceq + 's-goal-unwired-unselected,s-goal-unwired-selected,s-main-cycle-unselected,s-main-cycle-selected' -and + $postFixPlan.evidenceClassification -ceq 'POSTFIX_CARD_PARITY_FOCUSED_VALIDATION_V1') ` + 'Post-fix card plan did not preserve the exact after-only four-scene inventory.' $supervisorSource=[IO.File]::ReadAllText($supervisorPath) $workerSource=[IO.File]::ReadAllText($workerPath) $prepareSource=[IO.File]::ReadAllText((Join-Path $driverDirectory 'Prepare-FocusedUiCapture.ps1')) + $authorizeSource=[IO.File]::ReadAllText((Join-Path $driverDirectory 'Authorize-FocusedUiCapture.ps1')) Assert-CapTest ($supervisorSource.Contains("'FOCUSED_LIVE_CAPTURE'") -and $supervisorSource.Contains('FocusedAuthorizationPath') -and $supervisorSource.Contains('Assert-CapFocusedPrelaunchPaths') -and @@ -2989,12 +3003,18 @@ function Invoke-CapMatchedContracts { $workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and $workerSource.Contains('function Get-CapWorkerExpectedImageCount') -and $workerSource.Contains('$images.Count -eq (Get-CapWorkerExpectedImageCount)') -and - $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'") -and + $workerSource.Contains("-Action 'attention-action'") -and + $workerSource.Contains("fixtureIsolation='ONE_SCENE_ONE_DISPOSABLE_PHASE_ROOT'") -and + $prepareSource.Contains("[switch]`$PlanOnly") -and + $prepareSource.Contains("status='PREPARED_NO_AUTHORITY'") -and + $authorizeSource.Contains("pngArtifacts -ne 0") -and + $authorizeSource.Contains("Get-CapMatchedCapturePlan -Job `$job") -and $prepareSource.Contains('expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString(''o'')') -and $prepareSource.Contains('$null=New-Item -ItemType Directory -Path $destinationProjectRoot') -and -not $prepareSource.Contains("Join-Path `$PreparedFixtureRoot 'projects'")) ` 'Focused supervisor, worker chrome reader or checked-in preparation entry point is not wired.' - $focusedMutations=@('missing','extra','wrong-scene','wrong-phase','wrong-classification','parity-overclaim') + $focusedMutations=@('missing','extra','wrong-scene','wrong-phase','wrong-classification', + 'parity-overclaim','wrong-profile') foreach ($mutation in $focusedMutations) { $changed=$focused | ConvertTo-Json -Depth 30 | ConvertFrom-Json -DateKind String switch ($mutation) { @@ -3004,6 +3024,7 @@ function Invoke-CapMatchedContracts { 'wrong-phase' {$changed.focusedCapture.phases=@('before')} 'wrong-classification' {$changed.focusedCapture.evidenceClassification='C8R_RDP_DPI192_QUALIFIED'} 'parity-overclaim' {$changed.focusedCapture.officialParityEvidence=$true} + 'wrong-profile' {$changed.focusedCapture | Add-Member -NotePropertyName profile -NotePropertyValue 'Foreign'} } $null=Assert-CapRejects {Get-CapMatchedCapturePlan -Job $changed} 'Focused matched capture' } @@ -3019,9 +3040,9 @@ function Invoke-CapMatchedContracts { $changed.scenes=@($changed.scenes | Where-Object name -CNE $name) $null=Assert-CapRejects {Assert-CapSceneContract -Job $changed} 'Matched scene' } - return @{positive=4;negative=($names.Count+1+$focusedMutations.Count) + return @{positive=5;negative=($names.Count+1+$focusedMutations.Count) budgetAcceptedSeconds=@(810,1710);budgetRefusedSeconds=1711 - names=$names;focused=$plan;focusedMutations=$focusedMutations + names=$names;focused=$plan;postFixFocused=$postFixPlan;focusedMutations=$focusedMutations prelaunchPaths=(Invoke-CapFocusedPrelaunchPathContract) expectedImageCounts=(Invoke-CapFocusedExpectedImageCountContract) routing=(Invoke-CapMatchedSceneRoutingContract)} @@ -4119,6 +4140,7 @@ function Invoke-CapMatchedSceneRoutingContract { $captured=[Collections.Generic.List[object]]::new() $seeded=[Collections.Generic.List[string]]::new() $focusedCapture=$false + $matchedCapturePlan=[pscustomobject]@{profile='TerminalChrome'} $model=[pscustomobject]@{total=1;selected=0;node=$null;removed=$false;restart=0;hover=0;freezeVisibility=$false wireArmed=$false;marked=$false deadlineFailure=$false;captureFailure=$false;appCleanup=0;seedCleanup=0;peerCleanup=0;chromeReads=0 @@ -4153,6 +4175,7 @@ function Invoke-CapMatchedSceneRoutingContract { } 'wire-node' {$model.wireArmed=$true} 'mark-entry' {$model.marked=$true} + 'attention-action' {} } } function New-CapMockElement([string]$Id,[string]$Name,[int]$ProcessId,[double[]]$Bounds) { @@ -4204,11 +4227,21 @@ function Invoke-CapMatchedSceneRoutingContract { function Read-CapMatchedRows { param($AppRun) if ($scene -cnotmatch '^s-tabs-') { - return @($fixture.graphs[0].nodes | ForEach-Object { + $rows=@($fixture.graphs[0].nodes | ForEach-Object { [pscustomobject]@{AutomationId=(Get-CapMatchedAutomationId ("project-card:"+$fixture.graphs[0].project.path+":"+$_.id));Name=$_.title;ProcessId=4242L ParentAutomationId='graph';Selected=($_.id -ceq $model.node.id);IsEnabled=$true;IsOffscreen=$false Bounds=@(250,100,500,196)} }) + if ($scene -cmatch '^s-main-cycle-') { + $nodeId=@($fixture.states | Where-Object scene -CEQ $scene)[0].nodeId + $rows+= [pscustomobject]@{AutomationId=(Get-CapMatchedAutomationId ("attention-action:"+$fixture.graphs[0].project.path+":"+$nodeId)) + Name='Inspect';ProcessId=4242L;ParentAutomationId='graph';Selected=$false;IsEnabled=$true + IsOffscreen=$false;Bounds=@(412,151,478,168)} + $rows+= [pscustomobject]@{AutomationId=(Get-CapMatchedAutomationId ("cycle-reason:"+$fixture.graphs[0].project.path+":"+$nodeId)) + Name='cycle · no entry point';ProcessId=4242L;ParentAutomationId='graph';Selected=$false;IsEnabled=$true + IsOffscreen=$false;Bounds=@(261,175,478,188)} + } + return $rows } $visible=@(Get-MockVisibleIndexes) $count=$visible.Count @@ -4270,6 +4303,7 @@ function Invoke-CapMatchedSceneRoutingContract { Assert-CapTest ($legacyControls.Count -eq 0) ` 'Legacy wrapper mismatch no longer reproduces: matched rows unexpectedly returned chrome controls.' $focusedCapture=$true + $matchedCapturePlan=[pscustomobject]@{profile='TerminalChrome'} Invoke-CapMatchedScenes -Phase 'after' -SceneNames $focusedScenes -SkipRestart Assert-CapTest ($captured.Count -eq 11 -and $seeded.Count -eq 11 -and $model.restart -eq 0 -and $model.chromeReads -eq 11 -and @@ -4280,7 +4314,24 @@ function Invoke-CapMatchedSceneRoutingContract { $focusedRestartCount=$model.restart;$focusedHoverCount=$model.hover $many=@($captured | Where-Object scene -CEQ 's-tabs-many-first')[0] $manySelections=@($model.selections | Where-Object {$_ -cmatch '^s-tabs-many-first:'}) + $captured.Clear();$seeded.Clear();$model.restart=0;$model.hover=0 + $matchedCapturePlan=[pscustomobject]@{profile='PostFixCards'} + $postFixScenes=@(Get-CapMatchedFocusedSceneNames -Profile PostFixCards) + Invoke-CapMatchedScenes -Phase 'after' -SceneNames $postFixScenes -SkipRestart + $wireScene=@($captured | Where-Object scene -CEQ 's-goal-unwired-unselected')[0] + $markScene=@($captured | Where-Object scene -CEQ 's-goal-unwired-selected')[0] + $cycleScene=@($captured | Where-Object scene -CEQ 's-main-cycle-selected')[0] + Assert-CapTest ($captured.Count -eq 4 -and $seeded.Count -eq 0 -and + $wireScene.observation.recoveryRoutes.wire.mutation -ceq 'NONE_ARMED_THEN_CANCELLED' -and + $null -eq $wireScene.observation.recoveryRoutes.markEntry -and + $markScene.observation.recoveryRoutes.markEntry.mutation -ceq 'DECLARED_ENTRY_ADDED' -and + $null -eq $markScene.observation.recoveryRoutes.wire -and + $cycleScene.observation.cycleRoute.identitiesStable -cne $false -and + $cycleScene.observation.cycleRoute.returnAction -ceq 'Show in Graph') ` + 'Post-fix focused executor did not split disposable unwired routes or preserve cycle identities.' + $postFixSceneCount=$captured.Count $captured.Clear();$seeded.Clear();$model.appCleanup=0;$model.seedCleanup=0;$model.peerCleanup=0 + $matchedCapturePlan=[pscustomobject]@{profile='TerminalChrome'} $model.captureFailure=$true $cleanupFailure=Assert-CapRejects { Invoke-CapMatchedScenes -Phase 'after' -SceneNames $focusedScenes -SkipRestart @@ -4306,6 +4357,7 @@ function Invoke-CapMatchedSceneRoutingContract { } 'remained genuinely unavailable after exact overflow bridge 4' return @{scenes=$focusedSceneCount;seedDispatches=$focusedSeedCount realRestartDispatches=$focusedRestartCount;hoverDispatches=$focusedHoverCount + postFixScenes=$postFixSceneCount legacyExpectedControls=3;legacyObservedControls=$legacyControls.Count cleanupFailure=$cleanupFailure;deadlineFailure=$deadlineFailure manyFirstSelections=$manySelections;unavailableFailure=$unavailable @@ -4365,13 +4417,16 @@ function Invoke-CapFocusedExpectedImageCountContract { $matchedCapturePlan=[pscustomobject]@{phases=@('after');sceneNames=@(1..11);includeRestart=$false} function Test-CapMatchedAdapter {return $true} $focused=Get-CapWorkerExpectedImageCount + $matchedCapturePlan=[pscustomobject]@{phases=@('after');sceneNames=@(1..4);includeRestart=$false} + $postFixFocused=Get-CapWorkerExpectedImageCount $matchedCapturePlan=[pscustomobject]@{phases=@('before','after');sceneNames=@(1..17);includeRestart=$true} $full=Get-CapWorkerExpectedImageCount $transitionRehearsal=$true $transition=Get-CapWorkerExpectedImageCount - Assert-CapTest ($focused -eq 11 -and $full -eq 36 -and $transition -eq 0) ` + Assert-CapTest ($focused -eq 11 -and $postFixFocused -eq 4 -and + $full -eq 36 -and $transition -eq 0) ` 'Worker terminal status image counts drifted from focused/full/rehearsal plans.' - [pscustomobject]@{focused=$focused;full=$full;transition=$transition} + [pscustomobject]@{focused=$focused;postFixFocused=$postFixFocused;full=$full;transition=$transition} } $function.Extent.Text } @@ -4646,7 +4701,26 @@ function Invoke-CapMatchedManifestContract { } $null=Assert-CapRejects {Assert-CapMatchedManifest -Manifest $changed -Job $focusedJob} 'Matched manifest' } - return @{positive=2;negative=11;images=36;realRestarts=2;focusedImages=11;focusedRestarts=0 + $postFixJob=$focusedJob | ConvertTo-Json -Depth 35 | ConvertFrom-Json -DateKind String + $postFixJob.focusedCapture | Add-Member -NotePropertyName profile -NotePropertyValue 'PostFixCards' + $postFixJob.focusedCapture.sceneNames=@(Get-CapMatchedFocusedSceneNames -Profile PostFixCards) + $postFixJob.focusedCapture.evidenceClassification= + Get-CapMatchedFocusedClassification -Profile PostFixCards + $postFix=@{ + sceneAdapter='S_MATCHED_V12_DPI_V1';lane='S';status='COMPLETE' + executionMode='FOCUSED_LIVE_CAPTURE' + evidenceClassification='POSTFIX_CARD_PARITY_FOCUSED_VALIDATION_V1' + officialParityEvidence=$false + backendTurns=0;g11Qualification='ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED' + images=@(foreach ($scene in Get-CapMatchedFocusedSceneNames -Profile PostFixCards) { + @{phase='after';scene=$scene;captureMode='PRINTWINDOW';observedState=@{ + readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY'}} + }) + realRestarts=@() + } | ConvertTo-Json -Depth 35 | ConvertFrom-Json -DateKind String + Assert-CapMatchedManifest -Manifest $postFix -Job $postFixJob + return @{positive=3;negative=11;images=36;realRestarts=2;focusedImages=11 + postFixFocusedImages=4;focusedRestarts=0 evidence='SIMULATED_MANIFEST_NOT_CAPTURE'} } diff --git a/Tools/windows/UI-CAPTURE-SCENARIOS.md b/Tools/windows/UI-CAPTURE-SCENARIOS.md index 5052af5b..f04ae907 100644 --- a/Tools/windows/UI-CAPTURE-SCENARIOS.md +++ b/Tools/windows/UI-CAPTURE-SCENARIOS.md @@ -210,23 +210,38 @@ row validation. Unmatched scenes remain logical-geometry-only or unassessable. `FOCUSED_LIVE_CAPTURE` runs an exact validated subset through the same `Invoke-CapMatchedScenes` implementation used by the full matched adapter. It does not copy scene setup, UIA assertions, provider ownership, image capture, or -cleanup into a wrapper. The only admitted plan is the after/current-source F2 -terminal-chrome set: +cleanup into a wrapper. The original admitted plan is the after/current-source +F2 terminal-chrome set when `FocusedProfile=TerminalChrome`; the additional +post-fix card plan is defined below: `agent-unknown`, `backend-idle`, `goal-needs-you`, `loop-running`, `two-agent-selected`, `two-shell-selected`, `split-right-focused`, `many-first`, `overflow-last`, `hover-close`, and `last-guard`. -The mode is permanently classified +That profile is permanently classified `F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2` with `officialParityEvidence=false`. Missing, extra, duplicate, reordered, foreign, -or pre-restart scenes refuse. It still requires the exact 185-case real-zmx +or pre-restart scenes refuse. + +`FocusedProfile=PostFixCards` admits exactly +`s-goal-unwired-unselected`, `s-goal-unwired-selected`, +`s-main-cycle-unselected`, and `s-main-cycle-selected`, classified +`POSTFIX_CARD_PARITY_FOCUSED_VALIDATION_V1` with +`officialParityEvidence=false`. The unwired scenes capture before mutation and +use separate disposable phase roots: the unselected scene arms and cancels +`Wire it up`, while the selected scene invokes `Mark as entry` and requires +the controls to disappear with the same card identity retained. The selected +cycle scene invokes the stable `Inspect` action, returns through `Show in +Graph`, and requires the same card, action, reason, and selected identities. +This is bounded review evidence, not an automatic parity-ledger promotion. + +Both profiles require the exact 185-case real-zmx preflight, a clean current source head/tree, hash-bound current binaries, PMv2 physical admission, raw unresampled geometry, individual image privacy review, the numbered process journal, and zero-survivor release. -Prepare a fresh short root only after the source and controls are committed and -the 185-case report exists: +For the post-fix card profile, first prepare a fresh short root without granting +PNG authority: ```powershell pwsh -NoProfile -File Tools\windows\Prepare-FocusedUiCapture.ps1 ` @@ -236,10 +251,25 @@ pwsh -NoProfile -File Tools\windows\Prepare-FocusedUiCapture.ps1 ` -DaemonRuntimeRoot C:\k8\runtime ` -BackendFixtureBinary C:\k8\runtime\copilot.exe ` -OfflinePeerSource C:\k8\inputs\offline-peer-source.ps1 ` - -PreflightReport C:\fresh-preflight\report.json + -PreflightReport C:\fresh-preflight\report.json ` + -FocusedProfile PostFixCards -PlanOnly ``` -Use the returned paths and hashes with `Invoke-UiCapture.ps1 +The returned `focused-plan.json` proves the exact roster, +source/control/binary/provider/fixture hashes, PMv2 geometry, authorization +ceiling, cleanup expectations, zero launches, and zero PNGs. Review it, then +grant the single bounded authorization: + +```powershell +pwsh -NoProfile -File Tools\windows\Authorize-FocusedUiCapture.ps1 ` + -Repository $PWD -OwnedRoot C:\f7 ` + -FocusedPlanPath C:\f7\focused-plan.json ` + -FocusedPlanSha256 ACTUAL_64_HEX_SHA256 +``` + +The existing one-step terminal preparation remains available by omitting +`-PlanOnly` and using the default `TerminalChrome` profile. Use the returned +paths and hashes with `Invoke-UiCapture.ps1 -ExecutionMode FOCUSED_LIVE_CAPTURE`, including `-FocusedAuthorizationPath`/`-FocusedAuthorizationSha256` and the normal exact job, fixture-mapping, and preflight arguments. Preparation creates no process diff --git a/Tools/windows/UiCaptureDriver.psm1 b/Tools/windows/UiCaptureDriver.psm1 index 005b5126..775b4d17 100644 --- a/Tools/windows/UiCaptureDriver.psm1 +++ b/Tools/windows/UiCaptureDriver.psm1 @@ -1024,7 +1024,7 @@ function Get-CapActionSpec { [Parameter(Mandatory)][ValidateSet('actual-size', 'open-project', 'overview-destination', 'open-loop', 'file-exit', 'quick-chats-destination','open-quick-chat','workspace-show-graph','workspace-toggle-panel', 'workspace-restart-session','workspace-new-tab','workspace-split-right','workspace-select-tab','workspace-close-tab', - 'wire-node','mark-entry')][string]$Action, + 'wire-node','mark-entry','attention-action')][string]$Action, [string]$Name, [string]$SubjectId ) @@ -1081,6 +1081,13 @@ function Get-CapActionSpec { return [pscustomobject]@{AutomationId=(Get-CapMatchedAutomationId $SubjectId) AutomationIdPrefix=$null;Name='Mark as entry';CommandId=$null} } + 'attention-action' { + if ($SubjectId -cnotmatch '^attention-action:.+:[0-9A-Fa-f-]{36}$') { + throw 'Inspect requires its exact stable attention control identity.' + } + return [pscustomobject]@{AutomationId=(Get-CapMatchedAutomationId $SubjectId) + AutomationIdPrefix=$null;Name='Inspect';CommandId=$null} + } 'open-loop' { if ([string]::IsNullOrWhiteSpace($Name)) { throw 'open-loop requires a loop name.' } return [pscustomobject]@{ AutomationId = $null; AutomationIdPrefix = 'canvas-card-'; Name = $Name; CommandId = $null } @@ -1776,7 +1783,7 @@ function Invoke-CapAction { if ($Action -notin @('actual-size', 'open-project', 'overview-destination', 'open-loop', 'file-exit', 'quick-chats-destination','open-quick-chat','workspace-show-graph','workspace-toggle-panel', 'workspace-restart-session','workspace-new-tab','workspace-split-right','workspace-select-tab','workspace-close-tab', - 'wire-node','mark-entry')) { + 'wire-node','mark-entry','attention-action')) { throw "Unsupported capture action '$Action'." } if ($WindowProcessResolver -and -not $Invoker) { throw 'A custom HWND resolver is allowed only with the action mock.' } @@ -1817,7 +1824,7 @@ function Invoke-CapAction { $null = Assert-CapUiaProcessIds -ExpectedProcessId $ProcessIdentity.Pid -ObservedProcessIds @([int]$current.ProcessId) if ($Action -in @('quick-chats-destination','open-quick-chat','workspace-show-graph','workspace-toggle-panel', 'workspace-restart-session','workspace-new-tab','workspace-split-right','workspace-select-tab','workspace-close-tab', - 'wire-node','mark-entry')) { + 'wire-node','mark-entry','attention-action')) { if (-not $current.IsEnabled) { throw 'D action target is disabled.' } if ($current.ControlType -ne [System.Windows.Automation.ControlType]::Button -and $current.ControlType -ne [System.Windows.Automation.ControlType]::ListItem) { throw 'D action target has an unexpected control type.' } @@ -3601,11 +3608,28 @@ function Get-CapMatchedSceneNames { } function Get-CapMatchedFocusedSceneNames { + param( + [ValidateSet('TerminalChrome','PostFixCards')] + [string]$Profile = 'TerminalChrome' + ) + if ($Profile -ceq 'PostFixCards') { + return @('s-goal-unwired-unselected','s-goal-unwired-selected', + 's-main-cycle-unselected','s-main-cycle-selected') + } return @('s-tabs-agent-unknown','s-tabs-backend-idle','s-tabs-goal-needs-you','s-tabs-loop-running', 's-tabs-two-agent-selected','s-tabs-two-shell-selected','s-tabs-split-right-focused', 's-tabs-many-first','s-tabs-overflow-last','s-tabs-hover-close','s-tabs-last-guard') } +function Get-CapMatchedFocusedClassification { + param( + [ValidateSet('TerminalChrome','PostFixCards')] + [string]$Profile = 'TerminalChrome' + ) + if ($Profile -ceq 'PostFixCards') { return 'POSTFIX_CARD_PARITY_FOCUSED_VALIDATION_V1' } + return 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' +} + function Get-CapMatchedCapturePlan { param([Parameter(Mandatory)]$Job) if (-not $Job.PSObject.Properties['focusedCapture']) { @@ -3623,11 +3647,17 @@ function Get-CapMatchedCapturePlan { $focused=$Job.focusedCapture Assert-CapDataRecord $focused @{schemaVersion=[long];phases=[array];sceneNames=[array] evidenceClassification=[string];officialParityEvidence=[bool]} 'Focused matched capture' - $expected=Get-CapMatchedFocusedSceneNames + $profileProperty=$focused.PSObject.Properties['profile'] + $profile=if ($profileProperty) {[string]$profileProperty.Value} else {'TerminalChrome'} + if ($profile -cnotin @('TerminalChrome','PostFixCards')) { + throw 'Focused matched capture profile differs.' + } + $expected=Get-CapMatchedFocusedSceneNames -Profile $profile + $classification=Get-CapMatchedFocusedClassification -Profile $profile if ($focused.schemaVersion -ne 1 -or ($focused.phases -join ',') -cne 'after' -or ($focused.sceneNames -join ',') -cne ($expected -join ',') -or @($focused.sceneNames | Sort-Object -Unique).Count -ne $expected.Count -or - $focused.evidenceClassification -cne 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' -or + $focused.evidenceClassification -cne $classification -or $focused.officialParityEvidence -cne $false -or $Job.sceneAdapter -cne 'S_MATCHED_V12_DPI_V1') { throw 'Focused matched capture phases, scenes, classification or non-parity status differ.' @@ -3637,7 +3667,8 @@ function Get-CapMatchedCapturePlan { sceneNames=@($expected) includeRestart=$false executionMode='FOCUSED_LIVE_CAPTURE' - evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + profile=$profile + evidenceClassification=$classification officialParityEvidence=$false } } @@ -5936,7 +5967,8 @@ function Assert-CapNoWorkerTransientArtifacts { } Export-ModuleMember -Function @( - 'Get-CapMatchedSceneNames','Get-CapMatchedFocusedSceneNames','Get-CapMatchedCapturePlan', + 'Get-CapMatchedSceneNames','Get-CapMatchedFocusedSceneNames','Get-CapMatchedFocusedClassification', + 'Get-CapMatchedCapturePlan', 'Assert-CapFocusedPrelaunchPaths', 'Assert-CapMatchedFixture','Get-CapMatchedAutomationId', 'Get-CapMatchedVisibleTabIndexes','Get-CapMatchedTabObservation','Get-CapMatchedChromeControlObservation',