param( [string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il", [string]$BinaryDir = "runs/offline-real-client-window/full/zyl/Impp", [string]$OutputDir = "runs/offline-broute-reverse", [string]$ReportPath = "docs/source-discovery/2026-07-12-broute-bridge-static-map.md", [int]$WindowRadius = 4, [int]$MaxEvidencePerCandidate = 18 ) $ErrorActionPreference = "Stop" $repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path $ilDirFull = Join-Path $repo $IlDir $binaryDirFull = Join-Path $repo $BinaryDir $outDir = Join-Path $repo $OutputDir $reportFullPath = Join-Path $repo $ReportPath New-Item -ItemType Directory -Force -Path $outDir | Out-Null New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null $textSources = @( @{ path = "IMPlatformClient.exe.il"; role = "main client IL; login, plugin manager, local HTTP starter, chat/runtime" }, @{ path = "IMPP.Common.dll.il"; role = "common helpers; WinCommand, ProcessComm, DTOs" }, @{ path = "IMPP.ISphere.exe.il"; role = "ISphere companion app IL; CEF and command helpers" }, @{ path = "IMPP.UI.dll.il"; role = "UI/browser support IL" }, @{ path = "IMPP.Service.dll.il"; role = "file service implementation IL" }, @{ path = "smack.dll.il"; role = "XMPP/socks5/network IL" } ) $binarySources = @( @{ path = "HttpServerLib.dll"; role = "local HTTP server framework binary" }, @{ path = "IMPlatformClient.Web.exe"; role = "CEF/web helper process binary" }, @{ path = "INetwork.dll"; role = "native network abstraction" }, @{ path = "IOClientNetwork.dll"; role = "native network implementation" }, @{ path = "IMPlatformClient.exe"; role = "main client binary" } ) $liveProbeSources = @( "runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/services_registry_shortcuts.json", "runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/network_inventory.json" ) $candidates = @( @{ key = "local_http_win_server" display = "frmLogin.StartWinServer / HttpServerLib.HttpService" category = "localhost_http" classification = "requires_logged_in_process" usable_for_broute_send = $false recommendation = "do_not_pursue_before_in_process_adapter" reason = "Static IL builds a local HTTP service from logged-in user/auth state, but no send route or active live local listener was confirmed." patterns = @("StartWinServer", "HttpServerLib", "HttpService::\.ctor", "HttpServer::Start", "GetFreePort", "0\.0\.0\.0", "messageAuth", "RepairDatabase") }, @{ key = "http_route_framework" display = "HttpServerLib route/module framework" category = "localhost_http" classification = "static_only_unknown" usable_for_broute_send = $false recommendation = "no_existing_ipc_first" reason = "HttpServerLib has ServiceModule/RouteAttribute primitives, but client IL does not show RegisterModule or send-message routes." patterns = @("RouteAttribute", "RouteMethod", "ServiceModule", "ServiceRoute", "RegisterModule", "OnGet\s*\(", "OnPost\s*\(", "ExecuteRoute", "SearchRoute") }, @{ key = "plugin_named_pipe_stub" display = "PluginLightAppMulProcessCommunication NamedPipeServerStream" category = "named_pipe" classification = "not_a_bridge" usable_for_broute_send = $false recommendation = "do_not_use" reason = "A NamedPipeServerStream field exists, but StartLightAppMultProssCommunication has a one-instruction ret body." patterns = @("NamedPipeServerStream", "PluginLightAppMulProcessCommunication", "StartLightAppMultProssCommunication", "MulProcess", "LightApp") }, @{ key = "process_comm_wm_copydata" display = "IMPP.Common.Helper.ProcessComm WM_COPYDATA helper" category = "window_message" classification = "static_only_unknown" usable_for_broute_send = $false recommendation = "not_a_send_bridge" reason = "ProcessComm can send WM_COPYDATA to a foreground or titled window, but no receive handler or send-command contract is confirmed." patterns = @("IMPP\.Common\.Helper\.ProcessComm", "\bProcessComm\b", "COPYDATASTRUCT", "WM_COPYDATA", "SendMessageToTargetWindow", "FindWindow\(string lpClassName", "GetForegroundWindow\(\)") }, @{ key = "plugin_manager_command_surface" display = "PluginManager / IMPlugin command surface" category = "plugin" classification = "requires_logged_in_process" usable_for_broute_send = $false recommendation = "plugin_extension_not_first" reason = "Plugin evidence is UI/app-store/context-menu oriented and depends on logged-in PluginManager state; no Assembly.Load/LoadFrom or send API bridge was confirmed." patterns = @("PluginManager", "IMPlugin::get_Command", "LoadPluginContextMenu", "FrmAppStore", "FrmCefHomePage", "ShowPlugins", "Assembly\.Load", "LoadFrom", "AddPluginInfo") }, @{ key = "cef_web_helper" display = "IMPlatformClient.Web / CEF helper" category = "child_process" classification = "requires_logged_in_process" usable_for_broute_send = $false recommendation = "preview_only_not_send" reason = "The web helper/CEF surface exists, but static evidence points to UI/plugin browser support, not an external send/file API." patterns = @("IMPlatformClient\.Web", "CefProcessName", "CefRuntime", "WinCefBrowser", "NavigateTo", "SendProcessMessage", "V8Context") }, @{ key = "native_network_stack" display = "INetwork / IOClientNetwork native stack" category = "native_network" classification = "not_a_bridge" usable_for_broute_send = $false recommendation = "do_not_pursue_as_ipc" reason = "Native NETIO send/recv symbols are low-level transport, not a local command bridge into logged-in chat objects." patterns = @("INetwork", "IOClientNetwork", "NETIO_", "NETIO_C_", "StreamClient", "RegistRecvCallback", "SendPacket", "BasePipe") }, @{ key = "live_probe_named_pipe" display = "returned live-probe named pipe inventory" category = "named_pipe" classification = "static_only_unknown" usable_for_broute_send = $false recommendation = "do_not_assume_isphere_api" reason = "Live probe saw \\.\\pipe\\zfpinject-msg-server, but no matching iSphere static string or send contract was found in the configured samples." patterns = @("zfpinject-msg-server", "named_pipes", "connections", "remote_port", "10088") }, @{ key = "local_tcp_listener_candidates" display = "local TcpListener candidates" category = "tcp_listener" classification = "not_a_bridge" usable_for_broute_send = $false recommendation = "not_existing_ipc" reason = "TcpListener evidence is tied to TCP file manage or smack socks5, not a command API for text/file send. Returned live probe had only one remote XMPP connection." patterns = @("TcpListener", "TCPFileManager::_listener", "TCPSocks5Server", "GetActiveTcpListeners", "BeginAcceptSocket", "remote_port") } ) function Get-RelativePath([string]$Path) { if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) { return $Path.Substring($repo.Length).TrimStart('\', '/') } return $Path } function Get-TextEvidenceKind([string]$Line) { if ($Line -match "^\s*\.class\b") { return "type_definition" } if ($Line -match "^\s*\.method\b") { return "method_header" } if ($Line -match "\bend of method\b") { return "method_end" } if ($Line -match "\bcall(?:virt)?\b") { return "callsite" } if ($Line -match "\bnewobj\b") { return "constructor_call" } if ($Line -match "\bldstr\b") { return "string_literal" } if ($Line -match "^\s*(instance|static|StartWinServer|GetFreePort|SendMessageToTargetWindow)\b.*\(") { return "signature_continuation" } return "reference" } function Get-MethodContext($Lines, [int]$Index) { $start = [Math]::Max(0, $Index - 120) $methodHeader = $null $classHeader = $null for ($i = $Index; $i -ge $start; $i--) { $line = $Lines[$i] if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") { $methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() } } if ($null -eq $classHeader -and $line -match "^\s*\.class\b") { $classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() } } if ($methodHeader -and $classHeader) { break } } [pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader } } function New-TextEvidence($Lines, [int]$Index, [string]$SourceRel) { $from = [Math]::Max(0, $Index - $WindowRadius) $to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius) $window = New-Object System.Collections.Generic.List[object] for ($i = $from; $i -le $to; $i++) { $window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() }) } $context = Get-MethodContext -Lines $Lines -Index $Index [pscustomobject]@{ source = $SourceRel source_type = "text_il" line_number = $Index + 1 string_index = $null kind = Get-TextEvidenceKind -Line $Lines[$Index] text = $Lines[$Index].Trim() method_header = $context.method_header class_header = $context.class_header window = $window } } function Get-PrintableStrings([string]$Path, [int]$MinLength = 4) { $bytes = [System.IO.File]::ReadAllBytes($Path) $strings = New-Object System.Collections.Generic.List[string] $sb = New-Object System.Text.StringBuilder foreach ($b in $bytes) { if ($b -ge 32 -and $b -le 126) { [void]$sb.Append([char]$b) } else { if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) } $sb.Clear() | Out-Null } } if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) } $sb = New-Object System.Text.StringBuilder for ($i = 0; $i -lt ($bytes.Length - 1); $i += 2) { $b = $bytes[$i] $z = $bytes[$i + 1] if ($z -eq 0 -and $b -ge 32 -and $b -le 126) { [void]$sb.Append([char]$b) } else { if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) } $sb.Clear() | Out-Null } } if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) } return $strings.ToArray() } function Get-HttpServerReflection([string]$Path) { $result = [ordered]@{ path = Get-RelativePath -Path $Path; ok = $false; error = ""; types = @() } try { $asm = [System.Reflection.Assembly]::LoadFile($Path) $types = New-Object System.Collections.Generic.List[object] foreach ($type in $asm.GetTypes()) { if ($type.FullName -notmatch "HTTPServerLib\.(HttpServer|HttpService|ServiceModule|ServiceRoute|RouteAttribute|IServer|ActionResult)") { continue } $ctors = @($type.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | ForEach-Object { $params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name }) [pscustomobject]@{ is_public = $_.IsPublic; signature = "$($type.Name)($([string]::Join(', ', $params)))" } }) $methods = @($type.GetMethods([System.Reflection.BindingFlags]'Public,NonPublic,Instance,Static,DeclaredOnly') | ForEach-Object { $params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name }) [pscustomobject]@{ name = $_.Name; signature = "$($_.ReturnType.Name) $($_.Name)($([string]::Join(', ', $params)))" } }) $types.Add([pscustomobject]@{ full_name = $type.FullName; constructors = $ctors; methods = $methods }) } $result.ok = $true $result.types = $types.ToArray() } catch { $result.error = $_.Exception.Message } return [pscustomobject]$result } $textSourceResults = New-Object System.Collections.Generic.List[object] $binarySourceResults = New-Object System.Collections.Generic.List[object] $candidateBuckets = @{} foreach ($candidate in $candidates) { $candidateBuckets[[string]$candidate['key']] = New-Object System.Collections.Generic.List[object] } foreach ($source in $textSources) { $sourceFullPath = Join-Path $ilDirFull ([string]$source['path']) $sourceRel = Get-RelativePath -Path $sourceFullPath if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) { $textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null }) continue } $item = Get-Item -LiteralPath $sourceFullPath $lines = [System.IO.File]::ReadAllLines($sourceFullPath) $textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length }) foreach ($candidate in $candidates) { $key = [string]$candidate['key'] $compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) }) for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i] foreach ($rx in $compiled) { if ($rx.IsMatch($line)) { $candidateBuckets[$key].Add((New-TextEvidence -Lines $lines -Index $i -SourceRel $sourceRel)) break } } } } } foreach ($source in $binarySources) { $sourceFullPath = Join-Path $binaryDirFull ([string]$source['path']) $sourceRel = Get-RelativePath -Path $sourceFullPath if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) { $binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; string_count = 0; size_bytes = $null }) continue } $item = Get-Item -LiteralPath $sourceFullPath $strings = Get-PrintableStrings -Path $sourceFullPath -MinLength 4 $binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; string_count = $strings.Count; size_bytes = $item.Length }) foreach ($candidate in $candidates) { $key = [string]$candidate['key'] $compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) }) for ($i = 0; $i -lt $strings.Count; $i++) { $s = $strings[$i] foreach ($rx in $compiled) { if ($rx.IsMatch($s)) { $candidateBuckets[$key].Add([pscustomobject]@{ source = $sourceRel source_type = "binary_string" line_number = $null string_index = $i kind = "binary_string" text = $s method_header = $null class_header = $null window = @() }) break } } } } } $liveProbe = New-Object System.Collections.Generic.List[object] foreach ($relPath in $liveProbeSources) { $fullPath = Join-Path $repo $relPath if (Test-Path -LiteralPath $fullPath -PathType Leaf) { $raw = Get-Content -LiteralPath $fullPath -Raw $liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $true; length = $raw.Length; data = ($raw | ConvertFrom-Json) }) foreach ($candidate in $candidates) { $key = [string]$candidate['key'] $compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) }) foreach ($rx in $compiled) { if ($rx.IsMatch($raw)) { $candidateBuckets[$key].Add([pscustomobject]@{ source = $relPath source_type = "live_probe_json" line_number = $null string_index = $null kind = "live_probe_json" text = ($raw -replace "\s+", " ").Trim() method_header = $null class_header = $null window = @() }) break } } } } else { $liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $false; length = 0; data = $null }) } } $httpServerPath = Join-Path $binaryDirFull "HttpServerLib.dll" $httpReflection = if (Test-Path -LiteralPath $httpServerPath -PathType Leaf) { Get-HttpServerReflection -Path $httpServerPath } else { [pscustomobject]@{ path = Get-RelativePath -Path $httpServerPath; ok = $false; error = "missing"; types = @() } } $httpReflectionPath = Join-Path $outDir "httpserverlib-reflection.json" $httpReflection | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $httpReflectionPath -Encoding UTF8 if ($httpReflection.ok) { $reflectionText = ($httpReflection.types | ConvertTo-Json -Depth 8 -Compress) foreach ($candidate in $candidates) { $key = [string]$candidate['key'] $compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) }) foreach ($rx in $compiled) { if ($rx.IsMatch($reflectionText)) { $candidateBuckets[$key].Add([pscustomobject]@{ source = Get-RelativePath -Path $httpReflectionPath source_type = "reflection" line_number = $null string_index = $null kind = "reflection_metadata" text = "HttpServerLib reflection exposes matching types/methods; see httpserverlib-reflection.json" method_header = $null class_header = $null window = @() }) break } } } } $candidateResults = New-Object System.Collections.Generic.List[object] foreach ($candidate in $candidates) { $key = [string]$candidate['key'] $allEvidence = $candidateBuckets[$key].ToArray() $evidenceForReport = @( $allEvidence | Sort-Object @{ Expression = { if ($_.source_type -eq "text_il") { 0 } elseif ($_.source_type -eq "reflection") { 1 } elseif ($_.source_type -eq "live_probe_json") { 2 } else { 3 } } }, source, line_number, string_index | Select-Object -First $MaxEvidencePerCandidate ) $classification = if ($allEvidence.Count -eq 0) { "not_found" } else { [string]$candidate['classification'] } $candidateResults.Add([pscustomobject]@{ key = $key display = $candidate['display'] category = $candidate['category'] classification = $classification usable_for_broute_send = [bool]$candidate['usable_for_broute_send'] recommendation = $candidate['recommendation'] reason = $candidate['reason'] match_count = $allEvidence.Count evidence = $evidenceForReport }) } $recommendation = [pscustomobject]@{ pursue_existing_ipc_or_plugin_first = $false decision = "in_process_adapter_research" fallback_boundary = "external_sidecar_preview_only_until_bridge_exists" rationale = "Offline static evidence found no existing local IPC/plugin route that exposes send-message or send-file. HttpServerLib and plugin surfaces either require logged-in runtime state or are UI/framework-only; the named-pipe stub is empty." } $summary = [pscustomobject]@{ generated_at = (Get-Date).ToUniversalTime().ToString("o") repository = $repo il_dir = $IlDir binary_dir = $BinaryDir text_sources = $textSourceResults binary_sources = $binarySourceResults live_probe = $liveProbe httpserverlib_reflection = Get-RelativePath -Path $httpReflectionPath candidates = $candidateResults recommendation = $recommendation originals_copied = $false originals_modified = $false } $jsonPath = Join-Path $outDir "bridge-map.json" $summary | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $jsonPath -Encoding UTF8 $linesOut = New-Object System.Collections.Generic.List[string] $linesOut.Add("# B-Route Bridge/IPC/Plugin Static Map") $linesOut.Add("") $linesOut.Add("Date: 2026-07-12") $linesOut.Add("") $linesOut.Add("## Scope") $linesOut.Add("") $linesOut.Add("This report uses offline IL, binary string metadata, reflection metadata for ``HttpServerLib.dll``, and returned live-probe JSON. It does not log in, open sockets, attach hooks, inject into a process, replay traffic, or mutate client binaries.") $linesOut.Add("") $linesOut.Add("Machine-readable evidence: ``$OutputDir/bridge-map.json``") $linesOut.Add("Reflection metadata: ``$OutputDir/httpserverlib-reflection.json``") $linesOut.Add("") $linesOut.Add("## Recommendation") $linesOut.Add("") $linesOut.Add("| Question | Answer |") $linesOut.Add("| --- | --- |") $linesOut.Add("| Should B-route pursue existing IPC/plugin before an in-process adapter? | **No** |") $linesOut.Add("| Offline decision | ``$($recommendation.decision)`` |") $linesOut.Add("| External sidecar boundary | ``$($recommendation.fallback_boundary)`` |") $linesOut.Add("| Reason | $($recommendation.rationale) |") $linesOut.Add("") $linesOut.Add("## Sources") $linesOut.Add("") $linesOut.Add("### Text/IL sources") $linesOut.Add("") $linesOut.Add("| Source | Role | Exists | Lines | Size bytes |") $linesOut.Add("| --- | --- | --- | ---: | ---: |") foreach ($sourceResult in $textSourceResults) { $exists = if ($sourceResult.exists) { "yes" } else { "no" } $sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" } $linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |") } $linesOut.Add("") $linesOut.Add("### Binary/string sources") $linesOut.Add("") $linesOut.Add("| Source | Role | Exists | Strings | Size bytes |") $linesOut.Add("| --- | --- | --- | ---: | ---: |") foreach ($sourceResult in $binarySourceResults) { $exists = if ($sourceResult.exists) { "yes" } else { "no" } $sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" } $linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.string_count) | $sizeText |") } $linesOut.Add("") $linesOut.Add("## Candidate classification") $linesOut.Add("") $linesOut.Add("| Candidate | Category | Classification | Usable for B-route send | Recommendation | Matches |") $linesOut.Add("| --- | --- | --- | --- | --- | ---: |") foreach ($candidateResult in $candidateResults) { $usable = if ($candidateResult.usable_for_broute_send) { "yes" } else { "no" } $linesOut.Add("| ``$($candidateResult.display)`` | ``$($candidateResult.category)`` | ``$($candidateResult.classification)`` | $usable | ``$($candidateResult.recommendation)`` | $($candidateResult.match_count) |") } $linesOut.Add("") $linesOut.Add("## Verified facts") $linesOut.Add("") $linesOut.Add("- ``frmLogin.StartWinServer`` constructs ``HTTPServerLib.HttpService`` on ``0.0.0.0`` and starts ``HttpServer.Start`` in a thread, but it depends on ``IMPPManager.UserInfo`` and ``ServiceManager.UnifiedAuthentication`` state.") $linesOut.Add("- ``HttpServerLib.dll`` exposes route/module primitives such as ``HttpService.RegisterModule``, ``ServiceModule``, ``ServiceRoute``, ``RouteAttribute``, ``OnGet``, and ``OnPost``.") $linesOut.Add("- Main client IL does not show a concrete ``RegisterModule`` call or send-message/send-file HTTP route registration.") $linesOut.Add("- ``PluginLightAppMulProcessCommunication`` declares a ``NamedPipeServerStream`` field, but ``StartLightAppMultProssCommunication`` returns immediately.") $linesOut.Add("- Returned live probe showed one remote TCP connection to port ``10088`` and a named pipe ``\\.\\pipe\\zfpinject-msg-server``; neither proves an iSphere send API.") $linesOut.Add("- ``PluginManager``/``IMPlugin`` evidence is UI/app-store/context-menu oriented; no local assembly-load plugin bridge or send route was confirmed.") $linesOut.Add("") $linesOut.Add("## Candidate evidence") foreach ($candidateResult in $candidateResults) { $linesOut.Add("") $linesOut.Add("### $($candidateResult.display)") $linesOut.Add("") $linesOut.Add("Classification: ``$($candidateResult.classification)``; usable for B-route send: ``$($candidateResult.usable_for_broute_send)``; matches: $($candidateResult.match_count).") $linesOut.Add("") $linesOut.Add("Reason: $($candidateResult.reason)") if (@($candidateResult.evidence).Count -eq 0) { $linesOut.Add("") $linesOut.Add("No evidence found in configured offline sources.") continue } $linesOut.Add("") $linesOut.Add("| Source | Kind | Snippet | Context |") $linesOut.Add("| --- | --- | --- | --- |") foreach ($evidence in $candidateResult.evidence) { $sourceLoc = if ($evidence.line_number) { "$($evidence.source):$($evidence.line_number)" } elseif ($null -ne $evidence.string_index) { "$($evidence.source)#str$($evidence.string_index)" } else { $evidence.source } $method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" } $snippet = (($evidence.text -replace "\|", "\\|") -replace "`r?`n", " ") if ($snippet.Length -gt 220) { $snippet = $snippet.Substring(0, 220) + "..." } $method = ($method -replace "\|", "\\|") $linesOut.Add("| ``$sourceLoc`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |") } } $linesOut.Add("") $linesOut.Add("## Decision") $linesOut.Add("") $linesOut.Add("No existing IPC/plugin/local HTTP bridge is confirmed usable for B-route text send or file send. The next B-route branch should be ``in_process_adapter_research``. Until that exists, an external sidecar can only do preview/probe work, while A-route/RPA remains backup-only.") $linesOut.Add("") $linesOut.Add("## Next work") $linesOut.Add("") $linesOut.Add("1. Update the B-route next plan and capability source matrix with ``in_process_adapter_research`` as the offline decision.") $linesOut.Add("2. Create a separate plan before implementing any in-process adapter work.") $linesOut.Add("3. Do not promote RPA beyond backup in this branch.") ($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8 $requiredNames = @("HttpServerLib", "NamedPipe", "Plugin", "IMPlatformClient.Web", "ProcessComm", "INetwork", "IOClientNetwork", "in_process_adapter_research") $reportText = Get-Content -LiteralPath $reportFullPath -Raw $missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" }) if ($missing.Count -gt 0) { throw "Report missing required names: $($missing -join ', ')" } [pscustomobject]@{ ok = $true json_path = $jsonPath report_path = $reportFullPath reflection_path = $httpReflectionPath candidates = $candidateResults.Count decision = $recommendation.decision pursue_existing_ipc_or_plugin_first = $false originals_copied = $false originals_modified = $false } | ConvertTo-Json -Depth 6