diff --git a/Launcher.Next.App/PilotCli.cs b/Launcher.Next.App/PilotCli.cs index f30f3ce..93fd223 100644 --- a/Launcher.Next.App/PilotCli.cs +++ b/Launcher.Next.App/PilotCli.cs @@ -77,10 +77,10 @@ internal static class PilotCli private static readonly IExecutionPermitService PermitService = new ExecutionPermitService(); - private sealed record AutomationOptions(string PackageSource, string EntryAssembly, string PilotRoot, string FixtureMarker, string OrthoDrawing, string ModelDrawing, string EvidenceRoot, bool Diagnostic); + private sealed record AutomationOptions(string PackageSource, string EntryAssembly, string PilotRoot, string FixtureMarker, string OrthoDrawing, string ModelDrawing, string AnnotationTemplateDwg, string EvidenceRoot, bool Diagnostic); private sealed record PreviewEvidence(bool Completed, bool? CommandsCompleted, bool? InspectorsCompleted, bool? EvidenceFlushed, string? PlanIdRun1, string? PlanIdRun2, bool? PlanIdsEqual, int? EligibleCandidateCount, string? SelectedCandidateHash); private sealed record ApplyEvidence(bool Completed, bool? CommandsCompleted, bool? InspectorsCompleted, bool? EvidenceFlushed, bool? PermitAccepted, string? ApplyOutcomeRun1, string? ApplyOutcomeRun2, object? Baseline, object? AfterCommand1, object? AfterCommand2, object? AfterUndo); - private sealed record ApplyPermit(bool AllowApply, string FixtureMarkerPath, string TargetSnapshotId, string ExpectedPlanId, string VisibilityState); + private sealed record ApplyPermit(bool AllowApply, string FixtureMarkerPath, string TargetSnapshotId, string ExpectedPlanId, string VisibilityState, string AnnotationTemplateDwgPath); private sealed record AutomationSessionResult(bool Started, bool GracefulExitVerified, bool ForcedCleanupUsed, bool RollbackVerified, bool EvidenceCompleted); private static async Task AutomateFirstApplyAsync(string[] args) @@ -96,7 +96,7 @@ internal static class PilotCli var previewConfigPath = Path.Combine(options.EvidenceRoot, "automation-preview.config.json"); var previewEvidence = Path.Combine(options.EvidenceRoot, "preview-result.json"); var entryHash = manifest!.Files.Single(x => string.Equals(x.Path, options.EntryAssembly, StringComparison.OrdinalIgnoreCase)).Sha256; - await File.WriteAllTextAsync(previewConfigPath, JsonSerializer.Serialize(new { phase = "Preview", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, evidencePath = previewEvidence, fixtureMarkerPath = options.FixtureMarker, entryAssemblySha256 = entryHash, automationTranscriptPath = Path.Combine(options.EvidenceRoot, "preview-transcript.log"), diagnostic = options.Diagnostic })); + await File.WriteAllTextAsync(previewConfigPath, JsonSerializer.Serialize(new { phase = "Preview", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, annotationTemplateDwgPath = options.AnnotationTemplateDwg, evidencePath = previewEvidence, fixtureMarkerPath = options.FixtureMarker, entryAssemblySha256 = entryHash, automationTranscriptPath = Path.Combine(options.EvidenceRoot, "preview-transcript.log"), diagnostic = options.Diagnostic })); await File.WriteAllTextAsync(previewEvidence, "{\"completed\":false}"); var previewConfig = WithAutomationPaths(baseConfig, options, previewConfigPath, previewEvidence, null, "preview") with { PluginExecutionMode = PluginExecutionMode.DryRun, AllowApply = false, ApplyPermitPath = null, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = previewConfigPath, FixtureMarkerPath = options.FixtureMarker, LaunchMode = AcadLaunchMode.NextPilotPreview, AutomationDiagnostic = options.Diagnostic }; var previewSession = await RunAutomationSession(previewConfig, manifest!, manifestHash!, options, "preview", previewEvidence); @@ -108,10 +108,10 @@ internal static class PilotCli // It must never mint an Apply permit or start an Apply process. if (options.Diagnostic) return Success; var permitPath = Path.Combine(options.EvidenceRoot, "apply-permit.json"); - await File.WriteAllTextAsync(permitPath, JsonSerializer.Serialize(new ApplyPermit(true, options.FixtureMarker, preview.SelectedCandidateHash, preview.PlanIdRun1, "Visible"))); + await File.WriteAllTextAsync(permitPath, JsonSerializer.Serialize(new ApplyPermit(true, options.FixtureMarker, preview.SelectedCandidateHash, preview.PlanIdRun1, "Visible", options.AnnotationTemplateDwg))); var applyConfigPath = Path.Combine(options.EvidenceRoot, "automation-apply.config.json"); var applyEvidence = Path.Combine(options.EvidenceRoot, "apply-result.json"); - await File.WriteAllTextAsync(applyConfigPath, JsonSerializer.Serialize(new { phase = "Apply", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, evidencePath = applyEvidence, fixtureMarkerPath = options.FixtureMarker, entryAssemblySha256 = entryHash, automationTranscriptPath = Path.Combine(options.EvidenceRoot, "apply-transcript.log"), diagnostic = false })); + await File.WriteAllTextAsync(applyConfigPath, JsonSerializer.Serialize(new { phase = "Apply", orthoDrawingPath = options.OrthoDrawing, modelDrawingPath = options.ModelDrawing, annotationTemplateDwgPath = options.AnnotationTemplateDwg, evidencePath = applyEvidence, fixtureMarkerPath = options.FixtureMarker, entryAssemblySha256 = entryHash, automationTranscriptPath = Path.Combine(options.EvidenceRoot, "apply-transcript.log"), diagnostic = false })); await File.WriteAllTextAsync(applyEvidence, "{\"completed\":false}"); var applyConfig = WithAutomationPaths(baseConfig, options, applyConfigPath, applyEvidence, permitPath, "apply") with { PluginExecutionMode = PluginExecutionMode.Apply, AllowApply = true, ApplyPermitPath = permitPath, AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = applyConfigPath, FixtureMarkerPath = options.FixtureMarker, LaunchMode = AcadLaunchMode.NextPilotApply, AutomationDiagnostic = false }; var applySession = await RunAutomationSession(applyConfig, manifest!, manifestHash!, options, "apply", applyEvidence); @@ -178,9 +178,10 @@ internal static class PilotCli var diagnostic = false; for (var i = 0; i < args.Length; i++) { if (args[i] == "--diagnostic") { if (diagnostic) { error = "AUTOMATION_ARGUMENT_INVALID"; return false; } diagnostic = true; continue; } if (i + 1 >= args.Length || !args[i].StartsWith("--") || !d.TryAdd(args[i], args[++i])) { error = "AUTOMATION_ARGUMENT_INVALID"; return false; } } string? V(string x) => d.GetValueOrDefault(x); - if (d.Keys.Except(new[] { "--package-source", "--entry-assembly", "--pilot-root", "--fixture-marker", "--ortho-dwg", "--model-dwg", "--evidence-root" }).Any() || new[] { V("--package-source"), V("--entry-assembly"), V("--pilot-root"), V("--fixture-marker"), V("--ortho-dwg"), V("--model-dwg"), V("--evidence-root") }.Any(string.IsNullOrWhiteSpace)) { error = "AUTOMATION_ARGUMENT_INVALID"; return false; } - var paths = new[] { V("--package-source")!, V("--pilot-root")!, V("--fixture-marker")!, V("--ortho-dwg")!, V("--model-dwg")!, V("--evidence-root")! }; if (paths.Any(x => !Path.IsPathFullyQualified(x) || x.StartsWith("\\\\"))) { error = "AUTOMATION_PATH_INVALID"; return false; } - options = new(V("--package-source")!, V("--entry-assembly")!, V("--pilot-root")!, V("--fixture-marker")!, V("--ortho-dwg")!, V("--model-dwg")!, V("--evidence-root")!, diagnostic); return true; + if (d.Keys.Except(new[] { "--package-source", "--entry-assembly", "--pilot-root", "--fixture-marker", "--ortho-dwg", "--model-dwg", "--annotation-template-dwg", "--evidence-root" }).Any() || new[] { V("--package-source"), V("--entry-assembly"), V("--pilot-root"), V("--fixture-marker"), V("--ortho-dwg"), V("--model-dwg"), V("--annotation-template-dwg"), V("--evidence-root") }.Any(string.IsNullOrWhiteSpace)) { error = "AUTOMATION_ARGUMENT_INVALID"; return false; } + var paths = new[] { V("--package-source")!, V("--pilot-root")!, V("--fixture-marker")!, V("--ortho-dwg")!, V("--model-dwg")!, V("--annotation-template-dwg")!, V("--evidence-root")! }; if (paths.Any(x => !Path.IsPathFullyQualified(x) || x.StartsWith("\\\\"))) { error = "AUTOMATION_PATH_INVALID"; return false; } + if (!File.Exists(V("--annotation-template-dwg"))) { error = "ANNOTATION_TEMPLATE_ASSET_MISSING"; return false; } + options = new(V("--package-source")!, V("--entry-assembly")!, V("--pilot-root")!, V("--fixture-marker")!, V("--ortho-dwg")!, V("--model-dwg")!, V("--annotation-template-dwg")!, V("--evidence-root")!, diagnostic); return true; } private static Dictionary ContentHashes(string marker) { var root = Path.GetDirectoryName(marker)!; return Directory.GetFiles(root, "*", SearchOption.AllDirectories).Where(x => x.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".dcf", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(x).Equals("Project.xml", StringComparison.OrdinalIgnoreCase)).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToDictionary(x => Path.GetRelativePath(root,x), x => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(x))), StringComparer.OrdinalIgnoreCase); } private static bool HashesEqual(IReadOnlyDictionary x, IReadOnlyDictionary y) => x.Count == y.Count && x.All(p => y.TryGetValue(p.Key, out var value) && value == p.Value);