using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Services;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
// Explicit QA entry only. Flight, departure, safe exit and docking remain
// owned by the normal station-journey viewmodel and its existing guards.
private static async Task RunScoAcceptanceAsync(string[] args,string reportPath)
{
reportPath=Path.GetFullPath(reportPath);
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new SCO acceptance report path.");
var stationPath=GetArgument(args,"--accept-sco-station")??GetArgument(args,"--station-journey")
??throw new ArgumentException("--accept-sco-station must name the real destination station JSON.");
if(args.Contains("--no-session-automation"))throw new ArgumentException("SCO flight acceptance cannot run with --no-session-automation.");
var minutes=double.Parse(GetArgument(args,"--accept-sco-minutes")??"35",CultureInfo.InvariantCulture);
if(!double.IsFinite(minutes)||minutes is <1 or >60)throw new ArgumentException("SCO acceptance duration must be 1..60 minutes.");
var destination=JsonSerializer.Deserialize<Station>(await File.ReadAllTextAsync(stationPath),ToolboxSettings.JsonOptions)
??throw new InvalidDataException("SCO acceptance station is empty.");
if(destination.MarketId<=0||destination.IsPlanetary)throw new InvalidDataException("SCO acceptance requires a real orbital station.");
var resume=GetArgument(args,"--resume-cg-after-sco");
if(resume is not null&&!File.Exists(resume))throw new FileNotFoundException("Original CG configuration is missing.",resume);
Directory.CreateDirectory(Path.GetDirectoryName(reportPath)!);
var started=DateTimeOffset.UtcNow;var phase="preflight";string? failure=null;var passed=false;
var baseline=false;var trialUse=false;var trialPass=false;var promoted=false;var docked=false;
var pid=0;long processStart=0;string hash="",fingerprint="";ulong entity=0,world=0,epoch=0,armedSteps=0;
var initial=Runtime.Player;Task? journey=null;var expectedStop=false;var supervisorStop=false;var closed=false;var windowClosing=false;
var userStopVersion=ViewModel.NavigationUserStopVersion;
var exits=new List<JsonElement>();var stops=new List<JsonElement>();var lastExit="";var lastStop="";
var domainFallbacks=new List<object>();
var lineOptions=new JsonSerializerOptions(ToolboxSettings.JsonOptions){WriteIndented=false};
JsonElement? finalStatus=null;var events=0;var referenceRequalifications=0;var logGate=new object();DateTimeOffset lastFrame=default,lastFingerprint=default;
using var budget=new CancellationTokenSource(TimeSpan.FromMinutes(minutes));
var token=budget.Token;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
void Log(object value)
{lock(logGate){if(closed||events>=4096)return;events++;log.WriteLine(JsonSerializer.Serialize(value,lineOptions));log.Flush();}}
void StopOwned()
{
if(journey is {IsCompleted:false}&&ViewModel.NavigationActive)
ViewModel.StopNavigationForScoAcceptance();
}
void WindowClosed(object sender,WindowEventArgs e){windowClosing=true;if(!budget.IsCancellationRequested)budget.Cancel();StopOwned();}
void Progress(FlightAutomationProgress value)
{
Log(new{kind="journey-progress",at=DateTimeOffset.UtcNow,phase,value});
if(value.Stage is FlightAutomationStage.Failed||value.Stage==FlightAutomationStage.Stopped&&!expectedStop)
{
supervisorStop=true;
MainWindow.DispatcherQueue.TryEnqueue(()=>{if(!budget.IsCancellationRequested)budget.Cancel();StopOwned();});
}
}
void Check()
{
token.ThrowIfCancellationRequested();
if(windowClosing||ViewModel.NavigationUserStopVersion!=userStopVersion)
throw new OperationCanceledException("User Stop or window close cancelled SCO acceptance.");
if(File.Exists(reportPath+".stop"))throw new OperationCanceledException("SCO acceptance stop file received.");
var player=Runtime.Player;
if(!player.InGame||player.CommanderId!=initial.CommanderId||player.CurrentShipId!=initial.CurrentShipId||player.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("Commander, ship or real game session changed during SCO acceptance.");
if(!player.CargoReliable||player.CargoCount!=0||NavigationSafety.UnhealthyHull(player)||NavigationSafety.HasExternalThreat(player))
throw new InvalidOperationException("Real cargo, health or threat protection stopped SCO acceptance.");
using var process=Process.GetProcessById(pid);
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=processStart)throw new InvalidDataException("Game process identity changed.");
if(DateTimeOffset.UtcNow-lastFingerprint>=TimeSpan.FromSeconds(1))
{
lastFingerprint=DateTimeOffset.UtcNow;
var ship=player.Ships.SingleOrDefault(s=>s.ShipId==player.CurrentShipId)??throw new InvalidDataException("Current ship is missing.");
if(NativeSystemFlightObservation.LoadoutFingerprint(ship)!=fingerprint)throw new InvalidDataException("Ship loadout changed.");
}
if(supervisorStop)throw new OperationCanceledException("Owned station journey stopped or failed.");
}
async Task<JsonElement?> ReadAsync(string service)
{
using var ping=CancellationTokenSource.CreateLinkedTokenSource(token);ping.CancelAfter(TimeSpan.FromSeconds(2));
try{return await new NavigationPipeClient(pid,service).CallAsync(new{op="read"},ping.Token);}
catch(Exception e)when(NavigationRecovery.CanRetry(e,token)){return null;}
}
void Observe(JsonElement status)
{
if(ScoQaNumber(status,"processId")!=pid||ScoQaText(status,"gameSha256")!=hash||ScoQaText(status,"service")!=NativeCruiseControlChannel.Service)
throw new InvalidDataException("SCO service identity differs from this acceptance process.");
finalStatus=status.Clone();var sample=ScoQaObject(status,"sample");var policy=ScoQaObject(status,"scoExitEstimate");
var receipt=ScoQaObject(policy,"lastExitReceipt");
if(receipt.ValueKind==JsonValueKind.Object)
{
var key=receipt.GetRawText();
if(key!=lastExit){lastExit=key;if(exits.Count>=16)throw new InvalidDataException("Unexpected repeated exit receipts.");exits.Add(receipt.Clone());Log(new{kind="complete-exit-receipt",phase,receipt});}
}
var stop=ScoQaObject(status,"lastOriginalStop");
if(stop.ValueKind==JsonValueKind.Object&&ScoQaNumber(stop,"observedAtUnixMilliseconds")>=started.ToUnixTimeMilliseconds())
{
var key=stop.GetRawText();if(key!=lastStop){lastStop=key;if(stops.Count>=16)throw new InvalidDataException("Unexpected repeated original stops.");stops.Add(stop.Clone());Log(new{kind="original-stop-receipt",phase,stop});}
}
if(ScoQaFlag(sample,"playerOverride")||ScoQaFlag(sample,"healthEmergency"))throw new OperationCanceledException("Native manual takeover or health protection.");
if(DateTimeOffset.UtcNow-lastFrame>=TimeSpan.FromSeconds(1))
{
lastFrame=DateTimeOffset.UtcNow;
Log(new{kind="sco-frame",at=lastFrame,phase,sampleReady=ScoQaFlag(status,"sampleReady"),
state=ScoQaFinite(sample,"state"),entityId=ScoQaText(sample,"entityId"),referenceWorld=ScoQaText(sample,"referenceWorldAddress"),
speed=ScoQaFinite(sample,"speedMetresPerSecond"),distance=ScoQaFinite(sample,"distanceMetres"),
nativeEpoch=ScoQaFinite(sample,"originalUpdateClockEpoch"),nativeSteps=ScoQaFinite(sample,"originalUpdateCompletedSteps"),
nativeSimulationSeconds=ScoQaFinite(sample,"originalUpdateCompletedSimulationSeconds"),
approach=ScoQaMaybe(ScoQaObject(sample,"scoExitApproach")),issue=ScoQaText(policy,"issue"),proofAvailable=ScoQaFlag(policy,"proofAvailable")});
}
}
async Task<JsonElement?> PollAsync()
{
Check();var native=await ReadAsync("Navigation");
if(native is {} main&&(ScoQaFlag(main,"playerOverride")||ScoQaFlag(ScoQaObject(main,"shipHealth"),"emergency")))
throw new OperationCanceledException("Native manual takeover or health emergency.");
var status=await ReadAsync(NativeCruiseControlChannel.Service);if(status is {} current)Observe(current);
Check();return status;
}
bool Ready(JsonElement status,bool sameBinding)
{
var sample=ScoQaObject(status,"sample");var at=ScoQaNumber(sample,"observedAtUnixMilliseconds");var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if(!ScoQaFlag(status,"sampleReady")||!ScoQaFlag(sample,"verified")||!ScoQaFlag(sample,"inSupercruise")||ScoQaNumber(sample,"rawFsdState")!=2
||!ScoQaFlag(sample,"originalUpdateClockValid")
||!double.IsFinite(at)||at>now||now-at>200||ScoQaNumber(ScoQaObject(sample,"target"),"marketId")!=destination.MarketId)return false;
if(sameBinding&&(ScoQaId(sample,"entityId")!=entity||ScoQaId(sample,"referenceWorldAddress")!=world))
{
var nextWorld=ScoQaId(sample,"referenceWorldAddress");
if(ScoQaId(sample,"entityId")==entity&&nextWorld!=0&&nextWorld!=world
&&(journey is {IsCompleted:false}||expectedStop))throw new ScoQaReferenceChangedException(world,nextWorld);
throw new InvalidDataException("SCO baseline binding changed before trial/promotion.");
}
return true;
}
Runtime.FlightAutomation.Progress+=Progress;
MainWindow.Closed+=WindowClosed;
using var cancellation=token.Register(()=>MainWindow.DispatcherQueue.TryEnqueue(StopOwned));
try
{
if(!initial.InGame||initial.CommanderId.Length==0||initial.CurrentShipId is null||initial.LoadGameAt is null||!initial.CargoReliable||initial.CargoCount!=0)
throw new InvalidOperationException("SCO QA requires the identified empty current ship in the real game.");
if(Runtime.FlightAutomation.IsActive||ViewModel.RunActive||ViewModel.NavigationActive||ViewModel.SystemFlightActive)
throw new InvalidOperationException("Another task is active; QA will not replace it.");
var target=await new WindowsGameSessionDriver(()=>Runtime.Player,()=>Runtime.Settings.PluginGameExecutable).FindTargetAsync(token);
pid=target.ProcessId;processStart=target.StartedUtcTicks;
var hello=await new NavigationPipeClient(pid).CallAsync(new{op="describe"},token);
hash=ScoQaText(hello,"gameSha256");if(hash!=XFEEDToolBox.Core.Native.NativeSystemLocationReader.GameSha256||ScoQaNumber(hello,"processId")!=pid)
throw new InvalidDataException("Pinned game build is unverified.");
fingerprint=NativeSystemFlightObservation.LoadoutFingerprint(initial.Ships.Single(s=>s.ShipId==initial.CurrentShipId));
ViewModel.DemoMode=false;AppShellPage.Current!.Navigate(typeof(NavigationPage));Check();
var pipe=new NavigationPipeClient(pid,NativeCruiseControlChannel.Service);
var existing=await ReadAsync(NativeCruiseControlChannel.Service);
if(existing is {} old)
{
Observe(old);if(ScoQaFlag(old,"owned"))throw new InvalidOperationException("An existing SCO owner is active.");
await pipe.CallAsync(new{op="sco.exit.configure",mode="shadow"},token);
}
while(true)
{
try
{
Check();expectedStop=false;phase="baseline";var baselineStarted=DateTimeOffset.UtcNow;
Log(new{kind="stage",phase,started,baselineStarted,referenceRequalifications,stationPath,destination,pid,processStart,fingerprint,minutes});
if(journey is null||journey.IsCompleted)journey=ViewModel.RunStationJourneyAsync(destination);
ulong originalTrialDeadline=0;
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,false)&&ScoQaFlag(ScoQaObject(current,"scoExitEstimate"),"proofAvailable")
&&ScoQaNumber(ScoQaObject(current,"scoExitEstimate"),"provenAtUnixMilliseconds")>=baselineStarted.ToUnixTimeMilliseconds())
{
var policy=ScoQaObject(current,"scoExitEstimate");var receipt=ScoQaObject(policy,"lastExitReceipt");var sample=ScoQaObject(current,"sample");
if(ScoQaText(policy,"trustedLoadoutFingerprint")!=fingerprint||!ScoQaFlag(receipt,"pass")||ScoQaFlag(receipt,"trial")||ScoQaFlag(receipt,"production")
||ScoQaNumber(receipt,"endCompletedSteps")<=ScoQaNumber(receipt,"startCompletedSteps")||ScoQaNumber(receipt,"integratedPathDistanceMetres")<=0)
throw new InvalidDataException("Baseline lacks a complete same-session native exit receipt.");
entity=ScoQaId(sample,"entityId");world=ScoQaId(sample,"referenceWorldAddress");epoch=(ulong)ScoQaNumber(sample,"originalUpdateClockEpoch");
if(entity==0||world==0||ScoQaId(receipt,"entityId")!=entity||ScoQaId(receipt,"referenceWorldAddress")!=world)
throw new InvalidDataException("Baseline proof belongs to another native binding.");
baseline=true;break;
}
if(journey.IsCompleted)throw new InvalidOperationException("Baseline journey ended before a complete SCO proof.");
await Task.Delay(100,token);
}
phase="release-baseline";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
while(true)
{
var status=await PollAsync();
if(status is {} idle&&Ready(idle,true)&&!ScoQaFlag(idle,"owned")&&ScoQaNumber(idle,"leaseExpiresAtUnixMilliseconds")==0
&&ScoQaFlag(ScoQaObject(idle,"scoExitEstimate"),"currentSourceDomainVerified"))
{
armedSteps=(ulong)ScoQaNumber(ScoQaObject(idle,"sample"),"originalUpdateCompletedSteps");
originalTrialDeadline=(ulong)DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeMilliseconds();
await pipe.CallAsync(new{op="sco.exit.configure",mode="trial",entityId=entity,referenceWorldAddress=world,
expiresAtUnixMilliseconds=originalTrialDeadline,maximumBursts=2},token);break;
}
await Task.Delay(100,token);
}
Check();expectedStop=false;phase="trial";Log(new{kind="stage",phase,entity,world,epoch,armedSteps});
var consumedTrialSteps=armedSteps;var continuedFallbacks=0;var attemptTrialUse=false;var attemptStartedAt=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
journey=ViewModel.RunStationJourneyAsync(destination);
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,true))
{
var sample=ScoQaObject(current,"sample");var choice=ScoQaObject(sample,"scoExitApproach");
var currentPolicy=ScoQaObject(current,"scoExitEstimate");
if(ScoQaNumber(sample,"originalUpdateClockEpoch")!=epoch||ScoQaText(currentPolicy,"mode")!="bounded-trial"
||ScoQaNumber(currentPolicy,"trialExpiresAtUnixMilliseconds")!=originalTrialDeadline
||DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()>=(long)originalTrialDeadline
||ScoQaObject(currentPolicy,"lastPolicyRevocationFailure").ValueKind!=JsonValueKind.Null)
throw new InvalidOperationException("Original trial clock, deadline or authorization was revoked.");
if(ScoQaFlag(choice,"useEstimatedStop")&&ScoQaText(choice,"mode")=="trial")trialUse=attemptTrialUse=true;
var stop=ScoQaObject(current,"lastOriginalStop");var gate=ScoQaObject(stop,"gate");var stopChoice=gate.ValueKind==JsonValueKind.Object?ScoExitApproachDecision.Read(gate):null;
var stopAt=ScoQaNumber(stop,"observedAtUnixMilliseconds");
var actualEstimatedStop=ScoQaFlag(stop,"nativeMethodCalled")&&!ScoQaFlag(stop,"argument")&&ScoQaText(stop,"nativeMethodRva")=="2eaacd0"
&&ScoQaText(stop,"reason")=="approach-or-alignment"&&ScoQaId(stop,"entityId")==entity&&ScoQaId(stop,"referenceWorldAddress")==world
&&ScoQaFlag(gate,"identityVerified")&&ScoQaFlag(gate,"targetVerified")&&ScoQaFlag(gate,"geometryVerified")
&&stopChoice is {UseEstimatedStop:true,Mode:"trial"}&&double.IsFinite(stopAt)&&stopAt>=attemptStartedAt
&&stopChoice.TargetKey==ScoQaNumber(ScoQaObject(sample,"target"),"key")
&&stopChoice.Applies(entity,world,stopChoice.TargetKey,(ulong)stopAt)
&&stopChoice.RequiresStop(ScoQaNumber(gate,"distanceMetres"),ScoQaNumber(gate,"speedMetresPerSecond"),true,
ScoQaFlag(gate,"cruiseParametersVerified"),ScoQaNumber(gate,"cruiseApproachSeconds"),entity,world,stopChoice.TargetKey,(ulong)stopAt);
if(actualEstimatedStop)trialUse=attemptTrialUse=true;
var receipt=ScoQaObject(ScoQaObject(current,"scoExitEstimate"),"lastExitReceipt");
if(receipt.ValueKind==JsonValueKind.Object&&ScoQaNumber(receipt,"clockEpoch")==epoch&&ScoQaNumber(receipt,"endCompletedSteps")>consumedTrialSteps)
{
if(!ScoQaFlag(receipt,"pass"))throw new InvalidOperationException("Trial exit failed: "+ScoQaText(receipt,"reason"));
if(ScoQaId(receipt,"entityId")!=entity||ScoQaId(receipt,"referenceWorldAddress")!=world
||ScoQaNumber(receipt,"targetKey")!=ScoQaNumber(ScoQaObject(sample,"target"),"key"))
throw new InvalidDataException("Trial exit receipt belongs to another ship, scene or target.");
if(ScoQaNumber(receipt,"startCompletedSteps")<=consumedTrialSteps)
throw new InvalidDataException("Trial exit receipt predates the current attempt.");
if(!ScoQaFlag(receipt,"trial")||!attemptTrialUse||!actualEstimatedStop)
{
var continuation=new ScoTrialContinuationContext(pid,hash,fingerprint,entity,world,epoch,
(int)ScoQaNumber(ScoQaObject(sample,"target"),"key"),consumedTrialSteps,originalTrialDeadline,continuedFallbacks);
if(!ScoTrialFallbackContinuation.CanContinue(current,continuation,DateTimeOffset.UtcNow,out var why))
throw new InvalidOperationException("Only an actual estimated approach stop may pass; fallback cannot continue: "+why);
var consumedEnd=(ulong)ScoQaNumber(receipt,"endCompletedSteps");continuedFallbacks++;
var fallback=new{at=DateTimeOffset.UtcNow,entity,world,epoch,attempt=1,receiptStart=ScoQaNumber(receipt,"startCompletedSteps"),receiptEnd=consumedEnd,
attemptTrialUse,actualEstimatedStop,reason=why,originalTrialDeadline,remainingNativeBursts=1,deadlineExtended=false,rearmed=false};
domainFallbacks.Add(fallback);Log(new{kind="calibrated-domain-fallback",phase,fallback});
phase="release-domain-fallback";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
while(true)
{
Check();if((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()>=originalTrialDeadline)
throw new InvalidOperationException("Original trial authorization expired before its remaining burst.");
var next=await PollAsync();
if(next is {} idle&&Ready(idle,true))
{
var policy=ScoQaObject(idle,"scoExitEstimate");var freshSample=ScoQaObject(idle,"sample");
if(ScoQaNumber(freshSample,"originalUpdateClockEpoch")!=epoch||ScoQaText(policy,"mode")!="bounded-trial"
||ScoQaNumber(policy,"trialExpiresAtUnixMilliseconds")!=originalTrialDeadline||ScoQaNumber(policy,"maximumTrialBurstsRemaining")!=1
||ScoQaObject(policy,"lastPolicyRevocationFailure").ValueKind!=JsonValueKind.Null)
throw new InvalidOperationException("Original remaining trial authorization changed during release.");
if(!ScoQaFlag(idle,"owned")&&ScoQaNumber(idle,"leaseExpiresAtUnixMilliseconds")==0&&ScoQaNumber(freshSample,"state")==0
&&ScoQaFlag(policy,"currentSourceDomainVerified"))
{
if(ScoQaNumber(ScoQaObject(policy,"lastExitReceipt"),"endCompletedSteps")!=consumedEnd)
throw new InvalidDataException("Fallback receipt changed during release.");
if(!ScoTrialFallbackContinuation.CanContinue(idle,continuation,DateTimeOffset.UtcNow,out why))
throw new InvalidOperationException("Remaining trial burst lost its verified fallback receipt: "+why);
consumedTrialSteps=Math.Max(consumedEnd,(ulong)ScoQaNumber(freshSample,"originalUpdateCompletedSteps"));break;
}
}
await Task.Delay(100,token);
}
Check();expectedStop=false;phase="trial";attemptTrialUse=false;attemptStartedAt=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Log(new{kind="remaining-trial-burst",phase,attempt=2,consumedTrialSteps,originalTrialDeadline,remainingNativeBursts=1,deadlineExtended=false,rearmed=false});
journey=ViewModel.RunStationJourneyAsync(destination);continue;
}
trialPass=true;break;
}
}
if(journey.IsCompleted)throw new InvalidOperationException("Trial journey ended without a verified estimated exit.");
await Task.Delay(100,token);
}
// Promotion is a policy change on an idle channel. Release our
// journey explicitly instead of racing its next automatic burst.
phase="release-trial";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
phase="promote";
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,true)&&!ScoQaFlag(current,"owned")&&ScoQaNumber(current,"leaseExpiresAtUnixMilliseconds")==0
&&ScoQaNumber(ScoQaObject(current,"sample"),"state")==0
&&ScoQaFlag(ScoQaObject(current,"scoExitEstimate"),"currentSourceDomainVerified"))
{
var result=await pipe.CallAsync(new{op="sco.exit.configure",mode="production",entityId=entity,referenceWorldAddress=world,
expiresAtUnixMilliseconds=DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds()},token);
Observe(result);promoted=ScoQaFlag(ScoQaObject(result,"scoExitEstimate"),"productionAvailable");
if(!promoted)throw new InvalidDataException("Production promotion did not confirm its bounded configuration.");break;
}
await Task.Delay(100,token);
}
break;
}
catch(ScoQaReferenceChangedException change)when(referenceRequalifications<3)
{
Check();referenceRequalifications++;
Log(new{kind="natural-reference-requalification",at=DateTimeOffset.UtcNow,change.PreviousWorld,change.CurrentWorld,referenceRequalifications,
maximumRequalifications=3,originalBudgetUnchanged=true,oldProofReused=false});
await pipe.CallAsync(new{op="sco.exit.configure",mode="shadow"},token);
baseline=trialUse=trialPass=promoted=false;entity=world=epoch=armedSteps=0;
// Keep the healthy ordinary journey running. Only restart it
// if this QA already released it; user Stop remains terminal.
Check();
}
}
Check();expectedStop=false;phase="dock";Log(new{kind="stage",phase,promoted});
journey=ViewModel.RunStationJourneyAsync(destination);
while(!journey.IsCompleted){await PollAsync();await Task.Delay(100,token);}
await journey;Check();var actual=Runtime.Player;
docked=actual.Docked&&actual.MarketId==destination.MarketId&&actual.SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase)
&&actual.CargoReliable&&actual.CargoCount==0&&Runtime.FlightAutomation.Stage==FlightAutomationStage.Completed;
if(!docked)throw new InvalidOperationException("Real empty-ship docking at the acceptance station was not confirmed.");
passed=baseline&&trialUse&&trialPass&&promoted&&docked;
}
catch(Exception error){failure=error.ToString();passed=false;}
finally
{
expectedStop=true;StopOwned();
if(journey is {IsCompleted:false})try{await journey.WaitAsync(TimeSpan.FromSeconds(15));}catch(Exception e){failure=(failure??"")+" / release: "+e.Message;passed=false;}
Runtime.FlightAutomation.Progress-=Progress;
if(!passed&&pid!=0)try{await new NavigationPipeClient(pid,NativeCruiseControlChannel.Service).CallAsync(new{op="sco.exit.configure",mode="shadow"});}catch(Exception e){Log(new{kind="strategy-release-unconfirmed",message=e.Message});}
}
phase="report";
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new
{
started,completedAt=DateTimeOffset.UtcNow,passed,failure,stationPath,destination,pid,processStart,fingerprint,baseline,trialUse,trialPass,promoted,docked,
actual=new{Runtime.Player.SystemName,Runtime.Player.MarketId,Runtime.Player.Docked,Runtime.Player.CargoCount,Runtime.Player.CargoReliable,Runtime.Player.CurrentShipId},
totalMinutes=minutes,events,referenceRequalifications,exits,stops,domainFallbacks,lastPolicy=finalStatus is {} final?ScoQaMaybe(ScoQaObject(final,"scoExitEstimate")):null,
resumeCgConfiguration=resume,realJournalUsed=true,normalStationJourneyUsed=true,osInputSent=false,journalWrites=0,navRouteWrites=0
},new JsonSerializerOptions(ToolboxSettings.JsonOptions){WriteIndented=true}));
lock(logGate){log.Flush();closed=true;}
if(passed&&resume is not null)
{
// The completed flight receipt is historical evidence, not a new
// authorization after Stop/close received while writing it.
try{await PollAsync();Check();}
catch(Exception error)
{
MainWindow.Closed-=WindowClosed;
await File.WriteAllTextAsync(reportPath+".resume.json",JsonSerializer.Serialize(new
{at=DateTimeOffset.UtcNow,scoAcceptancePassed=true,resumeStarted=false,reason=error.Message},lineOptions));
if(!windowClosing)Runtime.Log("SCO实机验收","验收已完成;接续跑商前状态或运行授权改变,未启动跑商。");
return;
}
MainWindow.AppWindow.Show(false);AppShellPage.Current!.Navigate(typeof(PlannerPage));
var resumeAuthorized=false;
try
{
await ViewModel.RunAutomaticTradeAsync(resume,()=>
{
Check();
if(Runtime.FlightAutomation.IsActive||ViewModel.RunActive||ViewModel.NavigationActive||ViewModel.SystemFlightActive)
throw new InvalidOperationException("Another task started before the CG handoff; its control remains unchanged.");
MainWindow.Closed-=WindowClosed;resumeAuthorized=true;
});
}
catch(Exception error)when(!resumeAuthorized)
{
MainWindow.Closed-=WindowClosed;
await File.WriteAllTextAsync(reportPath+".resume.json",JsonSerializer.Serialize(new
{at=DateTimeOffset.UtcNow,scoAcceptancePassed=true,resumeStarted=false,reason=error.Message},lineOptions));
if(!windowClosing)Runtime.Log("SCO实机验收","配置准备期间状态或运行授权改变,未启动跑商。");
}
}
else
{
MainWindow.Closed-=WindowClosed;
if(!passed){ViewModel.ShowError("SCO 实机验收未通过:"+(failure??"unknown"));Runtime.Log("SCO实机验收",failure??"unknown");MainWindow.Close();}
}
}
private static JsonElement ScoQaObject(JsonElement value,string name)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(name,out var item)?item:default;
private static string ScoQaText(JsonElement value,string name){var item=ScoQaObject(value,name);return item.ValueKind==JsonValueKind.String?item.GetString()??"":"";}
private static bool ScoQaFlag(JsonElement value,string name)=>ScoQaObject(value,name).ValueKind==JsonValueKind.True;
private static double ScoQaNumber(JsonElement value,string name){var item=ScoQaObject(value,name);return item.ValueKind==JsonValueKind.Number&&item.TryGetDouble(out var result)?result:double.NaN;}
private static double? ScoQaFinite(JsonElement value,string name){var result=ScoQaNumber(value,name);return double.IsFinite(result)?result:null;}
private static JsonElement? ScoQaMaybe(JsonElement value)=>value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null?null:value;
private static ulong ScoQaId(JsonElement value,string name)=>ulong.TryParse(ScoQaText(value,name),NumberStyles.None,CultureInfo.InvariantCulture,out var result)?result:0;
private sealed class ScoQaReferenceChangedException(ulong previousWorld,ulong currentWorld):Exception("Natural reference world changed; a new same-binding baseline is required.")
{
public ulong PreviousWorld{get;}=previousWorld;
public ulong CurrentWorld{get;}=currentWorld;
}
}
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Services;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
// Explicit QA entry only. Flight, departure, safe exit and docking remain
// owned by the normal station-journey viewmodel and its existing guards.
private static async Task RunScoAcceptanceAsync(string[] args,string reportPath)
{
reportPath=Path.GetFullPath(reportPath);
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new SCO acceptance report path.");
var stationPath=GetArgument(args,"--accept-sco-station")??GetArgument(args,"--station-journey")
??throw new ArgumentException("--accept-sco-station must name the real destination station JSON.");
if(args.Contains("--no-session-automation"))throw new ArgumentException("SCO flight acceptance cannot run with --no-session-automation.");
var minutes=double.Parse(GetArgument(args,"--accept-sco-minutes")??"35",CultureInfo.InvariantCulture);
if(!double.IsFinite(minutes)||minutes is <1 or >60)throw new ArgumentException("SCO acceptance duration must be 1..60 minutes.");
var destination=JsonSerializer.Deserialize<Station>(await File.ReadAllTextAsync(stationPath),ToolboxSettings.JsonOptions)
??throw new InvalidDataException("SCO acceptance station is empty.");
if(destination.MarketId<=0||destination.IsPlanetary)throw new InvalidDataException("SCO acceptance requires a real orbital station.");
var resume=GetArgument(args,"--resume-cg-after-sco");
if(resume is not null&&!File.Exists(resume))throw new FileNotFoundException("Original CG configuration is missing.",resume);
Directory.CreateDirectory(Path.GetDirectoryName(reportPath)!);
var started=DateTimeOffset.UtcNow;var phase="preflight";string? failure=null;var passed=false;
var baseline=false;var trialUse=false;var trialPass=false;var promoted=false;var docked=false;
var pid=0;long processStart=0;string hash="",fingerprint="";ulong entity=0,world=0,epoch=0,armedSteps=0;
var initial=Runtime.Player;Task? journey=null;var expectedStop=false;var supervisorStop=false;var closed=false;var windowClosing=false;
var userStopVersion=ViewModel.NavigationUserStopVersion;
var exits=new List<JsonElement>();var stops=new List<JsonElement>();var lastExit="";var lastStop="";
var domainFallbacks=new List<object>();
var lineOptions=new JsonSerializerOptions(ToolboxSettings.JsonOptions){WriteIndented=false};
JsonElement? finalStatus=null;var events=0;var referenceRequalifications=0;var logGate=new object();DateTimeOffset lastFrame=default,lastFingerprint=default;
using var budget=new CancellationTokenSource(TimeSpan.FromMinutes(minutes));
var token=budget.Token;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
void Log(object value)
{lock(logGate){if(closed||events>=4096)return;events++;log.WriteLine(JsonSerializer.Serialize(value,lineOptions));log.Flush();}}
void StopOwned()
{
if(journey is {IsCompleted:false}&&ViewModel.NavigationActive)
ViewModel.StopNavigationForScoAcceptance();
}
void WindowClosed(object sender,WindowEventArgs e){windowClosing=true;if(!budget.IsCancellationRequested)budget.Cancel();StopOwned();}
void Progress(FlightAutomationProgress value)
{
Log(new{kind="journey-progress",at=DateTimeOffset.UtcNow,phase,value});
if(value.Stage is FlightAutomationStage.Failed||value.Stage==FlightAutomationStage.Stopped&&!expectedStop)
{
supervisorStop=true;
MainWindow.DispatcherQueue.TryEnqueue(()=>{if(!budget.IsCancellationRequested)budget.Cancel();StopOwned();});
}
}
void Check()
{
token.ThrowIfCancellationRequested();
if(windowClosing||ViewModel.NavigationUserStopVersion!=userStopVersion)
throw new OperationCanceledException("User Stop or window close cancelled SCO acceptance.");
if(File.Exists(reportPath+".stop"))throw new OperationCanceledException("SCO acceptance stop file received.");
var player=Runtime.Player;
if(!player.InGame||player.CommanderId!=initial.CommanderId||player.CurrentShipId!=initial.CurrentShipId||player.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("Commander, ship or real game session changed during SCO acceptance.");
if(!player.CargoReliable||player.CargoCount!=0||NavigationSafety.UnhealthyHull(player)||NavigationSafety.HasExternalThreat(player))
throw new InvalidOperationException("Real cargo, health or threat protection stopped SCO acceptance.");
using var process=Process.GetProcessById(pid);
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=processStart)throw new InvalidDataException("Game process identity changed.");
if(DateTimeOffset.UtcNow-lastFingerprint>=TimeSpan.FromSeconds(1))
{
lastFingerprint=DateTimeOffset.UtcNow;
var ship=player.Ships.SingleOrDefault(s=>s.ShipId==player.CurrentShipId)??throw new InvalidDataException("Current ship is missing.");
if(NativeSystemFlightObservation.LoadoutFingerprint(ship)!=fingerprint)throw new InvalidDataException("Ship loadout changed.");
}
if(supervisorStop)throw new OperationCanceledException("Owned station journey stopped or failed.");
}
async Task<JsonElement?> ReadAsync(string service)
{
using var ping=CancellationTokenSource.CreateLinkedTokenSource(token);ping.CancelAfter(TimeSpan.FromSeconds(2));
try{return await new NavigationPipeClient(pid,service).CallAsync(new{op="read"},ping.Token);}
catch(Exception e)when(NavigationRecovery.CanRetry(e,token)){return null;}
}
void Observe(JsonElement status)
{
if(ScoQaNumber(status,"processId")!=pid||ScoQaText(status,"gameSha256")!=hash||ScoQaText(status,"service")!=NativeCruiseControlChannel.Service)
throw new InvalidDataException("SCO service identity differs from this acceptance process.");
finalStatus=status.Clone();var sample=ScoQaObject(status,"sample");var policy=ScoQaObject(status,"scoExitEstimate");
var receipt=ScoQaObject(policy,"lastExitReceipt");
if(receipt.ValueKind==JsonValueKind.Object)
{
var key=receipt.GetRawText();
if(key!=lastExit){lastExit=key;if(exits.Count>=16)throw new InvalidDataException("Unexpected repeated exit receipts.");exits.Add(receipt.Clone());Log(new{kind="complete-exit-receipt",phase,receipt});}
}
var stop=ScoQaObject(status,"lastOriginalStop");
if(stop.ValueKind==JsonValueKind.Object&&ScoQaNumber(stop,"observedAtUnixMilliseconds")>=started.ToUnixTimeMilliseconds())
{
var key=stop.GetRawText();if(key!=lastStop){lastStop=key;if(stops.Count>=16)throw new InvalidDataException("Unexpected repeated original stops.");stops.Add(stop.Clone());Log(new{kind="original-stop-receipt",phase,stop});}
}
if(ScoQaFlag(sample,"playerOverride")||ScoQaFlag(sample,"healthEmergency"))throw new OperationCanceledException("Native manual takeover or health protection.");
if(DateTimeOffset.UtcNow-lastFrame>=TimeSpan.FromSeconds(1))
{
lastFrame=DateTimeOffset.UtcNow;
Log(new{kind="sco-frame",at=lastFrame,phase,sampleReady=ScoQaFlag(status,"sampleReady"),
state=ScoQaFinite(sample,"state"),entityId=ScoQaText(sample,"entityId"),referenceWorld=ScoQaText(sample,"referenceWorldAddress"),
speed=ScoQaFinite(sample,"speedMetresPerSecond"),distance=ScoQaFinite(sample,"distanceMetres"),
nativeEpoch=ScoQaFinite(sample,"originalUpdateClockEpoch"),nativeSteps=ScoQaFinite(sample,"originalUpdateCompletedSteps"),
nativeSimulationSeconds=ScoQaFinite(sample,"originalUpdateCompletedSimulationSeconds"),
approach=ScoQaMaybe(ScoQaObject(sample,"scoExitApproach")),issue=ScoQaText(policy,"issue"),proofAvailable=ScoQaFlag(policy,"proofAvailable")});
}
}
async Task<JsonElement?> PollAsync()
{
Check();var native=await ReadAsync("Navigation");
if(native is {} main&&(ScoQaFlag(main,"playerOverride")||ScoQaFlag(ScoQaObject(main,"shipHealth"),"emergency")))
throw new OperationCanceledException("Native manual takeover or health emergency.");
var status=await ReadAsync(NativeCruiseControlChannel.Service);if(status is {} current)Observe(current);
Check();return status;
}
bool Ready(JsonElement status,bool sameBinding)
{
var sample=ScoQaObject(status,"sample");var at=ScoQaNumber(sample,"observedAtUnixMilliseconds");var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if(!ScoQaFlag(status,"sampleReady")||!ScoQaFlag(sample,"verified")||!ScoQaFlag(sample,"inSupercruise")||ScoQaNumber(sample,"rawFsdState")!=2
||!ScoQaFlag(sample,"originalUpdateClockValid")
||!double.IsFinite(at)||at>now||now-at>200||ScoQaNumber(ScoQaObject(sample,"target"),"marketId")!=destination.MarketId)return false;
if(sameBinding&&(ScoQaId(sample,"entityId")!=entity||ScoQaId(sample,"referenceWorldAddress")!=world))
{
var nextWorld=ScoQaId(sample,"referenceWorldAddress");
if(ScoQaId(sample,"entityId")==entity&&nextWorld!=0&&nextWorld!=world
&&(journey is {IsCompleted:false}||expectedStop))throw new ScoQaReferenceChangedException(world,nextWorld);
throw new InvalidDataException("SCO baseline binding changed before trial/promotion.");
}
return true;
}
Runtime.FlightAutomation.Progress+=Progress;
MainWindow.Closed+=WindowClosed;
using var cancellation=token.Register(()=>MainWindow.DispatcherQueue.TryEnqueue(StopOwned));
try
{
if(!initial.InGame||initial.CommanderId.Length==0||initial.CurrentShipId is null||initial.LoadGameAt is null||!initial.CargoReliable||initial.CargoCount!=0)
throw new InvalidOperationException("SCO QA requires the identified empty current ship in the real game.");
if(Runtime.FlightAutomation.IsActive||ViewModel.RunActive||ViewModel.NavigationActive||ViewModel.SystemFlightActive)
throw new InvalidOperationException("Another task is active; QA will not replace it.");
var target=await new WindowsGameSessionDriver(()=>Runtime.Player,()=>Runtime.Settings.PluginGameExecutable).FindTargetAsync(token);
pid=target.ProcessId;processStart=target.StartedUtcTicks;
var hello=await new NavigationPipeClient(pid).CallAsync(new{op="describe"},token);
hash=ScoQaText(hello,"gameSha256");if(hash!=XFEEDToolBox.Core.Native.NativeSystemLocationReader.GameSha256||ScoQaNumber(hello,"processId")!=pid)
throw new InvalidDataException("Pinned game build is unverified.");
fingerprint=NativeSystemFlightObservation.LoadoutFingerprint(initial.Ships.Single(s=>s.ShipId==initial.CurrentShipId));
ViewModel.DemoMode=false;AppShellPage.Current!.Navigate(typeof(NavigationPage));Check();
var pipe=new NavigationPipeClient(pid,NativeCruiseControlChannel.Service);
var existing=await ReadAsync(NativeCruiseControlChannel.Service);
if(existing is {} old)
{
Observe(old);if(ScoQaFlag(old,"owned"))throw new InvalidOperationException("An existing SCO owner is active.");
await pipe.CallAsync(new{op="sco.exit.configure",mode="shadow"},token);
}
while(true)
{
try
{
Check();expectedStop=false;phase="baseline";var baselineStarted=DateTimeOffset.UtcNow;
Log(new{kind="stage",phase,started,baselineStarted,referenceRequalifications,stationPath,destination,pid,processStart,fingerprint,minutes});
if(journey is null||journey.IsCompleted)journey=ViewModel.RunStationJourneyAsync(destination);
ulong originalTrialDeadline=0;
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,false)&&ScoQaFlag(ScoQaObject(current,"scoExitEstimate"),"proofAvailable")
&&ScoQaNumber(ScoQaObject(current,"scoExitEstimate"),"provenAtUnixMilliseconds")>=baselineStarted.ToUnixTimeMilliseconds())
{
var policy=ScoQaObject(current,"scoExitEstimate");var receipt=ScoQaObject(policy,"lastExitReceipt");var sample=ScoQaObject(current,"sample");
if(ScoQaText(policy,"trustedLoadoutFingerprint")!=fingerprint||!ScoQaFlag(receipt,"pass")||ScoQaFlag(receipt,"trial")||ScoQaFlag(receipt,"production")
||ScoQaNumber(receipt,"endCompletedSteps")<=ScoQaNumber(receipt,"startCompletedSteps")||ScoQaNumber(receipt,"integratedPathDistanceMetres")<=0)
throw new InvalidDataException("Baseline lacks a complete same-session native exit receipt.");
entity=ScoQaId(sample,"entityId");world=ScoQaId(sample,"referenceWorldAddress");epoch=(ulong)ScoQaNumber(sample,"originalUpdateClockEpoch");
if(entity==0||world==0||ScoQaId(receipt,"entityId")!=entity||ScoQaId(receipt,"referenceWorldAddress")!=world)
throw new InvalidDataException("Baseline proof belongs to another native binding.");
baseline=true;break;
}
if(journey.IsCompleted)throw new InvalidOperationException("Baseline journey ended before a complete SCO proof.");
await Task.Delay(100,token);
}
phase="release-baseline";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
while(true)
{
var status=await PollAsync();
if(status is {} idle&&Ready(idle,true)&&!ScoQaFlag(idle,"owned")&&ScoQaNumber(idle,"leaseExpiresAtUnixMilliseconds")==0
&&ScoQaFlag(ScoQaObject(idle,"scoExitEstimate"),"currentSourceDomainVerified"))
{
armedSteps=(ulong)ScoQaNumber(ScoQaObject(idle,"sample"),"originalUpdateCompletedSteps");
originalTrialDeadline=(ulong)DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeMilliseconds();
await pipe.CallAsync(new{op="sco.exit.configure",mode="trial",entityId=entity,referenceWorldAddress=world,
expiresAtUnixMilliseconds=originalTrialDeadline,maximumBursts=2},token);break;
}
await Task.Delay(100,token);
}
Check();expectedStop=false;phase="trial";Log(new{kind="stage",phase,entity,world,epoch,armedSteps});
var consumedTrialSteps=armedSteps;var continuedFallbacks=0;var attemptTrialUse=false;var attemptStartedAt=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
journey=ViewModel.RunStationJourneyAsync(destination);
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,true))
{
var sample=ScoQaObject(current,"sample");var choice=ScoQaObject(sample,"scoExitApproach");
var currentPolicy=ScoQaObject(current,"scoExitEstimate");
if(ScoQaNumber(sample,"originalUpdateClockEpoch")!=epoch||ScoQaText(currentPolicy,"mode")!="bounded-trial"
||ScoQaNumber(currentPolicy,"trialExpiresAtUnixMilliseconds")!=originalTrialDeadline
||DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()>=(long)originalTrialDeadline
||ScoQaObject(currentPolicy,"lastPolicyRevocationFailure").ValueKind!=JsonValueKind.Null)
throw new InvalidOperationException("Original trial clock, deadline or authorization was revoked.");
if(ScoQaFlag(choice,"useEstimatedStop")&&ScoQaText(choice,"mode")=="trial")trialUse=attemptTrialUse=true;
var stop=ScoQaObject(current,"lastOriginalStop");var gate=ScoQaObject(stop,"gate");var stopChoice=gate.ValueKind==JsonValueKind.Object?ScoExitApproachDecision.Read(gate):null;
var stopAt=ScoQaNumber(stop,"observedAtUnixMilliseconds");
var actualEstimatedStop=ScoQaFlag(stop,"nativeMethodCalled")&&!ScoQaFlag(stop,"argument")&&ScoQaText(stop,"nativeMethodRva")=="2eaacd0"
&&ScoQaText(stop,"reason")=="approach-or-alignment"&&ScoQaId(stop,"entityId")==entity&&ScoQaId(stop,"referenceWorldAddress")==world
&&ScoQaFlag(gate,"identityVerified")&&ScoQaFlag(gate,"targetVerified")&&ScoQaFlag(gate,"geometryVerified")
&&stopChoice is {UseEstimatedStop:true,Mode:"trial"}&&double.IsFinite(stopAt)&&stopAt>=attemptStartedAt
&&stopChoice.TargetKey==ScoQaNumber(ScoQaObject(sample,"target"),"key")
&&stopChoice.Applies(entity,world,stopChoice.TargetKey,(ulong)stopAt)
&&stopChoice.RequiresStop(ScoQaNumber(gate,"distanceMetres"),ScoQaNumber(gate,"speedMetresPerSecond"),true,
ScoQaFlag(gate,"cruiseParametersVerified"),ScoQaNumber(gate,"cruiseApproachSeconds"),entity,world,stopChoice.TargetKey,(ulong)stopAt);
if(actualEstimatedStop)trialUse=attemptTrialUse=true;
var receipt=ScoQaObject(ScoQaObject(current,"scoExitEstimate"),"lastExitReceipt");
if(receipt.ValueKind==JsonValueKind.Object&&ScoQaNumber(receipt,"clockEpoch")==epoch&&ScoQaNumber(receipt,"endCompletedSteps")>consumedTrialSteps)
{
if(!ScoQaFlag(receipt,"pass"))throw new InvalidOperationException("Trial exit failed: "+ScoQaText(receipt,"reason"));
if(ScoQaId(receipt,"entityId")!=entity||ScoQaId(receipt,"referenceWorldAddress")!=world
||ScoQaNumber(receipt,"targetKey")!=ScoQaNumber(ScoQaObject(sample,"target"),"key"))
throw new InvalidDataException("Trial exit receipt belongs to another ship, scene or target.");
if(ScoQaNumber(receipt,"startCompletedSteps")<=consumedTrialSteps)
throw new InvalidDataException("Trial exit receipt predates the current attempt.");
if(!ScoQaFlag(receipt,"trial")||!attemptTrialUse||!actualEstimatedStop)
{
var continuation=new ScoTrialContinuationContext(pid,hash,fingerprint,entity,world,epoch,
(int)ScoQaNumber(ScoQaObject(sample,"target"),"key"),consumedTrialSteps,originalTrialDeadline,continuedFallbacks);
if(!ScoTrialFallbackContinuation.CanContinue(current,continuation,DateTimeOffset.UtcNow,out var why))
throw new InvalidOperationException("Only an actual estimated approach stop may pass; fallback cannot continue: "+why);
var consumedEnd=(ulong)ScoQaNumber(receipt,"endCompletedSteps");continuedFallbacks++;
var fallback=new{at=DateTimeOffset.UtcNow,entity,world,epoch,attempt=1,receiptStart=ScoQaNumber(receipt,"startCompletedSteps"),receiptEnd=consumedEnd,
attemptTrialUse,actualEstimatedStop,reason=why,originalTrialDeadline,remainingNativeBursts=1,deadlineExtended=false,rearmed=false};
domainFallbacks.Add(fallback);Log(new{kind="calibrated-domain-fallback",phase,fallback});
phase="release-domain-fallback";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
while(true)
{
Check();if((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()>=originalTrialDeadline)
throw new InvalidOperationException("Original trial authorization expired before its remaining burst.");
var next=await PollAsync();
if(next is {} idle&&Ready(idle,true))
{
var policy=ScoQaObject(idle,"scoExitEstimate");var freshSample=ScoQaObject(idle,"sample");
if(ScoQaNumber(freshSample,"originalUpdateClockEpoch")!=epoch||ScoQaText(policy,"mode")!="bounded-trial"
||ScoQaNumber(policy,"trialExpiresAtUnixMilliseconds")!=originalTrialDeadline||ScoQaNumber(policy,"maximumTrialBurstsRemaining")!=1
||ScoQaObject(policy,"lastPolicyRevocationFailure").ValueKind!=JsonValueKind.Null)
throw new InvalidOperationException("Original remaining trial authorization changed during release.");
if(!ScoQaFlag(idle,"owned")&&ScoQaNumber(idle,"leaseExpiresAtUnixMilliseconds")==0&&ScoQaNumber(freshSample,"state")==0
&&ScoQaFlag(policy,"currentSourceDomainVerified"))
{
if(ScoQaNumber(ScoQaObject(policy,"lastExitReceipt"),"endCompletedSteps")!=consumedEnd)
throw new InvalidDataException("Fallback receipt changed during release.");
if(!ScoTrialFallbackContinuation.CanContinue(idle,continuation,DateTimeOffset.UtcNow,out why))
throw new InvalidOperationException("Remaining trial burst lost its verified fallback receipt: "+why);
consumedTrialSteps=Math.Max(consumedEnd,(ulong)ScoQaNumber(freshSample,"originalUpdateCompletedSteps"));break;
}
}
await Task.Delay(100,token);
}
Check();expectedStop=false;phase="trial";attemptTrialUse=false;attemptStartedAt=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Log(new{kind="remaining-trial-burst",phase,attempt=2,consumedTrialSteps,originalTrialDeadline,remainingNativeBursts=1,deadlineExtended=false,rearmed=false});
journey=ViewModel.RunStationJourneyAsync(destination);continue;
}
trialPass=true;break;
}
}
if(journey.IsCompleted)throw new InvalidOperationException("Trial journey ended without a verified estimated exit.");
await Task.Delay(100,token);
}
// Promotion is a policy change on an idle channel. Release our
// journey explicitly instead of racing its next automatic burst.
phase="release-trial";expectedStop=true;StopOwned();await journey.WaitAsync(TimeSpan.FromSeconds(15),token);
phase="promote";
while(true)
{
var status=await PollAsync();
if(status is {} current&&Ready(current,true)&&!ScoQaFlag(current,"owned")&&ScoQaNumber(current,"leaseExpiresAtUnixMilliseconds")==0
&&ScoQaNumber(ScoQaObject(current,"sample"),"state")==0
&&ScoQaFlag(ScoQaObject(current,"scoExitEstimate"),"currentSourceDomainVerified"))
{
var result=await pipe.CallAsync(new{op="sco.exit.configure",mode="production",entityId=entity,referenceWorldAddress=world,
expiresAtUnixMilliseconds=DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds()},token);
Observe(result);promoted=ScoQaFlag(ScoQaObject(result,"scoExitEstimate"),"productionAvailable");
if(!promoted)throw new InvalidDataException("Production promotion did not confirm its bounded configuration.");break;
}
await Task.Delay(100,token);
}
break;
}
catch(ScoQaReferenceChangedException change)when(referenceRequalifications<3)
{
Check();referenceRequalifications++;
Log(new{kind="natural-reference-requalification",at=DateTimeOffset.UtcNow,change.PreviousWorld,change.CurrentWorld,referenceRequalifications,
maximumRequalifications=3,originalBudgetUnchanged=true,oldProofReused=false});
await pipe.CallAsync(new{op="sco.exit.configure",mode="shadow"},token);
baseline=trialUse=trialPass=promoted=false;entity=world=epoch=armedSteps=0;
// Keep the healthy ordinary journey running. Only restart it
// if this QA already released it; user Stop remains terminal.
Check();
}
}
Check();expectedStop=false;phase="dock";Log(new{kind="stage",phase,promoted});
journey=ViewModel.RunStationJourneyAsync(destination);
while(!journey.IsCompleted){await PollAsync();await Task.Delay(100,token);}
await journey;Check();var actual=Runtime.Player;
docked=actual.Docked&&actual.MarketId==destination.MarketId&&actual.SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase)
&&actual.CargoReliable&&actual.CargoCount==0&&Runtime.FlightAutomation.Stage==FlightAutomationStage.Completed;
if(!docked)throw new InvalidOperationException("Real empty-ship docking at the acceptance station was not confirmed.");
passed=baseline&&trialUse&&trialPass&&promoted&&docked;
}
catch(Exception error){failure=error.ToString();passed=false;}
finally
{
expectedStop=true;StopOwned();
if(journey is {IsCompleted:false})try{await journey.WaitAsync(TimeSpan.FromSeconds(15));}catch(Exception e){failure=(failure??"")+" / release: "+e.Message;passed=false;}
Runtime.FlightAutomation.Progress-=Progress;
if(!passed&&pid!=0)try{await new NavigationPipeClient(pid,NativeCruiseControlChannel.Service).CallAsync(new{op="sco.exit.configure",mode="shadow"});}catch(Exception e){Log(new{kind="strategy-release-unconfirmed",message=e.Message});}
}
phase="report";
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new
{
started,completedAt=DateTimeOffset.UtcNow,passed,failure,stationPath,destination,pid,processStart,fingerprint,baseline,trialUse,trialPass,promoted,docked,
actual=new{Runtime.Player.SystemName,Runtime.Player.MarketId,Runtime.Player.Docked,Runtime.Player.CargoCount,Runtime.Player.CargoReliable,Runtime.Player.CurrentShipId},
totalMinutes=minutes,events,referenceRequalifications,exits,stops,domainFallbacks,lastPolicy=finalStatus is {} final?ScoQaMaybe(ScoQaObject(final,"scoExitEstimate")):null,
resumeCgConfiguration=resume,realJournalUsed=true,normalStationJourneyUsed=true,osInputSent=false,journalWrites=0,navRouteWrites=0
},new JsonSerializerOptions(ToolboxSettings.JsonOptions){WriteIndented=true}));
lock(logGate){log.Flush();closed=true;}
if(passed&&resume is not null)
{
// The completed flight receipt is historical evidence, not a new
// authorization after Stop/close received while writing it.
try{await PollAsync();Check();}
catch(Exception error)
{
MainWindow.Closed-=WindowClosed;
await File.WriteAllTextAsync(reportPath+".resume.json",JsonSerializer.Serialize(new
{at=DateTimeOffset.UtcNow,scoAcceptancePassed=true,resumeStarted=false,reason=error.Message},lineOptions));
if(!windowClosing)Runtime.Log("SCO实机验收","验收已完成;接续跑商前状态或运行授权改变,未启动跑商。");
return;
}
MainWindow.AppWindow.Show(false);AppShellPage.Current!.Navigate(typeof(PlannerPage));
var resumeAuthorized=false;
try
{
await ViewModel.RunAutomaticTradeAsync(resume,()=>
{
Check();
if(Runtime.FlightAutomation.IsActive||ViewModel.RunActive||ViewModel.NavigationActive||ViewModel.SystemFlightActive)
throw new InvalidOperationException("Another task started before the CG handoff; its control remains unchanged.");
MainWindow.Closed-=WindowClosed;resumeAuthorized=true;
});
}
catch(Exception error)when(!resumeAuthorized)
{
MainWindow.Closed-=WindowClosed;
await File.WriteAllTextAsync(reportPath+".resume.json",JsonSerializer.Serialize(new
{at=DateTimeOffset.UtcNow,scoAcceptancePassed=true,resumeStarted=false,reason=error.Message},lineOptions));
if(!windowClosing)Runtime.Log("SCO实机验收","配置准备期间状态或运行授权改变,未启动跑商。");
}
}
else
{
MainWindow.Closed-=WindowClosed;
if(!passed){ViewModel.ShowError("SCO 实机验收未通过:"+(failure??"unknown"));Runtime.Log("SCO实机验收",failure??"unknown");MainWindow.Close();}
}
}
private static JsonElement ScoQaObject(JsonElement value,string name)=>value.ValueKind==JsonValueKind.Object&&value.TryGetProperty(name,out var item)?item:default;
private static string ScoQaText(JsonElement value,string name){var item=ScoQaObject(value,name);return item.ValueKind==JsonValueKind.String?item.GetString()??"":"";}
private static bool ScoQaFlag(JsonElement value,string name)=>ScoQaObject(value,name).ValueKind==JsonValueKind.True;
private static double ScoQaNumber(JsonElement value,string name){var item=ScoQaObject(value,name);return item.ValueKind==JsonValueKind.Number&&item.TryGetDouble(out var result)?result:double.NaN;}
private static double? ScoQaFinite(JsonElement value,string name){var result=ScoQaNumber(value,name);return double.IsFinite(result)?result:null;}
private static JsonElement? ScoQaMaybe(JsonElement value)=>value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null?null:value;
private static ulong ScoQaId(JsonElement value,string name)=>ulong.TryParse(ScoQaText(value,name),NumberStyles.None,CultureInfo.InvariantCulture,out var result)?result:0;
private sealed class ScoQaReferenceChangedException(ulong previousWorld,ulong currentWorld):Exception("Natural reference world changed; a new same-binding baseline is required.")
{
public ulong PreviousWorld{get;}=previousWorld;
public ulong CurrentWorld{get;}=currentWorld;
}
}