diff --git a/QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs b/QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs index 7cdd6064d..110ae1bad 100644 --- a/QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs +++ b/QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs @@ -167,11 +167,18 @@ public void FormDeactivated_NoWebView2Focus_DoesNotPark() /// /// Every item's breadcrumb selector is cancelled, so no open ToolStripDropDown — and /// therefore no WinForms modal-menu-mode message filter — can outlive deactivation. + /// + /// Issue #796 (AC2) made that cancel conditional: it now happens on a GENUINE deactivation + /// and not on one this form's own breadcrumb popup caused. The condition is stated + /// explicitly in the Arrange block below rather than left to the mock default, so the #677 + /// contract this test pins remains visible as a contract about the genuine case. + /// /// [TestMethod] public void FormDeactivated_CancelsSelectorOnEveryItemController() { // Arrange + _mockFormViewer.SetupGet(x => x.IsDeactivationSelfInflictedByOwnPopup).Returns(false); var first = new Mock(); var second = new Mock(); QfcFormController controller = CreateController(); @@ -244,5 +251,55 @@ public void FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues() act.Should().NotThrow(); second.Verify(x => x.CancelBreadcrumbSelector(), Times.Once()); } + + /// + /// Issue #796 (AC6). Scenario: the pure deactivation formatter is called with a fixed + /// argument tuple. Expected outcome: the returned line carries all three discriminating + /// field labels and the supplied group count, so the Phase 2 transcript can identify a + /// self-inflicted deactivation without inference. + /// + [TestMethod] + public void FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField() + { + // Arrange + const int GroupCount = 3; + + // Act + string line = QfcFormController.FormatDeactivationDiagnostics( + webView2Focused: true, + activeFormIsNull: true, + groupCount: GroupCount + ); + + // Assert + line.Should().Contain("WebView2Focused="); + line.Should().Contain("ActiveFormNull="); + line.Should().Contain("Groups="); + line.Should().Contain(GroupCount.ToString()); + } + + /// + /// Issue #796 (AC2). Scenario: the form loses activation because a breadcrumb popup this + /// form owns took it. Expected outcome: no item controller's selector is cancelled, so the + /// popup the gesture just opened survives its own opening. + /// + [TestMethod] + public void FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector() + { + // Arrange + _mockFormViewer.SetupGet(x => x.IsDeactivationSelfInflictedByOwnPopup).Returns(true); + var first = new Mock(); + var second = new Mock(); + QfcFormController controller = CreateController(); + InjectGroups(controller, first, second); + controller.RegisterFormEventHandlers(); + + // Act + _mockFormViewer.Raise(x => x.FormDeactivated += null, EventArgs.Empty); + + // Assert + first.Verify(x => x.CancelBreadcrumbSelector(), Times.Never()); + second.Verify(x => x.CancelBreadcrumbSelector(), Times.Never()); + } } } diff --git a/QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs b/QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs index f1491929b..778d5a02e 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs @@ -69,6 +69,12 @@ public void TextBoxSearchKeyDown_EscapeWhileDropDownClosed_RoutesNoIntentAndLeav /// /// The search textbox losing focus while the drop-down is open routes exactly one close /// intent — the dismissal WinForms menu mode used to provide for a capturing popup. + /// + /// Issue #796 (AC4) narrowed the condition rather than removing it: the leave dismisses only + /// a drop-down this search box itself opened. The Arrange therefore establishes that + /// search-driven ownership before the leave is raised. A mouse-driven open carries no + /// ownership and is deliberately no longer dismissed by this path. + /// /// [TestMethod] public void TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent() @@ -76,6 +82,7 @@ public void TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent() // Arrange Mock viewer = BuildViewer(isOpen: true); HarnessController controller = BuildController(viewer); + QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true); // Act controller.TextBoxSearch_Leave(null, EventArgs.Empty); diff --git a/QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs b/QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs new file mode 100644 index 000000000..1440a1700 --- /dev/null +++ b/QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using UtilitiesCS; + +namespace QuickFiler.Controllers.Tests +{ + /// + /// Issue #796 (AC4): the SearchOwnedDismissalLatch. TextBoxSearch_Leave previously took + /// dismissal ownership of the folder drop-down regardless of which gesture opened it, so a + /// mouse-driven open was dismissed by a leave the mouse gesture itself provoked. The latch + /// records that the open drop-down is one this search box opened, and the leave handler + /// dismisses only then. + /// + /// Class name follows the convention of the sibling file + /// QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs, which declares + /// QfcItemController_EventHandlersTests. No window, no external process, no temporary file. + /// + /// + [TestClass] + public class QfcItemController_SearchLeaveLatchTests + { + /// + /// Builds a folder-search handler whose FindFolder returns a fixed result, so the + /// search-driven open path can be driven without a live Outlook or COM host. + /// + private static Mock BuildFolderHandler(string[] matched) + { + Mock folderHandler = new Mock(); + folderHandler + .Setup(f => + f.FindFolder( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny< + IEnumerable<(string root, string excludedFolder, bool excludeChildren)> + >() + ) + ) + .Returns(matched); + return folderHandler; + } + + /// + /// Issue #796 (AC4). Scenario: the drop-down is open because a mouse gesture opened it, and + /// the folder search box then loses focus. Expected outcome: the leave handler does not + /// dismiss the drop-down, because the search box never took dismissal ownership of it. + /// + [TestMethod] + public void SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown() + { + // Arrange — the drop-down is open, but no search-driven open path ever ran, which is + // exactly the state a mouse gesture on the collapsed breadcrumb produces. + Mock viewer = new Mock(); + viewer.SetupGet(v => v.IsFolderDropDownOpen).Returns(true); + HarnessController controller = new HarnessController(); + QfcItemControllerTestSupport.SetField(controller, "_itemViewer", viewer.Object); + + // Act + controller.TextBoxSearch_Leave(null, EventArgs.Empty); + + // Assert + viewer.Verify(v => v.SetFolderDroppedDown(false), Times.Never()); + } + + /// + /// Issue #796 (AC4), paired positive. Scenario: the drop-down is open because typing in the + /// folder search box opened it, and the search box then loses focus. Expected outcome: the + /// leave handler still dismisses the drop-down exactly once, so the issue #680 dismissal + /// responsibility for a non-capturing search-driven popup is preserved. + /// + [TestMethod] + public void SearchLeaveAfterSearchDrivenOpen_ClosesDropDown() + { + // Arrange + string[] matched = { @"\\A\one", @"\\A\two" }; + Mock viewer = new Mock(); + viewer.SetupGet(v => v.SearchText).Returns("query"); + viewer.SetupGet(v => v.IsFolderDropDownOpen).Returns(true); + HarnessController controller = new HarnessController(); + QfcItemControllerTestSupport.SetField(controller, "_itemViewer", viewer.Object); + QfcItemControllerTestSupport.SetField( + controller, + "_folderHandler", + BuildFolderHandler(matched).Object + ); + + // Act — the search-driven open path, then the leave it eventually provokes. + controller.TextBoxSearch_TextChanged(null, EventArgs.Empty); + controller.TextBoxSearch_Leave(null, EventArgs.Empty); + + // Assert + viewer.Verify(v => v.PresentFolderSearchResults(matched), Times.Once()); + viewer.Verify(v => v.SetFolderDroppedDown(false), Times.Once()); + } + } +} diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index f0d479502..fc019ed21 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -80,6 +80,7 @@ + @@ -155,6 +156,7 @@ + diff --git a/QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs b/QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs new file mode 100644 index 000000000..26c277b16 --- /dev/null +++ b/QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs @@ -0,0 +1,290 @@ +using System; +using System.Drawing; +using System.Reflection; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Web.WebView2.Core; +using Moq; +using QuickFiler.Viewers; + +namespace QuickFiler.Test.Viewers +{ + /// + /// Issue #796 (AC6): the host-side close-ordering diagnostic must carry every field that + /// discriminates between the candidate close paths, so the Phase 2 transcript can be read + /// without inference. + /// + /// Issue #796 (AC3): a native-reason close arriving while a selection commit is in flight must + /// not cancel that selection, while a native-reason close with no commit in flight still must. + /// + /// + [TestClass] + public sealed class BreadcrumbDropDownCloseOrderingTests + { + /// + /// Scenario: the pure formatter is called with a fixed argument tuple. Expected outcome: + /// the returned line carries all six discriminating field labels and the supplied close + /// reason. Asserting on the formatter rather than on source text makes the AC6 evidence a + /// deterministic managed-seam assertion; no popup, window, or WebView2 surface is created. + /// + [TestMethod] + public void FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField() + { + // Arrange + const ToolStripDropDownCloseReason CloseReason = + ToolStripDropDownCloseReason.AppFocusChange; + + // Act + string line = BreadcrumbDropDownHost.FormatDropDownClosedDiagnostics( + CloseReason, + programmaticClose: false, + openState: true, + autoClose: true, + disposed: false, + pendingClose: true + ); + + // Assert + line.Should().Contain("CloseReason="); + line.Should().Contain("ProgrammaticClose="); + line.Should().Contain("OpenState="); + line.Should().Contain("AutoClose="); + line.Should().Contain("Disposed="); + line.Should().Contain("PendingClose="); + line.Should().Contain(CloseReason.ToString()); + } + + /// + /// Issue #796 (AC3). Scenario: the popup is open, a selection commit has been requested for + /// that popup lifetime, and the framework then reports a native close. Expected outcome: the + /// cancel delegate is not invoked, so the commit is not undone by the close that accompanies + /// it. + /// + /// + /// The framework cannot be made to choose a close reason in a headless test — no window is + /// shown, so ToolStripDropDown never raises Closed of its own accord. The test + /// therefore hands the handler a constructed reason and proves the branch, not the + /// framework's choice of reason. That limit is recorded rather than papered over. + /// + [TestMethod] + public void NativeCloseWhileCommitPending_DoesNotCancelSelection() + { + // Arrange + using (var harness = new CloseOrderingHostHarness()) + { + harness.OpenAndSettle(); + harness.Host.IsCommitPending = true; + + // Act + harness.RaiseNativeClose(ToolStripDropDownCloseReason.AppFocusChange); + + // Assert + harness + .CancelCount.Should() + .Be(0, "a close racing an in-flight commit must not cancel the selection"); + } + } + + /// + /// Issue #796 (AC3). Scenario: the popup is open, no selection commit has been requested, + /// and the framework then reports a native close. Expected outcome: the cancel delegate is + /// invoked exactly once. This is the scoping half of the pair — it is what proves the + /// suppression above is conditional on a pending commit rather than global. + /// + [TestMethod] + public void NativeCloseWithNoCommitPending_StillCancelsSelection() + { + // Arrange + using (var harness = new CloseOrderingHostHarness()) + { + harness.OpenAndSettle(); + harness.Host.IsCommitPending.Should().BeFalse("a fresh show clears the latch"); + + // Act + harness.RaiseNativeClose(ToolStripDropDownCloseReason.AppFocusChange); + + // Assert + harness + .CancelCount.Should() + .Be(1, "a close with no commit in flight still cancels the selection"); + } + } + + /// + /// Issue #796 (AC1). Scenario: the drop-down is opened by the gesture open path. Expected + /// outcome: the open task resolves true, the host reports open, the selection session + /// reports the selector open, and no Close reaches the host across the gesture. + /// + /// + /// What is asserted here is the managed seam. The part of AC1 that is NOT automatable is + /// that no FRAMEWORK close occurs: no framework drop-down is shown in a headless test, so + /// there is no ToolStripDropDown to raise Closed and no framework decision to + /// observe. That half of the criterion is covered by the Phase 2 manual observation and is + /// stated here rather than asserted, so a later reader does not read this test as proving + /// more than it does. + /// + [TestMethod] + public async Task GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose() + { + // Arrange + using (var harness = new ItemViewerDropDownHarness()) + { + // Act + harness.Viewer.SetFolderDroppedDown(true); + bool opened = await harness.Viewer.BreadcrumbOpenTask.ConfigureAwait(false); + + // Assert + opened.Should().BeTrue("the gesture open path must resolve to an opened popup"); + harness.Host.Object.IsOpen.Should().BeTrue("the host must still report open"); + harness + .Viewer.BreadcrumbCoordinator.IsSelectorOpen.Should() + .BeTrue("the selection session must still report the selector open"); + harness.Host.Verify( + host => host.Close(It.IsAny()), + Times.Never() + ); + } + } + + /// + /// Drives a real headlessly under an inline + /// synchronization context, in the style of the PendingHostHarness already used by + /// BreadcrumbPendingOpenCloseTests. No window is shown and no WebView2 is + /// initialised: the surface factory seam returns a plain panel and a stub messenger. + /// + private sealed class CloseOrderingHostHarness : IDisposable + { + private readonly SynchronizationContext _previousContext; + private readonly Panel _anchor; + private readonly Panel _surface = new Panel(); + private readonly StubMessenger _messenger = new StubMessenger(); + + internal CloseOrderingHostHarness() + { + _previousContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext( + new InlineSynchronizationContext() + ); + try + { + _anchor = new Panel(); + var environment = (CoreWebView2Environment) + FormatterServices.GetUninitializedObject(typeof(CoreWebView2Environment)); + Host = new BreadcrumbDropDownHost( + _anchor, + environment, + CreateSurfaceAsync, + () => FocusPendingCount++, + () => FocusAnchorCount++, + () => CancelCount++, + (dropDown, owner, point) => ShowCount++ + ); + } + catch + { + SynchronizationContext.SetSynchronizationContext(_previousContext); + throw; + } + } + + internal BreadcrumbDropDownHost Host { get; } + internal int ShowCount { get; private set; } + internal int FocusPendingCount { get; private set; } + internal int FocusAnchorCount { get; private set; } + internal int CancelCount { get; private set; } + + /// Opens the popup and asserts it reached the open state before the act step. + internal void OpenAndSettle() + { + Task opening = Host.OpenAsync( + new Rectangle(120, 240, 390, 25), + new Rectangle(0, 0, 1920, 1040), + new Size(390, 180) + ); + opening + .IsCompleted.Should() + .BeTrue("the inline context completes the open synchronously"); + opening.Result.Should().BeTrue(); + Host.IsOpen.Should().BeTrue(); + ShowCount.Should().Be(1); + CancelCount.Should().Be(0, "opening must not cancel anything"); + } + + /// + /// Hands the host's own native-close handler a constructed close reason. The handler is + /// private and no window exists to raise ToolStripDropDown.Closed, so reflection + /// is the only way to exercise the branch this pair is about. + /// + internal void RaiseNativeClose(ToolStripDropDownCloseReason reason) + { + MethodInfo handler = typeof(BreadcrumbDropDownHost).GetMethod( + "OnDropDownClosed", + BindingFlags.NonPublic | BindingFlags.Instance + ); + handler + .Should() + .NotBeNull( + because: "OnDropDownClosed must exist on BreadcrumbDropDownHost.Diagnostics.cs" + ); + handler.Invoke( + Host, + new object[] { Host.DropDown, new ToolStripDropDownClosedEventArgs(reason) } + ); + } + + public void Dispose() + { + try + { + Host.Dispose(); + if (!_surface.IsDisposed) + { + _surface.Dispose(); + } + _anchor.Dispose(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(_previousContext); + } + } + + private Task> CreateSurfaceAsync( + CoreWebView2Environment environment + ) => + Task.FromResult( + Tuple.Create( + _surface, + _messenger, + Task.CompletedTask + ) + ); + } + + /// A messenger that records nothing; the close-ordering branch posts no JSON. + private sealed class StubMessenger : IWebViewMessenger, IDisposable + { + private EventHandler _messageReceived; + + public event EventHandler MessageReceived + { + add => _messageReceived += value; + remove => _messageReceived -= value; + } + + public void PostJson(string json) { } + + public void Dispose() { } + } + + /// Runs posted callbacks inline so the popup lifecycle settles deterministically. + private sealed class InlineSynchronizationContext : SynchronizationContext + { + public override void Post(SendOrPostCallback callback, object state) => callback(state); + } + } +} diff --git a/QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs b/QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs index 79e953a2c..b795c645f 100644 --- a/QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs +++ b/QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs @@ -120,6 +120,39 @@ public async Task CloseCanceledFactory_AllowsOneFreshReopenWithoutLateMutation() } } + /// + /// Issue #796 (AC3). Scenario: a close is requested against a pending open while a + /// selection commit is already in flight for that popup lifetime. Expected outcome: the + /// selection is not cancelled, while the anchor focus step still runs. + /// + /// This is the pending-commit-versus-native-close guard. It is the complement of the two + /// retained `CancelCount.Should().Be(1)` assertions above: those two run with no commit in + /// flight and still cancel, so together the three show the suppression is conditional on a + /// pending commit rather than global. + /// + /// + [TestMethod] + public async Task CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection() + { + // Arrange + using (var harness = new PendingHostHarness()) + { + Task opening = harness.OpenAsync(); + harness.Host.IsCommitPending = true; + + // Act + bool closed = harness.Host.Close(BreadcrumbDropDownCloseReason.Uncommitted); + (await opening.ConfigureAwait(false)).Should().BeFalse(); + + // Assert + closed.Should().BeTrue("pending open work is a closeable selector state"); + harness + .CancelCount.Should() + .Be(0, "a close racing an in-flight commit must not cancel the selection"); + harness.FocusAnchorCount.Should().Be(1, "only the cancel step is suppressed"); + } + } + [TestMethod] public void ToggleAndEscapeWhileOpenIsPending_EachClosesHostExactlyOnce() { @@ -152,6 +185,51 @@ public void AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce() closeCount.Should().Be(1); } + /// + /// Issue #796 (AC5), the issue #438 AC-3 regression guard. Scenario: the selector is open + /// and the row set is replaced twice, as a search refresh or a late decoration does. + /// Expected outcome: no Close reaches the host and the session still reports the + /// selector open. The guard is meaningful because the session-preserving replacement path + /// in UtilitiesCS is deliberately left out of this item's diff, so what is observed here is + /// that untouched path. It reuses the headless-viewer plus mocked-host pattern the two + /// tests below already use rather than adding a third harness to this file. + /// + [TestMethod] + public void RowSetRefreshWhileOpen_NeverClosesHost() + { + // Arrange + using (var scope = new ViewerScope()) + { + Rectangle anchor = new Rectangle(0, 0, 300, 25); + Rectangle working = new Rectangle(0, 0, 1920, 1040); + var provider = new Mock(MockBehavior.Strict); + scope.Viewer.InitializeBreadcrumbPipeline(provider.Object); + scope.Viewer.BreadcrumbCoordinator.AddItems(new[] { "A", "B" }); + bool hostOpen = false; + var host = new Mock(); + host.SetupGet(value => value.IsOpen).Returns(() => hostOpen); + host.Setup(value => value.OpenAsync(anchor, working, It.IsAny())) + .Callback(() => hostOpen = true) + .ReturnsAsync(true); + host.Setup(value => + value.OpenAsync(anchor, working, It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(true); + scope.Viewer.ConfigureBreadcrumbDropDown(host.Object, () => anchor, () => working); + scope.Viewer.SetBreadcrumbDropDownState(true); + + // Act — two row-set replacements while the selector is open. + scope.Viewer.PresentBreadcrumbSearchResults(new[] { @"\\A\one" }); + scope.Viewer.PresentBreadcrumbSearchResults(new[] { @"\\A\one", @"\\A\two" }); + + // Assert + host.Verify(v => v.Close(It.IsAny()), Times.Never()); + scope + .Viewer.BreadcrumbCoordinator.IsSelectorOpen.Should() + .BeTrue("a row-set refresh must not end the selector session"); + } + } + private static int ExercisePendingViewerClose( Action close, BreadcrumbDropDownCloseReason expectedReason diff --git a/QuickFiler/Controllers/QfcFormController.Deactivate.cs b/QuickFiler/Controllers/QfcFormController.Deactivate.cs index 3fe999ecb..c783cf2bb 100644 --- a/QuickFiler/Controllers/QfcFormController.Deactivate.cs +++ b/QuickFiler/Controllers/QfcFormController.Deactivate.cs @@ -26,6 +26,58 @@ internal partial class QfcFormController internal void FormViewer_Deactivated(object sender, EventArgs e) => ParkFocusAndCancelSelectors(); + /// + /// Issue #796 (AC6): renders the one-line entry diagnostic for + /// . + /// + /// Whether a WebView2 child window held focus at entry. + /// + /// Whether Form.ActiveForm was null at entry. It was proposed as a discriminator on + /// the reasoning that a ToolStripDropDown is not a Form, so a null active form + /// would evidence a self-inflicted deactivation. The manual observation recorded for this + /// item refutes that reasoning: all three self-inflicted popup gestures reported + /// ActiveFormNull=False and the one deactivation caused by focus leaving the form + /// reported ActiveFormNull=True, so the values run opposite to the predicted + /// direction on all four observations. The field is retained as observed diagnostic data + /// only and is not read as evidence in either direction. See the AC2 item-viewer wiring + /// line of evidence/other/close-ordering-decision.md. + /// + /// The number of item groups the cancel loop will visit. + /// A single line carrying a sentence prefix and three Key=Value pairs. + /// + /// Pure and static so the AC6 evidence rests on a deterministic managed-seam assertion + /// rather than a source-text scan. + /// + internal static string FormatDeactivationDiagnostics( + bool webView2Focused, + bool activeFormIsNull, + int groupCount + ) => + "Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. " + + $"WebView2Focused={webView2Focused} ActiveFormNull={activeFormIsNull} " + + $"Groups={groupCount}"; + + /// + /// Issue #796 (AC6): renders the one-line per-item diagnostic for the cancel loop in + /// . + /// + /// The item's own number. + /// + /// Whether that item's breadcrumb selector was open, or null when the value could not be + /// observed. The parameter is nullable so the unavailable case is produced here rather than + /// at the call site, which keeps the per-item log statement a single unconditional call. + /// + /// + /// A single line carrying a sentence prefix and two Key=Value pairs. An unobserved + /// selector state renders as SelectorWasOpen=unavailable, never as a fabricated + /// boolean. + /// + internal static string FormatItemCancelDiagnostics(int itemNumber, bool? selectorWasOpen) => + "Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. " + + $"ItemNumber={itemNumber} " + + "SelectorWasOpen=" + + (selectorWasOpen?.ToString() ?? "unavailable"); + /// /// Parks focus off any focused WebView2 and cancels every item's breadcrumb selector. /// @@ -38,6 +90,13 @@ internal void FormViewer_Deactivated(object sender, EventArgs e) => /// internal void ParkFocusAndCancelSelectors() { + logger.Debug( + FormatDeactivationDiagnostics( + _formViewer?.IsWebView2Focused == true, + System.Windows.Forms.Form.ActiveForm == null, + _groups?.ItemGroups?.Count ?? 0 + ) + ); if (_formViewer?.IsWebView2Focused == true) { _formViewer.ParkFocusOffWebView2(); @@ -49,8 +108,26 @@ internal void ParkFocusAndCancelSelectors() return; } + // Issue #796 (AC2): a deactivation this form's own breadcrumb popup caused must not + // cancel the selector the gesture just opened. The guard is scoped to the cancel loop + // and deliberately not to the focus-parking step above, because the observation + // recorded for this item shows parking did not run on two of the three defective + // gestures and so cannot be what produces the defect. A viewer that reports nothing + // reports false, which is the genuine case, so the issue #677 contract is unchanged for + // every deactivation that is not self-inflicted. + if (_formViewer?.IsDeactivationSelfInflictedByOwnPopup == true) + { + return; + } + foreach (QfcItemGroup group in groups) { + logger.Debug( + FormatItemCancelDiagnostics( + group.ItemController?.ItemNumber ?? 0, + (group.ItemController as QfcItemController)?.IsBreadcrumbSelectorOpen + ) + ); try { group.ItemController?.CancelBreadcrumbSelector(); diff --git a/QuickFiler/Controllers/QfcItemController.EventHandlers.cs b/QuickFiler/Controllers/QfcItemController.EventHandlers.cs index ea6a747aa..5b9a3cf2d 100644 --- a/QuickFiler/Controllers/QfcItemController.EventHandlers.cs +++ b/QuickFiler/Controllers/QfcItemController.EventHandlers.cs @@ -179,6 +179,8 @@ internal void TextBoxSearch_TextChanged(object sender, EventArgs e) objItem: Mail ); _itemViewer.PresentFolderSearchResults(folders); + // Issue #796 (AC4): the search box opened this popup, so it owns dismissing it. + _searchOwnedDismissal = true; } // Issue #680: one-shot suppression latch for the Down-arrow focus handoff. Single producer: @@ -187,12 +189,35 @@ internal void TextBoxSearch_TextChanged(object sender, EventArgs e) // textbox's Leave, and that one leave must not dismiss the popup the gesture just claimed. private bool _searchLeaveHandoffPending; + // Issue #796 (AC4): the SearchOwnedDismissalLatch. Records that the drop-down currently open + // is one this search box opened, so the leave handler below dismisses only a popup the + // search box owns and never one a mouse gesture opened. Producers are the two search-driven + // open sites in this file; the consumer is TextBoxSearch_Leave. A mouse-driven open never + // sets it, which is why the mouse path needs no edit anywhere. + // + // It cannot reuse _searchLeaveHandoffPending above. That latch is one-shot and consumed + // destructively on its first read by design, so it is false again immediately after the + // handoff it guards while the popup is still open. Provenance must persist for as long as + // the popup is open, which is a different lifetime, and overloading the one-shot field would + // break the issue #680 contract. + private bool _searchOwnedDismissal; + + /// + /// Issue #796 (AC4): whether the drop-down currently open was opened from this item's folder + /// search box rather than by a mouse gesture on the collapsed breadcrumb. + /// + internal bool SearchOwnsDropDownDismissal => _searchOwnedDismissal; + internal void TextBoxSearch_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Down) { _itemViewer.SetFolderDroppedDown(true); _searchLeaveHandoffPending = true; + // Issue #796 (AC4): the Down-arrow gesture is the second search-driven open site, + // so it takes dismissal ownership too. The one-shot handoff latch above keeps its + // separate meaning and its separate lifetime; this one persists while the popup is. + _searchOwnedDismissal = true; _itemViewer.FocusFolderDropDown(); e.SuppressKeyPress = true; e.Handled = true; @@ -204,6 +229,9 @@ internal void TextBoxSearch_KeyDown(object sender, KeyEventArgs e) // drop-down is not open the key falls through untouched, so Escape keeps whatever // meaning it has elsewhere in the form. _itemViewer.SetFolderDroppedDown(false); + // Issue #796 (AC4): this dismissal ends the popup the search box owned, so the + // provenance latch is released with it. + _searchOwnedDismissal = false; e.SuppressKeyPress = true; e.Handled = true; } @@ -223,10 +251,36 @@ internal void TextBoxSearch_Leave(object sender, EventArgs e) return; } if (!_itemViewer.IsFolderDropDownOpen) + { + // No popup is open, so nothing owns dismissal any more. Releasing here keeps a + // stale provenance from surviving a close this controller did not perform. + _searchOwnedDismissal = false; return; + } + // Issue #796 (AC4): dismiss only a popup this search box opened. A mouse gesture on the + // collapsed breadcrumb never sets the latch, so the leave it provokes no longer closes + // the popup that gesture just opened, which is the defect this criterion names. + if (!_searchOwnedDismissal) + return; + _searchOwnedDismissal = false; _itemViewer.SetFolderDroppedDown(false); } + /// + /// Issue #796 (AC6): whether this item's breadcrumb selector is currently open. + /// + /// + /// Observational only. Nothing but the form-deactivation diagnostic reads it, and it + /// forwards the same IsFolderDropDownOpen expression this file already evaluates in + /// and , so it + /// introduces no new dependency and no branching of its own. It is declared on the concrete + /// controller rather than on IQfcItemController because that interface has a + /// compiled hand-written implementor in the test assembly and the target framework offers + /// no default interface members, so adding a member there would break that implementor with + /// CS0535. + /// + internal bool IsBreadcrumbSelectorOpen => _itemViewer.IsFolderDropDownOpen; + private void TopicThread_ItemSelectionChanged( object sender, ListViewItemSelectionChangedEventArgs e diff --git a/QuickFiler/Interfaces/IQfcFormViewer.cs b/QuickFiler/Interfaces/IQfcFormViewer.cs index b2c1c9ff8..b1cbb397f 100644 --- a/QuickFiler/Interfaces/IQfcFormViewer.cs +++ b/QuickFiler/Interfaces/IQfcFormViewer.cs @@ -68,5 +68,21 @@ public interface IQfcFormViewer : IForm /// child window is left holding the Outlook UI thread's Win32 keyboard focus. /// void ParkFocusOffWebView2(); + + // Seam B — issue #796 (AC2) self-inflicted deactivation intent. + + /// + /// Whether the window that took activation from this form is a breadcrumb popup owned by + /// this form, making the deactivation self-inflicted rather than a departure to a foreign + /// window. + /// + /// + /// The polarity is load-bearing and must not be inverted: false means GENUINE, that + /// is, not self-inflicted. A viewer that reports nothing therefore keeps the issue #677 + /// contract exactly, and a mock's default bool return of false means the + /// existing deactivation suite continues to describe a genuine deactivation without a new + /// Arrange line. + /// + bool IsDeactivationSelfInflictedByOwnPopup { get; } } } diff --git a/QuickFiler/QuickFiler.csproj b/QuickFiler/QuickFiler.csproj index 35def2f76..3543cd76e 100644 --- a/QuickFiler/QuickFiler.csproj +++ b/QuickFiler/QuickFiler.csproj @@ -414,6 +414,7 @@ + diff --git a/QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs b/QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs new file mode 100644 index 000000000..73b4f7e98 --- /dev/null +++ b/QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs @@ -0,0 +1,79 @@ +#nullable enable +using System.Windows.Forms; + +namespace QuickFiler.Viewers +{ + /// + /// Issue #796 (AC6): close-ordering diagnostics for the breadcrumb popup host. + /// + /// Held on a third partial-class part because BreadcrumbDropDownHost.cs stands at the + /// repository's 500-line ceiling, leaving no room there for a logger declaration, a message + /// formatter, and the log statement itself. The native-close handler moves here with them: it is + /// the site the diagnostic instruments, and relocating it is what buys the main part headroom. + /// + /// + /// This part adds no behaviour. The handler body is the one that previously lived in the main + /// part, with one Debug-level log statement added ahead of it. + /// + /// + public sealed partial class BreadcrumbDropDownHost + { + private static readonly log4net.ILog log = log4net.LogManager.GetLogger( + typeof(BreadcrumbDropDownHost) + ); + + /// + /// Renders the one-line close-ordering diagnostic emitted on entry to + /// . + /// + /// The reason WinForms gave for closing the drop-down. + /// Whether this host initiated the close itself. + /// The host's own open state at entry. + /// The drop-down's AutoClose setting at entry. + /// Whether the host has already been disposed. + /// Whether a close completion is already pending. + /// A single line carrying a sentence prefix and six Key=Value pairs. + /// + /// Pure and static so the AC6 evidence rests on a deterministic managed-seam assertion + /// rather than a source-text scan: a test calls this directly with a fixed argument tuple + /// and needs no popup, window, or WebView2 surface. + /// + internal static string FormatDropDownClosedDiagnostics( + ToolStripDropDownCloseReason closeReason, + bool programmaticClose, + bool openState, + bool autoClose, + bool disposed, + bool pendingClose + ) => + "Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. " + + $"CloseReason={closeReason} ProgrammaticClose={programmaticClose} " + + $"OpenState={openState} AutoClose={autoClose} " + + $"Disposed={disposed} PendingClose={pendingClose}"; + + private void OnDropDownClosed(object? sender, ToolStripDropDownClosedEventArgs e) + { + // Issue #796 (AC6): emitted at entry, ahead of the guard return, so a close this host + // suppresses is still visible in the ordering evidence the Phase 2 runbook collects. + log.Debug( + FormatDropDownClosedDiagnostics( + e.CloseReason, + _programmaticClose, + OpenState, + DropDown.AutoClose, + _disposed, + _openLifetime.IsPendingClose + ) + ); + if (_disposed || _programmaticClose || !OpenState) + return; + _openLifetime.InvalidateAndSchedule(() => + { + if (_disposed || _programmaticClose || !OpenState) + return; + OpenState = false; + FinishClose(BreadcrumbDropDownCloseReason.Uncommitted); + }); + } + } +} diff --git a/QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs b/QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs index fc12e36f9..5dce361a9 100644 --- a/QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs +++ b/QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs @@ -88,6 +88,26 @@ bool takeFocus return _openLifetime.OpenAsync(anchorScreenBounds, workingArea, desiredSize, takeFocus); } + /// + /// Issue #796 (AC3): whether a selection commit has been requested for the popup lifetime + /// that is currently open, so a close arriving with an Uncommitted reason must not + /// cancel the selection the commit is in the middle of making. + /// + /// + /// A settable internal property rather than a constructor parameter, matching the issue #677 + /// may-take-focus precedent, so every constructor keeps its baseline arity and the + /// reflection-based constructor binding the existing tests rely on is not disturbed. It is + /// declared on this part rather than on the main part because the main part stands close to + /// the repository's 500-line ceiling and this one does not. + /// + /// The lifetime is one popup opening: clears it as each fresh native + /// show begins, and nothing else clears it. That is deliberate — once a commit has been + /// requested for a given open popup, every later uncommitted-reason close of that same popup + /// is a close racing the commit, whichever order the two arrive in. + /// + /// + internal bool IsCommitPending { get; set; } + // Issue #680: AutoClose == false is the WinForms framework's own opt-out from // ModalMenuFilter menu-mode entry. Menu mode retargets every keystroke to the popup's window // handle whenever the popup does not contain focus, which is exactly the state a @@ -97,6 +117,10 @@ bool takeFocus // ordering by statement order. internal void ShowPopup(Point location, bool takeFocus) { + // Issue #796 (AC3): a fresh native show starts a new popup lifetime, which by definition + // has no commit in flight yet. Clearing here rather than at a close keeps the latch's + // meaning tied to the popup that is open, not to whichever close happened to run last. + IsCommitPending = false; DropDown.AutoClose = takeFocus; _showPopup(DropDown, Anchor, location); } diff --git a/QuickFiler/Viewers/BreadcrumbDropDownHost.cs b/QuickFiler/Viewers/BreadcrumbDropDownHost.cs index 3af7fdcc5..f04d4fa20 100644 --- a/QuickFiler/Viewers/BreadcrumbDropDownHost.cs +++ b/QuickFiler/Viewers/BreadcrumbDropDownHost.cs @@ -248,6 +248,12 @@ public bool Close(BreadcrumbDropDownCloseReason reason) { if (_disposed) return false; + // Issue #796 (AC3): an explicit-commit close is the commit reaching this host, so it is + // the point at which a commit becomes in flight for this popup lifetime. Recording it + // here rather than at the completion point is what lets a native uncommitted-reason + // close that arrives alongside it be recognised as racing the commit. + if (reason == BreadcrumbDropDownCloseReason.ExplicitCommit) + IsCommitPending = true; if (OpenState) { _openLifetime.InvalidateAndSchedule(() => CompleteClose(reason, true)); @@ -423,19 +429,6 @@ private void CloseNative() } } - private void OnDropDownClosed(object? sender, ToolStripDropDownClosedEventArgs e) - { - if (_disposed || _programmaticClose || !OpenState) - return; - _openLifetime.InvalidateAndSchedule(() => - { - if (_disposed || _programmaticClose || !OpenState) - return; - OpenState = false; - FinishClose(BreadcrumbDropDownCloseReason.Uncommitted); - }); - } - private void FinishClose(BreadcrumbDropDownCloseReason reason) { CompleteAll( @@ -446,7 +439,12 @@ private void FinishClose(BreadcrumbDropDownCloseReason reason) () => DropDown.AutoClose = true, () => { - if (reason == BreadcrumbDropDownCloseReason.Uncommitted) + // Issue #796 (AC3): an uncommitted-reason close arriving while a commit has + // been requested for this popup lifetime is a close racing the commit, so it + // must not undo it. The suppression is conditional on the latch and is + // therefore scoped: with no commit in flight the cancel still runs, which is + // what the retained BreadcrumbPendingOpenCloseTests cancel assertions pin. + if (reason == BreadcrumbDropDownCloseReason.Uncommitted && !IsCommitPending) _cancelSelection(); }, // Issue #677: only the focus step is gated; the cancel step above always runs. diff --git a/QuickFiler/Viewers/ItemViewer.Breadcrumb.cs b/QuickFiler/Viewers/ItemViewer.Breadcrumb.cs index 749f09133..179dbaabd 100644 --- a/QuickFiler/Viewers/ItemViewer.Breadcrumb.cs +++ b/QuickFiler/Viewers/ItemViewer.Breadcrumb.cs @@ -210,6 +210,10 @@ BreadcrumbDropDownHost is BreadcrumbDropDownHost existing // IBreadcrumbDropDownHost) keeps the interface unchanged, so mock hosts installed by // the injected 3-arg ConfigureBreadcrumbDropDown overload are unaffected. host.MayTakeFocus = MayRestoreBreadcrumbFocus; + // Issue #796 (AC2): report this popup to the owning form as one that can take activation + // from it, so the deactivation handler can tell a self-inflicted deactivation apart from + // a genuine one without reading Form.ActiveForm, which was measured to run inverted. + (FindForm() as QfcFormViewer)?.SetBreadcrumbPopupOwner(this, () => host.IsOpen); ConfigureBreadcrumbDropDown( host, () => diff --git a/QuickFiler/Viewers/QfcFormViewer.cs b/QuickFiler/Viewers/QfcFormViewer.cs index 53a5377d9..9c26f7f6f 100644 --- a/QuickFiler/Viewers/QfcFormViewer.cs +++ b/QuickFiler/Viewers/QfcFormViewer.cs @@ -206,6 +206,45 @@ public bool IsWebView2Focused /// public void ParkFocusOffWebView2() => this.ActiveControl = _l1v1L2h2_ButtonOK; + // Issue #796 (AC2): the breadcrumb popups that can take activation away from this form, one + // entry per item viewer that owns one. Keyed by the owning item viewer rather than held in a + // list so a reconfigured item viewer replaces its own entry instead of accumulating a second. + private readonly Dictionary> _breadcrumbPopupOwners = + new Dictionary>(); + + /// + /// Issue #796 (AC2): registers the predicate reporting whether the breadcrumb popup owned by + /// is currently open, and therefore able to hold activation. + /// + /// The item viewer that owns the popup. Ignored when null. + /// The open-state predicate. Ignored when null. + /// + /// Assigned by the item viewer immediately after it constructs its popup host, mirroring the + /// issue #677 may-take-focus precedent. Re-registering the same item viewer replaces its + /// entry, so repeated configuration passes leave exactly one predicate per item viewer. + /// + internal void SetBreadcrumbPopupOwner(Control itemViewer, Func popupIsOpen) + { + if (itemViewer == null || popupIsOpen == null) + { + return; + } + + _breadcrumbPopupOwners[itemViewer] = popupIsOpen; + } + + /// + /// + /// Derived from explicitly registered popup state, never from Form.ActiveForm. The + /// issue #796 manual observation measured ActiveFormNull=False on all three + /// self-inflicted popup gestures and ActiveFormNull=True on the one deactivation + /// caused by focus leaving the form, so that framework read runs opposite to the + /// discriminator originally proposed and is not usable here in either direction. A form with + /// no registered popup reports false, which is the GENUINE case and preserves issue #677. + /// + public bool IsDeactivationSelfInflictedByOwnPopup => + _breadcrumbPopupOwners.Values.Any(popupIsOpen => popupIsOpen()); + // Seam D — collapsed item-viewer template margin public Padding ItemViewerTemplateMargin => _QfcItemViewerTemplate?.Margin ?? default; diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/code-review.2026-09-07T17-05.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/code-review.2026-09-07T17-05.md new file mode 100644 index 000000000..e6a0e4741 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/code-review.2026-09-07T17-05.md @@ -0,0 +1,307 @@ +# Code Review — issue #796 (QuickFiler folder drop-down closes on open; row click does not select) + +- Timestamp: 2026-09-07T17-05 +- Issue: #796 +- Work Mode: full-bug +- Base: `a6b259160f9ac1fbe251708d897fd4721486259e`; head reported by the caller: `8e427fe1` +- Scope reviewed: the full code footprint, 15 paths under `QuickFiler/` and `QuickFiler.Test/` +- Method: read-only inspection of the supplied full-code diff plus direct reads of current file state. The Bash tool was not used, per the caller's binding constraint. + +## Verdict + +Approve. **Blocking findings: 0.** Seven non-blocking findings are recorded below, three of which +(CR-1, CR-2, CR-3) are recommended for promotion to follow-up issues rather than being fixed on this +branch. + +## 1. Does the ordering fix actually establish commit-before-cancel? + +This was the caller's specific question. The answer is a qualified yes, and the qualification matters. + +### What the mechanism actually is + +The change does not reorder anything. It makes the cancel step CONDITIONAL. `BreadcrumbDropDownHost.cs` +line 447: + +```csharp +if (reason == BreadcrumbDropDownCloseReason.Uncommitted && !IsCommitPending) + _cancelSelection(); +``` + +with `IsCommitPending` set at line 255-256 when a close arrives carrying `ExplicitCommit`, and cleared +only in `ShowPopup` (`BreadcrumbDropDownHost.Open.cs` line 123). + +### Why that nevertheless satisfies the stated invariant + +I traced the commit path rather than accepting the summary. `BreadcrumbDropDownOpenCoordinator.cs` +lines 186-189: when the selector reports it is no longer open, the coordinator calls +`CloseCore(BreadcrumbDropDownCloseReason.ExplicitCommit)`. The selector reports closed BECAUSE the +session already committed the pending identity upstream — `ExplicitCommit` is documented on the enum +(`IBreadcrumbDropDownHost.cs` line 11) as "Enter or row activation explicitly committed the pending +identity". + +So by the time `Close(ExplicitCommit)` reaches the host, the commit has already happened in the +session. The commit is therefore structurally before the host-level cancel on that path, and what the +latch adds is protection against a LATER uncommitted-reason close undoing it. The spec's own +Invariant section states the property in exactly this form — "cancels the pending selection if and +only if the close was not caused by this add-in's own activation or focus movement and no selection +commit is in flight" — and the implementation matches that wording, not the looser "commits before +cancels" phrasing in AC3. + +### The limit of the protection, stated because it is not stated in the code + +The latch protects only the ordering in which the commit's `Close` reaches the host FIRST. If a +native uncommitted-reason close reached `FinishClose` before the commit's `Close(ExplicitCommit)` were +issued, `IsCommitPending` would still be false and the cancel would run. + +This limit is not currently a live defect: on all three observed gestures the close carried +`CloseReason=CloseCalled` with `ProgrammaticClose=True`, i.e. it was downstream of the add-in's own +close call, not an independent framework close arriving first. But the added tests exercise only the +commit-first ordering (`NativeCloseWhileCommitPending_DoesNotCancelSelection` sets the latch, then +raises the close), so the reverse ordering is neither protected nor tested. Recording it so a later +reader does not assume more protection than exists. No action required now. + +### Scoping is correctly proved + +The suppression is proved to be conditional rather than global by three retained assertions that were +deliberately not modified: `CancelCount.Should().Be(1)` at `BreadcrumbPendingOpenCloseTests.cs` lines +48 and 79, plus the new `NativeCloseWithNoCommitPending_StillCancelsSelection`. That is the right +shape — a suppression that drove either of the retained assertions to zero would be a design signal, +and the spec says so in advance rather than after the fact. + +## 2. Correctness + +### AC2 guard ordering — verified, and it holds + +The AC2 guard reads `host.IsOpen`, whereas the AC6 per-item diagnostic reads +`_itemViewer.IsFolderDropDownOpen`, which is `BreadcrumbCoordinator?.IsSelectorOpen == true` +(`ItemViewer.FolderSearch.cs` line 93). These are two different states, so the observation's +`SelectorWasOpen=True` does not by itself establish that the guard would have fired. + +I checked the one thing that decides it. `BreadcrumbDropDownOpenLifetime.ShowCurrentSurface` sets +`_host.OpenState = true` (line 268) BEFORE calling `_host.ShowPopup(...)` (line 276), and +`BreadcrumbDropDownHost.IsOpen => OpenState` (line 191). The popup's native show is what takes +activation from the form, so at any deactivation the show provokes, `host.IsOpen` is already true and +the guard fires. The ordering is guaranteed by statement order within one UI-thread operation, not by +timing. Good. + +The residual window is narrow and I could not close it from source alone: a deactivation occurring +after the session opened but before `OpenState` is set — i.e. during asynchronous surface creation — +would not be caught. No popup is shown during that window, so a Win32 activation change there is +implausible, but it is not proven absent. + +### AC4 latch — correct, with one behaviour worth naming + +The latch has two producers (`TextBoxSearch_TextChanged` line 183, `TextBoxSearch_KeyDown` line 220) +and three release sites (Escape at 234, "nothing is open" at 257, and consumption at 265). The +consumer at 263 dismisses only what the search box owns. The mouse path needs no edit because it +never sets the flag, which is the cleanest possible expression of the requirement. + +The reasoning for not reusing `_searchLeaveHandoffPending` is correct and I verified it against that +field's own semantics: it is read-and-cleared at lines 247-251, so it is false again immediately after +the handoff it guards while the popup is still open. The two latches genuinely have different +lifetimes and overloading one would have broken #680. + +Behaviour worth naming, not a defect: `TextBoxSearch_TextChanged` sets the latch unconditionally. If a +mouse gesture opens the popup and the user then types into the search box, the search box TAKES +ownership of a popup it did not open, and a subsequent leave will dismiss it. That is defensible — +after typing, the search box is the thing driving the list — but it is a transfer of ownership that +neither the comment nor a test states. Consider documenting it. + +### AC5 — the guard is meaningful + +`RowSetRefreshWhileOpen_NeverClosesHost` asserts `Close` is never invoked across two row-set +replacements while the selector is open. The guard has real content precisely because the +session-preserving replacement path in `UtilitiesCS` is deliberately outside this diff, so what the +test observes is the untouched path. Using the existing headless-viewer plus mocked-host pattern +instead of adding a third harness to the file was the right call. + +## 3. Findings + +### CR-1 — Stale comment now contradicts the code directly above it (Minor, non-blocking) + +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs` line 450, inside `FinishClose`: + +```csharp +// Issue #677: only the focus step is gated; the cancel step above always runs. +FocusAnchorIfPermitted +``` + +The cancel step above no longer always runs. As of line 447 it is gated on two conditions. This +comment was true before this change and is false after it, and it sits three lines below the new +comment that explains the gating — so the file now asserts both that the cancel is conditional and +that it always runs. + +`CLAUDE.md` § C#6.3 requires comments to stay synchronized with behaviour. This is the one place in an +otherwise carefully commented diff where that slipped. + +Suggested replacement: "Issue #677 / #796: the focus step is gated on the may-take-focus predicate and +the cancel step above is gated on the pending-commit latch; the two gates are independent." + +Not blocking: a comment cannot change behaviour, every gate is green, and the misleading text is +adjacent to correct text that explains the actual rule. + +### CR-2 — `IsCommitPending` is never cleared on consumption, so a stale latch can survive into the next popup lifetime (Minor/latent, non-blocking) + +The XML documentation at `BreadcrumbDropDownHost.Open.cs` lines 102-107 states: "The lifetime is one +popup opening: `ShowPopup` clears it as each fresh native show begins, and nothing else clears it." + +That is accurate about the intent but it holds only when a native show actually begins. I traced the +paths that reach `FinishClose(Uncommitted)` and found one that does not: + +- `Reset()` → `ResetCoreAsync` reaches `CompleteClose` only `if (OpenState)` (line 316) — guarded. +- `Dispose()` → `DisposeCoreAsync` reaches it only `if (OpenState && !_resetPending)` (line 338) — guarded. +- `RestoreAfterOpenFailure()` (line 455) calls `FinishClose(BreadcrumbDropDownCloseReason.Uncommitted)` + UNCONDITIONALLY, and it is invoked from `BreadcrumbDropDownOpenLifetime.HandleOpenFailureAsync` + (line 376), which is reached from the `catch` around the whole open sequence (line 251-255). + +So the reachable sequence is: popup opens (`ShowPopup` clears the latch) → user commits +(`Close(ExplicitCommit)` sets it) → popup closes with the latch left true → a later open throws before +`ShowPopup` runs, for example during surface creation → `RestoreAfterOpenFailure` → `FinishClose(Uncommitted)` +→ the cancel is suppressed by a latch belonging to the PREVIOUS popup lifetime, leaving a selector +session open with no popup. + +Likelihood is low: it requires an open failure following a committed close in the same host instance. +But WebView2 initialisation failures are not hypothetical in this codebase — the spec's own Log +evidence section cites recurring `WebView2BreadcrumbHost` initialisation errors in a neighbouring +component. + +Suggested fix, for a follow-up issue rather than this branch: clear the latch when it is consumed or +when a close completes, for example set `IsCommitPending = false` at the end of `FinishClose`, or +clear it in `RestoreAfterOpenFailure` alongside `OpenState = false`. Either restores the documented +one-popup-lifetime semantics on every path. + +### CR-3 — The AC2 guard also gates the #791 Cancel teardown, where its predicate has no meaning (Major/latent, non-blocking) + +`ParkFocusAndCancelSelectors` has two callers, and the new guard at +`QfcFormController.Deactivate.cs` line 118 applies to both: + +1. `FormViewer_Deactivated` (line 26-27) — the Form.Deactivate event. The guard's semantics fit here. +2. `ActionCancelAsync` stage `"park-focus"` (`QfcFormController.EventHandlers.cs` line 144) — the + ordered #791 Cancel teardown. Here there is no deactivation at all, so "is this deactivation + self-inflicted by our own popup?" is not a question the caller is asking. The predicate reduces to + "is any breadcrumb popup open?", and if one is, the teardown's selector-cancel stage is skipped + entirely. + +The reason this matters is stated in this file's own class-level documentation (lines 12-14): "no +breadcrumb `ToolStripDropDown` may stay open, or WinForms modal menu mode keeps redirecting thread +keyboard messages to the popup after the user has left." + +I then walked the mitigation chain rather than stopping at the finding, and all three links exist: + +- `ActionCancelAsync` → `"groups-cleanup"` → `QfcCollectionController.Cleanup()` (line 2128) → + `RemoveControls()` (line 737) → `_itemGroups.ForEach(grp => grp.ItemController.Cleanup())` (line 749); +- `QfcItemController.Cleanup()` (`QfcItemController.ViewerSetup.cs` line 418) → + `(_itemViewer as ItemViewer)?.ResetBreadcrumb()`; +- `ResetBreadcrumb()` (`ItemViewer.Breadcrumb.cs` line 327) → lifecycle `Reset()` (line 207) → + `BreadcrumbDropDownOpenCoordinator.Reset()` (line 193), which posts + `if ((!_host.IsOpen || !_host.Close(Uncommitted)) && _isSelectorOpen()) _cancelSelector();` and then + `_host.Reset()`. + +So the popup IS closed and the selector IS cancelled during Cancel teardown — but at a later stage, +through a different path, and via a fire-and-forget `PostAsync` rather than the synchronous stage the +teardown was ordered to use. The net effect of this change on the Cancel path is a weakened ordering +guarantee, not a lost responsibility. That is why this is non-blocking. + +Suggested fix, for a follow-up issue: give `ParkFocusAndCancelSelectors` a parameter such as +`bool honourSelfInflictedGuard`, passed `true` from `FormViewer_Deactivated` and `false` from the +teardown stage, so the guard applies only where its predicate is meaningful. A regression test on the +teardown path ("Cancel with a popup open still cancels every selector at the park-focus stage") would +pin it. + +### CR-4 — Dead internal accessor (Minor, non-blocking) + +`QfcItemController.EventHandlers.cs` line 209, `SearchOwnsDropDownDismissal`, has no reader anywhere in +the tree. Fully adjudicated in `policy-audit.2026-09-07T17-05.md` § 8 F1; not repeated here. Not +blocking. Recommended: delete it, or give it the reader described in CR-6. + +### CR-5 — `_breadcrumbPopupOwners` entries are never removed (Informational) + +`QfcFormViewer.cs` lines 1067-1068 of the diff: a `Dictionary>` that gains entries +in `SetBreadcrumbPopupOwner` and never loses one. Each entry holds a strong reference to an item-viewer +`Control` key and a closure capturing a `BreadcrumbDropDownHost`. + +Growth is bounded, so this is informational rather than a leak finding: item viewers are pooled — the +comment at `QfcItemController.ViewerSetup.cs` line 416 says "before releasing the pooled viewer" — and +the dictionary is keyed by viewer, so re-registration replaces rather than appends. The choice of a +keyed dictionary over a list is the right one and is documented as such. + +Two smaller notes on the same member. First, the predicate's value after a host is disposed is +`OpenState`, which a disposed host leaves false, so a stale entry cannot wrongly report `true`. +Second, `IsDeactivationSelfInflictedByOwnPopup` invokes every registered predicate on every +deactivation via `.Any(...)`; with nine item groups that is at most nine field reads, so the cost is +immaterial. + +### CR-6 — The re-pin sets private state by string field name (Informational) + +`QfcItemController.SearchDismissalTests.cs` line 85: + +```csharp +QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true); +``` + +This couples the test to a private field's spelling, so a rename breaks it at run time rather than at +compile time. It is also the direct cause of CR-4: because the state is reached by reflection, the +`internal` accessor added for exactly this state never acquired a reader. + +The sibling suite shows the alternative works: +`SearchLeaveAfterSearchDrivenOpen_ClosesDropDown` establishes the same state by driving the real +`TextBoxSearch_TextChanged` open path, with no reflection. Driving the real path here too would remove +the string literal and let CR-4's accessor be deleted outright. Minimal-diff was a legitimate reason to +choose reflection for a deliberate re-pin; recording the trade-off. + +### CR-7 — The AC2 producer has no automated test (Informational) + +`QfcFormViewer.IsDeactivationSelfInflictedByOwnPopup` and the `ItemViewer.Breadcrumb.cs` registration +call are both in types carrying a class-level `[ExcludeFromCodeCoverage]`, and no test exercises +either. The AC2 tests mock `IQfcFormViewer`, so the CONSUMER's gating is proven and the PRODUCER's +derivation is not. Permitted by the ratified WinForms exemption; recorded because the spec's +automation table claims AC2 is automatable "Yes, fully", which is true at the seam and not end to end. + +## 4. Design, naming, and error handling — positive observations + +These are recorded because they are load-bearing for the verdict, not as praise. + +- **Pure formatters.** Making the three diagnostic renderers `static` and argument-driven converts what + would otherwise have been a source-text scan into a deterministic assertion, and lets the AC6 field + set be pinned without a popup, a window, or a WebView2. This is the correct way to make logging + testable and it should be reused. +- **Load-bearing polarity documented at the declaration.** `IQfcFormViewer.cs` states that `false` + means GENUINE and must not be inverted, and gives the reason: Moq's default `bool` return is `false`, + so the existing deactivate suite continues to describe a genuine deactivation. That is a contract a + future editor could otherwise invert without any test failing loudly. +- **A refuted hypothesis is documented at the site that would tempt someone to re-adopt it.** The + `activeFormIsNull` parameter documentation records that the `Form.ActiveForm` discriminator ran + opposite to prediction on all four observations and is retained as data only. The + `IsDeactivationSelfInflictedByOwnPopup` implementation repeats the warning. The decision record adds + that the refuted discriminator is deliberately NOT inverted and re-used, because one observation of a + single genuine deactivation is too thin a basis. That reasoning is correct and unusually disciplined. +- **The guard is scoped to the cancel loop and not to focus parking**, and the reason given is + measured rather than assumed: parking did not run on two of the three defective gestures + (`WebView2Focused=False`), so suppressing it cannot be what fixes the defect. The decision is + recorded as an explicit branch (`AC2-PARK-FOCUS-SUPPRESSED: NO`) rather than allowed to happen by + default, which is what the spec demanded. +- **The 500-line ceiling was managed by relocation rather than by exception.** Moving + `OnDropDownClosed` into the new part bought headroom, and the main file ended two lines SHORTER than + it started (498 → 496) despite receiving new code. +- **Error handling is untouched where it should be.** The per-item boundary catch and its rationale + comment survive verbatim, and the new guard sits outside the `try`, so a self-inflicted deactivation + returns before the loop rather than short-circuiting inside it. + +## 5. Summary of findings by severity + +| ID | Severity | Blocking | Summary | +|---|---|---|---| +| CR-1 | Minor | No | Stale `// only the focus step is gated` comment at `BreadcrumbDropDownHost.cs:450` now contradicts line 447 | +| CR-2 | Minor / latent | No | `IsCommitPending` not cleared on consumption; `RestoreAfterOpenFailure` can consume a previous lifetime's latch | +| CR-3 | Major / latent | No | AC2 guard also gates the #791 Cancel teardown caller; mitigated later in teardown by the reset chain | +| CR-4 | Minor | No | `SearchOwnsDropDownDismissal` has no reader anywhere in the tree | +| CR-5 | Informational | No | `_breadcrumbPopupOwners` entries are never removed; bounded by the item-viewer pool | +| CR-6 | Informational | No | Re-pin sets private state by string field name; causes CR-4 | +| CR-7 | Informational | No | AC2 producer side is in coverage-exempt types with no automated test | + +**Blocking findings: 0.** + +Recommended follow-up: promote CR-1, CR-2 and CR-3 through the potential-to-issue lifecycle so they +survive the merge of this feature folder. CR-3 is the one worth prioritising, because it changes the +ordering guarantees of a teardown path that a previous issue (#791) was opened specifically to make +deterministic. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t10-quickfiler-test-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t10-quickfiler-test-baseline.md new file mode 100644 index 000000000..a1fadc6fa --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t10-quickfiler-test-baseline.md @@ -0,0 +1,61 @@ +# P0-T10 — QuickFiler.Test baseline test result + +Timestamp: 2026-09-07T14-12 +Task: [P0-T10] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" /ResultsDirectory:TestResults\796\p0-t10 "/Logger:trx;LogFileName=p0-t10.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## Run summary, verbatim + +``` +Test Run Successful. +Total tests: 1370 + Passed: 1370 + Total time: 12.8905 Seconds +``` + +| Figure | Value | How obtained | +|---|---|---| +| Total | 1370 | read from the run summary | +| Passed | 1370 | read from the run summary | +| Failed | 0 | NOT PRINTED ON A PASSING RUN | +| Skipped | 0 | DERIVED as Total minus the sum of Passed and Failed | + +The Failed count is recorded as 0 with the note `NOT PRINTED ON A PASSING RUN` +because vstest.console.exe emits no `Failed:` line when the run has no failures. + +The Skipped count is DERIVED rather than read. vstest.console.exe prints no +`Skipped:` line on a run with no skipped tests, and the TRX `notExecuted` attribute +is hard-coded to 0, so neither source can supply the figure. The derivation used is +Total minus the sum of Passed and Failed: 1370 - (1370 + 0) = 0. + +The `TestCategory!=LiveOutlook` filter EXCLUDES rather than skips. Tests it removes +appear in neither the Total figure nor the derived Skipped figure, so the derived +Skipped value is not inflated by filtering. + +## BASELINE_FAILURE_SET + +EMPTY. No test is Failed at baseline. + +This named set is what the Phase 9 final run is compared against. This plan asserts +non-growth relative to this set; it does not assert a repository-wide "zero failed" +expectation anywhere. + +## Raw output + +The TRX is written to the gitignored path TestResults/796/p0-t10/p0-t10.trx and is +never committed, because a TRX embeds the host account name and machine name in its +`runUser` and `computerName` attributes. `.gitignore` line 39 ignores TestResults/ +through the bracket class `[Tt]est[Rr]esult*/`. + +Output Summary: 1370 tests run under the `TestCategory!=LiveOutlook` filter with +`/InIsolation`; 1370 Passed, 0 Failed, 0 Skipped (derived). EXIT_CODE 0. +BASELINE_FAILURE_SET is empty. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t11-coverage-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t11-coverage-baseline.md new file mode 100644 index 000000000..2522318f5 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t11-coverage-baseline.md @@ -0,0 +1,78 @@ +# P0-T11 — Coverage baseline for the QuickFiler.Test assembly + +Timestamp: 2026-09-07T14-14 +Task: [P0-T11] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput coverage\p0-t11-baseline.cobertura.xml +``` + +EXIT_CODE: 1 + +## Why a non-zero exit code is accepted here + +The recorded stderr contains the literal `is below the required 80`. The exact +message was: + +``` +Cobertura line coverage 24.1387% is below the required 80% threshold. +``` + +That is the single-line message scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1 +line 54 throws when the document-level line rate is under the runner's own 80 percent +floor. The throw occurs after the Cobertura post-processing at line 342, so the +numbers were still written and are readable. The run itself reported +`Test Run Successful. Total tests: 1370, Passed: 1370`. Any other non-zero exit code +would have failed this task. + +The 24.1387 percent figure is a whole-solution document-level rate produced by a run +scoped to a single test assembly; it is recorded as the baseline datum, not as a +policy verdict. + +## Output Summary — document-level attributes + +``` +line-rate=0.241387 +lines-covered=14867 +lines-valid=61590 +branch-rate=0.229747 +branches-covered=3664 +branches-valid=15948 +``` + +All six numeric attributes above are recorded. + +## Per-file rows for the five named files + +Rows were obtained by grouping every `//class` node by its `filename` attribute and +summing, per group, the count of `lines/line` child nodes and the count of +`lines/line[@hits>0]` child nodes. Class nodes are grouped by `filename` because a +C# async state machine is emitted as a separate class node and would otherwise split +one source file's denominator across several nodes. The relative child axis is used +rather than a descendant axis, because a descendant axis double-counts on nested +nodes. + +Filenames are reproduced verbatim as the tool emitted them, which uses backslash +separators. The forward-slash spellings in the plan name the same five files. + +| Filename as emitted | lines-covered | lines-valid | +|---|---|---| +| `QuickFiler\Controllers\QfcFormController.Deactivate.cs` | 25 | 25 | +| `QuickFiler\Viewers\BreadcrumbDropDownHost.cs` | 291 | 293 | +| `QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs` | 23 | 23 | +| `QuickFiler\Controllers\QfcItemController.EventHandlers.cs` | 89 | 108 | +| `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs` | 234 | 238 | + +None of the five is ABSENT. Every one of the five carries a class node whose +`filename` attribute names it, so no row records +`ABSENT: no class node carries this filename`, and task P9-T7 has no file to exclude +from the changed-code denominator on the strength of this baseline. + +## Raw output + +The Cobertura XML is at the gitignored path coverage/p0-t11-baseline.cobertura.xml +(`.gitignore` line 144 ignores everything under coverage/). diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t12-file-size-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t12-file-size-baseline.md new file mode 100644 index 000000000..1fa2fae60 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t12-file-size-baseline.md @@ -0,0 +1,54 @@ +# P0-T12 — File-size baseline for the write set + +Timestamp: 2026-09-07T14-14 +Task: [P0-T12] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '@("QuickFiler\Controllers\QfcFormController.Deactivate.cs","QuickFiler\Interfaces\IQfcFormViewer.cs","QuickFiler\Viewers\QfcFormViewer.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs","QuickFiler\Viewers\ItemViewer.Breadcrumb.cs","QuickFiler\Controllers\QfcItemController.EventHandlers.cs","QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs","QuickFiler\Resources\FolderBreadcrumb.html","QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs","QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +Every later task in this plan that re-measures a line count uses this idiom and no +other, so the baseline and the final audit are commensurable. The idiom +`(Get-Content $_ | Measure-Object -Line).Lines` is PROHIBITED throughout, because +`Measure-Object -Line` omits blank lines and under-reports every count by that file's +blank-line total. + +## Measured physical line counts + +| Path | Physical lines | +|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 73 | +| QuickFiler/Interfaces/IQfcFormViewer.cs | 72 | +| QuickFiler/Viewers/QfcFormViewer.cs | 293 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 498 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 107 | +| QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 456 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 263 | +| QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs | 395 | +| QuickFiler/Resources/FolderBreadcrumb.html | 490 | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 248 | +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 380 | + +## The four pinned values + +| Path | Required | Measured | Verdict | +|---|---|---|---| +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 498 | 498 | match | +| QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 456 | 456 | match | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 248 | 248 | match | +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 380 | 380 | match | + +All four measured values equal the values this plan was authored against. The tree +has not moved, so the file-size arithmetic in Phase 1 and Phase 5 stands and does not +need to be re-derived. + +Output Summary: 11 paths measured with the recorded idiom. All four pinned values +(498, 456, 248, 380) match. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t13-mcp-validator-probe.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t13-mcp-validator-probe.md new file mode 100644 index 000000000..810c87938 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t13-mcp-validator-probe.md @@ -0,0 +1,38 @@ +# P0-T13 — Plan validator MCP tool probe + +Timestamp: 2026-09-07T14-15 +Task: [P0-T13] +Issue: #796 + +Intended invocation: `mcp__drm-copilot__validate_orchestration_artifacts` with +`artifact_type: "plan"` and `artifact_path` set to +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md + +VALIDATOR NOT RUN: tool absent from this session's tool surface + +## Enumeration of the drm-copilot MCP tools actually available to this session + +- `mcp__drm-copilot__run_poshqc_format` +- `mcp__drm-copilot__run_poshqc_analyze` +- `mcp__drm-copilot__run_poshqc_analyze_autofix` +- `mcp__drm-copilot__run_poshqc_test` + +`mcp__drm-copilot__validate_orchestration_artifacts` is not among them, so the +invocation could not be attempted. The absence is recorded rather than worked around; +no substitute validator was run and no human-readable summary is offered in its place. + +## Incidental observation + +While establishing the available tool surface, one read-only +`mcp__drm-copilot__run_poshqc_analyze` call was issued against this worktree scoped +to the feature folder. It returned `ok: true` with the summary +`Ran bundled PoshQC analyze against ... with 1 selected scan folder(s)`. PoshQC +analyze is read-only and rewrites no tracked file; the P0-T14 porcelain capture that +follows this task is the record of the tree state after it. This call is not offered +as a substitute for the plan validator and satisfies no acceptance condition. + +EXIT_CODE: not applicable; the tool was not invoked. + +Output Summary: The plan validator MCP tool is absent from this session's tool +surface. This is a record-and-continue probe and not a halt gate, so execution +proceeds to P0-T14. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t14-scope-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t14-scope-baseline.md new file mode 100644 index 000000000..e229033c3 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t14-scope-baseline.md @@ -0,0 +1,91 @@ +# P0-T14 — Scope-lock baseline for the execution worktree + +Timestamp: 2026-09-07T14-15 +Task: [P0-T14] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command 'git rev-parse HEAD; git rev-parse --abbrev-ref HEAD; git status --porcelain --untracked-files=all' +``` + +EXIT_CODE: 0 + +HEAD: 336e30db5350845898e5a96f466df98678668d83 +Branch: bug/quickfiler-folder-dropdown-closes-on-open-796 +Base anchor used by every diff gate in this plan: c7ae69f1 + +`--untracked-files=all` is used because porcelain status otherwise collapses an +untracked directory to a single directory entry and would not enumerate the evidence +artifacts this plan creates. + +## Full porcelain output + +``` + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t10-quickfiler-test-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t11-coverage-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t12-file-size-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t13-mcp-validator-probe.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t2-dotnet-sdk-install.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t3-nuget-restore.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t4-analyzer-version-skew.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t5-dotnet-tool-restore.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t6-dotnet-coverage-probe.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t7-csharpier-check-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t8-analyzer-rebuild-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t9-nullable-rebuild-baseline.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/phase0-instructions-read.md +``` + +Fourteen entries. Every one of them is inside the feature folder: one modified plan +file carrying this phase's check-offs, and thirteen untracked Phase 0 evidence +artifacts. + +## PRE-EXISTING-DIRTY-SET: + +EMPTY. + +No porcelain path lies outside the feature folder. This is the normal result the plan +anticipates, because the preparation run that produced this plan committed the +feature folder and the promoted record and removed its own agent-memory writes before +finishing. Because the set is empty, every later porcelain gate in this plan is +strict: a gate that expects zero lines outside the feature folder and the write set +has no admitted exceptions to subtract. + +## Permitted-change set — the sixteen write-set paths + +| # | Path | Category | +|---|---|---| +| 1 | QuickFiler/Controllers/QfcFormController.Deactivate.cs | production, modify | +| 2 | QuickFiler/Interfaces/IQfcFormViewer.cs | production, modify | +| 3 | QuickFiler/Viewers/QfcFormViewer.cs | production, modify | +| 4 | QuickFiler/Viewers/BreadcrumbDropDownHost.cs | production, modify | +| 5 | QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | production, modify | +| 6 | QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | production, modify | +| 7 | QuickFiler/Controllers/QfcItemController.EventHandlers.cs | production, modify | +| 8 | QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs | production, modify | +| 9 | QuickFiler/Resources/FolderBreadcrumb.html | production, modify | +| 10 | QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | production, create | +| 11 | QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | test, modify | +| 12 | QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | test, modify | +| 13 | QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | test, create | +| 14 | QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs | test, create | +| 15 | QuickFiler/QuickFiler.csproj | compile entries, modify | +| 16 | QuickFiler.Test/QuickFiler.Test.csproj | compile entries, modify | + +Sixteen paths. This is the permitted-change set for the Phase 9 scope-boundary gate. + +## QuickFiler/QuickFiler.csproj.bak + +The tracked file QuickFiler/QuickFiler.csproj.bak exists in this worktree +(verified: `Test-Path` returned True). It is NOT in the write set and must not be +edited. It is not a .cs file, so the formatter does not touch it, and no task in this +plan reads it. It is recorded here so a later search for compile entries does not +mistake it for the project file. + +Output Summary: HEAD 336e30db, branch bug/quickfiler-folder-dropdown-closes-on-open-796, +14 porcelain entries all inside the feature folder, PRE-EXISTING-DIRTY-SET empty, and +the sixteen-path permitted-change set recorded. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t2-dotnet-sdk-install.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t2-dotnet-sdk-install.md new file mode 100644 index 000000000..a3561cc59 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t2-dotnet-sdk-install.md @@ -0,0 +1,71 @@ +# P0-T2 — Command channel determination and repo-pinned .NET SDK install + +Timestamp: 2026-09-07T14-05 +Task: [P0-T2] +Issue: #796 + +COMMAND-CHANNEL: A + +## Rung taken + +Rung 1. The isolation guard did not refuse the pwsh invocation, so rung 2 was not +taken and no Channel B substitution applies anywhere in this plan. + +Command: +`pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1` + +EXIT_CODE: 0 + +Output Summary: The script downloaded the SDK archive from +`https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.205/dotnet-sdk-8.0.205-win-x64.zip` +and reported `Installed repo-local .NET SDK 8.0.205 to /.dotnet-sdk.` +The absolute destination path the script printed is redacted to ``; no +absolute host path is recorded in this artifact. + +## SDK marker verification + +Command: +`pwsh -NoProfile -Command 'Test-Path .dotnet-sdk/sdk/8.0.205'` + +Result: `MarkerExists=True`. The directory `.dotnet-sdk/sdk/8.0.205` exists. + +## dotnet version verification on the recorded channel + +Command: +`pwsh -NoProfile -Command 'dotnet --version'` + +EXIT_CODE: 0 + +Recorded stdout, verbatim: + +``` +8.0.205 +``` + +The recorded stdout is a version string beginning with the two characters `8.`. +It is not the sentence `The repo-local .NET SDK is missing.`, which is what +global.json prints as its errorMessage when the pinned SDK is absent. + +The `dotnet` muxer resolved from the machine PATH; the SDK itself resolved to the +repo-local `.dotnet-sdk` directory through the `paths` entry in global.json, which +is why the printed version is the pinned 8.0.205 rather than a machine-global +version. + +## Working-directory prefix used on Channel A + +Every `pwsh -NoProfile -Command` invocation in this plan is issued from a tool whose +current directory is not this worktree, so each invocation is prefixed inside the +single-quoted script with `Set-Location ;` before the plan's command text. +This is a working-directory prefix and not a substitution of any command form: the +command text following it is the plan's text verbatim, with the plan's relative paths +preserved. It is required because `dotnet` searches upward from the current directory +for global.json, and because every relative path in this plan is relative to the +worktree root. `pwsh -NoProfile -File` invocations are issued with the script's +absolute path; `Install-RepoDotNetSdk.ps1` resolves its install directory from +`$PSScriptRoot` rather than from the current directory, so it installed into this +worktree. + +## Channel B equivalents + +Not applicable. The recorded channel is A. No later task in this plan substitutes a +Channel B command form. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t3-nuget-restore.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t3-nuget-restore.md new file mode 100644 index 000000000..445271794 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t3-nuget-restore.md @@ -0,0 +1,48 @@ +# P0-T3 — NuGet restore for TaskMaster.sln + +Timestamp: 2026-09-07T14-05 +Task: [P0-T3] +Issue: #796 +Channel used: A (recorded by P0-T2) + +Command: +`pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1 -SolutionPath TaskMaster.sln -Configuration Debug` + +EXIT_CODE: 0 + +## Output Summary + +The script resolved MSBuild through vswhere to the Visual Studio 18 Community +installation and invoked `MSBuild TaskMaster.sln /t:Restore /p:Configuration=Debug +"/p:Platform=Any CPU" /p:RestorePackagesConfig=true /m`. + +Restore summary, verbatim from the run: + +``` +Installed: + 172 package(s) to packages.config projects +Build succeeded. + 0 Warning(s) + 0 Error(s) +Time Elapsed 00:00:02.92 +``` + +The script terminates with a `throw` when MSBuild returns a non-zero exit code, so +the absence of that throw together with the `Build succeeded.` summary establishes +`EXIT_CODE: 0`. + +## Packages directory verification + +Command: +`pwsh -NoProfile -Command '(Get-ChildItem packages -Directory).Count'` + +Recorded output: `172` + +172 is greater than 0, so the packages directory exists at the worktree root and is +populated. Seventeen of the eighteen projects in the tree declare +`EnsureNuGetPackageBuildImports`, whose `` fires at +`BeforeTargets="PrepareForBuild"`; that hard failure and its CS0246 cascade are +therefore avoided for the subsequent analyzer and nullable rebuilds. + +Absolute host paths printed by MSBuild in its project banners are not reproduced in +this artifact. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t4-analyzer-version-skew.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t4-analyzer-version-skew.md new file mode 100644 index 000000000..6a82fe404 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t4-analyzer-version-skew.md @@ -0,0 +1,72 @@ +# P0-T4 — Analyzer package version agreement + +Timestamp: 2026-09-07T14-07 +Task: [P0-T4] +Issue: #796 +Channel used: A + +Command: a PowerShell measurement that, for each of the two project files, parses +that project's `packages.config` as XML into an id-to-version map, extracts every +`` path with the regex `Analyzer\s+Include="([^"]+)"`, +derives the package id and version from the `..\packages\.\...` folder +segment by longest-id match against the map, compares the two versions, and calls +`Test-Path` on the referenced .dll resolved relative to the project directory. + +EXIT_CODE: 0 + +## Why this is re-measured rather than assumed + +A missing analyzer HintPath is `error CS0006`, not a warning, so a skew would fail +the P0-T8 and P0-T9 rebuild gates rather than downgrade them. The skew was resolved +upstream in issue #647; this task re-measures rather than assuming the resolution is +present in this worktree. + +## QuickFiler/QuickFiler.csproj + +The analyzer `` spans lines 591-603. The `` entries +occupy lines 593-600 and 602; line 601 is an `` entry for +BannedSymbols.txt and line 592 is a comment. This matches the plan's cited range +593-602. + +| Package | csproj HintPath version | packages.config version | Verdict | .dll on disk | +|---|---|---|---|---| +| Meziantou.Analyzer | 3.0.203 | 3.0.203 | AGREES | yes | +| Roslynator.Analyzers (Roslynator.CSharp.Analyzers.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.Common.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.Core.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.CSharp.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| AsyncFixer | 2.1.0 | 2.1.0 | AGREES | yes | +| Microsoft.CodeAnalysis.BannedApiAnalyzers (BannedApiAnalyzers.dll) | 5.6.0 | 5.6.0 | AGREES | yes | +| Microsoft.CodeAnalysis.BannedApiAnalyzers (CSharp.BannedApiAnalyzers.dll) | 5.6.0 | 5.6.0 | AGREES | yes | +| SonarAnalyzer.CSharp | 10.33.0.1635 | 10.33.0.1635 | AGREES | yes | + +Nine rows. Every row AGREES and every referenced .dll exists on disk. + +## QuickFiler.Test/QuickFiler.Test.csproj + +The `` entries occupy lines 491-493 and 516-523. + +| Package | csproj HintPath version | packages.config version | Verdict | .dll on disk | +|---|---|---|---|---| +| MSTest.Analyzers (MSTest.Analyzers.CodeFixes.dll) | 4.4.0 | 4.4.0 | AGREES | yes | +| MSTest.Analyzers (MSTest.Analyzers.dll) | 4.4.0 | 4.4.0 | AGREES | yes | +| SonarAnalyzer.CSharp | 10.33.0.1635 | 10.33.0.1635 | AGREES | yes | +| Meziantou.Analyzer | 3.0.203 | 3.0.203 | AGREES | yes | +| Roslynator.Analyzers (Roslynator.CSharp.Analyzers.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.Common.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.Core.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| Roslynator.Analyzers (Roslynator_Analyzers_Roslynator.CSharp.dll) | 5.0.0 | 5.0.0 | AGREES | yes | +| AsyncFixer | 2.1.0 | 2.1.0 | AGREES | yes | +| Microsoft.CodeAnalysis.BannedApiAnalyzers (BannedApiAnalyzers.dll) | 5.6.0 | 5.6.0 | AGREES | yes | +| Microsoft.CodeAnalysis.BannedApiAnalyzers (CSharp.BannedApiAnalyzers.dll) | 5.6.0 | 5.6.0 | AGREES | yes | + +Eleven rows. Every row AGREES and every referenced .dll exists on disk. + +## Remediation + +None applied. No row is SKEWED and no referenced .dll is absent, so the task's +remediation branch (a repeat of the P0-T3 restore, or a corrected HintPath) was not +entered. + +Output Summary: 20 analyzer HintPath rows measured across the two project files. +20 of 20 read AGREES; 20 of 20 referenced .dll files are present on disk. No skew. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t5-dotnet-tool-restore.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t5-dotnet-tool-restore.md new file mode 100644 index 000000000..4789322e2 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t5-dotnet-tool-restore.md @@ -0,0 +1,44 @@ +# P0-T5 — Local dotnet tool manifest restore + +Timestamp: 2026-09-07T14-07 +Task: [P0-T5] +Issue: #796 +Channel used: A + +Command: +`pwsh -NoProfile -Command 'dotnet tool restore; "EXIT_CODE=$LASTEXITCODE"'` + +EXIT_CODE: 0 + +Recorded stdout, verbatim: + +``` +Tool 'csharpier' (version '1.2.6') was restored. Available commands: csharpier + +Restore was successful. +EXIT_CODE=0 +``` + +## CSharpier invocation verification + +The manifest is dotnet-tools.json at the repository root and pins CSharpier 1.2.6, +whose v1 CLI requires a subcommand. The CLAUDE.md form `dotnet tool run csharpier +format .` is therefore the correct invocation. That was verified by running the tool +rather than assumed. + +Command: +`pwsh -NoProfile -Command 'dotnet tool run csharpier --version'` + +EXIT_CODE: 0 + +Full recorded stdout, verbatim: + +``` +1.2.6 +``` + +The printed version begins with the two characters `1.`. It is not a +manifest-not-found error. + +Output Summary: Tool manifest restored, CSharpier 1.2.6 resolves and reports its +version through `dotnet tool run`. Both invocations returned EXIT_CODE 0. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t6-dotnet-coverage-probe.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t6-dotnet-coverage-probe.md new file mode 100644 index 000000000..dc84261ec --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t6-dotnet-coverage-probe.md @@ -0,0 +1,30 @@ +# P0-T6 — dotnet-coverage tool probe + +Timestamp: 2026-09-07T14-08 +Task: [P0-T6] +Issue: #796 +Channel used: A + +Rung taken: rung 1. The tool was already present, so rung 2 +(`dotnet tool install --global dotnet-coverage`) was not taken. + +Command: +`pwsh -NoProfile -Command 'dotnet-coverage --version'` + +EXIT_CODE: 0 + +Recorded stdout, verbatim: + +``` +18.10.0+f4cc39224845ffa74bf246c9da2399d50e5d6342 +``` + +## Why this matters + +scripts/vscode/Invoke-MSTestWithCoverage.ps1 throws the sentence +`dotnet-coverage not found.` at line 293 when the tool is absent. Every coverage +task in this plan, starting with P0-T11, runs through that script, so the tool's +absence would have made them unreachable. + +Output Summary: dotnet-coverage resolves and prints version +18.10.0+f4cc39224845ffa74bf246c9da2399d50e5d6342 with EXIT_CODE 0 on the first rung. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t7-csharpier-check-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t7-csharpier-check-baseline.md new file mode 100644 index 000000000..dfd63d0b1 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t7-csharpier-check-baseline.md @@ -0,0 +1,32 @@ +# P0-T7 — CSharpier baseline over the whole tree + +Timestamp: 2026-09-07T14-08 +Task: [P0-T7] +Issue: #796 +Channel used: A + +Command: +`pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` + +EXIT_CODE: 0 + +Recorded stdout, verbatim: + +``` +Checked 1601 files in 6592ms. +EXIT_CODE=0 +``` + +## Files reported as unformatted + +None. CSharpier lists each unformatted file on its own line before the summary line; +the recorded output carries no such line, only the summary. The list is therefore +empty. + +CSHARPIER-BASELINE: CLEAN + +The verdict line above carries exactly one of the two admitted values. Task P9-T1 +branches on it. + +Output Summary: 1601 files checked, 0 unformatted, EXIT_CODE 0. The tree is clean +against the manifest-pinned CSharpier 1.2.6 before any Phase 1 edit. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t8-analyzer-rebuild-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t8-analyzer-rebuild-baseline.md new file mode 100644 index 000000000..e459dde98 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t8-analyzer-rebuild-baseline.md @@ -0,0 +1,72 @@ +# P0-T8 — Analyzer baseline over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T14-10 +Task: [P0-T8] +Issue: #796 +Channel used: A + +RunStartedUtc: 2026-09-07T14:09:26.4332297Z + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p0-t8\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +MSBuild version reported: 18.9.1+a81b43525 for .NET Framework. + +## Build summary, verbatim + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:23.21 +``` + +ANALYZER-BASELINE-WARNINGS: 0 +ANALYZER-BASELINE-ERRORS: 0 + +These two totals are the baseline that the P1-T9 final analyzer gate is compared +against. + +## Compiler-invocation counts read back from the detailed log + +Raw log (gitignored, `.gitignore` line 84 ignores `*.log`): +TestResults/796/p0-t8/analyzer-rebuild.log + +Command: + +``` +pwsh -NoProfile -Command '$log = "TestResults\796\p0-t8\analyzer-rebuild.log"; "CscTaskCount=" + (Select-String -Path $log -Pattern "Task .Csc.").Count; "CscToolCount=" + (Select-String -Path $log -SimpleMatch "csc.exe").Count; (Get-Item QuickFiler\bin\Debug\QuickFiler.dll).LastWriteTimeUtc.ToString("o"); (Get-Item QuickFiler.Test\bin\Debug\QuickFiler.Test.dll).LastWriteTimeUtc.ToString("o")' +``` + +CscTaskCount=36 +CscToolCount=36 + +Both counts are greater than zero, so `CoreCompile` ran and the analyzers ran with +it. An exit code of 0 with both counts at zero would have been a FAILED gate. + +## Assembly-freshness corroboration + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T14:09:37.7556047Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T14:09:42.4832403Z | yes | + +Both assemblies were written after the run started, so neither of the two projects +this item touches was skipped. + +## Remediation branch + +Not entered. No `error CS0006` naming an analyzer assembly was emitted in any +project, so the second-attempt restore branch did not apply. + +Output Summary: /t:Rebuild of TaskMaster.sln with EnableNETAnalyzers and +EnforceCodeStyleInBuild returned EXIT_CODE 0 with 0 warnings and 0 errors. +36 Csc task invocations and 36 csc.exe tool invocations recorded in the detailed +log, and both touched assemblies rebuilt after RunStartedUtc, so the gate is not +vacuous. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t9-nullable-rebuild-baseline.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t9-nullable-rebuild-baseline.md new file mode 100644 index 000000000..3f315f587 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/p0-t9-nullable-rebuild-baseline.md @@ -0,0 +1,63 @@ +# P0-T9 — Nullable baseline over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T14-11 +Task: [P0-T9] +Issue: #796 +Channel used: A + +RunStartedUtc: 2026-09-07T14:10:48.3258170Z + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\796\p0-t9\nullable-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## No `/p:Nullable=enable` token + +The command line reproduced above contains no `/p:Nullable=enable` token. That is +confirmed by inspection of the command text and corroborated by a search of the +run's console output for the substring `Nullable=enable`, which returned 0 matches. + +No project in this repository carries a `` element, and neither +Directory.Build.props nor Directory.Build.targets sets a nullable property, so +nullable analysis is reached only through the per-file `#nullable enable` pragma. + +## Build summary, verbatim + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +NULLABLE-BASELINE-WARNINGS: 0 +NULLABLE-BASELINE-ERRORS: 0 + +These two totals are the baseline that the P1-T10 final nullable gate is compared +against. + +## Compiler-invocation counts read back from the detailed log + +Raw log (gitignored): TestResults/796/p0-t9/nullable-rebuild.log + +CscTaskCount=36 +CscToolCount=36 + +Both counts are greater than zero, so `CoreCompile` ran on every project and the +compiler and nullable-flow diagnostics actually executed. An exit code of 0 with +both counts at zero would have been a FAILED gate. + +## Assembly-freshness corroboration + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T14:10:59.0796692Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T14:11:04.9739118Z | yes | + +Output Summary: /t:Rebuild of TaskMaster.sln with TreatWarningsAsErrors returned +EXIT_CODE 0 with 0 warnings and 0 errors. 36 Csc task invocations and 36 csc.exe +tool invocations recorded, both touched assemblies rebuilt after RunStartedUtc, and +the command line carries no `/p:Nullable=enable` token. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/phase0-instructions-read.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/phase0-instructions-read.md new file mode 100644 index 000000000..de375b19d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/phase0-instructions-read.md @@ -0,0 +1,45 @@ +# Phase 0 — Policy Instructions Read (P0-T1) + +Timestamp: 2026-09-07T14-03 +Task: [P0-T1] +Issue: #796 +Work Mode: full-bug +Branch: bug/quickfiler-folder-dropdown-closes-on-open-796 + +Policy Order: CLAUDE.md, then .claude/rules/general-code-change.md, then +.claude/rules/general-unit-test.md, then .claude/rules/quality-tiers.md, then +.claude/rules/tonality.md, then .claude/rules/csharp.md, then +.claude/rules/plan-acceptance-gates.md. + +## Files read, in the required order + +1. CLAUDE.md — read in full (448 lines). +2. .claude/rules/general-code-change.md — read in full (81 lines). +3. .claude/rules/general-unit-test.md — read in full (106 lines). +4. .claude/rules/quality-tiers.md — read in full (52 lines). +5. .claude/rules/tonality.md — read in full (81 lines). +6. .claude/rules/csharp.md — read in full (97 lines). +7. .claude/rules/plan-acceptance-gates.md — read in full (258 lines). + +All seven paths listed above were read. Every path is repository-relative to the +worktree root and was read from the worktree this plan executes in. + +## Constraints carried forward into execution + +- Toolchain order for C#: format (CSharpier), then analyze (.NET analyzers), then + type-check (nullable), then test. Any failure or auto-fix restarts from format. +- `/t:Rebuild` is mandatory locally for the analyzer and nullable gates. `/t:Build` + can skip `CoreCompile` through MSBuild incrementality and exit 0 without running + analyzers. +- `/p:Nullable=enable` is not used. Nullable enforcement is per-file opt-in through + the `#nullable enable` pragma. +- 500-line ceiling on any production, test, or reusable script file. Markdown + documentation is exempt. +- MSTest as the test framework, Moq for mocking, FluentAssertions for assertions. +- No temporary files in tests. +- Tone policy: professional, factual, neutral. No humor, hyperbole, or decorative + metaphor in any authored content. +- Policy documents under .claude/rules/ must not be modified. + +Output Summary: All seven policy files read in the required order and recorded. +EXIT_CODE: 0 diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md new file mode 100644 index 000000000..92cfb0130 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md @@ -0,0 +1,54 @@ +# Issue #796 — acceptance-criteria status mirror + +Timestamp: 2026-09-07T15-03 +Task: [P8-T9] +Issue: #796 + +POSTING BLOCKED + +Reason: this executor does not post to GitHub. The orchestrator owns all GitHub +interaction for this item, including the issue update and the pull request. The text +below is the exact text intended for the issue and is recorded here so the posting can +be made from it verbatim without re-deriving it. + +The same six checkboxes have been mirrored into the local feature `issue.md`, whose +acceptance-criteria block now matches `spec.md` checkbox for checkbox. + +## Exact text intended for the issue + +All six acceptance criteria for issue #796 are delivered and verified on branch +`bug/quickfiler-folder-dropdown-closes-on-open-796`. + +- [x] AC1: Opening the list by arrow click or by Down in the search box leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem. +- [x] AC2: A deactivation of the QuickFiler form caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (the #677 contract is preserved for genuine deactivation). +- [x] AC3: A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs. +- [x] AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. +- [x] AC5: Row-set refreshes while open (search, late decoration) continue not to close the list (#438 AC-3 regression guard). +- [x] AC6: The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen. + +Evidence, per criterion, under the feature folder +`docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796`: + +| AC | Evidence | +|---|---| +| AC1 | evidence/regression-testing/p7-t3-ac1-ac5-guards.md, plus the fail-before artifact named by the `AC1-FAIL-BEFORE-CARRIER: AC2` line of evidence/other/close-ordering-decision.md, namely evidence/regression-testing/p4-t5-ac2-fail-before.md | +| AC2 | evidence/regression-testing/p4-t5-ac2-fail-before.md, evidence/regression-testing/p4-t9-ac2-pass-after.md | +| AC3 | evidence/regression-testing/p5-t3-ac3-fail-before.md, evidence/regression-testing/p5-t7-ac3-pass-after.md, evidence/regression-testing/p5-t6-scoping-guard.md | +| AC4 | evidence/regression-testing/p6-t4-ac4-fail-before.md, evidence/regression-testing/p6-t6-ac4-pass-after.md, and the deliberate re-pinning of the superseded issue #680 test recorded in evidence/regression-testing/p8-t1-search-dismissal-repin.md and verified green in evidence/regression-testing/p8-t2-search-dismissal-verification.md | +| AC5 | evidence/regression-testing/p7-t3-ac1-ac5-guards.md, evidence/qa-gates/p7-t4-ac5-exclusion.md | +| AC6 | evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md, the human observation artifact evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md, and the derived decision record evidence/other/close-ordering-decision.md | + +AC6 was satisfied first, as the plan's ordering constraint requires: the instrumentation +landed before any behavioural change, a human ran the runbook against a Debug build of +that instrumented commit, and the confirmed first-cause close path recorded in the +decision record is what selected the fixes for AC2 through AC4 rather than an inference. + +## Local mirror + +`docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md` +acceptance-criteria block updated in the same task. Its six checkboxes match the six in +`spec.md`, which is the authoritative acceptance-criteria source for this full-bug item. + +Output Summary: six of six acceptance criteria checked off in spec.md and mirrored into +issue.md; posting to GitHub is the orchestrator's step and has not been performed by this +executor. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md new file mode 100644 index 000000000..e21b6a1e4 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md @@ -0,0 +1,148 @@ +# Manual observation — drop-down close ordering (issue #796, plan task P2-T1) + +Timestamp: 2026-09-07T12-19 +Runbook: docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md +Build SHA under test: ec674e0c +Instrumentation commit: 0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0 +Performed by: repository maintainer (human-interaction exception HI-796-1) +Recorded by: orchestrator, transcribing the maintainer's reported observation verbatim + +## Provenance + +The maintainer executed the runbook end to end against a Debug build produced from branch head +ec674e0c. `0dfcb402` is the parent of `ec674e0c` and is the commit that added the AC6 +instrumentation, so the instrumentation is present in the build that was observed. `ec674e0c` adds +documentation and plan check-offs only and changes no compiled source relative to `0dfcb402`. + +Both required logger names produced lines in the observed segment: + +- `QuickFiler.Controllers.QfcFormController` +- `QuickFiler.Viewers.BreadcrumbDropDownHost` + +The optional third instrumentation site, `QuickFiler.Controllers.QfcItemController` +(`TextBoxSearch_Leave`), was not added, so candidate 3 is not directly observable in this run. AC6 +does not require it. + +Every line under consideration carries the thread name `VSTA_Main`. The runbook's same-thread premise +therefore holds and file order is the ordering. Ordering below is read from file order, not from +millisecond timestamps. + +## Redaction + +The excerpted lines contain no absolute host paths, no user names, no mailbox addresses, and no +folder names identifying a person, so redaction is a no-op on this transcript. Field names, decision- +carrying field values, and line order are intact. + +## Elision notice + +This excerpt is not the complete line set. The runs written as +`ItemNumber=n..m SelectorWasOpen=False` stand in for consecutive per-item +`ParkFocusAndCancelSelectors reached item.` lines that are identical except for the `ItemNumber` +value and the millisecond timestamp. Each elided run is stated explicitly at the position it occupies +in file order, so the ordering of the non-elided lines is unaffected by the elision. No line carrying +`SelectorWasOpen=True`, no `ParkFocusAndCancelSelectors entered.` line, and no `OnDropDownClosed` +line is elided. The maintainer holds the full transcript and can supply every line verbatim on +request. + +## Excerpt, in file order + +``` +--- Gesture A --- +2026-09-07 12:19:29,628 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:19:29,629 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=1 SelectorWasOpen=False +2026-09-07 12:19:29,630 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=2 SelectorWasOpen=True +2026-09-07 12:19:29,639 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +2026-09-07 12:19:29,647 .. 29,653 [VSTA_Main] ParkFocusAndCancelSelectors reached item. ItemNumber=3..9 SelectorWasOpen=False +--- Gesture B --- +2026-09-07 12:23:35,014 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=False Groups=9 +2026-09-07 12:23:35,020 .. 35,021 ItemNumber=1..2 SelectorWasOpen=False +2026-09-07 12:23:35,022 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=3 SelectorWasOpen=True +2026-09-07 12:23:35,028 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +2026-09-07 12:23:35,034 .. 35,039 ItemNumber=4..9 SelectorWasOpen=False +--- Gesture C --- +2026-09-07 12:26:07,437 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:26:07,438 .. 07,440 ItemNumber=1..3 SelectorWasOpen=False +2026-09-07 12:26:07,441 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=4 SelectorWasOpen=True +2026-09-07 12:26:07,443 .. 07,448 ItemNumber=5..9 SelectorWasOpen=False +2026-09-07 12:26:07,460 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=False Disposed=False PendingClose=False +2026-09-07 12:26:10,958 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=True Groups=9 (post-gesture focus move; all SelectorWasOpen=False) +2026-09-07 12:27:10,094 .. 10,197 Cancel teardown (QuickFiler closed by the operator). +``` + +## Blocks that are not gestures + +Two blocks appear in the segment after Gesture C and must not be read as gestures: + +- `12:26:10,958` — a second `ParkFocusAndCancelSelectors` entry with `WebView2Focused=True`, + `ActiveFormNull=True` and no selector open on any item. This is the maintainer's focus moving away + from the form after Gesture C completed. No selector was open, so no cancel occurred and no + `OnDropDownClosed` follows it. +- `12:27:10,094` onward — the ordinary Cancel teardown emitted when the maintainer closed QuickFiler. + +## Per-gesture verdicts + +The decision rules applied are those in the runbook's Verification section. + +### Gesture A — arrow click (12:19:29) + +File order: `ParkFocusAndCancelSelectors entered.` (`WebView2Focused=False`, `ActiveFormNull=False`, +`Groups=9`), then item 2 with `SelectorWasOpen=True`, then `OnDropDownClosed entered.` with +`CloseReason=CloseCalled`, `ProgrammaticClose=True`, `OpenState=False`, `AutoClose=True`. + +- **Candidate 1 — CONFIRMED.** The `ParkFocusAndCancelSelectors` entry line appears before the + `OnDropDownClosed` line, and that close reports `CloseCalled` with `ProgrammaticClose=True`. The + refutation conditions do not apply: the handler was entered, it reported `Groups=9` rather than + `Groups=0`, item 2 reported `SelectorWasOpen=True` so a cancel did occur, and the entry is strictly + before the close. +- **Candidate 2 — REFUTED.** The close reason is `CloseCalled`, not `AppFocusChange` or `AppClicked`, + and `ProgrammaticClose=True` rather than `False`. A `CloseCalled` reason is an explicit refutation + condition for candidate 2. +- **Candidate 3 — NOT DIRECTLY OBSERVABLE.** The optional `TextBoxSearch_Leave` site was not + instrumented. The observed ordering leaves no room for it: the selector on item 2 was already + cancelled by candidate 1 before the close arrived, so a later third close would have nothing left + to close. + +**First cause: candidate 1.** + +### Gesture B — Down key from the search box (12:23:35) + +File order: `ParkFocusAndCancelSelectors entered.` (`WebView2Focused=True`, `ActiveFormNull=False`, +`Groups=9`), then item 3 with `SelectorWasOpen=True`, then `OnDropDownClosed entered.` with +`CloseReason=CloseCalled`, `ProgrammaticClose=True`, `OpenState=False`, `AutoClose=True`. + +- **Candidate 1 — CONFIRMED.** Same shape as Gesture A: entry before close, close reason + `CloseCalled` with `ProgrammaticClose=True`, `Groups=9`, and a cancel actually performed on item 3. +- **Candidate 2 — REFUTED.** Close reason `CloseCalled` with `ProgrammaticClose=True`. +- **Candidate 3 — NOT DIRECTLY OBSERVABLE**, and the ordering leaves no room for it, for the same + reason as Gesture A. + +**First cause: candidate 1.** + +### Gesture C — type, list expands and stays open, then mouse-click a row (12:26:07) + +File order: `ParkFocusAndCancelSelectors entered.` (`WebView2Focused=False`, `ActiveFormNull=False`, +`Groups=9`), then item 4 with `SelectorWasOpen=True`, then the remaining items, then +`OnDropDownClosed entered.` with `CloseReason=CloseCalled`, `ProgrammaticClose=True`, +`OpenState=False`, **`AutoClose=False`**. + +- **Candidate 1 — CONFIRMED, and confirmed for the click-without-select symptom specifically.** + Clicking a row inside the popup deactivated the QuickFiler form, and the deactivation cancel ran + before the row's selection could commit. The entry precedes the close, the close reports + `CloseCalled` with `ProgrammaticClose=True`, `Groups=9`, and item 4 reported `SelectorWasOpen=True` + so a cancel occurred. +- **Candidate 2 — REFUTED.** Twice over: the close reason is `CloseCalled` rather than + `AppFocusChange` or `AppClicked`, and `AutoClose=False` at the moment it fired, which is itself an + explicit refutation condition for candidate 2. +- **Candidate 3 — NOT DIRECTLY OBSERVABLE**, and the ordering leaves no room for it: the selector was + already cancelled by candidate 1 before the close. + +**First cause: candidate 1.** + +## Conclusion carried forward to Phase 3 + +The form-deactivation cancel in `QfcFormController.ParkFocusAndCancelSelectors` (the issue #677 +behaviour) fires first on all three gestures. Candidate 2 is refuted on all three. Candidate 3 is not +directly observable and the observed ordering leaves no room for it on any gesture. + +The three gestures agree; no gesture is inconclusive. This artifact replaces the INFERRED Win32 +activation-ordering label in `spec.md` with an observation, which is what AC6 requires. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md new file mode 100644 index 000000000..25e1a2b5b --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md @@ -0,0 +1,401 @@ +# Close-ordering decision record (issue #796, plan Phase 3) + +Timestamp: 2026-09-07T13-47 +Issue: #796 +Work Mode: full-bug + +## Sole evidence source + +Every decision in this record is derived from the quoted content of: + +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md + +verified conformant at: + +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/p2-t2-manual-observation-conformance.md + +No decision below is taken from the plan's expectation, from the research artifact's +prediction, or from any summary of the observation. Where the observation contradicts a +prediction, the observation is followed and the contradiction is recorded explicitly. + +The three candidate close paths carry the numbering used by the runbook and the issue: + +1. `QfcFormController.ParkFocusAndCancelSelectors` cancelling every item's selector when + the QuickFiler form loses activation. +2. Native `ToolStripDropDown` auto-close reaching `BreadcrumbDropDownHost.OnDropDownClosed` + and then `FinishClose` with an `Uncommitted` reason. +3. `QfcItemController.TextBoxSearch_Leave` closing the drop-down when the search box loses + focus. + +--- + +## [P3-T1] Per-gesture first cause + +FIRST-CAUSE-GESTURE-A: CANDIDATE-1 + +Gesture A is the arrow click. Derived from this excerpt, quoted in file order: + +``` +2026-09-07 12:19:29,628 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:19:29,630 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=2 SelectorWasOpen=True +2026-09-07 12:19:29,639 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +``` + +and from the verdict the observation states for it: "**First cause: candidate 1.**" + +The deactivation entry line precedes the close line in file order; the close reports +`CloseReason=CloseCalled` with `ProgrammaticClose=True`; `Groups=9` rather than `Groups=0`; +and item 2 reports `SelectorWasOpen=True`, so a cancel actually occurred. Every confirm +condition is met and no refute condition applies. + +Refutation status of the other two candidates for Gesture A: + +- Candidate 2 — REFUTED. Quoted basis: "**Candidate 2 — REFUTED.** The close reason is + `CloseCalled`, not `AppFocusChange` or `AppClicked`, and `ProgrammaticClose=True` rather + than `False`. A `CloseCalled` reason is an explicit refutation condition for candidate 2." +- Candidate 3 — NOT DIRECTLY OBSERVABLE, and not refuted. Quoted basis: "**Candidate 3 — + NOT DIRECTLY OBSERVABLE.** The optional `TextBoxSearch_Leave` site was not instrumented. + The observed ordering leaves no room for it: the selector on item 2 was already cancelled + by candidate 1 before the close arrived, so a later third close would have nothing left to + close." The status recorded here is the observation's own wording, not a refutation. + +FIRST-CAUSE-GESTURE-B: CANDIDATE-1 + +Gesture B is the Down key pressed from the search box. Derived from this excerpt, quoted in +file order: + +``` +2026-09-07 12:23:35,014 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=False Groups=9 +2026-09-07 12:23:35,022 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=3 SelectorWasOpen=True +2026-09-07 12:23:35,028 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +``` + +and from the verdict the observation states for it: "**First cause: candidate 1.**" + +Refutation status of the other two candidates for Gesture B: + +- Candidate 2 — REFUTED. Quoted basis: "**Candidate 2 — REFUTED.** Close reason + `CloseCalled` with `ProgrammaticClose=True`." +- Candidate 3 — NOT DIRECTLY OBSERVABLE, and not refuted. Quoted basis: "**Candidate 3 — + NOT DIRECTLY OBSERVABLE**, and the ordering leaves no room for it, for the same reason as + Gesture A." + +FIRST-CAUSE-GESTURE-C: CANDIDATE-1 + +Gesture C is typing, the list expanding and staying open, then a mouse click on a row. +Derived from this excerpt, quoted in file order: + +``` +2026-09-07 12:26:07,437 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:26:07,441 [VSTA_Main] DEBUG QuickFiler.Controllers.QfcFormController [(null)] - Issue #796: QfcFormController.ParkFocusAndCancelSelectors reached item. ItemNumber=4 SelectorWasOpen=True +2026-09-07 12:26:07,460 [VSTA_Main] DEBUG QuickFiler.Viewers.BreadcrumbDropDownHost [(null)] - Issue #796: BreadcrumbDropDownHost.OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=False Disposed=False PendingClose=False +``` + +and from the verdict the observation states for it: "**Candidate 1 — CONFIRMED, and +confirmed for the click-without-select symptom specifically.** Clicking a row inside the +popup deactivated the QuickFiler form, and the deactivation cancel ran before the row's +selection could commit." followed by "**First cause: candidate 1.**" + +Refutation status of the other two candidates for Gesture C: + +- Candidate 2 — REFUTED, on two independent grounds. Quoted basis: "**Candidate 2 — + REFUTED.** Twice over: the close reason is `CloseCalled` rather than `AppFocusChange` or + `AppClicked`, and `AutoClose=False` at the moment it fired, which is itself an explicit + refutation condition for candidate 2." +- Candidate 3 — NOT DIRECTLY OBSERVABLE, and not refuted. Quoted basis: "**Candidate 3 — + NOT DIRECTLY OBSERVABLE**, and the ordering leaves no room for it: the selector was + already cancelled by candidate 1 before the close." + +### Note on the two blocks that are not gestures + +The observation records two further blocks in the same segment and states that neither is a +gesture: the `12:26:10,958` entry, which it reads as "the maintainer's focus moving away +from the form after Gesture C completed", and the `12:27:10,094` onward Cancel teardown +"emitted when the maintainer closed QuickFiler". Neither contributes to the three lines +above. The first block is used once, further down, as the record's only observed instance of +a deactivation the observation does not attribute to this add-in's own popup. + +--- + +## [P3-T2] AC1 fail-before carrier + +AC1-FAIL-BEFORE-CARRIER: AC2 + +Derivation is available and the halt branch is not taken: none of the three per-gesture +first-cause lines above reads INCONCLUSIVE, so the condition that would require the +undecidable value and a return to the human is not met. + +Lines derived from: the first-cause line for Gesture A and the first-cause line for +Gesture B recorded in the section above, corroborated by the first-cause line for Gesture C. +Gestures A and B are precisely the two gestures AC1 names — "Opening the list by arrow click +or by Down in the search box leaves it open" — so they are the lines that decide which +criterion carries the AC1 fail-before regression test. Both read CANDIDATE-1. Gesture C +reads CANDIDATE-1 as well, so the assignment does not depend on which of the three lines is +weighted. + +Mapping from the confirmed first cause to the criterion that carries the test: + +- Candidate 1 is `QfcFormController.ParkFocusAndCancelSelectors` cancelling every item's + selector on form deactivation. The criterion whose fix addresses that path is AC2, the + self-inflicted deactivation seam. +- Candidate 2 is the native auto-close reaching `FinishClose` with an `Uncommitted` reason. + The criterion whose fix addresses that path is AC3. Candidate 2 is refuted on all three + gestures, so AC3 does not carry the fail-before test. + +Supporting excerpt, quoted from the observation's Conclusion section: + +> The form-deactivation cancel in `QfcFormController.ParkFocusAndCancelSelectors` (the issue +> #677 behaviour) fires first on all three gestures. Candidate 2 is refuted on all three. + +This assignment was made from the recorded first-cause findings and not from the research +artifact's expectation. The research artifact records only that both mechanisms will very +likely be needed and that the log decides which is first; it does not itself decide, and its +prediction was not used as an input here. + +--- + +## [P3-T3] AC3 mechanism decisions + +AC3-ENFORCEMENT-SITE: HOST + +Derived from the `PendingClose=` and `ProgrammaticClose=` fields on the close line of every +gesture. Quoted, one per gesture, in file order: + +``` +2026-09-07 12:19:29,639 ... OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +2026-09-07 12:23:35,028 ... OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=True Disposed=False PendingClose=False +2026-09-07 12:26:07,460 ... OnDropDownClosed entered. CloseReason=CloseCalled ProgrammaticClose=True OpenState=False AutoClose=False Disposed=False PendingClose=False +``` + +Two observed field values decide the site. + +1. `PendingClose=False` on all three gestures. That field reports the open-lifetime member + `IsPendingClose`, which is the state the open coordinator consults when a close intent + arrives while an open is still in flight. It is false at the moment of every observed + close, so on every gesture the open had already completed and the coordinator held no + in-flight open for the close to race. A latch placed in the coordinator would therefore + sit on a path the observation shows was not taken, and it could not intercept any of the + three observed closes. +2. `ProgrammaticClose=True` with `CloseReason=CloseCalled` on all three gestures. Every + observed close arrived at the host's own handler as a close the add-in itself initiated, + downstream of the cancel. The handler that observes it, and the completion point that + decides whether to cancel, are both in the host. + +The enforcement site is therefore the host, meaning `FinishClose` in +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs`. This decision leaves +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` with no required change; it +remains in the write set as a bound, not as an obligation. + +AC3-HTML-POINTERDOWN: NOT REQUIRED + +The value REQUIRED is admissible only when the Gesture C transcript shows that no activation +message was produced. It does not, and it cannot: the transcript is silent on activation +altogether, and its silence carries no information. + +The reason is which sites were instrumented. The observation states it directly: + +> Both required logger names produced lines in the observed segment: +> +> - `QuickFiler.Controllers.QfcFormController` +> - `QuickFiler.Viewers.BreadcrumbDropDownHost` + +An activation message is the `selectorActivate` post from the breadcrumb page, handled by +the bridge coordinator and the router before it reaches the selection session. Neither of +those types is among the loggers that produced lines, and neither was instrumented at all, +so no activation would have produced a transcript line whether one was posted or not. An +absent line is therefore consistent with an activation that occurred and with an activation +that did not, and it discriminates between them not at all. Reading the absence as proof of +absence would be exactly the inference this observation exists to replace. + +Because the admissibility condition for REQUIRED cannot be met by this transcript, the value +recorded is the other admitted one. That is not merely the residual choice; the observation +supplies a positive account of the Gesture C symptom that needs no change to the page: + +> **Candidate 1 — CONFIRMED, and confirmed for the click-without-select symptom +> specifically.** Clicking a row inside the popup deactivated the QuickFiler form, and the +> deactivation cancel ran before the row's selection could commit. + +The observed cause of a row click failing to select is the deactivation cancel running +first, which is the path AC2 closes. Moving the row listener from `click` to a pointer-down +event is a change to the page whose only justification in the plan is a demonstrated absence +of the activation message, and that demonstration does not exist. The page +`QuickFiler/Resources/FolderBreadcrumb.html` is therefore not changed by this item, and the +sibling contention recorded against it does not need to be exercised. + +Recorded limitation, so a later reader does not mistake this for a settled negative: this +decision states that the evidence does not support the page change, not that the page change +has been shown unnecessary. If the AC2 seam lands and a row click still fails to select, the +question is reopened, and settling it then requires instrumenting the activation path rather +than re-reading this transcript. + +--- + +## [P3-T4] AC2 mechanism decisions + +AC2-PARK-FOCUS-SUPPRESSED: NO + +This is an explicit decision, taken from the evidence, not a default. The parking behaviour +does not change, and `FormDeactivated_WebView2Focused_ParksFocusOnce` at +QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs line 134 is not modified. + +Derived from the `WebView2Focused=` field on each gesture's entry line. Quoted, in file +order: + +``` +2026-09-07 12:19:29,628 ... ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:23:35,014 ... ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=False Groups=9 +2026-09-07 12:26:07,437 ... ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +``` + +That field reports the same expression that gates the parking call, so the field decides +whether parking ran. On Gesture A and on Gesture C it is False, so focus was not parked at +all on those two gestures. Yet both gestures still cancelled an open selector — Gesture A on +item 2 and Gesture C on item 4, each reported as `SelectorWasOpen=True` — and both still +reached the close. Suppressing a step that did not execute cannot alter either outcome, so +the observation supplies no case in which suppressing parking would have prevented the +defect. Gesture B is the one gesture where parking did run, and its close is the same shape +as the other two, so parking is not what distinguishes it either. + +The cancel loop is thus shown to be independent of the parking step: the cancel is what +closes the selector, on gestures where parking runs and on gestures where it does not. The +AC2 seam therefore gates the cancel loop only, and the paired negative test that the plan's +expect-fail inventory holds conditional on this decision is not brought into scope. + +Contradiction recorded against the research artifact, because the observed values are the +reverse of its source-derived expectation and a later reader should not assume a +transcription error. Research section 3.2 derives that the parking chain "is therefore live +only when a WebView2 is the active leaf — which is the arrow-click case", and that on the +search path the leaf is a `TextBox` so parking is skipped. Observation reverses both: the +arrow-click gesture reports `WebView2Focused=False` and the Down-key-from-the-search-box +gesture reports `WebView2Focused=True`. The decision above does not depend on which gesture +carries which value — it depends only on parking having been skipped on two gestures that +nevertheless exhibited the defect — but the reversal is load-bearing for the reachability +question recorded further down and is stated here once. + +AC2-ITEMVIEWER-WIRING: REQUIRED + +Derived from the `ActiveFormNull=` field, compared across the three gestures and the one +non-gesture block the observation identifies as a deactivation it does not attribute to this +add-in's own popup. Quoted, in file order: + +``` +2026-09-07 12:19:29,628 ... ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:23:35,014 ... ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=False Groups=9 +2026-09-07 12:26:07,437 ... ParkFocusAndCancelSelectors entered. WebView2Focused=False ActiveFormNull=False Groups=9 +2026-09-07 12:26:10,958 ... ParkFocusAndCancelSelectors entered. WebView2Focused=True ActiveFormNull=True Groups=9 (post-gesture focus move; all SelectorWasOpen=False) +``` + +and from the observation's reading of the fourth line: + +> `12:26:10,958` — a second `ParkFocusAndCancelSelectors` entry with `WebView2Focused=True`, +> `ActiveFormNull=True` and no selector open on any item. This is the maintainer's focus +> moving away from the form after Gesture C completed. + +The field reports whether `Form.ActiveForm` was null at entry. The spec and the research +artifact both proposed it as a low-cost discriminator, on the reasoning that a +`ToolStripDropDown` is not a `Form`, so a self-inflicted deactivation would show a null +active form and a genuine one a non-null active form. The observation refutes that +discriminator. All three self-inflicted gestures report `ActiveFormNull=False`, and the one +block the observation attributes to focus moving away from the form reports +`ActiveFormNull=True`. The values are not merely uninformative; they run opposite to the +predicted direction on all four observations. + +The consequence for the AC2 seam is direct. The seam's planned implementation was a member +on the form-viewer interface implemented in the concrete form viewer "as the only site that +reads non-injectable activation state", and `Form.ActiveForm` was the activation state it +was to read. That reading is refuted, so the concrete form viewer has no observed signal +from which to derive self-inflicted-versus-genuine on its own. The value must instead be +supplied by the code that knows the popup is being opened, which is the wiring in +`QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` that already assigns +`host.MayTakeFocus = MayRestoreBreadcrumbFocus;` at line 212. A popup-owns-activation +assignment beside that existing assignment is therefore required rather than optional. + +The refuted discriminator is not inverted and re-used. One observation of a single genuine +deactivation is too thin a basis on which to depend on the opposite of a framework heuristic +that has already been observed to behave contrary to its documented rationale, and an +explicitly assigned state does not depend on the heuristic at all. + +--- + +## [P3-T5] AC4 mechanism decision + +AC4-MECHANISM: SearchOwnedDismissalLatch + +AC4-NEW-MEMBER: REQUIRED + +### Observed reachability of the leave handler + +The record is required to state whether the Gesture A and Gesture C transcripts show a +`TextBoxSearch_Leave` entry at all. They do not. Neither transcript contains any such entry, +and neither does the Gesture B transcript. + +That absence supports no conclusion about whether the handler ran. The observation states +why: + +> The optional third instrumentation site, `QuickFiler.Controllers.QfcItemController` +> (`TextBoxSearch_Leave`), was not added, so candidate 3 is not directly observable in this +> run. AC6 does not require it. + +A handler carrying no logging site emits nothing whether it executes or not, so the missing +entry is equally consistent with a handler that ran and a handler that did not. What the +absence establishes is only that the transcript is silent. It is not evidence that the +handler was unreached, and it must not be read as corroborating research section 3.2's +source-derived prediction that candidate 3 is not reached on either reproduction path. That +prediction remains a derivation from source and is untested by this run. + +The observation's own indirect statement is recorded here in its own terms rather than +strengthened. For each gesture it states that candidate 3 is "NOT DIRECTLY OBSERVABLE" and +that the observed ordering "leaves no room for it", on the ground that the selector was +already cancelled by candidate 1 before the close arrived. That argument bears on whether a +third close could have been the FIRST cause. It does not bear on whether the leave handler +runs at all, which is the question AC4 addresses. + +One observed value weakens the source-derived prediction rather than supporting it. Research +section 3.2's reachability argument turns on which gesture has a WebView2 as the active +leaf, because the parking assignment is what can synchronously raise the search box's +`Leave`. It predicts a WebView2 leaf on the arrow-click path and a `TextBox` leaf on the +keyboard path. Observation reverses that on both gestures, as recorded in the AC2 section +above. The prediction's premise is therefore observed to be wrong in the specific respect its +conclusion depends on, so the AC4 gap must be treated as open and closed on its own terms. + +### Mechanism + +The gap AC4 names is that `TextBoxSearch_Leave` takes dismissal ownership of the drop-down +regardless of which gesture opened it. The existing issue #680 latch +`_searchLeaveHandoffPending` is one-shot and read-and-cleared, and its single producer is the +`Keys.Down` branch, so it covers exactly one leave on one gesture path and cannot express +ownership over a popup's lifetime. + +The mechanism recorded above is a provenance latch: a lifetime flag recording that the +currently open drop-down is one this controller opened from the search box, set by the two +search-driven open sites and consulted by the leave handler, which dismisses only when the +flag is set. A mouse-driven open never sets it, so the leave no longer dismisses a popup the +mouse opened, and the existing one-shot handoff latch keeps its present meaning unchanged. + +The mechanism is confined to the single production file the write set allows for AC4, +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs`. All three sites it touches are +declared in that file: the search-typing open path `TextBoxSearch_TextChanged` at line 173, +the Down-arrow open path `TextBoxSearch_KeyDown` at line 190, and the consumer +`TextBoxSearch_Leave` at lines 217-228. No other production file participates, no interface +changes, and the mouse open path is not modified — it is covered by not setting the flag, +which requires no edit anywhere outside this file. + +The mechanism is chosen so that it does not depend on the leave handler's reachability, which +this run leaves unobserved. It is correct whether or not the handler executes on any given +gesture: if it never runs, the flag is never read and nothing changes; if it runs, it +dismisses only a popup the search box owns. + +### Why a new member is required + +The existing `_searchLeaveHandoffPending` field cannot carry this meaning. It is consumed +destructively on its first read, by design — its in-source comment records the read-and-clear +as the mechanism by which "the Down-arrow handoff's own Leave is consumed exactly once" — so +it is false again immediately after the handoff it guards, while the popup is still open. +Provenance must persist for as long as the popup is open, which is a different lifetime. +Overloading the one-shot field would break the #680 contract that the plan requires be +preserved. + +The new member is therefore one additional private boolean field on the concrete +`QfcItemController`, declared in the same file beside the existing latch. It is a private +field on an internal partial class, so it changes no interface and no public surface, and it +disturbs no implementor anywhere in the tree. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/p2-t2-manual-observation-conformance.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/p2-t2-manual-observation-conformance.md new file mode 100644 index 000000000..5c75edfde --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/p2-t2-manual-observation-conformance.md @@ -0,0 +1,134 @@ +# P2-T2 — Conformance verification of the manual observation artifact + +Timestamp: 2026-09-07T13-47 +Task: [P2-T2] +Issue: #796 +Channel used: A + +## Source artifact + +Path: docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md + +That is the only file under the feature's evidence/other/ directory whose filename +begins with an ISO-8601 `yyyy-MM-ddTHH-mm` timestamp. The directory holds three other +files (preflight-round-1-delta.md, preflight-round-1-delta-b3-adjudication.md and +preflight-round-2-delta.md), none of which begins with a timestamp, so the P2-T1 +"exactly one such artifact" condition is met. + +## Checks P2-T2 names + +### Check 1 — lines from both required logger names appear in the transcript + +The runbook's Prerequisites section names two required logger names. Both produce +lines in the transcribed excerpt, and the excerpt shows the logger name inline on each +non-elided line. + +- `QuickFiler.Controllers.QfcFormController` — present on the `ParkFocusAndCancelSelectors` + entry line of every gesture and on the per-item lines. Non-elided instances appear in + the Gesture A, Gesture B and Gesture C blocks. +- `QuickFiler.Viewers.BreadcrumbDropDownHost` — present on the `OnDropDownClosed entered.` + line of every gesture, one per gesture. + +RESULT: PASS. Both required logger names are represented, and each is represented within +each of the three gesture blocks, so neither of the runbook's two single-site +inconclusive conditions is triggered. + +Corroboration that the observed build was the instrumented build: the runbook records +that `QuickFiler.Viewers.BreadcrumbDropDownHost` emits nothing at all before the AC6 +instrumentation lands, because that type declares no logger. Lines from that logger are +present, so the running build carried the instrumentation. + +### Check 2 — all lines under consideration carry the same thread name + +The source artifact states that every line under consideration carries the thread name +`VSTA_Main`, and every non-elided excerpt line displays `[VSTA_Main]` inline. The elided +runs are declared in the artifact's Elision notice as consecutive per-item +`ParkFocusAndCancelSelectors reached item.` lines differing only in `ItemNumber` and the +millisecond timestamp, and two of the four elided runs additionally display +`[VSTA_Main]` inline. + +RESULT: PASS. The runbook's same-thread premise holds, so file order is the ordering and +the ordering claim is not required to be reported as inconclusive on thread grounds. + +### Check 3 — a per-gesture confirmed-or-refuted statement for every one of the three gestures + +The artifact's `## Per-gesture verdicts` section carries one subsection per gesture, each +stating a status for all three candidates and a single first cause. + +| Gesture | Candidate 1 | Candidate 2 | Candidate 3 | First cause stated | +|---|---|---|---|---| +| A — arrow click | CONFIRMED | REFUTED | NOT DIRECTLY OBSERVABLE | candidate 1 | +| B — Down key from search box | CONFIRMED | REFUTED | NOT DIRECTLY OBSERVABLE | candidate 1 | +| C — type then mouse-click a row | CONFIRMED | REFUTED | NOT DIRECTLY OBSERVABLE | candidate 1 | + +RESULT: PASS. All three gestures carry an explicit statement. `NOT DIRECTLY OBSERVABLE` +for candidate 3 is the status the runbook's Verification section prescribes when the +optional third instrumentation site was not added, and the artifact additionally states, +per gesture, whether the observed ordering leaves room for a third close, which is the +second half of that same prescription. + +## Additional P2-T1 shape conditions re-checked here + +- `Timestamp:` field present, value `2026-09-07T12-19`. +- Commit SHA of the build recorded: `ec674e0c`, with the instrumentation commit + `0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0` recorded alongside it. +- Excerpts for Gesture A, Gesture B and Gesture C present in file order, in one fenced + block carrying the three gesture separators, with an explicit statement that ordering + is read from file order rather than from millisecond timestamps. +- Redaction statement present; the transcript carries no absolute host path, no user + name, no machine name and no mailbox address. + +## Build-identity equivalence, verified rather than accepted + +P2-T1 requires the build to be produced from the commit recorded in +evidence/qa-gates/p1-t15-instrumentation-commit.md, which records +`0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0`. The artifact records the build as `ec674e0c` +and asserts that `0dfcb402` is its parent and that `ec674e0c` changes no compiled source. +Both halves of that assertion were verified against the repository rather than taken on +trust. + +Command: `git log --oneline -8` + +Observed: `ec674e0c` is listed immediately above `0dfcb402`, so `0dfcb402` is its parent. + +Command: `git diff --name-only 0dfcb402 ec674e0c` + +Observed output, both paths and no others: + +``` +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t15-instrumentation-commit.md +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +``` + +EXIT_CODE: 0 + +No .cs file, no project file and no resource file appears, so the compiled source of the +observed build is identical to that of the recorded instrumentation commit. + +## Recorded limitations that do not affect conformance + +Two properties of the artifact are recorded here so that a later reader does not mistake +either for an unreported gap. Neither is a P2-T2 check and neither changes the result. + +1. **Elision.** The excerpt is not the complete line set. Runs of consecutive per-item + lines are collapsed into a single summarising line placed at the position that run + occupies in file order. The artifact states that no line carrying + `SelectorWasOpen=True`, no `ParkFocusAndCancelSelectors entered.` line and no + `OnDropDownClosed` line is elided, so every line the runbook's decision rules read is + present verbatim, and the relative order of the non-elided lines is unaffected. +2. **Candidate 3 is not directly observable.** The optional third instrumentation site at + `QfcItemController.TextBoxSearch_Leave` was not added. AC6 does not require it and the + runbook explicitly admits its absence. The consequence is carried forward to Phase 3 + rather than resolved here: the transcript can support no claim about whether that + handler ran, because a handler with no logging site produces no line whether it ran or + not. + +## Result + +MANUAL-OBSERVATION: CONFORMANT + +Output Summary: The manual observation artifact at +evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md satisfies all three +P2-T2 checks and all five P2-T1 shape conditions. Both required logger names appear, every +line under consideration carries the thread name `VSTA_Main`, and all three gestures carry +an explicit per-candidate confirmed-or-refuted statement. Execution proceeds to Phase 3. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/phase7-blocking-finding-out-of-write-set-test.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/phase7-blocking-finding-out-of-write-set-test.md new file mode 100644 index 000000000..24e433683 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/phase7-blocking-finding-out-of-write-set-test.md @@ -0,0 +1,112 @@ +# Blocking finding at the end of Phase 7 — a pre-existing test outside the write set now fails + +Timestamp: 2026-09-07T14-39 +Raised by: the executor running plan Phases 4 through 7 +Issue: #796 +Status: BLOCKING for the whole-assembly gate at task P9-T5. Not blocking for any gate in Phases 4 +through 7, all of which passed. + +## What was observed + +A whole-assembly run of QuickFiler.Test, executed as a final toolchain pass after task P7-T4 and +NOT as a plan task, reports one failing test. + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" /ResultsDirectory:TestResults\796\p7-full "/Logger:trx;LogFileName=p7-full.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 1 + +``` +Total tests: 1380 + Passed: 1379 + Failed: 1 +``` + +The single failure: + +``` +Failed TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent +Moq.MockException: +Expected invocation on the mock once, but was 0 times: v => v.SetFolderDroppedDown(False) +Performed invocations: + Mock (v): + IItemViewer.IsFolderDropDownOpen +``` + +It is declared in QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs. + +A scoped re-run of that one class confirms the failure is isolated: + +``` +Total tests: 6 + Passed: 5 + Failed: 1 +``` + +## Why it fails + +The test arranges a viewer reporting `IsFolderDropDownOpen == true` and no search-driven open, then +raises the search box's `Leave` and asserts `SetFolderDroppedDown(false)` exactly once. That +arrangement is precisely the mouse-driven-open state, and asserting that the leave dismisses it is +asserting the behaviour AC4 exists to remove: + +> AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. + +The AC4 fix landed at task P6-T5 makes `TextBoxSearch_Leave` dismiss only a popup the search box +itself opened. This test therefore now pins the defect rather than the contract, and it fails for +the intended reason rather than through a defect in the fix. + +## Why the issue #680 contract is nevertheless intact + +The two sibling tests in the same class that pin #680 both PASS after the fix: + +| Test | Result | +|---|---| +| TextBoxSearchLeave_AfterDownArrowHandoff_SuppressesExactlyOneClose | Passed | +| TextBoxSearchKeyDown_DownArrow_StillOpensAndFocusesTheDropDown | Passed | +| TextBoxSearchKeyDown_EscapeWhileDropDownOpen_RoutesExactlyOneCloseIntent | Passed | +| TextBoxSearchKeyDown_EscapeWhileDropDownClosed_RoutesNoIntentAndLeavesKeyUnhandled | Passed | +| TextBoxSearchLeave_WhileDropDownClosed_RoutesNoIntent | Passed | + +The Down-arrow handoff test passes because the Down-arrow branch is one of the two search-driven +open sites and therefore takes dismissal ownership: its first leave is still consumed by the +one-shot handoff latch and its second leave still dismisses. + +## Why the executor did not resolve it + +Two constraints forbid every available workaround, and neither may be relaxed by the executor: + +1. QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs is NOT one of the sixteen + write-set paths recorded in evidence/baseline/p0-t14-scope-baseline.md. Editing it would breach + the scope boundary that task P7-T4 and the Phase 9 scope gate both enforce. +2. Narrowing the AC4 fix so this test passes would mean not delivering AC4, because the state the + test arranges is exactly the state AC4 requires to stop being dismissed. That is weakening a + test's subject to make a gate pass. + +## Required plan delta + +The plan needs a Phase 8 or Phase 9 task, and a seventeenth write-set path, before task P9-T5 can +pass. Proposed shape, for `atomic-planner` to author properly: + +- Add `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` to the write set under + "Test — modify", raising the count from sixteen to seventeen, and update the matching count + statements in this plan and in spec.md's `## Write Set` section. +- Add a task that performs a DELIBERATE UPDATE of + TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent: keep the method name, keep the + `Times.Once()` assertion, and add one Arrange line driving a search-driven open before the leave, + so the test pins the same contract for the case that still holds. Add a paired negative test + asserting `Times.Never()` for the mouse-driven case, or record that + SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown in + QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs already provides it. +- Update the P7-T4 and Phase 9 scope gates to admit the seventeenth path. + +The whole-assembly baseline recorded at task P0-T10 is the reference for whether this is the only +such test; this finding reports the one failure that run surfaced and asserts nothing about tests +that run did not exercise. + +Output Summary: 1380 tests, 1379 passed, 1 failed; the one failure is a pre-existing test outside +the write set that pins the behaviour AC4 removes; resolving it requires a plan delta adding a +seventeenth write-set path, which the executor may not author on its own authority. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta-b3-adjudication.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta-b3-adjudication.md new file mode 100644 index 000000000..0c8f84b2d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta-b3-adjudication.md @@ -0,0 +1,177 @@ +# B3 scope adjudication — issue #796 + +- Timestamp: 2026-09-07T04:05 +- Raised by: atomic-planner, revision round 1, as `SELF-REVIEW: BLOCKED` +- Decided by: orchestrator +- Supersedes: Delta 3a, 3b and 3c of evidence/other/preflight-round-1-delta.md + +## What the planner found, and it is correct + +Preflight round 1 defect B3 established that the per-item `SelectorWasOpen` value the AC6 diagnostic +must report has no source reachable from the write set. The orchestrator adjudicated that by adding +`QuickFiler/Interfaces/IQfcItemController.cs` as a seventeenth write-set path and declaring a +get-only member on it. + +Applying that adjudication, the planner found it incomplete rather than wrong, and refused to hand +off. Adding a member to that interface breaks a compiled hand-written implementor that lies outside +the write set: + +- `QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs` line 337 declares + `private sealed class FakeQfcItemController : IQfcItemController` with concrete members for the + whole interface. +- That file is compiled: `QuickFiler.Test/QuickFiler.Test.csproj` line 217 carries its + `Compile Include` entry. +- The target is .NET Framework 4.8, so a default interface member is not available as an escape. + +The result is CS0535, and P1-T4's acceptance clause requiring the solution to compile is +unsatisfiable inside the seventeen-path write set. + +Both facts were independently re-verified by the orchestrator before this ruling: the +`FakeQfcItemController` declaration is at that line in that file, and it is the only implementor of +that interface in the test project. + +The planner also correctly established that `QuickFiler/Legacy/QfcController.cs` line 20 declares the +same interface but is NOT compiled, because `QuickFiler/QuickFiler.csproj` carries no `Compile +Include` entry matching Legacy. That one is therefore not a problem. + +Refusing to hand off rather than silently widening the write set was the correct call. A backticked +path is a write claim feeding blast-radius scheduling against three concurrently prepared sibling +items, and that decision was reserved to the orchestrator. + +## Ruling: take neither of the two options offered; take a third + +The planner offered three resolutions. The ruling rejects the first, adopts the second, and records +why the third stays rejected. + +### Rejected — add an eighteenth path for the fake implementor + +The proposal was to add `QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs` to the write set and +give the fake a get-only auto-property. It is rejected for two independent reasons. + +First, that path contains a space. Blast-radius derivation splits on whitespace, so the token would +be split into two fragments and the write claim would be silently lost rather than recorded. This +item would then be schedulable concurrently with any sibling that edits that file, which is the exact +failure the write-set discipline exists to prevent. A path containing a space can only be carried by +also stating it in prose for a human reader, which is a weaker guarantee than the extractor provides +for every other path. + +Second, it widens the diff for a debug-log field. The interface change was never the goal; the value +was. + +### ADOPTED — reach the value through the concrete controller, changing no interface + +The write set returns to SIXTEEN paths. `QuickFiler/Interfaces/IQfcItemController.cs` is removed from +it and is now named in the exclusion paragraph, unbackticked, alongside +QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs. + +The mechanism, verified against the tree by the orchestrator: + +- `QfcItemController` is declared `internal partial class QfcItemController` across eleven parts, one + of which is `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` at line 25. That file is + already a write-set path. +- `QfcItemGroup.ItemController` is declared `internal IQfcItemController ItemController` at + `QuickFiler/Controllers/QfcItemGroup.cs` line 39, in the same assembly. +- `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is in that same assembly and is already a + write-set path, so a cast from the interface to the concrete internal type compiles there. +- The expression `_itemViewer.IsFolderDropDownOpen` is ALREADY USED in + `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, at lines 200 and 225. The new member + introduces no new dependency and no new field access; it names an expression the file already + evaluates. + +So the value is reachable with two edits, both inside the existing sixteen paths, and no interface +changes. + +### Still rejected — drop `SelectorWasOpen` and amend the runbook + +This was rejected in the round-1 adjudication and stays rejected. The per-item open state is what +distinguishes a cancel that hit an open selector from a cancel that was a no-op. The cancel runs +unconditionally on every item today, so a cancel count alone carries no such information, and +candidate 1's refute rule depends on the distinction. + +## Replacement text + +### Delta 3a-R — replaces Delta 3a in full + +Revert the write set to sixteen paths. `QuickFiler/Interfaces/IQfcItemController.cs` is NOT a +write-set path. Every place in the plan that Delta 3a changed to say "seventeen write-set paths" — +the `## Write Set` introductory sentence, the P0-T14 acceptance, the P9-T10 acceptance, and the +structural self-check paragraph — reverts to "sixteen write-set paths". + +Add both of these to the plan's "Explicitly not in the write set" paragraph, WITHOUT backticks, +matching the form that paragraph already uses: +QuickFiler/Interfaces/IQfcItemController.cs, QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs + +Add this sentence to that same paragraph: + +> QuickFiler/Interfaces/IQfcItemController.cs is excluded deliberately rather than by omission: +> preflight round 1 proposed adding a member to it, and the planner established that doing so breaks +> the compiled hand-written implementor FakeQfcItemController in +> QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs with CS0535, because the target framework has +> no default interface members. The adopted resolution reaches the same value through an internal +> member on the concrete controller instead, so neither file is edited. + +The spec.md half of this revert has already been applied by the orchestrator. spec.md now states +sixteen paths, records the withdrawn seventeenth and why, folds the AC6 obligation into the +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` entry, and names both excluded files +unbackticked. Do not edit spec.md. + +### Delta 3b-R — replaces Delta 3b in full + +Replace the second sentence of [P1-T4] with: + +> Add the pure method `internal static string FormatDeactivationDiagnostics(bool webView2Focused, +> bool activeFormIsNull, int groupCount)` returning a single interpolated line containing the labels +> `WebView2Focused=`, `ActiveFormNull=` and `Groups=`, and the pure method `internal static string +> FormatItemCancelDiagnostics(int itemNumber, bool selectorWasOpen)` returning a single line +> containing the labels `ItemNumber=` and `SelectorWasOpen=`. The `selectorWasOpen` value has no +> source on the item-controller INTERFACE, which declares `ItemNumber` and +> `CancelBreadcrumbSelector()` and no selector-open state and no viewer accessor. It is not obtained +> by changing that interface: adding a member there breaks the compiled hand-written implementor +> FakeQfcItemController in QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs with CS0535, and the +> target framework offers no default interface member. It is obtained instead through the concrete +> controller, which is internal to the same assembly as the deactivate handler. This task therefore +> also adds one internal get-only member to +> `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, reporting whether this item's +> breadcrumb selector is currently open by forwarding to the item viewer's existing +> `IsFolderDropDownOpen`; that expression is already evaluated in that same file at lines 200 and +> 225, so no new dependency is introduced. The deactivate handler reads it by casting the loop's +> interface-typed item controller to the concrete internal type and reports `SelectorWasOpen=` from +> the result. Both edits are observational: no caller other than the diagnostic reads the member, and +> neither changes control flow, so Phase 1 remains free of behavioural change. + +### Delta 3c-R — replaces Delta 3c in full + +Replace the acceptance sentence of [P1-T4] with: + +> Acceptance: the file compiles, the existing catch block at lines 58-69 is unchanged in the diff, no +> `if`, `return`, `throw` or assignment other than the two log statements is added inside +> `ParkFocusAndCancelSelectors`, and the new internal member on the concrete item controller is +> declared and forwarded with no branching of its own. No file outside the sixteen write-set paths +> appears in the diff, and in particular neither QuickFiler/Interfaces/IQfcItemController.cs nor +> QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs is modified. The solution compiles under the +> P0-T8 command form, which is what proves both the cast and the forward are well typed. When the +> cast yields null, which cannot occur for any production item group but is reachable in principle, +> the diagnostic records the literal `SelectorWasOpen=unavailable` rather than a fabricated boolean, +> so the AC6 evidence never carries a value that was not observed. + +## Sweep consequences of this revert + +Re-run these, because the revert changes state the previous pass observed: + +- P1-T14's permitted set was widened from seven paths to nine by the withdrawn Delta 3. Re-derive it. + `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` remains permitted, because P1-T4 still + edits it. `QuickFiler/Interfaces/IQfcItemController.cs` is removed. +- P6-T1's "unchanged by this task" qualification stays as the planner wrote it, because P1-T4 still + touches `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`. +- Re-check every task that counts write-set paths or asserts that a file was not modified. +- Confirm the AC6 no-behavioural-change constraint still holds for Phase 1 under the revised + mechanism. + +## Everything else from round 1 stands + +The fourteen other dispositions the planner reported are accepted as applied. Do not revisit them, +and do not revisit the items listed under "Items round 1 confirmed as already correct" in +evidence/other/preflight-round-1-delta.md. The sweep changes the planner made beyond the supplied +text — the line-count idiom propagation, the P1-T2 band re-derivation to 485 against the [480, 486] +band, the conditional fifth expect-fail row, the P7-T3 combined-total correction, and the removal of +the absolute worktree path from P0-T14, P1-T15 and P9-T9 — are all accepted and must be preserved. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta.md new file mode 100644 index 000000000..99ee9e91c --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-1-delta.md @@ -0,0 +1,445 @@ +# Preflight round 1 delta — issue #796 + +- Timestamp: 2026-09-07T03:20 +- Plan under revision: docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +- Round 1 verdict: PREFLIGHT: REVISIONS REQUIRED / CONVERGENCE: FURTHER ROUNDS LIKELY +- Reviewer: atomic-executor under DIRECTIVE: PREFLIGHT VALIDATION ONLY, with command execution + +Round 1 ran commands rather than reasoning about them. Six observed values were requested; two were +observed directly, one was confirmed statically, and three could not be run. The reason three could +not be run is itself defect B1. + +## How to apply this delta + +Apply every item, blocking and non-blocking alike, in ONE revision round. Deferring the non-blocking +observations only re-surfaces them as findings in the next round. + +Where an item supplies replacement text, apply that text VERBATIM. Do not substitute your own +wording. Substituted text is unreviewed until the following round, so each paraphrase silently +converts a closed defect into a new unreviewed region. Where a command or path has been wrapped for +this document's width, reassemble it onto one line; a wrapped command is not a command. + +Report a per-item disposition for every item below, using exactly one of: +`applied-verbatim`, `applied-with-mechanical-reassembly`, or `not-applied-with-reason`. + +If you judge a supplied item to be wrong, do NOT silently rewrite it. Apply it as given, or leave it +unapplied and report the disagreement for the orchestrator to adjudicate. That is what surfaces +reviewer error instead of burying it. + +## Orchestrator adjudications, which modify three of the reviewer's items + +These three carry an orchestrator ruling. Where a ruling conflicts with the reviewer's text, the +ruling wins and the reviewer's text is amended as stated. + +### On B1 — the command channel is environment-dependent, not fixed + +The reviewer observed that every `pwsh` invocation is refused, and correctly reported it. The +refusal is a property of the isolated agent worktree this PREPARATION run executes in, and it is +not established for the environment the execution run will use. A previously recorded observation +holds that the refusal comes from the sandbox rather than from the agent type, and that +`atomic-executor` runs `pwsh` normally outside that sandbox. Reading the refusal as permanent +previously caused a false blocked halt. + +Ruling: ACCEPT Delta 1a, 1b and 1c as written, because the two-rung design is correct under both +environments and costs nothing when rung 1 succeeds. Add this constraint to the plan when applying +them: P0-T2 MUST probe rung 1 first and record the channel it observed. The plan must NOT hard-code +`COMMAND-CHANNEL: B`, must not state that pwsh is unavailable, and must not present Channel B as +the expected outcome. State instead that the channel is determined by observation at P0-T2 and that +either value is a normal result. + +### On B3 — the seventeenth path is accepted + +Ruling: ACCEPT. Add `QuickFiler/Interfaces/IQfcItemController.cs` to the write set as the +seventeenth path. The orchestrator has made the corresponding edit to spec.md's `## Write Set` +section, so Delta 3a's spec.md half is already done; apply only its plan half. + +Reasoning, recorded because the alternative was genuinely open. The narrower resolution was to drop +`SelectorWasOpen` from the per-item diagnostic and amend the runbook to match. That was rejected: +the per-item open state is what distinguishes a cancel that hit an open selector from a cancel that +was a no-op, and today the cancel runs unconditionally on every item, so a cancel COUNT alone +carries no such information. Candidate 1's refute rule depends on that distinction. The blast-radius +cost is marginal and falls in a project no concurrently prepared sibling item owns. + +### On B5 — the pre-existing dirty set will most likely be empty at execution time + +The reviewer measured five dirty paths outside the feature folder and proposed recording and +excluding them. The mechanism is right and is accepted. The measurement is a property of this +preparation run in progress, not of the tree the executor will start from: before this preparation +run finishes, the orchestrator commits the feature folder and the promoted record, and removes the +four agent-memory paths. The set the executor observes at P0-T14 will therefore most likely be +EMPTY, and the porcelain gates will be strict rather than relaxed. + +Ruling: ACCEPT Delta 5a, 5b and 5c, with one amendment. In Delta 5a, replace the sentence beginning +`Measured in the preflight pass this set holds five paths:` and ending `so the set cannot be emptied +by the executor.` with this text: + +> This set is expected to be EMPTY when the executor reaches this task, because the preparation run +> that produced this plan commits the feature folder and the promoted record and removes its own +> agent-memory writes before finishing. An empty set is the normal result and makes every later +> porcelain gate strict. The set is recorded rather than assumed empty because a non-empty set is a +> legitimate state the executor did not create and in some cases may not remediate: the .claude tree +> is one this plan may not edit at all. During the preflight pass that produced this task the set +> held five paths, four of them under .claude/agent-memory and one an untracked promotion record. + +## Blocking defects + +### B1 — the plan's sole command channel may be refused; no task carries a fallback + +Affected: P0-T2, T3, T5, T6, T7, T8, T9, T10, T11, T12, T14; P1-T2, T3, T6, T7, T8, T9, T10, T11, +T12, T13, T14, T15; P4-T5, T9; P5-T3, T5, T7; P6-T3, T4, T6; P7-T3, T4; P9-T1 through T11. + +Delta 1a — replace the paragraph at plan line 104, beginning `All C# tools are invoked through pwsh, +never through the Bash tool.` and ending `disallowed shell segment.`, with: + +> All C# tools are invoked through whichever of two command channels task P0-T2 records as +> available, and that task runs before every other command in this plan. Channel A is pwsh, in +> either the `-NoProfile -Command` or the `-NoProfile -File` form. Channel B is direct invocation +> from the Bash tool. Which channel is available is determined by observation at P0-T2 and not by +> this paragraph: in some sandboxes the isolation guard rejects every command whose name is pwsh, in +> both flag forms, and rejects an `env -C` prefixed form as well, while in others pwsh runs +> normally. Either recorded value is a normal result. Every pwsh block shown in a later task of this +> plan is a command SHAPE. When P0-T2 records `COMMAND-CHANNEL: B`, the executor runs the Channel B +> equivalent recorded in that task's artifact and notes the substitution in the artifact of the task +> it substituted for. +> +> Channel B has three constraints that are load-bearing and must not be relaxed. First, Git Bash +> applies MSYS path translation to forward-slash switches, rewriting `/m` into a filesystem path and +> producing MSB1008, so msbuild is invoked with dash switches (`-t:Rebuild -m -nodeReuse:false +> -p:Configuration=Debug -p:Platform="Any CPU"`) and vstest.console.exe is invoked with its +> forward-slash switches intact behind an `MSYS_NO_PATHCONV=1` assignment prefix. Second, a quoted +> absolute path in the command-NAME position is refused by the same guard separately from pwsh, so a +> Windows executable that is not on PATH is invoked by bare name behind a `PATH=` assignment prefix; +> a quoted absolute path passed as an ARGUMENT is permitted. Third, test DLL paths are passed to +> vstest.console.exe with backslash separators, because mixed separators make vstest report that the +> test source file was not found. + +Delta 1b — replace [P0-T2] in full with: + +> - [ ] [P0-T2] Determine the available command channel, then install the repo-pinned .NET SDK, +> recording both in evidence/baseline/p0-t2-dotnet-sdk-install.md under the feature folder. +> global.json pins SDK 8.0.205 with `paths` including `.dotnet-sdk`, and a fresh worktree has none, +> so every dotnet command prints the global.json errorMessage instead of a version until this task +> completes. Rung 1: attempt +> +> ``` +> pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1 +> ``` +> +> Rung 2, taken only when rung 1 is refused by the isolation guard rather than failing on its own +> merits: record the guard's refusal text verbatim, then perform the same install from the Bash +> tool, which is what that script does and all it does — create the .dotnet-sdk directory, download +> `https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.205/dotnet-sdk-8.0.205-win-x64.zip` into it, +> unzip it in place, and delete the zip. `.gitignore` already ignores `.dotnet*/`, so neither rung +> adds a porcelain entry. Acceptance: the artifact records the line `COMMAND-CHANNEL: A` or the +> line `COMMAND-CHANNEL: B` exactly once; it records `EXIT_CODE: 0` for the rung taken; the +> directory .dotnet-sdk/sdk/8.0.205 exists; and the recorded stdout of `dotnet --version`, run on +> the recorded channel, is a version string beginning with the two characters `8.` rather than the +> sentence `The repo-local .NET SDK is missing.` recorded verbatim. When the recorded channel is B, +> the artifact additionally records, one per line, the Channel B equivalent of each command form +> this plan uses later: the msbuild form, the vstest form, the csharpier form, the NuGet restore +> form, and the file-line-count form. Those recorded equivalents are the commands the later tasks +> run, and no later task may improvise one that is not recorded here. + +Delta 1c — replace the command block of [P0-T3], leaving its prose and acceptance otherwise intact, +with: + +> ``` +> pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1 -SolutionPath TaskMaster.sln -Configuration Debug +> ``` +> +> When P0-T2 recorded `COMMAND-CHANNEL: B`, run the recorded Channel B equivalent instead, which is +> a packages.config-based `nuget restore TaskMaster.sln` invoked by bare name behind a `PATH=` +> assignment prefix naming the directory holding nuget.exe. Record which channel was used. + +### B2 — [P0-T12]: the counting idiom cannot produce the four values the task pins + +`Measure-Object -Line` does not count blank lines. The four pinned files carry 44, 49, 30 and 39 +blank lines, so the plan's idiom reports 454, 407, 218 and 341 against the true 498, 456, 248 and +380. Every one diverges, so P0-T12's acceptance instructs the executor that the tree has moved — a +false diagnosis that halts Phase 0 on a tree whose counts are exactly as cited. The orchestrator +independently measured the same four physical-line counts with a line-oriented content search and +confirms 498, 456, 248 and 380. + +Second harm: P1-T2's band [480, 486] would be evaluated against roughly 441 and fail, and every +downstream ceiling gate (P4-T3, P4-T10, P5-T6, P5-T8, P6-T6, P7-T2, P9-T8) under-reports by 30 to 50 +lines, so a file at 540 physical lines reports 495 and passes a 500-line cap it violates. + +Delta 2 — replace the command block and the acceptance sentence of [P0-T12] with: + +> ``` +> pwsh -NoProfile -Command '@("QuickFiler\Controllers\QfcFormController.Deactivate.cs","QuickFiler\Interfaces\IQfcFormViewer.cs","QuickFiler\Viewers\QfcFormViewer.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs","QuickFiler\Viewers\ItemViewer.Breadcrumb.cs","QuickFiler\Controllers\QfcItemController.EventHandlers.cs","QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs","QuickFiler\Resources\FolderBreadcrumb.html","QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs","QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' +> ``` +> +> The physical-line idiom is `(Get-Content -LiteralPath $_).Count` on Channel A and `wc -l` on +> Channel B; the two agree. The idiom `(Get-Content $_ | Measure-Object -Line).Lines` is PROHIBITED +> throughout this plan, at baseline and at every later re-measurement alike, because +> `Measure-Object -Line` omits blank lines and therefore under-reports every count by that file's +> blank-line total. Measured in the preflight pass, the four pinned files carry 44, 49, 30 and 39 +> blank lines respectively, so that idiom would report 454, 407, 218 and 341 against the true 498, +> 456, 248 and 380. Acceptance: the artifact records one physical-line count per path, records the +> line `LINE-COUNT-IDIOM:` followed by the idiom actually used, and the recorded values for +> QuickFiler/Viewers/BreadcrumbDropDownHost.cs, QuickFiler/Viewers/ItemViewer.Breadcrumb.cs, +> QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs and +> QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs are 498, 456, 248 and 380 +> respectively. A divergence from those four values means the tree has moved since this plan was +> authored and the file-size arithmetic in Phase 1 and Phase 5 must be re-derived before +> proceeding. Every later task in this plan that re-measures a line count uses the idiom recorded +> on the `LINE-COUNT-IDIOM:` line and no other, so the baseline and the final audit are +> commensurable. + +### B3 — [P1-T4]: `selectorWasOpen` has no source reachable from the write set + +`IQfcItemController` declares `ItemNumber` and `CancelBreadcrumbSelector()` and no selector-open +state and no viewer accessor. The only member carrying that state is `IsFolderDropDownOpen` on the +item-viewer interface, which this item does not touch. + +Delta 3a, plan half only — add `QuickFiler/Interfaces/IQfcItemController.cs` to the +"Production — modify" list of the plan's `## Write Set` section, and change the sentence introducing +that section from "The plan's diff must stay within the sixteen paths below." to "The plan's diff +must stay within the seventeen paths below." Every other place in the plan that says "sixteen +write-set paths" — the P0-T14 acceptance, the P9-T10 acceptance, and the structural self-check +paragraph — becomes "seventeen write-set paths". The spec.md half of this delta has already been +applied by the orchestrator; do not repeat it. + +Delta 3b — replace the second sentence of [P1-T4] with: + +> Add the pure method `internal static string FormatDeactivationDiagnostics(bool webView2Focused, +> bool activeFormIsNull, int groupCount)` returning a single interpolated line containing the labels +> `WebView2Focused=`, `ActiveFormNull=` and `Groups=`, and the pure method `internal static string +> FormatItemCancelDiagnostics(int itemNumber, bool selectorWasOpen)` returning a single line +> containing the labels `ItemNumber=` and `SelectorWasOpen=`. The `selectorWasOpen` value has no +> source on `IQfcItemController` today: that interface declares `ItemNumber` and +> `CancelBreadcrumbSelector()` and no selector-open state and no viewer accessor, and the only member +> carrying the state is `IsFolderDropDownOpen` on the item-viewer interface, which this item does not +> touch. This task therefore also declares one get-only boolean member on +> `QuickFiler/Interfaces/IQfcItemController.cs` beside `CancelBreadcrumbSelector()` at line 60, with +> an XML doc comment stating that it reports whether this item's breadcrumb selector is currently +> open, implemented on the item controller as a forward to the item viewer's existing member. This is +> the only production interface change Phase 1 makes and it is observational: no caller other than +> the new diagnostic reads it, and it changes no control flow. Moq's default bool return is false, so +> no existing Arrange block in any suite that mocks this interface requires modification. + +Delta 3c — replace the acceptance sentence of [P1-T4] with: + +> Acceptance: the file compiles, the existing catch block at lines 58-69 is unchanged in the diff, no +> `if`, `return`, `throw` or assignment other than the two log statements is added inside +> `ParkFocusAndCancelSelectors`, and the new `IQfcItemController` member is declared and forwarded +> with no branching of its own. The solution compiles under the P0-T8 command form, which is what +> proves the forward is well typed against every implementor. + +### B4 — [P4-T7] YES branch lands a test no task makes pass, making [P4-T9] unsatisfiable + +Delta 4 — replace the YES-branch sentence of [P4-T7] with: + +> When the value is YES, the test receives the same explicit genuine-case Arrange line, and the +> paired negative test asserting `Times.Never()` on `ParkFocusOffWebView2()` for the self-inflicted +> case is added in this task; the same artifact records the paired test's name and records the line +> `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8`. Task P4-T8 then extends its guard to the +> focus-parking step as well as the cancel loop, so that paired test passes at P4-T9; the per-item +> boundary catch and its error logging at lines 58-69 remain unchanged in either branch. The paired +> negative test is a fifth expect-fail test in this plan when this branch is taken: it is added to +> the expect-fail inventory table with class QfcFormControllerDeactivateTests, landed by P4-T7, +> recorded Failed at no gate because no gate runs between P4-T7 and P4-T8, and made to pass by +> P4-T8. Task P9-T5 then reads the inventory as five rows rather than four. When the value is NO, no +> paired test is added, P4-T8's guard stays scoped to the cancel loop, and the inventory remains four +> rows. + +### B5 — porcelain gates unsatisfiable against a tree dirty outside the feature folder + +Apply Delta 5a, 5b and 5c as the reviewer wrote them, with the one sentence amended by the +orchestrator ruling recorded above. + +Delta 5a — append to the acceptance of [P0-T14]: + +> The artifact additionally records, under the heading `PRE-EXISTING-DIRTY-SET:`, every porcelain +> path present at this task that lies outside the feature folder, one path per line, together with +> its two-character status code. This set is expected to be EMPTY when the executor reaches this +> task, because the preparation run that produced this plan commits the feature folder and the +> promoted record and removes its own agent-memory writes before finishing. An empty set is the +> normal result and makes every later porcelain gate strict. The set is recorded rather than assumed +> empty because a non-empty set is a legitimate state the executor did not create and in some cases +> may not remediate: the .claude tree is one this plan may not edit at all. During the preflight pass +> that produced this task the set held five paths, four of them under .claude/agent-memory and one an +> untracked promotion record. Every later porcelain gate in this plan is evaluated against the +> porcelain output MINUS this recorded set, and a gate that would otherwise report zero lines is +> satisfied when the only lines it reports are members of this set. + +Delta 5b — replace the final clause of [P1-T14]'s acceptance with: + +> and the porcelain output lists no path outside the feature folder, the write set, and the +> `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md. + +Delta 5c — replace the final clause of [P1-T15]'s acceptance with: + +> and `git status --porcelain --untracked-files=all` afterwards lists no path outside the feature +> folder and the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md. + +### B6 — [P9-T11] contradicts its own command and has no clean-tree fixpoint + +Delta 6 — replace [P9-T11] in full with: + +> - [ ] [P9-T11] Close the loop for the worktree this plan executes in. If any of P9-T1 through +> P9-T8 failed or changed a tracked file, restart the loop at P9-T1 and record the second pass in +> evidence/qa-gates/p9-t11-final-loop.md under the feature folder; otherwise record the single +> clean pass there. Record in that artifact the commands of the final clean pass in the order +> format, lint, type-check, test, and the observed output of `git status --porcelain +> --untracked-files=all` taken at that point on the recorded command channel. Then check this task +> off in this plan file, stage with the P9-T9 pathspec form, and run `git commit --amend --no-edit` +> on the recorded command channel. Acceptance: the artifact names which of the two branches was +> taken and records the four commands of the final clean pass in order; and the porcelain output +> recorded in the artifact lists no path other than members of the `PRE-EXISTING-DIRTY-SET:` +> recorded in evidence/baseline/p0-t14-scope-baseline.md, this plan file, and this task's own +> artifact. A terminal gate demanding a porcelain output of zero lines is not used, because this +> task's own check-off and its own artifact are written into the feature folder after the last +> commit and would re-dirty the tree that such a gate measures; the amend is what folds them into +> the final commit, and the acceptance is evaluated on the porcelain reading taken immediately +> before it. The commit message is not changed by the amend, which is why no acceptance clause +> asserts over the amended message body. + +Note for the planner: the reviewer's original text for this delta named the absolute worktree path +and required the porcelain output to be recorded in the amended commit's message body. Both were +removed above. The absolute path is removed because a committed artifact must not carry a host path, +and the message-body clause is removed because it is the contradiction B6 reports. + +### B7 — [P0-T10]: `Skipped` is not printed on a successful vstest run + +Delta 7 — replace the first clause of [P0-T10]'s acceptance with: + +> the artifact records `EXIT_CODE:`, the Total and Passed counts read from the run summary, the +> Failed count read from the summary when the run printed a `Failed:` line and recorded as 0 with the +> note `NOT PRINTED ON A PASSING RUN` when it did not, and a Skipped count derived as Total minus the +> sum of Passed and Failed rather than read, because vstest.console.exe prints no `Skipped:` line on +> a run with no skipped tests and the TRX `notExecuted` attribute is hard-coded to 0; the artifact +> states that the derivation was used, and states that the `TestCategory!=LiveOutlook` filter excludes +> rather than skips, so filtered tests appear in neither the Total nor the derived Skipped figure; + +### B8 — [P3-T2]: `INCONCLUSIVE` is admitted by Phase 3 and then cannot be consumed by it + +Delta 8 — replace the acceptance sentence of [P3-T2] with: + +> Acceptance: the line exists exactly once with one of the two admitted values, and the record states +> which `FIRST-CAUSE-GESTURE-` line it was derived from. When every one of the three +> `FIRST-CAUSE-GESTURE-` lines reads INCONCLUSIVE, no derivation is available and this task is not +> discharged by guessing: the executor writes the line `AC1-FAIL-BEFORE-CARRIER: UNDERIVABLE` +> together with the three quoted INCONCLUSIVE lines, halts, and returns the observation to the human +> for a repeat of the Phase 2 runbook, exactly as `MANUAL-OBSERVATION: INCONCLUSIVE` does at P2-T2. +> That halt is the correct outcome, because assigning the AC1 fail-before responsibility from the +> plan's expectation rather than from the log is the specific substitution the AC6 ordering constraint +> exists to prevent. + +### B9 — [P0-T11] and [P9-T6]: a per-file coverage row is demanded for a partial-class part + +A Cobertura class element carries exactly one `filename` attribute, so a class whose methods span +several source files may be emitted under one filename and the other parts produce no group at all. +This item is marked UNVERIFIED by the reviewer because the coverage run could not be executed. + +Delta 9 — replace the acceptance clause of [P0-T11], from `Acceptance: the file +coverage/p0-t11-baseline.cobertura.xml exists` through `and QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs, +obtained with:`, with: + +> Acceptance: the file coverage/p0-t11-baseline.cobertura.xml exists and the artifact records all six +> numeric attributes above in `Output Summary:`, plus one row for each of +> QuickFiler/Controllers/QfcFormController.Deactivate.cs, QuickFiler/Viewers/BreadcrumbDropDownHost.cs, +> QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs, +> QuickFiler/Controllers/QfcItemController.EventHandlers.cs and +> QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs, each row carrying either that file's +> `lines-covered` and `lines-valid` or the literal `ABSENT: no class node carries this filename` with +> the name of the class node that did carry the enclosing type. The absent case is admitted and +> recorded rather than treated as a failure, because a Cobertura class element carries exactly one +> `filename` attribute while a partial class spans several source files, so a part other than the one +> the emitter chose produces no group at all; recording it as absent is what keeps the baseline and +> the final run comparable, and task P9-T7 excludes any file recorded ABSENT at both P0-T11 and +> P9-T6 from the changed-code denominator and names it in the NOT MEASURABLE list. The recorded +> filenames are reproduced verbatim as the tool emitted them, which uses backslash separators, and +> the artifact states that the forward-slash spellings above are the same five files. The rows are +> obtained with: + +Delta 9b — append to the acceptance of [P9-T6]: + +> Each of the five per-file rows carries either its two figures or the literal `ABSENT: no class node +> carries this filename`, on the same terms and for the same partial-class reason recorded at +> P0-T11, and a file recorded ABSENT at both tasks is excluded from the P9-T7 changed-code +> denominator and named in that task's NOT MEASURABLE list. + +## Non-blocking observations — apply all of these in the same round + +### N1 — [P9-T7] treats AC6 logging asymmetrically + +Append to [P9-T7]: + +> The AC6 log statements and pure formatter methods added by task P1-T4 to +> `QuickFiler/Controllers/QfcFormController.Deactivate.cs` are counted in the changed-code +> denominator, unlike the diagnostics part, because they sit in a file whose behavioural changes are +> also measured and separating them per line would make the figure unreproducible; the artifact +> records how many changed lines in that file are instrumentation. + +### N2 — [P5-T6] pins two of three `CancelCount` assertions + +The verified positions are 48, 79 and 113, with `FocusAnchorCount` at 49, 80 and 114. Append to +[P5-T6]: + +> A third `CancelCount.Should().Be(1)` assertion exists at line 113 and is likewise kept unchanged; +> it is not named as a scoping guard because the spec pins only the two at lines 48 and 79, but a fix +> that drives it to zero is the same design signal. + +### N3 — exclusion lists diverge between the plan and spec.md + +The plan's "Explicitly not in the write set" paragraph names `QuickFiler/Viewers/IItemViewer.cs` +while spec.md's equivalent paragraph omits it. Delta 3 makes that paragraph load-bearing, since B3's +resolution turns on that interface being excluded. Align the two lists. Write every path in both +exclusion paragraphs WITHOUT backticks, as both documents already do, because a backticked path in a +negative sentence is read as a write claim by downstream blast-radius derivation. + +### N4 — [P0-T4] measures a narrower scope than [P0-T8] gates + +P0-T4 checks analyzer agreement for QuickFiler and QuickFiler.Test only, while P0-T8 rebuilds the +whole solution, where a skew in any other project is `error CS0006` and fails the baseline with no +remediation branch defined. Both in-scope projects were verified to agree on all versions, so this is +a latent risk rather than a present failure. Add a remediation branch to P0-T8 stating that a +CS0006 naming an analyzer assembly in a project outside the write set is a missing restored package +rather than a source defect, that the remedy is to re-run the P0-T3 restore and record the second +attempt, and that the task halts with the diagnostic recorded if it recurs. + +### N5 — `QuickFiler/QuickFiler.csproj.bak` is tracked and holds a stale compile entry + +It is not a `.cs` file, so the formatter will not touch it, and no plan task reads it. Add one +sentence to the P0-T14 scope-baseline task noting that this file exists, is not in the write set, and +must not be edited, so that a later search for compile entries does not mistake it for the project +file. Write that path without backticks. + +### N6 — [P0-T13] probes for the MCP validator + +No change needed. For the record: the orchestrator ran +`mcp__drm-copilot__validate_orchestration_artifacts` with artifact_type plan against this plan on +2026-09-07 and it returned ok=true with no warnings, so the probe will record a validator result +rather than the absence line. The task is correctly written as record-and-continue. + +## Items round 1 confirmed as already correct — do not change them + +Changing any of these would reopen a closed question. + +- P0-T3's restore step is correctly ordered ahead of the first msbuild at P0-T8 and correctly states + the `EnsureNuGetPackageBuildImports` mechanism. +- The formatter tasks P0-T7, P9-T2, P1-T8 and P9-T1 correctly avoid asserting on a line the formatter + prints only when it rewrote a file, using exit code plus an empty file list and before-and-after + porcelain instead. +- The `/t:Rebuild`-only reasoning and the clause making a zero compiler-invocation count a FAILED gate + are correct as written. +- The AC6 ordering constraint is correctly implemented: Phase 1 is instrumentation only, Phase 2 is + the manual gate, Phase 3 is the decision record, and no behavioural-fix phase precedes it. +- Every evidence path resolves under the feature folder's evidence tree; no `artifacts/`-rooted + evidence path exists. +- The expect-fail carve-outs at P4-T5, P5-T3 and P6-T4 are complete, and the P1-T11 total of 9 is + arithmetically correct against the measured 7 test methods. +- Both planner findings were adjudicated CONFIRMED: the two `[ExcludeFromCodeCoverage]` sites are at + QuickFiler/Viewers/QfcFormViewer.cs line 17 and QuickFiler/Viewers/ItemViewer.cs line 20, the five + files P9-T7 measures carry no class-level exclusion so the scoped gate can still fail, and the + measured `[TestMethod]` count in QfcFormControllerDeactivateTests.cs is 7 rather than the 6 the + research prose states. + +## Required output for this revision round + +Return the plan path, a per-item disposition line for every item above (B1 through B9 and N1 through +N6), and the full `PLANNER-INTERNAL-REVIEW` and `SELF-REVIEW` record blocks re-derived against the +tree as it stands after this revision. A citation verified in the previous round is evidence about a +superseded state and may not be carried forward. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-2-delta.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-2-delta.md new file mode 100644 index 000000000..f35e8e48c --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/preflight-round-2-delta.md @@ -0,0 +1,167 @@ +# Preflight round 2 delta — issue #796 + +- Timestamp: 2026-09-07T05:10 +- Plan under revision: docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +- Round 2 verdict: PREFLIGHT: REVISIONS REQUIRED / CONVERGENCE: NO FURTHER ROUNDS EXPECTED +- Reviewer: atomic-executor, confirming round, text-and-citation pass with targeted read-only checks + +Round 2 confirmed every sweep that round 1's fixes were meant to close. The line-count idiom +propagation, the B3 reversal, the conditional fifth expect-fail row, the porcelain changes at P1-T14 +and P1-T15, the P7-T3 combined total, the AC6 phase ordering, whole-plan ordering integrity, evidence +paths, scope lock, path polarity, and cross-document consistency with spec.md all PASS. + +Three blocking defects remain. All three are consequences of the adopted B3 mechanism that no earlier +pass could have observed, which is the sibling-invalidation class the confirming round exists to +catch. All three are orchestrator-accepted without amendment. + +## How to apply this delta + +Apply every item, blocking and non-blocking alike, in ONE revision round. Apply the supplied text +VERBATIM; do not substitute your own wording. Where text was wrapped for this document's width, +reassemble it onto one line. Report a per-item disposition for all seven items using exactly one of +`applied-verbatim`, `applied-with-mechanical-reassembly`, or `not-applied-with-reason`. If you judge +an item wrong, apply it as given or leave it unapplied and report the disagreement; do not silently +rewrite it. + +## Orchestrator adjudication + +All three blocking defects are ACCEPTED as written. No amendment. The reasoning behind each was +independently checked against the tree: + +- B1 is correct on both legs. A non-nullable `bool` parameter cannot render an `unavailable` literal, + and `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` injects its item controllers + as Moq mocks of the interface, whose proxy is not the concrete type, so the successful-cast arm is + unreachable from every test this plan creates. A formatter no test executes, counted in a 90 percent + changed-line denominator with no admissible exclusion, is an unsatisfiable gate. +- B2 is correct. P9-T9's own acceptance requires it to record a SHA that does not exist until after + its commit, so its artifact is necessarily untracked afterwards, and P9-T10 writes another artifact + after that commit. The round-1 B6 fix closed this fixpoint only for P9-T11's own two writes; the + same class reappears one commit further along. +- B3 is the most consequential of the three and is correct. Line 56 of + QuickFiler/Controllers/QfcFormController.Deactivate.cs reads + `group.ItemController?.CancelBreadcrumbSelector()`. That null-conditional is the existing code's own + evidence that a group with a null item controller is reachable. An unguarded diagnostic would raise + a NullReferenceException, the per-item boundary catch at lines 58-69 would convert it into a + `logger.Error` entry, and a silent no-op would become a logged error. That is a behavioural change + in Phase 1, which the AC6 ordering constraint forbids, and no existing test injects a null item + controller so P1-T12 could not detect it. + +Note the relationship between B1 and B3: B1 moves the unavailable case INTO the formatter via a +nullable parameter, and B3 makes the argument expressions null-safe. They are jointly satisfiable +only together, and D3c reconciles the Decisions record with both. Apply all three as one unit. + +## Blocking defects + +### B1 — [P1-T4] and [P9-T7]: the second formatter is unreachable from every test the plan creates + +D1a. In [P1-T4], replace: + +> and the pure method `internal static string FormatItemCancelDiagnostics(int itemNumber, bool selectorWasOpen)` returning a single line containing the labels `ItemNumber=` and `SelectorWasOpen=`. + +with: + +> and the pure method `internal static string FormatItemCancelDiagnostics(int itemNumber, bool? selectorWasOpen)` returning a single line containing the labels `ItemNumber=` and `SelectorWasOpen=`, rendering the second label as `SelectorWasOpen=unavailable` when the argument is null and as the boolean otherwise. The parameter is nullable so that the unavailable case is produced inside the formatter. That keeps the per-item log statement a single unconditional call, and it keeps the formatter reachable from the existing deactivate suite, whose tests inject their item controllers as Moq mocks of the interface and therefore never produce a successful cast to the concrete type. + +D1b. In [P9-T7], replace: + +> states the framework reason it cannot be reached from a headless test + +with: + +> states the reason it cannot be reached from the tests this plan creates, which is either a framework limitation or the mocked-seam limitation named at the end of this task + +D1c. Append to [P9-T7]: + +> One changed line is unreachable from the test population this plan creates, and it is admitted here by name rather than left for the executor to discover at the gate: the internal selector-open member task P1-T4 adds to `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`. Its only reader is the per-item log statement in `ParkFocusAndCancelSelectors`, which reaches it only when the loop's interface-typed item controller casts successfully to the concrete internal type, and every test in QfcFormControllerDeactivateTests that injects item controllers injects them as Moq mocks of the interface, so that cast yields null in every test. The artifact names that line with its file and line number, states that reason, and counts it against the same at-most-3 allowance, leaving at most two further individually named exclusions available; the 90 percent threshold is then evaluated over the remaining measurable behavioural changed lines. + +### B2 — [P9-T11]: the porcelain permitted set omits two artifacts that cannot exist before the last commit + +D2. Replace the acceptance sentence of [P9-T11], from `Acceptance: the artifact names which of the two branches was taken` through `asserts over the amended message body.`, with: + +> Acceptance: the artifact names which of the two branches was taken and records the four commands of the final clean pass in order; the porcelain reading is taken immediately before the amend, after this task's own artifact and check-off have been written; and it lists no path other than members of the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md and paths inside the feature folder docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796. A terminal gate demanding a porcelain output of zero lines is not used, and neither is one permitting only this plan file and this task's own artifact: evidence/qa-gates/p9-t9-final-commit.md records a SHA that does not exist until after the P9-T9 commit, and evidence/qa-gates/p9-t10-scope-boundary.md is written after that commit, so both are necessarily untracked at this reading, as are this task's own artifact and its own check-off. The amend is what folds all four into the final commit. The gate remains strict outside the feature folder: any path under QuickFiler, under QuickFiler.Test, or anywhere else in the tree that is not a member of the recorded pre-existing set fails it. The commit message is not changed by the amend, which is why no acceptance clause asserts over the amended message body. + +### B3 — [P1-T4]: the per-item diagnostic dereferences a reference the existing code guards + +D3a. In [P1-T4], replace: + +> The deactivate handler reads it by casting the loop's interface-typed item controller to the concrete internal type and reports `SelectorWasOpen=` from the result. + +with: + +> The deactivate handler reads it by casting the loop's interface-typed item controller to the concrete internal type and passes the result to the formatter. Both per-item argument expressions are null-safe, because the existing code guards that same reference with a null-conditional at line 56: the item number is obtained with a null-propagating access and a null-coalescing default, and the selector-open value with a null-propagating access on the cast result, which yields null both for a null item controller and for a controller that is not the concrete type. No `if` is added and no exception can escape the added statement, so a group whose `ItemController` is null continues to reach the boundary catch not at all, and Phase 1 stays free of behavioural change. + +D3b. In [P1-T4], replace the final acceptance sentence: + +> When the cast yields null, which cannot occur for any production item group but is reachable in principle, the diagnostic records the literal `SelectorWasOpen=unavailable` rather than a fabricated boolean, so the AC6 evidence never carries a value that was not observed. + +with: + +> When the cast yields null, or when the loop's item controller is itself null, the diagnostic records the literal `SelectorWasOpen=unavailable` rather than a fabricated boolean, so the AC6 evidence never carries a value that was not observed. That case is produced inside the formatter from its nullable parameter, and every member access on the loop's item controller inside the added per-item log statement is written with a null-propagating or null-coalescing operator, matching the existing guard at line 56. + +D3c. Replace the final sentence of Decisions record item 9: + +> The null-cast branch P1-T4's acceptance requires is written as a conditional EXPRESSION supplying the argument of the single per-item log statement, not as an added `if` statement, a `return`, a `throw`, or an assignment to a new local; that is what lets the same task satisfy both the `SelectorWasOpen=unavailable` clause and the no-added-control-flow clause. + +with: + +> The null-cast branch P1-T4's acceptance requires is carried by the formatter's nullable parameter rather than by any statement in the handler: the per-item log statement is a single unconditional call whose arguments are null-propagating and null-coalescing expressions, so no `if` statement, `return`, `throw`, or assignment to a new local is added. That is what lets the same task satisfy the `SelectorWasOpen=unavailable` clause, the no-added-control-flow clause, and the requirement that the formatter be reachable from the existing deactivate suite, which injects its item controllers as Moq mocks of the interface and therefore never produces a successful cast to the concrete type. + +## Non-blocking observations — apply O1, O2 and O3 in the same round + +### O1 — [P1-T2] slack description does not describe the stated band + +The task says the band admits three lines of slack either side of the expected 485, while the stated +window is at least 480 and at most 486, which is five below and one above. The explicit bounds are +operative so the gate is unaffected. Replace the slack phrase with: + +> and the band admits five lines below the expected value and one above + +### O2 — expect-fail inventory row 4 overstates its own gate + +The table records NativeCloseWithNoCommitPending_StillCancelsSelection as "Recorded Failed at P5-T3", +while P5-T3 admits either Passed or Failed for that test and says both satisfy the carve-out. Nothing +is unsatisfiable; the column is simply stronger than the gate it names. Change that cell to: + +> Recorded Passed or Failed at P5-T3; both satisfy that gate's carve-out + +### O3 — [P7-T4] does not carve out the recorded pre-existing dirty set + +One of its two commands is a porcelain status, and its acceptance requires that neither output name a +path beginning with UtilitiesCS/ or UtilitiesCS.Test/. If the recorded pre-existing set ever held +such a path the gate would fail for a state the executor did not create. The set is expected empty, +so this is latent. Append to the P7-T4 acceptance: + +> A path that is a member of the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md does not fail this gate, because the executor did not create it and in some cases may not remediate it; the artifact names any such path it excluded on that basis. + +### O4 — [P0-T13] will record a validator result rather than the absence line + +No change needed. For the record, the orchestrator ran the validator against the revised plan and it +returned ok=true with no warnings, so the probe's second admitted outcome is the one that will be +observed. The task is correctly written as record-and-continue. + +## Confirmed correct in round 2 — do not change + +Changing any of these reopens a closed question and costs a round. + +- All eight downstream ceiling gates read the recorded `LINE-COUNT-IDIOM:` line; the blank-line-omitting + idiom survives only inside P0-T12's prohibition. +- The P1-T2 band arithmetic: 498 physical minus twelve handler lines minus one separating blank equals + 485, inside the stated window, and the window can fail because an unperformed move leaves 498. +- The write set is sixteen, both excluded files appear only unbackticked, and the enumeration returns + 101 backticked path occurrences across exactly 16 distinct values. +- The adopted cast mechanism is well typed: internal partial class at EventHandlers.cs line 25, + `internal IQfcItemController ItemController` at QfcItemGroup.cs line 39, same assembly. +- Every expect-fail carve-out is complete at its own gate position in both branches of P4-T7, with the + baseline counts of 7 and 5 test methods confirmed by measurement. +- P7-T3's combined total of 11 and the derived per-class subtotals. +- AC6 phase ordering, whole-plan ordering integrity, evidence paths, and cross-document consistency + with spec.md including the six acceptance criteria matching issue.md verbatim. +- The porcelain gates are safe against the bootstrap products, because .gitignore line 191 ignores the + packages tree and line 350 ignores the SDK directory. + +## Required output for this revision round + +Return the plan path, a per-item disposition line for all seven items (B1, B2, B3, O1, O2, O3), any +disagreement you are referring to the orchestrator, and the full `PLANNER-INTERNAL-REVIEW` and +`SELF-REVIEW` record blocks re-derived against the tree as it stands after this revision. A citation +verified in an earlier round is evidence about a superseded state and may not be carried forward. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t10-nullable-rebuild.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t10-nullable-rebuild.md new file mode 100644 index 000000000..7764fe0f6 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t10-nullable-rebuild.md @@ -0,0 +1,63 @@ +# P1-T10 — Nullable gate over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T14-25 +Task: [P1-T10] +Issue: #796 +Channel used: A + +RunStartedUtc: 2026-09-07T14:24:20.9418220Z + +Command: the P0-T9 command form with the log path +`TestResults\796\p1-t10\nullable-rebuild.log`: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\796\p1-t10\nullable-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## No `/p:Nullable=enable` token + +The recorded command line contains no `/p:Nullable=enable` token. Corroborated by a +search of the run's console output for the substring `Nullable=enable`, which +returned 0 matches. + +## Build summary, verbatim + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +## Comparison against the P0-T9 baseline + +| Total | P0-T9 baseline | P1-T10 | Verdict | +|---|---|---|---| +| Warnings | 0 | 0 | no greater than baseline | +| Errors | 0 | 0 | no greater than baseline | + +The new file QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs carries +`#nullable enable` on line 1, so it participates in nullable analysis and its +`CS86xx` diagnostics would have been promoted to build errors under +`TreatWarningsAsErrors`. It produced none. + +## Compiler-invocation counts read back from the detailed log + +Raw log (gitignored): TestResults/796/p1-t10/nullable-rebuild.log + +CscTaskCount=36 +CscToolCount=36 + +Both greater than zero. + +## Assembly-freshness corroboration + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T14:24:31.6560560Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T14:24:36.5646003Z | yes | + +Output Summary: EXIT_CODE 0 with 0 warnings and 0 errors, equal to the P0-T9 +baseline; 36 Csc task and 36 csc.exe tool invocations; both touched assemblies +rebuilt after RunStartedUtc; no `/p:Nullable=enable` token on the command line. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t13-debug-build-output.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t13-debug-build-output.md new file mode 100644 index 000000000..18ef02952 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t13-debug-build-output.md @@ -0,0 +1,88 @@ +# P1-T13 — Debug build output the Phase 2 runbook consumes + +Timestamp: 2026-09-07T14-26 +Task: [P1-T13] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command 'Get-ChildItem TaskMaster\bin\Debug\*.dll | ForEach-Object { $_.Name + " " + $_.LastWriteTimeUtc.ToString("o") }' +``` + +EXIT_CODE: 0 + +## Acceptance + +QuickFiler.dll is present in TaskMaster/bin/Debug. + +| Quantity | Value | +|---|---| +| QuickFiler.dll LastWriteTimeUtc | 2026-09-07T14:24:31.6560560Z | +| `RunStartedUtc:` recorded in evidence/qa-gates/p1-t10-nullable-rebuild.md | 2026-09-07T14:24:20.9418220Z | +| At or later than that RunStartedUtc | yes, by 10.7 seconds | + +The output the human will run against is therefore the instrumented output produced +by the P1-T10 rebuild, not a stale copy. + +## Full listing + +``` +AngleSharp.dll 2026-09-03T07:01:56.0000000Z +Apache.Arrow.dll 2026-05-03T03:36:34.0000000Z +Apache.Arrow.Scalars.dll 2026-05-03T03:36:28.0000000Z +C.math.dll 2016-10-19T19:25:40.0000000Z +Deedle.dll 2023-01-17T20:56:28.0000000Z +ExCSS.dll 2026-07-23T23:21:12.0000000Z +FSharp.Core.dll 2026-02-20T01:58:44.0000000Z +Generic.Math.dll 2017-08-26T05:48:52.0000000Z +log4net.dll 2026-08-18T20:25:00.0000000Z +log4net.Ext.Json.dll 2024-12-03T08:19:52.0000000Z +Microsoft.Bcl.AsyncInterfaces.dll 2026-07-24T15:52:40.0000000Z +Microsoft.Bcl.Memory.dll 2026-07-24T15:52:40.0000000Z +Microsoft.Bcl.TimeProvider.dll 2026-07-24T15:53:10.0000000Z +Microsoft.Data.Analysis.dll 2025-11-08T03:30:32.0000000Z +Microsoft.IO.RecyclableMemoryStream.dll 2024-06-11T20:23:46.0000000Z +Microsoft.ML.DataView.dll 2025-11-08T03:29:38.0000000Z +Microsoft.Office.Tools.Common.v4.0.Utilities.dll 2026-05-27T13:42:53.6920325Z +Microsoft.Office.Tools.Outlook.v4.0.Utilities.dll 2026-05-27T13:42:53.6960325Z +Microsoft.Web.WebView2.Core.dll 2026-08-23T23:21:22.0000000Z +Microsoft.Web.WebView2.WinForms.dll 2026-08-23T23:21:00.0000000Z +Mono.Reflection.dll 2019-11-26T22:39:12.0000000Z +Newtonsoft.Json.dll 2025-09-16T08:04:22.0000000Z +ObjectListView.dll 2016-05-05T23:35:46.0000000Z +QuickFiler.dll 2026-09-07T14:24:31.6560560Z +Svg.dll 2026-07-22T18:29:58.0000000Z +SVGControl.dll 2026-09-07T14:24:21.7909598Z +System.Buffers.dll 2025-03-19T20:55:38.0000000Z +System.Collections.Immutable.dll 2026-07-24T15:54:02.0000000Z +System.Diagnostics.DiagnosticSource.dll 2026-07-24T15:53:14.0000000Z +System.Interactive.Async.dll 2026-04-17T15:49:16.0000000Z +System.Interactive.dll 2026-04-17T15:49:16.0000000Z +System.Linq.Async.dll 2026-04-17T15:49:22.0000000Z +System.Memory.dll 2025-04-03T23:00:54.0000000Z +System.Numerics.Vectors.dll 2025-03-19T20:55:42.0000000Z +System.Reactive.Async.dll 2023-05-31T09:22:34.0000000Z +System.Reactive.dll 2026-07-17T09:44:36.0000000Z +System.Runtime.CompilerServices.Unsafe.dll 2025-04-03T23:00:52.0000000Z +System.Text.Encoding.CodePages.dll 2026-07-24T15:56:34.0000000Z +System.Threading.Tasks.Dataflow.dll 2026-07-24T15:56:38.0000000Z +System.Threading.Tasks.Extensions.dll 2025-04-03T23:00:52.0000000Z +Tags.dll 2026-09-07T14:24:27.4196276Z +TaskMaster.dll 2026-09-07T14:24:33.2382042Z +TaskTree.dll 2026-09-07T14:24:28.9331312Z +TaskVisualization.dll 2026-09-07T14:24:29.3973892Z +Tesseract.dll 2022-11-30T08:37:18.0000000Z +ToDoModel.dll 2026-09-07T14:24:28.3818566Z +UtilitiesCS.dll 2026-09-07T14:24:26.7788304Z +``` + +Every first-party assembly (QuickFiler, SVGControl, Tags, TaskMaster, TaskTree, +TaskVisualization, ToDoModel, UtilitiesCS) carries a LastWriteTimeUtc within the +P1-T10 rebuild window. The remaining entries are third-party dependencies copied from +the packages directory and carry their own package timestamps, which is expected. + +Output Summary: QuickFiler.dll is present in TaskMaster/bin/Debug with +LastWriteTimeUtc 2026-09-07T14:24:31.6560560Z, later than the P1-T10 RunStartedUtc of +2026-09-07T14:24:20.9418220Z. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t14-phase1-scope.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t14-phase1-scope.md new file mode 100644 index 000000000..4a132d2f4 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t14-phase1-scope.md @@ -0,0 +1,80 @@ +# P1-T14 — Phase 1 scope audit + +Timestamp: 2026-09-07T14-27 +Task: [P1-T14] +Issue: #796 +Channel used: A +Base anchor: c7ae69f1 + +Commands, in the order run: + +1. `pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796'` +2. `pwsh -NoProfile -Command 'git diff --cached --name-status c7ae69f1'` +3. `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'` + +EXIT_CODE: 0 for all three. + +The staging span is required because a name-listing diff cannot see a file this phase +created. Two of the eight non-feature-folder paths below are creations and appear as +`A` only because they were staged first. + +Step 1 emitted 25 `LF will be replaced by CRLF` warnings, one per Markdown evidence +file. These are Git line-ending normalisation notices from the repository's +`core.autocrlf` setting, not errors, and the command exited 0. + +## Non-feature-folder paths the diff listed + +Eight paths, every one of them on the permitted list for this point in plan order: + +| Path | Diff status | Landed by | +|---|---|---| +| QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | A | P1-T1 (created) | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | M | P1-T2 | +| QuickFiler/QuickFiler.csproj | M | P1-T3 | +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | M | P1-T4 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | M | P1-T4 | +| QuickFiler.Test/QuickFiler.Test.csproj | M | P1-T6 | +| QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | A | P1-T5 (created) | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | M | P1-T7 | + +The set is the union of the paths the seven preceding authoring tasks of this phase +edit or create. No other write-set path appears, which is correct, because no +behavioural phase has run yet. + +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` is a member because +P1-T4 added the observational selector-open member on the concrete item controller in +that file. That edit and the deactivate-handler edit are both observational and +neither changes control flow, so the AC6 ordering constraint that Phase 1 contains no +behavioural change still holds. + +## Paths that must NOT appear, and do not + +| Path | Present in the diff | +|---|---| +| QuickFiler/Interfaces/IQfcItemController.cs | no | +| QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs | no | + +The adopted mechanism changes no interface, so neither the interface nor its compiled +hand-written implementor in the test assembly is touched. + +## Feature-folder paths the diff listed + +Twenty-four further paths, all inside +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796: +the modified plan file carrying this run's check-offs, thirteen Phase 0 baseline +artifacts, seven Phase 1 qa-gates artifacts, and two Phase 1 regression-testing +artifacts. The diff total is therefore 32 paths: 8 write-set plus 24 feature-folder. + +This artifact is not among them. It is written after the diff that reports on it, so +it cannot appear in its own listing. + +## Porcelain output + +Every porcelain entry is either one of the eight write-set paths above or a path +inside the feature folder. No entry lies outside those two sets. The +`PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md is +EMPTY, so this gate is evaluated strictly with no admitted exceptions to subtract. + +Output Summary: The Phase 1 diff against c7ae69f1 lists 8 non-feature-folder paths, +all 8 on the permitted list, and the two prohibited paths are absent. Porcelain lists +no path outside the feature folder and the write set. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t15-instrumentation-commit.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t15-instrumentation-commit.md new file mode 100644 index 000000000..f9cd71ebd --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t15-instrumentation-commit.md @@ -0,0 +1,81 @@ +# P1-T15 — Phase 1 instrumentation commit + +Timestamp: 2026-09-07T14-28 +Task: [P1-T15] +Issue: #796 +Channel used: A +Branch: bug/quickfiler-folder-dropdown-closes-on-open-796 + +## COMMIT-SHA: 0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0 + +Short form: `0dfcb402`. Parent: `336e30db`. Base anchor for every diff gate in this +plan is unchanged at `c7ae69f1`. + +This is the SHA the Phase 2 runbook's step 1 requires as the commit of the build +under test. The Debug output confirmed at P1-T13 was produced from this working tree +state. + +Commands: + +``` +pwsh -NoProfile -Command 'git commit -m "instrument(796): add AC6 debug logging at the two named close-ordering sites" -m "Co-Authored-By: Claude Fable 5.1 "' +pwsh -NoProfile -Command 'git rev-parse HEAD' +``` + +EXIT_CODE: 0 for both. + +Staging used explicit pathspecs (`QuickFiler`, `QuickFiler.Test`, and the feature +folder), never a repository-wide stage, so no unrelated queued file could be swept +onto this branch. + +## Commit summary + +``` +[bug/quickfiler-folder-dropdown-closes-on-open-796 0dfcb402] instrument(796): add AC6 debug logging at the two named close-ordering sites + 34 files changed, 1743 insertions(+), 41 deletions(-) +``` + +34 files: the 8 write-set paths audited at P1-T14, plus the plan file carrying this +run's check-offs, plus 25 evidence artifacts (13 Phase 0 baseline, 8 Phase 1 +qa-gates, 2 Phase 1 regression-testing, and the P1-T14 scope audit; the qa-gates +count includes p1-t14-phase1-scope.md). + +Two paths were created rather than modified: +QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs and +QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs. + +No TRX, no raw MSBuild log, and no Cobertura XML is in the commit. Those live under +the gitignored paths TestResults/796/ and coverage/ and are outside the staged +pathspecs regardless. + +## Commit message, verbatim + +``` +instrument(796): add AC6 debug logging at the two named close-ordering sites + +Co-Authored-By: Claude Fable 5.1 +``` + +The trailer is the last line of the message and nothing follows it. + +## Porcelain immediately after the commit + +Command: `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'` + +Output: empty. Zero entries. The working tree was fully clean at that moment, so it +listed no path outside the feature folder, and none inside it either. The +`PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md is +EMPTY, so the gate was evaluated strictly. + +## State of this artifact + +This file records the SHA of the commit that precedes it, so it cannot be inside that +commit. After it is written the working tree carries exactly one entry, this file, +which lies inside the feature folder. The acceptance condition — that porcelain lists +no path outside the feature folder and the PRE-EXISTING-DIRTY-SET — therefore still +holds. P1-T15's command sequence contains no second commit, so this artifact is left +staged and uncommitted for a later phase to sweep. + +Output Summary: Phase 1 instrumentation committed as +0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0 with 34 files changed. Working tree clean +immediately after the commit. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t2-host-line-count.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t2-host-line-count.md new file mode 100644 index 000000000..e7a1728de --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t2-host-line-count.md @@ -0,0 +1,52 @@ +# P1-T2 — BreadcrumbDropDownHost.cs physical line count after the handler move + +Timestamp: 2026-09-07T14-17 +Task: [P1-T2] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '(Get-Content -LiteralPath QuickFiler\Viewers\BreadcrumbDropDownHost.cs).Count' +``` + +Idiom used: `(Get-Content -LiteralPath $_).Count`, which is the idiom recorded on the +`LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no +other. + +EXIT_CODE: 0 + +MEASURED-PHYSICAL-LINES: 485 + +## Band evaluation + +| Quantity | Value | +|---|---| +| Baseline physical lines (P0-T12) | 498 | +| Handler physical lines removed (426 through 437 inclusive) | 12 | +| Separating blank line removed (438) | 1 | +| Expected physical result | 485 | +| Admitted band | 480 through 486 inclusive | +| Measured | 485 | +| Verdict | inside the band, and equal to the expected value | + +The band must not be evaluated against a blank-line-omitting count, which would +report roughly 441 here and fail a correct move. The idiom recorded above counts +physical lines including blank lines. + +## Corroborating observations + +- `DropDown.Closed += OnDropDownClosed;` is still present in the main part: 1 match. + The subscription stays put and continues to bind after the move, because both parts + declare the same `sealed partial class BreadcrumbDropDownHost`. +- `private void OnDropDownClosed` no longer appears in the main part: 0 matches. +- `FinishClose` was not touched by this edit; the removal ended immediately before its + declaration. + +This is a pure move. The removed body is byte-identical to the body relocated in +P1-T1 apart from the log statement P1-T1 added ahead of it. + +Output Summary: BreadcrumbDropDownHost.cs measures 485 physical lines after the move, +equal to the expected value and inside the 480-486 band. Subscription preserved, +handler declaration absent from the main part. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t3-compile-entry.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t3-compile-entry.md new file mode 100644 index 000000000..2043dffe4 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t3-compile-entry.md @@ -0,0 +1,38 @@ +# P1-T3 — Compile entry for BreadcrumbDropDownHost.Diagnostics.cs + +Timestamp: 2026-09-07T14-17 +Task: [P1-T3] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command 'Select-String -Path QuickFiler\QuickFiler.csproj -SimpleMatch "BreadcrumbDropDownHost.Diagnostics.cs"' +``` + +EXIT_CODE: 0 + +Recorded output, verbatim: + +``` +QuickFiler\QuickFiler.csproj:417: +``` + +MATCHING-LINE-COUNT: 1 + +Exactly one matching line, at line 417, placed immediately after the existing entry +for `Viewers\BreadcrumbDropDownHost.Open.cs` at line 416. The two anchor entries +this plan cites, `Viewers\BreadcrumbDropDownHost.cs` at line 415 and +`Viewers\BreadcrumbDropDownHost.Open.cs` at line 416, were confirmed at those lines +before the edit. + +QuickFiler/QuickFiler.csproj is non-SDK-style, so without this entry the new partial +part would be silently not compiled and every downstream assertion about it would be +vacuous. + +The edit was applied to QuickFiler/QuickFiler.csproj. QuickFiler/QuickFiler.csproj.bak +is a tracked file that is not in the write set and was not read or edited. + +Output Summary: One `` entry added for the new diagnostics part; +the search reports exactly one matching line. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t6-compile-entry.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t6-compile-entry.md new file mode 100644 index 000000000..33c154e08 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t6-compile-entry.md @@ -0,0 +1,35 @@ +# P1-T6 — Compile entry for BreadcrumbDropDownCloseOrderingTests.cs + +Timestamp: 2026-09-07T14-21 +Task: [P1-T6] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command 'Select-String -Path QuickFiler.Test\QuickFiler.Test.csproj -SimpleMatch "BreadcrumbDropDownCloseOrderingTests.cs"' +``` + +EXIT_CODE: 0 + +Recorded output, verbatim: + +``` +QuickFiler.Test\QuickFiler.Test.csproj:83: +``` + +MATCHING-LINE-COUNT: 1 + +Exactly one matching line. The entry was placed alongside the existing Viewers +entries, which occupied lines 83 through 89 before the edit +(`Viewers\BreadcrumbDropDownHostTests.cs` at 83 through +`Viewers\BreadcrumbPendingOpenCloseTests.cs` at 89); the new entry takes line 83 and +shifts those seven down by one. + +QuickFiler.Test/QuickFiler.Test.csproj is non-SDK-style, so without this entry the +new test class would be silently not compiled and the P1-T11 assertion about it would +be vacuous. + +Output Summary: One `` entry added for the new test class; the +search reports exactly one matching line, at line 83. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t7-deactivate-suite-count.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t7-deactivate-suite-count.md new file mode 100644 index 000000000..f62dce569 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t7-deactivate-suite-count.md @@ -0,0 +1,32 @@ +# P1-T7 — QfcFormControllerDeactivateTests [TestMethod] count + +Timestamp: 2026-09-07T14-21 +Task: [P1-T7] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs -SimpleMatch "[TestMethod]").Count' +``` + +EXIT_CODE: 0 + +TESTMETHOD-COUNT: 8 + +## Derivation + +The P0-T12 baseline recorded the file at 248 physical lines declaring 7 +`[TestMethod]` members. This task added exactly one test method, +`FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField`, which asserts that +the pure `QfcFormController.FormatDeactivationDiagnostics` output contains the labels +`WebView2Focused=`, `ActiveFormNull=` and `Groups=` and the supplied group count. +7 + 1 = 8, and the measured count is 8. + +The test is MSTest, uses FluentAssertions for every assertion, creates no window, no +external process and no temporary file, and calls a pure static method with a fixed +argument tuple, so it is deterministic and order-independent. + +Output Summary: QfcFormControllerDeactivateTests declares 8 `[TestMethod]` members +after this task, matching the expected 7 + 1. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t8-csharpier.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t8-csharpier.md new file mode 100644 index 000000000..343d952a4 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t8-csharpier.md @@ -0,0 +1,99 @@ +# P1-T8 — CSharpier over QuickFiler and QuickFiler.Test + +Timestamp: 2026-09-07T14-22 +Task: [P1-T8] +Issue: #796 +Channel used: A + +Commands, in the order run: + +1. `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'` (before) +2. `pwsh -NoProfile -Command 'dotnet tool run csharpier format QuickFiler QuickFiler.Test; "EXIT_CODE=$LASTEXITCODE"'` +3. `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'` (after) +4. `pwsh -NoProfile -Command 'dotnet tool run csharpier check QuickFiler QuickFiler.Test; "EXIT_CODE=$LASTEXITCODE"'` + +FORMAT EXIT_CODE: 0 (not the acceptance evidence; see below) +CHECK EXIT_CODE: 0 + +Format stdout: `Formatted 326 files in 3198ms.` +Check stdout: `Checked 326 files in 2561ms.` with no file listed as unformatted. + +## Porcelain capture BEFORE the format run + +``` + M QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs + M QuickFiler.Test/QuickFiler.Test.csproj + M QuickFiler/Controllers/QfcFormController.Deactivate.cs + M QuickFiler/Controllers/QfcItemController.EventHandlers.cs + M QuickFiler/QuickFiler.csproj + M QuickFiler/Viewers/BreadcrumbDropDownHost.cs + M /plan.2026-09-06T21-59.md +?? QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs +?? QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs +?? /evidence/baseline/p0-t10-quickfiler-test-baseline.md +?? /evidence/baseline/p0-t11-coverage-baseline.md +?? /evidence/baseline/p0-t12-file-size-baseline.md +?? /evidence/baseline/p0-t13-mcp-validator-probe.md +?? /evidence/baseline/p0-t14-scope-baseline.md +?? /evidence/baseline/p0-t2-dotnet-sdk-install.md +?? /evidence/baseline/p0-t3-nuget-restore.md +?? /evidence/baseline/p0-t4-analyzer-version-skew.md +?? /evidence/baseline/p0-t5-dotnet-tool-restore.md +?? /evidence/baseline/p0-t6-dotnet-coverage-probe.md +?? /evidence/baseline/p0-t7-csharpier-check-baseline.md +?? /evidence/baseline/p0-t8-analyzer-rebuild-baseline.md +?? /evidence/baseline/p0-t9-nullable-rebuild-baseline.md +?? /evidence/baseline/phase0-instructions-read.md +?? /evidence/qa-gates/p1-t2-host-line-count.md +?? /evidence/qa-gates/p1-t3-compile-entry.md +?? /evidence/qa-gates/p1-t6-compile-entry.md +?? /evidence/qa-gates/p1-t7-deactivate-suite-count.md +``` + +28 entries. `` abbreviates +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796 +for width only; the captured text carried the full path. + +## Porcelain capture AFTER the format run + +Byte-identical to the capture above: the same 28 entries, in the same order, with the +same two-character status codes. No path changed status, none was added, and none was +removed. + +## What the two captures do and do not establish + +The exit code of the write-mode format invocation is identical on a clean run and on +a repairing one, which is why the before-and-after tree observation is the acceptance +evidence and the format exit code is not. + +Stated precisely, because the distinction matters: porcelain status cannot detect a +rewrite of a file that was ALREADY reported as `M`, since such a file stays `M` +whether or not the formatter touched it. Six of the seven `M` entries are Phase 1 +edits and were already `M` before the format run. What the identical captures do +establish is that the format run introduced no NEW modified path and left every +untracked path untracked — in particular it did not reformat any file outside the +Phase 1 write set into a modified state. + +The complementary evidence for the six already-modified files is the read-only check +invocation in step 4, which returned EXIT_CODE 0 over the same 326 files with no file +listed. A file the formatter had left unformatted would have been listed there. + +## Line counts after formatting + +| Path | Physical lines | +|---|---| +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 485 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | 79 | +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 132 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 278 | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 274 | +| QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | 49 | + +Measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of +evidence/baseline/p0-t12-file-size-baseline.md. Every file is under the 500-line +ceiling, and BreadcrumbDropDownHost.cs is unchanged at 485, so the P1-T2 band result +survives formatting. + +Output Summary: CSharpier check over QuickFiler and QuickFiler.Test returned +EXIT_CODE 0 across 326 files with none listed as unformatted. The porcelain captures +before and after the format run are identical. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t9-analyzer-rebuild.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t9-analyzer-rebuild.md new file mode 100644 index 000000000..61c2dff5d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p1-t9-analyzer-rebuild.md @@ -0,0 +1,74 @@ +# P1-T9 — Analyzer gate over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T14-23 +Task: [P1-T9] +Issue: #796 +Channel used: A + +RunStartedUtc: 2026-09-07T14:22:44.9396533Z + +Command: the P0-T8 command form with the log path +`TestResults\796\p1-t9\analyzer-rebuild.log`: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p1-t9\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## Build summary, verbatim + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:23.41 +``` + +## Comparison against the P0-T8 baseline + +| Total | P0-T8 baseline | P1-T9 | Verdict | +|---|---|---|---| +| Analyzer warnings | 0 | 0 | no greater than baseline | +| Analyzer errors | 0 | 0 | no greater than baseline | + +Baseline totals read from evidence/baseline/p0-t8-analyzer-rebuild-baseline.md. + +## Compiler-invocation counts read back from the detailed log + +Raw log (gitignored): TestResults/796/p1-t9/analyzer-rebuild.log + +CscTaskCount=36 +CscToolCount=36 + +Both greater than zero, so `CoreCompile` ran and the analyzers ran with it. + +## Assembly-freshness corroboration + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T14:22:56.5865620Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T14:23:01.5358378Z | yes | + +## What this run additionally establishes + +This is the run that proves the Phase 1 source compiles. It is the evidence cited by +the P1-T4 acceptance condition ("the solution compiles under the P0-T8 command form", +which is what proves both the cast and the forward are well typed) and by the P1-T5 +acceptance condition of the same wording. In particular it establishes that: + +- the new partial part `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` + compiles into the same `sealed partial class BreadcrumbDropDownHost` and reaches + the private fields `_disposed`, `_programmaticClose` and `_openLifetime`, the + internal `OpenState` property, and `DropDown.AutoClose`; +- the cast `(group.ItemController as QfcItemController)?.IsBreadcrumbSelectorOpen` is + well typed and yields `bool?`; +- the new internal member `QfcItemController.IsBreadcrumbSelectorOpen` forwards to + `IItemViewer.IsFolderDropDownOpen` without a type error; +- the new test class compiles against the two internal static formatters through the + `InternalsVisibleTo("QuickFiler.Test")` grant. + +Output Summary: EXIT_CODE 0 with 0 warnings and 0 errors, equal to the P0-T8 +baseline; 36 Csc task and 36 csc.exe tool invocations; both touched assemblies +rebuilt after RunStartedUtc. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t10-file-size.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t10-file-size.md new file mode 100644 index 000000000..32719d821 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t10-file-size.md @@ -0,0 +1,66 @@ +# P4-T10 — File-size gate for Phase 4 + +Timestamp: 2026-09-07T14-17 +Task: [P4-T10] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '@("QuickFiler\Controllers\QfcFormController.Deactivate.cs","QuickFiler\Interfaces\IQfcFormViewer.cs","QuickFiler\Viewers\QfcFormViewer.cs","QuickFiler\Viewers\ItemViewer.Breadcrumb.cs","QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +This is the idiom recorded on the `LINE-COUNT-IDIOM:` line of +evidence/baseline/p0-t12-file-size-baseline.md and no other. The prohibited +`Measure-Object -Line` idiom was not used. + +## Measured physical line counts + +| Path | Physical lines | Ceiling | Verdict | +|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 144 | 500 | within | +| QuickFiler/Interfaces/IQfcFormViewer.cs | 88 | 500 | within | +| QuickFiler/Viewers/QfcFormViewer.cs | 332 | 500 | within | +| QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 460 | 460 | at the plan ceiling, within | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 305 | 500 | within | + +Every recorded physical count is at most 500, and the count for +QuickFiler/Viewers/ItemViewer.Breadcrumb.cs is at most 460. + +## Pre-P4-T11 statement + +The count recorded above for `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is the +PRE-P4-T11 value. That file changes twice in this phase: once at P4-T8, which this measurement +covers, and once again at P4-T11, the documentation repair that follows this task. Task P4-T11 +appends its own re-measurement of that one path to THIS artifact under the heading +`POST-P4-T11 RE-MEASUREMENT:`. No second file-size artifact is created for the repair. + +Output Summary: five paths measured with the recorded idiom; all at most 500; the item viewer at +460 against its 460 ceiling; the deactivate handler figure is the pre-P4-T11 value. + +--- + +## POST-P4-T11 RE-MEASUREMENT: + +Timestamp: 2026-09-07T14-19 + +Command (same P0-T12 form, same recorded idiom): + +``` +pwsh -NoProfile -Command '(Get-Content -LiteralPath QuickFiler\Controllers\QfcFormController.Deactivate.cs).Count' +``` + +EXIT_CODE: 0 + +| Path | Pre-P4-T11 | Post-P4-T11 | Ceiling | Verdict | +|---|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 144 | 150 | 500 | within | + +The six-line increase is the net effect of a comment-only repair: the stranded ten-line block was +removed from one position and reinserted at another, which is line-neutral, and the corrected +`activeFormIsNull` parameter description is six lines longer than the refuted one it replaces. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t3-itemviewer-wiring.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t3-itemviewer-wiring.md new file mode 100644 index 000000000..7b0a3a31f --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t3-itemviewer-wiring.md @@ -0,0 +1,50 @@ +# P4-T3 — AC2 item-viewer wiring + +Timestamp: 2026-09-07T14-10 +Task: [P4-T3] +Issue: #796 +Channel used: A + +## Branch taken + +AC2-ITEMVIEWER-WIRING: REQUIRED + +Quoted from evidence/other/close-ordering-decision.md: + +> AC2-ITEMVIEWER-WIRING: REQUIRED + +and from the derivation recorded beneath that line: + +> The value must instead be supplied by the code that knows the popup is being opened, which is +> the wiring in `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` that already assigns +> `host.MayTakeFocus = MayRestoreBreadcrumbFocus;` at line 212. A popup-owns-activation +> assignment beside that existing assignment is therefore required rather than optional. + +The REQUIRED branch is taken. The assignment was added immediately beneath the existing +`host.MayTakeFocus = MayRestoreBreadcrumbFocus;` assignment, as one statement plus a three-line +reason comment. No constructor arity changed; the new state is reported to the owning form through +a settable-style registration method, matching the `MayTakeFocus` precedent of assigning after +construction. + +## Line-count gate + +Command: + +``` +pwsh -NoProfile -Command '(Get-Content -LiteralPath QuickFiler\Viewers\ItemViewer.Breadcrumb.cs).Count' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +| Path | Baseline | Measured after P4-T3 | Ceiling | Verdict | +|---|---|---|---|---| +| QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 456 | 460 | 460 | at ceiling, within gate | + +The count was re-measured after `dotnet tool run csharpier format QuickFiler QuickFiler.Test` +rewrote nothing in this file, so the recorded value is the post-format value and not a value the +formatter can still move. + +Output Summary: REQUIRED branch taken; the popup-owns-activation registration is in place beside +the may-take-focus assignment; the file measures 460 physical lines against the 460 ceiling. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t7-park-focus-decision.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t7-park-focus-decision.md new file mode 100644 index 000000000..c5c5785a9 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t7-park-focus-decision.md @@ -0,0 +1,50 @@ +# P4-T7 — Treatment of FormDeactivated_WebView2Focused_ParksFocusOnce + +Timestamp: 2026-09-07T14-13 +Task: [P4-T7] +Issue: #796 +Channel used: A + +## Branch taken + +NO branch. The test FormDeactivated_WebView2Focused_ParksFocusOnce is unchanged by this task, no +paired negative test is added, and the fix keeps focus parking unconditional. + +Quoted from evidence/other/close-ordering-decision.md: + +> AC2-PARK-FOCUS-SUPPRESSED: NO + +and from the derivation recorded beneath that line: + +> On Gesture A and on Gesture C it is False, so focus was not parked at all on those two gestures. +> Yet both gestures still cancelled an open selector — Gesture A on item 2 and Gesture C on item 4, +> each reported as `SelectorWasOpen=True` — and both still reached the close. Suppressing a step +> that did not execute cannot alter either outcome, so the observation supplies no case in which +> suppressing parking would have prevented the defect. + +## Consequences fixed by this branch + +- FormDeactivated_WebView2Focused_ParksFocusOnce is not modified. It is untouched in the diff for + this task. +- No paired `ParkFocusOffWebView2()` negative test is added. +- The line `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8` is deliberately ABSENT from this artifact, + so task P4-T8 scopes its guard to the cancel loop only and focus parking stays unconditional. +- The plan's expect-fail inventory therefore stands at four rows, not five, and task P9-T5 reads it + as four. + +## `[TestMethod]` count for the file + +Command: + +``` +pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs -SimpleMatch "[TestMethod]").Count' +``` + +EXIT_CODE: 0 +Measured count: 9 + +The count is unchanged from the value task P4-T4 recorded, which is the observable consequence of +this branch adding no test. + +Output Summary: NO branch taken; parking stays unconditional; no paired negative test added; the +suite holds 9 `[TestMethod]` members. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t8-guard-scope.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t8-guard-scope.md new file mode 100644 index 000000000..93e2c902a --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p4-t8-guard-scope.md @@ -0,0 +1,144 @@ +# P4-T8 — Scope of the AC2 guard in ParkFocusAndCancelSelectors + +Timestamp: 2026-09-07T14-15 +Task: [P4-T8] +Issue: #796 +Channel used: A + +## Branch taken + +CANCEL-LOOP ONLY. Focus parking stays unconditional. + +The branch is fixed by the line task P4-T7 recorded in +evidence/qa-gates/p4-t7-park-focus-decision.md. Quoted from that artifact: + +> - The line `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8` is deliberately ABSENT from this artifact, +> so task P4-T8 scopes its guard to the cancel loop only and focus parking stays unconditional. + +That line being absent is the condition the plan states for the cancel-loop-only branch, so the +guard was added immediately above the `foreach (QfcItemGroup group in groups)` cancel loop inside +the member `ParkFocusAndCancelSelectors`, and the `if (_formViewer?.IsWebView2Focused == true)` +focus-parking block above it is untouched. + +## What the diff for this task contains + +The whole diff for this task in `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is one +added guard plus its reason comment. In particular: + +- The per-item boundary `catch (Exception exception)` inside `ParkFocusAndCancelSelectors`, and the + `logger.Error` call that forms its whole body, are unchanged: neither appears in the diff. +- The AC6 instrumentation added by executed task P1-T4 is unchanged and still emitted at Debug + level: neither the entry `logger.Debug(FormatDeactivationDiagnostics(...))` call nor the per-item + `logger.Debug(FormatItemCancelDiagnostics(...))` call appears in the diff. +- The focus-parking block is unchanged: it does not appear in the diff. + +Verification command and result: + +``` +git diff -- QuickFiler/Controllers/QfcFormController.Deactivate.cs +``` + +EXIT_CODE: 0 + +The diff reports one hunk at the cancel-loop boundary, containing twelve added lines and no removed +or modified line. + +## Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p4-t8\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: Build succeeded. 0 Error(s). +Raw log (gitignored): TestResults/796/p4-t8/analyzer-rebuild.log + +The Failed-to-Passed transition of FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector +is measured by task P4-T9 and is that task's acceptance, not this one's. + +Output Summary: cancel-loop-only branch taken; focus parking unconditional; catch and AC6 log +statements unchanged; solution rebuilds clean. + +--- + +## P4-T11 DOC REPAIR: + +Timestamp: 2026-09-07T14-19 + +Measurement command (the position-independent form the plan specifies, which reads the maximal run +of consecutive `///` lines immediately above each of the two declarations): + +``` +pwsh -NoProfile -Command '$lines = Get-Content -LiteralPath QuickFiler\Controllers\QfcFormController.Deactivate.cs; function Block([int]$d) { $i = $d - 1; $b = @(); while ($i -ge 0 -and $lines[$i].TrimStart().StartsWith("///")) { $b = ,$lines[$i] + $b; $i-- }; return ,$b }; $p = ($lines | Select-String -SimpleMatch "internal void ParkFocusAndCancelSelectors()").LineNumber - 1; $f = ($lines | Select-String -SimpleMatch "internal static string FormatDeactivationDiagnostics(").LineNumber - 1; $bp = Block $p; $bf = Block $f; "Park-Summary=" + (@($bp | Select-String -SimpleMatch "").Count); "Park-791=" + (@($bp | Select-String -SimpleMatch "#791").Count); "Fmt-Summary=" + (@($bf | Select-String -SimpleMatch "").Count)' +``` + +EXIT_CODE: 0 + +Printed output: + +``` +Park-Summary=1 +Park-791=1 +Fmt-Summary=1 +``` + +Required: `Park-Summary=1`, `Fmt-Summary=1`, and `Park-791` at 1 or greater. All three met. + +### Diff scope + +Command: + +``` +git diff -U1 -- QuickFiler/Controllers/QfcFormController.Deactivate.cs +``` + +EXIT_CODE: 0 + +The diff for this task consists of three hunks, and every added, removed and modified line in all +three is a `///` comment line: the stranded pair removed from above +`FormatDeactivationDiagnostics`, the corrected `activeFormIsNull` parameter description, and the +same stranded pair reinserted immediately above `ParkFocusAndCancelSelectors`. The fourth hunk in +the file's diff against the branch HEAD is the AC2 guard landed by the earlier task P4-T8 and is not +part of this task's diff. No file outside the feature folder other than +`QuickFiler/Controllers/QfcFormController.Deactivate.cs` appears in this task's diff. + +### Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p4-t11\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: 0 Error(s). + +### DEVIATION RECORDED + +Deviation: this task also corrected the `` description on +`FormatDeactivationDiagnostics`, which is a change beyond the stranded-block move the task text +describes. + +What was wrong: the description asserted that "a null active form is corroborating evidence of a +self-inflicted deactivation and a non-null one of a genuine deactivation to a foreign window". The +manual observation this item produced measures the opposite on all four observations — +`ActiveFormNull=False` on each of the three self-inflicted popup gestures and `ActiveFormNull=True` +on the one deactivation attributed to focus leaving the form — as recorded under +`AC2-ITEMVIEWER-WIRING: REQUIRED` in evidence/other/close-ordering-decision.md. + +Why it was made here: the correction is comment-only and lands in the same file and the same task +whose whole subject is the `///` comments of that file, so it adds no path, no gate and no +executable change, and it stays inside this task's acceptance envelope that no line which is not a +`///` line may be added, removed or modified. Leaving a statement in shipped source that this +item's own evidence refutes was judged worse than the deviation. The deviation was authorised by +the executing directive for this phase run and is recorded here rather than left implicit. + +Scope of the deviation: one `` element in one file. It changes no behaviour, no signature +and no test. + +### POST-P4-T11 line count + +Appended to evidence/qa-gates/p4-t10-file-size.md under the heading +`POST-P4-T11 RE-MEASUREMENT:`, as the plan directs. No second file-size artifact was created. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t4-enforcement-site.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t4-enforcement-site.md new file mode 100644 index 000000000..5fcbe4d42 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t4-enforcement-site.md @@ -0,0 +1,59 @@ +# P5-T4 — AC3 enforcement site + +Timestamp: 2026-09-07T14-26 +Task: [P5-T4] +Issue: #796 +Channel used: A + +## Branch taken + +HOST. The commit-before-cancel ordering is enforced inside the member `FinishClose` in +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs`. + +Quoted from evidence/other/close-ordering-decision.md: + +> AC3-ENFORCEMENT-SITE: HOST + +and from the derivation recorded beneath that line: + +> The enforcement site is therefore the host, meaning `FinishClose` in +> `QuickFiler/Viewers/BreadcrumbDropDownHost.cs`. This decision leaves +> `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` with no required change; it +> remains in the write set as a bound, not as an obligation. + +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` was accordingly NOT modified by this +task. The COORDINATOR branch was not taken and no change was made there. + +## What changed in the host + +Two edits, both anchored on member names: + +1. Inside `FinishClose`, the cancel operation's condition gained the latch term, so an + `Uncommitted`-reason close does not cancel while a commit is in flight. The condition remains + conditional on the reason, so the suppression is scoped rather than global. +2. Inside `Close(BreadcrumbDropDownCloseReason reason)`, an `ExplicitCommit` reason now sets the + latch. That is the point at which a commit reaches this host, and it is the only in-scope + producer: the open coordinator reaches the host through `IBreadcrumbDropDownHost`, which this + item does not change, so it cannot set an internal property on the concrete host. + +Unchanged inside `FinishClose`, as the plan requires: the `DropDown.AutoClose = true` restore, which +remains the first operation, and the gated `FocusAnchorIfPermitted` argument, which remains the +last. The `MayTakeFocus` property default is likewise unchanged, still `() => true`. + +## Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p5-t4\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: Build succeeded. 0 Warning(s). 0 Error(s). +Raw log (gitignored): TestResults/796/p5-t4/analyzer-rebuild.log + +The transition of the two AC3 tests to Passed is measured by task P5-T7 and is that task's +acceptance rather than this one's. + +Output Summary: HOST branch taken; `FinishClose` consults the latch and `Close` sets it on an +explicit commit; the open coordinator is untouched; the solution rebuilds clean. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t5-html-listener.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t5-html-listener.md new file mode 100644 index 000000000..91a7652d1 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t5-html-listener.md @@ -0,0 +1,58 @@ +# P5-T5 — Row activation listener in FolderBreadcrumb.html + +Timestamp: 2026-09-07T14-27 +Task: [P5-T5] +Issue: #796 +Channel used: A + +## Branch taken + +AC3-HTML-POINTERDOWN: NOT REQUIRED + +NOT APPLICABLE. The row activation listener at QuickFiler/Resources/FolderBreadcrumb.html lines +289-291 is NOT moved from the `click` event to a pointer-down event, and the file is left unchanged +by this task. + +Quoted from evidence/other/close-ordering-decision.md: + +> AC3-HTML-POINTERDOWN: NOT REQUIRED + +and from the derivation recorded beneath that line: + +> The value REQUIRED is admissible only when the Gesture C transcript shows that no activation +> message was produced. It does not, and it cannot: the transcript is silent on activation +> altogether, and its silence carries no information. + +and: + +> The page `QuickFiler/Resources/FolderBreadcrumb.html` is therefore not changed by this item, and +> the sibling contention recorded against it does not need to be exercised. + +## Consequences + +- The page is untouched, so the recorded sibling contention with the concurrent item that owns the + row text projection in this same page is not exercised at all. +- The `selectorActivate` post count assertion the REQUIRED branch would have run does not apply, + because that assertion is stated by the plan only for the REQUIRED branch. + +## Verification that the file is unchanged + +Command: + +``` +git status --porcelain QuickFiler/Resources/FolderBreadcrumb.html +``` + +EXIT_CODE: 0 +Output: empty. The file carries no working-tree modification, which is the observable form of the +NOT REQUIRED branch. + +## Recorded limitation, carried forward from the decision record + +This branch records that the evidence does not support the page change, not that the page change +has been shown unnecessary. If the AC2 seam lands and a row click still fails to select, the +question is reopened, and settling it then requires instrumenting the activation path rather than +re-reading the Phase 2 transcript. + +Output Summary: NOT REQUIRED branch taken; FolderBreadcrumb.html unchanged and verified unchanged +by porcelain status. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t8-file-size.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t8-file-size.md new file mode 100644 index 000000000..53ca6fb2d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p5-t8-file-size.md @@ -0,0 +1,42 @@ +# P5-T8 — File-size gate for Phase 5 + +Timestamp: 2026-09-07T14-31 +Task: [P5-T8] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '@("QuickFiler\Viewers\BreadcrumbDropDownHost.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Diagnostics.cs","QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs","QuickFiler.Test\Viewers\BreadcrumbDropDownCloseOrderingTests.cs","QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +This is the idiom recorded on the `LINE-COUNT-IDIOM:` line of +evidence/baseline/p0-t12-file-size-baseline.md and no other. The prohibited +`Measure-Object -Line` idiom was not used. + +## Measured physical line counts + +| Path | Physical lines | Ceiling | Verdict | +|---|---|---|---| +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 496 | 500 | within | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 131 | 500 | within | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | 79 | 500 | within | +| QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs | 395 | 500 | within | +| QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | 253 | 500 | within | +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 413 | 500 | within | + +Every recorded physical count is at most 500. + +The main host part stands at 496 against the 500 ceiling, four lines of headroom. That is the file +executed task P1-T2 relieved by relocating `OnDropDownClosed` into the diagnostics part; the two +AC3 edits this phase made to it — the latch term inside `FinishClose` and the latch producer inside +`Close` — consumed eleven of the fifteen lines that relocation bought. The coordinator is unchanged +at its baseline 395, which is the observable form of the HOST enforcement-site decision. + +Output Summary: six paths measured with the recorded idiom; all at most 500; the main host part at +496 with four lines of headroom. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t1-ac4-seam.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t1-ac4-seam.md new file mode 100644 index 000000000..b6fad1b6c --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t1-ac4-seam.md @@ -0,0 +1,78 @@ +# P6-T1 — AC4 seam declaration + +Timestamp: 2026-09-07T14-34 +Task: [P6-T1] +Issue: #796 +Channel used: A + +## Branch taken + +AC4-NEW-MEMBER: REQUIRED + +Quoted from evidence/other/close-ordering-decision.md: + +> AC4-NEW-MEMBER: REQUIRED + +and: + +> AC4-MECHANISM: SearchOwnedDismissalLatch + +and from the derivation recorded beneath them: + +> The existing `_searchLeaveHandoffPending` field cannot carry this meaning. It is consumed +> destructively on its first read, by design [...] so it is false again immediately after the +> handoff it guards, while the popup is still open. Provenance must persist for as long as the +> popup is open, which is a different lifetime. Overloading the one-shot field would break the #680 +> contract that the plan requires be preserved. + +The REQUIRED branch is taken. The member was declared in +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, beside the existing one-shot latch, as +the private boolean field `_searchOwnedDismissal` together with the internal get-only accessor +`SearchOwnsDropDownDismissal` that reads it. No interface changed and no public surface changed: +the field is private and the accessor is internal on an internal partial class, matching the +precedent executed task P1-T4 set in this same file with `IsBreadcrumbSelectorOpen`. + +## What this task deliberately did NOT do + +The suppression behaviour is not present. `TextBoxSearch_Leave` is unchanged, so it still dismisses +regardless of provenance, and the Phase 6 fail-before test therefore fails at its assertion rather +than at compilation. The producers are also not yet written; both land at task P6-T5. + +The exact qualifier the plan requires: this task left +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` unchanged EXCEPT for the declaration +described above. An unqualified unchanged-file claim would be false, because executed task P1-T4 +already added the observational selector-open forward to this same file. + +The existing structures the mechanism extends are all unchanged by this task: the one-shot latch +`_searchLeaveHandoffPending`, its single producer in the `Keys.Down` branch, and its single +read-and-clear consumer in `TextBoxSearch_Leave`. + +## Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p6-t1\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: 1 Warning(s). 0 Error(s). +Raw log (gitignored): TestResults/796/p6-t1/analyzer-rebuild.log + +## The one warning, recorded rather than hidden + +``` +QuickFiler\Controllers\QfcItemController.EventHandlers.cs(201,22): warning CS0649: Field 'QfcItemController._searchOwnedDismissal' is never assigned to, and will always have its default value false +``` + +This is the expected and intended consequence of a task whose whole purpose is to declare the seam +without writing to it. It is transient: task P6-T5 adds the two producers, after which the field is +assigned and the diagnostic has no subject. It is recorded here so that the intermediate state is +auditable and so a later reader does not mistake it for drift. + +The warning does not fail this gate. This task's stated acceptance is that the solution compiles +under the P0-T8 command form, and it does, with EXIT_CODE 0. Whether the warning has in fact cleared +is re-measured at task P6-T5 and recorded in that task's evidence rather than predicted here. + +Output Summary: REQUIRED branch taken; the SearchOwnedDismissalLatch member is declared with no +suppression behaviour; the solution compiles with one expected transient CS0649. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t3-compile-entry.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t3-compile-entry.md new file mode 100644 index 000000000..26f7de0c7 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p6-t3-compile-entry.md @@ -0,0 +1,60 @@ +# P6-T3 — Compile entry for the AC4 latch test file + +Timestamp: 2026-09-07T14-38 +Task: [P6-T3] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command 'Select-String -Path QuickFiler.Test\QuickFiler.Test.csproj -SimpleMatch "QfcItemController.SearchLeaveLatchTests.cs"' +``` + +EXIT_CODE: 0 + +Matching lines: 1 + +``` +L159: +``` + +Exactly one matching line, as the gate requires. The entry was placed alongside the existing +Controllers entries, immediately after the entry for Controllers\QfcFormControllerDeactivateTests.cs +which stood at line 158, so that entry now stands at 158 and the new one at 159. + +The project is non-SDK-style, so without this entry the new file would be silently not compiled and +the fail-before evidence in task P6-T4 would be vacuous. Task P6-T4 additionally fails rather than +passes if its run reports a Total of 0, which is the independent check that the entry took effect. + +Diff scope for this task: + +``` +git diff --stat -- QuickFiler.Test/QuickFiler.Test.csproj +``` + +``` + QuickFiler.Test/QuickFiler.Test.csproj | 1 + + 1 file changed, 1 insertion(+) +``` + +One inserted line and nothing else. + +## Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p6-t3\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: 1 Warning(s). 0 Error(s). +Raw log (gitignored): TestResults/796/p6-t3/analyzer-rebuild.log + +The single warning is the same transient CS0649 recorded at task P6-T1 against the not-yet-assigned +`_searchOwnedDismissal` field. It is unrelated to this task's edit, which is one line in a project +file, and it clears when task P6-T5 adds the producers. + +Output Summary: exactly one compile entry for the new test file, at line 159; the solution rebuilds +clean apart from the transient CS0649 carried from P6-T1. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p7-t4-ac5-exclusion.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p7-t4-ac5-exclusion.md new file mode 100644 index 000000000..cac8e7296 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p7-t4-ac5-exclusion.md @@ -0,0 +1,89 @@ +# P7-T4 — AC5 exclusion gate + +Timestamp: 2026-09-07T14-55 +Task: [P7-T4] +Issue: #796 +Channel used: A + +## Commands + +``` +pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796' +pwsh -NoProfile -Command 'git diff --cached --name-status d78ae7f7' +pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all' +``` + +EXIT_CODE: 0 for all three. + +The staging span accompanies the name-listing diff because such a diff cannot see an untracked +file, and this item creates two. + +## Anchor + +The anchor is d78ae7f7, the second merge commit, and deliberately not c7ae69f1. Anchored at +c7ae69f1 this diff would enumerate the 59 files the second merge of origin/main brought in, several +of which lie under UtilitiesCS/ and UtilitiesCS.Test/, and this gate's acceptance requires that no +such path be listed — so the gate would fail on work this item did not do. + +## Result of `git diff --cached --name-status d78ae7f7` + +Code and project paths listed (12): + +| Status | Path | Write-set entry | +|---|---|---| +| M | QuickFiler/Controllers/QfcFormController.Deactivate.cs | 1 | +| M | QuickFiler/Interfaces/IQfcFormViewer.cs | 2 | +| M | QuickFiler/Viewers/QfcFormViewer.cs | 3 | +| M | QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 4 | +| M | QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 5 | +| M | QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 6 | +| M | QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 7 | +| M | QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 11 | +| M | QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 12 | +| M | QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | 13 | +| A | QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs | 14 | +| M | QuickFiler.Test/QuickFiler.Test.csproj | 16 | + +Every one of the twelve is a member of the sixteen-path permitted-change set recorded in +evidence/baseline/p0-t14-scope-baseline.md. Four write-set entries do not appear, each for a stated +reason: entry 10, QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs, and entry 15, +QuickFiler/QuickFiler.csproj, were both changed by Phase 1, whose commit is an ancestor of the +anchor, so they carry no delta against it; entry 8, +QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs, is untouched because the decision record +records `AC3-ENFORCEMENT-SITE: HOST`; and entry 9, QuickFiler/Resources/FolderBreadcrumb.html, is +untouched because the decision record records `AC3-HTML-POINTERDOWN: NOT REQUIRED`. + +All remaining listed paths lie inside the feature folder +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796. + +## The acceptance condition + +No path in the diff output begins with `UtilitiesCS/`. No path begins with `UtilitiesCS.Test/`. + +## Result of `git status --porcelain --untracked-files=all` + +``` +M QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs +M QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p7-t3-ac1-ac5-guards.md +``` + +Three entries, all staged. Two are write-set paths (entries 12 and 13) and one is inside the +feature folder. No path in this output begins with `UtilitiesCS/` or `UtilitiesCS.Test/` either. + +## Pre-existing dirty set + +The `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md is EMPTY, so +this gate is strict: no path was excluded from either output on that basis, and none needed to be. + +## Why the exclusion is what makes AC5 meaningful + +AC5 is the issue #438 AC-3 regression guard: a row-set refresh while the selector is open must not +close the list. It is satisfied by leaving the session-preserving replacement path in UtilitiesCS +untouched, which this gate is the proof of. The guard test +RowSetRefreshWhileOpen_NeverClosesHost, recorded Passed at task P7-T3, observes that untouched path +rather than a modified one. + +Output Summary: 12 code paths changed, all inside the sixteen-path write set; no path under +UtilitiesCS/ or UtilitiesCS.Test/ in either the anchored name-status diff or the porcelain status; +pre-existing dirty set empty so no exclusions were applied. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t1-csharpier-format.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t1-csharpier-format.md new file mode 100644 index 000000000..63ac238f4 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t1-csharpier-format.md @@ -0,0 +1,92 @@ +# P9-T1 — Final CSharpier format pass + +Timestamp: 2026-09-07T15-04 +Task: [P9-T1] +Issue: #796 +Channel used: A + +## Branch taken + +The verdict line quoted verbatim from evidence/baseline/p0-t7-csharpier-check-baseline.md: + +``` +CSHARPIER-BASELINE: CLEAN +``` + +Branch taken: the REPO-WIDE branch. A repo-wide pass on a clean baseline cannot reformat +a file outside this plan's write set, so the scoped QuickFiler and QuickFiler.Test form +was not used and no pre-existing drift is recorded out of scope. + +Command: + +``` +pwsh -NoProfile -Command 'dotnet tool run csharpier format .; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Formatter stdout, verbatim: + +``` +Formatted 1608 files in 7156ms. +``` + +The exit code of a write-mode invocation is identical on a clean run and on a repairing +one, so it is not the acceptance evidence. The before-and-after tree observation below is. + +## `git status --porcelain --untracked-files=all` immediately BEFORE the format + +``` + M QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md +``` + +## `git status --porcelain --untracked-files=all` immediately AFTER the format + +``` + M QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md +``` + +## Paths appearing only in the AFTER capture + +``` +NONE +``` + +The two captures are identical, so the repo-wide pass rewrote no file that was clean +before it. The Phase 4 through Phase 7 sources were already formatted by their own +in-phase CSharpier tasks and this pass left them alone. + +## The one already-modified file, checked separately + +Porcelain cannot distinguish "unchanged" from "rewritten" for a file that was already +`M` before the pass, because its status code is `M` in both captures. The one such .cs +file is the P8-T1 edit, so it was checked by diff shape rather than by status code: + +``` +git diff --numstat -- QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +7 0 QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +``` + +Seven added and none removed, identical to the reading taken at P8-T1 before this pass, +so the formatter did not rewrite that file either. The single added Arrange statement is +still one physical line under the 100-column default print width. + +Every path present after the pass is inside the feature folder or is the write-set path +`QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs`. + +Output Summary: baseline verdict CLEAN, so the repo-wide branch was taken; EXIT_CODE 0; +1608 files processed; the before and after porcelain captures are identical with no path +appearing only in the after capture; and the one already-modified .cs file is provably +unrewritten by its unchanged diff shape. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t10-scope-boundary.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t10-scope-boundary.md new file mode 100644 index 000000000..5c13b18a1 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t10-scope-boundary.md @@ -0,0 +1,130 @@ +# P9-T10 — Scope-boundary gate over the committed diff + +Timestamp: 2026-09-07T16-11 +Task: [P9-T10] +Issue: #796 +Channel used: A + +## Commands + +``` +pwsh -NoProfile -Command 'git diff --name-status a6b259160f9ac1fbe251708d897fd4721486259e..HEAD' +pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all' +``` + +Both EXIT_CODE: 0. + +HEAD at the time of this gate is 676966ef2d36e20f231cfb3949591d44a9adb92d, the P9-T9 +commit. + +## The anchor + +The diff is anchored to the explicit base ref +a6b259160f9ac1fbe251708d897fd4721486259e, written as the full forty-character SHA. An +unanchored diff compares the worktree against the index and would pass vacuously now that +the change is committed, which is why an explicit ref operand is used. The porcelain span +accompanies it because the two mechanisms are complementary and each alone is blind in +one state: the anchored diff cannot see an untracked file, and porcelain status goes +empty once a change is committed. + +The anchor's ancestry was verified rather than assumed, because a two-dot diff from a +non-ancestor would not measure this item's footprint: + +``` +pwsh -NoProfile -Command 'git merge-base --is-ancestor a6b259160f9ac1fbe251708d897fd4721486259e HEAD' +``` + +EXIT_CODE: 0, which is the ancestor-true result. That commit is the origin/main commit +the third merge brought in, so a two-dot diff from it yields exactly this item's own +additions over main with no sibling's merged work re-billed to this item, and it is +precisely the pull request footprint. + +## Result + +TOTAL PATHS LISTED: 84. + +| Class | Count | +|---|---| +| Inside the feature folder docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796 | 68 | +| One of the seventeen write-set paths | 15 | +| The single permitted path outside both | 1 | +| Any other path | 0 | +| Paths under .claude/, .codex/, .agents/, config/, .github/, UtilitiesCS/ or UtilitiesCS.Test/, or naming TaskMaster.sln or a repository-root build property file | 0 | + +68 + 15 + 1 = 84, so every listed path is accounted for by exactly one class and none is +double-counted or unclassified. + +The classification was computed mechanically from the diff output against the literal +seventeen-path write set, the feature-folder path prefix, and the one permitted path, +rather than read by eye. + +### The fifteen write-set paths listed + +``` +M QuickFiler/Controllers/QfcFormController.Deactivate.cs +M QuickFiler/Controllers/QfcItemController.EventHandlers.cs +M QuickFiler/Interfaces/IQfcFormViewer.cs +M QuickFiler/QuickFiler.csproj +A QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs +M QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs +M QuickFiler/Viewers/BreadcrumbDropDownHost.cs +M QuickFiler/Viewers/ItemViewer.Breadcrumb.cs +M QuickFiler/Viewers/QfcFormViewer.cs +M QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs +M QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +A QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs +M QuickFiler.Test/QuickFiler.Test.csproj +A QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs +M QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs +``` + +Fifteen of the seventeen. The two write-set paths the diff does NOT list are +QuickFiler/Resources/FolderBreadcrumb.html and +QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs, neither of which was changed. +Their absence does not fail this gate: the acceptance is an upper bound on which paths +may appear and not a lower bound on how many must appear, and no clause requires any +particular path to be listed. The HTML file's absence additionally matches the +`AC3-HTML-POINTERDOWN: NOT REQUIRED` decision recorded at P5-T5. + +### The single permitted path outside the feature folder and the write set + +``` +A docs/features/potential/promoted/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select.md +``` + +Exactly one such path was listed, and it is the one the acceptance clause permits by +name. It is the promoted potential record this item's own promotion created, and the +promotion lifecycle places that record outside the feature folder by construction. It is +deliberately not a member of the `## Write Set` section, whose count stays at seventeen, +because no task in this plan writes it and a backticked path would be read by downstream +blast-radius derivation as a write claim, which it is not. + +## Porcelain span + +``` + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md +``` + +Two paths, both inside the feature folder: the plan file carrying the P9-T9 check-off, +and the P9-T9 evidence artifact, which records a SHA that did not exist until the P9-T9 +commit had been made and therefore could not have been written before it. No path under +QuickFiler, under QuickFiler.Test, or anywhere else in the tree is untracked or modified. +The P9-T11 amend folds both into the final commit. + +## Acceptance clause by clause + +| Clause | Observed | Met | +|---|---|---| +| every listed path is inside the feature folder, or is one of the seventeen write-set paths, or is the single permitted promoted-record path | 68 + 15 + 1 = 84 of 84; 0 unclassified | yes | +| no listed path lies under the .claude, .codex or .agents trees, under config, or under .github | 0 such paths | yes | +| no listed path names TaskMaster.sln or a repository-root build property file | 0 such paths | yes | +| no path under UtilitiesCS/ or UtilitiesCS.Test/ is listed | 0 such paths | yes | + +Output Summary: the anchored two-dot diff from a6b259160f9ac1fbe251708d897fd4721486259e, +whose ancestry of HEAD was verified, lists 84 paths: 68 inside this item's feature folder, +15 write-set paths, and exactly one path outside both, the promoted potential record the +acceptance clause permits by name. Zero paths fall outside those three classes and zero +lie under any prohibited tree. The accompanying porcelain span lists two paths, both +inside the feature folder and both explained by the plan. All four acceptance clauses are +met. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t11-final-loop.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t11-final-loop.md new file mode 100644 index 000000000..f91d42213 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t11-final-loop.md @@ -0,0 +1,242 @@ +# P9-T11 — Closing the QA loop for the execution worktree + +Timestamp: 2026-09-07T16-12 +Task: [P9-T11] +Issue: #796 +Channel used: A + +## Branch taken + +RESTART BRANCH. The loop was restarted at P9-T1 and this artifact records the SECOND +PASS. + +The trigger is stated plainly rather than reasoned away: task **P9-T3 attempt 1 FAILED**. +Its acceptance clause compares the build-summary warning total against the P0-T8 baseline +of 0, and attempt 1 reported 3. The three warnings were `MSB3061` file-lock warnings +raised by the `CoreClean` target of TaskMaster/TaskMaster.csproj because a running +Microsoft Outlook process held three native DLLs under TaskMaster/bin/Debug open. That +cause was environmental and outside this item's diff, and it was cleared by closing +Outlook, after which P9-T3 attempt 2 met all four clauses. Both attempts are retained in +evidence/qa-gates/p9-t3-analyzer-rebuild.md. + +The environmental character of the failure does not change the branch. This task's +condition is "if any of P9-T1 through P9-T8 failed", and P9-T3 attempt 1 failed, so the +restart branch is the correct one and the single-clean-pass branch is not available. The +second pass below was executed in full, in toolchain order, after the cause was cleared, +so that the recorded clean pass contains no failing step anywhere inside it. + +No task in P9-T1 through P9-T8 changed a tracked file; the restart is triggered by the +failure alone. + +## The four commands of the final clean pass, in order + +### 1. Format + +``` +pwsh -NoProfile -Command 'dotnet tool run csharpier format .' +``` + +EXIT_CODE: 0 + +``` +Formatted 1608 files in 3370ms. +``` + +The repo-wide branch was used, matching the branch P9-T1 recorded, which is correct +because evidence/baseline/p0-t7-csharpier-check-baseline.md carries +`CSHARPIER-BASELINE: CLEAN`. + +A write-mode formatter exits 0 whether or not it rewrote anything, so the exit code is +not the evidence. The evidence is the before-and-after tree observation: + +Before: + +``` + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t10-scope-boundary.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md +``` + +After: identical, path for path. + +No path appears only in the after capture, and no `.cs` file carries an `M` status in +either capture. That second point removes the ambiguity P9-T1 had to resolve separately +by diff shape: at P9-T1 one `.cs` file was already `M` before the pass, so porcelain could +not distinguish "unchanged" from "rewritten" for it. Here every tracked `.cs` file was +clean before the pass and every one is clean after it, so an unchanged status code is +conclusive on its own. + +Verification, read-only: + +``` +pwsh -NoProfile -Command 'dotnet tool run csharpier check .' +``` + +EXIT_CODE: 0 + +``` +Checked 1608 files in 6457ms. +``` + +The check reported no unformatted file. + +### 2. Lint — .NET analyzers + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p9-t11\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +RunStartedUtc: 2026-09-07T20:11:13.6702864Z +EXIT_CODE: 0 + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:18.82 +``` + +CscTaskCount=36, CscToolCount=36. The gate is not vacuous: `/t:Rebuild` was used, not +`/t:Build`, and thirty-six compiler invocations were recorded in the detailed log. Zero +warnings, so the three `MSB3061` file-lock warnings of P9-T3 attempt 1 are gone and did +not recur. + +### 3. Type check — nullable analysis + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\796\p9-t11\nullable-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +RunStartedUtc: 2026-09-07T20:11:38.4358523Z +EXIT_CODE: 0 + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:16.68 +``` + +CscTaskCount=36, CscToolCount=36. The command line carries no `/p:Nullable=enable` token. + +### 4. Test + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" /ResultsDirectory:TestResults\796\p9-t11 "/Logger:trx;LogFileName=p9-t11.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +``` +Test Run Successful. +Total tests: 1380 + Passed: 1380 + Total time: 14.9534 Seconds +``` + +1380 of 1380 passed, reproducing the P9-T5 result exactly. No `Failed:` line was printed, +so the Failed set is empty. + +The assemblies under test are the ones step 3 produced: +QuickFiler/bin/Debug/QuickFiler.dll at 2026-09-07T20:11:46.4983058Z and +QuickFiler.Test/bin/Debug/QuickFiler.Test.dll at 2026-09-07T20:11:50.2586690Z, both after +that step's RunStartedUtc and both before this run. + +## Second-pass result + +All four steps passed in a single pass, in order, and none of them changed a tracked +file. The loop therefore terminates here and no third pass is required. + +Raw logs and the TRX are at the gitignored paths TestResults/796/p9-t11/ and are not +committed; a TRX embeds the host account name and machine name in its `runUser` and +`computerName` attributes. + +## Terminal porcelain reading + +Taken immediately before the amend, after this artifact and this task's check-off in the +plan file had both been written. Recorded below after observation, not predicted. + +Command, on the recorded command channel A: + +``` +pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all' +``` + +Observed output: + +``` + M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t10-scope-boundary.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t11-final-loop.md +?? docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md +``` + +Four paths, every one inside the feature folder +docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796: + +| Path | Why it is present at this reading | +|---|---| +| plan.2026-09-06T21-59.md | carries the P9-T10 and P9-T11 check-offs, written after the P9-T9 commit | +| evidence/qa-gates/p9-t9-final-commit.md | records a SHA that did not exist until the P9-T9 commit had been made | +| evidence/qa-gates/p9-t10-scope-boundary.md | written after that commit, because it measures it | +| evidence/qa-gates/p9-t11-final-loop.md | this artifact | + +The `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md is +EMPTY, so this gate admits no exception outside the feature folder and is strict. It +passes because no path lies outside the feature folder: no path under QuickFiler, none +under QuickFiler.Test, and none anywhere else in the tree is modified or untracked. + +A terminal gate demanding zero porcelain lines is not used here, and neither is one +permitting only the plan file and this task's own artifact, because all four paths above +are necessarily untracked or modified at this reading. The amend below is what folds them +into the final commit. + +## Amend + +``` +pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796' +pwsh -NoProfile -Command 'git commit --amend --no-edit' +``` + +The staging form is the P9-T9 pathspec form. `--no-edit` keeps the P9-T9 commit message +unchanged, which is why no acceptance clause asserts over the amended message body. + +The resulting SHA is deliberately NOT recorded in this artifact. An artifact that is +itself staged by the amend cannot carry the hash of the commit that contains it: writing +the hash changes the file, which changes the tree, which changes the hash. That is a +fixpoint with no solution, not an omission. The amended SHA is instead observable +directly from `git rev-parse HEAD` after the amend, and the P9-T9 artifact records the +pre-amend SHA 676966ef2d36e20f231cfb3949591d44a9adb92d for the audit trail. + +For the same reason the amend was applied twice. The first application folded in the +P9-T9, P9-T10 and P9-T11 artifacts and the plan check-offs, exactly as the terminal +porcelain reading above enumerates. It then became apparent that this section of this +artifact still held an unresolved placeholder, so the placeholder was replaced with the +paragraph above and a second `git commit --amend --no-edit` was applied to fold that +correction in. Both applications used `--no-edit`, so the commit message is unchanged by +either, and the branch still carries exactly one commit for this work rather than two. + +The terminal porcelain reading recorded above is the one this task's acceptance clause +names: it was taken immediately before the amend, after this artifact and this task's +check-off in the plan file had both been written, and it enumerates the four paths that +amend folds in. The second application carries only the correction to this paragraph. + +## Acceptance clause by clause + +| Clause | Observed | Met | +|---|---|---| +| the artifact names which of the two branches was taken | RESTART BRANCH, with the P9-T3 attempt 1 failure named as the trigger | yes | +| it records the four commands of the final clean pass in order | format, lint, type-check, test, all recorded above in that order, all EXIT_CODE 0 | yes | +| the porcelain reading is taken immediately before the amend, after this task's own artifact and check-off have been written | artifact written, then plan check-off written, then the reading taken | yes | +| it lists no path other than members of the `PRE-EXISTING-DIRTY-SET:` and paths inside the feature folder | 4 paths, all inside the feature folder; the pre-existing set is empty and needed no exception | yes | + +Output Summary: the RESTART branch was taken because P9-T3 attempt 1 failed on three +environmental `MSB3061` file-lock warnings. The second pass ran format, lint, type-check +and test in order and all four returned EXIT_CODE 0 with no tracked file changed: 1608 +files formatted and checked clean, two `/t:Rebuild` builds at 0 warnings and 0 errors with +36 Csc invocations each, and 1380 of 1380 tests passing. The terminal porcelain reading +lists four paths, all inside the feature folder, and the amend folds them into the final +commit. + diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t2-csharpier-check.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t2-csharpier-check.md new file mode 100644 index 000000000..ca3ce2d70 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t2-csharpier-check.md @@ -0,0 +1,40 @@ +# P9-T2 — Final CSharpier check + +Timestamp: 2026-09-07T15-05 +Task: [P9-T2] +Issue: #796 +Channel used: A + +## Scope + +Task P9-T1 took the REPO-WIDE branch, because evidence/baseline/p0-t7-csharpier-check-baseline.md +records `CSHARPIER-BASELINE: CLEAN`. This verification therefore runs over the same +repo-wide scope, not the QuickFiler and QuickFiler.Test scoped form. + +Command: + +``` +pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Stdout, verbatim: + +``` +Checked 1608 files in 5812ms. +``` + +## Full list of files the check reported as unformatted + +``` +(empty) +``` + +The check reported no file. CSharpier prints one line per unformatted file before its +summary line; the output above carries the summary line and nothing else, so the list is +empty rather than unread. The count of files checked, 1608, equals the count the P9-T1 +format pass reported, so the two invocations covered the same scope. + +Output Summary: EXIT_CODE 0 over 1608 files repo-wide with an empty unformatted-file +list. Formatting is the first stage of the final toolchain pass and it is clean. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t3-analyzer-rebuild.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t3-analyzer-rebuild.md new file mode 100644 index 000000000..f0bbcd39f --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t3-analyzer-rebuild.md @@ -0,0 +1,146 @@ +# P9-T3 — Final analyzer gate over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T15-07 +Task: [P9-T3] +Issue: #796 +Channel used: A + +Command: the P0-T8 command form with the log path +`TestResults\796\p9-t3\analyzer-rebuild.log`. + +## Attempt 1 — GATE NOT MET + +RunStartedUtc: 2026-09-07T19:05:46.6776541Z + +EXIT_CODE: 0 + +Build summary, verbatim: + +``` + 3 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:20.95 +``` + +CscTaskCount=36 +CscToolCount=36 + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T19:05:56.8789568Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T19:06:01.3438632Z | yes | + +Comparison against the P0-T8 baseline recorded in +evidence/baseline/p0-t8-analyzer-rebuild-baseline.md: + +| Total | P0-T8 baseline | P9-T3 attempt 1 | Verdict | +|---|---|---|---| +| Warnings | 0 | 3 | GREATER THAN BASELINE — gate not met | +| Errors | 0 | 0 | no greater than baseline | + +Three of the four acceptance clauses are met — exit code 0, both compiler-invocation +counts at 36, and both touched assemblies rebuilt after RunStartedUtc. The warning-total +clause is not, so this attempt does not satisfy the task. + +### The three warnings, and why they are not this item's + +All three are the same diagnostic, `MSB3061`, raised by the `CoreClean` target of +TaskMaster/TaskMaster.csproj. Each reports that a file under TaskMaster/bin/Debug could +not be deleted because a running Microsoft Outlook process holds it open. The three +files are `runtimes/win-x64/native/WebView2Loader.dll`, `x64/leptonica-1.82.0.dll` and +`x64/tesseract50.dll`. None is an analyzer diagnostic, none names a source file, none +arises in QuickFiler or QuickFiler.Test, and none is reachable from this item's diff. + +The interference began during execution and is timestamped. The Outlook process that +holds the locks reports `StartTime` 2026-09-07 14:59:58 local, which is +2026-09-07T18:59:58 UTC. That instant falls between two runs of the identical command +form: + +| Run | RunStartedUtc | Warnings | +|---|---|---| +| P8-T1 rebuild | 2026-09-07T18:59:16.8282314Z | 0 | +| Outlook process 47952 starts | 2026-09-07T18:59:58 | — | +| P8-T2 rebuild | 2026-09-07T19:00:52.2216349Z | 3 | +| P9-T3 attempt 1 | 2026-09-07T19:05:46.6776541Z | 3 | + +The last clean run of this command form preceded the Outlook start by 42 seconds and the +first warning-bearing run followed it by 54 seconds, with no change to any tracked file +in between. That places the cause outside this item's change with a measured boundary +rather than an assertion. + +The gate was not weakened to accommodate this. `MSB3061` is a file-lock warning rather +than an analyzer diagnostic, but the acceptance clause compares build-summary totals, and +the build-summary total is 3. Execution therefore paused here and the condition was +reported for resolution rather than reinterpreted. + +## Attempt 2 — GATE MET + +Timestamp: 2026-09-07T15-54 +Channel used: A + +The environmental cause recorded above was cleared by the maintainer: the Outlook +process that held the three files under TaskMaster/bin/Debug was closed. That was +verified before this attempt rather than assumed, with +`pwsh -NoProfile -Command "@(Get-Process -Name OUTLOOK -ErrorAction SilentlyContinue).Count"`, +which printed `0`. No Outlook process was started by this attempt, and none is required +by it. + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p9-t3\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +RunStartedUtc: 2026-09-07T19:54:22.1490275Z + +EXIT_CODE: 0 + +Build summary, verbatim: + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:17.92 +``` + +Compiler-invocation counts read back from the detailed log with the P0-T8 read-back +command form, against the raw log (gitignored, `.gitignore` line 84 ignores `*.log`) +TestResults/796/p9-t3/analyzer-rebuild.log: + +CscTaskCount=36 +CscToolCount=36 + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T19:54:31.8208498Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T19:54:35.2016372Z | yes | + +Comparison against the P0-T8 baseline recorded in +evidence/baseline/p0-t8-analyzer-rebuild-baseline.md: + +| Total | P0-T8 baseline | P9-T3 attempt 2 | Verdict | +|---|---|---|---| +| Warnings | 0 | 0 | no greater than baseline | +| Errors | 0 | 0 | no greater than baseline | + +Acceptance clause by clause: + +| Clause | Observed | Met | +|---|---|---| +| `EXIT_CODE: 0` | 0 | yes | +| at least one of `CscTaskCount` and `CscToolCount` greater than zero | both 36 | yes | +| both touched assemblies carry LastWriteTimeUtc at or later than `RunStartedUtc:` | 19:54:31.82Z and 19:54:35.20Z against 19:54:22.15Z | yes | +| analyzer warning and error totals no greater than the P0-T8 baseline | 0 and 0 against 0 and 0 | yes | + +The compiler-invocation counts are identical to the P0-T8 baseline at 36 and 36, so the +same thirty-six projects compiled and the gate is not vacuous. + +Output Summary: attempt 2 of the /t:Rebuild analyzer gate returned EXIT_CODE 0 with 0 +warnings and 0 errors, matching the P0-T8 baseline exactly. 36 Csc task invocations and +36 csc.exe tool invocations were recorded in the detailed log, and both touched +assemblies were rewritten after RunStartedUtc. All four acceptance clauses are met. The +attempt 1 record above is retained as the audit trail of the environmental file-lock +condition and its resolution. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t4-nullable-rebuild.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t4-nullable-rebuild.md new file mode 100644 index 000000000..056b96664 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t4-nullable-rebuild.md @@ -0,0 +1,97 @@ +# P9-T4 — Final nullable gate over TaskMaster.sln with /t:Rebuild + +Timestamp: 2026-09-07T15-55 +Task: [P9-T4] +Issue: #796 +Channel used: A + +Command, the P0-T9 command form with the log path +`TestResults\796\p9-t4\nullable-rebuild.log`: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\796\p9-t4\nullable-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +RunStartedUtc: 2026-09-07T19:55:29.7661680Z + +EXIT_CODE: 0 + +## No `/p:Nullable=enable` token + +The command line reproduced above contains no `/p:Nullable=enable` token. That is +confirmed by inspection of the command text and corroborated mechanically against the +detailed log, which records the full compiler command line for every project: + +``` +pwsh -NoProfile -Command '$log = "TestResults\796\p9-t4\nullable-rebuild.log"; "NullableEnableTokenCount=" + (Select-String -Path $log -SimpleMatch "Nullable=enable").Count' +``` + +NullableEnableTokenCount=0 + +The corroboration is meaningful rather than circular: a detailed-verbosity MSBuild log +reproduces the csc command line for each project, so a solution-wide nullable opt-in +introduced by any route — a command-line property, a project element, or a +Directory.Build file — would appear in it. It does not. + +## Build summary, verbatim + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:16.88 +``` + +## Compiler-invocation counts read back from the detailed log + +Raw log (gitignored, `.gitignore` line 84 ignores `*.log`): +TestResults/796/p9-t4/nullable-rebuild.log + +CscTaskCount=36 +CscToolCount=36 + +Both counts are greater than zero and both match the P0-T9 baseline exactly, so +`CoreCompile` ran on the same thirty-six projects and the nullable-flow diagnostics +actually executed. An exit code of 0 with both counts at zero would have been a FAILED +gate. + +## Assembly-freshness corroboration + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T19:55:37.7388044Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T19:55:41.1478330Z | yes | + +Both assemblies were rewritten after the run started, so neither of the two projects +this item touches was skipped. + +## Comparison against the P0-T9 baseline + +Baseline read from evidence/baseline/p0-t9-nullable-rebuild-baseline.md. + +| Total | P0-T9 baseline | P9-T4 | Verdict | +|---|---|---|---| +| Warnings | 0 | 0 | no greater than baseline | +| Errors | 0 | 0 | no greater than baseline | + +## Acceptance clause by clause + +| Clause | Observed | Met | +|---|---|---| +| `EXIT_CODE: 0` | 0 | yes | +| at least one of `CscTaskCount` and `CscToolCount` greater than zero | both 36 | yes | +| both touched assemblies carry LastWriteTimeUtc at or later than `RunStartedUtc:` | 19:55:37.74Z and 19:55:41.15Z against 19:55:29.77Z | yes | +| warning and error totals no greater than the P0-T9 baseline | 0 and 0 against 0 and 0 | yes | +| the recorded command line contains no `/p:Nullable=enable` token | absent from the command text, and 0 occurrences in the detailed log | yes | + +## Environmental note + +The MSB3061 file-lock warnings that blocked P9-T3 attempt 1 are absent here. No Outlook +process was running when this gate executed, and none was started for it. + +Output Summary: /t:Rebuild of TaskMaster.sln with TreatWarningsAsErrors returned +EXIT_CODE 0 with 0 warnings and 0 errors, matching the P0-T9 baseline exactly. 36 Csc +task invocations and 36 csc.exe tool invocations were recorded in the detailed log, both +touched assemblies were rewritten after RunStartedUtc, and neither the command line nor +the log carries a `Nullable=enable` token. All five acceptance clauses are met. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t5-full-assembly-tests.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t5-full-assembly-tests.md new file mode 100644 index 000000000..97ef2eb2d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t5-full-assembly-tests.md @@ -0,0 +1,148 @@ +# P9-T5 — Full QuickFiler.Test assembly run + +Timestamp: 2026-09-07T15-57 +Task: [P9-T5] +Issue: #796 +Channel used: A + +Command, the P0-T10 command form with the results directory `TestResults\796\p9-t5` +and the log file name `p9-t5.trx`: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" /ResultsDirectory:TestResults\796\p9-t5 "/Logger:trx;LogFileName=p9-t5.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +The assembly under test is the one the P9-T4 rebuild produced; that rebuild wrote +QuickFiler.Test/bin/Debug/QuickFiler.Test.dll at 2026-09-07T19:55:41.1478330Z, ahead of +this run, so this is not a stale binary. + +## Run summary, verbatim + +``` +Test Run Successful. +Total tests: 1380 + Passed: 1380 + Total time: 13.3662 Seconds +``` + +| Figure | Value | How obtained | +|---|---|---| +| Total | 1380 | read from the run summary | +| Passed | 1380 | read from the run summary | +| Failed | 0 | NOT PRINTED ON A PASSING RUN | + +## Failed set + +EMPTY. + +The run printed no `Failed:` line. Under the convention P0-T10 records and this task's +acceptance clause repeats, a run printing no `Failed:` line has an EMPTY Failed set +rather than an unread one, so the reading is a measurement and not an omission. + +That reading was corroborated against the TRX rather than left on the console summary +alone, because the console summary omits the line entirely on a passing run and an +omitted line cannot be distinguished from an unparsed one by reading the console: + +``` +pwsh -NoProfile -Command '$x = New-Object System.Xml.XmlDocument; $x.Load((Resolve-Path "TestResults\796\p9-t5\p9-t5.trx").Path); $r = $x.GetElementsByTagName("UnitTestResult"); "TotalResultNodes=" + $r.Count; $bad = @($r | Where-Object { $_.GetAttribute("outcome") -ne "Passed" }); "NonPassedCount=" + $bad.Count' +``` + +TotalResultNodes=1380 +NonPassedCount=0 + +Every one of the 1380 result nodes carries `outcome="Passed"`. No node carries Failed, +NotExecuted, Aborted, Timeout, or any other outcome. + +## Comparison against the P0-T10 baseline + +Baseline read from evidence/baseline/p0-t10-quickfiler-test-baseline.md. + +| Clause | Baseline | P9-T5 | Verdict | +|---|---|---|---| +| Failed set is a subset of BASELINE_FAILURE_SET | EMPTY | EMPTY | subset; no test outside the set Failed | +| Total no smaller than the baseline Total | 1370 | 1380 | not smaller | + +Because the BASELINE_FAILURE_SET is empty, the subset condition permits zero failures, +and zero is what was observed. The Total grew by 10, which is this item's own added +tests; the gate asserts non-shrinkage rather than an absolute total, so growth satisfies +it. + +## Expect-fail inventory + +INVENTORY COUNT READ: 4 rows. + +The count is four rather than five because task P4-T7 took its NO branch. The artifact +evidence/qa-gates/p4-t7-park-focus-decision.md opens its `## Branch taken` section with +`NO branch`, quotes `AC2-PARK-FOCUS-SUPPRESSED: NO` from the Phase 3 decision record as +its basis, and states in its own consequences list that the plan's expect-fail inventory +therefore stands at four rows and that task P9-T5 reads it as four. + +A caution on how that condition must be read, recorded because a literal search returns +the opposite answer to the correct one. The plan makes the fifth row conditional on the +P4-T7 artifact RECORDING the line `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8`. A search +of that artifact for the token was run in this task and it returns one hit, at line 30. +That hit is not a record of the line. It is the backticked subject of the sentence "The +line ... is deliberately ABSENT from this artifact", so the single occurrence of the +token is the artifact declaring that it does not carry the line. Reading the search hit +count as satisfaction of the condition would invert the decision and produce a +five-row inventory containing a test that was never written. The condition is therefore +resolved on the recorded branch — NO — and not on token presence. + +The conditional fifth row, the paired `ParkFocusOffWebView2()` negative test, is +accordingly not in the inventory. Its absence from the suite is corroborated +independently of the artifact, by enumerating every `testName` in the P9-T5 TRX +containing the substring `Park`: + +``` +ActionCancelAsync_ParksFocusAndCancelsBreadcrumbSelectors +Cleanup_WithParkedConsumer_ReturnsWithoutWaiting +FormDeactivated_NoWebView2Focus_DoesNotPark +FormDeactivated_WebView2Focused_ParksFocusOnce +``` + +Four names, none of them a paired negative test of +FormDeactivated_WebView2Focused_ParksFocusOnce. The substring `Park` was used rather +than the narrower `ParkFocus` deliberately: `ParkFocus` returns zero matches against +this suite, because the existing test spells the verb `Parks`, so a zero-match result +from it would have proved nothing about whether a paired test exists. + +Outcome per inventory row, read from the TRX by `testName`: + +``` +pwsh -NoProfile -Command '$x = New-Object System.Xml.XmlDocument; $x.Load((Resolve-Path "TestResults\796\p9-t5\p9-t5.trx").Path); $r = $x.GetElementsByTagName("UnitTestResult"); $names = @("FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector","NativeCloseWhileCommitPending_DoesNotCancelSelection","NativeCloseWithNoCommitPending_StillCancelsSelection","SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown"); foreach ($n in $names) { $m = @($r | Where-Object { $_.GetAttribute("testName") -eq $n }); "EXPECTFAIL " + $n + " count=" + $m.Count + " outcome=" + (($m | ForEach-Object { $_.GetAttribute("outcome") }) -join ",") }' +``` + +| # | Test | Class | Result nodes | Outcome | +|---|---|---|---|---| +| 1 | FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector | QfcFormControllerDeactivateTests | 1 | Passed | +| 2 | NativeCloseWhileCommitPending_DoesNotCancelSelection | BreadcrumbDropDownCloseOrderingTests | 1 | Passed | +| 3 | NativeCloseWithNoCommitPending_StillCancelsSelection | BreadcrumbDropDownCloseOrderingTests | 1 | Passed | +| 4 | SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown | QfcItemController_SearchLeaveLatchTests | 1 | Passed | + +Each name resolves to exactly one result node, so no row is satisfied by a +same-named test in a second class, and every one of the four is Passed. + +## Acceptance clause by clause + +| Clause | Observed | Met | +|---|---|---| +| Failed set a subset of BASELINE_FAILURE_SET, with no test outside that set Failed | Failed set EMPTY; baseline set EMPTY | yes | +| the empty-`Failed:`-line convention applied as P0-T10 records it | applied, and corroborated by 0 non-Passed nodes out of 1380 in the TRX | yes | +| every expect-fail inventory test recorded as Passed | all 4 Passed | yes | +| the artifact names which inventory count it read and why | 4 rows, because P4-T7 took its NO branch | yes | +| recorded Total no smaller than the baseline Total | 1380 against 1370 | yes | + +## Raw output + +The TRX is written to the gitignored path TestResults/796/p9-t5/p9-t5.trx and is never +committed, because a TRX embeds the host account name and machine name in its `runUser` +and `computerName` attributes. `.gitignore` line 39 ignores TestResults/ through the +bracket class `[Tt]est[Rr]esult*/`. + +Output Summary: 1380 tests run under the `TestCategory!=LiveOutlook` filter with +`/InIsolation`; 1380 Passed, Failed set EMPTY, EXIT_CODE 0. The Failed set is a subset +of the empty BASELINE_FAILURE_SET, the Total grew from 1370 to 1380, and all four rows +of the expect-fail inventory — four because P4-T7 took its NO branch — are recorded as +Passed. All five acceptance clauses are met. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t6-coverage-final.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t6-coverage-final.md new file mode 100644 index 000000000..1b5c7c2e9 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t6-coverage-final.md @@ -0,0 +1,132 @@ +# P9-T6 — Final coverage collection for the QuickFiler.Test assembly + +Timestamp: 2026-09-07T16-00 +Task: [P9-T6] +Issue: #796 +Channel used: A + +Command, the P0-T11 command form with the output path +`coverage\p9-t6-final.cobertura.xml`: + +``` +pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput coverage\p9-t6-final.cobertura.xml +``` + +EXIT_CODE: 1 + +## Why a non-zero exit code is accepted here + +The recorded stderr contains the literal `is below the required 80`. The exact message +was: + +``` +Cobertura line coverage 24.1857% is below the required 80% threshold. +``` + +That is the single-line message scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1 +line 54 throws when the document-level line rate is under the runner's own 80 percent +floor, and it is thrown from that file at line 54 column 9 as the stack line in the +console output confirms. The throw occurs after the Cobertura post-processing, so the +numbers were still written and are readable. The run itself reported +`Test Run Successful. Total tests: 1380, Passed: 1380`. This is exactly the condition +P0-T11 recorded and the same carve-out applies. Any other non-zero exit code would have +failed this task. + +The 24.1857 percent figure is a whole-solution document-level rate produced by a run +scoped to a single test assembly. It is recorded as the post-change datum, not as a +policy verdict, on the same terms as the 24.1387 percent the baseline recorded. + +## The output file exists + +coverage/p9-t6-final.cobertura.xml was written and is readable; the six attributes below +were read back out of it. It sits at a gitignored path (`.gitignore` line 144 ignores +everything under coverage/) and is therefore never committed. + +## Output Summary — document-level attributes + +``` +line-rate=0.241857 +lines-covered=14925 +lines-valid=61710 +branch-rate=0.230082 +branches-covered=3685 +branches-valid=16016 +``` + +All six numeric attributes are recorded, read with the P0-T11 extraction command form +against the final document. + +Side-by-side with the baseline recorded in +evidence/baseline/p0-t11-coverage-baseline.md: + +| Attribute | P0-T11 baseline | P9-T6 final | +|---|---|---| +| line-rate | 0.241387 | 0.241857 | +| lines-covered | 14867 | 14925 | +| lines-valid | 61590 | 61710 | +| branch-rate | 0.229747 | 0.230082 | +| branches-covered | 3664 | 3685 | +| branches-valid | 15948 | 16016 | + +The no-regression comparison of the two ratios is computed and recorded in P9-T7, which +is the task the plan assigns it to; the figures are reproduced here only so that both +sides of that comparison are readable from one artifact. + +## Per-file rows for the five named files + +Rows were obtained with the P0-T11 grouping command form: every `//class` node grouped +by its `filename` attribute, summing per group the count of `lines/line` child nodes and +the count of `lines/line[@hits>0]` child nodes. Class nodes are grouped by `filename` +because a C# async state machine is emitted as a separate class node and would otherwise +split one source file's denominator across several nodes. The relative child axis is +used rather than a descendant axis, because a descendant axis double-counts on nested +nodes. + +Filenames are reproduced verbatim as the tool emitted them, which uses backslash +separators. The forward-slash spellings the plan uses name the same files. + +| Filename as emitted | lines-covered | lines-valid | P0-T11 lines-covered | P0-T11 lines-valid | +|---|---|---|---|---| +| `QuickFiler\Controllers\QfcFormController.Deactivate.cs` | 48 | 48 | 25 | 25 | +| `QuickFiler\Viewers\BreadcrumbDropDownHost.cs` | 287 | 289 | 291 | 293 | +| `QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs` | 24 | 24 | 23 | 23 | +| `QuickFiler\Controllers\QfcItemController.EventHandlers.cs` | 97 | 118 | 89 | 108 | +| `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs` | 234 | 238 | 234 | 238 | + +None of the five is ABSENT. Every one carries at least one class node whose `filename` +attribute names it, so no row records `ABSENT: no class node carries this filename`. +Because no file is recorded ABSENT at both P0-T11 and P9-T6, task P9-T7 has no file to +exclude from the changed-code denominator on that basis and its NOT MEASURABLE list +carries no entry from this cause. + +The BreadcrumbDropDownHost.cs denominator fell from 293 to 289 while its covered count +fell from 291 to 287. That is the expected consequence of executed task P1-T2 relocating +`OnDropDownClosed` out of the main part; the relocated lines reappear in the row below. + +## The new diagnostics part + +A sixth row exists in the final document that has no baseline counterpart, because the +file did not exist at P0-T11: + +| Filename as emitted | lines-covered | lines-valid | +|---|---|---| +| `QuickFiler\Viewers\BreadcrumbDropDownHost.Diagnostics.cs` | 28 | 28 | + +It is recorded here for completeness. Task P9-T7 reports it on its own line and excludes +it from the behavioural changed-code figure, so that logging-only coverage cannot +inflate the figure for the behavioural changes. + +## Test population + +The coverage run executed 1380 tests with 1380 Passed, the same population and the same +result as the P9-T5 full-assembly run. The runner applies the `TestCategory!=LiveOutlook` +filter at scripts/vscode/Invoke-MSTestWithCoverage.ps1 line 76, which is the same filter +P9-T5 passes explicitly, so the two runs are comparable and neither starts an external +Outlook process. + +Output Summary: the final Cobertura document was written to +coverage/p9-t6-final.cobertura.xml and all six document-level attributes were read back +from it: line-rate 0.241857, lines-covered 14925, lines-valid 61710, branch-rate +0.230082, branches-covered 3685, branches-valid 16016. All five per-file rows carry +figures and none is ABSENT. EXIT_CODE 1 is accepted because the recorded stderr carries +the literal `is below the required 80`, exactly as at P0-T11. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t7-coverage-delta.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t7-coverage-delta.md new file mode 100644 index 000000000..c291dcabe --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t7-coverage-delta.md @@ -0,0 +1,286 @@ +# P9-T7 — Coverage delta + +Timestamp: 2026-09-07T16-05 +Task: [P9-T7] +Issue: #796 +Channel used: A + +Documents read: + +- baseline: coverage/p0-t11-baseline.cobertura.xml, the file this task names +- final: coverage/p9-t6-final.cobertura.xml + +Changed-line span: + +``` +pwsh -NoProfile -Command 'git diff -U0 c7ae69f1..HEAD -- QuickFiler' +``` + +## The three required figures + +| Figure | Value | +|---|---| +| Baseline coverage | 24.1387 percent (14867 / 61590) | +| Post-change coverage | 24.1857 percent (14925 / 61710) | +| Changed-code coverage | 97.5610 percent (40 / 41) | + +All three are numbers, not placeholders. + +## No-regression comparison + +Baseline ratio, computed from the two document-level attributes the baseline recorded: +14867 / 61590 = 0.2413866, or 24.1387 percent. + +Post-change ratio, computed from the same two attributes of the final document: +14925 / 61710 = 0.2418571, or 24.1857 percent. + +The post-change ratio is HIGHER than the baseline ratio by 0.0470 percentage points, so +it is not lower and the no-regression clause is met. The denominator grew by 120 valid +lines and the numerator grew by 58 covered lines. + +## The anchor, and why it is retained at c7ae69f1 + +The anchor is deliberately RETAINED at c7ae69f1 and was NOT moved to the second merge +commit d78ae7f7 or the third merge commit 5b8e0bf5. The plan states both reasons and +both were re-checked here rather than accepted: the diff is scoped to the QuickFiler +directory, which neither the second nor the third merge of origin/main touched, so +c7ae69f1 already yields exactly this item's own QuickFiler changed lines; and each of +those two merge commits already contains this item's committed Phase 1 instrumentation, +so anchoring at either would silently drop the instrumentation lines out of the +changed-line set and contradict this task's own requirement that they be counted in the +denominator. + +The span enumerates nine paths: + +``` +77 0 QuickFiler/Controllers/QfcFormController.Deactivate.cs +54 0 QuickFiler/Controllers/QfcItemController.EventHandlers.cs +16 0 QuickFiler/Interfaces/IQfcFormViewer.cs +1 0 QuickFiler/QuickFiler.csproj +79 0 QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs +24 0 QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs +12 14 QuickFiler/Viewers/BreadcrumbDropDownHost.cs +4 0 QuickFiler/Viewers/ItemViewer.Breadcrumb.cs +39 0 QuickFiler/Viewers/QfcFormViewer.cs +``` + +`QuickFiler/Resources/FolderBreadcrumb.html` and +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` are write-set paths that the +span does not enumerate, because neither was changed. Both are recorded below anyway, +the first in the NOT MEASURABLE list as the plan requires and the second here, so that a +reader does not mistake their absence for an omission. + +## Method + +Added and modified line numbers were taken from the `@@ -a,b +c,d @@` hunk headers of the +`-U0` diff, expanding each header to the line numbers `c` through `c + d - 1` and +discarding headers with `d = 0`, which are pure deletions and have no line in the +post-change file. + +Those line numbers were intersected, per file, with the `line` nodes of the final +Cobertura document, aggregating class nodes by their `filename` attribute so that a C# +async state machine emitted as a separate class node does not split one source file's +denominator. Cobertura filenames use backslash separators; the forward-slash spellings +in this artifact name the same files. Where two class nodes both carry a line, the +higher `hits` value is taken, so a line covered through one node is not recorded as +uncovered because a second node did not reach it. + +The measurable denominator for a file is therefore the set of its changed lines that +carry a `line` node. A changed line that carries no `line` node — a blank line, or a +comment line the emitter did not map — is outside the denominator by construction, since +it can appear in neither the covered numerator nor the valid denominator. + +## Comment-only changed lines, per measurable file + +Recorded as this task requires, for every measurable file in the diff span. A line is +classified comment-only when its trimmed text begins with `//`, `/*`, or `*`. + +| File | Changed lines | Comment-only | Removed from the denominator on this basis | +|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 77 | 44 | 44 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 54 | 39 | 39 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 12 | 9 | 4 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 24 | 21 | 21 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | 79 | 31 | 31 | + +The two files this task's acceptance names explicitly both carry a figure: 44 for +`QuickFiler/Controllers/QfcFormController.Deactivate.cs` and 39 for +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs`. Both exceed the counts the +plan attributes to executed task P1-T4 alone — 33 and 13 `///` lines — because later +phases added further comment lines to both files and because these counts include +ordinary `//` comments as well as `///` documentation comments. + +### One measured departure from the plan's stated mechanism, disclosed + +The plan's comment clause reasons that "a comment line carries no Cobertura `line` node +at all". That holds for 139 of the 144 comment-only changed lines across the five files +in the table above, including every one of the 83 in the two files this task names +explicitly. It does NOT hold for five lines in +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs`: lines 442 through 446 +are comment lines that each carry a `line` node with `hits=1`. Lines 251 through 254 in +the same file are comment lines that carry no node, so the behaviour is not uniform even +within one file. The likely mechanism is that the .coverage-to-Cobertura conversion maps +the full source span preceding a mapped statement, but the mechanism was not established +and is not relied on here. + +This is disclosed rather than smoothed over, because those five lines sit in the +denominator and are all covered, so including them raises the changed-code figure. The +figure is therefore reported a second way below with all comment lines removed from both +numerator and denominator, and the threshold is met on both computations. Neither +computation was selected after seeing which one passed; both are reported. + +## Instrumentation changed lines, per file + +Recorded as this task requires. Instrumentation means the AC6 log statements and the +pure formatter methods executed task P1-T4 added, and the internal selector-open member +the same task added. + +| File | Changed lines | Instrumentation | Measurable instrumentation | +|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 77 | 65 (lines 29-80, 93-99, 125-130) | 20 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 54 | 15 (lines 269-283) | 1 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 12 | 0 | 0 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 24 | 0 | 0 | + +The instrumentation lines in these two files ARE counted in the changed-code +denominator, as this task requires, unlike the diagnostics part. Separating them per line +would make the figure unreproducible, and both files carry behavioural changes that are +measured in the same span. + +Neither file carries a class-level `[ExcludeFromCodeCoverage]`. The exclusions in +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` are method-level and sit at +lines 60, 83, 97, 111 and 125 only. That was re-derived here rather than carried +forward, and the search idiom matters: the attribute is written fully qualified as +`[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]`, so a search for the literal +`[ExcludeFromCodeCoverage]` returns zero hits in this file and would wrongly suggest the +exclusions had been removed. + +## The diagnostics part, reported on its own line + +`QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs`: 79 changed lines, 28 +measurable, 28 covered, 100.0000 percent. + +It is EXCLUDED from the behavioural changed-code figure. The file contains logging and +the relocated close handler only, and its coverage contribution must not be used to +inflate the figure for the behavioural changes. It is not counted in the 41-line +denominator or the 40-line numerator above. + +## Per-file changed-code detail, behavioural files + +| File | Changed | Measurable | Covered | Uncovered | +|---|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 77 | 23 | 23 | 0 | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 54 | 10 | 8 | 2 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 12 | 8 | 8 | 0 | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 24 | 1 | 1 | 0 | +| **Total** | **167** | **42** | **40** | **2** | + +Measurable line numbers, with hit counts: + +``` +QfcFormController.Deactivate.cs 56(1) 57(1) 58(1) 76(1) 77(1) 78(1) 79(1) 93(1) 94(1) + 95(1) 96(1) 97(1) 98(1) 99(1) 118(1) 119(1) 120(1) + 125(1) 126(1) 127(1) 128(1) 129(1) 130(1) +QfcItemController.EventHandlers.cs 183(1) 209(0) 220(1) 234(1) 254(1) 257(1) 263(1) + 264(1) 265(1) 282(0) +BreadcrumbDropDownHost.cs 255(1) 256(1) 442(1) 443(1) 444(1) 445(1) 446(1) 447(1) +BreadcrumbDropDownHost.Open.cs 123(1) +``` + +## Individually named exclusions from the changed-code denominator + +Exactly ONE line is excluded. The allowance is at most 3, so two remain unused. No +blanket exclusion and no unnamed exclusion is taken. + +**Exclusion 1 of at most 3.** `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` +line 282: + +``` +internal bool IsBreadcrumbSelectorOpen => _itemViewer.IsFolderDropDownOpen; +``` + +Reason — the mocked-seam limitation this task names. This is the internal selector-open +member executed task P1-T4 added. Its only reader is the per-item log statement in +`ParkFocusAndCancelSelectors`, at +`QuickFiler/Controllers/QfcFormController.Deactivate.cs` line 128, which reaches it only +when the loop's interface-typed item controller casts successfully to the concrete +internal type `QfcItemController`. Every test in QfcFormControllerDeactivateTests that +injects item controllers injects them as Moq mocks of `IQfcItemController`, so that cast +yields null in every test and the member is never evaluated. The exclusion was admitted +by name in the plan rather than discovered at this gate, and the measurement confirms it: +the line is present in the final Cobertura document with `hits=0`. + +### The second uncovered line is NOT excluded + +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` line 209: + +``` +internal bool SearchOwnsDropDownDismissal => _searchOwnedDismissal; +``` + +This line is uncovered and it REMAINS IN THE DENOMINATOR. It is recorded here rather +than excluded, because the at-most-3 allowance is a permission and not an obligation, +and the threshold is met without spending it. A search of every `.cs` file in the tree +for the identifier `SearchOwnsDropDownDismissal` returns exactly one hit, the declaration +itself, so the member currently has no reader in production or in test code. That is the +reason no test covers it. It is reported to the reviewer as an observation; removing it +would be a code change, and this task computes and records a figure rather than editing +source. + +## The changed-code figure + +Behavioural measurable changed lines: 42. +Less the one individually named exclusion at EventHandlers.cs line 282: 41. +Covered among those 41: 40. + +**Changed-code coverage = 40 / 41 = 97.5610 percent.** + +That is at least 90 percent, so the threshold clause is met. + +Two corroborating computations, neither of which is the reported figure: + +- With no exclusion at all, 40 / 42 = 95.2381 percent. Still at or above 90 percent, so + the result does not depend on the exclusion being taken. +- With every comment-only line removed from both numerator and denominator, which + neutralises the five-line departure disclosed above, 35 / 36 = 97.2222 percent. Still + at or above 90 percent, so the result does not depend on that departure either. + +## NOT MEASURABLE write-set files, each with its citation + +| File | Reason | Citation | +|---|---|---| +| `QuickFiler/Viewers/QfcFormViewer.cs` | class-level `[ExcludeFromCodeCoverage]` suppresses the whole type | attribute at QuickFiler/Viewers/QfcFormViewer.cs line 17, verified in this pass; the final Cobertura document carries 0 class nodes naming this file | +| `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` | partial part of `ItemViewer`, which carries a class-level `[ExcludeFromCodeCoverage]` | attribute at QuickFiler/Viewers/ItemViewer.cs line 20, verified in this pass; the final Cobertura document carries 0 class nodes naming this file | +| `QuickFiler/Interfaces/IQfcFormViewer.cs` | interface-only file with no executable lines | the final Cobertura document carries 0 class nodes naming this file | +| `QuickFiler/Resources/FolderBreadcrumb.html` | not C#; carries no coverage figure of any kind. It is additionally unchanged in this span | absent from the `--numstat` listing above | +| `QuickFiler/QuickFiler.csproj` | project file, not compiled source | 1 changed line, the `` entry executed task P1-T3 added at line 417 | +| `QuickFiler.Test/QuickFiler.Test.csproj` | project file, not compiled source; additionally outside the `-- QuickFiler` span, which matches the QuickFiler directory only | absent from the `--numstat` listing above | + +Their 59 changed lines — 39 in QfcFormViewer.cs, 4 in ItemViewer.Breadcrumb.cs, 16 in +IQfcFormViewer.cs and 1 in QuickFiler.csproj — are in neither the numerator nor the +denominator of the changed-code figure. Reporting a figure for them would report a +number that has no source. + +No file was recorded `ABSENT: no class node carries this filename` at both P0-T11 and +P9-T6, so no file enters this list on that basis. + +## Acceptance clause by clause + +| Clause | Observed | Met | +|---|---|---| +| all three figures present as numbers rather than placeholders | 24.1387, 24.1857, 97.5610 | yes | +| changed-code figure at least 90 percent over the measurable behavioural changed lines | 97.5610 percent, and 95.2381 or 97.2222 under the two corroborating computations | yes | +| post-change `lines-covered` / `lines-valid` not lower than the baseline ratio | 0.2418571 against 0.2413866 | yes | +| every NOT MEASURABLE entry carries its citation | six entries, each cited | yes | +| individually named exclusions at most 3, each naming file, line and reason | 1 exclusion, EventHandlers.cs line 282, mocked-seam limitation | yes | +| comment-only changed-line count recorded per measurable file, and at minimum for the two named files | recorded for all five, including 44 and 39 for the two named | yes | +| instrumentation changed-line count recorded per file | recorded for all four behavioural files | yes | +| diagnostics part reported on its own line and excluded from the behavioural figure | 28 / 28, excluded | yes | + +Output Summary: baseline coverage 24.1387 percent, post-change coverage 24.1857 percent, +changed-code coverage 97.5610 percent over 41 measurable behavioural changed lines with +40 covered. The post-change document ratio is higher than the baseline ratio, so there is +no regression. One individually named exclusion is taken of an allowance of three, the +mocked-seam-unreachable member at EventHandlers.cs line 282; the second uncovered line, +EventHandlers.cs line 209, is left in the denominator rather than excluded and is +reported as an unreferenced member. All eight acceptance clauses are met. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t8-file-size-audit.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t8-file-size-audit.md new file mode 100644 index 000000000..13bb1e259 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t8-file-size-audit.md @@ -0,0 +1,89 @@ +# P9-T8 — File-size audit of the write set after the final formatter pass + +Timestamp: 2026-09-07T16-07 +Task: [P9-T8] +Issue: #796 +Channel used: A + +This audit runs after P9-T1, because the formatter can change line counts and an audit +taken before it would measure a superseded state. P9-T1 and P9-T2 are both checked off, +and P9-T2 recorded a clean `csharpier check` over the same scope, so the tree measured +here is the post-format tree. + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +That is the idiom recorded on the `LINE-COUNT-IDIOM:` line of +evidence/baseline/p0-t12-file-size-baseline.md, and no other idiom was used, so this +audit and the baseline are commensurable. `(Get-Content $_ | Measure-Object -Line).Lines` +remains prohibited, because `Measure-Object -Line` omits blank lines and under-reports +every count by that file's blank-line total. + +Command, the P0-T12 command form extended with the three files this plan creates and +with the seventeenth write-set path: + +``` +pwsh -NoProfile -Command '@("QuickFiler\Controllers\QfcFormController.Deactivate.cs","QuickFiler\Interfaces\IQfcFormViewer.cs","QuickFiler\Viewers\QfcFormViewer.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs","QuickFiler\Viewers\ItemViewer.Breadcrumb.cs","QuickFiler\Controllers\QfcItemController.EventHandlers.cs","QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs","QuickFiler\Resources\FolderBreadcrumb.html","QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs","QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Diagnostics.cs","QuickFiler.Test\Viewers\BreadcrumbDropDownCloseOrderingTests.cs","QuickFiler.Test\Controllers\QfcItemController.SearchLeaveLatchTests.cs","QuickFiler.Test\Controllers\QfcItemController.SearchDismissalTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' +``` + +EXIT_CODE: 0 + +## Scope of the audit + +Fifteen paths. The write set holds seventeen; the two omitted are +`QuickFiler/QuickFiler.csproj` and `QuickFiler.Test/QuickFiler.Test.csproj`, which this +task's scope — every .cs and .html path in the write set — does not cover. + +## Measured physical line counts + +| Path | P0-T12 baseline | Final | Delta | At most 500 | +|---|---|---|---|---| +| QuickFiler/Controllers/QfcFormController.Deactivate.cs | 73 | 150 | +77 | yes | +| QuickFiler/Interfaces/IQfcFormViewer.cs | 72 | 88 | +16 | yes | +| QuickFiler/Viewers/QfcFormViewer.cs | 293 | 332 | +39 | yes | +| QuickFiler/Viewers/BreadcrumbDropDownHost.cs | 498 | 496 | -2 | yes | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs | 107 | 131 | +24 | yes | +| QuickFiler/Viewers/ItemViewer.Breadcrumb.cs | 456 | 460 | +4 | yes | +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 263 | 317 | +54 | yes | +| QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs | 395 | 395 | 0 | yes | +| QuickFiler/Resources/FolderBreadcrumb.html | 490 | 490 | 0 | yes | +| QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs | 248 | 305 | +57 | yes | +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 380 | 458 | +78 | yes | +| QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs | 0 — did not exist at P0-T12; created by this plan | 79 | +79 | yes | +| QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | 0 — did not exist at P0-T12; created by this plan | 290 | +290 | yes | +| QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs | 0 — did not exist at P0-T12; created by this plan | 102 | +102 | yes | +| QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs | NO PHASE 0 BASELINE | 181 | not computable | yes | + +Every one of the fifteen recorded physical counts is at most 500. The largest is 496, +at `QuickFiler/Viewers/BreadcrumbDropDownHost.cs`, four lines below the ceiling. + +## The stated exception + +`QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` records the +literal `NO PHASE 0 BASELINE` in its baseline column rather than a figure. It entered the +write set after Phase 0 had executed, when executing AC4 surfaced a pre-existing test in +it asserting the behaviour AC4 deliberately changes, so it carries no P0-T12 figure and +recording one would be recording a number with no source. Its delta is correspondingly +not computable. + +That exception is distinct from the three created files above it in the table. Those +three also carry no row in the P0-T12 artifact, but their baseline is well defined and +has a source: they did not exist in the tree at P0-T12, so their baseline length is 0 and +their delta is their whole length. `NO PHASE 0 BASELINE` is not used for them, because it +would understate what is known about them. + +## Two counts worth noting + +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs` FELL by two lines, from 498 to 496, +against a plan that adds to it. That is the expected consequence of executed task P1-T2 +relocating `OnDropDownClosed` into the new diagnostics part: the relocation removed more +lines than the later AC3 edit added back. The headroom that relocation bought is what +kept the file under the ceiling. + +`QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` grew by 78 lines to 458, +which is the largest growth in the test files and leaves 42 lines of headroom. + +Output Summary: 15 paths measured with the recorded idiom +`(Get-Content -LiteralPath $_).Count`. Every count is at most 500, the maximum being 496. +Eleven paths carry their P0-T12 baseline figure beside the final count, three carry a +baseline of 0 with the reason that they did not exist at P0-T12, and one carries the +literal `NO PHASE 0 BASELINE` under the exception this task states. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md new file mode 100644 index 000000000..df4b5ed12 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t9-final-commit.md @@ -0,0 +1,75 @@ +# P9-T9 — Final commit + +Timestamp: 2026-09-07T16-09 +Task: [P9-T9] +Issue: #796 +Channel used: A + +## Staging + +Explicit pathspecs were staged rather than everything, because a repository-wide stage +can sweep an unrelated queued promotion file into this item's branch: + +``` +pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796' +``` + +EXIT_CODE: 0 + +The command emitted 13 `LF will be replaced by CRLF` advisory warnings, one per Markdown +file staged. These are line-ending normalisation notices from the repository's +`core.autocrlf` configuration, not errors, and they change no content. + +## Commit + +EXIT_CODE: 0 + +SHA: 676966ef2d36e20f231cfb3949591d44a9adb92d + +Short SHA: 676966ef +Branch: bug/quickfiler-folder-dropdown-closes-on-open-796 +Parent: 5b8e0bf58417dcaf25e69aa05e7c6e5962b3e1aa + +Statistics: 15 files changed, 1377 insertions(+), 40 deletions(-). + +Subject line: + +``` +test(796): re-pin the search-dismissal suite to AC4 and record the Phase 9 QA gates +``` + +## `--name-status` listing + +``` +M QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/issue-updates/issue-796.2026-09-07T15-03.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t1-csharpier-format.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t2-csharpier-check.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t3-analyzer-rebuild.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t4-nullable-rebuild.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t5-full-assembly-tests.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t6-coverage-final.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t7-coverage-delta.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/qa-gates/p9-t8-file-size-audit.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md +A docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md +M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md +M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md +M docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md +``` + +Fifteen paths: one write-set test file and fourteen feature-folder paths. No path lies +outside the write set or the feature folder, and no path under UtilitiesCS/, +UtilitiesCS.Test/, .claude/, .codex/, .agents/, config/, .github/, or naming +TaskMaster.sln or a repository-root build property file appears. The scope-boundary gate +that measures this formally is P9-T10. + +Four evidence artifacts written after this commit are necessarily absent from the listing +above and are folded in by the P9-T11 amend: this artifact itself, the P9-T10 +scope-boundary artifact, the P9-T11 loop artifact, and the P9-T10 and P9-T11 check-offs +in the plan file. + +Output Summary: staged with the explicit three-pathspec form and committed as +676966ef2d36e20f231cfb3949591d44a9adb92d, 15 files changed with 1377 insertions and 40 +deletions. Both acceptance clauses are met: the SHA is recorded and the commit's +`--name-status` listing is reproduced in full. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md new file mode 100644 index 000000000..ff5965833 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md @@ -0,0 +1,63 @@ +# P1-T11 — AC6 instrumentation tests + +Timestamp: 2026-09-07T14-25 +Task: [P1-T11] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~QfcFormControllerDeactivateTests" /ResultsDirectory:TestResults\796\p1-t11 "/Logger:trx;LogFileName=p1-t11.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## Result lines, verbatim + +``` + Passed FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField [50 ms] + Passed RegisterFormEventHandlers_SubscribesFormDeactivated [282 ms] + Passed UnregisterFormEventHandlers_UnsubscribesFormDeactivated [2 ms] + Passed FormDeactivated_WebView2Focused_ParksFocusOnce [6 ms] + Passed FormDeactivated_NoWebView2Focus_DoesNotPark [< 1 ms] + Passed FormDeactivated_CancelsSelectorOnEveryItemController [19 ms] + Passed FormDeactivated_NullGroupsOrNullItemGroups_DoesNotThrow [3 ms] + Passed FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues [1 ms] + Passed FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField [< 1 ms] + +Test Run Successful. +Total tests: 9 + Passed: 9 + Total time: 1.6277 Seconds +``` + +## Acceptance + +| Condition | Observed | +|---|---| +| `EXIT_CODE: 0` | 0 | +| FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField Passed | Passed | +| FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField Passed | Passed | +| Total recorded as 9 | 9 | +| No test recorded as Failed | none; the run printed no `Failed:` line and no `Failed` result line | + +The Total of 9 is the 8 methods QfcFormControllerDeactivateTests holds after P1-T7 +plus the 1 method BreadcrumbDropDownCloseOrderingTests holds after P1-T5. + +No expect-fail test exists in either class at this point in plan order, so no +carve-out applies to this gate. The complete expect-fail inventory for this plan +lands its first entry at P4-T4, which is three phases later. + +## Incidental observation + +The six pre-existing QfcFormControllerDeactivateTests methods all pass unchanged +against the instrumented handler, including the two that assert focus-parking +behaviour and the two that assert null-safety and per-item exception containment. +That is direct evidence that the two added log statements changed no behaviour those +tests exercise. + +## Raw output + +The TRX is written to the gitignored path TestResults/796/p1-t11/p1-t11.trx and is +never committed. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t12-behaviour-neutrality.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t12-behaviour-neutrality.md new file mode 100644 index 000000000..d8ab5a602 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p1-t12-behaviour-neutrality.md @@ -0,0 +1,62 @@ +# P1-T12 — Behaviour-neutrality gate over the open and close lifecycle suites + +Timestamp: 2026-09-07T14-26 +Task: [P1-T12] +Issue: #796 +Channel used: A + +Command: the P1-T11 command form with the results directory +`TestResults\796\p1-t12`, the log file name `p1-t12.trx`, and the four-class filter: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbPendingOpenCloseTests|FullyQualifiedName~BreadcrumbDropDownHostTests|FullyQualifiedName~BreadcrumbDropDownIntegrationTests|FullyQualifiedName~BreadcrumbSelectorOpenRetryTests" /ResultsDirectory:TestResults\796\p1-t12 "/Logger:trx;LogFileName=p1-t12.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +## Run summary, verbatim + +``` +Test Run Successful. +Total tests: 59 + Passed: 59 + Total time: 2.5469 Seconds +``` + +## Acceptance + +| Condition | Observed | +|---|---| +| `EXIT_CODE: 0` | 0 | +| No test recorded as Failed | none; the run printed no `Failed:` line and no `Failed` result line | +| Recorded Total of at least 1 | 59 | + +The Total of 59 is well above 1, so the gate did not pass on an empty population. +Phase 1 adds no test to any of these four classes. + +## Tests that directly exercise the relocated handler + +Four of the 59 assert on the native-close path that P1-T2 moved into the diagnostics +part, and all four passed: + +``` + Passed NativeClosedEvent_CancelsOnceAndIgnoresLaterCloseNotifications [< 1 ms] + Passed FinishClose_DropDownClosedPath_PredicateFalse_DoesNotFocusAnchor [2 ms] + Passed NativeAutomaticClose_RestoresOriginalCommittedIdentityWithoutPendingPublicationAndReturnsFocusOnce [46 ms] + Passed AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce [109 ms] +``` + +These are the assertions that would have detected a behavioural change in the move: +the handler is still bound through the `DropDown.Closed += OnDropDownClosed;` +subscription that stayed in the main part, its guard still suppresses a repeat close, +and its scheduled continuation still reaches `FinishClose` with the Uncommitted +reason. + +Phase 1 changes no behaviour these suites exercise, so a failure here would have +meant the instrumentation phase changed behaviour and the phase would have had to be +reverted rather than accepted. There was none. + +## Raw output + +The TRX is written to the gitignored path TestResults/796/p1-t12/p1-t12.trx and is +never committed. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t4-deactivate-suite-count.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t4-deactivate-suite-count.md new file mode 100644 index 000000000..07e6b4ac6 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t4-deactivate-suite-count.md @@ -0,0 +1,34 @@ +# P4-T4 — Deactivate suite `[TestMethod]` count after the AC2 fail-before test + +Timestamp: 2026-09-07T14-11 +Task: [P4-T4] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs -SimpleMatch "[TestMethod]").Count' +``` + +EXIT_CODE: 0 + +Measured `[TestMethod]` count: 9 + +The plan requires 9, derived as the 8 the file held after executed task P1-T7 plus the one test +this task added, FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector. + +## Compile check + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p4-t4\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 +Build summary: Build succeeded. 0 Warning(s). 0 Error(s). +Raw log (gitignored): TestResults/796/p4-t4/analyzer-rebuild.log + +Output Summary: the file declares 9 `[TestMethod]` members and the solution rebuilds clean with +analyzers enabled. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t5-ac2-fail-before.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t5-ac2-fail-before.md new file mode 100644 index 000000000..d182f1b7d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t5-ac2-fail-before.md @@ -0,0 +1,63 @@ +# P4-T5 — AC2 fail-before run (expect-fail) + +Timestamp: 2026-09-07T14-12 +Task: [P4-T5] [expect-fail] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~QfcFormControllerDeactivateTests" /ResultsDirectory:TestResults\796\p4-t5 "/Logger:trx;LogFileName=p4-t5.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 1 +ExpectedExitCode: 1 + +Raw results (gitignored, never committed): TestResults/796/p4-t5/p4-t5.trx + +## Run summary + +``` +Total tests: 9 + Passed: 8 + Failed: 1 +``` + +## Per-test results + +| Test | Result | +|---|---| +| RegisterFormEventHandlers_SubscribesFormDeactivated | Passed | +| UnregisterFormEventHandlers_UnsubscribesFormDeactivated | Passed | +| FormDeactivated_WebView2Focused_ParksFocusOnce | Passed | +| FormDeactivated_NoWebView2Focus_DoesNotPark | Passed | +| FormDeactivated_CancelsSelectorOnEveryItemController | Passed | +| FormDeactivated_NullGroupsOrNullItemGroups_DoesNotThrow | Passed | +| FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues | Passed | +| FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField | Passed | +| FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector | Failed | + +## Carve-out + +The single Failed test is exactly FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector, +landed by task P4-T4 as a deliberately-failing regression test and made to pass by task P4-T8. Every +one of the other 8 tests in the class is recorded Passed. No test other than that exact named test +is Failed, so the gate's explicit single-name carve-out is satisfied exactly and not merely +non-vacuously. + +## Failure detail (the assertion that matters) + +The failure is a runtime Moq verification failure at the assertion, not a compile or arrange +failure: + +``` +Moq.MockException: +Expected invocation on the mock should never have been performed, but was 1 times: x => x.CancelBreadcrumbSelector() +``` + +That is the defect under repair: with the seam declared and implemented but not yet consulted by +`ParkFocusAndCancelSelectors`, a self-inflicted deactivation still cancels every item's selector. + +Output Summary: 9 total, 8 passed, 1 failed; the one failure is the named expect-fail test and the +failure is the intended assertion. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t9-ac2-pass-after.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t9-ac2-pass-after.md new file mode 100644 index 000000000..78578b9f6 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p4-t9-ac2-pass-after.md @@ -0,0 +1,48 @@ +# P4-T9 — AC2 pass-after run + +Timestamp: 2026-09-07T14-16 +Task: [P4-T9] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~QfcFormControllerDeactivateTests" /ResultsDirectory:TestResults\796\p4-t9 "/Logger:trx;LogFileName=p4-t9.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Raw results (gitignored, never committed): TestResults/796/p4-t9/p4-t9.trx + +## Run summary + +``` +Test Run Successful. +Total tests: 9 + Passed: 9 +``` + +vstest.console.exe prints no `Failed:` line on a successful run, so the Failed count is recorded as +0 with the note NOT PRINTED ON A PASSING RUN. The run additionally prints no `Skipped:` line, and +Total minus Passed is 0, so no test was skipped. + +## Per-test results + +| Test | P4-T5 (fail-before) | P4-T9 (pass-after) | +|---|---|---| +| RegisterFormEventHandlers_SubscribesFormDeactivated | Passed | Passed | +| UnregisterFormEventHandlers_UnsubscribesFormDeactivated | Passed | Passed | +| FormDeactivated_WebView2Focused_ParksFocusOnce | Passed | Passed | +| FormDeactivated_NoWebView2Focus_DoesNotPark | Passed | Passed | +| FormDeactivated_CancelsSelectorOnEveryItemController | Passed | Passed | +| FormDeactivated_NullGroupsOrNullItemGroups_DoesNotThrow | Passed | Passed | +| FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues | Passed | Passed | +| FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField | Passed | Passed | +| FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector | Failed | Passed | + +FormDeactivated_CancelsSelectorOnEveryItemController, which pins the issue #677 contract with two +`Times.Once()` assertions, is Passed on both runs, so the guard did not become global. + +Output Summary: 9 total, 9 passed, 0 failed; the AC2 expect-fail test transitioned Failed to Passed +and the #677 contract test stayed Passed. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t3-ac3-fail-before.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t3-ac3-fail-before.md new file mode 100644 index 000000000..8588547e8 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t3-ac3-fail-before.md @@ -0,0 +1,62 @@ +# P5-T3 — AC3 fail-before run (expect-fail) + +Timestamp: 2026-09-07T14-24 +Task: [P5-T3] [expect-fail] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests" /ResultsDirectory:TestResults\796\p5-t3 "/Logger:trx;LogFileName=p5-t3.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 1 +ExpectedExitCode: 1 + +Raw results (gitignored, never committed): TestResults/796/p5-t3/p5-t3.trx + +## Run summary + +``` +Total tests: 3 + Passed: 2 + Failed: 1 +``` + +## Per-test results + +| Test | Result | +|---|---| +| FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField | Passed | +| NativeCloseWhileCommitPending_DoesNotCancelSelection | Failed | +| NativeCloseWithNoCommitPending_StillCancelsSelection | Passed | + +## Carve-out + +- NativeCloseWhileCommitPending_DoesNotCancelSelection is Failed, which this gate requires. +- FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField, landed by task P1-T5, is + Passed, which this gate requires. +- NativeCloseWithNoCommitPending_StillCancelsSelection is recorded PASSED. The gate admits either + Passed or Failed for this one test, because it asserts the behaviour the unfixed code already has + for the clear-latch case; the observed value is recorded here rather than assumed. +- No test other than the two named ones is Failed. In fact only one of the two is Failed, so the + carve-out is satisfied strictly. + +## Failure detail (the assertion that matters) + +``` +Expected harness.CancelCount to be 0 because a close racing an in-flight commit must not cancel the selection, but found 1 (difference of 1). +``` + +That is the defect under repair: with the latch declared and cleared at open time but not yet +consulted by `FinishClose`, a native-reason close cancels the selection even while a commit has been +requested for that popup lifetime. + +The failure is a runtime assertion failure, not a compile or arrange failure. The harness's own +pre-act assertions — that the open task completed, that the host reports open, that the show +delegate ran exactly once, and that the cancel delegate had not yet run — all passed, so the +headless host really reached the open state before the close was handed to it. + +Output Summary: 3 total, 2 passed, 1 failed; the one failure is the named expect-fail test and the +scoping companion passed at this gate. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t6-scoping-guard.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t6-scoping-guard.md new file mode 100644 index 000000000..0009b38a9 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t6-scoping-guard.md @@ -0,0 +1,80 @@ +# P5-T6 — Pending-commit-versus-native-close scoping guard + +Timestamp: 2026-09-07T14-28 +Task: [P5-T6] +Issue: #796 +Channel used: A + +File: QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs + +## `[TestMethod]` count + +Command: + +``` +pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs -SimpleMatch "[TestMethod]").Count' +``` + +EXIT_CODE: 0 +Measured count: 6 + +That is the five pre-existing tests, all kept, plus the one guard this task added, +CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection. No pre-existing test was renamed, +weakened or deleted. + +## Physical line count + +Command: + +``` +pwsh -NoProfile -Command '(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs).Count' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +| Path | Baseline | Measured | Ceiling | Verdict | +|---|---|---|---|---| +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 380 | 413 | 440 | within | + +## The two pinned scoping assertions + +Command: + +``` +pwsh -NoProfile -Command 'Select-String -Path QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs -SimpleMatch "CancelCount.Should()" -Context 0,1' +``` + +EXIT_CODE: 0 + +| Line | Text | Status | +|---|---|---| +| 48 | `harness.CancelCount.Should().Be(1);` | unchanged, still `.Be(1)`, still at line 48 | +| 79 | `harness.CancelCount.Should().Be(1);` | unchanged, still `.Be(1)`, still at line 79 | +| 113 | `harness.CancelCount.Should().Be(1);` | unchanged, still `.Be(1)`, still at line 113 | + +The two the spec pins, at lines 48 and 79, still read `.Be(1)` at their original positions. The +third, at line 113, is likewise unchanged; it is not named as a scoping guard by the spec but a fix +driving it to zero would be the same design signal, so its state is recorded too. + +The new guard was inserted below all three, which is why none of the three moved. The +`FocusAnchorCount` assertions at lines 49, 80 and 114 are likewise unchanged and still read `.Be(1)` +because the harness leaves the may-take-focus predicate at its permissive default, which this item +does not change. + +## Why the new guard is the complement of those assertions + +The retained assertions run with no commit in flight and still cancel. The new guard runs the same +close path with the commit latch set and does not cancel, while still asserting +`FocusAnchorCount == 1` so the suppression is shown to be confined to the cancel step rather than +skipping the whole completion. Together they show the AC3 suppression is conditional rather than +global. + +## Compile check + +EXIT_CODE: 0 for the analyzer rebuild recorded at TestResults/796/p5-t6/analyzer-rebuild.log +(gitignored). Build summary: 0 Error(s). + +Output Summary: 6 `[TestMethod]` members; 413 physical lines against the 440 ceiling; both pinned +`CancelCount` assertions still read `.Be(1)` at lines 48 and 79. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t7-ac3-pass-after.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t7-ac3-pass-after.md new file mode 100644 index 000000000..810e33ee1 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p5-t7-ac3-pass-after.md @@ -0,0 +1,69 @@ +# P5-T7 — AC3 pass-after run + +Timestamp: 2026-09-07T14-30 +Task: [P5-T7] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~BreadcrumbPendingOpenCloseTests" /ResultsDirectory:TestResults\796\p5-t7 "/Logger:trx;LogFileName=p5-t7.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Raw results (gitignored, never committed): TestResults/796/p5-t7/p5-t7.trx + +## Run summary + +``` +Test Run Successful. +Total tests: 9 + Passed: 9 +``` + +No test in either class is recorded as Failed. vstest.console.exe prints no `Failed:` line on a +successful run, so the Failed count is 0 with the note NOT PRINTED ON A PASSING RUN. + +## Per-test results + +| Test | Class | Result | +|---|---|---| +| FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField | BreadcrumbDropDownCloseOrderingTests | Passed | +| NativeCloseWhileCommitPending_DoesNotCancelSelection | BreadcrumbDropDownCloseOrderingTests | Passed | +| NativeCloseWithNoCommitPending_StillCancelsSelection | BreadcrumbDropDownCloseOrderingTests | Passed | +| CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent | BreadcrumbPendingOpenCloseTests | Passed | +| CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus | BreadcrumbPendingOpenCloseTests | Passed | +| CloseCanceledFactory_AllowsOneFreshReopenWithoutLateMutation | BreadcrumbPendingOpenCloseTests | Passed | +| CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection | BreadcrumbPendingOpenCloseTests | Passed | +| ToggleAndEscapeWhileOpenIsPending_EachClosesHostExactlyOnce | BreadcrumbPendingOpenCloseTests | Passed | +| AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce | BreadcrumbPendingOpenCloseTests | Passed | + +vstest.console.exe prints one combined Total for a multi-class filter and no per-class subtotal, so +the class column above was derived by attributing each recorded per-test result line to the class +that declares it, not read from the runner. + +## The proof that the suppression did not become global + +Both named scoping tests are Passed: + +- CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent — Passed. It carries the + retained `CancelCount.Should().Be(1)` assertion at line 48. +- CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus — Passed. It carries the retained + `CancelCount.Should().Be(1)` assertion at line 79. + +Neither was driven to zero by the fix, so a close with no commit in flight still cancels. + +## Fail-before to pass-after transition + +| Test | P5-T3 (fail-before) | P5-T7 (pass-after) | +|---|---|---| +| NativeCloseWhileCommitPending_DoesNotCancelSelection | Failed | Passed | +| NativeCloseWithNoCommitPending_StillCancelsSelection | Passed | Passed | + +The only expect-fail tests in these classes were landed by P5-T2 and made to pass by P5-T4, both of +which precede this gate. + +Output Summary: 9 total, 9 passed, 0 failed; the AC3 expect-fail test transitioned Failed to Passed +and both retained scoping assertions still hold. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t4-ac4-fail-before.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t4-ac4-fail-before.md new file mode 100644 index 000000000..0c7047419 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t4-ac4-fail-before.md @@ -0,0 +1,66 @@ +# P6-T4 — AC4 fail-before run (expect-fail) + +Timestamp: 2026-09-07T14-40 +Task: [P6-T4] [expect-fail] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~QfcItemController_SearchLeaveLatchTests" /ResultsDirectory:TestResults\796\p6-t4 "/Logger:trx;LogFileName=p6-t4.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 1 +ExpectedExitCode: 1 + +Raw results (gitignored, never committed): TestResults/796/p6-t4/p6-t4.trx + +## Run summary + +``` +Total tests: 2 + Passed: 1 + Failed: 1 +``` + +Total is 2 and not 0, so the compile entry task P6-T3 added took effect and this gate is not passing +on an empty population. + +## Per-test results + +| Test | Result | +|---|---| +| SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown | Failed | +| SearchLeaveAfterSearchDrivenOpen_ClosesDropDown | Passed | + +## Carve-out + +The single Failed test is exactly SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown, landed by +task P6-T2 as a deliberately-failing regression test and made to pass by task P6-T5. The paired +positive test is Passed. No test other than that exact named test is Failed. + +The filter names the new class exactly and does not match QfcItemController_EventHandlersTests: the +run reports a Total of 2, which is the count of `[TestMethod]` members in the new class alone, and +none of the other class's fourteen tests appears in the result lines. + +## Failure detail (the assertion that matters) + +``` +Moq.MockException: +Expected invocation on the mock should never have been performed, but was 1 times: v => v.SetFolderDroppedDown(False) +``` + +That is the AC4 gap: with the latch declared but not consulted, `TextBoxSearch_Leave` dismisses a +drop-down that a mouse gesture opened and the search box never owned. The recorded invocation list +shows the handler read `IsFolderDropDownOpen` and then dismissed, which is the unconditional +ownership the fix removes. + +## Note on unrelated console output + +The run's standard output carries a FluentAssertions licensing notice emitted by the assertion +library on first use. It is not a test result and has no bearing on this gate; it is recorded here +only so a later reader does not mistake it for a diagnostic from the code under test. + +Output Summary: 2 total, 1 passed, 1 failed; the one failure is the named expect-fail test and the +paired positive test passed. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t6-ac4-pass-after.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t6-ac4-pass-after.md new file mode 100644 index 000000000..a052dbe92 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p6-t6-ac4-pass-after.md @@ -0,0 +1,78 @@ +# P6-T6 — AC4 pass-after run + +Timestamp: 2026-09-07T14-43 +Task: [P6-T6] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~QfcItemController_SearchLeaveLatchTests|FullyQualifiedName~QfcItemController_EventHandlersTests" /ResultsDirectory:TestResults\796\p6-t6 "/Logger:trx;LogFileName=p6-t6.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Raw results (gitignored, never committed): TestResults/796/p6-t6/p6-t6.trx + +## Run summary + +``` +Test Run Successful. +Total tests: 18 + Passed: 18 +``` + +No test in either class is recorded as Failed. The 18 are the 2 the new +QfcItemController_SearchLeaveLatchTests declares plus the 16 QfcItemController_EventHandlersTests +declares; the split was derived by attributing each recorded per-test result line to its declaring +class, because vstest.console.exe prints one combined Total for a multi-class filter. + +## Fail-before to pass-after transition + +| Test | P6-T4 (fail-before) | P6-T6 (pass-after) | +|---|---|---| +| SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown | Failed | Passed | +| SearchLeaveAfterSearchDrivenOpen_ClosesDropDown | Passed | Passed | + +The only expect-fail test in these classes was landed by P6-T2 and made to pass by P6-T5, both of +which precede this gate. + +## The issue #680 contract, still pinned + +Three pre-existing tests exercise the paths the AC4 change touches, and all three are Passed: + +| Test | Result | What it pins | +|---|---|---| +| TextBoxSearch_KeyDown_WhenDownArrow_DropsDownAndFocusesFolder | Passed | the Down-arrow open still drops down and focuses | +| TextBoxSearch_KeyDown_WhenNotDownArrow_DoesNothing | Passed | a non-Down key still falls through untouched | +| TextBoxSearch_TextChanged_UsesInjectedFolderSearchHandler_PresentsSearchResultsWithoutFocusOrCommit | Passed | the search-typing path still presents results with no focus transfer and no committed selection | + +The third is the one that could have detected an unintended viewer call added to +`TextBoxSearch_TextChanged`, because it asserts `SetFolderDroppedDown` is never called there. It is +Passed, so the producer this task added to that handler writes only the private latch. + +## File-size measurement required by this task + +Command: + +``` +pwsh -NoProfile -Command '(Get-Content -LiteralPath QuickFiler\Controllers\QfcItemController.EventHandlers.cs).Count' +``` + +EXIT_CODE: 0 + +LINE-COUNT-IDIOM: (Get-Content -LiteralPath $_).Count + +| Path | Baseline | Measured | Ceiling | Verdict | +|---|---|---|---|---| +| QuickFiler/Controllers/QfcItemController.EventHandlers.cs | 263 | 317 | 500 | within | + +## Analyzer state after the fix + +The transient CS0649 recorded at tasks P6-T1 and P6-T3 against the not-yet-assigned +`_searchOwnedDismissal` field has cleared: the rebuild at TestResults/796/p6-t5/analyzer-rebuild.log +(gitignored) reports 0 Warning(s) and 0 Error(s), which is the baseline warning total. + +Output Summary: 18 total, 18 passed, 0 failed; the AC4 expect-fail test transitioned Failed to +Passed; the #680 contract tests still pass; the handler file measures 317 lines against 500. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p7-t3-ac1-ac5-guards.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p7-t3-ac1-ac5-guards.md new file mode 100644 index 000000000..d11b43203 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p7-t3-ac1-ac5-guards.md @@ -0,0 +1,112 @@ +# P7-T3 — AC1 and AC5 managed-seam guard run + +Timestamp: 2026-09-07T14-52 +Task: [P7-T3] +Issue: #796 +Channel used: A + +Command: + +``` +pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~BreadcrumbPendingOpenCloseTests" /ResultsDirectory:TestResults\796\p7-t3 "/Logger:trx;LogFileName=p7-t3.trx"; "EXIT_CODE=$LASTEXITCODE"' +``` + +EXIT_CODE: 0 + +Raw results (gitignored, never committed): TestResults/796/p7-t3/p7-t3.trx + +## Run summary + +``` +Test Run Successful. +Total tests: 11 + Passed: 11 +``` + +The combined Total is 11, which is the figure this gate requires: the 4 `[TestMethod]` members +BreadcrumbDropDownCloseOrderingTests holds after task P7-T1, plus the 7 +BreadcrumbPendingOpenCloseTests holds after task P7-T2. + +vstest.console.exe prints one combined Total for a multi-class filter and prints no per-class +subtotal. The two per-class figures below were therefore DERIVED, by attributing each recorded +per-test result line to the class that declares it, rather than read from the runner. + +| Class | Derived count | +|---|---| +| BreadcrumbDropDownCloseOrderingTests | 4 | +| BreadcrumbPendingOpenCloseTests | 7 | + +The derived figures agree with the independently measured `[TestMethod]` counts recorded below. + +## Per-test results + +| Test | Class | Result | +|---|---|---| +| FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField | BreadcrumbDropDownCloseOrderingTests | Passed | +| NativeCloseWhileCommitPending_DoesNotCancelSelection | BreadcrumbDropDownCloseOrderingTests | Passed | +| NativeCloseWithNoCommitPending_StillCancelsSelection | BreadcrumbDropDownCloseOrderingTests | Passed | +| GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose | BreadcrumbDropDownCloseOrderingTests | Passed | +| CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent | BreadcrumbPendingOpenCloseTests | Passed | +| CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus | BreadcrumbPendingOpenCloseTests | Passed | +| CloseCanceledFactory_AllowsOneFreshReopenWithoutLateMutation | BreadcrumbPendingOpenCloseTests | Passed | +| CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection | BreadcrumbPendingOpenCloseTests | Passed | +| ToggleAndEscapeWhileOpenIsPending_EachClosesHostExactlyOnce | BreadcrumbPendingOpenCloseTests | Passed | +| AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce | BreadcrumbPendingOpenCloseTests | Passed | +| RowSetRefreshWhileOpen_NeverClosesHost | BreadcrumbPendingOpenCloseTests | Passed | + +No test in either class is recorded as Failed. Every expect-fail test in these classes was made to +pass by task P5-T4, which precedes this gate. + +## Companion measurements for tasks P7-T1 and P7-T2 + +Neither of those two tasks names an artifact of its own, so their measurements are recorded here. + +`[TestMethod]` counts, measured with the P1-T7 command form applied to each file: + +``` +pwsh -NoProfile -Command '(Select-String -Path -SimpleMatch "[TestMethod]").Count' +``` + +EXIT_CODE: 0 + +| File | Measured | Required | +|---|---|---| +| QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs | 4 | 4 (P7-T1) | +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 7 | 7 (P7-T2) | + +Physical line count for the file P7-T2 constrains, measured with the idiom recorded on the +`LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other: + +| File | Measured | Ceiling | Verdict | +|---|---|---|---| +| QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs | 458 | 470 | within | + +## The pinned scoping assertions, re-verified after the Phase 7 insertions + +Task P5-T6 recorded the two spec-pinned `CancelCount.Should().Be(1)` assertions at lines 48 and 79. +The Phase 7 insertion was placed below them and the anchor and working-area values it needs were +declared as locals inside the new test rather than as class fields, specifically so those two +assertions keep their recorded positions. Re-measured after P7-T2: + +``` +L48: harness.CancelCount.Should().Be(1); +L79: harness.CancelCount.Should().Be(1); +L113: harness.CancelCount.Should().Be(1); +L49: harness.FocusAnchorCount.Should().Be(1); +L80: harness.FocusAnchorCount.Should().Be(1); +L114: harness.FocusAnchorCount.Should().Be(1); +``` + +All six are unchanged in text and unchanged in position. + +## What AC1 does and does not assert here + +GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose asserts at the managed seam that the open +task resolves true, that the host reports open, that the selection session reports the selector +open, and that no `Close` reaches the mocked host across the gesture open path. The part of AC1 that +is not automatable is that no FRAMEWORK close occurs: no framework drop-down is shown in a headless +test, so there is no `ToolStripDropDown` to raise `Closed`. That limit is stated in the test's own +doc comment rather than asserted, and it is covered by the Phase 2 manual observation. + +Output Summary: 11 total, 11 passed, 0 failed; combined Total matches the required 11; the two +per-class counts were derived and agree with the independently measured `[TestMethod]` counts. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md new file mode 100644 index 000000000..7071ffb22 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t1-search-dismissal-repin.md @@ -0,0 +1,110 @@ +# P8-T1 — Deliberate re-pinning of TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent + +Timestamp: 2026-09-07T15-00 +Task: [P8-T1] +Issue: #796 +Channel used: A + +## What was changed and why + +QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs arrived with the +original issue #680 fix at commit 660793e5, which predates this branch. Its test +`TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent` arranged an open +drop-down with no search-driven open and asserted one close intent. That is exactly the +mouse-driven state AC4 deliberately stops dismissing, so the test pinned a contract this +item supersedes. + +The test is updated, never weakened and never deleted: + +- the method name is unchanged; +- the assertion still reads `viewer.Verify(v => v.SetFolderDroppedDown(false), Times.Once());`, + so the issue #680 dismissal-ownership contract stays visibly pinned for the case that + still holds; +- exactly one Arrange line was added, establishing search-driven ownership before the + leave is raised; +- the doc comment was amended to state the search-ownership condition. + +## The single added Arrange statement, verbatim + +``` + QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true); +``` + +It is placed in the Arrange block immediately after the existing `BuildController(viewer)` +call. `QfcItemControllerTestSupport.SetField(QfcItemController, string, object)` is +declared at QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs line 40 and is +the same reflection injector this file already uses to install `_itemViewer`. The field +`_searchOwnedDismissal` is the AC4 provenance latch landed by task P6-T5, declared at +QuickFiler/Controllers/QfcItemController.EventHandlers.cs line 203 and read by +`TextBoxSearch_Leave` at line 263. + +## Diff shape + +Command: + +``` +git diff --numstat -- QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +``` + +Output: + +``` +7 0 QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs +``` + +Seven lines added, none removed. Six of the seven are `///` documentation-comment lines +amending the test's doc comment. The seventh is the single Arrange statement quoted +above, which is therefore the only non-comment line this task adds anywhere. No +production file is edited by this task, and the other five `[TestMethod]` members in the +class are unchanged. + +## `[TestMethod]` count, measured with the P1-T7 command form applied to this file + +Command: + +``` +pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Controllers\QfcItemController.SearchDismissalTests.cs -SimpleMatch "[TestMethod]").Count' +``` + +Output: + +``` +6 +``` + +Six, unchanged by this task: it adds no test and removes none. + +## Compile gate — the P0-T8 command form + +Command: the P0-T8 command form with the log path +`TestResults\796\p8-t1\analyzer-rebuild.log`. + +RunStartedUtc: 2026-09-07T18:59:16.8282314Z + +EXIT_CODE: 0 + +Build summary, verbatim: + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:22.07 +``` + +CscTaskCount=36 +CscToolCount=36 + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T18:59:27.6510553Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T18:59:31.5292881Z | yes | + +Raw log (gitignored): TestResults/796/p8-t1/analyzer-rebuild.log + +Output Summary: the method name and the `Times.Once()` assertion are unchanged; one +Arrange statement and six comment lines were added and nothing was removed; the file +still declares 6 `[TestMethod]` members; the solution compiles under the P0-T8 command +form with EXIT_CODE 0, 0 warnings and 0 errors, 36 Csc task and 36 csc.exe tool +invocations, and both touched assemblies rebuilt after RunStartedUtc. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md new file mode 100644 index 000000000..a9f6dd6f2 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/regression-testing/p8-t2-search-dismissal-verification.md @@ -0,0 +1,103 @@ +# P8-T2 — Rebuild, then scoped verification of the re-pinned search-dismissal class + +Timestamp: 2026-09-07T15-01 +Task: [P8-T2] +Issue: #796 +Channel used: A + +## Step 1 — mandatory rebuild + +The rebuild is part of this task and is not optional. P8-T1 edits test source, so a run +against QuickFiler.Test/bin/Debug/QuickFiler.Test.dll as task P7-T4 left it would measure +a stale binary and report the pre-update result. + +Command: the P0-T8 command form with the log path +`TestResults\796\p8-t2\analyzer-rebuild.log`. + +RunStartedUtc: 2026-09-07T19:00:52.2216349Z + +EXIT_CODE: 0 + +Build summary, verbatim: + +``` + 3 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:18.65 +``` + +CscTaskCount=36 +CscToolCount=36 + +| Assembly | LastWriteTimeUtc | At or later than RunStartedUtc | +|---|---|---| +| QuickFiler/bin/Debug/QuickFiler.dll | 2026-09-07T19:01:01.2395024Z | yes | +| QuickFiler.Test/bin/Debug/QuickFiler.Test.dll | 2026-09-07T19:01:05.1808112Z | yes | + +Both touched assemblies were rebuilt after RunStartedUtc, so the run below measured the +post-P8-T1 binary rather than the one Phase 7 left. + +The three warnings are recorded here rather than passed over. All three are `MSB3061` +raised by the `CoreClean` target of TaskMaster/TaskMaster.csproj, reporting that three +native payload files under TaskMaster/bin/Debug could not be deleted because a running +Microsoft Outlook process holds them open. None is an analyzer diagnostic, none names a +source file, and none arises in QuickFiler or QuickFiler.Test. The immediately preceding +P8-T1 run of the identical command form recorded 0 Warning(s), which places the cause +outside this item's diff. This task's acceptance turns on `EXIT_CODE: 0` only and makes +no warning-total comparison; the warning-total comparison against the P0-T8 baseline is +made at task P9-T3. + +Raw log (gitignored): TestResults/796/p8-t2/analyzer-rebuild.log + +## Step 2 — scoped run + +Command: the P1-T11 command form with the results directory `TestResults\796\p8-t2`, the +log file name `p8-t2.trx`, and the filter +`FullyQualifiedName~QfcItemController_SearchDismissalTests`. + +EXIT_CODE: 0 + +Run summary, verbatim: + +``` +Test Run Successful. +Total tests: 6 + Passed: 6 + Total time: 1.5434 Seconds +``` + +Total is 6, which is this class's `[TestMethod]` count. A Total of 0 would mean the +filter selected nothing and any other Total would mean the class's membership changed; +neither occurred. + +## Named results, all six + +``` +Passed TextBoxSearchKeyDown_EscapeWhileDropDownOpen_RoutesExactlyOneCloseIntent [234 ms] +Passed TextBoxSearchKeyDown_EscapeWhileDropDownClosed_RoutesNoIntentAndLeavesKeyUnhandled [1 ms] +Passed TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent [< 1 ms] +Passed TextBoxSearchLeave_WhileDropDownClosed_RoutesNoIntent [< 1 ms] +Passed TextBoxSearchLeave_AfterDownArrowHandoff_SuppressesExactlyOneClose [< 1 ms] +Passed TextBoxSearchKeyDown_DownArrow_StillOpensAndFocusesTheDropDown [< 1 ms] +``` + +`TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent` is named explicitly +among the Passed results above rather than inferred from the absence of a failure. No +test is recorded as Failed. No expect-fail test is declared in this class anywhere in +this plan, so no carve-out applies and any Failed result would have failed this gate +outright. + +## The gate was demonstrably able to fail + +Before P8-T1, this exact class was measured RED. The whole-assembly run recorded in +evidence/other/phase7-blocking-finding-out-of-write-set-test.md reports Total 6, Passed +5, Failed 1, with `TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent` +failing on `Expected invocation on the mock once, but was 0 times`. Only the P8-T1 +update turns it green, and a later regression in the AC4 latch turns it red again. + +Raw TRX (gitignored, never committed): TestResults/796/p8-t2/p8-t2.trx + +Output Summary: rebuild EXIT_CODE 0 with both touched assemblies refreshed; scoped run +EXIT_CODE 0, Total 6, Passed 6, Failed 0, with the re-pinned test named explicitly among +the Passed results. AC4 may now be checked off at P8-T6. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/feature-audit.2026-09-07T17-05.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/feature-audit.2026-09-07T17-05.md new file mode 100644 index 000000000..dcdd5f728 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/feature-audit.2026-09-07T17-05.md @@ -0,0 +1,285 @@ +# Feature Audit — issue #796 (QuickFiler folder drop-down closes on open; row click does not select) + +- Timestamp: 2026-09-07T17-05 +- Issue: #796 +- Work Mode: **full-bug** — marker read from `issue.md` line 12 (`- Work Mode: full-bug`) and corroborated by `spec.md` line 9 +- Authoritative AC source: **`spec.md` only** (`## Acceptance Criteria`, lines 326-331). `user-story.md` does not exist for this feature and its absence is by design, recorded at `spec.md` line 11. +- Baseline: `a6b259160f9ac1fbe251708d897fd4721486259e`; head reported by the caller: `8e427fe1` +- `issue.md` also carries a mirrored copy of the six criteria (lines 79-84). It is not the AC source in full-bug mode; it is noted so the mirror is kept consistent. + +## Method + +Every criterion below was evaluated against three classes of evidence, in this order: the code as it +currently stands in the worktree (read directly), the executor's fail-before / pass-after regression +artifacts, and the manual observation and decision record. Where a criterion's evidence is weaker than +its wording, that is stated rather than smoothed over. + +The Bash tool was not used, per the caller's binding constraint. Consequences are recorded in +`policy-audit.2026-09-07T17-05.md` § "Review Method and Its Limits". + +## AC Evaluation + +### AC1 — Opening the list by arrow click or by Down leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem + +**Verdict: PASS** (with a residual recorded below). + +Evidence chain, each link checked: + +1. The first cause of the flash was OBSERVED, not inferred, on both gestures AC1 names. Gesture A + (arrow click) and Gesture B (Down key) both read `FIRST-CAUSE-GESTURE-*: CANDIDATE-1` in + `evidence/other/close-ordering-decision.md`, derived from file-ordered excerpts in + `evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md`. +2. The observed close was not an independent framework close. Every close on every gesture reported + `CloseReason=CloseCalled ProgrammaticClose=True`, i.e. it was the add-in's own close call arriving + downstream of the deactivation cancel. Candidate 2 is REFUTED on all three gestures, twice over on + Gesture C (`AutoClose=False` at the moment it fired). +3. The cancel that produced that close is exactly what the AC2 guard now suppresses + (`QfcFormController.Deactivate.cs` line 118). +4. The guard fires at the right moment. I verified this rather than assuming it: + `BreadcrumbDropDownOpenLifetime.ShowCurrentSurface` sets `_host.OpenState = true` (line 268) before + calling `_host.ShowPopup(...)` (line 276), and `IsOpen => OpenState` + (`BreadcrumbDropDownHost.cs` line 191), so `host.IsOpen` is already true at any deactivation the + native show provokes. +5. Managed-seam assertion: `GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose` + (`BreadcrumbDropDownCloseOrderingTests.cs`) asserts the open task resolves true, the host reports + open, the session reports the selector open, and `Close` is never invoked across the gesture. + +Evidence paths: `evidence/other/close-ordering-decision.md`, +`evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md`, +`evidence/regression-testing/p7-t3-ac1-ac5-guards.md`, `evidence/qa-gates/p9-t5-full-assembly-tests.md`. + +Residual, recorded so PASS is not read as more than it is: no post-fix live observation exists. The +part of AC1 that is not automatable — that no FRAMEWORK close occurs, because no framework drop-down +is shown in a headless test — is stated in the test's own `` (lines 359-367) rather than +asserted, and in the spec's automation-feasibility table. PASS is recorded because the causal chain +closes on measured evidence at every link: the only observed close on gestures A and B was +programmatic and downstream of the cancel, so removing the cancel removes the close. A post-fix +reproduction of gestures A and B remains advisable before the issue is closed, but Outlook is +deliberately closed and this review did not relaunch it. + +### AC2 — A deactivation caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (#677 preserved) + +**Verdict: PASS.** + +- Both branches are pinned by tests on the real seam: + `FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector` asserts `Times.Never()` on both + injected controllers; `FormDeactivated_CancelsSelectorOnEveryItemController` retains `Times.Once()` + on both and now states the genuine case explicitly in Arrange rather than relying on a mock default. +- Fail-before is documented at `evidence/regression-testing/p4-t5-ac2-fail-before.md`; pass-after at + `evidence/regression-testing/p4-t9-ac2-pass-after.md`. The test appears in the expect-fail inventory + and is recorded Passed in the final TRX. +- The #677 contract is preserved by polarity, not by convention: `false` means GENUINE, so a viewer + that reports nothing keeps the pre-change behaviour. The polarity is documented as load-bearing at + the interface declaration (`IQfcFormViewer.cs`). +- The guard is scoped to the cancel loop and not to focus parking, and that scoping is an explicitly + recorded decision (`AC2-PARK-FOCUS-SUPPRESSED: NO`) derived from the measured `WebView2Focused` + values, not a default. +- The discriminator originally proposed (`Form.ActiveForm == null`) was REFUTED by the observation on + all four data points and is correctly not used, and correctly not inverted and re-used either. + +Limitations recorded, neither of which defeats the criterion: the producer implementation +(`QfcFormViewer.IsDeactivationSelfInflictedByOwnPopup` and the `ItemViewer.Breadcrumb.cs` registration) +sits in coverage-exempt WinForms types with no automated test (CR-7); and the guard also applies to +the #791 Cancel teardown caller, where its predicate is not meaningful (CR-3). CR-3 concerns a +different caller and a different contract, so it does not make AC2 itself unmet. + +Evidence paths: `evidence/regression-testing/p4-t5-ac2-fail-before.md`, +`evidence/regression-testing/p4-t9-ac2-pass-after.md`, `evidence/qa-gates/p4-t3-itemviewer-wiring.md`, +`evidence/qa-gates/p4-t7-park-focus-decision.md`, `evidence/qa-gates/p4-t8-guard-scope.md`, +`evidence/regression-testing/p4-t4-deactivate-suite-count.md`. + +### AC3 — A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs + +**Verdict: PARTIAL.** + +The criterion has two clauses and they carry different evidential weight. + +**Clause 2 — "the selection is committed before any auto-close cancel runs": PASS.** The pending-commit +latch is implemented at the site the evidence selected (`AC3-ENFORCEMENT-SITE: HOST`, derived from +`PendingClose=False` on all three gestures, which shows a coordinator-sited latch would have sat on a +path not taken). Both polarities are pinned — `NativeCloseWhileCommitPending_DoesNotCancelSelection` +and `NativeCloseWithNoCommitPending_StillCancelsSelection` — plus +`CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection` on the pending-open path, and the two +pre-existing `CancelCount.Should().Be(1)` assertions are retained unchanged as the scoping guard. I +also traced that `Close(ExplicitCommit)` is issued by the coordinator only after the session has +already committed, so the commit genuinely precedes the host-level cancel decision on that path +(detailed in `code-review.2026-09-07T17-05.md` § 1). + +**Clause 1 — "a mouse click on a row selects that row": not established post-fix.** The delivered +change makes no alteration to the row-activation path. `QuickFiler/Resources/FolderBreadcrumb.html` +was deliberately not changed, on the recorded decision `AC3-HTML-POINTERDOWN: NOT REQUIRED`. That +decision rests on a positive account — the observed first cause of Gesture C was candidate 1, the +deactivation cancel running before the row's selection could commit, which AC2 now suppresses — and +that account is sound. But the decision record itself states the limit in terms this audit will not +soften: + +> this decision states that the evidence does not support the page change, not that the page change +> has been shown unnecessary. If the AC2 seam lands and a row click still fails to select, the +> question is reopened. + +and `spec.md` carries the matching assumption as explicitly UNSETTLED (line 174). No post-fix +observation of a row click exists, and no automated test can supply one: the activation path travels +through the bridge coordinator and router, neither of which is instrumented, and the transcript's +silence on activation "discriminates between them not at all". + +So clause 1 currently rests on a causal argument, not an observation, and the feature's own record +flags it as reopenable. That is precisely the condition PARTIAL exists to express. + +**Recommendation:** AC3 should be UNCHECKED in `spec.md` line 328 until one of the following is +recorded: + +1. a single post-fix Gesture C observation (type, then click a row) confirming the row selects — one + runbook step, at whatever time Outlook is next open; or +2. an explicit maintainer acceptance of clause 1 on the causal argument, transcribed into `spec.md` + so the acceptance is visible in the merged history rather than living only in a review artifact. + +This is a verification and documentation gap, not a code defect. **It is not blocking**, and no code +change is requested for it. + +Evidence paths: `evidence/regression-testing/p5-t3-ac3-fail-before.md`, +`evidence/regression-testing/p5-t7-ac3-pass-after.md`, `evidence/regression-testing/p5-t6-scoping-guard.md`, +`evidence/qa-gates/p5-t4-enforcement-site.md`, `evidence/qa-gates/p5-t5-html-listener.md`, +`evidence/other/close-ordering-decision.md` § P3-T3. + +### AC4 — The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path + +**Verdict: PASS.** + +- The mechanism is the `_searchOwnedDismissal` provenance latch, verified live in the current source: + written at lines 183, 220, 234, 257 and 265 and read at line 263 in + `if (!_searchOwnedDismissal) return;`, which is the guard the criterion asks for. Producers are the + two search-driven open sites; the mouse path is covered by never setting the flag, so it needs no + edit anywhere. +- RED-first is documented and is genuine: `evidence/regression-testing/p6-t4-ac4-fail-before.md` + records EXIT_CODE 1 with `ExpectedExitCode: 1`, Total 2 / Passed 1 / Failed 1, the failing test named + exactly, and the Moq message "Expected invocation on the mock should never have been performed, but + was 1 times" — a failure for the intended reason. Total 2 rather than 0 also proves the new compile + entry took effect. +- Both polarities are pinned: `SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown` (`Times.Never()`) + and `SearchLeaveAfterSearchDrivenOpen_ClosesDropDown` (`Times.Once()`, driving the real open path). +- The #680 contract is preserved: the one-shot `_searchLeaveHandoffPending` latch is untouched and its + read-and-clear semantics are intact at lines 247-251; the five sibling #680 tests in + `SearchDismissalTests` are unchanged and pass. +- The dead accessor at line 209 does not affect this verdict: the field, not the property, is the + mechanism, and the field is fully live. See `policy-audit.2026-09-07T17-05.md` § 8 F1. + +Evidence paths: `evidence/qa-gates/p6-t1-ac4-seam.md`, `evidence/qa-gates/p6-t3-compile-entry.md`, +`evidence/regression-testing/p6-t4-ac4-fail-before.md`, `evidence/regression-testing/p6-t6-ac4-pass-after.md`, +`evidence/regression-testing/p8-t1-search-dismissal-repin.md`, +`evidence/regression-testing/p8-t2-search-dismissal-verification.md`. + +### AC5 — Row-set refreshes while open continue not to close the list (#438 AC-3 regression guard) + +**Verdict: PASS.** + +- `RowSetRefreshWhileOpen_NeverClosesHost` performs two row-set replacements while the selector is + open and asserts `Close` is never invoked on the mocked host and that + `BreadcrumbCoordinator.IsSelectorOpen` is still true. +- The guard is meaningful rather than circular: the session-preserving replacement path in + `UtilitiesCS` is deliberately outside this diff, so what the test observes is the untouched path. + The exclusion is verified two ways — the scope-boundary gate records 0 paths under `UtilitiesCS/` or + `UtilitiesCS.Test/`, and the caller-supplied full-code diff contains no such path. + +Evidence paths: `evidence/regression-testing/p7-t3-ac1-ac5-guards.md`, +`evidence/qa-gates/p7-t4-ac5-exclusion.md`, `evidence/qa-gates/p9-t10-scope-boundary.md`. + +### AC6 — The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen + +**Verdict: PASS.** + +Every clause of this criterion is separately checkable and each was checked: + +- **Both named sites are instrumented.** `ParkFocusAndCancelSelectors` logs at entry + (`QfcFormController.Deactivate.cs` lines 93-99) plus one line per item in the cancel loop (lines + 125-130). `OnDropDownClosed` logs at entry, ahead of the guard return + (`BreadcrumbDropDownHost.Diagnostics.cs`), so a close the host suppresses is still visible. +- **The required fields are present.** Entry: `WebView2Focused`, `ActiveFormNull`, `Groups`; per item: + `ItemNumber`, `SelectorWasOpen` (rendered `unavailable` rather than a fabricated boolean when + unobserved). Close: `CloseReason` (from the event args, which the handler previously discarded), + `ProgrammaticClose`, `OpenState`, `AutoClose`, `Disposed`, `PendingClose`. Two tests pin the field + sets against the pure formatters, so the AC6 evidence is a deterministic managed-seam assertion + rather than a source-text scan. +- **It was FIRST.** The instrumentation is commit `0dfcb402f4e3323c7f652b63701edd9bc5eb9fe0` + (`evidence/qa-gates/p1-t15-instrumentation-commit.md`), and every behavioural change landed in later + phases. The observation artifact records the build under test as `ec674e0c`, whose parent is + `0dfcb402` and which "adds documentation and plan check-offs only and changes no compiled source", + so the instrumentation is present in the observed build. +- **The ordering was read off the log and the fix was chosen from it.** The decision record derives + five decisions from quoted, file-ordered excerpts, and follows the observation over the prediction + in the two places they conflict. The `CloseReason` value — previously discarded — is what settled + candidate 2. +- **Logging shape matches repository convention:** sentence prefix followed by `Key=Value` pairs, + `logger` in the controller neighbourhood and `log` in the viewer neighbourhood, Debug level, no + control-flow change. + +The optional third site at the search-leave handler was not added; AC6 does not require it, and the +consequence — that candidate 3 is not directly observable — is recorded honestly in both the +observation and the decision record rather than being papered over with an argument from silence. + +Judged, as directed, on the HI-796-1 artifact plus the automated instrumentation tests. Outlook was +not relaunched. + +Evidence paths: `evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md`, +`evidence/regression-testing/p1-t12-behaviour-neutrality.md`, `evidence/qa-gates/p1-t2-host-line-count.md`, +`evidence/qa-gates/p1-t15-instrumentation-commit.md`, +`evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md`, +`evidence/other/p2-t2-manual-observation-conformance.md`, `evidence/other/close-ordering-decision.md`, +`runbooks/confirm-dropdown-close-ordering.runbook.md`. + +## Verdict Table + +| AC | Verdict | Checkbox action | +|---|---|---| +| AC1 | PASS | leave checked | +| AC2 | PASS | leave checked | +| AC3 | PARTIAL | **should be unchecked** pending clause 1 disposition | +| AC4 | PASS | leave checked | +| AC5 | PASS | leave checked | +| AC6 | PASS | leave checked | + +Per the `acceptance-criteria-tracking` protocol, a reviewer checks off PASS items that are not already +checked and leaves PARTIAL items unchecked with the gap documented. All six are already marked `[x]` +in `spec.md`, so no item required a new check-off. AC3 is the one mark this review cannot support as +written; it is reported here rather than altered, because unchecking it is a decision about the +criterion's disposition that belongs with the orchestrator and the maintainer, and because AC3's gap +is a verification gap with two acceptable dispositions rather than a defect requiring code. + +### Acceptance Criteria Status + +- Source: `docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md` +- Total AC items: 6 +- Checked off (delivered): 6 +- Verified PASS by this review: 5 +- Verified PARTIAL by this review: 1 +- Remaining (unchecked): 0 +- Items this review cannot support as checked: AC3 — "A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs." The commit-before-cancel clause is PASS; the row-selects clause has no post-fix evidence and is recorded as reopenable by the feature's own decision record. + +## Baseline Comparison + +| Dimension | Baseline | Head | Delta | +|---|---|---|---| +| `QuickFiler.Test` total tests | 1370 | 1380 | +10 | +| `QuickFiler.Test` failures | 0 (empty set) | 0 (empty set) | none | +| Analyzer warnings / errors | 0 / 0 | 0 / 0 | none | +| Nullable warnings / errors | 0 / 0 | 0 / 0 | none | +| CSharpier unformatted files | 0 | 0 | none | +| Repo-wide line coverage | 24.1387% | 24.1857% | +0.0470 pp | +| Repo-wide branch coverage | 22.9747% | 23.0082% | +0.0335 pp | +| Largest write-set file | 498 lines | 496 lines | −2 | +| Code paths changed vs base | — | 15 (all write-set members) | — | + +## Residuals Recommended for Follow-up + +Not blocking, and recorded here so they are not lost at merge: + +1. **AC3 clause 1 disposition** — one post-fix Gesture C observation, or a transcribed maintainer + acceptance in `spec.md`. +2. **CR-3** — the AC2 guard also gates the #791 Cancel teardown caller. Scope the guard to the + deactivation caller and add a teardown regression test. +3. **CR-2** — `IsCommitPending` survives a popup lifetime when an open fails before `ShowPopup`. +4. **CR-1 / CR-4 / CR-6** — stale comment at `BreadcrumbDropDownHost.cs:450`; the dead + `SearchOwnsDropDownDismissal` accessor; the reflection-by-field-name coupling in the re-pin. These + three are best fixed together, since removing the reflection would give the accessor a reader or + make its deletion obvious. +5. **`issue.md` AC mirror** — if AC3 is unchecked in `spec.md`, unchecked it in the `issue.md` mirror + at line 81 too, so the two do not diverge. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md new file mode 100644 index 000000000..97adacf1c --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md @@ -0,0 +1,95 @@ +# quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select (Issue #796) + +- Date captured: 2026-09-06 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select/ (Issue #796) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #796 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/796 +- Last Updated: 2026-09-07 +- Work Mode: full-bug + +## Summary + +In the QuickFiler item view, opening the folder drop-down makes the list flash open and immediately close, whether opened by clicking the arrow or by pressing Down in the search box. Typing letters in the search box does expand the list and keep it open, but clicking an item in the expanded list closes it without selecting that item; only the Up and Down keys change the selection. The list should stay open until it is closed explicitly, an item is selected, or a different QfcItem is selected, and a click on an item should select it. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in; the drop-down is a WebView2 breadcrumb (`QuickFiler\Resources\FolderBreadcrumb.html`) in `ItemViewer` plus a `ToolStripDropDown` popup hosting a second WebView2 (`BreadcrumbDropDownHost`); debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) +- Command/flags used: Outlook ribbon -> QuickFiler (ordinary and High Confidence) +- Data source or fixture: live mailbox, Inbox view + +## Steps to Reproduce + +1. Launch QuickFiler. On any item, click the drop-down arrow in the folder field. Observe: the list opens and closes within a fraction of a second. +2. Put the caret in the search box and press Down. Observe: the same flash. +3. Type two or three letters in the search box. Observe: the list expands with search results and stays open. +4. Click an item in the expanded list. Observe: the list closes and the folder field still shows the previous selection. +5. Use Up/Down keys instead. Observe: the selection changes as expected. + +Reproduces on every item; timing after item load does not matter (maintainer confirmed "it always occurs"). + +## Expected Behavior + +- Opening the list by mouse or keyboard keeps it open until: the user closes it (Escape, Left arrow, clicking the arrow again), an item is selected, or a different QfcItem is selected. +- Clicking an item in the open list selects that item and closes the list. +- A refresh of the row set while the list is open (search results, late suggestion decoration) does not close it (already guaranteed by #438 AC-3 and must remain so). + +## Actual Behavior + +- Open-by-arrow and open-by-Down both close immediately. +- Click on a row closes the list and discards the selection. +- Keyboard selection works. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- No ERROR/WARN lines are logged for this behavior; the close is a normal code path. Runtime instrumentation is required to confirm which of the two candidate close paths fires first (see Suspected Cause). + +## Impact / Severity + +- [ ] Blocker +- [x] High +- [ ] Medium +- [ ] Low + +Mouse selection of a filing folder is not possible in the item view; the user must type a search string and navigate with the keyboard. + +## Suspected Cause / Notes + +Confirmed by code read (2026-09-06): + +- The drop-down is not a ComboBox. The arrow is `#dropDownButton` in `FolderBreadcrumb.html:440-442`, which posts `selectorToggle`. The open pipeline is `BreadcrumbBridgeCoordinator.HandleSelectorMessage` (`:349-358`) -> `FolderBreadcrumbBridgeRouter.OpenSelector` -> `BreadcrumbSelectionSession.Open` -> `SelectorOpenStateChanged` -> `BreadcrumbDropDownOpenCoordinator.HandleSelectorOpenStateChanged` (`:178-191`) -> `BreadcrumbDropDownHost.OpenAsync` -> `BreadcrumbDropDownOpenLifetime.OpenCoreAsync` (`:215-256`) -> `FocusCurrentSurface` (`BreadcrumbDropDownOpenLifetime.Focus.cs:32-51`) -> `_host.FocusPending()`. The popup is a `ToolStripDropDown` with `AutoClose = true` (`BreadcrumbDropDownHost.cs:165-172`, `BreadcrumbDropDownHost.Open.cs:98-102`). The mouse toggle and the programmatic open share one request path (`QuickFiler.Test\Viewers\BreadcrumbSelectorOpenRetryTests.cs:55`), which is consistent with mouse and keyboard failing identically. +- Asynchronous suggestion decoration is not the cause: `SetSuggestionsAsync` / `SetSuggestionFallbacks` route through `ReplaceRowsPreservingSession` (`FolderBreadcrumbBridgeRouter.cs:478-482`) -> `BreadcrumbSelectionSession.ReconcileRowsReplaced` (`:119-147`), which preserves `IsOpen` and raises no `SelectorOpenStateChanged`. +- Three code paths cancel the selector session from outside the user's gesture and each closes the popup: + 1. `QfcFormController.ParkFocusAndCancelSelectors` (`QuickFiler\Controllers\QfcFormController.Deactivate.cs:39-57`, wired to `Form.Deactivate` at `QfcFormController.SetupDisposal.cs:175`, added by #677) cancels every item's selector when the QuickFiler form loses activation and moves `ActiveControl` back into the form (`QfcFormViewer.cs:207`). Opening the popup calls `Control.Focus()` on the popup's own top-level window (`ItemViewer.Breadcrumb.cs:203`), which deactivates the QuickFiler form. There is no latch distinguishing a self-inflicted deactivation from a real one. `FinishOpenCore` (`BreadcrumbDropDownOpenCoordinator.cs:273-288`) also re-checks `_isSelectorOpen()` after the async open and closes if the session was cancelled meanwhile. Primary suspect for the flash. (Win32 activation ordering is inferred; the downstream code is confirmed.) + 2. Native `ToolStripDropDown` auto-close -> `BreadcrumbDropDownHost.OnDropDownClosed` (`:426-437`) -> `FinishClose(Uncommitted)` (`:439-455`) -> `_cancelSelection()` = `BreadcrumbCoordinator.CancelSelector()` (`ItemViewer.Breadcrumb.cs:205`). This is the mechanism behind the click-without-select symptom: a click inside the popup's WebView2 shifts activation, the popup auto-closes, and the uncommitted selection is cancelled before the row's selection message commits. This hazard was recorded as unverifiable in `docs\features\archive\2026-08-07-quickfiler-search-keystroke-focus-steal-438\research\2026-08-08T10-30-...-research.md:172`. + 3. `QfcItemController.TextBoxSearch_Leave` (`QuickFiler\Controllers\QfcItemController.EventHandlers.cs:217-228`) closes the drop-down on search-box leave; its `_searchLeaveHandoffPending` latch (#680) is set only for the Down-arrow path (`:195`). Since Down also flashes, this path is not the primary cause but the missing mouse-path latch remains a gap. +- `MayRestoreBreadcrumbFocus` (`ItemViewer.Breadcrumb.cs:270-274`) requires `Form.ActiveForm` to be the QuickFiler form, so once the popup owns activation the focus step becomes a no-op while the cancel step always runs (asymmetry documented at `BreadcrumbDropDownHost.cs:452`). +- Existing tests that the fix must reconcile with: `QfcFormControllerDeactivateTests.FormDeactivated_CancelsSelectorOnEveryItemController` (`QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs:172`) pins cancel-on-deactivate; `BreadcrumbPendingOpenCloseTests` (`:124`, `:143`) encode "close wins over a pending open". +- Typing keeps the list open because `ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) reports no `OpenStateChanged` (#438 AC-3), and because after typing the search box holds focus inside the QuickFiler form, so no deactivation occurs. + +## Proposed Fix / Validation Ideas + +Acceptance criteria settled with the maintainer on 2026-09-06: + +- [x] AC1: Opening the list by arrow click or by Down in the search box leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem. +- [x] AC2: A deactivation of the QuickFiler form caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (the #677 contract is preserved for genuine deactivation). +- [ ] AC3: A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs. +- [x] AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. +- [x] AC5: Row-set refreshes while open (search, late decoration) continue not to close the list (#438 AC-3 regression guard). +- [x] AC6: The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen. + +Validation: + +- [ ] Unit coverage areas: self-inflicted-deactivation latch (Moq the form viewer's active-form seam); commit-before-cancel ordering in `BreadcrumbDropDownHost.FinishClose`; mouse-path leave latch; existing deactivate and pending-open tests updated to the new contract rather than weakened. +- [ ] Integration scenario to retest: open by mouse, open by Down, refresh while open, click a row, select a different QfcItem while open. +- [ ] Manual verification notes: the runbook `docs\features\archive\...-438\runbooks\verify-search-focus-retention.runbook.md` covers the search path; extend it with the arrow-click and row-click gestures. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md new file mode 100644 index 000000000..0e2d5bb86 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/plan.2026-09-06T21-59.md @@ -0,0 +1,517 @@ +# 2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select (Plan) + +- **Issue:** #796 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-06T21-59 +- **Status:** Draft +- **Version:** 0.2 +- **Work Mode:** full-bug +- **Branch:** bug/quickfiler-folder-dropdown-closes-on-open-796. Cut point (history only): the branch was cut from origin/main at c431dc32. First merge: after this plan cleared preflight, origin/main at dc8ca6d3 was merged into this branch at merge commit c7ae69f1, which brought in 146 changed files from two sibling bug items of the same parallel run and resolved with no conflicts. Second merge: after Phase 0 and Phase 1 executed and were committed, origin/main had advanced one merge further to 206a3f7e, and it was merged into this branch at merge commit d78ae7f7, and resolved with no conflicts. That second merge brought in 59 changed files measured against the merge base dc8ca6d3, and every incoming path lies under TaskMaster/, TaskMaster.Test/, UtilitiesCS/, UtilitiesCS.Test/, or a sibling item's feature folder and promoted record; none lies under QuickFiler/ or QuickFiler.Test/, and no write-set path was touched. Third merge: after Phase 8 and tasks P9-T1 and P9-T2 had executed and been committed, origin/main had advanced to a6b259160f9ac1fbe251708d897fd4721486259e, and it was merged into this branch at merge commit 5b8e0bf5, which is the current branch HEAD and resolved with no conflicts. That third merge brought in 1644 changed paths measured against the merge base 206a3f7e, with 0 insertions and 0 deletions across all of them, which is the signature of a pure rename set and is consistent with its stated purpose of correcting feature-folder date prefixes; a name-only diff of that range filtered to *.cs, *.csproj, *.sln, *.props, *.targets and *.config is empty, the same diff filtered to QuickFiler, QuickFiler.Test, UtilitiesCS, .claude, config and .github is empty, and the same diff filtered to paths containing 796 or quickfiler-folder-dropdown is empty, so no write-set path, no cited line number, and no path this plan names by name was moved or altered by it. Base anchors (operative), stated per gate because after the third merge they are deliberately NOT uniform and must not be unified by a later sweep: task P1-T14 is anchored at c7ae69f1 and has already executed, so that anchor is historical and is left exactly as it was when it ran; task P7-T4 is anchored at d78ae7f7; task P9-T10 is anchored at a6b259160f9ac1fbe251708d897fd4721486259e; and task P9-T7 is deliberately retained at c7ae69f1, because its diff is scoped to the QuickFiler directory, which the second merge did not touch, and because moving it to d78ae7f7 would drop this item's already-committed Phase 1 instrumentation lines out of the changed-line denominator that same task requires them to be counted in. The cut point is a historical statement about where the branch started and is not used as a diff anchor anywhere in this plan. +- **Authoritative acceptance-criteria source:** spec.md, section `## Acceptance Criteria` (AC1 through AC6). user-story.md does not exist and its absence is correct for full-bug mode. + +**Fail-closed evidence rule:** every baseline, QA, and coverage-comparison artifact named in this plan is mandatory. If any required artifact is missing or incomplete, the outcome is BLOCKED or INCOMPLETE, never PASS. + +**Evidence accounting rule:** each evidence-producing task names its artifact path. Do not mark an evidence-backed task complete without the artifact. + +--- + +## Path convention used by this document + +A backticked path in this plan is a write claim consumed by downstream blast-radius derivation. Every backticked path appears in the `## Write Set` section below and nowhere else. Every other file reference in this plan — citations, read-only scripts, evidence artifacts, gitignored raw tool output, and files deliberately excluded from the diff — is written without backticks on purpose, because the extractor has no notion of polarity and would read a backticked exclusion as a write claim. + +--- + +## AC6 is a hard ordering constraint on this plan + +AC6 is not merely a criterion to satisfy somewhere in the plan. It fixes the order of the phases, and the order is enforced as follows. A reader must not mistake this for a preference. + +1. Phase 1 is the FIRST implementation phase after Phase 0 and it contains ONLY instrumentation of the two sites AC6 names: `ParkFocusAndCancelSelectors` in QuickFiler/Controllers/QfcFormController.Deactivate.cs and `OnDropDownClosed`, moved into the new diagnostics part. No behavioural change of any kind appears in Phase 1. The only production edits Phase 1 makes are the addition of Debug-level log statements, the pure formatter methods those statements call, one internal get-only member on the concrete item controller that supplies a value one of those statements reports, and a pure relocation of `OnDropDownClosed` forced by the 500-line ceiling. The added member is observational in the same sense as the log statements: nothing but the diagnostic reads it, it forwards an expression the file it lands in already evaluates, and it adds no branching of its own. Phase 1 changes no interface, so no implementor anywhere in the tree is disturbed. +2. Phase 2 is a manual-observation gate. A human executes the runbook at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md against a Debug build produced from the Phase 1 commit, and produces an evidence artifact under the feature's evidence/other/ directory stating which candidate close path was confirmed and which were refuted, per gesture. The executor cannot satisfy this task by itself. That is intentional and is covered by the permitted human-interaction exception the orchestrator has already recorded. It must not be weakened into an optional step and must not be routed around. +3. Phase 3 reads that artifact and writes a normalised decision record into the feature's evidence/other/ directory naming which of the three candidate close paths was confirmed as the FIRST cause and which were refuted, plus the derived mechanism decisions the later phases consume. +4. Only after Phase 3 may any behavioural-fix phase appear. Phases 4 through 7 are the behavioural work and every one of them reads the Phase 3 decision record. + +The issue marks the Win32 activation ordering as INFERRED, not confirmed. This plan therefore specifies no fix that presupposes the inference. Research section 10 records that both the AC2 latch and the AC3 commit-before-cancel ordering will very likely be needed, and that the log determines which is the FIRST cause and therefore which carries the AC1 fail-before regression test. Phases 4 and 5 are both planned for that reason, and the assignment of the AC1 fail-before responsibility is made in Phase 3 from the evidence rather than asserted here. + +--- + +## Write Set + +The plan's diff must stay within the seventeen paths below. Every path is repository-relative and uses forward slashes. + +Two separate questions about an additional path have been settled at different times, and they are recorded separately below so that neither is read as the other. + +First, and now closed with no path added: preflight round 1 proposed QuickFiler/Interfaces/IQfcItemController.cs, to carry the per-item selector-open state the AC6 diagnostic reports. That proposal was adopted and then withdrawn after the planner established that adding a member to that interface breaks a compiled hand-written implementor outside the write set, on a target framework with no default interface members. The adopted resolution reaches the same value through an internal member on the concrete item controller, whose file is already in the write set, so that proposal added no path and no interface changes. That file remains excluded and is named again in the exclusion paragraph below. + +Second, and the reason the count is now seventeen rather than sixteen: `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` was added in the current revision pass, after executing AC4 surfaced a pre-existing test in that file which asserts the behaviour AC4 deliberately changes. This is a write-set derivation oversight and NOT merge damage, and it must not be recorded as merge damage: `git log --follow` places the file at commit 660793e5, the original issue #680 fix, which predates this branch and both merges of origin/main into it. The write set was derived from the files the fix would edit, and it did not include a pre-existing test asserting the behaviour an acceptance criterion deliberately changes. The path contains no whitespace, so it is expressible as a blast-radius write claim and is backticked here as one; that is the point on which it differs from QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs, the candidate path recorded in the exclusion paragraph below as rejected because its name contains a space and blast-radius derivation splits on whitespace. Task P8-T1 performs the deliberate update to that file, and task P8-T2 verifies it. + +This section and the equivalent section in spec.md's `## Write Set` state the same count of seventeen and the same two records. + +Two ALREADY-EXECUTED tasks state the count as sixteen inside their own acceptance text: P0-T14, whose artifact reproduces the permitted-change set, and P1-T4, whose acceptance forbids a diff outside the write set. Both had executed before the seventeenth path was added, so each is a historical record of the state it acted on and is deliberately left exactly as it was written, on the same basis as task P1-T14's eight-path list. Neither is read by any unexecuted gate. The only unexecuted task that counts the write set is P9-T10, whose acceptance clause is updated to seventeen in this same pass. + +### Production — modify + +- `QuickFiler/Controllers/QfcFormController.Deactivate.cs` +- `QuickFiler/Interfaces/IQfcFormViewer.cs` +- `QuickFiler/Viewers/QfcFormViewer.cs` +- `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` +- `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` +- `QuickFiler/Resources/FolderBreadcrumb.html` + +### Production — create + +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` + +### Test — modify + +- `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` +- `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` +- `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` + +### Test — create + +- `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` +- `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs` + +### Compile entries — modify + +- `QuickFiler/QuickFiler.csproj` +- `QuickFiler.Test/QuickFiler.Test.csproj` + +### Sibling contention + +`QuickFiler/Resources/FolderBreadcrumb.html` is genuinely shared. A concurrent sibling item edits the breadcrumb bridge router and the render projection classes for the row TEXT projection. This item owns the OPEN and CLOSE lifecycle and the selection-commit ordering only, and its change to that page is confined to the row activation listener at lines 289-291 (verified in this pass: the `click` listener posting `selectorActivate` spans exactly 289-291). The contention is recorded rather than avoided by dropping the file from the write set. + +### Explicitly not in the write set + +These files were considered and no change is expected, so they are named without backticks on purpose: QuickFiler/Interfaces/IQfcItemController.cs, QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs, QuickFiler/Viewers/IItemViewer.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs, UtilitiesCS/UtilitiesCS.csproj, UtilitiesCS.Test/UtilitiesCS.Test.csproj, QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs, QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.Focus.cs, QuickFiler/Viewers/IBreadcrumbDropDownHost.cs. This list is the same thirteen paths, in the same order, as the equivalent paragraph in spec.md's `## Write Set` section, and every path in both is written without backticks because a backticked path in a negative sentence is read as a write claim by downstream blast-radius derivation. QuickFiler/Interfaces/IQfcItemController.cs is excluded deliberately rather than by omission: preflight round 1 proposed adding a member to it, and the planner established that doing so breaks the compiled hand-written implementor FakeQfcItemController in QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs with CS0535, because the target framework has no default interface members. The adopted resolution reaches the same value through an internal member on the concrete controller instead, so neither file is edited. QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs is additionally not expressible as a write claim: it contains a space, and blast-radius derivation splits on whitespace, so the token would be split into two fragments and the claim would be silently lost rather than recorded. The AC5 regression guard is satisfied by leaving the session-preserving replacement path untouched, which is why the UtilitiesCS router and session files must stay out of the diff. No file under the .claude tree, the .codex tree, or the .agents tree is edited. No published JSON file under the config directory, no GitHub workflow file, no solution file, and no repository-root build property file is edited. + +--- + +## Verified constraints this plan is built on + +The figures below were not all measured in a single pass, and this section does not claim they were. Phase 0 and Phase 1 have executed and three merges of origin/main have landed since this plan was first authored, so each bullet states its own currency: a bullet that names d78ae7f7 was re-derived against the tree at d78ae7f7 in the revision pass that followed the execution of Phase 0 and Phase 1, and a bullet whose figures an executed task moved records that movement together with the resulting current positions rather than carrying the superseded ones silently. The third merge of origin/main landed at merge commit 5b8e0bf5, which is the current branch HEAD. It carried 0 insertions and 0 deletions across 1644 renamed documentation paths and touched no .cs, .csproj, .sln, .props, .targets or .config file, so every line number, count, and file citation in the bullets below that was re-derived at d78ae7f7 is still exact at 5b8e0bf5, and none of them required repair in the revision pass that followed that merge. + +- QuickFiler/Viewers/BreadcrumbDropDownHost.cs was 498 lines against the 500-line ceiling when this plan was authored, and declares no logger of any kind. AC6's host-side instrumentation therefore required the new partial part `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs`, following the partial-split precedent of `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` (107 lines). Executed task P1-T2 has since performed that relocation, so the rest of this bullet records the POST-MOVE state, re-derived against the tree at d78ae7f7 in the current revision pass: the main part is 485 lines; `OnDropDownClosed` no longer appears in it; the member `FinishClose`, which is the later AC3 edit site, opens at line 426 and its closing brace is at line 442; inside it the `DropDown.AutoClose = true` restore is at line 433 and the gated `FocusAnchorIfPermitted` argument is at line 440 (every line number in this sentence current at d78ae7f7). The PRE-MOVE figures — `OnDropDownClosed` at lines 426-437 and `FinishClose` at lines 439-455 — were the basis of the P1-T2 line-count arithmetic. That task has already executed, so those figures are retained here as the historical statement of what it acted on and are deliberately not repaired; they are not the positions any unexecuted task reads. The subscription `DropDown.Closed += OnDropDownClosed;` at line 171 and the `MayTakeFocus` property at line 216 were re-derived in the same pass and are unmoved, because the relocation removed only lines below both. +- The new diagnostics part must begin with the `#nullable enable` directive, because the moved handler signature is `OnDropDownClosed(object? sender, ToolStripDropDownClosedEventArgs e)` and nullable enforcement in this repository is per-file opt-in. QuickFiler/Viewers/BreadcrumbDropDownHost.cs line 1 and QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs line 1 both carry it. +- The `DropDown.Closed += OnDropDownClosed;` subscription at QuickFiler/Viewers/BreadcrumbDropDownHost.cs line 171 stays in the main part and continues to bind after the move, because both parts declare the same `sealed partial class BreadcrumbDropDownHost`. +- QuickFiler/Viewers/ItemViewer.Breadcrumb.cs is 456 lines. Growth there is constrained; the AC2 wiring change, if the decision record calls for one, is one assigned line beside `host.MayTakeFocus = MayRestoreBreadcrumbFocus;` at line 212. +- QuickFiler.Test/Viewers/BreadcrumbDropDownHostTests.cs is 499 lines and QuickFiler.Test/Viewers/BreadcrumbDropDownIntegrationTests.cs is 500 lines. Neither can absorb a new host-level test, which is why `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` is created. +- QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs is 477 lines, leaving 23 lines of headroom, which is why `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs` is created. Its class name follows the existing convention in that file, where the file QfcItemController.EventHandlersTests.cs declares the class QfcItemController_EventHandlersTests at line 25; the new file declares QfcItemController_SearchLeaveLatchTests. +- `QuickFiler/QuickFiler.csproj` and `QuickFiler.Test/QuickFiler.Test.csproj` are non-SDK-style. Every added .cs file needs an explicit `` entry or it is silently not compiled. The existing entries at QuickFiler.csproj lines 415-416 and QuickFiler.Test.csproj lines 83-89 are the pattern to follow. Both line ranges were re-derived against the current tree in the second-merge reconciliation pass. Two rounds of insertion have moved citations inside these two files, so the arithmetic is positional rather than uniform and is recorded here once. The first merge of origin/main inserted one compile entry into the QuickFiler project file and two into the QuickFiler.Test project file. Executed task P1-T3 then inserted the diagnostics-part entry into the QuickFiler project file at line 417, and executed task P1-T6 inserted the close-ordering test entry into the QuickFiler.Test project file at line 83, each shifting every later line in its own file by one further. The second merge of origin/main touched neither project file; that was re-derived against the tree rather than assumed. Current positions, all measured in this pass: QuickFiler.csproj line 415 is the entry for Viewers\BreadcrumbDropDownHost.cs, line 416 is the entry for Viewers\BreadcrumbDropDownHost.Open.cs, line 417 is the entry P1-T3 added for Viewers\BreadcrumbDropDownHost.Diagnostics.cs, and the analyzer-reference block that task P0-T4 cites at lines 593-602 now stands at lines 594-603; that citation is left as written because P0-T4 has already executed and it was correct at the moment it ran. QuickFiler.Test.csproj lines 83 through 89 are seven consecutive Viewers compile entries, of which line 83 is the entry P1-T6 added; the Controllers entry for QfcFormControllerDeactivateTests.cs now stands at line 158, and the Helper Classes entry for QfcThemeHelperTests.cs at line 220. The citations at task P6-T3 and at Decisions record item 9 have been corrected to those two current values, because neither has executed and both are read at execution time. +- QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs was 248 lines and declared 7 `[TestMethod]` members when task P0-T12 pinned it; research section 6.1 says "six test methods" in prose while its own table lists seven, and the measured value 7 is the one that task used. Executed task P1-T7 has since appended one test, so the CURRENT figures, re-derived against the tree at d78ae7f7 in this pass, are 274 lines and 8 `[TestMethod]` members. Task P4-T4's assertion of 9 is derived from the current 8 and not from the superseded 7, so no unexecuted gate reads the pinned figures. QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs is 380 lines and declares 5 `[TestMethod]` members, both re-derived in the same pass and unmoved. +- `QuickFiler/Viewers/QfcFormViewer.cs` carries a class-level `[ExcludeFromCodeCoverage]` at line 17, and `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` is a partial part of `ItemViewer`, which carries the same attribute at QuickFiler/Viewers/ItemViewer.cs line 20. Lines changed in either file are therefore NOT MEASURABLE by the coverage tooling. The changed-line coverage gate in Phase 9 is scoped to the measurable files for that reason, and records the excluded files explicitly rather than reporting a figure that has no source. +- `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` carries method-level `[ExcludeFromCodeCoverage]` attributes at lines 60, 83, 97, 111 and 125 only. `TextBoxSearch_KeyDown` at line 190 and `TextBoxSearch_Leave` at lines 217-228 are not excluded and are measurable. +- NOT STALE, re-derived against the tree at d78ae7f7 in the current revision pass and recorded here so that a later reader who notices Phase 1 edited these two files does not assume citation drift and reopen the question. Every citation this plan makes against `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` and against `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` is correct as written and required no repair. The reason is positional: each Phase 1 edit APPENDED past every cited region in its file rather than inserting before it. Task P1-T4 added the member `IsBreadcrumbSelectorOpen` at line 243 of the controller file, below all five method-level `[ExcludeFromCodeCoverage]` attributes (still at 60, 83, 97, 111 and 125), below `_searchLeaveHandoffPending` (188), `TextBoxSearch_KeyDown` (190), its single producer (195), `TextBoxSearch_Leave` (217) and the read-and-clear consumer (219-224); the unexecuted tasks that read those positions are P6-T1, P6-T5 and P9-T7. Task P1-T7 added FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField at line 255, the end of the test file, below FormDeactivated_WebView2Focused_ParksFocusOnce (134), the doc comment at 167-170, FormDeactivated_CancelsSelectorOnEveryItemController (172) and its two `Times.Once()` assertions (185-186); the unexecuted tasks that read those positions are P3-T4, P4-T6 and P4-T7. The five exclusion line numbers matter beyond their count because task P9-T7's acceptance paragraph reads them; they did not move, so no acceptance gate in this plan reads a stale citation, and the drift the current revision pass repaired in the two other files is an instruction-position defect only. +- `QuickFiler/Interfaces/IQfcFormViewer.cs` is an interface-only file with no executable lines. `QuickFiler/Resources/FolderBreadcrumb.html` is not C#. Neither carries a coverage figure. +- The sole production implementor of `IQfcFormViewer` is `QuickFiler/Viewers/QfcFormViewer.cs` (declared `public partial class QfcFormViewer : Form, IQfcFormViewer` at line 18), and re-derivation in this pass confirms it is the only declaration in the tree that names the interface in a base list. Every other reference in a .cs file in the tree, other than the interface's own declaration at QuickFiler/Interfaces/IQfcFormViewer.cs line 12, is a consuming type usage in QuickFiler/Controllers, a Moq mock in QuickFiler.Test, a region marker, or a doc comment, and none of them is an implementation, so adding one interface member updates exactly one implementor and no consumer. + +--- + +## Toolchain and command conventions + +All C# tools are invoked through whichever of two command channels task P0-T2 records as available, and that task runs before every other command in this plan. Channel A is pwsh, in either the `-NoProfile -Command` or the `-NoProfile -File` form. Channel B is direct invocation from the Bash tool. Which channel is available is determined by observation at P0-T2 and not by this paragraph: in some sandboxes the isolation guard rejects every command whose name is pwsh, in both flag forms, and rejects an `env -C` prefixed form as well, while in others pwsh runs normally. Either recorded value is a normal result. Every pwsh block shown in a later task of this plan is a command SHAPE. When P0-T2 records `COMMAND-CHANNEL: B`, the executor runs the Channel B equivalent recorded in that task's artifact and notes the substitution in the artifact of the task it substituted for. + +Channel B has three constraints that are load-bearing and must not be relaxed. First, Git Bash applies MSYS path translation to forward-slash switches, rewriting `/m` into a filesystem path and producing MSB1008, so msbuild is invoked with dash switches (`-t:Rebuild -m -nodeReuse:false -p:Configuration=Debug -p:Platform="Any CPU"`) and vstest.console.exe is invoked with its forward-slash switches intact behind an `MSYS_NO_PATHCONV=1` assignment prefix. Second, a quoted absolute path in the command-NAME position is refused by the same guard separately from pwsh, so a Windows executable that is not on PATH is invoked by bare name behind a `PATH=` assignment prefix; a quoted absolute path passed as an ARGUMENT is permitted. Third, test DLL paths are passed to vstest.console.exe with backslash separators, because mixed separators make vstest report that the test source file was not found. + +The CLAUDE.md toolchain order is: format, then analyze, then type-check, then test. Any failure or auto-fix restarts the loop from format. + +**The analyzer and nullable gates are vacuous without /t:Rebuild.** MSBuild's legacy non-SDK up-to-date check is timestamp-based and does not invalidate on a command-line `/p:` change, so a warm `/t:Build` returns exit code 0 having skipped `CoreCompile` on every project and analyzed nothing. Every analyzer and nullable task in this plan, baseline and final alike, therefore: + +- uses `/t:Rebuild`, never `/t:Build`; +- writes an MSBuild file log at detailed verbosity and reads a compiler-invocation count back out of it as a mandatory acceptance condition. An exit code of 0 with a zero compiler-invocation count is a FAILED gate, not a passing one; +- corroborates the count with an assembly-freshness observation on the two projects this item touches, so the gate cannot pass while those projects were skipped; +- passes `/nodeReuse:false`, because `/m` parallel rebuilds otherwise leave resident MSBuild worker processes that destabilise the subsequent test run. + +Applying the same form to the baseline task as to the final task is deliberate. If only one of the two used Rebuild, the comparison between them would silently depend on execution order. + +`/p:Nullable=enable` is NOT used anywhere in this plan. No project in this repository carries a `` element. The repository root does carry Directory.Build.props and Directory.Build.targets, re-derived in this pass, and neither sets a nullable property: the props file sets only RxUseUnsupportedPackagesConfig for issue #730, and the targets file sets only VSTO signing properties. Nothing in the build graph therefore opts any file into nullable analysis outside the per-file pragma, and the property is a solution-wide opt-in that conscripts every file which never adopted the pragma; it produced roughly two hundred errors in one project on a prior run and CI omits it deliberately. + +### Raw tool output versus committed evidence + +.gitignore line 84 ignores `*.log`, line 144 ignores everything under coverage/, line 39 ignores TestResults/ through the bracket class `[Tt]est[Rr]esult*/`, and line 57 ignores artifacts/. A raw MSBuild log, a raw Cobertura XML, or a TRX written under those paths therefore exists on disk and can never reach git. That is deliberate here: raw tool output goes to the gitignored paths TestResults/796/ and coverage/, and every command step additionally writes a committed Markdown evidence artifact under the feature's evidence tree carrying `Timestamp:`, `Command:`, `EXIT_CODE:` and `Output Summary:`, plus the extracted numbers and the raw output path. TRX files in particular are never committed, because a TRX embeds the host account name and machine name in its `runUser` and `computerName` attributes. + +### Test-run scoping and why + +Every test run in this plan targets QuickFiler.Test/bin/Debug/QuickFiler.Test.dll only. The UtilitiesCS.Test shell-icon test classes stall vstest.console.exe on this machine, so a repo-wide run is not used; every production file in the write set belongs to the QuickFiler project, whose only test assembly is QuickFiler.Test. `/InIsolation` is mandatory for the Moq-based assemblies and matches CI. Full-assembly runs additionally carry the exact filter `TestCategory!=LiveOutlook`, which is the same population the repository coverage runner applies at scripts/vscode/Invoke-MSTestWithCoverage.ps1 line 76, so a full-assembly run and a coverage run are comparable and neither starts an external Outlook process. Scoped runs use a `FullyQualifiedName~` substring filter naming exactly one test class; vstest 18.x rejects `OR` inside a filter, so multi-class filters join clauses with the pipe character. + +The FIRST merge of origin/main into this branch added two test classes to the QuickFiler.Test project, BreadcrumbBridgeRouterScoreJoinTests and QfcDatamodelRethrowTests, and modified a third, QfcItemController_FolderHandlingTests. All three names were checked against every `FullyQualifiedName~` substring this plan uses, and none of them contains any of those substrings, so no scoped run in this plan changes population as a result of that merge. The SECOND merge of origin/main added no class to the QuickFiler.Test project and modified none, so no scoped run in this plan changes population as a result of it either. That was re-derived against the current tree in the second-merge reconciliation pass rather than accepted as reported: every class declaration in the QuickFiler.Test project whose name contains any `FullyQualifiedName~` substring this plan uses was enumerated, and the result is exactly the classes this plan already names, plus the two further declaring parts of the partial class BreadcrumbDropDownHostTests, which are the same class and therefore the same population. No merge-added class collides with any filter substring. The only whole-assembly gate in this plan is P9-T5, and its acceptance is expressed relative to the P0-T10 baseline captured in this same post-merge tree rather than as an absolute total, so it is unaffected as well. No gate in this plan asserts an absolute test total over the whole QuickFiler.Test assembly. + +The current revision pass adds exactly one further `FullyQualifiedName~` substring to this plan, QfcItemController_SearchDismissalTests, used only by task P8-T2. It was re-derived against the current tree in that same pass rather than assumed: exactly one class declaration in the QuickFiler.Test project contains that token, at QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs line 26, and the similarly-named class ItemViewerSearchDismissalContractTests at QuickFiler.Test/Viewers/ItemViewerSearchDismissalContractTests.cs line 17 does not contain it, so that filter selects exactly one class and its population is that class's `[TestMethod]` members and nothing else. The substring is likewise disjoint from every other filter this plan uses: no existing filter substring matches that class, which is why the failure this revision repairs was surfaced by a whole-assembly run rather than by any scoped gate in Phases 4 through 7. + +### Expect-fail discipline + +The `[expect-fail]` tag is applied to the RUN tasks that record a deliberately-failing result, not to the authoring tasks that write the test source, because an authoring task does not itself fail. The complete expect-fail inventory for this plan, in plan order, is: + +| Test | Class the test lands in | Landed by | Recorded Failed at | Made to pass by | +|---|---|---|---|---| +| FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector | QfcFormControllerDeactivateTests | P4-T4 | P4-T5 | P4-T8 | +| The paired `ParkFocusOffWebView2()` negative test, ONLY when P4-T7 takes its YES branch | QfcFormControllerDeactivateTests | P4-T7 | no gate; none runs between P4-T7 and P4-T8 | P4-T8 | +| NativeCloseWhileCommitPending_DoesNotCancelSelection | BreadcrumbDropDownCloseOrderingTests | P5-T2 | P5-T3 | P5-T4 | +| NativeCloseWithNoCommitPending_StillCancelsSelection | BreadcrumbDropDownCloseOrderingTests | P5-T2 | Recorded Passed or Failed at P5-T3; both satisfy that gate's carve-out | P5-T4 | +| SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown | QfcItemController_SearchLeaveLatchTests | P6-T2 | P6-T4 | P6-T5 | + +The conditional row, second in the table, is conditional on the P4-T7 decision and is present in the inventory only when evidence/qa-gates/p4-t7-park-focus-decision.md records `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8`. The inventory is therefore four rows in the NO branch and five in the YES branch, and every gate that counts the inventory reads the branch before counting. + +Every scoped-run acceptance gate in this plan was swept against that inventory at the gate's own position in plan order, not at the end state, and in both branches of the P4-T7 decision. No gate runs between P4-T7 and P4-T8, so the conditional fifth test is never live at any gate and adds no carve-out to any of them. The gates that run a filter matching a class holding a live expect-fail test at that moment carry an explicit single-name carve-out naming the exact test, the earlier task that landed it, and the later task that makes it pass. The carve-outs are complete, not merely non-empty: no other test in any filtered class is permitted to be Failed at any gate. + +--- + +### Phase 0 — Baseline capture and worktree bootstrap + +A fresh agent worktree cannot run any C# gate without the first three bootstrap tasks. Every downstream `EXIT_CODE: 0` acceptance in this plan is unreachable if they are skipped, so they precede the first dotnet or msbuild command. + +- [x] [P0-T1] Read the policy files in the required order and record them in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/baseline/phase0-instructions-read.md. Read, in order: CLAUDE.md, .claude/rules/general-code-change.md, .claude/rules/general-unit-test.md, .claude/rules/quality-tiers.md, .claude/rules/tonality.md, .claude/rules/csharp.md, .claude/rules/plan-acceptance-gates.md. Acceptance: the artifact exists, carries `Timestamp:` and `Policy Order:`, and lists all seven paths as read. + +- [x] [P0-T2] Determine the available command channel, then install the repo-pinned .NET SDK, recording both in evidence/baseline/p0-t2-dotnet-sdk-install.md under the feature folder. global.json pins SDK 8.0.205 with `paths` including `.dotnet-sdk`, and a fresh worktree has none, so every dotnet command prints the global.json errorMessage instead of a version until this task completes. Rung 1: attempt + + ``` + pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1 + ``` + + Rung 2, taken only when rung 1 is refused by the isolation guard rather than failing on its own merits: record the guard's refusal text verbatim, then perform the same install from the Bash tool, which is what that script does and all it does — create the .dotnet-sdk directory, download `https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.205/dotnet-sdk-8.0.205-win-x64.zip` into it, unzip it in place, and delete the zip. `.gitignore` already ignores `.dotnet*/`, so neither rung adds a porcelain entry. Acceptance: the artifact records the line `COMMAND-CHANNEL: A` or the line `COMMAND-CHANNEL: B` exactly once; it records `EXIT_CODE: 0` for the rung taken; the directory .dotnet-sdk/sdk/8.0.205 exists; and the recorded stdout of `dotnet --version`, run on the recorded channel, is a version string beginning with the two characters `8.` rather than the sentence `The repo-local .NET SDK is missing.` recorded verbatim. When the recorded channel is B, the artifact additionally records, one per line, the Channel B equivalent of each command form this plan uses later: the msbuild form, the vstest form, the csharpier form, the NuGet restore form, and the file-line-count form. Those recorded equivalents are the commands the later tasks run, and no later task may improvise one that is not recorded here. + +- [x] [P0-T3] Restore NuGet packages for TaskMaster.sln by running scripts/vscode/Invoke-Restore.ps1 and record the result in evidence/baseline/p0-t3-nuget-restore.md under the feature folder. Seventeen of the eighteen projects in the tree declare `EnsureNuGetPackageBuildImports`, whose `` fires at `BeforeTargets="PrepareForBuild"`, so without this msbuild hard-fails with a missing-packages error in those seventeen projects followed by a CS0246 cascade. SVGControl/SVGControl.csproj is the one project that does not declare that target. Command: + + ``` + pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1 -SolutionPath TaskMaster.sln -Configuration Debug + ``` + + When P0-T2 recorded `COMMAND-CHANNEL: B`, run the recorded Channel B equivalent instead, which is a packages.config-based `nuget restore TaskMaster.sln` invoked by bare name behind a `PATH=` assignment prefix naming the directory holding nuget.exe. Record which channel was used. + + Acceptance: the artifact records `EXIT_CODE: 0` and the packages directory exists at the worktree root, evidenced by the recorded output of `pwsh -NoProfile -Command '(Get-ChildItem packages -Directory).Count'` being greater than 0. + +- [x] [P0-T4] Measure analyzer package version agreement between `QuickFiler/QuickFiler.csproj` and QuickFiler/packages.config, and between `QuickFiler.Test/QuickFiler.Test.csproj` and QuickFiler.Test/packages.config, recording the measured result in evidence/baseline/p0-t4-analyzer-version-skew.md under the feature folder. A missing analyzer HintPath is `error CS0006`, not a warning. This skew was resolved upstream in issue #647; re-measure it rather than assuming it is present or absent. The QuickFiler csproj entries, re-derived against the post-merge tree, are at lines 593-602 and name Meziantou.Analyzer 3.0.203, Roslynator.Analyzers 5.0.0, AsyncFixer 2.1.0, Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0 and SonarAnalyzer.CSharp 10.33.0.1635. Acceptance: the artifact lists, per analyzer, the csproj HintPath version and the packages.config version, states AGREES or SKEWED for each, and for every path listed records whether the referenced .dll exists on disk. If any row is SKEWED or any .dll is absent, the artifact records the remediation applied (a repeat of P0-T3, or a corrected HintPath) and the re-measured result; the task is complete only when every row reads AGREES with the .dll present. + +- [x] [P0-T5] Restore the local dotnet tool manifest for this worktree and record the result in evidence/baseline/p0-t5-dotnet-tool-restore.md under the feature folder. The manifest is dotnet-tools.json at the repository root and pins CSharpier 1.2.6, whose v1 CLI requires a subcommand, so the CLAUDE.md form `dotnet tool run csharpier format .` is the correct invocation. Verify that by running it rather than assuming it. Command: + + ``` + pwsh -NoProfile -Command 'dotnet tool restore; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Acceptance: the artifact records `EXIT_CODE: 0` for the restore, and additionally records the full stdout of `pwsh -NoProfile -Command 'dotnet tool run csharpier --version'`, which must print a version beginning with the two characters `1.` rather than a manifest-not-found error. + +- [x] [P0-T6] Probe for the dotnet-coverage tool used by scripts/vscode/Invoke-MSTestWithCoverage.ps1 and record the outcome in evidence/baseline/p0-t6-dotnet-coverage-probe.md under the feature folder. That script throws the sentence "dotnet-coverage not found." at line 293 when the tool is absent, which would make every coverage task in this plan unreachable. Rung 1: run `pwsh -NoProfile -Command 'dotnet-coverage --version'` and record the printed version. Rung 2, taken only if rung 1 fails: run `pwsh -NoProfile -Command 'dotnet tool install --global dotnet-coverage'` and record its `EXIT_CODE`, then repeat rung 1. Acceptance: the artifact records which rung was taken, and the final rung-1 invocation prints a version string with `EXIT_CODE: 0`. + +- [x] [P0-T7] Capture the CSharpier baseline over the whole tree and record it in evidence/baseline/p0-t7-csharpier-check-baseline.md under the feature folder. Command: + + ``` + pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Acceptance: the artifact records `EXIT_CODE:` verbatim, the full list of any files the check reports as unformatted, and the explicit verdict line `CSHARPIER-BASELINE: CLEAN` when the exit code is 0 with no files listed, or `CSHARPIER-BASELINE: PRE-EXISTING DRIFT` otherwise. Task P9-T1 branches on that verdict line, so it must be present and must carry exactly one of those two values. + +- [x] [P0-T8] Capture the analyzer baseline over TaskMaster.sln with the Rebuild target and record it in evidence/baseline/p0-t8-analyzer-rebuild-baseline.md under the feature folder. Record the UTC clock reading immediately before the run in the artifact as `RunStartedUtc:`. Command: + + ``` + pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\796\p0-t8\analyzer-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Then read the compiler-invocation counts back out of the log: + + ``` + pwsh -NoProfile -Command '$log = "TestResults\796\p0-t8\analyzer-rebuild.log"; "CscTaskCount=" + (Select-String -Path $log -Pattern "Task .Csc.").Count; "CscToolCount=" + (Select-String -Path $log -SimpleMatch "csc.exe").Count; (Get-Item QuickFiler\bin\Debug\QuickFiler.dll).LastWriteTimeUtc.ToString("o"); (Get-Item QuickFiler.Test\bin\Debug\QuickFiler.Test.dll).LastWriteTimeUtc.ToString("o")' + ``` + + Acceptance: the artifact records `EXIT_CODE: 0`; records both counts and at least one of `CscTaskCount` and `CscToolCount` is greater than zero; records the two assembly LastWriteTimeUtc values and both are at or later than the recorded `RunStartedUtc:`; and records the analyzer warning and error totals from the build summary as the baseline the final gate is compared against. An exit code of 0 with both counts at zero is a FAILED gate. + + Remediation branch, because this task rebuilds the whole solution while P0-T4 measures analyzer version agreement for QuickFiler and QuickFiler.Test only: an `error CS0006` naming an analyzer assembly in a project outside the write set is a missing restored package rather than a source defect. The remedy is to re-run the P0-T3 restore and record the second attempt in this task's artifact. If the same diagnostic recurs after that second attempt, the task halts with the diagnostic recorded verbatim rather than being marked complete. + +- [x] [P0-T9] Capture the nullable baseline over TaskMaster.sln with the Rebuild target and record it in evidence/baseline/p0-t9-nullable-rebuild-baseline.md under the feature folder, recording `RunStartedUtc:` immediately before the run. Command: + + ``` + pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\796\p0-t9\nullable-rebuild.log;Verbosity=detailed"; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Acceptance: identical in form to P0-T8, reading TestResults/796/p0-t9/nullable-rebuild.log, and additionally recording that the command line contains no `/p:Nullable=enable` token. + +- [x] [P0-T10] Capture the QuickFiler.Test/bin/Debug/QuickFiler.Test.dll baseline test result and record it in evidence/baseline/p0-t10-quickfiler-test-baseline.md under the feature folder. Command: + + ``` + pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" /ResultsDirectory:TestResults\796\p0-t10 "/Logger:trx;LogFileName=p0-t10.trx"; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Acceptance: the artifact records `EXIT_CODE:`, the Total and Passed counts read from the run summary, the Failed count read from the summary when the run printed a `Failed:` line and recorded as 0 with the note `NOT PRINTED ON A PASSING RUN` when it did not, and a Skipped count derived as Total minus the sum of Passed and Failed rather than read, because vstest.console.exe prints no `Skipped:` line on a run with no skipped tests and the TRX `notExecuted` attribute is hard-coded to 0; the artifact states that the derivation was used, and states that the `TestCategory!=LiveOutlook` filter excludes rather than skips, so filtered tests appear in neither the Total nor the derived Skipped figure; and the explicit named set of any test that is Failed at baseline. This named set is the BASELINE_FAILURE_SET that the Phase 9 final run is compared against; a repository-wide "zero failed" expectation is not asserted anywhere in this plan, only non-growth relative to this set. + +- [x] [P0-T11] Capture the coverage baseline for the QuickFiler.Test assembly and record it in evidence/baseline/p0-t11-coverage-baseline.md under the feature folder. Command: + + ``` + pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput coverage\p0-t11-baseline.cobertura.xml + ``` + + Then extract the numbers: + + ``` + pwsh -NoProfile -Command '[xml]$c = Get-Content coverage\p0-t11-baseline.cobertura.xml; "line-rate=" + $c.coverage.GetAttribute("line-rate"); "lines-covered=" + $c.coverage.GetAttribute("lines-covered"); "lines-valid=" + $c.coverage.GetAttribute("lines-valid"); "branch-rate=" + $c.coverage.GetAttribute("branch-rate"); "branches-covered=" + $c.coverage.GetAttribute("branches-covered"); "branches-valid=" + $c.coverage.GetAttribute("branches-valid")' + ``` + + Acceptance: the file coverage/p0-t11-baseline.cobertura.xml exists and the artifact records all six numeric attributes above in `Output Summary:`, plus one row for each of QuickFiler/Controllers/QfcFormController.Deactivate.cs, QuickFiler/Viewers/BreadcrumbDropDownHost.cs, QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs, QuickFiler/Controllers/QfcItemController.EventHandlers.cs and QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs, each row carrying either that file's `lines-covered` and `lines-valid` or the literal `ABSENT: no class node carries this filename` with the name of the class node that did carry the enclosing type. The absent case is admitted and recorded rather than treated as a failure, because a Cobertura class element carries exactly one `filename` attribute while a partial class spans several source files, so a part other than the one the emitter chose produces no group at all; recording it as absent is what keeps the baseline and the final run comparable, and task P9-T7 excludes any file recorded ABSENT at both P0-T11 and P9-T6 from the changed-code denominator and names it in the NOT MEASURABLE list. The recorded filenames are reproduced verbatim as the tool emitted them, which uses backslash separators, and the artifact states that the forward-slash spellings above are the same five files. The rows are obtained with: + + ``` + pwsh -NoProfile -Command '[xml]$c = Get-Content coverage\p0-t11-baseline.cobertura.xml; $c.SelectNodes("//class") | Group-Object { $_.GetAttribute("filename") } | ForEach-Object { $n = $_.Name; $v = 0; $h = 0; $_.Group | ForEach-Object { $v += $_.SelectNodes("lines/line").Count; $h += $_.SelectNodes("lines/line[@hits>0]").Count }; "$n lines-covered=$h lines-valid=$v" }' + ``` + + Class nodes are grouped by their `filename` attribute because a C# async state machine is emitted as a separate class node and would otherwise split one source file's denominator across several nodes. The relative child axis is used rather than a descendant axis, because a descendant axis double-counts on nested nodes. `EXIT_CODE:` is recorded verbatim. A non-zero exit code is accepted for this task only when the recorded stderr contains the literal `is below the required 80`, which is the single-line message scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1 line 54 throws when the document-level line rate is under the runner's own 80 percent floor; that throw occurs after the Cobertura post-processing at line 342, so the numbers are still written and readable. Any other non-zero exit code fails the task. + +- [x] [P0-T12] Capture the file-size baseline for every .cs and .html path in the write set and record it in evidence/baseline/p0-t12-file-size-baseline.md under the feature folder. Command: + + ``` + pwsh -NoProfile -Command '@("QuickFiler\Controllers\QfcFormController.Deactivate.cs","QuickFiler\Interfaces\IQfcFormViewer.cs","QuickFiler\Viewers\QfcFormViewer.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.cs","QuickFiler\Viewers\BreadcrumbDropDownHost.Open.cs","QuickFiler\Viewers\ItemViewer.Breadcrumb.cs","QuickFiler\Controllers\QfcItemController.EventHandlers.cs","QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs","QuickFiler\Resources\FolderBreadcrumb.html","QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs","QuickFiler.Test\Viewers\BreadcrumbPendingOpenCloseTests.cs") | ForEach-Object { $_ + " " + (Get-Content -LiteralPath $_).Count }' + ``` + + The physical-line idiom is `(Get-Content -LiteralPath $_).Count` on Channel A and `wc -l` on Channel B; the two agree. The idiom `(Get-Content $_ | Measure-Object -Line).Lines` is PROHIBITED throughout this plan, at baseline and at every later re-measurement alike, because `Measure-Object -Line` omits blank lines and therefore under-reports every count by that file's blank-line total. Measured in the preflight pass, the four pinned files carry 44, 49, 30 and 39 blank lines respectively, so that idiom would report 454, 407, 218 and 341 against the true 498, 456, 248 and 380. Acceptance: the artifact records one physical-line count per path, records the line `LINE-COUNT-IDIOM:` followed by the idiom actually used, and the recorded values for QuickFiler/Viewers/BreadcrumbDropDownHost.cs, QuickFiler/Viewers/ItemViewer.Breadcrumb.cs, QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs and QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs are 498, 456, 248 and 380 respectively. A divergence from those four values means the tree has moved since this plan was authored and the file-size arithmetic in Phase 1 and Phase 5 must be re-derived before proceeding. Every later task in this plan that re-measures a line count uses the idiom recorded on the `LINE-COUNT-IDIOM:` line and no other, so the baseline and the final audit are commensurable. + +- [x] [P0-T13] Probe for the plan validator MCP tool and record the result in evidence/baseline/p0-t13-mcp-validator-probe.md under the feature folder. Attempt one invocation of `mcp__drm-copilot__validate_orchestration_artifacts` with `artifact_type: "plan"` and `artifact_path` set to this plan file. Acceptance: the artifact records either the validator's exit status and output, or the exact line `VALIDATOR NOT RUN: tool absent from this session's tool surface`. This is a record-and-continue probe; it is not a halt gate, and execution proceeds either way. + +- [x] [P0-T14] Record the scope-lock baseline for the worktree this plan executes in, writing it to evidence/baseline/p0-t14-scope-baseline.md under the feature folder. Commands: + + ``` + pwsh -NoProfile -Command 'git rev-parse HEAD; git rev-parse --abbrev-ref HEAD; git status --porcelain --untracked-files=all' + ``` + + Acceptance: the artifact records the HEAD SHA, records the branch name as bug/quickfiler-folder-dropdown-closes-on-open-796, records the full porcelain output, and reproduces the sixteen write-set paths as the permitted-change set for the Phase 9 scope-boundary gate. `--untracked-files=all` is required because porcelain status collapses an untracked directory to a single directory entry and would not enumerate the evidence artifacts this plan creates. + + The artifact additionally records, under the heading `PRE-EXISTING-DIRTY-SET:`, every porcelain path present at this task that lies outside the feature folder, one path per line, together with its two-character status code. This set is expected to be EMPTY when the executor reaches this task, because the preparation run that produced this plan commits the feature folder and the promoted record and removes its own agent-memory writes before finishing. An empty set is the normal result and makes every later porcelain gate strict. The set is recorded rather than assumed empty because a non-empty set is a legitimate state the executor did not create and in some cases may not remediate: the .claude tree is one this plan may not edit at all. During the preflight pass that produced this task the set held five paths, four of them under .claude/agent-memory and one an untracked promotion record. Every later porcelain gate in this plan is evaluated against the porcelain output MINUS this recorded set, and a gate that would otherwise report zero lines is satisfied when the only lines it reports are members of this set. + + The artifact also records that the tracked file QuickFiler/QuickFiler.csproj.bak exists, is not in the write set, and must not be edited, so that a later search for compile entries does not mistake it for the project file. It is not a .cs file, so the formatter does not touch it, and no task in this plan reads it. + +--- + +### Phase 1 — AC6 instrumentation only, no behavioural change + +This phase adds Debug-level log statements at the two sites AC6 names, the pure formatter methods they call, one internal get-only member on the concrete item controller supplying the per-item selector-open value the deactivation diagnostic reports, and the pure relocation of `OnDropDownClosed` that the 500-line ceiling forces. It changes no control flow, no guard, no ordering, no default and no interface. The message-formatting logic is factored into pure `internal static` methods so AC6 carries a real deterministic managed-seam assertion rather than a source-text scan; the handlers themselves do nothing but pass current state to the formatter and hand the result to the logger. + +- [x] [P1-T1] Create `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` as a third partial part of the sealed class. The file begins with the `#nullable enable` directive, declares the static log4net field using the field name the QuickFiler/Viewers neighbourhood uses (`log`, matching QuickFiler/Viewers/BreadcrumbUiDispatcher.cs lines 17-19, not the `logger` name the QuickFiler/Controllers neighbourhood uses), declares the pure method `internal static string FormatDropDownClosedDiagnostics(ToolStripDropDownCloseReason closeReason, bool programmaticClose, bool openState, bool autoClose, bool disposed, bool pendingClose)`, and hosts the relocated `OnDropDownClosed` handler whose first statement is `log.Debug(FormatDropDownClosedDiagnostics(...))` emitted at entry, before the existing guard return, so a suppressed close is still visible. The formatter returns an interpolated single line with a sentence prefix followed by Key=Value pairs, matching the repository convention at QuickFiler/Controllers/QfcFormController.EventHandlers.cs lines 31 and 129, and its output contains the six field labels `CloseReason=`, `ProgrammaticClose=`, `OpenState=`, `AutoClose=`, `Disposed=` and `PendingClose=`. The pending-close value is read from the existing `_openLifetime.IsPendingClose` member declared at QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs line 103. Acceptance: the file exists, its first line is the `#nullable enable` directive, it declares the formatter with the exact signature given above, and it declares the log4net field named log. + +- [x] [P1-T2] Remove the relocated `OnDropDownClosed` handler from `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` lines 426-437, leaving the `DropDown.Closed += OnDropDownClosed;` subscription at line 171 and `FinishClose` untouched. This is a pure move: the removed body is byte-identical to the body relocated in P1-T1 apart from the added log statement. Acceptance: the file's physical line count, measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, is at most 486 and at least 480, and the recorded value is written into evidence/qa-gates/p1-t2-host-line-count.md under the feature folder. The band is derived against physical lines: the file is 498 physical lines, the handler occupies physical lines 426 through 437 inclusive, which is twelve lines, and its separating blank line at 438 is removed with it, so the expected physical result is 485 and the band admits five lines below the expected value and one above. A result outside the band means something other than the move happened. The band must not be evaluated against a blank-line-omitting count, which would report roughly 441 here and fail a correct move. + +- [x] [P1-T3] Add `` to `QuickFiler/QuickFiler.csproj` immediately after the existing entry for Viewers\BreadcrumbDropDownHost.Open.cs at line 416. The project is non-SDK-style, so without this entry the new part is silently not compiled and every downstream assertion about it becomes vacuous. Acceptance: the recorded output of `pwsh -NoProfile -Command 'Select-String -Path QuickFiler\QuickFiler.csproj -SimpleMatch "BreadcrumbDropDownHost.Diagnostics.cs"'` names exactly one matching line, written into evidence/qa-gates/p1-t3-compile-entry.md under the feature folder. + +- [x] [P1-T4] Add the AC6 instrumentation to `QuickFiler/Controllers/QfcFormController.Deactivate.cs` at `ParkFocusAndCancelSelectors` (lines 39-71). Add the pure method `internal static string FormatDeactivationDiagnostics(bool webView2Focused, bool activeFormIsNull, int groupCount)` returning a single interpolated line containing the labels `WebView2Focused=`, `ActiveFormNull=` and `Groups=`, and the pure method `internal static string FormatItemCancelDiagnostics(int itemNumber, bool? selectorWasOpen)` returning a single line containing the labels `ItemNumber=` and `SelectorWasOpen=`, rendering the second label as `SelectorWasOpen=unavailable` when the argument is null and as the boolean otherwise. The parameter is nullable so that the unavailable case is produced inside the formatter. That keeps the per-item log statement a single unconditional call, and it keeps the formatter reachable from the existing deactivate suite, whose tests inject their item controllers as Moq mocks of the interface and therefore never produce a successful cast to the concrete type. The `selectorWasOpen` value has no source on the item-controller INTERFACE, which declares `ItemNumber` and `CancelBreadcrumbSelector()` and no selector-open state and no viewer accessor. It is not obtained by changing that interface: adding a member there breaks the compiled hand-written implementor FakeQfcItemController in QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs with CS0535, and the target framework offers no default interface member. It is obtained instead through the concrete controller, which is internal to the same assembly as the deactivate handler. This task therefore also adds one internal get-only member to `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, reporting whether this item's breadcrumb selector is currently open by forwarding to the item viewer's existing `IsFolderDropDownOpen`; that expression is already evaluated in that same file at lines 200 and 225, so no new dependency is introduced. The deactivate handler reads it by casting the loop's interface-typed item controller to the concrete internal type and passes the result to the formatter. Both per-item argument expressions are null-safe, because the existing code guards that same reference with a null-conditional at line 56: the item number is obtained with a null-propagating access and a null-coalescing default, and the selector-open value with a null-propagating access on the cast result, which yields null both for a null item controller and for a controller that is not the concrete type. No `if` is added and no exception can escape the added statement, so a group whose `ItemController` is null continues to reach the boundary catch not at all, and Phase 1 stays free of behavioural change. Both edits are observational: no caller other than the diagnostic reads the member, and neither changes control flow, so Phase 1 remains free of behavioural change. Emit the first at method entry and the second once per item inside the existing loop, both through the existing static `logger` field already in scope from QuickFiler/Controllers/QfcFormController.cs and already used in this file at line 64. The item number is read from the existing `IQfcItemController.ItemNumber` member. The existing per-item boundary catch with its error logging is preserved unchanged. Acceptance: the file compiles, the existing catch block at lines 58-69 is unchanged in the diff, no `if`, `return`, `throw` or assignment other than the two log statements is added inside `ParkFocusAndCancelSelectors`, and the new internal member on the concrete item controller is declared and forwarded with no branching of its own. No file outside the sixteen write-set paths appears in the diff, and in particular neither QuickFiler/Interfaces/IQfcItemController.cs nor QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs is modified. The solution compiles under the P0-T8 command form, which is what proves both the cast and the forward are well typed. When the cast yields null, or when the loop's item controller is itself null, the diagnostic records the literal `SelectorWasOpen=unavailable` rather than a fabricated boolean, so the AC6 evidence never carries a value that was not observed. That case is produced inside the formatter from its nullable parameter, and every member access on the loop's item controller inside the added per-item log statement is written with a null-propagating or null-coalescing operator, matching the existing guard at line 56. + +- [x] [P1-T5] Create `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` declaring the class BreadcrumbDropDownCloseOrderingTests with the AC6 host-side test FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField, which calls the pure formatter with a fixed argument tuple and asserts with FluentAssertions that the returned string contains each of the six field labels and the supplied close-reason value. The test is MSTest, uses no window, no external process and no temporary file. Acceptance: the file exists, declares 1 `[TestMethod]` member, and the solution compiles under the P0-T8 command form. + +- [x] [P1-T6] Add `` to `QuickFiler.Test/QuickFiler.Test.csproj` alongside the existing Viewers entries at lines 83-89. Acceptance: the recorded output of `pwsh -NoProfile -Command 'Select-String -Path QuickFiler.Test\QuickFiler.Test.csproj -SimpleMatch "BreadcrumbDropDownCloseOrderingTests.cs"'` names exactly one matching line, written into evidence/qa-gates/p1-t6-compile-entry.md under the feature folder. + +- [x] [P1-T7] Add the AC6 controller-side test FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField to `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs`, asserting that the pure formatter's output contains the labels `WebView2Focused=`, `ActiveFormNull=` and `Groups=` and the supplied group count. Acceptance: the file now declares 8 `[TestMethod]` members, measured with `pwsh -NoProfile -Command '(Select-String -Path QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs -SimpleMatch "[TestMethod]").Count'`, and the count is recorded in evidence/qa-gates/p1-t7-deactivate-suite-count.md under the feature folder. + +- [x] [P1-T8] Format the paths this phase touched by running CSharpier over QuickFiler and QuickFiler.Test and record the result in evidence/qa-gates/p1-t8-csharpier.md under the feature folder. Commands, in order: capture `git status --porcelain --untracked-files=all` into the artifact; run `pwsh -NoProfile -Command 'dotnet tool run csharpier format QuickFiler QuickFiler.Test; "EXIT_CODE=$LASTEXITCODE"'`; capture `git status --porcelain --untracked-files=all` again into the artifact; then run `pwsh -NoProfile -Command 'dotnet tool run csharpier check QuickFiler QuickFiler.Test; "EXIT_CODE=$LASTEXITCODE"'`. Acceptance: the check invocation records `EXIT_CODE: 0`, and the artifact records both porcelain captures so a repairing run is distinguishable from a clean one. The exit code of the write-mode format invocation is identical on a clean run and a repairing one, which is why the before-and-after tree observation is the acceptance evidence and the format exit code is not. + +- [x] [P1-T9] Run the analyzer gate over TaskMaster.sln with the Rebuild target and record it in evidence/qa-gates/p1-t9-analyzer-rebuild.md under the feature folder, using the P0-T8 command form with the log path TestResults\796\p1-t9\analyzer-rebuild.log and `RunStartedUtc:` captured immediately before the run. Acceptance: `EXIT_CODE: 0`; at least one of `CscTaskCount` and `CscToolCount` greater than zero; both QuickFiler/bin/Debug/QuickFiler.dll and QuickFiler.Test/bin/Debug/QuickFiler.Test.dll carrying a LastWriteTimeUtc at or later than `RunStartedUtc:`; and the analyzer warning and error totals no greater than the P0-T8 baseline totals recorded in evidence/baseline/p0-t8-analyzer-rebuild-baseline.md. + +- [x] [P1-T10] Run the nullable gate over TaskMaster.sln with the Rebuild target and record it in evidence/qa-gates/p1-t10-nullable-rebuild.md under the feature folder, using the P0-T9 command form with the log path TestResults\796\p1-t10\nullable-rebuild.log. Acceptance: identical in form to P1-T9, compared against the P0-T9 baseline, and the recorded command line contains no `/p:Nullable=enable` token. + +- [x] [P1-T11] Run the two AC6 test classes from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll and record the result in evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md under the feature folder. Command: + + ``` + pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~QfcFormControllerDeactivateTests" /ResultsDirectory:TestResults\796\p1-t11 "/Logger:trx;LogFileName=p1-t11.trx"; "EXIT_CODE=$LASTEXITCODE"' + ``` + + Acceptance: `EXIT_CODE: 0`; FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField and FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField both recorded as Passed; Total recorded as 9, being the 8 methods QfcFormControllerDeactivateTests holds after P1-T7 plus the 1 method BreadcrumbDropDownCloseOrderingTests holds after P1-T5; and no test recorded as Failed. No expect-fail test exists in either class at this point in plan order, so no carve-out applies to this gate. + +- [x] [P1-T12] Run the behaviour-neutrality gate over the four existing suites that pin the open and close lifecycle in QuickFiler.Test/bin/Debug/QuickFiler.Test.dll and record it in evidence/regression-testing/p1-t12-behaviour-neutrality.md under the feature folder. Use the P1-T11 command form with the results directory TestResults\796\p1-t12, the log file name p1-t12.trx, and the filter `FullyQualifiedName~BreadcrumbPendingOpenCloseTests|FullyQualifiedName~BreadcrumbDropDownHostTests|FullyQualifiedName~BreadcrumbDropDownIntegrationTests|FullyQualifiedName~BreadcrumbSelectorOpenRetryTests`. Acceptance: `EXIT_CODE: 0`, no test recorded as Failed, and a recorded Total of at least 1 so the gate cannot pass on an empty population. Phase 1 adds no test to any of these four classes and changes no behaviour they exercise, so any failure here means the instrumentation phase changed behaviour and the phase must be reverted rather than accepted. + +- [x] [P1-T13] Confirm the Debug build output the runbook consumes exists at TaskMaster/bin/Debug and record it in evidence/qa-gates/p1-t13-debug-build-output.md under the feature folder. Command: + + ``` + pwsh -NoProfile -Command 'Get-ChildItem TaskMaster\bin\Debug\*.dll | ForEach-Object { $_.Name + " " + $_.LastWriteTimeUtc.ToString("o") }' + ``` + + Acceptance: the artifact lists QuickFiler.dll among the files present and its LastWriteTimeUtc is at or later than the `RunStartedUtc:` recorded in evidence/qa-gates/p1-t10-nullable-rebuild.md, so the output the human will run is the instrumented output rather than a stale copy. + +- [x] [P1-T14] Audit that Phase 1 changed only the files it was permitted to change, recording the result in evidence/qa-gates/p1-t14-phase1-scope.md under the feature folder. Commands: `pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796'` followed by `pwsh -NoProfile -Command 'git diff --cached --name-status c7ae69f1'` and `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'`. The staging span is required because a name-listing diff cannot see a file this phase created. Acceptance: every path the diff lists is either inside the feature folder or is one of these eight write-set paths: `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` (created by P1-T1), `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` (P1-T2), `QuickFiler/QuickFiler.csproj` (P1-T3), `QuickFiler/Controllers/QfcFormController.Deactivate.cs` (P1-T4), `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` (P1-T4), `QuickFiler.Test/QuickFiler.Test.csproj` (P1-T6), `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` (created by P1-T5) and `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` (P1-T7); and the porcelain output lists no path outside the feature folder, the write set, and the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md. The set is the union of the paths the seven preceding authoring tasks of this phase edit or create, and it is derived from those tasks rather than asserted here. `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` is a member because task P1-T4 adds the observational selector-open member on the concrete item controller in that file; that edit and the deactivate-handler edit are both observational and neither changes control flow, so the AC6 ordering constraint that Phase 1 contains no behavioural change still holds. QuickFiler/Interfaces/IQfcItemController.cs is not a member and must not appear in the diff, because the adopted mechanism changes no interface. No other write-set path may appear at this point in plan order, because no behavioural phase has run yet. + +- [x] [P1-T15] Commit the Phase 1 instrumentation from the worktree this plan executes in and record the resulting SHA in evidence/qa-gates/p1-t15-instrumentation-commit.md under the feature folder. The runbook's step 1 requires the commit SHA of the build under test, so the commit must exist before Phase 2 begins. Command: `pwsh -NoProfile -Command 'git commit -m "instrument(796): add AC6 debug logging at the two named close-ordering sites"'` followed by `pwsh -NoProfile -Command 'git rev-parse HEAD'`. Acceptance: the artifact records the SHA, and `git status --porcelain --untracked-files=all` afterwards lists no path outside the feature folder and the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md. + +--- + +### Phase 2 — Manual observation gate, blocking + +This phase cannot be completed by the executor. It is the permitted human-interaction exception the orchestrator has already recorded, and it is the mechanism by which the INFERRED Win32 activation ordering becomes an observation. It is not a merge gate and it does not weaken AC6; AC6 requires the instrumentation to exist and the ordering to be confirmed, and this phase is the procedure that produces the confirmation. + +- [x] [P2-T1] BLOCKING HUMAN TASK. A human executes the runbook at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md end to end against a Debug build produced from the commit recorded in evidence/qa-gates/p1-t15-instrumentation-commit.md, and writes the resulting evidence artifact into docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/ with a filename beginning with an ISO-8601 `yyyy-MM-ddTHH-mm` timestamp. Acceptance: exactly one such artifact exists under that directory; it carries a `Timestamp:` field; it records the commit SHA of the build; it contains the redacted log excerpts for Gesture A, Gesture B and Gesture C in file order, not sorted by timestamp; and it states, per gesture, which candidate close path was confirmed and which were refuted. The executor must not synthesise this artifact, must not infer its content, and must not proceed past it. If the artifact is absent, the correct outcome is a halt with a request for the human step, not a substitute. + +- [x] [P2-T2] Verify the human artifact under docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/ is conformant, and record the verification in evidence/other/p2-t2-manual-observation-conformance.md under the feature folder. Check that lines from both required logger names appear in the transcript, that all lines under consideration carry the same thread name, and that a per-gesture confirmed-or-refuted statement is present for every one of the three gestures. Acceptance: the verification artifact contains exactly one of the two literal lines `MANUAL-OBSERVATION: CONFORMANT` or `MANUAL-OBSERVATION: INCONCLUSIVE`, and it names the source artifact's path. `MANUAL-OBSERVATION: INCONCLUSIVE` halts execution and returns the observation to the human; it must not be resolved by inference, because the entire purpose of the observation is to replace an inference with a measurement. + +--- + +### Phase 3 — Evidence-derived decision record + +Phase 3 reads only the two Phase 2 artifacts and writes a normalised, machine-checkable decision record. Every behavioural task in Phases 4 through 7 reads one of the literal lines below. No decision in this phase may be made from the plan's expectations; each must be traceable to a quoted excerpt from the human artifact. + +- [x] [P3-T1] Create the decision record at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md and write the per-gesture first-cause findings into it. The file contains exactly three lines of the form `FIRST-CAUSE-GESTURE-A:` followed by one of the four values CANDIDATE-1, CANDIDATE-2, CANDIDATE-3 or INCONCLUSIVE, and the same for `FIRST-CAUSE-GESTURE-B:` and `FIRST-CAUSE-GESTURE-C:`. Each line is followed by the quoted excerpt from the human artifact that establishes it and by the refutation status of the other two candidates for that gesture. Acceptance: the file exists and each of the three literal labels appears on exactly one line carrying one of the four admitted values. + +- [x] [P3-T2] Append the AC1 fail-before assignment to docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md as one line reading `AC1-FAIL-BEFORE-CARRIER:` followed by either AC2 or AC3. Research section 10 records that both the AC2 latch and the AC3 commit-before-cancel ordering will very likely be needed and that the log determines which is the FIRST cause and therefore which carries the AC1 fail-before regression test; this task makes that assignment from the recorded first-cause findings rather than from that expectation. Acceptance: the line exists exactly once with one of the two admitted values, and the record states which `FIRST-CAUSE-GESTURE-` line it was derived from. When every one of the three `FIRST-CAUSE-GESTURE-` lines reads INCONCLUSIVE, no derivation is available and this task is not discharged by guessing: the executor writes the line `AC1-FAIL-BEFORE-CARRIER: UNDERIVABLE` together with the three quoted INCONCLUSIVE lines, halts, and returns the observation to the human for a repeat of the Phase 2 runbook, exactly as `MANUAL-OBSERVATION: INCONCLUSIVE` does at P2-T2. That halt is the correct outcome, because assigning the AC1 fail-before responsibility from the plan's expectation rather than from the log is the specific substitution the AC6 ordering constraint exists to prevent. + +- [x] [P3-T3] Append the AC3 mechanism decisions to docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md as two lines: `AC3-ENFORCEMENT-SITE:` followed by either HOST or COORDINATOR, deciding whether the commit-before-cancel ordering is enforced in `FinishClose` in `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` or in `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`; and `AC3-HTML-POINTERDOWN:` followed by either REQUIRED or NOT REQUIRED, deciding whether the row activation listener at `QuickFiler/Resources/FolderBreadcrumb.html` lines 289-291 must move from the `click` event, which fires on mouseup, to a pointer-down event. REQUIRED is admissible only when the Gesture C transcript shows that no activation message was produced. Acceptance: both lines exist exactly once with admitted values, and each carries the excerpt it was derived from. + +- [x] [P3-T4] Append the AC2 mechanism decisions to docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md as two lines: `AC2-PARK-FOCUS-SUPPRESSED:` followed by YES or NO, deciding whether focus parking is also suppressed for a self-inflicted deactivation and therefore whether FormDeactivated_WebView2Focused_ParksFocusOnce at QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs line 134 changes; and `AC2-ITEMVIEWER-WIRING:` followed by REQUIRED or NOT REQUIRED, deciding whether the seam needs a popup-owns-activation assignment in `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` beside the existing `host.MayTakeFocus` assignment at line 212. This is an explicit decision task; the parking behaviour must not change by default. Acceptance: both lines exist exactly once with admitted values and each carries its derivation. + +- [x] [P3-T5] Append the AC4 mechanism decision to docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md as one line reading `AC4-MECHANISM:` followed by a single-token mechanism name, and one line reading `AC4-NEW-MEMBER:` followed by REQUIRED or NOT REQUIRED. The mechanism must be implementable entirely within `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, because that is the only production file the write set allows for AC4. The record must state whether the Gesture A and Gesture C transcripts show a `TextBoxSearch_Leave` entry at all, since research section 3.2 derives from source that candidate 3 is not reached on either reproduction path and the AC4 gap is a real but separate gap. Acceptance: both lines exist exactly once, the mechanism named is confined to that one production file, and the record states the observed reachability of the leave handler. + +- [x] [P3-T6] Update the candidate status table in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, section `## Root Cause Analysis`, replacing each INFERRED first-cause label with OBSERVED or REFUTED according to the decision record, and update the two assumption bullets in the spec's `## Assumptions, Constraints, Dependencies` section the same way. Acceptance: the spec no longer contains the token INFERRED in a first-cause position for any of the three candidates, and each replaced label cites the decision-record line that justifies it. Sibling sections are swept, not the table alone: the `## Repro & Evidence` conclusion and the `### Correction to a citation carried from the issue` paragraph are re-read and corrected if the evidence contradicts them. + +--- + +### Phase 4 — AC2, the self-inflicted deactivation seam + +The seam is declared and implemented before the failing test is written. That ordering is deliberate: a test citing a not-yet-existing interface member is a compile error, which reddens the entire test assembly and produces a fail-before signal that proves nothing about the defect. Declaring the seam first without wiring it into the cancel loop keeps the defect intact and turns the fail-before into a runtime red at exactly the assertion that matters. + +- [x] [P4-T1] Declare the self-inflicted-deactivation seam on `QuickFiler/Interfaces/IQfcFormViewer.cs` as one boolean get-only property beside the existing issue #677 deactivation intents at lines 51-70, with an XML doc comment stating that it reports whether the window that took activation is a breadcrumb popup owned by this form. The polarity is load-bearing and is fixed here: false means GENUINE, that is not self-inflicted. Moq's default bool return is false, so this polarity keeps the existing Arrange block of the deactivate suite valid without modification. Do not invert it. Acceptance: the interface declares the member and the solution still compiles under the P0-T8 command form. + +- [x] [P4-T2] Implement the seam in `QuickFiler/Viewers/QfcFormViewer.cs` beside `IsWebView2Focused` at lines 190-201 and `ParkFocusOffWebView2` at line 207, as the only form-side site that reads non-injectable activation state. The member is NOT yet consumed by the deactivate handler, so the defect is preserved at this task. Note for the coverage gate: this file carries a class-level `[ExcludeFromCodeCoverage]` at line 17, so the lines added here are not measurable and the Phase 9 changed-line coverage gate records them as NOT MEASURABLE with that citation rather than reporting a figure with no source. Acceptance: `QuickFiler/Viewers/QfcFormViewer.cs` compiles as the sole implementor of the interface under the P0-T8 command form, and the deactivate handler in `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is unchanged by this task. + +- [x] [P4-T3] Assign the popup-owns-activation state in `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` beside the existing `host.MayTakeFocus = MayRestoreBreadcrumbFocus;` assignment at line 212, but only if evidence/other/close-ordering-decision.md records `AC2-ITEMVIEWER-WIRING: REQUIRED`. Any new host state is a settable internal property assigned after construction, matching the `MayTakeFocus` precedent at QuickFiler/Viewers/BreadcrumbDropDownHost.cs line 216; the reflection-based constructor binding warned about at lines 210-214 of that file must not be disturbed, so no constructor-arity change is made. Acceptance: when the decision line reads REQUIRED, the assignment exists and the file's physical line count, measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, stays at or below 460; when it reads NOT REQUIRED, the file is unchanged and evidence/qa-gates/p4-t3-itemviewer-wiring.md under the feature folder records the line `AC2-ITEMVIEWER-WIRING: NOT REQUIRED` together with the statement NOT APPLICABLE. Both branches are gated; neither is a silent skip. + +- [x] [P4-T4] Add the AC2 fail-before test FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector to `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs`, setting the new seam to report self-inflicted on a `Mock`, injecting two item controllers through the existing private-field reflection helper the class already uses, raising the deactivation event with `Mock.Raise`, and asserting `Times.Never()` on `CancelBreadcrumbSelector()` for both controllers. Acceptance: the file compiles and now declares 9 `[TestMethod]` members, measured with the P1-T7 command form and recorded in evidence/regression-testing/p4-t4-deactivate-suite-count.md under the feature folder. + +- [x] [P4-T5] [expect-fail] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the deactivate suite and record the fail-before evidence in evidence/regression-testing/p4-t5-ac2-fail-before.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p4-t5, the log file name p4-t5.trx, and the filter `FullyQualifiedName~QfcFormControllerDeactivateTests`. Acceptance, stated with an explicit single-name carve-out: the run records FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector as Failed; it records every one of the other 8 tests in that class as Passed; and it records no failed test other than that exact named test, which task P4-T4 landed as a deliberately-failing regression test and which stays Failed until task P4-T8 lands the guard. `ExpectedExitCode: 1` is recorded in the artifact so the non-zero exit normalises to a pass for this gate. + +- [x] [P4-T6] Perform the deliberate update of FormDeactivated_CancelsSelectorOnEveryItemController at `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` line 172. Keep the method at its current name and keep both `Times.Once()` assertions at lines 185-186 so the #677 contract stays visibly pinned. Add exactly one Arrange line setting the new seam to report a GENUINE deactivation, and amend the doc comment at lines 167-170 to state the conditionality. Because the chosen polarity makes false mean genuine and Moq's default bool return is false, the existing Arrange block remains valid and the change is a doc-comment amendment plus one explicit line. This test is deliberately updated, never weakened and never deleted. Acceptance: the method name is unchanged, both `Times.Once()` assertions are unchanged, exactly one Arrange line and the doc comment are added by this task, and the file compiles under the P0-T8 command form. + +- [x] [P4-T7] Decide and apply the treatment of FormDeactivated_WebView2Focused_ParksFocusOnce at `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` line 134, reading the line `AC2-PARK-FOCUS-SUPPRESSED:` from evidence/other/close-ordering-decision.md. When the value is NO, the test stays unchanged and evidence/qa-gates/p4-t7-park-focus-decision.md under the feature folder records that the fix keeps focus parking unconditional. When the value is YES, the test receives the same explicit genuine-case Arrange line, and the paired negative test asserting `Times.Never()` on `ParkFocusOffWebView2()` for the self-inflicted case is added in this task; the same artifact records the paired test's name and records the line `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8`. Task P4-T8 then extends its guard to the focus-parking step as well as the cancel loop, so that paired test passes at P4-T9; the per-item boundary `catch (Exception exception)` inside the member `ParkFocusAndCancelSelectors` in `QuickFiler/Controllers/QfcFormController.Deactivate.cs`, and the `logger.Error` call that forms its whole body, remain unchanged in either branch (that catch opens at line 117 and its body ends at line 128, both current at d78ae7f7). The paired negative test is a fifth expect-fail test in this plan when this branch is taken: it is added to the expect-fail inventory table with class QfcFormControllerDeactivateTests, landed by P4-T7, recorded Failed at no gate because no gate runs between P4-T7 and P4-T8, and made to pass by P4-T8. Task P9-T5 then reads the inventory as five rows rather than four. When the value is NO, no paired test is added, P4-T8's guard stays scoped to the cancel loop, and the inventory remains four rows. This is an explicit decision task, so neither branch is a default. Acceptance: the artifact exists, names the branch taken, quotes the decision-record line, and the resulting `[TestMethod]` count for the file is recorded. + +- [x] [P4-T8] Land the AC2 guard in `QuickFiler/Controllers/QfcFormController.Deactivate.cs` by gating, on the new seam, the `foreach (QfcItemGroup group in groups)` per-item cancel loop inside the member `ParkFocusAndCancelSelectors`, so a self-inflicted deactivation cancels nothing and any other deactivation still cancels every item controller's selector. Every position in this task is anchored on a member or construct name, with the line number given only as a secondary hint stated as current at d78ae7f7, because this plan has already lost citation accuracy in this file twice from the same mechanism — once from a merge and once from its own executed Phase 1 — and a member name does not decay under either. The member `ParkFocusAndCancelSelectors` opens at line 85; the cancel loop it contains opens at line 105 and its closing brace is at line 129. The scope of the suppression is fixed by the P4-T7 branch and is not a default: when evidence/qa-gates/p4-t7-park-focus-decision.md records `PARK-FOCUS-SUPPRESSION: IN SCOPE FOR P4-T8`, this task also gates the focus-parking step on the same seam — that step is the `if (_formViewer?.IsWebView2Focused == true)` block whose single statement calls `ParkFocusOffWebView2()`, at lines 94-97 — so the paired negative test P4-T7 landed passes at P4-T9; when that line is absent, the suppression is scoped to the cancel loop only and focus parking stays unconditional. In both branches the existing per-item boundary `catch (Exception exception)` inside the same member, and the `logger.Error` call that forms its whole body, stay unchanged (that catch opens at line 117 and its body ends at line 128), and the AC6 instrumentation added in P1-T4 stays in place at Debug level. Acceptance: the cancel loop reads the new seam; the artifact evidence/qa-gates/p4-t8-guard-scope.md under the feature folder names which of the two branches was taken and quotes the P4-T7 line it read; the per-item catch and the AC6 log statements are unchanged in the diff for this task; and the solution compiles under the P0-T8 command form. The Failed-to-Passed transition of FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector, and of the paired negative test when the YES branch was taken, is measured by the next task, P4-T9, and is that task's acceptance rather than this one's. + +- [x] [P4-T9] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the deactivate suite and record the pass-after evidence in evidence/regression-testing/p4-t9-ac2-pass-after.md under the feature folder, using the P4-T5 command form with the results directory TestResults\796\p4-t9 and the log file name p4-t9.trx. Acceptance: `EXIT_CODE: 0` and no test in `FullyQualifiedName~QfcFormControllerDeactivateTests` recorded as Failed. The expect-fail tests live in this class at this point in plan order are FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector, landed by P4-T4 and made to pass by P4-T8, and, only when P4-T7 took its YES branch, the paired `ParkFocusOffWebView2()` negative test, landed by P4-T7 and made to pass by P4-T8. Every one of those tasks precedes this gate in both branches, so the gate is satisfiable at its position in plan order and no carve-out is needed here. + +- [x] [P4-T10] Re-measure the line counts of `QuickFiler/Controllers/QfcFormController.Deactivate.cs`, `QuickFiler/Interfaces/IQfcFormViewer.cs`, `QuickFiler/Viewers/QfcFormViewer.cs`, `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` and `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` with the P0-T12 command form, using the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, and record them in evidence/qa-gates/p4-t10-file-size.md under the feature folder. `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is named here as a file that changes TWICE in this phase: once at P4-T8, which this task measures, and once again at P4-T11, the documentation repair that follows this task. This artifact is the phase's only file-size gate and no second one is created for that repair, so P4-T11 appends its own re-measurement of that one path to THIS artifact under the heading `POST-P4-T11 RE-MEASUREMENT:`. Acceptance: every recorded physical count is at most 500; the count for QuickFiler/Viewers/ItemViewer.Breadcrumb.cs is at most 460; and the artifact states that the count it records for `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is the pre-P4-T11 value and that P4-T11 will append the post-repair value under that heading. + +- [x] [P4-T11] Repair the stranded XML documentation comment in `QuickFiler/Controllers/QfcFormController.Deactivate.cs` that executed task P1-T4 introduced. This is a pure documentation repair with no behavioural effect: it moves comment lines only, adds and removes no executable statement, and therefore does not disturb the AC6 ordering constraint that Phase 1 contained no behavioural change. The defect is repaired in Phase 4 rather than by amending Phase 1 because Phase 4 already edits this file at P4-T8 and the file is already in the write set, so the repair adds no path and no new gate. The defect, re-derived against the tree at d78ae7f7: the `` and `` pair that documents the member `ParkFocusAndCancelSelectors` — the remarks describing the issue #791 `_formViewer` null guard — was left at lines 29-38 when P1-T4 inserted the formatter methods beneath it. It now sits immediately above the member `FormatDeactivationDiagnostics`, which carries its own `` at lines 39-54, so that one member has two `` elements attached while `ParkFocusAndCancelSelectors` at line 85 has no doc comment at all. Those three line numbers are provenance only and must not be read as positions to look at when this task runs: task P4-T8 precedes this task, edits this same file, and will have shifted them. The operative locators are the two member names and the command below, which finds both declarations by name and is therefore position-independent. Move the stranded pair down so it is the doc-comment block immediately preceding `ParkFocusAndCancelSelectors`, and leave `FormatDeactivationDiagnostics` with exactly the one `` it authored for itself. Change no other text in the file. Measure the result with: + + ``` + pwsh -NoProfile -Command '$lines = Get-Content -LiteralPath QuickFiler\Controllers\QfcFormController.Deactivate.cs; function Block([int]$d) { $i = $d - 1; $b = @(); while ($i -ge 0 -and $lines[$i].TrimStart().StartsWith("///")) { $b = ,$lines[$i] + $b; $i-- }; return ,$b }; $p = ($lines | Select-String -SimpleMatch "internal void ParkFocusAndCancelSelectors()").LineNumber - 1; $f = ($lines | Select-String -SimpleMatch "internal static string FormatDeactivationDiagnostics(").LineNumber - 1; $bp = Block $p; $bf = Block $f; "Park-Summary=" + (@($bp | Select-String -SimpleMatch "").Count); "Park-791=" + (@($bp | Select-String -SimpleMatch "#791").Count); "Fmt-Summary=" + (@($bf | Select-String -SimpleMatch "").Count)' + ``` + + That command reads the maximal run of consecutive `///` lines immediately above each of the two declarations and counts the `` opening tags in each run, which is what makes the assertion able to fail: on the tree as it stands the command prints `Park-Summary=0`, `Park-791=0` and `Fmt-Summary=2`, and only the completed repair produces the required values. A whole-file `` count is deliberately not used, because the file holds five `` tags before the repair and five after, so a whole-file count is invariant under the very change this task makes and would gate nothing. Acceptance: the command prints `Park-Summary=1`, `Fmt-Summary=1`, and `Park-791` at 1 or greater, recorded in evidence/qa-gates/p4-t8-guard-scope.md under the feature folder beneath the heading `P4-T11 DOC REPAIR:` rather than in an artifact of its own; the solution compiles under the P0-T8 command form; the diff for this task touches no file outside the feature folder docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796 other than `QuickFiler/Controllers/QfcFormController.Deactivate.cs`, and in that file it adds, removes and modifies no line that is not a `///` comment line; and the re-measured physical line count of that file, taken with the P0-T12 command form using the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, is at most 500 and is appended to evidence/qa-gates/p4-t10-file-size.md under the heading `POST-P4-T11 RE-MEASUREMENT:`. + +--- + +### Phase 5 — AC3, commit-before-cancel ordering + +Same seam-before-red-test ordering as Phase 4, and for the same reason. The latch is declared on a part with headroom, not on the 500-line-constrained main part. + +- [x] [P5-T1] Declare the pending-commit latch on `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` as a settable internal property, matching the `MayTakeFocus` precedent at QuickFiler/Viewers/BreadcrumbDropDownHost.cs line 216 rather than a constructor parameter, so the reflection-based constructor binding warned about at lines 210-214 of that file is not disturbed. The property is declared and set at open time in `ShowPopup` at lines 98-102 of this file if the mechanism requires it, but it is NOT yet consulted by `FinishClose`, so the defect is preserved at this task. This part is 107 lines and has ample headroom; the main part does not. Acceptance: the solution compiles under the P0-T8 command form; the new member is declared on this part and not on the main part; and `FinishClose` in `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` is unchanged by this task, which is what preserves the defect for the fail-before run. + +- [x] [P5-T2] Add the two AC3 fail-before tests to `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs`: NativeCloseWhileCommitPending_DoesNotCancelSelection, asserting the cancel delegate is not invoked when a native-reason close arrives while the commit latch is set; and NativeCloseWithNoCommitPending_StillCancelsSelection, asserting the cancel delegate IS invoked when the latch is clear. Both drive a real host headlessly with delegate counters in the style of the PendingHostHarness already used by QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs, under an inline synchronization context, with no window shown and no WebView2 initialised. The framework cannot be made to choose a close reason in a headless test, so these tests hand the handler a constructed reason and prove the branch, not the framework's choice; that limit is recorded rather than papered over. Acceptance: the file compiles and declares 3 `[TestMethod]` members. + +- [x] [P5-T3] [expect-fail] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the close-ordering suite and record the fail-before evidence in evidence/regression-testing/p5-t3-ac3-fail-before.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p5-t3, the log file name p5-t3.trx, and the filter `FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests`. Acceptance, with an explicit carve-out: the run records NativeCloseWhileCommitPending_DoesNotCancelSelection as Failed; it records FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField, landed by task P1-T5, as Passed; and it records no failed test other than the two exact named tests NativeCloseWhileCommitPending_DoesNotCancelSelection and NativeCloseWithNoCommitPending_StillCancelsSelection, which task P5-T2 landed as deliberately-failing regression tests and which stay Failed until task P5-T4 lands the fix. NativeCloseWithNoCommitPending_StillCancelsSelection may be recorded as either Passed or Failed at this gate, because it asserts the behaviour the unfixed code already has for the clear-latch case; the artifact records which, and both outcomes satisfy the carve-out. `ExpectedExitCode: 1` is recorded. + +- [x] [P5-T4] Land the AC3 commit-before-cancel ordering. When evidence/other/close-ordering-decision.md records `AC3-ENFORCEMENT-SITE: HOST`, consult the latch inside the member `FinishClose` in `QuickFiler/Viewers/BreadcrumbDropDownHost.cs`, so an `Uncommitted`-reason close does not cancel while a commit is in flight. Every position in this task is anchored on a member or construct name, with the line number given only as a secondary hint stated as current at d78ae7f7, because executed task P1-T2 moved `OnDropDownClosed` out of this file and shifted every position below it; a member name does not decay under that. `FinishClose` opens at line 426 and its closing brace is at line 442. Inside it, the `DropDown.AutoClose = true` restore (line 433) and the gated `FocusAnchorIfPermitted` argument (line 440) are unchanged, and the `MayTakeFocus` property default (line 216, which the P1-T2 relocation did not move because it sits above the removed span) is unchanged. When the decision records `AC3-ENFORCEMENT-SITE: COORDINATOR`, make the same change at `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` instead and record in evidence/qa-gates/p5-t4-enforcement-site.md under the feature folder that `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` was left unchanged. Both branches are gated. Acceptance: evidence/qa-gates/p5-t4-enforcement-site.md exists, records which branch was taken, quotes the decision-record line, and the solution compiles under the P0-T8 command form. The transition of the two AC3 tests to Passed is measured by task P5-T7 and is that task's acceptance rather than this one's. + +- [x] [P5-T5] Move the row activation listener at `QuickFiler/Resources/FolderBreadcrumb.html` lines 289-291 from the `click` event, which fires on mouseup, to a pointer-down event so the `selectorActivate` message is produced before dismissal, but only if evidence/other/close-ordering-decision.md records `AC3-HTML-POINTERDOWN: REQUIRED`. The change is confined to that listener registration inside the `expanded && row.isSelectable` branch at lines 285-292 and touches no projection logic, because a concurrent sibling item owns the row text projection in this same page. When the decision records NOT REQUIRED, the file is left unchanged and evidence/qa-gates/p5-t5-html-listener.md under the feature folder records the line `AC3-HTML-POINTERDOWN: NOT REQUIRED` together with the statement NOT APPLICABLE. Acceptance: the artifact exists in both branches and names which was taken; when REQUIRED, `pwsh -NoProfile -Command '(Select-String -Path QuickFiler\Resources\FolderBreadcrumb.html -SimpleMatch "selectorActivate").Count'` still reports exactly 1 and the recorded diff for the file touches only lines inside the range 285 to 292. + +- [x] [P5-T6] Add the pending-commit-versus-native-close guard to `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` while keeping all five existing tests. The two literal `CancelCount.Should().Be(1)` assertions at lines 48 and 79 are KEPT UNCHANGED and are the guard proving that any commit-time cancel suppression is SCOPED rather than global: in both of those tests no commit is in flight, so the correct post-fix value remains 1. A fix that drives either of them to zero is a design signal that the suppression is too broad; it is not a test to update. The `FocusAnchorCount` assertions at lines 49, 80 and 114 likewise stay at 1 because the harness leaves the may-take-focus predicate at its permissive default, which must not be changed. A third `CancelCount.Should().Be(1)` assertion exists at line 113 and is likewise kept unchanged; it is not named as a scoping guard because the spec pins only the two at lines 48 and 79, but a fix that drives it to zero is the same design signal. Acceptance: the file declares 6 `[TestMethod]` members, its physical line count, measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, is at most 440, and the two `CancelCount` assertions still read `.Be(1)` at their positions, recorded in evidence/regression-testing/p5-t6-scoping-guard.md under the feature folder. + +- [x] [P5-T7] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the close-ordering and pending-open suites and record the pass-after evidence in evidence/regression-testing/p5-t7-ac3-pass-after.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p5-t7, the log file name p5-t7.trx, and the filter `FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~BreadcrumbPendingOpenCloseTests`. Acceptance: `EXIT_CODE: 0`; no test in either class recorded as Failed; and CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent and CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus both recorded as Passed, which is what proves the suppression did not become global. The only expect-fail tests in these classes were landed by P5-T2 and made to pass by P5-T4, both of which precede this gate. + +- [x] [P5-T8] Re-measure the line counts of `QuickFiler/Viewers/BreadcrumbDropDownHost.cs`, `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs`, `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs`, `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`, `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` and `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` with the P0-T12 command form, using the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, and record them in evidence/qa-gates/p5-t8-file-size.md under the feature folder. Acceptance: every recorded physical count is at most 500. + +--- + +### Phase 6 — AC4, the mouse-path leave latch + +- [x] [P6-T1] Declare any new member the chosen AC4 mechanism requires in `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, without the suppression behaviour, so the Phase 6 fail-before test compiles and fails at its assertion rather than at compilation. Take this task only if evidence/other/close-ordering-decision.md records `AC4-NEW-MEMBER: REQUIRED`; otherwise leave the file unchanged by this task and record the line `AC4-NEW-MEMBER: NOT REQUIRED` together with the statement NOT APPLICABLE in evidence/qa-gates/p6-t1-ac4-seam.md under the feature folder. The qualifier is exact: task P1-T4 already added the observational selector-open forward to this file, so an unqualified unchanged-file claim would be false in both branches. The existing one-shot latch `_searchLeaveHandoffPending` at line 188, its single producer in the `Keys.Down` branch at line 195, and its single consumer in `TextBoxSearch_Leave` at lines 217-228 are the structures the mechanism extends. Acceptance: the artifact exists in both branches, names which was taken, and the solution compiles under the P0-T8 command form. + +- [x] [P6-T2] Create `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs` declaring the class QfcItemController_SearchLeaveLatchTests, following the naming convention the sibling file QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs uses at line 25. It holds the AC4 fail-before test SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown, which drives the mouse open path with a `Mock`, raises the search-box leave, and asserts `SetFolderDroppedDown(false)` is never called, plus a paired positive test asserting the leave still closes the drop-down when it was not opened by a mouse gesture. No window, no external process, no temporary file. Acceptance: the file exists and declares 2 `[TestMethod]` members. + +- [x] [P6-T3] Add `` to `QuickFiler.Test/QuickFiler.Test.csproj` alongside the existing Controllers entries, including the one for Controllers\QfcFormControllerDeactivateTests.cs at line 158. Without it the new file is silently not compiled and the fail-before evidence in P6-T4 becomes vacuous. Acceptance: the recorded output of `pwsh -NoProfile -Command 'Select-String -Path QuickFiler.Test\QuickFiler.Test.csproj -SimpleMatch "QfcItemController.SearchLeaveLatchTests.cs"'` names exactly one matching line, recorded in evidence/qa-gates/p6-t3-compile-entry.md under the feature folder, and the solution compiles under the P0-T8 command form. + +- [x] [P6-T4] [expect-fail] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the new latch suite and record the fail-before evidence in evidence/regression-testing/p6-t4-ac4-fail-before.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p6-t4, the log file name p6-t4.trx, and the filter `FullyQualifiedName~QfcItemController_SearchLeaveLatchTests`. The filter names the new class exactly and does not match QfcItemController_EventHandlersTests. Acceptance, with an explicit carve-out: the run records SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown as Failed; it records the paired positive test as Passed; and it records no failed test other than that exact named test, which task P6-T2 landed as a deliberately-failing regression test and which stays Failed until task P6-T5 lands the fix. `ExpectedExitCode: 1` is recorded. If the run reports a Total of 0, the compile entry in P6-T3 did not take effect and the task fails rather than passing on an empty population. + +- [x] [P6-T5] Land the AC4 fix in `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` so the #680 leave-handoff latch covers the mouse open path as well as the `Keys.Down` branch at lines 190-199, using the mechanism named on the `AC4-MECHANISM:` line of evidence/other/close-ordering-decision.md. The Down-arrow handoff contract must keep working: the single producer at line 195 and the read-and-clear consumer at lines 219-224 stay functional. Acceptance: the mechanism named on the decision-record line is the one implemented, the diff for this task touches no production file other than that one, and the solution compiles under the P0-T8 command form. The transition of SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown to Passed is measured by task P6-T6 and is that task's acceptance rather than this one's. + +- [x] [P6-T6] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to both item-controller event suites and record the pass-after evidence in evidence/regression-testing/p6-t6-ac4-pass-after.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p6-t6, the log file name p6-t6.trx, and the filter `FullyQualifiedName~QfcItemController_SearchLeaveLatchTests|FullyQualifiedName~QfcItemController_EventHandlersTests`. Acceptance: `EXIT_CODE: 0` and no test in either class recorded as Failed. The only expect-fail test in these classes was landed by P6-T2 and made to pass by P6-T5, both of which precede this gate. Additionally record the physical line count of `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, which must be at most 500. + +--- + +### Phase 7 — AC1 and AC5 managed-seam guards + +- [x] [P7-T1] Add the AC1 guard test GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose to `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs`, asserting at the managed seam that the open task resolves true, that the host reports open, that no `Close` reaches the mocked host, and that the session reports the selector open across the gesture open path. The part that is not automatable is that no FRAMEWORK close occurs, because no framework dropdown is shown; that limit is stated in the test's doc comment rather than asserted. Acceptance: the file declares 4 `[TestMethod]` members, measured with the P1-T7 command form applied to this file, and the solution compiles under the P0-T8 command form. The test result is measured by task P7-T3. + +- [x] [P7-T2] Add the AC5 regression guard test RowSetRefreshWhileOpen_NeverClosesHost to `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs`, asserting that `Close` is never invoked on a `Mock` across a row-set refresh while the selector is open, reusing the headless ItemViewer plus mocked-host pattern the file already uses at lines 124 and 143 rather than introducing a third harness. This is the #438 AC-3 regression guard; the underlying session-preserving replacement path in UtilitiesCS is deliberately left out of the diff, which is what makes the guard meaningful. Acceptance: the file declares 7 `[TestMethod]` members, measured with the P1-T7 command form applied to this file, and its physical line count, measured with the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, is at most 470. The test result is measured by task P7-T3. + +- [x] [P7-T3] Run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the two guard suites and record the result in evidence/regression-testing/p7-t3-ac1-ac5-guards.md under the feature folder, using the P1-T11 command form with the results directory TestResults\796\p7-t3, the log file name p7-t3.trx, and the filter `FullyQualifiedName~BreadcrumbDropDownCloseOrderingTests|FullyQualifiedName~BreadcrumbPendingOpenCloseTests`. Acceptance: `EXIT_CODE: 0`, no test in either class recorded as Failed, and the combined Total recorded as 11, being the 4 `[TestMethod]` members BreadcrumbDropDownCloseOrderingTests holds after P7-T1 plus the 7 BreadcrumbPendingOpenCloseTests holds after P7-T2. vstest.console.exe prints one combined Total for a multi-class filter and prints no per-class subtotal, so the two per-class figures are derived by counting the recorded per-test result lines and the artifact states that they were derived rather than read. Every expect-fail test in these classes was made to pass by P5-T4, which precedes this gate. + +- [x] [P7-T4] Verify the AC5 exclusion holds by confirming that no file under UtilitiesCS/OutlookObjects/Folder appears in the diff, recording the result in evidence/qa-gates/p7-t4-ac5-exclusion.md under the feature folder. Commands: `pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796'` followed by `pwsh -NoProfile -Command 'git diff --cached --name-status d78ae7f7'` and `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'`. The staging span accompanies the name-listing diff because such a diff cannot see an untracked file. The anchor is d78ae7f7, the second merge commit, and not c7ae69f1: anchored at c7ae69f1 this diff would enumerate the 59 files the second merge of origin/main brought in, and this gate's acceptance requires that no path beginning with UtilitiesCS/ or UtilitiesCS.Test/ be listed, so the gate would fail on work this item did not do. Acceptance: neither command output names any path beginning with UtilitiesCS/ or UtilitiesCS.Test/. A path that is a member of the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md does not fail this gate, because the executor did not create it and in some cases may not remediate it; the artifact names any such path it excluded on that basis. + +--- + +### Phase 8 — Deliberate test re-pinning, then acceptance-criteria check-off and document updates + +This phase does two things. It first performs the deliberate update of the one pre-existing test that pins the behaviour AC4 removes, and verifies that update. It then checks off each acceptance criterion in its own task. One AC per task, so no check-off can be recorded without its own evidence citation. + +The order inside this phase is load-bearing and is not a preference. Tasks P8-T1 and P8-T2 precede the AC4 check-off at P8-T6. Until P8-T1 lands and P8-T2 verifies it, `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` still holds a test asserting the contract AC4 supersedes, and that test is measured Failed on the tree as it stands. Checking AC4 off ahead of that would record a criterion as delivered against a red gate. + +- [x] [P8-T1] Perform the deliberate update of TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent in `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs`, whose declaration is at line 74 of that file. Keep the method at its current name and keep its `Times.Once()` assertion on `SetFolderDroppedDown(false)` at line 84, so the issue #680 dismissal-ownership contract stays visibly pinned for the case that still holds. Add exactly one Arrange line establishing a search-driven open, so the search box owns the dismissal before the leave is raised: `QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true);`, placed in the Arrange block after the existing BuildController call at line 78. That helper is the same reflection injector the file already uses at line 170 to install `_itemViewer`; its signature is `SetField(QfcItemController controller, string name, object value)` at QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs line 40, so a boxed boolean is accepted, and the resulting statement is one physical line under CSharpier's default 100-column print width, this repository declaring no .csharpierrc that narrows it. The field `_searchOwnedDismissal` is the AC4 provenance latch task P6-T5 landed, declared at `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` line 203 and read by `TextBoxSearch_Leave` at line 263; no production file is edited by this task. Also amend the test's doc comment at lines 69-72 to state the search-ownership condition, namely that the leave dismisses only a drop-down this search box itself opened. This test is deliberately updated, never weakened and never deleted, in exactly the sense task P4-T6 states for the issue #677 test it updates: the subject is re-pinned to the contract that now holds, rather than the assertion being relaxed to accommodate the fix. The other five `[TestMethod]` members in the class stay unchanged, and this task adds and removes no test. This task precedes the AC4 check-off at P8-T6 for the reason stated in this phase's preamble. Acceptance: the method name is unchanged; its assertion still reads `Times.Once()`; the only non-comment line this task adds anywhere is that one Arrange statement; the file still declares 6 `[TestMethod]` members, measured with the P1-T7 command form applied to this file and recorded in evidence/regression-testing/p8-t1-search-dismissal-repin.md under the feature folder; and the solution compiles under the P0-T8 command form. + +- [x] [P8-T2] Rebuild, then run QuickFiler.Test/bin/Debug/QuickFiler.Test.dll scoped to the search-dismissal class, and record the result in evidence/regression-testing/p8-t2-search-dismissal-verification.md under the feature folder. The rebuild is not optional and is part of this task: P8-T1 edits test source, and a run against the assembly as P7-T4 left it would measure a stale binary and report the pre-update result. Run the P0-T8 command form first with the log path TestResults\796\p8-t2\analyzer-rebuild.log, then run the P1-T11 command form with the results directory TestResults\796\p8-t2, the log file name p8-t2.trx, and the filter `FullyQualifiedName~QfcItemController_SearchDismissalTests`. That filter selects exactly one class, re-derived against the current tree in this revision pass and recorded in the test-run scoping section above. Acceptance: the rebuild records `EXIT_CODE: 0`; the scoped run records `EXIT_CODE: 0`; the run records a Total of 6; no test is recorded as Failed; and TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent is named explicitly among the Passed results rather than inferred from the absence of a failure. Six is the class's `[TestMethod]` count, re-derived against the current tree in this revision pass, and P8-T1 does not change it; a Total of 0 means the filter selected nothing and any other Total means the class's membership changed, and either fails this gate. No expect-fail test is declared in this class anywhere in this plan, and every row of the expect-fail inventory was made to pass by P4-T8, P5-T4 or P6-T5, all of which precede this gate, so no carve-out applies here and any Failed result fails the gate outright. This gate is currently measured RED and therefore demonstrably able to fail: the whole-assembly run recorded in evidence/other/phase7-blocking-finding-out-of-write-set-test.md reports this exact class at Total 6, Passed 5, Failed 1, with TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent failing on `Expected invocation on the mock once, but was 0 times`. Only the P8-T1 update turns it green, and a later regression in the AC4 latch turns it red again. + +- [x] [P8-T3] Check off AC1 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md by changing its `- [ ] AC1:` line to `- [x] AC1:`, citing evidence/regression-testing/p7-t3-ac1-ac5-guards.md and the `AC1-FAIL-BEFORE-CARRIER:` line of evidence/other/close-ordering-decision.md together with the fail-before artifact that carrier names. Acceptance: exactly one AC line changed by this task, and the citation resolves to an artifact that exists. + +- [x] [P8-T4] Check off AC2 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, citing evidence/regression-testing/p4-t5-ac2-fail-before.md and evidence/regression-testing/p4-t9-ac2-pass-after.md. Acceptance: exactly one AC line changed by this task, and both citations resolve. + +- [x] [P8-T5] Check off AC3 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, citing evidence/regression-testing/p5-t3-ac3-fail-before.md, evidence/regression-testing/p5-t7-ac3-pass-after.md and evidence/regression-testing/p5-t6-scoping-guard.md. Acceptance: exactly one AC line changed by this task, and all three citations resolve. + +- [x] [P8-T6] Check off AC4 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, citing evidence/regression-testing/p6-t4-ac4-fail-before.md and evidence/regression-testing/p6-t6-ac4-pass-after.md. Acceptance: exactly one AC line changed by this task, and both citations resolve. + +- [x] [P8-T7] Check off AC5 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, citing evidence/regression-testing/p7-t3-ac1-ac5-guards.md and evidence/qa-gates/p7-t4-ac5-exclusion.md. Acceptance: exactly one AC line changed by this task, and both citations resolve. + +- [x] [P8-T8] Check off AC6 in docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md, citing evidence/regression-testing/p1-t11-ac6-instrumentation-tests.md, the human artifact under evidence/other/ that P2-T1 produced, and evidence/other/close-ordering-decision.md. Acceptance: exactly one AC line changed by this task, and all three citations resolve. + +- [x] [P8-T9] Mirror the acceptance-criteria status into docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md and write the issue-update mirror to evidence/issue-updates/issue-796..md under the feature folder, where the timestamp uses the `yyyy-MM-ddTHH-mm` form. Acceptance: the mirror artifact carries `Timestamp:`, the exact text intended for the issue, and one of `PostedAs: body`, `PostedAs: comment` or a `POSTING BLOCKED` header with the reason; and the six AC checkboxes in issue.md match the six in spec.md. + +--- + +### Phase 9 — Final QA loop, coverage delta, scope boundary, commit + +The loop runs format, then lint, then type-check, then test, in that order. If any step fails or changes files, the loop restarts at P9-T1. + +- [x] [P9-T1] Run the CSharpier formatter and record the result in evidence/qa-gates/p9-t1-csharpier-format.md under the feature folder. Read the verdict line recorded in evidence/baseline/p0-t7-csharpier-check-baseline.md. When it reads `CSHARPIER-BASELINE: CLEAN`, run `pwsh -NoProfile -Command 'dotnet tool run csharpier format .; "EXIT_CODE=$LASTEXITCODE"'`, because a repo-wide pass on a clean baseline cannot reformat a file outside this plan's write set. When it reads `CSHARPIER-BASELINE: PRE-EXISTING DRIFT`, run `pwsh -NoProfile -Command 'dotnet tool run csharpier format QuickFiler QuickFiler.Test; "EXIT_CODE=$LASTEXITCODE"'` instead, and record the pre-existing drift as out of scope for this item, because a repo-wide pass would sweep unrelated files into the diff and break the Phase 9 scope-boundary gate. Both branches are gated. Acceptance: the artifact records which branch was taken and quotes the baseline verdict line; it records `git status --porcelain --untracked-files=all` both immediately before and immediately after the format invocation, which is the observation that distinguishes a clean run from a repairing one because the write-mode exit code is identical in both cases; and every path that appears only in the after capture is inside the feature folder or the write set. + +- [x] [P9-T2] Verify formatting over the same scope as P9-T1 and record it in evidence/qa-gates/p9-t2-csharpier-check.md under the feature folder, running `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` when the P9-T1 branch was the repo-wide one, or the QuickFiler and QuickFiler.Test scoped form otherwise. Acceptance: `EXIT_CODE: 0` and the artifact records the full list of files the check reported, which must be empty for the scope run. + +- [x] [P9-T3] Run the final analyzer gate over TaskMaster.sln with the Rebuild target and record it in evidence/qa-gates/p9-t3-analyzer-rebuild.md under the feature folder, using the P0-T8 command form with the log path TestResults\796\p9-t3\analyzer-rebuild.log and `RunStartedUtc:` captured immediately before the run. Acceptance: `EXIT_CODE: 0`; at least one of `CscTaskCount` and `CscToolCount` greater than zero; both QuickFiler/bin/Debug/QuickFiler.dll and QuickFiler.Test/bin/Debug/QuickFiler.Test.dll carrying a LastWriteTimeUtc at or later than `RunStartedUtc:`, which is what proves the projects this item touches were actually compiled; and the analyzer warning and error totals no greater than the P0-T8 baseline. An exit code of 0 with both counts at zero is a FAILED gate. + +- [x] [P9-T4] Run the final nullable gate over TaskMaster.sln with the Rebuild target and record it in evidence/qa-gates/p9-t4-nullable-rebuild.md under the feature folder, using the P0-T9 command form with the log path TestResults\796\p9-t4\nullable-rebuild.log. Acceptance: identical in form to P9-T3, compared against the P0-T9 baseline, and the recorded command line contains no `/p:Nullable=enable` token. + +- [x] [P9-T5] Run the full QuickFiler.Test/bin/Debug/QuickFiler.Test.dll assembly and record it in evidence/qa-gates/p9-t5-full-assembly-tests.md under the feature folder, using the P0-T10 command form with the results directory TestResults\796\p9-t5 and the log file name p9-t5.trx. Acceptance: the recorded set of Failed tests is a subset of the BASELINE_FAILURE_SET recorded in evidence/baseline/p0-t10-quickfiler-test-baseline.md, with no test outside that set Failed, the set being read under the same convention P0-T10 records, namely that a run printing no `Failed:` line has an empty Failed set rather than an unread one; every one of the expect-fail tests listed in this plan's expect-fail inventory recorded as Passed, that inventory being four rows when P4-T7 took its NO branch and five rows when it took its YES branch, with the artifact naming which count it read and why; and the recorded Total no smaller than the baseline Total. A repository-wide zero-failure expectation is not asserted, because the baseline may already carry environmental failures; non-growth relative to the recorded baseline set is the gate. + +- [x] [P9-T6] Run the final coverage collection for the QuickFiler.Test assembly and record it in evidence/qa-gates/p9-t6-coverage-final.md under the feature folder, using the P0-T11 command form with the output path coverage\p9-t6-final.cobertura.xml. Acceptance: the file coverage/p9-t6-final.cobertura.xml exists; the artifact records the same six document-level numeric attributes the baseline recorded plus the same per-file `lines-covered` and `lines-valid` figures, grouped by the `filename` attribute; and `EXIT_CODE:` is recorded verbatim, with a non-zero exit accepted only when the recorded stderr contains the literal `is below the required 80`, exactly as at P0-T11. Each of the five per-file rows carries either its two figures or the literal `ABSENT: no class node carries this filename`, on the same terms and for the same partial-class reason recorded at P0-T11, and a file recorded ABSENT at both tasks is excluded from the P9-T7 changed-code denominator and named in that task's NOT MEASURABLE list. + +- [x] [P9-T7] Compute and record the coverage delta in evidence/qa-gates/p9-t7-coverage-delta.md under the feature folder, reading coverage/p0-t11-baseline.cobertura.xml and coverage/p9-t6-final.cobertura.xml. The artifact reports three figures: baseline coverage, post-change coverage, and changed-code coverage. Changed-code coverage is computed by intersecting the added and modified line numbers from `pwsh -NoProfile -Command 'git diff -U0 c7ae69f1..HEAD -- QuickFiler'` with the covered line numbers in the final Cobertura document, per file, aggregating class nodes by their `filename` attribute. This anchor is deliberately RETAINED at c7ae69f1 and must not be moved to the second merge commit d78ae7f7, nor to the third merge commit 5b8e0bf5, by a later sweep for stale anchors: the diff is scoped to the QuickFiler directory, a span that neither the second nor the third merge of origin/main touched, so c7ae69f1 already yields exactly this item's own QuickFiler changed lines and needs no repair; and each of those two merge commits already contains this item's committed Phase 1 instrumentation, so anchoring there would silently drop the instrumentation lines out of the changed-line set and contradict this same task's explicit requirement below that they be counted in the denominator. The following write-set files are recorded as NOT MEASURABLE with their citation rather than carrying a figure: `QuickFiler/Viewers/QfcFormViewer.cs` and `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs`, both suppressed by a class-level `[ExcludeFromCodeCoverage]` (at QfcFormViewer.cs line 17 and at ItemViewer.cs line 20 for the partial class the breadcrumb part belongs to); `QuickFiler/Interfaces/IQfcFormViewer.cs`, which is interface-only and has no executable lines; `QuickFiler/Resources/FolderBreadcrumb.html`, which is not C#; and the two `.csproj` files. `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` is reported on its own line and is excluded from the behavioural changed-code figure, because it contains logging only and its coverage contribution must not be used to inflate the figure for the behavioural changes. Acceptance: all three figures are present as numbers rather than placeholders; the changed-code figure is at least 90 percent over the measurable behavioural changed lines; the post-change document-level `lines-covered` divided by `lines-valid` is not lower than the baseline ratio computed from the same two attributes; and every NOT MEASURABLE entry carries its citation. A changed behavioural line may be excluded from the changed-code denominator only when the artifact names that exact file and line number and states the reason it cannot be reached from the tests this plan creates, which is either a framework limitation or the mocked-seam limitation named at the end of this task, and the total number of such individually named exclusions is at most 3. A blanket exclusion, an unnamed exclusion, or a fourth exclusion fails this task. A changed line that is an XML documentation comment line is outside the measurable denominator by construction and is NOT charged against that at-most-3 allowance, because a comment line carries no Cobertura `line` node at all, so it can appear in neither the covered numerator nor the valid denominator and its presence in the denominator would deflate the figure for a line that no test could ever cover. This clause is not a general softening: it is scoped to comment lines, and it exists because the span above already enumerates comment-only changed lines that no test can cover: executed task P1-T4 added 33 `///` lines in `QuickFiler/Controllers/QfcFormController.Deactivate.cs` and 13 in `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, both counts measured against c7ae69f1 in the preflight pass. Task P4-T11 makes a further comment-only edit to the first of those files, but whether its own moved pair is enumerated at all depends on how the diff aligns it against an anchor at which that pair already sat immediately above `ParkFocusAndCancelSelectors`; the clause does not rely on that alignment, because it asks only for the count of comment-only changed lines the span actually enumerates. The artifact records, for every measurable file in the diff span, how many of its changed lines are comment-only and states that they were removed from the denominator on this basis; it records that figure at minimum for `QuickFiler/Controllers/QfcFormController.Deactivate.cs` and for `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, both of which already carry comment-only changed lines added by executed task P1-T4. The AC6 log statements and pure formatter methods added by task P1-T4 to `QuickFiler/Controllers/QfcFormController.Deactivate.cs`, and the internal selector-open member the same task adds to `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`, are counted in the changed-code denominator, unlike the diagnostics part, because both sit in files whose behavioural changes are also measured and separating them per line would make the figure unreproducible; the artifact records, per file, how many changed lines are instrumentation. Neither file carries a class-level `[ExcludeFromCodeCoverage]`, so both are measurable: the exclusions in `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` are method-level and sit at lines 60, 83, 97, 111 and 125 only. One changed line is unreachable from the test population this plan creates, and it is admitted here by name rather than left for the executor to discover at the gate: the internal selector-open member task P1-T4 adds to `QuickFiler/Controllers/QfcItemController.EventHandlers.cs`. Its only reader is the per-item log statement in `ParkFocusAndCancelSelectors`, which reaches it only when the loop's interface-typed item controller casts successfully to the concrete internal type, and every test in QfcFormControllerDeactivateTests that injects item controllers injects them as Moq mocks of the interface, so that cast yields null in every test. The artifact names that line with its file and line number, states that reason, and counts it against the same at-most-3 allowance, leaving at most two further individually named exclusions available; the 90 percent threshold is then evaluated over the remaining measurable behavioural changed lines. + +- [x] [P9-T8] Audit file sizes across every .cs and .html path in the write set after the final formatter pass and record the result in evidence/qa-gates/p9-t8-file-size-audit.md under the feature folder, using the P0-T12 command form extended with the three files this plan creates, namely the new diagnostics part, the new close-ordering test suite and the new search-leave latch test suite, and extended also with `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs`, the seventeenth write-set path, each already named in the Write Set section above. This audit runs after P9-T1, because the formatter can change line counts and an audit taken before it would measure a superseded state. It uses the idiom recorded on the `LINE-COUNT-IDIOM:` line of evidence/baseline/p0-t12-file-size-baseline.md and no other, so the final audit and the baseline are commensurable. Acceptance: every recorded physical count is at most 500, and the artifact records the baseline count from evidence/baseline/p0-t12-file-size-baseline.md beside each final count, with one stated exception: `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` entered the write set after Phase 0 had executed and therefore carries no P0-T12 baseline figure, so the artifact records the literal `NO PHASE 0 BASELINE` beside that path's final count rather than a figure with no source. + +- [x] [P9-T9] Commit the completed change and all evidence from the worktree this plan executes in, recording the SHA in evidence/qa-gates/p9-t9-final-commit.md under the feature folder. Stage explicit pathspecs rather than everything: `pwsh -NoProfile -Command 'git add QuickFiler QuickFiler.Test docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796'`, then commit. Explicit pathspecs are used because a repository-wide stage can sweep an unrelated queued promotion file into this item's branch. Acceptance: the artifact records the SHA and the commit's `--name-status` listing. + +- [x] [P9-T10] Run the scope-boundary gate over the committed diff and record it in evidence/qa-gates/p9-t10-scope-boundary.md under the feature folder. Commands: `pwsh -NoProfile -Command 'git diff --name-status a6b259160f9ac1fbe251708d897fd4721486259e..HEAD'` and `pwsh -NoProfile -Command 'git status --porcelain --untracked-files=all'`. The diff is anchored to the explicit base ref a6b259160f9ac1fbe251708d897fd4721486259e, written as the full forty-character SHA, because an unanchored diff compares the worktree against the index and passes vacuously once the change is committed; the porcelain span accompanies it because the two mechanisms are complementary and each alone is blind in one state. origin/main has now been merged into this branch three times: once after this plan cleared preflight, once after Phase 0 and Phase 1 had executed and been committed, and once after Phase 8 and tasks P9-T1 and P9-T2 had executed and been committed. The operative anchor a6b259160f9ac1fbe251708d897fd4721486259e is the origin/main commit that third merge brought in. Because that merge has landed, it is now an ancestor of HEAD, so a two-dot diff from it yields exactly this item's own additions over main with no sibling's merged work re-billed to this item; it is also precisely the pull request footprint, so this gate measures the same set of paths the reviewer will see. Each earlier candidate anchor is rejected for its own measured reason. An anchor at the second merge commit d78ae7f7 would enumerate the 1644 renamed documentation paths the third merge brought in, all of them under other items' feature folders. An anchor at the first merge commit c7ae69f1 would enumerate those and additionally the 59 files the second merge brought in. An anchor at the branch-cut commit recorded on the Branch metadata line above would enumerate all of those and additionally the 146 sibling-authored files the first merge brought in. Each would bill work this item did not do to this item's footprint and fail the gate. A symbolic ref is still not used in its place: origin/main moves again while this item executes, and a three-dot form against a base that is an ancestor degenerates and re-bills the merges to this item. This anchor sits FURTHER BACK than d78ae7f7, so it makes this gate list strictly MORE paths than an anchor at d78ae7f7 would, which is safe because this gate's acceptance is an upper bound on which paths may appear and not a lower bound on how many must appear; no clause below requires any particular path to be listed. The additional paths it exposes are this item's own Phase 0 preparation commits. The footprint at this anchor was measured as 76 paths: 14 code paths, every one of them under QuickFiler/ or QuickFiler.Test/ and every one a member of the seventeen-path write set; 61 paths inside this item's feature folder; and exactly one path outside both, which the acceptance below permits by name. Acceptance: every path the diff lists is either inside the feature folder docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796, or is one of the seventeen write-set paths, or is the single path docs/features/potential/promoted/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select.md, which is permitted because it is the promoted potential record this item's own promotion created and the promotion lifecycle places that record outside the feature folder by construction. That one permitted path is deliberately written without backticks, and it is deliberately NOT added to the `## Write Set` section, whose count therefore stays at seventeen: no task in this plan writes it, and under this document's path convention a backticked path is read by downstream blast-radius derivation as a write claim, which this is not. No listed path lies under the .claude, .codex or .agents trees, under the config directory, under .github, or names TaskMaster.sln or a repository-root build property file; and no path under UtilitiesCS/ or UtilitiesCS.Test/ is listed. + +- [x] [P9-T11] Close the loop for the worktree this plan executes in. If any of P9-T1 through P9-T8 failed or changed a tracked file, restart the loop at P9-T1 and record the second pass in evidence/qa-gates/p9-t11-final-loop.md under the feature folder; otherwise record the single clean pass there. Record in that artifact the commands of the final clean pass in the order format, lint, type-check, test, and the observed output of `git status --porcelain --untracked-files=all` taken at that point on the recorded command channel. Then check this task off in this plan file, stage with the P9-T9 pathspec form, and run `git commit --amend --no-edit` on the recorded command channel. Acceptance: the artifact names which of the two branches was taken and records the four commands of the final clean pass in order; the porcelain reading is taken immediately before the amend, after this task's own artifact and check-off have been written; and it lists no path other than members of the `PRE-EXISTING-DIRTY-SET:` recorded in evidence/baseline/p0-t14-scope-baseline.md and paths inside the feature folder docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796. A terminal gate demanding a porcelain output of zero lines is not used, and neither is one permitting only this plan file and this task's own artifact: evidence/qa-gates/p9-t9-final-commit.md records a SHA that does not exist until after the P9-T9 commit, and evidence/qa-gates/p9-t10-scope-boundary.md is written after that commit, so both are necessarily untracked at this reading, as are this task's own artifact and its own check-off. The amend is what folds all four into the final commit. The gate remains strict outside the feature folder: any path under QuickFiler, under QuickFiler.Test, or anywhere else in the tree that is not a member of the recorded pre-existing set fails it. The commit message is not changed by the amend, which is why no acceptance clause asserts over the amended message body. + +--- + +## Decisions record + +1. **AC6 instrumentation is factored into pure formatter methods.** The handlers call `internal static string` formatters that build the Key=Value line, and the tests assert over the formatter's return value. The alternative considered was a source-text scan asserting a log statement exists at each site; it was rejected because a test that reads a repository source file introduces filesystem I/O into a unit test and because a prose or source-text search is not stable under reformatting, whereas a named test and its node ID are. +2. **`OnDropDownClosed` is relocated rather than duplicated.** `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` is 498 lines against a 500-line ceiling and declares no logger, so the instrumentation cannot be added in place. The relocation is a pure move into the new partial part and is what buys the headroom the Phase 5 edit to `FinishClose` needs. +3. **The AC2 polarity is false-means-genuine.** Moq's default bool return is false, so this keeps the existing Arrange block of FormDeactivated_CancelsSelectorOnEveryItemController valid without modification and holds the diff on that method to a doc-comment amendment plus one explicit line. Inverting it would require editing every existing Arrange block in the suite. +4. **New host state is a settable internal property, never a constructor parameter.** QuickFiler/Viewers/BreadcrumbDropDownHost.cs lines 210-214 explicitly warn that the constructor binding is reflection-based with arity and shape matching, and the `MayTakeFocus` property at line 216 is the established precedent for adding state without disturbing it. +5. **Coverage is collected over the QuickFiler.Test assembly, not repo-wide.** The UtilitiesCS.Test shell-icon test classes stall vstest.console.exe on this machine, and every production file in the write set belongs to the QuickFiler project. Baseline and final coverage use the identical scope so the two are comparable. +6. **The runner's own 80 percent floor is not this plan's coverage gate.** scripts/vscode/Invoke-MSTestWithCoverage.ps1 asserts a document-level floor after it writes the post-processed Cobertura XML, so an assembly-scoped run can throw on that floor while still producing readable numbers. This plan's gate is the changed-code figure and the no-regression comparison of the two `lines-covered` over `lines-valid` ratios, and the runner's throw is admitted only when its distinctive message is recorded. +7. **Every test run in this plan is scoped to one assembly and named classes.** The alternative, a repository-wide run, was rejected for the stall reason above and because it would compare two different populations against the coverage runner's own `TestCategory!=LiveOutlook` filter. +8. **Raw tool output is written to gitignored paths and summarised into committed Markdown.** `*.log`, coverage/, TestResults/ and artifacts/ are all gitignored, so a raw log or TRX named as the evidence artifact could never be committed; a TRX additionally embeds the host account and machine names. +9. **The AC6 per-item selector-open value is read through the concrete controller, not through an interface member.** QuickFiler/Interfaces/IQfcItemController.cs was proposed as an additional write-set path and withdrawn, and it is not the path the write set later grew to seventeen by: QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs line 337 declares the compiled hand-written implementor `FakeQfcItemController`, its `` entry is at QuickFiler.Test/QuickFiler.Test.csproj line 220, and .NET Framework 4.8.1, which is what both write-set projects target (QuickFiler/QuickFiler.csproj line 13 and QuickFiler.Test/QuickFiler.Test.csproj line 18), has no default interface members, so a new interface member is `CS0535` in a file outside the write set. Adding that file as a further path was also rejected, because its name contains a space and blast-radius derivation splits on whitespace, so the write claim would be split into two fragments and silently lost. The adopted mechanism costs no interface change: `QfcItemController` is `internal partial` across twelve parts, one of them `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` at line 25; the count was eleven when this plan was first authored and the merge of origin/main added a twelfth part, QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs, which declares only EnsureBreadcrumbPipeline and therefore collides with no member task P1-T4 adds; `QfcItemGroup.ItemController` is `internal IQfcItemController ItemController` at QuickFiler/Controllers/QfcItemGroup.cs line 39 in the same assembly; and `QuickFiler/Controllers/QfcFormController.Deactivate.cs` is in that same assembly, so the deactivate handler casts the loop's interface-typed item controller to the concrete internal type. The null-cast branch P1-T4's acceptance requires is carried by the formatter's nullable parameter rather than by any statement in the handler: the per-item log statement is a single unconditional call whose arguments are null-propagating and null-coalescing expressions, so no `if` statement, `return`, `throw`, or assignment to a new local is added. That is what lets the same task satisfy the `SelectorWasOpen=unavailable` clause, the no-added-control-flow clause, and the requirement that the formatter be reachable from the existing deactivate suite, which injects its item controllers as Moq mocks of the interface and therefore never produces a successful cast to the concrete type. + +--- + +## Structural self-check performed by the planner + +This section records a structural self-check, not a validator result. The `mcp__drm-copilot__validate_orchestration_artifacts` tool is not present in this planning session's tool surface, so the mandatory plan validator was NOT RUN by the planner and no validator outcome is claimed or inferred. Task P0-T13 probes for it at execution time. + +Task count, obtained by enumerating every line in this document that begins a task and counting the enumerated lines, not by adding a delta to a previously stated figure: this plan holds **86** tasks. The per-phase counts, each obtained the same way, are Phase 0: 14, Phase 1: 15, Phase 2: 2, Phase 3: 6, Phase 4: 11, Phase 5: 8, Phase 6: 6, Phase 7: 4, Phase 8: 9, Phase 9: 11. The figure moved to 86 from the 84 the previous revision pass established, because Phase 8 gained the deliberate test re-pinning task P8-T1 and its verification task P8-T2 at the head of the phase, and the seven check-off and mirror tasks that phase already held were renumbered from P8-T1 through P8-T7 to P8-T3 through P8-T9; the per-phase list above is the enumeration that establishes the new total. This paragraph is the only statement of a document-wide total in this plan. Outside this paragraph the numeral 86 does not occur anywhere in this document, and the only occurrence of the numeral 84 outside this paragraph is the .gitignore line number cited in the raw-tool-output section above. + +Checked by inspection of this document: phase headings use the exact `### Phase N — ` form with an em dash, including the retitled Phase 8; task identifiers are sequential within each phase with no gaps, which the enumeration above confirms for the extended Phase 4 and the renumbered Phase 8 as well; every task's opening line carries a path token; the seventeen write-set paths are the only backticked paths in the document, and every path named in an exclusion sentence is unbackticked; every acceptance condition was read against the state the plan will be in when its task runs; every `git diff` carries an explicit ref operand; every name-listing diff carries a companion staging or porcelain span; the write-mode formatter tasks carry a before-and-after tree observation rather than relying on an exit code; and no acceptance token contains a placeholder or interpolation marker. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/policy-audit.2026-09-07T17-05.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/policy-audit.2026-09-07T17-05.md new file mode 100644 index 000000000..bad704872 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/policy-audit.2026-09-07T17-05.md @@ -0,0 +1,497 @@ +# Policy Audit — issue #796 (QuickFiler folder drop-down closes on open; row click does not select) + +- Timestamp: 2026-09-07T17-05 +- Issue: #796 +- Work Mode: full-bug (marker read from `issue.md` line 12: `- Work Mode: full-bug`) +- Feature folder: `docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796` +- Base ref: `a6b259160f9ac1fbe251708d897fd4721486259e` (origin/main, ancestry verified by the executor at P9-T10) +- Branch head reported by the caller: `8e427fe1` +- Reviewed worktree: `C:/Users/DanMoisan/repos/TaskMaster/.claude/worktrees/agent-af8210acca019debc` +- Policies applied, in order: `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/quality-tiers.md`, `.claude/rules/tonality.md` + +## Template Provenance + +The MCP tool `mcp__drm-copilot__resolve_policy_audit_template_asset` is not available in this +session, so this artifact was hand-authored preserving every canonical major heading required by +`.claude/skills/policy-audit-template-usage/SKILL.md`. `mcp__drm-copilot__validate_orchestration_artifacts` +is likewise unavailable and was not run. This is a tooling limitation, recorded rather than +concealed; the structural requirements of the skill are met. + +## Review Method and Its Limits (recorded as an assumption) + +The caller imposed a binding tooling constraint: the Bash tool must not be used in this worktree +because an unattended `git` invocation from a review agent hangs indefinitely. All evidence below +was therefore gathered with read-only file inspection (Read, Grep, Glob) plus the pre-materialised +diff the caller supplied at +`C:/Users/DANMOI~1/AppData/Local/Temp/claude/C--Users-DanMoisan-repos-TaskMaster-wt-2026-09-06T17-16/b3e58737-6e95-4aae-b188-50cc8e7cf80a/scratchpad/796-code-diff.patch`, +which is `git diff a6b25916..HEAD -- QuickFiler QuickFiler.Test`. + +Consequences, stated so no reader over-reads this audit: + +- The supplied patch is pathspec-limited to `QuickFiler` and `QuickFiler.Test`. It is therefore the + complete CODE footprint but not the complete path footprint. +- The claim that the full branch diff contains 84 paths — 15 code, 68 feature-folder, 1 promoted + potential record — and zero paths under `UtilitiesCS/`, `UtilitiesCS.Test/`, `.claude/`, `.codex/`, + `.agents/`, `config/`, `.github/` or `TaskMaster.sln` could not be re-derived by this reviewer with + `git`. It is corroborated by second-party evidence at + `evidence/qa-gates/p9-t10-scope-boundary.md`, which records the anchored two-dot `--name-status` + diff, its ancestry check, and a mechanical classification of all 84 paths. This audit treats that + as the scope record and marks the independent re-derivation UNVERIFIED-BY-TOOLING, not absent. +- `artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt` do not exist in this + worktree (verified by glob over `artifacts/**`; only `pr_body_*` and `artifacts/orchestration/**` + are present). They could not be regenerated without shell access. The caller-supplied full-code + patch plus the scope-boundary artifact were used in their place. + +## Rejected Scope Narrowing + +No caller instruction attempted to narrow the audit to a plan, task, phase, or file subset, and none +declared any language "out of scope", "informational only", or "not applicable". Two caller +statements were examined against the scope invariant and neither is a narrowing: + +1. "Do NOT use the Bash tool at all." This constrains the TOOL, not the scope. The full branch code + diff was supplied in file form precisely so the full-footprint audit could still be performed. It + is recorded above as a method limitation with its consequences enumerated. +2. "Do not raise the pre-existing absolute repository coverage level as a blocking finding against + this item." This directs the DISPOSITION of a finding, not its existence. The finding is recorded + below with an explicit FAIL verdict against the policy floor and a non-blocking disposition, which + is the treatment this repository has applied to the same condition before. No coverage check was + skipped and no language verdict was suppressed. + +Neither statement was acted on as a narrowing. The audit scope is the full branch diff against +`a6b25916`. + +## Evidence Location Compliance + +`validate_evidence_locations.py` could not be executed (no shell). Compliance was established by +enumerating the feature folder with Glob and classifying every path. + +- Every evidence artifact in this feature lies under `<FEATURE>/evidence/<kind>/` with `<kind>` in + {`baseline`, `qa-gates`, `regression-testing`, `issue-updates`, `other`}. All five are canonical + sub-paths per `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`. +- `<FEATURE>/research/` and `<FEATURE>/runbooks/` are feature documents, not `evidence/` artifacts, + and are outside the evidence scheme. +- Zero files were written under `artifacts/baselines/`, `artifacts/baseline/`, `artifacts/qa/`, + `artifacts/qa-gates/`, `artifacts/evidence/`, `artifacts/coverage/`, `artifacts/regression-testing/` + or `artifacts/post-change/`. The only `artifacts/` content in this worktree is + `artifacts/orchestration/` (an allowed non-evidence sub-path) and pre-existing `pr_body_*` files + belonging to other issues. +- Raw coverage XML, TRX files and MSBuild logs were written to the gitignored `coverage/` and + `TestResults/` directories and are not committed. That is correct: none of them is an evidence + artifact path, and a TRX embeds host and account names. + +No evidence-location violation was found. No `EVIDENCE_LOCATION_OVERRIDE_REJECTED` entry is required. + +## Executive Summary + +The change is a bug fix in `QuickFiler` delivering four production mechanisms (an AC2 self-inflicted +deactivation seam, an AC3 pending-commit latch, an AC4 search-owned dismissal latch, and AC6 debug +instrumentation) plus five test changes. Fifteen code paths changed; all fifteen are declared write-set +members. + +The mandated four-stage C# toolchain completed a single clean pass in order: CSharpier check clean over +1608 files, `/t:Rebuild` analyzer gate 0 warnings / 0 errors with 36 compiler invocations, `/t:Rebuild` +nullable gate 0 warnings / 0 errors with 36 compiler invocations and no `/p:Nullable=enable` token, and +1380 of 1380 `QuickFiler.Test` tests passing against a baseline of 1370 with an empty failure set. + +Coverage: changed-code line coverage is 97.5610 percent (40/41), corroborated at 95.2381 percent with no +exclusion taken and 97.2222 percent with all comment lines removed; all three clear the 90 percent +new-code floor. Repository-wide line coverage is 24.1857 percent, which FAILS the policy floor. That +figure is pre-existing (24.1387 percent at baseline), was moved upward by this item, and is recorded +below as a FAIL with a non-blocking disposition. + +Verdict: PASS. Blocking findings: 0. + +## 1. General Unit Test Policy Compliance + +Source: `.claude/rules/general-unit-test.md`. + +| Requirement | Verdict | Evidence | +|---|---|---| +| Independence — tests run in any order | PASS | The three added suites construct all state per test. `CloseOrderingHostHarness` and `ItemViewerDropDownHarness` save and restore `SynchronizationContext.Current` in constructor/`Dispose`, so no ambient state leaks across tests. | +| Isolation — one unit per test | PASS | Each added test drives one seam: one formatter, one close branch, one leave branch, one open path. | +| Fast execution | PASS | Whole assembly of 1380 tests runs in 13.4 s (`evidence/qa-gates/p9-t5-full-assembly-tests.md`). | +| Determinism — no flakiness, no wall-clock | PASS | `InlineSynchronizationContext.Post` runs callbacks inline so the popup lifecycle settles synchronously; `OpenAndSettle` asserts `opening.IsCompleted` before acting. Grep over `QuickFiler.Test` for `Thread.Sleep`, `Task.Delay`, `DateTime.Now`, `new Random(` returns no hit in any file changed by this item. | +| Readability, documented intent | PASS | Every added `[TestMethod]` carries a `<summary>` stating Scenario and Expected outcome, and the two whose proof is limited carry a `<remarks>` stating the limit (`BreadcrumbDropDownCloseOrderingTests.cs` lines 305-310 and 359-367). | +| Arrange–Act–Assert structure | PASS | All added tests carry literal `// Arrange`, `// Act`, `// Assert` comments in that order. | +| Clear failure messages | PASS | The `CancelCount` assertions carry FluentAssertions `because` reasons ("a close racing an in-flight commit must not cancel the selection"; "a close with no commit in flight still cancels the selection"). | +| No external dependencies (DB, network, process, live Outlook) | PASS | The surface factory seam returns a plain `Panel` and a stub messenger; `CoreWebView2Environment` is obtained via `FormatterServices.GetUninitializedObject` rather than a real environment. No window is shown. The full run uses `TestCategory!=LiveOutlook`. | +| No temporary files in tests | PASS | Grep of the three changed/added test files finds no `Path.GetTempPath`, `Path.GetTempFileName`, `File.Create` or equivalent. | +| No mutable global state | PASS | The only ambient state touched is `SynchronizationContext.Current`, which both harnesses restore in `Dispose` (the `CloseOrderingHostHarness` constructor also restores it on a throwing construction path). | +| Scenario completeness — positive, negative, boundary | PASS | Every new mechanism is landed as a polarity PAIR: AC2 genuine/self-inflicted, AC3 commit-pending/no-commit-pending, AC4 search-driven/mouse-driven. | +| Coverage floors | PARTIAL | Changed-code and new-file figures pass; repo-wide line and branch rates fail the floor. See section 5. | +| No production file excluded from coverage measurement | PASS (with recorded policy conflict) | This diff adds no coverage `exclude` entry and no `[ExcludeFromCodeCoverage]` attribute. The pre-existing class-level attributes on `QfcFormViewer` (line 17) and `ItemViewer` (line 20) are WinForms form-derived types, formally exempted by the ratified COM/VSTO/WinForms carve-out in `CLAUDE.md` § UT2. That carve-out and the flat prohibition in `.claude/rules/general-unit-test.md` § Coverage Exclusion Policy are in unreconciled conflict repo-wide; this item neither creates nor widens the conflict. Recorded, not charged to this change. | +| Test file location | PASS (with recorded convention conflict) | `.claude/rules/general-unit-test.md` requires a `tests/` tree. This repository's C# convention is a sibling `<Project>.Test` assembly, used by every existing C# test in the tree. The new files follow the repository convention and mirror the production namespace. Pre-existing repo-wide divergence, not introduced here. | +| Determinism infrastructure (banned APIs) | PASS | No banned timing API appears in any changed test file. | + +## 2. General Code Change Policy Compliance + +Source: `.claude/rules/general-code-change.md` and `CLAUDE.md` § General Code Change Policy. + +| Requirement | Verdict | Evidence | +|---|---|---| +| Bugfix workflow — failing regression test first | PASS | Documented RED-first evidence for every behavioural criterion: `evidence/regression-testing/p4-t5-ac2-fail-before.md`, `p5-t3-ac3-fail-before.md`, `p6-t4-ac4-fail-before.md`. P6-T4 records EXIT_CODE 1 with `ExpectedExitCode: 1`, Total 2 / Passed 1 / Failed 1, and the exact Moq message proving the failure is the intended one, not a harness defect. | +| Minimal, targeted fix | PASS | Fifteen code paths, all write-set members. Two conditional write-set paths (`FolderBreadcrumb.html`, `BreadcrumbDropDownOpenCoordinator.cs`) were deliberately NOT changed after the evidence refused to justify them; recorded at `evidence/other/close-ordering-decision.md` (`AC3-HTML-POINTERDOWN: NOT REQUIRED`, `AC3-ENFORCEMENT-SITE: HOST`). | +| Simplicity first | PASS | Each mechanism is a single boolean latch or a single guarded early return. No new abstraction, no new interface beyond one property. | +| Separation of concerns | PASS | The three diagnostic formatters are pure static string builders taking primitives, so the AC6 evidence rests on deterministic managed-seam assertions rather than a source-text scan. Logging is separated from the close handler's control flow. | +| Extensibility / no breaking public API | PASS | One member added to the internal add-in interface `IQfcFormViewer`; its only implementor, `QfcFormViewer`, is updated in the same diff. `IBreadcrumbDropDownHost` and `IQfcItemController` are unchanged, so mock hosts and the hand-written test implementor are undisturbed. Host constructor arity is unchanged; `IsCommitPending` is a settable internal property, matching the #677 `MayTakeFocus` precedent. | +| Error handling — fail fast, no silent swallow | PASS | No new catch is introduced. The pre-existing per-item boundary catch in the cancel loop is preserved with its rationale comment intact (`QfcFormController.Deactivate.cs` lines 135-146). | +| Logging uses the project pattern | PASS | Controllers use `logger`, viewers use `log`; the new `BreadcrumbDropDownHost.Diagnostics.cs` declares `log4net.LogManager.GetLogger(typeof(BreadcrumbDropDownHost))`. Both new lines are Debug level and neither alters control flow. The `OnDropDownClosed` line is emitted at entry, ahead of the guard return, as the spec requires. | +| Comment why, not what | PASS with one exception | Comments are consistently rationale-bearing. One pre-existing comment is now falsified by the change — see CR-1 in the code review. | +| File size ≤ 500 lines | PASS | 15 paths measured post-format with the baseline-commensurable idiom `(Get-Content -LiteralPath $_).Count`; maximum 496 at `BreadcrumbDropDownHost.cs`, which FELL from 498 because `OnDropDownClosed` was relocated to the new part. `evidence/qa-gates/p9-t8-file-size-audit.md`. Test files are included in the audit, as this repository requires. | +| Toolchain loop in exact order, restart on failure | PASS | `evidence/qa-gates/p9-t11-final-loop.md` records the RESTART branch honestly: P9-T3 attempt 1 failed on three `MSB3061` file-lock warnings caused by a running Outlook process, the cause was timestamped to a 42-second window outside this item's diff, the gate was NOT reinterpreted to accommodate it, and the loop was rerun end to end after the cause was cleared. | +| Dependencies | PASS | No package added or changed. | +| I/O boundaries | PASS | The new logic touches no disk, network or COM. `Form.ActiveForm` is read only for a diagnostic field that the code explicitly does not act on. | + +## 3. Language-Specific Code Change Policy Compliance (C#) + +Source: `CLAUDE.md` § C# Code Change Policy. + +| Requirement | Verdict | Evidence | +|---|---|---| +| CSharpier via `dotnet tool run`, manifest-pinned | PASS | `dotnet tool run csharpier check .` EXIT_CODE 0, "Checked 1608 files", empty unformatted list (`evidence/qa-gates/p9-t2-csharpier-check.md`); reproduced in the final loop. | +| `dotnet format` NOT used | PASS | No artifact records a `dotnet format` invocation; every formatting command in the evidence tree is `dotnet tool run csharpier`. | +| Analyzers with `/t:Rebuild` (never `/t:Build`) | PASS | Command reproduced verbatim in `evidence/qa-gates/p9-t3-analyzer-rebuild.md`; `/t:Rebuild` present, `CscTaskCount=36` and `CscToolCount=36` read back from the detailed log, so the gate is not vacuous. 0 warnings, 0 errors, matching the P0-T8 baseline of 0/0. | +| Nullable gate with `/t:Rebuild` and `/p:TreatWarningsAsErrors=true` | PASS | `evidence/qa-gates/p9-t4-nullable-rebuild.md`: EXIT_CODE 0, 0/0, 36/36 invocations, both touched assemblies rewritten after `RunStartedUtc`. | +| No solution-wide `/p:Nullable=enable` | PASS | Absent from the command text and corroborated mechanically: `NullableEnableTokenCount=0` over the detailed-verbosity log, which reproduces every project's csc command line. | +| Per-file nullable opt-in respected | PASS | The one new production file, `BreadcrumbDropDownHost.Diagnostics.cs`, opens with `#nullable enable` and its handler signature uses `object? sender`, matching the part it was moved from. | +| Strong contracts, explicit types at boundaries | PASS | Every added member has an explicit return type and XML documentation. `SetBreadcrumbPopupOwner` validates both arguments and returns without side effect when either is null. | +| `var` only where obvious | PASS | The added production code uses explicit types throughout. | +| Naming conventions | PASS | `PascalCase` members (`IsCommitPending`, `IsDeactivationSelfInflictedByOwnPopup`, `FormatDropDownClosedDiagnostics`), `_camelCase` private fields (`_searchOwnedDismissal`, `_breadcrumbPopupOwners`). Names are descriptive rather than abbreviated. | +| XML documentation on non-obvious public/internal API | PASS | Notably strong: the `IQfcFormViewer` member documents that its polarity is load-bearing and must not be inverted, and the `activeFormIsNull` parameter documents that the discriminator it was introduced for was measured to run inverted and is therefore retained as data only. | +| Non-SDK-style projects need explicit `<Compile Include>` | PASS | Three entries added — one in `QuickFiler.csproj` for the diagnostics part, two in `QuickFiler.Test.csproj` for the new test files. Effectiveness is proved rather than assumed: P6-T4 reports Total 2 for the new latch class, which is non-zero only if the compile entry took effect. | +| Internal over public where possible | PASS | Every new member except the interface property and its implementation is `internal` or `private`. | + +## 4. Language-Specific Unit Test Policy Compliance (C#) + +Source: `CLAUDE.md` § C# Unit Test Policy. + +| Requirement | Verdict | Evidence | +|---|---|---| +| MSTest framework | PASS | `[TestClass]` / `[TestMethod]` from `Microsoft.VisualStudio.TestTools.UnitTesting` in all changed test files. No xUnit or NUnit reference introduced. | +| Moq for mocking | PASS | `Mock<IItemViewer>`, `Mock<IFolderSearchHandler>`, `Mock<IBreadcrumbDropDownHost>`, `Mock<IQfcItemController>`, `MockBehavior.Strict` where a provider must not be called. | +| FluentAssertions for assertions | PASS | Every new assertion is `Should()`-based. No MSTest `Assert.*` call was added. | +| Test commands | PASS | `vstest.console.exe` resolved through `vswhere` to `Common7\IDE\Extensions\TestPlatform\vstest.console.exe`, run with `/InIsolation` and the repository runsettings. | +| Deliberate updates to tests that pin changed contracts, not weakening | PASS | Two updates, both disclosed and both minimal. See section 8. | + +## 5. Test Coverage Detail + +Coverage artifacts inspected (not regenerated): `coverage/p9-t6-final.cobertura.xml` and +`coverage/p0-t11-baseline.cobertura.xml`. The canonical path `artifacts/csharp/coverage.xml` is +absent; per the precedent that a feature-run Cobertura document counts as the artifact, the two +documents above were read directly and their document-level attributes were re-verified by this +reviewer with a literal grep, not taken from the executor's summary. + +Independent verification performed by this reviewer: + +- `coverage/p9-t6-final.cobertura.xml` carries `line-rate="0.241857"`, `lines-covered="14925"`, + `lines-valid="61710"` — confirmed present. +- `coverage/p0-t11-baseline.cobertura.xml` carries `line-rate="0.241387"`, `lines-covered="14867"`, + `lines-valid="61590"` — confirmed present. +- `QfcFormViewer.cs` appears in ZERO class nodes of the final document, corroborating the + class-level `[ExcludeFromCodeCoverage]` at source line 17 and the executor's NOT MEASURABLE row. +- `BreadcrumbDropDownHost.Diagnostics.cs` is present in the final document, so the new part is + instrumented rather than silently absent. + +Coverage verdicts by language. Every language with changed files carries an explicit PASS or FAIL. + +| Language and scope | Measured | Threshold | Verdict | +|---|---|---|---| +| C# (csharp) repo-wide line coverage 24.1857% | 24.1857% | >= 85% (`.claude/rules/quality-tiers.md`), >= 80% (`CLAUDE.md`) | FAIL — non-blocking disposition | +| C# (csharp) repo-wide branch coverage 23.0082% | 23.0082% | >= 75% | FAIL — non-blocking disposition | +| C# (csharp) repo-wide no-regression: 24.1387% -> 24.1857% coverage | +0.0470 pp | must not decrease | PASS | +| C# (csharp) changed-code line coverage 97.5610% (40/41) | 97.5610% | >= 90% new code, >= 85% modified | PASS | +| C# (csharp) changed-code corroboration, no exclusion taken, coverage 95.2381% (40/42) | 95.2381% | >= 90% | PASS | +| C# (csharp) changed-code corroboration, comment lines removed, coverage 97.2222% (35/36) | 97.2222% | >= 90% | PASS | +| C# (csharp) new file BreadcrumbDropDownHost.Diagnostics.cs coverage 100.0000% (28/28) | 100.0000% | >= 85% | PASS | +| PowerShell (Pester) — 0 changed `.ps1` files in the branch diff, so 0 files enter coverage; coverage obligation nil | 0 files | line >= 85%, no branch gate | PASS | +| Python — 0 changed `.py` files in the branch diff; coverage 0 files measured | 0 files | >= 85% / >= 75% | PASS | +| TypeScript — 0 changed `.ts`/`.tsx` files in the branch diff; coverage 0 files measured | 0 files | >= 85% / >= 75% | PASS | + +Disposition of the two repo-wide FAIL rows. Both are recorded as FAIL because the measured figures +are below the policy floors and a below-floor figure is never recorded as PASS. Both carry a +NON-BLOCKING disposition and no remediation-inputs artifact, on these grounds, each of which is +independently checkable: + +1. The condition is pre-existing. The baseline document, produced before any change on this branch, + records 24.1387 percent from the same single-assembly-scoped runner. +2. The item improved it. Numerator +58, denominator +120, ratio +0.0470 pp. +3. The changed-code floor is met three separate ways, none of which was chosen after seeing which + one passed. +4. The figure is a whole-solution document rate produced by a run scoped to one test assembly, so it + is not a measurement of repository quality by any single item's authorship. + +Per-file rows for the five named production files, baseline versus final (executor measurement, from +`evidence/qa-gates/p9-t6-coverage-final.md`): + +| File | Final covered / valid | Baseline covered / valid | +|---|---|---| +| QfcFormController.Deactivate.cs | 48 / 48 | 25 / 25 | +| BreadcrumbDropDownHost.cs | 287 / 289 | 291 / 293 | +| BreadcrumbDropDownHost.Open.cs | 24 / 24 | 23 / 23 | +| QfcItemController.EventHandlers.cs | 97 / 118 | 89 / 108 | +| BreadcrumbDropDownOpenCoordinator.cs | 234 / 238 | 234 / 238 | + +No changed file regressed on its changed lines. The one changed-line exclusion taken +(`QfcItemController.EventHandlers.cs` line 282, `IsBreadcrumbSelectorOpen`) is individually named +with a reason that is mechanically checkable: its only reader casts an `IQfcItemController` to the +concrete `QfcItemController`, and every deactivate test injects Moq mocks of the interface, so the +cast yields null and the member is never evaluated. One of an allowance of three was spent. + +The second uncovered changed line, `EventHandlers.cs` line 209, was deliberately left in the +denominator rather than excluded. That is the correct choice and is adjudicated as F1 in section 8. + +## 6. Test Execution Metrics + +| Metric | Value | Source | +|---|---|---| +| Assembly | `QuickFiler.Test.dll` | `evidence/qa-gates/p9-t5-full-assembly-tests.md` | +| Total | 1380 | run summary and TRX (`TotalResultNodes=1380`) | +| Passed | 1380 | run summary | +| Failed | 0 | `NonPassedCount=0` over all 1380 TRX result nodes | +| Baseline total | 1370 | `evidence/baseline/p0-t10-quickfiler-test-baseline.md` | +| Baseline failure set | EMPTY | same | +| Net new tests | +10 | 1380 − 1370 | +| Filter | `TestCategory!=LiveOutlook`, `/InIsolation` | command recorded verbatim | +| Duration | 13.4 s | run summary | +| Final-loop re-run | 1380 / 1380, EXIT_CODE 0 | `evidence/qa-gates/p9-t11-final-loop.md` | +| Expect-fail inventory | 4 rows, all Passed after the fix | resolved by `testName` against the TRX | + +The "Failed: 0" reading is a measurement, not an omission: the console prints no `Failed:` line on a +passing run, so the executor corroborated it against the TRX result nodes. The inventory count of 4 +rather than 5 is correctly derived from the recorded `AC2-PARK-FOCUS-SUPPRESSED: NO` branch, and the +executor recorded that a literal token search of the P4-T7 artifact returns the OPPOSITE answer +because the single hit is the artifact declaring the token absent. That is the kind of trap that +usually produces a wrong count; it was caught. + +## 7. Code Quality Checks + +| Check | Command | Result | +|---|---|---| +| Format | `dotnet tool run csharpier format .` then `check .` | EXIT_CODE 0, 1608 files, empty unformatted list | +| Lint / analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | EXIT_CODE 0, 0 warnings, 0 errors, 36/36 compiler invocations | +| Type check / nullable | `msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | EXIT_CODE 0, 0 warnings, 0 errors, 36/36 compiler invocations, 0 `Nullable=enable` tokens | +| Test | `vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:... /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook"` | EXIT_CODE 0, 1380 / 1380 | + +Gate anti-vacuity was checked in both build gates: `/t:Rebuild` was used rather than `/t:Build`, the +compiler-invocation counts were read back from a detailed log and equal the baseline's 36, and both +touched assemblies carry a `LastWriteTimeUtc` later than the run start. A warm `/t:Build` returning +exit 0 with `CoreCompile` skipped — the documented failure mode for these gates — is excluded. + +Tonality (`.claude/rules/tonality.md`): the diff's comments, the XML documentation, and the evidence +artifacts are factual and measured throughout. Several places state a limit rather than overstating a +result — for example `BreadcrumbDropDownCloseOrderingTests.cs` lines 359-367 explicitly record which +half of AC1 the test does NOT prove. No humour, hyperbole, or decorative metaphor was found. PASS. + +## 8. Gaps and Exceptions + +### F1 — Dead internal accessor `SearchOwnsDropDownDismissal` (adjudicated: NOT blocking) + +`QuickFiler/Controllers/QfcItemController.EventHandlers.cs` line 209: + +```csharp +internal bool SearchOwnsDropDownDismissal => _searchOwnedDismissal; +``` + +Verified independently by this reviewer: a Grep for the identifier across every `.cs` file in the +worktree returns exactly one hit, the declaration. The property has no reader in production or test +code. + +The distinction the caller asked to be made precisely, re-verified here rather than accepted: + +- The underlying FIELD `_searchOwnedDismissal` is fully live. Grep places it at line 203 + (declaration), written at 183, 220, 234, 257 and 265, and READ at 263 in `if (!_searchOwnedDismissal) return;` + — which is the AC4 latch itself. The AC4 mechanism is delivered and exercised. +- Only the exposing property is dead. The re-pinned test reaches the state by reflection + (`QfcItemControllerTestSupport.SetField(controller, "_searchOwnedDismissal", true)` at + `QfcItemController.SearchDismissalTests.cs` line 85), by field name, which is why the property + never acquired a reader. + +Adjudication: NOT blocking, stated plainly. It is an `internal`, get-only, side-effect-free +expression-bodied member on an internal partial class. It changes no public surface, cannot be called +by any consumer outside the assembly, introduces no branch, and cannot alter behaviour. Its entire +cost is one uncovered line, and that line was deliberately left IN the changed-code denominator +rather than excluded, so the 97.5610 percent figure is reported against it rather than around it. +It is a tidiness defect, not a correctness or policy defect. Under +`.claude/rules/general-code-change.md` ("make the public surface area small and intentional") it is a +minor violation with no consequence. + +Recommended, not required: either delete the property, or make the re-pinned test set the state +through the property's sibling path instead of reflecting on a field name — which would also remove +the string-literal field-name coupling noted as CR-6 in the code review. Neither is a condition of +merge. + +### F2 — Falsified plan premise about comment lines and Cobertura (adjudicated: adequately handled) + +The plan reasoned that "a comment line carries no Cobertura `line` node at all", so comment-only +changed lines could not enter the coverage denominator. The executor measured that this holds for 139 +of 144 comment-only changed lines, and fails for five: `BreadcrumbDropDownHost.cs` lines 442-446 each +carry a `line` node with `hits=1`, while lines 251-254 in the same file carry none, so the behaviour +is not even uniform within one file. + +Adjudication: the disclosure and the three-way reporting are an adequate response, and better than +adequate in three specific respects. + +1. The departure was disclosed in the direction that DISADVANTAGES the executor's own figure. Those + five lines are covered, so including them raises the changed-code percentage; the executor said so + explicitly ("those five lines sit in the denominator and are all covered, so including them raises + the changed-code figure") rather than letting the favourable arithmetic pass silently. +2. The result is shown not to depend on the disputed mechanism. 97.5610 percent (reported), 95.2381 + percent (no exclusion taken), 97.2222 percent (all comment lines removed from both numerator and + denominator, which neutralises the departure entirely). All three clear 90 percent. +3. The artifact records "Neither computation was selected after seeing which one passed; both are + reported", and the third computation is precisely the one that removes the benefit of the + departure. Post-hoc metric selection is the failure mode this disclosure forecloses. +4. The proposed mechanism ("the conversion maps the full source span preceding a mapped statement") + is labelled as unestablished and explicitly not relied upon. That is the correct epistemic + handling of an unverified explanation. + +No further action is required. NOT blocking. + +### E1 — Human-interaction exception HI-796-1 (AC6 runtime observation) + +Recorded as a permitted exception, following the #400 and #438 precedent. The runbook at +`runbooks/confirm-dropdown-close-ordering.runbook.md` was executed by the maintainer against a Debug +build from `ec674e0c`, whose parent `0dfcb402` is the instrumentation commit; the observation states +that `ec674e0c` changes no compiled source relative to its parent, so the instrumentation is present +in the observed build. The transcript is at +`evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md` and its conformance check at +`evidence/other/p2-t2-manual-observation-conformance.md`. + +Quality notes on the exception, since a manual artifact is the weakest evidence class in this +repository and deserves scrutiny rather than deference: + +- It carries an explicit Elision notice stating exactly which line runs were elided and guaranteeing + that no `SelectorWasOpen=True` line, no `entered.` line and no `OnDropDownClosed` line is elided, + so file order among decision-carrying lines is intact. +- It carries a Redaction note: no absolute host paths, user names, mailbox addresses or personal + folder names appear in the excerpt. Verified by reading the artifact. +- It records values that CONTRADICT the predictions of the spec and the research artifact + (`ActiveFormNull` inverted on all four observations; `WebView2Focused` reversed on both gestures) + and follows the observation rather than the prediction. A manual artifact that only confirmed its + own predictions would warrant more suspicion than this one does. +- It states its own blind spot rather than filling it: candidate 3 is recorded as NOT DIRECTLY + OBSERVABLE, with the reason that a handler carrying no logging site emits nothing whether it ran or + not, and the decision record refuses to read that silence as refutation. + +Outlook is deliberately closed and was not relaunched by this review. No gate here depends on +relaunching it. + +### E2 — Deliberate test update, not a weakening (confirmed by re-derivation, not assumption) + +`QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs`, method +`TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent`. The caller asked that this +reasoning be confirmed rather than assumed. It was, on five independent points: + +1. The method name is retained. Verified in the current file at line 80. +2. The `Times.Once()` assertion on `SetFolderDroppedDown(false)` is retained. Verified at line 91. +3. Exactly one Arrange line was added (line 85) plus a `<para>` in the doc comment. The diff hunk + confirms no other line changed in the method. +4. The class's `[TestMethod]` count is unchanged at 6. Verified by a Grep count over the current + file, matching the spec's stated requirement. +5. The case the method no longer covers is covered elsewhere with the opposite assertion: + `SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown` asserts `Times.Never()` on the same call + (`QfcItemController.SearchLeaveLatchTests.cs` line 178). Coverage of the mouse-driven case is + moved, not lost. + +The provenance claim was also checked against a second source. `evidence/other/phase7-blocking-finding-out-of-write-set-test.md` +records that the executor STOPPED at this test rather than editing it, on the ground that the file was +not then a write-set member and that narrowing the AC4 fix to keep it green would mean not delivering +AC4. The plan delta, the seventeenth write-set path and the spec's Test Strategy section were authored +in response. That is the correct handling of a pre-existing test that pins a contract an AC deliberately +changes, and it is the opposite of quietly weakening a test to make a gate pass. + +### E3 — Two write-set paths deliberately not changed + +`QuickFiler/Resources/FolderBreadcrumb.html` and `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` +are write-set members that the diff does not touch. This is correct and is evidence-driven, not an +omission: the write set is an upper bound on what may change, and both paths were adjudicated in the +decision record from measured field values (`PendingClose=False` on all three gestures refutes a +coordinator-sited latch; the transcript's silence on activation cannot meet the admissibility condition +for the HTML change). The decision record additionally records the limitation that this "states the +evidence does not support the page change, not that the page change has been shown unnecessary". + +### E4 — Producer side of the AC2 seam is not covered by any automated test + +`QfcFormViewer.IsDeactivationSelfInflictedByOwnPopup` and the `ItemViewer.Breadcrumb.cs` registration +call both sit in types carrying a class-level `[ExcludeFromCodeCoverage]`, and no test exercises +either. The AC2 tests mock `IQfcFormViewer`, so what is proven is the CONSUMER's gating, not the +producer's derivation. This is permitted by the ratified WinForms exemption in `CLAUDE.md` § UT2 and +is consistent with the spec's own automation-feasibility table, but the spec's claim that AC2 is +automatable "Yes, fully" is slightly overstated: fully at the seam, not end to end. Recorded as a +limitation, not charged as a violation. + +## 9. Summary of Changes + +Fifteen code paths, all write-set members. + +Production — modified (7): +- `QuickFiler/Controllers/QfcFormController.Deactivate.cs` — AC6 entry and per-item diagnostics via two pure static formatters; AC2 guard scoped to the cancel loop. +- `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` — AC4 `_searchOwnedDismissal` provenance latch with two producers and one consumer; AC6 `IsBreadcrumbSelectorOpen`. +- `QuickFiler/Interfaces/IQfcFormViewer.cs` — AC2 seam declaration with load-bearing polarity documented. +- `QuickFiler/Viewers/QfcFormViewer.cs` — AC2 popup-owner registry and predicate implementation. +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` — AC2 registration beside the existing `MayTakeFocus` assignment (4 lines). +- `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` — AC3 latch set on `ExplicitCommit`; AC3 conditional cancel in `FinishClose`; `OnDropDownClosed` relocated out. +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` — AC3 `IsCommitPending` property; cleared in `ShowPopup`. + +Production — created (1): +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` — AC6 host-side instrumentation, the log4net field, and the relocated native-close handler. + +Test — modified (3) and created (2): +- `QfcFormControllerDeactivateTests.cs` (+1 explicit genuine-case Arrange, +2 tests), `BreadcrumbPendingOpenCloseTests.cs` (+2 tests, all five existing tests retained), `QfcItemController.SearchDismissalTests.cs` (deliberate re-pin), `BreadcrumbDropDownCloseOrderingTests.cs` (new, 4 tests), `QfcItemController.SearchLeaveLatchTests.cs` (new, 2 tests). + +Compile entries — modified (2): `QuickFiler.csproj` (1 entry), `QuickFiler.Test.csproj` (2 entries). + +## 10. Compliance Verdict + +**PASS. Blocking findings: 0.** + +Section verdicts: §1 PASS (coverage sub-row PARTIAL, dispositioned), §2 PASS, §3 PASS, §4 PASS, +§5 PASS for every changed-code and new-file threshold with two pre-existing repo-wide FAIL rows +carrying a non-blocking disposition, §6 PASS, §7 PASS, §8 two adjudicated findings, both NOT blocking. + +No remediation-inputs artifact is produced, because no finding requires remediation before merge. The +non-blocking findings recorded in `code-review.2026-09-07T17-05.md` (CR-1 through CR-3 in particular) +are recommended for promotion to follow-up issues through the potential-to-issue lifecycle rather +than being fixed in this branch, so that they survive the merge of this feature folder. + +## Appendix A: Test Inventory + +Tests added by this change (10 net new): + +| # | Test | File | Criterion | +|---|---|---|---| +| 1 | FormatDeactivationDiagnostics_IncludesEveryDiscriminatingField | QfcFormControllerDeactivateTests.cs | AC6 | +| 2 | FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector | QfcFormControllerDeactivateTests.cs | AC2 (expect-fail row) | +| 3 | FormatDropDownClosedDiagnostics_IncludesEveryDiscriminatingField | BreadcrumbDropDownCloseOrderingTests.cs | AC6 | +| 4 | NativeCloseWhileCommitPending_DoesNotCancelSelection | BreadcrumbDropDownCloseOrderingTests.cs | AC3 (expect-fail row) | +| 5 | NativeCloseWithNoCommitPending_StillCancelsSelection | BreadcrumbDropDownCloseOrderingTests.cs | AC3 scoping guard | +| 6 | GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose | BreadcrumbDropDownCloseOrderingTests.cs | AC1 | +| 7 | CloseWhilePendingOpenAndCommitPending_DoesNotCancelSelection | BreadcrumbPendingOpenCloseTests.cs | AC3 | +| 8 | RowSetRefreshWhileOpen_NeverClosesHost | BreadcrumbPendingOpenCloseTests.cs | AC5 (#438 AC-3 guard) | +| 9 | SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown | QfcItemController.SearchLeaveLatchTests.cs | AC4 (expect-fail row) | +| 10 | SearchLeaveAfterSearchDrivenOpen_ClosesDropDown | QfcItemController.SearchLeaveLatchTests.cs | AC4 paired positive | + +Tests deliberately updated (2): `FormDeactivated_CancelsSelectorOnEveryItemController` (+1 explicit +genuine-case Arrange line; `Times.Once()` on both controllers retained) and +`TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent` (+1 Arrange line; name and +`Times.Once()` retained). + +Tests deliberately NOT changed, serving as scoping guards: the two `CancelCount.Should().Be(1)` +assertions at `BreadcrumbPendingOpenCloseTests.cs` lines 48 and 79, which prove the AC3 cancel +suppression is conditional rather than global, and `FormDeactivated_WebView2Focused_ParksFocusOnce`, +whose non-modification is the recorded `AC2-PARK-FOCUS-SUPPRESSED: NO` decision. + +## Appendix B: Toolchain Commands Reference + +``` +dotnet tool run csharpier format . +dotnet tool run csharpier check . +msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook" +pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput coverage\p9-t6-final.cobertura.xml +``` + +Read-only commands this reviewer would have run had shell access been permitted, listed so the gap is +auditable: `git diff --name-status a6b25916..HEAD`, `git status --porcelain --untracked-files=all`, +`python scripts/dev_tools/validate_evidence_locations.py --root .`. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/research/2026-09-06T21-30-quickfiler-folder-dropdown-close-ordering-research.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/research/2026-09-06T21-30-quickfiler-folder-dropdown-close-ordering-research.md new file mode 100644 index 000000000..801864c2d --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/research/2026-09-06T21-30-quickfiler-folder-dropdown-close-ordering-research.md @@ -0,0 +1,461 @@ +# Issue #796 — QuickFiler folder drop-down close ordering: research + +- Issue: #796 +- Worktree: branch `bug/quickfiler-folder-dropdown-closes-on-open-796`, cut from `origin/main` at `c431dc32` +- Date: 2026-09-06 +- Scope: OPEN/CLOSE lifecycle and selection-commit ordering only. Row-text projection (archive-root lineage) is owned by a concurrent sibling item and is out of scope here. + +Acceptance criteria AC1 through AC6 are reproduced verbatim from `issue.md` in section 9. They are authoritative, are not renumbered, and are not weakened anywhere in this document. + +No numeric acceptance criterion is proposed in this artifact. No count, enumeration, or population is asserted as a `spec.md` acceptance criterion, so no `## Numeric Derivation Evidence` section is required. + +--- + +## 1. Citation re-derivation against the current tree + +Every citation in the issue's `## Suspected Cause / Notes` section was re-read in this worktree. Paths are repository-relative with forward slashes. + +### 1.1 Confirmed exact — symbol exists and line range matches + +| Issue citation | Path | Finding | +|---|---|---| +| `FolderBreadcrumb.html:440-442` (`#dropDownButton` posts `selectorToggle`) | `QuickFiler/Resources/FolderBreadcrumb.html` | Confirmed. Lines 440-442 are the `dropDownButton.addEventListener("click", ...)` handler posting `{ type: "selectorToggle" }`. | +| `BreadcrumbBridgeCoordinator.HandleSelectorMessage (:349-358)` | `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs` | Toggle branch confirmed at 349-358 (`case BreadcrumbSelectorToggleMessage _:` -> `CancelSelector()` / `OpenSelector()`). See drift note 1.2 (a) on the method declaration line. | +| `FolderBreadcrumbBridgeRouter.OpenSelector` | `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs:185-186` | Confirmed. `public BreadcrumbSelectionTransition OpenSelector() => Mutate(_selectionSession.OpenSelector);` | +| `BreadcrumbDropDownOpenCoordinator.HandleSelectorOpenStateChanged (:178-191)` | `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` | Confirmed exact, 178-191. | +| `BreadcrumbDropDownOpenLifetime.OpenCoreAsync (:215-256)` | `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs` | Confirmed exact, 215-256. | +| `BreadcrumbDropDownOpenLifetime.Focus.cs:32-51` (`FocusCurrentSurface`) | `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.Focus.cs` | Confirmed. Method spans 32-51 (expression body closes at 51; file is 53 lines). `_host.FocusPending()` at :40, guarded by `if (takeFocus)` at :39. | +| `BreadcrumbDropDownHost.cs:165-172` (`AutoClose = true`) | `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` | Confirmed. `AutoClose = true` at :167 inside the `DropDown` initializer at 165-170; `DropDown.Closed += OnDropDownClosed;` at :171. | +| `BreadcrumbDropDownHost.Open.cs:98-102` | `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` | Confirmed. `ShowPopup` at 98-102. See finding 3.1: this member sets `DropDown.AutoClose = takeFocus`, which the issue does not record. | +| `QuickFiler.Test/Viewers/BreadcrumbSelectorOpenRetryTests.cs:55` | same | Confirmed. `SetFolderDroppedDownTrue_UsesSameOpenRequestAsMouseSelectorToggle` declared at :55. | +| `FolderBreadcrumbBridgeRouter.cs:478-482` (`ReplaceRowsPreservingSession`) | `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs` | Confirmed exact, 478-482. | +| `BreadcrumbSelectionSession.ReconcileRowsReplaced (:119-147)` | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs` | Confirmed exact, 119-147. Preserves `IsOpen` (no write to it on any path). | +| `QfcFormController.SetupDisposal.cs:175` | `QuickFiler/Controllers/QfcFormController.SetupDisposal.cs` | Confirmed exact. `_formViewer.FormDeactivated += this.FormViewer_Deactivated;` | +| `QfcFormViewer.cs:207` | `QuickFiler/Viewers/QfcFormViewer.cs` | Confirmed exact. `public void ParkFocusOffWebView2() => this.ActiveControl = _l1v1L2h2_ButtonOK;` | +| `ItemViewer.Breadcrumb.cs:203` (popup focus delegate) | `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` | Confirmed exact. `() => host.ControlHost?.Control.Focus(),` | +| `ItemViewer.Breadcrumb.cs:205` (cancel delegate) | same | Confirmed exact. `() => BreadcrumbCoordinator?.CancelSelector(),` | +| `BreadcrumbDropDownOpenCoordinator.cs:273-288` (`FinishOpenCore`) | same | Confirmed exact, 273-288. Re-checks `_isSelectorOpen()` after the async open. | +| `BreadcrumbDropDownHost.cs:426-437` (`OnDropDownClosed`) | same | Confirmed exact, 426-437. | +| `BreadcrumbDropDownHost.cs:439-455` (`FinishClose`) | same | Confirmed exact, 439-455. `_cancelSelection()` at :450 under `reason == Uncommitted`. | +| `BreadcrumbDropDownHost.cs:452` (asymmetry comment) | same | Confirmed exact. `// Issue #677: only the focus step is gated; the cancel step above always runs.` | +| `QfcItemController.EventHandlers.cs:217-228` (`TextBoxSearch_Leave`) | `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` | Confirmed exact, 217-228. | +| `QfcItemController.EventHandlers.cs:195` (`_searchLeaveHandoffPending = true`) | same | Confirmed exact, inside the `Keys.Down` branch at 192-199. | +| `ItemViewer.Breadcrumb.cs:270-274` (`MayRestoreBreadcrumbFocus`) | same | Confirmed exact, 270-274. | +| `QfcFormControllerDeactivateTests.cs:172` | `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` | Confirmed exact. `FormDeactivated_CancelsSelectorOnEveryItemController` declared at :172. | +| `BreadcrumbPendingOpenCloseTests` (`:124`, `:143`) | `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` | Confirmed exact. `ToggleAndEscapeWhileOpenIsPending_EachClosesHostExactlyOnce` at :124; `AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce` at :143. See drift note 1.2 (d) on what these tests actually encode. | +| `FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55` (`ReplaceItemsPreservingSession`) | `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs` | Confirmed exact, 38-55. Emits `Handled \| RenderRequired` only — no `OpenStateChanged`, no `SelectionChanged`. | + +### 1.2 Citation drift — reported, not silently corrected + +(a) **`BreadcrumbBridgeCoordinator.HandleSelectorMessage (:349-358)`.** The cited range is the toggle `case` body, not the method. The method `HandleSelectorMessage` is declared at `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs:343` and ends at :379. Anyone re-reading the citation as a method range will miss the `BreadcrumbSelectorActivationMessage` case at 362-364, which is the row-click commit path and is load-bearing for AC3. + +(b) **`QfcFormController.ParkFocusAndCancelSelectors` (`QfcFormController.Deactivate.cs:39-57`).** The symbol exists and the declaration is at :39, but the method body ends at :71, not :57. Lines 58-70 are the per-item boundary `catch` with `logger.Error(...)`. The cited range truncates the method and omits the only existing log call in the file. The file is 73 lines total. + +(c) **`BreadcrumbSelectionSession.Open`.** The pipeline description names `BreadcrumbSelectionSession.Open` as the direct callee of `FolderBreadcrumbBridgeRouter.OpenSelector`. The direct callee is `BreadcrumbSelectionSession.OpenSelector` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs:197-202`), which calls `Open()` (`:307-319`). Both symbols exist; there is one hop the citation omits. `OpenSelector` is what supplies the `OpenStateChanged` effect flag (`:200`); `Open()` alone does not. + +(d) **`BreadcrumbPendingOpenCloseTests` encode "close wins over a pending open".** The two cited tests (`:124`, `:143`) do exist at those lines, but what they assert is narrower than the phrase suggests: each asserts that exactly one `IBreadcrumbDropDownHost.Close(expectedReason)` call reaches a mocked host when a close intent arrives while the open task is unresolved. They pin *close idempotency*, not cancel-versus-commit precedence. The tests that literally encode "a close beats a pending open, and cancels" are the two earlier ones in the same file: `CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent` (`:22`) and `CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus` (`:55`), which assert `harness.CancelCount.Should().Be(1)` at `:48` and `:79`. Section 6 treats all four. + +(e) **Candidate 2 attributed to the click-without-select symptom.** The issue states that native `ToolStripDropDown` auto-close is "the mechanism behind the click-without-select symptom". For the reproduction as written (step 3 types letters, step 4 clicks a row) this is very likely wrong, because a search-driven open sets `DropDown.AutoClose = false` (`QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs:100`). With `AutoClose == false` the framework's auto-dismiss is disabled, so candidate 2 cannot be the first cause on that path. See section 3.1 and section 5. + +### 1.3 Citations that could not be checked in this worktree + +`docs/features/archive/2026-08-07-quickfiler-search-keystroke-focus-steal-438/research/2026-08-08T10-30-...-research.md:172` is cited with an elided filename. The `2026-08-07-...-438` archive folder exists in this tree, but the exact file and line were not resolved because the citation is not a complete path. Treated as unverified. + +--- + +## 2. The OPEN pipeline and the CLOSE/CANCEL pipeline + +Both pipelines are traced end to end below. "sync" means the step runs to completion on the calling stack. "posted" means it is enqueued on the captured UI synchronization boundary (`BreadcrumbUiDispatcher` / `BreadcrumbPopupUiOperations`) and resumes on a later message-pump turn. "await" marks a genuine asynchronous suspension. + +### 2.1 OPEN — mouse (arrow click) + +| # | Step | File:line | Boundary | +|---|---|---|---| +| 1 | `#dropDownButton` click posts `{type:"selectorToggle"}` | `QuickFiler/Resources/FolderBreadcrumb.html:440-442` | JS, in the collapsed WebView2 | +| 2 | `IWebViewMessenger.MessageReceived` -> `OnMessageReceived` -> `ObserveInboundAsync` -> `DispatchInboundMessageAsync` | `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs:273-306` | async; `IsSelectorMessage` true | +| 3 | `_dispatcher.Dispatch(() => HandleSelectorMessage(json))` | same, `:301` | **posted** (await) | +| 4 | `HandleSelectorMessage` toggle case -> `OpenSelector()` | same, `:343`, `:349-358` | sync on UI boundary | +| 5 | `BreadcrumbBridgeCoordinator.OpenSelector` -> `_router.OpenSelector()` | same, `:149` | sync | +| 6 | `FolderBreadcrumbBridgeRouter.OpenSelector` -> `_selectionSession.OpenSelector` | `UtilitiesCS/.../FolderBreadcrumbBridgeRouter.cs:185-186` | sync | +| 7 | `BreadcrumbSelectionSession.OpenSelector` -> `Open()`; sets `IsOpen = true`; returns `Handled \| OpenStateChanged` | `UtilitiesCS/.../BreadcrumbSelectionSession.cs:197-202`, `:307-319` | sync | +| 8 | `ApplyTransition` -> `_dispatcher.Dispatch(() => PublishTransition(transition))` | `BreadcrumbBridgeCoordinator.cs:186-194` | **posted** | +| 9 | `PublishTransition` raises `SelectorOpenStateChanged` | same, `:239-242` | sync on UI boundary | +| 10 | `BreadcrumbItemViewerLifecycleCoordinator.OnSelectorOpenStateChanged` -> `_openCoordinator.HandleSelectorOpenStateChanged()` | `QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs:237-238` | sync | +| 11 | `HandleSelectorOpenStateChanged` -> `_operations.PostAsync(...)` -> `_isSelectorOpen()` true -> `RequestOpen()` | `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:178-191` | **posted** | +| 12 | `RequestOpen` -> `_currentOpenTask = OpenCoreAsync(_generation)` | same, `:111-125` | sync under `_sync` | +| 13 | `OpenCoreAsync` -> `RunAsync(() => BeginOpenCore(generation))` | same, `:220-231` | **await** | +| 14 | `BeginOpenCore` consumes the no-focus latch (`takeFocus = !_nextOpenTakesNoFocus`), computes anchor/size, calls `_host.OpenAsync(anchor, workingArea, size)` — the 3-parameter overload, i.e. `takeFocus: true` | same, `:239-271` | sync | +| 15 | `BreadcrumbDropDownHost.OpenAsync` -> `OpenWithFocusIntentAsync(..., true)`; not already open -> `_openLifetime.OpenAsync(..., takeFocus: true)` | `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs:18-22`, `:53-89` | sync | +| 16 | `BreadcrumbDropDownOpenLifetime.OpenAsync` takes a lease, invalidates the previous generation, schedules `OpenCoreAsync` via `RunOnOwnerAsync` | `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:44-72` | **posted + await** | +| 17 | `OpenCoreAsync` -> `EnsureSurfaceAsync(lease)` (creates the popup WebView2 on first use; early-returns when `HasInstalledSurface`) | same, `:215-256`, `:290-343` | **await** | +| 18 | `PlaceSurfaceAsync` sizes host/control/dropdown | `QuickFiler/Viewers/BreadcrumbPopupUiOperations.cs:188-216` | **await** | +| 19 | `ShowCurrentSurface` -> sets `_host.OpenState = true`, then `_host.ShowPopup(location, takeFocus)` | `BreadcrumbDropDownOpenLifetime.cs:258-279` | **await**, then sync | +| 20 | `ShowPopup` sets `DropDown.AutoClose = takeFocus` and calls `_showPopup` = `ShowOwnedPopup` -> `dropDown.Show(anchor, ...)` | `BreadcrumbDropDownHost.Open.cs:98-102`; `BreadcrumbPopupUiOperations.cs:100-105` | sync | +| 21 | `FocusCurrentSurface(lease, takeFocus)` -> `_host.FocusPending()` | `BreadcrumbDropDownOpenLifetime.Focus.cs:32-51` | **await**, then sync | +| 22 | `FocusPending` evaluates `MayTakeFocus()` (= `ItemViewer.MayRestoreBreadcrumbFocus`) and, if permitted, `host.ControlHost?.Control.Focus()` on the popup's own WebView2 | `BreadcrumbDropDownHost.cs:288-292`; `ItemViewer.Breadcrumb.cs:203`, `:270-274` | sync | +| 23 | `CompleteOpenAsync` resolves the open task; `OpenCoreAsync` then runs `FinishOpenCore(generation, opened)` which re-checks `_isSelectorOpen()` and closes with `ExplicitCommit` if the session was cancelled meanwhile | `BreadcrumbDropDownOpenLifetime.cs:154-186`; `BreadcrumbDropDownOpenCoordinator.cs:229-231`, `:273-288` | **await**, then sync | + +**Ordering substance.** Step 22 (the Win32 focus move onto the popup's top-level window) happens *before* step 23 (`FinishOpenCore`). If the focus move deactivates the QuickFiler form, the resulting `Form.Deactivate` handler runs on the same UI thread and can cancel the session while the open task is still resolving. `FinishOpenCore` then observes `_isSelectorOpen() == false` and issues `CloseCore(ExplicitCommit)` itself. That is a second, independent close path fed by the first. + +### 2.2 OPEN — keyboard (Down in the search box) + +Identical from step 11 onward. The entry differs: + +1. `QfcItemController.TextBoxSearch_KeyDown`, `Keys.Down` branch — `QuickFiler/Controllers/QfcItemController.EventHandlers.cs:190-199`. Three synchronous statements in order: `_itemViewer.SetFolderDroppedDown(true)`; `_searchLeaveHandoffPending = true`; `_itemViewer.FocusFolderDropDown()`. +2. `ItemViewer.SetFolderDroppedDown` -> `SetBreadcrumbDropDownState(true)` -> `_breadcrumbLifecycleCoordinator.SetDroppedDown(true, FocusBreadcrumbCore)` — `QuickFiler/Viewers/ItemViewer.FolderSearch.cs:31-32`; `ItemViewer.Breadcrumb.cs:288-300`. +3. `BreadcrumbItemViewerLifecycleCoordinator.SetDroppedDown` -> `_openCoordinator.SetDroppedDown(true)` — `BreadcrumbItemViewerLifecycleCoordinator.cs:192-205`. +4. `BreadcrumbDropDownOpenCoordinator.SetDroppedDown` **posts** `_openSelector()` (= `BreadcrumbBridgeCoordinator.OpenSelector`) — `BreadcrumbDropDownOpenCoordinator.cs:159-176`. The open reaches `RequestOpen` through the `SelectorOpenStateChanged` event, exactly as in the mouse path. +5. `FocusFolderDropDown()` -> `FocusBreadcrumb()` -> posted `FocusBreadcrumbCore()` -> `_l0vhBreadcrumb_WebView2.Focus()` — `ItemViewer.FolderSearch.cs:47`; `ItemViewer.Breadcrumb.cs:246-255`, `:276-286`. This focuses the **collapsed anchor** on the same form, which raises `Leave` on the search textbox — the reason the #680 latch exists. + +No no-focus latch is set on either gesture path, so `BeginOpenCore` computes `takeFocus == true` for both. `BreadcrumbSelectorOpenRetryTests.SetFolderDroppedDownTrue_UsesSameOpenRequestAsMouseSelectorToggle` (`QuickFiler.Test/Viewers/BreadcrumbSelectorOpenRetryTests.cs:55`) pins that the two produce the same open request, which is consistent with mouse and keyboard failing identically. + +### 2.3 OPEN — search typing (the path that works) + +`QfcItemController.TextBoxSearch_TextChanged` (`QuickFiler/Controllers/QfcItemController.EventHandlers.cs:173-182`) -> `ItemViewer.PresentFolderSearchResults` -> `PresentBreadcrumbSearchResults` -> `BreadcrumbItemViewerLifecycleCoordinator.PresentSearchResults` (in `BreadcrumbItemViewerLifecycleCoordinator.Search.cs`), which latches `LatchNextOpenTakesNoFocus()` (`BreadcrumbDropDownOpenCoordinator.cs:139-147`) so `BeginOpenCore` computes `takeFocus == false`. Consequences, both verified in source: + +- `ShowPopup` sets `DropDown.AutoClose = false` (`BreadcrumbDropDownHost.Open.cs:100`). +- `FocusCurrentSurface` skips `_host.FocusPending()` (`BreadcrumbDropDownOpenLifetime.Focus.cs:39-40`). + +Row-set refreshes route through `ReplaceItemsPreservingSession` (`UtilitiesCS/.../FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) and `ReplaceRowsPreservingSession` -> `ReconcileRowsReplaced` (`FolderBreadcrumbBridgeRouter.cs:478-482`; `BreadcrumbSelectionSession.cs:119-147`), neither of which writes `IsOpen` or emits `OpenStateChanged`. The issue's claim that async suggestion decoration is not the cause is confirmed. + +### 2.4 CLOSE/CANCEL — every route into `FinishClose` + +`BreadcrumbDropDownHost.FinishClose(reason)` (`QuickFiler/Viewers/BreadcrumbDropDownHost.cs:439-455`) is the single completion point. It runs three operations through `CompleteAll`: + +1. `DropDown.AutoClose = true` (restore default, #680). +2. `if (reason == Uncommitted) _cancelSelection();` — `_cancelSelection` is `() => BreadcrumbCoordinator?.CancelSelector()` (`ItemViewer.Breadcrumb.cs:205`). +3. `FocusAnchorIfPermitted()` — gated by `MayTakeFocus()`; the cancel above is not gated (comment at `:452`). + +Four routes reach it: + +- **R1, native auto-close.** WinForms raises `ToolStripDropDown.Closed` -> `OnDropDownClosed` (`:426-437`). Guards `_disposed`, `_programmaticClose`, `!OpenState`; then `_openLifetime.InvalidateAndSchedule(...)` — **posted** — re-checks the same three guards and calls `FinishClose(Uncommitted)`. This is the only route that supplies `Uncommitted` without a caller choosing it. +- **R2, programmatic close.** `Close(reason)` (`:247-257`) -> `InvalidateAndSchedule(() => CompleteClose(reason, true))` — **posted** -> `CompleteClose` (`:397-411`) sets `_programmaticClose`, calls `CloseNative()` (`:413-424`), then `FinishClose(reason)`. +- **R3, open-failure rollback.** `RestoreAfterOpenFailure` (`:457-469`) -> `FinishClose(Uncommitted)`. +- **R4, dispose / reset.** `DisposeCoreAsync` (`:329-352`) and `ResetCoreAsync` (`:303-327`) -> `CompleteClose(Uncommitted, true)`. + +The coordinator's side of R2 is `BreadcrumbDropDownOpenCoordinator.CloseCore` (`:324-359`), reached from `SetDroppedDown(false)` (`:174`), `HandleSelectorOpenStateChanged` when the session reports closed (`:189`, reason `ExplicitCommit`), and `FinishOpenCore` (`:284`, reason `ExplicitCommit`). + +### 2.5 CANCEL originating outside the popup + +`QfcFormController.FormViewer_Deactivated` -> `ParkFocusAndCancelSelectors` (`QuickFiler/Controllers/QfcFormController.Deactivate.cs:26-27`, `:39-71`), wired at `QfcFormController.SetupDisposal.cs:175`, runs entirely **synchronously** inside the WinForms `Form.Deactivate` event: + +1. `if (_formViewer?.IsWebView2Focused == true) _formViewer.ParkFocusOffWebView2();` (`:41-44`). `ParkFocusOffWebView2` is `this.ActiveControl = _l1v1L2h2_ButtonOK` (`QfcFormViewer.cs:207`), a synchronous WinForms active-control change that raises `Leave` on the previously active control before returning. +2. `foreach (QfcItemGroup group in groups) group.ItemController?.CancelBreadcrumbSelector();` (`:52-70`), each in its own `try`/`catch` with `logger.Error`. +3. `CancelBreadcrumbSelector` -> `ItemViewer.CancelBreadcrumbSelector` (`QuickFiler/Viewers/ItemViewer.FolderSearch.cs:43`) -> `BreadcrumbCoordinator.CancelSelector()` -> session `Cancel()` -> `Handled \| OpenStateChanged \| RenderRequired` -> posted `PublishTransition` -> `SelectorOpenStateChanged` -> `HandleSelectorOpenStateChanged` -> `_isSelectorOpen()` false -> `CloseCore(ExplicitCommit)` -> `_host.Close(ExplicitCommit)` -> R2. + +Note the reason: a deactivation-driven cancel closes the host with `ExplicitCommit`, so `FinishClose` does **not** call `_cancelSelection()` a second time. The session was already cancelled at step 3. + +### 2.6 The row-click commit path (AC3) + +1. Expanded selectable row registers `click` -> `post({ type: "selectorActivate", identity: row.identity })` — `QuickFiler/Resources/FolderBreadcrumb.html:289-291`. `click` fires on **mouseup**, not mousedown. +2. `HandleSelectorMessage` -> `case BreadcrumbSelectorActivationMessage activation: ActivateSelector(activation.Identity)` — `BreadcrumbBridgeCoordinator.cs:362-364`, `:173-174`. +3. `FolderBreadcrumbBridgeRouter.ActivateSelector` (`:194-195`) -> `BreadcrumbSelectionSession.ActivateSelector` (`:238-259`) -> `Activate(identity)` (`:353-375`). +4. **When the session is open**, `Activate` sets `PendingIdentity` and calls `CommitPending()` (`:361-364`), which commits and ends the session; the effect set includes `SelectionChanged` and `OpenStateChanged`, so the host closes with `ExplicitCommit` and `FinishClose` performs no cancel. This is the correct behavior AC3 asks for. +5. **When the session is already closed**, `Activate` takes the branch at `:367-374`: it still selects the row in the model and updates `CommittedIdentity`. `ActivateSelector` returns `Handled \| RenderRequired \| SelectionChanged` (no `OpenStateChanged`, since `closed` is false). + +Step 5 is important: a late `selectorActivate` on a closed session would still change the selection. The reported symptom is that the selection does **not** change. The most parsimonious reading, given step 1, is that the `selectorActivate` message is never produced at all — the popup is dismissed on **mousedown**, so no `mouseup` and therefore no `click` is delivered to the page. That is a hypothesis about browser/window behavior, not a code-verified fact; it is exactly the kind of thing the AC6 instrumentation must settle, and section 4 states the discriminating evidence. + +--- + +## 3. Two structural findings the issue does not record + +### 3.1 `AutoClose` is not constant — it tracks `takeFocus` + +`QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs:98-102`: + +```csharp +internal void ShowPopup(Point location, bool takeFocus) +{ + DropDown.AutoClose = takeFocus; + _showPopup(DropDown, Anchor, location); +} +``` + +The constructor's `AutoClose = true` (`BreadcrumbDropDownHost.cs:167`) is only the initial value; `FinishClose` restores it to `true` (`:446`); and every show overwrites it with the gesture's focus intent. This maps one-to-one onto the reported symptom asymmetry: + +| Gesture | `takeFocus` | `AutoClose` at show | `FocusPending()` called | Reported behavior | +|---|---|---|---|---| +| Arrow click | true | true | yes | flashes closed | +| Down in search box | true | true | yes | flashes closed | +| Typing (search) | false | false | no | stays open | + +Both candidate 1 and candidate 2 are gated by `takeFocus` in the same direction, so this asymmetry does **not** discriminate between them. It does, however, refute candidate 2 as the first cause of the *click-without-select* symptom in the reproduction as written (step 3 types, so the popup was shown with `AutoClose == false`). + +### 3.2 `ParkFocusOffWebView2` can synchronously trigger candidate 3 + +`ParkFocusAndCancelSelectors` parks focus (`Deactivate.cs:41-44`) **before** the cancel loop (`:52-70`). Parking is `this.ActiveControl = _l1v1L2h2_ButtonOK`, which synchronously raises `Leave` on the outgoing active control. If the search textbox is the outgoing control, `QfcItemController.TextBoxSearch_Leave` (`EventHandlers.cs:217-228`) runs *inside* the parking assignment and, with `_searchLeaveHandoffPending == false`, issues `SetFolderDroppedDown(false)` — a close with reason `Uncommitted`, ahead of the deactivate handler's own cancel loop. + +However, `IsWebView2Focused` (`QfcFormViewer.cs:190-201`) walks the `ActiveControl` chain and returns true only when the leaf is a `WebView2`. When the caret is in the search textbox, the leaf is a `TextBox`, so parking is skipped and this chain does not fire. The chain is therefore live only when a WebView2 is the active leaf — which is the arrow-click case, where the outgoing control is the collapsed breadcrumb WebView2, not the search textbox. In other words: on current code, candidate 3 is **not** reachable through candidate 1 on either reproduction path. This is a derivation from source, not an observation. + +--- + +## 4. Discriminating evidence for the three candidates, and what AC6 must log + +### 4.1 What would confirm or refute each candidate as the *first* cause + +**Candidate 1 — `QfcFormController.ParkFocusAndCancelSelectors` (form deactivation).** + +- *Confirms:* an entry log line from `ParkFocusAndCancelSelectors` appears in the log **before** any line from `OnDropDownClosed`, and the subsequent `OnDropDownClosed` line reports `CloseReason=CloseCalled` and `ProgrammaticClose=True` (i.e. the native close was our own `CloseNative`, downstream of the cancel). +- *Refutes:* `ParkFocusAndCancelSelectors` is never entered during the flash, or it is entered but reports `Groups=0` / `Cancelled=0`, or it is entered strictly after `OnDropDownClosed`. +- Additional discriminator available with no new plumbing: log `Form.ActiveForm == null` at entry. `ToolStripDropDown` is not a `Form`, so if the popup itself owns activation, `Form.ActiveForm` is null rather than naming another window. A null `ActiveForm` at deactivation is evidence of a self-inflicted deactivation; a non-null `ActiveForm` naming a foreign form is evidence of a genuine one. (`Form.ActiveForm` returning null for a non-Form active window is framework behavior, asserted from background knowledge, not verified in this session.) + +**Candidate 2 — native `ToolStripDropDown` auto-close.** + +- *Confirms:* an `OnDropDownClosed` line appears **first**, with `CloseReason` equal to `AppFocusChange` or `AppClicked` and `ProgrammaticClose=False`, `OpenState=True`, `AutoClose=True`. +- *Refutes:* `CloseReason=CloseCalled` (our own `CloseNative` did it), or the line does not appear at all before `ParkFocusAndCancelSelectors`, or `AutoClose=False` at the moment it fires. +- `ToolStripDropDownClosedEventArgs.CloseReason` is currently **discarded**: the `e` parameter of `OnDropDownClosed` (`BreadcrumbDropDownHost.cs:426`) is never read. It is the single most discriminating value available and costs nothing to record. + +**Candidate 3 — `QfcItemController.TextBoxSearch_Leave`.** + +- *Confirms:* a `TextBoxSearch_Leave` line reporting `HandoffPending=False, DropDownOpen=True` immediately precedes the close. +- *Refutes:* no such line, or `HandoffPending=True` (the Down-arrow handoff consumed it correctly). +- Section 3.2 predicts this candidate is unreachable on both reproduction paths. AC6 names only two instrumentation sites, so this candidate would be confirmed or refuted only indirectly — by whether the observed ordering leaves room for a third close. Adding a third temporary log line at `EventHandlers.cs:217` would settle it directly and is recommended even though AC6 does not require it. + +### 4.2 Logging facility, exact call form, and correlation identifier + +**`QfcFormController.ParkFocusAndCancelSelectors` — facility present, no new field needed.** + +`QfcFormController` is a partial class with a static log4net field already in scope: + +```csharp +// QuickFiler/Controllers/QfcFormController.cs:21-23 +private static readonly log4net.ILog logger = log4net.LogManager.GetLogger( + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType +); +``` + +It is already used in the target file at `QfcFormController.Deactivate.cs:64` (`logger.Error(...)`). The exact call form used by the nearest sibling partial for structured stage logging is: + +```csharp +// QuickFiler/Controllers/QfcFormController.EventHandlers.cs:31 +logger.Debug($"Cancel teardown stage completed. Stage={stage}"); +// QuickFiler/Controllers/QfcFormController.EventHandlers.cs:129 +logger.Info($"Cancel teardown starting. AlreadyCancelled={already}"); +``` + +Interpolated string, sentence prefix, then `Key=Value` pairs. AC6 instrumentation should match that shape. + +**`BreadcrumbDropDownHost.OnDropDownClosed` — no logger exists; one must be added.** + +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs` contains no logging of any kind. Its only failure sink is `_uiOperations.Report(exception)` (`:485`), which routes to `BreadcrumbUiDispatcher`'s error sink. The immediate neighbours in the same directory declare a static log4net field with this exact shape: + +```csharp +// QuickFiler/Viewers/BreadcrumbUiDispatcher.cs:17-19 +private static readonly log4net.ILog log = log4net.LogManager.GetLogger( + typeof(BreadcrumbUiDispatcher) +); +// QuickFiler/Viewers/BreadcrumbWebViewSurfaceFactory.cs:21-23 uses the same typeof(...) form +``` + +Note the field name differs by neighbourhood: `QuickFiler/Controllers` uses `logger`, `QuickFiler/Viewers` uses `log`. + +**Blocking constraint:** `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` is **498 lines** against the repository's 500-line ceiling. A logger field plus two or three log statements cannot fit. The instrumentation must go into a new partial-class part (the file already has a precedent for this: `BreadcrumbDropDownHost.Open.cs` exists specifically to keep the main part under the ceiling — see its class comment at `BreadcrumbDropDownHost.Open.cs:8-14`). That means `OnDropDownClosed` itself has to move, or delegate to a diagnostics helper declared in the new part. + +**Correlation identifier: none exists that both sites can carry.** Verified: + +- Form-controller side: `IQfcItemController.ItemNumber` exists (`QuickFiler/Interfaces/IQfcItemController.cs:45`), a 1-based item index reachable from `group.ItemController` inside the cancel loop. Usable immediately. +- Host side: the host knows only `Anchor` and its own instance. `Anchor.Name` is the Designer constant `"L0vhBreadcrumb_WebView2"` for every item viewer (`QuickFiler/Viewers/ItemViewer.Designer.cs:206`), so it does **not** discriminate between items. `BreadcrumbDropDownOpenLease.Generation` (`BreadcrumbDropDownOpenLifetime.cs:18`) and `BreadcrumbDropDownOpenCoordinator._generation` (`:27`) are per-host monotonic counters, but neither is exposed on `IBreadcrumbDropDownHost` and neither is visible to the form controller. +- Consequently, correlating the two sites today requires either (a) an ordinal surrogate such as `System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this)` logged at both ends plus a third line from `ItemViewer` tying the two together, or (b) threading `ItemNumber` (or a `Func<string>` owner descriptor) into `BreadcrumbDropDownHost` at construction in `ItemViewer.Breadcrumb.cs:198-207`. Option (b) is a constructor-arity change and would disturb the reflection-based constructor binding that `BreadcrumbDropDownHost.cs:210-214` explicitly warns about; a settable `internal` property assigned right after construction (the pattern already used for `MayTakeFocus` at `ItemViewer.Breadcrumb.cs:212`) avoids that. + +**Sequence source.** No application-level sequence number is needed. The log4net conversion pattern is `%date [%thread] %-5level %logger [%property{NDC}] - %message%newline` (`TaskMaster/log4net.config:30`), which yields millisecond timestamps and the thread name. Both instrumentation sites run on the single Outlook UI thread (`[VSTA_Main]` in the observed log), and log4net appends in call order, so **file order is the ordering** for same-thread lines. Millisecond timestamps alone would be insufficient — the whole flash occurs inside a fraction of a second and several of these steps can share a millisecond — so any analysis must rely on file order, not on comparing timestamps. + +**Minimum useful line content.** + +- `ParkFocusAndCancelSelectors` entry: method name, `WebView2Focused=`, `ActiveFormIsThisForm=` (or `ActiveFormNull=`), `Groups=<count>`; and per item, `ItemNumber=<n>`, `SelectorWasOpen=<bool>`. +- `OnDropDownClosed` entry: method name, `CloseReason=<e.CloseReason>`, `ProgrammaticClose=<_programmaticClose>`, `OpenState=<OpenState>`, `AutoClose=<DropDown.AutoClose>`, `Disposed=<_disposed>`, `PendingClose=<_openLifetime.IsPendingClose>`. Log at entry, before the guard returns, so a suppressed close is still visible. + +--- + +## 5. The self-inflicted-versus-genuine deactivation seam + +**What AC2 needs to distinguish.** At the moment `Form.Deactivate` fires, the handler must answer: did activation move to this add-in's own breadcrumb popup (suppress the cancel), or to any other window (cancel, preserving the #677 contract)? + +**What is currently injectable.** + +- `IQfcFormViewer` (`QuickFiler/Interfaces/IQfcFormViewer.cs`) exposes `FormDeactivated` (`:57`), `IsWebView2Focused` (`:64`), `ParkFocusOffWebView2()` (`:70`). All three are already mocked with Moq in `QfcFormControllerDeactivateTests`, and the event is delivered with `Mock.Raise`. Adding a fourth member here is a pure interface extension with an existing test pattern. +- `_groups` is injected by private-field reflection in the same test class (`QfcFormControllerDeactivateTests.cs:72-92`), so the item-controller fan-out is fully controllable. +- `IBreadcrumbDropDownHost` (`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs:19-67`) exposes `IsOpen`. `BreadcrumbDropDownHost.MayTakeFocus` is an `internal Func<bool>` settable property on the concrete type (`BreadcrumbDropDownHost.cs:216`) — an existing, proven predicate seam that tests already drive. +- `BreadcrumbPendingOpenCloseTests.PendingHostHarness` (`:197-274`) constructs a real `BreadcrumbDropDownHost` headlessly with delegate seams for focus-pending, focus-anchor, cancel, and show, under an inline synchronization context. No window is created and no WebView2 is initialized. + +**What is not injectable.** + +- `Form.ActiveForm` is a WinForms static, read directly and privately in `ItemViewer.MayRestoreBreadcrumbFocus` (`ItemViewer.Breadcrumb.cs:270-274`). `ItemViewer` is `[ExcludeFromCodeCoverage]` (`ItemViewer.cs:20`) and the method is `private`. No test can drive it. +- `Control.Focus()`, `ToolStripDropDown.Show`, `ToolStripDropDown.AutoClose`, and `ToolStripDropDownClosedEventArgs.CloseReason` are framework state. `CloseReason` is supplied by the framework and cannot be produced in a headless test without showing a real dropdown; a test can only assert on how the handler *branches* given a reason it was handed. +- `QfcFormViewer.ParkFocusOffWebView2` and `IsWebView2Focused` (`QfcFormViewer.cs:190-207`) read real `ActiveControl` chains on a real Form. + +**Minimum seam.** A boolean intent reported by the viewer at deactivation time, consumed by `ParkFocusAndCancelSelectors`: + +``` +IQfcFormViewer: + /// True when the window that took activation is a breadcrumb popup owned by this form. + bool DeactivationIsSelfInflicted { get; } +``` + +The production implementation lives in `QfcFormViewer` and is the only place that touches non-injectable state (`Form.ActiveForm == null` combined with "one of this form's item viewers reports an open drop-down", the latter reachable through the already-existing `IItemViewer.IsFolderDropDownOpen` at `QuickFiler/Viewers/IItemViewer.cs` and `ItemViewer.FolderSearch.cs:93`). `QfcFormController` then reads a single mockable boolean, and every AC2 branch is unit-testable with `Mock<IQfcFormViewer>` and no window. + +An alternative with a smaller interface footprint is a `Func<bool>` property on `QfcFormController` assigned by the same wiring that assigns `host.MayTakeFocus` — matching the `MayTakeFocus` precedent exactly. The interface-member form is preferred because the deactivate suite already mocks `IQfcFormViewer` and would need no new construction seam. + +**Residual that no unit test can cover.** Whether the popup taking focus actually produces a `Form.Deactivate` on this thread, and whether `Form.ActiveForm` is null at that moment, is Win32/WinForms runtime behavior. It is the INFERRED premise the issue flags and cannot be turned into a deterministic test in this repository. AC6's instrumentation is the mechanism that converts it from inferred to observed; it is not a substitute for a test, and the resulting fix must still be unit-tested at the managed seam. + +--- + +## 6. The two existing test files that pin the current contract + +### 6.1 `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` (248 lines) + +Class `QfcFormControllerDeactivateTests`. Headless: `Mock<IQfcFormViewer>`, `Mock<IQfcItemController>`, `_groups` injected by reflection, event delivered with `Mock.Raise`. Six test methods: + +| Line | Method | Currently asserts | +|---|---|---| +| 96 | `RegisterFormEventHandlers_SubscribesFormDeactivated` | the handler is wired during `RegisterFormEventHandlers` | +| 113 | `UnregisterFormEventHandlers_UnsubscribesFormDeactivated` | the handler is unwired during `UnregisterFormEventHandlers` | +| 134 | `FormDeactivated_WebView2Focused_ParksFocusOnce` | `IsWebView2Focused == true` -> `ParkFocusOffWebView2()` `Times.Once()` | +| 153 | `FormDeactivated_NoWebView2Focus_DoesNotPark` | `IsWebView2Focused == false` -> `ParkFocusOffWebView2()` `Times.Never()` | +| **172** | **`FormDeactivated_CancelsSelectorOnEveryItemController`** | with two injected item controllers, **each** receives `CancelBreadcrumbSelector()` `Times.Once()` (assertions at `:185-186`) | +| 194 | `FormDeactivated_NullGroupsOrNullItemGroups_DoesNotThrow` | null `_groups` and null `ItemGroups` are both non-throwing | +| 227 | `FormDeactivated_ItemCancelThrows_DoesNotPropagateAndContinues` | a throwing item does not propagate and the next item is still cancelled | + +**Which assertion changes under AC2.** Only `FormDeactivated_CancelsSelectorOnEveryItemController` (`:172`). Today it raises `FormDeactivated` against a bare `Mock<IQfcFormViewer>` and asserts the cancel is unconditional. Under AC2 the cancel becomes conditional on the new self-inflicted seam. + +**The deliberate update — not a weakening.** Split the single unconditional assertion into a matched pair, keeping the same method name for the genuine case so the #677 contract remains visibly pinned: + +1. Keep `FormDeactivated_CancelsSelectorOnEveryItemController` at its current name and its current `Times.Once()` assertions on both controllers, and add one Arrange line setting the new seam to report a **genuine** deactivation. Because Moq's default `bool` return is `false`, choosing `false` to mean "genuine / not self-inflicted" makes the existing Arrange block correct without modification and keeps the diff to a doc-comment amendment. That is the recommended polarity. +2. Add a new sibling test — for example `FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector` — that sets the seam to report self-inflicted and asserts `CancelBreadcrumbSelector()` `Times.Never()` on both controllers. This is the AC2 fail-before test. +3. Leave `FormDeactivated_WebView2Focused_ParksFocusOnce` (`:134`) unchanged only if the fix keeps focus parking unconditional. If parking is also suppressed for a self-inflicted deactivation, that test needs the same explicit genuine-case Arrange line and a paired negative. Decide this explicitly; do not let it change by default. + +The file is 248 lines, so both additions fit under the 500-line ceiling without a split. + +### 6.2 `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` (380 lines) + +Class `BreadcrumbPendingOpenCloseTests`. Five test methods: + +| Line | Method | Currently asserts | +|---|---|---| +| 22 | `CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent` | a `Close(Uncommitted)` while the surface factory is unresolved completes the open task as `false` without waiting; `ShowCount == 0`; `FocusPendingCount == 0`; first close `true`, repeat `false`; **`CancelCount == 1` (`:48`)**; `FocusAnchorCount == 1` (`:49`); `IsOpen == false` | +| 55 | `CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus` | same shape for a close during document readiness; `ReadyEventCount == 0`; **`CancelCount == 1` (`:79`)**; `FocusAnchorCount == 1` (`:80`); `PopupMessenger` null | +| 86 | `CloseCanceledFactory_AllowsOneFreshReopenWithoutLateMutation` | after a cancelled first attempt a fresh open succeeds exactly once; `ShowCount == 1`, `FocusPendingCount == 1`, `CancelCount == 1`, `FocusAnchorCount == 1`; the stale surface and messenger are disposed exactly once and the current ones are not | +| **124** | **`ToggleAndEscapeWhileOpenIsPending_EachClosesHostExactlyOnce`** | through a headless `ItemViewer` with a `Mock<IBreadcrumbDropDownHost>`: `SetBreadcrumbDropDownState(false)` produces exactly one `Close(Uncommitted)`; `HandleSelectorKey(Escape)` produces exactly one `Close(ExplicitCommit)` | +| **143** | **`AutomaticSelectorCloseWhileOpenIsPending_ClosesHostExactlyOnce`** | `BreadcrumbCoordinator.CancelSelector()` produces exactly one `Close(ExplicitCommit)` | + +**Which assertion changes under the new contract.** The five tests exercise *explicitly requested* closes, never a native auto-close and never a form deactivation, so none of them is contradicted by AC1, AC2 or AC4. Two assertions are at risk depending on the fix shape: + +- **`CancelCount == 1` at `:48` and `:79`.** These count invocations of the `cancelSelection` delegate supplied to the host constructor, reached through `FinishClose(Uncommitted)`. If the AC3 fix makes `FinishClose` consult a pending-commit latch and suppress the cancel while a commit is in flight, these two must be reasoned about explicitly: in both tests no commit is in flight, so the correct post-fix value remains `1` and the assertion must be **kept unchanged** as the guard proving the suppression is scoped, not global. If a proposed fix makes either of these `0`, that is evidence the suppression is too broad — treat it as a design signal, not as a test to update. +- **`FocusAnchorCount == 1` at `:49`, `:80`, `:114`.** These count the `focusAnchor` delegate, which `FinishClose` invokes through `FocusAnchorIfPermitted`. The harness leaves `MayTakeFocus` at its `() => true` default (`BreadcrumbDropDownHost.cs:216`), so they stay `1` unless the fix changes the default. Do not change the default. +- **`:124` and `:143`.** Under AC1 these remain correct as written: an explicit toggle-off and an explicit `CancelSelector()` must still close the host exactly once. The deliberate update here is **no change**, plus a new sibling test asserting the converse — that a *native* close arriving while an activation-commit is pending does not cancel (the AC3 fail-before test). That sibling needs a `ToolStripDropDownClosedEventArgs`-driven entry, which the existing `PendingHostHarness` cannot produce because it never shows a real dropdown; see section 8 for the automation limit. + +The file is 380 lines. One or two added tests fit; a third harness would likely require a new file. + +### 6.3 Adjacent test files that will feel the change + +`QuickFiler.Test/Viewers/BreadcrumbDropDownHostTests.cs` is **499** lines and `QuickFiler.Test/Viewers/BreadcrumbDropDownIntegrationTests.cs` is **500** lines. Both are at or one line from the ceiling. Any new host-level or integration-level test must go into a new file, which in a non-SDK-style project also means a new `<Compile Include>` entry. + +--- + +## 7. Candidate write set + +Non-SDK-style projects (explicit `<Compile Include>` required for every added or removed `.cs`): **`QuickFiler`**, **`QuickFiler.Test`**, **`UtilitiesCS`**, **`UtilitiesCS.Test`** — all four declare `<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">`. On the write set below, only `QuickFiler` and `QuickFiler.Test` gain files, so only those two `.csproj` files must be edited. + +### Production + +- `QuickFiler/Controllers/QfcFormController.Deactivate.cs` — AC6 instrumentation of `ParkFocusAndCancelSelectors`; AC2 self-inflicted guard around the cancel loop. 73 lines, ample room. +- `QuickFiler/Interfaces/IQfcFormViewer.cs` — AC2: declare the self-inflicted-deactivation seam. 72 lines. +- `QuickFiler/Viewers/QfcFormViewer.cs` — AC2: production implementation of the seam (the only site that reads `Form.ActiveForm`-class state on the form side). 293 lines. +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` — **NEW**. AC6 instrumentation of `OnDropDownClosed` plus the `log4net.ILog` field. Required because `BreadcrumbDropDownHost.cs` is 498/500 lines and cannot absorb them; follows the existing `BreadcrumbDropDownHost.Open.cs` partial-split precedent. +- `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` — AC3: commit-before-cancel ordering in `FinishClose`; move or delegate `OnDropDownClosed` to the new diagnostics part. Any net line growth must be offset by the move. +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` — AC1/AC3: if the fix changes the `AutoClose = takeFocus` policy in `ShowPopup`, or adds a pending-commit latch set at open time. 107 lines. +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` — AC2: assign the new self-inflicted/popup-owns-activation state alongside the existing `host.MayTakeFocus = MayRestoreBreadcrumbFocus` at `:212`. 456 lines; watch the ceiling. +- `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` — AC4: extend the `_searchLeaveHandoffPending` latch to the mouse open path. 263 lines. +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` — AC3, only if the commit-before-cancel ordering has to be enforced at the coordinator rather than the host. 395 lines. +- `QuickFiler/Resources/FolderBreadcrumb.html` — AC3, only if the row-activation handler must move from `click` (mouseup) to `pointerdown`/`mousedown` so the message is produced before dismissal. **Contention note:** this file is also the breadcrumb page the concurrent sibling item renders row text into. The change contemplated here is confined to the row event listener at `:289-291` and touches no projection logic, but the file is shared and the contention is recorded here rather than hidden. +- `QuickFiler/QuickFiler.csproj` — **required**: add a `<Compile Include="Viewers\BreadcrumbDropDownHost.Diagnostics.cs" />` entry. + +### Test + +- `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` — AC2: deliberate update of `FormDeactivated_CancelsSelectorOnEveryItemController` plus the new self-inflicted negative test. 248 lines. +- `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` — AC1/AC3: keep the existing five, add the pending-commit-versus-native-close guard if it fits. 380 lines. +- `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` — **NEW**. AC3 commit-before-cancel ordering at the host seam; `BreadcrumbDropDownHostTests.cs` (499) and `BreadcrumbDropDownIntegrationTests.cs` (500) have no room. +- `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs` — **NEW**, or extend `QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs` (477 lines, 23 lines of headroom). AC4 mouse-path latch. Prefer the new file; 23 lines is not enough for an Arrange-Act-Assert pair with doc comments. +- `QuickFiler.Test/QuickFiler.Test.csproj` — **required**: add `<Compile Include>` entries for each new test file above. + +### Explicitly not in the write set + +These were considered and are stated without backticks because no change is expected: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs, UtilitiesCS/UtilitiesCS.csproj, UtilitiesCS.Test/UtilitiesCS.Test.csproj, QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs, QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.Focus.cs, QuickFiler/Viewers/IBreadcrumbDropDownHost.cs. The AC5 regression guard is satisfied by leaving the session-preserving replacement path untouched, which is why the UtilitiesCS router and session files must stay out of the diff. + +--- + +## 8. Runtime log evidence + +Source: `<taskmaster-repo>/TaskMaster/bin/Debug/logs/debug_2026-09-06.log`, read only, outside this worktree. Not modified and not copied into the repository. + +### 8.1 What the log does show + +- Four complete QuickFiler sessions on 2026-09-06, each ending with a `QfcHomeController` "Home cleanup complete; ribbon release callback invoked." line at 17:35:42, 19:08:03, 19:56:14 and 20:15:31. +- Per-item load telemetry from `QuickFiler.Controllers.QfcItemController` ("Probability debug [QfcItemController.LoadFolderHandlerAsync ...] ... TopScore=") and from `QuickFiler.Controllers.QfcDatamodel`, `QfcStreamingDequeueConfidenceGate`, `QfcCollectionController`, `QuickFiler.Helper_Classes.ConversationResolver`. +- The #791 Cancel-teardown stage lines from `QuickFiler.Controllers.QfcFormController` at the end of each session (`Cancel teardown starting. AlreadyCancelled=False`, then `Stage=cancel-token`, `reset-keyboard`, `park-focus`, `unregister-handlers`, `hide-form`, `quiesce-loader`, `groups-cleanup`). +- Many ERROR and WARN lines, none of them from the QuickFiler item-view drop-down. The recurring ones are `UtilitiesCS.EmailIntelligence.ImageStripper` ("Failed to initialise tesseract engine"), `UtilitiesCS.HelperClasses.SegmentStopWatch` ("SegmentStopWatch created on UI thread 1"), and `QuickFiler.Viewers.WebView2BreadcrumbHost` / `QuickFiler.Controllers.EfcFormController` ("Breadcrumb CoreWebView2 initialization failed ... 0x8007139F"). + +### 8.2 The `WebView2BreadcrumbHost` errors are a different component + +`WebView2BreadcrumbHost` is referenced in production only from `QuickFiler/Viewers/WebView2BreadcrumbHost.cs`, `QuickFiler/Viewers/IBreadcrumbWebHost.cs`, `QuickFiler/Viewers/EfcViewer.cs` and `QuickFiler/Controllers/EfcFormController.cs`. The logged stack frames confirm the caller is `EfcFormController.InitializeBreadcrumbHostAsync`. This is the EmailFilerControl breadcrumb, not the QuickFiler `ItemViewer` popup pipeline (`BreadcrumbDropDownHost`). These errors are unrelated to #796 and are the subject of a separate promoted entry (`docs/features/potential/promoted/2026-09-06-breadcrumb-webview2-init-fails-resource-not-in-correct-state.md`). + +### 8.3 What the log does not show — the AC6 justification + +Searching the whole file for log4net logger names of the QuickFiler item-view drop-down pipeline returns **nothing**: + +- `QuickFiler.Viewers.BreadcrumbDropDownHost` — zero lines (the type declares no logger at all). +- `QuickFiler.Viewers.BreadcrumbUiDispatcher`, `BreadcrumbCollapsedSurfaceController`, `BreadcrumbWebViewSurfaceFactory`, `BreadcrumbMessengerHub`, `ItemViewer`, `QfcFormViewer` — zero lines each. The only `QuickFiler.Viewers.*` logger that appears anywhere in the file is `WebView2BreadcrumbHost`. +- `QuickFiler.Controllers.QfcFormController` appears only for the Cancel-teardown stage lines; nothing from `ParkFocusAndCancelSelectors`, which logs only on the per-item exception path (`QfcFormController.Deactivate.cs:64`) and therefore emitted nothing. + +The most direct evidence is the interaction window itself. In the fourth session the last load-time line is at `20:02:28,953` and the next line in the entire file is `20:15:31,320` (`Cancel teardown starting`). That is a thirteen-minute window of live user interaction with the QuickFiler form — the window in which the reproduction gestures occur — containing **zero** log lines of any level. + +**Verdict.** The issue's claim is confirmed: the close is a normal, non-exceptional code path and produces no diagnostic of any kind. The many ERROR/WARN lines in the file all belong to other components and none of them is emitted by the drop-down open or close pipeline. Runtime instrumentation is therefore the only way to establish the ordering, which is exactly what AC6 requires as the first implementation step. + +--- + +## 9. Acceptance criteria, verbatim + +- [ ] AC1: Opening the list by arrow click or by Down in the search box leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem. +- [ ] AC2: A deactivation of the QuickFiler form caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (the #677 contract is preserved for genuine deactivation). +- [ ] AC3: A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs. +- [ ] AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. +- [ ] AC5: Row-set refreshes while open (search, late decoration) continue not to close the list (#438 AC-3 regression guard). +- [ ] AC6: The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen. + +--- + +## 10. Candidate status summary + +| Candidate | Status | Basis | +|---|---|---| +| 1 — `QfcFormController.ParkFocusAndCancelSelectors` on form deactivation | **Downstream code CONFIRMED; first-cause status INFERRED** | The wiring (`SetupDisposal.cs:175`), the unconditional cancel loop (`Deactivate.cs:52-70`), the popup focus call (`ItemViewer.Breadcrumb.cs:203`), the absence of any self-inflicted latch, and the `FinishOpenCore` re-check (`BreadcrumbDropDownOpenCoordinator.cs:273-288`) are all verified in source. That focusing the popup's WebView2 actually deactivates the QuickFiler form is Win32/WinForms runtime behavior and remains unverified. Strongest candidate on both reproduction paths; on the search path (step 4) it is the only one of the three not already excluded by code. | +| 2 — native `ToolStripDropDown` auto-close -> `OnDropDownClosed` -> `FinishClose(Uncommitted)` | **Code path CONFIRMED; first-cause status INFERRED for gesture opens, LIKELY REFUTED for the search path** | `OnDropDownClosed` (`:426-437`) and the unconditional `_cancelSelection()` under `Uncommitted` (`:449-450`) are verified. For gesture opens `AutoClose == true` (`Open.cs:100` with `takeFocus == true`), so the framework auto-close is armed. For a search-driven open `AutoClose == false`, which disables the framework auto-dismiss, so this candidate cannot be the first cause of the step-4 click-without-select symptom as the issue states. `e.CloseReason` is currently discarded and is the value that settles this. | +| 3 — `QfcItemController.TextBoxSearch_Leave` | **Code path CONFIRMED; LIKELY NOT REACHED on either reproduction path** | The handler (`EventHandlers.cs:217-228`) and the Down-only latch (`:195`) are verified. On the arrow-click path the search textbox never holds focus, so no `Leave` occurs. On the search path `IsWebView2Focused` is false (the leaf is a `TextBox`), so `ParkFocusOffWebView2` is skipped and the synchronous `ActiveControl`-change route into `Leave` does not fire; and a click into the popup moves activation to a different top-level window rather than to another control on the same form, which does not raise `Leave`. The AC4 gap (no mouse-path latch) is real and must still be closed, but this candidate is not the first cause. | + +Recommended sequencing: land AC6 instrumentation first (it is a two-site, log-only change with the new-partial-file constraint noted in section 7), reproduce, read the ordering off file order in a single-thread log, then choose between the AC2 latch and the AC3 commit-before-cancel ordering as the primary fix. Both will very likely be needed; the log determines which one is the *first* cause and therefore which one carries the fail-before regression test for AC1. + +--- + +## Automation Feasibility + +Manual verification of this defect requires a live Outlook process, a real WebView2 surface, a real `ToolStripDropDown`, and human mouse and keyboard gestures. None of that is automatable in this repository: the test policy forbids external processes, and no window may be shown. + +### Automatable as MSTest / Moq / FluentAssertions unit tests + +| Requirement | Automatable | Seam | +|---|---|---| +| AC2 — cancel suppressed on self-inflicted deactivation, still fires on genuine deactivation | **Yes, fully** | `Mock<IQfcFormViewer>` + `Mock.Raise(x => x.FormDeactivated += null, ...)` + reflection-injected `_groups`, exactly as `QfcFormControllerDeactivateTests` already does. Both branches of the new seam are drivable. | +| AC3 — commit runs before any auto-close cancel | **Partially** | The *branching* is testable at the `BreadcrumbDropDownHost` seam using `PendingHostHarness`-style delegate counters: assert the cancel delegate is not invoked while a commit latch is set, and is invoked when it is not. What is **not** testable is producing a genuine framework `ToolStripDropDownClosedEventArgs` with `CloseReason == AppFocusChange`; a test can only invoke the handler with a constructed args value, which proves the branch, not the framework's choice of reason. | +| AC4 — mouse-path leave latch | **Yes, fully** | `QfcItemController` with a `Mock<IItemViewer>`; drive the mouse open path, then raise the search-box `Leave` and assert `SetFolderDroppedDown(false)` is not called. `IItemViewer.IsFolderDropDownOpen` and `SearchLeave` are already interface members. | +| AC5 — refresh while open does not close | **Yes, fully, and already covered** | `ReplaceItemsPreservingSession` emits no `OpenStateChanged` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`); existing UtilitiesCS.Test router suites pin this. A `Mock<IBreadcrumbDropDownHost>` assertion of `Close(It.IsAny<...>())` `Times.Never()` across a refresh is the regression guard at the viewer level. | +| AC6 — instrumentation exists at the two named sites | **Partially** | That a log statement exists can be pinned structurally (a source-text or reflection assertion, as the repository has done elsewhere for declaration-only seams). That the emitted *ordering* is what the fix assumes cannot be asserted without the live host. | +| AC1 — list stays open on gesture open | **Partially** | Every managed step is assertable: the open task resolves `true`, `host.IsOpen` stays `true`, no `Close` reaches the mocked host, and the session reports `IsSelectorOpen == true`. What is not assertable is that no *framework* close occurs, because no framework dropdown is shown. | + +### Requires a human, and the recommended response for each + +1. **The popup taking focus deactivates the QuickFiler form (candidate 1's premise).** Not automatable — Win32 activation on a live UI thread. *Recommended response:* AC6 instrumentation plus a one-time manual observation recorded as a runbook step and an evidence artifact under the feature's `evidence/` tree. Do not attempt a synthetic test; record the observation as the confirmation of the INFERRED premise. +2. **`ToolStripDropDownClosedEventArgs.CloseReason` produced by the framework for this gesture.** Not automatable. *Recommended response:* capture it in the AC6 log line and cite the log excerpt as evidence in the fix's spec. This converts an inference into an observation without adding a merge gate that cannot pass. +3. **A row click in the expanded WebView2 produces (or fails to produce) a `selectorActivate` message.** Not automatable — requires a real WebView2 and a real mouse gesture. *Recommended response:* extend the existing #438 runbook (`docs/features/archive/2026-08-07-quickfiler-search-keystroke-focus-steal-438/runbooks/verify-search-focus-retention.runbook.md`) with the arrow-click and row-click gestures, as the issue's Validation section already proposes, and record the result as a manual-verification evidence artifact. If the instrumentation shows the message never arrives, the HTML `click` -> `pointerdown` change becomes the AC3 fix and its own managed-side assertion (that a `pointerdown`-sourced `selectorActivate` commits) is unit-testable at the coordinator. +4. **WinForms modal menu mode retargeting keyboard while `AutoClose == true`.** Not automatable; this is the documented #680 residual. *Recommended response:* carry forward the #680 precedent — assert the managed precondition (`DropDown.AutoClose` at the moment `_showPopup` runs) rather than the framework consequence, and document the framework consequence as a manual check. +5. **Selecting a different QfcItem while the list is open (AC1's last clause).** Partially automatable at the collection-controller seam with mocks; the visual outcome is not. *Recommended response:* unit-test the managed close intent, manual-verify the visual outcome, and record both. + +General recommendation: follow the precedent set by #400 and #438 — a documented, maintainer-sanctioned manual-verification exception with a runbook and an evidence artifact, rather than a merge gate that no automated suite can satisfy. Every AC retains at least one automatable managed-seam assertion, so no acceptance criterion is left with manual verification as its only evidence. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md new file mode 100644 index 000000000..37c642040 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md @@ -0,0 +1,254 @@ +# Human-Exception Runbook — Confirm QuickFiler Folder Drop-Down Close Ordering (Issue #796, AC6) + +This runbook is a contract-conformant human-exception runbook per +`.claude/skills/human-exception-runbook/SKILL.md`. It discharges the single manual observation +recorded as a permitted `exception` for issue #796 acceptance criterion AC6. + +## Cue + +Act on this runbook when the orchestrator has recorded an `exception` response for the following +requirement: + +> AC6 requires the runtime ordering between `QfcFormController.ParkFocusAndCancelSelectors` and +> `BreadcrumbDropDownHost.OnDropDownClosed` to be confirmed before the fix is chosen. The +> instrumentation itself is fully automatable, but reading the resulting ordering requires a live +> Outlook process, a real WebView2 surface, a real `ToolStripDropDown`, and human mouse and keyboard +> gestures. The repository unit-test policy forbids external processes and shown windows, so no +> automated test can produce the observation. + +Execute the runbook after an engineer has landed the AC6 instrumentation at the two named sites and +produced a Debug build from it, and before the fix shape is chosen. The observation determines which +of three candidate close paths fires first, which in turn determines whether the AC2 self-inflicted +deactivation latch or the AC3 commit-before-cancel ordering carries the primary fix and the +fail-before regression test. + +This runbook is not a merge gate. It does not replace, relax, or substitute for AC6: AC6 requires the +instrumentation to exist and the ordering to be confirmed, and this runbook is the procedure by which +the confirmation is produced reliably. + +The three candidate close paths, as recorded in `issue.md` "Suspected Cause / Notes", are: + +1. `QfcFormController.ParkFocusAndCancelSelectors` cancelling every item's selector when the + QuickFiler form loses activation. +2. Native `ToolStripDropDown` auto-close reaching `BreadcrumbDropDownHost.OnDropDownClosed` and then + `FinishClose` with an `Uncommitted` reason. +3. `QfcItemController.TextBoxSearch_Leave` closing the drop-down when the search box loses focus. + +## Prerequisites + +- **An instrumented Debug build.** A build produced from branch + `bug/quickfiler-folder-dropdown-closes-on-open-796` after the AC6 instrumentation landed, with + output in the repository's `TaskMaster/bin/Debug` directory, registered as a VSTO add-in and loaded + by the Outlook profile you will use. The instrumentation must emit, at minimum: + - an entry line from `QfcFormController.ParkFocusAndCancelSelectors` + (`QuickFiler/Controllers/QfcFormController.Deactivate.cs:39-71`) recording the method name, + `WebView2Focused=`, an active-form indicator, `Groups=<count>`, and per item `ItemNumber=<n>` and + `SelectorWasOpen=<bool>`; + - an entry line from `BreadcrumbDropDownHost.OnDropDownClosed` + (`QuickFiler/Viewers/BreadcrumbDropDownHost.cs:426-437`) recording the method name, + `CloseReason=<e.CloseReason>`, `ProgrammaticClose=`, `OpenState=`, `AutoClose=`, `Disposed=`, and + `PendingClose=`, emitted at method entry **before** the guard returns, so a suppressed close is + still visible. +- **Optional third instrumentation site.** A log line at `QfcItemController.TextBoxSearch_Leave` + (`QuickFiler/Controllers/QfcItemController.EventHandlers.cs:217-228`) recording + `HandoffPending=` and `DropDownOpen=`. AC6 does not require it. If it is absent, candidate 3 can + only be assessed indirectly, by whether the observed ordering leaves room for a third close. +- **Classic Outlook for Windows**, with the QuickFiler add-in loaded. A VSTO add-in surfaces its + commands through a custom Ribbon tab or group; confirming that tab is present is the check that the + add-in loaded. +- **A mailbox with enough folders** that typing two or three letters into the QuickFiler folder + search box returns at least two rows in the expanded list. Identify the letter fragment before + starting. +- **Log file location and format.** The runtime log for a Debug build is written to the repository's + `TaskMaster/bin/Debug/logs/` directory, one file per day named `debug_<yyyy-MM-dd>.log` + (`TaskMaster/log4net.config:22-23`). Each line follows the conversion pattern + `%date [%thread] %-5level %logger [%property{NDC}] - %message%newline` + (`TaskMaster/log4net.config:30`), so every line carries a millisecond timestamp, the thread name, + the level, and the logger name. On the log observed on 2026-09-06 the Outlook UI thread appears as + `VSTA_Main`. +- **Logger names to filter on.** `QuickFiler.Controllers.QfcFormController` and + `QuickFiler.Viewers.BreadcrumbDropDownHost`, plus `QuickFiler.Controllers.QfcItemController` if the + optional third site was instrumented. +- **Expected absence before instrumentation lands.** `QuickFiler.Viewers.BreadcrumbDropDownHost` + emits nothing today, because that type declares no logger at all until the AC6 instrumentation adds + one. Seeing no lines from that logger in a pre-instrumentation log is expected and is not a failed + prerequisite. If lines from that logger are still absent after the instrumentation is supposed to + have landed, the running build is not the instrumented build; see Verification. +- **Read-only handling of logs.** Do not modify, move, truncate, or delete any existing log file. The + log is read, not edited. If a clean segment is wanted, close Outlook, record the current end of the + file, and reopen Outlook, so the new session's lines follow the recorded end point. +- **A text viewer able to open the file without locking it.** The appender uses + `FileAppender+MinimalLock` (`TaskMaster/log4net.config:21`), which does not hold the file open + between writes, so the file can be read while Outlook is running. Use a viewer that opens the file + read-only. + +## Step-by-step Instructions + +1. Confirm the build under test. Verify that the assemblies in the repository's + `TaskMaster/bin/Debug` directory were produced after the AC6 instrumentation was committed, and + that both instrumentation call sites named in Prerequisites are present in the source that + produced them. Record the commit SHA of the build. +2. Close any running Outlook process, so the session boundary in the log is unambiguous. +3. Record the baseline end of today's log. Open + `TaskMaster/bin/Debug/logs/debug_<yyyy-MM-dd>.log` read-only, and write down the timestamp and + text of the last line currently in the file (or the current line count). Do not edit, truncate, + move, or delete the file. If today's file does not exist yet, record that fact instead. +4. Start classic Outlook for Windows. Confirm the QuickFiler add-in loaded by locating its Ribbon tab + or group; a VSTO add-in exposes its commands through custom Ribbon tabs and groups. If the tab is + absent, the add-in did not load and the remaining steps will produce no evidence. +5. Select a mail item in the Inbox view and start QuickFiler from the Ribbon. +6. Wait until item loading has finished. Per-item load telemetry from + `QuickFiler.Controllers.QfcItemController` stops appearing in the log when loading completes; + waiting keeps those lines out of the gesture segment you will read. +7. Write down the current wall-clock time and the label "Gesture A" before performing the next step. + Repeat this labelling before each gesture, so the log segment for each gesture can be identified + later. +8. Gesture A — arrow click. On any item, single-click the drop-down arrow in the folder field. + Record what you observe: whether the list opens, whether it closes on its own, and approximately + how long it remained open. Perform no further gesture for at least three seconds. +9. Write down the current time and the label "Gesture B". +10. Gesture B — keyboard open. Click into the folder search box to place the caret, then press the + Down key once. Record what you observe, using the same three observations as step 8. Perform no + further gesture for at least three seconds. +11. Write down the current time and the label "Gesture C". +12. Gesture C — type, then click a row. Type the two-or-three-letter fragment identified in + Prerequisites into the search box. Confirm the list expands and stays open. Then click one row in + the expanded list with the mouse. Record whether the list closed and whether the folder field + changed to the clicked row's folder or still shows the previous selection. +13. Perform no unrelated gestures between steps 8 and 12. If any additional click, key press, or + window switch occurs, write it down with its time; it will appear in the log segment and must not + be mistaken for one of the three gestures. +14. Close QuickFiler and exit Outlook. +15. Open `TaskMaster/bin/Debug/logs/debug_<yyyy-MM-dd>.log` read-only and locate the first line that + follows the baseline recorded in step 3. Everything from that point forward is this session's + segment. +16. Within that segment, locate the lines whose logger name is + `QuickFiler.Controllers.QfcFormController` or `QuickFiler.Viewers.BreadcrumbDropDownHost`, plus + `QuickFiler.Controllers.QfcItemController` if the optional third site was instrumented. Use the + gesture times recorded in steps 7, 9 and 11 to split the segment into the three gestures. +17. Transcribe, separately for Gesture A, Gesture B and Gesture C, the matching lines **in the order + they appear in the file**. Do not reorder them, and do not sort or group them by timestamp. +18. Apply the decision rules in the Verification section to each gesture's transcript, and record for + each gesture which candidate was confirmed and which were refuted. +19. Write the evidence artifact. Create a file under + `docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/` + whose filename begins with an ISO-8601 `yyyy-MM-ddTHH-mm` timestamp, for example + `2026-09-07T09-15-dropdown-close-ordering-observation.md`. The artifact must contain: + - a `Timestamp:` field carrying the same ISO-8601 `yyyy-MM-ddTHH-mm` value; + - the commit SHA of the build recorded in step 1; + - the excerpted log lines for each of the three gestures, in file order; + - an explicit statement, per gesture, of which candidate was confirmed and which were refuted, + using the rules in Verification. +20. Redact before saving. In the excerpt, replace absolute host paths, user names, mailbox addresses, + and any folder name that identifies a person with a neutral placeholder (for example + `<repo-root>`, `<user>`, `<mailbox>`, `<folder-1>`). Keep the field names, the field values that + carry the decision (close reason, boolean flags, counts), and the line order intact. + +## Verification + +**Ordering is read from file order, not from timestamps.** Both instrumentation sites run on the +single Outlook UI thread, log4net appends events to the file in call order, and the entire flash +occurs inside a fraction of a second, so several of these lines can share a millisecond timestamp. +Comparing millisecond timestamps is therefore not a sound way to order them. The order of the lines +in the file is the ordering. Confirm before applying the rules below that all lines under +consideration carry the same thread name (`VSTA_Main` on the log observed on 2026-09-06); if they do +not, the same-thread premise does not hold for those lines and the ordering claim must be reported as +inconclusive rather than resolved. + +Apply the following rules separately to each gesture's transcript. + +**Candidate 1 — `QfcFormController.ParkFocusAndCancelSelectors` (form deactivation).** + +- Confirmed when a `ParkFocusAndCancelSelectors` entry line appears **before** any + `OnDropDownClosed` line, and that `OnDropDownClosed` line reports a close reason of `CloseCalled` + with the programmatic-close flag `True`. +- Refuted when `ParkFocusAndCancelSelectors` is never entered during the flash, or is entered but + reports `Groups=0` or zero cancels, or is entered strictly **after** `OnDropDownClosed`. + +**Candidate 2 — native `ToolStripDropDown` auto-close.** + +- Confirmed when an `OnDropDownClosed` line appears **first**, reporting a close reason of + `AppFocusChange` or `AppClicked`, with the programmatic-close flag `False`, the open-state flag + `True`, and the auto-close flag `True`. +- Refuted by a close reason of `CloseCalled`, or by an auto-close flag of `False` at the moment it + fires. + +**Candidate 3 — `QfcItemController.TextBoxSearch_Leave`.** + +- Confirmed when a `TextBoxSearch_Leave` line reporting a `False` handoff-pending flag and a `True` + drop-down-open flag immediately precedes the close. +- If the optional third instrumentation site was not added, record candidate 3 as not directly + observable in this run, and state whether the observed ordering leaves room for a third close. + +**Outcome to record.** For each gesture, state which single candidate fired first, and state the +status of the other two as confirmed, refuted, or not directly observable. The three gestures may +yield different answers; record each separately rather than generalising from one. + +**Inconclusive results and what they indicate.** + +- No lines from `QuickFiler.Viewers.BreadcrumbDropDownHost` anywhere in the segment: the running + build is not the instrumented build, or the instrumentation is not reached. Re-check step 1 and + repeat. Do not report an ordering. +- Lines from only one of the two required sites: the ordering cannot be established. Report which + site produced lines and which did not, and return the observation to the engineer. +- Lines present but on differing thread names: report as inconclusive, per the same-thread premise + above. +- Do not resolve an inconclusive result by inference. The purpose of this observation is to replace + an inference with a measurement. + +**Completion.** The runbook is complete when the evidence artifact described in step 19 exists under +the feature's `evidence/other/` directory, carries the `Timestamp:` field, contains the redacted log +excerpts in file order, and states which candidate was confirmed and which were refuted for each +gesture. + +## Source and Citation + +- Requirement origin and the three candidate close paths, the reproduction gestures, and AC1-AC6: + `docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/issue.md`, + sections "Steps to Reproduce" and "Suspected Cause / Notes" (repository file) — updated_at: + 2026-09-06. +- Decision rules for each candidate (confirm and refute conditions), the minimum log-line content, + and the file-order-not-timestamps ordering rule: + `docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/research/2026-09-06T21-30-quickfiler-folder-dropdown-close-ordering-research.md` + sections 4.1, 4.2 and 10 (repository file) — updated_at: 2026-09-06. +- Justification that the drop-down pipeline currently emits no log lines, and that + `QuickFiler.Viewers.BreadcrumbDropDownHost` produces zero lines because the type declares no + logger: same research artifact, section 8.3 — updated_at: 2026-09-06. +- Log file directory, per-day file name pattern, conversion pattern, and the `MinimalLock` locking + model: `TaskMaster/log4net.config` lines 21, 22-23 and 30 (repository file) — updated_at: + 2026-09-06. +- Instrumentation site 1 and the existing log4net field it uses: + `QuickFiler/Controllers/QfcFormController.Deactivate.cs:39-71` and + `QuickFiler/Controllers/QfcFormController.cs:21-23` (repository files) — updated_at: 2026-09-06. +- Instrumentation site 2 and the close completion point whose reason the rules read: + `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:426-437` (`OnDropDownClosed`) and `:439-455` + (`FinishClose`) (repository files) — updated_at: 2026-09-06. +- Optional third instrumentation site and its handoff latch: + `QuickFiler/Controllers/QfcItemController.EventHandlers.cs:217-228` and `:195` (repository files) — + updated_at: 2026-09-06. +- Third-party UI step (steps 4 and 5, locating the add-in's Ribbon tab or group in Outlook), sourced + web-second: Microsoft Learn, "Customize the UI for Office applications", section "Custom Ribbon + UI", which states that a VSTO Add-in can create its own Ribbon tabs and groups to give users access + to the solution's functionality, and lists Outlook among the supported applications. Source URL: + https://learn.microsoft.com/en-us/visualstudio/vsto/office-ui-customization — ms.date: 2017-02-02; + updated_at: 2026-04-24; retrieved 2026-09-06. +- Sourcing-rule note (MCP-first / web-second): no callable `mcp__*` documentation-retrieval tool is + wired in this repository, re-verified 2026-09-06, so the MCP-first clause could not be satisfied + for the third-party UI step above. `WebFetch` was used as the web-second mechanism. This limitation + is recorded in the two-axis-model-selection spec's Out of Scope section and is not resolved here. +- **Unsourced-step disclosure.** The Prerequisites mention verifying add-in load state through the + Outlook COM Add-ins dialog (File > Options > Add-ins) as an alternative to locating the Ribbon tab. + No external source was obtained for that navigation path: on 2026-09-06 the candidate Microsoft + Learn URLs `https://learn.microsoft.com/en-us/microsoft-365-apps/outlook/manage/view-manage-install-add-ins`, + `https://learn.microsoft.com/en-us/office/troubleshoot/outlook/determine-if-add-in-causing-problem` + and `https://learn.microsoft.com/en-us/visualstudio/vsto/how-to-install-and-uninstall-vsto-add-ins` + each returned HTTP 404, and the support.microsoft.com add-ins article retrieved on the same date + does not give the Outlook COM Add-ins navigation. That alternative is therefore presented as + unsourced; the sourced Ribbon-tab check in step 4 is the procedure's actual load check. +- Repository steps that are grounded in this repository's own code and configuration rather than a + vendor UI (steps 1-3 and 6-20) are sourced by the repository citations above and require no + external URL. +- Precedent for a maintainer-sanctioned manual-verification exception with a runbook and an evidence + artifact: + `docs/features/archive/2026-08-07-quickfiler-search-keystroke-focus-steal-438/runbooks/verify-search-focus-retention.runbook.md` + (repository file) — updated_at: 2026-08-08. diff --git a/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md new file mode 100644 index 000000000..aaaca0927 --- /dev/null +++ b/docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/spec.md @@ -0,0 +1,358 @@ +# 2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select (Spec) + +- **Issue:** #796 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-06 +- **Status:** Draft +- **Version:** 0.2 +- **Work Mode:** full-bug + +> Work-mode note: this feature runs in full-bug mode. This spec is the single authoritative acceptance-criteria source. No user-story.md exists for this feature and its absence is by design, not a gap. + +> Path convention in this document: a backticked path is a write claim consumed by downstream blast-radius derivation. Every backticked path in this document appears in the `## Write Set` section. All other file references — citations, prior-art references, runbooks, evidence directories, and files explicitly excluded from the diff — are written without backticks on purpose. + +## Context + +In the QuickFiler item view, opening the folder drop-down makes the list flash open and immediately close, whether opened by clicking the arrow or by pressing Down in the search box. Typing letters in the search box does expand the list and keep it open, but clicking an item in the expanded list closes it without selecting that item; only the Up and Down keys change the selection. The list should stay open until it is closed explicitly, an item is selected, or a different QfcItem is selected, and a click on an item should select it. + +Environment: +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in. The drop-down is not a ComboBox: it is a WebView2 breadcrumb page hosted by ItemViewer, plus a ToolStripDropDown popup hosting a second WebView2 owned by BreadcrumbDropDownHost. +- Build under test: debug build from TaskMaster/bin/Debug, HEAD c431dc32 (2026-09-06). +- Entry point: Outlook ribbon -> QuickFiler (ordinary and High Confidence). +- Data source: live mailbox, Inbox view. + +Severity: High. Mouse selection of a filing folder is not possible in the item view; the user must type a search string and navigate with the keyboard. + +Primary technical source: the verified research artifact at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/research/2026-09-06T21-30-quickfiler-folder-dropdown-close-ordering-research.md. Its line counts, its AutoClose finding, and its non-SDK-style project finding were independently re-verified. + +## Repro & Evidence + +Steps to reproduce: +1. Launch QuickFiler. On any item, click the drop-down arrow in the folder field. Observe: the list opens and closes within a fraction of a second. +2. Put the caret in the search box and press Down. Observe: the same flash. +3. Type two or three letters in the search box. Observe: the list expands with search results and stays open. +4. Click an item in the expanded list. Observe: the list closes and the folder field still shows the previous selection. +5. Use Up/Down keys instead. Observe: the selection changes as expected. + +Reproduces on every item; timing after item load does not matter (maintainer confirmed "it always occurs"). + +Expected: +- Opening the list by mouse or keyboard keeps it open until the user closes it (Escape, Left arrow, clicking the arrow again), an item is selected, or a different QfcItem is selected. +- Clicking an item in the open list selects that item and closes the list. +- A refresh of the row set while the list is open (search results, late suggestion decoration) does not close it. This is already guaranteed by #438 AC-3 and must remain so. + +Actual: +- Open-by-arrow and open-by-Down both close immediately. +- Click on a row closes the list and discards the selection. +- Keyboard selection works. + +Log evidence: +- Research section 8 examined TaskMaster/bin/Debug/logs/debug_2026-09-06.log (read only, outside the worktree). Zero log lines of any level are emitted by the QuickFiler item-view drop-down pipeline. In the fourth session the last load-time line is at 20:02:28,953 and the next line in the entire file is at 20:15:31,320, a thirteen-minute window of live interaction containing no log lines at all. +- The recurring WebView2BreadcrumbHost initialization errors in that log come from EfcFormController.InitializeBreadcrumbHostAsync, a different component (EmailFilerControl), and are tracked separately. They are not evidence for this defect. +- Conclusion: the close is a normal, non-exceptional code path that produced no diagnostic. Runtime instrumentation is the only way to establish the ordering, which is why AC6 exists. This conclusion is not contradicted by the evidence; it is discharged by it. The AC6 instrumentation has since landed at the two named sites, the runbook was executed against a Debug build carrying it, and the resulting transcript is recorded at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md. The two log-line observations above remain accurate statements about the pre-instrumentation log file they cite and are retained as such. + +## Scope & Non-Goals + +In scope: +- The OPEN and CLOSE lifecycle of the folder drop-down popup, on both the mouse (arrow-click) and keyboard (Down) gesture paths. +- The selection-commit ordering for a row click relative to any auto-close cancel. +- The self-inflicted-versus-genuine form-deactivation distinction consumed by the #677 cancel-on-deactivate handler. +- The #680 search-leave handoff latch, extended to the mouse open path. +- Debug instrumentation at the two sites named by AC6. + +Out of scope / non-goals: +- Row TEXT projection and the breadcrumb bridge router's render projection classes. A concurrent sibling item owns those. See the sibling-contention note in `## Write Set`. +- The EmailFilerControl breadcrumb (WebView2BreadcrumbHost / EfcFormController) initialization failures. +- Any redesign of the breadcrumb selection session model in UtilitiesCS. The AC5 regression guard is satisfied by leaving the session-preserving replacement path untouched, so the UtilitiesCS router and session files must stay out of the diff. + +Explicitly excluded systems and paths: no edits under the .claude tree, the .codex tree, or the .agents tree; no edits to the published JSON files under the config directory; no edits to any GitHub workflow file; no edits to the solution file or the repository-root build property files. + +## Root Cause Analysis + +All statements below were re-derived against the current worktree in research sections 1 through 3. Where a claim is inferred rather than verified, it is labelled. + +### Verified structure + +- The arrow is the `#dropDownButton` element in the breadcrumb page, which posts a `selectorToggle` message. The open pipeline is: bridge coordinator selector-message handling -> router `OpenSelector` -> session `OpenSelector` -> `Open` -> `SelectorOpenStateChanged` -> `HandleSelectorOpenStateChanged` -> host `OpenAsync` -> open-lifetime `OpenCoreAsync` -> `FocusCurrentSurface` -> `FocusPending`. +- The mouse toggle and the programmatic (keyboard) open share one request path. An existing test, SetFolderDroppedDownTrue_UsesSameOpenRequestAsMouseSelectorToggle in QuickFiler.Test/Viewers/BreadcrumbSelectorOpenRetryTests.cs, pins that equivalence. This is consistent with mouse and keyboard failing identically. +- `AutoClose` is not constant. The constructor sets it true, `FinishClose` restores it to true, and every show overwrites it with the gesture's focus intent: `ShowPopup` assigns `DropDown.AutoClose = takeFocus`. Gesture opens run with `takeFocus == true`; a search-driven open latches no-focus, so it runs with `takeFocus == false` and both the framework auto-dismiss and `FocusPending` are disabled. That asymmetry maps one-to-one onto the reported symptom asymmetry. +- Asynchronous suggestion decoration is not the cause. The session-preserving row replacement path preserves `IsOpen` and emits no `OpenStateChanged`. +- `FinishClose` is the single close completion point. Under reason `Uncommitted` it calls the cancel delegate unconditionally, while the focus restoration step is gated by a may-take-focus predicate. That asymmetry is already documented in an in-source comment. +- A deactivation-driven cancel closes the host with reason `ExplicitCommit`, so `FinishClose` does not cancel a second time; the session was already cancelled upstream. + +### The three candidate close paths + +Statuses below were settled by the AC6 manual observation and are recorded from the decision record at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/other/close-ordering-decision.md, which derives them from the observation artifact at evidence/other/2026-09-07T12-19-dropdown-close-ordering-observation.md. + +1. Form deactivation. `ParkFocusAndCancelSelectors` runs synchronously inside the WinForms Form.Deactivate event, parks the active control back into the form, then cancels every item controller's selector. There is no latch distinguishing a self-inflicted deactivation from a genuine one. Opening the popup calls Focus on the popup's own top-level window. Downstream code is CONFIRMED, and the first-cause status is now **OBSERVED**: the deactivation handler was entered before the drop-down close on every one of the three gestures, and each close reported `CloseReason=CloseCalled` with `ProgrammaticClose=True`. Cited decision-record lines: the three per-gesture first-cause lines, each reading CANDIDATE-1. One qualification is carried forward rather than dropped: that the deactivation is caused by the popup taking focus rests on its coincidence with the gesture, because the `Form.ActiveForm` discriminator that was to corroborate it read contrary to prediction. See the assumption bullet in `## Assumptions, Constraints, Dependencies`. +2. Native ToolStripDropDown auto-close, reaching `OnDropDownClosed` -> `FinishClose(Uncommitted)` -> cancel. The code path is CONFIRMED. Its first-cause status is now **REFUTED** on every gesture, keyboard and mouse alike, not only on the search path: every observed close reported `CloseReason=CloseCalled` with `ProgrammaticClose=True` rather than `AppFocusChange` or `AppClicked` with `ProgrammaticClose=False`, and on the row-click gesture `AutoClose=False` at the moment it fired, which is a second independent refutation condition. Cited decision-record lines: the three per-gesture first-cause lines, whose recorded refutation status for candidate 2 is REFUTED on each. The `ToolStripDropDownClosedEventArgs.CloseReason` value, previously discarded by the handler, is the value that settled this. +3. Search-box leave. The leave handler closes the drop-down; its #680 handoff latch is set only on the Down-arrow branch. The code path is CONFIRMED. Its first-cause status is **NOT DIRECTLY OBSERVABLE** in this run and is neither observed nor refuted: AC6 names only two instrumentation sites, the optional third site at the leave handler was not added, and a handler with no logging site emits nothing whether it runs or not, so the transcript's silence carries no information about it. The earlier source-derived argument that it is not reached on either reproduction path is additionally weakened, because that argument turns on which gesture has a WebView2 as the active leaf and the observed `WebView2Focused=` values reverse the prediction on both gestures: the arrow-click gesture reported False and the Down-key gesture reported True. The AC4 gap is real, is treated as open, and is closed on its own terms. Cited decision-record lines: the AC4 mechanism and new-member lines, together with the observed-reachability statement recorded beside them. + +### Why the row click does not select + +When the session is open, activation commits and ends the session with `ExplicitCommit`, so no cancel runs. When the session is already closed, activation still updates the committed identity. The reported symptom is that the selection does not change at all, which was hypothesised to be explained by the activation message never being produced: the row listener registers `click`, which fires on mouseup, and if the popup is dismissed on mousedown no mouseup and therefore no click reaches the page. + +That hypothesis is NOT settled by the AC6 evidence, contrary to what this section previously anticipated. AC6 instruments the deactivation handler and the drop-down close handler only. The activation message travels through the bridge coordinator and the router, neither of which is instrumented, so no activation would have produced a log line whether one was posted or not and the transcript's silence discriminates nothing. Cited decision-record line: the AC3 HTML pointer-down line, recorded as NOT REQUIRED precisely because the admissibility condition for the alternative — a transcript showing that no activation message was produced — cannot be met by this run. + +What the evidence does supply is a different and sufficient account of the same symptom. On the row-click gesture the deactivation cancel ran first and cancelled the open selector before the close arrived, so the session was already cancelled when the row was clicked. That path is the one AC2 closes. If a row click still fails to select once the AC2 seam lands, the mousedown-dismissal hypothesis is reopened, and settling it then requires instrumenting the activation path rather than re-reading this transcript. + +### Correction to a citation carried from the issue + +The issue attributes the click-without-select symptom to candidate 2. That attribution is now REFUTED rather than merely doubted, and this paragraph no longer defers the question: the AC6 evidence has been produced and it settles it. On the row-click gesture the close reported `CloseReason=CloseCalled` with `ProgrammaticClose=True`, and `AutoClose=False` at the moment it fired, each of which independently refutes candidate 2; the deactivation handler had already been entered and had already cancelled the open selector before that close arrived. The click-without-select symptom is attributable to candidate 1. Cited decision-record line: the Gesture C first-cause line, reading CANDIDATE-1, whose recorded refutation status for candidate 2 is REFUTED on two grounds. + +## Proposed Fix + +### Invariant + +A close of the folder drop-down cancels the pending selection if and only if the close was not caused by this add-in's own activation or focus movement and no selection commit is in flight. Every other close either commits or leaves the committed selection untouched. + +### Ordering constraint imposed by AC6 (mandatory) + +The FIRST implementation step is instrumentation, not a fix. It adds debug log lines to `ParkFocusAndCancelSelectors` and to `OnDropDownClosed`. The reproduction is then run and the ordering is read off the log. The choice among the three candidate close paths is made FROM that evidence. + +No fix that presupposes the Win32 activation ordering may be written before that evidence exists. That evidence now exists: the instrumentation landed, the runbook was executed, and the ordering was read off the log. The premise that focusing the popup's WebView2 deactivates the QuickFiler form is therefore no longer inferred — the deactivation handler is OBSERVED to run, ahead of the close, on all three gestures. The constraint above is retained as the historical record of why the instrumentation preceded the fix; it is discharged, not relaxed. + +Log-line requirements, so the evidence is actually discriminating: +- Both sites run on the single Outlook UI thread and log4net appends in call order, so FILE ORDER is the ordering. Millisecond timestamps are not sufficient: the entire flash occurs inside a fraction of a second and several steps can share a millisecond. Any analysis must rely on file order. +- `ParkFocusAndCancelSelectors` entry line: method name, WebView2Focused, whether the active form is null or is this form, group count; and per item, the item number and whether that item's selector was open. +- `OnDropDownClosed` entry line: method name, CloseReason (from the event args, which are presently discarded), programmatic-close flag, open state, AutoClose, disposed flag, pending-close flag. Log at entry, BEFORE the guard returns, so a suppressed close is still visible. +- A third temporary line at the search-leave handler is recommended although AC6 does not require it; without it candidate 3 can only be excluded indirectly. +- Logging shape must match the existing repository conventions: an interpolated string with a sentence prefix followed by Key=Value pairs. The field name differs by neighbourhood: controllers use `logger`, viewers use `log`. + +Discriminators, stated in advance so the evidence read is not post-hoc: +- Candidate 1 is confirmed if a `ParkFocusAndCancelSelectors` line precedes any `OnDropDownClosed` line and the subsequent close reports CloseReason=CloseCalled with the programmatic-close flag set. It is refuted if that method is never entered during the flash, or reports zero groups or zero cancels, or is entered strictly after the close. +- Candidate 2 is confirmed if an `OnDropDownClosed` line appears first with CloseReason of AppFocusChange or AppClicked, programmatic-close false, open state true, AutoClose true. It is refuted by CloseReason=CloseCalled, by the line not appearing first, or by AutoClose false at that moment. +- Candidate 3 is confirmed by a leave line reporting handoff-pending false with the drop-down open immediately preceding the close, and refuted by the absence of that line or by handoff-pending true. + +An additional low-cost discriminator: log whether the active form is null at deactivation entry. A ToolStripDropDown is not a Form, so a null active form is evidence of a self-inflicted deactivation and a non-null active form naming a foreign window is evidence of a genuine one. That framework behavior is asserted from background knowledge and is not verified in this repository, so it is corroborating rather than decisive. + +### Design summary — what changes where + +Contingent on the AC6 evidence. The shape below is the planned response; the branch actually taken is selected from the log. + +- AC2, self-inflicted deactivation seam. Add a boolean intent member to the form-viewer interface, implemented in the concrete form viewer as the only site that reads non-injectable activation state, and consumed by the deactivate handler to gate the cancel loop. Polarity: false means genuine (not self-inflicted). See the test-strategy section for why that polarity is load-bearing. An alternative with a smaller interface footprint is a predicate property assigned by the same wiring that assigns the existing may-take-focus predicate; the interface-member form is preferred because the deactivate suite already mocks the interface and would need no new construction seam. +- AC3, commit-before-cancel ordering. Add a pending-commit latch consulted by `FinishClose` so an uncommitted-reason close does not cancel while a commit is in flight. If the row-activation message is shown by the evidence never to be produced, the additional change is to move the row activation listener from `click` (mouseup) to a pointer-down event so the activation message is produced before dismissal. +- AC4, mouse-path latch. Extend the #680 handoff latch so it is also set on the mouse open path, not only the Down-arrow branch. +- AC6, instrumentation. Host-side instrumentation goes into a new partial part, for the file-size reason recorded below. +- AC1 and AC5 are outcomes of the above plus the untouched session-preserving replacement path; neither introduces its own production change beyond what AC2, AC3 and AC4 deliver. + +### Boundaries and invariants to preserve + +- The #677 contract: a genuine deactivation of the QuickFiler form still cancels every item controller's selector. +- The #438 AC-3 contract: a row-set refresh while open does not close the list. This is preserved by leaving the session-preserving replacement path in UtilitiesCS out of the diff entirely. +- The #680 contract: the search-leave handoff continues to work on the Down-arrow path. +- Cancel suppression must be SCOPED. A close with no commit in flight and no self-inflicted activation must still cancel. See the test-strategy section, where two existing assertions are kept unchanged specifically to prove the suppression is scoped rather than global. +- The reflection-based constructor binding on the drop-down host must not be disturbed. Any new host state is a settable internal property assigned after construction, matching the existing may-take-focus precedent, not a constructor-arity change. +- The may-take-focus predicate default must not be changed. + +### File-size constraints (independently verified) + +- QuickFiler/Viewers/BreadcrumbDropDownHost.cs is 498 lines against the repository's 500-line ceiling and declares no logger of any kind. AC6's host-side instrumentation cannot be added to it. It requires the new partial part `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs`, following the existing partial-split precedent set by `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs`. Any net line growth in the main part must be offset by moving `OnDropDownClosed`, or by having it delegate to a diagnostics helper declared in the new part. +- QuickFiler.Test/Viewers/BreadcrumbDropDownHostTests.cs is 499 lines and QuickFiler.Test/Viewers/BreadcrumbDropDownIntegrationTests.cs is 500 lines. Neither can absorb new host-level tests, so a new test file is required. +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` is 456 lines. Growth there is constrained; keep the AC2 wiring change to the minimum. +- QuickFiler.Test/Controllers/QfcItemController.EventHandlersTests.cs is 477 lines, leaving 23 lines of headroom. That is not enough for an Arrange-Act-Assert pair with doc comments, so the AC4 tests go in a new file. + +### Correlation identifier for the AC6 evidence + +No identifier exists today that both instrumentation sites can carry. The form-controller side can log the item number, which already exists on the item-controller interface. The host side knows only its anchor and its own instance, and the anchor name is a Designer constant identical for every item viewer, so it does not discriminate between items. If per-item correlation proves necessary when the log is read, use an ordinal surrogate logged at both ends plus a tying line, or a settable internal owner-descriptor property on the host assigned right after construction. Do not change the host constructor arity. + +### Error handling and logging updates + +- Instrumentation is Debug level and must not change control flow. The `OnDropDownClosed` line is emitted at entry, before any guard return. +- The existing per-item boundary catch with error logging in the deactivate handler is preserved. +- No new exception is introduced by the instrumentation step. + +### Rollback considerations + +The instrumentation step is log-only and independently revertable. The AC2, AC3 and AC4 changes are each independently revertable and each carries its own regression test. + +## Assumptions, Constraints, Dependencies + +- Assumption (SETTLED by AC6, with a split verdict): focusing the popup's WebView2 deactivates the QuickFiler form on this thread, and the active form is null at that moment. The first half is OBSERVED — the form's deactivation handler was entered during each of the three gestures, before the drop-down close. The second half is REFUTED — all three gestures reported `ActiveFormNull=False`, and the one observed deactivation the observation attributes to focus moving away from the form reported `ActiveFormNull=True`, so the values run opposite to the predicted direction on all four observations. The corollary is that the AC2 seam cannot derive self-inflicted-versus-genuine from `Form.ActiveForm` and must be supplied an explicitly assigned state. Cited decision-record line: the AC2 item-viewer wiring line, reading REQUIRED. +- Assumption (UNSETTLED, and not settleable from the AC6 evidence): a row click may be dismissed on mousedown so no click event and therefore no activation message reaches the page. AC6 instruments the deactivation handler and the drop-down close handler only; the activation path is not instrumented, so the run produces no signal either way and the absence of an activation line is uninformative. The assumption is neither relied upon nor discarded, and no change to the breadcrumb page is made on the strength of it. Cited decision-record line: the AC3 HTML pointer-down line, reading NOT REQUIRED. +- Constraint: `QuickFiler/QuickFiler.csproj` and `QuickFiler.Test/QuickFiler.Test.csproj` are non-SDK-style. Both declare `<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">` (verified). Every added .cs file requires an explicit `<Compile Include>` entry or it is silently not compiled. +- Constraint: 500-line ceiling per file. See the file-size section. +- Constraint: tests are MSTest with Moq and FluentAssertions. No temporary files, no live Outlook process, no shown window. +- Dependency: the AC6 evidence must exist before the fix branch is chosen. +- Dependency: the manual-verification runbook at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md is authored in parallel with this spec. + +## Data / API / Config Impact + +- User-facing behavior change: the folder drop-down stays open on gesture opens and a row click selects. No API, schema, config key, or CLI surface changes. +- Interface change: one added member on the form-viewer interface (AC2). This is an internal add-in interface with in-repo implementations and mocks only; all implementors are updated in the same diff. +- Logging: new Debug-level lines at two production sites (AC6). No telemetry, no persisted data, no new log destination. The existing log4net configuration is unchanged. +- Backward compatibility: no persisted state, no serialized contract, and no cross-assembly public API is affected. + +## Write Set + +Every path below is a concrete file the fix's diff will create or modify. Paths are repository-relative, use forward slashes, and contain no spaces. Seventeen paths. + +Two separate questions about an additional path have been settled at different times, and they are recorded separately here so that neither is read as the other. + +First, and now closed with no path added: preflight round 1 proposed QuickFiler/Interfaces/IQfcItemController.cs, to carry the per-item selector-open state the AC6 diagnostic reports. That path is written here without backticks on purpose, because it is not a write claim. That proposal was adopted and then withdrawn after the planner established that adding a member to that interface breaks a compiled hand-written implementor outside the write set, on a target framework with no default interface members. The adopted resolution reaches the same value through an internal member on the concrete item controller, whose file is already in the write set, so that proposal added no path and no interface changes. + +Second, and the reason the count is now seventeen rather than sixteen: `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` was added after implementation of AC4 surfaced a pre-existing test in that file which asserts the behaviour AC4 deliberately changes. This is a write-set derivation oversight and NOT merge damage: `git log --follow` places the file at commit 660793e5, the original issue #680 fix, which predates the fix branch and both merges of origin/main into it. The write set was derived from the files the fix would edit, and it did not include a pre-existing test asserting the behaviour an acceptance criterion deliberately changes. The path contains no whitespace, so it is expressible as a blast-radius write claim and is backticked here as one; that is the point on which it differs from QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs, the candidate path recorded in the exclusion paragraph below as rejected because its name contains a space and blast-radius derivation splits on whitespace. The deliberate update to that file is specified under `## Test Strategy` below. + +### Production — modify + +- `QuickFiler/Controllers/QfcFormController.Deactivate.cs` — modify — AC6 instrumentation of `ParkFocusAndCancelSelectors`, and the AC2 self-inflicted guard around the cancel loop. +- `QuickFiler/Interfaces/IQfcFormViewer.cs` — modify — AC2: declare the self-inflicted-deactivation seam. +- `QuickFiler/Viewers/QfcFormViewer.cs` — modify — AC2: production implementation of the seam; the only form-side site that reads non-injectable activation state. +- `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` — modify — AC3: commit-before-cancel ordering in `FinishClose`; move or delegate `OnDropDownClosed` into the new diagnostics part to stay under the 500-line ceiling (currently 498). +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` — modify — AC1 and AC3: the `AutoClose = takeFocus` policy in `ShowPopup`, and any pending-commit latch set at open time. +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` — modify — AC2: assign the new popup-owns-activation state alongside the existing may-take-focus assignment. +- `QuickFiler/Controllers/QfcItemController.EventHandlers.cs` — modify — AC4: extend the #680 leave-handoff latch to the mouse open path. Also AC6: expose this item's selector-open state to the per-item deactivation diagnostic as an internal get-only member forwarding to the item viewer's existing `IsFolderDropDownOpen`. The expression `_itemViewer.IsFolderDropDownOpen` is already used in this file at lines 200 and 225, so no new dependency is introduced. +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` — modify — AC3: only if the commit-before-cancel ordering must be enforced at the coordinator rather than at the host; the AC6 evidence decides. +- `QuickFiler/Resources/FolderBreadcrumb.html` — modify — AC3: move the row activation listener at lines 289-291 from `click` (which fires on mouseup) to a pointer-down event so the activation message is produced before dismissal. SIBLING CONTENTION: a concurrent sibling item edits this same page for the row TEXT projection. This item owns only the row activation listener and touches no projection logic. The contention is recorded here rather than avoided by dropping the file. + +### Production — create + +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs` — create — AC6 host-side instrumentation of `OnDropDownClosed` plus the log4net field. Required because the main part is 498/500 lines and declares no logger; follows the existing `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` partial-split precedent. + +### Test — modify + +- `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs` — modify — AC2: deliberate update of the deactivate contract plus the new self-inflicted negative test. 248 lines, ample room. +- `QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs` — modify — AC1 and AC3: keep all five existing tests, add the pending-commit-versus-native-close guard. 380 lines. +- `QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs` — modify — AC4: deliberate update of `TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent`, the pre-existing issue #680 test that asserts a leave dismisses a drop-down regardless of which gesture opened it. That is precisely the state AC4 requires to stop being dismissed. The method name and its `Times.Once()` assertion are kept; one Arrange line establishing a search-driven open is added; the other five `[TestMethod]` members in the class are unchanged. + +### Test — create + +- `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs` — create — AC3: commit-before-cancel ordering at the host seam. The two adjacent host-level suites are at 499 and 500 lines and have no room. +- `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs` — create — AC4: mouse-path leave latch. The existing event-handler suite has only 23 lines of headroom. + +### Compile entries — modify + +- `QuickFiler/QuickFiler.csproj` — modify — non-SDK-style project (verified `<Project ToolsVersion="15.0" ...>`); add the `<Compile Include>` entry for the new diagnostics part or it is silently not compiled. +- `QuickFiler.Test/QuickFiler.Test.csproj` — modify — non-SDK-style project (verified); add the `<Compile Include>` entries for both new test files. + +### Explicitly not in the write set + +These files were considered and no change is expected, so they are named without backticks on purpose: QuickFiler/Interfaces/IQfcItemController.cs, QuickFiler.Test/Helper Classes/QfcThemeHelperTests.cs, QuickFiler/Viewers/IItemViewer.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs, UtilitiesCS/UtilitiesCS.csproj, UtilitiesCS.Test/UtilitiesCS.Test.csproj, QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs, QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs, QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.Focus.cs, QuickFiler/Viewers/IBreadcrumbDropDownHost.cs. The AC5 regression guard is satisfied by leaving the session-preserving replacement path untouched, which is why the UtilitiesCS router and session files must stay out of the diff. + +## Test Strategy + +Framework: MSTest, with Moq for mocking and FluentAssertions for assertions. No temporary files, no external processes, no live Outlook, no shown window. + +### Existing tests that pin the current contract + +Each of the three files below is deliberately UPDATED, not weakened and not deleted. + +**QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs — method `FormDeactivated_CancelsSelectorOnEveryItemController` at line 172.** + +This method currently pins an unconditional cancel-on-deactivate for every item controller, asserting `Times.Once()` on two injected controllers. Under AC2 the cancel becomes conditional. The deliberate update is: + +1. Keep the method at its current name and its current `Times.Once()` assertions on both controllers, so the #677 contract remains visibly pinned. Add one Arrange line setting the new seam to report a GENUINE deactivation, and amend the doc comment. +2. Polarity choice, load-bearing: false means genuine (not self-inflicted). Moq's default `bool` return is false, so this keeps the existing Arrange block valid without modification and holds the diff on this method to a doc-comment amendment plus one explicit line. Do not invert the polarity. +3. Add a sibling test, for example `FormDeactivated_SelfInflictedByOwnPopup_DoesNotCancelAnySelector`, that sets the seam to report self-inflicted and asserts `Times.Never()` on both controllers. This is the AC2 fail-before test. +4. `FormDeactivated_WebView2Focused_ParksFocusOnce` at line 134 stays unchanged only if the fix keeps focus parking unconditional. If parking is also suppressed for a self-inflicted deactivation, that test needs the same explicit genuine-case Arrange line plus a paired negative. Decide this explicitly; do not let it change by default. + +The other four methods in the file are unaffected. At 248 lines the file absorbs both additions under the ceiling. + +**QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs.** + +The two tests the issue cites at lines 124 and 143 assert close IDEMPOTENCY — that exactly one `Close` of the expected reason reaches a mocked host when a close intent arrives while the open task is unresolved. They do not encode cancel-versus-commit precedence. Under AC1 they remain correct as written and the deliberate update for them is NO CHANGE. + +The assertions that actually encode cancel precedence are the literal `CancelCount.Should().Be(1)` assertions at lines 48 and 79, in `CloseWhileFactoryPending_InvalidatesOpenAndRepeatedCloseIsIdempotent` and `CloseWhileReadinessPending_RejectsLateReadyAttachShowAndFocus`. In both tests no commit is in flight, so the correct post-fix value remains 1. Both assertions are KEPT UNCHANGED and serve as the guard proving that any commit-time cancel suppression is SCOPED rather than global. A fix that drives either of them to zero is a design signal that the suppression is too broad; it is not a test to update. + +The `FocusAnchorCount` assertions at lines 49, 80 and 114 count the focus-anchor delegate and stay at 1 because the harness leaves the may-take-focus predicate at its permissive default. Do not change that default. + +The added test is the AC3 fail-before guard: a native-reason close arriving while an activation commit is pending must not cancel. At 380 lines the file has room for one or two added tests; a third harness would require a new file. + +**QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs — method `TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent` at line 74.** + +This method arranges an open drop-down with no search-driven open and asserts that the search box's `Leave` routes exactly one close intent. That arrangement is exactly the mouse-driven-open state AC4 requires to stop being dismissed, so the method pins the behaviour AC4 deliberately changes. Narrowing the AC4 fix to keep it green would mean not delivering AC4, and deleting or weakening the method is not available either. The deliberate update is: + +1. Keep the method at its current name and keep its `Times.Once()` assertion on `SetFolderDroppedDown(false)`, so the issue #680 dismissal-ownership contract stays visibly pinned for the case that still holds. +2. Add exactly one Arrange line establishing a search-driven open, so the search box owns the dismissal before the leave is raised. +3. Amend the method's doc comment to state the search-ownership condition. +4. The other five `[TestMethod]` members in the class stay unchanged. The class's `[TestMethod]` count stays at 6. + +The mouse-driven case that this method no longer covers is covered by `SearchLeaveAfterMouseDrivenOpen_DoesNotCloseDropDown` in `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs`, which asserts `Times.Never()` for it, so no coverage is lost by the update. + +### New tests by acceptance criterion + +- AC1: at the managed seam, assert the open task resolves true, the host reports open, no `Close` reaches the mocked host, and the session reports the selector open, across the gesture open path. New assertions in `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs`. +- AC2: both branches of the new seam, in `QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs`, using the existing mocked interface, `Mock.Raise` for the deactivation event, and the existing reflection-injected group fan-out. +- AC3: in `QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs`, drive the host with delegate counters in the style of the existing pending-open harness and assert the cancel delegate is not invoked while a commit latch is set and IS invoked when it is not. If the pointer-down change is taken, add a coordinator-level assertion that a pointer-down-sourced activation commits. +- AC4: in `QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs`, with a mocked item viewer, drive the mouse open path, raise the search-box leave, and assert the drop-down is not closed. +- AC5: assert `Close` is never invoked on a mocked host across a row-set refresh while open. The underlying replacement path already emits no open-state change and existing router suites pin that. +- AC6: pin structurally that a log statement exists at each of the two named sites, in the manner the repository already uses for declaration-only seams. + +### Automation feasibility, carried from research + +Every acceptance criterion retains at least one automatable managed-seam assertion. No criterion is left with manual verification as its only evidence. + +| AC | Automatable at a managed seam | The part that is not automatable | +|---|---|---| +| AC1 | Partially. Open task resolves true, host stays open, no `Close` reaches the mocked host, session reports open. | That no FRAMEWORK close occurs, because no framework dropdown is shown. | +| AC2 | Yes, fully. Both branches of the new seam are drivable with the existing mock and reflection-injected groups. | Whether the popup taking focus actually raises Form.Deactivate on this thread. | +| AC3 | Partially. The branching is testable with delegate counters: cancel suppressed while the commit latch is set, invoked when it is not. | Producing a genuine framework closed-event argument with a framework-chosen CloseReason. A test can only hand the handler a constructed value, which proves the branch, not the framework's choice. | +| AC4 | Yes, fully. Mocked item viewer; drive the mouse open path and raise leave. | None material. | +| AC5 | Yes, fully, and largely already covered by existing router suites. | None material. | +| AC6 | Partially. That the log statements exist can be pinned structurally. | That the emitted ORDERING is what the fix assumes; that requires the live host. | + +### Manual verification (permitted exception, not a merge gate) + +Confirming the runtime ordering requires a live Outlook process, a real WebView2 surface, a real ToolStripDropDown, and human mouse and keyboard gestures. None of that is automatable in this repository: the test policy forbids external processes and no window may be shown. The orchestrator has recorded this as a permitted human-interaction exception, following the precedent set by #400 and #438. + +Runbook: docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/runbooks/confirm-dropdown-close-ordering.runbook.md + +Manual steps to perform and record: +1. Run the AC6-instrumented build and reproduce the arrow-click flash; capture the log excerpt showing file order across the two instrumented sites. +2. Reproduce the Down-arrow flash; capture the same. +3. Reproduce the type-then-click case; capture the same, and specifically capture whether an activation message is produced. +4. Refresh the row set while open and confirm the list does not close. +5. Select a different QfcItem while the list is open and confirm the list closes. + +The manual result is captured as an evidence artifact under the feature's canonical evidence tree at docs/features/active/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select-796/evidence/. This manual step is NOT a merge gate. It converts the INFERRED premise into an observation. It does not weaken AC6 and does not substitute for the automated assertions listed above. + +### Coverage + +The repository line-coverage floor applies to the changed production files. New tests must cover the changed lines in the deactivate handler, the host close path, and the item-controller leave latch. The instrumentation partial part contains logging only; its coverage contribution is incidental and must not be used to inflate the figure for the behavioral changes. + +### Toolchain (run in this exact order; restart from step 1 on any failure or auto-fix) + +1. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) +2. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +4. `vstest.console.exe <test-assembly-paths> /EnableCodeCoverage` + +Use the Rebuild target, never Build: MSBuild's up-to-date check does not invalidate on a command-line property change, so a warm Build returns exit 0 with compilation skipped and no analyzer or nullable diagnostic is produced. Do NOT add the solution-wide `/p:Nullable=enable` property; it is deliberately absent from CI and produces hundreds of errors against files that never opted in. + +## Acceptance Criteria + +- [x] AC1: Opening the list by arrow click or by Down in the search box leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem. +- [x] AC2: A deactivation of the QuickFiler form caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (the #677 contract is preserved for genuine deactivation). +- [ ] AC3: A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs. +- [x] AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. +- [x] AC5: Row-set refreshes while open (search, late decoration) continue not to close the list (#438 AC-3 regression guard). +- [x] AC6: The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen. + +## Verification Conditions + +These are supporting conditions, not acceptance criteria. They are tracked here so nothing is added to the acceptance-criteria section above. + +- The full toolchain completes in a single clean pass in the order given, with the Rebuild target and without the solution-wide nullable property. +- Both new .cs files have explicit `<Compile Include>` entries in their non-SDK-style project files; verified by confirming the new tests actually execute rather than silently not compiling. +- No file in the write set exceeds 500 lines after the change, with particular attention to the host main part (498 before), the breadcrumb item-viewer part (456 before), and the pending-open test suite (380 before). +- The two `CancelCount` assertions at lines 48 and 79 of the pending-open suite still pass unchanged. +- The AC6 log excerpt is captured as an evidence artifact under the feature's evidence tree before the fix branch is chosen, and the spec's candidate status table is updated from INFERRED to OBSERVED or REFUTED accordingly. +- No file under the .claude, .codex or .agents trees, no published config JSON, no workflow file, no solution file, and no repository-root build property file appears in the diff. + +## Risks & Mitigations + +- Risk: the AC6 evidence contradicts the candidate ranking and the planned AC2 or AC3 change is not the first cause. Mitigation: the ordering constraint makes the instrumentation the first step precisely so the fix is chosen from evidence; the spec commits to no fix that presupposes the inferred Win32 ordering. +- Risk: a commit-time cancel suppression is written too broadly and silently disables the legitimate cancel path. Mitigation: the two `CancelCount` assertions at lines 48 and 79 of the pending-open suite are kept unchanged as the scoping guard; either dropping to zero is a design signal. +- Risk: shared-file contention on the breadcrumb HTML page with the concurrent sibling item. Mitigation: the file is declared in the write set with a contention note so the scheduler serializes rather than silently interleaves; this item's change is confined to the row activation listener. +- Risk: a new .cs file is added without a project compile entry and its tests silently do not exist. Mitigation: both project files are in the write set and the verification conditions require confirming the new tests actually execute. +- Risk: instrumentation exceeds the 500-line ceiling on the host. Mitigation: the new partial diagnostics part, following the established partial-split precedent. +- Risk: the manual verification is treated as a merge gate that no automated suite can satisfy. Mitigation: it is explicitly recorded as a permitted exception with a runbook, and every acceptance criterion retains an automatable managed-seam assertion. + +## Rollout & Follow-up + +- Rollout: single branch `bug/quickfiler-folder-dropdown-closes-on-open-796`, one pull request against main. No configuration change, no migration, no feature flag. +- The instrumentation added for AC6 is retained at Debug level after the fix lands; it is the only diagnostic in this pipeline and its absence is what made this defect unobservable. +- Post-fix follow-up: if the AC6 evidence refutes candidate 3 as expected but the mouse-path latch gap remains, AC4 is still delivered in this change; no separate issue is required. +- Links: issue #796 at https://github.com/drmoisan/TaskMaster/issues/796; the research artifact and the runbook cited above; related prior work #438, #677, #680. diff --git a/docs/features/potential/promoted/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select.md b/docs/features/potential/promoted/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select.md new file mode 100644 index 000000000..487e22c80 --- /dev/null +++ b/docs/features/potential/promoted/2026-09-06-quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select.md @@ -0,0 +1,93 @@ +# quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select (Issue #796) + +- Date captured: 2026-09-06 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select/ (Issue #796) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #796 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/796 +- Last Updated: 2026-09-07 +## Summary + +In the QuickFiler item view, opening the folder drop-down makes the list flash open and immediately close, whether opened by clicking the arrow or by pressing Down in the search box. Typing letters in the search box does expand the list and keep it open, but clicking an item in the expanded list closes it without selecting that item; only the Up and Down keys change the selection. The list should stay open until it is closed explicitly, an item is selected, or a different QfcItem is selected, and a click on an item should select it. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in; the drop-down is a WebView2 breadcrumb (`QuickFiler\Resources\FolderBreadcrumb.html`) in `ItemViewer` plus a `ToolStripDropDown` popup hosting a second WebView2 (`BreadcrumbDropDownHost`); debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) +- Command/flags used: Outlook ribbon -> QuickFiler (ordinary and High Confidence) +- Data source or fixture: live mailbox, Inbox view + +## Steps to Reproduce + +1. Launch QuickFiler. On any item, click the drop-down arrow in the folder field. Observe: the list opens and closes within a fraction of a second. +2. Put the caret in the search box and press Down. Observe: the same flash. +3. Type two or three letters in the search box. Observe: the list expands with search results and stays open. +4. Click an item in the expanded list. Observe: the list closes and the folder field still shows the previous selection. +5. Use Up/Down keys instead. Observe: the selection changes as expected. + +Reproduces on every item; timing after item load does not matter (maintainer confirmed "it always occurs"). + +## Expected Behavior + +- Opening the list by mouse or keyboard keeps it open until: the user closes it (Escape, Left arrow, clicking the arrow again), an item is selected, or a different QfcItem is selected. +- Clicking an item in the open list selects that item and closes the list. +- A refresh of the row set while the list is open (search results, late suggestion decoration) does not close it (already guaranteed by #438 AC-3 and must remain so). + +## Actual Behavior + +- Open-by-arrow and open-by-Down both close immediately. +- Click on a row closes the list and discards the selection. +- Keyboard selection works. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- No ERROR/WARN lines are logged for this behavior; the close is a normal code path. Runtime instrumentation is required to confirm which of the two candidate close paths fires first (see Suspected Cause). + +## Impact / Severity + +- [ ] Blocker +- [x] High +- [ ] Medium +- [ ] Low + +Mouse selection of a filing folder is not possible in the item view; the user must type a search string and navigate with the keyboard. + +## Suspected Cause / Notes + +Confirmed by code read (2026-09-06): + +- The drop-down is not a ComboBox. The arrow is `#dropDownButton` in `FolderBreadcrumb.html:440-442`, which posts `selectorToggle`. The open pipeline is `BreadcrumbBridgeCoordinator.HandleSelectorMessage` (`:349-358`) -> `FolderBreadcrumbBridgeRouter.OpenSelector` -> `BreadcrumbSelectionSession.Open` -> `SelectorOpenStateChanged` -> `BreadcrumbDropDownOpenCoordinator.HandleSelectorOpenStateChanged` (`:178-191`) -> `BreadcrumbDropDownHost.OpenAsync` -> `BreadcrumbDropDownOpenLifetime.OpenCoreAsync` (`:215-256`) -> `FocusCurrentSurface` (`BreadcrumbDropDownOpenLifetime.Focus.cs:32-51`) -> `_host.FocusPending()`. The popup is a `ToolStripDropDown` with `AutoClose = true` (`BreadcrumbDropDownHost.cs:165-172`, `BreadcrumbDropDownHost.Open.cs:98-102`). The mouse toggle and the programmatic open share one request path (`QuickFiler.Test\Viewers\BreadcrumbSelectorOpenRetryTests.cs:55`), which is consistent with mouse and keyboard failing identically. +- Asynchronous suggestion decoration is not the cause: `SetSuggestionsAsync` / `SetSuggestionFallbacks` route through `ReplaceRowsPreservingSession` (`FolderBreadcrumbBridgeRouter.cs:478-482`) -> `BreadcrumbSelectionSession.ReconcileRowsReplaced` (`:119-147`), which preserves `IsOpen` and raises no `SelectorOpenStateChanged`. +- Three code paths cancel the selector session from outside the user's gesture and each closes the popup: + 1. `QfcFormController.ParkFocusAndCancelSelectors` (`QuickFiler\Controllers\QfcFormController.Deactivate.cs:39-57`, wired to `Form.Deactivate` at `QfcFormController.SetupDisposal.cs:175`, added by #677) cancels every item's selector when the QuickFiler form loses activation and moves `ActiveControl` back into the form (`QfcFormViewer.cs:207`). Opening the popup calls `Control.Focus()` on the popup's own top-level window (`ItemViewer.Breadcrumb.cs:203`), which deactivates the QuickFiler form. There is no latch distinguishing a self-inflicted deactivation from a real one. `FinishOpenCore` (`BreadcrumbDropDownOpenCoordinator.cs:273-288`) also re-checks `_isSelectorOpen()` after the async open and closes if the session was cancelled meanwhile. Primary suspect for the flash. (Win32 activation ordering is inferred; the downstream code is confirmed.) + 2. Native `ToolStripDropDown` auto-close -> `BreadcrumbDropDownHost.OnDropDownClosed` (`:426-437`) -> `FinishClose(Uncommitted)` (`:439-455`) -> `_cancelSelection()` = `BreadcrumbCoordinator.CancelSelector()` (`ItemViewer.Breadcrumb.cs:205`). This is the mechanism behind the click-without-select symptom: a click inside the popup's WebView2 shifts activation, the popup auto-closes, and the uncommitted selection is cancelled before the row's selection message commits. This hazard was recorded as unverifiable in `docs\features\archive\2026-08-07-quickfiler-search-keystroke-focus-steal-438\research\2026-08-08T10-30-...-research.md:172`. + 3. `QfcItemController.TextBoxSearch_Leave` (`QuickFiler\Controllers\QfcItemController.EventHandlers.cs:217-228`) closes the drop-down on search-box leave; its `_searchLeaveHandoffPending` latch (#680) is set only for the Down-arrow path (`:195`). Since Down also flashes, this path is not the primary cause but the missing mouse-path latch remains a gap. +- `MayRestoreBreadcrumbFocus` (`ItemViewer.Breadcrumb.cs:270-274`) requires `Form.ActiveForm` to be the QuickFiler form, so once the popup owns activation the focus step becomes a no-op while the cancel step always runs (asymmetry documented at `BreadcrumbDropDownHost.cs:452`). +- Existing tests that the fix must reconcile with: `QfcFormControllerDeactivateTests.FormDeactivated_CancelsSelectorOnEveryItemController` (`QuickFiler.Test\Controllers\QfcFormControllerDeactivateTests.cs:172`) pins cancel-on-deactivate; `BreadcrumbPendingOpenCloseTests` (`:124`, `:143`) encode "close wins over a pending open". +- Typing keeps the list open because `ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) reports no `OpenStateChanged` (#438 AC-3), and because after typing the search box holds focus inside the QuickFiler form, so no deactivation occurs. + +## Proposed Fix / Validation Ideas + +Acceptance criteria settled with the maintainer on 2026-09-06: + +- [ ] AC1: Opening the list by arrow click or by Down in the search box leaves it open until Escape, Left, a second arrow click, an item selection, or selection of a different QfcItem. +- [ ] AC2: A deactivation of the QuickFiler form caused by the popup taking focus does not cancel the selector session; a deactivation caused by any other window still does (the #677 contract is preserved for genuine deactivation). +- [ ] AC3: A mouse click on a row in the open list selects that row and closes the list; the selection is committed before any auto-close cancel runs. +- [ ] AC4: The #680 leave-handoff latch covers the mouse open path as well as the Down-arrow path. +- [ ] AC5: Row-set refreshes while open (search, late decoration) continue not to close the list (#438 AC-3 regression guard). +- [ ] AC6: The first implementation step instruments `ParkFocusAndCancelSelectors` and `OnDropDownClosed` with debug log lines so the runtime ordering is confirmed before the fix is chosen. + +Validation: + +- [ ] Unit coverage areas: self-inflicted-deactivation latch (Moq the form viewer's active-form seam); commit-before-cancel ordering in `BreadcrumbDropDownHost.FinishClose`; mouse-path leave latch; existing deactivate and pending-open tests updated to the new contract rather than weakened. +- [ ] Integration scenario to retest: open by mouse, open by Down, refresh while open, click a row, select a different QfcItem while open. +- [ ] Manual verification notes: the runbook `docs\features\archive\...-438\runbooks\verify-search-focus-retention.runbook.md` covers the search path; extend it with the arrow-click and row-click gestures. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch