using System.Text.Json; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; using Autodesk.ProcessPower.PlantInstance; using Autodesk.ProcessPower.ProjectManager; using PRecAnnot.Abstractions; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; namespace PRecAnnot.Plant3D2026; // Test-only coordinator. It is not registered in Ribbon and is inert unless // the explicit NextPilot automation environment and local marker are present. internal sealed record FirstApplyAutomationConfiguration { public required string Phase { get; init; } public string? OrthoDrawingPath { get; init; } public string? ModelDrawingPath { get; init; } public required string EvidencePath { get; init; } public required string FixtureMarkerPath { get; init; } } internal sealed record AutomationEntityCounts(int Total, int PipeBlocks, int PxpOaXdata, int Owned); internal static class FirstLimitedApplyAutomationBootstrap { private enum BootstrapState { Created, Prepared, Armed, Starting, Completed } private static BootstrapState state; private static bool observedIdle; private static int idleCallbackCount; private static int coordinatorStartCount; internal static int IdleCallbackCount => idleCallbackCount; internal static int CoordinatorStartCount => coordinatorStartCount; // Called from Initialize: BCL gates and one lightweight subscription only. internal static void Prepare() { if (state != BootstrapState.Created) return; state = BootstrapState.Prepared; AcadApplication.Idle += OnIdle; RuntimeDiagnostics.Write("AutomationBootstrapSubscribed", resultStatus: "Prepared"); } // Public test command is intentionally side-effect free with respect to Plant APIs. internal static void Arm() { if (state is BootstrapState.Prepared) { // The command itself is the post-NETLOAD barrier. The next Idle // callback, not this command callback, is allowed to start work. observedIdle = true; state = BootstrapState.Armed; } } private static void OnIdle(object? sender, EventArgs args) { try { idleCallbackCount++; RuntimeDiagnostics.Write("AutomationBootstrapIdle", resultStatus: state.ToString()); // The script engine can consume the custom command after NETLOAD. The // first idle is therefore the explicit post-NETLOAD arming point. if (state == BootstrapState.Prepared) { observedIdle = true; state = BootstrapState.Armed; } if (!observedIdle || state != BootstrapState.Armed) return; state = BootstrapState.Starting; AcadApplication.Idle -= OnIdle; coordinatorStartCount++; RuntimeDiagnostics.Write("AutomationCoordinatorStarting", resultStatus: "FirstPostStartupIdle"); new FirstLimitedApplyAutomationCoordinator().Start(); } catch (System.Exception exception) { state = BootstrapState.Completed; AcadApplication.Idle -= OnIdle; FirstLimitedApplyAutomationCoordinator.WriteBootstrapFailure(exception); } } } internal sealed record FirstApplyAutomationEvidence { public int SchemaVersion { get; init; } = 1; public required string Phase { get; init; } public bool Completed { get; init; } public bool? Prepared { get; init; } public bool? FunctionalEvidenceCompleted { get; init; } public string? ErrorCode { get; init; } public int? DbmodBefore { get; init; } public int? DbmodAfterCommand1 { get; init; } public int? DbmodAfterCommand2 { get; init; } public int? DbmodAfterUndo { get; init; } public AutomationEntityCounts? Baseline { get; init; } public AutomationEntityCounts? AfterCommand1 { get; init; } public AutomationEntityCounts? AfterCommand2 { get; init; } public AutomationEntityCounts? AfterUndo { get; init; } public IReadOnlyList CompletedCommands { get; init; } = Array.Empty(); public string? PlanIdRun1 { get; init; } public string? PlanIdRun2 { get; init; } public bool? PlanIdsEqual { get; init; } public int? EligibleCandidateCount { get; init; } public string? SelectedCandidateHash { get; init; } public string? ViewHash { get; init; } public int? CreateRun1 { get; init; } public int? CreateRun2 { get; init; } public int? SkipRun2 { get; init; } public string? ApplyOutcomeRun1 { get; init; } public string? ApplyOutcomeRun2 { get; init; } public bool? PermitAccepted { get; init; } public bool? CommandsCompleted { get; init; } public bool? InspectorsCompleted { get; init; } public bool? EvidenceFlushed { get; init; } public bool? ExitAttemptQueued { get; init; } public bool? QuitCommandWillStart { get; init; } public bool? QuitCommandEnded { get; init; } public string? ExitFailureCode { get; init; } public int? IdleCallbackCount { get; init; } public int? CoordinatorStartCount { get; init; } // Local test evidence only. This is intentionally never copied into Git // evidence because exception stacks may contain machine-specific paths. public string? ExceptionText { get; init; } public string? PreviousState { get; init; } public string? CurrentState { get; init; } public IReadOnlyList StateTransitions { get; init; } = Array.Empty(); } internal sealed class FirstLimitedApplyAutomationCoordinator { private enum CoordinatorState { Created, Starting, Running, Completed, Failed, ExitQueued } private sealed record PxpResult(string? PlanId, IReadOnlyList Candidates, string? View, int Create, int Skip, bool? PermitAccepted, string? ApplyOutcome); private static string? selectedTargetSnapshotId; private static readonly List pxpResults = []; internal static string? SelectedTargetSnapshotId => selectedTargetSnapshotId; internal static void WriteBootstrapFailure(System.Exception exception) => RuntimeDiagnostics.Write("AutomationBootstrapFailed", "ERROR", resultStatus: "AUTOMATION_BOOTSTRAP_FAILED", exception: exception); internal static void RecordPxpOa(string? planId, IReadOnlyList candidates, string? view, int create, int skip, bool? permitAccepted) { if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal)) pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted, null)); } internal static void RecordApplyOutcome(PipeAnnotationWriteOutcome outcome) { if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal) || pxpResults.Count == 0) return; var last = pxpResults[^1]; pxpResults[^1] = last with { ApplyOutcome = outcome.ToString() }; } private FirstApplyAutomationConfiguration? config; private Document? document; private readonly List automationOpenedDocuments = []; private readonly List completed = []; private int step; private int? before, after1, after2, afterUndo; private AutomationEntityCounts? baseline, counts1, counts2, countsUndo; private readonly List transitions = ["Created"]; private CoordinatorState state = CoordinatorState.Created; private CoordinatorState previousState = CoordinatorState.Created; private bool documentHandlersSubscribed; private bool lifecycleHandlersSubscribed; private bool fixtureDocumentActivated; private bool projectActivationStarted; private bool exitQueued; private bool exitHandlerSubscribed; private string? exceptionText; private bool functionalCompleted; private bool evidenceFlushed; private bool exitAttemptQueued; private string? exitFailureCode; public void Start() { try { Transition(CoordinatorState.Starting, "PostStartupReady"); config = ReadConfiguration(); if (string.Equals(config.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase)) { CompleteLifecycleProbe(); return; } if (string.IsNullOrWhiteSpace(config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED"); // A first startup Idle is sufficient to arm automation, but not to // call Plant APIs. Establish an AutoCAD document first and wait for // its activation plus a quiescent callback before project activation. AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated; AcadApplication.Idle += OnIdle; lifecycleHandlersSubscribed = true; if (!string.IsNullOrWhiteSpace(config.ModelDrawingPath)) automationOpenedDocuments.Add(AcadApplication.DocumentManager.Open(config.ModelDrawingPath, false)); transitions.Add("DocumentOpenStarted"); RuntimeDiagnostics.Write("AutomationBeforeDocumentOpen", resultStatus: "PostStartupReady"); document = AcadApplication.DocumentManager.Open(config.OrthoDrawingPath, false); automationOpenedDocuments.Add(document); AcadApplication.DocumentManager.MdiActiveDocument = document; transitions.Add("DocumentOpened"); RuntimeDiagnostics.Write("AutomationAfterDocumentOpen", resultStatus: "WaitingForDocumentActivation", document: document); document.CommandEnded += OnCommandEnded; document.CommandFailed += OnCommandFailed; document.CommandCancelled += OnCommandCancelled; documentHandlersSubscribed = true; Transition(CoordinatorState.Running, "AutomationRunning"); // DocumentActivated can be raised synchronously by Open before the // coordinator receives the returned Document. Confirm the current // active document after assignment so that one missed event cannot // strand the workflow waiting for another startup Idle. // Open was requested for this exact local fixture and it has just // been made MDI-active. Queue a no-op public command regardless of // wrapper identity/name normalisation; CommandEnded is the actual // readiness proof before any Plant API is used. fixtureDocumentActivated = true; transitions.Add("FixtureDocumentActiveAfterOpen"); QueueReadinessBarrier(); } catch (System.Exception ex) { FailFromCallback("Start", ex, "AUTOMATION_START_FAILED"); } } private void CompleteLifecycleProbe() { transitions.Add("LifecycleProbeCompleted"); WriteEvidence(true, null); // LifecycleProbe is already running from the first verified Idle callback. // Do not wait for a second Idle: a startup-script host is permitted not to // raise one, which otherwise leaves acad.exe responsive but stranded. RequestControlledExit(idleSafe: true); } private void OnIdle(object? sender, EventArgs e) => RunCallback("Idle", TryActivateProjectAndVerifyContext); private void OnDocumentActivated(object sender, DocumentCollectionEventArgs e) { RunCallback("DocumentActivated", () => { if (document is null || e.Document is null) return; if (!string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) return; fixtureDocumentActivated = true; transitions.Add("FixtureDocumentActivated"); TryActivateProjectAndVerifyContext(); }); } private void TryActivateProjectAndVerifyContext() { if (IsTerminal || document is null || !fixtureDocumentActivated || projectActivationStarted) return; if (!AcadApplication.IsQuiescent) return; projectActivationStarted = true; AcadApplication.Idle -= OnIdle; AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated; lifecycleHandlersSubscribed = false; try { transitions.Add("ProjectActivationStarted"); RuntimeDiagnostics.Write("AutomationBeforeProjectActivation", resultStatus: "DocumentActivatedAndQuiescent", document: document); var projectXml = Path.Combine(Path.GetDirectoryName(config!.FixtureMarkerPath)!, "Project.xml"); var project = PlantProject.LoadProject(projectXml, false, string.Empty, string.Empty); PlantApplication.SetCurrentProject(project, true); transitions.Add("ProjectActivated"); RuntimeDiagnostics.Write("AutomationAfterProjectActivation", resultStatus: "ProjectAvailable", document: document); transitions.Add("ContextVerificationStarted"); var context = new PlantRuntimeContextReader().ReadCurrentContext(document); if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1) { Complete(false, $"AUTOMATION_ORTHO_CONTEXT_INVALID:P={context.HasPlantProject};O={context.IsOrthoDrawing};V={context.Viewports.Count}"); return; } var candidates = new PlantObjectReader().ReadObjects(context.Viewports.Single(), CancellationToken.None) .Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null) .Select(x => x.SnapshotId).OrderBy(x => x, StringComparer.Ordinal).ToArray(); if (candidates.Length == 0) { Complete(false, "AUTOMATION_NO_ELIGIBLE_CANDIDATE"); return; } selectedTargetSnapshotId = candidates[0]; transitions.Add("ContextVerified"); before = Dbmod(); baseline = Counts(); Queue("PXP-OA"); } catch (System.Exception ex) { FailFromCallback("ContextVerification", ex, "AUTOMATION_CONTEXT_FAILED"); } } private void Queue(string command) => _ = DispatchCommandAsync(command, false); private void QueueReadinessBarrier() { _ = DispatchCommandAsync("PHP_REC_AUTOMATION_READY", true); } private async Task DispatchCommandAsync(string command, bool readiness) { if (IsTerminal || document is null) return; var expected = document; transitions.Add(readiness ? "ReadinessCommandDispatchRequested" : "CommandDispatchRequested:" + command); try { await AcadApplication.DocumentManager.ExecuteInCommandContextAsync(async _ => { transitions.Add("CommandContextEntered:" + command); var active = AcadApplication.DocumentManager.MdiActiveDocument; if (active is null || !string.Equals(active.Name, expected.Name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AUTOMATION_COMMAND_DOCUMENT_NOT_ACTIVE"); transitions.Add(readiness ? "ReadinessBarrierQueued" : "CommandQueued:" + command); await active.Editor.CommandAsync(command); transitions.Add("CommandInvocationReturned:" + command); }, null); } catch (System.Exception exception) { FailFromCallback("CommandDispatcher", exception, "AUTOMATION_COMMAND_CONTEXT_FAILED"); } } private void OnCommandEnded(object sender, CommandEventArgs e) { RunCallback("CommandEnded", () => { var name = e.GlobalCommandName?.Trim().TrimStart('.', '_').ToUpperInvariant(); if (name == "PHP_REC_AUTOMATION_READY") { transitions.Add("ReadinessBarrierCompleted"); TryActivateProjectAndVerifyContext(); return; } if (name is not ("PXP-OA" or "UNDO" or "U")) return; completed.Add(name); transitions.Add("CommandCompleted:" + name); if (step++ == 0) { after1 = Dbmod(); counts1 = Counts(); Queue("PXP-OA"); return; } if (step == 2 && string.Equals(config?.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { after2 = Dbmod(); counts2 = Counts(); Queue("_.UNDO _1"); return; } if (string.Equals(config?.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { afterUndo = Dbmod(); countsUndo = Counts(); Complete(true, null, safeToExitNow: true); } else { after2 = Dbmod(); counts2 = Counts(); Complete(true, null, safeToExitNow: true); } }); } private void OnCommandFailed(object sender, CommandEventArgs e) => RunCallback("CommandFailed", () => Complete(false, "AUTOMATION_COMMAND_FAILED:" + e.GlobalCommandName)); private void OnCommandCancelled(object sender, CommandEventArgs e) => RunCallback("CommandCancelled", () => Complete(false, "AUTOMATION_COMMAND_CANCELLED:" + e.GlobalCommandName)); private int Dbmod() => Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD")); private AutomationEntityCounts Counts() { if (document is null) throw new InvalidOperationException("AUTOMATION_DOCUMENT_UNAVAILABLE"); using var tx = document.Database.TransactionManager.StartOpenCloseTransaction(); var table = (BlockTable)tx.GetObject(document.Database.BlockTableId, OpenMode.ForRead); var paper = (BlockTableRecord)tx.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForRead); var total = 0; var pipes = 0; var xdata = 0; var owned = 0; foreach (ObjectId id in paper) { total++; if (tx.GetObject(id, OpenMode.ForRead) is BlockReference block && string.Equals(block.Name, "PIPE", StringComparison.OrdinalIgnoreCase)) pipes++; if (tx.GetObject(id, OpenMode.ForRead) is Entity entity) { using var data = entity.GetXDataForApplication("PXP-OA"); if (data is not null) { xdata++; if (data.AsArray().Any(x => x.TypeCode == 1000)) owned++; } } } return new(total, pipes, xdata, owned); } private FirstApplyAutomationConfiguration ReadConfiguration() { if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_DISABLED"); if (!string.Equals(RuntimeDiagnostics.RuntimeMode, "DryRun", StringComparison.OrdinalIgnoreCase) && !string.Equals(RuntimeDiagnostics.RuntimeMode, "Apply", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AUTOMATION_MODE_BLOCKED"); var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION_CONFIG"); if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_CONFIGURATION_REQUIRED"); var loaded = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("AUTOMATION_CONFIGURATION_INVALID"); if (!File.Exists(loaded.FixtureMarkerPath) || !string.Equals(File.ReadAllText(loaded.FixtureMarkerPath).Trim(), "P_REC_ANNOT_TEST_FIXTURE=FIRST_LIMITED_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_FIXTURE_MARKER_INVALID"); if (!string.Equals(loaded.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase) && (string.IsNullOrWhiteSpace(loaded.OrthoDrawingPath) || !Path.GetFullPath(loaded.OrthoDrawingPath).StartsWith(Path.GetDirectoryName(Path.GetFullPath(loaded.FixtureMarkerPath))! + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("AUTOMATION_FIXTURE_OUTSIDE_MARKER_ROOT"); return loaded; } private void Complete(bool success, string? error, bool safeToExitNow = false) { if (IsTerminal) return; functionalCompleted = success; Transition(success ? CoordinatorState.Completed : CoordinatorState.Failed, success ? "Completed" : "Failed"); UnsubscribeHandlers(); TryWriteEvidence(success, error); selectedTargetSnapshotId = null; pxpResults.Clear(); RequestControlledExit(safeToExitNow); } // The managed Application.Quit call can leave a startup-script host alive // when it is invoked from a CommandEnded callback. Queue AutoCAD's native // QUIT command instead; the command processor runs it after the current // public PXP-OA command has fully returned and discards every open drawing. private void RequestControlledExit(bool idleSafe = false) { if (exitQueued) return; exitQueued = true; Transition(CoordinatorState.ExitQueued, "ExitQueued"); if (idleSafe) { DispatchNativeQuit(); return; } AcadApplication.Idle += OnExitIdle; exitHandlerSubscribed = true; } private void OnExitIdle(object? sender, EventArgs args) { RunCallback("ControlledExitIdle", DispatchNativeQuit); } private void DispatchNativeQuit() { try { if (exitHandlerSubscribed) AcadApplication.Idle -= OnExitIdle; exitHandlerSubscribed = false; exitAttemptQueued = true; if (!AcadApplication.IsQuiescent) { transitions.Add("QuitDispatchDeferredNotQuiescent"); TryWriteEvidence(functionalCompleted, null); return; } // LifecycleProbe opens no fixture document, but the real startup // document is available at this point (observed as Drawing1.dwg). // Use that live document only to queue native QUIT; never retain it // past this callback or use it for fixture/Plant work. var exitDocument = AcadApplication.DocumentManager.MdiActiveDocument; if (exitDocument is null) { exitFailureCode = "AUTOMATION_EXIT_DOCUMENT_UNAVAILABLE"; transitions.Add("QuitDispatchSkippedNoLiveDocument"); TryWriteEvidence(functionalCompleted, null); return; } transitions.Add("QuitDispatchAttempted"); // Persist the exit boundary before handing control back to AutoCAD; // process termination can otherwise prevent the final evidence flush. TryWriteEvidence(functionalCompleted, null); exitDocument.SendStringToExecute("_.QUIT _N\n", true, true, false); document = null; automationOpenedDocuments.Clear(); } catch (System.Exception exception) { // This is terminal cleanup: contain the exception and leave process // cleanup to Launcher rather than re-entering document APIs. // A failed graceful-exit dispatch is not a failure of already // completed PXP-OA/UNDO work. Launcher owns bounded forced cleanup. exitFailureCode = "AUTOMATION_CONTROLLED_EXIT_FAILED:" + exception.GetType().Name; transitions.Add("QuitDispatchFailed"); TryWriteEvidence(functionalCompleted, null); } } private bool IsTerminal => state is CoordinatorState.Completed or CoordinatorState.Failed or CoordinatorState.ExitQueued; private void Transition(CoordinatorState next, string transition) { previousState = state; state = next; transitions.Add(transition); } private void RunCallback(string callback, Action action) { if (IsTerminal && callback != "ControlledExitIdle") return; try { action(); } catch (System.Exception exception) { FailFromCallback(callback, exception, "AUTOMATION_CALLBACK_FAILED"); } } private void FailFromCallback(string callback, System.Exception exception, string code) { exceptionText = exception.ToString(); RuntimeDiagnostics.Write("AutomationCallbackFailed", "ERROR", resultStatus: code + ":" + callback, exception: exception); if (!IsTerminal) Complete(false, code + ":" + callback + ":" + exception.GetType().Name); else { UnsubscribeHandlers(); if (functionalCompleted) { exitFailureCode = code + ":" + callback + ":" + exception.GetType().Name; transitions.Add("ExitCallbackFailed"); TryWriteEvidence(true, null); } else TryWriteEvidence(false, code + ":" + callback + ":" + exception.GetType().Name); } } private void UnsubscribeHandlers() { if (documentHandlersSubscribed && document is not null) { try { document.CommandEnded -= OnCommandEnded; document.CommandFailed -= OnCommandFailed; document.CommandCancelled -= OnCommandCancelled; } catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationHandlerUnsubscribeFailed", "ERROR", resultStatus: "AUTOMATION_UNSUBSCRIBE_FAILED", exception: exception); } } documentHandlersSubscribed = false; if (lifecycleHandlersSubscribed) { try { AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated; AcadApplication.Idle -= OnIdle; } catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationHandlerUnsubscribeFailed", "ERROR", resultStatus: "AUTOMATION_UNSUBSCRIBE_FAILED", exception: exception); } } lifecycleHandlersSubscribed = false; } private bool TryWriteEvidence(bool success, string? error) { try { evidenceFlushed = true; WriteEvidence(success, error); return true; } catch (System.Exception exception) { evidenceFlushed = false; RuntimeDiagnostics.Write("AutomationEvidenceWriteFailed", "ERROR", resultStatus: "AUTOMATION_EVIDENCE_WRITE_FAILED", exception: exception); return false; } } private void WriteEvidence(bool success, string? error) { if (config is null) return; var first = pxpResults.ElementAtOrDefault(0); var second = pxpResults.ElementAtOrDefault(1); var expectedCommands = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase) ? 3 : 2; var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, ApplyOutcomeRun1 = first?.ApplyOutcome, ApplyOutcomeRun2 = second?.ApplyOutcome, PermitAccepted = first?.PermitAccepted, CommandsCompleted = completed.Count >= expectedCommands, InspectorsCompleted = baseline is not null && counts1 is not null && counts2 is not null && (!string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase) || countsUndo is not null), EvidenceFlushed = evidenceFlushed, ExitAttemptQueued = exitAttemptQueued, ExitFailureCode = exitFailureCode, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, ExceptionText = exceptionText, PreviousState = previousState.ToString(), CurrentState = state.ToString(), StateTransitions = transitions }; Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!); File.WriteAllText(config.EvidencePath, JsonSerializer.Serialize(evidence)); } }