namespace WxAgent.Core; public sealed class UiTraversalGuard(int maxNodes = 10_000, int maxDepth = 40) { private readonly HashSet _visited = new(StringComparer.Ordinal); public int NodeCount { get; private set; } public bool Truncated { get; private set; } public bool CycleDetected { get; private set; } public bool TryVisit(string runtimeId, int depth) { if (NodeCount >= maxNodes || depth > maxDepth) { Truncated = true; return false; } if (runtimeId.Length > 0 && !_visited.Add(runtimeId)) { CycleDetected = true; return false; } NodeCount++; return true; } } public sealed record UiSnapshotComparison(IReadOnlyList AddedAutomationIds, IReadOnlyList RemovedAutomationIds, IReadOnlyList MissingRequiredControls) { public static UiSnapshotComparison Compare(UiNodeSnapshot baseline, UiNodeSnapshot current) { static IEnumerable ReadIds(UiNodeSnapshot root) => new[] { root.AutomationId }.Concat(root.Children.SelectMany(ReadIds)); static HashSet Ids(UiNodeSnapshot root) => ReadIds(root) .Where(id => id.Length > 0 && !id.StartsWith("[id:", StringComparison.Ordinal)) .ToHashSet(StringComparer.Ordinal); var before = Ids(baseline); var after = Ids(current); return new UiSnapshotComparison( after.Except(before).Order(StringComparer.Ordinal).ToArray(), before.Except(after).Order(StringComparer.Ordinal).ToArray(), WechatLocators.RequiredAutomationIds.Where(id => !after.Contains(id)).ToArray()); } }