From 961479b4e12986e661128bf8d9f5ce0c21bff0bf Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 17:58:07 -0400 Subject: [PATCH 01/37] docs(epic): add quickfiler-suite-determinism-foundation manifest Scopes the first of three planned epics over the QuickFiler defect corpus: four children covering issues 511+571, 445, 491, and 449. Also restores three promoted potential documents that existed only on the stale epic/quickfiler-per-file-coverage-integration branch and never reached main. They are the authoritative requirements source for these children; the GitHub issue bodies for 445, 449, and 491 read "(not provided in potential file)" in every section below the summary. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../epic.md | 184 ++++++++++++++++++ ...iler-explorer-controller-latent-defects.md | 94 +++++++++ ...kfiler-keyboard-action-contract-defects.md | 135 +++++++++++++ ...6-08-07-quickfiler-test-form1-live-form.md | 66 +++++++ 4 files changed, 479 insertions(+) create mode 100644 docs/features/epics/quickfiler-suite-determinism-foundation/epic.md create mode 100644 docs/features/potential/promoted/2026-08-07-quickfiler-explorer-controller-latent-defects.md create mode 100644 docs/features/potential/promoted/2026-08-07-quickfiler-keyboard-action-contract-defects.md create mode 100644 docs/features/potential/promoted/2026-08-07-quickfiler-test-form1-live-form.md diff --git a/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md b/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md new file mode 100644 index 000000000..aa259cee1 --- /dev/null +++ b/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md @@ -0,0 +1,184 @@ +--- +epic: quickfiler-suite-determinism-foundation +integration_branch: epic/quickfiler-suite-determinism-foundation-integration +created_at: 2026-08-21T17-45 +intent: + epic_type: enabler + business_outcome_hypothesis: >- + Removing the two sources of nondeterminism from the QuickFiler test suite, and settling the + three isolated contract defects that later QuickFiler work must build on, produces a suite + whose red is trustworthy — so that the remaining 43 open QuickFiler defects can be certified + against evidence rather than against a suite that fails on some runs and passes on others. + leading_indicators: + - The full nine-assembly suite passes on ten consecutive runs under induced CPU load. + - No unit-test run creates a visible window on the desktop. + - The IKbdAction contract has no commented-out members and no implementer reporting a + delegate type it does not store. + nfrs: + - No test is stabilized by adding a sleep, a retry, or a timing tolerance. + - Coverage of QuickFiler.csproj is retained or improved at every child merge. + - No production file exceeds 500 lines after change. + - Full C# toolchain (csharpier, analyzers, nullable, MSTest with coverage) green per child. +features: + - issue_num: 511 + feature_folder: 2026-08-21-winformspumphost-suite-determinism-511 + depends_on: [] + - issue_num: 445 + feature_folder: 2026-08-21-quickfiler-keyboard-action-contract-defects-445 + depends_on: [] + - issue_num: 491 + feature_folder: 2026-08-21-quickfiler-test-form1-live-form-491 + depends_on: [] + - issue_num: 449 + feature_folder: 2026-08-21-quickfiler-explorer-controller-latent-defects-449 + depends_on: [] +--- + +# Epic: QuickFiler Suite Determinism Foundation + +## Goal + +Make the QuickFiler test suite deterministic and headless, and settle three isolated contract +defects, so that the remaining QuickFiler defect backlog can be delivered against a suite whose +failures mean something. + +This epic is the first of three planned over the QuickFiler defect corpus. It is deliberately the +smallest and the least entangled: every child here owns a file set that no other child in this +epic and no child of the two later epics contends on, with the single exception of the shared +test project file discussed under Shared-Surface Coordination. + +## Scope + +Five issues across four children: + +- **#511 + #571 — `WinFormsPumpHost` suite determinism.** `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` + runs `Application.Run(new ApplicationContext())` on a dedicated STA thread and never adds a form + or control, so no window handle is ever created. Eight consumer tests plus thirteen self-tests + depend on it. #571's two intermittent failures + (`InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` and + `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState`) fail inside + `QfcItemController.InvokeBeginInvoke` at `QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs:256` + precisely because `Control.Invoke` is reached before a handle exists. #511 and #571 are one + feature, not two, for the reason in Decomposition Rationale below. +- **#445 — keyboard-action contract defects.** Three defects across `KaChar.cs`, `KaKey.cs`, + `KaStringAsync.cs`, `KbdActions.cs`, and `QuickFiler/Interfaces/IKbdAction.cs`: an inconsistent + `Activated` gate in `KaStringAsync.KeyEquals`, an `ArgumentOutOfRangeException` on + `KeyEquals("")`, and `KaChar.DelegateType` reporting `typeof(Action)` while storing an + `Action`. +- **#491 — live form in the test project.** `QuickFiler.Test/Form1.cs` and its designer are + compiled into the test assembly and construct a real form. +- **#449 — explorer-controller latent defects.** Two latent defects in + `QuickFiler/Controllers/QfcExplorerController.cs` plus a block of dead duplicated code. + +## Non-Goals + +- The `IItemViewer` UI-thread seam consolidation (#489) is **not** in this epic. It rewrites + `IItemViewer`, `ItemViewer.cs`, and `ItemViewer.WebViewThread.cs`, which the third epic's + ItemViewer child owns. It is scheduled there. +- Replacing the real message pump with a synchronization-context seam wholesale is **not** + mandated here. See Decomposition Rationale. +- No `.claude/**` file is edited by any child of this epic. Where an issue cites a rule file, the + citation is the policy the fix is measured against, not an edit target. + +## Shared Design + +The suite's nondeterminism has one shape: a real WinForms control is reached through a real +`Control.Invoke` before its window handle exists, and whether the handle exists depends on OS +scheduling. The existing seam is already interface-typed and already mockable — see Decomposition +Rationale — so the correction is deterministic fixture setup, not new abstraction. + +## Decomposition Rationale + +**#511 and #571 are one child, not two.** `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs:51` +defines a `UiThreadDispatcherGate` and a `SwapUiThreadDispatcher` helper that mutate the +process-wide static `UtilitiesCS.UiThread._dispatcher` by reflection, serializing the pump tests +across two test classes. Any change to the host or its harness must preserve that serialization or +`QfcItemController.SeamFactoryTests` and `QfcItemController.InitializationTests` deadlock against +each other under class-level parallelization. Two branches cannot safely make that change +independently. + +The two issues are also in **tension rather than dependency**, and the child must reconcile them +rather than assume an order. #511 proposes replacing the real pump with an injectable +synchronization-context seam. Executed literally, that deletes or reclassifies the very tests #571 +wants to stabilize, along with the coverage justifications recorded at +`QuickFiler/Controllers/QfcItemController.Initialization.cs:166, 261, 293, 404, 448` and +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:31, 256`. The child's spec must decide the +direction, not inherit it. + +**The marshalling seam already exists.** `QfcItemController` holds `IItemViewer` (not a concrete +control) at `QuickFiler/Controllers/QfcItemController.cs:51`, and `Invoke`, `BeginInvoke`, and +`InvokeRequired` are re-declared on the interface at `QuickFiler/Viewers/IItemViewer.cs:95-100` +specifically to stay mockable. A second seam, `UtilitiesCS.Threading.IUiDispatcher`, is held at +`QuickFiler/Controllers/QfcItemController.cs:66`. Both are already exercised without a pump in +`QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs:99-115`. This epic therefore +introduces no new seam; the planning premise that a shared test-support seam must be built first +did not survive inspection. + +**Forcing a handle is not a prohibited timing hack.** `.claude/rules/csharp.md:95` prohibits +"adding sleeps, retries, or timing hacks to mask flaky behavior." Deterministically establishing a +control's window handle on the pump thread before the act removes the race rather than masking it, +and is therefore permitted. The child must still record this reading in its spec, because #571's +own text raises the question. + +## Shared-Surface Coordination + +`QuickFiler.Test/QuickFiler.Test.csproj` is a legacy non-SDK project with 116 explicit +`` entries, so any child that adds or removes a test file must edit it. Two +children here do, and their regions are partitioned: + +- **#491 owns the `Form1` region** — `QuickFiler.Test/QuickFiler.Test.csproj:161-165` (the + `Form1.cs` and `Form1.Designer.cs` compile entries) and `:180-181` (the `Form1.resx` embedded + resource). No other child may touch those lines. +- **#449 owns one appended `Compile Include`** for the explorer-controller test file it must + create, because no `*Explorer*` test file exists today. It appends to the `Controllers` item + group and must not touch the `Form1` region. +- **#511/#571 and #445 add no compile entry.** Their regression tests belong in existing files + that already carry entries: `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` and + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` for the former; + `KaCharTests.cs`, `KaKeyTests.cs`, `KaStringAsyncTests.cs`, `KbdActionsTests.cs`, and + `KbdActionsRemainingBranchesTests.cs` for the latter. + +With those regions partitioned, all four children sit in wave 0 and the dependency graph is empty. + +## Waves + +Wave 0 (all four, no dependency edges): #511/#571, #445, #491, #449. + +The graph is intentionally empty. Ordering in this epic comes from the csproj region partition +above, not from `depends_on` edges, because no child's fix changes a contract another child in this +epic consumes. + +## Complexity Assessment + +| child | band | rationale | +| --- | --- | --- | +| #511 + #571 | C3 | Two issues in tension that the child must reconcile; a process-wide static mutated by reflection; 8 consumer tests and 13 self-tests in blast radius; a policy reading to settle. | +| #449 | C3 | Two latent defects plus dead duplicated code in a 1,065-line legacy neighbour; no existing test file, so the harness is new; touches `UtilitiesCS` mail-filing collaborators. | +| #445 | C2 | Five small files, but one genuine behavioural decision (whether the third `KeyEquals` branch should be `Activated`-gated) and a `DelegateType` removal that must not break `KaCharAsync`/`KaKeyAsync`. | +| #491 | C2 | Bounded removal of a live form from the test assembly, with csproj and one dependent test file. | + +## Execution Notes for epic-orchestrator + +1. **Re-normalize prepared plans to LF before revalidating.** `core.autocrlf` is `true` in this + repository, so each prepared plan committed here as LF materializes as CRLF in a freshly created + child worktree, and the MCP `plan` validator has rejected CRLF plans. A validator failure on a + plan that passed during preparation is this effect, not a defect in the plan. +2. **`vstest` requires `/InIsolation`.** Without it, binding redirects in each assembly's + `app.config` are ignored and roughly 1,695 phantom failures appear with empty messages and + sub-millisecond durations, surfacing as a `TypeInitializationException` from Moq via + `System.Threading.Tasks.Extensions`. Use + `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`. +3. **Exclude `\.claude\` from recursive `*.Test.dll` discovery.** Six stale agent worktrees exist + under `.claude/worktrees/`. None currently holds build output, but any of them will once built, + and a CI-style recursive search would then load stale assemblies. +4. **Do not rely on any `PreToolUse` hook.** Every hook in this repository currently reads + `$toolInput.command` while the payload nests the value at `$toolInput.tool_input.command`, so + the property is always null and each hook returns `permissionDecision: allow`. The epic wave + barrier, merge gate, and worktree-removal gate are all inert. Confirm every wave transition from + `git worktree list --porcelain`, `git branch`, and `gh pr view --json state,mergedAt,headRefOid`. +5. **No Python toolchain exists here.** There is no `scripts/dev_tools/` and no Poetry manifest, so + any skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence. The + PowerShell equivalents live under `.claude/lib/`. +6. **The four child issues are already open.** Each child must call only + `mcp__drm-copilot__new_active_feature_folder`; `potential_to_issue` has no idempotent path and + would file a duplicate. diff --git a/docs/features/potential/promoted/2026-08-07-quickfiler-explorer-controller-latent-defects.md b/docs/features/potential/promoted/2026-08-07-quickfiler-explorer-controller-latent-defects.md new file mode 100644 index 000000000..043912e0d --- /dev/null +++ b/docs/features/potential/promoted/2026-08-07-quickfiler-explorer-controller-latent-defects.md @@ -0,0 +1,94 @@ +# quickfiler-explorer-controller-latent-defects (Issue #449) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-explorer-controller-latent-defects/ (Issue #449) +- Found during: research for issue #435 (child F6 of epic #136, QuickFiler per-file coverage) + +- Issue: #449 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/449 +- Last Updated: 2026-08-08 +## Summary + +Two independent latent defects in `QuickFiler/Controllers/QfcExplorerController.cs`, plus a block of +dead duplicated code. All three were found by reading during F6 coverage research and none is fixed by +F6, whose acceptance criteria forbid behavior changes. + +## Defect 1 — `ExplConvView_Cleanup()` throws `NotImplementedException` + +`ExplConvView_Cleanup()` is declared on the public interface `IQfcExplorerController` +(`QuickFiler/Interfaces/IQfcExplorerController.cs:12`) but its implementation throws +`NotImplementedException`. Any caller reaching it fails at runtime rather than degrading. + +The intended semantics appear to be recoverable from the legacy implementation at +`QuickFiler/Legacy/QuickFileController.cs:851-869` (not compiled), which should be read before +implementing rather than reinventing the behavior. + +Mitigating factor: the member currently has no production callers, so the throw is not reachable in +normal operation today. That makes it latent rather than active — but it is a live trap for the next +caller. + +## Defect 2 — `OpenQFItem` re-resolves the active explorer + +`OpenQFItem` calls `_globals.Ol.App.ActiveExplorer()` a second time at +`QuickFiler/Controllers/QfcExplorerController.cs:140` instead of reusing the `_activeExplorer` field +captured in the constructor at line 35. + +This is both a redundant COM round-trip and a correctness hazard: if the active explorer changed +between construction and the call, the method operates on a different `Explorer` than the rest of the +type, so the object's view of "the" explorer becomes internally inconsistent. + +## Defect 3 — dead duplicated code block + +`QuickFiler/Controllers/QfcExplorerController.cs:183-321` (the `#region Email Sorting To Rewrite`) +contains six private/internal statics — `SanitizeArrayLineTSV`, `StripTabsCrLf`, +`WriteCSV_StartNewFileIfDoesNotExist`, `SanitizeArray`, `SaveMessageAsMSG`, +`GetCurrentExplorerFolder`. A repo-wide search confirms they are referenced only from inside that same +region (lines 193, 241, 264). Every external caller binds to separate copies in +`UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs`, +`UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs`, and +`ToDoModel/Email Utilities/SortItemsToExistingFolder.cs`, which carry their own tests in +`UtilitiesCS.Test`. + +Two latent defects were additionally observed inside this dead block: +- `WriteCSV_StartNewFileIfDoesNotExist` passes transposed arguments to `Path.Combine`. +- `SanitizeArray` writes into a `null` `ref string[]`, which would throw if ever reached. + +Because the block is unreachable, neither defect can fire today. Deleting the region is +behavior-neutral for the QuickFiler assembly and removes roughly 139 lines of uncoverable +filesystem-I/O code from the coverage denominator. + +## Why This Is Filed Separately + +All three items were found during read-only research for the F6 coverage child (issue #435). F6's +acceptance criteria require no behavior change to observable QuickFiler flows, and fixing a +`NotImplementedException` or changing which `Explorer` instance is used are both behavior changes. +Recording them only as prose inside a feature folder would lose them at merge. + +## Impact + +- Defect 1: runtime failure for the next caller of a public interface member. +- Defect 2: redundant COM call plus a potential inconsistency window. +- Defect 3: no runtime impact; carrying cost is coverage-denominator pollution and duplicated code + that can drift from the maintained copies in `UtilitiesCS`. + +## Acceptance Criteria (early draft) + +- [ ] `ExplConvView_Cleanup()` either implements the legacy semantics from + `QuickFiler/Legacy/QuickFileController.cs:851-869` or is removed from `IQfcExplorerController` + with all implementers updated; the decision is recorded with rationale. +- [ ] `OpenQFItem` reuses the constructor-captured `_activeExplorer` field, or the reason a fresh + `ActiveExplorer()` call is required is documented in code. +- [ ] The dead `#region Email Sorting To Rewrite` block is deleted, with a test run confirming no + behavior change. +- [ ] Deterministic regression tests cover each changed path; no temporary files, no live forms. +- [ ] Full C# toolchain passes: csharpier, analyzer build, nullable build, coverage-enabled vstest. + +## Coordination Note + +The dead-code deletion overlaps the file F6 is actively covering. Sequence this issue AFTER F6 merges, +or coordinate through the epic, to avoid a conflict on `QfcExplorerController.cs`. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) diff --git a/docs/features/potential/promoted/2026-08-07-quickfiler-keyboard-action-contract-defects.md b/docs/features/potential/promoted/2026-08-07-quickfiler-keyboard-action-contract-defects.md new file mode 100644 index 000000000..e4994edd7 --- /dev/null +++ b/docs/features/potential/promoted/2026-08-07-quickfiler-keyboard-action-contract-defects.md @@ -0,0 +1,135 @@ +# quickfiler-keyboard-action-contract-defects (Issue #445) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-keyboard-action-contract-defects/ (Issue #445) +- Discovered during: research for issue #430 (`quickfiler-keyboard-actions-coverage`, child F3 of epic #136) + +- Issue: #445 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/445 +- Last Updated: 2026-08-08 +## Summary + +Three related contract defects in the QuickFiler keyboard-action types. All three were verified by +direct file read at `origin/epic/quickfiler-per-file-coverage-integration` (base commit `56ca1cea`). +None is fixed by issue #430, which carries a no-behavior-change acceptance criterion and characterizes +current behavior in tests instead. + +## Defect 1 — `KaStringAsync.KeyEquals` applies the `Activated` gate inconsistently + +`KeyEquals` guards its `Update` invocation with `Activated` in two of three branches but not the third: + +```csharp +// QuickFiler/Controllers/KaStringAsync.cs:57-78 +public bool KeyEquals(string other) +{ + if (Key.Contains(other)) + { + if (Activated && Update is not null) // gated + Update(Key.Substring(other.Length - 1, 1)); + return true; + } + else if (other.Length == 1) + { + if (Activated && ToggleControl is not null) // gated + ToggleControl(); + } + else if (other.Length > 1) + { + if (Update is not null) // NOT gated + Update(Key.Substring(0, 1)); + if (Activated && ToggleControl is not null) + ToggleControl(); + } + Activated = false; + return false; +} +``` + +The `other.Length > 1` branch invokes `Update` regardless of `Activated`. Whether this is intentional +or an omission is not determinable from the code; there is no comment explaining it. This is the +highest-value untested behavior in the cluster. + +## Defect 2 — `KeyEquals("")` throws `ArgumentOutOfRangeException` + +`Key.Contains("")` is `true` for every string, so an empty `other` enters the first branch and +evaluates `Key.Substring(other.Length - 1, 1)` — that is, `Substring(-1, 1)` — which throws +`ArgumentOutOfRangeException` (`KaStringAsync.cs:62`). + +This is currently double-shielded and is therefore a robustness gap rather than a live crash: +`KeyboardHandler` only ever probes with length `>= 1`, and production supplies a null `Update`, so the +guarded call is not reached. Both shields are incidental, not contractual. + +## Defect 3 — `KaChar.DelegateType` reports the wrong type + +```csharp +// QuickFiler/Controllers/KaChar.cs:11 +public class KaChar : IKbdAction> +// QuickFiler/Controllers/KaChar.cs:37 +public Action Delegate +// QuickFiler/Controllers/KaChar.cs:43-46 +public Type DelegateType +{ + get => typeof(Action); +} +``` + +`KaChar` stores an `Action` but `DelegateType` reports `typeof(Action)`. Impact today is +nil because no consumer reads `DelegateType`. + +## Related — `Update` and `DelegateType` are orphaned public API + +`Update` and `DelegateType` appear on four implementer types but on no interface. The corresponding +contract members are commented out: + +```csharp +// QuickFiler/Interfaces/IKbdAction.cs:12-16 +T Key { get; set; } +U Delegate { get; set; } +bool KeyEquals(T other); +//Action Update { get; set; } +//Type DelegateType { get; } +``` + +Restoring `DelegateType` to the interface **will not compile**: `KaCharAsync` (`KaChar.cs:58`) and +`KaKeyAsync` do not declare it. The viable cleanup direction is therefore removal from the implementers +rather than restoration to the interface. Defect 3 disappears if `DelegateType` is removed. + +## Impact + +No confirmed user-visible failure. Defects 2 and 3 are latent. Defect 1 is a genuine behavioral +ambiguity that will become load-bearing the moment `Update` is non-null on a multi-character probe. +All three are the kind of contract inconsistency that makes the surrounding code unsafe to refactor. + +## Why these were not fixed in issue #430 + +Issue #430 (child F3) carries an explicit acceptance criterion of **no behavior change to observable +QuickFiler keyboard flows**. Each of these fixes is a behavior change. F3's new tests characterize the +current behavior, including the ungated `Update` call and the empty-string throw, so that a later fix +has a red-before-green baseline to work against. + +## Proposed Fix Direction + +1. Decide whether the `other.Length > 1` branch should be `Activated`-gated, and make all three + branches consistent with the decision. +2. Add an explicit guard or documented contract for empty `other` in `KeyEquals`. +3. Remove `DelegateType` from `KaChar`, `KaKey`, and any sibling implementer, or correct it to + `typeof(Action)` if a consumer is introduced. Remove the commented-out members from + `IKbdAction.cs` or restore them deliberately with all implementers updated. + +## Acceptance Criteria (early draft) + +- [ ] The `Activated`-gating contract for `KaStringAsync.KeyEquals` is decided, applied consistently + across all three branches, and documented in-code. +- [ ] `KeyEquals` handles an empty `other` without throwing `ArgumentOutOfRangeException`, or rejects it + with an explicit, documented argument exception. +- [ ] `DelegateType` is either removed from all implementers or reports the actual stored delegate type. +- [ ] The commented-out members in `IKbdAction.cs` are resolved (removed or restored with all + implementers updated). +- [ ] Regression tests cover each changed behavior, replacing the characterization tests added by #430. +- [ ] Full C# toolchain passes: csharpier, analyzer build, nullable build, coverage-enabled vstest. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) +- [ ] Sequence after epic #136 child F3 (#430) merges, so the characterization tests exist first diff --git a/docs/features/potential/promoted/2026-08-07-quickfiler-test-form1-live-form.md b/docs/features/potential/promoted/2026-08-07-quickfiler-test-form1-live-form.md new file mode 100644 index 000000000..88b001470 --- /dev/null +++ b/docs/features/potential/promoted/2026-08-07-quickfiler-test-form1-live-form.md @@ -0,0 +1,66 @@ +# quickfiler-test-form1-live-form (Issue #491) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-test-form1-live-form/ (Issue #491) +- Discovered during: preparation research for issue #456 (epic #136, child F14) + +- Issue: #491 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/491 +- Last Updated: 2026-08-08 +## Summary + +A live `System.Windows.Forms.Form` is compiled into the `QuickFiler.Test` assembly, and a second, +unrelated item of dead production surface exists in `ItemViewer.Breadcrumb.cs`. Both are test-policy +and design-debt items rather than runtime defects, and both are outside epic #136 child F14's +production file set. + +## Item 1 — live `Form` compiled into the unit-test assembly + +`QuickFiler.Test/Form1.cs:5` and `QuickFiler.Test/Form1.Designer.cs:3` declare +`public partial class Form1 : System.Windows.Forms.Form`, whose `InitializeComponent` constructs three +`QuickFiler.ItemViewer` instances (`Form1.Designer.cs:32-34`). + +No test instantiates it — verified: the only `Form1` references in the test project are its own two +files — so no policy violation occurs today. But `.claude/rules/general-unit-test.md` and epic #136's +"never construct live forms" rule are one `new Form1()` away from being breached, and the type is dead +weight in the test assembly. + +Candidate disposition: delete both files, or move them to a manual harness project outside the unit +test assembly. + +## Item 2 — three `internal` members of `ItemViewer.Breadcrumb.cs` have no production caller + +`AttachBreadcrumbMessengerWhenReadyAsync` (`ItemViewer.Breadcrumb.cs:100-124`), +`AttachBreadcrumbMessenger` (`:126-140`), and `BreadcrumbOpenTask` (`:29-30`) are invoked only from +tests. A repository-wide search for each identifier returns the declaration plus call sites in +`QuickFiler.Test/Viewers/BreadcrumbCollapsedSurfaceReadinessTests.cs:438`, +`BreadcrumbSubfolderActivationTests.cs:340`, +`BreadcrumbSelectorOpenRetryTests.cs:38,41,61,69,265`, +`BreadcrumbCoordinatorLifecycleTests.cs:123`, and +`BreadcrumbDropDownIntegrationTests.cs:415-421`. No `QuickFiler/**` production file references them. + +This is roughly 40 lines of production surface maintained solely for tests. It is not a bug, and it +must not simply be deleted — doing so would break seven existing tests. The disposition is either to +promote these members to the production attach path (the `AttachCollapsedMessenger` route is +arguably what `CreateCollapsedBreadcrumbCandidate` should use) or to mark them explicitly as test +seams so their status is legible. + +## Acceptance Criteria (early draft) + +- [ ] No `Form`-derived type is compiled into `QuickFiler.Test`, or it is isolated in a non-unit-test + project. +- [ ] The three test-only `internal` members are either wired into the production path or explicitly + documented as test seams. +- [ ] Existing tests continue to pass. + +## Constraints & Risks + +- Item 2 touches `ItemViewer.Breadcrumb.cs`, assigned to epic child F14 (issue #456); reconcile + against F14's plan before scheduling. +- Deleting `Form1` changes the `QuickFiler.Test.csproj` compile set; preserve CRLF and keep the edit + to minimal adjacent hunks. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) From 025b350e27c3095ca9253a0543dac8197bb7c49c Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 18:05:03 -0400 Subject: [PATCH 02/37] docs(epic): record stale doc references, child hard constraints, later-epic preconditions Adds three sections to the quickfiler-suite-determinism-foundation manifest: - Known-stale potential-document references. Six measured drifts; children must re-derive line numbers rather than trust a file:line citation. - Hard constraints for children: no .claude edits, mandatory vstest /InIsolation, the directive that #511 must not delete #571's coverage, Python-absent reporting, and the canonical evidence path scheme. - Recorded preconditions for later epics: QfcCollectionController at 4.7x the line cap, missing quality-tiers.yml, the in-flight 61-file potential-doc restoration, and the zero-collision result across 20 quickfiler branches. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../epic.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md b/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md index aa259cee1..a08e9f0bb 100644 --- a/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md +++ b/docs/features/epics/quickfiler-suite-determinism-foundation/epic.md @@ -182,3 +182,80 @@ epic consumes. 6. **The four child issues are already open.** Each child must call only `mcp__drm-copilot__new_active_feature_folder`; `potential_to_issue` has no idempotent path and would file a duplicate. + +## Known-Stale Potential-Document References + +The promoted potential documents are the authoritative requirements source for this corpus, but +their line references have drifted against `main`. Every child MUST re-derive its own line numbers +by reading the target file and MUST NOT trust a `file:line` citation in its potential document. A +child that edits a region named by a drifted reference will edit the wrong code. + +Measured drift, recorded for the whole four-epic corpus (only the first two rows affect this epic +indirectly; the rest are recorded so later epics do not re-discover them): + +| Document | Drift | +| --- | --- | +| `286.md` | Stale by +17 lines. `RemoveSpecificControlGroupAsync` is at `:1159-1248`, not `:1142-1233`. | +| `462.md` | Stale by approximately +46 lines. | +| `474.md` | Premise false. The document asserts `IQfcFormController` and `IFilerFormController` are unrelated; `QuickFiler/Controllers/IQfcFormController.cs:13` already inherits `IFilerFormController`, which reduces the defect to a field and constructor retype. | +| `482.md` | Misattributes the divergent expansion registries to `QfcItemController.Navigation.cs`; they are in `QuickFiler/Controllers/QfcItemController.EventWiring.cs:306-389`. | +| `498.md` | Places `BreadcrumbRow.cs` and `BreadcrumbMessageCodec.cs` under `QuickFiler/Controllers/`; both are under `UtilitiesCS/OutlookObjects/Folder/`. | +| `440.md` | Asserts the two breadcrumb surfaces share `BreadcrumbRow`. They do not: the EFC surface uses `BreadcrumbRow`, the QFC surface uses `BreadcrumbStateRow`. | + +## Hard Constraints for Children + +1. **Do not edit anything under `.claude/**`.** That tree is push-down-owned: a sync overwrites all + of it (skills, lib, hooks, agents, rules, `settings.json`) plus `config/blast-radius.json` and + `config/orchestration-routing.json` from an upstream bundle with no merge, so any local edit is + destroyed. Where an issue cites a rule file, the citation is the policy the fix is measured + against, not an edit target. Safe to edit: `CLAUDE.md`, `coverage.config`, + `Directory.Build.targets`, `quality-tiers.yml`, `.github/workflows/**`, `scripts/**`, `tests/**`, + every C# project, and `.claude/agent-memory/**`. +2. **`vstest` requires `/InIsolation`.** Without it, each assembly's `app.config` binding redirects + are ignored and roughly 1,695 phantom failures appear with empty messages and sub-millisecond + durations, surfacing as a Moq `TypeInitializationException` via + `System.Threading.Tasks.Extensions`. A child that omits the flag will see a fabricated mass + regression and must not attempt to "fix" it. Use + `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`, + and exclude `\.claude\` from recursive `*.Test.dll` discovery so stale agent-worktree builds are + not loaded. +3. **#511 must not delete #571's coverage.** #511's proposed remedy — replacing the real pump with + an injectable synchronization-context seam — executed literally would delete or reclassify the + very tests #571 stabilizes, along with the coverage justifications at + `QuickFiler/Controllers/QfcItemController.Initialization.cs:166, 261, 293, 404, 448` and + `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:31, 256`. #571's root cause is narrow: + `WinFormsPumpHost.RunPumpThread` calls `Application.Run(new ApplicationContext())` and never adds + a form or control, so no window handle is ever created, and only the two synchronous `Initialize` + paths reach `Control.Invoke`. The spec must reconcile the two issues and retain the coverage. +4. **No Python toolchain exists.** There is no `scripts/dev_tools/` and no Poetry manifest, so any + skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence. Report it + as such; do not fabricate a result and do not silently skip it. PowerShell equivalents are under + `.claude/lib/`. +5. **Evidence paths are non-overridable**: `/evidence//` only. No `artifacts/` + sub-path other than `artifacts/orchestration/` may hold evidence. + +## Recorded Preconditions for Later Epics + +Recorded here so they are not re-litigated, and deliberately NOT solved by this epic: + +- **`QuickFiler/Controllers/QfcCollectionController.cs` is 2,349 lines**, 4.7x the 500-line cap in + `.claude/rules/general-code-change.md`, with no partial-class siblings. Nine corpus issues target + it, and only #468 reduces it — to approximately 2,114 lines, still 4.2x. `feature-review` will + raise the cap violation on every pull request touching the file. **Epic 2 + (`quickfiler-qfc-controllers`) must either carry a ratified exemption or add a partial-class split + child before its collection-controller work.** `QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs` + is exactly at the 500-line cap, so every new regression test there needs a new file and therefore + a new project-file compile entry. +- **`quality-tiers.yml` does not exist at the repository root**, although + `.claude/rules/quality-tiers.md` states every project must be classified there and that an + unclassified project fails CI. No QuickFiler tier classification is available to cite. Owned + outside this epic. +- **The remaining potential-document restoration is in flight.** 61 files under + `docs/features/potential/promoted/` exist on `origin/epic/quickfiler-per-file-coverage-integration` + and are absent from `main` (98 versus 55). They are being restored to `main` under a separate pull + request. Epics 2 through 4 are gated on that landing. No child of this epic may write under + `docs/features/potential/**`. +- **No in-flight collision hazard.** All 20 `feature/quickfiler-*` branches are reachable from + `epic/quickfiler-per-file-coverage-integration`, and that branch differs from `origin/main` by + zero files under `QuickFiler/` and `QuickFiler.Test/`; the per-file-coverage epic's code has + already landed on `main`. No unmerged non-QuickFiler branch touches QuickFiler. From 0a5f571d525b4f661c8d51011761e4c6d041466a Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 19:04:13 -0400 Subject: [PATCH 03/37] docs(prep): preserve incomplete preparation artifacts for #511 epic-planner preserved this work after the preparation orchestrator was terminated by an infrastructure error (API 529 Overloaded), not by a task failure. Preparation did NOT complete: no atomic-executor preflight clearance was obtained for this item. Present: issue.md, spec.md, research artifact, and an atomic plan that passes the MCP plan validator. Absent: PREFLIGHT: ALL CLEAR. Committed so a relaunched child resumes from this commit instead of losing an uncommitted worktree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../issue.md | 136 +++ .../plan.2026-08-21T18-10.md | 212 +++++ ...host-suite-determinism.2026-08-21T18-20.md | 859 ++++++++++++++++++ .../spec.md | 753 +++++++++++++++ 4 files changed, 1960 insertions(+) create mode 100644 docs/features/active/winformspumphost-suite-determinism-511/issue.md create mode 100644 docs/features/active/winformspumphost-suite-determinism-511/plan.2026-08-21T18-10.md create mode 100644 docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md create mode 100644 docs/features/active/winformspumphost-suite-determinism-511/spec.md diff --git a/docs/features/active/winformspumphost-suite-determinism-511/issue.md b/docs/features/active/winformspumphost-suite-determinism-511/issue.md new file mode 100644 index 000000000..f2c6ef821 --- /dev/null +++ b/docs/features/active/winformspumphost-suite-determinism-511/issue.md @@ -0,0 +1,136 @@ +# winformspumphost-suite-determinism (Issues #511 and #571) + +- Work Mode: full-bug +- Type: bug +- Primary Issue: #511 +- Primary Issue URL: https://github.com/drmoisan/TaskMaster/issues/511 +- Secondary Issue: #571 +- Secondary Issue URL: https://github.com/drmoisan/TaskMaster/issues/571 +- Epic: quickfiler-suite-determinism-foundation (child 1 of 4, wave 0) +- Integration Branch: epic/quickfiler-suite-determinism-foundation-integration +- Branch: bug/winformspumphost-suite-determinism-511 +- Last Updated: 2026-08-21T18-20 + +> Provenance note. This file was authored by the orchestrator, not copied by +> `mcp__drm-copilot__new_active_feature_folder`. That tool scaffolded `spec.md` and the plan +> template but produced no `issue.md`, because the folder short-name +> (`winformspumphost-suite-determinism`) does not match either promoted source filename. The two +> promoted records named under "Requirements Sources" below remain in place and are the +> authoritative requirements source; this file is a consolidation, not a replacement. + +> Acceptance-criteria authority. Work Mode is `full-bug`, so per the `acceptance-criteria-tracking` +> skill the authoritative acceptance-criteria source for this feature is `spec.md` only. The +> criteria are not duplicated here. + +## Requirements Sources + +Both promoted records are richer than the GitHub issue bodies and are authoritative: + +- `docs/features/potential/promoted/2026-08-08-winformspumphost-tests-load-flaky-visible-window.md` (#511) +- `docs/features/potential/promoted/2026-08-15-qfc-item-controller-init-tests-flaky-window-handle.md` (#571) + +Issue state was verified against durable GitHub state on 2026-08-21 with +`gh issue view --json number,title,state,labels,url`. Both are `OPEN` and carry the `bug` +label. No promotion tool was invoked to create them; see the "Promotion" section. + +## Summary + +Two open defects describe one underlying condition in the QuickFiler test suite and are closed +together by this feature. + +**#511 (High) — `WinFormsPumpHost` tests are load-flaky and display a visible window.** +Tests built on `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` start a real WinForms message pump +on a dedicated STA thread and construct real WinForms controls. They failed nondeterministically +under sustained high CPU load (approximately 96%), requiring six attempts to obtain one clean +full-suite baseline during issue #438 work on 2026-08-08, and a visible window appeared during an +otherwise headless run. + +**#571 (Medium) — Two initialization tests fail intermittently on a missing window handle.** +`InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` and +`InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` fail intermittently in a +full-suite run with `InvalidOperationException: Invoke or BeginInvoke cannot be called on a control +until the window handle has been created`, but pass every time when the class runs in isolation. + +## Root Cause (established, to be confirmed by research) + +`WinFormsPumpHost.RunPumpThread` installs a `WindowsFormsSynchronizationContext` and then calls +`Application.Run(new ApplicationContext())` without ever adding a form or a control, so no window +handle is ever created on the pump thread. The pump harness in +`QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` constructs a real +`QuickFiler.ItemViewer` (a `UserControl`) on that thread and never parents it or forces handle +creation. `QfcItemController.InvokeBeginInvoke` reaches `_itemViewer.Invoke(action)`, and +`Control.Invoke` throws unless the native window handle already exists. Whether it happens to exist +depends on ambient WinForms state that differs between a full-suite run and a single-class run. + +## The Tension Between #511 and #571 + +#511 and #571 are in tension, not in dependency order. #511 proposes replacing the real message +pump with an injectable synchronization-context seam. Executed literally, that would delete or +reclassify the very tests #571 stabilizes, together with the pump-hosted coverage justifications +recorded in `QuickFiler/Controllers/QfcItemController.Initialization.cs` and +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`. + +`spec.md` must state the reconciliation decision explicitly rather than inherit an order from +either issue. It must also record the reading that deterministically establishing a window handle +on the pump thread before the act removes the race rather than masking it, and is therefore not a +prohibited timing hack under the "Prohibited Behaviors" section of `.claude/rules/csharp.md`. + +## Constraints Binding This Feature + +1. **No new project-file compile entry.** `QuickFiler.Test/QuickFiler.Test.csproj` is a legacy + non-SDK project. Sibling child #491 owns its `Form1` region and sibling child #449 owns one + appended `Controllers` entry. This feature must not touch the project file at all, so regression + tests belong in files that already carry compile entries: + `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` and + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`. +2. **Preserve the dispatcher serialization.** + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` defines a + `UiThreadDispatcherGate` semaphore and a `SwapUiThreadDispatcher` helper that mutate the + process-wide static `UtilitiesCS.UiThread._dispatcher` by reflection, serializing the pump tests + across two test classes. Any change to the host or its harness must preserve that serialization + or `QfcItemController.SeamFactoryTests` and `QfcItemController.InitializationTests` will deadlock + against each other under class-level parallelization. +3. **The marshalling seam already exists.** `QfcItemController` holds `IItemViewer` rather than a + concrete control; `Invoke`, `BeginInvoke`, and `InvokeRequired` are re-declared on that interface + for mockability; and a second seam `UtilitiesCS.Threading.IUiDispatcher` is also held. Both are + already exercised pump-free in + `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs`. Issue #230, which built + `WinFormsPumpHost`, is closed. No new seam is to be planned on the assumption that none exists. +4. **No `.claude/**` edits.** That tree is push-down-owned; a sync overwrites it from an upstream + bundle with no merge. Where an issue cites a rule file, the citation is the policy the fix is + measured against, not an edit target. +5. **Re-derive every line number.** `file:line` citations in the promoted records and the epic + manifest have drifted. The epic's "Known-Stale Potential-Document References" section is binding. +6. **`vstest` must carry `/InIsolation`**, and recursive `*.Test.dll` discovery must exclude + `\.claude\` so stale agent-worktree builds are not loaded. Omitting `/InIsolation` produces + roughly 1,695 phantom failures with empty messages, surfacing as a Moq + `TypeInitializationException` via `System.Threading.Tasks.Extensions`. +7. **No Python toolchain exists.** There is no `scripts/dev_tools/` and no Poetry manifest, so any + skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence and must be + reported as such rather than fabricated or silently skipped. +8. **Evidence paths are non-overridable**: `/evidence//` only. + +## Toolchain + +Run in this exact order; restart from the first step if any step fails or changes files. + +1. `dotnet tool restore` +2. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) +3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +4. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +5. `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + +Use `/t:Rebuild`, never `/t:Build`: a warm `/t:Build` skips `CoreCompile` on every project and runs +no analyzers, returning exit 0 without gating anything. Never add `/p:Nullable=enable`; no project +carries a `` element and there is no `Directory.Build.props`, so the property conscripts +files that never opted in and diverges from `.github/workflows/ci.yml`. + +## Promotion + +Issues #511 and #571 were already open and their promoted potential records already existed, so no +potential-entry or issue-promotion tool was invoked. `mcp__drm-copilot__potential_to_issue` has no +idempotent path and always creates a new issue; calling it would have filed duplicates. Only +`mcp__drm-copilot__new_active_feature_folder` was called. Receipts are recorded truthfully in +`artifacts/orchestration/orchestrator-state.winformspumphost-suite-determinism.json` under +`delegation_receipts.promotion`, with the potential-entry and issue receipts marked +`status: pre-existing`. diff --git a/docs/features/active/winformspumphost-suite-determinism-511/plan.2026-08-21T18-10.md b/docs/features/active/winformspumphost-suite-determinism-511/plan.2026-08-21T18-10.md new file mode 100644 index 000000000..848a29891 --- /dev/null +++ b/docs/features/active/winformspumphost-suite-determinism-511/plan.2026-08-21T18-10.md @@ -0,0 +1,212 @@ +# winformspumphost-suite-determinism (Atomic Plan) + +- **Issue:** #511 (primary), #571 (secondary) +- **Parent:** epic `quickfiler-suite-determinism-foundation` (child 1 of 4, wave 0) +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T18-10 +- **Status:** Ready for preflight +- **Version:** 1.0 +- **Work Mode:** `full-bug` +- **Branch:** `bug/winformspumphost-suite-determinism-511` +- **Integration Branch:** `epic/quickfiler-suite-determinism-foundation-integration` +- **Acceptance-criteria source:** `docs/features/active/winformspumphost-suite-determinism-511/spec.md`, section `## Acceptance Criteria` (14 criteria). Work Mode is `full-bug`, so `spec.md` is the sole authoritative AC source; no `user-story.md` exists and none is to be created. + +**Fail-closed evidence rule:** Every baseline artifact, QA-gate artifact, and coverage-comparison artifact named below is mandatory. If any 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 check a task off without the artifact present and complete. + +**Evidence-location invariant (non-overridable).** All evidence goes under `docs/features/active/winformspumphost-suite-determinism-511/evidence//` where the kind is one of `baseline`, `regression-testing`, `qa-gates`, `issue-updates`, `other`, `remediation-baseline`. No `artifacts/` sub-path other than `artifacts/orchestration/` may hold evidence. The spec names `evidence/regression-testing/`, `evidence/baseline/`, and `evidence/qa-gates/` and is already canonical; no override was supplied and none was rejected. + +**Feature root abbreviation used below.** `FEATURE` denotes `docs/features/active/winformspumphost-suite-determinism-511`. Every evidence path in a task is written out in full so no path in this plan carries an interpolation marker. + +--- + +## Decision Already Settled — Do Not Re-Open + +Retain the real WinForms message pump. Fix #571 by forcing **invisible** window-handle creation for the fixture's `ItemViewer` on the pump thread. The change touches exactly three test files and zero production files: + +1. `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` (409 lines pre-change) — inside `BuildPumpHarnessCoreAsync`, immediately after the viewer is constructed on the pump thread and before `SwapUiThreadDispatcher` is called. +2. `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs` (467 lines pre-change, 33 of headroom) — the standalone arrange block that constructs its own viewer, near lines 432 through 435. +3. `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` (290 lines pre-change, 210 of headroom) — the only home for new regression tests. + +The instrument is a read of `viewer.Handle`, which is non-recursive, executed on the pump thread through `host.InvokeAsync`. `CreateControl()` is rejected: it is `Visible`-gated and recurses into visible children, which would drag both `Microsoft.Web.WebView2.WinForms.WebView2` controls into handle creation. The exact insertion text is: + +``` + _ = await host.InvokeAsync(() => viewer.Handle).ConfigureAwait(false); +``` + +The in-repo maintainer-ratified precedent is `Tags.Test/TagControllerRendering.StaTests.cs`, whose comment reads `// Act: force invisible handle creation, then invoke the real draw path.` A second precedent is `UtilitiesCS.Test/EmailIntelligence/OSBrowser_Tests.cs:233`. + +`BuildPumpHarnessCoreAsync` is the single choke point for both consumer classes: `QfcItemController_SeamFactoryTests` reaches it through the `internal static BuildPumpHarnessAsync` wrapper at `SeamFactoryTests.cs:313` and `:384`. + +`PumpHarness.Viewer` is exposed as `internal QuickFiler.ItemViewer Viewer { get; }` at `QfcItemController.InitializationTests.Part2.cs:319`, so a test can assert `harness.Viewer.IsHandleCreated`. The two WebView2 children are reachable as `L0v2h2_WebView2` (`QuickFiler/Viewers/ItemViewer.cs:309`) and `L0vhBreadcrumb_WebView2` (`QuickFiler/Viewers/ItemViewer.Breadcrumb.cs:19`). + +## Open Question Carried Into Phase 1 — Settle It By Execution + +Static reading predicts both named tests should fail on **every** run: `Control.Invoke` throws unconditionally without a created handle, and the research found no handle-creating call anywhere in the `ResolveControlGroups` then `SetupThemes` then `PopulateControls` path. #571 nevertheless records the tests passing on some runs. The prime suspect is third-party `Microsoft.Web.WebView2.WinForms.WebView2` `ISupportInitialize` or implicit-initialization behaviour, whose source is not present in this repository. + +**This plan does not assert a pre-fix failure rate it has not measured.** Phase 1 exists to measure it. Explicit instruction for the executor: if the two named tests turn out to **pass** pre-fix on some or all runs, record the observed `harness.Viewer.IsHandleCreated` value for that run and treat the green pre-fix run as data about the race window, **not** as evidence the defect is absent. The chosen remedy is correct under either explanation, because forcing the handle removes the dependency in the passing direction whichever holds. Do not narrow, widen, or abandon the remedy on the basis of the Phase 1 result; record it and continue. + +## Binding Constraints + +1. **No `QuickFiler.Test/QuickFiler.Test.csproj` edit.** The project carries 116 explicit `` entries and zero wildcard includes, so no new test file can be compiled. Sibling child #491 owns the `Form1` region (lines 161 through 165 and 180 through 181); sibling child #449 owns one appended `Controllers` entry. All new tests go in `QfcItemController.InitializationTests.Part3.cs`, which already carries a compile entry. +2. **Change no production file.** Nothing under `QuickFiler/` may appear in the diff. +3. **Do not edit anything under `.claude/`.** That tree is push-down-owned; a sync overwrites it with no merge. Rule files are the policy this fix is measured against, not edit targets. The single exception recognized by this plan is `.claude/agent-memory/`, which is agent bookkeeping and not part of the fix; the scope-lock acceptance conditions carve it out explicitly. +4. **Preserve the dispatcher serialization.** `Part2.cs:51` defines `UiThreadDispatcherGate`, a `SemaphoreSlim(1, 1)`. `SwapUiThreadDispatcher` at `:139` mutates the process-wide static `UtilitiesCS.UiThread._dispatcher` by reflection. `QfcItemController_SeamFactoryTests` acquires the same gate through `BuildPumpHarnessAsync`. Breaking the acquire-and-release structure deadlocks the two classes under class-level parallelization. +5. **500-line cap** on every touched file. Pre-change budget: `Part2.cs` 409, `ViewerSetupTests.cs` 467, `Part3.cs` 290. Do **not** add to `WinFormsPumpHostTests.cs` (443) or `FocusAndThemeTests.cs` (497, three lines of headroom). +6. **No sleeps, retries, or timing tolerances.** `.claude/rules/csharp.md` "Prohibited Behaviors" bans them. `PumpTimeoutMs = 60000` and `TimeoutMs = 30000` retain their current values. +7. **Tests are MSTest plus Moq plus FluentAssertions.** No temporary files; that prohibition is absolute. +8. **No Python toolchain exists** in this repository: there is no `scripts/dev_tools/` and no Poetry manifest. No Python command appears anywhere in this plan. A skill step naming one is unrunnable by absence and must be reported as such rather than fabricated or silently skipped. +9. **Do not rely on any `PreToolUse` hook.** Every hook in this repository currently reads `$toolInput.command` while the payload nests the value at `$toolInput.tool_input.command`, so all of them return `permissionDecision: allow`. Verify from durable `git` and `gh` state instead. + +## Toolchain Commands — Use Verbatim + +Run in this order; restart from step 1 if any step fails or changes files. + +1. `dotnet tool restore` +2. `dotnet tool run csharpier format .` then verify with `dotnet tool run csharpier check .` +3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +4. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +5. `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + +Non-negotiable command facts: + +- **Always `/t:Rebuild`, never `/t:Build`.** MSBuild's up-to-date check does not invalidate on a command-line `/p:` change, so a warm `/t:Build` returns exit 0 with `CoreCompile` skipped on every project and runs no analyzers. The gate cannot fail. Every analyzer and nullable task below therefore asserts a **zero** count of the log line `Skipping target "CoreCompile"`. Do **not** assert a `csc.exe` count; that count is zero even on a real compile, so such an assertion gates nothing. +- **Never add `/p:Nullable=enable`.** No project carries a `` element and there is no `Directory.Build.props`, so the property conscripts every file that never opted in and diverges from `.github/workflows/ci.yml`. +- **`/InIsolation` is mandatory.** Without it each assembly's `app.config` binding redirects are ignored and roughly 1,695 phantom failures appear with empty messages and sub-millisecond durations, surfacing as a Moq `TypeInitializationException` via `System.Threading.Tasks.Extensions`. **That mass-failure signature means the flag is missing. It is a fabricated regression and must NOT be "fixed".** +- **Recursive `*.Test.dll` discovery must exclude `\.claude\`.** `scripts/vscode/Invoke-MSTestWithCoverage.ps1` filters only on `\bin\\`, `\obj\`, and `\ref\` (lines 296 through 302) and does **not** exclude `\.claude\`. Inside this worktree that is currently harmless because `.claude/worktrees/` does not exist here, but it is a real hazard when the script is run from the main checkout. Every task below that invokes the script asserts the discovered assembly count is exactly 9. +- **The nine test projects** are `QuickFiler.Test`, `SVGControl.Test`, `Tags.Test`, `TaskMaster.Test`, `TaskTree.Test`, `TaskVisualization.Test`, `ToDoModel.Test`, `UtilitiesCS.Test`, `VBFunctions.Test`. Every one builds to `bin\Debug\`. +- **Use `pwsh -NoProfile` with absolute paths** for the msbuild and vstest invocations. The Bash tool mangles MSBuild switches: `/m` becomes `M:/`, producing MSB1008. + +### Canonical assembly list + +``` + QuickFiler.Test\bin\Debug\QuickFiler.Test.dll + SVGControl.Test\bin\Debug\SVGControl.Test.dll + Tags.Test\bin\Debug\Tags.Test.dll + TaskMaster.Test\bin\Debug\TaskMaster.Test.dll + TaskTree.Test\bin\Debug\TaskTree.Test.dll + TaskVisualization.Test\bin\Debug\TaskVisualization.Test.dll + ToDoModel.Test\bin\Debug\ToDoModel.Test.dll + UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll + VBFunctions.Test\bin\Debug\VBFunctions.Test.dll +``` + +### Resolving `vstest.console.exe` + +```powershell + $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 +``` + +### Coverage numbers + +`vstest.console.exe /EnableCodeCoverage` emits a binary `.coverage` file, not a percentage. Numeric coverage in this plan is produced by `./scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -CoverageOutput coverage\coverage.cobertura.xml`, which wraps the same nine assemblies with `dotnet-coverage` and emits Cobertura XML. The headline figure is the root `line-rate` attribute; the `QuickFiler` figure is that package's `line-rate`; the changed-module figure is the `line-rate` of the Cobertura classes whose `filename` begins `QuickFiler\Controllers\QfcItemController`. Every coverage task below records real numbers. `UNVERIFIED` is not an acceptable value in any coverage field. + +`QuickFiler/Viewers/ItemViewer.cs` carries a whole-type `[ExcludeFromCodeCoverage]` at line 20, so the fixture change moves no coverage into or out of the denominator. The requirement is therefore **no regression** in `QfcItemController` coverage, not an increase. + +--- + +### Phase 0 — Policy Reads and Baseline Capture + +- [ ] [P0-T1] Read `CLAUDE.md` in full and record the four numbered policy sections it embeds (General Code Change, General Unit Test, C# Code Change, C# Unit Test). Acceptance: the file has been read end to end and its Policy Compliance Order list is quoted in the Phase 0 artifact. +- [ ] [P0-T2] Read `.claude/rules/general-code-change.md` in full. Acceptance: the 500-line file-size limit and the mandatory toolchain loop are quoted in the Phase 0 artifact. +- [ ] [P0-T3] Read `.claude/rules/general-unit-test.md` in full. Acceptance: the coverage thresholds and the Determinism Infrastructure banned-API list are quoted in the Phase 0 artifact. +- [ ] [P0-T4] Read `.claude/rules/csharp.md` in full. Acceptance: the four toolchain commands and the six "Prohibited Behaviors" bullets are quoted in the Phase 0 artifact. +- [ ] [P0-T5] Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/phase0-instructions-read.md` carrying `Timestamp:`, `Policy Order:` (the four files in the order read), and an explicit list of the files read with the quoted content required by P0-T1 through P0-T4. Acceptance: the file exists and all three required fields are present and non-empty. +- [ ] [P0-T6] Record git identity baseline: current branch name, `git rev-parse HEAD`, `git merge-base origin/epic/quickfiler-suite-determinism-foundation-integration HEAD` (falling back to `git merge-base origin/main HEAD` and recording which was used), and `git status --porcelain`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/git-identity.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` carrying the branch, the HEAD sha, the merge-base sha, and the porcelain line count. Acceptance: the artifact exists, all four fields are present, and the recorded merge-base sha is a 40-character hex string. Note: the HEAD sha is recorded as provenance only. No later task gates on a pinned sha; later scope-lock tasks gate on tree invariants against the recorded merge base. +- [ ] [P0-T7] Record the pre-change line count of each of the three files by running `Get-Content -LiteralPath QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs, QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs, QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` once per file and counting the returned lines. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/file-size-budget.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` listing the three counts. Acceptance: the artifact exists and the three recorded counts are 409, 467, and 290 respectively; a different count is recorded verbatim and flagged as drift rather than silently adjusted. +- [ ] [P0-T8] Run `dotnet tool restore` from the worktree root. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/tool-restore.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` naming the restored CSharpier version. Acceptance: `EXIT_CODE: 0` and the recorded CSharpier version is 1.2.6. +- [ ] [P0-T9] Run `dotnet tool run csharpier check .` read-only from the worktree root. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/csharpier-check.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the number of files reported as unformatted. Acceptance: the artifact exists with all four fields; the exit code is recorded verbatim whatever it is, and a non-zero baseline exit code is recorded as a pre-existing condition rather than repaired here. +- [ ] [P0-T10] Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` from the worktree root, capturing the full build log to `coverage\analyzer-baseline.log`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/analyzer-gate.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the warning count, the error count, and the count of log lines matching `Skipping target "CoreCompile"`. Acceptance: `EXIT_CODE: 0` and the recorded `Skipping target "CoreCompile"` count is exactly 0, proving the analyzers actually ran. +- [ ] [P0-T11] Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true'` from the worktree root, capturing the log to `coverage\nullable-baseline.log`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/nullable-gate.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count and the count of log lines matching `Skipping target "CoreCompile"`. Acceptance: `EXIT_CODE: 0` and the recorded `Skipping target "CoreCompile"` count is exactly 0. Confirm in the artifact that the command carried no `/p:Nullable=enable`. +- [ ] [P0-T12] Run the full nine-assembly suite once with the mandated command shape: the resolved `vstest.console.exe`, the nine assembly paths from the canonical assembly list above, `/EnableCodeCoverage`, `/InIsolation`, `/TestCaseFilter:"TestCategory!=LiveOutlook"`, and `/Logger:trx`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/suite-run.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording total, passed, failed, and skipped counts plus the TRX path. Acceptance: the artifact exists with all four fields, and the recorded total exceeds 1,000 tests, confirming all nine assemblies loaded. If roughly 1,695 failures appear with empty messages, `/InIsolation` was omitted; re-run with the flag and record the correction. Do not "fix" the phantom failures. +- [ ] [P0-T13] Capture baseline numeric coverage by running `pwsh -NoProfile -File .\scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SearchRoot . -CoverageOutput coverage\baseline.cobertura.xml` from the worktree root, then read the root `line-rate` and `branch-rate` attributes, the `QuickFiler` package `line-rate`, and the `line-rate` of every Cobertura class whose `filename` begins `QuickFiler\Controllers\QfcItemController`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/coverage.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording all four figures as numeric percentages to two decimal places. Acceptance: the artifact exists, the script reported exactly 9 discovered test assemblies, and no coverage field contains the token `UNVERIFIED` or an empty value. +- [ ] [P0-T14] Record the Python-toolchain absence finding. Confirm by directory listing that `scripts/dev_tools/` does not exist and that no `pyproject.toml` exists at the worktree root, then write `docs/features/active/winformspumphost-suite-determinism-511/evidence/baseline/no-python-toolchain.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` stating that any skill step naming a Python dev-tools module is unrunnable by absence and is reported as such rather than fabricated or silently skipped. Acceptance: the artifact exists with all four fields and records both negative existence checks. + +### Phase 1 — Empirical Pre-Fix Behaviour + +The spec requires that the pre-fix failure behaviour be established **by execution**, not by static reading. The first named regression test is authored here rather than in Phase 3 for two reasons that are recorded so a reviewer does not read it as phase drift: the repository Bugfix Workflow requires a failing regression test **before** the fix, and it is the only instrument that reports the harness viewer's `IsHandleCreated` value on a run where the two end-to-end tests happen to pass. Phase 3 authors the second named test and verifies both. + +- [ ] [P1-T1] [expect-fail] Author `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` in `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` as a `[TestMethod]` carrying `[Timeout(PumpTimeoutMs)]`, following the Arrange-Act-Assert shape of the existing tests in that file (construct `WinFormsPumpHost`, call `BuildPumpHarnessAsync(host, darkMode: false)`, restore in `finally`, `await host.StopAsync()`). It must assert with FluentAssertions that `harness.Viewer.IsHandleCreated` is `true` and that `await host.InvokeAsync(() => harness.Viewer.InvokeRequired)` is `false`. Append it after the last existing method in the partial class rather than inserting it between existing methods, so the line numbers spec AC 1 and AC 2 cite (`Part3.cs:175` and `Part3.cs:131`) remain accurate. It must contain no sleep, no retry, and no timing tolerance. Acceptance: the method exists in that file with exactly that name, uses `[TestMethod]`, `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` is still declared at line 131 and `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` at line 175, and the file's line count is less than 500. +- [ ] [P1-T2] Rebuild so the new probe is compiled, using `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE: 0` and `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` has a write time later than the P1-T1 edit time. +- [ ] [P1-T3] [expect-fail] Run the class-filtered scope ten consecutive times with the resolved `vstest.console.exe`, the assembly `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, `/InIsolation`, `/Logger:trx`, and `/TestCaseFilter:"FullyQualifiedName~QfcItemController_InitializationTests"`. Record, per run, the pass or fail outcome of `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState`, of `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates`, and of `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread`, together with the observed harness viewer `IsHandleCreated` value derived from the probe outcome. Write the ten TRX files and a per-run table to `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/prefix-classfiltered.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode: 1`, and `Output Summary:`. Acceptance: ten distinct TRX files exist and the table has exactly ten rows with no empty cell. A run in which the probe passes is recorded as `IsHandleCreated: true` for that run and is data about the race window, not evidence the defect is absent. +- [ ] [P1-T4] [expect-fail] Run the full nine-assembly suite ten consecutive times with the resolved `vstest.console.exe`, the nine assembly paths from the canonical assembly list, `/EnableCodeCoverage`, `/InIsolation`, `/Logger:trx`, and `/TestCaseFilter:"TestCategory!=LiveOutlook"`. Record the same three per-test outcomes plus the derived `IsHandleCreated` value per run. Write the ten TRX files and the per-run table to `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/prefix-fullsuite.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode: 1`, and `Output Summary:`. Acceptance: ten distinct TRX files exist and the table has exactly ten rows with no empty cell. +- [ ] [P1-T5] Consolidate P1-T3 and P1-T4 into the single pre-fix baseline artifact `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/prefix-baseline.2026-08-21T18-10.md`, carrying `Timestamp:`, a twenty-row table (ten class-filtered runs and ten full-suite runs) with columns for run index, scope, the two named tests' outcomes, the probe outcome, and the observed `IsHandleCreated` value, plus the observed failure rate stated as a fraction of the runs actually executed. Acceptance: the artifact exists, the table has exactly twenty rows, and the failure rate is stated as a measured fraction rather than as a prediction. No sentence in the artifact may claim a rate that was not observed in these twenty runs. +- [ ] [P1-T6] Record the disposition of the open intermittency question in `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/intermittency-question.2026-08-21T18-10.md`, carrying `Timestamp:` and, in prose, which of the two candidate explanations the measured data supports, which it rules out, and which remains open. If the probe reported `IsHandleCreated: true` on any pre-fix run, name that run and state that some path outside the traced initialization sequence created the handle, with the third-party WebView2 `ISupportInitialize` route named as the unverified prime suspect. Acceptance: the artifact exists and does not close the question by assertion; a sentence claiming a mechanism must cite an observation from P1-T5's table. + +### Phase 2 — Handle-Forcing Fixture Change + +- [ ] [P2-T1] Edit `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, inside `BuildPumpHarnessCoreAsync`, inserting the statement `_ = await host.InvokeAsync(() => viewer.Handle).ConfigureAwait(false);` immediately after the viewer is constructed on the pump thread and strictly before the `SwapUiThreadDispatcher(viewer.UiDispatcher)` call. Precede it with a comment recording why: `Control.Invoke` throws on a handle-less control, `Application.Run(new ApplicationContext())` never creates one, reading `.Handle` is non-recursive so the two WebView2 children are not dragged in, and `CreateControl()` would recurse into them. Acceptance: the file contains the exact statement text quoted above exactly once, that statement's line index is greater than the line index of the viewer construction and less than the line index of the `SwapUiThreadDispatcher` call, and no other line of the file is changed. +- [ ] [P2-T2] Verify the `Part2.cs` invariants survived the edit: `UiThreadDispatcherGate` is still declared as a `SemaphoreSlim(1, 1)`, `BuildPumpHarnessAsync` still calls `UiThreadDispatcherGate.WaitAsync` before delegating and still calls `UiThreadDispatcherGate.Release` in its `catch`, `PumpHarness.Restore` still calls `UiThreadDispatcherGate.Release` exactly once behind its `_restored` guard, and the file's line count is less than 500. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/gate-structure-part2.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the four findings and the post-edit line count. Acceptance: all four invariants hold and the recorded line count is less than 500. +- [ ] [P2-T3] Edit `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs`, in the standalone arrange block of `ResolveControlGroupsAsync_ThroughThePumpHost_PopulatesTipsAndControlGroups` (near lines 432 through 435, where the viewer is constructed through `host.InvokeAsync`), inserting the same statement `_ = await host.InvokeAsync(() => viewer.Handle).ConfigureAwait(false);` immediately after the viewer construction and before the `HarnessController` is created. Keep the accompanying comment to at most two lines: this file has 33 lines of headroom against the 500-line cap. Acceptance: the file contains the exact statement text exactly once and its line count is less than 500. +- [ ] [P2-T4] Verify the post-edit line count of all three touched files by counting the lines returned by `Get-Content -LiteralPath` for each of `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs`, and `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`, and write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/file-size-after-fixture-change.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` listing each file with its pre-change and post-change count. Acceptance: each of the three recorded post-change counts is less than 500, and `QfcItemController.ViewerSetupTests.cs` is recorded at 475 or fewer lines. +- [ ] [P2-T5] Confirm the production-file scope lock holds after the fixture change: run `git diff --name-only $MergeBase` using the merge-base sha recorded in P0-T6 and confirm the result contains zero paths beginning `QuickFiler/` and zero paths ending `.csproj`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/scope-lock-after-phase2.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording both counts. Acceptance: both recorded counts are exactly 0. +- [ ] [P2-T6] Rebuild with `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"'`, then run `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` alone using `/TestCaseFilter:"FullyQualifiedName~BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread"` against `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` with `/InIsolation` and `/Logger:trx`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/probe-flips-green.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the pre-fix outcome from P1-T5 and the post-fix outcome side by side. Acceptance: the build exits 0, the TRX records `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` as passed with zero failures, and the artifact states the pre-fix outcome for the same test from the P1-T5 table. This is the fail-proof for the fixture change: a probe that was already green on every pre-fix run must be recorded as such and flagged, because it then proves nothing about the fix and P1-T6's disposition governs. + +### Phase 3 — Regression Tests + +- [ ] [P3-T1] Author `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` in `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` as a `[TestMethod]` carrying `[Timeout(PumpTimeoutMs)]`, following the same Arrange-Act-Assert shape. It must assert with FluentAssertions, reading both values on the pump thread through `host.InvokeAsync`, that `harness.Viewer.L0v2h2_WebView2.IsHandleCreated` is `false` and that `harness.Viewer.L0vhBreadcrumb_WebView2.IsHandleCreated` is `false`, each with a `because` clause recording that this pins the minimality of `.Handle` over `CreateControl()`. Append it after the method added by P1-T1 rather than inserting it between existing methods, so the line numbers spec AC 1 and AC 2 cite remain accurate. It must contain no sleep, no retry, and no timing tolerance. Acceptance: the method exists in that file with exactly that name, asserts on both named WebView2 properties, and `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` is still declared at line 131 with `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` at line 175. +- [ ] [P3-T2] Verify `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` line count after both new tests. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/file-size-part3.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the pre-change count of 290 and the post-change count. Acceptance: the recorded post-change count is less than 500. +- [ ] [P3-T3] Rebuild with `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE: 0` with zero compile errors. +- [ ] [P3-T4] Run both named regression tests using `/TestCaseFilter:"FullyQualifiedName~BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread|FullyQualifiedName~BuildPumpHarness_DoesNotCreateTheWebViewChildHandles"` against `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` with `/InIsolation` and `/Logger:trx`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/named-regression-tests.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording each test's outcome. Acceptance: the TRX records exactly 2 executed tests, 2 passed, 0 failed, and 0 skipped. A skipped or not-run test is a failure of this task, not a pass. +- [ ] [P3-T5] Run the eight pump-hosted consumer tests using `/TestCaseFilter:"FullyQualifiedName~ThroughThePumpHost|FullyQualifiedName~WithFaultingWebViewSeam|FullyQualifiedName~WithInjectedSeams"` against `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` with `/InIsolation` and `/Logger:trx`. This is where the known side effect surfaces: forcing the handle flips `Theme.cs:433` `_lblItemNumber.InvokeRequired` and `ViewerSetup.cs:361` `_itemViewer.InvokeRequired` from `false` to `true` on off-pump evaluation, so those paths now marshal instead of running inline. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/consumer-tests.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` listing each test name and outcome. Acceptance: the recorded failed count is 0 and the recorded executed count is at least 8. +- [ ] [P3-T6] Run the thirteen `WinFormsPumpHostTests` self-tests using `/TestCaseFilter:"FullyQualifiedName~WinFormsPumpHostTests"` against `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` with `/InIsolation` and `/Logger:trx`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/pumphost-selftests.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` listing each test name and outcome. Acceptance: the TRX records exactly 13 executed tests, 13 passed, 0 failed, and 0 skipped. +- [ ] [P3-T7] Run `QfcItemController_SeamFactoryTests` and `QfcItemController_InitializationTests` in the **same** invocation using `/TestCaseFilter:"FullyQualifiedName~QfcItemController_SeamFactoryTests|FullyQualifiedName~QfcItemController_InitializationTests"` against `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` with `/InIsolation` and `/Logger:trx`, so class-level parallelization exercises the shared `UiThreadDispatcherGate`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/gate-serialization.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the per-class pass counts and total wall-clock duration. Acceptance: the recorded failed count is 0, both class names appear in the TRX with at least one passed test each, and no test is recorded as failing on its `[Timeout]`. A `[Timeout]`-attributed failure here indicates the unmitigated gate cascade rather than the handle race and must be recorded as such before any retry. +- [ ] [P3-T8] Assert the diff of the three touched files introduces no prohibited timing construct. Run `git diff --unified=0 $MergeBase -- QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` and search the added lines only for each of the four literals `Thread.Sleep`, `Task.Delay`, `SpinWait`, and `PumpTimeoutMs =`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/no-timing-hacks.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording one count per literal. Acceptance: the counts for `Thread.Sleep`, `Task.Delay`, and `SpinWait` in added lines are each exactly 0; the count for `PumpTimeoutMs =` in added lines is exactly 0; and file inspection confirms `PumpTimeoutMs = 60000` in `QfcItemController.InitializationTests.cs`, `QfcItemController.ViewerSetupTests.cs`, and `QfcItemController.SeamFactoryTests.cs`, and `TimeoutMs = 30000` in `WinFormsPumpHostTests.cs`, each retaining its current value. The four literals are quoted verbatim here so the search is over text this plan states rather than over paraphrase. + +### Phase 4 — Determinism Verification + +- [ ] [P4-T1] Start a CPU load generator so the ten-run determinism record is captured under contention, matching the #511 observation conditions. Start `[Environment]::ProcessorCount - 1` background PowerShell jobs, each running a pure busy loop with no sleep and no file I/O, then sample utilization with `Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 1 -MaxSamples 5`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/load-generator-start.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the job count and the five sampled utilization values. Acceptance: the recorded job count equals `[Environment]::ProcessorCount - 1` and the mean of the five sampled utilization values is at least 80. The load generator is test-harness scaffolding, not test code; it introduces no sleep, retry, or timing tolerance into any test and creates no temporary file. +- [ ] [P4-T2] Under that load, run the full nine-assembly suite ten consecutive times with the resolved `vstest.console.exe`, the nine assembly paths from the canonical assembly list, `/EnableCodeCoverage`, `/InIsolation`, `/Logger:trx`, and `/TestCaseFilter:"TestCategory!=LiveOutlook"`, writing each run's TRX under `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/`. Acceptance: ten distinct TRX files exist and each records a failed count of exactly 0. A single failing run fails this task; do not re-run to obtain a tenth green result without recording every attempt. +- [ ] [P4-T3] Stop the load generator: sample `Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 1 -MaxSamples 5` once more before stopping, then `Stop-Job` and `Remove-Job` every job started in P4-T1. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/load-generator-stop.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the pre-stop utilization samples and the post-stop job count. Acceptance: the recorded mean pre-stop utilization is at least 80, confirming load was sustained across the whole ten-run window, and the recorded post-stop job count is exactly 0. +- [ ] [P4-T4] Assert `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` and `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` are recorded as passed in each of the ten P4-T2 TRX files. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/named-tests-ten-runs.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` carrying a ten-row table with one column per named test. Acceptance: the table has exactly ten rows and every cell reads passed. +- [ ] [P4-T5] Assert `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` and `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` are recorded as passed in each of the same ten TRX files. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/regression-tests-ten-runs.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` carrying a ten-row table with one column per test. Acceptance: the table has exactly ten rows and every cell reads passed. +- [ ] [P4-T6] Consolidate the determinism record into `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/determinism-ten-runs.2026-08-21T18-10.md`, carrying `Timestamp:`, the ten TRX paths, per-run total and failed counts, per-run wall-clock duration, the sustained CPU-utilization figures from P4-T1 and P4-T3, and a direct comparison against the P1-T5 pre-fix table. Acceptance: the artifact exists, names all ten TRX paths, and states the pre-fix and post-fix outcomes for both named tests side by side using measured values only. + +### Phase 5 — Final QC Loop + +This loop is unconditional. Every command-bearing task below must execute its stated command and record the result; `SKIPPED` is not a valid completion state for any of them. If any step fails or changes files, restart from P5-T1. + +- [ ] [P5-T1] Run `dotnet tool restore` from the worktree root. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-tool-restore.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `EXIT_CODE: 0`. +- [ ] [P5-T2] Apply formatting **scoped to the three touched files** with `dotnet tool run csharpier format QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`. The mutating pass is deliberately scoped: a repo-wide `format .` would rewrite unrelated files and break the three-file scope-lock acceptance conditions. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-csharpier-format.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording how many of the three files the formatter rewrote. Acceptance: `EXIT_CODE: 0` and the artifact records the rewritten-file count. If the count is greater than 0, restart the loop from P5-T1 after this task completes. +- [ ] [P5-T3] Verify formatting read-only with `dotnet tool run csharpier check .` from the worktree root. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-csharpier-check.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the unformatted-file count and, if non-zero, whether every reported file is one of the three touched files or a pre-existing condition recorded in P0-T9. Acceptance: no file among the three touched files is reported as unformatted. +- [ ] [P5-T4] Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` from the worktree root, capturing the log to `coverage\analyzer-final.log`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-analyzer-gate.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the warning count, the error count, and the count of log lines matching `Skipping target "CoreCompile"`. Acceptance: `EXIT_CODE: 0`, the recorded error count is 0, and the recorded `Skipping target "CoreCompile"` count is exactly 0. +- [ ] [P5-T5] Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true'` from the worktree root, capturing the log to `coverage\nullable-final.log`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-nullable-gate.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count and the count of log lines matching `Skipping target "CoreCompile"`. Acceptance: `EXIT_CODE: 0`, the recorded error count is 0, the recorded `Skipping target "CoreCompile"` count is exactly 0, and the artifact confirms the command carried no `/p:Nullable=enable`. +- [ ] [P5-T6] Run the full nine-assembly suite once with the resolved `vstest.console.exe`, the nine assembly paths from the canonical assembly list, `/EnableCodeCoverage`, `/InIsolation`, `/Logger:trx`, and `/TestCaseFilter:"TestCategory!=LiveOutlook"`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-suite-run.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording total, passed, failed, and skipped counts and the TRX path. Acceptance: the recorded failed count is exactly 0 and the recorded total is at least the total recorded in P0-T12 plus 2, accounting for the two new regression tests. +- [ ] [P5-T7] Capture post-change numeric coverage with `pwsh -NoProfile -File .\scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SearchRoot . -CoverageOutput coverage\final.cobertura.xml` from the worktree root, then read the root `line-rate` and `branch-rate`, the `QuickFiler` package `line-rate`, and the `line-rate` of every Cobertura class whose `filename` begins `QuickFiler\Controllers\QfcItemController`. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-coverage.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording all four figures as numeric percentages to two decimal places, plus a copy of `coverage\final.cobertura.xml`. Acceptance: the script reported exactly 9 discovered test assemblies and no coverage field is empty or contains the token `UNVERIFIED`. +- [ ] [P5-T8] Verify the coverage delta and thresholds. Write `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/coverage-delta.2026-08-21T18-10.md` with `Timestamp:` and a table carrying, for each of the four measured figures, the baseline value from P0-T13, the post-change value from P5-T7, and the signed delta; plus a changed-line coverage row stating the coverage of the lines added by this change. Record explicitly that `QuickFiler/Viewers/ItemViewer.cs` carries a whole-type `[ExcludeFromCodeCoverage]` at line 20, so the fixture change moves no coverage into or out of the denominator and the obligation is no regression rather than an increase. Acceptance: the `QuickFiler` package `line-rate` delta is greater than or equal to 0, every `QfcItemController` class `line-rate` delta is greater than or equal to 0, and every cell in the table holds a numeric value rather than a placeholder. +- [ ] [P5-T9] Re-audit the 500-line cap **after** the final formatting pass, because CSharpier can add lines. Count the lines returned by `Get-Content -LiteralPath` for each of `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs`, and `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`, and record all three in `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-file-size-audit.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: each of the three recorded counts is less than 500. +- [ ] [P5-T10] Record the clean-pass attestation in `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/final-clean-pass.2026-08-21T18-10.md` with `Timestamp:` and, for each of P5-T1 through P5-T9, the command run and its exit code, plus the number of loop restarts performed and the reason for each. Acceptance: the artifact records that P5-T1 through P5-T9 all completed without failure and without changing files in a **single** consecutive pass. If any earlier step changed a file or failed, the loop was restarted from P5-T1 and only the final consecutive pass may be recorded as the clean one. + +### Phase 6 — Acceptance Criteria and Audit Handoff + +- [ ] [P6-T1] File the follow-up GitHub issue for #511's visible-window half with `gh issue create`, titled for the re-attribution to `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs`, whose body records that the only enabled call showing a real top-level `Form` in the nine-assembly corpus is `viewer.Show()` at `ProgressViewer_Tests.cs:73` on a `ProgressViewer : Form`, that a headless helper `CreateHeadlessViewer` already exists at `ProgressViewer_Tests.cs:33-34`, that the re-attribution is a code reading rather than a reproduced observation, and that `epic.md` forbids any child of this epic from writing under `docs/features/potential/`. Mirror the posted text to `docs/features/active/winformspumphost-suite-determinism-511/evidence/issue-updates/followup-progressviewer.2026-08-21T18-10.md` with `Timestamp:`, the exact posted text, `PostedAs: body`, and the issue URL. Acceptance: the mirror artifact exists and records a concrete issue number and URL; if `gh` is unavailable, the artifact carries a `POSTING BLOCKED` header with the reason and this task is not checked off. +- [ ] [P6-T2] Update `docs/features/active/winformspumphost-suite-determinism-511/spec.md`, section `## Rollout & Follow-up`, required follow-up item 1, replacing the instruction to record the number with the concrete issue number filed in P6-T1. Acceptance: the spec's item 1 names a concrete issue number and no longer contains an unfilled instruction to record one. +- [ ] [P6-T3] Check off spec AC 1 (`InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` passes in every one of ten consecutive full nine-assembly runs, with the ten TRX results stored under `evidence/regression-testing/`) in `spec.md`, citing the P4-T4 artifact and the ten TRX paths. Acceptance: exactly one AC checkbox changes state and the evidence pointer resolves to an existing file. +- [ ] [P6-T4] Check off spec AC 2 (`InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` passes in every one of those same ten runs), citing the P4-T4 artifact. Acceptance: exactly one AC checkbox changes state and the evidence pointer resolves to an existing file. +- [ ] [P6-T5] Check off spec AC 3 (the ten consecutive full nine-assembly runs are executed under induced CPU load and are all green), citing the P4-T1, P4-T2, P4-T3, and P4-T6 artifacts. Acceptance: exactly one AC checkbox changes state and the cited artifacts record a sustained mean utilization of at least 80 across the run window. +- [ ] [P6-T6] Check off spec AC 4 (an empirical pre-fix baseline artifact records, per run across ten runs, the pass or fail outcome of both named tests and the observed harness viewer `IsHandleCreated` value), citing the P1-T5 artifact. Acceptance: exactly one AC checkbox changes state and the cited artifact's table has twenty rows with no empty cell. +- [ ] [P6-T7] Check off spec AC 5 (`BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` exists in `Part3.cs`, asserts the harness viewer's `IsHandleCreated` is `true` before the act, and passes), citing the P3-T4 artifact. Acceptance: exactly one AC checkbox changes state. +- [ ] [P6-T8] Check off spec AC 6 (`BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` exists in `Part3.cs`, asserts both WebView2 children remain handle-less, and passes), citing the P3-T4 artifact. Acceptance: exactly one AC checkbox changes state. +- [ ] [P6-T9] Check off spec AC 7 (zero diff hunks in `QfcItemController.Initialization.cs` and `QfcItemController.ViewerSetup.cs`, with the de-exemption blocks intact). Verify by running `git diff --numstat $MergeBase -- QuickFiler/Controllers/QfcItemController.Initialization.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and confirming it produces zero output lines, and by confirming `QfcItemController.Initialization.cs` still contains at least 7 occurrences of the literal `#230`, `QfcItemController.ViewerSetup.cs` still contains the literal `#230` at its de-exemption block near line 254, and `QfcItemController.ViewerSetup.cs` still contains the literal `ExcludeFromCodeCoverage` at its retained exemption block near line 41. Record all findings in `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/deexemption-intact.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the numstat output line count is exactly 0, the `#230` count in `Initialization.cs` is at least 7, and exactly one AC checkbox changes state. The three searched literals `#230` and `ExcludeFromCodeCoverage` are quoted verbatim here and are single-line tokens present in the tracked tree. +- [ ] [P6-T10] Check off spec AC 8 (all 21 pump-host call sites pass in the final run: the 13 self-tests plus the 8 consumer tests), citing the P3-T5, P3-T6, and P5-T6 artifacts. Acceptance: exactly one AC checkbox changes state and the cited artifacts record 13 passed self-tests and at least 8 passed consumer tests with zero failures. +- [ ] [P6-T11] Check off spec AC 9 (the diff lists exactly three code files, all under `QuickFiler.Test/`, and no file under `QuickFiler/`, no `*.csproj`, and no path under `.claude/`). Verify by running `git diff --name-only $MergeBase`, filtering the result to paths ending `.cs`, `.csproj`, `.props`, `.targets`, or `.config`, and confirming the filtered set is exactly the three paths `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs`, and `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`; then confirming zero paths in the unfiltered list begin `QuickFiler/`, zero end `.csproj`, and zero begin `.claude/` other than paths beginning `.claude/agent-memory/`. Record the counts in `docs/features/active/winformspumphost-suite-determinism-511/evidence/qa-gates/scope-lock-final.2026-08-21T18-10.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the filtered set has exactly 3 members matching those three paths, the three prohibited counts are each 0, and exactly one AC checkbox changes state. **Plan-level clarification recorded against the spec's wording:** the `.claude/agent-memory/` carve-out is added because that subtree is tracked agent bookkeeping outside the fix; if the executor wrote nothing there, the carve-out changes nothing and the count is 0 either way. +- [ ] [P6-T12] Check off spec AC 10 (`QfcItemController_SeamFactoryTests` and `QfcItemController_InitializationTests` both pass in the same run, and `UiThreadDispatcherGate` and `SwapUiThreadDispatcher` retain their acquire-and-release structure), citing the P3-T7 and P2-T2 artifacts. Acceptance: exactly one AC checkbox changes state and both cited artifacts record their conditions met. +- [ ] [P6-T13] Check off spec AC 11 (every changed file is under 500 lines after the change), citing the P5-T9 artifact. Acceptance: exactly one AC checkbox changes state and the cited artifact records three counts each less than 500. +- [ ] [P6-T14] Check off spec AC 12 (the diff introduces no `Thread.Sleep`, `Task.Delay`, `SpinWait`, retry loop, or raised timeout constant, and every existing timeout constant retains its value), citing the P3-T8 artifact. Acceptance: exactly one AC checkbox changes state and the cited artifact records four zero counts plus the four confirmed constant values. +- [ ] [P6-T15] Check off spec AC 13 (the five-step toolchain completes green in a single final pass, coverage is captured under `evidence/qa-gates/`, and measured `QuickFiler` line coverage is at least the pre-fix baseline), citing the P5-T10, P5-T7, and P5-T8 artifacts. Acceptance: exactly one AC checkbox changes state and the cited coverage-delta artifact records a `QuickFiler` line-rate delta greater than or equal to 0. +- [ ] [P6-T16] Check off spec AC 14 (`## Rollout & Follow-up` records #511's visible-window half as out of scope with its re-attribution and names the filed follow-up issue number), citing the P6-T1 mirror artifact and the P6-T2 spec edit. Acceptance: exactly one AC checkbox changes state and the spec names a concrete issue number. +- [ ] [P6-T17] Write the acceptance-criteria status summary to `docs/features/active/winformspumphost-suite-determinism-511/evidence/other/ac-status-summary.2026-08-21T18-10.md` with `Timestamp:` and one row per criterion carrying the criterion number, its verbatim first line, its state, and its evidence artifact path. Acceptance: the summary has exactly 14 rows, every row names an artifact path that resolves to an existing file, and the row states agree with the checkbox states in `spec.md`. +- [ ] [P6-T18] Commit every source and evidence change on `bug/winformspumphost-suite-determinism-511` with a message naming issues #511 and #571, then confirm the tree is clean. Acceptance: `git status --porcelain` produces zero output lines, and `git diff --name-only $MergeBase` still satisfies the P6-T11 scope-lock conditions after the commit. +- [ ] [P6-T19] Hand off to feature review with an evidence index listing every artifact path produced by Phases 0 through 6, the branch name, the merge-base sha from P0-T6, the head sha after P6-T18, and the four residual conditions the spec records as stated rather than fixed: the unmitigated MSTest `[Timeout]` and `UiThreadDispatcherGate` cascade, residual CPU-contention sensitivity, #511's re-attributed visible-window half, and the `InvokeBeginInvoke` production asymmetry. Write the index to `docs/features/active/winformspumphost-suite-determinism-511/evidence/other/review-handoff.2026-08-21T18-10.md` with `Timestamp:`. Acceptance: the index exists, every listed artifact path resolves to an existing file, and all four residual conditions are named. No pull-request creation and no CI monitoring is performed by this plan; both are handled outside it. + +--- + +## Residual Conditions Recorded, Not Claimed Fixed + +1. **The MSTest `[Timeout]` and `UiThreadDispatcherGate` cascade is not fixed here.** MSTest records a `[Timeout]` failure on a `Task`-returning test without aborting the continuation, so a timed-out pump test has not yet run its `finally` and therefore has not released the process-wide gate or reverted `UtilitiesCS.UiThread._dispatcher`. `[DoNotParallelize]` on the two classes is a candidate mitigation with no timing content, but it is outside the minimal fix. If P4-T2 cannot produce ten green runs, this is the first suspect and the finding is recorded rather than worked around. +2. **Residual CPU-contention sensitivity is stated, not claimed fixed.** Running real message pumps under sustained high load remains inherently slower. Retaining the pump-hosted coverage is a deliberate trade: the alternative buys a pump-free suite at the cost of eight coverage justifications and a rewrite this child is neither scoped nor permitted to perform. +3. **#511's visible-window half is re-attributed, not fixed here.** It is filed as its own issue in P6-T1 and is deliberately not an acceptance criterion of this feature. +4. **The `InvokeBeginInvoke` production asymmetry is not addressed.** Adding an `InvokeRequired` guard there is a production behaviour change, would make the pump-hosted `Initialize(bool)` test pass without exercising a real `Control.Invoke`, and its natural test home has three lines of headroom. It belongs to its own issue. diff --git a/docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md b/docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md new file mode 100644 index 000000000..3025977fe --- /dev/null +++ b/docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md @@ -0,0 +1,859 @@ +# WinFormsPumpHost Suite Determinism — Research (#511 + #571) + +Timestamp: 2026-08-21T18-20 + +Feature: `winformspumphost-suite-determinism-511` (epic child 1 of 4, +`quickfiler-suite-determinism-foundation`) + +Scope of this document: research only. No source file, project file, configuration file, or +`.claude/**` file was modified. No build and no test run was executed. Every claim below is +grounded in a file read or a grep against the worktree at +`C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a5bd77000d205e542`, or is explicitly +labelled as documented framework behaviour or as an open question. + +Paths in this document are repository-relative for readability. The absolute root for every one of +them is the worktree path above. + +--- + +## Executive summary + +1. **#511's "visible window" is not attributable to `WinFormsPumpHost` or to anything in this + feature's blast radius.** `Application.Run(new ApplicationContext())` with no `MainForm` shows + nothing, `QuickFiler.Test/Form1.cs` is never instantiated, and the `ItemViewer`'s WebView2 + children never receive a window handle. The only enabled test in the whole nine-assembly corpus + that shows a real top-level `Form` is `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73` + (`viewer.Show()` on `ProgressViewer : Form`). That is a different assembly and a different + defect. Confidence: high. +2. **#571's root cause is a single unguarded `Control.Invoke`.** Every other Control-marshalling + call reached during initialization is guarded by `InvokeRequired`, which returns `false` for a + handle-less control; `QfcItemController.InvokeBeginInvoke` + (`QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs:248-258`) is the only one that is + not. That, and only that, is why the two synchronous `Initialize` paths fail while the three + asynchronous ones do not. +3. **Recommended direction:** deterministically create the `ItemViewer`'s window handle on the pump + thread inside the shared harness (read `viewer.Handle`), following the maintainer-ratified + in-repo precedent at `Tags.Test/TagControllerRendering.StaTests.cs:37-48`. Do not replace the + pump with a synchronization-context seam, and do not change `InvokeBeginInvoke`'s production + shape in this child. +4. **The epic's coverage-justification line numbers have NOT drifted** — all seven cited positions + are exact. What is wrong is that the list is *incomplete*: there are seven de-exemption comment + blocks in `QfcItemController.Initialization.cs`, not five. +5. **A hard, previously unrecorded constraint:** + `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` is **497 lines** — three + lines below the 500-line cap. It is the natural home for a `ToggleTips`/`InvokeBeginInvoke` + regression test and it is effectively full. Combined with the ban on editing + `QuickFiler.Test.csproj`, this materially constrains where regression tests can live and argues + against the production-guard remedy. + +--- + +## Q1 — What creates the visible window that #511 reports? + +**Answer: nothing in this feature's blast radius. Confidence: high for the pump host and the +QuickFiler test assembly; medium-high for the WebView2 sub-question.** + +### Q1.1 The pump host itself creates no visible window + +`QuickFiler.Test/TestSupport/WinFormsPumpHost.cs:295-346` (`RunPumpThread`) does exactly three +things that touch WinForms: + +- `:303-305` installs a `WindowsFormsSynchronizationContext`. +- `:323` subscribes `Application.ThreadException`. +- `:325-326` `applicationContext = new ApplicationContext(); Application.Run(applicationContext);` + +The `ApplicationContext` is constructed with the parameterless constructor, so its `MainForm` is +`null`. `Application.Run(ApplicationContext)` shows a window only through `context.MainForm`; with +no `MainForm` there is no window to show. No `Form`, `UserControl`, or `Control` is created +anywhere in `WinFormsPumpHost.cs` — grep for `new Form`, `.Show()`, `.ShowDialog()` returns only +the XML-doc mention at `:12` and the `Application.Run` call at `:326`. + +Two **invisible** windows are created on the pump thread as a side effect, and neither is a desktop +window: + +- A WPF message-only dispatcher window, because `Dispatcher.CurrentDispatcher` is touched on the + pump thread (`WinFormsPumpHost.cs:245` `Dispatcher.FromThread(_thread)`; + `QuickFiler/Viewers/ItemViewer.cs:28` `_uiDispatcher = Dispatcher.CurrentDispatcher;`). +- The WinForms *parking window*, if and when any parentless child control's handle is created on + that thread. It is never shown. + +### Q1.2 `QuickFiler.Test/Form1.cs` is dead — confirmed + +`Form1` appears in `QuickFiler.Test` only at `QuickFiler.Test/Form1.cs:5,7`, +`QuickFiler.Test/Form1.Designer.cs:3,195,202,203`, the `.csproj` compile/resource entries +(`QuickFiler.Test/QuickFiler.Test.csproj:161,164,165,180,181`), and the stale +`QuickFiler.Test/QuickFiler.Test.csproj.bak`. There is **no construction site**. Ground truth +confirmed; this remains #491's scope, not ours. + +A second dead form-ish declaration exists: `QuickFiler.Test/Controllers/QfcHomeControllerTests.cs:243` +declares `public class QfcFormViewerDerived : QfcFormViewer` with a `Show()` override at `:248`. +Grep for `QfcFormViewerDerived` across all `*.cs` returns only the declaration at `:243` and its +constructor at `:245` — it is never instantiated, so it never shows anything. (Reporting it because +it is adjacent to #491's scope; it is not ours to remove.) + +### Q1.3 The WebView2 controls do not produce a window in these tests + +`QuickFiler/Viewers/ItemViewer.Designer.cs` constructs two +`Microsoft.Web.WebView2.WinForms.WebView2` controls at `:46` and `:49`, wraps them in +`ISupportInitialize.BeginInit()`/`EndInit()` at `:89-90` and `:6166-6167`, and adds them to the +table-layout panel at `:116` and `:119`. Their only event wiring is +`ItemViewer.Designer.cs:256` → `ItemViewer.cs:166-169`, whose entire body is +`Console.WriteLine("Parent Changed")`. + +In the pump-hosted tests the browser process is unreachable: the harness injects a +`Mock` whose `CreateEnvironmentAsync` and `EnsureCoreWebView2Async` both +throw `WebViewSentinelException` +(`QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs:261-281`). More +fundamentally, a WinForms `Control` has **no HWND at all** until its handle is created, and nothing +in the initialization path creates the `ItemViewer`'s handle (see Q2), so the WebView2 children have +no window either. + +Residual uncertainty (why this is "medium-high", not "high"): the WebView2 WinForms control's +implicit-initialization trigger (`ISupportInitialize.EndInit`, `OnParentChanged`, +`OnVisibleChanged`, `OnHandleCreated`) is third-party code that is not in this repository and that I +could not read. If any of those paths creates a visible window on a *handle-less, parentless* +control, that would contradict the finding. This is recorded in Open Questions with a cheap +verification. + +### Q1.4 `WpfDispatcherYieldTests` creates no window + +#511 names this suite. It is at `UtilitiesCS.Test/OutlookObjects/Folder/WpfDispatcherYieldTests.cs` +(class at `:13`), and its `StaDispatcherHost` is at `:172-199`. That host runs +`System.Windows.Threading.Dispatcher.Run()` on an STA thread (`:183`) and creates no `Form` and no +`Control`. Note that #511's own text points at +`UtilitiesCS.Test/Threading/WpfUiDispatcherTests.cs` for the analogue; a `StaDispatcherHost` does +exist there at `:161`, and seven more copies exist elsewhere +(`TaskMaster.Test/AppGlobals/AppOlObjectsFolderTreeServiceLifecycleTests.cs:334`, +`UtilitiesCS.Test/EmailIntelligence/FilterOlFoldersControllerInitializationTests.cs:347`, +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderTreeServiceInvalidationTests.cs:404`, +`.../OutlookFolderTreeServiceDisposalTests.cs:409`, `.../OutlookFolderTreeServiceConcurrencyTests.cs:133`, +`.../OutlookFolderHierarchyReaderTests.cs:402`, `.../FolderTreeSnapshotBuilderYieldTests.cs:118`). +None of them creates a window. + +### Q1.5 What the visible window IS attributable to + +A repository-wide grep for `.Show()` / `.ShowDialog()` / `Application.Run(` across every `*Test*` +project yields exactly one enabled call that shows a real top-level `Form`: + +``` +UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73 viewer.Show(); +``` + +inside `[TestMethod] CancelSource_WhenAssigned_EnablesButtonAndCancelsSameSourceOnClick` +(`:40-41`), on a real `ProgressViewer` constructed at `:49`. `ProgressViewer` is a `Form`: +`UtilitiesCS/Threading/ProgressViewer.cs:16` — `public partial class ProgressViewer : Form`. The +class is `[STATestClass]` (`:30`), so `Show()` executes on a real STA thread and produces a genuine +desktop window. The test never calls `Hide()`; it disposes the viewer in `finally` (`:89-92`), so +the window is transient but real. + +Every other candidate is exonerated: + +| Candidate | Verdict | Evidence | +| --- | --- | --- | +| `UtilitiesCS.Test/ResourceTests.cs:21,29,112` (`frm.ShowDialog()`) | Not run | `[Ignore("Interactive form smoke test; excluded from unattended test runs.")]` at `:17`, `:25`, `:108` | +| `UtilitiesCS.Test/EmailIntelligence/FilterOlFoldersControllerRefreshDisposalTests.cs:133,188` (`viewer.Show()`) | Fake, not a Form | `RecordingFilterViewer : IFilterOlFoldersViewer` at `FilterOlFoldersControllerInitializationTests.cs:420`; `Show()` is a counter | +| `UtilitiesCS.Test/Threading/ProgressPane_Tests.cs:68,118,176` (`new ProgressPane()`) | `UserControl`, never shown | `UtilitiesCS/Threading/ProgressPane.cs:15` — `: UserControl` | +| `QuickFiler.Test/QfcViewer_Test.cs:27,43,58,61,67` | All commented out | leading `//` on every line | +| `MyBox`/`InputBox`/`NotImplementedDialog` `DialogInvoker = viewer => viewer.ShowDialog()` | Seam assignment, viewer is a double | e.g. `UtilitiesCS.Test/Dialogs/MyBoxModelessTests.cs:49` asserts "the real viewer.Show() must never be called in a test" | +| `ProgressTrackerPane` | Not a control | `UtilitiesCS/Threading/ProgressTrackerPane.cs:9` — `: IProgress<(int, string)>` | + +**Consequence for the spec.** #511's Actual-Behavior bullet "A visible window appeared during the +run, because the host constructs a real WinForms control and pumps a real message loop" states a +causal claim that the evidence does not support. This child cannot honestly close that bullet by +changing `WinFormsPumpHost`. The spec should: + +- scope #511 to the *load-flakiness* half of the report, which is real and is this child's to fix; +- record the visible-window finding as an evidence-based re-attribution to + `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73`; and +- promote that re-attribution to its own issue through the promotion lifecycle rather than leaving + it as prose in a feature folder that disappears at merge. + +A one-line fix exists for the re-attributed defect (`ProgressViewer` construction is already +headless-capable via `CreateHeadlessViewer` at `ProgressViewer_Tests.cs:33-34`), but it is in +`UtilitiesCS.Test`, outside this child's declared file set, and belongs to the separate issue. + +--- + +## Q2 — Why do only 2 of the 6 pump-hosted tests fail? + +**Answer: because `Control.InvokeRequired` returns `false` for a handle-less control, and every +Control-marshalling call on the initialization paths is guarded by it except +`QfcItemController.InvokeBeginInvoke`.** + +### Q2.1 The failing call site + +`QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs:248-258`: + +```csharp +public void InvokeBeginInvoke(bool async, System.Action action) +{ + if (async) + { + _itemViewer.BeginInvoke(action); + } + else + { + _itemViewer.Invoke(action); + } +} +``` + +`ToggleTips` at `:202-217` is the only caller reached during initialization (`:204` +`InvokeBeginInvoke(async, ...)`). + +Documented framework behaviour (`System.Windows.Forms.Control`): + +- `Control.Invoke` **and** `Control.BeginInvoke` both throw + `InvalidOperationException("Invoke or BeginInvoke cannot be called on a control until the window + handle has been created.")` when no control in the target's parent chain has a created handle. + The `async == true` branch is therefore *not* safe either; it is simply never taken by the tests + that fail. +- `Control.InvokeRequired` searches up the parent chain for a control with a window handle and + **returns `false` when none is found**. This is the documented behaviour, not an implementation + detail. + +### Q2.2 Per-test trace + +`_itemViewer` is `IItemViewer` (`QuickFiler/Controllers/QfcItemController.cs:51`), bound in the +harness to a real `QuickFiler.ItemViewer` constructed on the pump thread at +`QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs:84`. It is never +parented to a `Form` and its handle is never forced, so `IsHandleCreated` is `false` for the whole +test. + +| # | Test | file:line | Entry point | Reaches `Control.Invoke`? | Why | +| --- | --- | --- | --- | --- | --- | +| 1 | `InitializeSequentialAsync_ThroughThePumpHost_CompletesAndInitializesState` | `Part3.cs:40` | `InitializeSequentialAsync()` (`Initialization.cs:295`) | **No** | `SetThemeLight(async: true)` → `Theme.SetQfcTheme(true)` → `_uiDispatcher.InvokeAsync` (`Theme.cs:431`), the injected inline dispatcher. Tips use `ToggleTipsAsync` (`Initialization.cs:318`-region → `FocusAndTheme.cs:219`), which awaits `tip.ToggleAsync` and never touches `Control.Invoke`. | +| 2 | `InitializeGraphicsAsync_ThroughThePumpHost_CompletesAndAppliesDarkTheme` | `Part3.cs:83` | `InitializeGraphicsAsync()` (`Initialization.cs:263`) | **No** | `SetThemeDark(async: false)` (`Initialization.cs:279`) → `Theme.SetQfcTheme(false)` → the `else if (_lblItemNumber.InvokeRequired)` guard at `Theme.cs:433` evaluates **false** (no handle anywhere), so the `else` at `:437-440` calls `SetQfcTheme()` inline. Tips/nav use the `Async` variants. | +| 3 | `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` | `Part3.cs:131` | `Initialize(bool async: false)` (`Initialization.cs:168`) | **YES — fails** | `Initialization.cs:185` `ToggleTips(async: false, ...)` → `FocusAndTheme.cs:204` → `:256` `_itemViewer.Invoke(action)`, unguarded. | +| 4 | `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` | `Part3.cs:175` | private nine-arg `Initialize(...)` (`Initialization.cs:138`) with `async: false` | **YES — fails** | `Initialization.cs:161` `Initialize(async);` funnels into case 3. | +| 5 | `InitializeAsync_ThroughThePumpHost_RunsToTheMockedWebViewSeamAndFaults` | `Part3.cs:245` | `InitializeAsync()` (`Initialization.cs:202`) | **No** | `SetThemeDark/Light(async: true)` (`Initialization.cs:216-219`) → `_uiDispatcher.InvokeAsync`. Tips/nav use the `Async` variants. Execution stops at the mocked web-view seam. | +| 6 | `ResolveControlGroupsAsync_ThroughThePumpHost_PopulatesTipsAndControlGroups` | `ViewerSetupTests.cs:426` | `ResolveControlGroupsAsync(ItemViewer)` (`ViewerSetup.cs:258`) | **No** | The member only awaits `itemViewer.UiSyncContext` (`:269`) and builds `QfcTipsDetails`; no `Control.Invoke`. | + +This confirms the epic's Hard Constraint 3 claim ("only the two synchronous `Initialize` paths +reach `Control.Invoke`") and supplies the missing mechanism: **it is not that the async paths use +`BeginInvoke` instead — `BeginInvoke` would throw identically. It is that they never call +`InvokeBeginInvoke` at all, and the one sibling that does marshal synchronously +(`Theme.SetQfcTheme(false)`) is `InvokeRequired`-guarded.** + +### Q2.3 The guard pattern is the repository's own convention + +Two production call sites in the same execution path already use it: + +- `UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs:433` — `else if (_lblItemNumber.InvokeRequired)`. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:361` — `if (_itemViewer.InvokeRequired)` + inside `AssignControls`, which is on the `Initialize(bool)` path via `PopulateControls` + (`ViewerSetup.cs:313-318`). + +Both are reached *before* `ToggleTips` in `Initialize(bool)`, and both take the non-marshalling +branch. `InvokeBeginInvoke` is the sole outlier. + +### Q2.4 A contradiction I could not resolve + +Static reading predicts that tests 3 and 4 fail on **every** run, because `Control.Invoke` throws +unconditionally when no handle exists and I found no code path in +`ResolveControlGroups` → `SetupThemes` → `PopulateControls` that creates one: + +- `ResolveControlGroups` (`ViewerSetup.cs:208-252`) uses `GetAllChildren` + (`UtilitiesCS/Extensions/WinFormsExtensions.cs:146-158`), which only walks `Control.Controls`. +- `QfcTipsDetails` (`UtilitiesCS/HelperClasses/ToolTips/QfcTipsDetails.cs`) contains no + `Handle`/`CreateControl`/`CreateGraphics` reference; its only `Invoke` mentions are commented out + at `:46-49` and `:94-97`. +- `QfcThemeHelper.SetupThemes` (`QuickFiler/Helper Classes/QfcThemeHelper.cs:36-93`) only captures + control references into a `QfcThemeControlSet`. +- `AssignControls` (`ViewerSetup.cs:358-...`) sets `Text`/colour properties, which WinForms caches + without creating a handle. + +#571 nevertheless records "run 1 passed both tests, run 2 failed both, run 3 passed both", and +class-isolated runs passing 9 of 9 every attempt. Either (a) some third-party path — most plausibly +the WebView2 control's `ISupportInitialize.EndInit` or implicit-initialization logic — creates the +`ItemViewer`'s handle non-deterministically, or (b) the recorded observation attributes a different +failure mode to these two names. See Open Questions. **This does not change the recommendation**: +forcing the handle removes the dependency in the passing direction whichever explanation holds. + +--- + +## Q3 — Candidate remedies for #571, evaluated + +Common evaluation axes: deterministic handle; visible window; production behaviour change; +pump-hosted coverage preserved; interaction with `UiThreadDispatcherGate` +(`Part2.cs:51`, acquired at `:67`, released at `:74` and `:341`). + +### The `.Handle` versus `CreateControl()` distinction (load-bearing — stated precisely) + +- **`Control.Handle` (getter).** Documented: reading `Handle` forces creation of the control's + window handle if it does not already exist. It creates **only that control's** handle. For a + parentless child control, WinForms parks the new HWND on the thread's hidden parking window; the + parking window is never shown, so nothing becomes visible. In-repo precedent, explicitly + maintainer-ratified: `Tags.Test/TagControllerRendering.StaTests.cs:39-41` + + ```csharp + // Act: force invisible handle creation, then invoke the real draw path. + var handle = checkBox.Handle; + handle.Should().NotBe(IntPtr.Zero); + ``` + + with the class doc at `:12-17` stating "an unshown WinForms `CheckBox` control (never a `Form`) + is constructed on an STA thread; the test never shows a window, uses no message + pump/timer/sleep, and disposes the control." A second precedent is + `UtilitiesCS.Test/EmailIntelligence/OSBrowser_Tests.cs:233` — `_ = browser.Handle;`. + +- **`Control.CreateControl()`.** Documented: it does **not** create the handle if the control's + `Visible` property is `false`. That caveat does **not** save us here — a parentless `UserControl` + reports `Visible == true` (the visibility walk terminates at the control itself when there is no + parent), so `CreateControl()` *would* create the handle. The real objection is different and + stronger: `CreateControl()` **recurses into every visible child control** and additionally fires + `OnCreateControl`. On `ItemViewer` that means creating handles for both + `Microsoft.Web.WebView2.WinForms.WebView2` controls (`ItemViewer.Designer.cs:46,49`), which is + exactly the third-party surface Q1.3 flags as unverified. In-repo precedent exists + (`UtilitiesCS.Test/OutlookObjects/Store/StoreWrapperViewerTests.cs:113`) but on a much simpler + control tree. + + **Summary: `.Handle` forces creation regardless of `Visible` and touches only the one control; + `CreateControl()` is `Visible`-gated and recursive. For `ItemViewer`, `.Handle` is strictly the + narrower instrument.** + +### Evaluation table + +| # | Remedy | Deterministic handle | Visible window | Production change | Pump coverage preserved | Gate interaction | +| --- | --- | --- | --- | --- | --- | --- | +| **(a)** | Read `viewer.Handle` on the pump thread in `BuildPumpHarnessCoreAsync` (and in `ViewerSetupTests.cs:432-435`) | **Yes** — documented, unconditional | **No** — parked on the hidden parking window; ratified precedent | **None** | **Yes, all 8** | None. Runs inside the section already serialized by the gate. | +| (b) | `viewer.CreateControl()` | Yes here (parentless `UserControl` is `Visible`) | No, same parking mechanism | None | Yes | None | +| (c) | Parent the viewer to a hidden `Form` created on the pump thread | Yes (force the `Form`'s handle) | No if never `Show()`n — `Form` is created with `Visible == false` | None | Yes | None, but adds a `Form` to dispose in `Restore` | +| (d) | `Application.Run(new ApplicationContext { MainForm = hiddenForm })` | Yes | **Likely YES** — `Application.Run(ApplicationContext)` makes `MainForm` visible when the loop starts | None | Yes | None | +| (e) | Add an anchor control to `WinFormsPumpHost` generically | Yes for the anchor, **not** for `ItemViewer` — `FindMarshalingControl` walks *parents*, and the viewer is not parented to the anchor | No | None | Yes | None | +| (f) | Make `InvokeBeginInvoke` consult `InvokeRequired`/`IsHandleCreated` | N/A — removes the need | No | **Yes** | Yes | None | + +### Discussion + +**(a) is the recommendation.** It is the narrowest change that removes the race; it is confined to +test-support code; it changes no production line; it preserves all eight consumer tests and all +seven coverage justifications; and it has an explicit, maintainer-ratified in-repo precedent whose +comment already asserts the no-visible-window property. + +**(b)** is acceptable but strictly wider than (a) for no benefit, and it drags the two WebView2 +controls into handle creation. Reject on minimality. + +**(c)** works and is arguably the most faithful simulation of production (in production the viewer +*is* parented). It costs a `Form` that must be created, tracked, and disposed on the pump thread in +`PumpHarness.Restore` (`Part2.cs:331-342`), and it widens the blast radius of a shared fixture that +two test classes depend on. Reject as second choice, not as wrong. + +**(d)** should be rejected. `Application.Run(ApplicationContext)` starts the message loop and makes +`context.MainForm` visible; that is how `Application.Run(Form)` shows a window at all. Adopting it +would *introduce* the visible window that #511 complains about. Confidence: medium-high; if a +future author wants it, the visibility behaviour must be verified empirically first. + +**(e)** does not work for the stated purpose and this is worth recording so it is not re-proposed. +`Control.Invoke` resolves its marshaling control by walking the target's **parent chain**. An +anchor control owned by the host is not an ancestor of the harness's `ItemViewer`, so the viewer +still has no handle in its chain and still throws. (e) would only help if the host *parented* every +consumer's control, which is remedy (c) in disguise. + +**(f)** is the one remedy that fixes the production asymmetry rather than the test. It is +attractive on the merits — `InvokeBeginInvoke` is the only unguarded marshaller in the class, and +`Theme.cs:433` plus `ViewerSetup.cs:361` establish the house pattern. It is nevertheless **not +recommended for this child**, for four reasons: + +1. It changes production behaviour in a bug-fix child whose mandate is test determinism. On a + handle-less control the guard would silently run UI mutation on the calling thread instead of + throwing; that is a real behavioural change, not an annotation. +2. It would make the pump-hosted `Initialize(bool)` test pass **without ever exercising a real + `Control.Invoke`**, which is a coverage regression in substance even if not in line count — + precisely the outcome epic Hard Constraint 3 exists to prevent. +3. The `IItemViewer` UI-thread seam consolidation is explicitly out of scope: epic.md:75-77 assigns + `IItemViewer`/`ItemViewer` rework (#489) to the third epic's ItemViewer child. +4. **Its natural test home is full.** + `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` is 497 lines against the + 500-line cap. See Q6. + +If a later reviewer disagrees, (f) should be raised as its own issue against `InvokeBeginInvoke`, +not folded in here. + +### Why (a) is not a prohibited timing hack + +`.claude/rules/csharp.md:95` prohibits "Adding sleeps, retries, or timing hacks to mask flaky +behavior." The distinguishing test is *whether the race still exists after the change*: + +- A sleep, a retry, or a timing tolerance leaves the race in place and lowers the probability of + observing it. The failure remains reachable; only its frequency changes. +- Reading `viewer.Handle` on the pump thread before the act **eliminates the precondition of the + failure**. After it, `IsHandleCreated` is `true` unconditionally and for the whole lifetime of + the fixture, on every machine, at every load level. There is no residual window in which the + test can fail for this reason, so there is nothing left to mask. + +It is also not a wall-clock wait, not probabilistic, and not order-dependent — the three properties +the determinism rules in `.claude/rules/general-unit-test.md` ("Determinism Infrastructure") care +about. `Tags.Test/TagControllerRendering.StaTests.cs:12-17` records that the maintainer already +accepted exactly this reasoning for exactly this instrument. The spec must nevertheless state this +reading explicitly, because #571's own "Suspected Cause / Notes" (`:99-103`) asserts the opposite +("Adding a sleep, a retry, or a handle-forcing call would violate the 'Prohibited Behaviors' +section"). That sentence is the one place where the promoted record and the epic disagree; the +epic's reading (epic.md:117-121) governs, and this document supplies the argument it asks for. + +### Known side effect of (a) that the plan must anticipate + +Forcing the `ItemViewer`'s handle flips currently-`false` `InvokeRequired` guards to `true` whenever +they are evaluated off the pump thread. Two are on the paths under test: + +- `Theme.cs:433` `_lblItemNumber.InvokeRequired` — evaluated during + `InitializeGraphicsAsync`'s `SetThemeDark(async: false)`, which resumes on a thread-pool thread + after `await Task.Run(...)` (`Initialization.cs:266-275`). It will now marshal to the pump + thread via `_lblItemNumber.Invoke` (`Theme.cs:435`) instead of running inline. +- `ViewerSetup.cs:361` `_itemViewer.InvokeRequired` in `AssignControls`, reached from + `PopulateControlsAsync` → `AssignControlsAsync` (`ViewerSetup.cs:342-356`). + +Both should succeed, because a live pump is precisely what the fixture provides, and both become +*more* production-faithful. But this is a genuine behaviour change in the tests and is the most +likely source of a surprise during execution. The plan should treat "tests 1, 2, 5, 6 still pass +after the handle is forced" as an explicit acceptance criterion, not an assumption. + +--- + +## Q4 — Blast radius of changing `WinFormsPumpHost` + +The epic's count of "eight consumer tests plus thirteen self-tests" is **verified exact**. + +### Consumers (8) + +| # | Test method | file:line | Uses `BuildPumpHarnessAsync`? | Host construction | +| --- | --- | --- | --- | --- | +| 1 | `InitializeSequentialAsync_ThroughThePumpHost_CompletesAndInitializesState` | `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs:40` | Yes (`:47`) | `:43` | +| 2 | `InitializeGraphicsAsync_ThroughThePumpHost_CompletesAndAppliesDarkTheme` | `...Part3.cs:83` | Yes (`:90`) | `:86` | +| 3 | `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` | `...Part3.cs:131` | Yes (`:138`) | `:134` | +| 4 | `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` | `...Part3.cs:175` | Yes (`:183`) | `:179` | +| 5 | `InitializeAsync_ThroughThePumpHost_RunsToTheMockedWebViewSeamAndFaults` | `...Part3.cs:245` | Yes (`:252`) | `:248` | +| 6 | `CreateSequentialAsync_WithInjectedSeams_ReturnsAnInitializedController` | `QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs:305` | Yes (`:313`) | `:308` | +| 7 | `CreateAsync_WithFaultingWebViewSeam_FaultsWithThatExceptionAfterInitializing` | `...SeamFactoryTests.cs:376` | Yes (`:384`) | `:379` | +| 8 | `ResolveControlGroupsAsync_ThroughThePumpHost_PopulatesTipsAndControlGroups` | `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs:426` | **No** — builds its own viewer at `:432-435` | `:429` | + +Consumer 8 is the outlier that matters for remedy (a): it does **not** go through +`BuildPumpHarnessAsync`, so it does not take `UiThreadDispatcherGate` and it will not receive a +forced handle if the change is made only in `BuildPumpHarnessCoreAsync`. It also does not currently +need one (Q2 row 6), but leaving it asymmetric is a latent trap. The plan should either force the +handle in both places or record explicitly why consumer 8 is exempt. + +### Self-tests (13), all in `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` + +`:32`, `:59`, `:88`, `:115`, `:153`, `:183`, `:218`, `:270`, `:302`, `:334`, `:367`, `:395`, `:416`. + +### Which assertions would break under a host change + +I read all 13. **None asserts the absence of a handle**, and none asserts on any pump-host internal +beyond its public surface. What they do assert: + +- `:38-50` — `SyncContext` is non-null and is a `WindowsFormsSynchronizationContext`; `ThreadId` + differs from the MSTest thread. *Unaffected by any remedy.* +- `:74`, `:101`, `:134-140`, `:168`, `:194-199`, `:230`, `:249-255` — work runs on `host.ThreadId`. + *Unaffected.* +- `:284-287`, `:317-320`, `:349-352` — exception identity and message from faulted work. + *Unaffected.* +- `:380-386` — every posting member faults with `ObjectDisposedException` after `StopAsync`. + *Unaffected by (a); would need review under (d)/(e) because shutdown ordering changes.* +- `:405` — `Dispose` is idempotent. *Unaffected by (a); under (d) the `MainForm` closing would + itself end the loop, changing this path.* +- `:437-440` — `StopAsync` rethrows an exception recorded by `Application.ThreadException`. + *Unaffected by (a); under (d)/(e) a `MainForm`/anchor changes what the loop owns at shutdown.* + +**Conclusion: remedy (a) has a blast radius of zero on the 13 self-tests and touches one shared +fixture method plus, optionally, one standalone test's arrange block.** Remedies (d) and (e) would +require re-reading the shutdown self-tests. This asymmetry is a further argument for (a). + +--- + +## Q5 — Coverage justifications that must not be deleted (re-derived) + +**Correction to the delegation premise: these line numbers have NOT drifted.** All seven cited +positions in the epic and in my instructions are exact against the current worktree. What is wrong +is that the enumeration is *incomplete* — `QfcItemController.Initialization.cs` carries **seven** +de-exemption comment blocks, not five. The epic's five are exactly the lines on which the literal +string `WinFormsPumpHost` appears; two further blocks depend on the pump seam without naming it. + +### `QuickFiler/Controllers/QfcItemController.Initialization.cs` + +| Block | Lines | Member (line) | Quote (first line) | Depends on | +| --- | --- | --- | --- | --- | +| A **(not in the epic list)** | 135-137 | private nine-arg `Initialize` (`:138`) | "#230: de-exempted. The overload funnels into Initialize(bool); the former barrier was the missing WinForms message pump for that body, not headless construction. Covered by QfcItemController_InitializationTests.InitializeNineArgOverload_ThroughThePumpHost_*." | Consumer **4** — one of the two failing tests | +| B | 164-167 (epic cites `:166`) | `Initialize(bool async)` (`:168`) | "#230: de-exempted. The orchestration runs against a real ItemViewer and its tail dispatches InitializeWebViewAsync through the viewer's WPF dispatcher; both require a live message loop, which the WinFormsPumpHost test seam supplies. Covered by QfcItemController_InitializationTests.InitializeBool_ThroughThePumpHost_*." | Consumer **3** — the other failing test | +| C **(not in the epic list)** | 196-201 | `InitializeAsync()` (`:202`) | "#230: de-exempted. The former barrier was the missing WinForms message pump for this orchestration, not headless construction. Covered by QfcItemController_InitializationTests.InitializeAsync_ThroughThePumpHost_*, which runs every line and asserts the controlled fault at the mocked web-view seam." | Consumer **5** | +| D | 259-262 (epic cites `:261`) | `InitializeGraphicsAsync()` (`:263`) | "#230: de-exempted. The former barrier was the missing WinForms message pump, not headless construction: the orchestration marshals through the concrete ItemViewer's WinForms context. The WinFormsPumpHost test seam supplies that loop, so the member is covered by ...InitializeGraphicsAsync_ThroughThePumpHost_*." | Consumer **2** | +| E | 291-294 (epic cites `:293`) | `InitializeSequentialAsync()` (`:295`) | same wording as D, "...covered by ...InitializeSequentialAsync_ThroughThePumpHost_*." | Consumer **1** | +| F | 403-408 (epic cites `:404`) | `CreateAsync(...)` (`:409`) | "#230: de-exempted. The optional seam parameters below give the factory the injection point it previously lacked, and the WinFormsPumpHost test seam supplies the message loop InitializeAsync needs. Covered by QfcItemController_SeamFactoryTests.CreateAsync_WithFaultingWebViewSeam_*..." | Consumer **7** | +| G | 447-450 (epic cites `:448`) | `CreateSequentialAsync(...)` (`:451`) | "#230: de-exempted. The optional seam parameters below give the factory the injection point it previously lacked, and the WinFormsPumpHost test seam supplies the message loop InitializeSequentialAsync needs. Covered by QfcItemController_SeamFactoryTests.CreateSequentialAsync_WithInjectedSeams_*." | Consumer **6** | + +### `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` + +| Block | Lines | Member (line) | Nature | Depends on | +| --- | --- | --- | --- | --- | +| H | 30-40, with `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` at `:41` (epic cites `:31`) | `InitializeWebViewAsync()` (`:42`) | **A RETAINED exemption**, not a de-exemption. Quote: "Residual, retained. #230 resolved the pump barrier: the `await _itemViewer.UiSyncContext` on line 55 is now drainable by the WinFormsPumpHost test seam, and tests do reach the IWebViewCoreInitializer seam call. The RESIDUAL barrier is the ((ItemViewer)_itemViewer).L0v2h2_WebView2.CoreWebView2 dependency below..." | Consumers 5 and 7 reach the seam call; the attribute stays | +| I | 254-257 (epic cites `:256`) | `ResolveControlGroupsAsync(ItemViewer)` (`:258`) | De-exemption. Quote: "#230: de-exempted. The former barrier was the missing WinForms message pump - the member awaits itemViewer.UiSyncContext, which never resumes on a thread-pool MSTest thread. The WinFormsPumpHost test seam supplies that loop, so the member is now covered by QfcItemController_ViewerSetupTests.ResolveControlGroupsAsync_ThroughThePumpHost_*." | Consumer **8** | + +Note on H: the internal reference "on line 55" inside the comment is still accurate — +`ViewerSetup.cs:55` is `CoreWebView2EnvironmentOptions options = new("–incognito ");` and the +`await _itemViewer.UiSyncContext;` is at `:58`. That internal cross-reference is off by three and +should be left alone (it is out of scope and rewriting it invites churn), but a reader should not +be surprised by it. + +**Evidence value.** Every one of the eight pump-hosted consumer tests is the named coverage evidence +for at least one de-exempted production member. Deleting, `[Ignore]`-ing, or reclassifying any of +them out of the unit suite invalidates the corresponding comment and re-opens the exemption +question for that member. That is the concrete content of epic Hard Constraint 3, and it is why +#511's literal proposed remedy ("replace the real pump with an injectable synchronization-context +seam", `511.md:62`) must not be executed as written. + +Also note that `QuickFiler/Viewers/ItemViewer.cs:20` carries a whole-type +`[ExcludeFromCodeCoverage]`, so the viewer itself contributes nothing to the coverage denominator; +the entire coverage value of the pump-hosted tests is in `QfcItemController`. + +--- + +## Q6 — The 500-line cap + +The cap is stated in `.claude/rules/general-code-change.md` ("File Size Limit ... No production +code, test code, or reusable script file may exceed **500 lines**") and in `CLAUDE.md` § General +Code Change Policy 4.1. + +### Verified current sizes and headroom + +| File | Last line | Headroom to 500 | +| --- | --- | --- | +| `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` | **497** | **3** | +| `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs` | 467 | 33 | +| `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` | 443 | 57 | +| `QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs` | 436 | 64 | +| `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` | 409 | 91 | +| `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` | 290 | **210** | +| `QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs` | 326 | 174 | +| `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` | 482 | 18 | + +`QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` at **482 lines** is a new finding worth flagging: +only 18 lines of headroom in the host itself. Remedies (d) and (e), which add members to the host, +have very little room; remedy (a), which does not touch the host at all, has none of that problem. + +### No wildcard include — confirmed + +`QuickFiler.Test/QuickFiler.Test.csproj` is a legacy non-SDK project with explicit `` +entries only. Grep for `Compile Include="**` and for `*.cs` glob patterns returns nothing; the +relevant entries are literal paths: + +``` +145: +146: +147: +159: +160: +``` + +**Therefore no new test file can be added without editing the csproj, and the csproj is off-limits +for this child** (epic.md:136-139: "#511/#571 and #445 add no compile entry"). + +### Can the regression tests fit? + +Yes, comfortably, **provided they are placed in `Part3.cs`**. + +Recommended placement, with 210 lines of headroom in `Part3.cs`: + +1. `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` — asserts + `harness.Viewer.IsHandleCreated` is `true` and, queried from the pump thread, + `harness.Viewer.InvokeRequired` is `false`. ~35 lines with the required XML doc. +2. `InitializeBool_ThroughThePumpHost_ReachesControlInvokeWithoutThrowing` — a focused regression + for #571 asserting that `ToggleTips(async: false, ...)` through `InvokeBeginInvoke` completes. + ~40 lines. (Arguably subsumed by existing consumer 3, but a named regression test is what the + Bugfix Workflow in `CLAUDE.md` requires.) +3. Optionally `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` — asserts the two WebView2 + children remain handle-less after the fix, pinning the minimality property from Q3. ~30 lines. + +Total ≈ 105 lines against 210 available. `WinFormsPumpHostTests.cs`'s 57 lines of headroom are +**not** needed under remedy (a), because the host is unchanged. That is a further argument for (a): +under (d)/(e) the host would change, self-tests would need to be added, and 57 lines is thin for +two documented MSTest methods. + +**If a future decision requires touching `InvokeBeginInvoke` (remedy (f)), its natural test home +`FocusAndThemeTests.cs` has 3 lines of headroom and cannot absorb a test.** The options would be to +put the test in `Part3.cs` (acceptable but poorly located), or to split `FocusAndThemeTests.cs`, +which requires a csproj compile entry and is therefore blocked for this child. This is a concrete, +independent reason to defer (f) to its own issue. + +--- + +## Q7 — Load-flakiness beyond the handle + +**Answer: the missing handle explains #571 but does NOT fully explain #511. There is a second, +independent load-sensitivity, and it is an amplifier rather than a root cause.** + +### Timeout inventory + +| Constant | Value | file:line | Applied to | +| --- | --- | --- | --- | +| `PumpTimeoutMs` | **60000** (60 s) | `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.cs:38` | `Part3.cs:39,82,130,174,244` | +| `PumpTimeoutMs` | **60000** | `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs:34` | `:425` | +| `PumpTimeoutMs` | **60000** | `QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs:293` | `:304,375` | +| `TimeoutMs` | **30000** (30 s) | `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs:24` | all 13 self-tests | + +**Judgement on adequacy.** 60 s for a pump-hosted initialization and 30 s for a host self-test are +generous in absolute terms and are documented as harness bounds, not waits +(`InitializationTests.cs:32-37`, `WinFormsPumpHostTests.cs:16-20`). Under sustained ~96% CPU with +coverage instrumentation attached, a full `ItemViewer` construction plus `MailItemHelper` +materialization plus theme setup is not obviously inside 60 s, but I have no measurement and will +not assert one. The values are defensible; the **failure mode when they fire** is the problem. + +### No sleeps, no retries, no wall-clock waits — confirmed + +Grep for `Thread.Sleep`, `Task.Delay`, `SpinWait` in `WinFormsPumpHost.cs` and +`WinFormsPumpHostTests.cs` returns nothing. This is consistent with `BannedSymbols.txt` enforcement +described in `.claude/rules/csharp.md` § Analyzer Stack. The host's waits are all on deterministic +signals. + +### Genuine blocking waits (all unbounded, all on completion signals) + +| Site | Call | Thread | Risk | +| --- | --- | --- | --- | +| `WinFormsPumpHost.cs:60` | `_ready.Wait()` | MSTest | Blocks until the pump thread sets `_ready` in a `finally` (`:315`). Unbounded, but the `finally` is unconditional. | +| `WinFormsPumpHost.cs:65` | `_thread.Join()` (startup-failure path) | MSTest | Reached only when `_initializationError != null`, which returns immediately at `:318-321`. | +| `WinFormsPumpHost.cs:240` | `StopAsync().GetAwaiter().GetResult()` in `Dispose` | MSTest | **Sync-over-async.** Safe only because the host's contract (`:22-24`) guarantees no `SynchronizationContext` is installed on the MSTest thread. Exercised by `WinFormsPumpHostTests.cs:35` (`using`) and `:401`. | +| `WinFormsPumpHost.cs:264` | `_thread.Join()` in `StopCoreAsync` | Continuation | Unbounded; depends on the loop having exited, which `_stopped.Task` at `:263` already proves. | +| `Part2.cs:67` | `UiThreadDispatcherGate.WaitAsync()` | MSTest | **Unbounded and process-wide.** See below. | + +### The real load amplifier: `[Timeout]` plus the process-wide gate + +`UiThreadDispatcherGate` (`Part2.cs:51`) is a `SemaphoreSlim(1, 1)` acquired at `:67` and released +in exactly two places: the catch block at `:72-76` (construction failure) and +`PumpHarness.Restore` at `:341`, which every consumer calls from `finally`. + +MSTest's `[Timeout]` on a `Task`-returning test does **not** abort the test's continuation; it +records a failure and moves on while the underlying task keeps running. Consequences under load: + +1. A pump test that overruns 60 s is reported failed, but its `finally` — and therefore + `Restore()`, the `SwapUiThreadDispatcher` rollback (`Part2.cs:139-149,339`), and the gate release + — has not yet run. +2. The next pump test in either `QfcItemController_InitializationTests` or + `QfcItemController_SeamFactoryTests` blocks on `WaitAsync()` at `:67` for up to its own 60 s. +3. Meanwhile the timed-out test's `Restore()` may fire mid-flight and revert the process-wide + static `UtilitiesCS.UiThread._dispatcher` out from under the newly started test — the exact + hazard the gate's own doc comment at `Part2.cs:36-46` describes. + +That is a **cascade**: one load-induced overrun converts into several correlated failures, which +matches #511's report that six full-suite attempts were needed for one clean baseline far better +than a single flaky assertion would. + +Two aggravating details: + +- `QfcItemController_ViewerSetupTests.ResolveControlGroupsAsync_ThroughThePumpHost_*` + (`ViewerSetupTests.cs:426`) constructs its own `ItemViewer` on its own pump host and **never takes + the gate**, so it runs concurrently with the gated tests under class-level parallelization. It + does not swap the static dispatcher, so it is not a correctness hazard today, but it does add a + third live message pump and a third full `ItemViewer` control tree to the process under load. +- Nine test assemblies share one testhost process under + `vstest.console.exe ... /InIsolation`, so the pump threads compete with everything else in the + suite for CPU. + +### What this child can and cannot fix + +- Fixing the handle (Q3 remedy (a)) removes #571 entirely and removes one whole class of #511's + failures. +- The `[Timeout]`/gate cascade is **not** fixed by the handle. Mitigating it properly means either + (i) making the gate release exception-safe against a non-running `finally` — which MSTest's + timeout semantics make hard — or (ii) serializing the pump-hosted tests at the framework level + rather than with a semaphore, e.g. by `[DoNotParallelize]` on the classes that share the static. + Option (ii) is a small, honest change with no timing content and is worth costing in the spec. +- The remaining CPU-contention sensitivity of running three real message pumps under 96% load is + inherent to keeping the pump-hosted coverage. It should be stated as a residual, not silently + claimed as fixed. + +--- + +## Reconciliation of #511 and #571 + +### Recommended direction + +**Keep the real message pump. Make the fixture deterministic. Re-scope #511's visible-window claim +to the evidence.** + +Concretely: + +1. In `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, + `BuildPumpHarnessCoreAsync` (`:79-132`), immediately after the viewer is constructed on the pump + thread at `:84`, force the viewer's window handle **on the pump thread** by reading + `viewer.Handle` inside the same `host.InvokeAsync` factory (or a second `InvokeAsync`), with a + comment citing `Tags.Test/TagControllerRendering.StaTests.cs:39-41` and stating why this is not a + timing hack. +2. Apply the same one line to the standalone consumer at `ViewerSetupTests.cs:432-435`, or record + why it is exempt. +3. Add the regression tests to `Part3.cs` (210 lines of headroom), not to a new file. +4. Consider `[DoNotParallelize]` on `QfcItemController_InitializationTests` and + `QfcItemController_SeamFactoryTests` to close the `[Timeout]`/gate cascade of Q7. +5. Change **no** production file. Change **no** `.claude/**` file. Change **no** `.csproj`. +6. Re-attribute #511's visible-window symptom to + `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73` and promote it to its own issue through + the promotion lifecycle. Do not claim a fix this child cannot make. + +### Argument for + +- It is the only direction that satisfies epic Hard Constraint 3: all eight pump-hosted consumer + tests survive, and all nine coverage-justification blocks (seven de-exemptions in + `Initialization.cs`, one de-exemption and one retained exemption in `ViewerSetup.cs`) stay true. +- It removes the race rather than reducing its probability, which is the distinction + `.claude/rules/csharp.md:95` actually draws. +- It has an in-repo, maintainer-ratified precedent for the exact instrument, with an explicit + no-visible-window assertion attached (`Tags.Test/TagControllerRendering.StaTests.cs:12-17,39-41`). +- Its blast radius on the 13 self-tests is zero, and it needs none of + `WinFormsPumpHost.cs`'s scarce 18 lines of headroom. +- It is one line of behaviour in a shared fixture, which is the smallest change that can work. + +### Argument against the main alternative + +The main alternative is #511's literal proposal: **replace the real pump with an injectable +synchronization-context / dispatcher seam, and move any irreducible cases out of the unit suite** +(`511.md:62`). + +Against it: + +- **It deletes the evidence it is supposed to protect.** Every one of the nine justification blocks + named in Q5 says, in terms, that the member is covered *because the pump seam supplies a live + message loop*. Replacing the pump with a fake context makes `await _itemViewer.UiSyncContext` + (`ViewerSetup.cs:269`, `:58`) resume on a synthetic context, which is a different behaviour from + the one those members were de-exempted for. Reclassifying the tests out of the unit suite deletes + the coverage outright. +- **The seam it proposes already exists and is already used.** `IItemViewer` re-declares + `InvokeRequired`, `Invoke`, `BeginInvoke` at `QuickFiler/Viewers/IItemViewer.cs:135-137` + specifically for mockability, and `UtilitiesCS.Threading.IUiDispatcher` is held at + `QfcItemController.cs:66`. Both are exercised without any pump at + `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs:99-115` + (`BuildExecutingViewer`) and throughout `QfcItemController.ConversationTests.cs` (e.g. `:216`, + `:329`). The pump-hosted tests exist *precisely because* the seam-mocked tests do not exercise the + concrete `ItemViewer` control tree. Adding a third seam would duplicate coverage that already + exists while destroying coverage that does not. +- **It does not fix the thing it was filed for.** Q1 shows the visible window is not the pump's. + Replacing the pump would leave `ProgressViewer_Tests.cs:73` showing a window on every full-suite + run. +- **Cost and risk are an order of magnitude higher.** It rewrites a 482-line test-support type with + 18 lines of headroom, touches all eight consumers and all 13 self-tests, and would require csproj + edits that this child is forbidden to make. + +The honest statement of the trade: the alternative buys a suite with no real message loop, which is +genuinely more robust under CPU contention, at the cost of nine coverage justifications and a +rewrite that this child is not scoped or permitted to perform. If the maintainer later decides the +pump must go, that is a separate, larger piece of work and it must be preceded by an explicit +decision about what happens to the de-exempted members — not folded into a determinism fix. + +--- + +## Line-number drift corrections + +Every `file:line` citation from `511.md`, `571.md`, and `epic.md`, checked against the worktree. + +| Source | Citation | True current position | Status | +| --- | --- | --- | --- | +| `571.md:61` | `QfcItemController.FocusAndTheme.cs:256` (`_itemViewer.Invoke`) | `:256` | **Exact** | +| `571.md:63,90` | `QfcItemController.FocusAndTheme.cs:204` (`ToggleTips` → `InvokeBeginInvoke`) | `:204` | **Exact** | +| `571.md:86` | `QfcItemController.FocusAndTheme.cs:256` (`InvokeBeginInvoke` calls `IItemViewer.Invoke`) | method at `:248`, call at `:256` | **Exact** | +| `571.md:95-98` | "a sibling test in the same file already documents this hazard and works around it with a headless `ProgressTrackerPane` built via `FormatterServices.GetUninitializedObject` (see the comment block in `UtilitiesCS.Test/Extensions/AsyncSerialization_Tests.cs`)" | **Premise false.** That file contains no `GetUninitializedObject` call; its only `ProgressTrackerPane` uses are `(ProgressTrackerPane)null!` at `:73` and doc text at `:286,288`. The `GetUninitializedObject` pattern lives at `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:33-34` (`CreateHeadlessViewer`). Also `ProgressTrackerPane` is not a control at all (`UtilitiesCS/Threading/ProgressTrackerPane.cs:9` — `: IProgress<...>`). | **Wrong file; wrong type** | +| `571.md:99-103` | "Adding a sleep, a retry, or a handle-forcing call would violate the 'Prohibited Behaviors' section of `.claude/rules/csharp.md`" | `.claude/rules/csharp.md:95` reads "Adding sleeps, retries, or timing hacks to mask flaky behavior." It does not name handle forcing. `epic.md:117-121` governs and permits it. | **Overstated** | +| `511.md:21,58` | `UtilitiesCS.Test/Threading/WpfUiDispatcherTests.cs` `StaDispatcherHost` | `:161`. The `WpfDispatcherYieldTests` suite it also names is at `UtilitiesCS.Test/OutlookObjects/Folder/WpfDispatcherYieldTests.cs:13`, with its own `StaDispatcherHost` at `:172`. | **Both exist; two different files** | +| `511.md:37` | "A visible window appeared during the run, because the host constructs a real WinForms control and pumps a real message loop" | Causal claim unsupported. See Q1. | **Re-attributed** | +| `epic.md:60` | `QfcItemController.FocusAndTheme.cs:256` | `:256` | **Exact** | +| `epic.md:104,225` | `QfcItemController.Initialization.cs:166, 261, 293, 404, 448` | `:166`, `:261`, `:293`, `:404`, `:448` all land inside the intended comment blocks | **Exact but incomplete** — two further de-exemption blocks at `:135-137` and `:196-201` are omitted | +| `epic.md:105,226` | `QfcItemController.ViewerSetup.cs:31, 256` | `:31` (inside the 30-40 **retained**-exemption block) and `:256` (inside the 254-257 de-exemption block) | **Exact**; note `:31` is a retained exemption, not a de-exemption | +| `epic.md:109` | `QuickFiler/Controllers/QfcItemController.cs:51` (`IItemViewer _itemViewer`) | `:51` | **Exact** | +| `epic.md:112` | `QuickFiler/Controllers/QfcItemController.cs:66` (`IUiDispatcher _uiDispatcher`) | `:66` | **Exact** | +| `epic.md:110` | `QuickFiler/Viewers/IItemViewer.cs:95-100` (`Invoke`/`BeginInvoke`/`InvokeRequired` re-declaration) | **`:135-137`** (`InvokeRequired` `:135`, `Invoke` `:136`, `BeginInvoke` `:137`), inside `#pragma warning disable CS0108` at `:134`-`:139`; `int Height` at `:138` | **Drifted, +40** | +| `epic.md:113` | `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs:99-115` (`BuildExecutingViewer`) | `:99-115` | **Exact** | +| `epic.md:92` | `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs:51` (`UiThreadDispatcherGate`) | `:51` | **Exact** | +| `epic.md:117` | `.claude/rules/csharp.md:95` | `:95` | **Exact** | +| `epic.md:129-131` | `QuickFiler.Test/QuickFiler.Test.csproj:161-165` (Form1 compile) and `:180-181` (Form1.resx) | `:161`, `:164`, `:165`; `:180`, `:181` | **Exact** | +| `epic.md:125` | "116 explicit `` entries" | Not recounted (out of scope); the structural claim — explicit entries, no wildcard — is **verified** | **Structure confirmed** | +| `epic.md:56` | "Eight consumer tests plus thirteen self-tests" | 8 consumers and 13 self-tests, enumerated in Q4 | **Exact** | +| `epic.md:245-247` | `QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs` "exactly at the 500-line cap" | Not verified (outside this child's scope) | **Unchecked** | +| Delegation prompt | "`ItemViewer.cs:21` — `public partial class ItemViewer : UserControl, IItemViewer, IContainerControlLocal`" | `:21`; note the `[ExcludeFromCodeCoverage]` at `:20` | **Exact** | +| Delegation prompt | `Part3.cs` 290 / `Part2.cs` 409 / `WinFormsPumpHostTests.cs` 443 / `FocusAndTheme.cs` 326 | Confirmed (last closing brace on each of those lines) | **Exact** | +| Delegation prompt | "These line numbers have drifted — re-derive them" (re: the Q5 coverage justifications) | **They have not drifted.** All seven positions are exact. | **Premise corrected** | + +Additional drifted citation found in a code comment, recorded for awareness but **out of scope**: +`QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs:119` cites +`Theme.cs:414-432` for `SetQfcTheme(bool)`; the member is now at +`UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs:427-445`. Do not fix it in this child (it is in a +497-line file with three lines of headroom and belongs to no issue here). + +--- + +## Testing implications (no test code written) + +Consistent with `CLAUDE.md` § General Unit Test Policy, `.claude/rules/general-unit-test.md`, and +the C# Unit Test Policy (MSTest + Moq + FluentAssertions, no temporary files). + +1. **Bugfix workflow.** `CLAUDE.md` § Bugfix Workflow requires a failing regression test first. For + #571 the failing test can be written as an assertion on the harness invariant + (`harness.Viewer.IsHandleCreated`), which fails deterministically before the fix and passes + after — a better regression than relying on the intermittent end-to-end symptom. +2. **Placement.** All new tests go in + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` (210 lines of + headroom). No new file, therefore no `.csproj` edit. +3. **Scenario coverage** for the new fixture behaviour: positive (handle created on the pump + thread), boundary (`InvokeRequired` is `false` when queried *on* the pump thread and `true` when + queried off it), and minimality (the WebView2 children remain handle-less). The negative case — + `Control.Invoke` throwing without a handle — should **not** be added as a new test, because it + would assert framework behaviour rather than repository behaviour. +4. **Regression scope for execution.** All eight consumer tests and all 13 self-tests must be run + and must pass, not just the two named in #571, because forcing the handle flips + `InvokeRequired` guards on the other paths (Q3, "Known side effect"). +5. **Determinism evidence for #511.** The leading indicator in `epic.md:13` is ten consecutive + green full-suite runs under induced CPU load. That is an evidence artifact, and it belongs under + `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/` + per the evidence-location invariant. No `artifacts/` sub-path other than + `artifacts/orchestration/` may hold it. +6. **Run command.** `vstest.console.exe /EnableCodeCoverage /InIsolation + /TestCaseFilter:"TestCategory!=LiveOutlook"`, with `\.claude\` excluded from recursive + `*.Test.dll` discovery (epic.md:216-221). Omitting `/InIsolation` fabricates roughly 1,695 + phantom failures. +7. **Coverage.** `QuickFiler/Viewers/ItemViewer.cs:20` is `[ExcludeFromCodeCoverage]` for the whole + type, so the fixture change moves no coverage. `QfcItemController` coverage must not regress; + the nine justification blocks of Q5 are the checklist. + +--- + +## Open questions / unverifiable without execution + +I could not run a build or a test. The following are genuinely open. + +1. **The #571 intermittency mechanism is unexplained (highest-value open question).** Static + reading says consumers 3 and 4 must fail on every run, because `Control.Invoke` throws + unconditionally without a handle and I found no handle-creating call in + `ResolveControlGroups` → `SetupThemes` → `PopulateControls`. #571 records them passing on some + runs. Cheapest disambiguation: run + `/TestCaseFilter:"FullyQualifiedName~QfcItemController_InitializationTests"` and, in the same + run, assert `harness.Viewer.IsHandleCreated` at the top of consumer 3. If it is `false` and the + test still passes, my reading of `Control.Invoke` is wrong; if it is sometimes `true`, something + third-party creates the handle and that something must be identified before the fix is called + minimal. **Either outcome leaves remedy (a) correct**; only the *explanation* in the spec + changes. +2. **WebView2 implicit initialization.** Whether + `Microsoft.Web.WebView2.WinForms.WebView2`'s `ISupportInitialize.EndInit`, + `OnParentChanged`, `OnVisibleChanged`, or `OnHandleCreated` can create a window on a parentless, + handle-less control. Third-party code not present in this repository. Verification: after + forcing `viewer.Handle`, assert + `viewer.L0v2h2_WebView2.IsHandleCreated == false` and + `viewer.L0vhBreadcrumb_WebView2.IsHandleCreated == false`. This is proposed as regression test 3 + in Q6. +3. **Whether reading `.Handle` on `ItemViewer` really leaves children handle-less.** This follows + from `Control.CreateHandle` being non-recursive while `Control.CreateControl` is recursive. + High confidence from documented `Control` semantics, but not executed. Same verification as (2). +4. **Whether `Application.Run(ApplicationContext)` shows `context.MainForm`.** Asserted at + medium-high confidence in Q3 remedy (d). Not executed. Only matters if someone revives (d). +5. **Whether `PumpTimeoutMs = 60000` is actually adequate under ~96% CPU with coverage attached.** + No measurement exists. #511 records six attempts for a clean baseline but does not record which + tests failed or with what message. A fresh capture under induced load is called for by + `511.md:41` and should accompany the fix. +6. **MSTest `[Timeout]` semantics on a `Task`-returning test in this exact MSTest version.** The Q7 + cascade argument depends on the timed-out test's continuation surviving and its `finally` + running late. This is the documented behaviour for async MSTest tests, but the specific version + in `QuickFiler.Test` was not confirmed and the cascade was not reproduced. +7. **Whether `[DoNotParallelize]` is available and appropriate.** Proposed in the Reconciliation as + a Q7 mitigation. Its presence in the referenced MSTest version and its interaction with + `UiThreadDispatcherGate` were not verified. +8. **The `ProgressViewer_Tests.cs:73` re-attribution was not reproduced.** It is a code reading + (`ProgressViewer : Form` at `UtilitiesCS/Threading/ProgressViewer.cs:16`, `viewer.Show()` in an + enabled `[STATestClass]` `[TestMethod]`), not an observation of a window on a desktop. Before + the follow-up issue is filed, someone should watch a full-suite run and confirm the window is + the `ProgressViewer`. +9. **Whether the visible window observed on 2026-08-08 was a single event or recurrent.** + `511.md:41` records that no failure log was retained. If the window is not reproducible, the + re-attribution in Q1.5 is the best available explanation but is not the only possible one. diff --git a/docs/features/active/winformspumphost-suite-determinism-511/spec.md b/docs/features/active/winformspumphost-suite-determinism-511/spec.md new file mode 100644 index 000000000..031e6389d --- /dev/null +++ b/docs/features/active/winformspumphost-suite-determinism-511/spec.md @@ -0,0 +1,753 @@ +# winformspumphost-suite-determinism (Spec) + +- **Issue:** #511 (primary) — https://github.com/drmoisan/TaskMaster/issues/511 +- **Secondary Issue:** #571 — https://github.com/drmoisan/TaskMaster/issues/571 +- **Parent (optional):** epic `quickfiler-suite-determinism-foundation` (child 1 of 4, wave 0) +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T18-40 +- **Status:** Approved +- **Version:** 1.0 +- **Work Mode:** `full-bug` +- **Branch:** `bug/winformspumphost-suite-determinism-511` +- **Integration Branch:** `epic/quickfiler-suite-determinism-foundation-integration` + +> Acceptance-criteria authority. Work Mode is `full-bug`, so per the `acceptance-criteria-tracking` +> skill this file is the **sole** authoritative acceptance-criteria source for this feature. No +> `user-story.md` exists for this feature and none is to be created. The atomic plan, execution, and +> feature audit are all measured against the `## Acceptance Criteria` section below. + +> Evidence-location invariant. Every evidence artifact this feature produces goes under +> `docs/features/active/winformspumphost-suite-determinism-511/evidence//`. No `artifacts/` +> sub-path other than `artifacts/orchestration/` may hold evidence. + +## Context + +- **Summary of the bug and its impact.** Two open defects describe one underlying condition in the + `QuickFiler.Test` suite. `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` starts a real WinForms + message pump on a dedicated STA thread by calling `Application.Run(new ApplicationContext())` at + `:325-326` and never adds a form or a control, so no native window handle is ever created on the + pump thread. The pump harness constructs a real `QuickFiler.ItemViewer` (a `UserControl`) on that + thread and never parents it or forces handle creation. Two pump-hosted initialization tests reach + `Control.Invoke` through `QfcItemController.InvokeBeginInvoke` and fail with + `InvalidOperationException: Invoke or BeginInvoke cannot be called on a control until the window + handle has been created` (#571). Separately, the pump-hosted suite has been reported load-flaky: + six full-suite attempts were required to obtain one clean baseline under sustained high CPU load + during issue #438 work on 2026-08-08 (#511). Requirements sources: + `docs/features/potential/promoted/2026-08-08-winformspumphost-tests-load-flaky-visible-window.md` + and `docs/features/potential/promoted/2026-08-15-qfc-item-controller-init-tests-flaky-window-handle.md`. + Primary evidence source: + `docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md`. +- **Observed environment(s).** Windows 11 Pro 10.0.26200; .NET Framework 4.8.1; MSTest executed via + `vstest.console.exe` (VS18 test platform) across nine `*.Test.dll` assemblies with + `/EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`. #511 was observed + through `./scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .`. +- **Customer impact and severity.** No production defect. The affected parties are reviewers and + autonomous agents: a suite that fails on some runs and passes on others trains both to re-run + rather than investigate, and it can fail an otherwise-green protected check at random. #511 is + recorded High; #571 is recorded Medium. +- **First observed date and version(s) impacted.** #511 observed 2026-08-08 during issue #438 + orchestration. #571 observed 2026-08-15 (run 1 passed both named tests, run 2 failed both, run 3 + passed both). Both conditions are pre-existing on `main`; `WinFormsPumpHost` was introduced by + issue #230, which is closed. + +## Repro & Evidence + +- **Steps to reproduce (#571).** Build the solution in Debug, then run the full nine-assembly suite + with `vstest.console.exe /EnableCodeCoverage /InIsolation + /TestCaseFilter:"TestCategory!=LiveOutlook"` and repeat. The two named tests fail on some runs and + pass on others. Running only + `/TestCaseFilter:"FullyQualifiedName~QfcItemController_InitializationTests"` passed 9 of 9 on + every recorded attempt. +- **Steps to reproduce (#511, load-flakiness half).** Drive the machine to sustained high CPU + utilization (observed at approximately 96%), run the full suite with coverage, and repeat. Six + attempts were required for one clean baseline on 2026-08-08. +- **Expected vs actual behavior.** Expected: identical inputs and environment produce identical + results, per `.claude/rules/general-unit-test.md`. Actual: the two named tests fail + intermittently, and the pump-hosted suite as a whole degrades under CPU contention. +- **Logs/screenshots/error snippets.** The #571 stack trace, extracted from the TRX of the failing + 2026-08-15 run: + + ``` + System.InvalidOperationException: Invoke or BeginInvoke cannot be called on a + control until the window handle has been created. + at System.Windows.Forms.Control.MarshaledInvoke(...) + at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args) + at QuickFiler.ItemViewer.QuickFiler.IItemViewer.Invoke(Delegate method) + at QuickFiler.Controllers.QfcItemController.InvokeBeginInvoke(Boolean async, Action action) + in QuickFiler\Controllers\QfcItemController.FocusAndTheme.cs:line 256 + at QuickFiler.Controllers.QfcItemController.ToggleTips(Boolean async, ToggleState desiredState) + in QuickFiler\Controllers\QfcItemController.FocusAndTheme.cs:line 204 + ``` + + No failure log was retained for #511; the observation is recorded in the issue #438 execution + report. A fresh capture under induced load accompanies this fix (see `## Test Strategy`). +- **Frequency / determinism.** #571 is intermittent and correlates with full-suite execution rather + than class-isolated execution. #511's load-flakiness is intermittent and correlates with CPU + contention. #511's visible-window observation is a single recorded event with no retained log; it + is re-attributed rather than reproduced (see `## Root Cause Analysis` and + `## Rollout & Follow-up`). + +## Scope & Non-Goals + +### In scope + +- Deterministic creation of the `ItemViewer` window handle on the pump thread inside the shared + pump harness, so that `Control.Invoke` has an existing handle before any act. +- #571 in full: both named intermittent failures. +- #511's **load-flakiness half**: removing the handle race removes one whole class of the load- + induced failures reported in #511. +- Regression tests for the new fixture invariant, placed in + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`. +- An empirical pre-fix and post-fix determinism record captured as evidence. + +### Out of scope / non-goals + +- **#511's visible-window half is out of scope and is re-attributed.** The evidence does not + support the causal claim in #511's Actual Behavior bullet that the visible window is produced by + `WinFormsPumpHost`. See `## Root Cause Analysis`. This half is recorded under + `## Rollout & Follow-up` as requiring its own issue against + `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs`. It is deliberately **not** an acceptance + criterion of this feature, so that the feature audit does not score it as unmet. +- **#511's literal proposed remedy is rejected**: replacing the real message pump with an injectable + synchronization-context or dispatcher seam. Rationale in `## Root Cause Analysis` and + `## Proposed Fix`. +- **No production file changes.** `QfcItemController.InvokeBeginInvoke` keeps its current shape in + this feature. Making it consult `InvokeRequired`/`IsHandleCreated` is a production behaviour + change and belongs to its own issue. +- **No `IItemViewer` member additions.** The `IItemViewer` UI-thread seam consolidation is issue + #489, assigned by `epic.md` Non-Goals to the third epic's ItemViewer child. +- **No `QuickFiler.Test/QuickFiler.Test.csproj` edit**, therefore no new test file. +- **No `.claude/**` edit.** Rule files are the policy this fix is measured against, not edit + targets. +- **The MSTest `[Timeout]` / `UiThreadDispatcherGate` cascade is not fixed here.** The research + identifies a second, independent load amplifier: MSTest's `[Timeout]` on a `Task`-returning test + records a failure without aborting the continuation, so a timed-out pump test has not yet run its + `finally` and therefore has not released the process-wide `UiThreadDispatcherGate` semaphore or + reverted `UtilitiesCS.UiThread._dispatcher`. `[DoNotParallelize]` on + `QfcItemController_InitializationTests` and `QfcItemController_SeamFactoryTests` is a candidate + mitigation with no timing content, but it is not part of the minimal fix and is recorded as a + follow-up candidate under `## Rollout & Follow-up`. +- **Residual CPU-contention sensitivity is stated, not claimed fixed.** Running real message pumps + under approximately 96% load remains inherently slower; retaining the pump-hosted coverage is a + deliberate trade. + +### Explicitly excluded systems, integrations, or datasets + +- `UtilitiesCS.Test` (all files), including `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs`. +- `QuickFiler.Test/Form1.cs` and its designer and resource entries — owned by sibling child #491. +- The appended `Controllers` compile entry in `QuickFiler.Test/QuickFiler.Test.csproj` — owned by + sibling child #449. +- `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` — 497 lines, three lines of + headroom against the 500-line cap; nothing is added to it. +- `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` — 482 lines, 18 lines of headroom; the chosen + remedy does not touch it. + +## Root Cause Analysis + +### Confirmed root cause of #571 + +`QfcItemController.InvokeBeginInvoke` (`QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs:248`) +reaches `_itemViewer.Invoke(action)` at `:256`. `Control.Invoke` throws +`InvalidOperationException` unless a control in the target's parent chain has a created native +handle. `WinFormsPumpHost.RunPumpThread` calls `Application.Run(new ApplicationContext())` at +`QuickFiler.Test/TestSupport/WinFormsPumpHost.cs:325-326` and never adds a form or control, so no +handle is created on the pump thread; the harness constructs the real `ItemViewer` on that thread +and never parents it. `_itemViewer` is `IItemViewer` (`QuickFiler/Controllers/QfcItemController.cs:51`), +bound to that real viewer, and `IsHandleCreated` is `false` for the whole test. + +Only two of the six pump-hosted tests reach that call: + +- `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` — `Initialize(bool async: false)` + (`Initialization.cs:168`) → `Initialization.cs:185` `ToggleTips(async: false, ...)` → + `FocusAndTheme.cs:204` → `:256`. +- `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` — the private nine-arg + `Initialize` (`Initialization.cs:138`) funnels into the same path at `Initialization.cs:161`. + +The mechanism that exonerates the other four is `Control.InvokeRequired`, which searches the parent +chain for a created handle and returns `false` when none is found. The asynchronous paths do not +call `InvokeBeginInvoke` at all — they marshal through the injected `IUiDispatcher` — and the one +sibling that marshals synchronously, `Theme.SetQfcTheme(false)`, is guarded by +`_lblItemNumber.InvokeRequired` at `UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs:433`. +`ViewerSetup.cs:361` uses the same guard inside `AssignControls`. `InvokeBeginInvoke` is the sole +unguarded marshaller. Note that `Control.BeginInvoke` throws identically on a handle-less control, +so the `async == true` branch is not inherently safe; it is simply never taken by the failing tests. + +### Signals/evidence supporting it + +- The #571 TRX stack trace names `FocusAndTheme.cs:256` and `:204` exactly. +- Static trace of all six pump-hosted tests, recorded as a per-test table in the research artifact + (Q2.2), matches the observed failure set exactly: rows 3 and 4 reach `Control.Invoke`, rows 1, 2, + 5, and 6 do not. +- `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` contains no `new Form`, no `.Show()`, and no + `.ShowDialog()`. + +### Unresolved question — the intermittency mechanism is not explained + +Static reading predicts that both named tests fail on **every** run, because `Control.Invoke` +throws unconditionally without a handle and the research found no handle-creating call anywhere in +the `ResolveControlGroups` → `SetupThemes` → `PopulateControls` path (`ResolveControlGroups` walks +only `Control.Controls`; `QfcTipsDetails` contains no `Handle`/`CreateControl`/`CreateGraphics` +reference; `QfcThemeHelper.SetupThemes` only captures control references; `AssignControls` sets +cached properties). #571 nevertheless records the tests passing on some runs. Two explanations +remain open: + +1. Some third-party path creates the `ItemViewer`'s handle non-deterministically. The prime suspect + is the `Microsoft.Web.WebView2.WinForms.WebView2` control's `ISupportInitialize.EndInit` or + implicit-initialization logic (`QuickFiler/Viewers/ItemViewer.Designer.cs:46,49`, wrapped in + `BeginInit`/`EndInit` at `:89-90` and `:6166-6167`). That code is not present in this repository + and could not be read. +2. The recorded observation attributes a different failure mode to these two test names. + +**This question is deliberately left open and must not be closed by assertion.** The chosen remedy +is correct under either explanation, because forcing the handle removes the dependency in the +passing direction whichever holds. `## Test Strategy` requires that the pre-fix failure behaviour +be established **empirically**, by repeated runs with the observed `IsHandleCreated` value recorded, +rather than asserted from static reading. + +### #511's visible-window symptom is re-attributed, not fixed + +The visible window is **not attributable to anything in this feature's blast radius**: + +- `Application.Run(ApplicationContext)` shows a window only through `context.MainForm`, and the + parameterless `ApplicationContext` constructor leaves `MainForm` null. +- `WinFormsPumpHost` constructs no `Form`, `UserControl`, or `Control` at all. +- `QuickFiler.Test/Form1.cs` has **zero construction sites**; it appears only in its own + declaration, its designer, the project-file entries, and a stale `.csproj.bak`. +- The `ItemViewer`'s two WebView2 children never obtain a handle, because nothing in the + initialization path creates the parent's handle, and the harness injects a + `Mock` whose members throw `WebViewSentinelException`. +- The two windows that *are* created on the pump thread are a WPF message-only dispatcher window + and the WinForms parking window. Neither is a desktop window and neither is shown. + +A repository-wide search across every test project yields exactly one enabled call that shows a +real top-level `Form`: `viewer.Show()` at `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73`, +on a `ProgressViewer` that derives from `Form` (`UtilitiesCS/Threading/ProgressViewer.cs:16`) inside +an `[STATestClass]`. That is a different assembly, outside this epic, and the re-attribution is a +code reading rather than a reproduced observation. Consequently **#511's visible-window half is out +of scope here and must be filed as its own issue**; this feature does not claim a fix it cannot +make. + +### Why #511's literal remedy is rejected + +Replacing the real pump with an injectable synchronization-context seam would re-exempt coverage +that issue #230 deliberately de-exempted, which epic hard constraint 3 forbids. The affected +justifications, enumerated precisely: + +| Block | `QfcItemController.Initialization.cs` line | Member | Named coverage evidence | +| --- | --- | --- | --- | +| A | 135 | private nine-arg `Initialize` (`:138`) | `InitializeNineArgOverload_ThroughThePumpHost_*` — one of the two failing tests | +| B | 164 | `Initialize(bool async)` (`:168`) | `InitializeBool_ThroughThePumpHost_*` — the other failing test | +| C | 196 | `InitializeAsync()` (`:202`) | `InitializeAsync_ThroughThePumpHost_*` | +| D | 259 | `InitializeGraphicsAsync()` (`:263`) | `InitializeGraphicsAsync_ThroughThePumpHost_*` | +| E | 291 | `InitializeSequentialAsync()` (`:295`) | `InitializeSequentialAsync_ThroughThePumpHost_*` | +| F | 403 | `CreateAsync(...)` (`:409`) | `CreateAsync_WithFaultingWebViewSeam_*` | +| G | 447 | `CreateSequentialAsync(...)` (`:451`) | `CreateSequentialAsync_WithInjectedSeams_*` | + +That is **seven** de-exemption blocks, not the five the epic manifest cited; `:135` was omitted and +is the target of one of the two failing tests. There is **one further de-exemption** at +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:254` (member `ResolveControlGroupsAsync` +at `:258`). Correction to the epic's citation: `ViewerSetup.cs:30` with its +`[ExcludeFromCodeCoverage]` attribute at `:41` is a **retained** exemption block, not a +de-exemption; the epic cited `:31`. The seven de-exemption line numbers above are exact against the +current worktree, so the "re-derive every line number" instruction resolved to a completeness +correction rather than a drift correction. + +Every one of the eight pump-hosted consumer tests is the named coverage evidence for at least one +de-exempted production member. Deleting, `[Ignore]`-ing, or reclassifying any of them out of the +unit suite invalidates the corresponding comment and re-opens the exemption question. Two further +arguments against the literal remedy: the seam it proposes **already exists** (`IItemViewer` +re-declares `InvokeRequired`/`Invoke`/`BeginInvoke` at `QuickFiler/Viewers/IItemViewer.cs:135-137` +for mockability, and `IUiDispatcher` is held at `QfcItemController.cs:66`; both are already +exercised pump-free in `QfcItemController.FocusAndThemeTests.cs:99-115`), and it would not fix the +thing it was filed for, because the visible window is not the pump's. + +### Affected components/modules + +- Test support and harness (changed): `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, + `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs`, + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`. +- Production (read, **not** changed): `QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs`, + `QfcItemController.Initialization.cs`, `QfcItemController.ViewerSetup.cs`, + `QuickFiler/Viewers/ItemViewer.cs`, `QuickFiler/Viewers/IItemViewer.cs`, + `UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs`. + +## Proposed Fix + +### Design summary (what changes where): + +**Retain the real message pump; make the test fixture deterministic; re-scope #511's visible-window +claim.** This is the recorded reconciliation decision for the tension the epic assigns this child to +settle, and it is not re-opened by the plan. + +Force invisible window-handle creation for the `ItemViewer` **on the pump thread**, inside the +shared harness, by reading `viewer.Handle`. Two sites: + +1. `BuildPumpHarnessCoreAsync` in + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs`, immediately after + the viewer is constructed on the pump thread, inside the same `host.InvokeAsync` body (or a + second `InvokeAsync` on the same host). +2. The equivalent point in the standalone arrange block of + `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs` near `:426`-`:435`, which + builds its own viewer and does **not** go through `BuildPumpHarnessAsync`. Forcing the handle in + both places keeps the two sites symmetric; leaving only one would be a latent trap even though + that consumer does not currently reach `Control.Invoke`. + +**Prefer reading `.Handle` over calling `CreateControl()`.** Reading `Control.Handle` forces +creation of that control's handle only and is non-recursive. `Control.CreateControl()` is +`Visible`-gated and **recurses into every visible child control**, which on `ItemViewer` would drag +both `Microsoft.Web.WebView2.WinForms.WebView2` controls into handle creation — exactly the +third-party surface the research flags as unverified. `.Handle` is therefore strictly the narrower +instrument. For a parentless child control WinForms parks the new HWND on the thread's hidden +parking window, which is never shown, so nothing becomes visible. + +### This is not a prohibited timing hack + +`.claude/rules/csharp.md` "Prohibited Behaviors" bans "adding sleeps, retries, or timing hacks to +mask flaky behavior". Reading `Control.Handle` is none of those, and the distinguishing test is +whether the race still exists after the change: + +- A sleep, a retry, or a timing tolerance leaves the race in place and only lowers the probability + of observing the failure. The failure remains reachable. +- Reading `viewer.Handle` on the pump thread before the act **eliminates the precondition of the + failure**. `IsHandleCreated` is then `true` unconditionally, for the whole lifetime of the + fixture, on every machine, at every load level. There is no residual window in which the test can + fail for this reason, so there is nothing left to mask. + +It is also not a wall-clock wait, not probabilistic, and not order-dependent — the three properties +the "Determinism Infrastructure" section of `.claude/rules/general-unit-test.md` constrains. The +in-repo precedent is maintainer-ratified: `Tags.Test/TagControllerRendering.StaTests.cs` does +exactly this, with the comment `// Act: force invisible handle creation, then invoke the real draw +path.` followed by `var handle = checkBox.Handle;` and a later +`checkBox.IsHandleCreated.Should().BeTrue();`, and its class documentation records that the test +never shows a window and uses no message pump, timer, or sleep. A second precedent is +`UtilitiesCS.Test/EmailIntelligence/OSBrowser_Tests.cs:233` (`_ = browser.Handle;`). + +This reading is stated on the record because #571's own "Suspected Cause / Notes" asserts the +opposite — that a handle-forcing call would violate the prohibition. That sentence is the single +point where the promoted record and the epic disagree; the epic's reading governs, and the argument +above is the one the epic asks this spec to supply. + +### Boundaries and invariants to preserve: + +1. **No `.csproj` edit.** `QuickFiler.Test/QuickFiler.Test.csproj` carries 116 explicit + `` entries and **zero wildcard includes**, so no new test file can be compiled. + Sibling #491 owns the `Form1` region; sibling #449 owns one appended `Controllers` entry. All + regression tests therefore go in a file that already carries an entry, specifically + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` (290 lines, 210 of + headroom). +2. **Preserve the cross-class serialization.** + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs:51` defines + `UiThreadDispatcherGate`, a `SemaphoreSlim(1, 1)`, and `SwapUiThreadDispatcher` at `:139` mutates + the process-wide static `UtilitiesCS.UiThread._dispatcher` by reflection. + `QfcItemController_SeamFactoryTests` (`SeamFactoryTests.cs:29`) is a **separate** `[TestClass]` + that acquires the same gate by calling the `internal static BuildPumpHarnessAsync` at `:313` and + `:384`. `BuildPumpHarnessCoreAsync` is therefore the single choke point for both consumer + classes — which is exactly why the fix belongs there — and any change must preserve the gate or + the two classes deadlock under class-level parallelization. +3. **Change no production file.** The fix is confined to test-support and harness code, so + production behaviour is unchanged by construction. +4. **Do not add a member to `IItemViewer`.** A handle-guard remedy would require `IsHandleCreated` + on the interface, which re-declares only `InvokeRequired`/`Invoke`/`BeginInvoke` at + `QuickFiler/Viewers/IItemViewer.cs:135-137` (the epic's `:95-100` citation is drifted by +40). + The `IItemViewer` seam consolidation is issue #489, assigned to a later epic. +5. **No `.claude/**` edit.** +6. **Preserve all 21 pump-host call sites.** 13 self-tests in + `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` (at `:32`, `:59`, `:88`, `:115`, `:153`, + `:183`, `:218`, `:270`, `:302`, `:334`, `:367`, `:395`, `:416`) plus 8 consumer tests. The + research read all 13 self-tests: none asserts handle absence and none asserts on any pump-host + internal beyond its public surface, so the harness-level remedy has a blast radius of zero on + them. +7. **The 500-line cap** applies to every touched file. + +### Dependencies or blocked work: + +- None inbound. This child sits in wave 0 with an empty dependency graph. +- Outbound: two follow-up issues are identified under `## Rollout & Follow-up`; neither blocks this + fix. + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +| File | Current lines | Change | Cap headroom after | +| --- | --- | --- | --- | +| `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` | 409 | force `viewer.Handle` on the pump thread in `BuildPumpHarnessCoreAsync`, with an explanatory comment | ~85 | +| `QuickFiler.Test/Controllers/QfcItemController.ViewerSetupTests.cs` | 467 | force `viewer.Handle` on the pump thread in the standalone arrange block near `:426`-`:435` | ~30 | +| `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` | 290 | add the regression tests | ~105 | + +No other file is modified. Files deliberately **not** touched, with their headroom recorded because +the cap pressure is real: `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` at 443 lines (57 of +headroom), `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` at 497 lines (3 of +headroom), and `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` at 482 lines (18 of headroom). + +#### Functions/classes/CLI commands impacted: + +- `QfcItemController_InitializationTests.BuildPumpHarnessCoreAsync` (harness; changed). +- `QfcItemController_ViewerSetupTests.ResolveControlGroupsAsync_ThroughThePumpHost_PopulatesTipsAndControlGroups` + arrange block (changed). +- `QfcItemController_InitializationTests` (new regression tests added). +- `QfcItemController_SeamFactoryTests` (unchanged; consumes the changed harness through + `BuildPumpHarnessAsync`). +- `WinFormsPumpHost` and `WinFormsPumpHostTests` (unchanged). + +#### Data flow and validation changes: + +None. No data, no serialization format, no configuration key changes. The only behavioural delta is +that the fixture's `ItemViewer` has a created native window handle from the moment the harness +returns. + +#### Error handling and logging updates: + +None. No logging pattern changes. The failure mode being removed is an exception thrown by the +framework, not a logged condition. + +#### Rollback/feature-flag considerations (if applicable): + +No feature flag. Rollback is a revert of the three test files; no production surface is affected, so +a revert cannot regress runtime behaviour. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +No public interface changes. No member is added to `IItemViewer` or to `WinFormsPumpHost`. The +harness's returned `PumpHarness` shape is unchanged; only the state of the viewer it exposes +changes. + +#### Required configuration keys and defaults: + +None. + +#### Backward-compatibility expectations: + +Full. The production assembly is byte-compatible because no production file changes. + +#### Performance constraints (latency/throughput/memory): + +Handle creation for one `UserControl` is a single native window creation and is not a measurable +cost against a 60-second harness bound. The pump-hosted timeout constants are unchanged: +`PumpTimeoutMs = 60000` at `QfcItemController.InitializationTests.cs:38`, +`ViewerSetupTests.cs:34`, and `SeamFactoryTests.cs:293`; `TimeoutMs = 30000` at +`WinFormsPumpHostTests.cs:24`. No timeout value is raised, because raising a timeout is a timing +tolerance and is prohibited. + +## Assumptions, Constraints, Dependencies + +### Assumptions (environment, data, access) + +- Reading `Control.Handle` forces creation of that control's handle and is non-recursive, while + `Control.CreateControl()` is recursive — documented `System.Windows.Forms.Control` behaviour, high + confidence, not executed in this repository. The minimality consequence (WebView2 children remain + handle-less) is asserted by a named regression test rather than assumed. +- `Control.InvokeRequired` returns `false` when no control in the parent chain has a created handle + — documented behaviour, and the mechanism that explains why four of the six pump-hosted tests do + not fail. +- Nine `*.Test.dll` assemblies constitute the full suite, and they are discoverable from the build + output with `\.claude\` excluded. + +### Constraints (budget, performance, compatibility) + +- No `.csproj` edit; no new test file (constraint 1 above). +- No production file change (constraint 3 above). +- No `IItemViewer` member addition (constraint 4 above). +- No `.claude/**` edit (constraint 5 above). +- The `UiThreadDispatcherGate` serialization must survive (constraint 2 above). +- Every touched file stays under 500 lines. +- MSTest + Moq + FluentAssertions only. No temporary files. No sleeps, retries, or timing + tolerances. +- Evidence goes only under + `docs/features/active/winformspumphost-suite-determinism-511/evidence//`. +- **No Python toolchain exists in this repository** — there is no `scripts/dev_tools/` and no Poetry + manifest — so no Python command appears anywhere in this spec or in the plan derived from it. A + skill step naming one is unrunnable by absence and must be reported as such. + +### External dependencies (services, libraries, releases) + +- `Microsoft.Web.WebView2.WinForms` — third-party, read-only here. Its implicit-initialization + behaviour is the prime suspect for the unresolved intermittency question and is probed by a named + regression test rather than by reading its source, which is not present in this repository. +- No new package reference is added. + +### Known side effect the plan must anticipate + +Forcing the `ItemViewer`'s handle flips currently-`false` `InvokeRequired` guards to `true` whenever +they are evaluated off the pump thread. Two are on paths under test: + +- `Theme.cs:433` `_lblItemNumber.InvokeRequired`, evaluated during `InitializeGraphicsAsync`'s + `SetThemeDark(async: false)`, which resumes on a thread-pool thread after `await Task.Run(...)`. + It will now marshal to the pump thread via `Theme.cs:435` instead of running inline. +- `ViewerSetup.cs:361` `_itemViewer.InvokeRequired` in `AssignControls`, reached from + `PopulateControlsAsync` → `AssignControlsAsync`. + +Both should succeed, because a live pump is precisely what the fixture supplies, and both become +more production-faithful. This is nevertheless a genuine behaviour change in the tests and is the +most likely source of a surprise during execution, which is why "the other four pump-hosted tests +still pass" is an explicit acceptance criterion rather than an assumption. + +## Data / API / Config Impact + +- **User-facing or API changes:** none. No production file changes, so no public or internal + production API is affected. +- **Data or migration considerations:** none. +- **Logging/telemetry updates (if any):** none. +- **Compatibility notes (CLI flags, config schemas, versioning):** no change to `coverage.config`, + `Directory.Build.targets`, `quality-tiers.yml`, any `*.csproj`, or any workflow file. The + `vstest.console.exe` invocation is unchanged from the repository standard, including the mandatory + `/InIsolation`. + +## Test Strategy + +### Empirical pre-fix baseline (required, must not be replaced by static reasoning) + +Before the fix, establish the failure behaviour of the two named tests **by repeated execution**: + +1. Run `/TestCaseFilter:"FullyQualifiedName~QfcItemController_InitializationTests"` ten times and + record per-run pass/fail for each named test. +2. Run the full nine-assembly suite ten times and record per-run pass/fail for each named test. +3. In the same runs, record the observed `IsHandleCreated` value for the harness viewer, so the + unresolved intermittency question in `## Root Cause Analysis` is answered by observation. If the + value is `false` while the test passes, the static reading of `Control.Invoke` is wrong; if it is + sometimes `true`, something third-party creates the handle and that something is named before the + fix is described as minimal. +4. Record the result under + `docs/features/active/winformspumphost-suite-determinism-511/evidence/regression-testing/`. + +The pre-fix behaviour is **not** to be asserted from static reading. The chosen remedy does not +change with the answer; only the explanation recorded in the spec does. + +### Regression tests to add or update + +All in `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs` (210 lines of +headroom), because no new file can be compiled without a `.csproj` edit: + +1. `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` — asserts the harness viewer's + `IsHandleCreated` is `true` and that, queried on the pump thread, `InvokeRequired` is `false`. + This is the failing-test-first artifact required by the Bugfix Workflow: it fails + deterministically before the fix and passes after, which is a better regression than depending on + the intermittent end-to-end symptom. +2. `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` — asserts both WebView2 children remain + handle-less after the fix, pinning the minimality property of `.Handle` over `CreateControl()` + and simultaneously probing open question 2. + +The negative case — `Control.Invoke` throwing without a handle — is deliberately **not** added, +because it would assert framework behaviour rather than repository behaviour. + +### Unit tests for the fixed behaviour and boundaries + +MSTest `[TestClass]`/`[TestMethod]`, Moq for the seam doubles already used by the harness, and +FluentAssertions for every new assertion. Scenario coverage for the new fixture invariant: +positive (handle created on the pump thread), boundary (`InvokeRequired` is `false` on the pump +thread), and minimality (children remain handle-less). + +### Edge cases and negative scenarios + +- The other four pump-hosted consumer tests must still pass after the `InvokeRequired` guards flip + (see "Known side effect"). +- `QfcItemController_SeamFactoryTests` must still pass in the same run as + `QfcItemController_InitializationTests`, proving the `UiThreadDispatcherGate` serialization + survived. +- All 13 `WinFormsPumpHostTests` self-tests must still pass, including the post-`StopAsync` + `ObjectDisposedException` and `Dispose`-idempotence cases. + +### Error handling and logging verification + +Not applicable: no error-handling or logging code changes. The verification is the absence of the +`InvalidOperationException` in the TRX output. + +### Coverage impact and targets for changed lines/modules + +`QuickFiler/Viewers/ItemViewer.cs` carries a whole-type `[ExcludeFromCodeCoverage]` at `:20`, so the +fixture change moves no coverage into or out of the denominator. `QfcItemController` coverage must +not regress; the seven `Initialization.cs` de-exemption blocks plus the `ViewerSetup.cs:254` +de-exemption are the checklist. Coverage is captured with `/EnableCodeCoverage` and the report is +stored under `evidence/qa-gates/`, with the pre-fix figure under `evidence/baseline/`. + +### Toolchain commands to run (format → lint → type-check → test) + +Run in this exact order and restart from the first step if any step fails or changes files: + +1. `dotnet tool restore` +2. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) +3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +4. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +5. `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + +Binding details: + +- Always `/t:Rebuild`, never `/t:Build`. MSBuild's up-to-date check does not invalidate on a + command-line `/p:` change, so a warm `/t:Build` returns exit 0 with `CoreCompile` skipped on every + project and runs no analyzers. +- Never add `/p:Nullable=enable`. No project carries a `` element and there is no + `Directory.Build.props`, so the property conscripts files that never opted in and diverges from + `.github/workflows/ci.yml`. +- `/InIsolation` is mandatory. Without it each assembly's `app.config` binding redirects are + ignored and roughly 1,695 phantom failures appear with empty messages, surfacing as a Moq + `TypeInitializationException` via `System.Threading.Tasks.Extensions`. A run missing the flag + shows a fabricated mass regression that must not be "fixed". +- Exclude `\.claude\` from recursive `*.Test.dll` discovery so stale agent-worktree builds are not + loaded. + +### Manual validation steps + +Watch one full-suite run and record whether a top-level window appears. A window that does appear is +evidence for the `ProgressViewer_Tests` re-attribution and belongs in the follow-up issue, not in +this feature's acceptance. + +## Acceptance Criteria + +- [ ] `InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates` + (`QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs:175`) passes in + every one of ten consecutive full nine-assembly runs, with the ten TRX results stored under + `evidence/regression-testing/`. +- [ ] `InitializeBool_ThroughThePumpHost_CompletesAndInitializesState` + (`QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs:131`) passes in + every one of those same ten consecutive full nine-assembly runs. +- [ ] The ten consecutive full nine-assembly runs are executed under induced CPU load and are all + green, using `vstest.console.exe /EnableCodeCoverage /InIsolation + /TestCaseFilter:"TestCategory!=LiveOutlook"`, with the evidence stored under + `evidence/regression-testing/`. (Ten under induced load is chosen over #571's "at least 5" + because it is the epic's stated leading indicator, it targets #511's load-induced cascade + directly, and it satisfies #571's threshold a fortiori.) +- [ ] An empirical pre-fix baseline artifact exists under `evidence/regression-testing/` recording, + per run across ten runs, the pass/fail outcome of both named tests and the observed harness + viewer `IsHandleCreated` value, establishing the pre-fix failure behaviour by execution rather + than by static reading. +- [ ] `BuildPumpHarness_ForcesTheViewerWindowHandleOnThePumpThread` exists in + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`, asserts the + harness viewer's `IsHandleCreated` is `true` before the act, and passes. +- [ ] `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` exists in + `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part3.cs`, asserts both + WebView2 children remain handle-less, and passes. +- [ ] `git diff` reports zero hunks in both + `QuickFiler/Controllers/QfcItemController.Initialization.cs` and + `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, and file inspection confirms all + seven `#230` de-exemption comment blocks in `Initialization.cs` (lines 135, 164, 196, 259, + 291, 403, 447), the `#230` de-exemption block at `ViewerSetup.cs:254`, and the retained + `[ExcludeFromCodeCoverage]` block at `ViewerSetup.cs:30-41` are present and unmodified. +- [ ] All 21 pump-host call sites pass in the final run: the 13 self-tests in + `QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs` and the 8 consumer tests (5 in + `QfcItemController.InitializationTests.Part3.cs`, 2 in `QfcItemController.SeamFactoryTests.cs`, + 1 in `QfcItemController.ViewerSetupTests.cs`). +- [ ] `git diff --name-only` against the merge base lists exactly three code files, all under + `QuickFiler.Test/` (`Controllers/QfcItemController.InitializationTests.Part2.cs`, + `Controllers/QfcItemController.ViewerSetupTests.cs`, + `Controllers/QfcItemController.InitializationTests.Part3.cs`), and lists no file under + `QuickFiler/`, no `*.csproj`, and no path under `.claude/` other than `.claude/agent-memory/`, + which epic hard constraint 1 lists as safe to edit and which is agent bookkeeping rather than + part of the fix. +- [ ] `QfcItemController_SeamFactoryTests` and `QfcItemController_InitializationTests` both pass in + the same run, and file inspection confirms `UiThreadDispatcherGate` + (`QfcItemController.InitializationTests.Part2.cs:51`) and `SwapUiThreadDispatcher` (`:139`) + retain their acquire-and-release structure. +- [ ] Every changed file is under 500 lines after the change: + `QfcItemController.InitializationTests.Part2.cs` (was 409), + `QfcItemController.ViewerSetupTests.cs` (was 467), and + `QfcItemController.InitializationTests.Part3.cs` (was 290). +- [ ] `git diff` introduces no occurrence of `Thread.Sleep`, `Task.Delay`, `SpinWait`, a retry loop, + or a raised timeout constant, and every existing timeout constant retains its current value + (`PumpTimeoutMs = 60000`, `TimeoutMs = 30000`). +- [ ] The five-step toolchain in `## Test Strategy` completes green in a single final pass, coverage + is captured under `evidence/qa-gates/`, and measured `QuickFiler` line coverage is greater + than or equal to the pre-fix baseline recorded under `evidence/baseline/`. +- [ ] `## Rollout & Follow-up` records #511's visible-window half as out of scope with its + re-attribution to `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs`, and names the filed + follow-up issue number for it, so the feature audit does not score that half as an unmet + criterion of this feature. + +## Risks & Mitigations + +### Technical or operational risks + +1. **The flipped `InvokeRequired` guards change behaviour on the four currently-passing pump-hosted + tests.** After the handle exists, `Theme.cs:433` and `ViewerSetup.cs:361` marshal instead of + running inline. This is the most likely source of an execution surprise. + *Mitigation:* all 21 pump-host call sites are an explicit acceptance criterion, and the fix runs + inside the section already serialized by `UiThreadDispatcherGate` against a live pump. +2. **The intermittency mechanism is unexplained.** If a third-party path is creating the handle + non-deterministically, the fix is still correct but the "minimal" claim is weaker than stated. + *Mitigation:* the empirical pre-fix baseline and + `BuildPumpHarness_DoesNotCreateTheWebViewChildHandles` are both acceptance criteria; the spec + records the question as open rather than resolving it by assertion. +3. **The `[Timeout]` / `UiThreadDispatcherGate` cascade is not fixed.** A load-induced overrun can + still convert into several correlated failures, because MSTest records the timeout without + aborting the continuation, so the gate release in `PumpHarness.Restore` has not yet run. + *Mitigation:* recorded as a residual and as a follow-up candidate (`[DoNotParallelize]`), not + claimed fixed. If the ten-run green requirement cannot be met, this is the first suspect. +4. **Residual CPU-contention sensitivity.** Retaining the pump-hosted coverage retains real message + pumps under load. *Mitigation:* stated as an accepted trade, not silently claimed away. The + alternative buys a pump-free suite at the cost of nine coverage justifications and a rewrite this + child is neither scoped nor permitted to perform. +5. **Cap pressure on adjacent files.** `WinFormsPumpHost.cs` (18 lines of headroom) and + `FocusAndThemeTests.cs` (3 lines of headroom) cannot absorb additions. + *Mitigation:* the chosen remedy touches neither. +6. **A later reviewer may prefer the production guard on `InvokeBeginInvoke`.** + *Mitigation:* the argument against including it here is recorded in `## Scope & Non-Goals`, and + a follow-up issue is identified below rather than the change being folded in. + +### Mitigations and rollbacks + +Rollback is a revert of the three test files. No production surface is affected, so a revert cannot +regress runtime behaviour. There is no feature flag and none is warranted. + +## Rollout & Follow-up + +### Release/rollout steps + +1. Land on `bug/winformspumphost-suite-determinism-511`, pull request into + `epic/quickfiler-suite-determinism-foundation-integration`. +2. Confirm the wave transition from `git worktree list --porcelain`, `git branch`, and + `gh pr view --json state,mergedAt,headRefOid`. Do not rely on any `PreToolUse` hook: every hook + in this repository currently reads `$toolInput.command` while the payload nests the value at + `$toolInput.tool_input.command`, so the epic wave barrier and merge gate are inert. +3. Attach the pre-fix baseline, the ten-run determinism record, and the coverage report from + `evidence/` to the pull request context. + +### Post-fix monitoring or clean-up tasks + +- Watch the next several full-suite runs for any recurrence of the `InvalidOperationException` from + `FocusAndTheme.cs:256`. A recurrence means the handle is being lost or the fixture is being + bypassed. +- Watch for `[Timeout]`-attributed failures in `QfcItemController_InitializationTests` or + `QfcItemController_SeamFactoryTests`, which indicate the unmitigated gate cascade rather than the + handle race. + +### Required follow-up issues + +1. **#511's visible-window half — out of scope here, re-attributed, needs its own issue.** The + evidence does not support attributing the visible window to `WinFormsPumpHost` or to anything in + this feature's blast radius. The only enabled test in the nine-assembly corpus that shows a real + top-level window is `viewer.Show()` at `UtilitiesCS.Test/Threading/ProgressViewer_Tests.cs:73`, + on a `ProgressViewer : Form` (`UtilitiesCS/Threading/ProgressViewer.cs:16`) inside an + `[STATestClass]`. A one-line remedy exists — the file already has a headless construction helper + `CreateHeadlessViewer` at `ProgressViewer_Tests.cs:33-34` — but it is in `UtilitiesCS.Test`, + outside this child's file set and outside this epic. **File this as its own issue and record the + number here.** Note two constraints on how it is filed: the re-attribution is a code reading, not + a reproduced observation, so someone should watch a full-suite run and confirm the window is the + `ProgressViewer` before the issue asserts causation; and `epic.md` forbids any child of this epic + from writing under `docs/features/potential/**`, so the follow-up is filed directly as a GitHub + issue rather than by creating a potential entry. +2. **The `InvokeBeginInvoke` production asymmetry.** + `QfcItemController.InvokeBeginInvoke` (`FocusAndTheme.cs:248`) is the only unguarded marshaller + in the class; `Theme.cs:433` and `ViewerSetup.cs:361` establish the repository's + `InvokeRequired`-guard pattern. Adding the guard is attractive on the merits but is a production + behaviour change (on a handle-less control it would silently run UI mutation on the calling + thread instead of throwing), it would make the pump-hosted `Initialize(bool)` test pass without + exercising a real `Control.Invoke` — a coverage regression in substance — and its natural test + home `FocusAndThemeTests.cs` has three lines of headroom. File as its own issue. +3. **The MSTest `[Timeout]` / `UiThreadDispatcherGate` cascade.** Candidate mitigation: + `[DoNotParallelize]` on `QfcItemController_InitializationTests` and + `QfcItemController_SeamFactoryTests`. It has no timing content, but its availability in this + MSTest version and its interaction with the gate were not verified. File as its own issue if the + ten-run determinism requirement exposes it. + +### Links + +- Primary issue #511: https://github.com/drmoisan/TaskMaster/issues/511 +- Secondary issue #571: https://github.com/drmoisan/TaskMaster/issues/571 +- Consolidated issue record: `docs/features/active/winformspumphost-suite-determinism-511/issue.md` +- Research artifact: + `docs/features/active/winformspumphost-suite-determinism-511/research/winformspumphost-suite-determinism.2026-08-21T18-20.md` +- Epic: `docs/features/epics/quickfiler-suite-determinism-foundation/epic.md` +- Requirements sources: + `docs/features/potential/promoted/2026-08-08-winformspumphost-tests-load-flaky-visible-window.md`, + `docs/features/potential/promoted/2026-08-15-qfc-item-controller-init-tests-flaky-window-handle.md` From 64f8ca84ee1cc90f68ff3b88d9ee2d2e11549741 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 19:04:15 -0400 Subject: [PATCH 04/37] docs(prep): preserve incomplete preparation artifacts for #445 epic-planner preserved this work after the preparation orchestrator was terminated by an infrastructure error (API 529 Overloaded), not by a task failure. Preparation did NOT complete: no atomic-executor preflight clearance was obtained for this item. Present: issue.md, spec.md, research artifact, and an atomic plan that passes the MCP plan validator. Absent: PREFLIGHT: ALL CLEAR. Committed so a relaunched child resumes from this commit instead of losing an uncommitted worktree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../issue.md | 137 ++++ .../plan.2026-08-21T18-09.md | 317 ++++++++ ...ction-contract-defects.2026-08-21T18-20.md | 687 +++++++++++++++++ .../spec.md | 698 ++++++++++++++++++ 4 files changed, 1839 insertions(+) create mode 100644 docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/issue.md create mode 100644 docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/plan.2026-08-21T18-09.md create mode 100644 docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md create mode 100644 docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md diff --git a/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/issue.md b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/issue.md new file mode 100644 index 000000000..143cd66c4 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/issue.md @@ -0,0 +1,137 @@ +# quickfiler-keyboard-action-contract-defects (Issue #445) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-keyboard-action-contract-defects/ (Issue #445) +- Discovered during: research for issue #430 (`quickfiler-keyboard-actions-coverage`, child F3 of epic #136) + +- Issue: #445 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/445 +- Last Updated: 2026-08-08 +- Work Mode: full-bug + +## Summary + +Three related contract defects in the QuickFiler keyboard-action types. All three were verified by +direct file read at `origin/epic/quickfiler-per-file-coverage-integration` (base commit `56ca1cea`). +None is fixed by issue #430, which carries a no-behavior-change acceptance criterion and characterizes +current behavior in tests instead. + +## Defect 1 — `KaStringAsync.KeyEquals` applies the `Activated` gate inconsistently + +`KeyEquals` guards its `Update` invocation with `Activated` in two of three branches but not the third: + +```csharp +// QuickFiler/Controllers/KaStringAsync.cs:57-78 +public bool KeyEquals(string other) +{ + if (Key.Contains(other)) + { + if (Activated && Update is not null) // gated + Update(Key.Substring(other.Length - 1, 1)); + return true; + } + else if (other.Length == 1) + { + if (Activated && ToggleControl is not null) // gated + ToggleControl(); + } + else if (other.Length > 1) + { + if (Update is not null) // NOT gated + Update(Key.Substring(0, 1)); + if (Activated && ToggleControl is not null) + ToggleControl(); + } + Activated = false; + return false; +} +``` + +The `other.Length > 1` branch invokes `Update` regardless of `Activated`. Whether this is intentional +or an omission is not determinable from the code; there is no comment explaining it. This is the +highest-value untested behavior in the cluster. + +## Defect 2 — `KeyEquals("")` throws `ArgumentOutOfRangeException` + +`Key.Contains("")` is `true` for every string, so an empty `other` enters the first branch and +evaluates `Key.Substring(other.Length - 1, 1)` — that is, `Substring(-1, 1)` — which throws +`ArgumentOutOfRangeException` (`KaStringAsync.cs:62`). + +This is currently double-shielded and is therefore a robustness gap rather than a live crash: +`KeyboardHandler` only ever probes with length `>= 1`, and production supplies a null `Update`, so the +guarded call is not reached. Both shields are incidental, not contractual. + +## Defect 3 — `KaChar.DelegateType` reports the wrong type + +```csharp +// QuickFiler/Controllers/KaChar.cs:11 +public class KaChar : IKbdAction> +// QuickFiler/Controllers/KaChar.cs:37 +public Action Delegate +// QuickFiler/Controllers/KaChar.cs:43-46 +public Type DelegateType +{ + get => typeof(Action); +} +``` + +`KaChar` stores an `Action` but `DelegateType` reports `typeof(Action)`. Impact today is +nil because no consumer reads `DelegateType`. + +## Related — `Update` and `DelegateType` are orphaned public API + +`Update` and `DelegateType` appear on four implementer types but on no interface. The corresponding +contract members are commented out: + +```csharp +// QuickFiler/Interfaces/IKbdAction.cs:12-16 +T Key { get; set; } +U Delegate { get; set; } +bool KeyEquals(T other); +//Action Update { get; set; } +//Type DelegateType { get; } +``` + +Restoring `DelegateType` to the interface **will not compile**: `KaCharAsync` (`KaChar.cs:58`) and +`KaKeyAsync` do not declare it. The viable cleanup direction is therefore removal from the implementers +rather than restoration to the interface. Defect 3 disappears if `DelegateType` is removed. + +## Impact + +No confirmed user-visible failure. Defects 2 and 3 are latent. Defect 1 is a genuine behavioral +ambiguity that will become load-bearing the moment `Update` is non-null on a multi-character probe. +All three are the kind of contract inconsistency that makes the surrounding code unsafe to refactor. + +## Why these were not fixed in issue #430 + +Issue #430 (child F3) carries an explicit acceptance criterion of **no behavior change to observable +QuickFiler keyboard flows**. Each of these fixes is a behavior change. F3's new tests characterize the +current behavior, including the ungated `Update` call and the empty-string throw, so that a later fix +has a red-before-green baseline to work against. + +## Proposed Fix Direction + +1. Decide whether the `other.Length > 1` branch should be `Activated`-gated, and make all three + branches consistent with the decision. +2. Add an explicit guard or documented contract for empty `other` in `KeyEquals`. +3. Remove `DelegateType` from `KaChar`, `KaKey`, and any sibling implementer, or correct it to + `typeof(Action)` if a consumer is introduced. Remove the commented-out members from + `IKbdAction.cs` or restore them deliberately with all implementers updated. + +## Acceptance Criteria (early draft) + +- [ ] The `Activated`-gating contract for `KaStringAsync.KeyEquals` is decided, applied consistently + across all three branches, and documented in-code. +- [ ] `KeyEquals` handles an empty `other` without throwing `ArgumentOutOfRangeException`, or rejects it + with an explicit, documented argument exception. +- [ ] `DelegateType` is either removed from all implementers or reports the actual stored delegate type. +- [ ] The commented-out members in `IKbdAction.cs` are resolved (removed or restored with all + implementers updated). +- [ ] Regression tests cover each changed behavior, replacing the characterization tests added by #430. +- [ ] Full C# toolchain passes: csharpier, analyzer build, nullable build, coverage-enabled vstest. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) +- [ ] Sequence after epic #136 child F3 (#430) merges, so the characterization tests exist first diff --git a/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/plan.2026-08-21T18-09.md b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/plan.2026-08-21T18-09.md new file mode 100644 index 000000000..a2c802f47 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/plan.2026-08-21T18-09.md @@ -0,0 +1,317 @@ +# quickfiler-keyboard-action-contract-defects (Plan) + +- **Issue:** #445 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T18-09 +- **Status:** Draft +- **Version:** 1.0 +- **Work Mode:** `full-bug` (persisted marker `- Work Mode: full-bug` at `issue.md:11`) +- **Language in scope:** C# only +- **Authoritative requirements source:** `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md` (Status: Approved, AC1 through AC21). `issue.md` is background only and its draft criteria are NOT the AC source. +- **Research (authoritative over `issue.md` on every factual conflict):** `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md` + +**Fail-closed evidence rule:** Every baseline command step, every final-QC command step, and the coverage-comparison step has its own evidence artifact. If any required artifact is missing or is missing a required field, the verdict 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 on disk. + +## Evidence Location Contract (non-overridable) + +All evidence artifacts are written under: + +`docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence//` + +with `` in `baseline`, `regression-testing`, `qa-gates`, `issue-updates`, `other`. Filenames end with `..md`, where `` is the capture time in `yyyy-MM-ddTHH-mm` form (for example `2026-08-21T19-05`). Every artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. + +Any `artifacts/` sub-path other than `artifacts/orchestration/` is FORBIDDEN for evidence. `artifacts/baselines/`, `artifacts/baseline/`, `artifacts/qa/`, `artifacts/qa-gates/`, `artifacts/coverage/`, and `artifacts/evidence/` are rejected. If any instruction, prompt, or task text supplies a different evidence location, reject it, use the canonical path above, and record `EVIDENCE_LOCATION_OVERRIDE_REJECTED: replaced with `. + +## Resolved Environment (verified; use these, do not re-derive) + +- Workspace root (`WS`): `C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-aa16be3c847acea9b` +- `dotnet` is NOT on PATH in this worktree. Repo-local SDK: `C:\Users\DanMoisan\repos\TaskMaster\.dotnet-sdk\dotnet.exe`. A `dotnet tool restore` through it succeeded here. Fallback provisioner: `pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1`. +- `msbuild`: `C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe` +- `vstest.console.exe`: `C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe` +- Test assembly for the scoped runs: `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` (`OutputPath` `bin\Debug\` for `Debug|Any CPU`, `AssemblyName` `QuickFiler.Test`). +- Run all C# tooling from `pwsh -NoProfile` with the absolute paths above, launched from `WS`. Do NOT use the Bash tool for these: it mangles the MSBuild switch `/m` into `M:/` and produces MSB1008. + +## Non-negotiable Command Constraints (reason stated so they are not "helpfully" undone) + +1. **`/t:Rebuild`, never `/t:Build`.** MSBuild's up-to-date check does not invalidate on a command-line `/p:` change, so a warm `/t:Build` returns exit 0 with `CoreCompile` skipped on every project and runs no analyzers. The gate cannot fail. CI uses `/t:Build` only because a runner checkout is always cold. +2. **Never add `/p:Nullable=enable`.** No project carries a `` element and there is no `Directory.Build.props`, so the property conscripts every file that never adopted the pragma. It produced 195 errors in `UtilitiesCS.csproj` and CI omits it deliberately. Nullable enforcement here is per-file opt-in via `#nullable enable` plus `/p:TreatWarningsAsErrors=true`. +3. **`/InIsolation` is mandatory on every vstest invocation.** Without it each assembly's `app.config` binding redirects are ignored and roughly 1,695 phantom failures appear with empty messages and sub-millisecond durations, surfacing as a Moq `TypeInitializationException` via `System.Threading.Tasks.Extensions`. An executor that sees that mass regression must add the flag, not "fix" the tests. +4. **Exclude `\.claude\` from recursive `*.Test.dll` discovery using the WORKSPACE-RELATIVE path.** `WS` is itself located under `.claude\worktrees\`, so filtering on the absolute path would exclude every assembly in the workspace. Filter on the output of `Resolve-Path -Relative`. +5. **CSharpier is invoked only through `dotnet tool run`** so the manifest-pinned 1.2.6 is used. The mutating `format` pass is scoped to this plan's own file list; the verifying `check .` pass is repo-wide and read-only. Scoping the mutating pass prevents an unrelated reformat from breaking the Phase 4 scope-lock gates. + +## Hard Constraints (encoded as tasks or notes) + +1. **Do not edit anything under `.claude/**`.** That tree is push-down-owned: a sync overwrites all of it from an upstream bundle with no merge, so any local edit is destroyed. Rule files cited in this plan are the policy the fix is measured against, never edit targets. Verified by P4-T3. +2. **Do not edit `QuickFiler.Test/QuickFiler.Test.csproj` at all.** A sibling epic child owns its `Form1` region. All five relevant test files already carry `` entries at `:96-100`, and every new test lands in an existing file, so no entry is needed. Verified by P4-T1. +3. **Do not write under `docs/features/potential/**`.** Verified by P4-T3. +4. **No Python toolchain exists in this repository** — no `scripts/dev_tools/`, no Poetry manifest, no `pyproject.toml`. Any step naming `poetry run python -m scripts.dev_tools.*` is UNRUNNABLE BY ABSENCE. No such step appears in this plan. If a downstream skill emits one, record it as `UNRUNNABLE BY ABSENCE` with the search scope and patterns; do not fabricate a result and do not silently skip it. There is no `--cov` argument anywhere in this plan and no `pytest` acceptance condition; the scaffold's mention of `tests/bugs//#445-.py` was a template artifact and has been deleted. +5. **Do not rely on any PreToolUse hook.** Every hook in this repository is currently inert and returns `permissionDecision: allow`. Verify everything from durable `git` state and from command exit codes. +6. **`QuickFiler/Controllers/QfcCollectionController.cs` is 2,349 lines** — a pre-existing 500-line-cap violation, out of scope. Do not touch it and do not attempt to remediate its size. Reading `:1363-1385` is sufficient. +7. **The out-of-scope fourth defect (the non-prefix `Substring` at `KaStringAsync.cs:62`) must NOT be fixed.** `spec.md` AC19 requires it to be filed as a new issue. P6-T1 does that; its mechanism is the single item in this plan that needs orchestrator confirmation, because the epic manifest forbids this child from writing under `docs/features/potential/**`. +8. **Scope of this plan.** Implementation plus final QC plus AC check-off. It contains NO task for opening a pull request, authoring a PR body, monitoring CI, or merging. The plan stops after the AC status summary. + +## Decided Change Set (nine edits; no decision is re-opened) + +| # | File | Change | +|---|---|---| +| 1 | `QuickFiler/Controllers/KaStringAsync.cs:72` | add `Activated &&` to the branch-3 guard | +| 2 | `QuickFiler/Controllers/KaStringAsync.cs` top of `KeyEquals` | guard clause: null rejected with `ArgumentNullException`, empty rejected with `ArgumentException` | +| 3 | `QuickFiler/Controllers/KaStringAsync.cs` above `KeyEquals` | XML doc comment recording the latch contract and the argument contract | +| 4 | `QuickFiler/Controllers/KaChar.cs:43-46` and `:6` | delete `DelegateType`; delete the then-unused `using System.Windows.Forms;` | +| 5 | `QuickFiler/Controllers/KaChar.cs:50-55`, `:92-97` | delete the dead `Update` property and its backing field from `KaChar` and `KaCharAsync` | +| 6 | `QuickFiler/Controllers/KaKey.cs:43-46` | delete `DelegateType`; KEEP `using System.Windows.Forms;` because `Keys` is its key type | +| 7 | `QuickFiler/Controllers/KaKey.cs:50-55`, `:92-97` | delete the dead `Update` property and its backing field from `KaKey` and `KaKeyAsync` | +| 8 | `QuickFiler/Interfaces/IKbdAction.cs:15-16` | delete both commented-out lines; the four live members at `:11-14` are untouched | +| 9 | `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` | rename one test; add four new tests | + +**RETAIN `Update` on `KaStringAsync` (`:81-86`).** It is genuinely read at `:61`, `:62`, `:72`, `:73` and written by the five-argument constructor at `:25`. + +**`QuickFiler/Controllers/KbdActions.cs` is NOT modified.** It appears in the issue's file list but no edit is required; other issues in a later epic own it. + +All `file:line` citations are as read on 2026-08-21 and shift as edits land. They are locators; every acceptance condition is phrased against observable code text or behavior. + +## Hard Anti-Regression Constraint + +Do NOT make branch 1 fall through to the `Activated = false` reset at `KaStringAsync.cs:77` for symmetry. The early `return true` at `:63` is load-bearing. `KeyboardHandler` re-arms `Activated` only at filter length 1 (`KeyboardHandler.cs:186-187`) and then performs three passes within one keystroke: `ContainsKey` (`:181`), `FilterKeys` (`:188`), and the indexer/`Find` (`:194`). If branch 1 cleared the latch, the `ContainsKey` pass would consume the activation and the item-number label advance would stop. P2-T4 is the dedicated verification task for this constraint (AC3). + +## Latch Contract to Record Verbatim in the XML Doc Comment + +`spec.md` AC2 mandates a doc comment stating the latch contract but does not pin its wording, and `spec.md` itself carries a precision note explaining that its draft sentence over-generalized. The corrected wording below is the one to record. Do NOT assert "each element's side effects fire at most once per keystroke" without qualification: that is false for branch 1. + +> `Activated` is a per-keystroke latch that gates every observable side effect of `KeyEquals` — both `Update` and `ToggleControl`. A **matching** probe (branch 1) deliberately does not clear the latch and returns early, so a matching element's `Update` continues to fire on each pass `KeyboardHandler` makes within one keystroke; that repetition is intentional and is what advances the item-number label. A **non-matching** probe (branches 2 and 3) clears the latch, so a non-matching element's side effects fire at most once per keystroke regardless of how many times a LINQ predicate is re-enumerated. + +## The Four New Tests and the One Rename + +All land in `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` and reuse the existing `NewKa` helper at `:20-25`. MSTest `[TestClass]`/`[TestMethod]`, FluentAssertions 8.10.0, Moq 4.20.72 available but not required (captured locals suffice). No temporary file, no clock, no timer, no mutable global state. + +- **Rename.** `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` (`:133-152`) becomes `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse`. The body is unchanged: it sets `ka.Activated = true` at `:141`, so it exercises branch 3 with the gate satisfied and passes unmodified. Only the misleading name changes. +- **(a) `KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse`** — `Key = "abc"`, non-null `Update` capturing into a list, `Activated` left at its `false` default, probe `"zz"`. Assert the capture list is empty and the result is `false`. **Genuinely red before the fix** (today branch 3's ungated `Update` fires with `"a"`). +- **(b) `KeyEquals_LatchSurvivesMatchThenNonMatchTransition_StillResetsToFirstChar`** — `Key = "abc"`, `Activated = true`, non-null `Update` capturing into a list, non-null `ToggleControl` setting a flag. Act: `KeyEquals("ab")` then `KeyEquals("zz")`. Assert the capture list is exactly `"b"` then `"a"`, the toggle flag is `true`, and `Activated` is `false`. Passes before and after; it pins the reasoning behind the anti-regression invariant and fails if branch 1's early return is removed. +- **(c) `KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther`** — one `[TestMethod]` covering both instance-state variants, because AC6 requires the rejection to hold for every combination of instance state. Variant 1: default instance (`Activated` false, `Update` null). Variant 2: `Activated = true` with a non-null `Update`. Both assert `ThrowExactly().WithParameterName("other")`. **`ThrowExactly` is load-bearing:** `ArgumentOutOfRangeException` derives from `ArgumentException`, so a plain `Throw()` would already pass today for variant 2 and the assertion would gate nothing. **Genuinely red before the fix** (variant 1 returns `true` without throwing; variant 2 throws `ArgumentOutOfRangeException`). +- **(d) `KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther`** — `Action act = () => ka.KeyEquals(null);` asserting `ThrowExactly().WithParameterName("other")`. **The parameter-name clause is what makes this red before the fix:** today the throw originates inside `string.Contains`, whose parameter is named `value`, not `other`. Research section 4.3 records that the explicit guard changes the exception's origin, not its type, so a type-only assertion would pass unchanged. P1-T9 records the observed pre-fix parameter name verbatim. + +Tests (a), (c), and (d) are authored BEFORE the production edit and observed failing, per CLAUDE.md Bugfix Workflow section 1. P1-T9 is tagged `[expect-fail]`. A fail-before exception dossier is NOT required: real red runs are available. `issue.md`'s claim that #430 left characterization tests asserting these defects is FALSE and was disproved by the research artifact; there are no characterization tests to replace, and nothing is deleted. + +## Literal Register (asserted tokens, quoted here in prose so every search gate is exonerated and falsifiable) + +Each token below is a short, single-line, non-interpolated literal that an acceptance condition searches for. None contains `<`, `>`, `${`, `$(`, or `%`. Every count is currently non-zero and must become zero, or is currently zero and must become non-zero, so each assertion is falsifiable in both directions. + +- `DelegateType` — repo-wide over `*.cs`: 3 now (two declarations plus one comment), 0 after. +- `_update` — the `Update` backing field. `QuickFiler/Controllers/KaChar.cs`: 6 lines now, 0 after. `QuickFiler/Controllers/KaKey.cs`: 6 lines now, 0 after. `QuickFiler/Controllers/KaStringAsync.cs`: 3 lines now, 3 after (retained). +- `using System.Windows.Forms;` — `KaChar.cs`: 1 now, 0 after. `KaKey.cs`: 1 now, 1 after. +- `if (Activated && Update is not null)` — `KaStringAsync.cs`: 1 now (branch 1 at `:61`), 2 after (branch 3 at `:72` acquires the same text). +- `if (Update is not null)` — `KaStringAsync.cs`: 1 now (the ungated branch-3 guard), 0 after. +- `nameof(other)` — `KaStringAsync.cs`: 0 now, 2 after (one per guard-clause throw). +- `ArgumentNullException(nameof(other))` — `KaStringAsync.cs`: 0 now, 1 after. +- `bool KeyEquals(T other);` — `QuickFiler/Interfaces/IKbdAction.cs`: 1 now, 1 after. One of the four live interface members that must survive unchanged. +- `Keys` — `QuickFiler/Controllers/KaChar.cs`: 1 now (inside `DelegateType`), 0 after. `QuickFiler/Controllers/KaKey.cs`: unchanged, because `Keys` is that type's key type throughout. +- `return true;` — `KaStringAsync.cs`: 1 now, 1 after. A second occurrence, or zero, means branch 1's early return was moved or removed. +- `Activated = false` — `KaStringAsync.cs`: 1 now, 1 after. Case-sensitive, so the field initializer `_activated = false` at `:50` does not match. +- `Key.Substring(other.Length - 1, 1)` — `KaStringAsync.cs`: 1 now, 1 after. The out-of-scope fourth defect must remain. +- `Key.Contains(other)` — `KaStringAsync.cs`: 1 now, 1 after. Branch 1's substring semantics are unchanged. +- `latch` — `KaStringAsync.cs`: 0 now, at least 1 after (the XML doc comment). +- `///` — `KaStringAsync.cs`: 0 now, at least 15 after. +- `[TestMethod]` — `KaStringAsyncTests.cs`: 8 now, 12 after. +- `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` — `KaStringAsyncTests.cs`: 1 now, 0 after. It is not a substring of the new name. +- `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse` — 0 now, 1 after. +- `KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse` — 0 now, 1 after. +- `KeyEquals_LatchSurvivesMatchThenNonMatchTransition_StillResetsToFirstChar` — 0 now, 1 after. +- `KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther` — 0 now, 1 after. +- `KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther` — 0 now, 1 after. +- `Be("b"` — `KaStringAsyncTests.cs`: 1 now, 1 after. AC19 pins this existing assertion. +- `ThrowExactly` — `KaStringAsyncTests.cs`: 0 now, at least 3 after. +- `Skipping target "CoreCompile"` — the MSBuild message that proves an analyzer step was vacuous. Asserted count in each analyzer log: 0. A warm `/t:Build` would make it non-zero, so the assertion is falsifiable. +- `CoreCompile:` — the MSBuild target-start line that proves compilation actually ran. Asserted count in each analyzer log: at least 9 (there are nine `*.Test.csproj` projects alone). Do NOT assert a `csc.exe` occurrence count: it is zero even on a real compile and would be an unfalsifiable gate. + +## Uniform Count Idiom + +Every count assertion uses this exact form, so that a zero result and a non-zero result are both observable and `git grep`'s exit 1 on no-match does not read as a command failure: + +```powershell +(git grep -n -F 'TOKEN' -- 'PATHSPEC' | Measure-Object -Line).Lines +``` + +`git grep` searches the working-tree contents of tracked files, so an uncommitted edit is visible and no commit task is required. Use forward slashes in git pathspecs. Restrict every pathspec to `*.cs` or to a single named file so that this plan document, the spec, and the research artifact are never counted. + +## Coverage Policy Position + +`coverage.config` excludes none of the five in-scope files; its only `` block (`:13-21`) lists seven third-party module patterns. CLAUDE.md UT2 names `KbdActions<>` explicitly as a testable seam that is NOT exempt. `KaStringAsync`, `KaChar`, `KaKey`, and `IKbdAction` are pure value objects with no COM dependency and fall outside every limb of the COM/VSTO/WinForms exemption. **No coverage exemption is sought, no `coverage.config` change is made, and no `[ExcludeFromCodeCoverage]` attribute is added.** + +A pre-existing, unadjudicated threshold divergence exists and is reported against both figures rather than silently resolved: + +- CLAUDE.md UT2: repository-wide line coverage `>= 80%`, new modules/classes/methods `>= 90%`. +- `.claude/rules/general-unit-test.md` and `.claude/rules/quality-tiers.md`: line `>= 85%`, branch `>= 75%` uniformly across T1 through T4. + +P5-T8 reports baseline, post-change, and changed-line coverage against BOTH threshold sets. The repository-wide figure is reported and tracked but is NOT a blocking gate for this bugfix (it is a pre-existing repository state this change does not create). The blocking coverage gates are: no regression on the changed lines, and `>= 90%` on the newly added production lines. + +`scripts\vscode\Invoke-MSTestWithCoverage.ps1` is NOT used as the coverage runner. Its `Assert-CoberturaLineCoverageThreshold` helper (`scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1:487`) throws when repository-wide line coverage is below 80 percent, and that throw happens before the Cobertura post-processing writes its output. This plan invokes `dotnet-coverage collect` directly with a derived settings file so that measurement always completes and the numeric headline is always recorded. + +--- + +### Phase 0 — Baseline Capture and Toolchain Bootstrap + +- [ ] [P0-T1] Read `CLAUDE.md` in full (all sections: Policy Compliance Order, General Code Change Policy, General Unit Test Policy, C# Code Change Policy, C# Unit Test Policy, Tone Policy, C# Toolchain). Acceptance: the file has been read end to end and its path is listed in the P0-T6 artifact. +- [ ] [P0-T2] Read `.claude/rules/general-code-change.md` in full. Acceptance: the file has been read end to end and its path is listed in the P0-T6 artifact. +- [ ] [P0-T3] Read `.claude/rules/general-unit-test.md` in full. Acceptance: the file has been read end to end and its path is listed in the P0-T6 artifact. +- [ ] [P0-T4] Read `.claude/rules/csharp.md` in full (C# is the only language in scope). Acceptance: the file has been read end to end and its path is listed in the P0-T6 artifact. +- [ ] [P0-T5] Read `.claude/rules/tonality.md` in full, plus `.claude/rules/plan-acceptance-gates.md` for the acceptance-condition rules this plan is measured against. Acceptance: both files have been read end to end and both paths are listed in the P0-T6 artifact. Note: reading a rule file is never a licence to edit it; `.claude/**` is push-down-owned per Hard Constraint 1. +- [ ] [P0-T6] Write the policy-read evidence artifact to `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/phase0-instructions-read..md`. Acceptance: the artifact contains a `Timestamp:` field, a `Policy Order:` field naming the order `CLAUDE.md` then `general-code-change.md` then `general-unit-test.md` then `csharp.md`, and an explicit bulleted list of every file read in P0-T1 through P0-T5. +- [ ] [P0-T7] Record the branch and tree baseline. Run from `WS`: +```powershell +git rev-parse HEAD +git rev-parse --abbrev-ref HEAD +git status --porcelain +``` +Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/git-baseline..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing the full 40-character HEAD SHA, the branch name, and the verbatim `git status --porcelain` output. The recorded SHA is a datum, not an expectation: no later task asserts a specific SHA value. +- [ ] [P0-T8] Establish a working `dotnet` and record which path was used. Try `C:\Users\DanMoisan\repos\TaskMaster\.dotnet-sdk\dotnet.exe --version` first; if that fails, run `pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1` and retry. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/dotnet-bootstrap..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` naming the absolute `dotnet` path that returned a version string and the version itself. That path is referred to below as `DOTNET`. +- [ ] [P0-T9] Verify the NuGet package restore rather than assuming it. Run `(Get-ChildItem -Path packages -Directory).Count` from `WS`; if the count is below 150 or the `packages` directory is absent, run `nuget restore TaskMaster.sln` and re-count. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/nuget-restore..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with the numeric package-directory count, and that count is greater than or equal to 150. +- [ ] [P0-T10] Restore the CSharpier manifest tool: `& $DOTNET tool restore`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/dotnet-tool-restore..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` naming CSharpier and its restored version. +- [ ] [P0-T11] Capture the CSharpier formatting baseline, read-only: `& $DOTNET tool run csharpier check .`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/csharpier-check..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with the numeric count of files checked and the numeric count of files needing formatting. The expected baseline is 1517 files checked with zero needing formatting; any other number is recorded verbatim, not adjusted. +- [ ] [P0-T12] Capture the analyzer baseline with a non-vacuity proof. Run from `WS`: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true /fl '/flp:logfile=msbuild-analyzer-baseline.log;verbosity=detailed' +(Select-String -SimpleMatch -Pattern 'Skipping target "CoreCompile"' -Path msbuild-analyzer-baseline.log | Measure-Object).Count +(Select-String -SimpleMatch -Pattern 'CoreCompile:' -Path msbuild-analyzer-baseline.log | Measure-Object).Count +``` +`/t:Rebuild` is used, never `/t:Build`, for the reason in Non-negotiable Command Constraint 1. The log filename ends in `.log`, which `.gitignore:84` already ignores, so it never appears in a `git status` scope-lock gate. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/msbuild-analyzers..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing the warning count, the error count, the `Skipping target "CoreCompile"` count (which must be 0), and the `CoreCompile:` count (which must be at least 9). +- [ ] [P0-T13] Capture the nullable/type-check baseline. Run from `WS`: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true /fl '/flp:logfile=msbuild-nullable-baseline.log;verbosity=detailed' +(Select-String -SimpleMatch -Pattern 'Skipping target "CoreCompile"' -Path msbuild-nullable-baseline.log | Measure-Object).Count +``` +No `/p:Nullable=enable` is added, for the reason in Non-negotiable Command Constraint 2. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/msbuild-nullable..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing the error count and the `Skipping target "CoreCompile"` count, which must be 0. +- [ ] [P0-T14] Resolve the test-assembly list using workspace-relative filtering. Run from `WS`: +```powershell +$assemblies = Get-ChildItem -Path . -Recurse -Filter '*.Test.dll' | Where-Object { $_.FullName -match '\\bin\\Debug\\' -and $_.FullName -notmatch '\\obj\\' -and $_.FullName -notmatch '\\ref\\' } | ForEach-Object { Resolve-Path -LiteralPath $_.FullName -Relative } | Where-Object { $_ -notmatch '\\\.claude\\' } +$assemblies.Count +$assemblies +``` +The filter is applied to the RELATIVE path because `WS` itself sits under `.claude\worktrees\`; an absolute-path filter would discard every assembly (Non-negotiable Command Constraint 4). Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/test-assembly-resolution..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` listing every resolved relative path and the count, which must be 9 (one per `*.Test.csproj`) and must include `.\QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`. +- [ ] [P0-T15] Capture the full-suite pass/fail baseline with the CLAUDE.md CUT3 test command. Run from `WS`, using the `$assemblies` list from P0-T14: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' @assemblies /EnableCodeCoverage /InIsolation '/TestCaseFilter:TestCategory!=LiveOutlook' '/ResultsDirectory:coverage' +``` +Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/vstest-baseline..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing the numeric Passed, Failed, and Skipped totals and, when Failed is non-zero, the complete list of failing fully-qualified test names. That list is the pre-existing-failure set that P5-T6 compares against. +- [ ] [P0-T16] Probe the coverage toolchain and record the result without halting. Run `Get-Command dotnet-coverage -ErrorAction SilentlyContinue`. If absent, run `& $DOTNET tool install --global dotnet-coverage` and re-probe. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/coverage-tool-probe..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` stating either `dotnet-coverage PRESENT` with its resolved path and version, or `dotnet-coverage ABSENT` with the install command and its exit code. If it is still absent after the install attempt, record `BLOCKER: numeric Cobertura coverage unavailable` in this artifact and continue to the next task; P5-T8 then records BLOCKED with a pointer to this artifact. Do not record `SKIPPED` as a passing outcome anywhere. +- [ ] [P0-T17] Derive the effective coverage settings file. Copy `coverage.config` to `coverage\effective-coverage.config` and insert one additional child element `.*\.Test\.dll$` inside `/Configuration/CodeCoverage/ModulePaths/Exclude`, preserving all seven existing third-party exclusions. This excludes test assemblies from the coverage denominator per CLAUDE.md UT2. `coverage/*` is already ignored by `.gitignore:144`, so the derived file never appears in a scope-lock gate. The canonical `coverage.config` is NOT modified. Acceptance: `coverage\effective-coverage.config` exists, parses as XML, its `Exclude` element has exactly 8 `ModulePath` children, and `(git status --porcelain -- coverage.config | Measure-Object -Line).Lines` is 0. +- [ ] [P0-T18] Capture the numeric coverage baseline. Run from `WS`, using the `$assemblies` list from P0-T14: +```powershell +& dotnet-coverage collect --output coverage\baseline.cobertura.xml --output-format cobertura --settings coverage\effective-coverage.config -- 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' @assemblies '/Settings:scripts\vscode\TaskMaster.cli.runsettings' /InIsolation '/TestCaseFilter:TestCategory!=LiveOutlook' +$cov = ([xml](Get-Content -Raw coverage\baseline.cobertura.xml)).coverage +$cov.'line-rate'; $cov.'branch-rate'; $cov.'lines-covered'; $cov.'lines-valid'; $cov.'branches-covered'; $cov.'branches-valid' +``` +The outer `dotnet-coverage` supplies instrumentation, so the inner vstest invocation deliberately omits `/EnableCodeCoverage`; `/InIsolation` is still mandatory. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/coverage-baseline..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing NUMERIC values for repository line rate as a percentage, branch rate as a percentage, lines-covered, lines-valid, branches-covered, and branches-valid, plus the per-file covered/total line counts aggregated across every Cobertura `class` element whose `filename` ends with `KaStringAsync.cs`, `KaChar.cs`, `KaKey.cs`, or `IKbdAction.cs`. Aggregation by `filename` is required because a C# compiler-generated state machine or closure appears as a separate `class` element; measuring one element alone understates the file. `UNVERIFIED` is not an acceptable value for any of these fields. +- [ ] [P0-T19] Capture the structural-count baseline for every token in the Literal Register. Run each count with the Uniform Count Idiom against the pathspec named in the register, plus the per-file line counts of `QuickFiler/Controllers/KaStringAsync.cs`, `QuickFiler/Controllers/KaChar.cs`, `QuickFiler/Controllers/KaKey.cs`, `QuickFiler/Interfaces/IKbdAction.cs`, and `QuickFiler.Test/Controllers/KaStringAsyncTests.cs`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/baseline/structural-counts..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with one numeric row per register token and one row per file line count, and the recorded values match the "now" column of the Literal Register (`DelegateType` 3, `KaChar.cs` `_update` 6, `KaKey.cs` `_update` 6, `KaStringAsync.cs` `_update` 3, `if (Activated && Update is not null)` 1, `return true;` 1, `latch` 0, `///` 0, `[TestMethod]` 8, `ThrowExactly` 0) and the file line counts 95, 99, 99, 18, 168. + +### Phase 1 — Regression Tests Authored Before the Fix + +- [ ] [P1-T1] In `QuickFiler.Test/Controllers/KaStringAsyncTests.cs`, rename the method at `:134` from `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` to `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse`. Change nothing else in the method: it sets `ka.Activated = true` at `:141` and therefore exercises branch 3 with the gate satisfied, so it passes unmodified after the fix. Acceptance: `(git grep -n -F 'KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 0 and `(git grep -n -F 'KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1. +- [ ] [P1-T2] Add test (a) `KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse` to `QuickFiler.Test/Controllers/KaStringAsyncTests.cs`, using the `NewKa` helper: `Key = "abc"`, an `update` callback appending to a `List`, `Activated` left at its `false` default, Arrange-Act-Assert sections and an intent comment. Act on `KeyEquals("zz")`. Assert with FluentAssertions that the list is empty and the result is `false`. Acceptance: `(git grep -n -F 'KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1. +- [ ] [P1-T3] Add test (b) `KeyEquals_LatchSurvivesMatchThenNonMatchTransition_StillResetsToFirstChar`, using `NewKa` with `Key = "abc"`, an `update` callback appending to a `List`, a `toggle` callback setting a bool, and `Activated = true`. Act on `KeyEquals("ab")` then `KeyEquals("zz")`. Assert the list is exactly `"b"` then `"a"`, the toggle flag is `true`, and `Activated` is `false`. Include an intent comment recording that this test pins the branch-1 early return described in the Hard Anti-Regression Constraint. Acceptance: `(git grep -n -F 'KeyEquals_LatchSurvivesMatchThenNonMatchTransition_StillResetsToFirstChar' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1. +- [ ] [P1-T4] Add test (c) `KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther` covering both instance-state variants in one `[TestMethod]`: variant 1 a default instance with `Activated` false and `Update` null; variant 2 an instance with `Activated = true` and a non-null `Update`. Each variant asserts `ThrowExactly().WithParameterName("other")` on `() => ka.KeyEquals("")`. `ThrowExactly` is required, not `Throw`, because `ArgumentOutOfRangeException` derives from `ArgumentException` and a base-type assertion would already pass today for variant 2 and would gate nothing. Acceptance: `(git grep -n -F 'KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1 and `(git grep -n -F 'ThrowExactly' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is at least 2. +- [ ] [P1-T5] Add test (d) `KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther`, asserting `ThrowExactly().WithParameterName("other")` on `() => ka.KeyEquals(null)`. Include an intent comment recording that the parameter-name clause is what distinguishes the explicit guard from today's throw inside `string.Contains`, whose parameter is named `value`. Acceptance: `(git grep -n -F 'KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1. +- [ ] [P1-T6] Verify the test-file structural deltas. Acceptance: `(git grep -n -F '[TestMethod]' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 12 (8 at baseline plus 4 new), `(git grep -n -F 'ThrowExactly' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is at least 3, and `(git grep -n -F 'Be("b"' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is still 1. +- [ ] [P1-T7] Format the test file and verify repo-wide. Run `& $DOTNET tool run csharpier format QuickFiler.Test\Controllers\KaStringAsyncTests.cs` then `& $DOTNET tool run csharpier check .`. The mutating pass is scoped to this one file so that no unrelated reformat can break the Phase 4 scope locks. Acceptance: the `check .` invocation exits 0 with zero files needing formatting. +- [ ] [P1-T8] Build the solution so the new tests compile against UNMODIFIED production code. Run `& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"`. Acceptance: exit code 0 and `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` has a write time later than the P1-T2 edit. +- [ ] [P1-T9] [expect-fail] Observe the pre-fix red run. Run from `WS`: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation '/TestCaseFilter:FullyQualifiedName~KaStringAsyncTests' +``` +Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/regression-testing/red-before-fix..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode: 1`, and `Output Summary:` with a per-test outcome row for all 12 methods, in which `KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse` is Failed, `KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther` is Failed, and the observed exception type and parameter name for `KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther` are recorded verbatim together with its Passed/Failed outcome. No fail-before exception dossier is required because real failing runs are available. + +### Phase 2 — KaStringAsync Contract Fix + +- [ ] [P2-T1] Add the argument guard clause at the very top of `KaStringAsync.KeyEquals`, above the `Key.Contains(other)` test in `QuickFiler/Controllers/KaStringAsync.cs`. The null test must come first, because `other.Length` on a null reference throws `NullReferenceException` before any guard can run. Reject null with `throw new ArgumentNullException(nameof(other));` and reject empty with `throw new ArgumentException(...)` whose message explains that `string.Contains(string.Empty)` is true for every key so an empty probe would otherwise match every registered action, and whose second argument is `nameof(other)`. Acceptance: `(git grep -n -F 'ArgumentNullException(nameof(other))' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 1 and `(git grep -n -F 'nameof(other)' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 2. +- [ ] [P2-T2] Add the `Activated &&` conjunct to the branch-3 guard in `QuickFiler/Controllers/KaStringAsync.cs` so that the line currently reading `if (Update is not null)` at `:72` becomes textually identical to branch 1's guard. No other guard in the method is weakened or reordered. Acceptance: `(git grep -n -F 'if (Activated && Update is not null)' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 2 (1 at baseline) and `(git grep -n -F 'if (Update is not null)' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 0. +- [ ] [P2-T3] Add the XML documentation comment immediately above `KeyEquals` in `QuickFiler/Controllers/KaStringAsync.cs`, recording verbatim the corrected latch-contract wording from the "Latch Contract to Record Verbatim" section of this plan, plus `param`, `returns`, and two `exception` elements documenting the `ArgumentNullException` and `ArgumentException` contracts of P2-T1, plus a sentence recording that `KbdActions` methods with a `string` key inherit the new precondition so an empty key argument now surfaces an `ArgumentException` from the predicate rather than matching every element. Do NOT write the unqualified claim that each element's side effects fire at most once per keystroke: that is false for branch 1. Acceptance: `(git grep -n -F 'latch' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is at least 1 (0 at baseline), `(git grep -n -F '///' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is at least 15 (0 at baseline), and the comment text is read back and confirmed to contain the mandated wording, with the confirmation recorded in `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/latch-contract-doc..md` alongside `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [ ] [P2-T4] Verify the hard anti-regression constraint: branch 1's early return is preserved verbatim and branch 1 does NOT fall through to the trailing reset. Acceptance: `(git grep -n -F 'return true;' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is exactly 1 and `(git grep -n -F 'Activated = false' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is exactly 1 (the case-sensitive search does not match the `_activated = false` field initializer). Both counts equal their P0-T19 baseline. Record the two counts and the reason in `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/early-return-preserved..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [ ] [P2-T5] Format the changed production file and verify repo-wide. Run `& $DOTNET tool run csharpier format QuickFiler\Controllers\KaStringAsync.cs` then `& $DOTNET tool run csharpier check .`. Acceptance: the `check .` invocation exits 0 with zero files needing formatting. Re-run the P2-T2 count assertion after formatting to confirm the guard text survived the formatter on one line. +- [ ] [P2-T6] Rebuild and observe the green run for the same scope as P1-T9. Run `& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"` then `& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation '/TestCaseFilter:FullyQualifiedName~KaStringAsyncTests'`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/regression-testing/green-after-fix..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` showing Passed 12, Failed 0, Skipped 0, with the four new test names and the renamed test name each individually listed as Passed. + +### Phase 3 — Dead API Removal + +- [ ] [P3-T1] Delete the `DelegateType` property from `QuickFiler/Controllers/KaChar.cs` (`:43-46` plus its surrounding blank line). Acceptance: `(git grep -n -F 'DelegateType' -- 'QuickFiler/Controllers/KaChar.cs' | Measure-Object -Line).Lines` is 0 (1 at baseline). +- [ ] [P3-T2] Delete the now-unused `using System.Windows.Forms;` from `QuickFiler/Controllers/KaChar.cs:6`. `Keys` appeared in that file only inside `DelegateType`. Acceptance: `(git grep -n -F 'using System.Windows.Forms;' -- 'QuickFiler/Controllers/KaChar.cs' | Measure-Object -Line).Lines` is 0 and `(git grep -n -F 'Keys' -- 'QuickFiler/Controllers/KaChar.cs' | Measure-Object -Line).Lines` is 0. +- [ ] [P3-T3] Delete the dead `Update` property AND its `_update` backing field from `KaChar` in `QuickFiler/Controllers/KaChar.cs` (`:50-55` plus the preceding blank line). Both must go: leaving the field alone produces an unused-field diagnostic that `/p:TreatWarningsAsErrors=true` promotes to an error. Acceptance: after this task and P3-T4 together, `(git grep -n -F '_update' -- 'QuickFiler/Controllers/KaChar.cs' | Measure-Object -Line).Lines` is 0; after this task alone it is 3. +- [ ] [P3-T4] Delete the dead `Update` property AND its `_update` backing field from `KaCharAsync` in `QuickFiler/Controllers/KaChar.cs` (`:92-97` plus the preceding blank line). Acceptance: `(git grep -n -F '_update' -- 'QuickFiler/Controllers/KaChar.cs' | Measure-Object -Line).Lines` is 0 (6 at baseline). +- [ ] [P3-T5] Delete the `DelegateType` property from `QuickFiler/Controllers/KaKey.cs` (`:43-46` plus its surrounding blank line). KEEP `using System.Windows.Forms;` in this file: `Keys` is `KaKey`'s key type throughout. Acceptance: `(git grep -n -F 'DelegateType' -- 'QuickFiler/Controllers/KaKey.cs' | Measure-Object -Line).Lines` is 0 (1 at baseline) and `(git grep -n -F 'using System.Windows.Forms;' -- 'QuickFiler/Controllers/KaKey.cs' | Measure-Object -Line).Lines` is still 1. +- [ ] [P3-T6] Delete the dead `Update` property AND its `_update` backing field from `KaKey` in `QuickFiler/Controllers/KaKey.cs` (`:50-55` plus the preceding blank line). Acceptance: after this task alone, `(git grep -n -F '_update' -- 'QuickFiler/Controllers/KaKey.cs' | Measure-Object -Line).Lines` is 3. +- [ ] [P3-T7] Delete the dead `Update` property AND its `_update` backing field from `KaKeyAsync` in `QuickFiler/Controllers/KaKey.cs` (`:92-97` plus the preceding blank line). Acceptance: `(git grep -n -F '_update' -- 'QuickFiler/Controllers/KaKey.cs' | Measure-Object -Line).Lines` is 0 (6 at baseline). +- [ ] [P3-T8] Delete both commented-out member lines at `QuickFiler/Interfaces/IKbdAction.cs:15-16`. Do not touch the four live members at `:11-14` and do not add any member to the interface: `KaStringAsync`, `KaCharAsync`, and `KaKeyAsync` do not declare `DelegateType`, so restoring it would not compile. Acceptance: `(git grep -n -F 'DelegateType' -- 'QuickFiler/Interfaces/IKbdAction.cs' | Measure-Object -Line).Lines` is 0, `(git grep -n -F 'Update' -- 'QuickFiler/Interfaces/IKbdAction.cs' | Measure-Object -Line).Lines` is 0, and `(git grep -n -F 'bool KeyEquals(T other);' -- 'QuickFiler/Interfaces/IKbdAction.cs' | Measure-Object -Line).Lines` is 1. +- [ ] [P3-T9] Verify the removal boundary: `Update` is gone from four implementers and retained on `KaStringAsync`. Acceptance: `(git grep -n -F 'DelegateType' -- '*.cs' | Measure-Object -Line).Lines` is 0 repository-wide over C# sources (3 at baseline), and `(git grep -n -F '_update' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is still exactly 3. Record both in `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/dead-api-removal..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [ ] [P3-T10] Format the three changed files and verify repo-wide. Run `& $DOTNET tool run csharpier format QuickFiler\Controllers\KaChar.cs QuickFiler\Controllers\KaKey.cs QuickFiler\Interfaces\IKbdAction.cs` then `& $DOTNET tool run csharpier check .`. Acceptance: the `check .` invocation exits 0 with zero files needing formatting. +- [ ] [P3-T11] Prove the deletions compile. Run `& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"`. Acceptance: exit code 0 with zero errors. The safety of the six member deletions rests on the zero-read-site evidence plus this clean compile; no new test is required for them. + +### Phase 4 — Scope Locks and Out-of-Scope Verification + +- [ ] [P4-T1] Verify no edit to the test project file. Acceptance: `(git status --porcelain -- 'QuickFiler.Test/QuickFiler.Test.csproj' | Measure-Object -Line).Lines` is 0 and `(git diff --name-only -- 'QuickFiler.Test/QuickFiler.Test.csproj' | Measure-Object -Line).Lines` is 0. All five relevant test files already carry `` entries at `:96-100` and every new test landed in an existing file, so no entry was needed. +- [ ] [P4-T2] Verify the three read-only production files are unmodified. Acceptance: `(git status --porcelain -- 'QuickFiler/Controllers/KbdActions.cs' 'QuickFiler/Controllers/KeyboardHandler.cs' 'QuickFiler/Controllers/QfcCollectionController.cs' | Measure-Object -Line).Lines` is 0. `QfcCollectionController.cs` remains a 2,349-line pre-existing 500-line-cap violation and is deliberately not remediated here. +- [ ] [P4-T3] Verify the forbidden trees are untouched. Acceptance: `(git status --porcelain -- 'docs/features/potential' | Measure-Object -Line).Lines` is 0, and `(git status --porcelain -- '.claude' ':(exclude).claude/agent-memory/*' | Measure-Object -Line).Lines` is 0. The `agent-memory` exclusion is required because `.claude/agent-memory/` is a tracked directory that agents legitimately write to, so an unscoped gate would be unsatisfiable by construction; every other path under `.claude/**` is push-down-owned and must show no change. +- [ ] [P4-T4] Verify the out-of-scope fourth defect was NOT fixed. Acceptance: `(git grep -n -F 'Key.Substring(other.Length - 1, 1)' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 1, `(git grep -n -F 'Key.Contains(other)' -- 'QuickFiler/Controllers/KaStringAsync.cs' | Measure-Object -Line).Lines` is 1, and `(git grep -n -F 'Be("b"' -- 'QuickFiler.Test/Controllers/KaStringAsyncTests.cs' | Measure-Object -Line).Lines` is 1. All three equal their P0-T19 baseline values. +- [ ] [P4-T5] Verify no pre-existing test was deleted or weakened. Run `& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation '/TestCaseFilter:TestCategory!=LiveOutlook'`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/quickfiler-test-suite..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` showing Failed 0, and confirming that `KbdActionsTests`, `KbdActionsRemainingBranchesTests`, `KaCharTests`, and `KaKeyTests` each report the same Passed count as the P0-T15 baseline, and that `git status --porcelain` shows no modification to those four test files. + +### Phase 5 — Final QC Loop + +The four stages below run in the order format, analyzers, type-check, test. If ANY stage fails or modifies a file, restart this phase from P5-T1. No task in this phase may record `EXIT_CODE: SKIPPED` as a passing outcome. + +- [ ] [P5-T1] Formatting stage, mutating pass scoped to this plan's file list. Run `& $DOTNET tool run csharpier format QuickFiler\Controllers\KaStringAsync.cs QuickFiler\Controllers\KaChar.cs QuickFiler\Controllers\KaKey.cs QuickFiler\Interfaces\IKbdAction.cs QuickFiler.Test\Controllers\KaStringAsyncTests.cs`. Acceptance: exit code 0, and `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/csharpier-format..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` stating how many of the five files the formatter rewrote. If the count is non-zero, the restart rule applies and this phase begins again at P5-T1 after the rewrite. +- [ ] [P5-T2] Formatting verification, repo-wide and read-only. Run `& $DOTNET tool run csharpier check .`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/csharpier-check..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` with the numeric files-checked count and zero files needing formatting. +- [ ] [P5-T3] Post-format file-size audit. This runs AFTER the final formatting pass because the formatter can change line counts. Measure the line counts of `QuickFiler/Controllers/KaStringAsync.cs`, `QuickFiler/Controllers/KaChar.cs`, `QuickFiler/Controllers/KaKey.cs`, `QuickFiler/Interfaces/IKbdAction.cs`, and `QuickFiler.Test/Controllers/KaStringAsyncTests.cs`. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/file-size-audit..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with one numeric line count per file; every count is strictly below 500; and `KaChar.cs` is strictly below 99, `KaKey.cs` strictly below 99, and `IKbdAction.cs` strictly below 18 (their P0-T19 baselines), so the three shrinking files are proven to have shrunk. +- [ ] [P5-T4] Linting/analyzer stage with a non-vacuity assertion. Run from `WS`: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true /fl '/flp:logfile=msbuild-analyzer-final.log;verbosity=detailed' +(Select-String -SimpleMatch -Pattern 'Skipping target "CoreCompile"' -Path msbuild-analyzer-final.log | Measure-Object).Count +(Select-String -SimpleMatch -Pattern 'CoreCompile:' -Path msbuild-analyzer-final.log | Measure-Object).Count +``` +`/t:Rebuild` is used, never `/t:Build`, per Non-negotiable Command Constraint 1. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/msbuild-analyzers..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` with an error count of 0, a warning count no greater than the P0-T12 baseline, a `Skipping target "CoreCompile"` count of exactly 0, and a `CoreCompile:` count of at least 9. +- [ ] [P5-T5] Type-checking stage. Run from `WS`: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true /fl '/flp:logfile=msbuild-nullable-final.log;verbosity=detailed' +(Select-String -SimpleMatch -Pattern 'Skipping target "CoreCompile"' -Path msbuild-nullable-final.log | Measure-Object).Count +``` +No `/p:Nullable=enable` and no `/t:Build`, per Non-negotiable Command Constraints 1 and 2. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/msbuild-nullable..md` records `Timestamp:`, `Command:`, `EXIT_CODE: 0`, and `Output Summary:` with an error count of 0 and a `Skipping target "CoreCompile"` count of exactly 0. +- [ ] [P5-T6] Testing stage, full suite with the CLAUDE.md CUT3 command. Re-resolve `$assemblies` with the P0-T14 relative-path idiom, then run: +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' @assemblies /EnableCodeCoverage /InIsolation '/TestCaseFilter:TestCategory!=LiveOutlook' '/ResultsDirectory:coverage' +``` +Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/vstest-final..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with the numeric Passed, Failed, and Skipped totals, the QuickFiler.Test Passed count, and the failing-test-name set. The gate passes only when the failing-test-name set is a subset of the P0-T15 baseline failing set, and the QuickFiler.Test Failed count is exactly 0. An empty baseline failing set therefore requires zero failures repository-wide; a non-empty baseline requires that this change introduced no new failure, and the surviving pre-existing failures are listed by name in the artifact. +- [ ] [P5-T7] Post-change numeric coverage capture. Re-resolve `$assemblies`, then run: +```powershell +& dotnet-coverage collect --output coverage\postchange.cobertura.xml --output-format cobertura --settings coverage\effective-coverage.config -- 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe' @assemblies '/Settings:scripts\vscode\TaskMaster.cli.runsettings' /InIsolation '/TestCaseFilter:TestCategory!=LiveOutlook' +$cov = ([xml](Get-Content -Raw coverage\postchange.cobertura.xml)).coverage +$cov.'line-rate'; $cov.'branch-rate'; $cov.'lines-covered'; $cov.'lines-valid'; $cov.'branches-covered'; $cov.'branches-valid' +``` +Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/coverage-postchange..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` with the same NUMERIC field set as P0-T18, including the per-file covered/total line counts aggregated by Cobertura `filename` for `KaStringAsync.cs`, `KaChar.cs`, `KaKey.cs`, and `IKbdAction.cs`. `UNVERIFIED` is not acceptable. If P0-T16 recorded `dotnet-coverage ABSENT` after its install attempt, record `BLOCKED` here with a pointer to the P0-T16 artifact; `SKIPPED` is not a valid outcome. +- [ ] [P5-T8] Coverage delta and threshold report. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/coverage-delta..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing (i) the baseline repository line and branch percentages from P0-T18, (ii) the post-change repository line and branch percentages from P5-T7, (iii) the per-file covered/total line counts for the four production files before and after, (iv) the changed-line coverage: for every line added or modified by this change in `KaStringAsync.cs` per `git diff`, its Cobertura hit count, and the aggregate newly-added-line coverage percentage, and (v) an explicit comparison against BOTH threshold sets, namely CLAUDE.md UT2 (repository line `>= 80%`, new code `>= 90%`) and `.claude/rules/general-unit-test.md` with `.claude/rules/quality-tiers.md` (line `>= 85%`, branch `>= 75%`), stating that the divergence is pre-existing and unadjudicated and is not resolved by this issue. The blocking conditions are: newly-added production line coverage is `>= 90%`, and no changed line that was covered at baseline is uncovered after the change. The repository-wide figure is reported and tracked but is not a blocking gate for this bugfix. No coverage exemption is sought; `coverage.config` is unchanged. +- [ ] [P5-T9] Record the uninterrupted-pass attestation for the final QC loop. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/final-qc-pass-attestation..md` records `Timestamp:`, `Command:` (the ordered list of the P5-T1 through P5-T7 commands as executed), `EXIT_CODE:` (each stage's exit code), and `Output Summary:` stating that P5-T1 rewrote zero files, that no stage failed, and that the four stages therefore completed as one uninterrupted pass in the order format, lint, type-check, test. If any stage failed or rewrote a file, this artifact instead records the restart and the phase begins again at P5-T1. + +### Phase 6 — Follow-Up Filing and Acceptance-Criteria Check-Off + +Acceptance criteria are checked off in `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md`, the sole AC source for work mode `full-bug`. Change only `- [ ]` to `- [x]`; never alter criterion text; never add a criterion. Each task below evaluates exactly one criterion, cites the evidence artifact that establishes it, and sets the checkbox to match the verdict, leaving it unchecked with a recorded gap when the criterion is not met. + +- [ ] [P6-T1] File the follow-up GitHub issue required by AC19 for the out-of-scope non-prefix `Substring` defect at `KaStringAsync.cs:62` (branch 1 guards on `Key.Contains(other)` but computes `Key.Substring(other.Length - 1, 1)`, which is only meaningful when `other` is a prefix of `Key`; resolving it requires choosing between `Contains` and `StartsWith`, a keyboard-filtering behavior change currently pinned by `KbdActionsTests.cs:71-76`). **MECHANISM REQUIRES ORCHESTRATOR CONFIRMATION — this is the only such task in the plan.** The epic manifest forbids this child from writing under `docs/features/potential/**`, so the normal promotion-lifecycle route is unavailable. Use `gh issue create` directly, or, if the orchestrator directs otherwise, hand the filing to the epic orchestrator and record the handoff. There is no Python toolchain, so no `poetry run python -m scripts.dev_tools.*` promotion step is available: that route is UNRUNNABLE BY ABSENCE. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/issue-updates/followup-substring-defect..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, the exact issue title and body text, `PostedAs: body` with the new issue URL and number, or a `POSTING BLOCKED` header naming the mechanism blocker and the orchestrator handoff; and, when a number was obtained, that number is written into the "Follow-up issue for the non-prefix `Substring` defect" bullet of the spec's Rollout & Follow-up section. +- [ ] [P6-T2] Evaluate and check off AC1 (branch-3 gating applied) in `spec.md`, citing the P2-T2 count evidence that `if (Activated && Update is not null)` occurs twice in `KaStringAsync.cs` and `if (Update is not null)` occurs zero times. +- [ ] [P6-T3] Evaluate and check off AC2 (contract documented in-code) in `spec.md`, citing `evidence/qa-gates/latch-contract-doc..md` from P2-T3 and the `latch` and `///` counts. +- [ ] [P6-T4] Evaluate and check off AC3 (anti-regression: the early return is preserved) in `spec.md`, citing `evidence/qa-gates/early-return-preserved..md` from P2-T4 and the P2-T6 result that `KeyEquals_ContainsMatchWhileActivated_InvokesUpdateAndReturnsTrue` passed unmodified. +- [ ] [P6-T5] Evaluate and check off AC4 (null argument rejected explicitly) in `spec.md`, citing the P2-T1 guard-clause evidence and the P2-T6 Passed result for `KeyEquals_NullProbe_ThrowsArgumentNullExceptionNamingOther`. +- [ ] [P6-T6] Evaluate and check off AC5 (empty argument rejected explicitly) in `spec.md`, citing the P2-T1 guard-clause evidence and the P2-T6 Passed result for `KeyEquals_EmptyProbe_ThrowsArgumentExceptionNamingOther`, including the documented message text. +- [ ] [P6-T7] Evaluate and check off AC6 (the `ArgumentOutOfRangeException` path is closed) in `spec.md`, citing the P2-T6 Passed result for the two-variant empty-probe test and the `ThrowExactly` assertion form that distinguishes `ArgumentException` from its `ArgumentOutOfRangeException` subclass. +- [ ] [P6-T8] Evaluate and check off AC7 (`DelegateType` removed from both implementers) in `spec.md`, citing the P3-T9 evidence that the repository-wide `DelegateType` count over `*.cs` is 0 and that no `DelegateType` member was added to `IKbdAction.cs`. +- [ ] [P6-T9] Evaluate and check off AC8 (dead `Update` removed from four implementers) in `spec.md`, citing the P3-T4 and P3-T7 evidence that `_update` occurs zero times in `KaChar.cs` and `KaKey.cs`. +- [ ] [P6-T10] Evaluate and check off AC9 (`Update` retained on `KaStringAsync`) in `spec.md`, citing the P3-T9 evidence that `_update` still occurs exactly 3 times in `KaStringAsync.cs` and that the five-argument constructor still assigns it. +- [ ] [P6-T11] Evaluate and check off AC10 (unused `using` removed from `KaChar.cs` only) in `spec.md`, citing the P3-T2 and P3-T5 counts for `using System.Windows.Forms;` in the two files. +- [ ] [P6-T12] Evaluate and check off AC11 (commented-out interface members removed, live members unchanged) in `spec.md`, citing the P3-T8 counts and the P3-T11 clean compile proving no implementer signature changed. +- [ ] [P6-T13] Evaluate and check off AC12 (test renamed, body otherwise unchanged) in `spec.md`, citing the P1-T1 counts for the old and new method names. +- [ ] [P6-T14] Evaluate and check off AC13 (defect-1 regression test added, red before and green after) in `spec.md`, citing `evidence/regression-testing/red-before-fix..md` and `evidence/regression-testing/green-after-fix..md` for `KeyEquals_MultiCharNonMatchWhileNotActivated_DoesNotInvokeUpdateAndReturnsFalse`. +- [ ] [P6-T15] Evaluate and check off AC14 (latch-survives-transition test added) in `spec.md`, citing the P2-T6 Passed result for `KeyEquals_LatchSurvivesMatchThenNonMatchTransition_StillResetsToFirstChar`. +- [ ] [P6-T16] Evaluate and check off AC15 (defect-2 regression tests added, red before and green after) in `spec.md`, citing both regression-testing artifacts. If the P1-T9 artifact records the null-probe test as Passed before the fix rather than Failed, leave AC15 unchecked and record the gap explicitly, naming the observed pre-fix exception parameter name from the P1-T9 artifact as the reason: research section 4.3 establishes that the explicit guard changes the exception's origin rather than its type, so the parameter-name clause is the only red-before lever available for the null case. +- [ ] [P6-T17] Evaluate and check off AC16 (no pre-existing test deleted or weakened) in `spec.md`, citing `evidence/qa-gates/quickfiler-test-suite..md` from P4-T5. +- [ ] [P6-T18] Evaluate and check off AC17 (no test-project file edit) in `spec.md`, citing the P4-T1 zero-line `git status --porcelain` and `git diff --name-only` results for `QuickFiler.Test/QuickFiler.Test.csproj`. +- [ ] [P6-T19] Evaluate and check off AC18 (scope boundaries respected) in `spec.md`, citing the P4-T2 and P4-T3 zero-line results, and noting the `.claude/agent-memory/*` exclusion and why it is required. +- [ ] [P6-T20] Evaluate and check off AC19 (out-of-scope fourth defect not fixed, and filed) in `spec.md`, citing the P4-T4 counts for the unchanged `Substring` and `Contains` expressions and the unchanged `Be("b"` assertion, plus the P6-T1 artifact and the recorded issue number. If P6-T1 recorded `POSTING BLOCKED`, leave AC19 unchecked and record the gap. +- [ ] [P6-T21] Evaluate and check off AC20 (file-size limit respected) in `spec.md`, citing `evidence/qa-gates/file-size-audit..md` from P5-T3. +- [ ] [P6-T22] Evaluate and check off AC21 (full C# toolchain green in one uninterrupted pass) in `spec.md`, citing `evidence/qa-gates/csharpier-check..md`, `msbuild-analyzers..md`, `msbuild-nullable..md`, `vstest-final..md`, and `final-qc-pass-attestation..md`. Check the box only when all four stage exit codes are 0, the `Skipping target "CoreCompile"` count is 0 in both MSBuild logs, and the P5-T6 Failed count is 0. If the P5-T6 Failed count is non-zero but its failing-test-name set is identical to the P0-T15 baseline set, leave AC21 unchecked and record the pre-existing-failure gap with both artifact references. +- [ ] [P6-T23] Write the acceptance-criteria status summary. Acceptance: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/other/ac-status-summary..md` records `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` containing the block `Source:` naming `spec.md`, `Total AC items: 21`, `Checked off (delivered): M`, `Remaining (unchecked): 21 - M`, and `Items remaining:` listing the verbatim criterion text of every unchecked item with its recorded gap. The same summary is reproduced in the executor's final completion report. The plan ends here: no pull-request, PR-body, CI-monitoring, or merge task is in scope. diff --git a/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md new file mode 100644 index 000000000..d5e0262cf --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md @@ -0,0 +1,687 @@ +# Research — QuickFiler Keyboard-Action Contract Defects (Issue #445) + +- **Timestamp:** 2026-08-21T18-20 +- **Issue:** #445 +- **Branch:** `bug/quickfiler-keyboard-action-contract-defects-445` +- **Base:** `origin/epic/quickfiler-suite-determinism-foundation-integration` +- **Feature folder:** `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/` +- **Scope:** research only; no source file was modified. + +All line numbers below were re-derived by direct file read in this worktree on 2026-08-21. The +`file:line` citations in `issue.md` were captured against base commit `56ca1cea` and several have +drifted; corrections are recorded in section 1. + +--- + +## 0. Toolchain Availability (recorded before any procedural claim) + +**There is no Python toolchain in this repository.** + +- `SearchScope:` repository root of the worktree. +- `SearchPatterns:` `scripts/dev_tools/**`, `pyproject.toml`, `poetry.lock`, `**/pyproject.toml` +- `SearchResult:` none. + +Any skill or plan step naming `poetry run python -m scripts.dev_tools.*` is **unrunnable by +absence** in this repository. No such command was executed and no result is fabricated. + +Related: `spec.md:76` reads "Unit tests (pytest) for the fixed behavior and boundaries". This is an +unedited template artifact. The applicable framework is MSTest per CLAUDE.md CUT1. + +The applicable toolchain is the four-stage C# loop in CLAUDE.md CUT3: +`dotnet tool run csharpier format .` → analyzer `msbuild /t:Rebuild` → nullable `msbuild /t:Rebuild` +→ `vstest.console.exe /EnableCodeCoverage`. + +--- + +## 1. Corrected Line Numbers (issue.md citations re-derived) + +| Claim in `issue.md` | Cited | **Actual (verified)** | Status | +|---|---|---|---| +| `KeyEquals` span | `KaStringAsync.cs:57-78` | **`KaStringAsync.cs:57-79`** | off by one (closing brace at :79) | +| Branch 1 gate | (implied :59-61) | **`:59` condition, `:61` gate, `:62` `Update` call** | confirmed | +| Empty-string `Substring` | `KaStringAsync.cs:62` | **`:62`** | confirmed | +| Branch 2 (`Length == 1`) | (implied) | **`:65` condition, `:67` gate, `:68` `ToggleControl`** | confirmed | +| Branch 3 (`Length > 1`) | (implied) | **`:70` condition, `:72` ungated guard, `:73` `Update` call** | confirmed | +| Branch 3 `ToggleControl` | (implied) | **`:74` gate, `:75` call** | confirmed | +| `Activated = false` | `:77` (shown as `:46` in the excerpt's local numbering) | **`:77`** | confirmed | +| `KaChar` class decl | `KaChar.cs:11` | **`:11`** | confirmed | +| `KaChar.Delegate` | `KaChar.cs:37` | **`:37`** (backing field `:36`) | confirmed | +| `KaChar.DelegateType` | `KaChar.cs:43-46` | **`:43-46`** | confirmed | +| `KaKey.DelegateType` | not cited | **`KaKey.cs:43-46`** | added | +| `IKbdAction` commented members | `IKbdAction.cs:12-16` | **`:15-16`** (the two comments); `:11-14` are the live members | narrowed | +| `KaCharAsync` decl | `KaChar.cs:58` | **`:58`** | confirmed | +| `KaKeyAsync` decl | not cited | **`KaKey.cs:58`** | added | + +Additional call-site line numbers, verified: + +- `KbdActions.ContainsKey` — **`KbdActions.cs:49`** +- `KbdActions.FilterKeys` — **`KbdActions.cs:51`** +- `KbdActions.Find` — **`KbdActions.cs:53-69`** +- `KbdActions.FindIndex` — **`KbdActions.cs:71-88`** +- `KbdActions` indexer — **`KbdActions.cs:36-47`** (getter calls `Find` at `:38`; setter at `:41`) +- `KeyboardHandler` string-filter loop — **`KeyboardHandler.cs:178-202`** +- Sole production `KaStringAsync` construction — **`QfcCollectionController.cs:1376-1383`** + +--- + +## 2. Current-State Analysis + +### 2.1 The type under change + +`QuickFiler/Controllers/KaStringAsync.cs` (95 lines). `KeyEquals` in full, as it stands: + +```csharp +// KaStringAsync.cs:57-79 +public bool KeyEquals(string other) +{ + if (Key.Contains(other)) // :59 BRANCH 1 + { + if (Activated && Update is not null) // :61 gated + Update(Key.Substring(other.Length - 1, 1)); // :62 + return true; // :63 early return — NO Activated reset + } + else if (other.Length == 1) // :65 BRANCH 2 + { + if (Activated && ToggleControl is not null)// :67 gated + ToggleControl(); // :68 + } + else if (other.Length > 1) // :70 BRANCH 3 + { + if (Update is not null) // :72 NOT gated + Update(Key.Substring(0, 1)); // :73 + if (Activated && ToggleControl is not null)// :74 gated + ToggleControl(); // :75 + } + Activated = false; // :77 reached only from branches 2 and 3 + return false; // :78 +} +``` + +Two structural facts drive everything in section 3 and are easy to miss: + +1. **Branch 1 returns at `:63` before the `Activated = false` reset at `:77`.** A matching element + therefore never clears its own activation. `Activated` behaves as a latch that only a + *non-matching* probe can clear. +2. **The `other.Length == 0` case is unreachable as a fall-through.** `Key.Contains("")` is `true` + for every string, so an empty `other` always enters branch 1. The `if/else if/else if` chain has + no reachable path to `:77` other than branches 2 and 3. + +### 2.2 Ownership of the mutable members + +`SearchScope:` all `*.cs` in the repository worktree. +`SearchPatterns:` `Activated`, `ToggleControl`, `Update`, `\.Update\b`, `Update =`, `Update\(` +`SearchResult:` + +- `Activated` — declared **only** on `KaStringAsync` (`:50-55`). Read at `:61`, `:67`, `:74`; + written at `:77`. The **only** external write site in the entire repository is + **`KeyboardHandler.cs:187`**. +- `ToggleControl` — declared **only** on `KaStringAsync` (`:88-93`). Assigned only by the + five-argument constructor at `:26`. **No other assignment exists anywhere.** +- `Update` — declared on five types: `KaStringAsync.cs:81-86`, `KaChar.cs:50-55`, + `KaCharAsync` (`KaChar.cs:92-97`), `KaKey.cs:50-55`, `KaKeyAsync` (`KaKey.cs:92-97`). + Read **only** at `KaStringAsync.cs:61,62,72,73`. Written **only** at `KaStringAsync.cs:25` + (the five-argument constructor). + +Consequence: on `KaChar`, `KaCharAsync`, `KaKey`, and `KaKeyAsync`, `Update` is **write-never, +read-never dead API**. It is live only on `KaStringAsync`. + +### 2.3 The production construction site — both callbacks are null + +`QuickFiler/Controllers/QfcCollectionController.cs:1363-1385` is the **only** production code that +constructs a `KaStringAsync` with the five-argument constructor: + +```csharp +// QfcCollectionController.cs:1376-1383 +var stringAsyncAction = new KaStringAsync( + "Collection", + key, + (s) => ChangeByIndexAsync(int.Parse(s) - 1), + //(s) => grp.ItemViewer.LblItemNumber.Text = s, // :1380 update — COMMENTED OUT + null, // :1381 update + null // :1382 toggleControl +); +``` + +Two findings of first-rank importance: + +1. **`Update` and `ToggleControl` are both `null` in production, always.** The other registration + path, `KbdActions.Add(string, TKey, VDelegate)` (`KbdActions.cs:90-104`), builds its element with + `UClass instance = new()` at `:99` — the parameterless constructor (`KaStringAsync.cs:12`), which + assigns neither. There is no post-construction assignment anywhere (section 2.2). Therefore + **every `Update is not null` and `ToggleControl is not null` guard in `KeyEquals` evaluates + `false` in production today.** +2. **The commented-out line `:1380` recovers the design intent of `Update`.** It is + `grp.ItemViewer.LblItemNumber.Text = s` — `Update` writes a single character into the + per-row item-number label. This is the UI affordance the branches drive (section 3.3). + +`Key` is always non-empty in production: `GenerateStringKbdAction` assigns from `Digits` +(`QfcCollectionController.cs:114-128`), which returns `_itemGroups?.Count >= 10 ? 2 : 1` — only 1 or +2, so `:1369` or `:1373` always assigns. The `key = ""` initialization at `:1366` is never observed. + +### 2.4 The only keystroke-driven consumer + +`QuickFiler/Controllers/KeyboardHandler.cs:178-202`, inside `KeyDownTaskAsync`: + +```csharp +else if (StringActionsAsync != null) // :178 +{ + _filterBuilder.Append(char.ToLower((char)e.KeyValue)); // :180 + if (StringActionsAsync.ContainsKey(_filterBuilder.ToString())) // :181 PASS A + { + e.SuppressKeyPress = true; + e.Handled = true; + + if (_filterBuilder.Length == 1) + StringActionsAsync.ForEach(x => x.Activated = true); // :187 RE-ARM + var actions = StringActionsAsync.FilterKeys(_filterBuilder.ToString()); // :188 PASS B + if (actions.Length == 0) + _filterBuilder.Length = 0; + else if (actions.Length == 1) + { + var keyName = actions[0].Key; + await StringActionsAsync[keyName](keyName); // :194 PASS C + D + _filterBuilder.Length = 0; + } + } + else + { + _filterBuilder.Length--; // :200 + } +} +``` + +Probe-length analysis (answers "what string lengths, in what order"): + +- `:180` appends **before** every probe, so **every probe at `:181` has length >= 1**. A + non-matching keystroke is undone at `:200`, so the filter only ever grows along a matching path. + `:190` and `:195` reset the length to 0, but the next keystroke appends first. + **No production caller can pass an empty string to `KeyEquals`.** +- The probe at `:181` and at `:188` is the filter (`"1"`, then `"12"`, ...). The probe at `:194` is + `actions[0].Key` — the **full registered key**, not the filter. +- **`Activated = true` is set for all elements only when `_filterBuilder.Length == 1`** (`:186-187`), + i.e. once per filter sequence, and **after** the `ContainsKey` pass at `:181` has already run. + `ForEach` here is `EnumerableEx.ForEach` from `System.Interactive` 7.0.1 (in + `QuickFiler/packages.config:59`); there is no in-repo declaration + (`SearchScope:` all `*.cs`; `SearchPatterns:` `static.{0,80}ForEach`, `(void|IEnumerable<\w+>) ForEach`; + `SearchResult:` only the commented-out `UtilitiesCS/Extensions/IEnumerableExtensions.cs:94`). + It performs one pass and calls no `KeyEquals`. + +`KeyboardHandler` carries **`[ExcludeFromCodeCoverage]` at `KeyboardHandler.cs:22`**. The only +component that exercises these branches with real keystrokes is coverage-exempt, which is precisely +why the branch contract must be pinned by unit tests on `KaStringAsync` itself. + +--- + +## 3. Q1 — The `Activated`-Gating Contract for `KaStringAsync.KeyEquals` + +### 3.1 How many times `KeyEquals` fires per lookup (the deferred-LINQ question) + +`Find` (`KbdActions.cs:53-69`) builds a deferred `Where` query at `:55` and then re-enumerates it: + +| Call | Enumerations of the predicate | Count for a list of N with first match at 0-based index k | +|---|---|---| +| `ContainsKey` (`:49`, `Any`) | short-circuits at first `true` | `k+1`; `N` when no match | +| `FilterKeys` (`:51`, `ToArray`) | full | `N` | +| `Find` (`:53`) — 0 matches | `Count()` only | `N` | +| `Find` — exactly 1 match | `Count()` at `:56` **then** `First()` at `:62` | **`N + k + 1`** | +| `Find` — 2+ matches | `Count()` at `:56` then `Select(...)` at `:66` | `2N` | +| `FindIndex` (`:71`) — 1 match | `Count()` at `:74` then `_list.FindIndex` at `:80` | **`N + k + 1`** | +| Indexer get/set (`:38`, `:41`) | delegates to `Find` | as `Find` | + +`Enumerable.Where` returns an iterator, not an `ICollection`, so `Count()` has no fast path and +walks the whole sequence. **A single `Find` therefore invokes `KeyEquals` on the matching element +twice and on each preceding non-matching element twice.** `KeyEquals` has side effects, so this is +a semantic fact, not a performance note. + +### 3.2 What the re-enumeration does to each branch — the decisive asymmetry + +Combine section 3.1 with the two structural facts of section 2.1: + +- **A matching element** takes branch 1, which returns at `:63` without clearing `Activated`. Its + `Update` therefore fires on **every** enumeration pass — twice per `Find`. +- **A non-matching element** falls through to `:77` and clears `Activated` on the **first** pass. + On every later pass its **gated** `ToggleControl` is suppressed, but its **ungated** `Update` at + `:73` fires again. + +So today: **the gated side effects are self-limiting to one invocation per lookup (the `Activated` +latch absorbs the re-enumeration), while the ungated `Update` fires once per enumeration pass — a +count determined by `Find`'s internal use of `Count()` plus `First()`, not by user intent.** + +That is the strongest available characterization of the defect. It is not merely "one branch is +inconsistent"; the ungated call has an invocation count that is an artifact of a LINQ implementation +detail in a different class. + +### 3.3 What `Update` means in each branch (design intent, reconstructed) + +From `QfcCollectionController.cs:1380`, `Update(s)` sets a row's item-number label to the single +character `s`. The label is therefore a **"current typing depth" indicator**, showing one character +at a time. Under that model the three branches are coherent: + +| Branch | Condition | `Update` argument | Affordance | +|---|---|---|---| +| 1 (`:59`) | `Key.Contains(other)` | `Key.Substring(other.Length - 1, 1)` = **`Key[other.Length-1]`** | advance the label to the character at the current depth | +| 2 (`:65`) | non-match, `Length == 1` | *(none)* | at depth 1 the row never advanced, so it already shows `Key[0]`; only toggle it off | +| 3 (`:70`) | non-match, `Length > 1` | `Key.Substring(0, 1)` = **`Key[0]`** | the row may have advanced past depth 0, so reset the label to depth 0, then toggle it off | + +**Correction to the delegation brief.** Branch 1 passes `Key[other.Length - 1]` — the character at +the **last position of the matched prefix**, i.e. the character the user just matched. It is **not** +"the character AFTER the matched prefix". For `Key="abc"`, `other="ab"` it yields `"b"`, not `"c"`; +the existing test at `KaStringAsyncTests.cs:89-91` asserts exactly `"b"` and confirms this reading. + +The omission of `Update` from branch 2 is not a fourth inconsistency — it is correct under this +model, because a row that has never matched is already displaying `Key[0]`. + +### 3.4 The two candidate contracts + +**Option A — gate all three branches (`Activated && Update is not null` at `:72`).** + +**Option B — ungate branch 1 as well, so `Update` never checks `Activated` and only +`ToggleControl` does.** The argument for B is real and is recorded rather than discarded: +`ToggleControl` is a *toggle* — invoking it twice returns the control to its original state, so it +is genuinely non-idempotent and *must* be latched. `Update` is an *assignment* and is idempotent, so +it arguably needs no latch. Under B, `Activated` is redefined as "the latch that protects the +non-idempotent callback", which explains the current code exactly as written. + +### 3.5 Which option preserves observable production behavior + +**Neither option changes observable production behavior, and this is verifiable rather than +assumed.** `Update` and `ToggleControl` are `null` on every `KaStringAsync` instance that production +ever creates (section 2.3), so `Update is not null` at `:72` is `false` on every production +evaluation and the guarded call is unreachable. Any redistribution of the `Activated` condition +across the three branches is therefore a **no-op in production today**. The change is observable +only in tests, and in any future code that supplies a non-null `Update`. + +This removes the usual "which option is safer" tie-breaker and forces the decision onto contract +quality alone. + +### 3.6 Recommendation — Option A, gate all three branches + +**Recommendation: add the `Activated` conjunct to branch 3 at `KaStringAsync.cs:72`, making all +three branches gate uniformly on `Activated`, and document the latch semantics in an XML comment.** + +Four supporting findings: + +1. **It makes every side effect's invocation count independent of LINQ re-enumeration.** Today the + ungated `Update` fires 2x per `Find` and 1x per `ContainsKey`/`FilterKeys` pass; under Option A it + fires at most once per element per keystroke, matching `ToggleControl`. The current behaviour is + only harmless because the callback happens to be an idempotent assignment — a property no + signature enforces and no test asserts. +2. **It loses no semantically meaningful label reset.** The reset in branch 3 matters only for a row + that had *advanced* its label. Any such row executed branch 1 with `Update`, which requires + `Activated == true` (`:61`) and returns at `:63` without clearing it. Therefore, **at the moment a + row first stops matching, its `Activated` is still `true`**, and the gated reset still fires. + Only *subsequent* probes of an already-reset row are suppressed — and those writes are redundant, + re-writing `Key[0]` over `Key[0]`. +3. **It matches the acceptance criterion as written** (`issue.md:124-125`: "applied consistently + across all three branches"). +4. **It requires no change to any existing test** (section 5), whereas Option B inverts + `KeyEquals_ContainsMatchWhileNotActivated_ReturnsTrueWithoutUpdate` + (`KaStringAsyncTests.cs:98-114`), turning a passing characterization test into a contradiction. + +**Counter-argument to Option A, stated fairly.** If `Activated` is really "the latch for the +non-idempotent callback" (section 3.4), then gating an idempotent assignment behind it is +over-application, and Option B is the more precise contract. Under Option A, a hypothetical future +`Update` that is *not* idempotent — say, one that animates or appends — would be suppressed on rows +deactivated earlier in the sequence, whereas today it would fire. The rebuttal is that finding 2 +shows the *first* reset after an advance is always still gated-through, so a non-idempotent `Update` +would be *better* served by Option A (exactly one reset) than by the status quo (one reset per +enumeration pass, a count no caller can predict). + +### 3.7 Implementation trap — do not "finish" the consistency by moving the reset + +A natural follow-on edit is to make branch 1 also fall through to `Activated = false` at `:77`, for +symmetry. **This would break the feature.** On a keystroke of length >= 2 there is no re-arm +(`:186-187` runs only at length 1), and the call order is `ContainsKey` (`:181`) → `FilterKeys` +(`:188`) → indexer/`Find` (`:194`). If branch 1 cleared `Activated`, the `ContainsKey` pass would +consume the activation and the `FilterKeys` pass would no longer advance the label. **The early +return at `:63` is load-bearing and must be preserved verbatim.** + +--- + +## 4. Q2 — Contract for an Empty `other` + +### 4.1 Exact current behavior + +`Key.Contains("")` is `true` for every string, so `KeyEquals("")` enters branch 1 and evaluates +`Key.Substring(-1, 1)` at `:62` — `ArgumentOutOfRangeException`. **The throw is conditional on +`Activated && Update is not null`.** With either false, `KeyEquals("")` returns `true` without +throwing, which makes `ContainsKey("")` true and `FilterKeys("")` return **every** registered action. +Because production always has `Update == null` (section 2.3), the *reachable* production +misbehaviour is the silent "empty matches everything", not the exception. + +### 4.2 Reachability + +No production caller can pass an empty string (section 2.4: `:180` appends before every probe). But +`KbdActions` is a `public` generic type and `ContainsKey`, `FilterKeys`, `Find`, and the +indexer are public API. `Find("")` against a registry holding two or more actions matches all of +them and throws `InvalidOperationException` from `KbdActions.cs:67`. + +### 4.3 Options + +| Option | Behavior | Existing test that changes | Production caller affected | +|---|---|---|---| +| 1. Early-return `false` | empty probe matches nothing | **none** | none | +| 2. Early-return `true` | preserves `Contains("")==true`; `FilterKeys("")` still returns everything | **none** | none | +| 3. Throw `ArgumentException` | explicit rejection at the boundary | **none** | none | +| 4. Guard only the `Substring` | keeps `true`, removes the crash, leaves "matches everything" undocumented | **none** | none | + +**No existing test passes an empty string**, so all four options are neutral with respect to the +current suite and each requires new tests. +`SearchScope:` `QuickFiler*/**/*.cs`. `SearchPatterns:` `KeyEquals\(""\)`, `KeyEquals\(string\.Empty\)`. +`SearchResult:` none. + +**Recommendation: Option 3 — throw `ArgumentException` from a guard clause placed at the top of +`KeyEquals`, before the `Key.Contains(other)` test.** It satisfies the acceptance criterion's own +second limb (`issue.md:126-127`, "or rejects it with an explicit, documented argument exception"), +it matches CLAUDE.md General §3 and C#4.1 ("fail fast and explicitly"), and it has zero production +callers to break. Option 1 is the acceptable fallback if a reviewer prefers a total, non-throwing +function; Option 2 is the weakest because it preserves the "empty matches every action" semantics +that make `FilterKeys("")` meaningless. + +**Optional hardening in the same guard.** `KeyEquals(null)` currently throws +`ArgumentNullException` from inside `string.Contains`. Promoting that to an explicit +`throw new ArgumentNullException(nameof(other))` in the same guard block costs one line and makes +the contract self-documenting. It changes the exception's origin, not its type. + +### 4.4 The fourth latent defect — `Substring(other.Length - 1, 1)` is wrong for a non-prefix match + +**Confirmed as a genuine, distinct defect.** Branch 1's guard is `Key.Contains(other)` — a substring +test — but its `Substring(other.Length - 1, 1)` is only meaningful when `other` is a **prefix** of +`Key`. Counter-example: `Key = "abc"`, `other = "b"`. `Contains` is `true`; the expression yields +`Substring(0, 1) == "a"`, which is neither the matched character `"b"` nor the following character +`"c"`. + +It is reachable in principle whenever `Digits == 2`: with keys `"01".."12"`, typing `"1"` matches +`"01"` by `Contains` at index 1 (not a prefix), and `Update` would receive `"0"` instead of `"1"`. +It has no observable effect today because `Update` is null. + +The existing test `KeyEquals_ContainsMatchWhileActivated_InvokesUpdateAndReturnsTrue` +(`KaStringAsyncTests.cs:76-96`) uses `Key="abc"`, `other="ab"` — a prefix — so it does not expose it. + +**Scope recommendation: OUT of scope for #445.** It is not among the three defects the issue +enumerates, and CLAUDE.md's Bugfix Workflow §2 directs that a deeper design problem uncovered during +a fix be raised as a new issue rather than widening scope. Fixing it correctly requires deciding +whether the branch should test `StartsWith` instead of `Contains` — a behavior change to keyboard +filtering that `KbdActionsTests.cs:71-76` currently pins to substring semantics +("`KaStringAsync.KeyEquals` substring matching must remain available for keyboard filtering"). +**Recommended action: promote to a new potential entry / GitHub issue.** It does not conflict with +the Q1 or Q2 changes, which touch branch 3's gate and a top-of-method guard respectively, leaving +`:62` untouched. + +--- + +## 5. Q3 — `DelegateType` and `Update` Disposition + +### 5.1 `DelegateType` has zero read sites + +`SearchScope:` all `*.cs` in the worktree (a repo-wide search including `docs/**` was run first and +returned only prose hits in the feature/potential markdown, which are not code). +`SearchPatterns:` `DelegateType` +`SearchResult:` exactly three, none of them a read: + +- `QuickFiler/Interfaces/IKbdAction.cs:16` — a comment, `//Type DelegateType { get; }` +- `QuickFiler/Controllers/KaKey.cs:43` — declaration (`:43-46`) +- `QuickFiler/Controllers/KaChar.cs:43` — declaration (`:43-46`) + +**Confirmed: no consumer reads `DelegateType`.** It is not on the interface, so no polymorphic call +site can reach it either. Removing both declarations breaks no compilation and no test +(`KaCharTests.cs` and `KaKeyTests.cs` contain no occurrence — see section 6). + +`KaKey.DelegateType` returning `typeof(Action)` is *correct* for `KaKey` (which stores +`Action` at `:36-41`); `KaChar.DelegateType` returning the same is *wrong* for `KaChar` (which +stores `Action` at `:36-41`). Removal resolves defect 3 without needing to decide the right +value. + +**Implementation note (analyzer-relevant).** `Keys` appears in `KaChar.cs` **only** at `:45`, inside +`DelegateType`. After removal, `using System.Windows.Forms;` at `KaChar.cs:6` becomes unused and +should be removed in the same edit. `KaKey.cs` uses `Keys` as its key type throughout, so its +`using` stays. Note that IDE0005 is evidently not error-level in this build: `IKbdAction.cs:1-5` +carries five `using` directives none of which the file uses, and it compiles today. Removing the +`using` is hygiene, not a build requirement. + +### 5.2 `Update` should be removed from four of the five implementers + +`Update` **is** read — but only inside `KaStringAsync.KeyEquals` (`:61,62,72,73`), and it is written +only by `KaStringAsync`'s own constructor (`:25`). Section 2.2 establishes there is no other read or +write anywhere in the repository. + +| Type | `Update` declared at | Read anywhere? | Written anywhere? | Disposition | +|---|---|---|---|---| +| `KaStringAsync` | `KaStringAsync.cs:81-86` | **yes** (`:61,62,72,73`) | yes (`:25`) | **KEEP** | +| `KaChar` | `KaChar.cs:50-55` | no | no | **REMOVE** | +| `KaCharAsync` | `KaChar.cs:92-97` | no | no | **REMOVE** | +| `KaKey` | `KaKey.cs:50-55` | no | no | **REMOVE** | +| `KaKeyAsync` | `KaKey.cs:92-97` | no | no | **REMOVE** | + +### 5.3 The two commented-out members in `IKbdAction.cs:15-16` + +**Recommendation: delete both lines.** + +- `//Type DelegateType { get; }` (`:16`) — restoring it **will not compile**: `KaStringAsync` + (`KaStringAsync.cs:10`), `KaCharAsync` (`KaChar.cs:58`) and `KaKeyAsync` (`KaKey.cs:58`) do not + declare it. Confirmed by direct read of all three. Since section 5.1 removes the only two + declarations, the comment documents a member that will no longer exist anywhere. +- `//Action Update { get; set; }` (`:15`) — restoring it would force all five implementers + to keep `Update`, contradicting section 5.2, which removes it from four. `Update` is an + implementation detail of `KaStringAsync`'s filtering feedback, not a contract shared by a `char`- + or `Keys`-keyed action. + +Deleting both satisfies `issue.md:129-130` ("resolved (removed or restored with all implementers +updated)"). `IKbdAction.cs` shrinks from 18 to 16 lines; its live members at `:11-14` are untouched, +so no implementer changes. + +--- + +## 6. Q4 — Existing Characterization Tests + +### 6.1 Headline finding: no existing test asserts any of the three defects + +The premise recorded at `issue.md:108-111` — that #430's tests "characterize the current behavior, +including the ungated `Update` call and the empty-string throw, so that a later fix has a +red-before-green baseline" — **is not borne out by the committed tests.** + +- The multi-char branch test sets **`ka.Activated = true`** (`KaStringAsyncTests.cs:141`), so it + exercises the branch with the gate *satisfied*. It does not distinguish gated from ungated and + **passes unchanged under Option A**. +- **No test passes an empty string** (`SearchScope:` `QuickFiler*/**/*.cs`; `SearchPatterns:` + `KeyEquals\(""\)`, `KeyEquals\(string\.Empty\)`; `SearchResult:` none). +- **No test references `DelegateType`** (`SearchScope:` all `*.cs`; `SearchPatterns:` `DelegateType`; + `SearchResult:` three hits, all in production files, listed in section 5.1). + +**Consequence for planning:** there is no red-before-green baseline to inherit, and +`issue.md:131` ("replacing the characterization tests added by #430") describes work that does not +exist — **nothing needs replacing or deleting.** The regression tests for defects 1 and 2 must be +authored fresh, and each will be genuinely red before the fix. + +### 6.2 Per-test disposition + +**`QuickFiler.Test/Controllers/KaStringAsyncTests.cs` — 168 lines** + +| Test method | Lines | Asserts a defect? | Disposition under Option A | +|---|---|---|---| +| `Constructor_LowercasesKeyAndStoresMembers` | 27-40 | no | unchanged | +| `KeySetter_LowercasesValue` | 42-53 | no | unchanged | +| `Delegate_AwaitsAndCompletesSynchronously` | 55-74 | no | unchanged | +| `KeyEquals_ContainsMatchWhileActivated_InvokesUpdateAndReturnsTrue` | 76-96 | no (branch 1, gated) | **unchanged**; also pins `Substring(other.Length-1,1)` via `.Be("b")` at `:89-91` — leave as-is, section 4.4 is out of scope | +| `KeyEquals_ContainsMatchWhileNotActivated_ReturnsTrueWithoutUpdate` | 98-114 | no | **unchanged** under A; would need **inversion** under Option B | +| `KeyEquals_SingleCharNonMatchWhileActivated_InvokesToggleControlAndReturnsFalse` | 116-131 | no (branch 2, gated) | unchanged | +| `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` | 133-152 | **no** — sets `Activated = true` at `:141` | **RETAIN, RENAME** to `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse`; the current name implies coverage of the ungated case that it does not provide | +| `KeyEquals_NullDelegatesAreToleratedInNonMatchBranches` | 154-166 | no | unchanged | + +**`QuickFiler.Test/Controllers/KbdActionsTests.cs` — 88 lines** + +| Test method | Lines | Disposition | +|---|---|---| +| `Add_WhenSourceAndStoredKeysAreDistinct_DoesNotTreatSubstringAsDuplicate` | 13-29 | unchanged | +| `Add_WhenSourceAndStoredKeyAreExactDuplicate_ThrowsArgumentException` | 31-47 | unchanged | +| `FilterKeys_WhenDistinctStoredKeysCoexist_PreservesKeyboardMatchingSemantics` | 49-86 | unchanged — elements come from `Add(sourceId,key,delegate)` (`KbdActions.cs:99`, parameterless ctor), so `Update`/`ToggleControl` are null and `Activated` is false. **Note:** `:71-76` pins `Contains`-based substring matching; it is the test that would block a `StartsWith` fix for section 4.4 | + +**`QuickFiler.Test/Controllers/KbdActionsRemainingBranchesTests.cs` — 181 lines** +All 11 tests use `KaKey`, whose `KeyEquals` (`KaKey.cs:48`) is `Key == other` — side-effect-free. +No test references `Update` or `DelegateType`. **All unchanged**, including after `Update` is removed +from `KaKey` (section 5.2). + +**`QuickFiler.Test/Controllers/KaCharTests.cs` — 155 lines** +Nine tests, none referencing `DelegateType` or `Update`. **All unchanged.** + +**`QuickFiler.Test/Controllers/KaKeyTests.cs` — 144 lines** +Seven tests, none referencing `DelegateType` or `Update`. **All unchanged.** + +### 6.3 New tests required + +All belong in `KaStringAsyncTests.cs` (MSTest + FluentAssertions per CLAUDE.md CUT1/CUT2): + +1. **Defect 1 regression (red before fix):** `Key="abc"`, `other="zz"`, non-null `Update`, + `Activated = false` → assert `Update` is **not** invoked and the result is `false`. This is the + test that fails today and passes after gating `:72`. +2. **Defect 1, latch-survives-transition:** a row that matched at depth 1 then fails at depth 2 + still receives its `Key[0]` reset, proving finding 2 of section 3.6. +3. **Defect 2:** `KeyEquals("")` → assert `ArgumentException` with the documented message + (per the section 4.3 recommendation). Add the `Activated=true`/non-null-`Update` variant so the + old `ArgumentOutOfRangeException` path is explicitly closed. +4. **Optional:** `KeyEquals(null)` → `ArgumentNullException`. + +### 6.4 Test-file sizes against the 500-line cap + +| File | Lines | Headroom | +|---|---|---| +| `KaStringAsyncTests.cs` | **168** | grows by ~50-60 lines for section 6.3 → ~225. Comfortable | +| `KbdActionsRemainingBranchesTests.cs` | **181** | unchanged | +| `KaCharTests.cs` | **155** | unchanged | +| `KaKeyTests.cs` | **144** | unchanged | +| `KbdActionsTests.cs` | **88** | unchanged | + +**None is at or near the 500-line cap.** No test-file split is required. + +### 6.5 Test project file — confirmed, do not edit + +`QuickFiler.Test/QuickFiler.Test.csproj` already carries all five compile entries: + +``` +96: +97: +98: +99: +100: +``` + +**No `.csproj` edit is required or permitted** — a sibling epic child owns this file. Because all +new tests go into existing files, no new `` entry is needed. + +--- + +## 7. Q5 — Production File Sizes + +| File | Lines | Post-change estimate | vs 500 cap | +|---|---|---|---| +| `QuickFiler/Controllers/KaStringAsync.cs` | **95** | ~110 (guard clause + XML doc) | far under | +| `QuickFiler/Controllers/KaChar.cs` | **99** | ~88 (remove `DelegateType` `:43-46`, two `Update` props, one `using`) | far under | +| `QuickFiler/Controllers/KaKey.cs` | **99** | ~90 (remove `DelegateType` `:43-46`, two `Update` props) | far under | +| `QuickFiler/Interfaces/IKbdAction.cs` | **18** | 16 | far under | +| `QuickFiler/Controllers/KbdActions.cs` | **146** | 146 (unchanged) | far under | + +**Confirmed: no in-scope file approaches the 500-line cap; three of the five shrink.** + +Two adjacent files, recorded for awareness only — **neither is modified by this work**: + +- `QuickFiler/Controllers/KeyboardHandler.cs` — **414 lines**, under the cap but with limited + headroom. It is `[ExcludeFromCodeCoverage]` (`:22`). +- `QuickFiler/Controllers/QfcCollectionController.cs` — **2349 lines**, a **pre-existing violation** + of the 500-line rule. Out of scope; do not touch. Reading `:1363-1385` is sufficient. + +--- + +## 8. Q6 — Coverage Baseline Context + +### 8.1 `coverage.config` excludes none of these files + +`coverage.config` (24 lines, repo root) contains a single `` block listing +seven third-party module patterns: `Deedle`, `FSharp`, `Castle.Core`, `FluentAssertions`, `Moq`, +`Microsoft.Testing`, `MSTest` (`:14-20`). There is **no** `` or `` exclusion and +no QuickFiler entry. + +`SearchScope:` `coverage.config` (full read). +`SearchPatterns:` `KaStringAsync`, `KaChar`, `KaKey`, `KbdActions`, `IKbdAction`, `QuickFiler` +`SearchResult:` none. + +**Confirmed: none of the five production files is excluded from coverage measurement.** + +### 8.2 `KbdActions<>` is explicitly NOT exempt — confirmed + +CLAUDE.md UT2, COM/VSTO/WinForms coverage exemption, final sentence: + +> Testable seams within otherwise-COM-bound assemblies (e.g., `ToDoLoader`, `IDList` arithmetic, +> **`KbdActions<>`**, path/settings helpers) are explicitly NOT exempt and must meet the `>= 80%` +> floor. + +**Confirmed verbatim.** `KbdActions<>` is named as a non-exempt testable seam. `KaStringAsync`, +`KaChar`, `KaKey`, and `IKbdAction` are pure value objects with no Outlook/COM dependency and fall +outside every limb (a), (b), (c) of the exemption, so they too are in the testable denominator. + +The one exemption in this cluster is **`KeyboardHandler`**, which carries +`[ExcludeFromCodeCoverage]` at `KeyboardHandler.cs:22` — consistent with limb (c) (it depends on +`Microsoft.Office.Interop.Outlook` via `:15` and on WinForms event args). + +### 8.3 Threshold divergence, recorded without adjudication + +CLAUDE.md UT2 states a repository-wide line-coverage floor of **`>= 80%`** with **`>= 90%`** for new +modules/classes/methods. `.claude/rules/general-unit-test.md` and `.claude/rules/quality-tiers.md` +state **`>= 85%` line / `>= 75%` branch** uniformly across T1-T4. This divergence is pre-existing and +is not resolved by this issue. Under CLAUDE.md's Policy Compliance Order, CLAUDE.md is applied first. +The changes proposed here add tests and delete dead members, so coverage for the touched files should +rise under either figure; **no coverage exemption is sought or needed.** + +--- + +## 9. Consolidated Change Set and Test Strategy + +### 9.1 Files to change (five production files, one test file) + +| # | File | Change | Defect | +|---|---|---|---| +| 1 | `QuickFiler/Controllers/KaStringAsync.cs:72` | add `Activated &&` to the branch-3 guard | 1 | +| 2 | `QuickFiler/Controllers/KaStringAsync.cs:57` (top of method) | guard clause rejecting empty (and optionally null) `other` | 2 | +| 3 | `QuickFiler/Controllers/KaStringAsync.cs:57` | XML doc recording the `Activated` latch contract and the empty-string contract | 1, 2 | +| 4 | `QuickFiler/Controllers/KaChar.cs:43-46` | delete `DelegateType`; also delete `using System.Windows.Forms;` at `:6` | 3 | +| 5 | `QuickFiler/Controllers/KaChar.cs:50-55`, `:92-97` | delete dead `Update` from `KaChar` and `KaCharAsync` | related | +| 6 | `QuickFiler/Controllers/KaKey.cs:43-46` | delete `DelegateType` | 3 | +| 7 | `QuickFiler/Controllers/KaKey.cs:50-55`, `:92-97` | delete dead `Update` from `KaKey` and `KaKeyAsync` | related | +| 8 | `QuickFiler/Interfaces/IKbdAction.cs:15-16` | delete both commented-out members | related | +| 9 | `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` | rename one test (`:134`); add the four tests of section 6.3 | 1, 2 | + +**Not changed:** `KbdActions.cs` (owned in part by #472/#482 in a later epic — do not do their work), +`KeyboardHandler.cs`, `QfcCollectionController.cs`, `QuickFiler.Test.csproj`, and the four other test +files. + +### 9.2 Test strategy (no test code authored here, per research-only scope) + +- **Framework:** MSTest `[TestClass]`/`[TestMethod]`, Moq where a mock is warranted, FluentAssertions + for assertions (CLAUDE.md CUT1, CUT2). The existing `KaStringAsyncTests.NewKa` helper (`:20-25`) + already supplies optional `update`/`toggle` callbacks and should be reused. +- **Bugfix ordering (CLAUDE.md Bugfix Workflow §1):** author the defect-1 and defect-2 regression + tests **first** and observe them fail. Section 6.1 establishes both will be genuinely red, so a + fail-before exception dossier is **not** required. +- **Determinism:** these are pure value objects. Assertions use simple captured locals, no clock, no + timer, no temp file, no external dependency. `[ExcludeFromCodeCoverage]` on `KeyboardHandler` means + no test should attempt to drive the branches through it; test `KaStringAsync` directly. +- **Scenario completeness (CLAUDE.md UT2):** for `KeyEquals`, cover each of the three branches at + both `Activated` states and at both null/non-null `Update`, plus the empty-string and null + boundaries. That is the matrix the current suite leaves half-covered. +- **Deletion safety:** the removals in changes 4-8 need no new test; their safety is established by + the zero-read-site evidence in section 5 and is proven by the analyzer and nullable builds + compiling. +- **Evidence:** baseline and QA-gate artifacts go to + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence//` + per `evidence-and-timestamp-conventions`, using the `yyyy-MM-ddTHH-mm` timestamp format. + +### 9.3 Open decision for the planner + +Section 4.3 recommends **Option 3 (throw `ArgumentException`)** for the empty-string contract, with +Option 1 (return `false`) as the fallback. Both are test-neutral against the existing suite. The +acceptance criterion at `issue.md:126-127` permits either. This is the one place where a reviewer +preference could reasonably override the recommendation. + +### 9.4 Follow-up to raise as a separate issue + +The non-prefix `Substring` defect of section 4.4 (`KaStringAsync.cs:62`). Out of scope for #445; +recommend promoting to a potential entry / GitHub issue rather than widening this bugfix. diff --git a/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md new file mode 100644 index 000000000..6bc981549 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md @@ -0,0 +1,698 @@ +# quickfiler-keyboard-action-contract-defects (Spec) + +- **Issue:** #445 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T18-45 +- **Status:** Approved +- **Version:** 1.0 + +## Context + +Three related contract defects in the QuickFiler keyboard-action types (`KaStringAsync`, `KaChar`, +`KaKey`, and the `IKbdAction` interface). The defects were first recorded in `issue.md` against +base commit `56ca1cea` and were re-verified line by line in this worktree on 2026-08-21 by the +research artifact +`docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md`. +Where the two disagree, the research artifact is authoritative; its corrections are carried into this +spec. + +The three defects are: + +1. **Inconsistent `Activated` gating in `KaStringAsync.KeyEquals`.** Branches 1 and 2 gate their side + effects on `Activated`; branch 3 (`other.Length > 1`) invokes `Update` without the gate. +2. **`KeyEquals("")` has no defined contract.** `Key.Contains("")` is `true` for every string, so an + empty probe enters branch 1 and, when `Activated && Update is not null`, evaluates + `Key.Substring(-1, 1)` and throws `ArgumentOutOfRangeException`. When the guard is false it + silently returns `true`, so an empty probe "matches" every registered action. +3. **`DelegateType` reports the wrong type on `KaChar`.** `KaChar` stores an `Action` but + `DelegateType` returns `typeof(Action)`. `DelegateType` and a dead `Update` property are + orphaned public API on several implementers, and the corresponding interface members are commented + out in `IKbdAction.cs`. + +Work mode for this issue is `full-bug`. This `spec.md` is the sole authoritative acceptance-criteria +source; no `user-story.md` exists or will be created. + +### Correction carried from research — no characterization tests exist + +`issue.md` states (section "Why these were not fixed in issue #430") that #430's tests characterize +the current defective behavior and that this work must "replace the characterization tests added by +#430". **That premise is false and is superseded by the research artifact.** No committed test +asserts any of the three defects: + +- `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` + (`QuickFiler.Test/Controllers/KaStringAsyncTests.cs:133-152`) sets `ka.Activated = true` at `:141`, + so it exercises branch 3 with the gate **satisfied**. It does not distinguish gated from ungated + and passes unchanged after the fix. Its name implies coverage of the ungated case that it does not + provide, so it is renamed. +- No committed test passes an empty string to `KeyEquals`. +- No committed test references `DelegateType`. + +Consequences for this work: nothing needs replacing or deleting; both regression tests are authored +fresh; each will be genuinely red before the fix and green after; and no fail-before exception +dossier is required, because real red-before-green runs are available. + +## Repro & Evidence + +- **Steps to reproduce (with data/flags/inputs):** + - *Defect 1 (ungated branch 3).* Construct `new KaStringAsync("src", "abc", func, update, toggle)` + with a non-null `update` callback. Leave `Activated` at its default `false` + (`KaStringAsync.cs:50`). Call `KeyEquals("zz")`. The `Update` callback is invoked with `"a"` + despite `Activated` being `false`, because the branch-3 guard at `KaStringAsync.cs:72` reads + `if (Update is not null)` with no `Activated` conjunct. + - *Defect 2 (empty probe).* On the same instance, set `Activated = true` and call `KeyEquals("")`. + `Key.Contains("")` is `true`, so control enters branch 1 and `KaStringAsync.cs:62` evaluates + `Key.Substring(-1, 1)`, which throws `ArgumentOutOfRangeException`. With `Activated == false` or + `Update == null`, the same call returns `true` without throwing, so + `KbdActions.FilterKeys("")` returns every registered action and + `Find("")` throws `InvalidOperationException` from `KbdActions.cs:67` whenever two or more + actions are registered. + - *Defect 3 (`DelegateType`).* Read `KaChar.cs:11` (`KaChar : IKbdAction>`), + `KaChar.cs:37` (`public Action Delegate`), and `KaChar.cs:43-46` + (`DelegateType => typeof(Action)`). The reported type does not match the stored delegate + type. +- **Expected vs actual behavior:** + - Defect 1 — Expected: no `KeyEquals` side effect fires while `Activated` is `false`. Actual: + branch 3's `Update` fires regardless of `Activated`. + - Defect 2 — Expected: an empty probe is either rejected explicitly or defined to match nothing. + Actual: undefined; either a low-level `ArgumentOutOfRangeException` or a silent + "matches-everything" result depending on unrelated state. + - Defect 3 — Expected: a type-reporting member reports the stored delegate type, or does not + exist. Actual: `KaChar.DelegateType` reports `Action` for a stored `Action`. +- **Logs/screenshots/error snippets:** none. All three defects are established by direct file read, + not by a captured runtime failure. Defect 2's exception was reproduced by reading the argument + arithmetic (`other.Length - 1 == -1`), not observed in a running session. +- **Frequency / determinism (always, intermittent, data-dependent):** All three are deterministic + functions of the arguments and of instance state; none is timing- or data-dependent. All three are + currently **latent in production**: the only five-argument construction site, + `QuickFiler/Controllers/QfcCollectionController.cs:1376-1383`, passes `null` for both `update` + (`:1381`) and `toggleControl` (`:1382`), and the other registration path, + `KbdActions.Add(string, TKey, VDelegate)`, builds its element with `UClass instance = new()` at + `KbdActions.cs:99` — the parameterless constructor at `KaStringAsync.cs:12`, which assigns neither + callback. Every `Update is not null` and `ToggleControl is not null` guard in `KeyEquals` therefore + evaluates `false` on every production evaluation today. + +## Scope & Non-Goals + +- **In scope:** + - `QuickFiler/Controllers/KaStringAsync.cs` — apply the `Activated` gate to branch 3; add a + fail-fast argument guard for `null` and empty `other`; add an XML doc comment recording both + contracts. + - `QuickFiler/Controllers/KaChar.cs` — delete `DelegateType`; delete the dead `Update` property + from `KaChar` and `KaCharAsync`; delete the then-unused `using System.Windows.Forms;`. + - `QuickFiler/Controllers/KaKey.cs` — delete `DelegateType`; delete the dead `Update` property from + `KaKey` and `KaKeyAsync`. + - `QuickFiler/Interfaces/IKbdAction.cs` — delete the two commented-out members at `:15-16`. + - `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` — rename one existing test; add the four new + tests described in **Test Strategy**. +- **Out of scope / non-goals:** + - **A fourth latent defect at `KaStringAsync.cs:62` is deliberately excluded.** Branch 1's guard is + `Key.Contains(other)` — a substring test — but its argument `Key.Substring(other.Length - 1, 1)` + is only meaningful when `other` is a **prefix** of `Key`. For `Key = "abc"` and `other = "b"`, + `Contains` is `true` and the expression yields `"a"`, which is neither the matched character nor + the following one. It is reachable in principle whenever the digit width is 2 (registered keys + `"01"`..`"12"`; typing `"1"` matches `"01"` at index 1, not as a prefix). It has no observable + effect today because `Update` is null in production. Fixing it correctly requires deciding + whether branch 1 should test `StartsWith` instead of `Contains` — a keyboard-filtering behavior + change that `QuickFiler.Test/Controllers/KbdActionsTests.cs:71-76` currently pins to substring + semantics. Per CLAUDE.md Bugfix Workflow section 2, it is recorded and promoted to a new issue + rather than widening this bugfix. The existing assertion at `KaStringAsyncTests.cs:89-91` + (`.Be("b")` for `Key = "abc"`, `other = "ab"`) pins the prefix case and is left exactly as-is. + - `QuickFiler/Controllers/KbdActions.cs` — not modified. Its LINQ re-enumeration behavior is + diagnostic input to the gating decision, not a target of this fix; parts of this file are owned + by other issues in a later epic. + - `QuickFiler/Controllers/KeyboardHandler.cs` — not modified. It is `[ExcludeFromCodeCoverage]` at + `KeyboardHandler.cs:22` and is read only to establish call-order and probe-length facts. + - `QuickFiler/Controllers/QfcCollectionController.cs` — not modified. At 2349 lines it is a + pre-existing violation of the 500-line rule; reading `:1363-1385` is sufficient and this work + does not touch it or attempt to remediate its size. + - `QuickFiler.Test/QuickFiler.Test.csproj` — not modified. All five relevant test files already + carry `` entries at `:96-100`, and a sibling epic child owns this file. + - No change to the `Contains`-based matching semantics of `KeyEquals`. + - No coverage exemption, no `coverage.config` change, no `[ExcludeFromCodeCoverage]` addition. +- **Explicitly excluded systems, integrations, or datasets:** + - No file under `.claude/**` is edited. Rule and policy files are cited as the standard this fix is + measured against, never as edit targets. + - No file under `docs/features/potential/**` is written by this work. + - No Outlook, COM, or Microsoft Graph interaction. The changed types are pure value objects. + - There is no Python toolchain in this repository (no `scripts/dev_tools/`, no Poetry manifest), so + any step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence and must be + reported as such rather than executed or simulated. + +## Root Cause Analysis + +- **Current hypothesis or confirmed root cause:** + - *Defect 1 — confirmed.* `KaStringAsync.cs:72` omits the `Activated &&` conjunct that `:61` and + `:67` and `:74` all carry. There is no comment explaining the omission and no test pinning it, so + it is treated as an omission rather than an intentional asymmetry. + - *Defect 2 — confirmed.* `KeyEquals` has no argument validation. `string.Contains("")` returning + `true` for every receiver makes the empty probe enter branch 1, where the offset arithmetic + `other.Length - 1` becomes `-1`. + - *Defect 3 — confirmed.* `KaChar.DelegateType` was written with the same body as + `KaKey.DelegateType` (`typeof(Action)`), which is correct for `KaKey` and wrong for + `KaChar`. Because `DelegateType` is not on `IKbdAction`, the compiler never cross-checks it. +- **Signals/evidence supporting it:** + - **Invocation-count evidence for defect 1.** `KbdActions.Find` (`KbdActions.cs:53-69`) builds a + deferred `Where` query at `:55` and then re-enumerates it: `.Count()` at `:56`, then `.First()` + at `:62`. `Enumerable.Where` returns an iterator with no `ICollection` fast path, so `Count()` + walks the whole sequence. A single `Find` therefore invokes `KeyEquals` roughly `N + k + 1` times + for a list of `N` elements whose first match is at 0-based index `k`. Because branch 1 returns at + `KaStringAsync.cs:63` **before** the `Activated = false` reset at `:77`, the gated side effects + are self-limiting to one invocation per non-matching element per keystroke, while the ungated + `Update` at `:73` fires once per enumeration pass. The ungated call's invocation count is + therefore determined by a LINQ implementation detail inside a different class rather than by user + intent. + - **Ownership evidence for defect 3.** A repository-wide search over `*.cs` for `DelegateType` + returns exactly three hits, none of them a read: the comment at `IKbdAction.cs:16` and the two + declarations at `KaChar.cs:43` and `KaKey.cs:43`. A repository-wide search for `Update` shows it + declared on five types but read only at `KaStringAsync.cs:61,62,72,73` and written only at + `KaStringAsync.cs:25`. On `KaChar`, `KaCharAsync`, `KaKey`, and `KaKeyAsync` it is + write-never/read-never dead API. + - **Reachability evidence.** `KeyboardHandler.cs:180` appends to the filter before every probe, so + every probe at `:181`, `:188`, and `:194` has length `>= 1`. No production caller can pass an + empty string. `Activated` is re-armed for all elements only when the filter length is 1 + (`KeyboardHandler.cs:186-187`), after the `ContainsKey` pass at `:181` has already run. +- **Affected components/modules (paths, services, pipelines):** + - `QuickFiler/Controllers/KaStringAsync.cs` (95 lines) + - `QuickFiler/Controllers/KaChar.cs` (99 lines) + - `QuickFiler/Controllers/KaKey.cs` (99 lines) + - `QuickFiler/Interfaces/IKbdAction.cs` (18 lines) + - `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` (168 lines) + - Read-only context: `QuickFiler/Controllers/KbdActions.cs`, + `QuickFiler/Controllers/KeyboardHandler.cs`, + `QuickFiler/Controllers/QfcCollectionController.cs`. + +## Proposed Fix + +### Design summary (what changes where): + +Three decisions are fixed and are not open for re-litigation during implementation. + +**Decision 1 — the `Activated`-gating contract: gate all three branches.** +Add the `Activated &&` conjunct to the branch-3 guard at `KaStringAsync.cs:72`, so that all three +branches of `KeyEquals` gate uniformly. The contract, to be recorded verbatim in an XML doc comment +on `KeyEquals`, is: + +> `Activated` is a per-keystroke latch. Every observable side effect of `KeyEquals` — both `Update` +> and `ToggleControl` — fires only while `Activated` is true. A matching probe (branch 1) +> deliberately does NOT clear the latch; a non-matching probe clears it at `KaStringAsync.cs:77`. +> Consequently each element's side effects fire at most once per keystroke, regardless of how many +> times a LINQ predicate is re-enumerated. + +*Precision note (does not alter the decision).* The "at most once per keystroke" limit is exact for +the non-matching branches (2 and 3), whose element clears the latch at `:77` on its first pass. A +**matching** element takes branch 1 and returns at `:63` without clearing the latch, by design (see +the invariant below), so its idempotent `Update` may be re-executed on later enumeration passes +within the same keystroke. That repetition is intentional and load-bearing; it is what allows the +label to advance across the three passes of a single keystroke. + +*Rationale.* Gating branch 3 makes every non-matching element's side-effect count independent of LINQ +re-enumeration. Today the ungated `Update` fires once per enumeration pass — a count set by `Find`'s +internal `Count()`-then-`First()` sequence, not by user intent — and this is harmless only because +the callback happens to be an idempotent assignment, a property no signature enforces and no test +asserts. + +*Counter-argument, recorded fairly.* `ToggleControl` is non-idempotent — invoking it twice restores +the original state — and therefore genuinely must be latched, whereas `Update` is an idempotent +assignment. `Activated` may therefore have been intended to latch only the toggle, which would argue +for Option B: ungate `Update` in every branch and let only `ToggleControl` consult `Activated`. +Option B is **rejected** because it leaves the matching row's `Update` count re-enumeration-dependent +and because it inverts the currently-passing test +`KeyEquals_ContainsMatchWhileNotActivated_ReturnsTrueWithoutUpdate` +(`KaStringAsyncTests.cs:98-114`), turning a passing test into a contradiction. + +*Production impact: none.* Both callbacks are `null` on every `KaStringAsync` instance production +creates (`QfcCollectionController.cs:1376-1383` passes `null`/`null`; `KbdActions.cs:99` uses the +parameterless constructor), so `Update is not null` evaluates `false` on every production evaluation. +The change is observable only in tests and in any future code that supplies a non-null `Update`. + +**Decision 2 — the empty-argument contract: explicit fail-fast guard.** +Add a guard clause at the **top** of `KeyEquals`, before the `Key.Contains(other)` test: + +- `other == null` → `throw new ArgumentNullException(nameof(other))`. +- `other` empty (`string.Empty`) → `throw new ArgumentException(...)` with a documented message + explaining that an empty probe would otherwise match every registered action, and with the + parameter name supplied. + +*Rationale.* The reachable production misbehaviour today is not the exception but the silent +"empty matches everything" semantics, because production always has `Update == null`. Those semantics +make `FilterKeys("")` return every registered action and make `Find("")` throw +`InvalidOperationException` from `KbdActions.cs:67`. No production caller can pass an empty string +(`KeyboardHandler.cs:180` appends before every probe), so no caller breaks. The guard satisfies the +second limb of the issue's own acceptance criterion ("or rejects it with an explicit, documented +argument exception") and matches CLAUDE.md General Code Change Policy section 3 and C#4.1, +"fail fast and explicitly". + +*Fallback, recorded.* Option 1 — early-return `false` for an empty probe — is acceptable if a +reviewer prefers a total, non-throwing predicate. Both options are neutral against the existing +suite, since no committed test passes an empty string. The acceptance criteria below encode the +fail-fast option; adopting the fallback would require amending this spec first. + +**Decision 3 — `DelegateType` and the commented-out members: remove, do not restore.** + +- Delete `DelegateType` from `KaChar.cs:43-46` and `KaKey.cs:43-46`. Zero read sites exist + repository-wide. +- Delete the dead `Update` property (and its backing field) from `KaChar` (`KaChar.cs:50-55`), + `KaCharAsync` (`KaChar.cs:92-97`), `KaKey` (`KaKey.cs:50-55`), and `KaKeyAsync` + (`KaKey.cs:92-97`). +- **Retain `Update` on `KaStringAsync` (`KaStringAsync.cs:81-86`).** It is genuinely read at `:61`, + `:62`, `:72`, `:73` and written by the five-argument constructor at `:25`. +- Delete both commented-out lines at `IKbdAction.cs:15-16`. +- Remove the then-unused `using System.Windows.Forms;` at `KaChar.cs:6`. `Keys` appears in + `KaChar.cs` only inside `DelegateType` (`:45`). `KaKey.cs` keeps its `using System.Windows.Forms;` + because `Keys` is its key type throughout. + +*Rationale.* Restoring `DelegateType` to `IKbdAction` **will not compile**: `KaStringAsync` +(`KaStringAsync.cs:10`), `KaCharAsync` (`KaChar.cs:58`), and `KaKeyAsync` (`KaKey.cs:58`) do not +declare it. Restoring `Update` to the interface would force all five implementers to keep a member +that is dead on four of them. Removal resolves defect 3 without requiring a decision on the correct +`DelegateType` value. + +*Public-API notice (CLAUDE.md General Code Change Policy section 7.2).* Deleting `DelegateType` from +two public classes and `Update` from four public classes is a **breaking change to the public API +surface** of `QuickFiler.Controllers`. All in-repo consumers were enumerated by repository-wide +search over `*.cs`: `DelegateType` has zero read sites and `Update` has read sites only inside +`KaStringAsync` itself. The removal is called out explicitly here so that the change description and +the pull-request body carry the same notice. + +### Boundaries and invariants to preserve: + +- **HARD CONSTRAINT — do not make branch 1 fall through to the `Activated = false` reset at + `KaStringAsync.cs:77` for symmetry.** The early `return true` at `KaStringAsync.cs:63` is + load-bearing and must be preserved verbatim. `KeyboardHandler` re-arms `Activated` only at filter + length 1 (`KeyboardHandler.cs:186-187`) and then performs three passes within one keystroke: + `ContainsKey` (`:181`), `FilterKeys` (`:188`), and the indexer/`Find` (`:194`). If branch 1 cleared + the latch, the `ContainsKey` pass would consume the activation and the label advance would stop. + This constraint is encoded as an explicit anti-regression acceptance criterion. +- The `Contains`-based matching semantics of branch 1 are unchanged. `KbdActionsTests.cs:71-76` pins + substring matching and must continue to pass without modification. +- The offset expression at `KaStringAsync.cs:62`, `Key.Substring(other.Length - 1, 1)`, is unchanged. + The out-of-scope fourth defect concerns that expression and is deferred to a follow-up issue. +- Branch 2 (`other.Length == 1`) legitimately invokes no `Update`. That omission is correct under the + reconstructed design intent — a row that has never matched is already displaying `Key[0]` — and is + not a fourth inconsistency to "fix". +- `KaStringAsync.Activated` remains a public settable property. `KeyboardHandler.cs:187` is the only + external write site in the repository and is not modified. +- `IKbdAction`'s four live members at `IKbdAction.cs:11-14` are unchanged. No member is added + to the interface, so no implementer signature changes. +- Test-file and production-file sizes remain under the 500-line cap + (`.claude/rules/general-code-change.md`, "File Size Limit"). + +### Dependencies or blocked work: + +- No external dependency, package, or service. No new NuGet reference. +- `QuickFiler.Test/QuickFiler.Test.csproj` is owned by a sibling epic child and must not be edited. + Because every new test lands in an existing file, no `` entry is needed. +- `QuickFiler/Controllers/KbdActions.cs` is partly owned by other issues in a later epic and is not + modified here. +- The out-of-scope fourth defect must be filed as a new GitHub issue before this work is considered + complete (see **Rollout & Follow-up**). + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +| # | File | Change | Defect | +|---|---|---|---| +| 1 | `QuickFiler/Controllers/KaStringAsync.cs` (branch-3 guard, `:72`) | add the `Activated &&` conjunct | 1 | +| 2 | `QuickFiler/Controllers/KaStringAsync.cs` (top of `KeyEquals`, `:57`) | add the null/empty guard clause | 2 | +| 3 | `QuickFiler/Controllers/KaStringAsync.cs` (above `KeyEquals`) | add the XML doc comment recording both contracts | 1, 2 | +| 4 | `QuickFiler/Controllers/KaChar.cs:43-46` | delete `DelegateType`; delete `using System.Windows.Forms;` at `:6` | 3 | +| 5 | `QuickFiler/Controllers/KaChar.cs:50-55`, `:92-97` | delete the dead `Update` property and backing field from `KaChar` and `KaCharAsync` | related | +| 6 | `QuickFiler/Controllers/KaKey.cs:43-46` | delete `DelegateType` | 3 | +| 7 | `QuickFiler/Controllers/KaKey.cs:50-55`, `:92-97` | delete the dead `Update` property and backing field from `KaKey` and `KaKeyAsync` | related | +| 8 | `QuickFiler/Interfaces/IKbdAction.cs:15-16` | delete both commented-out members | related | +| 9 | `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` | rename one test (`:134`); add four new tests | 1, 2 | + +All line numbers are as read in this worktree on 2026-08-21 and will shift as edits are applied. + +#### Functions/classes/CLI commands impacted: + +- `KaStringAsync.KeyEquals(string)` — behavior and contract change (gating, argument validation, + documentation). +- `KaChar.DelegateType`, `KaKey.DelegateType` — deleted. +- `KaChar.Update`, `KaCharAsync.Update`, `KaKey.Update`, `KaKeyAsync.Update` — deleted. +- `KaStringAsync.Update` — retained unchanged. +- `IKbdAction` — two comment lines deleted; live surface unchanged. +- No CLI command exists for this component. + +#### Data flow and validation changes: + +- `KeyEquals` gains an input-validation stage that runs before any matching logic. The validated + precondition is: `other` is non-null and non-empty. +- No persisted data, serialization format, or wire format is involved. `KaStringAsync`, + `KaChar`, and `KaKey` are in-memory value objects. +- Downstream `KbdActions` methods (`ContainsKey`, `FilterKeys`, `Find`, + `FindIndex`, and the indexer) inherit the new precondition when `TKey` is `string`: an empty key + argument now surfaces an `ArgumentException` from the predicate rather than matching every element. + This is a deliberate consequence and is documented in the XML comment. + +#### Error handling and logging updates: + +- Two explicit exceptions are added at the `KeyEquals` boundary: `ArgumentNullException` for `null` + and `ArgumentException` for empty. Both are thrown from the guard clause, not from library + internals, so the exception origin names the offending parameter. +- No logging is added. `KaStringAsync` has no logger and introducing one would exceed the minimal + scope of a bugfix. `KbdActions` already logs its own duplicate-key and multiple-match conditions + via log4net (`KbdActions.cs:17-19`, `:85`, `:96`, `:117`) and is unchanged. +- No broad `catch` is introduced anywhere. + +#### Rollback/feature-flag considerations (if applicable): + +- No feature flag. The change is a small, self-contained source edit; rollback is a revert of the + pull request. +- Rollback risk is low because both callbacks are `null` in production today, so the gating change + and the guard clause are unobservable through the shipping code path. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +`bool KaStringAsync.KeyEquals(string other)` + +| Input | Precondition | Result | Side effects | +|---|---|---|---| +| `other == null` | violated | throws `ArgumentNullException(nameof(other))` | none | +| `other == string.Empty` | violated | throws `ArgumentException` naming `other` | none | +| `Key.Contains(other)` (branch 1) | satisfied | `true`, returned at `:63` without clearing `Activated` | `Update(Key.Substring(other.Length - 1, 1))` when `Activated && Update is not null` | +| non-match, `other.Length == 1` (branch 2) | satisfied | `false`; `Activated` cleared | `ToggleControl()` when `Activated && ToggleControl is not null` | +| non-match, `other.Length > 1` (branch 3) | satisfied | `false`; `Activated` cleared | `Update(Key.Substring(0, 1))` **and** `ToggleControl()`, each when `Activated` and the respective callback is non-null | + +`Key` is normalized to lower case by both the constructor (`:23`) and the setter (`:40`); that +behavior is unchanged. + +#### Required configuration keys and defaults: + +None. This component reads no configuration. `coverage.config` is not modified. + +#### Backward-compatibility expectations: + +- **Source-breaking for external consumers of the removed members.** `KaChar.DelegateType`, + `KaKey.DelegateType`, and the `Update` property on `KaChar`, `KaCharAsync`, `KaKey`, and + `KaKeyAsync` are deleted. No in-repo consumer reads any of them; a repository-wide search over + `*.cs` established this. There is no published external consumer of these types. +- **Behavior-compatible in production.** Because `Update` and `ToggleControl` are `null` on every + production instance, neither the gating change nor the empty-argument guard alters any observable + QuickFiler keyboard flow today. +- `IKbdAction`'s live contract is unchanged, so no implementer outside the four listed files is + affected. + +#### Performance constraints (latency/throughput/memory): + +No performance requirement applies and none is introduced. The gating change adds one boolean test on +a branch that already evaluates a null check; the guard clause adds one null test and one length test +per call. `KeyEquals` is invoked on the order of `N + k + 1` times per `Find` for small `N` (the +number of visible QuickFiler rows), executed on a keystroke. No allocation is added. + +## Assumptions, Constraints, Dependencies + +- **Assumptions (environment, data, access):** + - The full C# toolchain is available on the executing machine: `dotnet` with the manifest-pinned + CSharpier 1.2.6, `msbuild`, and `vstest.console.exe`. + - `dotnet tool restore` has been run once for this worktree before the first CSharpier invocation. + - `KaStringAsync.Key` is non-empty in production. `GenerateStringKbdAction` + (`QfcCollectionController.cs:1363-1385`) assigns from a digit width of 1 or 2, so `:1369` or + `:1373` always assigns and the `key = ""` initialization at `:1366` is never observed. + - Existing committed tests are treated as part of the spec (CLAUDE.md General Code Change Policy + section 7.3); the only permitted change to them is the single rename recorded below. +- **Constraints (budget, performance, compatibility):** + - No file may exceed 500 lines. Post-change estimates: `KaStringAsync.cs` about 110 lines, + `KaChar.cs` about 88, `KaKey.cs` about 90, `IKbdAction.cs` 16, `KaStringAsyncTests.cs` about 225. + All are far under the cap; three production files shrink. + - `QuickFiler.Test/QuickFiler.Test.csproj` must not be edited. + - No file under `.claude/**` or `docs/features/potential/**` may be written by this work. + - Evidence artifacts are written only under + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence//`, + with `yyyy-MM-ddTHH-mm` timestamps. +- **External dependencies (services, libraries, releases):** + - Test libraries already referenced by `QuickFiler.Test`: MSTest, Moq, FluentAssertions. No new + package is added. + - `System.Interactive` 7.0.1 supplies `EnumerableEx.ForEach` used at `KeyboardHandler.cs:187` + (`QuickFiler/packages.config:59`). It is read-only context; no version change. + +## Data / API / Config Impact + +- **User-facing or API changes:** No user-facing change. The public C# API of + `QuickFiler.Controllers` loses six members (`DelegateType` on two types, `Update` on four types) and + `KeyEquals` gains two documented argument exceptions. See the public-API notice above. +- **Data or migration considerations:** None. No persisted state, no schema, no migration. +- **Logging/telemetry updates (if any):** None. No logging statement is added, removed, or changed. +- **Compatibility notes (CLI flags, config schemas, versioning):** No CLI flag, no config schema, no + version bump. `coverage.config` is unchanged and continues to exclude none of the five in-scope + files; its only `` block (`:14-20`) lists seven third-party module patterns. + +## Test Strategy + +- **Framework:** MSTest (`[TestClass]` / `[TestMethod]` from + `Microsoft.VisualStudio.TestTools.UnitTesting`), Moq where a mock is warranted, and + FluentAssertions for assertions, per CLAUDE.md CUT1 and CUT2. The scaffold template that this + document replaces named "pytest"; that was a template artifact and is corrected here. There is no + Python toolchain in this repository. + +- **Regression tests to add or update:** + - **Rename (one existing test).** `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` + (`QuickFiler.Test/Controllers/KaStringAsyncTests.cs:134`) becomes + `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse`. The test + body is unchanged: it sets `ka.Activated = true` at `:141` and therefore exercises branch 3 with + the gate satisfied, passing unchanged after the fix. The rename removes the false implication + that it covers the ungated case. + - **New test (a) — defect 1 regression, ungated branch 3.** Arrange a `KaStringAsync` with + `Key = "abc"` and a non-null `Update` callback, leaving `Activated` at `false`. Act: + `KeyEquals("zz")`. Assert `Update` was **not** invoked and the result is `false`. This test is + red before the fix and green after. + - **New test (b) — latch survives the match-to-non-match transition.** A row that matches at depth + 1 and then fails at depth 2 still receives its `Key[0]` reset, because branch 1 returns without + clearing `Activated`. This pins the reasoning behind the anti-regression invariant and would fail + if branch 1's early return were removed. + - **New test (c) — defect 2, empty probe.** `KeyEquals("")` throws `ArgumentException`. Include the + `Activated = true` / non-null `Update` variant so that the previous + `ArgumentOutOfRangeException` path is explicitly closed. + - **New test (d) — defect 2, null probe.** `KeyEquals(null)` throws `ArgumentNullException`. + - All new tests live in `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` (168 lines today, + growing to roughly 225 — comfortably under the 500-line cap). Reuse the existing `NewKa` helper at + `:20-25`, which already supplies optional `update` and `toggle` callbacks. + +- **Unit tests (MSTest) for the fixed behavior and boundaries:** For `KeyEquals`, cover each of the + three branches at both `Activated` states and at both null and non-null `Update`, plus the empty + and null argument boundaries. That is the matrix the committed suite leaves half-covered. The + deletions in changes 4 through 8 need no new test; their safety is established by the zero-read-site + evidence and is proven by the analyzer and nullable builds compiling. + +- **Edge cases and negative scenarios (invalid inputs, missing data, boundary values):** + `other == null`; `other == string.Empty`; `other` of length 1 matching and non-matching; `other` + of length greater than 1 matching and non-matching; `Update` null and non-null; `ToggleControl` + null and non-null; `Activated` true and false. The existing test + `KeyEquals_NullDelegatesAreToleratedInNonMatchBranches` (`:154-166`) already covers the + both-callbacks-null case and passes unchanged. + +- **Error handling and logging verification:** Assert the exception **type** and that the thrown + `ArgumentException` names the `other` parameter, using FluentAssertions' `Should().Throw()`. + Assert against the parameter name rather than the full message text so that message wording can be + refined without breaking the test. No logging assertion is required because no logging is added. + +- **Bugfix ordering:** Per CLAUDE.md Bugfix Workflow section 1, author the regression tests first and + observe them fail before the production edit. Both defect-1 and defect-2 regression tests will be + genuinely red, so no fail-before exception dossier is required. Record the red run and the + subsequent green run under + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/` + using `yyyy-MM-ddTHH-mm` timestamps. + +- **Determinism:** The types under test are pure value objects. Tests use captured locals and simple + callbacks — no clock, no timer, no temporary file, no external dependency, no mutable global state. + Do not attempt to drive these branches through `KeyboardHandler`, which is + `[ExcludeFromCodeCoverage]` at `KeyboardHandler.cs:22` and depends on Outlook Interop and WinForms + event arguments. + +- **Coverage impact and targets for changed lines/modules:** `coverage.config` excludes none of the + five in-scope files. CLAUDE.md UT2 names `KbdActions<>` explicitly as a testable seam that is **not** + exempt from the coverage floor; `KaStringAsync`, `KaChar`, `KaKey`, and `IKbdAction` are pure value + objects with no COM dependency and fall outside every limb of the COM/VSTO/WinForms exemption. No + exemption is sought and none is needed: this change adds tests and deletes dead members, so coverage + of the touched files should rise. A threshold divergence exists between CLAUDE.md UT2 (80 percent + repository-wide, 90 percent for new code) and `.claude/rules/general-unit-test.md` / + `.claude/rules/quality-tiers.md` (85 percent line, 75 percent branch). The divergence is + pre-existing, is not adjudicated by this issue, and does not change the outcome here, because + coverage of the touched files rises under either figure. + +- **Toolchain commands to run (format → lint → type-check → test):** run in this exact order; if any + step fails or modifies a file, restart from step 1. + 1. `dotnet tool restore` + 2. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) + 3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + 4. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + 5. `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + + Constraints on those commands: + - Use `/t:Rebuild`, never `/t:Build`. MSBuild's up-to-date check does not invalidate on a + command-line `/p:` change, so a warm `/t:Build` returns exit 0 with `CoreCompile` skipped and runs + no analyzers. + - Never add `/p:Nullable=enable`. No project carries a `` element and there is no + `Directory.Build.props`, so the property conscripts files that never adopted the pragma. CI omits + it deliberately. + - `/InIsolation` is mandatory. Without it, roughly 1,695 phantom failures appear as a Moq + `TypeInitializationException`. + - Exclude paths containing `\.claude\` from recursive `*.Test.dll` discovery, so that stale builds + in agent worktrees are not collected. + - There is no Python toolchain. Any step naming `poetry run python -m scripts.dev_tools.*` is + unrunnable by absence and must be reported as such rather than executed or reported as passing. + +- **Manual validation steps (if required):** None required. All three defects are unobservable through + the shipping keyboard flow because both callbacks are `null` in production, so a manual Outlook + session would produce no signal either before or after the fix. A reviewer may confirm the + production no-op by reading `QfcCollectionController.cs:1376-1383` and `KbdActions.cs:99`. + +## Acceptance Criteria + +- [ ] **AC1 — Branch-3 gating applied.** In `QuickFiler/Controllers/KaStringAsync.cs`, the branch-3 + guard (at `:72` as read on 2026-08-21) reads `if (Activated && Update is not null)`, so all + three branches of `KeyEquals` gate their `Update` and `ToggleControl` side effects on + `Activated`. No other guard in the method is weakened. +- [ ] **AC2 — Contract documented in-code.** `KaStringAsync.KeyEquals` carries an XML documentation + comment that states the `Activated` latch contract: every observable side effect fires only + while `Activated` is true; a matching probe (branch 1) deliberately does not clear the latch; + a non-matching probe clears it at the trailing `Activated = false`. The comment also documents + the null and empty argument contract of AC4 and AC5. +- [ ] **AC3 — Anti-regression: the early return is preserved.** Branch 1 of `KeyEquals` still returns + `true` immediately (at `:63` as read on 2026-08-21) and does **not** fall through to the + trailing `Activated = false` reset. The existing test + `KeyEquals_ContainsMatchWhileActivated_InvokesUpdateAndReturnsTrue` + (`QuickFiler.Test/Controllers/KaStringAsyncTests.cs:76-96`), which asserts + `ka.Activated.Should().BeTrue()` after a matching probe, passes unmodified. +- [ ] **AC4 — Null argument rejected explicitly.** `KaStringAsync.KeyEquals(null)` throws + `ArgumentNullException` naming `other`, thrown from a guard clause placed above the + `Key.Contains(other)` test rather than from inside `string.Contains`. +- [ ] **AC5 — Empty argument rejected explicitly.** `KaStringAsync.KeyEquals("")` throws + `ArgumentException` naming `other`, with a message explaining that an empty probe would + otherwise match every registered action. +- [ ] **AC6 — The `ArgumentOutOfRangeException` path is closed.** `KeyEquals("")` throws + `ArgumentException` (AC5) for every combination of instance state, including `Activated = true` + with a non-null `Update`; `Key.Substring(other.Length - 1, 1)` is never evaluated with a + negative start index. +- [ ] **AC7 — `DelegateType` removed from both implementers.** The `DelegateType` property is deleted + from `QuickFiler/Controllers/KaChar.cs` (`:43-46`) and `QuickFiler/Controllers/KaKey.cs` + (`:43-46`). A repository-wide search over `*.cs` for `DelegateType` returns zero hits. No + `DelegateType` member is added to `QuickFiler/Interfaces/IKbdAction.cs`. +- [ ] **AC8 — Dead `Update` removed from four implementers.** The `Update` property and its backing + field are deleted from `KaChar` (`KaChar.cs:50-55`), `KaCharAsync` (`KaChar.cs:92-97`), `KaKey` + (`KaKey.cs:50-55`), and `KaKeyAsync` (`KaKey.cs:92-97`). +- [ ] **AC9 — `Update` retained on `KaStringAsync`.** The `Update` property remains on + `QuickFiler/Controllers/KaStringAsync.cs` (`:81-86`) and the five-argument constructor still + assigns it (`:25`), because it is read at `:61`, `:62`, `:72`, and `:73`. +- [ ] **AC10 — Unused `using` removed from `KaChar.cs` only.** `using System.Windows.Forms;` is + removed from `QuickFiler/Controllers/KaChar.cs:6`, and `QuickFiler/Controllers/KaKey.cs` + retains its `using System.Windows.Forms;` because `Keys` remains its key type. +- [ ] **AC11 — Commented-out interface members removed.** Both commented-out lines at + `QuickFiler/Interfaces/IKbdAction.cs:15-16` are deleted. The four live members at `:11-14` are + byte-identical to their pre-change text, and no implementer signature changes. +- [ ] **AC12 — Test renamed.** `KeyEquals_MultiCharNonMatch_InvokesUpdateWithFirstCharAndReturnsFalse` + (`QuickFiler.Test/Controllers/KaStringAsyncTests.cs:134`) is renamed to + `KeyEquals_MultiCharNonMatchWhileActivated_InvokesUpdateWithFirstCharAndReturnsFalse`, and its + body is otherwise unchanged. +- [ ] **AC13 — Defect-1 regression test added, red before and green after.** A new test in + `QuickFiler.Test/Controllers/KaStringAsyncTests.cs` arranges `Activated = false` with a non-null + `Update` and a multi-character non-matching probe, then asserts `Update` is not invoked and the + result is `false`. The test is observed **failing** against the unmodified production code and + **passing** after the fix; both runs are recorded under + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/`. +- [ ] **AC14 — Latch-survives-transition test added.** A new test asserts that a row which matches at + depth 1 and then fails at depth 2 still receives its `Key[0]` reset, pinning the behavior that + AC3 protects. +- [ ] **AC15 — Defect-2 regression tests added, red before and green after.** New tests assert + `ArgumentException` for `KeyEquals("")` (including the `Activated = true` / non-null `Update` + variant) and `ArgumentNullException` for `KeyEquals(null)`. Each is observed failing before the + guard clause is added and passing after; both runs are recorded under the same `evidence/qa-gates/` + directory. +- [ ] **AC16 — No pre-existing test is deleted or weakened.** The other seven tests in + `KaStringAsyncTests.cs` and every test in `KbdActionsTests.cs`, + `KbdActionsRemainingBranchesTests.cs`, `KaCharTests.cs`, and `KaKeyTests.cs` pass without + modification. The only permitted change to committed test code is the AC12 rename and the + addition of the AC13 through AC15 tests. +- [ ] **AC17 — No test-project file edit.** `git diff` reports no change to + `QuickFiler.Test/QuickFiler.Test.csproj`. All new tests land in existing files, so no + `` entry is required. +- [ ] **AC18 — Scope boundaries respected.** No file under `.claude/**` and no file under + `docs/features/potential/**` is modified. `QuickFiler/Controllers/KbdActions.cs`, + `QuickFiler/Controllers/KeyboardHandler.cs`, and + `QuickFiler/Controllers/QfcCollectionController.cs` are unmodified. +- [ ] **AC19 — Out-of-scope fourth defect not fixed, and filed.** `KaStringAsync.cs:62` still reads + `Update(Key.Substring(other.Length - 1, 1))`, branch 1 still tests `Key.Contains(other)`, and + the assertion at `KaStringAsyncTests.cs:89-91` (`.Be("b")` for `Key = "abc"`, `other = "ab"`) is + unchanged. A new GitHub issue is filed for the non-prefix `Substring` defect and its number is + recorded in **Rollout & Follow-up**. +- [ ] **AC20 — File-size limit respected.** No changed file exceeds 500 lines. + `QuickFiler/Controllers/KaChar.cs`, `QuickFiler/Controllers/KaKey.cs`, and + `QuickFiler/Interfaces/IKbdAction.cs` are each shorter after the change than before. +- [ ] **AC21 — Full C# toolchain green.** In one final uninterrupted pass, in this order: + `dotnet tool run csharpier check .` reports no file needing formatting; + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + succeeds; `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + succeeds (no `/p:Nullable=enable`, no `/t:Build`); and + `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + reports zero failures. Command transcripts are recorded under + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/`. + +## Risks & Mitigations + +- **Technical or operational risks:** + - *An implementer "completes" the symmetry by removing branch 1's early return.* This is the single + highest-impact risk in the change: it would silently stop the QuickFiler label from advancing, + because the `ContainsKey` pass at `KeyboardHandler.cs:181` would consume the activation before + `FilterKeys` at `:188` ran. Mitigated by the hard constraint in **Boundaries and invariants to + preserve**, by AC3 as an explicit anti-regression criterion, and by the AC14 test. + - *The empty-argument guard breaks an unenumerated caller.* Mitigated by a repository-wide search + that found no caller able to supply an empty string, and by the structural argument that + `KeyboardHandler.cs:180` appends before every probe. If a reviewer still objects, the recorded + fallback (early-return `false`) is available, but adopting it requires amending AC5, AC6, and + AC15 first. + - *The public-member deletions break an out-of-repo consumer.* Mitigated by the zero-read-site + evidence and by the explicit public-API notice; these types are internal to the QuickFiler + add-in and have no published external surface. + - *The gating change is claimed to have production effect and is over-tested or over-reviewed.* + Mitigated by recording, in the spec and in the pull-request body, the verified fact that both + callbacks are `null` on every production instance. + - *Line-number drift.* Every `file:line` citation in this spec is as read on 2026-08-21 and will + shift as edits land. Mitigated by phrasing acceptance criteria in terms of the observable code + text and behavior, with line numbers as locators only. +- **Mitigations and rollbacks:** + - Regression tests are authored before the production edit and observed red, so each criterion has + a failing-then-passing witness rather than an assertion of correctness. + - Rollback is a single revert of the pull request; there is no data migration, no feature flag, and + no persisted state to unwind. + - If any toolchain stage fails or rewrites a file, the loop restarts from formatting, per CLAUDE.md + General Code Change Policy section 8.1. + +## Rollout & Follow-up + +- **Release/rollout steps:** + 1. Author the AC13 through AC15 regression tests and observe them fail. + 2. Apply the nine changes listed under **Files/modules to change**. + 3. Run the full toolchain in order until one pass completes clean, recording transcripts under + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/evidence/qa-gates/`. + 4. Check off each acceptance criterion in this file as its evidence lands, per the + `acceptance-criteria-tracking` protocol. This `spec.md` is the sole acceptance-criteria source + for work mode `full-bug`; no `user-story.md` exists. + 5. Open the pull request with the public-API notice from **Design summary** reproduced in the body. +- **Post-fix monitoring or clean-up tasks:** + - **Recommended follow-up issue (required by AC19):** the non-prefix `Substring` defect at + `QuickFiler/Controllers/KaStringAsync.cs:62`. Branch 1 guards on `Key.Contains(other)` but + computes `Key.Substring(other.Length - 1, 1)`, which is only meaningful when `other` is a prefix + of `Key`. Resolving it requires deciding between `Contains` and `StartsWith` for branch 1, which + would change keyboard-filtering behavior currently pinned by `KbdActionsTests.cs:71-76`. Per + CLAUDE.md Bugfix Workflow section 2, this is filed as a new issue rather than folded into #445. + Record the issue number here once filed. + - **Awareness only, not owned by this issue:** `QuickFiler/Controllers/QfcCollectionController.cs` + is 2349 lines, a pre-existing violation of the 500-line rule; and + `QuickFiler/Controllers/KeyboardHandler.cs` is 414 lines with limited headroom. + - **Awareness only:** the coverage-threshold divergence between CLAUDE.md UT2 (80 / 90 percent) and + `.claude/rules/general-unit-test.md` and `.claude/rules/quality-tiers.md` (85 percent line, + 75 percent branch) is pre-existing and is not adjudicated here. + - No production monitoring applies; the fix is unobservable through the shipping code path. +- **Links: issue, PRs, related docs** + - Issue: https://github.com/drmoisan/TaskMaster/issues/445 + - Requirements source: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/issue.md` + - Research (authoritative over `issue.md` where they conflict): + `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md` + - Plan: `docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/plan.2026-08-21T18-09.md` + - Related: issue #430 (`quickfiler-keyboard-actions-coverage`, child F3 of epic #136), whose + no-behavior-change acceptance criterion is why these defects were deferred. + - Follow-up issue for the non-prefix `Substring` defect: to be filed (see AC19). From 1b2db4f5859e136dabbbe462c06f1ae6d85a5dc9 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 19:04:17 -0400 Subject: [PATCH 05/37] docs(prep): preserve incomplete preparation artifacts for #491 epic-planner preserved this work after the preparation orchestrator was terminated by an infrastructure error (API 529 Overloaded), not by a task failure. Preparation did NOT complete: no atomic-executor preflight clearance was obtained for this item. Present: issue.md, spec.md, research artifact, and an atomic plan that passes the MCP plan validator. Absent: PREFLIGHT: ALL CLEAR. Committed so a relaunched child resumes from this commit instead of losing an uncommitted worktree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../issue.md | 68 +++ .../plan.2026-08-21T18-11.md | 236 +++++++++ ...form1-removal-research.2026-08-21T18-15.md | 353 +++++++++++++ .../spec.md | 494 ++++++++++++++++++ 4 files changed, 1151 insertions(+) create mode 100644 docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/issue.md create mode 100644 docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/plan.2026-08-21T18-11.md create mode 100644 docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md create mode 100644 docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md diff --git a/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/issue.md b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/issue.md new file mode 100644 index 000000000..972e1cfba --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/issue.md @@ -0,0 +1,68 @@ +# quickfiler-test-form1-live-form (Issue #491) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-test-form1-live-form/ (Issue #491) +- Discovered during: preparation research for issue #456 (epic #136, child F14) + +- Issue: #491 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/491 +- Last Updated: 2026-08-08 +- Work Mode: full-bug + +## Summary + +A live `System.Windows.Forms.Form` is compiled into the `QuickFiler.Test` assembly, and a second, +unrelated item of dead production surface exists in `ItemViewer.Breadcrumb.cs`. Both are test-policy +and design-debt items rather than runtime defects, and both are outside epic #136 child F14's +production file set. + +## Item 1 — live `Form` compiled into the unit-test assembly + +`QuickFiler.Test/Form1.cs:5` and `QuickFiler.Test/Form1.Designer.cs:3` declare +`public partial class Form1 : System.Windows.Forms.Form`, whose `InitializeComponent` constructs three +`QuickFiler.ItemViewer` instances (`Form1.Designer.cs:32-34`). + +No test instantiates it — verified: the only `Form1` references in the test project are its own two +files — so no policy violation occurs today. But `.claude/rules/general-unit-test.md` and epic #136's +"never construct live forms" rule are one `new Form1()` away from being breached, and the type is dead +weight in the test assembly. + +Candidate disposition: delete both files, or move them to a manual harness project outside the unit +test assembly. + +## Item 2 — three `internal` members of `ItemViewer.Breadcrumb.cs` have no production caller + +`AttachBreadcrumbMessengerWhenReadyAsync` (`ItemViewer.Breadcrumb.cs:100-124`), +`AttachBreadcrumbMessenger` (`:126-140`), and `BreadcrumbOpenTask` (`:29-30`) are invoked only from +tests. A repository-wide search for each identifier returns the declaration plus call sites in +`QuickFiler.Test/Viewers/BreadcrumbCollapsedSurfaceReadinessTests.cs:438`, +`BreadcrumbSubfolderActivationTests.cs:340`, +`BreadcrumbSelectorOpenRetryTests.cs:38,41,61,69,265`, +`BreadcrumbCoordinatorLifecycleTests.cs:123`, and +`BreadcrumbDropDownIntegrationTests.cs:415-421`. No `QuickFiler/**` production file references them. + +This is roughly 40 lines of production surface maintained solely for tests. It is not a bug, and it +must not simply be deleted — doing so would break seven existing tests. The disposition is either to +promote these members to the production attach path (the `AttachCollapsedMessenger` route is +arguably what `CreateCollapsedBreadcrumbCandidate` should use) or to mark them explicitly as test +seams so their status is legible. + +## Acceptance Criteria (early draft) + +- [ ] No `Form`-derived type is compiled into `QuickFiler.Test`, or it is isolated in a non-unit-test + project. +- [ ] The three test-only `internal` members are either wired into the production path or explicitly + documented as test seams. +- [ ] Existing tests continue to pass. + +## Constraints & Risks + +- Item 2 touches `ItemViewer.Breadcrumb.cs`, assigned to epic child F14 (issue #456); reconcile + against F14's plan before scheduling. +- Deleting `Form1` changes the `QuickFiler.Test.csproj` compile set; preserve CRLF and keep the edit + to minimal adjacent hunks. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) diff --git a/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/plan.2026-08-21T18-11.md b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/plan.2026-08-21T18-11.md new file mode 100644 index 000000000..2c9afdc41 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/plan.2026-08-21T18-11.md @@ -0,0 +1,236 @@ +# quickfiler-test-form1-live-form (Atomic Plan) + +- **Issue:** #491 +- **Parent:** epic `quickfiler-suite-determinism-foundation` +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T19-05 +- **Status:** Ready for preflight (revision 2) +- **Version:** 1.2 +- **Work Mode:** `full-bug` (from `issue.md`). Acceptance-criteria source is `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md` only. `user-story.md` is correctly absent and its absence is not a blocker. + +## Objective + +Delete the dead `QuickFiler.Test.Form1` type (three files) and its `QuickFiler.Test.csproj` entries, and add one MSTest structural guard that makes the "no live form in a unit-test assembly" policy permanently enforced. + +## Conventions used by every task in this plan + +- **Working directory.** Every command is run from the repository worktree root (the directory containing `TaskMaster.sln`). All paths in this plan are repository-relative. +- **Shell.** Run C# toolchain commands from a `pwsh -NoProfile` session. When nesting through `pwsh -NoProfile -Command`, wrap the entire payload in single quotes so `$` tokens are expanded by the child shell, not by the parent; double any single quote that must appear inside the payload. MSBuild switches such as `/m` are mangled into `M:/` (MSB1008) by shells that rewrite POSIX-looking arguments, so MSBuild and vstest must be invoked from `pwsh`, through the absolute executable paths resolved in Phase 0. +- **No shell state persists between tasks.** Each tool invocation starts a fresh shell, so `$msbuild`, `$vstest`, and `$assemblies` are never carried over from the task that produced them. Every MSBuild or vstest task must either re-resolve `$msbuild` and `$vstest` with the P0-T7 vswhere commands inside the same `pwsh` session that runs the build or test, or substitute the literal absolute paths recorded in the P0-T7 artifact. Likewise `$assemblies` must be re-populated by the P0-T17 enumeration command inside the same session that invokes vstest; a bare `@assemblies` in a fresh session expands to nothing, and the run then receives no assemblies at all while still reporting a zero failure count. +- **Toolchain bootstrap order.** `global.json` pins `sdk.paths` to `.dotnet-sdk`, and `QuickFiler.Test.csproj` fails the build through `EnsureNuGetPackageBuildImports` when `packages/` is absent. P0-T11 and P0-T12 therefore precede every `dotnet` and every MSBuild invocation in this plan, and no task may be reordered ahead of them. +- **Evidence root.** `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/`. The only valid `kind` sub-folders are `baseline`, `regression-testing`, `qa-gates`, `issue-updates`, `other`. Every evidence path in this plan resolves under that root. No evidence may be written anywhere outside it; in particular, no baseline, QA-gate, coverage, or regression artifact may be written under any top-level tooling output directory, the sole permitted non-evidence orchestration location being the orchestration checkpoint folder. No delegation prompt supplied a non-canonical evidence path, so no `EVIDENCE_LOCATION_OVERRIDE_REJECTED` record is required. +- **Timestamps.** Every evidence filename below contains the literal token `TIMESTAMP`. Replace it with the actual ISO-8601 capture time in the form `yyyy-MM-ddTHH-mm` at the moment the artifact is written. +- **Evidence schema.** Every command-step artifact contains, at minimum, the lines `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. A step whose gate is expected to be red additionally carries `ExpectedExitCode:`. +- **No helper scripts under `evidence/`.** Do not create any `.ps1`, `.sh`, or `.py` file anywhere under the evidence tree. Feature-review language matching is extension-only and path-blind; one retained script there forces a coverage failure. +- **No Python toolchain.** This repository has no `scripts/dev_tools/` and no Poetry manifest. Any skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence. Record it as unrunnable; do not fabricate a result and do not silently skip it. No Python coverage argument appears anywhere in this plan because no Python coverage tool exists in this repository; C# coverage is measured exclusively through `scripts\vscode\Invoke-MSTestWithCoverage.ps1`. +- **`.claude/**` is read-only.** No child of this epic may edit anything under `.claude/`. Where this plan cites a rule file, the citation is the policy the work is measured against, not an edit target. +- **Hooks are inert.** Every `PreToolUse` hook in this repository currently reads a property that is always null and returns `permissionDecision: allow`. Verify every gate from durable `git` state and from recorded exit codes, never from a hook result. +- **Re-derive every line number.** Line numbers cited in this plan, in `spec.md`, in the research document, and in `epic.md` have drifted across this corpus. Phase 0 and Phase 2 each re-derive them from the working tree, and the executor must use the re-derived values, not the cited ones. + +## Literals this plan instructs the executor to create or assert + +These tokens are quoted here, outside any command span, so that assertions naming them are recognized as instructions rather than as searches for text that cannot exist: + +- `NoLiveFormInTestAssemblyTests.cs` — the new guard test file. +- `NoLiveFormInTestAssemblyTests` — the new `[TestClass]`. +- `ExecutingAssembly_ContainsNoFormDerivedType` — the new `[TestMethod]`. +- `NoLiveFormInTestAssemblyTests.ExecutingAssembly_ContainsNoFormDerivedType` — the fully-qualified name the green guard run must report. +- `Skipping target "CoreCompile"` — the MSBuild up-to-date message whose occurrence count must be zero in every rebuild log produced by this plan. +- `CoreCompile:` — the MSBuild target banner whose occurrence count must be at least one in every rebuild log produced by this plan, proving real compilation occurred. +- `QuickFiler.Test.Form1` — the type the Phase 1 guard must name in its red failure message, and the token that must be absent from `QuickFiler.Test.csproj` after Phase 2. + +## The confined csproj edit + +`QuickFiler.Test/QuickFiler.Test.csproj` is edited by three sibling epic children concurrently (#511/#571, #445, #449). This child owns exactly two regions and touches nothing else in the file. Any edit outside these two regions risks a fan-in conflict. + +**Owned region A** — the two `Form1` compile blocks, currently at lines 161 through 166 inclusive: + +```xml + + Form + + + Form1.cs + +``` + +The net effect of this plan on region A is that all six lines are replaced by the single line: + +```xml + +``` + +Placing the new compile entry here, rather than appending it elsewhere in the file, is what keeps the whole edit inside the owned partition. The plan reaches that net state in two steps: Phase 1 inserts the new entry immediately after the closing tag of the `Form1.Designer.cs` compile block (so the guard can compile while `Form1` still exists), and Phase 2 then deletes the six `Form1` lines, leaving the new entry as the sole survivor at that position. + +Three of the file's five occurrences of the literal `Form1` live inside region A: the `Form1.cs` compile tag, the `Form1.Designer.cs` compile tag, and that block's `DependentUpon` child. The other two live in region B. + +**Owned region B** — the entire `` whose sole child is the `Form1.resx` embedded resource, currently at lines 179 through 183 inclusive: + +```xml + + + Form1.cs + + +``` + +All five lines are deleted; two of them carry the literal `Form1`. MSBuild tolerates an empty ``, but leaving one behind is dead structure with no purpose. + +**Constraints on the csproj edit, all mandatory:** + +- The file uses **CRLF** line endings. Preserve CRLF in the edited file. Confine edits to minimal adjacent hunks inside the two owned regions. +- The three entries `` (currently line 365), `` (currently line 366), and `` (currently line 420) **must be retained unmodified**. 46 other files in `QuickFiler.Test` use `System.Windows.Forms`, so these references are load-bearing far beyond `Form1`. +- The `Controllers` compile item group closes at line 178 and is sibling child #449's append point. Do not touch it. +- `QuickFiler.Test/QuickFiler.Test.csproj.bak` is **not** edited and **not** deleted. It is referenced by no solution entry and compiled by no toolchain command. +- `.csharpierignore` excludes `*.csproj`, so this edit is invisible to the CSharpier check. No `.csharpierignore` change is needed or implied. + +## The guard test + +Create `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs` with the following content. The test reflects over type metadata only. It must not construct any form, control, or `BackgroundWorker`, and it must scope to the executing assembly, never to a referenced one: the referenced `QuickFiler` assembly legitimately contains the unrelated production type `QuickFiler.Viewers.Form1`, and the guard must not flag it. + +```csharp +using System; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace QuickFiler.Test +{ + /// + /// Structural guard: no live WinForms window type may be compiled into this unit-test + /// assembly. Reflection is over type metadata only; nothing is instantiated. + /// + [TestClass] + public class NoLiveFormInTestAssemblyTests + { + [TestMethod] + public void ExecutingAssembly_ContainsNoFormDerivedType() + { + // Arrange - metadata only; scoped to the executing assembly, never a referenced one. + Type formType = typeof(System.Windows.Forms.Form); + Assembly executing = Assembly.GetExecutingAssembly(); + + // Act + string[] formDerivedTypeNames = GetLoadableTypes(executing) + .Where(candidate => formType.IsAssignableFrom(candidate)) + .Select(candidate => candidate.FullName) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + + // Assert + formDerivedTypeNames + .Should() + .BeEmpty( + "a unit-test assembly must not compile a live System.Windows.Forms.Form type" + ); + } + + // Reflection over a large test assembly can hit a single type whose dependencies fail to + // resolve, and GetTypes then throws for the whole assembly. That would leave this guard + // permanently red for a reason unrelated to what it measures, so the loaded subset carried + // on the exception is used instead; its null entries are the types that did not load. + private static Type[] GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(candidate => candidate != null).ToArray(); + } + } + } +} +``` + +CSharpier output wins over this listing. If `dotnet tool run csharpier format .` rewrites the file, keep the formatter's output. + +--- + +### Phase 0 — Context, Policy Reads, and Baseline Capture + +Policy reading order for this phase, applied exactly: `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/plan-acceptance-gates.md`, then the feature documents. All five rule files are read-only. + +- [ ] [P0-T1] Read `CLAUDE.md` in full and record its four embedded policy section titles (General Code Change Policy, C# Code Change Policy, General Unit Test Policy, C# Unit Test Policy) in the Phase 0 read record drafted in P0-T4. Acceptance: the four section titles are recorded verbatim. +- [ ] [P0-T2] Read, in this order and without editing any of them, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/quality-tiers.md`, and `.claude/rules/plan-acceptance-gates.md`. Acceptance: all four paths are listed in the Phase 0 read record with a one-line statement of the constraint each imposes on this change. +- [ ] [P0-T3] Read `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md`, `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md`, `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/issue.md`, and `docs/features/epics/quickfiler-suite-determinism-foundation/epic.md`. Acceptance: the count of acceptance-criteria checkboxes found under the `## Acceptance Criteria` heading of `spec.md` is recorded and equals 11. +- [ ] [P0-T4] Write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-instructions-read.TIMESTAMP.md` containing the lines `Timestamp:`, `Policy Order:`, and an explicit bulleted list of every file read in P0-T1 through P0-T3. Acceptance: the file exists and all three required field labels are present. +- [ ] [P0-T5] Run `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, and `git status --porcelain`, and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-branch-and-base.TIMESTAMP.md` recording the current branch, the current HEAD sha, the declared branch name `bug/quickfiler-test-form1-live-form-491`, the declared base commit `025b350e27c3095ca9253a0543dac8197bb7c49c`, and the porcelain output. Acceptance: the artifact records the current branch name and the current HEAD sha as observed values, and states whether the observed branch equals the declared branch. The observed HEAD sha is recorded, not asserted equal to any pinned value. The porcelain output is expected to be non-empty at branch head because tracked files under `.claude/agent-memory/` are already dirty; the artifact records that fact so later scope checks compare against a known starting state. +- [ ] [P0-T6] Run `git ls-files scripts/dev_tools` and `git ls-files pyproject.toml` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-python-toolchain-absent.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: both commands produce empty output, and the artifact states that any skill step naming a `poetry run python -m scripts.dev_tools` invocation is unrunnable by absence in this repository. +- [ ] [P0-T7] Resolve the absolute paths of `MSBuild.exe` and `vstest.console.exe` with `& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe` and `& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -products * -requires Microsoft.VisualStudio.PackageGroup.TestTools.Core -find Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe`, and write both resolved paths to `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-tool-resolution.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: both resolved paths are non-empty and both files exist on disk. Every later MSBuild and vstest task uses these absolute paths, re-resolved or substituted literally in its own session. +- [ ] [P0-T8] Re-derive the two owned csproj regions from the working tree by running `pwsh -NoProfile -Command '$lines = Get-Content -LiteralPath "QuickFiler.Test/QuickFiler.Test.csproj"; $hits = 0..($lines.Count - 1) | Where-Object { $lines[$_] -like "*Form1*" }; $window = $hits | ForEach-Object { ($_ - 3)..($_ + 3) } | Sort-Object -Unique | Where-Object { $_ -ge 0 -and $_ -lt $lines.Count }; $window | ForEach-Object { "{0}: {1}" -f ($_ + 1), $lines[$_].Trim() }'` and write the result to `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-csproj-line-derivation.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the artifact records the observed first and last line numbers of the `Form1` compile block and of the `Form1.resx` item group, and states in one sentence that the executor uses these observed numbers and does not trust any number cited in this plan, in `spec.md`, in the research document, or in `epic.md`. A plain match-only search reports only the lines that carry the literal and omits the closing tag of region A and both `` tags of region B, so the windowed form above is mandatory: it emits contiguous numbered context around every hit, from which the full extent of both owned regions is directly readable. +- [ ] [P0-T9] Confirm the retained references by running `pwsh -NoProfile -Command 'Select-String -LiteralPath "QuickFiler.Test/QuickFiler.Test.csproj" -SimpleMatch "System.Drawing", "System.Drawing.Design", "System.Windows.Forms" | ForEach-Object { "{0}: {1}" -f $_.LineNumber, $_.Line.Trim() }'` and append the observed line numbers of the three reference entries to the P0-T8 artifact. Acceptance: exactly three reference entries are located and their observed line numbers are recorded. +- [ ] [P0-T10] Create the scratch log directories and confirm they are git-ignored by running `pwsh -NoProfile -Command 'New-Item -ItemType Directory -Force -Path "coverage\msbuild", "coverage\logs" | Out-Null'` followed by `git check-ignore -q coverage/msbuild` and `git check-ignore -q coverage/logs`, and record both exit codes in `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-scratch-log-location.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: both directories exist and both `git check-ignore` commands report `EXIT_CODE: 0`, confirming raw MSBuild and vstest logs written under `coverage/msbuild/` and `coverage/logs/` are excluded from version control and cannot leave `git status --porcelain` dirty. `Tee-Object` does not create a missing directory, so this task must precede every task that tees to `coverage/logs/`. Derived counts are copied into the committed evidence artifacts; raw logs are not committed. +- [ ] [P0-T11] Install the repo-local .NET SDK by running `pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-dotnet-sdk-bootstrap.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `EXIT_CODE: 0`, the directory `.dotnet-sdk` exists, and `pwsh -NoProfile -Command 'dotnet --version'` prints a version string. `global.json` pins `sdk.paths` to `.dotnet-sdk`, so every `dotnet` invocation in this plan fails with the repository's custom `errorMessage` until this task completes. +- [ ] [P0-T12] Restore NuGet packages for the whole solution by running `nuget restore TaskMaster.sln` (repo fallback if `nuget.exe` is not on PATH: `pwsh -NoProfile -File scripts\vscode\Invoke-Restore.ps1`) and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-nuget-restore.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `EXIT_CODE: 0` and the directory `packages/` exists. This mirrors `.github/workflows/_build-analyzers.yml:43-45`; without it `QuickFiler.Test.csproj:452-458` fails the build through `EnsureNuGetPackageBuildImports`. +- [ ] [P0-T13] Run `dotnet tool restore` once for this worktree and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-dotnet-tool-restore.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `EXIT_CODE: 0`. This must precede the first CSharpier invocation so the manifest-pinned CSharpier 1.2.6 is used rather than a global install, and it must follow P0-T11 because the pinned SDK is what resolves the tool manifest. +- [ ] [P0-T14] Run `dotnet tool run csharpier check .` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-csharpier-check.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the number of files reported as needing formatting. Acceptance: the artifact records an integer exit code and an integer count of unformatted files, establishing the pre-change formatter state. +- [ ] [P0-T15] Run `& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true /fileLogger "/fileLoggerParameters:LogFile=coverage\msbuild\phase0-analyzers.log;Verbosity=normal"` using the MSBuild path resolved in P0-T7, then write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-msbuild-analyzers.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count, the warning count, the count of log lines matching `CoreCompile:`, and the count of log lines matching `Skipping target "CoreCompile"`. Acceptance: `EXIT_CODE: 0`, the `CoreCompile:` count is at least 1, and the `Skipping target "CoreCompile"` count is exactly 0. The target is `/t:Rebuild`; a warm `/t:Build` returns exit 0 having skipped `CoreCompile` on every project, so the analyzer gate could not fail. +- [ ] [P0-T16] Run `& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true /fileLogger "/fileLoggerParameters:LogFile=coverage\msbuild\phase0-nullable.log;Verbosity=normal"` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-msbuild-nullable.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count, the `CoreCompile:` count, and the `Skipping target "CoreCompile"` count. Acceptance: `EXIT_CODE: 0`, `CoreCompile:` count at least 1, `Skipping target "CoreCompile"` count exactly 0, and the artifact's `Command:` line contains no `Nullable=enable` property. Adding that property is prohibited: no project here carries a `` element, CI omits it deliberately, and forcing it produced 195 unrelated errors in `UtilitiesCS.csproj`. +- [ ] [P0-T17] Enumerate the test assemblies that later vstest tasks will consume by running `pwsh -NoProfile -Command '$all = @(Get-ChildItem -Path . -Recurse -Filter *.Test.dll -File | ForEach-Object { Resolve-Path -Relative $_.FullName } | Where-Object { $_ -like "*\bin\Debug\*" -and $_ -notlike "*\obj\*" -and $_ -notlike "*\ref\*" }); $claude = @($all | Where-Object { $_ -like "*\.claude\*" }); $kept = @($all | Where-Object { $_ -notlike "*\.claude\*" }); "PREFILTER={0} CLAUDE={1} KEPT={2}" -f $all.Count, $claude.Count, $kept.Count; $kept'` and write the emitted counts and the kept list to `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-test-assembly-discovery.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the three counts are recorded as integers, the kept count is at least 1, and every kept path contains the segment `bin\Debug`. The `.claude` count is recorded from the pre-filter list as an observation about where this worktree sits and is deliberately not asserted to be zero: a post-filter zero is guaranteed by the filter itself and could never fail. This task runs after P0-T15 and P0-T16 because no `bin\Debug` output exists until the first rebuild completes, and an enumeration run earlier returns an empty list that would silently give the baseline vstest run no assemblies. The filter operates on relative paths so that an enclosing `.claude` worktree segment cannot suppress every result. +- [ ] [P0-T18] Run the full suite with the P0-T17 assembly list, re-populated in the same session, as `& $vstest @assemblies /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`, teeing output to `coverage/logs/phase0-vstest.log`, and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-vstest-baseline.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the total, passed, failed, and skipped test counts as integers and the number of assemblies actually passed on the command line. Acceptance: the assembly count is at least 1, all four test counts are integers, and the failed count is recorded. `/InIsolation` is mandatory: without it each assembly's `app.config` binding redirects are ignored and roughly 1,695 phantom failures appear with empty messages and sub-millisecond durations, via a Moq `TypeInitializationException` from `System.Threading.Tasks.Extensions`. If such a mass failure appears, the flag is missing; it is not a regression caused by any change and must not be "fixed". +- [ ] [P0-T19] Capture the baseline coverage figure with `pwsh -NoProfile -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput docs\features\active\2026-08-07-quickfiler-test-form1-live-form-491\evidence\baseline\coverage-baseline.cobertura.xml`, and additionally write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-coverage-capture.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the Cobertura file exists at that path, its root element carries non-empty `lines-covered`, `lines-valid`, and `line-rate` attributes, the capture artifact records `EXIT_CODE: 0`, and Koverage post-processing is proven to have run by `pwsh -NoProfile -Command 'Select-Xml -Path "docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/coverage-baseline.cobertura.xml" -XPath "//class" | Where-Object { $_.Node.GetAttribute("filename") -like "*:*" } | Measure-Object | ForEach-Object { $_.Count }'` returning 0, because post-processing rewrites every `class/@filename` to a workspace-relative path. A non-zero exit code means `Assert-CoberturaLineCoverageThreshold` (`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-490`) threw at `Invoke-MSTestWithCoverage.ps1:341` after `dotnet-coverage` had already written the RAW, unfiltered Cobertura to the same path; that file must not be used as either side of the Phase 4 comparison. This harness Koverage-post-processes the raw Cobertura output and strips third-party `` elements, so it emits a filtered first-party figure; both sides of the Phase 4 comparison must come from this same script. +- [ ] [P0-T20] Extract the baseline numbers with `pwsh -NoProfile -Command 'Select-Xml -Path "docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/coverage-baseline.cobertura.xml" -XPath "/coverage" | ForEach-Object { $_.Node.GetAttribute("lines-covered"); $_.Node.GetAttribute("lines-valid"); $_.Node.GetAttribute("line-rate") }'` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/baseline/phase0-coverage-baseline.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `Output Summary:` records the numeric headline line-coverage percentage computed as the `line-rate` attribute multiplied by 100 and rendered to four decimal places, together with the integer `lines-covered` and `lines-valid` values. The recorded values must be actual numbers; `UNVERIFIED`, `N/A`, or any other placeholder makes this task incomplete. + +### Phase 1 — Regression Guard (must fail first) + +The guard must be demonstrably red while `Form1` still exists. No file is deleted in this phase. The new compile entry is inserted inside owned region A so the guard can compile; the six `Form1` lines remain in place until Phase 2. + +- [ ] [P1-T1] Create `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs` with exactly the content given under "The guard test" above, in namespace `QuickFiler.Test`, containing one `[TestClass]` named `NoLiveFormInTestAssemblyTests` and one `[TestMethod]` named `ExecutingAssembly_ContainsNoFormDerivedType`. Acceptance: the file exists, contains exactly one `[TestClass]` attribute and exactly one `[TestMethod]` attribute, contains no `new ` expression constructing a form, control, or `BackgroundWorker`, calls `Assembly.GetExecutingAssembly` exactly once, and contains a `catch` clause for `ReflectionTypeLoadException` so that one unloadable unrelated type cannot leave the guard permanently red. +- [ ] [P1-T2] Re-read the current line numbers of the `Form1.Designer.cs` compile block's closing tag in `QuickFiler.Test/QuickFiler.Test.csproj`, then insert the single line ` ` immediately after that closing tag, preserving CRLF line endings and leading four-space indentation. Acceptance: the new entry sits between the `TestSupport\WinFormsPumpHostTests.cs` compile entry and the `Helper Classes\ConversationResolverTests.cs` compile entry, and `git diff --numstat -- QuickFiler.Test/QuickFiler.Test.csproj` reports exactly 1 added line and 0 deleted lines. +- [ ] [P1-T3] Run `dotnet tool run csharpier format .` and then `dotnet tool run csharpier check .`, writing `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/regression-testing/phase1-csharpier.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the check command reports `EXIT_CODE: 0`. If the format command rewrote `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs`, keep the formatter's output. +- [ ] [P1-T4] Rebuild with `& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true /fileLogger "/fileLoggerParameters:LogFile=coverage\msbuild\phase1-build.log;Verbosity=normal"` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/regression-testing/phase1-build.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count, the `CoreCompile:` count, and the `Skipping target "CoreCompile"` count. Acceptance: `EXIT_CODE: 0`, `CoreCompile:` count at least 1, `Skipping target "CoreCompile"` count exactly 0, and `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` exists with a write time later than the start of this task. +- [ ] [P1-T5] [expect-fail] Run the guard alone with `& $vstest .\QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook&FullyQualifiedName~NoLiveFormInTestAssemblyTests"`, teeing output to `coverage/logs/phase1-guard-red.log`, and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/regression-testing/phase1-guard-red.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode: 1`, and `Output Summary:` recording the total, passed, and failed counts. Acceptance: total is 1, passed is 0, failed is 1, and `EXIT_CODE:` equals `ExpectedExitCode:`. A green result here means the guard is not scoped to the executing assembly or the compile entry did not take effect; either case is a defect in the guard, not a licence to proceed. +- [ ] [P1-T6] Confirm the guard failed for the correct reason by recording, in the P1-T5 artifact, the verbatim assertion-failure message read from `coverage/logs/phase1-guard-red.log`. Acceptance: the recorded failure message names the type `QuickFiler.Test.Form1`. A failure naming any other type, or a failure caused by a load or reflection exception rather than by the FluentAssertions `BeEmpty` assertion, fails this task. + +### Phase 2 — Removal + +- [ ] [P2-T1] Re-derive the current line numbers of the six `Form1` compile lines and of the five-line `Form1.resx` item group in `QuickFiler.Test/QuickFiler.Test.csproj` by rerunning the P0-T8 command, and append the re-derived numbers to `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase2-csproj-edit.TIMESTAMP.md`. Acceptance: the re-derived numbers are recorded and differ from the P0-T8 numbers by exactly the one line inserted in P1-T2, or the discrepancy is explained in the artifact. The executor edits by the re-derived numbers, not by any number cited in this plan. +- [ ] [P2-T2] Delete the three tracked files with `git rm QuickFiler.Test/Form1.cs QuickFiler.Test/Form1.Designer.cs QuickFiler.Test/Form1.resx`. Acceptance: `git status --porcelain -- QuickFiler.Test` shows all three paths staged as deletions, and none of the three paths exists on disk. +- [ ] [P2-T3] Delete the six `Form1` compile lines from `QuickFiler.Test/QuickFiler.Test.csproj` at the re-derived positions from P2-T1, leaving the `NoLiveFormInTestAssemblyTests.cs` compile entry in place. Acceptance: `pwsh -NoProfile -Command 'Select-String -LiteralPath "QuickFiler.Test/QuickFiler.Test.csproj" -SimpleMatch "Form1" | Measure-Object | ForEach-Object { $_.Count }'` returns 2. The same command returns 5 before this task runs; region A carries the literal on three of its six lines (the `Form1.cs` compile tag, the `Form1.Designer.cs` compile tag, and its `DependentUpon` child), and the two survivors are the `Form1.resx` embedded-resource tag and its `DependentUpon` child, both removed by P2-T4. +- [ ] [P2-T4] Delete the entire five-line `Form1.resx` item group, including its opening and closing `` tags, at the re-derived positions from P2-T1. Acceptance: `pwsh -NoProfile -Command 'Select-String -LiteralPath "QuickFiler.Test/QuickFiler.Test.csproj" -SimpleMatch "Form1" | Measure-Object | ForEach-Object { $_.Count }'` returns 0, and no empty `` element remains at that position. +- [ ] [P2-T5] Verify the file still uses CRLF line endings by running `pwsh -NoProfile -Command '$b = [System.IO.File]::ReadAllBytes("QuickFiler.Test/QuickFiler.Test.csproj"); $lf = 0; $crlf = 0; for ($i = 0; $i -lt $b.Length; $i++) { if ($b[$i] -eq 10) { $lf++; if ($i -gt 0 -and $b[$i - 1] -eq 13) { $crlf++ } } } "LF=$lf CRLF=$crlf"'` and record the result in the P2-T1 artifact. Acceptance: the CRLF count equals the LF count, proving no line was converted to a bare LF. +- [ ] [P2-T6] Verify the three retained reference entries are present and unmodified by rerunning the P0-T9 command and comparing against the P0-T9 record. Acceptance: exactly three reference entries are located, and their text is byte-identical to the P0-T9 record. +- [ ] [P2-T7] Verify the csproj edit is confined to the two owned regions by running `git diff --numstat -- QuickFiler.Test/QuickFiler.Test.csproj` and `git diff -U0 -- QuickFiler.Test/QuickFiler.Test.csproj`, recording both outputs in the P2-T1 artifact. Acceptance: the diff contains exactly two hunks, the added-line count is exactly 1, the deleted-line count is exactly 11, and no hunk touches any line belonging to the `Controllers` compile item group or to any `Reference` item group. + +### Phase 3 — Verification Loop + +Run steps P3-T1 through P3-T7 in order as one uninterrupted toolchain pass. If any step fails, or if any step modifies a file, restart from P3-T1. Do not leave this loop while any step is failing. + +- [ ] [P3-T1] Run `dotnet tool run csharpier format .` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-csharpier-format.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the number of files reformatted. Acceptance: `EXIT_CODE: 0` and the count of reformatted files is recorded as an integer. +- [ ] [P3-T2] Run `dotnet tool run csharpier check .` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-csharpier-check.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `EXIT_CODE: 0` and the reported count of files needing formatting is 0. +- [ ] [P3-T3] Audit file size after formatting by running `pwsh -NoProfile -Command 'Get-Content -LiteralPath "QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs" | Measure-Object -Line | ForEach-Object { $_.Lines }'` and record the result in `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-file-size-audit.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the recorded line count is 500 or fewer. This audit runs after the formatting pass because CSharpier can change line counts. +- [ ] [P3-T4] Run `& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true /fileLogger "/fileLoggerParameters:LogFile=coverage\msbuild\phase3-analyzers.log;Verbosity=normal"` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-msbuild-analyzers.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count, the `CoreCompile:` count, and the `Skipping target "CoreCompile"` count. Acceptance: `EXIT_CODE: 0`, error count 0, `CoreCompile:` count at least 1, and `Skipping target "CoreCompile"` count exactly 0. +- [ ] [P3-T5] Run `& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true /fileLogger "/fileLoggerParameters:LogFile=coverage\msbuild\phase3-nullable.log;Verbosity=normal"` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-msbuild-nullable.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the error count, the `CoreCompile:` count, and the `Skipping target "CoreCompile"` count. Acceptance: `EXIT_CODE: 0`, error count 0, `CoreCompile:` count at least 1, `Skipping target "CoreCompile"` count exactly 0, and the `Command:` line contains no `Nullable=enable` property. +- [ ] [P3-T6] Re-enumerate test assemblies with the P0-T17 command in the same session, then run `& $vstest @assemblies /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`, teeing output to `coverage/logs/phase3-vstest.log`, and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-vstest.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the total, passed, failed, and skipped counts and the number of assemblies actually passed on the command line. Acceptance: `EXIT_CODE: 0`, the assembly count is at least 1, and the failed count is exactly 0. +- [ ] [P3-T7] Prove the guard is green by name by running `& $vstest .\QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook&FullyQualifiedName~NoLiveFormInTestAssemblyTests"`, teeing output to `coverage/logs/phase3-guard-green.log`, and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-guard-green.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording the total, passed, and failed counts and the fully-qualified name of the test that ran. Acceptance: `EXIT_CODE: 0`, total is 1, passed is 1, failed is 0, and the recorded fully-qualified name ends in `NoLiveFormInTestAssemblyTests.ExecutingAssembly_ContainsNoFormDerivedType`. This is the green counterpart to the red run recorded in P1-T5 and is the evidence acceptance criterion 1 is checked off against. +- [ ] [P3-T8] Record the clean consecutive pass by writing `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase3-clean-pass.TIMESTAMP.md` listing, from one uninterrupted iteration, the exit codes of P3-T1, P3-T2, P3-T4, P3-T5, P3-T6, and P3-T7, plus the output of `git status --porcelain -- QuickFiler.Test docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491` taken immediately after P3-T2. Acceptance: all six recorded exit codes are 0, and the scoped porcelain output contains no path that was modified by P3-T1 after P3-T2 ran. The pathspec narrowing is mandatory: tracked files under `.claude/agent-memory/` are already dirty at branch head, so an unscoped porcelain listing reports entries this plan neither owns nor may commit and the clean-pass gate could never be satisfied. If the loop was restarted, only the exit codes from the final uninterrupted iteration are recorded, and the number of restarts is stated. + +### Phase 4 — Coverage Comparison and Acceptance-Criteria Check-off + +- [ ] [P4-T1] Capture the post-change coverage figure through the same harness with `pwsh -NoProfile -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput docs\features\active\2026-08-07-quickfiler-test-form1-live-form-491\evidence\qa-gates\coverage-postchange.cobertura.xml`, and additionally write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase4-coverage-capture.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: the Cobertura file exists at that path, its root element carries non-empty `lines-covered`, `lines-valid`, and `line-rate` attributes, the capture artifact records `EXIT_CODE: 0`, and Koverage post-processing is proven to have run by `pwsh -NoProfile -Command 'Select-Xml -Path "docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/coverage-postchange.cobertura.xml" -XPath "//class" | Where-Object { $_.Node.GetAttribute("filename") -like "*:*" } | Measure-Object | ForEach-Object { $_.Count }'` returning 0, because post-processing rewrites every `class/@filename` to a workspace-relative path. A non-zero exit code means `Assert-CoberturaLineCoverageThreshold` (`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-490`) threw at `Invoke-MSTestWithCoverage.ps1:341` after `dotnet-coverage` had already written the RAW, unfiltered Cobertura to the same path; that file must not be used as either side of the Phase 4 comparison. Do not emit any solution-level aggregate coverage report outside the feature evidence tree; coverage evidence for this change lives only under the feature evidence tree. +- [ ] [P4-T2] Extract the post-change numbers with `pwsh -NoProfile -Command 'Select-Xml -Path "docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/coverage-postchange.cobertura.xml" -XPath "/coverage" | ForEach-Object { $_.Node.GetAttribute("lines-covered"); $_.Node.GetAttribute("lines-valid"); $_.Node.GetAttribute("line-rate") }'` and write `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase4-coverage-postchange.TIMESTAMP.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Acceptance: `Output Summary:` records the numeric post-change line-coverage percentage to four decimal places plus the integer `lines-covered` and `lines-valid` values, all as actual numbers. +- [ ] [P4-T3] Write the comparison artifact `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase4-coverage-comparison.TIMESTAMP.md` recording, side by side, the baseline percentage from P0-T20 and the post-change percentage from P4-T2, the two `lines-covered` values, the two `lines-valid` values, and the arithmetic difference of each pair. Acceptance: the post-change percentage is greater than or equal to the baseline percentage; both figures were produced by `Invoke-MSTestWithCoverage.ps1` and are therefore both Koverage-filtered first-party figures; and the artifact states explicitly that no raw `dotnet-coverage collect` figure was substituted on either side. `Invoke-MSTestWithCoverage.ps1:99` adds `.*\.Test\.dll$` to the dotnet-coverage instrumentation excludes, and `Invoke-MSTestWithCoverage.Helpers.ps1:39-41` skips every `.Test`-suffixed assembly when building the Koverage allowlist, whose packages are then removed at `:417-421` before the root totals are recomputed at `:442-445`. `QuickFiler.Test.Form1`'s 187 coverable lines are consequently outside this harness's denominator both before and after the change. The expected `lines-valid` difference is exactly 0 and the expected `lines-covered` difference is exactly 0; any non-zero difference in either value identifies an unrelated change and must be explained in the artifact. The 187-line argument describes a raw, unfiltered `dotnet-coverage` denominator and does not apply to the harness this plan mandates. +- [ ] [P4-T4] Write the test-count parity artifact `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/qa-gates/phase4-test-count-parity.TIMESTAMP.md` recording the baseline total, passed, failed, and skipped counts from P0-T18 alongside the post-change counts from P3-T6. Acceptance: the post-change failed count is 0 and the post-change total equals the baseline total plus exactly 1, that one being the new guard test. If either recorded total is a non-numeric placeholder reported by an aborted host, or the two totals differ by any value other than 1, re-run the P3-T6 command exactly once and record the re-run's total, passed, failed, and skipped counts in the same artifact as a separate labelled block; the acceptance comparison is then made against the re-run counts, and the artifact states that a re-run occurred and why. At most one re-run is permitted. A second discrepancy identifies a regressed or dropped test and must be resolved before this task is checked off. The bounded allowance exists because the multi-assembly run is documented as load-flaky and an aborted host reports a non-numeric total, which is an environment failure rather than a test-count regression. +- [ ] [P4-T5] Write the Item 2 deferral record `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/issue-updates/issue-491.TIMESTAMP.md` containing `Timestamp:`, the exact text stating that Item 2 of the potential document — the three `internal` members `AttachBreadcrumbMessengerWhenReadyAsync`, `AttachBreadcrumbMessenger`, and `BreadcrumbOpenTask` in `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` — is deferred to the later ItemViewer-owning epic and is not dropped from tracking, and a `PostedAs:` line. Acceptance: the artifact exists with all three elements. If `gh` is available, post the text as a comment on issue #491, set `PostedAs: comment`, and record the comment URL; if `gh` is unavailable, write a `POSTING BLOCKED` header with the reason and set `PostedAs: unknown`. Do not write anything under `docs/features/potential/`; that location is barred to every child of this epic. +- [ ] [P4-T6] Check off acceptance criterion 1 in `spec.md` (no `System.Windows.Forms.Form`-derived type is compiled into the `QuickFiler.Test` assembly, proven by a named MSTest guard test) by changing its `- [ ]` to `- [x]`, citing the P3-T7 artifact as evidence. Acceptance: exactly one checkbox changes state in this task, its criterion text is unmodified, and the cited artifact records the named guard test with passed count 1 and failed count 0. +- [ ] [P4-T7] Check off acceptance criterion 2 in `spec.md` (the three `Form1` files are deleted from the working tree), citing the P2-T2 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and `git ls-files QuickFiler.Test/Form1.cs QuickFiler.Test/Form1.Designer.cs QuickFiler.Test/Form1.resx` produces empty output. +- [ ] [P4-T8] Check off acceptance criterion 3 in `spec.md` (the compile and embedded-resource entries are removed, confined to the two owned regions, with the new guard entry inside the same owned region), citing the P2-T7 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited diff shows exactly two hunks with 1 added and 11 deleted lines. +- [ ] [P4-T9] Check off acceptance criterion 4 in `spec.md` (the three `System.Drawing`, `System.Drawing.Design`, and `System.Windows.Forms` reference entries remain present and unmodified), citing the P2-T6 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact shows byte-identical reference text against the P0-T9 record. +- [ ] [P4-T10] Check off acceptance criterion 5 in `spec.md` (CSharpier format and check both complete with no diffs), citing the P3-T1 and P3-T2 artifacts. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and both cited artifacts record `EXIT_CODE: 0`. +- [ ] [P4-T11] Check off acceptance criterion 6 in `spec.md` (the analyzer MSBuild command completes with zero analyzer errors), citing the P3-T4 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact records exit code 0, error count 0, and a `Skipping target "CoreCompile"` count of 0. +- [ ] [P4-T12] Check off acceptance criterion 7 in `spec.md` (the warnings-as-errors MSBuild command completes with zero errors and no command ever passes the nullable-enable property), citing the P3-T5 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, the cited artifact records exit code 0 and error count 0, and no `Command:` line in any artifact produced by this plan contains a `Nullable=enable` property. +- [ ] [P4-T13] Check off acceptance criterion 8 in `spec.md` (the vstest run with coverage, isolation, and the `LiveOutlook` category filter completes with zero failing tests), citing the P3-T6 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact records a failed count of 0. +- [ ] [P4-T14] Check off acceptance criterion 9 in `spec.md` (no pre-existing `QuickFiler.Test` test regresses; test-count and pass-count parity apart from the one new guard test), citing the P4-T4 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact shows a post-change total equal to the baseline total plus 1. +- [ ] [P4-T15] Check off acceptance criterion 10 in `spec.md` (post-change line coverage is greater than or equal to the baseline, both recorded as actual numbers), citing the P4-T3 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact records two numeric percentages with the post-change value greater than or equal to the baseline value. +- [ ] [P4-T16] Check off acceptance criterion 11 in `spec.md` (Item 2 is explicitly recorded as deferred to a later epic's ItemViewer-owning child), citing the P4-T5 artifact. Acceptance: exactly one checkbox changes state, its criterion text is unmodified, and the cited artifact names all three deferred members. + +### Phase 5 — Documentation and Status + +- [ ] [P5-T1] Write the acceptance-criteria status summary to `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/other/ac-status-summary.TIMESTAMP.md` in the form required by the `acceptance-criteria-tracking` skill, with the fields Source, Total AC items, Checked off, Remaining, and Items remaining. Acceptance: Source names `spec.md` only, Total AC items is 11, and Checked off plus Remaining equals 11. +- [ ] [P5-T2] Update this plan file in place, changing `- [ ]` to `- [x]` for every task whose acceptance condition was met and whose evidence artifact exists on disk with all required fields populated. Acceptance: no task is marked complete whose evidence artifact is absent or whose artifact is missing any of `Timestamp:`, `Command:`, `EXIT_CODE:`, or `Output Summary:`. +- [ ] [P5-T3] Update the `- **Status:**` and `- **Last Updated:**` header fields of `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md` to reflect delivery, changing no criterion text and adding no new criterion. Acceptance: exactly two header lines change, and the count of checkbox items under the `## Acceptance Criteria` heading is still 11. +- [ ] [P5-T4] Stage and commit only the owned paths with `git add -A -- QuickFiler.Test docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491` followed by `git commit -m "fix(quickfiler-test): remove dead Form1 from the test assembly and guard against live forms (#491)"`. Before staging, confirm no derived coverage settings file survives under the evidence tree by running `pwsh -NoProfile -Command '(Get-ChildItem -Path "docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence" -Recurse -Filter *.effective-coverage.config -File | Measure-Object).Count'` and recording the result in the artifact; that count must be 0, because `Invoke-MSTestWithCoverage.ps1:171-190` writes a derived settings file adjacent to the coverage output — inside `evidence/baseline/` and `evidence/qa-gates/` — and removes it only in a `finally` block, so a hard kill leaves it behind for staging to pick up. Do not use a bare `git add -A`: tracked files under `.claude/agent-memory/` are already dirty at branch head, and committing them here would violate the epic's Hard Constraint 1 and the scope lock verified in P5-T5. Acceptance: the derived-settings count is 0; `git status --porcelain -- QuickFiler.Test docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491` produces empty output; any residual porcelain entry outside those two pathspecs is recorded verbatim in the artifact and must lie under `.claude/agent-memory/`; and `git rev-parse HEAD` records a new commit sha in `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/evidence/other/phase5-commit.TIMESTAMP.md` alongside `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [ ] [P5-T5] Verify scope lock by running `git diff --name-only 025b350e27c3095ca9253a0543dac8197bb7c49c..HEAD` and recording the full changed-path list in the P5-T4 artifact. Acceptance: every path in the list is one of `QuickFiler.Test/Form1.cs`, `QuickFiler.Test/Form1.Designer.cs`, `QuickFiler.Test/Form1.resx`, `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs`, `QuickFiler.Test/QuickFiler.Test.csproj`, or a path under `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/`. No path under `.claude/`, under `docs/features/potential/`, or under any other project directory may appear. `QuickFiler.Test/QuickFiler.Test.csproj.bak` must not appear. diff --git a/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md new file mode 100644 index 000000000..59c36fc66 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md @@ -0,0 +1,353 @@ +--- +issue: 491 +epic: quickfiler-suite-determinism-foundation +created_at: 2026-08-21T18-15 +status: research complete, no source changed +--- + +# Issue #491 — `QuickFiler.Test.Form1` live-form removal — research + +All line numbers below were re-derived directly from the worktree at +`C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a32345a9498cf124e` on 2026-08-21. No +source file was edited to produce this document. + +## Verdict + +`QuickFiler.Test.Form1` is **DEAD**: it has zero references anywhere in the tracked tree outside +its own three files and the four `QuickFiler.Test.csproj` entries that compile it. The correct +disposition is **removal** — delete `Form1.cs`, `Form1.Designer.cs`, `Form1.resx`, and their +`QuickFiler.Test.csproj` entries. The `.resx` carries no data entries and no +`ComponentResourceManager` consumer exists, so it is safe to remove alongside the two `.cs` files. +Removing the three files raises the measured line-coverage rate (removes 187 always-uncovered +lines from the denominator; the numerator is unaffected because those 187 lines currently +contribute 0 covered lines). Item 2 of the potential document (the three test-only `internal` +members of `ItemViewer.Breadcrumb.cs`) is **out of scope for this child**: it names a different +production file, the epic manifest scopes #491 to the live form only, and the epic's Hard +Constraint 5 area plus its "Recorded Preconditions" section explicitly prohibits any child of this +epic from writing under `docs/features/potential/**`, which further promotion of Item 2 would +require. + +## A. Reachability + +A repository-wide search for the identifier `Form1` (word-boundary pattern `\bForm1\b`) returns 82 +files. Of those, only three are the type's own declaration files +(`QuickFiler.Test/Form1.cs`, `QuickFiler.Test/Form1.Designer.cs`) and its resource +(`QuickFiler.Test/Form1.resx`, matched via the csproj entry, not textually). The remaining hits +fall into four disjoint, non-overlapping categories, none of which reference +`QuickFiler.Test.Form1`: + +1. Documentation and evidence prose (issue/potential/epic/research markdown files, and historic + Cobertura XML evidence files under `docs/features/**/evidence/**`, which record the class by + name as coverage data, not as a code reference). +2. `QuickFiler.Test/QuickFiler.Test.csproj` and `QuickFiler.Test/QuickFiler.Test.csproj.bak` (the + four/five build-file entries; `.bak` is addressed under G). +3. **A different, unrelated `Form1` type**: `QuickFiler/Viewers/Form1.cs` and + `QuickFiler/Viewers/Form1.Designer.cs`, in namespace `QuickFiler.Viewers` (production code, not + test code). Its `Form1.cs` (read in full) is a four-line constructor-only partial class with no + further members. It is a distinct type from `QuickFiler.Test.Form1` and out of scope for #491; + it is noted here only because the broad `\bForm1\b` search surfaces it and a plan author must + not confuse the two. +4. Two other test-project live forms of the same defect shape in unrelated projects + (`UtilitiesCS.Test/Form1.cs`, `SVGControl.Test/Form1.cs`). These are separate defects in + separate assemblies, not in scope for #491, and not addressed further here. + +No test, no `[TestClass]`, no reflection-based discovery, and no resource lookup references +`QuickFiler.Test.Form1`. A targeted search for reflection patterns +(`Assembly.GetTypes`, `Activator.CreateInstance`, `GetType("`) inside `QuickFiler.Test/` returns +four call sites, none naming `Form1`: +`QuickFiler.Test/Viewers/BreadcrumbMessengerHubTests.cs:341`, +`QuickFiler.Test/Viewers/BreadcrumbSubfolderActivationTests.cs:402`, and two occurrences in +`QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs:128,177` — all of these +construct unrelated types (`Activator.CreateInstance(field.FieldType)` for a theme field, and a +message type in the breadcrumb hub tests), and none of the four is textually or semantically +connected to `Form1`. + +The delegation prompt's fact 2 is confirmed independently: a direct search of +`QuickFiler.Test/Viewers/BreadcrumbCollapsedSurfaceReadinessTests.cs` for the literal `Form1` +returns zero hits (the file is absent from the 82-file `\bForm1\b` result set). The file is not a +`Form1` dependent. It is a caller of the potential document's Item 2 production surface: line 438 +reads `=> Viewer.AttachBreadcrumbMessengerWhenReadyAsync(messenger, readiness);`, invoking the +`ItemViewer.Breadcrumb.cs` member named in Item 2. This confirms the delegation prompt's framing: +the file belongs to Item 2's caller set, not to Item 1 (`Form1`) at all. + +**Verdict: DEAD.** + +## B. Disposition + +Given the DEAD verdict, deletion (option i) is correct, not a headless-construction retrofit +(option ii). A retrofit is a proportionate response only when a type has a legitimate reason to +exist under test — for example a production headless-viewer construction path (see the repository +memory precedent for `ItemViewer` headless construction). `Form1` has no such reason: it is not a +production type, it is not invoked by any test, and its own body +(`QuickFiler.Test/Form1.cs:22-34`, `LoadControlGroup`) exists solely to demonstrate manually adding +`ItemViewer` controls to a `TableLayoutPanel` at design time — a manual/visual harness, not a unit +test. Keeping it under a headless construction seam would still leave three files and roughly 190 +lines of pure demonstration code inside the unit-test assembly, contradicting the epic's own +determinism leading indicator ("No unit-test run creates a visible window on the desktop") and the +repository's file-cohesion guidance in `.claude/rules/general-code-change.md`. Deletion is the +proportionate and reversible response (the file's git history remains available if a manual harness +is ever wanted, and the epic's Non-Goals section does not request one). + +## C. The .resx coupling + +Confirmed: `QuickFiler.Test/Form1.Designer.cs` (read in full, 227 lines) contains no +`ComponentResourceManager` and no `resources.ApplyResources` call anywhere in `InitializeComponent` +(lines 29-212) or elsewhere in the file. All control properties are set with literal values +(`System.Drawing.Point`, `System.Drawing.Size`, `System.Windows.Forms.Padding`, etc.), not via +resource lookup. + +`QuickFiler.Test/Form1.resx` was read in full (120 lines). It contains only the standard ResX +schema boilerplate (``, the two `` elements for `resmimetype` and +`version`, and the `reader`/`writer` type-name `` elements) and **zero `` +elements**. The file carries no actual resource entries — it is an empty ResX shell, present only +because the WinForms designer always emits a sibling `.resx` for a `Form`-derived partial class, +regardless of whether any resource is used. + +Consequently `Form1.resx` is orphaned in the sense that it has always been vestigial: no code reads +from it, and no code ever will, because there is nothing in it to read. Removing it breaks no +`ResourceManager` lookup and creates no satellite-assembly gap. A repository-wide search inside +`QuickFiler.Test/` for `ResourceManager`, `GetString(`, and `GetObject(` calls against a +`QuickFiler.Test`-scoped resource found none; the only `ResourceManager`-adjacent hit in the test +project is `QuickFiler.Test/ResourceTests.cs`, which belongs to a different project entirely +(`UtilitiesCS.Test`, per the earlier `\bForm1\b` file list) and is unrelated to `Form1.resx`. + +## D. ItemGroup emptiness + +Re-derived directly from `QuickFiler.Test/QuickFiler.Test.csproj` (CRLF file): + +``` +179 +180 +181 Form1.cs +182 +183 +``` + +Lines 180-182 (the `EmbeddedResource` element) are the sole child of the `` opened at +179 and closed at 183. MSBuild tolerates an empty `` — it is legal, inert XML with no +build effect — so there is no correctness requirement to remove the wrapper tags. + +**Recommendation: delete the whole block, lines 179-183, not just 180-182.** Reasons: + +1. Leaving an empty `` behind is dead structure with no purpose; the general repository + guidance to keep files intentional and free of unused scaffolding applies to project files as + much as to source files. +2. The deletion is scoped entirely inside the Form1 region the epic manifest assigns exclusively to + #491 (see F below and the epic's Shared-Surface Coordination section). Deleting 179-183 does not + touch any sibling child's entry: sibling #449 appends to the `Controllers` item group, which the + re-derived csproj shows ends at line 178 (`` closing the item group that opens at + line 57 and lists ``/``/etc. + entries). #449's append point (after line 178, inside or after that group) is unaffected by + removing the wholly separate 179-183 `ItemGroup`. +3. Leaving 179 and 183 (empty tags) while removing only 180-182 gains nothing: the wrapper carries + no attribute and no conditional logic, so there is no reason to preserve it "in case a future + entry is added" — a future entry would simply re-open a new `` inline with the rest of + the file's existing multi-`ItemGroup` structure (the file already has ten-plus separate + `` blocks for compiles, resources, references, etc.), matching existing style. + +## E. Coverage denominator + +`coverage.config` (read in full, 25 lines) excludes only third-party module paths by regex: +`Deedle`, `FSharp`, `Castle\.Core`, `FluentAssertions`, `Moq`, `Microsoft\.Testing`, `MSTest`. It +contains no entry for `QuickFiler.Test` or any first-party assembly. `QuickFiler.Test.dll` is not +excluded from coverage instrumentation. + +**Coverable-line counts for `QuickFiler.Test.Form1`, extracted from +`docs/features/active/2026-07-21-quickfiler-folder-selector-dropdown-400/evidence/baseline/diagnostic-quickfiler.2026-07-21T15-53.cobertura.xml`:** + +The Cobertura file records `QuickFiler.Test.Form1` as two `` elements (one per source file, +because the partial class spans two files): one at line 16154 +(`filename=...\Form1.Designer.cs`) spanning to the closing `` at line 16495, and one at +line 16496 (`filename=...\Form1.cs`) spanning to line 16585. Counting the distinct `` entries in each class's summary `` block (lines 16332-16493 for the +Designer.cs class, lines 16554-16583 for the Form1.cs class — the class-level summary block, not +the duplicate per-method `` sub-blocks, to avoid double counting): + +| File | Coverable lines | Covered lines | `hits="1"` count | +| --- | --- | --- | --- | +| `Form1.Designer.cs` | 157 | 0 | 0 | +| `Form1.cs` | 30 | 0 | 0 | +| **Total** | **187** | **0** | **0** | + +Every single `` entry under both classes carries `hits="0"`; the class-level `line-rate` +attributes confirm this independently (`line-rate="0"` on both classes, lines 16154 and 16496). + +**Arithmetic — effect of removal on the measured rate.** This particular Cobertura file's root +element (line 2) records `lines-covered="21027"` and `lines-valid="84749"` (`line-rate= +0.24810912223153075`), and its `` element (grepped) shows twelve `` entries +covering the whole solution plus several vendored/third-party assemblies (`log4net`, +`Mono.Reflection`, `System.Interactive`, `System.Linq.Async`, `Microsoft.IO.RecyclableMemoryStream`) +that the shipped harness's post-processing step (below) strips before the officially-reported +Koverage figure is produced. This file is therefore a **raw, pre-post-processing diagnostic +capture**, not the harness's final filtered artifact; per the repository memory on raw-vs- +postprocessed Cobertura root attributes, its root totals must not be compared numerically against a +Koverage-postprocessed root. The arithmetic below uses this single file's own root totals +consistently on both sides of the comparison (same file, same methodology), which is valid for +illustrating the *direction and approximate scale* of the effect; it is not a claim about the +harness's officially reported percentage. + +- Before: `lines-covered = 21027`, `lines-valid = 84749` → rate `= 21027 / 84749 ≈ 0.248109` +- After removing Form1's 187 always-uncovered lines: `lines-covered' = 21027` (unchanged — Form1 + contributed 0 covered lines), `lines-valid' = 84749 − 187 = 84562` → rate + `= 21027 / 84562 ≈ 0.248633` + +**Removing Form1 raises the measured line-coverage rate.** The numerator is unaffected because +Form1's 187 lines were never covered; the denominator shrinks, so the ratio strictly increases. +This holds for any denominator scope (whole-repository, `QuickFiler.Test`-package-only, or a +first-party-only filtered scope) as long as the scope includes Form1's 187 lines before removal and +excludes them after — the direction of the effect does not depend on which of those scopes is used, +only the magnitude does. + +**Exact commands for a numeric baseline and post-change comparison.** Read directly from +`scripts/vscode/Invoke-MSTestWithCoverage.ps1` (348 lines) and its sibling helper files: + +``` +pwsh -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 ` + -SearchRoot . ` + -Configuration Debug ` + -CoverageOutput '\evidence\baseline\coverage-baseline.cobertura.xml' +``` + +and, identically shaped, for the post-change capture: + +``` +pwsh -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 ` + -SearchRoot . ` + -Configuration Debug ` + -CoverageOutput '\evidence\qa-gates\coverage-postchange.cobertura.xml' +``` + +Real parameter names, confirmed from the script's `param()` block (lines 1-13) and the +`Invoke-MSTestWithCoverageMain` function signature (lines 248-259): `-SearchRoot`, `-Configuration` +(defaults to `Debug` when omitted or blank), `-CoverageOutput` (defaults to +`coverage\coverage.cobertura.xml`, repo-root-relative), and `-NoExecute` (a discovery-only switch +that returns before running collection — useful for a dry-run assembly-discovery check but produces +no XML). The script (lines 296-306) discovers test assemblies by recursively globbing +`*.Test.dll` under `\bin\$Configuration\`, excluding `\obj\` and `\ref\` paths; it does not by +itself exclude `.claude\worktrees\`, so per the epic's Execution Note 3 a plan invoking it directly +against the whole repository should scope `-SearchRoot` to avoid picking up stale worktree builds. +It resolves `vstest.console.exe` via `vswhere.exe`, requires the `dotnet-coverage` global tool, and +runs the collection through `Invoke-DotnetCoverageCollection` (lines 172-246 of the same file), +which composes the outer `dotnet-coverage collect --settings coverage.config` invocation together +with the inner `vstest.console.exe ... /InIsolation /TestCaseFilter:TestCategory!=LiveOutlook` +call, using the `TaskMaster.cli.runsettings` file resolved by `Resolve-RunSettingsPath` (lines +15-39). + +**Two-denominator hazard.** After collection, the script (lines 333-341) explicitly +**post-processes** the raw Cobertura XML for "Koverage compatibility": it rewrites absolute paths +to workspace-relative paths, injects a `` element, and — critically — +**removes `` elements for third-party assemblies not part of the solution** (dotnet-coverage +instruments every loaded DLL at runtime, including vendored/third-party code, which the raw capture +above shows: `log4net`, `Mono.Reflection`, `System.Interactive`, `System.Linq.Async`, and +`Microsoft.IO.RecyclableMemoryStream` all appear as `` elements in the raw diagnostic file +used above). **The harness therefore emits the filtered, first-party-only figure as its final +output**, not the raw multi-package figure this research used for illustrative arithmetic. A plan +that captures a baseline and a post-change figure with this exact script will get two +directly-comparable filtered figures; it must not substitute a raw `dotnet-coverage collect` output +in place of one side of that comparison, and must not compare a filtered figure against an +unfiltered one. + +`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1` and +`scripts/vscode/Invoke-MSTestWithCoverage.ClosureFilter.ps1` exist alongside the main script (both +referenced by dot-sourcing at line 261 of the main script, `. (Join-Path $ScriptRoot +'Invoke-MSTestWithCoverage.Helpers.ps1')`); the helper file supplies `ConvertTo-KoverageCoberturaXml` +and `Assert-CoberturaLineCoverageThreshold`, called at lines 340-341 of the main script. Neither +file's internal implementation was needed to answer this question beyond confirming the +post-processing step exists and does what the main script's comments (lines 333-337) describe; a +plan author only needs the main script's command-line contract above. + +## F. Item 2 scope boundary + +`AttachBreadcrumbMessengerWhenReadyAsync`, `AttachBreadcrumbMessenger`, and `BreadcrumbOpenTask` +were re-verified present in `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs`: +`BreadcrumbOpenTask` at line 29, `AttachBreadcrumbMessengerWhenReadyAsync` at line 100, and +`AttachBreadcrumbMessenger` at line 126 — all still `internal`. A repository-wide search for the +two method names (excluding the `Task` property, which is not directly greppable by a +distinct verb) inside `QuickFiler/` (production code only) returns exactly one file: +`QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` itself (the declaration site). No other production +file under `QuickFiler/` calls any of the three members — they remain production-callerless, +confirming the potential document's Item 2 claim still holds. + +**Recommendation: Item 2 does not belong in this child's (#491) scope.** Three independent reasons, +all confirmed directly rather than inferred: + +1. **File-set boundary.** The epic manifest (`docs/features/epics/quickfiler-suite-determinism-foundation/epic.md`, + Scope section, line 68-69) scopes #491 explicitly to "live form in the test project... `Form1.cs` + and its designer." Item 2 touches `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs`, a wholly + different production file with no textual or structural overlap with `Form1`. +2. **Epic-level non-goal.** The epic's Non-Goals section states the `IItemViewer` UI-thread seam + consolidation (#489), which rewrites `IItemViewer`, `ItemViewer.cs`, and + `ItemViewer.WebViewThread.cs`, belongs to a later epic's ItemViewer child, not this one. Item 2's + members live in a sibling partial-class file of the same `ItemViewer` type family + (`ItemViewer.Breadcrumb.cs`), so a decision to promote them into the production call path is a + design decision about `ItemViewer`'s breadcrumb-attach contract — squarely the kind of decision + the epic reserves for the later ItemViewer-owning child, not for a determinism-cleanup child. +3. **Explicit write prohibition.** The epic's "Recorded Preconditions for Later Epics" section + states plainly: "No child of this epic may write under `docs/features/potential/**`." The + potential document's own Item 2 disposition options ("promote these members to the production + attach path... or... mark them explicitly as test seams") both require either a code change to a + file this child does not own, or a documentation update that would need to live under + `docs/features/potential/**` (or an equivalent restricted location) to record the seam status + formally — either path crosses a boundary this child is not permitted to cross. + +The tradeoff: leaving Item 2 unaddressed means those ~40 lines of test-only production surface +persist without a resolution, and the underlying question ("should `AttachCollapsedMessenger` +route through the seam these members expose, or should the seam be documented as intentional test +infrastructure?") remains open. That is an acceptable and, per the epic's own explicit constraints, +a required deferral — not a gap introduced by this research. The orchestrator should route Item 2 +to a separate issue in the later ItemViewer-owning epic (or, if urgency warrants, to a fifth +sibling issue outside this epic), rather than folding it into #491. + +## G. Toolchain and build risk + +**CSharpier.** `.csharpierignore` (read in full, 15 lines) excludes `*.csproj`, `*.props`, and +`*.targets` from the CSharpier check entirely — so the csproj edit removing the Form1 compile/ +resource entries is invisible to `dotnet tool run csharpier check .`. The three deleted `.cs`/ +`.resx` files simply cease to exist and drop out of the check's input set; CSharpier does not fail +on a file's absence. No `.csharpierignore` change is needed or implied. + +**`.csproj.bak` and `TaskMaster.sln`.** `QuickFiler.Test/QuickFiler.Test.csproj.bak` also contains +`Form1` compile/resource entries (at its own line numbers 82-98, which differ from the live +`.csproj`'s 161-183 because the `.bak` predates the large test-file growth recorded in the live +project). `TaskMaster.sln` was searched for any project reference matching +`QuickFiler\.Test\.csproj` and returns exactly one hit: line 25, referencing +`QuickFiler.Test\QuickFiler.Test.csproj` (the live file). No solution entry names +`QuickFiler.Test.csproj.bak`. `.bak` files are not part of any MSBuild project or solution graph and +are not compiled by any of the four toolchain commands (CSharpier reads `*.cs`/`*.xml`/ +`packages.config` only per `.csharpierignore`'s scope statement; MSBuild `/t:Rebuild` operates on +the solution's project references, which do not include `.bak`; `vstest.console.exe` runs built +test assemblies, not source). Confirmed: `.bak` presents no build risk and requires no edit as part +of this change, though a plan author may choose to delete it for hygiene — that is optional, not +required. + +**Other `System.Windows.Forms` usage in `QuickFiler.Test`.** A search for `using +System.Windows.Forms;` inside `QuickFiler.Test/` returns 46 files, none of which is `Form1.cs` or +`Form1.Designer.cs` (both of which reference `System.Windows.Forms` via fully-qualified names +rather than a `using` directive, per the Designer.cs content read above, and `Form1.cs`'s own +`using` block, read above, lists only `System`). The 46 files span the `Viewers/`, `Controllers/`, +`TestSupport/`, and `Helper Classes/` directories and include, among others, +`QuickFiler.Test/TestSupport/WinFormsPumpHost.cs` (the sibling determinism child #511's subject) and +numerous breadcrumb/controller test files that construct or interact with WinForms controls under +test (e.g., via the pump host or direct control construction in isolated tests). This confirms the +project's `System.Windows.Forms` and `System.Drawing` assembly references are load-bearing far +beyond `Form1` and **must be retained** in `QuickFiler.Test.csproj`. Re-derived from the csproj +(lines 365-366, 420): ``, +``, and +``. A plan must not propose removing any of these three +`` entries. + +## Open questions + +- The exact officially-reported (Koverage-postprocessed, first-party-only) baseline percentage for + `QuickFiler.Test` before this change was not captured in this research session — no toolchain + command was run, per the researcher's hard constraint against running `msbuild`/`vstest`. A plan + must capture that baseline itself using the exact `Invoke-MSTestWithCoverage.ps1` invocation + documented under E before making the change, and a second, identically-shaped invocation after, + to get a directly comparable pair of numbers on the harness's actual filtered denominator (as + opposed to this research's illustrative raw-file arithmetic). +- Whether the maintainer wants `QuickFiler.Test/QuickFiler.Test.csproj.bak` deleted for hygiene + alongside the live csproj edit is a judgment call left to the plan author; it carries no build + risk either way (see G). +- Item 2's eventual disposition (promote to production call path vs. document as an intentional + test seam) is unresolved and, per the analysis in F, is deliberately left unresolved by this + research and by this child's scope. diff --git a/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md new file mode 100644 index 000000000..5bf0e5645 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/spec.md @@ -0,0 +1,494 @@ +# quickfiler-test-form1-live-form (Spec) + +- **Issue:** #491 +- **Parent (optional):** epic `quickfiler-suite-determinism-foundation` +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T19-06 +- **Status:** Approved +- **Version:** 1.1 + +## Context + +`QuickFiler.Test/Form1.cs` and `QuickFiler.Test/Form1.Designer.cs` declare +`public partial class Form1 : System.Windows.Forms.Form`, compiled directly into the +`QuickFiler.Test` unit-test assembly. `Form1.Designer.cs:32-34` constructs three +`QuickFiler.ItemViewer` instances inside `InitializeComponent`. Research confirms the type is +never instantiated by any test today, so no runtime failure currently occurs. The defect is +latent: `.claude/rules/general-unit-test.md` and this epic's determinism goal require that no +unit-test run construct a live WinForms window, and this type is one `new Form1()` call away from +breaching that rule while contributing no test value. It is dead weight that must be removed +before it can be misused. + +This child is issue #491 of the `quickfiler-suite-determinism-foundation` epic, scoped in the +epic manifest to "live form in the test project... `Form1.cs` and its designer" only. + +## Repro & Evidence + +There is no runtime repro. `Form1` is never constructed by any existing test, `[TestClass]`, or +reflection-based discovery path — confirmed by a repository-wide word-boundary search for `Form1` +(82 files) and a targeted search for `Assembly.GetTypes`, `Activator.CreateInstance`, and +`GetType(` inside `QuickFiler.Test/` (4 call sites, none referencing `Form1`). No policy violation +is observable today by running the existing suite. + +The defect is a latent policy violation and unused production surface inside the test assembly, +not an active runtime failure. Per the repository's Bugfix Workflow, the "repro" for this class of +defect is the regression guard test specified below: it must be **red** before removal (because +`Form1.Designer.cs:3` currently declares the only `Form`-derived type compiled into +`QuickFiler.Test`) and **green** after removal (because no `Form`-derived type remains in the +assembly). The guard's pre-change failure is the closest analogue to a repro this defect admits. + +- Steps to reproduce: none — no test currently exercises `Form1`. +- Expected vs actual behavior: expected — no `Form`-derived type is compiled into a unit-test + assembly; actual — one is (`QuickFiler.Test.Form1`), unused. +- Logs/screenshots/error snippets: none applicable. +- Frequency / determinism: not applicable; this is a static compile-time defect, not an + intermittent runtime one. + +## Scope & Non-Goals + +- In scope: + - Delete `QuickFiler.Test/Form1.cs`, `QuickFiler.Test/Form1.Designer.cs`, and + `QuickFiler.Test/Form1.resx`. + - Remove the corresponding `` and `` entries from + `QuickFiler.Test/QuickFiler.Test.csproj`, confined to the two owned regions described under + Proposed Fix. + - Add the assembly-level regression guard test specified under Test Strategy. +- Out of scope / non-goals: + - **Item 2 of the potential document is explicitly deferred**, not addressed by this child. See + the dedicated subsection below. + - `QuickFiler/Viewers/Form1.cs` and `QuickFiler/Viewers/Form1.Designer.cs` — a different, + unrelated production type in namespace `QuickFiler.Viewers`. It is a four-line + constructor-only partial class with no further members, is not test code, and is not touched + by this change. + - `UtilitiesCS.Test/Form1.cs` and `SVGControl.Test/Form1.cs` — the same defect shape in two + unrelated test assemblies. Separate defects, not addressed here. + - `QuickFiler.Test/QuickFiler.Test.csproj.bak` — not referenced by `TaskMaster.sln` (the only + solution reference to a `QuickFiler.Test.csproj`-named file is the live `.csproj` at + `TaskMaster.sln:25`), not compiled by any toolchain command, and not part of the owned region. + Left untouched. + - Any part of `QuickFiler.Test.csproj` outside the two owned regions (see Proposed Fix). Three + sibling epic children (`#511`/`#571`, `#445`, `#449`) work concurrently against the same file; + touching any other region risks a fan-in conflict. + - Any change to `System.Drawing`, `System.Drawing.Design`, or `System.Windows.Forms` + `` entries in `QuickFiler.Test.csproj`. These are retained (see Proposed Fix). +- Explicitly excluded systems, integrations, or datasets: none — this is a self-contained test + assembly composition change with no data, config, or external-integration surface. + +### Item 2 is out of scope for this child + +The potential document's Item 2 (the three test-only `internal` members +`AttachBreadcrumbMessengerWhenReadyAsync`, `AttachBreadcrumbMessenger`, and `BreadcrumbOpenTask` in +the production file `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs`) is deliberately deferred, for +three reasons, each verified directly rather than inferred: + +1. **File-set boundary.** The epic manifest's Scope section (`epic.md:68-69`) scopes #491 + explicitly to "live form in the test project... `Form1.cs` and its designer." Item 2 touches a + wholly different production file with no textual or structural overlap with `Form1`. +2. **Epic-level non-goal.** The epic's Non-Goals section reserves the `IItemViewer` UI-thread seam + consolidation (#489) — which rewrites `IItemViewer`, `ItemViewer.cs`, and + `ItemViewer.WebViewThread.cs` — for a later epic's ItemViewer child. Item 2's members live in a + sibling partial-class file of the same `ItemViewer` type family + (`ItemViewer.Breadcrumb.cs`), so deciding whether to promote them into the production call path + is a design decision about `ItemViewer`'s breadcrumb-attach contract, squarely the kind of + decision the epic reserves for that later child. +3. **Explicit write prohibition.** The epic's Hard Constraint 1 forbids any child of this epic from + editing `.claude/**`, and its Recorded Preconditions bar writing under + `docs/features/potential/**`. Both of Item 2's candidate dispositions — promoting the members + into the production attach path, or documenting them as intentional test seams — require either + a code change to a file this child does not own, or a documentation update that would need to + live under a restricted location to record the seam status formally. + +Re-verification during research (2026-08-21) confirmed the three members are still present, still +`internal`, and still production-callerless: a repository-wide search for +`AttachBreadcrumbMessengerWhenReadyAsync` and `AttachBreadcrumbMessenger` inside `QuickFiler/` +(production code only) returns exactly one file, `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` +itself (the declaration site). Nothing is lost by deferring: the members and their five dependent +test files (`BreadcrumbCollapsedSurfaceReadinessTests.cs`, `BreadcrumbSubfolderActivationTests.cs`, +`BreadcrumbSelectorOpenRetryTests.cs`, `BreadcrumbCoordinatorLifecycleTests.cs`, +`BreadcrumbDropDownIntegrationTests.cs`) continue to function unchanged whether or not this child +runs. The orchestrator is responsible for reporting Item 2 upward for scheduling in the later +ItemViewer-owning epic; this spec records the deferral so it is not silently dropped. + +## Root Cause Analysis + +- Current hypothesis or confirmed root cause: `QuickFiler.Test/Form1.cs` and + `QuickFiler.Test/Form1.Designer.cs` were added to the test assembly as a manual/visual harness — + `Form1.cs:22-34`'s `LoadControlGroup` method exists solely to demonstrate manually adding + `ItemViewer` controls to a `TableLayoutPanel` at design time, not to run as an automated test. + No production or test caller ever needed it to compile into `QuickFiler.Test`; it was never + removed after ceasing to serve that manual purpose. +- Signals/evidence supporting it: zero references to `Form1` outside its own three files and the + four `QuickFiler.Test.csproj` entries; zero reflection-based discovery paths reach it; the + `.resx` carries zero `` elements (pure WinForms-designer boilerplate with nothing to load). +- Affected components/modules (paths, services, pipelines): + - `QuickFiler.Test/Form1.cs` + - `QuickFiler.Test/Form1.Designer.cs` + - `QuickFiler.Test/Form1.resx` + - `QuickFiler.Test/QuickFiler.Test.csproj` (compile/resource entries only) + +## Proposed Fix + +### Design summary (what changes where) + +Delete the three dead files and their `QuickFiler.Test.csproj` entries. Add one new MSTest guard +that asserts, by reflection over type metadata only, that the executing test assembly contains no +`System.Windows.Forms.Form`-derived type. This converts a currently-unenforced policy expectation +into a permanent, automatically-checked invariant, and prevents the class of regression this issue +reports from recurring. + +### Boundaries and invariants to preserve + +- `QuickFiler/Viewers/Form1.cs` (the unrelated production type in `QuickFiler.Viewers`) is not + touched. +- The three ``, + ``, and + `` entries in `QuickFiler.Test.csproj` are retained. + 46 other files in `QuickFiler.Test` (spanning `Viewers/`, `Controllers/`, `TestSupport/`, and + `Helper Classes/`, including `QuickFiler.Test/TestSupport/WinFormsPumpHost.cs`) depend on + `System.Windows.Forms` via `using` directives, so these references are load-bearing far beyond + `Form1` and must not be removed. +- `QuickFiler.Test/QuickFiler.Test.csproj.bak` is not edited. + +### Dependencies or blocked work + +None. The epic's dependency graph for wave 0 is empty; #491 has no `depends_on` edge to any +sibling child, and its owned csproj regions do not overlap any sibling child's region. + +### Implementation strategy (what changes, not sequencing) + +#### Files/modules to change + +- Delete: `QuickFiler.Test/Form1.cs`, `QuickFiler.Test/Form1.Designer.cs`, + `QuickFiler.Test/Form1.resx`. +- Add: a new MSTest guard test file, recommended path + `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs` (mirroring the existing repository style of + placing test files at the `.Test/` root, alongside + `QuickFiler.Test/QfcViewer_Test.cs` and `QuickFiler.Test/SetupAssemblyInitializer.cs`). +- Edit: `QuickFiler.Test/QuickFiler.Test.csproj`, confined to two owned regions. + +#### CSPROJ region ownership (re-derive before editing) + +This child owns exactly two regions of `QuickFiler.Test/QuickFiler.Test.csproj`, and the executor +must re-derive the current line numbers from the working tree before editing rather than trusting +any number recorded here or in the epic manifest: + +- **Lines 161-166** — the `Form1.cs` and `Form1.Designer.cs` `` blocks, closing + tags included. (The epic manifest cites "161-165"; the closing tag at 166 is part of the region + and must be included in the edit.) +- **Lines 179-183** — the entire `` whose sole child is the `Form1.resx` + `` element. (The epic manifest cites "180-181"; lines 179 and 183 are the + `` open/close tags wrapping that single child, and removing the child without removing + the now-empty wrapper leaves dead structure with no purpose.) + +Because a new test file requires its own `` entry, and adding that entry anywhere +outside these two regions would collide with a sibling child's concurrent edit, the new entry must +be placed **inside** the owned region: lines 161-166 are replaced by a single new +`` entry, so the net edit to the csproj stays +wholly within the two owned regions. Lines 179-183 are deleted in full (the `` becomes +empty and is removed, not merely its child element). + +No other part of `QuickFiler.Test.csproj` is touched. In particular, the `Controllers` item group +that sibling child #449 appends to (ending at line 178 per research) is left untouched, and the +three `System.Windows.Forms`-family `` entries are left untouched. + +#### Functions/classes/CLI commands impacted + +- Removed: `QuickFiler.Test.Form1` (partial class, two files). +- Added: one new `[TestClass]` in `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs` containing one + `[TestMethod]` implementing the regression guard (see Test Strategy). +- No production code, CLI command, or public API is touched. + +#### Data flow and validation changes + +None. This is a test-assembly composition change; no runtime data flow is affected. + +#### Error handling and logging updates + +None applicable — no error-handling or logging code is touched by this change. + +#### Rollback/feature-flag considerations (if applicable) + +No feature flag is needed. Rollback is a plain revert of the commit; the deleted files' history +remains available in git if a manual visual harness is ever wanted outside the unit-test assembly. + +### Technical specifications (interfaces/contracts) + +#### Inputs/outputs and formats + +Not applicable — no interface or contract is added or changed. + +#### Required configuration keys and defaults + +None. `coverage.config` requires no change: it excludes only third-party module paths by regex +(`Deedle`, `FSharp`, `Castle\.Core`, `FluentAssertions`, `Moq`, `Microsoft\.Testing`, `MSTest`) and +carries no `QuickFiler.Test` or first-party exclusion entry, so `QuickFiler.Test.dll` remains fully +instrumented after the change with no configuration edit required. + +#### Backward-compatibility expectations + +No public API is removed or changed; `Form1` was never a public contract consumed outside its own +files. No backward-compatibility break is introduced. + +#### Performance constraints (latency/throughput/memory) + +Not applicable — no measurable performance surface is affected. + +## Assumptions, Constraints, Dependencies + +- Assumptions (environment, data, access): the worktree state matches the research document's + 2026-08-21 re-derivation; the executor must re-confirm all cited line numbers and search results + before editing, per the epic's "Known-Stale Potential-Document References" warning. +- Constraints (budget, performance, compatibility): + - No `.claude/**` file may be edited (epic Hard Constraint 1); a rule file cited here is the + policy the fix is measured against, not an edit target. + - `vstest` invocations must carry `/InIsolation` and + `/TestCaseFilter:"TestCategory!=LiveOutlook"` (epic Hard Constraint 2); omitting `/InIsolation` + produces roughly 1,695 phantom failures from a Moq `TypeInitializationException` that must not + be mistaken for a real regression. + - Recursive `*.Test.dll` discovery must exclude `\.claude\worktrees\` paths to avoid loading + stale agent-worktree builds. + - `msbuild` analyzer and nullable gates must use `/t:Rebuild`, never `/t:Build` (a warm + `/t:Build` skips `CoreCompile` and the gate cannot fail), and must never pass + `/p:Nullable=enable` (no project in this repository opts in solution-wide, and forcing it + produces hundreds of unrelated errors). + - CRLF line endings in `QuickFiler.Test.csproj` must be preserved; edits should be confined to + minimal adjacent hunks within the two owned regions. +- External dependencies (services, libraries, releases): none beyond the existing MSTest, Moq, and + FluentAssertions packages already referenced by `QuickFiler.Test.csproj`. + +## Data / API / Config Impact + +- User-facing or API changes: none. +- Data or migration considerations: none. +- Logging/telemetry updates (if any): none. +- Compatibility notes (CLI flags, config schemas, versioning): none. `coverage.config` is unchanged + (see Technical Specifications above). + +## Test Strategy + +### Regression guard — the load-bearing design decision + +Because `Form1` is never instantiated, there is no runtime repro to reproduce with a failing test. +The regression test required by the Bugfix Workflow is instead a deterministic, assembly-level +structural guard: + +- A single MSTest `[TestClass]`, recommended file + `QuickFiler.Test/NoLiveFormInTestAssemblyTests.cs`, asserting via FluentAssertions that + `Assembly.GetExecutingAssembly().GetTypes()` contains **no** type assignable to + `System.Windows.Forms.Form`. +- The check must be scoped to the **executing** (`QuickFiler.Test`) assembly only, never to a + referenced assembly. `QuickFiler/Viewers/Form1.cs` is a legitimate production `Form`-derived type + in the referenced `QuickFiler` assembly, and the guard must not flag it. +- The test must **not** construct any form, control, or `BackgroundWorker`. Reflection over + `Type` metadata (`GetTypes()`, `IsAssignableFrom`/`IsSubclassOf`) is metadata-only and requires no + instantiation; instantiating any WinForms type inside a unit test is exactly the failure mode + this issue exists to prevent. +- Verified pre-change state: `Form1.Designer.cs:3` declares the only `Form`-derived type in + `QuickFiler.Test` today, so this guard is **red** before the fix (fails, because `Form1` is + found) and **green** after (passes, because no `Form`-derived type remains). +- Frameworks: MSTest attributes (`[TestClass]`, `[TestMethod]`), FluentAssertions for the + assertion. Moq is not needed — the guard has no dependency to mock. + +### Unit tests (MSTest) for the fixed behavior and boundaries + +- New: the guard test above (positive case — after the fix, the assertion passes). +- Existing: no existing `QuickFiler.Test` test constructs, references, or depends on `Form1`, so no + existing test requires modification as a direct consequence of this change. + +### Edge cases and negative scenarios (invalid inputs, missing data, boundary values) + +- The guard must not produce a false pass by scoping to the wrong assembly (e.g., accidentally + scanning the referenced `QuickFiler` assembly, which legitimately contains `Form`-derived types). + This is covered by asserting against `Assembly.GetExecutingAssembly()` specifically. +- The guard must not produce a false pass by matching only the exact type `Form1` rather than any + `Form`-derived type; the assertion checks assignability to `System.Windows.Forms.Form`, not a + named-type comparison, so it also catches any future reintroduction of a differently-named live + form. + +### Error handling and logging verification + +Not applicable — no error-handling or logging path is introduced or changed. + +### Coverage impact and targets for changed lines/modules + +`QuickFiler.Test.Form1` contributes 187 always-uncovered coverable lines (157 from +`Form1.Designer.cs`, 30 from `Form1.cs`), all recorded at `hits="0"` in the baseline Cobertura +evidence. That 187-line figure is real, but it describes a **raw, unfiltered `dotnet-coverage` +denominator only** — the count of coverable lines `dotnet-coverage` reports before any +repository-specific post-processing. For the harness this spec mandates +(`scripts\vscode\Invoke-MSTestWithCoverage.ps1`), the expected effect of removing `Form1` on the +measured figure is **no change**: an expected delta of exactly 0 on both `lines-valid` and +`lines-covered`. + +This was verified by reading the harness implementation directly, at two independent points where +`QuickFiler.Test.dll` — the assembly `Form1` is compiled into — is excluded from measurement: + +1. **Instrumentation exclusion.** `scripts/vscode/Invoke-MSTestWithCoverage.ps1:99` sets + `$testAssemblyPattern = '.*\.Test\.dll$'` and appends it to the derived settings file's + `ModulePaths/Exclude` list, so `QuickFiler.Test.dll` is never instrumented by `dotnet-coverage` + in the first place. +2. **Allowlist exclusion.** `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:39-41` skips + every project whose resolved assembly name ends in `.Test` when building the Koverage project + allowlist. The in-file comment at `:20-23` states the intent directly: test projects are + excluded so that `ConvertTo-KoverageCoberturaXml` strips their `` elements from **both** + the numerator (`lines-covered`) and the denominator (`lines-valid`). That strip is performed at + `:417-421`, and the root `` element's `line-rate`, `lines-covered`, and `lines-valid` + attributes are recomputed from the surviving packages at `:442-445`. + +Because `Form1`'s 187 lines live in `QuickFiler.Test.dll`, they are outside this harness's +denominator **both before and after** the change: `QuickFiler.Test` is never instrumented (point 1) +and, even if it were, its packages would be stripped from the Koverage-filtered totals before those +totals are written back to the output file (point 2). The 187-line reduction is a real property of +a raw `dotnet-coverage collect` capture; it is not a property of the filtered figure this spec's +acceptance criteria are measured against. + +- Baseline capture (before any file is deleted): + ``` + pwsh -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 ` + -SearchRoot . ` + -Configuration Debug ` + -CoverageOutput 'docs\features\active\2026-08-07-quickfiler-test-form1-live-form-491\evidence\baseline\coverage-baseline.cobertura.xml' + ``` +- Post-change capture (after the fix, same harness): + ``` + pwsh -File scripts\vscode\Invoke-MSTestWithCoverage.ps1 ` + -SearchRoot . ` + -Configuration Debug ` + -CoverageOutput 'docs\features\active\2026-08-07-quickfiler-test-form1-live-form-491\evidence\qa-gates\coverage-postchange.cobertura.xml' + ``` +- Both parameter names (`-SearchRoot`, `-Configuration`, `-CoverageOutput`) are confirmed from the + script's `param()` block. +- **Two-denominator hazard.** `Invoke-MSTestWithCoverage.ps1` post-processes the raw Cobertura + output for Koverage compatibility and strips `` elements for third-party assemblies not + part of the solution, so it emits a filtered, first-party-only figure as its final output. The + baseline and post-change figures must both come from this same script's output; a raw + `dotnet-coverage collect` figure must never be substituted for either side of the comparison, and + a filtered figure must never be compared against an unfiltered one. + - **Sequencing detail (verified by reading the script).** `Invoke-MSTestWithCoverage.ps1` writes + the **raw** Cobertura capture to the path named by `-CoverageOutput` first, then calls + `Assert-CoberturaLineCoverageThreshold` on the filtered content at `:341`, and only overwrites + that same file with the **filtered** content at `:343` if the assertion at `:341` does not + throw. `Assert-CoberturaLineCoverageThreshold` (`Invoke-MSTestWithCoverage.Helpers.ps1:487-490`) + throws when computed line coverage is below 80%. If it throws, execution stops before `:343` + runs, and the file left on disk at `-CoverageOutput` is the **raw, unfiltered** capture — not + the filtered figure a reader might expect. A capture must therefore be checked for a zero exit + code from `Invoke-MSTestWithCoverage.ps1` before either side of the baseline/post-change + comparison is read from disk; a nonzero exit invalidates that side of the comparison regardless + of what the file on disk appears to contain. +- Acceptance condition: post-change line coverage (from the filtered Koverage output) is `>=` + baseline line coverage (from the same filtered output), with both values recorded as actual + numbers in the evidence artifacts above — not as placeholders or estimates. For this specific + change, the expected delta is 0 on both `lines-valid` and `lines-covered` (see "Coverage impact + and targets for changed lines/modules" above); a delta of 0 satisfies this condition, and any + observed delta must still be recorded as an actual number rather than assumed. + +### Toolchain commands to run (format -> lint -> type-check -> test) + +Run in this exact order, restarting from the top if any step fails or modifies files: + +1. `dotnet tool run csharpier format .` then verify with `dotnet tool run csharpier check .` + (`.csharpierignore` excludes `*.csproj`, so the csproj edit is invisible to this check; the new + `.cs` test file is formatted and checked normally). +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` + (no `/p:Nullable=enable`) +4. `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`, + with recursive `*.Test.dll` discovery (if used) excluding `\.claude\worktrees\`. + +### Manual validation steps (if required) + +None required. The guard test and the coverage comparison are sufficient automated verification; +there is no user-facing surface to validate manually. + +## Acceptance Criteria + +- [ ] No `System.Windows.Forms.Form`-derived type is compiled into the `QuickFiler.Test` assembly, + proven by a named MSTest guard test (`NoLiveFormInTestAssemblyTests` or equivalent) that + reflects over `Assembly.GetExecutingAssembly().GetTypes()` and fails if any such type is + present. +- [ ] `QuickFiler.Test/Form1.cs`, `QuickFiler.Test/Form1.Designer.cs`, and + `QuickFiler.Test/Form1.resx` are deleted from the working tree. +- [ ] The corresponding `` and `` entries are removed + from `QuickFiler.Test/QuickFiler.Test.csproj`, with the edit confined to the two owned line + regions (the re-derived `Form1.cs`/`Form1.Designer.cs` compile block, and the re-derived + `Form1.resx` ``), and with the new guard test's `` entry placed + inside the same owned region rather than elsewhere in the file. +- [ ] The ``, ``, + and `` entries remain present and unmodified in + `QuickFiler.Test.csproj`. +- [ ] `dotnet tool run csharpier format .` and `dotnet tool run csharpier check .` both complete + with no diffs. +- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + completes with zero analyzer errors. +- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + completes with zero errors, and no command in this change ever passes + `/p:Nullable=enable`. +- [ ] `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"` + completes with zero failing tests. +- [ ] No pre-existing `QuickFiler.Test` test regresses as a result of this change (test-count and + pass-count parity with the pre-change run, apart from the one new guard test). +- [ ] Post-change line coverage (captured via `Invoke-MSTestWithCoverage.ps1`) is greater than or + equal to the baseline line coverage captured via the same script before the change, with both + values recorded as actual numbers in the evidence artifacts. For this harness the expected + delta is 0 (see Coverage impact and targets above); the criterion remains satisfied by an + observed delta of 0 and is not satisfied by an unrecorded or estimated value. +- [ ] Item 2 of the potential document (the three `internal` members of + `ItemViewer.Breadcrumb.cs`) is explicitly recorded as deferred to a later epic's + ItemViewer-owning child, not silently dropped from tracking. + +## Risks & Mitigations + +- Technical or operational risks: + - **Csproj fan-in conflict.** Three sibling epic children (`#511`/`#571`, `#445`, `#449`) edit + the same `QuickFiler.Test.csproj` concurrently. An edit outside the two owned regions risks + colliding with a sibling's concurrent change. + - Mitigation: confine every csproj edit to the two owned regions (161-166, 179-183, re-derived + at edit time), including the new guard test's compile entry. + - **Line-number drift.** The epic manifest's cited line numbers (161-165, 180-181) already + differ slightly from the freshly re-derived numbers (161-166, 179-183) used in this spec. + - Mitigation: the executor re-derives exact line numbers from the working tree immediately + before editing, per the epic's "Known-Stale Potential-Document References" warning, and does + not trust any cited number, including the ones in this spec. + - **Coverage-figure hazard.** Comparing a raw `dotnet-coverage collect` figure against the + harness's Koverage-filtered figure would produce a meaningless, non-comparable pair of numbers. + - Mitigation: both the baseline and post-change captures use the identical + `Invoke-MSTestWithCoverage.ps1` invocation shape, so both sides are filtered identically. + - **Phantom vstest failures.** Omitting `/InIsolation` produces roughly 1,695 unrelated phantom + failures that could be mistaken for a real regression caused by this change. + - Mitigation: always run `vstest.console.exe` with `/InIsolation` and the documented + `TestCategory!=LiveOutlook` filter, per epic Hard Constraint 2. +- Mitigations and rollbacks: the change is a pure deletion plus one additive guard test; rollback + is a plain `git revert` of the commit, with no data migration or feature flag involved. + +### Corrected assumption: coverage-delta claim (preflight finding) + +The original research for this child computed the 187-line figure from the raw Cobertura +denominator without reading the implementation of +`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1`, and the research document says so +explicitly. That gap produced an incorrect claim, since corrected in the "Coverage impact and +targets" subsection above: the harness excludes `.Test`-suffixed assemblies from both +instrumentation and the Koverage allowlist, so removing `Form1` is expected to leave the measured +`lines-valid` and `lines-covered` totals unchanged, not to raise them. + +- Mitigation: coverage claims about this repository's measured figures must be verified against + the harness's post-processing code (`Invoke-MSTestWithCoverage.ps1` and its `Helpers.ps1` + companion), not inferred from a raw Cobertura capture or from the coverable-line count alone. + Any future spec or plan asserting a specific coverage-delta direction must cite the harness + behavior it relies on, the same way this correction does. + +## Rollout & Follow-up + +- Release/rollout steps: standard PR merge through the epic's per-child pull-request flow; no + staged rollout, feature flag, or migration is required. +- Post-fix monitoring or clean-up tasks: + - The orchestrator reports Item 2 (the three `ItemViewer.Breadcrumb.cs` test-only members) + upward for scheduling as a separate issue in the later ItemViewer-owning epic. + - Optional, not required: `QuickFiler.Test/QuickFiler.Test.csproj.bak` also carries stale `Form1` + entries (at its own line numbers 82-98) and could be deleted for hygiene in a future, + unrelated change; it carries no build risk either way and is explicitly out of scope here. +- Links: issue #491 + (https://github.com/drmoisan/TaskMaster/issues/491); epic + `docs/features/epics/quickfiler-suite-determinism-foundation/epic.md`; research + `docs/features/active/2026-08-07-quickfiler-test-form1-live-form-491/research/form1-removal-research.2026-08-21T18-15.md`; + original potential document + `docs/features/potential/promoted/2026-08-07-quickfiler-test-form1-live-form.md`. From 8092f04b14fc87545b3be901a7a077bb84c591aa Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Fri, 21 Aug 2026 19:21:10 -0400 Subject: [PATCH 06/37] docs(prep): preserve incomplete preparation artifacts for #449 epic-planner preserved this work after the preparation orchestrator was terminated by an infrastructure error (API 529 Overloaded), not by a task failure. Preparation did NOT complete: the atomic-executor preflight was delegated but its signal never returned, so no PREFLIGHT: ALL CLEAR exists. Present: issue.md, spec.md, research artifact, and an atomic plan. Absent: preflight clearance. Committed so a relaunched child resumes from this commit instead of losing an uncommitted worktree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016LdWAA7aMkzJ27NUW7WzaT --- .../issue.md | 96 ++ .../plan.2026-08-21T18-09.md | 198 +++ ...rer-controller-defects.2026-08-21T18-20.md | 1039 +++++++++++++++ .../spec.md | 1136 +++++++++++++++++ 4 files changed, 2469 insertions(+) create mode 100644 docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/issue.md create mode 100644 docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/plan.2026-08-21T18-09.md create mode 100644 docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/research/qfc-explorer-controller-defects.2026-08-21T18-20.md create mode 100644 docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/spec.md diff --git a/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/issue.md b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/issue.md new file mode 100644 index 000000000..e383b6a3e --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/issue.md @@ -0,0 +1,96 @@ +# quickfiler-explorer-controller-latent-defects (Issue #449) + +- Date captured: 2026-08-07 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-explorer-controller-latent-defects/ (Issue #449) +- Found during: research for issue #435 (child F6 of epic #136, QuickFiler per-file coverage) + +- Issue: #449 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/449 +- Last Updated: 2026-08-08 +- Work Mode: full-bug + +## Summary + +Two independent latent defects in `QuickFiler/Controllers/QfcExplorerController.cs`, plus a block of +dead duplicated code. All three were found by reading during F6 coverage research and none is fixed by +F6, whose acceptance criteria forbid behavior changes. + +## Defect 1 — `ExplConvView_Cleanup()` throws `NotImplementedException` + +`ExplConvView_Cleanup()` is declared on the public interface `IQfcExplorerController` +(`QuickFiler/Interfaces/IQfcExplorerController.cs:12`) but its implementation throws +`NotImplementedException`. Any caller reaching it fails at runtime rather than degrading. + +The intended semantics appear to be recoverable from the legacy implementation at +`QuickFiler/Legacy/QuickFileController.cs:851-869` (not compiled), which should be read before +implementing rather than reinventing the behavior. + +Mitigating factor: the member currently has no production callers, so the throw is not reachable in +normal operation today. That makes it latent rather than active — but it is a live trap for the next +caller. + +## Defect 2 — `OpenQFItem` re-resolves the active explorer + +`OpenQFItem` calls `_globals.Ol.App.ActiveExplorer()` a second time at +`QuickFiler/Controllers/QfcExplorerController.cs:140` instead of reusing the `_activeExplorer` field +captured in the constructor at line 35. + +This is both a redundant COM round-trip and a correctness hazard: if the active explorer changed +between construction and the call, the method operates on a different `Explorer` than the rest of the +type, so the object's view of "the" explorer becomes internally inconsistent. + +## Defect 3 — dead duplicated code block + +`QuickFiler/Controllers/QfcExplorerController.cs:183-321` (the `#region Email Sorting To Rewrite`) +contains six private/internal statics — `SanitizeArrayLineTSV`, `StripTabsCrLf`, +`WriteCSV_StartNewFileIfDoesNotExist`, `SanitizeArray`, `SaveMessageAsMSG`, +`GetCurrentExplorerFolder`. A repo-wide search confirms they are referenced only from inside that same +region (lines 193, 241, 264). Every external caller binds to separate copies in +`UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs`, +`UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs`, and +`ToDoModel/Email Utilities/SortItemsToExistingFolder.cs`, which carry their own tests in +`UtilitiesCS.Test`. + +Two latent defects were additionally observed inside this dead block: +- `WriteCSV_StartNewFileIfDoesNotExist` passes transposed arguments to `Path.Combine`. +- `SanitizeArray` writes into a `null` `ref string[]`, which would throw if ever reached. + +Because the block is unreachable, neither defect can fire today. Deleting the region is +behavior-neutral for the QuickFiler assembly and removes roughly 139 lines of uncoverable +filesystem-I/O code from the coverage denominator. + +## Why This Is Filed Separately + +All three items were found during read-only research for the F6 coverage child (issue #435). F6's +acceptance criteria require no behavior change to observable QuickFiler flows, and fixing a +`NotImplementedException` or changing which `Explorer` instance is used are both behavior changes. +Recording them only as prose inside a feature folder would lose them at merge. + +## Impact + +- Defect 1: runtime failure for the next caller of a public interface member. +- Defect 2: redundant COM call plus a potential inconsistency window. +- Defect 3: no runtime impact; carrying cost is coverage-denominator pollution and duplicated code + that can drift from the maintained copies in `UtilitiesCS`. + +## Acceptance Criteria (early draft) + +- [ ] `ExplConvView_Cleanup()` either implements the legacy semantics from + `QuickFiler/Legacy/QuickFileController.cs:851-869` or is removed from `IQfcExplorerController` + with all implementers updated; the decision is recorded with rationale. +- [ ] `OpenQFItem` reuses the constructor-captured `_activeExplorer` field, or the reason a fresh + `ActiveExplorer()` call is required is documented in code. +- [ ] The dead `#region Email Sorting To Rewrite` block is deleted, with a test run confirming no + behavior change. +- [ ] Deterministic regression tests cover each changed path; no temporary files, no live forms. +- [ ] Full C# toolchain passes: csharpier, analyzer build, nullable build, coverage-enabled vstest. + +## Coordination Note + +The dead-code deletion overlaps the file F6 is actively covering. Sequence this issue AFTER F6 merges, +or coordinate through the epic, to avoid a conflict on `QfcExplorerController.cs`. + +## Next Step + +- [ ] Promote to GitHub issue (bug template) diff --git a/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/plan.2026-08-21T18-09.md b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/plan.2026-08-21T18-09.md new file mode 100644 index 000000000..3b194b772 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/plan.2026-08-21T18-09.md @@ -0,0 +1,198 @@ +# Atomic Plan — quickfiler-explorer-controller-latent-defects (Issue #449) + +- **Issue:** #449 +- **Epic:** `quickfiler-suite-determinism-foundation` (wave 0, complexity band C3) +- **Work mode:** `full-bug` +- **Requirements source:** `docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/spec.md` (Status `Approved`, AC-1 through AC-16, decisions D1 through D7). Under `full-bug` this `spec.md` is the sole acceptance-criteria source; `issue.md`'s early-draft list is superseded. +- **Primary evidence:** `docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/research/qfc-explorer-controller-defects.2026-08-21T18-20.md` +- **Feature folder (FEATURE):** `docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449` +- **Worktree root (WORKTREE):** `C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a78a924c87d7f1f73` + +## Execution conventions + +1. **Run every C# tool through `pwsh -NoProfile` with absolute paths.** The Bash tool mangles MSBuild switches: `/m` becomes `M:/` and MSBuild reports MSB1008. When a payload is passed as `pwsh -NoProfile -Command '...'`, single-quote the outer payload and double-quote inner literals, because a double-quoted outer payload expands `$` variables in the parent shell before `pwsh` ever sees them. +2. **Toolchain order is fixed:** (1) `dotnet tool restore`, (2) `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .`, (3) analyzer msbuild, (4) nullable msbuild, (5) `vstest.console.exe` with coverage. If any step fails or modifies a file, restart the loop from step 2. +3. **`/t:Rebuild`, never `/t:Build`.** MSBuild's incremental up-to-date check does not invalidate on a command-line `/p:` change, so a warm `/t:Build` returns exit 0 with `CoreCompile` skipped on every project and runs no analyzers — the gate cannot fail. Where the analyzer gate is asserted, assert a **zero** count of the string `Skipping target "CoreCompile"` in the build log, not a `csc.exe` count. +4. **Never add `/p:Nullable=enable`.** It is a solution-wide opt-in that `.github/workflows/ci.yml` omits deliberately; it produced 195 errors in `UtilitiesCS.csproj`. Nullable enforcement here is per-file via `#nullable enable`, and `QfcExplorerController.cs` carries no such pragma. +5. **`/InIsolation` is mandatory on every `vstest` run.** Without it each assembly's `app.config` binding redirects are ignored, roughly 1,695 phantom failures appear with empty messages and sub-millisecond durations, and the symptom is a Moq `TypeInitializationException` via `System.Threading.Tasks.Extensions`. A mass regression of that shape means the flag is missing; it must not be "fixed" any other way. +6. **Test-assembly discovery must exclude `\.claude\` relative to WORKTREE, not absolutely.** WORKTREE is itself under `.claude\worktrees\`, so an absolute-path substring exclusion would discard every assembly in this tree. Apply the exclusion to the path **suffix after WORKTREE**. The CI reference invocation is `.github/workflows/_mstest-coverage.yml`, which filters on `\bin\Debug\` and excludes `\obj\` and `\ref\`; the `\.claude\` exclusion is added on top of those three. +7. **No Python toolchain exists in this repository.** There is no `scripts/dev_tools/` and no Poetry manifest. Any skill step naming `poetry run python -m scripts.dev_tools.*` is **unrunnable by absence**; record it as such in the evidence artifact rather than fabricating a result or silently omitting it. No plan task below states a Python coverage-target argument, because no Python coverage runner exists here to consume one; C# coverage is collected by `dotnet-coverage` and read from the Cobertura report. +8. **Do not rely on any PreToolUse hook.** Every hook in this repository currently reads `$toolInput.command` while the payload nests the value at `$toolInput.tool_input.command`, so each hook returns `permissionDecision: allow`. Verify every gate from durable `git` state and from the command's own exit code. +9. **Evidence paths are non-overridable.** Every artifact resolves under `/evidence//` with `` in `baseline`, `regression-testing`, `qa-gates`, `issue-updates`, `other`. `evidence/coverage/` is not a canonical kind and must not be used, and no `artifacts/` sub-path other than `artifacts/orchestration/` may hold evidence. +10. Every command-bearing task writes an artifact carrying `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Baseline and final-QC test artifacts record **numeric** coverage values in `Output Summary:`; `UNVERIFIED` is not an acceptable value. + +## Hard constraints restated + +- **No file under `.claude/**` may be edited.** That tree is push-down-owned; a sync overwrites it with no merge. Where an issue or this plan cites a rule file, the citation is the policy the fix is measured against, not an edit target. +- **No file under `docs/features/potential/**` may be written** by this child. +- **No edit to `QuickFiler/QuickFiler.csproj`.** The dead region is inside an already-compiled file, and the uncompiled `Legacy/` and `Notes/` files have no compile entries to remove. +- **No edit to `QuickFiler/Notes/notes_interfaces.cs`.** It is not compiled and is outside this issue's file set; its duplicate `IQfcExplorerController` declaration at `:52-59` stays deliberately inconsistent with the compiled contract. +- **No edit to `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` (1,429 lines) or `EmailFiler.cs` (465 lines).** They are the surviving maintained copies of the duplicated helpers and carry their own tests in `UtilitiesCS.Test`. Consolidating the three copies is a separate, larger change and is not planned here. +- **No split refactor** for `SortEmail.cs` (1,429 lines) or `QuickFiler/Legacy/QuickFileController.cs` (1,065 lines). Both are pre-existing 500-line-cap violations, neither is edited, and neither appears in the diff. +- **Tests must be deterministic and headless.** No temporary file, no live form, no message pump, no `MessageBox.Show`, no `Thread.Sleep`, no `Task.Delay`, no wall-clock wait. MSTest, Moq, and FluentAssertions only. +- **`QuickFiler.Test/QuickFiler.Test.csproj` is a shared surface.** #449 owns exactly one appended `Compile Include` line in the `Controllers` item group. The `Form1` compile region at `:161-166` and the `Form1.resx` `EmbeddedResource` at `:180-182` are owned exclusively by sibling child #491 and must not be touched. + +## Literals this plan creates — quoted verbatim for the acceptance-gate plan-quotation condition + +The following identifiers do not exist in the tracked tree today. The plan's own tasks create them, and they are quoted here verbatim, outside every command span, so that acceptance conditions asserting them are exonerated under rule G5 of `.claude/rules/plan-acceptance-gates.md`. + +Injectable modal-dialog seam member name (D5): `NotInViewDialogInvoker` + +Test class and namespace: `QfcExplorerControllerTests` in `QuickFiler.Controllers.Tests` + +Test method names: + +- `OpenQFItem_WhenActiveExplorerChangesAfterConstruction_UsesTheConstructorCapturedExplorer` +- `OpenQFItem_WhenMailIsAlreadyInTheCurrentFolder_DoesNotChangeCurrentFolder` +- `OpenQFItem_WhenItemIsSelectableInView_ClearsAndAddsSelection` +- `OpenQFItem_WhenItemNotSelectableInView_InvokesDialogSeamOnce` +- `OpenQFItem_WhenDialogSeamReturnsYes_DisplaysMailItem` +- `OpenQFItem_WhenDialogSeamReturnsNo_DoesNotDisplayMailItem` +- `ExplConvView_ToggleOn_WhenFlagSet_AppliesRememberedView` +- `ExplConvView_ToggleOn_WhenFlagClear_DoesNothing` +- `ExplConvView_ToggleOff_WhenConversationsNotGrouped_DoesNothing` +- `ExplConvView_ToggleOff_WhenSiblingViewMissing_CopiesAndSavesTemporaryView` +- `GetSiblingView_WhenNamedViewPresent_ReturnsIt` +- `GetSiblingView_WhenNamedViewAbsent_ReturnsNull` +- `CurrentConversationState_ReflectsCommandBarPressedState` +- `ExplConvView_ReturnState_WhenFlagSet_TogglesOn` + +## Two spec details resolved by this plan + +**(a) AC-8 says "nine directives" but enumerates ten line numbers.** The D4 disposition table is authoritative: ten directives are removed (lines 1, 2, 3, 4, 5, 6, 7, 8, 13, 15) and six are retained (lines 9, 10, 11, 12, 14, 16), which sums to the sixteen directives present. The word "nine" is a miscount in the AC prose. This plan removes **nine** of them in Phase 4 (lines 1, 2, 3, 5, 6, 7, 8, 13, 15) and the tenth, `using System.Diagnostics.CodeAnalysis;` at line 4, in Phase 5 together with the `[ExcludeFromCodeCoverage]` attribute that is its only consumer. Removing line 4 before the attribute would break the analyzer build with CS0246. + +**(b) The seam must not resurrect `using System;`.** D4 removes `using System;` because `NotImplementedException` at line 63 is its last consumer. A seam declared as an unqualified `Func<...>` would re-introduce a `System` consumer and contradict AC-8. The seam is therefore declared with the fully-qualified type name `System.Func<...>`, matching the file's existing fully-qualified style at lines 23-24 (`log4net.ILog`, `System.Reflection.MethodBase`) which is exactly why D4 judges `using System;` orphaned. If the analyzer build nevertheless reports CS0246 for the fully-qualified form, restore `using System;` and record the restoration — that is the self-verifying property D4 relies on. + +## Coverage measurement seam + +The `QfcExplorerController` figure must be computed by aggregating **every** Cobertura `` element whose `filename` attribute ends with the path segment for `QuickFiler\Controllers\QfcExplorerController.cs`, summing hit and total line counts across them. `OpenQFItem` is `async`, so the compiler emits its state machine as a separate `` element with a mangled name, and lambdas emit further separate elements. Reading a single `` element would report a figure for a fragment of the file. The direct `dotnet-coverage collect` invocation planned below performs no closure post-processing, so those elements are present in the raw report. + +--- + +### Phase 0 — Policy reads and baseline capture + +- [ ] [P0-T1] Read `CLAUDE.md` at the WORKTREE root in full (all sections, including the embedded General Code Change Policy, General Unit Test Policy, C# Code Change Policy, and C# Unit Test Policy). **Acceptance:** the file has been read end to end and its path is recorded in the Phase 0 artifact produced by [P0-T5]. +- [ ] [P0-T2] Read `.claude/rules/general-code-change.md` in full. **Acceptance:** the file has been read end to end and its path is recorded in the Phase 0 artifact produced by [P0-T5]. This file is read-only; no task in this plan edits it. +- [ ] [P0-T3] Read `.claude/rules/general-unit-test.md` in full. **Acceptance:** the file has been read end to end and its path is recorded in the Phase 0 artifact produced by [P0-T5]. This file is read-only; no task in this plan edits it. +- [ ] [P0-T4] Verify that `.claude/rules/csharp.md` exists, and if it does, read it in full. **Acceptance:** the artifact produced by [P0-T5] records either the confirmed read with the file's line count, or the literal statement that the file is absent. Do not assert a read of a file that does not exist. (Planner note: the file was present at plan-authoring time.) +- [ ] [P0-T5] Write `/evidence/baseline/phase0-instructions-read.md` carrying `Timestamp:`, `Policy Order:` (the order given by `.claude/skills/policy-compliance-order/SKILL.md`: `CLAUDE.md`, then `.claude/rules/general-code-change.md`, then `.claude/rules/general-unit-test.md`, then the C# rules), and an explicit list of every file read in [P0-T1] through [P0-T4] with its line count. **Acceptance:** the file exists and contains all three required field labels plus the file list. +- [ ] [P0-T6] Write `/evidence/baseline/environment-preconditions..md` recording four verified environment facts with the command used to verify each: (a) there is no `scripts/dev_tools/` directory and no Poetry manifest, so any `poetry run python -m scripts.dev_tools.*` step is unrunnable by absence and is neither fabricated nor silently skipped; (b) `quality-tiers.yml` does not exist at the WORKTREE root, so no QuickFiler tier classification can be cited; (c) the only machine-enforced numeric coverage gate found in this repository is the repo-wide 80% line rate at `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-489`, with no per-file, per-assembly, or branch-coverage gate anywhere under `scripts/`; (d) PreToolUse hooks are inert and no gate in this plan relies on one. **Acceptance:** the artifact exists and carries `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` plus the four lettered findings. +- [ ] [P0-T7] Record the baseline git state: the merge-base SHA against the epic integration branch (or `main` if the integration branch is absent), the current `HEAD` SHA, and `git status --porcelain`. Write `/evidence/baseline/git-state..md`. Store the merge-base SHA in that artifact; later diff gates read it from there. Do not pin the `HEAD` SHA as a plan expectation — gate on tree invariants, not on a specific SHA. **Acceptance:** the artifact exists, carries `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and names the merge-base SHA explicitly. +- [ ] [P0-T8] Baseline toolchain step 1. Run `dotnet tool restore` from WORKTREE via `pwsh -NoProfile`. Write `/evidence/baseline/step1-dotnet-tool-restore..md`. **Acceptance:** the artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and `EXIT_CODE:` is `0`. +- [ ] [P0-T9] Baseline toolchain step 2 (read-only). Run `dotnet tool run csharpier check .` from WORKTREE. Do **not** run `csharpier format .` at baseline: the baseline must describe the unmodified merge-base tree. Write `/evidence/baseline/step2-csharpier-check..md`. **Acceptance:** the artifact carries all four required fields and its `Output Summary:` records the number of files reported as needing formatting (zero or otherwise) as the baseline formatting state. +- [ ] [P0-T10] Baseline toolchain step 3 (analyzers). Run, from WORKTREE via `pwsh -NoProfile`, `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` with the console output captured to a log file. `/t:Rebuild` is load-bearing: a warm `/t:Build` skips `CoreCompile` on every project and returns exit 0 having run no analyzers. Write `/evidence/baseline/step3-analyzer-build..md`. **Acceptance:** the artifact carries all four required fields, its `Output Summary:` records the warning and error counts, and it records that the count of occurrences of the string `Skipping target "CoreCompile"` in the captured log is **zero**. +- [ ] [P0-T11] Baseline toolchain step 4 (nullable / type check). Run, from WORKTREE via `pwsh -NoProfile`, `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`. Do not add `/p:Nullable=enable`. Write `/evidence/baseline/step4-nullable-build..md`. **Acceptance:** the artifact carries all four required fields, records the error count, and states in `Output Summary:` that `/p:Nullable=enable` was not supplied and that `/t:Rebuild` was used. +- [ ] [P0-T12] Baseline toolchain step 5 (tests with coverage). Discover test assemblies by recursing from WORKTREE for `*.Test.dll`, keeping only paths whose suffix after WORKTREE matches `\bin\Debug\` and excluding suffixes matching `\obj\`, `\ref\`, and `\.claude\`. Apply the exclusion to the WORKTREE-relative suffix, not to the absolute path, because WORKTREE itself lies under `.claude\worktrees\`. Then run `dotnet-coverage collect --output --output-format cobertura --settings coverage.config -- /Settings:scripts/vscode/TaskMaster.cli.runsettings /InIsolation /TestCaseFilter:TestCategory!=LiveOutlook`, resolving `vstest.console.exe` through `vswhere`. Write the Cobertura report to the gitignored `coverage/` directory, not into the evidence tree. Write `/evidence/baseline/step5-vstest-coverage..md`. **Acceptance:** the artifact carries all four required fields; `Output Summary:` records the total, passed, failed, and skipped test counts, and three **numeric** coverage values read from the Cobertura report: the repo-wide root `line-rate` as a percentage, the `QuickFiler` package line rate as a percentage, and the `QfcExplorerController` figure. If the run reports a mass failure with empty messages and sub-millisecond durations, treat it as a missing `/InIsolation` flag and re-run with the flag rather than modifying any test. +- [ ] [P0-T13] Record the baseline `QfcExplorerController` coverage value explicitly as **absent from the report**, not as zero. The class-level `[ExcludeFromCodeCoverage]` at `QuickFiler/Controllers/QfcExplorerController.cs:20` suppresses every member, so the class contributes no `` element and no lines to the Cobertura output; "absent" is the correct baseline value and "0%" would be a fabricated figure. Append this statement, with the search performed over the Cobertura report and its result, to `/evidence/baseline/step5-vstest-coverage..md`. **Acceptance:** the artifact states the search performed, its result, and the word absent as the recorded baseline value for that class. +- [ ] [P0-T14] Record the pre-change line counts of the three files that AC-16 measures: `QuickFiler/Controllers/QfcExplorerController.cs`, `QuickFiler/Interfaces/IQfcExplorerController.cs`, and `QuickFiler.Test/QuickFiler.Test.csproj`. Write `/evidence/baseline/file-line-counts..md`. **Acceptance:** the artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` and three numeric line counts (expected 323, 15, and 484 respectively; record the measured values whatever they are). +- [ ] [P0-T15] Record, in `/evidence/baseline/file-line-counts..md`, the pre-existing 500-line-cap violations that this change does **not** touch: `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` and `QuickFiler/Legacy/QuickFileController.cs`, with their measured line counts and the statement that neither is edited and neither will appear in the diff. **Acceptance:** both file paths and both measured line counts appear in the artifact alongside the not-edited statement. + +### Phase 1 — Regression test first, expected to fail + +Per D7 and research §8.2, defect 2 is the only one of the three that admits a constructible failing-before test. This phase must complete **before** any deletion in Phases 3 and 4, because deleting the dead region and the orphaned `using` directives renumbers `QfcExplorerController.cs` and would make the pre-change observation harder to reconstruct. + +- [ ] [P1-T1] Create `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs` with namespace `QuickFiler.Controllers.Tests` and class `QfcExplorerControllerTests` decorated `[TestClass]`, containing only the shared mock-graph fixture from research §5.2: a `MockRepository` with `MockBehavior.Loose`, a `Mock` whose `GetPressedMso("ShowInConversations")` returns `false`, a `Mock` returning that `CommandBars`, a `Mock` whose `ActiveExplorer()` returns that Explorer, a `Mock` with `SetupGet(g => g.Ol.App)` and `SetupGet(g => g.Ol.ViewWide)`, a `Mock`, and a `Mock` whose `FormController` returns it. MSTest, Moq, and FluentAssertions only. **Acceptance:** the file exists at that exact path, declares the class and namespace named above, and contains no `Thread.Sleep`, `Task.Delay`, `MessageBox.Show`, temporary-file API, or `Form` construction. +- [ ] [P1-T2] Append exactly one line, ``, to `QuickFiler.Test/QuickFiler.Test.csproj` **immediately after** the existing line ``, which is currently line 119. The appended line must use **CRLF**, matching the rest of the file. The `Form1` compile region at `:161-166` and the `Form1.resx` `EmbeddedResource` at `:180-182` are owned exclusively by sibling child #491 and must not be touched. The line-119 placement rather than a tail append after line 158 is deliberate merge-conflict avoidance: line 158 sits within git's three-line merge context of the `Form1` region, whereas line 119 is 42 lines clear of it. `*.csproj` is listed in `.csharpierignore`, so csharpier will not reformat the file. **Acceptance:** the file grows from 484 to 485 lines; a diff of the project file shows exactly one added line adjacent to the `QfcDatamodelLivenessTests` entry and no change anywhere within lines 161 through 182 of the pre-change file. +- [ ] [P1-T3] Write the defect-2 regression test `OpenQFItem_WhenActiveExplorerChangesAfterConstruction_UsesTheConstructorCapturedExplorer` into the new file exactly as research §8.2 specifies. Use `SetupSequence` on `ActiveExplorer()` returning the captured Explorer first (consumed by the constructor at line 35) and the drifted Explorer second (what line 140 resolves today). Arrange the guard at lines 135-137 to be entered: the captured Explorer's `CurrentFolder` returns a folder whose `FolderPath` is the literal backslash-prefixed mailbox path A, and `mailItem.Parent` returns a folder whose `FolderPath` is the corresponding path B. Set `IsItemSelectableInView` to return `true` so the dialog branch is never reached. Construct with `QfEnums.InitTypeEnum.Find` so neither `HasFlag(Sort)` conjunct is true. Use `MockBehavior.Loose` for the drifted Explorer so the pre-fix failure surfaces as a FluentAssertions message rather than a Moq strict-mode exception. Assert **both** of: `capturedExplorer.VerifySet(e => e.CurrentFolder = destination.Object, Times.Once())` and `driftedExplorer.VerifySet(e => e.CurrentFolder = It.IsAny(), Times.Never())`. **Acceptance:** the named test method exists in the file, is decorated `[TestMethod]`, and contains both `VerifySet` assertions. +- [ ] [P1-T4] Format the new test file with csharpier scoped to that file: `dotnet tool run csharpier format QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`, then verify with `dotnet tool run csharpier check QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`. Scope the mutating pass to this plan's own path; do not run a repository-wide `format .` here. **Acceptance:** the scoped `check` invocation exits `0`. +- [ ] [P1-T5] Build the solution so the new test file compiles against the **unchanged** production code: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`. If the build reports CS0246 or a Moq setup-shape error, correct the test file only; no production file changes in this phase. **Acceptance:** the build exits `0` and the count of occurrences of `Skipping target "CoreCompile"` in the captured log is zero. +- [ ] [P1-T6] `[expect-fail]` Run only the defect-2 test and observe it **fail** against unfixed production code. Use the assembly discovery of [P0-T12] restricted to `QuickFiler.Test.dll`, with `/InIsolation` and a `/TestCaseFilter` naming `OpenQFItem_WhenActiveExplorerChangesAfterConstruction_UsesTheConstructorCapturedExplorer`. Write `/evidence/regression-testing/expect-fail-defect2..md` carrying `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode:` set to the non-zero value the runner reports for a failing test, `Output Summary:`, and the verbatim failure message showing which of the two `VerifySet` assertions fired first. **Acceptance:** the artifact exists, records the named test as failed, and quotes the assertion failure text. A passing result at this point means the arrangement does not reach line 140 and the test must be corrected before proceeding. + +### Phase 2 — Defect 2 fix + +- [ ] [P2-T1] Apply the D2 one-line fix at `QuickFiler/Controllers/QfcExplorerController.cs:140`, inside the private helper `NavigateToOutlookFolder(MailItem)` (lines 133-143): replace the assignment target `_globals.Ol.App.ActiveExplorer().CurrentFolder` with `_activeExplorer.CurrentFolder`, leaving the right-hand side `(MAPIFolder)mailItem.Parent` unchanged. No other line in the file changes in this phase. **Acceptance:** line 140 reads the `_activeExplorer` form and the file's line count is unchanged at 323. +- [ ] [P2-T2] Re-run the named test `OpenQFItem_WhenActiveExplorerChangesAfterConstruction_UsesTheConstructorCapturedExplorer` after a `/t:Rebuild` of the solution, using the same command as [P1-T6]. Write `/evidence/regression-testing/pass-after-defect2..md` carrying `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. **Acceptance:** `EXIT_CODE:` is `0`, the artifact records one test executed and one passed, and it cross-references the `[P1-T6]` fail-before artifact by filename. +- [ ] [P2-T3] Verify AC-4's residual-re-resolution condition. Run `git grep -n -F "ActiveExplorer()" -- QuickFiler/Controllers/QfcExplorerController.cs` and confirm it returns exactly one line, the constructor capture at line 35. Record the command and its full output in `/evidence/regression-testing/ac4-active-explorer-count..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. **Acceptance:** the output contains exactly one matching line and that line is the constructor assignment. + +### Phase 3 — Defect 1 contract removal + +- [ ] [P3-T1] Delete the line `void ExplConvView_Cleanup();` from `QuickFiler/Interfaces/IQfcExplorerController.cs:12`. No other line in that file changes. **Acceptance:** the file drops from 15 lines to 14 and the interface declares exactly five members: `BlShowInConversations { get; set; }`, `Task OpenQFItem(MailItem mailItem)`, `void ExplConvView_ToggleOff()`, `void ExplConvView_ToggleOn()`, and `void ExplConvView_ReturnState()`. +- [ ] [P3-T2] Delete lines 60 through 64 of `QuickFiler/Controllers/QfcExplorerController.cs` — the `//PRIORITY:` comment on line 60 and the four-line throwing implementation on lines 61 through 64 — plus the blank line that would otherwise be left doubled. Do not delete the unrelated `//PRIORITY:` comment at line 47 or the one at line 145. **Acceptance:** the file no longer declares a member of that name, and the `//PRIORITY:` comments concerning `BlShowInConversations` and `OpenQFItem` are still present. +- [ ] [P3-T3] Confirm that `QuickFiler/Notes/notes_interfaces.cs` is unmodified. It declares a duplicate `IQfcExplorerController` carrying the removed member at `:52-59`, but it is not compiled and is explicitly out of scope; it is intentionally left inconsistent with the compiled contract. **Acceptance:** a diff of that path against the merge-base SHA recorded in [P0-T7] is empty. +- [ ] [P3-T4] Run the analyzer build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`. The build is the gate for this phase: the interface has exactly one implementer, so the compiler enforces the paired edit. Write `/evidence/regression-testing/phase3-analyzer-build..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`. **Acceptance:** `EXIT_CODE:` is `0` and the count of occurrences of `Skipping target "CoreCompile"` in the captured log is zero. +- [ ] [P3-T5] Run the nullable build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`, without `/p:Nullable=enable`. Write `/evidence/regression-testing/phase3-nullable-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0`. `QfcExplorerController.cs` carries no `#nullable enable` pragma, so this gate imposes no new obligation on it; a failure here indicates a genuine compiler error from the removal. +- [ ] [P3-T6] Verify AC-1's search condition. Run `git grep -n -F "ExplConvView_Cleanup" -- "*.cs"` and confirm the remaining hits are only in the uncompiled `QuickFiler/Legacy/QuickFileController.cs` and `QuickFiler/Notes/notes_interfaces.cs`. Record the command and its full output in `/evidence/regression-testing/ac1-cleanup-references..md`. **Acceptance:** no hit resolves to a compiled file; specifically, no hit is in `QuickFiler/Interfaces/IQfcExplorerController.cs` or `QuickFiler/Controllers/QfcExplorerController.cs`. +- [ ] [P3-T7] Write the defect-1 fail-before-exception dossier at `/evidence/regression-testing/fail-before-exception.defect1..md` with the content specified in research §8.1. Required fields: `Timestamp:`, `Command:` (the search command that produced the absence proof), `EXIT_CODE:`, `WhyFailingRunImpossible:` (the remedy removes a member that no compiled production or test code calls, so there is no observable behaviour whose change a test could detect; a test asserting the member's absence would assert the non-existence of an API rather than a behaviour and would permanently block restoration), and the absence proof as `SearchScope:`, `SearchPatterns:`, and `SearchResult:`. The `SearchResult:` must enumerate all five pre-change hits with their file and line, marking the two `Legacy/` hits and the one `Notes/` hit as NOT COMPILED with the supporting fact that `QuickFiler/QuickFiler.csproj` contains zero `Compile Include` entries for either directory, and must record that no file under `QuickFiler.Test` references the member. **Acceptance:** the artifact exists at that path, carries all five required field labels, and its `Command:` reproduces the recorded `SearchResult:` when re-run against the merge-base tree. + +### Phase 4 — Defect 3 dead-region deletion and using-directive hygiene + +- [ ] [P4-T1] Delete lines 183 through 321 of `QuickFiler/Controllers/QfcExplorerController.cs` — the entire `#region Email Sorting To Rewrite`, with `#region` on line 183 and `#endregion` on line 321, 139 lines — including the six private/internal statics `SanitizeArrayLineTSV`, `StripTabsCrLf`, `WriteCSV_StartNewFileIfDoesNotExist`, `SanitizeArray`, `SaveMessageAsMSG`, `GetCurrentExplorerFolder`, and the commented-out `Cleanup_Files` block at lines 314-319. The two latent defects inside the block — the transposed `Path.Combine` arguments at line 223 and the write into a null `ref string[]` at line 259 — are **deleted, not fixed**; fixing unreachable code would be a change with no observable effect. **Acceptance:** the file no longer contains a `#region` directive, and its line count is approximately 184 (measure and record the actual value). +- [ ] [P4-T2] Remove nine orphaned `using` directives from `QuickFiler/Controllers/QfcExplorerController.cs`, per the D4 disposition table: lines 1 (`using System;`), 2 (`using System.Collections.Generic;`), 3 (`using System.Diagnostics;`), 5 (`using System.IO;`), 6 (`using System.Linq;`), 7 (`using System.Text;`), 8 (`using System.Text.RegularExpressions;`), 13 (`using ToDoModel;`), and 15 (`using UtilitiesCS.OutlookExtensions;`). Do **not** remove line 4 (`using System.Diagnostics.CodeAnalysis;`) in this phase — its only consumer, the class-level attribute, is still present and is removed in Phase 5. Retain lines 9, 10, 11, 12, 14, and 16. **Acceptance:** exactly seven `using` directives remain in the file at the end of this phase (the six permanent retentions plus the deferred line 4). +- [ ] [P4-T3] Record that the `using` removals are **hygiene, not a gate fix**, and state the reason: an orphaned `using` fails neither gate in this repository. `IDE0005`'s analyzer is not wired into these non-SDK projects (`QuickFiler/QuickFiler.csproj` references only Meziantou, Roslynator, AsyncFixer, `Microsoft.CodeAnalysis.BannedApiAnalyzers`, and `SonarAnalyzer.CSharp`); no `IDE0005` severity is configured in the repo-root `.editorconfig` and there is no `.globalconfig`; and `CS8019` is a hidden diagnostic that `/p:TreatWarningsAsErrors=true` does not promote. Three of the removed directives (lines 7, 13, 15) are already unused today on green `main`, which is direct empirical confirmation. Write this to `/evidence/other/d4-using-hygiene-rationale..md`. **Acceptance:** the artifact exists and states the hygiene classification, the three already-unused directives, and the self-verifying property described in [P4-T4]. +- [ ] [P4-T4] Run the analyzer build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`. This is the self-verifying gate for [P4-T2]: a directive that was in fact required fails the build with CS0246 or CS1061, in which case restore that specific directive and record the restoration in the [P4-T3] artifact. Write `/evidence/regression-testing/phase4-analyzer-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0`, the count of occurrences of `Skipping target "CoreCompile"` in the captured log is zero, and any restored directive is named in the artifact. +- [ ] [P4-T5] Run the nullable build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`, without `/p:Nullable=enable`. Write `/evidence/regression-testing/phase4-nullable-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0`. +- [ ] [P4-T6] Verify AC-6's search condition. Run `git grep -n -E "SanitizeArrayLineTSV|StripTabsCrLf|WriteCSV_StartNewFileIfDoesNotExist|SanitizeArray|SaveMessageAsMSG|GetCurrentExplorerFolder" -- QuickFiler QuickFiler.Test` and confirm it returns no match. Record the command, the exit code, and the empty output in `/evidence/regression-testing/ac6-dead-region-removed..md`. **Acceptance:** the search returns zero matching lines across both path scopes. A non-empty result means an identifier survived the deletion and must be removed before proceeding. +- [ ] [P4-T7] Write the defect-3 fail-before-exception dossier at `/evidence/regression-testing/fail-before-exception.defect3..md` with the content specified in research §8.3. Required fields: `Timestamp:`, `Command:`, `EXIT_CODE:`, `WhyFailingRunImpossible:` (the change deletes six private/internal statics that no compiled entry point can reach, so no test input can execute any of the deleted lines and there is no observable behaviour to assert before or after), and the absence proof as `SearchScope:`, `SearchPatterns:`, `SearchResult:`. The proof must record that every reference to the six identifiers outside the deleted region binds to an independent copy in `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs`, `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs`, or `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs`, and that no file under `QuickFiler.Test` references any of the six — including the `internal static StripTabsCrLf` that `[assembly: InternalsVisibleTo("QuickFiler.Test")]` at `QuickFiler/Properties/AssemblyInfo.cs:5` would otherwise expose. The dossier must also name the before/after full-suite comparison recorded under `evidence/qa-gates/` as the alternative proof of no behaviour change. **Acceptance:** the artifact exists at that path and carries all five required field labels plus the alternative-proof pointer. + +### Phase 5 — Coverage seam and attribute removal + +- [ ] [P5-T1] Remove the class-level `[ExcludeFromCodeCoverage]` attribute from `QuickFiler/Controllers/QfcExplorerController.cs`. After Phase 4 the file is renumbered, so locate it as the attribute immediately preceding the declaration `internal class QfcExplorerController : IQfcExplorerController`. The attribute is pre-existing (added 2026-06-13 in commit `a564add0d`) and is not introduced by this change. Under D5 it is removed rather than narrowed onto `OpenQFItem`, which overrides research §6.4: the seam introduced below makes both `OpenQFItem` branches testable, so narrowing would leave a testable member unmeasured. **Acceptance:** the attribute no longer precedes the class declaration. +- [ ] [P5-T2] Remove `using System.Diagnostics.CodeAnalysis;` — the tenth and final directive of the D4 disposition table, deferred from [P4-T2] because the attribute removed in [P5-T1] was its only consumer. **Acceptance:** exactly six `using` directives remain in the file: `System.Threading.Tasks`, `System.Windows.Forms`, `Microsoft.Office.Interop.Outlook`, `QuickFiler.Interfaces`, `UtilitiesCS`, and the `Outlook` alias. +- [ ] [P5-T3] Add the injectable modal-dialog seam to `QfcExplorerController` as an `internal` settable auto-property with a production default, following the repository's settable-delegate seam idiom demonstrated by `QfcHomeController.QfcExplorerControllerLoader` at `QuickFiler/Controllers/QfcHomeController.cs:175-182`. The member name is `NotInViewDialogInvoker`. Declare the delegate type **fully qualified** as `System.Func`, so the seam does not resurrect the `using System;` directive that D4 removed; this matches the file's existing fully-qualified style at `log4net.ILog` and `System.Reflection.MethodBase`. The default initialiser invokes `MessageBox.Show` with the four supplied arguments and returns its `DialogResult`. Add a short comment recording why the seam exists: the not-in-view branch calls a modal WinForms dialog that cannot be exercised in a headless unit test. **Acceptance:** the member `NotInViewDialogInvoker` is declared `internal` with both a getter and a setter and a default initialiser that calls `MessageBox.Show`. +- [ ] [P5-T4] Route the not-in-view dialog call through the seam. The call currently at line 168 assigns `DialogResult result = MessageBox.Show(...)` with the message `Selected message is not in view. Would you like to open it?`, the caption `Error`, `MessageBoxButtons.YesNo`, and `MessageBoxIcon.Error`. Change only the invocation target to `NotInViewDialogInvoker`, passing the same four arguments in the same order. The user-visible dialog text, buttons, and icon are unchanged; only the invocation route changes. `mailItem.Display()` at line 176 needs no seam, because `MailItem` is already mocked in this repository's tests. **Acceptance:** the not-in-view branch calls the seam, and the four argument values are byte-identical to the pre-change call. +- [ ] [P5-T5] Run the analyzer build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`. If it reports CS0246 for the fully-qualified `System.Func` form, restore `using System;` and record the restoration in the [P4-T3] artifact; that restoration is the self-verifying property D4 relies on and is not a plan failure. Write `/evidence/regression-testing/phase5-analyzer-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0` and the count of occurrences of `Skipping target "CoreCompile"` in the captured log is zero. +- [ ] [P5-T6] Run the nullable build `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`, without `/p:Nullable=enable`. Write `/evidence/regression-testing/phase5-nullable-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0`. +- [ ] [P5-T7] Verify AC-9. Run `git grep -n -F "ExcludeFromCodeCoverage" -- QuickFiler/Controllers/QfcExplorerController.cs` and confirm it returns no match — no attribute of that name remains anywhere in the file, at class level or member level. Record the command, exit code, and empty output in `/evidence/regression-testing/ac9-attribute-removed..md`. **Acceptance:** the search returns zero matching lines. +- [ ] [P5-T8] Verify the AC-10 search condition on the dialog call. Run `git grep -n -F "MessageBox.Show" -- QuickFiler/Controllers/QfcExplorerController.cs` and confirm it returns exactly one line, and that the line is inside the default initialiser of `NotInViewDialogInvoker` rather than in the body of `OpenQFItem`. Record the command and its full output in `/evidence/regression-testing/ac10-dialog-seam-route..md`. **Acceptance:** exactly one matching line is returned and it lies within the seam's default initialiser. +- [ ] [P5-T9] Add the test `OpenQFItem_WhenItemNotSelectableInView_InvokesDialogSeamOnce` to `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`. Arrange `IsItemSelectableInView` to return `false` so the not-in-view branch is taken, replace `NotInViewDialogInvoker` with a counting stub returning `DialogResult.No`, invoke `OpenQFItem` and await it, and assert the stub was invoked exactly once with the four expected argument values. The seam default must never be exercised, so no dialog is displayed. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and asserts an invocation count of exactly one. +- [ ] [P5-T10] Add the test `OpenQFItem_WhenDialogSeamReturnsYes_DisplaysMailItem`. Replace `NotInViewDialogInvoker` with a stub returning `DialogResult.Yes` and assert `mailItem.Verify(m => m.Display(It.IsAny()), Times.Once())` using the `Display` overload shape the PIA actually declares. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P5-T11] Add the test `OpenQFItem_WhenDialogSeamReturnsNo_DoesNotDisplayMailItem`. Replace `NotInViewDialogInvoker` with a stub returning `DialogResult.No` and assert `Display` is never invoked. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P5-T12] Format the test file with `dotnet tool run csharpier format QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`, then rebuild and run the three seam tests from [P5-T9] through [P5-T11] with `/InIsolation` and a `/TestCaseFilter` naming them. Write `/evidence/regression-testing/phase5-seam-tests..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` recording three executed and three passed. **Acceptance:** `EXIT_CODE:` is `0` and all three named tests pass. + +### Phase 6 — Remaining test set + +These are the characterisation and branch-coverage tests from research §5.5, tests 2 through 10. They pass without any further production change; they exist to raise measured coverage of the now-measured class and to characterise the behaviour that must not change. + +- [ ] [P6-T1] Confirm the `Views` indexer parameter type at compile time. Research §5.3 expects the PIA indexer parameter to be `object`, making the Moq setup `views.Setup(v => v[It.IsAny()]).Returns(view.Object)`. Write that setup, build, and if the compiler rejects it, adjust the parameter type only — it is a one-token change and cannot invalidate the mock harness. Record the confirmed type in `/evidence/other/views-indexer-parameter-type..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`. **Acceptance:** the artifact names the compiling parameter type and the build exits `0`. +- [ ] [P6-T2] Add the test `OpenQFItem_WhenMailIsAlreadyInTheCurrentFolder_DoesNotChangeCurrentFolder`, covering the defect-2 guard at lines 135-137 by giving the captured Explorer's `CurrentFolder` and `mailItem.Parent` the same `FolderPath`. Assert that `CurrentFolder` is never assigned on either Explorer. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T3] Add the test `OpenQFItem_WhenItemIsSelectableInView_ClearsAndAddsSelection`, covering the positive path at lines 156-159. Set `IsItemSelectableInView` to `true` and assert `ClearSelection` and `AddToSelection` are each invoked once. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T4] Add an in-test comment at the fixture recording the branch-control detail: constructing with `QfEnums.InitTypeEnum.Find` (value 2, per `QuickFiler/Helper Classes/QfEnums.cs:8`) makes `_initType.HasFlag(QfEnums.InitTypeEnum.Sort)` false at lines 151 and 179, but both conjunctions use the **non-short-circuiting** `&` operator, so `AutoFile.AreConversationsGrouped(_activeExplorer)` is still evaluated and the `CommandBars` mock setup therefore remains **mandatory** rather than optional. **Acceptance:** the comment is present in the test file and names the non-short-circuiting operator and the mandatory `CommandBars` setup. +- [ ] [P6-T5] Add the test `ExplConvView_ToggleOn_WhenFlagSet_AppliesRememberedView`, covering lines 123-131 using the `Views` indexer mock confirmed in [P6-T1]. Assert `View.Apply()` is invoked once and `BlShowInConversations` becomes `false`. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T6] Add the test `ExplConvView_ToggleOn_WhenFlagClear_DoesNothing`, covering the negative branch at line 125. Assert no `Views` access and no `Apply()` invocation. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T7] Add the test `ExplConvView_ToggleOff_WhenConversationsNotGrouped_DoesNothing`, covering the negative branch at line 74 by returning `false` from `GetPressedMso("ShowInConversations")`. Assert `BlShowInConversations` is unchanged and `CurrentView` is not read. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T8] Add the test `ExplConvView_ToggleOff_WhenSiblingViewMissing_CopiesAndSavesTemporaryView`, covering lines 95-103. Arrange the sibling lookup to find no view named `tmpNoConversation`, and assert `Copy`, the `XML` assignment, `Save`, and `Apply` occur in that arrangement. Set `Ol.ViewWide` on the globals mock, which this path reads at line 90. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T9] Add the test `GetSiblingView_WhenNamedViewPresent_ReturnsIt`, covering lines 108-121 using the `GetEnumerator` mocking precedent from `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs`. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T10] Add the test `GetSiblingView_WhenNamedViewAbsent_ReturnsNull`, covering the loop-exhausted path of lines 108-121. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T11] Add the test `CurrentConversationState_ReflectsCommandBarPressedState` as a `[DataTestMethod]` with two `[DataRow]` cases, `true` and `false`, covering the `internal` property at lines 55-58 reached through `[assembly: InternalsVisibleTo("QuickFiler.Test")]`. **Acceptance:** the named test exists, declares two data rows, and both cases pass. +- [ ] [P6-T12] Add the test `ExplConvView_ReturnState_WhenFlagSet_TogglesOn`, covering lines 66-70. **Acceptance:** the named test exists, is decorated `[TestMethod]`, and passes. +- [ ] [P6-T13] Do **not** add the optional reflection test `Contract_ExplConvView_Cleanup_IsNotDeclaredOnTheInterface` listed as item 11 in research §5.5. It asserts the absence of a member rather than a behaviour, encodes nothing, and would permanently block a future restoration; the D7 dossier written in [P3-T7] is the recorded substitute. Record this decision and its reason in `/evidence/other/d7-reflection-test-declined..md`. **Acceptance:** the artifact exists, states the decision, and no test of that name exists in the test file. +- [ ] [P6-T14] Verify the 500-line cap on the test file. Count the lines of `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`. If the count is at or above 500, split the conversation-view tests ([P6-T5] through [P6-T10]) into a second file `QuickFiler.Test/Controllers/QfcExplorerController.ConversationViewTests.cs` and append a second `` line in the same partitioned `Controllers` region immediately after the entry added by [P1-T2], again in CRLF and again without touching the `Form1` region. Record the measured count and whether the split was performed in `/evidence/other/test-file-size..md`. **Acceptance:** every test file in the diff measures under 500 lines and the artifact records each measured count. +- [ ] [P6-T15] Format the test file or files with csharpier scoped to those exact paths, rebuild, and run the whole `QfcExplorerControllerTests` class with `/InIsolation` and a `/TestCaseFilter` naming the class. Write `/evidence/regression-testing/phase6-class-run..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` giving executed, passed, failed, and skipped counts. **Acceptance:** `EXIT_CODE:` is `0`, zero tests failed, and zero tests were skipped. + +### Phase 7 — Final QC loop and evidence + +Every task in this phase executes its stated command unconditionally. `EXIT_CODE: SKIPPED` is not a passing outcome for any task here; no task text in this phase authorizes a skip branch. If any step fails or modifies a file, restart the loop from [P7-T2]. + +- [ ] [P7-T1] Final QC step 1. Run `dotnet tool restore` from WORKTREE. Write `/evidence/qa-gates/step1-dotnet-tool-restore..md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`. **Acceptance:** `EXIT_CODE:` is `0`. +- [ ] [P7-T2] Final QC step 2, apply. Run `dotnet tool run csharpier format .` from WORKTREE. Write `/evidence/qa-gates/step2a-csharpier-format..md` with the four required fields, and record in `Output Summary:` the list of files the formatter modified. If it modified any file outside this plan's declared path set, revert that file and re-run scoped to the plan's paths instead. **Acceptance:** `EXIT_CODE:` is `0` and every modified path is one this plan declares. +- [ ] [P7-T3] Final QC step 2, verify. Run `dotnet tool run csharpier check .` from WORKTREE. Write `/evidence/qa-gates/step2b-csharpier-check..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0` and zero files are reported as needing formatting. +- [ ] [P7-T4] Final QC step 3, analyzers. Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` with the console output captured to a log file. Write `/evidence/qa-gates/step3-analyzer-build..md` with the four required fields. **Acceptance:** `EXIT_CODE:` is `0`, the artifact records the warning and error counts, and it records that the count of occurrences of the string `Skipping target "CoreCompile"` in the captured log is **zero**, which is the evidence that analyzers actually ran. +- [ ] [P7-T5] Final QC step 4, nullable. Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` without `/p:Nullable=enable`. Write `/evidence/qa-gates/step4-nullable-build..md` with the four required fields, and state in `Output Summary:` that `/p:Nullable=enable` was not supplied and `/t:Rebuild` was used. **Acceptance:** `EXIT_CODE:` is `0`. +- [ ] [P7-T6] Final QC step 5, tests with coverage. Repeat the discovery and invocation of [P0-T12] verbatim, including the WORKTREE-relative `\.claude\` exclusion and `/InIsolation`. Write `/evidence/qa-gates/step5-vstest-coverage..md` with the four required fields. `Output Summary:` must record the executed, passed, failed, and skipped test counts and three **numeric** post-change coverage values: the repo-wide root Cobertura `line-rate` as a percentage, the `QuickFiler` package line rate as a percentage, and the `QfcExplorerController` figure aggregated across every `` element whose `filename` ends with the `QfcExplorerController.cs` path segment. **Acceptance:** `EXIT_CODE:` is `0`, zero tests failed, and all three coverage values are numeric. +- [ ] [P7-T7] Run the full suite a second time with the identical command from [P7-T6] and compare the pass set. Write `/evidence/qa-gates/step5-second-consecutive-run..md` with the four required fields. **Acceptance:** the two runs report the same executed and passed counts and the same set of failing tests (empty in both), which is the AC-13 determinism evidence. +- [ ] [P7-T8] Produce the AC-7 before/after full-suite comparison. Compare the baseline run recorded in `/evidence/baseline/step5-vstest-coverage..md` against the post-change run recorded in `/evidence/qa-gates/step5-vstest-coverage..md`, and write `/evidence/qa-gates/suite-comparison-before-after..md` naming both source artifacts, the two executed counts, the two passed counts, the delta, and the explicit list of newly added test names. **Acceptance:** the artifact shows the same set of passing tests in both runs with the tests added by this plan as the only additions and no new failures. A new failure is a stop condition; do not proceed to the acceptance-criteria check-offs. +- [ ] [P7-T9] Produce the coverage delta and threshold report at `/evidence/qa-gates/coverage-delta..md`. It must state, as numbers: the baseline repo-wide line rate and the post-change repo-wide line rate; the baseline `QuickFiler` package line rate and the post-change figure; the baseline `QfcExplorerController` value (recorded as absent from the report, per [P0-T13], because the class-level attribute suppressed it) and the post-change `QfcExplorerController` line rate now that the class is measured; and the changed-code coverage for the lines this change touched. It must also compare the post-change repo-wide figure against the 80% threshold enforced at `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-489`. **Acceptance:** the artifact carries every value listed above as a number, with the sole exception of the baseline class value, which is recorded as absent. +- [ ] [P7-T10] State the epic NFR outcome in `/evidence/qa-gates/coverage-delta..md`. The NFR is "Coverage of `QuickFiler.csproj` is retained or improved at every child merge." If the post-change `QuickFiler` package figure is at or above the baseline figure, record the NFR as met with both numbers. If it is below, record the shortfall as a number together with its reason — under D5 the class enters the coverage denominator for the first time, so a previously invisible file now contributes uncovered lines — and state explicitly that **no blanket class-level `[ExcludeFromCodeCoverage]` is restored**. **Acceptance:** the artifact records either the met statement with both numbers or the shortfall with its numeric size, its reason, and the no-restoration statement. Reporting the shortfall honestly is a passing outcome for this task; restoring the attribute is not. +- [ ] [P7-T11] Verify the AC-13 determinism prohibitions in the test file or files. Run `git grep -n -E "Thread.Sleep|Task.Delay|MessageBox.Show|Path.GetTempPath|new Form|Application.Run" -- QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs` and, if the [P6-T14] split was performed, against the second test file as well. Record the command, exit code, and empty output in `/evidence/qa-gates/ac13-determinism-scan..md`. **Acceptance:** the search returns zero matching lines in every test file added by this change. +- [ ] [P7-T12] Commit every source change and every evidence artifact produced by this plan on the feature branch, with a message naming issue #449. This commit is a prerequisite for the diff-based gate in [P7-T13]: a diff computed against the merge base with uncommitted work in the tree misses untracked new files, and the new test file is untracked until it is committed. **Acceptance:** `git status --porcelain` is empty except for paths this plan explicitly declares out of scope, and `git log -1` names issue #449. +- [ ] [P7-T13] Verify AC-16. Run `git diff --stat ..HEAD` and a line count of each changed file. Write `/evidence/qa-gates/ac16-file-size-cap..md` recording the diff stat, each changed file with its post-change line count, and the pre-emptive attribution statement that `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` and `QuickFiler/Legacy/QuickFileController.cs` are pre-existing 500-line-cap violations that this change does not edit. **Acceptance:** every file in the diff measures under 500 lines, and neither of the two named over-cap files appears in the diff stat. +- [ ] [P7-T14] Verify the AC-12 project-file condition. Run `git diff ..HEAD -- QuickFiler.Test/QuickFiler.Test.csproj` and confirm it shows only added `Compile Include` lines adjacent to the `QfcDatamodelLivenessTests` entry, with no change anywhere in the `Form1` compile region or the `Form1.resx` embedded-resource region owned by sibling child #491. Record the full diff in `/evidence/qa-gates/ac12-csproj-diff..md`. **Acceptance:** the diff contains only the added compile entry or entries and touches no line of the `Form1` regions. +- [ ] [P7-T15] Record in `/evidence/qa-gates/step5-vstest-coverage..md` that no Python toolchain step was run because none exists — there is no `scripts/dev_tools/` and no Poetry manifest — so any skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence, and that this is recorded rather than fabricated or silently omitted. **Acceptance:** the statement is present in the artifact with the verification command that established the absence. +- [ ] [P7-T16] Check off **AC-1** in `spec.md` per `.claude/skills/acceptance-criteria-tracking/SKILL.md`, citing `/evidence/regression-testing/ac1-cleanup-references..md` and the Phase 3 analyzer and nullable build artifacts as evidence. `spec.md` is the sole acceptance-criteria source under `full-bug`. **Acceptance:** exactly one checkbox changes state in this task, and the cited evidence artifact exists on disk. +- [ ] [P7-T17] Check off **AC-2** in `spec.md`, citing the section headed `## Removed contract — legacy semantics for future restoration` in `spec.md` itself. That section is authored content in the approved spec and requires no code change; verify it is present and contains the verbatim legacy body, the semantic summary, the modern member-equivalence table, the fallback implementation, and the catch-asymmetry analysis. **Acceptance:** exactly one checkbox changes state and the five listed elements are confirmed present. +- [ ] [P7-T18] Check off **AC-3** in `spec.md`, citing `/evidence/regression-testing/expect-fail-defect2..md` for the fail-before observation and `/evidence/regression-testing/pass-after-defect2..md` for the pass-after observation. **Acceptance:** exactly one checkbox changes state, and both cited artifacts exist and name the same test method. +- [ ] [P7-T19] Check off **AC-4** in `spec.md`, citing `/evidence/regression-testing/ac4-active-explorer-count..md`. **Acceptance:** exactly one checkbox changes state and the cited artifact records exactly one matching line. +- [ ] [P7-T20] Check off **AC-5** in `spec.md`, citing the Root Cause Analysis section and decision D2 in `spec.md`, which record that the defective call sits in the private helper `NavigateToOutlookFolder(MailItem)` reached from `OpenQFItem` rather than directly in `OpenQFItem`, that line 140 was the only re-resolution in the file, and that the issue criterion's alternative branch does not apply. **Acceptance:** exactly one checkbox changes state and all three statements are confirmed present in `spec.md`. +- [ ] [P7-T21] Check off **AC-6** in `spec.md`, citing `/evidence/regression-testing/ac6-dead-region-removed..md`. **Acceptance:** exactly one checkbox changes state and the cited artifact records a zero-match search result. +- [ ] [P7-T22] Check off **AC-7** in `spec.md`, citing `/evidence/qa-gates/suite-comparison-before-after..md`. **Acceptance:** exactly one checkbox changes state and the cited artifact names both run artifacts and states the comparison. +- [ ] [P7-T23] Check off **AC-8** in `spec.md`, citing the Phase 4 and Phase 5 analyzer and nullable build artifacts as the self-verifying proof that no removed directive was required, together with `/evidence/other/d4-using-hygiene-rationale..md`. Record in the check-off note that the AC prose says "nine directives" while enumerating ten line numbers, that the D4 table is authoritative with ten removals and six retentions, and that this plan removed nine in Phase 4 and the tenth in Phase 5. **Acceptance:** exactly one checkbox changes state and the reconciliation note is recorded. +- [ ] [P7-T24] Check off **AC-9** in `spec.md`, citing `/evidence/regression-testing/ac9-attribute-removed..md`. **Acceptance:** exactly one checkbox changes state and the cited artifact records a zero-match search result. +- [ ] [P7-T25] Check off **AC-10** in `spec.md`, citing `/evidence/regression-testing/ac10-dialog-seam-route..md` and `/evidence/regression-testing/phase5-seam-tests..md`. **Acceptance:** exactly one checkbox changes state, the seam-route artifact records exactly one matching line inside the seam default, and the seam-tests artifact records three passing tests. +- [ ] [P7-T26] Check off **AC-11** in `spec.md`, citing `/evidence/baseline/step5-vstest-coverage..md`, `/evidence/qa-gates/step5-vstest-coverage..md`, and `/evidence/qa-gates/coverage-delta..md`. Confirm that no evidence was written to `evidence/coverage/` or to any `artifacts/` sub-path other than `artifacts/orchestration/`, and that AC-9 still holds. **Acceptance:** exactly one checkbox changes state, all three cited artifacts exist, and the canonical-path confirmation is recorded. +- [ ] [P7-T27] Check off **AC-12** in `spec.md`, citing `/evidence/qa-gates/ac12-csproj-diff..md`. **Acceptance:** exactly one checkbox changes state and the cited diff shows no change within the `Form1` regions. +- [ ] [P7-T28] Check off **AC-13** in `spec.md`, citing `/evidence/qa-gates/ac13-determinism-scan..md` and `/evidence/qa-gates/step5-second-consecutive-run..md`. **Acceptance:** exactly one checkbox changes state, the scan artifact records zero matches, and the second-run artifact shows an identical pass set. +- [ ] [P7-T29] Check off **AC-14** in `spec.md`, citing `/evidence/regression-testing/fail-before-exception.defect1..md` and `/evidence/regression-testing/fail-before-exception.defect3..md`. Confirm each dossier carries `Timestamp:`, `Command:`, `EXIT_CODE:`, `WhyFailingRunImpossible:`, `SearchScope:`, `SearchPatterns:`, and `SearchResult:`, and that each `Command:` reproduces its recorded `SearchResult:`. **Acceptance:** exactly one checkbox changes state and both dossiers carry all seven field labels. +- [ ] [P7-T30] Check off **AC-15** in `spec.md`, citing the five Phase 7 toolchain artifacts `/evidence/qa-gates/step1-dotnet-tool-restore..md`, `step2a-csharpier-format`, `step2b-csharpier-check`, `step3-analyzer-build`, `step4-nullable-build`, and `step5-vstest-coverage`. Confirm the pass was a single uninterrupted loop with no file modified by a formatting step, that `/t:Rebuild` was used and `/t:Build` was not, that `/p:Nullable=enable` was absent, that `/InIsolation` was present, and that `\.claude\` was excluded from test-assembly discovery. **Acceptance:** exactly one checkbox changes state, all six artifacts exist with `EXIT_CODE:` `0`, and all five constraints are confirmed in the check-off note. +- [ ] [P7-T31] Check off **AC-16** in `spec.md`, citing `/evidence/qa-gates/ac16-file-size-cap..md`. **Acceptance:** exactly one checkbox changes state and the cited artifact shows every diffed file under 500 lines with neither over-cap file present. +- [ ] [P7-T32] Emit the acceptance-criteria status summary required by `.claude/skills/acceptance-criteria-tracking/SKILL.md`, covering all sixteen criteria with their final state and evidence pointer, and write it to `/evidence/qa-gates/ac-status-summary..md`. **Acceptance:** the summary lists AC-1 through AC-16, each with a state and at least one evidence path that exists on disk, and its counts agree with the checkbox state in `spec.md`. +- [ ] [P7-T33] Commit the `spec.md` check-off edits and the Phase 7 evidence artifacts added after [P7-T12]. **Acceptance:** `git status --porcelain` is empty and the working tree is clean. diff --git a/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/research/qfc-explorer-controller-defects.2026-08-21T18-20.md b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/research/qfc-explorer-controller-defects.2026-08-21T18-20.md new file mode 100644 index 000000000..0bbf2d5d0 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/research/qfc-explorer-controller-defects.2026-08-21T18-20.md @@ -0,0 +1,1039 @@ +# Research — QfcExplorerController latent defects (Issue #449) + +- **Timestamp:** 2026-08-21T18-20 +- **Issue:** #449 (`quickfiler-explorer-controller-latent-defects`) +- **Epic:** `quickfiler-suite-determinism-foundation` (wave 0, C3) +- **Worktree:** `C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a78a924c87d7f1f73` +- **Authoritative requirements:** `C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a78a924c87d7f1f73\docs\features\potential\promoted\2026-08-07-quickfiler-explorer-controller-latent-defects.md` +- **Mode:** research-only. No C# file, `.csproj`, or `.claude/**` file was modified. + +All line numbers in this document were re-derived by reading the files in this worktree, per the epic's +"Known-Stale Potential-Document References" constraint. + +--- + +## 0. Corrections and discrepancies found in supplied facts + +Every orchestrator-supplied fact was re-verified. All were confirmed. Three additions and two +discrepancies are recorded here. + +### 0.1 Confirmed as supplied + +| Supplied fact | Verification | +| --- | --- | +| `QfcExplorerController.cs` is 323 lines | Confirmed (line count = 323). | +| `[ExcludeFromCodeCoverage]` at line 20, `internal class ... : IQfcExplorerController` at line 21 | Confirmed. | +| `ExplConvView_Cleanup()` at lines 61-64 throws `NotImplementedException` (line 63) | Confirmed. | +| Declared on the interface at `IQfcExplorerController.cs:12` | Confirmed. | +| Second `ActiveExplorer()` at line 140, inside private `NavigateToOutlookFolder(MailItem)` (lines 133-143); `OpenQFItem` at 146-181 calls it at 149 | Confirmed. | +| Constructor captures `_activeExplorer` at line 35 | Confirmed. | +| `#region Email Sorting To Rewrite` spans 183-321 | Confirmed (`#region` at 183, `#endregion` at 321). | +| `QfcExplorerController` is the only implementer; every test reference is `Mock` | Confirmed. | +| `ExplConvView_Cleanup` has zero production callers; only other call site is `QuickFiler/Legacy/QuickFileController.cs:673` | Confirmed. | +| `QuickFileController.cs` and `Notes/notes_interfaces.cs` are not compiled | Confirmed, and broadened — see 0.2. | +| Legacy implementation at `QuickFileController.cs:851-869` | Confirmed exactly. | +| `Mock` and `Mock` are proven patterns | Confirmed; see 5.2 for the citation set. | +| `[assembly: InternalsVisibleTo("QuickFiler.Test")]` present | Confirmed at `QuickFiler/Properties/AssemblyInfo.cs:5`. | +| No `quality-tiers.yml`; no `scripts/dev_tools/`; no Poetry manifest | Confirmed. | + +### 0.2 Additions (broader than supplied) + +1. **No file under `QuickFiler/Legacy/` is compiled at all.** `QuickFiler/QuickFiler.csproj` contains + zero `` entries (grep for `Compile Include="Legacy` returns no match). + The supplied fact named two files; the correct statement is that the entire `Legacy/` folder plus + `Notes/` is out of the build. This strengthens the deletion-safety argument in §3 and removes any + doubt about `QfcGroupOperationsLegacy.cs` and `QfcController.cs`, which also reference + `ExplConvView_*` members. + +2. **The concrete class is constructed in two production sites**, not zero: + - `QuickFiler/Controllers/QfcHomeController.cs:182` — `new QfcExplorerController(initType, globals, homeController);` + - `QuickFiler/Controllers/EfcHomeControllerDependencyFactories.cs:155` — `return new QfcExplorerController(initType, globals, homeController);` + + Both go through a replaceable factory delegate, which is why every existing test binds + `Mock`. + +3. **The legacy type initialises `_objViewMem` in its constructor; the modern type does not.** + `QuickFileController.cs:145-147`: + ```csharp + BlShowInConversations = CurrentConversationState; + if (BlShowInConversations) + _objViewMem = ((Outlook.View)_activeExplorer.CurrentView).Name; + ``` + `QfcExplorerController`'s constructor (lines 27-37) sets only `_initType`, `_globals`, + `_activeExplorer`, `_parent`. This is load-bearing for Q1 and is analysed in §1.2. + +### 0.3 Discrepancies to record in the spec + +1. **Feature-folder name mismatch with the epic.** `docs/features/epics/quickfiler-suite-determinism-foundation/epic.md:33` + declares `feature_folder: 2026-08-21-quickfiler-explorer-controller-latent-defects-449`. The folder + that exists on disk — and the one this research artifact was written into per the orchestrator's + non-overridable path — is `docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449`. + No `2026-08-21-*` folder exists. The epic manifest and the on-disk folder disagree. Flagging, not + resolving: the epic file is outside this issue's scope, and the on-disk folder already carries + `issue.md`, `spec.md` and `plan.2026-08-21T18-09.md`. + +2. **The potential document's coverage-denominator claim is false.** It asserts that deleting the dead + region "removes roughly 139 lines of uncoverable filesystem-I/O code from the coverage denominator." + Because of the pre-existing class-level `[ExcludeFromCodeCoverage]`, those lines are already absent + from the denominator. Deletion changes the measured denominator by exactly zero. See §6.1. + +--- + +## 1. Q1 — `ExplConvView_Cleanup` contract decision + +### 1.1 The legacy body, verbatim (`QuickFiler/Legacy/QuickFileController.cs:851-869`) + +```csharp +public void ExplConvView_Cleanup() +{ + ObjView = _activeExplorer.CurrentFolder.Views[_objViewMem]; + try + { + ObjView.Apply(); + ObjViewTemp?.Delete(); + BlShowInConversations = false; + } + catch (System.Exception) + { + ObjViewTemp = GetSiblingView( + (Outlook.View)_activeExplorer.CurrentView, + "tmpNoConversation" + ); + + ObjViewTemp?.Delete(); + } +} +``` + +Its sole legacy call site is `QuickFileController.cs:667-680` (`ButtonCancel_Click`), guarded by +`if (BlShowInConversations)`. Semantically: on cancel, restore the remembered view and delete the +temporary `tmpNoConversation` view; on failure, best-effort locate and delete the temporary view. + +### 1.2 The four specific points raised + +**(a) `Views[_objViewMem]` sits outside the `try`. Is a null `_objViewMem` an uncaught throw hazard?** + +Yes, and in the modern type the hazard is strictly worse than in the legacy type. + +- `_objViewMem` is `private string` (`QfcExplorerController.cs:44`), default `null`. +- It is assigned in exactly one place in the modern type: `ExplConvView_ToggleOff()` at lines 88-90. +- The legacy type additionally assigns it in its constructor (`QuickFileController.cs:145-147`), so a + legacy instance created while conversations were grouped already had a non-null value before any + toggle ran. **The modern constructor does not do this.** +- The `Views` indexer parameter is `object` (production code passes a `string` at + `QfcExplorerController.cs:127` and `QuickFileController.cs:853, 926`). Passing `null` compiles. + What Outlook's COM implementation does with a null index cannot be determined statically and was + not verified — no live Outlook process is available in this environment. The plausible outcomes are + a `System.ArgumentException` or a `System.Runtime.InteropServices.COMException`; either is thrown + from outside the `try` and therefore propagates uncaught. + +Conclusion: a verbatim port would introduce a reachable `NullReferenceException`-class failure into a +public API on the very first call made before `ExplConvView_ToggleOff()`. Any port **must** guard +`_objViewMem` (and should move the resolution inside the protected region), which means the port is +not a port — it is a redesign of behaviour that has no caller. + +**(b) The `catch` does not set `BlShowInConversations = false` while the `try` path does. Intentional or defect?** + +The asymmetry is defensible as intentional and should **not** be "corrected" inside this issue. + +Reading that makes it intentional: the flag means "a conversation-view restore is still owed." On the +success path the restore happened, so the debt is cleared. On the failure path `ObjView.Apply()` did +not succeed, the explorer is still showing the temporary non-conversation view, and the debt stands. +Leaving the flag `true` keeps `ExplConvView_ReturnState()` (`QfcExplorerController.cs:66-70`) willing +to retry. + +Reading that makes it a defect: the retry runs `ExplConvView_ToggleOn()` +(`QfcExplorerController.cs:123-131`), whose first statement is the identical +`_activeExplorer.CurrentFolder.Views[_objViewMem]` resolution that just failed, so a retry is expected +to fail identically. The flag then leaks a permanently-true state. + +Recommendation: preserve the legacy asymmetry if the member is implemented, record the two readings in +an XML doc comment, and do not change it. Changing it is a behaviour change to a path with no caller, +which the Bugfix Workflow's "change only what is needed" rule forbids. Under the recommended decision +(§1.4) the question does not arise at all. + +**(c) `catch (System.Exception)` versus `.claude/rules/general-code-change.md`.** + +The rule text is: "Do not use broad catch-all handlers unless you immediately re-raise or propagate +with added context." `CLAUDE.md` C#4.1 repeats it: "Avoid catching broad `Exception` unless at a clear +boundary and with added context." The legacy body swallows silently and adds nothing, so a verbatim +port is a policy violation on its face. + +A policy-compliant shape for this body, given the try block is pure COM interop +(`View.Apply()`, `View.Delete()`) plus a missing-view-name lookup: + +```csharp +catch (System.Exception ex) when (ex is COMException || ex is ArgumentException) +{ + log.Warn( + $"Could not restore Outlook view '{_objViewMem}'; removing the temporary view instead.", + ex + ); + ObjViewTemp = GetSiblingView((Outlook.View)_activeExplorer.CurrentView, "tmpNoConversation"); + ObjViewTemp?.Delete(); +} +``` + +Two named exception types (`System.Runtime.InteropServices.COMException`, `System.ArgumentException`) +plus a log call satisfies "added context" and is not a catch-all. `COMException` requires adding +`using System.Runtime.InteropServices;` to the file's using block. Note the residual risk: an Outlook +PIA can also surface `System.UnauthorizedAccessException` and `System.InvalidCastException` from these +call paths, and neither would be caught by the narrowed filter, so narrowing changes runtime +behaviour relative to the legacy body. This is a further argument that "port the legacy semantics" is +not achievable without behaviour change. + +**(d) Does every piece the port needs exist on the modern type?** + +| Legacy member | Modern equivalent | Status | +| --- | --- | --- | +| `ObjView` (public field, `:42`) | `_objView` (private field, `:43`) | Present, renamed and narrowed. | +| `_objViewMem` (`:43`) | `_objViewMem` (`:44`) | Present. **Never initialised by the constructor** — see (a). | +| `ObjViewTemp` (public field, `:44`) | `ObjViewTemp` (public field, `:45`) | Present, identical. | +| `GetSiblingView(View, string)` (`:871-884`) | `GetSiblingView(View, string)` (`:108-121`) | Present, byte-identical body. | +| `BlShowInConversations` (`:185`) | `BlShowInConversations` (`:49-53`) | Present. | +| `_activeExplorer` (`:145` etc.) | `_activeExplorer` (`:42`) | Present. | +| `CurrentConversationState` (`:170`, private) | `CurrentConversationState` (`:55-58`, internal) | Present but **never referenced anywhere in the repository** — zero call sites in `QuickFiler` and zero in `QuickFiler.Test`. | + +Nothing the port needs is missing. The only gap is behavioural, not structural: constructor-time +initialisation of `_objViewMem`. + +**(e) Is there a logging pattern in place, and is `log` referenced?** + +Yes and no, respectively. + +`QfcExplorerController.cs:23-25` declares: +```csharp +private static readonly log4net.ILog log = log4net.LogManager.GetLogger( + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType +); +``` +A repository-scoped grep for `\blog\b` inside this file returns **exactly one hit — line 23, the +declaration itself.** The field is declared and never used. Any implementation added by this issue +should use it (that is the project's logging pattern and satisfies the "added context" requirement in +(c)); removing the member instead leaves the field unused, which is the status quo and produces no +new diagnostic (the field is `static readonly` with a method-call initialiser, so neither CS0169 nor +CS0414 applies). + +### 1.3 Rejected alternative — implement the legacy semantics + +Rejected. Summary of why, with the fallback implementation retained in §1.5 in case the planner or a +reviewer overrides this recommendation: + +- The body cannot be ported verbatim without violating `.claude/rules/general-code-change.md` (broad + catch) and without importing an uncaught null-index hazard that the legacy type mitigated in its + constructor and the modern type does not. +- Correcting both problems means authoring roughly 20 lines of new, previously-nonexistent production + behaviour for an API with zero callers, which the Bugfix Workflow explicitly scopes out ("change + only what is needed"; "if you uncover deeper design problems, open a new issue"). +- Those 20 lines then need tests, and the tests would assert behaviour no production path consumes. + +### 1.4 Recommendation — **remove the member** + +Remove `void ExplConvView_Cleanup();` from `QuickFiler/Interfaces/IQfcExplorerController.cs:12` and +remove the implementation and its `//PRIORITY:` comment from `QuickFiler/Controllers/QfcExplorerController.cs:60-64`. + +Evidence supporting removal: + +1. **Zero callers.** Repository-wide grep for `ExplConvView_Cleanup` across `*.cs` returns four hits: + `QuickFiler/Interfaces/IQfcExplorerController.cs:12` (declaration), + `QuickFiler/Controllers/QfcExplorerController.cs:61` (the throwing implementation), + `QuickFiler/Legacy/QuickFileController.cs:673` and `:851` (uncompiled), and + `QuickFiler/Notes/notes_interfaces.cs:58` (uncompiled duplicate interface). No compiled production + or test code calls it. +2. **Zero mock setups.** No file under `QuickFiler.Test` sets up or verifies `ExplConvView_Cleanup`, so + removing it from the interface breaks no `Mock`. +3. **Exactly one implementer**, so the "update all implementers" clause of the acceptance criterion is + a one-line edit. +4. **Policy alignment.** `CLAUDE.md` §4.2 ("Make the public surface area small and intentional") and + C#5.2 ("Keep public surface area intentional and minimal") both favour removal of an unimplemented, + uncalled member. The general policy's compatibility clause ("Avoid breaking public APIs. If a + breaking change is necessary, update all callers in-repo and call it out clearly") is satisfied: + there are no callers, and the change is called out here and in the PR body. +5. **It eliminates the trap rather than papering it.** The potential document's own framing is "a live + trap for the next caller." After removal a would-be caller gets a compile error at authoring time + instead of a `NotImplementedException` at runtime. + +Consequential edits required by removal: +- `QuickFiler/Controllers/QfcExplorerController.cs` — delete lines 60-64. +- `QuickFiler/Interfaces/IQfcExplorerController.cs` — delete line 12. +- Do **not** edit `QuickFiler/Notes/notes_interfaces.cs`. It is not compiled and is outside this + issue's file set; its duplicate `IQfcExplorerController` declaration at `:52-59` is a documentation + artefact. +- Removal makes `using System;` (line 1) orphaned — see §4. + +Knowledge preservation: the epic forbids any child writing under `docs/features/potential/**` +(Recorded Preconditions), so the legacy body must be preserved in this feature folder instead. Record +the verbatim legacy body and the semantic summary from §1.1 in the feature's `spec.md` under a +"Removed contract — legacy semantics for future restoration" heading, and reference it from the PR +body. Do not rely on the uncompiled `Legacy/` file as the record; it is a deletion candidate for a +later epic. + +### 1.5 Fallback implementation, if the decision is overridden to "implement" + +```csharp +/// +/// Restores the Outlook view remembered by and removes the +/// temporary "tmpNoConversation" view. On failure the temporary view is still removed, but +/// is deliberately left set: the restore did not happen, so +/// the caller still owes one. This asymmetry is inherited from the legacy implementation. +/// +public void ExplConvView_Cleanup() +{ + if (string.IsNullOrEmpty(_objViewMem)) + { + // No view was remembered, so there is nothing to restore. Guarding here rather than + // letting the Views indexer throw: the legacy type initialised _objViewMem in its + // constructor and this type does not, so a null value is reachable on the first call. + ObjViewTemp?.Delete(); + BlShowInConversations = false; + return; + } + + try + { + _objView = _activeExplorer.CurrentFolder.Views[_objViewMem]; + _objView.Apply(); + ObjViewTemp?.Delete(); + BlShowInConversations = false; + } + catch (System.Exception ex) when (ex is COMException || ex is ArgumentException) + { + log.Warn($"Could not restore Outlook view '{_objViewMem}'.", ex); + ObjViewTemp = GetSiblingView((Outlook.View)_activeExplorer.CurrentView, "tmpNoConversation"); + ObjViewTemp?.Delete(); + } +} +``` +Requires adding `using System.Runtime.InteropServices;`. Note that this is not the legacy behaviour: +the guard is new, the exception filter is narrower, and the resolution moved inside the `try`. + +--- + +## 2. Q2 — Defect 2 remedy + +### 2.1 The remedy + +At `QuickFiler/Controllers/QfcExplorerController.cs:140`, replace + +```csharp +_globals.Ol.App.ActiveExplorer().CurrentFolder = (MAPIFolder)mailItem.Parent; +``` + +with + +```csharp +_activeExplorer.CurrentFolder = (MAPIFolder)mailItem.Parent; +``` + +Confirmed as the correct and complete remedy. It is a one-line change inside the private helper +`NavigateToOutlookFolder(MailItem)` (lines 133-143). + +### 2.2 Every other `_globals` use in the file + +Repository grep for `_globals` within `QfcExplorerController.cs` returns six hits: + +| Line | Use | Assessment | +| --- | --- | --- | +| 34 | `_globals = appGlobals;` | Constructor assignment. Unchanged. | +| 35 | `_activeExplorer = _globals.Ol.App.ActiveExplorer();` | The single authoritative capture. Unchanged. | +| 40 | `private IApplicationGlobals _globals;` | Field declaration. Unchanged. | +| 90 | `_objViewMem = _globals.Ol.ViewWide;` | Reads a settings string, not a COM re-resolution. Unchanged. | +| 140 | `_globals.Ol.App.ActiveExplorer().CurrentFolder = ...` | **The defect.** | +| 162 | `//MAPIFolder drafts = _globals.Ol.NamespaceMAPI...` | Commented out. Unchanged. | + +**Line 140 is the only re-resolution in the file.** No other member re-derives the explorer. + +### 2.3 Is there a behavioural dependency on the fresh call? + +No. Analysis: + +- `_activeExplorer` is assigned once, at line 35, and is never reassigned anywhere in the file (no + other `_activeExplorer =` occurrence exists). +- Nothing in `QfcExplorerController` subscribes to Outlook explorer lifecycle events, and no public + member accepts a replacement explorer. +- Every other COM operation in the type — `CurrentConversationState` (line 57), + `ExplConvView_ToggleOff` (74, 77, 81), `ExplConvView_ToggleOn` (127), + `NavigateToOutlookFolder`'s own guard (line 136), `AutoFile.AreConversationsGrouped` (141, 152), + `IsItemSelectableInView`/`ClearSelection`/`AddToSelection` (156, 158, 159) — already uses + `_activeExplorer`. +- Line 136 reads `_activeExplorer.CurrentFolder.FolderPath` and line 140 writes + `ActiveExplorer().CurrentFolder`. As written, the guard and the assignment can address **different + Explorer objects**, which is exactly the internal-inconsistency hazard the potential document + describes. The fix makes read and write address the same object, which is the stronger correctness + argument, ahead of the saved COM round-trip. + +There is therefore no code path requiring the fresh call, and no in-code documentation of one is +needed. The acceptance criterion's alternative branch ("or the reason a fresh `ActiveExplorer()` call +is required is documented in code") does not apply. + +--- + +## 3. Q3 — Defect 3 deletion safety + +Repository-wide grep over `*.cs` for the six identifiers. Results split by location. + +### 3.1 References inside `QuickFiler/Controllers/QfcExplorerController.cs` + +All hits fall inside the region 183-321, with none outside it. + +| Symbol | Declaration | In-file call sites | +| --- | --- | --- | +| `SanitizeArrayLineTSV` | 185 (`private static`) | none | +| `StripTabsCrLf` | 203 (`internal static`) | 193, 264 | +| `WriteCSV_StartNewFileIfDoesNotExist` | 216 (`private static`) | none (comment at 215) | +| `SanitizeArray` | 249 (`private static`) | 241 | +| `SaveMessageAsMSG` | 272 (`private static`) | none (comment at 271) | +| `GetCurrentExplorerFolder` | 278 (`private static`) | none (comment at 277) | + +Three of the six (`SanitizeArrayLineTSV`, `SaveMessageAsMSG`, `GetCurrentExplorerFolder`) have **zero** +call sites even inside the region. Two (`WriteCSV_StartNewFileIfDoesNotExist` is the only entry point, +and it is itself uncalled) form a closed island. + +### 3.2 References outside `QfcExplorerController.cs` + +Every external hit binds to a different type. Grouped by owning type: + +- `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` — declares its own + `SaveMessageAsMSG` (`:1092`, `internal static`, different signature: `(MailItem, string)`), + `SanitizeArrayLineTSV` (`:1344`), `StripTabsCrLf` (`:1361`), + `WriteCSV_StartNewFileIfDoesNotExist` (`:1374`, `public static`), `SanitizeArray` (`:1407`). + Internal call sites at `:491`, `:1338`, `:1353`, `:1399`, `:1420`. +- `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs` — declares instance copies: + `SanitizeArrayLineTSV` (`:211`, `private`), `StripTabsCrLf` (`:224`, `internal`). Call sites at + `:196`, `:218`. +- `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs` — declares its own copies of all six: + `SanitizeArrayLineTSV` (`:255`), `StripTabsCrLf` (`:273`), + `WriteCSV_StartNewFileIfDoesNotExist` (`:285`), `SanitizeArray` (`:317`), `SaveMessageAsMSG` (`:350`), + `GetCurrentExplorerFolder` (`:355`). Call sites at `:77`, `:84`, `:125`, `:223`, `:229`, `:263`, + `:310`, `:330`. +- `TaskMaster/AppGlobals/AppOlObjects.cs:279` — calls `SortEmail.WriteCSV_StartNewFileIfDoesNotExist(...)`, + explicitly type-qualified to `SortEmail`. + +None of these resolve to a `QfcExplorerController` member. `QfcExplorerController` is `internal` to the +`QuickFiler` assembly, and none of `UtilitiesCS`, `ToDoModel`, or `TaskMaster` references `QuickFiler` +for these symbols; the type qualification at `AppOlObjects.cs:279` removes any residual ambiguity. + +### 3.3 `StripTabsCrLf` — the `internal static` case, explicitly + +`StripTabsCrLf` at `QfcExplorerController.cs:203` is `internal static`, so it is reachable from +anywhere in the `QuickFiler` assembly and, via `[assembly: InternalsVisibleTo("QuickFiler.Test")]` +(`QuickFiler/Properties/AssemblyInfo.cs:5`), from `QuickFiler.Test`. + +**No file under `QuickFiler.Test` references any of the six symbols.** The only test references in the +repository are: +- `UtilitiesCS.Test/EmailIntelligence/SortEmail_Tests.cs:141, 147, 158, 164, 277, 295, 302, 307, 320, 324` — all against `SortEmail.*`. +- `UtilitiesCS.Test/EmailIntelligence/EmailFiler_Tests.cs:43, 47` — against an `EmailFiler` instance. + +Additional hits appear in `docs/features/archive/.../vstest-final.txt` and in the potential document +itself; both are text artefacts, not code. + +**Conclusion: the deletion plan is unchanged. No test edit is required and no `QuickFiler` production +file other than `QfcExplorerController.cs` is affected.** Deleting lines 183-321 (139 lines) is +behaviour-neutral for the `QuickFiler` assembly, and takes the file from 323 to approximately 184 +lines. + +### 3.4 The two latent defects inside the block + +Both confirmed, both unreachable, both disappear with the deletion: +- `WriteCSV_StartNewFileIfDoesNotExist` line 223 — `File.Exists(Path.Combine(strFileName, strFileLocation))` + transposes the arguments relative to line 242's `FileIO2.WriteTextFile(strFileName, strOutput, folderpath: strFileLocation)`. +- `SanitizeArray` line 221/241/259 — `strOutput` is initialised to `null` at line 221 and passed + `ref` into `SanitizeArray`, which writes `strOutput[j]` at line 259 without allocating. This throws + `NullReferenceException` if reached. + +Neither can fire today. Do not "fix" them; delete them. + +--- + +## 4. Q4 — Orphaned `using` directives + +### 4.1 Per-directive determination + +The using block is lines 1-16. The determination below enumerates every type reference in lines 1-182 +and, separately, the effect of removing `ExplConvView_Cleanup` (§1.4). + +| Line | Directive | Required by lines 1-182? | Verdict after region deletion | +| --- | --- | --- | --- | +| 1 | `using System;` | `NotImplementedException` at line 63 **only**. `System.Reflection.MethodBase` at line 24 is fully qualified; `log4net.ILog` at 23 is fully qualified. | **Retained** if `ExplConvView_Cleanup` is implemented (§1.5) or kept. **ORPHANED** under the recommended removal (§1.4), because line 63 is the last `System`-namespace reference. | +| 2 | `using System.Collections.Generic;` | No. Only use is `IList` in `SaveMessageAsMSG` (line 272). The `foreach` at line 112 needs no using. | **ORPHANED** | +| 3 | `using System.Diagnostics;` | No. Only use is `Debug.WriteLine` (line 253). | **ORPHANED** | +| 4 | `using System.Diagnostics.CodeAnalysis;` | Yes — `[ExcludeFromCodeCoverage]` at line 20. | **Retained** if the attribute stays or is narrowed (§6.3). Orphaned only if the attribute is removed outright. | +| 5 | `using System.IO;` | No. Only uses are `File.Exists` and `Path.Combine` (line 223). | **ORPHANED** | +| 6 | `using System.Linq;` | No. Only uses are `.Where`/`.Select`/`.ToArray` at 192-194 and 263-265. | **ORPHANED** | +| 7 | `using System.Text;` | No — and no use anywhere in the file, including the dead region. No `StringBuilder`, no `Encoding`. | **Already orphaned before this change** (pre-existing). | +| 8 | `using System.Text.RegularExpressions;` | No. Only use is `Regex` at lines 205 and 209. | **ORPHANED** | +| 9 | `using System.Threading.Tasks;` | Yes — `Task` at 146, 154, 158, 159, 180. | **Retained** | +| 10 | `using System.Windows.Forms;` | Yes — `DialogResult` (168), `MessageBox` (168), `MessageBoxButtons` (171), `MessageBoxIcon` (172). | **Retained** | +| 11 | `using Microsoft.Office.Interop.Outlook;` | Yes — `Explorer` (42), `MailItem` (133, 146), `MAPIFolder` (136, 140), `Views` (111), `OlViewSaveOption` (99). | **Retained** | +| 12 | `using QuickFiler.Interfaces;` | Yes — `IQfcExplorerController` (21), `IFilerHomeController` (30, 41). | **Retained** | +| 13 | `using ToDoModel;` | No — see §4.2. | **Already orphaned before this change** (pre-existing). | +| 14 | `using UtilitiesCS;` | Yes — `IApplicationGlobals` (29, 40), `AutoFile` (141, 152). | **Retained** | +| 15 | `using UtilitiesCS.OutlookExtensions;` | No — see §4.2. | **Already orphaned before this change** (pre-existing). | +| 16 | `using Outlook = Microsoft.Office.Interop.Outlook;` | Yes — `Outlook.View` at 43, 45, 77, 93, 97, 101, 108, 110, 112, 123. | **Retained** | + +### 4.2 Namespace resolution for the specific symbols asked about + +| Symbol | Declaring namespace | Which directive supplies it | +| --- | --- | --- | +| `QfEnums` | `QuickFiler` (`QuickFiler/Helper Classes/QfEnums.cs:1-3`) | **None.** The file's namespace is `QuickFiler.Controllers`, so `QuickFiler` is reachable by enclosing-namespace lookup. `using ToDoModel;` does **not** supply it. | +| `AutoFile` | `UtilitiesCS` (`UtilitiesCS/EmailIntelligence/EmailParsingSorting/AutoFile.cs:11-13`) | `using UtilitiesCS;` | +| `IApplicationGlobals` | `UtilitiesCS` (`UtilitiesCS/Interfaces/IGlobals/IApplicationGlobals.cs:5-7`) | `using UtilitiesCS;` | +| `IFilerHomeController` | `QuickFiler.Interfaces` (`QuickFiler/Interfaces/IFilerHomeController.cs:9-11`) | `using QuickFiler.Interfaces;` | +| `GetPressedMso` | **Not an extension method.** It is a native member of `Microsoft.Office.Core.CommandBars`. Proof: `UtilitiesCS.Test/EmailIntelligence/AutoFile_Tests.cs:57` does `mockCommandBars.Setup(cb => cb.GetPressedMso("ShowInConversations"))` — Moq can only `Setup` an interface or virtual member, never an extension method. | **None.** Member access off a returned value requires no using. | +| `IsItemSelectableInView` | **Not an extension method.** Native member of `Microsoft.Office.Interop.Outlook.Explorer`. Proof: `TaskTree.Test/TaskTreeControllerActivateTests.cs:57` does `explorer.Setup(e => e.IsItemSelectableInView(It.IsAny()))`. | **None.** | +| `IsInitialized` | `UtilitiesCS` — `UtilitiesCS/Extensions/ArrayExtensions.cs:9` (namespace) / `:193` (the `T[]` overload used at line 187). | `using UtilitiesCS;` — **not** `UtilitiesCS.OutlookExtensions`. | +| `SliceRow` | `UtilitiesCS` — `UtilitiesCS/Extensions/ArrayExtensions.cs:102`. | `using UtilitiesCS;` | +| `FileIO2` (line 242) | `UtilitiesCS` — `UtilitiesCS/To Depricate/FileIO2.cs:12`. | `using UtilitiesCS;` | + +This is why `using ToDoModel;` and `using UtilitiesCS.OutlookExtensions;` are already unused today: +the two extension methods the region uses live in the root `UtilitiesCS` namespace, and the two +Outlook members that look like extensions are native PIA members. + +**`System.Text` is already unused before the deletion.** So are `ToDoModel` and +`UtilitiesCS.OutlookExtensions`. Three of the ten directives listed in the question are pre-existing +orphans; five more become orphans as a consequence of the deletion; `System` becomes an orphan only +under the recommended Q1 decision; `System.Diagnostics.CodeAnalysis` depends on the Q6 decision. + +**Caveat:** this determination is symbol-level, not compiler-verified. Extension-method resolution can +surprise. The removal is self-verifying, though: if any directive is actually required, the analyzer +build fails with CS0246/CS1061 and the executor restores it. Removal is therefore low-risk; retention +is zero-risk. + +### 4.3 Is an unused `using` enforced by the build here? + +**No. Leaving an orphaned using would not fail either gate. Removal is hygiene, not a gate requirement.** + +Evidence: + +1. **No `IDE0005` severity is configured.** Grep of the repo-root `.editorconfig` for `IDE0` returns + zero hits. There is no `.globalconfig` in the repository. +2. **The analyzer that produces `IDE0005` is not wired into this project.** `QuickFiler/QuickFiler.csproj` + is a legacy non-SDK project (`` + at line 4, `v4.8.1` at line 13, + `` at line 567). Its + `` set (lines 582-591) is Meziantou, Roslynator, AsyncFixer, + `Microsoft.CodeAnalysis.BannedApiAnalyzers`, and `SonarAnalyzer.CSharp`. Neither + `Microsoft.CodeAnalysis.NetAnalyzers` nor `Microsoft.CodeAnalysis.CSharp.CodeStyle` — the packages + that carry `IDE0005` — is referenced. The command-line `/p:EnableNETAnalyzers=true` + `/p:EnforceCodeStyleInBuild=true` properties are SDK-project properties and do not inject an + analyzer into a non-SDK project. +3. **The compiler's own `CS8019` ("unnecessary using directive") is a hidden diagnostic**, not a + warning, so `/p:TreatWarningsAsErrors=true` does not promote it. +4. **Empirical confirmation.** `using System.Text;`, `using ToDoModel;` and + `using UtilitiesCS.OutlookExtensions;` are unused in this file *today*, on `main`, and both the + analyzer gate and the nullable gate are green there. If any wired analyzer (for example Sonar + `S1128`, which is absent from the `.editorconfig` severity list and therefore keeps its package + default) reported unused usings at `warning` severity, the type-check gate would already be red. + +Recommendation: remove the eight-to-ten orphaned directives anyway. `CLAUDE.md` C#5.3 ("Prefer +explicit `using` directives at file scope") and the general policy's "Prefer clear, explicit imports" +both favour it, `csharpier` will not reorder or remove them for you, and doing it in the same commit +as the region deletion keeps the diff coherent. State in the PR body that this is hygiene, not a gate +fix, so a reviewer does not read it as an unrelated refactor. + +--- + +## 5. Q5 — Test harness design + +### 5.1 File, class, and project-file entry + +- **Path:** `C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a78a924c87d7f1f73\QuickFiler.Test\Controllers\QfcExplorerControllerTests.cs` +- **Class:** `QfcExplorerControllerTests` +- **Namespace:** `QuickFiler.Controllers.Tests` — matches `QuickFiler.Test/Controllers/QfcHomeControllerTests.cs:20`. + +Recommended without reservation. Verified no collision: no file under `QuickFiler.Test/Controllers/` +matches `*Explorer*`; the closest names are `EfcHomeController*Tests.cs`, none of which conflicts. + +**Project-file entry.** `QuickFiler.Test/QuickFiler.Test.csproj` needs one appended line. The +`Controllers` compile entries run from `:58` to `:158`, and `:158` is the last of them +(``). Append immediately after line 158: + +```xml + +``` + +This satisfies the epic's Shared-Surface Coordination partition exactly: it does not touch the `Form1` +region at `:161-166` (owned by #491) nor the `Form1.resx` `EmbeddedResource` at `:180-182`. The file is +currently 484 lines; the append makes it 485, still under the 500-line cap. Note for coordination: +#491's removal of the `Form1` entries reduces it by 8 lines, so the two children move it in opposite +directions with net headroom. + +### 5.2 The constructor's mock graph + +`QfcExplorerController(QfEnums.InitTypeEnum, IApplicationGlobals, IFilerHomeController)` reaches COM at +line 35 only: `_globals.Ol.App.ActiveExplorer()`. + +Chain, with the declaring definitions: +- `UtilitiesCS/Interfaces/IGlobals/IApplicationGlobals.cs:11` — `IOlObjects Ol { get; }` +- `UtilitiesCS/Interfaces/IGlobals/IOlObjects.cs:13` — `Application App { get; }` + (`Microsoft.Office.Interop.Outlook.Application`) +- `UtilitiesCS/Interfaces/IGlobals/IOlObjects.cs:28` — `string ViewWide { get; }` (needed by + `ExplConvView_ToggleOff` at line 90) +- `QuickFiler/Interfaces/IFilerHomeController.cs:31` — `IFilerFormController FormController { get; }` +- `QuickFiler/Interfaces/IFilerFormController.cs:17` — `void MinimizeFormViewer();` + +**Existing test that already builds this exact chain:** `QuickFiler.Test/Controllers/QfcHomeControllerTests.cs:39-47`, +using a `MockRepository(MockBehavior.Strict)` and Moq's recursive `SetupGet(x => x.Ol.App)`: + +```csharp +this._mockRepository = new MockRepository(MockBehavior.Strict); +this._mockApplicationGlobals = this._mockRepository.Create(); +this._mockOlApp = this._mockRepository.Create(); +this._mockExplorer = this._mockRepository.Create(); +this._mockOlApp.Setup(x => x.ActiveExplorer()).Returns(_mockExplorer.Object); +this._mockApplicationGlobals.SetupGet(x => x.Ol.App).Returns(_mockOlApp.Object); +``` + +A second precedent for the same chain shape is `QuickFiler.Test/Controllers/QfcHomeControllerPropertyTests.cs:54` +and `QfcHomeControllerIssue218Tests.cs:36`. + +Minimal fixture for this issue: + +```csharp +var repo = new MockRepository(MockBehavior.Loose); + +var commandBars = repo.Create(); +commandBars.Setup(c => c.GetPressedMso("ShowInConversations")).Returns(false); + +var explorer = repo.Create(); +explorer.Setup(e => e.CommandBars).Returns(commandBars.Object); + +var olApp = repo.Create(); +olApp.Setup(a => a.ActiveExplorer()).Returns(explorer.Object); + +var globals = repo.Create(); +globals.SetupGet(g => g.Ol.App).Returns(olApp.Object); +globals.SetupGet(g => g.Ol.ViewWide).Returns("Wide"); // only for ToggleOff tests + +var formController = repo.Create(); +var parent = repo.Create(); +parent.SetupGet(p => p.FormController).Returns(formController.Object); + +var controller = new QfcExplorerController( + QfEnums.InitTypeEnum.Find, // deliberately NOT Sort — see 5.4 + globals.Object, + parent.Object +); +``` + +Assembly references are already present: `QuickFiler.Test/QuickFiler.Test.csproj:278-280` references +`Microsoft.Office.Interop.Outlook` and `:326-328` references `office` (the `Microsoft.Office.Core` +PIA), both with `False`, which is what Moq requires. +`Mock` is proven at `UtilitiesCS.Test/EmailIntelligence/AutoFile_Tests.cs:56-59`. + +### 5.3 Mockability audit of the specific members asked about + +| Member | Declaring type | Interface member? | Mockable? | Evidence | +| --- | --- | --- | --- | --- | +| `Explorer.CurrentFolder` (get and set) | `Microsoft.Office.Interop.Outlook.Explorer` | Yes | **Yes** | Production assigns it at `QfcExplorerController.cs:140`, so the setter exists. `Mock` proven at `QuickFiler.Test/Controllers/QfcDatamodelTests.cs:259` and `UtilitiesCS.Test/OutlookObjects/Table/OlTableExtensions_Tests.cs:529`. `VerifySet` is the natural assertion for defect 2. | +| `MAPIFolder.Views` | `MAPIFolder` | Yes | **Yes** | `Mock` proven at `TaskMaster.Test/Ribbon/RibbonControllerTests.cs:365` and `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs:57`. | +| `Views` indexer | `Views` | Yes (C# indexer; production binds it at `QfcExplorerController.cs:127` and `QuickFileController.cs:853, 926`) | **Yes, with one confirmation step** | No `Mock` exists in the repo yet. Indexer mocking is proven on another Outlook collection at `QuickFiler.Test/Helper Classes/MailItemInfoTests.cs:64-65` (`mockRecipients.Setup(x => x[It.IsAny()])`). **Confirm at implementation time** that the PIA indexer parameter is `object` rather than a typed overload; the setup is then `views.Setup(v => v[It.IsAny()]).Returns(view.Object)`. If the compiler rejects that shape, the parameter type is the only thing to adjust. | +| `View.Apply()` | `Outlook.View` | Yes | **Yes** | `Mock` proven at `UtilitiesCS.Test/OutlookObjects/Table/OlTableExtensions_Tests.cs:530-531` (sets `v.Name`). `Apply()`, `Save()`, `Copy(string, OlViewSaveOption)`, `XML`, `Parent` are on the same interface. | +| `View.Delete()` | `Outlook.View` | Yes | **Yes** | Same interface. Only needed if Q1 is decided as "implement". | +| `Views` enumeration (for `GetSiblingView`, line 112) | `Views : IEnumerable` | Yes | **Yes** | Two proven forms in-repo: direct, `mockUDPs.Setup(u => u.GetEnumerator()).Returns(list.GetEnumerator())` at `UtilitiesCS.Test/Extensions/DfDeedle_COM_Tests.cs:55` and `:838`; and the `.As()` fallback at `:581-583`. | +| `Explorer.CurrentView` | `Explorer` | Yes, returns `object` | **Yes** | `mockExplorer.Setup(e => e.CurrentView).Returns(mockView.Object)` at `OlTableExtensions_Tests.cs:532`. | +| `Explorer.CommandBars` / `CommandBars.GetPressedMso` | `Explorer` / `Microsoft.Office.Core.CommandBars` | Yes | **Yes** | `AutoFile_Tests.cs:56-59`. | +| `Explorer.IsItemSelectableInView`, `ClearSelection`, `AddToSelection` | `Explorer` | Yes | **Yes** | `TaskTree.Test/TaskTreeControllerActivateTests.cs:57` for the first. | +| `MailItem.Parent` | `MailItem` | Yes, returns `object` | **Yes** | Cast to `MAPIFolder` at lines 136, 140. | + +**Nothing in the changed paths is unmockable.** No sealed class, no static, no non-virtual concrete +member stands in the way. This is the same evidence that undercuts the coverage exemption in §6.2. + +### 5.4 Untestable members, and how to avoid them + +The tests must not create a live form, must not start a message pump, must not use temporary files, +and must not call `MessageBox.Show`. Assessment: + +- **`OpenQFItem`'s else branch, lines 166-178**, is the only truly untestable region. Line 168 calls + `MessageBox.Show(...)` — a modal WinForms dialog — and line 176 calls `mailItem.Display()`. This + branch **must be left uncovered**. It is reached only when `_activeExplorer.IsItemSelectableInView(mailItem)` + returns `false` (line 156). +- **Everything else in `OpenQFItem` is testable**, because the branch is selectable: set + `explorer.Setup(e => e.IsItemSelectableInView(It.IsAny())).Returns(true)` and the method + takes the `ClearSelection`/`AddToSelection` path (158-159) instead. +- **`_parent.FormController.MinimizeFormViewer()` (line 148) is not a barrier.** `IFilerFormController` + is an interface (`QuickFiler/Interfaces/IFilerFormController.cs:17`) and `MinimizeFormViewer()` is a + `void` member on it. The mock chain is two lines (see 5.2). `MinimizeFormViewer` has a real + implementation at `QfcFormController.Actions.cs:197` that touches a form, but the test never + constructs it. **`OpenQFItem` should therefore be IN scope for tests, not excluded.** +- **`Task.Run` at lines 154, 158, 159, 180** is production async, not a test timing hack. The method is + `await`-ed by the test, so the result is deterministic. No `Task.Delay` and no `Thread.Sleep` is + introduced, so `.claude/rules/general-unit-test.md`'s banned-API list is respected. Moq mocks are + invoked from the thread-pool thread, which is safe. +- **`CurrentConversationState` (lines 55-58)** is `internal` and testable via `InternalsVisibleTo`; it + needs only the `CommandBars` setup. + +Branch control for a defect-2 test: pass `QfEnums.InitTypeEnum.Find` (value 2, per +`QuickFiler/Helper Classes/QfEnums.cs:8`) so `_initType.HasFlag(QfEnums.InitTypeEnum.Sort)` is false +at lines 151 and 179. Note that both use the **non-short-circuiting `&`**, so +`AutoFile.AreConversationsGrouped(_activeExplorer)` is still evaluated and the `CommandBars` setup +remains mandatory. That is a real behavioural detail worth a comment in the test. + +### 5.5 Recommended test set + +| # | Test | Target | Notes | +| --- | --- | --- | --- | +| 1 | `OpenQFItem_WhenActiveExplorerChangesAfterConstruction_UsesTheConstructorCapturedExplorer` | Defect 2 | The fail-before test. See §8.2. | +| 2 | `OpenQFItem_WhenMailIsAlreadyInTheCurrentFolder_DoesNotChangeCurrentFolder` | Defect 2 guard (lines 135-137) | Same `FolderPath` on both sides. | +| 3 | `OpenQFItem_WhenItemIsSelectableInView_ClearsAndAddsSelection` | lines 156-159 | Positive path. | +| 4 | `ExplConvView_ToggleOn_WhenFlagSet_AppliesRememberedView` | lines 123-131 | Requires the `Views` indexer mock. | +| 5 | `ExplConvView_ToggleOn_WhenFlagClear_DoesNothing` | line 125 negative branch | | +| 6 | `ExplConvView_ToggleOff_WhenConversationsNotGrouped_DoesNothing` | line 74 negative branch | | +| 7 | `ExplConvView_ToggleOff_WhenSiblingViewMissing_CopiesAndSavesTemporaryView` | lines 95-103 | Exercises `GetSiblingView` returning null plus `View.Copy`. | +| 8 | `GetSiblingView_WhenNamedViewPresent_ReturnsIt` / `_WhenAbsent_ReturnsNull` | lines 108-121 | Uses the `GetEnumerator` precedent. | +| 9 | `CurrentConversationState_ReflectsCommandBarPressedState` | lines 55-58 | Two cases. | +| 10 | `ExplConvView_ReturnState_WhenFlagSet_TogglesOn` | lines 66-70 | | +| 11 | `Contract_ExplConvView_Cleanup_IsNotDeclaredOnTheInterface` | Defect 1, **optional** | Reflection assertion; see §8.1 for why the dossier is preferred instead. | + +Keep the file under 500 lines. If tests 1-11 exceed it, split into +`QfcExplorerControllerTests.cs` and `QfcExplorerController.ConversationViewTests.cs` and append two +csproj lines rather than one — still within the partitioned region. + +### 5.6 One pre-existing policy tension to flag in the spec + +`.claude/rules/general-unit-test.md` ("Test File Location") requires test files to live in a `tests/` +directory tree mirroring the production source. This repository's entire C# corpus uses +`.Test/` sibling projects instead, and `CLAUDE.md`'s C# Unit Test Policy — which sits above +the rule summaries in the compliance order — does not restate the `tests/` requirement. Placing the +new file at `QuickFiler.Test/Controllers/` matches the repository and matches the epic's explicit +instruction ("#449 owns one appended `Compile Include` ... It appends to the `Controllers` item +group"). Record this in the spec so `feature-review` does not raise it as a new violation. + +--- + +## 6. Q6 — Coverage story + +### 6.1 Does the class-level attribute make the deletion coverage-neutral? **Yes.** + +The tooling evidence is in-repo and direct. `scripts/vscode/Invoke-MSTestWithCoverage.ClosureFilter.ps1:217-222` +states, as the premise of the whole filter: + +> "A method-level `[ExcludeFromCodeCoverage]` attribute suppresses the attributed member but not the +> lambdas declared inside it ... This filter removes them by inferring exemption from the declaring +> member's absence from the instrumented method set of the same declaring type and source file." + +That is, the tool's own design depends on the fact that an exempt member emits **no `` +element** in the Cobertura output. A *class*-level attribute suppresses every member, so +`QfcExplorerController` contributes no `` and no lines to the report at all. + +Corroborating configuration: +- `coverage.config` (24 lines) excludes only third-party module paths (`Deedle`, `FSharp`, + `Castle.Core`, `FluentAssertions`, `Moq`, `Microsoft.Testing`, `MSTest`). It contains no + `QuickFiler` entry and no source-level exclusion, so the attribute is the only mechanism in play. +- `Directory.Build.targets` (30 lines) concerns VSTO manifest and assembly signing only. It has no + coverage content. + +**Conclusion: the potential document's claim is false.** Deleting lines 183-321 changes the coverage +denominator by zero while the class-level attribute is present. The genuine benefits of the deletion +are: removal of duplicated code that can drift from the maintained `UtilitiesCS` copies, removal of +two latent defects, and a 139-line reduction toward the file-size cap. The spec should restate the +benefit in those terms and explicitly correct the potential document's wording. + +### 6.2 Does `CLAUDE.md` clause (c) still apply to this class? + +**No, on two independent grounds.** Clause (c) exempts: + +> "Outlook Interop event handler classes in `TaskVisualization`, `QuickFiler`, `TaskMaster`, +> `ToDoModel`, and `Tags` that directly depend on `Microsoft.Office.Interop.Outlook.Application`, +> `MailItem`, `Store`, or `MAPIFolder` **without an injectable seam**." + +1. **`QfcExplorerController` is not an event handler class.** It subscribes to no Outlook event, wires + no `Explorer` or `Application` event, and declares no event handler method. It is a command + controller. +2. **It has an injectable seam.** `IApplicationGlobals` is constructor-injected (line 29), and every + COM object it touches is reached through that seam or through the `Explorer` captured from it at + line 35. §5.3 demonstrates that all ten relevant members are mockable, with a proven in-repo + precedent for each. The clause's own qualifying condition is therefore not satisfied. + +Clause (c) also carries a counter-clause that points the same way: "Testable seams within otherwise +COM-bound assemblies ... are explicitly NOT exempt and must meet the `>= 80%` floor." + +### 6.3 The `.claude/rules/general-unit-test.md` conflict, and the recommended reading + +`.claude/rules/general-unit-test.md` (Coverage Exclusion Policy) states flatly: "No production file may +be excluded from coverage measurement," and instructs feature-review agents to treat any `exclude` +entry matching a production source path as **Blocking**. Its enumerated enforcement target is +tooling-config `exclude` entries, not source attributes — but `CLAUDE.md` UT2 itself treats the two as +the same instrument ("Exemption is applied via `[ExcludeFromCodeCoverage]` attributes in source code +(reviewable in PRs) **or** via `coverage.config` assembly-level excludes"). + +**Recommended reading**, and the one the spec should record: + +> The class-level `[ExcludeFromCodeCoverage]` on `QfcExplorerController` is not grounded in either +> policy. `CLAUDE.md` clause (c) does not reach it (not an event handler; has an injectable seam), and +> `.claude/rules/general-unit-test.md` forbids excluding a production file from measurement outright. +> The attribute is a pre-existing, unratified exclusion. + +**Risk under `feature-review`:** touching a file that carries an unratified production-file exclusion +invites the reviewer to raise the exclusion as Blocking on this PR, even though this change did not +introduce it (it was added 2026-06-13 in commit `a564add0d`). Leaving the attribute entirely +untouched is the option most likely to draw that finding; narrowing it, with the reasoning above +written into the PR body, is the option that pre-empts it. + +Also record for the reviewer: **the only machine-enforced numeric coverage gate in this repository is +a repo-wide 80% line rate.** `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-489` throws +when `line-rate * 100 < 80`, reading the root `line-rate` attribute of the merged Cobertura document. +There is no per-file gate, no per-assembly gate, and no branch-coverage gate anywhere in +`scripts/`. The uniform 85%/75% thresholds in `.claude/rules/general-unit-test.md` and +`.claude/rules/quality-tiers.md` are not enforced by any script found in this worktree, and +`quality-tiers.yml` — which `quality-tiers.md` names as its source of truth — does not exist. State +this in the spec so the plan does not gate on an unenforceable number. + +### 6.4 Recommendation — **NARROW the attribute** + +Three options, weighed against the epic NFR "Coverage of `QuickFiler.csproj` is retained or improved +at every child merge." + +| Option | Denominator effect | NFR effect | Assessment | +| --- | --- | --- | --- | +| **KEEP** the class-level attribute | Zero. Class stays invisible. | Trivially satisfied (nothing changes). | Safest for the gate, but leaves an unratified exclusion on a file this PR touches, and makes every test written in §5 invisible to the metric. The tests still run and still prove the fix; they just earn nothing. | +| **REMOVE** it outright | Whole class enters the denominator, including `OpenQFItem`'s untestable `MessageBox` branch (lines 166-178). | At risk. After deletion the class is ~184 lines; the modal-dialog branch is 8-10 executable lines. Reaching the aspirational 85% line figure on the class is marginal, though the enforced repo-wide 80% gate is unaffected by a single small class. | Most policy-pure, highest scope cost, and the only option that can plausibly move a number in the wrong direction. | +| **NARROW** — remove the class-level attribute and apply `[ExcludeFromCodeCoverage]` to `OpenQFItem` alone | Everything except `OpenQFItem` enters the denominator. `NavigateToOutlookFolder` is a *separate private method*, so it stays instrumented and is covered through `OpenQFItem` calls made by the tests. | **Improved.** The newly-measured members are all fully coverable (§5.3), so they enter at a high covered ratio and raise `QuickFiler.csproj`'s figure. | **Recommended.** | + +**Recommendation: NARROW.** Specifically: + +1. Delete `[ExcludeFromCodeCoverage]` from line 20. +2. Add `[ExcludeFromCodeCoverage]` immediately above `public async Task OpenQFItem(MailItem mailItem)` + (currently line 146), with an in-code comment recording the exact reason: the else branch calls + `MessageBox.Show`, a modal WinForms dialog that cannot be exercised in a headless unit test, and no + modal-dialog seam is reachable from `QuickFiler.Test` (see §6.5). +3. Keep `using System.Diagnostics.CodeAnalysis;` (line 4) — still required. +4. Note that this preserves coverage of the defect-2 fix site: `NavigateToOutlookFolder` (lines + 133-143) is not attributed and remains in the denominator, covered by tests 1-3 of §5.5. + +If the planner judges even this to be out of scope for a defect-fix issue, **KEEP** is the acceptable +fallback; **REMOVE** should not be chosen, because it puts an untestable modal-dialog branch into the +denominator with no seam available to retire it. + +### 6.5 Rejected alternative for `OpenQFItem`'s dialog + +`UtilitiesCS` has a modal-dialog seam — `MyBox.DialogInvoker` +(`UtilitiesCS/Dialogs/MyBox.cs:41-45`), exercised at `UtilitiesCS.Test/Dialogs/MyBox_ShowDialog_Tests.cs` +and `UtilitiesCS.Test/EmailIntelligence/AutoFile_Tests.cs:43`. Rejected for two reasons: + +1. `DialogInvoker` is declared `internal`, and `UtilitiesCS/Properties/AssemblyInfo.cs:18-20` grants + `InternalsVisibleTo` only to `DynamicProxyGenAssembly2`, `UtilitiesCS.Test`, and `ToDoModel.Test`. + `QuickFiler.Test` is not on the list, so using the seam would require editing a shared surface + outside this issue's file set. +2. Replacing `MessageBox.Show` with `MyBox.ShowDialog` changes the dialog the user sees. That is a + behaviour change beyond the three defects and is forbidden by the Bugfix Workflow's minimal-fix + rule. + +--- + +## 7. Q7 — File-size cap + +`.claude/rules/general-code-change.md` caps production, test, and reusable-script files at **500 lines**. +Measured line counts in this worktree: + +| File | Lines | Over cap? | Touched by this change? | +| --- | --- | --- | --- | +| `QuickFiler/Controllers/QfcExplorerController.cs` | **323** | No | **Yes** — drops to approximately 184 after the region deletion, 179 after also removing `ExplConvView_Cleanup`. | +| `QuickFiler/Interfaces/IQfcExplorerController.cs` | **15** | No | **Yes** — one line removed (14). | +| `QuickFiler/Legacy/QuickFileController.cs` | **1,065** | **Yes, 2.1x** | **No.** Read-only reference. Not compiled (no `` entry anywhere in `QuickFiler.csproj`). Pre-existing violation, not caused or worsened by this change. | +| `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs` | **465** | No | **No** — see below. | +| `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` | **1,429** | **Yes, 2.9x** | **No** — see below. Pre-existing violation, not caused or worsened by this change. | +| `QuickFiler.Test/QuickFiler.Test.csproj` | **484** | No | **Yes** — one appended line (485). 15 lines of headroom remain. | +| `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs` | 0 (new) | Must stay under 500 | **Yes** — new file. | +| `QuickFiler/QuickFiler.csproj` | 595 | Above 500, but a generated non-SDK project file, not authored source | **No** — no production csproj edit is required. | + +### `EmailFiler.cs` and `SortEmail.cs` need NO edit — orchestrator's reading CONFIRMED + +Verified by symbol-level inspection (§3.2). The two files declare their own independent copies of the +helpers, called only from within their own types, and carry their own tests in +`UtilitiesCS.Test/EmailIntelligence/SortEmail_Tests.cs` and `EmailFiler_Tests.cs`. There is no +reference in either direction between them and `QfcExplorerController`. Deleting the `QuickFiler` +region leaves them untouched and unbroken. + +They are the *surviving maintained copies* of the duplicated helpers, exactly as the orchestrator read +it. The spec should say so explicitly and should **not** propose consolidating the three copies — that +is a separate refactor, larger than this issue, and would drag two 500-line-cap violations into a +defect-fix PR. + +### Cap-violation attribution guidance for the PR body + +`feature-review` raises the 500-line cap against files in the diff. Only `QfcExplorerController.cs` +(323 → ~179), `IQfcExplorerController.cs` (15 → 14), `QuickFiler.Test.csproj` (484 → 485) and the new +test file will be in the diff, and none is over the cap. `SortEmail.cs` and `QuickFileController.cs` +should not appear in the diff at all. State this in the PR body pre-emptively. + +--- + +## 8. Q8 — Regression-test-first sequencing + +`CLAUDE.md`'s Bugfix Workflow requires a failing regression test first. Assessment per defect. + +### 8.1 Defect 1 — `ExplConvView_Cleanup` + +**Under the recommended decision (remove the member): a behavioural fail-before test is structurally +impossible.** There is no observable behaviour to assert, because the member has no callers and, +after the change, does not exist. Two candidate mechanisms and their assessment: + +- *Reflection contract test* — `typeof(IQfcExplorerController).GetMethod("ExplConvView_Cleanup").Should().BeNull()`. + This genuinely fails before and passes after, and the general policy does list "Contract / schema + tests" as a category. But it asserts the *absence* of a member, which permanently blocks a future + restoration and encodes no behaviour. Listed as optional test 11 in §5.5; **not recommended**. +- *Compiler as the gate* — removing a member from an interface with one implementer is enforced by the + build itself, and the absence of callers is provable by grep. **Recommended.** + +Record a **`fail-before-exception` dossier** at +`/evidence/regression-testing/fail-before-exception..md`, per +`.claude/skills/evidence-and-timestamp-conventions/SKILL.md`. Required content: + +``` +Timestamp: +Command: git grep -n "ExplConvView_Cleanup" -- "*.cs" +EXIT_CODE: 0 + +WhyFailingRunImpossible: The remedy removes a member that no compiled production or test code +calls, so there is no observable behaviour whose change a test could detect. A test asserting the +member's absence would assert the non-existence of an API rather than a behaviour, and would +permanently block restoration. + +Absence-of-caller proof: + SearchScope: entire repository, *.cs + SearchPatterns: ExplConvView_Cleanup + SearchResult: + QuickFiler/Interfaces/IQfcExplorerController.cs:12 (declaration, removed by this change) + QuickFiler/Controllers/QfcExplorerController.cs:61 (implementation, removed by this change) + QuickFiler/Legacy/QuickFileController.cs:673, :851 (NOT COMPILED — no + entry exists in QuickFiler/QuickFiler.csproj) + QuickFiler/Notes/notes_interfaces.cs:58 (NOT COMPILED — same proof) + Mock-setup proof: no file under QuickFiler.Test/ references ExplConvView_Cleanup. + Compiler proof: the interface has exactly one implementer, so the build enforces the paired edit. +``` + +**If the decision is overridden to "implement":** a fail-before test *is* constructible and should be +written — `System.Action act = () => controller.ExplConvView_Cleanup(); act.Should().NotThrow();` +fails today (line 63 throws) and passes after. Follow it with behavioural assertions on +`View.Apply()` and `View.Delete()` via `VerifyAll`. + +### 8.2 Defect 2 — `OpenQFItem` re-resolves the explorer + +**A genuinely failing-before test IS constructible.** This is the strongest of the three and should +carry the plan's `[expect-fail]` task. + +Mechanism — make the two explorers distinguishable by sequencing `ActiveExplorer()`: + +```csharp +olApp.SetupSequence(a => a.ActiveExplorer()) + .Returns(capturedExplorer.Object) // consumed by the constructor, line 35 + .Returns(driftedExplorer.Object); // what line 140 would resolve today +``` + +Arrange so the guard at lines 135-137 is entered: `capturedExplorer.CurrentFolder` returns a folder +whose `FolderPath` is `@"\\Mailbox\A"`, and `mailItem.Parent` returns a folder whose `FolderPath` is +`@"\\Mailbox\B"`. Set `IsItemSelectableInView` to `true` so the `MessageBox` branch is never reached, +and construct with `QfEnums.InitTypeEnum.Find` so neither `HasFlag(Sort)` conjunct is true. + +Assert: +```csharp +capturedExplorer.VerifySet(e => e.CurrentFolder = destination.Object, Times.Once()); +driftedExplorer.VerifySet(e => e.CurrentFolder = It.IsAny(), Times.Never()); +``` + +Before the fix, line 140 assigns `driftedExplorer.CurrentFolder`, so both assertions fail. After the +fix, both pass. Use `MockBehavior.Loose` for `driftedExplorer` so the pre-fix failure surfaces as a +clean FluentAssertions message rather than a Moq strict-mode exception. + +### 8.3 Defect 3 — dead-code deletion + +**A fail-before test is NOT constructible.** The block is unreachable from every compiled entry point +(§3), so no input to any public or internal API can cause any of its 139 lines to execute. There is no +observable behaviour that differs before and after. A reflection assertion that +`typeof(QfcExplorerController).GetMethod("StripTabsCrLf", BindingFlags.NonPublic | BindingFlags.Static)` +is `null` would fail before and pass after, but it asserts the absence of a private implementation +detail and is brittle; it is not recommended. + +Record a **`fail-before-exception` dossier** with: + +``` +Timestamp: +Command: git grep -n -E "SanitizeArrayLineTSV|StripTabsCrLf|WriteCSV_StartNewFileIfDoesNotExist|SanitizeArray|SaveMessageAsMSG|GetCurrentExplorerFolder" -- "*.cs" +EXIT_CODE: 0 + +WhyFailingRunImpossible: The change deletes six private/internal statics that no compiled entry +point can reach, so no test input can execute any of the deleted lines. There is no observable +behaviour to assert before or after. + +Absence-of-reference proof: every reference to the six identifiers outside lines 183-321 of +QuickFiler/Controllers/QfcExplorerController.cs binds to an independent copy in +UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs, +UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs, or +ToDoModel/Email Utilities/SortItemsToExistingFolder.cs. No file under QuickFiler.Test references +any of the six. + +Alternative proof of no behaviour change: the full nine-assembly suite passes identically before +and after. Record both runs under /evidence/qa-gates/. +``` + +The acceptance criterion for defect 3 asks for "a test run confirming no behavior change," which is +satisfied by the before/after suite comparison rather than by a new test. + +### 8.4 Recommended plan sequencing + +1. Phase 2 (`[expect-fail]`): write **only** the defect-2 test (§8.2) plus the non-fail-before + characterisation tests from §5.5 that already pass. Run; confirm the defect-2 test fails and the + others pass. +2. Phase 3a: apply the one-line defect-2 fix (line 140). Re-run; defect-2 test passes. +3. Phase 3b: remove `ExplConvView_Cleanup` from the interface and the class. Build is the gate. +4. Phase 3c: delete lines 183-321 and the orphaned `using` directives. Build is the gate. +5. Phase 3d: narrow the `[ExcludeFromCodeCoverage]` attribute (§6.4). +6. Phase 4: full toolchain, in order, and both fail-before-exception dossiers. + +Order matters: the defect-2 test must be written and observed failing **before** any deletion, because +deleting the region and the orphaned usings changes the file's line numbering and would make the +pre-change observation harder to reconstruct. + +--- + +## 9. Toolchain and environment notes carried from the epic + +Restated here so the plan does not re-derive them. These are the epic's Hard Constraints, verified +against this worktree where verifiable. + +1. **`vstest` requires `/InIsolation`.** Use + `vstest.console.exe /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"`. + Without it, binding redirects in each assembly's `app.config` are ignored and roughly 1,695 phantom + failures appear. +2. **Exclude `\.claude\` from recursive `*.Test.dll` discovery.** Stale agent worktrees exist under + `.claude/worktrees/`, including this one; a CI-style recursive search would load stale assemblies. +3. **Do not edit anything under `.claude/**`.** Push-down-owned; local edits are destroyed by sync. +4. **No Python toolchain exists.** There is no `scripts/dev_tools/` and no Poetry manifest (verified). + Any skill step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by absence — report + it as such; do not fabricate a result. PowerShell equivalents are under `.claude/lib/`. +5. **`quality-tiers.yml` does not exist** at the repository root (verified). No QuickFiler tier + classification is available to cite. +6. **Evidence paths are non-overridable:** `/evidence//` only. +7. **Analyzer gate must use `/t:Rebuild`,** not `/t:Build`; the nullable gate must **not** add + `/p:Nullable=enable`. Both per `CLAUDE.md` C#1.2 and C#1.3. + +--- + +## 10. Decision summary for the spec + +| Question | Decision | +| --- | --- | +| **Q1** | **Remove** `ExplConvView_Cleanup()` from `IQfcExplorerController.cs:12` and `QfcExplorerController.cs:60-64`. Zero callers, zero mock setups, one implementer. Preserve the legacy body verbatim in `spec.md` for future restoration. Fallback implementation retained at §1.5 if overridden. | +| **Q2** | Replace `_globals.Ol.App.ActiveExplorer().CurrentFolder = ...` at line 140 with `_activeExplorer.CurrentFolder = ...`. Line 140 is the only re-resolution in the file. No behavioural dependency on the fresh call exists; no in-code justification is needed. | +| **Q3** | Delete lines 183-321 unconditionally. All six statics are referenced only within that region; every external reference binds to `SortEmail`, `EmailFiler`, or `SortItemsToExistingFolder`. **No `QuickFiler.Test` file references any of the six.** No test edit required. | +| **Q4** | Eight directives become orphaned: lines 2, 3, 5, 6, 8 (by deletion), 1 (by the Q1 removal), plus lines 7, 13, 15 which are **already** orphaned today. Retained: 4 (attribute), 9, 10, 11, 12, 14, 16. **Unused usings are not enforced by either gate here** — `IDE0005`'s analyzer is not wired into this non-SDK project, no `IDE0005` severity is configured, and `CS8019` is hidden. Removal is hygiene; do it, and label it as such. | +| **Q5** | `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs`, class `QfcExplorerControllerTests`, namespace `QuickFiler.Controllers.Tests`. Append one `` after `QuickFiler.Test.csproj:158`. Mock chain per §5.2, modelled on `QfcHomeControllerTests.cs:39-47`. **Every relevant COM member is mockable** with an in-repo precedent. `OpenQFItem` is IN scope; only its `MessageBox.Show` else branch (lines 166-178) is untestable and must stay uncovered. | +| **Q6** | The class-level `[ExcludeFromCodeCoverage]` means the deletion is **coverage-neutral** — the potential document's denominator claim is false. `CLAUDE.md` clause (c) does **not** apply (not an event handler; has an injectable seam). **Recommendation: NARROW** — remove the class-level attribute, apply it to `OpenQFItem` only. This satisfies the epic NFR positively. `KEEP` is the acceptable fallback; `REMOVE` is not recommended. | +| **Q7** | Over-cap and **untouched**: `SortEmail.cs` (1,429), `QuickFileController.cs` (1,065) — both pre-existing, neither in the diff. In the diff and all under cap: `QfcExplorerController.cs` 323 → ~179, `IQfcExplorerController.cs` 15 → 14, `QuickFiler.Test.csproj` 484 → 485. **`EmailFiler.cs` and `SortEmail.cs` need NO edit** — confirmed. | +| **Q8** | Defect 2: genuine fail-before test via `SetupSequence` on `ActiveExplorer()` (§8.2). Defects 1 and 3: fail-before structurally impossible; record `fail-before-exception..md` dossiers under `/evidence/regression-testing/` with the absence-of-reference proofs given in §8.1 and §8.3. | + +--- + +## 11. Open items the spec must resolve, not inherit + +1. The feature-folder name disagreement between `epic.md:33` and the on-disk folder (§0.3.1). +2. The Q6 attribute decision is a judgment call with a real `feature-review` risk in either direction. + Whichever is chosen, write the reasoning from §6.2 and §6.3 into the PR body so the reviewer sees + that the exclusion's policy grounding was examined rather than ignored. +3. The `Views` indexer parameter type (§5.3) is the single unverified compile-level detail in the test + design. It is a one-token adjustment if wrong and cannot invalidate the harness. +4. Whether the catch-asymmetry reading in §1.2(b) should be recorded as a latent defect. Under the + recommended Q1 removal the code disappears and the question is moot; the epic forbids writing a new + potential document, so if it is judged worth tracking it must go through the issue-promotion path + after this child merges, not into `docs/features/potential/**`. diff --git a/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/spec.md b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/spec.md new file mode 100644 index 000000000..18cb78064 --- /dev/null +++ b/docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/spec.md @@ -0,0 +1,1136 @@ +# quickfiler-explorer-controller-latent-defects (Spec) + +- **Issue:** #449 +- **Parent (optional):** epic `quickfiler-suite-determinism-foundation` (wave 0, complexity band C3) +- **Owner:** drmoisan +- **Last Updated:** 2026-08-21T18-35 +- **Status:** Approved +- **Version:** 1.0 + +> **Work mode `full-bug`.** Per `.claude/skills/acceptance-criteria-tracking/SKILL.md`, this file is the +> sole authoritative acceptance-criteria source for issue #449. No `user-story.md` exists for this +> issue and none is to be created: the requirements are defect-driven and support no user story. +> `issue.md` carries the original early-draft criteria; the `## Acceptance Criteria` section below +> supersedes them and is the section executors and reviewers check off. + +## Context + +Three independent items in `QuickFiler/Controllers/QfcExplorerController.cs`, all found by reading +during the F6 per-file coverage research for issue #435 and none fixable there, because F6's +acceptance criteria forbid behavior changes: + +1. `ExplConvView_Cleanup()` is declared on the public interface `IQfcExplorerController` and its only + implementation throws `NotImplementedException`. +2. The private helper `NavigateToOutlookFolder(MailItem)` re-resolves the active Explorer through + `_globals.Ol.App.ActiveExplorer()` instead of reusing the constructor-captured `_activeExplorer`, + so the method's guard and its assignment can address different `Explorer` objects. +3. A 139-line `#region Email Sorting To Rewrite` holds six private/internal statics that are + duplicated from maintained copies in `UtilitiesCS` and `ToDoModel` and are unreachable from every + compiled entry point. Two further latent defects sit inside that unreachable block. + +The authoritative requirements mirror is `issue.md` in this folder. The primary evidence source is +`research/qfc-explorer-controller-defects.2026-08-21T18-20.md` (1,039 lines), which re-derived every +line number in this worktree. Per the epic's "Known-Stale Potential-Document References" constraint, +no `file:line` citation was carried from the potential document without re-derivation; every citation +in this spec was confirmed against the research artifact or re-read directly from disk. + +## Repro & Evidence + +- **Steps to reproduce (with data/flags/inputs):** None of the three items is reachable from a normal + Outlook session today, so there is no user-facing repro. Each is reproduced by static evidence and, + for defect 2, by a constructible unit test: + - **Defect 1** — call any `IQfcExplorerController.ExplConvView_Cleanup()` implementation. The single + implementer (`QuickFiler/Controllers/QfcExplorerController.cs:61-64`) throws + `NotImplementedException` at line 63 unconditionally. No compiled caller exists, so the throw is + latent. + - **Defect 2** — construct `QfcExplorerController`, then change the process's active Explorer, then + call `OpenQFItem(mailItem)` with a mail item whose parent folder differs from the captured + Explorer's current folder. `QuickFiler.Test` reproduces this deterministically with + `SetupSequence` on `Outlook.Application.ActiveExplorer()`; see Test Strategy. + - **Defect 3** — no repro is possible. The region is unreachable; see Root Cause Analysis. +- **Expected vs actual behavior:** + - Defect 1: expected either working cleanup semantics or no such contract member; actual is a + declared contract member that fails at runtime for its first caller. + - Defect 2: expected the guard at + `QuickFiler/Controllers/QfcExplorerController.cs:135-137` and the assignment at line 140 to read + and write the same `Explorer`; actual is that line 136 reads + `_activeExplorer.CurrentFolder.FolderPath` while line 140 writes + `_globals.Ol.App.ActiveExplorer().CurrentFolder`, which is a freshly resolved and possibly + different object. + - Defect 3: expected one maintained copy of each helper; actual is three independent copies, of + which the `QuickFiler` copy is dead and carries two defects of its own. +- **Logs/screenshots/error snippets:** None available. No live Outlook process exists in this + environment, and the affected paths produce no log output — the file's `log4net` logger at + `QuickFiler/Controllers/QfcExplorerController.cs:23-25` is declared and never referenced anywhere in + the file. +- **Frequency / determinism (always, intermittent, data-dependent):** + - Defect 1: deterministic on any call; zero calls exist today. + - Defect 2: the redundant COM round-trip is deterministic on every `NavigateToOutlookFolder` call + that enters the guard; the correctness hazard is data-dependent on whether the active Explorer + changed between construction and the call. + - Defect 3: never fires. Unreachable. + +## Scope & Non-Goals + +### In scope + +- `QuickFiler/Interfaces/IQfcExplorerController.cs` — remove line 12 + (`void ExplConvView_Cleanup();`). 15 lines to 14. +- `QuickFiler/Controllers/QfcExplorerController.cs` — remove lines 60-64 (the `//PRIORITY:` comment and + the throwing implementation); change line 140 to use `_activeExplorer`; delete lines 183-321 (the + `#region Email Sorting To Rewrite`); remove the class-level `[ExcludeFromCodeCoverage]` at line 20 and + add an injectable modal-dialog seam consumed by line 168; remove nine orphaned `using` directives. + 323 lines to approximately 179. +- `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs` — new file. Must remain under 500 lines. +- `QuickFiler.Test/QuickFiler.Test.csproj` — exactly one appended `` line. 484 to 485. +- `docs/features/active/2026-08-07-quickfiler-explorer-controller-latent-defects-449/evidence/**` — + regression-testing dossiers, QA-gate output, coverage figures. + +### Out of scope / non-goals + +- **Implementing** `ExplConvView_Cleanup` semantics. See decision D1. +- **Fixing** the two latent defects inside the dead region (transposed `Path.Combine` arguments; a + `null` `ref string[]` written into). They are deleted, not fixed. See decision D3. +- **Consolidating** the three copies of the six duplicated helpers. + `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs` (1,429 lines) and + `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFiler.cs` (465 lines) are the surviving + maintained copies; they carry their own tests in `UtilitiesCS.Test` and need no edit. Consolidation is + a separate, larger refactor and would drag a pre-existing 500-line-cap violation into a defect-fix + pull request. +- **Splitting** `SortEmail.cs` (2.9x the cap) or `QuickFiler/Legacy/QuickFileController.cs` (1,065 + lines, 2.1x the cap). Both are pre-existing violations, neither is edited, and neither appears in the + diff. `QuickFileController.cs` is read-only reference material and is not compiled. The Bugfix + Workflow forbids widening scope; if a split is judged worth tracking it belongs in a new issue filed + after this child merges, and the epic forbids any child writing under `docs/features/potential/**`. +- **Editing `QuickFiler/Notes/notes_interfaces.cs`**, even though it declares a duplicate + `IQfcExplorerController` carrying `ExplConvView_Cleanup` at `:52-59`. It is not compiled and is + outside this issue's file set. +- **Editing `QuickFiler/QuickFiler.csproj`.** No production project-file edit is required: the dead + region is inside an already-compiled file, and the uncompiled `Legacy/` and `Notes/` files have no + compile entries to remove. +- **Editing anything under `.claude/**`.** That tree is push-down-owned per the epic's Hard Constraints. + Where this spec cites a rule file, the citation is the policy the fix is measured against, not an + edit target. +- **Correcting the catch-asymmetry in the legacy `ExplConvView_Cleanup` body.** Under D1 the code is not + imported, so the question is moot; the analysis is preserved below as knowledge. +- **Touching the `Form1` region of `QuickFiler.Test.csproj`** (`:161-166`) or the `Form1.resx` + `EmbeddedResource` (`:180-182`). Sibling child #491 owns those lines exclusively. + +### Explicitly excluded systems, integrations, or datasets + +- No live Outlook process, no live WinForms form, no message pump, no temporary files, and no + filesystem or network access in any test added by this issue. +- No Python toolchain step. There is no `scripts/dev_tools/` and no Poetry manifest in this repository + (verified). Any skill or plan step naming `poetry run python -m scripts.dev_tools.*` is unrunnable by + absence and must be reported as such, never fabricated and never silently skipped. + +## Root Cause Analysis + +- **Current hypothesis or confirmed root cause:** + - **Defect 1 — confirmed.** The member is a stub that was never implemented. The `//PRIORITY:` comment + at `QuickFiler/Controllers/QfcExplorerController.cs:60` marks it as known-incomplete work carried + forward from the uncompiled legacy controller. The contract was declared before the behavior + existed, and nothing has ever called it. + - **Defect 2 — confirmed.** The code was ported from + `QuickFiler/Legacy/QuickFileController.cs`, which used the same `_globals`-rooted expression, and the + modern type's constructor capture of `_activeExplorer` + (`QuickFiler/Controllers/QfcExplorerController.cs:35`) was introduced without updating this one call + site. Line 140 is the only re-resolution left in the file; the other five `_globals` uses are the + constructor assignment (34), the authoritative capture (35), the field declaration (40), a settings + read `_globals.Ol.ViewWide` (90), and a commented-out line (162). + - **Defect 3 — confirmed.** The region is a copy of helpers that were later given maintained homes in + `UtilitiesCS` and `ToDoModel`. Its entry point + (`WriteCSV_StartNewFileIfDoesNotExist`, declared at line 216) is itself uncalled, so the whole block + is a closed island. Three of the six statics (`SanitizeArrayLineTSV` at 185, `SaveMessageAsMSG` at + 272, `GetCurrentExplorerFolder` at 278) have zero call sites even inside the region. +- **Signals/evidence supporting it:** + - **Defect 1.** A repository-wide search for `ExplConvView_Cleanup` across `*.cs` returns five hits: + `QuickFiler/Interfaces/IQfcExplorerController.cs:12` (declaration), + `QuickFiler/Controllers/QfcExplorerController.cs:61` (the throwing implementation), + `QuickFiler/Legacy/QuickFileController.cs:673` and `:851` (not compiled), and + `QuickFiler/Notes/notes_interfaces.cs:58` (not compiled). No file under `QuickFiler.Test` sets up or + verifies the member on any `Mock`. `QfcExplorerController` is the only + implementer. + - **Defect 2.** Line 136 reads `_activeExplorer.CurrentFolder.FolderPath`; line 140 writes + `_globals.Ol.App.ActiveExplorer().CurrentFolder`. `_activeExplorer` is assigned exactly once (line + 35) and never reassigned. Nothing in the type subscribes to Outlook Explorer lifecycle events and no + member accepts a replacement Explorer. Every other COM operation in the type already uses + `_activeExplorer`: lines 57, 74, 77, 81, 127, 136, 141, 152, 156, 158, 159. + - **Defect 3.** All in-file references to the six statics fall inside lines 183-321 (call sites at 193, + 241, 264). Every external reference binds to an independent copy: `SortEmail.cs` declares its own at + `:1092`, `:1344`, `:1361`, `:1374`, `:1407`; `EmailFiler.cs` at `:211`, `:224`; + `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs` declares all six at `:255`, `:273`, `:285`, + `:317`, `:350`, `:355`; and `TaskMaster/AppGlobals/AppOlObjects.cs:279` is explicitly type-qualified + to `SortEmail`. **No file under `QuickFiler.Test` references any of the six**, including the + `internal static StripTabsCrLf` at line 203 that `InternalsVisibleTo` would otherwise expose. The + only test references in the repository are in + `UtilitiesCS.Test/EmailIntelligence/SortEmail_Tests.cs` and + `UtilitiesCS.Test/EmailIntelligence/EmailFiler_Tests.cs`, all against the `UtilitiesCS` copies. + - **Broadened deletion-safety fact.** No file under `QuickFiler/Legacy/` is compiled at all: + `QuickFiler/QuickFiler.csproj` contains zero `