[CmdletBinding()] param([string] $NodeExecutable = '') Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) $wrapperPath = Join-Path $repositoryRoot 'scripts\Invoke-LegacyPgmParityRun.ps1' $stagePath = Join-Path $repositoryRoot 'scripts\LegacyPgmParityStage.mjs' $protocolPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyBridge\LegacyBridgeProtocol.cs' $playoutUiPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\Web\playout-ui.js' $appUiPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\Web\app.js' foreach ($path in @($wrapperPath, $stagePath, $protocolPath, $playoutUiPath, $appUiPath)) { if (-not [IO.File]::Exists($path)) { throw "Required parity contract file is missing: $path" } } if ([string]::IsNullOrWhiteSpace($NodeExecutable)) { $command = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $command) { $NodeExecutable = $command.Source } else { $NodeExecutable = Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe' } } $NodeExecutable = [IO.Path]::GetFullPath($NodeExecutable) if (-not [IO.File]::Exists($NodeExecutable)) { throw "Node executable is missing: $NodeExecutable" } $tokens = $null $parseErrors = $null [Management.Automation.Language.Parser]::ParseFile($wrapperPath, [ref]$tokens, [ref]$parseErrors) | Out-Null if ($parseErrors.Count -ne 0) { throw "PowerShell syntax failed at line $($parseErrors[0].Extent.StartLineNumber): $($parseErrors[0].Message)" } & $NodeExecutable --check $stagePath if ($LASTEXITCODE -ne 0) { throw 'Node syntax check failed.' } $wrapper = [IO.File]::ReadAllText($wrapperPath, [Text.Encoding]::UTF8) $stage = [IO.File]::ReadAllText($stagePath, [Text.Encoding]::UTF8) $protocol = [IO.File]::ReadAllText($protocolPath, [Text.Encoding]::UTF8) $playoutUi = [IO.File]::ReadAllText($playoutUiPath, [Text.Encoding]::UTF8) $appUi = [IO.File]::ReadAllText($appUiPath, [Text.Encoding]::UTF8) $forbidden = @( 'Wickedness\.MBNStockWebView\.LegacyParity_[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+_', '(?i)\$(?:Pgm|App)ProcessId\s*=\s*[0-9]{3,}', 'MBNWEB-20260718-DEV-LIVE-02', 'legacy-named-[0-9]', 'MBN_STOCK_WEBVIEW\.LegacyParityApp\.Playout\.dll', 'C:\\Users\\[^\\]+', 'C:\\Tornado2' ) foreach ($pattern in $forbidden) { if ([regex]::IsMatch($wrapper + "`n" + $stage, $pattern)) { throw "A stale runtime/package/named-playlist pin was found: $pattern" } } $requiredWrapperTokens = @( 'Get-AppxPackage -Name $ExpectedPackageName', '$package.IsDevelopmentMode -ne $true', "'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug'", '$debugSegments[1] -cne ''win-x64''', '$debugSegments[2] -cne ''AppX''', 'Find-UniqueProcessByPath', 'Get-EstablishedPgmConnection', '[Net.IPAddress]::IsLoopback($localAddress)', '[Net.IPAddress]::IsLoopback($remoteAddress)', '-PgmProcessId $pgmProcess.Id', '-RemotePort $connection.LocalPort', "'MBN_STOCK_WEBVIEW.LegacyParityApp.exe'", "'MBN_STOCK_WEBVIEW.LegacyParityApp.dll'", "'MBN_STOCK_WEBVIEW.Playout.dll'", "[string] `$ExpectedPgmExecutable = ''", '$env:MBN_STOCK_PGM_EXECUTABLE', "'..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts'", 'Use-ManifestDiscoverySettings -Manifest $manifest', 'Assert-PinsExact -Pinned $manifest.pins', 'SetTopMostForCapture', 'CopyFromScreen', '[AllowEmptyString()] [string] $Text', '[Diagnostics.ProcessStartInfo]::new()', '$start.RedirectStandardOutput = $true', '$start.RedirectStandardError = $true', '$stdoutTask = $process.StandardOutput.ReadToEndAsync()', '$exitCode = [int]$process.ExitCode', 'STOPPED.json', 'FileMode]::CreateNew', "'^(?!0000-)\d{4}-'", "'RunNext'", 'VgAtAFMAdABvAGMAawAgAJ3JjK0VyPS8ocGczdzCpMJc0SAAZgBvAHIAIADkuXzHvawcyFQAVgAgACgAMgA2AC4AMAAzAC4AMgA2ACkA' ) foreach ($token in $requiredWrapperTokens) { if (-not $wrapper.Contains($token)) { throw "Wrapper safety contract is missing: $token" } } if ($wrapper.Contains('SetForegroundWindow')) { throw 'Evidence capture must not steal keyboard focus from either application.' } if ($wrapper.Contains('Start-Process -FilePath $nodePath')) { throw 'Windows PowerShell Start-Process cannot provide a trustworthy Node exit code on this workstation.' } if (@([regex]::Matches($wrapper, '\[AllowEmptyString\(\)\]\s*\[string\]\s*\$Text')).Count -ne 2) { throw 'Both empty Network evidence writes and empty Network delta counts must remain supported.' } $requiredStageTokens = @( "'search-stocks'", "'select-stock'", "'cut-pointer-down'", "'drop-selected-cuts'", "'activate-playlist-row'", "'take-in'", "'next-playout'", "'take-out'", "'open-manual-financial'", "'search-manual-financial'", "'activate-manual-financial-result'", "'select-comparison-pair'", "'delete-selected-comparison-pair'", "'playout-take-in'", "'playout-next'", "'playout-take-out'", 'Input.dispatchMouseEvent', 'OUTCOME_UNKNOWN', 'current.state.currentCode === code', 'payload?.allowAppend === true', 'physicalDragStockCut(client, physicalIndex, label)', "types[0] !== 'cut-pointer-down'", "types[1] !== 'drop-selected-cuts'", 'comparisonPairRows(comparison)', 'deleteTemporaryComparisonPair(client, description, baseline, addedPairId)', 'restoreComparisonSelection(client, selectedPairId)', 'savedPairBaseline', 'resolved: row.isSceneResolved === true', 'row.resolved !== true', 'const FIXED_VISIBLE_ROW_IDENTITIES = new Map([', 'expected.market != null && row.market !== expected.market', "['5077', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '6\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }]", "['5088', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '12\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }]", "second.pageText !== '1/20'", "result.state.rows.some(row => row.pageText !== '1/20')", "current.state.pageCount === 20", "current.state.itemCount === 120", "current.state.itemCount === 240", "const STATE_TIMEOUT_MS = 180_000", "case 'take-out-recovery-8001'", "case 'take-out-recovery-5077-page2'", "case 'resume-30-manifest'" ) foreach ($token in $requiredStageTokens) { if (-not $stage.Contains($token)) { throw "One-shot stage contract is missing: $token" } } if ($stage.Contains("postAndWait(client, 'cut-pointer-down'") -or $stage.Contains("postAndWait(client, 'drop-selected-cuts'")) { throw 'Stock cut setup must use the actual pointer drag path instead of direct native messages.' } if (@([regex]::Matches($stage, 'allowMissingState:\s*true')).Count -ne 1 -or $stage -notmatch "Boolean\(value\.state\),\s*'initial native state',\s*STATE_TIMEOUT_MS,\s*\{\s*allowMissingState:\s*true\s*\}" -or $stage -notmatch "PROBE_VIOLATION[\s\S]*?const state = result\.state;\s*if \(allowMissingState && !state\) return;[\s\S]*?LIVE_STATE_MISMATCH") { throw 'Only bootstrap may tolerate the probe-to-native-state initialization gap.' } if (@([regex]::Matches($stage, '\bgraphicCode\b')).Count -ne 2) { throw 'The runner must not infer hidden cut aliases from WebView playlist state.' } if (-not $protocol.Contains('row.IsSceneResolved')) { throw 'Latest serialized playlist state no longer exposes IsSceneResolved.' } foreach ($messageType in @( 'search-stocks', 'select-stock', 'cut-pointer-down', 'drop-selected-cuts', 'activate-playlist-row', 'take-in', 'next-playout', 'take-out', 'open-manual-financial', 'search-manual-financial', 'activate-manual-financial-result', 'select-comparison-pair', 'delete-selected-comparison-pair')) { if (-not $protocol.Contains('"' + $messageType + '"') -and -not $playoutUi.Contains('"' + $messageType + '"') -and -not $appUi.Contains('"' + $messageType + '"')) { throw "Latest source no longer exposes native message '$messageType'." } } foreach ($buttonId in @('playout-take-in', 'playout-next', 'playout-take-out')) { if (-not $playoutUi.Contains('"' + $buttonId + '"')) { throw "Latest playout UI no longer exposes '$buttonId'." } } if (-not $appUi.Contains('element.dataset.rowId = row.rowId') -or -not $appUi.Contains('rowHeader.className = "playlist-row-header"')) { throw 'Latest playlist row/header DOM contract changed.' } $packageBuildRoot = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug' $fixtureRoots = @() if ([IO.Directory]::Exists($packageBuildRoot)) { foreach ($manifestFile in @(Get-ChildItem -LiteralPath $packageBuildRoot -Recurse -File ` -Filter 'AppxManifest.xml' -ErrorAction Stop)) { $candidateRoot = $manifestFile.Directory.FullName $relative = $candidateRoot.Substring([IO.Path]::GetFullPath($packageBuildRoot).TrimEnd('\').Length).TrimStart('\') $segments = @($relative.Split('\') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($segments.Count -eq 3 -and $segments[0] -match '^net[0-9]+\.[0-9]+-windows[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and $segments[1] -ceq 'win-x64' -and $segments[2] -ceq 'AppX' -and [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe')) -and [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.LegacyParityApp.dll')) -and [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.Playout.dll'))) { $fixtureRoots += $candidateRoot } } } if ($fixtureRoots.Count -lt 1) { throw 'No exact x64 Debug target-framework/win-x64/AppX runtime-file fixture was found.' } $runtimeFixtureRoot = @($fixtureRoots | Sort-Object)[-1] $runtimeFixtureFiles = @( 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe', 'MBN_STOCK_WEBVIEW.LegacyParityApp.dll', 'MBN_STOCK_WEBVIEW.Playout.dll' ) $runtimeFixtureHashes = @($runtimeFixtureFiles | ForEach-Object { $fixturePath = Join-Path $runtimeFixtureRoot $_ [ordered]@{ name = $_ length = [IO.FileInfo]::new($fixturePath).Length sha256 = (Get-FileHash -LiteralPath $fixturePath -Algorithm SHA256).Hash } }) $expectedRoutes = @( '5016', '50160', '5078', '8067', '5086', '50860', '5023', '5024', '5077', '5082', '5083', '5084', '5085', '5088', '6067', '8035', '5011', '8003', '5037', '8001', '5026', '5087', '5029', '8032', '5032', '5076', '5079', '5080', '5081', '5025' ) $routeBlock = [regex]::Match( $stage, 'const THIRTY_CODES = \[(?.*?)\];', [Text.RegularExpressions.RegexOptions]::Singleline) if (-not $routeBlock.Success) { throw 'THIRTY_CODES block is missing.' } $actualRoutes = @([regex]::Matches($routeBlock.Groups['body'].Value, "'(?[0-9]+)'") | ForEach-Object { $_.Groups['code'].Value }) if (($actualRoutes -join ',') -cne ($expectedRoutes -join ',')) { throw 'The 30-route validation order changed.' } foreach ($token in @( 'S6001_ASSET_INDEPENDENT_ACTIONS', "actionId: 'fixed-072'", "actionId: 'fixed-073'", "actionId: 'fixed-074'", "actionId: 'fixed-075'", "subtype: 'DubaiCrude'", "subtype: 'WtiCrude'", "subtype: 'BrentCrude'", "subtype: 'Gold'", 'physicalDoubleClickAndWait(client, selector', "current.state.currentCode === '6001'")) { if (-not $stage.Contains($token)) { throw "The asset-independent s6001 physical-input contract is missing: $token" } } $staticJson = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $wrapperPath -Action StaticAudit if ($LASTEXITCODE -ne 0) { throw 'Wrapper StaticAudit failed.' } $static = $staticJson | ConvertFrom-Json if ($static.ok -ne $true -or $static.authoritativeCutCount -ne 33 -or $static.authoritativeAssetCount -ne 2 -or $static.workflowStageCounts.Core5001 -ne 6 -or $static.workflowStageCounts.ThirtyRoutes -ne 125 -or $static.workflowStageCounts.PagedBoundary -ne 24 -or $static.workflowStageCounts.S6001AssetIndependent -ne 18 -or $static.workflowStageCounts.Recovery8001 -ne 1 -or $static.workflowStageCounts.Recovery5077Page2 -ne 1 -or $static.workflowStageCounts.ThirtyRoutes20To29 -ne 41 -or $static.runtimeProcessesQueried -ne $false -or $static.cdpAttached -ne $false -or $static.nativeMessagesSent -ne 0) { throw 'Wrapper StaticAudit result is not exact.' } if (@($static.authoritativeCutHashes).Count -ne 33 -or @($static.authoritativeCutHashes | Where-Object { [string]::IsNullOrWhiteSpace([string]$_.path) -or [long]$_.length -le 0 -or [string]$_.sha256 -notmatch '^[0-9A-F]{64}$' }).Count -ne 0) { throw 'Wrapper StaticAudit cut identities contain a null or invalid value.' } if (@($static.authoritativeAssetHashes).Count -ne 2 -or @($static.authoritativeAssetHashes | Where-Object { [string]::IsNullOrWhiteSpace([string]$_.path) -or [long]$_.length -le 0 -or [string]$_.sha256 -notmatch '^[0-9A-F]{64}$' }).Count -ne 0) { throw 'Wrapper StaticAudit asset identities contain a null or invalid value.' } [ordered]@{ ok = $true checkedAtUtc = [DateTime]::UtcNow.ToString('o') powershellSyntax = 'passed' nodeSyntax = 'passed' latestBridgeAndDomContract = 'passed' hardcodedRuntimeProcessIds = 0 hardcodedPackageFullNames = 0 namedPlaylistDependencies = 0 developmentDebugRuntimeFixture = $runtimeFixtureRoot developmentDebugRuntimeFiles = $runtimeFixtureHashes routeCount = $actualRoutes.Count workflowStageCounts = $static.workflowStageCounts runtimeProcessesQueried = $false cdpAttached = $false nativeMessagesSent = 0 } | ConvertTo-Json -Depth 8 -Compress