[CmdletBinding()]
param(
    [ValidateSet('StaticAudit', 'DryPreflight', 'Initialize', 'Inspect', 'RunStage', 'RunNext')]
    [string] $Action = 'StaticAudit',

    [string] $EvidenceDirectory = '',

    [ValidateSet('Core5001', 'ThirtyRoutes', 'PagedBoundary', 'S6001AssetIndependent', 'Recovery8001', 'Recovery5077Page2', 'ThirtyRoutes20To29')]
    [string] $Workflow = 'Core5001',

    [string] $Stage = '',
    [int] $RouteIndex = -1,
    [int] $PageIndex = -1,
    [int] $DebugPort = 9339,
    [int] $ExpectedPgmPort = 30001,
    [int] $StageTimeoutSeconds = 900,

    [string] $ExpectedPackageName = 'Wickedness.MBNStockWebView.LegacyParity',
    [string] $ExpectedPgmExecutable = '',
    [string] $ExpectedAppWindowTitle = '',
    [string] $ExpectedPgmWindowTitle = 'PGM',
    [string] $ExpectedNetworkWindowTitle = '',
    [string] $OriginalCutsDirectory = '',
    [string] $NodeExecutable = ''
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if ([string]::IsNullOrWhiteSpace($ExpectedNetworkWindowTitle)) {
    $ExpectedNetworkWindowTitle = [Text.Encoding]::Unicode.GetString(
        [Convert]::FromBase64String('JLG40szGbNAgAKi6yLIw0cG5IAA9zA=='))
}
if ([string]::IsNullOrWhiteSpace($ExpectedAppWindowTitle)) {
    $ExpectedAppWindowTitle = [Text.Encoding]::Unicode.GetString(
        [Convert]::FromBase64String('VgAtAFMAdABvAGMAawAgAJ3JjK0VyPS8ocGczdzCpMJc0SAAZgBvAHIAIADkuXzHvawcyFQAVgAgACgAMgA2AC4AMAAzAC4AMgA2ACkA'))
}

$workspaceRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if ([string]::IsNullOrWhiteSpace($OriginalCutsDirectory)) {
    $OriginalCutsDirectory = [IO.Path]::GetFullPath((Join-Path $workspaceRoot `
            '..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts'))
} else {
    $OriginalCutsDirectory = [IO.Path]::GetFullPath($OriginalCutsDirectory)
}
if ([string]::IsNullOrWhiteSpace($ExpectedPgmExecutable) -and
    -not [string]::IsNullOrWhiteSpace($env:MBN_STOCK_PGM_EXECUTABLE)) {
    $ExpectedPgmExecutable = [IO.Path]::GetFullPath($env:MBN_STOCK_PGM_EXECUTABLE)
} elseif (-not [string]::IsNullOrWhiteSpace($ExpectedPgmExecutable)) {
    $ExpectedPgmExecutable = [IO.Path]::GetFullPath($ExpectedPgmExecutable)
}

$script:Utf8NoBom = [Text.UTF8Encoding]::new($false)
$script:TargetUrl = 'https://legacy-parity.mbn.local/index.html'
$script:NodeStageScript = Join-Path $PSScriptRoot 'LegacyPgmParityStage.mjs'
$script:RouteCodes = @(
    '5001', '5074',
    '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', '6001'
)
$script:S6001AssetRelativePaths = @(
    [Text.Encoding]::Unicode.GetString(
        [Convert]::FromBase64String('aQBtAGEAZwBlAHMAXAD8yCDHMK5tAGUAcgBnAGUALgBwAG4AZwA=')),
    [Text.Encoding]::Unicode.GetString(
        [Convert]::FromBase64String('aQBtAGEAZwBlAHMAXAAzADUANwA1ADIAOQAxADMAXwBsAC4AagBwAGcA'))
)

function Write-NewText {
    param(
        [Parameter(Mandatory = $true)] [string] $Path,
        [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Text
    )
    $fullPath = [IO.Path]::GetFullPath($Path)
    $stream = [IO.File]::Open($fullPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::Read)
    try {
        $writer = [IO.StreamWriter]::new($stream, $script:Utf8NoBom)
        try { $writer.Write($Text) } finally { $writer.Dispose() }
    } finally {
        if ($null -ne $stream) { $stream.Dispose() }
    }
}

function Write-NewJson {
    param(
        [Parameter(Mandatory = $true)] [string] $Path,
        [Parameter(Mandatory = $true)] [object] $Value
    )
    Write-NewText -Path $Path -Text (($Value | ConvertTo-Json -Depth 40) + [Environment]::NewLine)
}

function Get-CanonicalJson {
    param([Parameter(Mandatory = $true)] [object] $Value)
    return ($Value | ConvertTo-Json -Depth 40 -Compress)
}

function Get-ResolvedNodeExecutable {
    if (-not [string]::IsNullOrWhiteSpace($NodeExecutable)) {
        $resolved = [IO.Path]::GetFullPath($NodeExecutable)
        if (-not [IO.File]::Exists($resolved)) { throw "Node executable does not exist: $resolved" }
        return $resolved
    }
    $command = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($null -ne $command) { return [IO.Path]::GetFullPath($command.Source) }
    $bundled = Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
    if ([IO.File]::Exists($bundled)) { return [IO.Path]::GetFullPath($bundled) }
    throw 'Node was not found. Supply -NodeExecutable with an absolute node.exe path.'
}

function Get-FileIdentity {
    param([Parameter(Mandatory = $true)] [string] $Path)
    $fullPath = [IO.Path]::GetFullPath($Path)
    if (-not [IO.File]::Exists($fullPath)) { throw "Pinned file does not exist: $fullPath" }
    $file = [IO.FileInfo]::new($fullPath)
    return [ordered]@{
        path = $fullPath
        length = $file.Length
        lastWriteUtc = $file.LastWriteTimeUtc.ToString('o')
        sha256 = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash
    }
}

function Initialize-NativeTypes {
    if ('CodexLegacyParityNative20260722' -as [type]) { return }
    Add-Type -AssemblyName System.Drawing
    Add-Type @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

public static class CodexLegacyParityNative20260722
{
    public delegate bool WindowCallback(IntPtr window, IntPtr parameter);

    [StructLayout(LayoutKind.Sequential)]
    public struct Rect { public int Left, Top, Right, Bottom; }

    [DllImport("user32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumWindows(WindowCallback callback, IntPtr parameter);

    [DllImport("user32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumChildWindows(IntPtr parent, WindowCallback callback, IntPtr parameter);

    [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
    private static extern int GetWindowText(IntPtr window, StringBuilder text, int count);

    [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
    private static extern int GetClassName(IntPtr window, StringBuilder text, int count);

    [DllImport("user32.dll", SetLastError=true)]
    private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool IsWindowVisible(IntPtr window);

    [DllImport("user32.dll", SetLastError=true)]
    private static extern IntPtr SendMessageTimeout(IntPtr window, uint message, UIntPtr wParam,
        IntPtr lParam, uint flags, uint timeoutMilliseconds, out UIntPtr result);

    [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
    private static extern IntPtr SendMessageTimeout(IntPtr window, uint message, UIntPtr wParam,
        StringBuilder lParam, uint flags, uint timeoutMilliseconds, out UIntPtr result);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetWindowRect(IntPtr window, out Rect rect);

    [DllImport("user32.dll", EntryPoint="GetWindowLongPtrW", SetLastError=true)]
    private static extern IntPtr GetWindowLongPtr64(IntPtr window, int index);

    [DllImport("user32.dll", EntryPoint="GetWindowLongW", SetLastError=true)]
    private static extern int GetWindowLong32(IntPtr window, int index);

    [DllImport("user32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetWindowPos(IntPtr window, IntPtr insertAfter,
        int x, int y, int width, int height, uint flags);

    public static bool IsTopMost(IntPtr window)
    {
        const int GwlExStyle = -20;
        const long WsExTopMost = 0x00000008L;
        long style = IntPtr.Size == 8
            ? GetWindowLongPtr64(window, GwlExStyle).ToInt64()
            : GetWindowLong32(window, GwlExStyle);
        return (style & WsExTopMost) != 0;
    }

    public static bool SetTopMostForCapture(IntPtr window, bool topMost)
    {
        IntPtr insertAfter = topMost ? new IntPtr(-1) : new IntPtr(-2);
        const uint SwpNoSize = 0x0001;
        const uint SwpNoMove = 0x0002;
        const uint SwpNoActivate = 0x0010;
        const uint SwpShowWindow = 0x0040;
        return SetWindowPos(window, insertAfter, 0, 0, 0, 0,
            SwpNoSize | SwpNoMove | SwpNoActivate | SwpShowWindow);
    }

    [DllImport("kernel32.dll", CharSet=CharSet.Unicode)]
    private static extern int GetPackageFullName(IntPtr process, ref uint packageFullNameLength,
        StringBuilder packageFullName);

    public static string GetProcessPackageFullName(IntPtr process)
    {
        const int AppmodelErrorNoPackage = 15700;
        const int ErrorInsufficientBuffer = 122;
        uint length = 0;
        int first = GetPackageFullName(process, ref length, null);
        if (first == AppmodelErrorNoPackage) return null;
        if (first != ErrorInsufficientBuffer || length == 0)
            throw new InvalidOperationException("GetPackageFullName length query failed: " + first);
        var buffer = new StringBuilder((int)length);
        int second = GetPackageFullName(process, ref length, buffer);
        if (second != 0) throw new InvalidOperationException("GetPackageFullName failed: " + second);
        return buffer.ToString();
    }

    public static IntPtr[] FindTopLevelWindows(uint processId, string exactTitle)
    {
        var matches = new List<IntPtr>();
        if (!EnumWindows(delegate(IntPtr window, IntPtr parameter) {
            uint owner;
            if (GetWindowThreadProcessId(window, out owner) == 0 || owner != processId) return true;
            var title = new StringBuilder(1024);
            GetWindowText(window, title, title.Capacity);
            if (String.Equals(title.ToString(), exactTitle, StringComparison.Ordinal)) matches.Add(window);
            return true;
        }, IntPtr.Zero)) throw new InvalidOperationException("Top-level window enumeration failed.");
        return matches.ToArray();
    }

    public static string GetWindowTitle(IntPtr window)
    {
        var title = new StringBuilder(1024);
        GetWindowText(window, title, title.Capacity);
        return title.ToString();
    }

    public static bool GetVisible(IntPtr window) { return IsWindowVisible(window); }

    private static IntPtr FindUniqueRichEdit(IntPtr parent)
    {
        var matches = new List<IntPtr>();
        if (!EnumChildWindows(parent, delegate(IntPtr window, IntPtr parameter) {
            var className = new StringBuilder(256);
            if (GetClassName(window, className, className.Capacity) > 0 &&
                String.Equals(className.ToString(), "RichEdit20W", StringComparison.Ordinal)) matches.Add(window);
            return true;
        }, IntPtr.Zero)) throw new InvalidOperationException("Network child enumeration failed.");
        if (matches.Count != 1)
            throw new InvalidOperationException("Expected exactly one Network Monitoring RichEdit20W; found " + matches.Count + ".");
        return matches[0];
    }

    private static string ReadTextOnce(IntPtr window)
    {
        const uint WmGetText = 0x000D, WmGetTextLength = 0x000E, Flags = 0x0003;
        UIntPtr lengthResult;
        if (SendMessageTimeout(window, WmGetTextLength, UIntPtr.Zero, IntPtr.Zero, Flags, 5000,
            out lengthResult) == IntPtr.Zero) throw new InvalidOperationException("Network length read timed out.");
        long length = checked((long)lengthResult.ToUInt64());
        if (length < 0 || length > 10000000) throw new InvalidOperationException("Network text length is unsafe.");
        var text = new StringBuilder(checked((int)length + 65537));
        UIntPtr copied;
        if (SendMessageTimeout(window, WmGetText, new UIntPtr((uint)text.Capacity), text, Flags, 5000,
            out copied) == IntPtr.Zero || copied.ToUInt64() != (ulong)text.Length)
            throw new InvalidOperationException("Network text copy failed.");
        return text.ToString();
    }

    public static string ReadStableNetworkText(IntPtr parent)
    {
        IntPtr edit = FindUniqueRichEdit(parent);
        string first = ReadTextOnce(edit);
        string second = ReadTextOnce(edit);
        if (!String.Equals(first, second, StringComparison.Ordinal))
            throw new InvalidOperationException("Network text changed during the read.");
        return first;
    }
}
'@
}

function Get-ExactWindowDescriptor {
    param(
        [Parameter(Mandatory = $true)] [int] $ProcessIdValue,
        [Parameter(Mandatory = $true)] [string] $ExactTitle,
        [bool] $RequireVisible
    )
    $matches = @([CodexLegacyParityNative20260722]::FindTopLevelWindows([uint32]$ProcessIdValue, $ExactTitle))
    if ($matches.Count -ne 1) {
        throw "Expected one '$ExactTitle' window owned by PID $ProcessIdValue; found $($matches.Count)."
    }
    $handle = [IntPtr]$matches[0]
    $visible = [CodexLegacyParityNative20260722]::GetVisible($handle)
    if ($RequireVisible -and -not $visible) { throw "Pinned '$ExactTitle' window is not visible." }
    return [ordered]@{
        handle = $handle.ToInt64().ToString([Globalization.CultureInfo]::InvariantCulture)
        title = [CodexLegacyParityNative20260722]::GetWindowTitle($handle)
        visible = $visible
    }
}

function Get-ProcessPathSafe {
    param([Parameter(Mandatory = $true)] [Diagnostics.Process] $Process)
    try { return $Process.Path } catch { return $null }
}

function Find-UniqueProcessByPath {
    param([Parameter(Mandatory = $true)] [string] $ExpectedPath)
    $fullPath = [IO.Path]::GetFullPath($ExpectedPath)
    $matches = @()
    foreach ($candidate in @(Get-Process -ErrorAction Stop)) {
        $candidatePath = Get-ProcessPathSafe -Process $candidate
        if ($null -ne $candidatePath -and [string]::Equals(
                [IO.Path]::GetFullPath($candidatePath), $fullPath, [StringComparison]::OrdinalIgnoreCase)) {
            $matches += $candidate
        }
    }
    if ($matches.Count -ne 1) { throw "Expected exactly one process at '$fullPath'; found $($matches.Count)." }
    return $matches[0]
}

function Get-ProcessDescriptor {
    param(
        [Parameter(Mandatory = $true)] [Diagnostics.Process] $Process,
        [string] $WindowTitle = '',
        [bool] $RequireVisibleWindow = $false
    )
    $path = Get-ProcessPathSafe -Process $Process
    if ([string]::IsNullOrWhiteSpace($path)) { throw "PID $($Process.Id) has no readable executable path." }
    $packageFullName = [CodexLegacyParityNative20260722]::GetProcessPackageFullName($Process.Handle)
    $descriptor = [ordered]@{
        processId = $Process.Id
        startUtc = $Process.StartTime.ToUniversalTime().ToString('o')
        executable = Get-FileIdentity -Path $path
        packageFullName = $packageFullName
    }
    if (-not [string]::IsNullOrWhiteSpace($WindowTitle)) {
        $descriptor.window = Get-ExactWindowDescriptor -ProcessIdValue $Process.Id -ExactTitle $WindowTitle -RequireVisible $RequireVisibleWindow
    }
    return $descriptor
}

function Get-ListenerDescriptor {
    param(
        [Parameter(Mandatory = $true)] [int] $Port,
        [int] $ExpectedOwner = -1
    )
    $listeners = @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop)
    if ($listeners.Count -eq 0) { throw "No TCP listener is present on port $Port." }
    $owners = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique)
    if ($owners.Count -ne 1) { throw "TCP port $Port has more than one listener owner." }
    if ($ExpectedOwner -ge 0 -and $owners[0] -ne $ExpectedOwner) {
        throw "TCP port $Port is owned by PID $($owners[0]), expected $ExpectedOwner."
    }
    $endpoints = @($listeners | ForEach-Object {
        '{0}:{1}|{2}|{3}' -f $_.LocalAddress, $_.LocalPort, $_.State, $_.OwningProcess
    } | Sort-Object)
    return [ordered]@{ port = $Port; ownerProcessId = [int]$owners[0]; endpoints = $endpoints }
}

function Get-EstablishedPgmConnection {
    param(
        [Parameter(Mandatory = $true)] [int] $AppProcessId,
        [Parameter(Mandatory = $true)] [int] $PgmProcessId,
        [Parameter(Mandatory = $true)] [int] $PgmPort
    )
    $connections = @(Get-NetTCPConnection -State Established -RemotePort $PgmPort -ErrorAction Stop |
        Where-Object { $_.OwningProcess -eq $AppProcessId })
    if ($connections.Count -ne 1) {
        throw "Expected one established app-to-PGM connection owned by PID $AppProcessId; found $($connections.Count)."
    }
    $connection = $connections[0]
    $localAddress = $null
    $remoteAddress = $null
    if (-not [Net.IPAddress]::TryParse([string]$connection.LocalAddress, [ref]$localAddress) -or
        -not [Net.IPAddress]::TryParse([string]$connection.RemoteAddress, [ref]$remoteAddress) -or
        -not [Net.IPAddress]::IsLoopback($localAddress) -or
        -not [Net.IPAddress]::IsLoopback($remoteAddress)) {
        throw 'The app-to-PGM connection is not entirely loopback.'
    }
    $reciprocal = @(Get-NetTCPConnection -State Established -LocalPort $PgmPort `
            -RemotePort $connection.LocalPort -ErrorAction Stop |
        Where-Object {
            $_.OwningProcess -eq $PgmProcessId -and
            [string]$_.LocalAddress -ceq [string]$connection.RemoteAddress -and
            [string]$_.RemoteAddress -ceq [string]$connection.LocalAddress
        })
    if ($reciprocal.Count -ne 1) {
        throw "Expected one reciprocal PGM-to-app connection owned by PID $PgmProcessId; found $($reciprocal.Count)."
    }
    $pgmSide = $reciprocal[0]
    return [ordered]@{
        appSide = [ordered]@{
            ownerProcessId = $connection.OwningProcess
            localAddress = $connection.LocalAddress
            localPort = $connection.LocalPort
            remoteAddress = $connection.RemoteAddress
            remotePort = $connection.RemotePort
            state = [string]$connection.State
        }
        pgmSide = [ordered]@{
            ownerProcessId = $pgmSide.OwningProcess
            localAddress = $pgmSide.LocalAddress
            localPort = $pgmSide.LocalPort
            remoteAddress = $pgmSide.RemoteAddress
            remotePort = $pgmSide.RemotePort
            state = [string]$pgmSide.State
        }
    }
}

function Get-CdpTargetDescriptor {
    param([Parameter(Mandatory = $true)] [int] $Port)
    $targets = @(Invoke-RestMethod -Uri "http://127.0.0.1:$Port/json/list" -Method Get -TimeoutSec 10)
    $pages = @($targets | Where-Object { $_.type -ceq 'page' -and $_.url -ceq $script:TargetUrl })
    if ($pages.Count -ne 1) { throw "Expected exactly one CDP parity target; found $($pages.Count)." }
    $target = $pages[0]
    if ([string]::IsNullOrWhiteSpace([string]$target.id) -or
        [string]::IsNullOrWhiteSpace([string]$target.webSocketDebuggerUrl)) {
        throw 'The parity CDP target is missing an ID or WebSocket endpoint.'
    }
    $endpoint = [Uri]::new([string]$target.webSocketDebuggerUrl)
    if ($endpoint.Scheme -cne 'ws' -or $endpoint.Host -cne '127.0.0.1' -or
        $endpoint.Port -ne $Port -or $endpoint.AbsolutePath -cne "/devtools/page/$($target.id)" -or
        -not [string]::IsNullOrEmpty($endpoint.Query) -or -not [string]::IsNullOrEmpty($endpoint.Fragment)) {
        throw 'The parity CDP WebSocket endpoint identity is not exact.'
    }
    return [ordered]@{
        id = [string]$target.id
        type = [string]$target.type
        title = [string]$target.title
        url = [string]$target.url
        webSocketDebuggerUrl = [string]$target.webSocketDebuggerUrl
    }
}

function Get-CutInventory {
    param(
        [Parameter(Mandatory = $true)] [string] $SourceDirectory,
        [Parameter(Mandatory = $true)] [string] $RuntimeDirectory
    )
    $sourceRoot = [IO.Path]::GetFullPath($SourceDirectory)
    $runtimeRoot = [IO.Path]::GetFullPath($RuntimeDirectory)
    $items = @()
    foreach ($code in $script:RouteCodes) {
        $sourcePath = Join-Path $sourceRoot "$code.t2s"
        $runtimePath = Join-Path $runtimeRoot "$code.t2s"
        $source = Get-FileIdentity -Path $sourcePath
        $runtime = Get-FileIdentity -Path $runtimePath
        if ($source.sha256 -cne $runtime.sha256 -or $source.length -ne $runtime.length) {
            throw "Runtime cut $code does not exactly match the authoritative legacy cut."
        }
        $items += [ordered]@{
            code = $code
            sourcePath = $source.path
            runtimePath = $runtime.path
            length = $runtime.length
            sha256 = $runtime.sha256
        }
    }
    return $items
}

function Get-AssetInventory {
    param(
        [Parameter(Mandatory = $true)] [string] $SourceDirectory,
        [Parameter(Mandatory = $true)] [string] $RuntimeDirectory
    )
    $sourceRoot = [IO.Path]::GetFullPath($SourceDirectory)
    $runtimeRoot = [IO.Path]::GetFullPath($RuntimeDirectory)
    $items = @()
    foreach ($relativePath in $script:S6001AssetRelativePaths) {
        $source = Get-FileIdentity -Path (Join-Path $sourceRoot $relativePath)
        $runtime = Get-FileIdentity -Path (Join-Path $runtimeRoot $relativePath)
        if ($source.sha256 -cne $runtime.sha256 -or $source.length -ne $runtime.length) {
            throw "Runtime asset $relativePath does not exactly match the authoritative legacy asset."
        }
        $items += [ordered]@{
            relativePath = $relativePath
            sourcePath = $source.path
            runtimePath = $runtime.path
            length = $runtime.length
            sha256 = $runtime.sha256
        }
    }
    return $items
}

function Get-CurrentPins {
    Initialize-NativeTypes
    if ($DebugPort -le 0 -or $DebugPort -gt 65535) { throw "Invalid CDP port: $DebugPort" }
    if ($ExpectedPgmPort -le 0 -or $ExpectedPgmPort -gt 65535) { throw "Invalid PGM port: $ExpectedPgmPort" }
    if ([string]::IsNullOrWhiteSpace($ExpectedPgmExecutable)) {
        throw 'Pass -ExpectedPgmExecutable or set MBN_STOCK_PGM_EXECUTABLE for runtime checks.'
    }

    $packages = @(Get-AppxPackage -Name $ExpectedPackageName -ErrorAction Stop |
        Where-Object { $_.Name -ceq $ExpectedPackageName })
    if ($packages.Count -ne 1) { throw "Expected one registered package '$ExpectedPackageName'; found $($packages.Count)." }
    $package = $packages[0]
    if ($package.IsDevelopmentMode -ne $true -or [string]$package.Architecture -cne 'X64') {
        throw 'Only the x64 loose Debug development package is authorized for this run.'
    }
    $installLocation = [IO.Path]::GetFullPath($package.InstallLocation)
    $workspaceRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
    $debugPackageRoot = [IO.Path]::GetFullPath((Join-Path $workspaceRoot `
        'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug'))
    $debugPrefix = $debugPackageRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
    if (-not $installLocation.StartsWith($debugPrefix, [StringComparison]::OrdinalIgnoreCase)) {
        throw "Package InstallLocation is outside the workspace x64 Debug boundary: $installLocation"
    }
    $debugRelative = $installLocation.Substring($debugPrefix.Length)
    $debugSegments = @($debugRelative.Split([IO.Path]::DirectorySeparatorChar) |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if ($debugSegments.Count -ne 3 -or
        $debugSegments[0] -notmatch '^net[0-9]+\.[0-9]+-windows[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -or
        $debugSegments[1] -cne 'win-x64' -or $debugSegments[2] -cne 'AppX') {
        throw "Package InstallLocation is not an exact Debug target-framework/win-x64/AppX path: $installLocation"
    }
    $appExecutable = Join-Path $installLocation 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe'
    if (-not [IO.File]::Exists($appExecutable)) { throw "Package app executable was not found under $installLocation." }

    $appProcess = Find-UniqueProcessByPath -ExpectedPath $appExecutable
    $pgmProcess = Find-UniqueProcessByPath -ExpectedPath $ExpectedPgmExecutable
    $app = Get-ProcessDescriptor -Process $appProcess -WindowTitle $ExpectedAppWindowTitle -RequireVisibleWindow $true
    $pgm = Get-ProcessDescriptor -Process $pgmProcess -WindowTitle $ExpectedPgmWindowTitle -RequireVisibleWindow $true
    if ($app.packageFullName -cne $package.PackageFullName) {
        throw "App PID $($app.processId) does not carry the registered package identity."
    }
    $pgm.networkWindow = Get-ExactWindowDescriptor -ProcessIdValue $pgmProcess.Id `
        -ExactTitle $ExpectedNetworkWindowTitle -RequireVisible $false

    $pgmListener = Get-ListenerDescriptor -Port $ExpectedPgmPort -ExpectedOwner $pgmProcess.Id
    $appConnection = Get-EstablishedPgmConnection -AppProcessId $appProcess.Id `
        -PgmProcessId $pgmProcess.Id -PgmPort $ExpectedPgmPort
    $cdpListener = Get-ListenerDescriptor -Port $DebugPort
    $cdpOwnerProcess = Get-Process -Id $cdpListener.ownerProcessId -ErrorAction Stop
    $cdpOwner = Get-ProcessDescriptor -Process $cdpOwnerProcess
    $cdpTarget = Get-CdpTargetDescriptor -Port $DebugPort

    $runtimeFiles = @()
    foreach ($relative in @(
            'MBN_STOCK_WEBVIEW.LegacyParityApp.exe',
            'MBN_STOCK_WEBVIEW.LegacyParityApp.dll',
            'MBN_STOCK_WEBVIEW.Playout.dll')) {
        $runtimeFiles += Get-FileIdentity -Path (Join-Path $installLocation $relative)
    }

    $nodePath = Get-ResolvedNodeExecutable
    $cutInventory = Get-CutInventory -SourceDirectory $OriginalCutsDirectory `
        -RuntimeDirectory (Join-Path $installLocation 'Cuts')
    $assetInventory = Get-AssetInventory -SourceDirectory $OriginalCutsDirectory `
        -RuntimeDirectory (Join-Path $installLocation 'Cuts')

    return [ordered]@{
        schemaVersion = 1
        package = [ordered]@{
            name = [string]$package.Name
            packageFullName = [string]$package.PackageFullName
            packageFamilyName = [string]$package.PackageFamilyName
            publisher = [string]$package.Publisher
            version = $package.Version.ToString()
            architecture = [string]$package.Architecture
            isDevelopmentMode = [bool]$package.IsDevelopmentMode
            installLocation = $installLocation
            debugPackageRoot = $debugPackageRoot
        }
        app = $app
        pgm = $pgm
        pgmListener = $pgmListener
        appToPgmConnection = $appConnection
        cdp = [ordered]@{
            listener = $cdpListener
            owner = $cdpOwner
            target = $cdpTarget
        }
        runtimeFiles = $runtimeFiles
        cutInventory = $cutInventory
        assetInventory = $assetInventory
        tools = [ordered]@{
            wrapper = Get-FileIdentity -Path $PSCommandPath
            node = Get-FileIdentity -Path $nodePath
            nodeStage = Get-FileIdentity -Path $script:NodeStageScript
        }
    }
}

function Assert-PinsExact {
    param([Parameter(Mandatory = $true)] [object] $Pinned)
    $current = Get-CurrentPins
    $expectedJson = Get-CanonicalJson -Value $Pinned
    $currentJson = Get-CanonicalJson -Value $current
    if ($currentJson -cne $expectedJson) {
        throw 'A pinned package, process, executable hash, window, listener, connection, CDP target, cut, or tool identity changed.'
    }
    return $current
}

function Get-NetworkCounts {
    param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Text)
    $lines = @($Text -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    return [ordered]@{
        helloRequest = @($lines | Where-Object { $_ -match '\[R\]\s+HELLO\b' }).Count
        helloSuccess = @($lines | Where-Object { $_ -match '\[S\]\s+SUCCESS\s+HELLO\b' }).Count
        loadSceneRequest = @($lines | Where-Object { $_ -match '\[R\]\s+LOAD_SCENE\b' }).Count
        loadSceneSuccess = @($lines | Where-Object { $_ -match '\[S\]\s+SUCCESS\s+LOAD_SCENE\b' }).Count
        prepareRequest = @($lines | Where-Object { $_ -match '\[R\]\s+SCENE_PREPARE\b' }).Count
        prepareSuccess = @($lines | Where-Object { $_ -match '\[S\]\s+SUCCESS\s+SCENE_PREPARE\b' }).Count
        playRequest = @($lines | Where-Object { $_ -match '\[R\]\s+PLAY\b' }).Count
        playSuccess = @($lines | Where-Object { $_ -match '\[S\]\s+SUCCESS\s+PLAY\b' }).Count
        scenePlayed = @($lines | Where-Object { $_ -match '\[A\]\s+SCENE_PLAYED\b' }).Count
        unloadRequest = @($lines | Where-Object { $_ -match '\[R\]\s+UNLOAD_SCENE\b' }).Count
        unloadSuccess = @($lines | Where-Object { $_ -match '\[S\]\s+SUCCESS\s+UNLOAD_SCENE\b' }).Count
        failure = @($lines | Where-Object { $_ -match '\bFAILURE\b' }).Count
        error = @($lines | Where-Object { $_ -match '\bERROR\b' }).Count
    }
}

function Read-StableNetworkText {
    param([Parameter(Mandatory = $true)] [IntPtr] $NetworkWindow)
    $lastError = $null
    for ($attempt = 1; $attempt -le 8; $attempt += 1) {
        try { return [CodexLegacyParityNative20260722]::ReadStableNetworkText($NetworkWindow) }
        catch {
            $lastError = $_
            Start-Sleep -Milliseconds 125
        }
    }
    throw $lastError
}

function Capture-WindowImage {
    param(
        [Parameter(Mandatory = $true)] [IntPtr] $Window,
        [Parameter(Mandatory = $true)] [string] $Path
    )
    if ([IO.File]::Exists($Path)) { throw "Evidence image already exists: $Path" }
    $rect = New-Object CodexLegacyParityNative20260722+Rect
    if (-not [CodexLegacyParityNative20260722]::GetWindowRect($Window, [ref]$rect)) {
        throw 'Window rectangle could not be read.'
    }
    $width = $rect.Right - $rect.Left
    $height = $rect.Bottom - $rect.Top
    if ($width -lt 100 -or $height -lt 100 -or $width -gt 8000 -or $height -gt 5000) {
        throw "Window rectangle is unsafe: ${width}x${height}."
    }
    # CopyFromScreen is required for the DirectX PGM surface, but it otherwise
    # records an unrelated foreground window when the target is occluded. Raise
    # only this exact pinned window without activating it, allow one repaint,
    # capture, and restore its original top-most state in a finally block.
    $wasTopMost = [CodexLegacyParityNative20260722]::IsTopMost($Window)
    if (-not [CodexLegacyParityNative20260722]::SetTopMostForCapture($Window, $true)) {
        throw 'The exact evidence window could not be raised for an unobscured capture.'
    }
    Start-Sleep -Milliseconds 250
    $bitmap = $null
    $graphics = $null
    try {
        $bitmap = [Drawing.Bitmap]::new($width, $height)
        $graphics = [Drawing.Graphics]::FromImage($bitmap)
        $graphics.CopyFromScreen($rect.Left, $rect.Top, 0, 0, $bitmap.Size)
        $bitmap.Save($Path, [Drawing.Imaging.ImageFormat]::Png)
    } finally {
        if ($null -ne $graphics) { $graphics.Dispose() }
        if ($null -ne $bitmap) { $bitmap.Dispose() }
        if (-not $wasTopMost -and
            -not [CodexLegacyParityNative20260722]::SetTopMostForCapture($Window, $false)) {
            throw 'The evidence window top-most state could not be restored.'
        }
    }
}

function Get-NetworkDelta {
    param(
        [Parameter(Mandatory = $true)] [string] $Current,
        [string] $PreviousPath = ''
    )
    if ([string]::IsNullOrWhiteSpace($PreviousPath)) {
        return [ordered]@{ anchorMatched = $null; anchorLength = 0; delta = $Current }
    }
    $previousFullPath = [IO.Path]::GetFullPath($PreviousPath)
    if (-not [IO.File]::Exists($previousFullPath)) { throw "Previous Network evidence is missing: $previousFullPath" }
    $previous = [IO.File]::ReadAllText($previousFullPath, [Text.Encoding]::UTF8)
    if ($previous.Length -eq 0) {
        return [ordered]@{ anchorMatched = $true; anchorLength = 0; delta = $Current }
    }
    $anchorLength = [Math]::Min(4096, $previous.Length)
    $anchor = $previous.Substring($previous.Length - $anchorLength)
    $first = $Current.IndexOf($anchor, [StringComparison]::Ordinal)
    $last = $Current.LastIndexOf($anchor, [StringComparison]::Ordinal)
    if ($first -lt 0 -or $first -ne $last) {
        throw 'The rolling Network Monitoring anchor is missing or non-unique.'
    }
    return [ordered]@{
        anchorMatched = $true
        anchorLength = $anchorLength
        delta = $Current.Substring($first + $anchorLength)
    }
}

function Capture-ReadOnlyEvidence {
    param(
        [Parameter(Mandatory = $true)] [object] $Pins,
        [Parameter(Mandatory = $true)] [string] $OutputDirectory,
        [string] $PreviousNetworkPath = ''
    )
    Initialize-NativeTypes
    $directory = [IO.Path]::GetFullPath($OutputDirectory)
    if (-not [IO.Directory]::Exists($directory)) { throw "Evidence stage directory is missing: $directory" }

    $pgmImage = Join-Path $directory 'pgm-screen.png'
    $appImage = Join-Path $directory 'app-screen.png'
    $networkPath = Join-Path $directory 'network-full.txt'
    $networkDeltaPath = Join-Path $directory 'network-delta.txt'
    $summaryPath = Join-Path $directory 'capture.json'

    $pgmWindow = [IntPtr]::new([long]$Pins.pgm.window.handle)
    $appWindow = [IntPtr]::new([long]$Pins.app.window.handle)
    $networkWindow = [IntPtr]::new([long]$Pins.pgm.networkWindow.handle)

    # Capture the PGM frame first. Capture-WindowImage temporarily changes only
    # the pinned window's z-order; it never focuses, restores, clicks, or sends a
    # command to either application.
    Capture-WindowImage -Window $pgmWindow -Path $pgmImage
    Capture-WindowImage -Window $appWindow -Path $appImage
    $networkText = Read-StableNetworkText -NetworkWindow $networkWindow
    Write-NewText -Path $networkPath -Text $networkText

    $deltaResult = Get-NetworkDelta -Current $networkText -PreviousPath $PreviousNetworkPath
    Write-NewText -Path $networkDeltaPath -Text $deltaResult.delta
    $summary = [ordered]@{
        schemaVersion = 1
        capturedAtUtc = [DateTime]::UtcNow.ToString('o')
        previousNetworkPath = $(if ([string]::IsNullOrWhiteSpace($PreviousNetworkPath)) { $null } else { [IO.Path]::GetFullPath($PreviousNetworkPath) })
        anchorMatched = $deltaResult.anchorMatched
        anchorLength = $deltaResult.anchorLength
        fullCharacters = $networkText.Length
        deltaCharacters = $deltaResult.delta.Length
        counts = Get-NetworkCounts -Text $deltaResult.delta
        pgmImagePath = $pgmImage
        appImagePath = $appImage
        networkFullPath = $networkPath
        networkDeltaPath = $networkDeltaPath
    }
    Write-NewJson -Path $summaryPath -Value $summary
    return $summary
}

function Get-WorkflowStages {
    param([Parameter(Mandatory = $true)] [string] $Name)
    if ($Name -ceq 'Recovery8001') {
        return @([pscustomobject]@{ action = 'take-out-recovery-8001'; routeIndex = 19; pageIndex = $null })
    }
    if ($Name -ceq 'Recovery5077Page2') {
        return @([pscustomobject]@{ action = 'take-out-recovery-5077-page2'; routeIndex = $null; pageIndex = 2 })
    }
    if ($Name -ceq 'ThirtyRoutes20To29') {
        $resume = @([pscustomobject]@{ action = 'resume-30-manifest'; routeIndex = $null; pageIndex = $null })
        for ($index = 20; $index -lt 30; $index += 1) {
            foreach ($actionName in @('activate-30', 'take-in-30-fast', 'wait-30', 'take-out-30')) {
                $resume += [pscustomobject]@{ action = $actionName; routeIndex = $index; pageIndex = $null }
            }
        }
        return $resume
    }
    $items = @([pscustomobject]@{ action = 'bootstrap'; routeIndex = $null; pageIndex = $null })
    switch ($Name) {
        'Core5001' {
            foreach ($actionName in @('setup-5001-5074', 'take-in-5001', 'next-5074', 'page-next-5074', 'take-out-5074')) {
                $items += [pscustomobject]@{ action = $actionName; routeIndex = $null; pageIndex = $null }
            }
        }
        'ThirtyRoutes' {
            foreach ($actionName in @('build-30-base', 'build-30-comparison', 'build-30-financial', 'build-30-manual')) {
                $items += [pscustomobject]@{ action = $actionName; routeIndex = $null; pageIndex = $null }
            }
            for ($index = 0; $index -lt 30; $index += 1) {
                foreach ($actionName in @('activate-30', 'take-in-30-fast', 'wait-30', 'take-out-30')) {
                    $items += [pscustomobject]@{ action = $actionName; routeIndex = $index; pageIndex = $null }
                }
            }
        }
        'PagedBoundary' {
            $items += [pscustomobject]@{ action = 'build-paged'; routeIndex = $null; pageIndex = $null }
            $items += [pscustomobject]@{ action = 'take-in-5077'; routeIndex = $null; pageIndex = $null }
            for ($page = 1; $page -le 19; $page += 1) {
                $items += [pscustomobject]@{ action = 'page-next-5077'; routeIndex = $null; pageIndex = $page }
            }
            $items += [pscustomobject]@{ action = 'next-5088'; routeIndex = $null; pageIndex = $null }
            $items += [pscustomobject]@{ action = 'take-out-5088'; routeIndex = $null; pageIndex = $null }
        }
        'S6001AssetIndependent' {
            $items += [pscustomobject]@{ action = 'build-s6001-asset-independent'; routeIndex = $null; pageIndex = $null }
            for ($index = 0; $index -lt 4; $index += 1) {
                foreach ($actionName in @('activate-s6001', 'take-in-s6001', 'wait-s6001', 'take-out-s6001')) {
                    $items += [pscustomobject]@{ action = $actionName; routeIndex = $index; pageIndex = $null }
                }
            }
        }
        default { throw "Unknown workflow: $Name" }
    }
    return $items
}

function Get-SuccessfulStageRecords {
    param([Parameter(Mandatory = $true)] [string] $Directory)
    $records = @()
    foreach ($child in @(Get-ChildItem -LiteralPath $Directory -Directory -ErrorAction Stop |
            Where-Object { $_.Name -match '^(?!0000-)\d{4}-' } | Sort-Object Name)) {
        $successPath = Join-Path $child.FullName 'SUCCESS.json'
        if ([IO.File]::Exists($successPath)) {
            $records += (Get-Content -LiteralPath $successPath -Raw -Encoding UTF8 | ConvertFrom-Json)
        }
    }
    return $records
}

function Assert-CompletedStageSequence {
    param(
        [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Records,
        [Parameter(Mandatory = $true)] [object[]] $ExpectedStages
    )
    if ($Records.Count -gt $ExpectedStages.Count) { throw 'Evidence contains more successful stages than the workflow.' }
    for ($index = 0; $index -lt $Records.Count; $index += 1) {
        $record = $Records[$index]
        $expected = $ExpectedStages[$index]
        $recordRoute = if ($null -ne $record.stage.routeIndex) { [int]$record.stage.routeIndex } else { $null }
        $recordPage = if ($null -ne $record.stage.pageIndex) { [int]$record.stage.pageIndex } else { $null }
        if ([int]$record.sequence -ne ($index + 1) -or
            [string]$record.stage.action -cne [string]$expected.action -or
            $recordRoute -ne $expected.routeIndex -or $recordPage -ne $expected.pageIndex) {
            throw "Successful evidence sequence is not exact at index $index."
        }
    }
}

function Get-PreviousNetworkPath {
    param([Parameter(Mandatory = $true)] [string] $Directory)
    $candidates = @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter 'network-full.txt' -ErrorAction Stop |
        Sort-Object FullName)
    if ($candidates.Count -eq 0) { return '' }
    return $candidates[-1].FullName
}

function New-StopLatch {
    param(
        [Parameter(Mandatory = $true)] [string] $Directory,
        [Parameter(Mandatory = $true)] [string] $Reason,
        [string] $StageDirectory = '',
        [object] $Details = $null
    )
    $path = Join-Path $Directory 'STOPPED.json'
    if ([IO.File]::Exists($path)) { return }
    $record = [ordered]@{
        schemaVersion = 1
        stoppedAtUtc = [DateTime]::UtcNow.ToString('o')
        reason = $Reason
        stageDirectory = $(if ([string]::IsNullOrWhiteSpace($StageDirectory)) { $null } else { $StageDirectory })
        details = $Details
        rule = 'No later playout command may be dispatched from this run directory.'
    }
    try { Write-NewJson -Path $path -Value $record } catch { if (-not [IO.File]::Exists($path)) { throw } }
}

function ConvertTo-NodeArgumentString {
    param([Parameter(Mandatory = $true)] [object[]] $Values)
    $quoted = foreach ($value in $Values) {
        $text = [string]$value
        if ($text.Contains('"') -or $text.Contains([char]0)) {
            throw 'A Node stage argument contains a forbidden character.'
        }
        '"' + $text + '"'
    }
    return ($quoted -join ' ')
}

function Invoke-NodeStage {
    param(
        [Parameter(Mandatory = $true)] [object] $Manifest,
        [Parameter(Mandatory = $true)] [object] $ExpectedStage,
        [Parameter(Mandatory = $true)] [string] $OutputDirectory
    )
    $stdoutPath = Join-Path $OutputDirectory 'node-stdout.json'
    $stderrPath = Join-Path $OutputDirectory 'node-stderr.json'
    $nodePath = [string]$Manifest.pins.tools.node.path
    $nodeScriptPath = [string]$Manifest.pins.tools.nodeStage.path
    $arguments = @($nodeScriptPath, '--port', [string]$Manifest.debugPort, '--action', [string]$ExpectedStage.action)
    if ($null -ne $ExpectedStage.routeIndex) { $arguments += @('--route-index', [string]$ExpectedStage.routeIndex) }
    if ($null -ne $ExpectedStage.pageIndex) { $arguments += @('--page-index', [string]$ExpectedStage.pageIndex) }

    $start = [Diagnostics.ProcessStartInfo]::new()
    $start.FileName = $nodePath
    $start.WorkingDirectory = $PSScriptRoot
    $start.UseShellExecute = $false
    $start.CreateNoWindow = $true
    $start.RedirectStandardOutput = $true
    $start.RedirectStandardError = $true
    $start.Arguments = ConvertTo-NodeArgumentString -Values $arguments

    $process = [Diagnostics.Process]::new()
    $process.StartInfo = $start
    try {
        if (-not $process.Start()) { throw 'The Node stage process did not start.' }
        $stdoutTask = $process.StandardOutput.ReadToEndAsync()
        $stderrTask = $process.StandardError.ReadToEndAsync()
        $exited = $process.WaitForExit($StageTimeoutSeconds * 1000)
        if (-not $exited) {
            $terminated = $false
            try {
                $process.Kill()
                $terminated = $process.WaitForExit(5000)
            } catch { }
            if ($terminated) {
                $process.WaitForExit()
                Write-NewText -Path $stdoutPath -Text $stdoutTask.GetAwaiter().GetResult()
                Write-NewText -Path $stderrPath -Text $stderrTask.GetAwaiter().GetResult()
            }
            return [ordered]@{
                ok = $false
                timedOut = $true
                exitCode = $null
                parsed = $null
                error = "Node stage exceeded $StageTimeoutSeconds seconds. Outcome is unknown."
                stdoutPath = $stdoutPath
                stderrPath = $stderrPath
            }
        }

        $process.WaitForExit()
        $exitCode = [int]$process.ExitCode
        $stdout = $stdoutTask.GetAwaiter().GetResult()
        $stderr = $stderrTask.GetAwaiter().GetResult()
        Write-NewText -Path $stdoutPath -Text $stdout
        Write-NewText -Path $stderrPath -Text $stderr
        $stdout = $stdout.Trim()
        $stderr = $stderr.Trim()
        $parsed = $null
        $parseError = $null
        try {
            $candidate = if (-not [string]::IsNullOrWhiteSpace($stdout)) { $stdout } else { $stderr }
            if ([string]::IsNullOrWhiteSpace($candidate)) { throw 'Node stage returned no JSON.' }
            $parsed = $candidate | ConvertFrom-Json
        } catch { $parseError = $_.Exception.Message }
        $ok = $exitCode -eq 0 -and $null -ne $parsed -and $parsed.ok -eq $true
        return [ordered]@{
            ok = $ok
            timedOut = $false
            exitCode = $exitCode
            parsed = $parsed
            error = $parseError
            stdoutPath = $stdoutPath
            stderrPath = $stderrPath
        }
    } finally {
        $process.Dispose()
    }
}

function Invoke-StaticAudit {
    $nodePath = Get-ResolvedNodeExecutable
    if (-not [IO.File]::Exists($script:NodeStageScript)) { throw "Node stage script is missing: $script:NodeStageScript" }
    $tokens = $null
    $errors = $null
    [Management.Automation.Language.Parser]::ParseFile($PSCommandPath, [ref]$tokens, [ref]$errors) | Out-Null
    if ($errors.Count -ne 0) { throw "PowerShell parser reported $($errors.Count) error(s): $($errors[0].Message)" }

    $nodeCheckOutput = @(& $nodePath --check $script:NodeStageScript 2>&1)
    if ($LASTEXITCODE -ne 0) { throw "Node syntax check failed: $($nodeCheckOutput -join [Environment]::NewLine)" }
    $nodeSource = [IO.File]::ReadAllText($script:NodeStageScript, [Text.Encoding]::UTF8)
    foreach ($required in @('OUTCOME_UNKNOWN', 'TARGET_IDENTITY_MISMATCH', 'take-in-30-fast', 'page-next-5077', 'take-out-recovery-5077-page2', 'build-s6001-asset-independent', 'take-in-s6001')) {
        if (-not $nodeSource.Contains($required)) { throw "Node stage script is missing required safety token: $required" }
    }

    $sourceRoot = [IO.Path]::GetFullPath($OriginalCutsDirectory)
    $cuts = @()
    foreach ($code in $script:RouteCodes) {
        $path = Join-Path $sourceRoot "$code.t2s"
        $cuts += Get-FileIdentity -Path $path
    }
    $assets = @()
    foreach ($relativePath in $script:S6001AssetRelativePaths) {
        $assets += Get-FileIdentity -Path (Join-Path $sourceRoot $relativePath)
    }
    $workflowCounts = [ordered]@{}
    foreach ($name in @('Core5001', 'ThirtyRoutes', 'PagedBoundary', 'S6001AssetIndependent', 'Recovery8001', 'Recovery5077Page2', 'ThirtyRoutes20To29')) {
        $workflowCounts[$name] = @(Get-WorkflowStages -Name $name).Count
    }
    return [ordered]@{
        ok = $true
        audit = 'static-only'
        auditedAtUtc = [DateTime]::UtcNow.ToString('o')
        wrapper = Get-FileIdentity -Path $PSCommandPath
        node = Get-FileIdentity -Path $nodePath
        nodeStage = Get-FileIdentity -Path $script:NodeStageScript
        authoritativeCutCount = $cuts.Count
        # OrderedDictionary keys are not projected as ordinary properties by
        # Select-Object in Windows PowerShell 5.1. Preserve the validated
        # identity dictionaries directly so the evidence cannot silently emit
        # null paths or hashes.
        authoritativeCutHashes = @($cuts)
        authoritativeAssetCount = $assets.Count
        authoritativeAssetHashes = @($assets)
        workflowStageCounts = $workflowCounts
        runtimeProcessesQueried = $false
        cdpAttached = $false
        nativeMessagesSent = 0
    }
}

function Resolve-EvidenceDirectory {
    if ([string]::IsNullOrWhiteSpace($EvidenceDirectory)) {
        throw "-EvidenceDirectory is required for action '$Action'."
    }
    return [IO.Path]::GetFullPath($EvidenceDirectory)
}

function Invoke-InitializeRun {
    $directory = Resolve-EvidenceDirectory
    if ([IO.Directory]::Exists($directory) -or [IO.File]::Exists($directory)) {
        throw "Initialization never overwrites an existing evidence path: $directory"
    }
    $pins = Get-CurrentPins
    [IO.Directory]::CreateDirectory($directory) | Out-Null
    $baselineDirectory = Join-Path $directory '0000-baseline'
    [IO.Directory]::CreateDirectory($baselineDirectory) | Out-Null
    $manifest = [ordered]@{
        schemaVersion = 1
        runId = [IO.Path]::GetFileName($directory)
        initializedAtUtc = [DateTime]::UtcNow.ToString('o')
        workflow = $Workflow
        debugPort = $DebugPort
        targetUrl = $script:TargetUrl
        discovery = [ordered]@{
            packageName = $ExpectedPackageName
            pgmExecutable = [string]$pins.pgm.executable.path
            appWindowTitle = [string]$pins.app.window.title
            pgmWindowTitle = [string]$pins.pgm.window.title
            networkWindowTitle = [string]$pins.pgm.networkWindow.title
            originalCutsDirectory = [IO.Path]::GetFullPath($OriginalCutsDirectory)
            nodeExecutable = [string]$pins.tools.node.path
            pgmPort = [int]$pins.pgmListener.port
        }
        rules = @(
            'One Node invocation dispatches at most one user-visible playout command.',
            'No playout command is retried after timeout, OutcomeUnknown, or identity mismatch.',
            'STOPPED.json permanently blocks later commands in this evidence directory.',
            'Playlist rows are built in memory; no named playlist fixture is loaded.'
        )
        expectedStages = Get-WorkflowStages -Name $Workflow
        pins = $pins
        baselineNetworkPath = Join-Path $baselineDirectory 'network-full.txt'
    }
    Write-NewJson -Path (Join-Path $directory 'manifest.json') -Value $manifest
    try {
        $capture = Capture-ReadOnlyEvidence -Pins $pins -OutputDirectory $baselineDirectory
        Assert-PinsExact -Pinned $pins | Out-Null
        Write-NewJson -Path (Join-Path $baselineDirectory 'SUCCESS.json') -Value ([ordered]@{
            initializedAtUtc = [DateTime]::UtcNow.ToString('o')
            capture = $capture
        })
    } catch {
        New-StopLatch -Directory $directory -Reason 'Initialization evidence or post-capture identity validation failed.' `
            -StageDirectory $baselineDirectory -Details $_.Exception.Message
        throw
    }
    return $manifest
}

function Read-RunManifest {
    param([Parameter(Mandatory = $true)] [string] $Directory)
    $manifestPath = Join-Path $Directory 'manifest.json'
    if (-not [IO.File]::Exists($manifestPath)) { throw "Run manifest is missing: $manifestPath" }
    $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
    if ($manifest.schemaVersion -ne 1 -or $manifest.targetUrl -cne $script:TargetUrl -or
        $null -eq $manifest.discovery) {
        throw 'Run manifest schema or target identity is unsupported.'
    }
    return $manifest
}

function Use-ManifestDiscoverySettings {
    param([Parameter(Mandatory = $true)] [object] $Manifest)
    $script:DebugPort = [int]$Manifest.debugPort
    $script:ExpectedPgmPort = [int]$Manifest.discovery.pgmPort
    $script:ExpectedPackageName = [string]$Manifest.discovery.packageName
    $script:ExpectedPgmExecutable = [string]$Manifest.discovery.pgmExecutable
    $script:ExpectedAppWindowTitle = [string]$Manifest.discovery.appWindowTitle
    $script:ExpectedPgmWindowTitle = [string]$Manifest.discovery.pgmWindowTitle
    $script:ExpectedNetworkWindowTitle = [string]$Manifest.discovery.networkWindowTitle
    $script:OriginalCutsDirectory = [string]$Manifest.discovery.originalCutsDirectory
    $script:NodeExecutable = [string]$Manifest.discovery.nodeExecutable
}

function Invoke-InspectRun {
    $directory = Resolve-EvidenceDirectory
    $manifest = Read-RunManifest -Directory $directory
    Use-ManifestDiscoverySettings -Manifest $manifest
    $current = Assert-PinsExact -Pinned $manifest.pins
    $completed = @(Get-SuccessfulStageRecords -Directory $directory)
    $expected = @(Get-WorkflowStages -Name ([string]$manifest.workflow))
    Assert-CompletedStageSequence -Records $completed -ExpectedStages $expected
    return [ordered]@{
        ok = $true
        action = 'Inspect'
        stopped = [IO.File]::Exists((Join-Path $directory 'STOPPED.json'))
        completedStageCount = $completed.Count
        totalStageCount = $expected.Count
        nextStage = $(if ($completed.Count -lt $expected.Count) { $expected[$completed.Count] } else { $null })
        pins = $current
        inspectedAtUtc = [DateTime]::UtcNow.ToString('o')
        nativeMessagesSent = 0
    }
}

function Invoke-RunStage {
    param([bool] $UseExpectedStage = $false)
    $directory = Resolve-EvidenceDirectory
    $stopPath = Join-Path $directory 'STOPPED.json'
    if ([IO.File]::Exists($stopPath)) {
        throw "This evidence run is permanently stopped. Inspect: $stopPath"
    }
    $manifest = Read-RunManifest -Directory $directory
    Use-ManifestDiscoverySettings -Manifest $manifest
    if (-not $UseExpectedStage -and [string]::IsNullOrWhiteSpace($Stage)) { throw '-Stage is required for RunStage.' }

    $lockPath = Join-Path $directory 'RUNNING.lock'
    $lockStream = $null
    try {
        $lockStream = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
    } catch {
        throw "Another stage is running, or an interrupted lock requires inspection: $lockPath"
    }

    try {
        $completed = @(Get-SuccessfulStageRecords -Directory $directory)
        $expectedStages = @(Get-WorkflowStages -Name ([string]$manifest.workflow))
        Assert-CompletedStageSequence -Records $completed -ExpectedStages $expectedStages
        if ($completed.Count -ge $expectedStages.Count) { throw 'All workflow stages are already complete.' }
        $expected = $expectedStages[$completed.Count]
        if (-not $UseExpectedStage) {
            $requestedRoute = if ($RouteIndex -ge 0) { $RouteIndex } else { $null }
            $requestedPage = if ($PageIndex -ge 0) { $PageIndex } else { $null }
            if ($Stage -cne $expected.action -or $requestedRoute -ne $expected.routeIndex -or $requestedPage -ne $expected.pageIndex) {
                throw "Out-of-order stage request. Expected action=$($expected.action), routeIndex=$($expected.routeIndex), pageIndex=$($expected.pageIndex)."
            }
        }

        $sequence = $completed.Count + 1
        $safeStage = $expected.action -replace '[^a-z0-9-]', '-'
        $stageDirectory = Join-Path $directory ('{0:D4}-{1}-{2}' -f $sequence, $safeStage, [Guid]::NewGuid().ToString('N').Substring(0, 8))
        [IO.Directory]::CreateDirectory($stageDirectory) | Out-Null
        Write-NewJson -Path (Join-Path $stageDirectory 'REQUEST.json') -Value ([ordered]@{
            sequence = $sequence
            requestedAtUtc = [DateTime]::UtcNow.ToString('o')
            stage = $expected
        })

        try {
            $beforePins = Assert-PinsExact -Pinned $manifest.pins
            Write-NewJson -Path (Join-Path $stageDirectory 'pins-before.json') -Value $beforePins
        } catch {
            New-StopLatch -Directory $directory -Reason 'Pinned runtime identity changed before stage dispatch.' `
                -StageDirectory $stageDirectory -Details $_.Exception.Message
            throw
        }

        $nodeResult = Invoke-NodeStage -Manifest $manifest -ExpectedStage $expected -OutputDirectory $stageDirectory
        Write-NewJson -Path (Join-Path $stageDirectory 'node-result.json') -Value $nodeResult

        $postIdentityError = $null
        try {
            $afterPins = Assert-PinsExact -Pinned $manifest.pins
            Write-NewJson -Path (Join-Path $stageDirectory 'pins-after.json') -Value $afterPins
        } catch { $postIdentityError = $_.Exception.Message }

        $capture = $null
        $captureError = $null
        if ([string]::IsNullOrWhiteSpace($postIdentityError)) {
            try {
                $previousNetworkPath = Get-PreviousNetworkPath -Directory $directory
                # Exclude the current stage file if an earlier partial capture somehow exists.
                if ($previousNetworkPath -like "$stageDirectory*") {
                    $previousCandidates = @(Get-ChildItem -LiteralPath $directory -Recurse -File -Filter 'network-full.txt' |
                        Where-Object { $_.FullName -notlike "$stageDirectory*" } | Sort-Object FullName)
                    $previousNetworkPath = if ($previousCandidates.Count -gt 0) { $previousCandidates[-1].FullName } else { '' }
                }
                $capture = Capture-ReadOnlyEvidence -Pins $manifest.pins -OutputDirectory $stageDirectory `
                    -PreviousNetworkPath $previousNetworkPath
                $afterCapturePins = Assert-PinsExact -Pinned $manifest.pins
                Write-NewJson -Path (Join-Path $stageDirectory 'pins-after-capture.json') -Value $afterCapturePins
            } catch { $captureError = $_.Exception.Message }
        } else {
            $captureError = 'Capture was skipped because the pinned runtime identity had already changed.'
        }

        $failureReasons = @()
        if (-not $nodeResult.ok) { $failureReasons += 'Node one-shot stage failed, timed out, or returned invalid JSON.' }
        if (-not [string]::IsNullOrWhiteSpace($postIdentityError)) { $failureReasons += 'Pinned runtime identity changed after stage execution.' }
        if (-not [string]::IsNullOrWhiteSpace($captureError)) { $failureReasons += 'Read-only evidence capture or rolling Network anchor validation failed.' }
        if ($failureReasons.Count -gt 0) {
            $failure = [ordered]@{
                failedAtUtc = [DateTime]::UtcNow.ToString('o')
                reasons = $failureReasons
                node = $nodeResult
                postIdentityError = $postIdentityError
                captureError = $captureError
                capture = $capture
            }
            Write-NewJson -Path (Join-Path $stageDirectory 'FAILURE.json') -Value $failure
            New-StopLatch -Directory $directory -Reason ($failureReasons -join ' ') `
                -StageDirectory $stageDirectory -Details $failure
            throw "Stage failed and the run is now stopped: $($failureReasons -join ' ')"
        }

        $success = [ordered]@{
            schemaVersion = 1
            sequence = $sequence
            completedAtUtc = [DateTime]::UtcNow.ToString('o')
            stage = $expected
            node = $nodeResult.parsed
            capture = $capture
        }
        Write-NewJson -Path (Join-Path $stageDirectory 'SUCCESS.json') -Value $success
        return $success
    } finally {
        if ($null -ne $lockStream) { $lockStream.Dispose() }
        if ([IO.File]::Exists($lockPath)) { [IO.File]::Delete($lockPath) }
    }
}

switch ($Action) {
    'StaticAudit' {
        Invoke-StaticAudit | ConvertTo-Json -Depth 40 -Compress
    }
    'DryPreflight' {
        $pins = Get-CurrentPins
        [ordered]@{
            ok = $true
            action = 'DryPreflight'
            inspectedAtUtc = [DateTime]::UtcNow.ToString('o')
            pins = $pins
            evidenceWritten = $false
            cdpAttached = $false
            nativeMessagesSent = 0
        } | ConvertTo-Json -Depth 40 -Compress
    }
    'Initialize' {
        Invoke-InitializeRun | ConvertTo-Json -Depth 40 -Compress
    }
    'Inspect' {
        Invoke-InspectRun | ConvertTo-Json -Depth 40 -Compress
    }
    'RunStage' {
        Invoke-RunStage -UseExpectedStage $false | ConvertTo-Json -Depth 40 -Compress
    }
    'RunNext' {
        Invoke-RunStage -UseExpectedStage $true | ConvertTo-Json -Depth 40 -Compress
    }
    default { throw "Unsupported action: $Action" }
}
