fix: finalize script-driven limited Apply evidence

This commit is contained in:
codex
2026-08-04 14:49:54 +03:00
parent 07ab3c436f
commit aefd3aa487
3 changed files with 31 additions and 9 deletions
@@ -60,7 +60,7 @@ public sealed class EntryPoint : IExtensionApplication
public void AutomationAfterPxp2Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("AfterPxp2"); public void AutomationAfterPxp2Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("AfterPxp2");
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)] [CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizePreview(); public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizeAutomation();
private static void RunDiagnostic(string commandName) private static void RunDiagnostic(string commandName)
{ {
@@ -201,6 +201,7 @@ public sealed class EntryPoint : IExtensionApplication
RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document); RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document);
RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document); RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document);
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState); var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState);
ScriptDrivenFirstApplyAutomation.RecordApplyOutcome(outcome);
FirstLimitedApplyAutomationCoordinator.RecordApplyOutcome(outcome); FirstLimitedApplyAutomationCoordinator.RecordApplyOutcome(outcome);
details["ApplyOutcome"] = outcome.ToString(); details["ApplyOutcome"] = outcome.ToString();
details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0; details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0;
@@ -15,7 +15,7 @@ namespace PRecAnnot.Plant3D2026;
internal static class ScriptDrivenFirstApplyAutomation internal static class ScriptDrivenFirstApplyAutomation
{ {
private sealed record Config(string Phase, string OrthoDrawingPath, string EvidencePath, string FixtureMarkerPath, string ExpectedEntryAssemblySha256, bool Diagnostic); private sealed record Config(string Phase, string OrthoDrawingPath, string EvidencePath, string FixtureMarkerPath, string ExpectedEntryAssemblySha256, bool Diagnostic);
private sealed record Run(int Sequence, string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, int? DbmodBefore, int? DbmodAfter, AutomationEntityCounts? Before, AutomationEntityCounts? After, bool WillStart, bool Ended, bool Failed, bool Cancelled, int? SourceRows, int? MembershipResolved); private sealed record Run(int Sequence, string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted, string? ApplyOutcome, int? DbmodBefore, int? DbmodAfter, AutomationEntityCounts? Before, AutomationEntityCounts? After, bool WillStart, bool Ended, bool Failed, bool Cancelled, int? SourceRows, int? MembershipResolved);
private static Config? config; private static Config? config;
private static Document? fixture; private static Document? fixture;
private static Database? database; private static Database? database;
@@ -90,7 +90,14 @@ internal static class ScriptDrivenFirstApplyAutomation
if (!prepared || terminal || runs.Count == 0) return; if (!prepared || terminal || runs.Count == 0) return;
var last = runs[^1]; var last = runs[^1];
if (!last.WillStart || last.PlanId is not null) return; if (!last.WillStart || last.PlanId is not null) return;
runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, SourceRows = sourceRows, MembershipResolved = membershipResolved }; runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, PermitAccepted = permitAccepted, SourceRows = sourceRows, MembershipResolved = membershipResolved };
}
internal static void RecordApplyOutcome(PipeAnnotationWriteOutcome outcome)
{
if (!prepared || terminal || runs.Count == 0) return;
var last = runs[^1];
runs[^1] = last with { ApplyOutcome = outcome.ToString() };
} }
internal static bool TryGetPreparedContext(Document? commandDocument, out PlantRuntimeContext context) internal static bool TryGetPreparedContext(Document? commandDocument, out PlantRuntimeContext context)
@@ -105,19 +112,29 @@ internal static class ScriptDrivenFirstApplyAutomation
internal static string? SelectedTargetSnapshotId => prepared && !terminal ? selectedTargetSnapshotId : null; internal static string? SelectedTargetSnapshotId => prepared && !terminal ? selectedTargetSnapshotId : null;
internal static void FinalizePreview() internal static void FinalizeAutomation()
{ {
try try
{ {
if (!prepared) throw new InvalidOperationException("AUTOMATION_NOT_PREPARED"); if (!prepared) throw new InvalidOperationException("AUTOMATION_NOT_PREPARED");
if (runs.Count != 2) throw new InvalidOperationException("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA"); if (runs.Count != 2) throw new InvalidOperationException("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA");
if (runs.Any(x => !x.WillStart || !x.Ended || x.Failed || x.Cancelled)) throw new InvalidOperationException("AUTOMATION_PXP_OA_NOT_ENDED"); if (runs.Any(x => !x.WillStart || !x.Ended || x.Failed || x.Cancelled)) throw new InvalidOperationException("AUTOMATION_PXP_OA_NOT_ENDED");
if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId)) || !string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER"); if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId))) throw new InvalidOperationException("PXP_OA_PLAN_RESULT_NOT_CAPTURED");
if (!string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER");
if (runs.Any(x => x.Create != 1)) throw new InvalidOperationException("AUTOMATION_PREVIEW_CREATE_NOT_ONE"); if (runs.Any(x => x.Create != 1)) throw new InvalidOperationException("AUTOMATION_PREVIEW_CREATE_NOT_ONE");
if (runs[0].Candidates.Count == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE"); if (runs[0].Candidates.Count == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE");
if (Dbmod() != dbmodBaseline || !Equals(Counts(database!), baseline)) throw new InvalidOperationException("AUTOMATION_INSPECTOR_INVARIANCE_FAILED"); var apply = string.Equals(config!.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
if (apply)
{
if (runs[0].PermitAccepted != true) throw new InvalidOperationException("AUTOMATION_APPLY_PERMIT_NOT_ACCEPTED");
if (!string.Equals(runs[0].ApplyOutcome, PipeAnnotationWriteOutcome.Created.ToString(), StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_APPLY_CREATE_NOT_CONFIRMED");
if (!string.Equals(runs[1].ApplyOutcome, PipeAnnotationWriteOutcome.AlreadyExists.ToString(), StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_APPLY_DUPLICATE_NOT_CONFIRMED");
var undoCounts = Counts(database!);
if (!Equals(undoCounts, baseline)) throw new InvalidOperationException("AUTOMATION_UNDO_DID_NOT_RESTORE_BASELINE");
}
else if (Dbmod() != dbmodBaseline || !Equals(Counts(database!), baseline)) throw new InvalidOperationException("AUTOMATION_INSPECTOR_INVARIANCE_FAILED");
terminal = true; terminal = true;
transitions.Add("PreviewCompleted"); transitions.Add(apply ? "ApplyCompleted" : "PreviewCompleted");
Write(true, null); Write(true, null);
Unsubscribe(); Unsubscribe();
} }
@@ -132,7 +149,7 @@ internal static class ScriptDrivenFirstApplyAutomation
var correlation = Correlate("WillStart", sender, args.GlobalCommandName, runs.Count < 2); var correlation = Correlate("WillStart", sender, args.GlobalCommandName, runs.Count < 2);
correlations.Add(correlation); correlations.Add(correlation);
if (correlation.RejectionReason is not null) return; if (correlation.RejectionReason is not null) return;
runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null)); runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, null, null, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null));
transitions.Add("PxpOaWillStart:" + runs.Count); transitions.Add("PxpOaWillStart:" + runs.Count);
}); });
} }
@@ -203,7 +220,9 @@ internal static class ScriptDrivenFirstApplyAutomation
{ {
if (config is null) return; if (config is null) return;
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1); var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").ToArray(), 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 = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null, EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents, CommandCorrelations = correlations }; var apply = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
var afterUndo = apply && terminal ? Counts(database!) : null;
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, DbmodAfterUndo = apply && terminal ? Dbmod() : null, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, AfterUndo = afterUndo, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").Concat(apply && commandEvents.Any(x => string.Equals(x, "Ended:UNDO", StringComparison.OrdinalIgnoreCase)) ? ["UNDO"] : []).ToArray(), 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 = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, ApplyOutcomeRun1 = first?.ApplyOutcome, ApplyOutcomeRun2 = second?.ApplyOutcome, PermitAccepted = first?.PermitAccepted, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended) && (!apply || commandEvents.Any(x => string.Equals(x, "Ended:UNDO", StringComparison.OrdinalIgnoreCase))), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null && (!apply || afterUndo is not null), EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents, CommandCorrelations = correlations };
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!); Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true); var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true);
} }
@@ -36,6 +36,8 @@ public sealed class PxpOaRegistrationTests
[Fact] public void ScriptDrivenPxpOa_BindsPlantContextToCommandDocument() => Assert.Contains("ReadCurrentContext(document)", Entry); [Fact] public void ScriptDrivenPxpOa_BindsPlantContextToCommandDocument() => Assert.Contains("ReadCurrentContext(document)", Entry);
[Fact] public void ScriptDrivenPxpOa_UsesPreparedFixtureContextBeforeImplicitLookup() => Assert.Contains("TryGetPreparedContext(document, out var scriptContext)", Entry); [Fact] public void ScriptDrivenPxpOa_UsesPreparedFixtureContextBeforeImplicitLookup() => Assert.Contains("TryGetPreparedContext(document, out var scriptContext)", Entry);
[Fact] public void ScriptDrivenPrepare_SelectsCanonicalTargetAndRetainsReadOnlyContext() { Assert.Contains("preparedContext = context", Observer); Assert.Contains("selectedTargetSnapshotId = candidates[0]", Observer); } [Fact] public void ScriptDrivenPrepare_SelectsCanonicalTargetAndRetainsReadOnlyContext() { Assert.Contains("preparedContext = context", Observer); Assert.Contains("selectedTargetSnapshotId = candidates[0]", Observer); }
[Fact] public void ScriptDrivenApply_RecordsOutcomeAgainstTheAcceptedRun() { Assert.Contains("internal static void RecordApplyOutcome", Observer); Assert.Contains("ApplyOutcome = outcome.ToString()", Observer); }
[Fact] public void ScriptDrivenApply_RequiresActualUndoBaselineRestoration() { Assert.Contains("AUTOMATION_UNDO_DID_NOT_RESTORE_BASELINE", Observer); Assert.Contains("Ended:UNDO", Observer); }
[Fact] public void PlacementReadFailure_IsDiagnosedInsteadOfBeingSilentlyDiscarded() { var source = File.ReadAllText(Path.Combine(ProjectRoot, "PlantReadPath.cs")); Assert.Contains("OrthoPipePlacementReadFailed", source); Assert.Contains("ORTHO_PIPE_PLACEMENT_UNAVAILABLE", source); } [Fact] public void PlacementReadFailure_IsDiagnosedInsteadOfBeingSilentlyDiscarded() { var source = File.ReadAllText(Path.Combine(ProjectRoot, "PlantReadPath.cs")); Assert.Contains("OrthoPipePlacementReadFailed", source); Assert.Contains("ORTHO_PIPE_PLACEMENT_UNAVAILABLE", source); }
[Fact] public void PlacementReader_OpensSourceDatabaseWhenItIsNotActive() { var source = File.ReadAllText(Path.Combine(ProjectRoot, "PlantReadPath.cs")); Assert.Contains("sourceId.GetDatabase(true, ref openedByQuery)", source); } [Fact] public void PlacementReader_OpensSourceDatabaseWhenItIsNotActive() { var source = File.ReadAllText(Path.Combine(ProjectRoot, "PlantReadPath.cs")); Assert.Contains("sourceId.GetDatabase(true, ref openedByQuery)", source); }
} }