diff --git a/.gitignore b/.gitignore index c704992..3cc874d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ /link/ /link +# Generated build output is never source or package input. +**/bin/ +**/obj/ + # DocumentAutomation generated files and local source data DocumentAutomation/**/bin/ DocumentAutomation/**/obj/ diff --git a/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/EntryPoint.cs index 7c487cc..2b6aef8 100644 --- a/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/EntryPoint.cs @@ -9,8 +9,18 @@ public sealed class EntryPoint : IExtensionApplication { public void Initialize() { - RuntimeMarker.Write("AssemblyInitializeStarted"); - RuntimeMarker.Write("AssemblyInitializeCompleted"); + // Keep the load boundary free of Plant/DataLinks access. Any exception + // here must remain observable independently of a project fixture. + RuntimeMarker.Write("InitializeEntered"); + try + { + RuntimeMarker.Write("AssemblyInitializeCompleted"); + } + catch (System.Exception exception) + { + RuntimeMarker.Write("AssemblyInitializeFailed", exception.GetType().Name + ":" + exception.Message); + throw; + } } public void Terminate() => RuntimeMarker.Write("AssemblyTerminateCompleted"); @@ -37,8 +47,27 @@ public sealed class EntryPoint : IExtensionApplication }, UpdateExecutionMode.DryRun, ControlledFixturePermit: false)); + var status = new + { + schemaVersion = 1, + operationId = Environment.GetEnvironmentVariable("P_REC_UPDATE_OPERATION_ID") ?? "unknown", + processId = Environment.ProcessId, + command, + assemblyVersion = typeof(EntryPoint).Assembly.GetName().Version?.ToString(), + assemblySha256 = RuntimeMarker.AssemblySha256(), + product = "Plant3D", + productYear = 2026, + activeDocumentPresent = document is not null, + activePlantProjectPresent = HasPlantProject(), + dryRunInfrastructure = "Available", + domainApply = "BlockedByExactFixture", + diagnostics = plan.Diagnostics + }; document?.Editor.WriteMessage($"\nP_Rec_Update {command}: {(plan.Accepted ? "DryRun planned; no project data changed." : string.Join(", ", plan.Diagnostics))}"); - RuntimeMarker.Write(plan.Accepted ? "CommandRegistrationVerified" : "CommandPreflightRejected"); + // STATUS proves registration and reports preflight state; it must not + // be classified as a failed command merely because no controlled Plant + // project fixture is open. + RuntimeMarker.Write(command == "P-R-UPD-STATUS" || plan.Accepted ? "CommandRegistrationVerified" : "CommandPreflightRejected", System.Text.Json.JsonSerializer.Serialize(status)); } private static bool HasPlantProject() diff --git a/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/RuntimeMarker.cs b/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/RuntimeMarker.cs index b6f3b4a..60c8b03 100644 --- a/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/RuntimeMarker.cs +++ b/Plant3D2026/P_Rec_Update/src/PRecUpdate.Plant3D2026/RuntimeMarker.cs @@ -1,19 +1,30 @@ using System.Security.Cryptography; using System.Text; +using System.Text.Json; namespace PRecUpdate.Plant3D2026; internal static class RuntimeMarker { - internal static void Write(string state) + internal static void Write(string state, string? detail = null) { var root = Environment.GetEnvironmentVariable("P_REC_UPDATE_MARKER_ROOT"); if (string.IsNullOrWhiteSpace(root)) return; try { Directory.CreateDirectory(root); - var payload = $"{DateTimeOffset.UtcNow:O}|{state}|{typeof(EntryPoint).Assembly.GetName().Name}"; - File.WriteAllText(Path.Combine(root, state + ".marker"), payload, new UTF8Encoding(false)); + var payload = JsonSerializer.Serialize(new + { + schemaVersion = 1, + operationId = Environment.GetEnvironmentVariable("P_REC_UPDATE_OPERATION_ID") ?? "unknown", + processId = Environment.ProcessId, + timestampUtc = DateTimeOffset.UtcNow, + state, + detail, + assembly = typeof(EntryPoint).Assembly.GetName().Name, + assemblySha256 = AssemblySha256() + }); + File.WriteAllText(Path.Combine(root, state + ".marker.json"), payload, new UTF8Encoding(false)); } catch { /* Evidence cannot make Plant initialization fail. */ } } diff --git a/Plant3D2026/P_Rec_Update/tests/PRecUpdate.Core.Tests/UpdatePlanBuilderTests.cs b/Plant3D2026/P_Rec_Update/tests/PRecUpdate.Core.Tests/UpdatePlanBuilderTests.cs index 1c1d3dc..9fcace0 100644 --- a/Plant3D2026/P_Rec_Update/tests/PRecUpdate.Core.Tests/UpdatePlanBuilderTests.cs +++ b/Plant3D2026/P_Rec_Update/tests/PRecUpdate.Core.Tests/UpdatePlanBuilderTests.cs @@ -46,4 +46,17 @@ public sealed class UpdatePlanBuilderTests Assert.True(plan.MayWriteDrawing); Assert.True(plan.MayWriteProjectData); } + + [Fact] + public void Status_probe_preflight_is_deterministic_and_never_write_capable() + { + var request = new UpdateRequest(true, false, false, new HashSet(), UpdateExecutionMode.DryRun, false); + var first = builder.Build(request); + var second = builder.Build(request); + Assert.False(first.Accepted); + Assert.Equal(first.Diagnostics, second.Diagnostics); + Assert.False(first.MayWriteDrawing); + Assert.False(first.MayWriteProjectData); + Assert.Contains("PLANT_PROJECT_REQUIRED", first.Diagnostics); + } }