diff --git a/.claude/agent-memory/atomic-executor/MEMORY.md b/.claude/agent-memory/atomic-executor/MEMORY.md index b5b985592..50823fdd7 100644 --- a/.claude/agent-memory/atomic-executor/MEMORY.md +++ b/.claude/agent-memory/atomic-executor/MEMORY.md @@ -111,6 +111,7 @@ - [Async state machine emits no `` element](project_async_state_machine_emits_no_method_element.md) — per-method union is empty; `.//line` double-counts - [First-party coverage denominator (#197)](project_coverage_firstparty_denominator_method.md) — count per-`` - [dotnet-coverage denominator nondeterminism](project_dotnet_coverage_denominator_nondeterminism.md) — 47% vs 81% from +- [Failed coverage run leaves RAW Cobertura](project_failed_coverage_run_leaves_raw_unprocessed_cobertura.md) — throw precedes post-processing; 0.70 raw vs 0.85 processed - [Koverage Cobertura post-processing shape](project_koverage_cobertura_postprocessing_shape.md) — passing run = proce - [C# canonical coverage artifact conversion](project_csharp_canonical_coverage_artifact_conversion.md) — hook reads JaCoC - [Cobertura runsettings `` override](project_cobertura_runsettings_attributes_override.md) — silently disable diff --git a/.claude/agent-memory/atomic-executor/project_baseline_sha_diff_conflates_merged_base.md b/.claude/agent-memory/atomic-executor/project_baseline_sha_diff_conflates_merged_base.md index 418576a14..48295388b 100644 --- a/.claude/agent-memory/atomic-executor/project_baseline_sha_diff_conflates_merged_base.md +++ b/.claude/agent-memory/atomic-executor/project_baseline_sha_diff_conflates_merged_base.md @@ -19,6 +19,16 @@ would have failed the feature for files it never touched — including **Why:** two-dot `..HEAD` is "in HEAD, not in base", which after a merge is precisely the branch's own additions. A diff against a pre-merge SHA has no such property. +**Three-dot does NOT protect you.** Confirmed again on #656, 2026-09-01, where the plan wrote every +footprint gate as `...HEAD`. When the pinned SHA is an *ancestor* of HEAD, +`merge-base(PINNED, HEAD)` is the pinned SHA itself, so the three-dot form silently degenerates to +the two-dot diff against it and inherits the whole defect. Measured: `...HEAD` = **299** +paths including 9 under `QuickFiler/`+`QuickFiler.Test/` and a `.csproj`, versus +`origin/main...HEAD` = **10**. Four gates asserting "exactly the single line" and "both outputs +empty" were unsatisfiable as written. The tell is that the plan's base predates the branch's +reconciliation merge — check `git merge-base HEAD` against `` before trusting any +`...` gate. + **How to apply:** run the command the task names AND the `..HEAD` form, record both, and make the `..HEAD` classification the authoritative one with an explicit attribution column for the merge-induced rows. Do not silently substitute — the task text is still the plan of record. Same diff --git a/.claude/agent-memory/atomic-executor/project_failed_coverage_run_leaves_raw_unprocessed_cobertura.md b/.claude/agent-memory/atomic-executor/project_failed_coverage_run_leaves_raw_unprocessed_cobertura.md new file mode 100644 index 000000000..af6ae29e5 --- /dev/null +++ b/.claude/agent-memory/atomic-executor/project_failed_coverage_run_leaves_raw_unprocessed_cobertura.md @@ -0,0 +1,34 @@ +--- +name: failed-coverage-run-leaves-raw-unprocessed-cobertura +description: A non-zero Invoke-MSTestWithCoverage run leaves coverage.cobertura.xml RAW (absolute class/@filename, no root lines-covered/lines-valid, all-modules line-rate ~0.70 not ~0.85); recover in memory with ConvertTo-KoverageCoberturaXml +metadata: + type: project +--- + +`scripts/vscode/Invoke-MSTestWithCoverage.ps1` throws on a non-zero inner vstest exit code +(`:235-237`) BEFORE the Koverage post-processing block (`:339-343`). `dotnet-coverage` has +already written `coverage\coverage.cobertura.xml` by then, so the file exists but is the RAW +collector output: + +- `class/@filename` is still an ABSOLUTE path, so a lookup keyed on the repo-relative + `QuickFiler\Viewers\Foo.cs` form finds nothing. +- The root `coverage` node carries no `lines-covered` / `lines-valid`; those are set only at + `Invoke-MSTestWithCoverage.Helpers.ps1:442-445`. +- The root `line-rate` is the ALL-MODULES rate, not the first-party allowlist rate. Measured + gap: issue #608's failed run recorded `line-rate="0.7017"` / `lines-valid="81570"`, against + ~0.853 / ~64k on processed runs of comparable trees. Reading the raw number as a coverage + regression is a false conclusion. + +**Why:** the throw is not caught (only a `finally` that removes derived settings), so it +propagates past `Set-Content` and nothing rewrites the document. + +**How to apply:** when a coverage run exits non-zero but you still need the figures, do not read +the emitted file directly. Dot-source `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1` and +convert in memory: +`ConvertTo-KoverageCoberturaXml -XmlContent (Get-Content coverage\coverage.cobertura.xml -Raw -Encoding UTF8) -RepoRoot (Get-Location).Path` +(both parameters are mandatory strings), then read the six values from the converted document. +Note the same ordering means `Assert-CoberturaLineCoverageThreshold` never runs on a failed run, +so a sub-80% raw rate is not itself the cause of the non-zero exit. + +Related: [[project_dotnet_coverage_denominator_nondeterminism]], +[[project_koverage_cobertura_postprocessing_shape]]. diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 145d7358b..d2a1c091a 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -26,6 +26,7 @@ - [#644 cycle 2](project_644_cycle2_sweep_gate_evasion_seams.md) — rewording out of a detector's match set is gate evasion; SHA-256 pair - [#644 PA-7](project_644_pa7_redaction_plan_seams.md) — untracked audit artifact still enters main; name-status diff blind to it - [#648](project_648_ungated_static_swap_plan_seams.md) — lines-valid equality UNSATISFIABLE, use 5% tolerance; `git tag` not idempotent +- [#656](project_656_closecompleted_guard_plan_seams.md) — no TestCaseFilter override in either wrapper; wrapper writes no TRX; class nodes lack lines-covered/lines-valid; TestResults\ must be created first - [#680](project_680_menu_mode_plan_seams.md) — HostTests.cs 499 not 500; TRX 5-shape identifiers, `grep -a`; exact line arithmetic ## Plan-structure traps diff --git a/.claude/agent-memory/atomic-planner/project_656_closecompleted_guard_plan_seams.md b/.claude/agent-memory/atomic-planner/project_656_closecompleted_guard_plan_seams.md new file mode 100644 index 000000000..fea64fb80 --- /dev/null +++ b/.claude/agent-memory/atomic-planner/project_656_closecompleted_guard_plan_seams.md @@ -0,0 +1,33 @@ +--- +name: project-656-closecompleted-guard-plan-seams +description: Planning seams found authoring the #656 CloseCore completed-close guard plan — wrapper scripts accept no TestCaseFilter, the coverage runner prints no percentage, and MSBuild non-vacuity is better proved by assembly mtimes than by log text +metadata: + type: project +--- + +Seams found while authoring the atomic plan for issue #656 (`BreadcrumbDropDownOpenCoordinator.CloseCore` completed-close guard). + +**Why:** each of these would have produced an unsatisfiable or vacuous acceptance condition, and none is visible from the plan text alone. + +**How to apply:** re-check these before authoring any TaskMaster C# plan that runs a scoped test, asserts a coverage number, or asserts MSBuild non-vacuity. + +- **Neither test wrapper accepts a `TestCaseFilter` override.** `scripts/vscode/Invoke-MSTest.ps1:54` and `scripts/vscode/Invoke-MSTestWithCoverage.ps1:76` both hardcode `'/InIsolation', '/TestCaseFilter:TestCategory!=LiveOutlook'` with no parameter to extend it. A `[expect-fail]` red run scoped to one test therefore requires a **direct** `vstest.console.exe` invocation that reproduces both protections explicitly — `/InIsolation` plus a `TestCategory!=LiveOutlook&FullyQualifiedName~` conjunction. That is not the prohibited "bare" call; editing the wrapper would breach a footprint boundary. +- **`Invoke-MSTestWithCoverage.ps1` prints no coverage percentage on a successful run.** `Assert-CoberturaLineCoverageThreshold` (`Invoke-MSTestWithCoverage.Helpers.ps1:459-491`) only *throws* below 80%. Never assert a printed percentage. Read `line-rate` from `coverage/coverage.cobertura.xml` instead. That path is git-ignored (`.gitignore:144`, `coverage/*`). +- **Its `EXIT_CODE: 0` strictly implies zero failed tests**, because `Invoke-MSTestWithCoverage.ps1:235-237` throws when the inner vstest exit code is non-zero. That fact is what lets a plan assert zero failures without asserting a console literal. +- **Cobertura `class/@filename` is repo-relative with backslashes**, and classes are merged one node per file (`ConvertTo-KoverageRelativePath` at `Helpers.ps1:50-97`, `Merge-CoberturaClassesByFilename`). Per-file coverage lookups must use `QuickFiler\Viewers\X.cs`, not a forward-slash or absolute form. +- **Prove MSBuild non-vacuity with output-assembly `LastWriteTime`, not log text.** Asserting the absence of `Skipping target "CoreCompile"` needs a positive control, and whether that string appears at `Verbosity=normal` is an unobserved assumption. Record a `Gate Start:` wall-clock stamp and assert `bin\Debug\.dll` `LastWriteTime` is later — that proves recompilation regardless of verbosity. +- **`Select-String` has no `-Recurse` parameter.** Pipe `Get-ChildItem -Recurse -Filter *.cs` into it. And use `@(Select-String ...).Count`, never `(Select-String ...).Count`, so a zero-match result is a number. +- **`Invoke-VSBuild.ps1` is unusable under a footprint boundary** — it runs `Sync-PackageReferences.ps1` over every `.csproj` and rewrites `HintPath` values. Resolve MSBuild through vswhere directly, as `Invoke-Restore.ps1:22-30` does. +- **Bootstrap facts for a fresh agent worktree:** `.dotnet-sdk/` and `packages/` are both absent and both git-ignored (`.gitignore:350` `.dotnet*/`; `.gitignore:191` `**/[Pp]ackages/*`), and `dotnet-tools.json` sits at the worktree ROOT, not under `.config/`. `dotnet-coverage` must also resolve or `Invoke-MSTestWithCoverage.ps1:292-294` throws. +- **Guard-literal replacement was safely zero-hit checkable:** `if (_closeCompleted)` is NOT a substring of `if (_closeCompleted && !hostOpen)` because the searched literal carries the closing parenthesis. Pair it with a rule that the new doc comments must not contain the old literal. +- **Count `_host.` on non-comment lines only** (`^\s*[^/\s].*_host\.`, baseline 5 lines). The AC-19 doc comments would otherwise inflate the count that AC-2's lock-discipline gate reads. + +**Round-2 preflight delta (2026-08-31) added four more:** + +- **A Cobertura `class` node carries no `lines-covered`/`lines-valid`.** `Invoke-MSTestWithCoverage.Helpers.ps1:374-380` sets exactly `line-rate`, `branch-rate`, `complexity` on a class node; `:442-445` sets `lines-covered`/`lines-valid` on the ROOT `coverage` node only. `GetAttribute` returns an empty string for the missing pair, so a per-file covered/valid gate reading them is unsatisfiable. Derive per-file counts from the class-relative `./lines/line` set (count, and count with `hits > 0`) — the descendant axis double-counts. +- **The coverage wrapper produces NO TRX.** `Invoke-MSTestWithCoverage.ps1:70-76` passes no `/Logger:trx` and `scripts/vscode/TaskMaster.cli.runsettings` declares no logger. Test counts and failing names for a wrapper run must come from the tee'd console log, never from a TRX. Only a direct `vstest.console.exe` call that passes `/Logger:trx` writes one. +- **`TestResults\` does not exist in a fresh worktree**, and neither the msbuild file logger nor `Tee-Object -FilePath` creates a missing parent directory. Add a Phase 0 task that `New-Item -Force`s every subdirectory the plan writes into, before the first task that writes one. +- **A wrapper failure anywhere kills the coverage baseline, not just the test gate.** The throw at `:235-237` precedes the post-processing at `:339-343`, so on a non-zero exit the emitted `coverage\coverage.cobertura.xml` is the RAW document: absolute `class/@filename`, no root `lines-covered`/`lines-valid`. A "record the failure and continue" tolerance branch must say how the baseline coverage numbers are still obtained (dot-source the helpers and run `ConvertTo-KoverageCoberturaXml` in memory) or declare BLOCKED. +- **Anchor a per-file coverage no-regression gate on `lines-valid`, not the rate.** Per-file `lines-covered` drifts by up to four lines between two runs of the same tree (`.claude/agent-memory/orchestrator/coverage-lines-covered-is-nondeterministic.md`), so a near-1.0 baseline rate fails on correct work. Require a reproduction run before failing. + +See [[declaration-only-seam-task-for-fail-before]] (not needed here — the test compiles against unmodified production code, so the red is runtime), [[trx-needs-resultsdirectory]], [[absolute-counts-in-shared-files-go-stale]]. diff --git a/.claude/agent-memory/feature-review/MEMORY.md b/.claude/agent-memory/feature-review/MEMORY.md index 7f333d091..62d879fbb 100644 --- a/.claude/agent-memory/feature-review/MEMORY.md +++ b/.claude/agent-memory/feature-review/MEMORY.md @@ -1,3 +1,4 @@ +- [breadcrumb Close returns before OpenState=false](project_breadcrumb-close-returns-before-openstate-false.md) — #656: reopen-path enumeration doesn't prove `_closeCompleted && IsOpen` unreachable; CompleteClose is queued, not synchronous - [verify-zero-own-effect-coverage-noise-491](project_verify-zero-own-effect-coverage-noise-491.md) — verify "coverage shortfall is noise": grep both Cobertura XMLs for the assembly; distrust deleted-raw-XML narratives (#491) - [poshqc-bundled-coverage-artifact-reads-zero](project_poshqc-bundled-coverage-artifact-reads-zero.md) — run_poshqc_test's canonical Pester XML reads 0 covered (invalid capture) -> honest non-blocking FAIL row; adjudicate from committed direct-Pester JaCoCo (#441) - [441-review-residuals-and-494-handoff](project_441-review-residuals-and-494-handoff.md) — #441 PASS/0 blocking; 85.0317% vs 85% margin is #494's call; NF-1 uncovered Helpers.ps1:220; #529-#532 OPEN deliberately diff --git a/.claude/agent-memory/feature-review/project_breadcrumb-close-returns-before-openstate-false.md b/.claude/agent-memory/feature-review/project_breadcrumb-close-returns-before-openstate-false.md new file mode 100644 index 000000000..e7c49d9f7 --- /dev/null +++ b/.claude/agent-memory/feature-review/project_breadcrumb-close-returns-before-openstate-false.md @@ -0,0 +1,40 @@ +--- +name: breadcrumb-close-returns-before-openstate-false +description: "#656: BreadcrumbDropDownHost.Close returns true before OpenState=false, so `_closeCompleted && IsOpen` IS occupiable in production; reopen-path enumerations answer a narrower question than they are credited with" +metadata: + type: project +--- + +`BreadcrumbDropDownHost.Close` (`QuickFiler/Viewers/BreadcrumbDropDownHost.cs:251-254`) returns +`true` after only *scheduling* `CompleteClose`. `CompleteClose` (`:397-411`) is what sets +`OpenState = false`, and it is dispatched via `BreadcrumbDropDownOpenLifetime.ScheduleInvalidating` +-> `ScheduleObserved` -> `RunOnOwnerAsync` -> `_uiOperations.PostAsync`, i.e. queued on the **same** +`BreadcrumbPopupUiOperations` dispatcher the coordinator posts its own work to. + +Consequence: `_closeCompleted == true && _host.IsOpen == true` is a state the **production** host can +occupy, with no substituted seam and no reopen. + +**Why:** #656's spec and research artifact proved by exhaustive enumeration that no production path +reopens the host without `RequestOpen`/`Invalidate` (only `OpenState = true` is +`BreadcrumbDropDownOpenLifetime.cs:268`, reachable only via `RequestOpen`, which clears the flag). +That enumeration is correct, but it answers "can the host be *reopened* without the entry points?" +The guard `if (_closeCompleted && !hostOpen)` depends on a different proposition — "can both flags be +true at once?" — which the asynchronous close window answers yes. The spec reported the narrower +result as though it settled the broader one, and concluded a rollback would be "observationally +identical on every shipped path". Not established. + +Second-order effect: a second `_host.Close` inside that window re-enters the `OpenState == true` +branch, whose `InvalidateAndSchedule` bumps the lifetime generation and makes the *first* scheduled +`CompleteClose` fail its lease check. The second reason wins, and `FinishClose` calls +`_cancelSelection()` only for `Uncommitted` (`:449-451`) — so an Uncommitted-then-ExplicitCommit pair +can drop the selection cancel. + +Also: every test fake clears `IsOpen` synchronously inside `Close` +(`BreadcrumbDropDownOpenCoordinatorTests.cs:436-437`), so no test represents this timing. 100% +changed-line coverage and a 91% class branch rate did not touch it. + +**How to apply:** when any breadcrumb review credits a reopen-path enumeration with proving a guard +unreachable, check whether the guard's actual predicate is about reopening or about two states +coinciding. Trace `Close` to whatever sets `OpenState = false` and confirm it is synchronous before +accepting the claim. Related: [[verify-the-asserted-evidence-mechanism]], +[[505-coordinator-prime-toggle-race]]. diff --git a/.claude/agent-memory/task-researcher/MEMORY.md b/.claude/agent-memory/task-researcher/MEMORY.md index 44fc681f5..924c28633 100644 --- a/.claude/agent-memory/task-researcher/MEMORY.md +++ b/.claude/agent-memory/task-researcher/MEMORY.md @@ -48,6 +48,7 @@ - [reflective-caller-closure-635](project_reflective_caller_closure_635.md) — #635/#468: removal was THIRTEEN members (`_templateTlp` omitted from AC-16); `GetField(` never searched (172 hits) yet is the only reaching mechanism; QuickFiler is ComVisible(false) (2026-08-29) - [selectrow-two-families-637](project_selectrow_two_families_637.md) — #637: TWO unrelated SelectRow families (bare grep over-counts ~10x); blanket TryMakeArchiveRelative rejects relative/Trash rows; ButtonOK_Click does NOT rethrow (2026-08-29) - [analyzer-severity-ceiling-and-runsettings-split](project_analyzer_severity_ceiling_and_runsettings_split.md) — MSTEST0032 is the ONLY rule above suggestion; no .globalconfig; Invoke-MSTest.ps1 docstring names the wrong runsettings (2026-08-31) +- [issue-656-bypass-path-does-not-exist](project_issue_656_bypass_path_does_not_exist.md) — #656 premise FALSE: no bypassing reopen path exists; SR-4's "no IsOpen under _sync" already violated at :112; both nominated owner files at the 500-line cap (2026-08-31) ## Artifact hygiene - [Never embed absolute host paths](../_shared_no_absolute_host_paths.md) — no `C:\Users\\...`, bare account, or machine name in ANY artifact; use `` / `` / `` / ``. vstest names TRX `__.trx` by default, so control `/ResultsDirectory:` + `LogFileName=` or rename before citing. diff --git a/.claude/agent-memory/task-researcher/project_issue_656_bypass_path_does_not_exist.md b/.claude/agent-memory/task-researcher/project_issue_656_bypass_path_does_not_exist.md new file mode 100644 index 000000000..185155ac0 --- /dev/null +++ b/.claude/agent-memory/task-researcher/project_issue_656_bypass_path_does_not_exist.md @@ -0,0 +1,48 @@ +--- +name: issue-656-bypass-path-does-not-exist +description: "#656 _closeCompleted residual: the bypassing reopen path the issue assumes does NOT exist; only open transition is BreadcrumbDropDownOpenLifetime.cs:268, reachable only via RequestOpen; the two nominated owner files are at the 500-line cap" +metadata: + type: project +--- + +Issue #656 (`_closeCompleted` stays stale after a host reopen that bypasses `RequestOpen`/`Invalidate`) +has a **false premise on shipped code**. Verified 2026-08-31 by exhaustive enumeration: + +- The repository's ONLY statement that makes the breadcrumb drop-down host open is + `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:268` (`_host.OpenState = true;`). + `BreadcrumbDropDownHost.IsOpen => OpenState` is get-only; all four other `OpenState` writes are `false`. +- Closed chain: `RequestOpen (…OpenCoordinator.cs:115, clears the flag at :114) → OpenCoreAsync (:218) + → BeginOpenCore (:258/:259) → BreadcrumbDropDownHost.Open.cs:22/:37 → :88 → + BreadcrumbDropDownOpenLifetime.cs:67-69 → :243 → :268`. No production caller of + `IBreadcrumbDropDownHost.OpenAsync` exists outside `BeginOpenCore`. +- Native-show family also checked: `ShowPopup` / `_showPopup` / `ShowOwnedPopup` are invoked once each, + downstream of `OpenState = true`; the only `ToolStripDropDown` event subscribed is `Closed`. + +**Why:** the issue's "Suspected Cause" asserts the bypassing paths "live in the ItemViewer breadcrumb +lifecycle host surface" (feature 488's files). They do not. That claim came from #501's SR-4 known +limitation being recorded as an ownership hand-off, not from an enumeration. + +**How to apply:** treat #656 as latent-correctness hardening, not a user-facing defect. Do NOT site a +fix in `BreadcrumbItemViewerLifecycleCoordinator.cs` (497 lines) or `BreadcrumbDropDownHost.cs` +(498 lines) — both are 2-3 lines from the repo's 500-line cap, so any edit forces a partial split first. +The only sane footprint is `BreadcrumbDropDownOpenCoordinator.cs` (378 lines) plus +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (173 lines; Part2 is 455 and +the primary partial is 463, both too full for a new test). + +Two facts that constrain any remedy: + +1. SR-4 (`docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md:426-437`) rejected + `if (_closeCompleted && !_host.IsOpen) return true;` because it reads `_host.IsOpen` under `_sync`. + **Honest qualification:** `RequestOpen` at `…OpenCoordinator.cs:112` ALREADY reads `_host.IsOpen` + under `_sync`, so "never read IsOpen under the lock" is not an invariant the file holds. SR-4's real + objection is adding a *second* instance of a pattern a sibling feature was removing. Don't overstate it. +2. The refinement would be a **no-op in production anyway**: `BreadcrumbUiDispatcher.Dispatch`/`DispatchValue` + run INLINE on the captured boundary (`BreadcrumbUiDispatcher.cs:78-95`, `:166-178`), so + `CompleteClose` → `OpenState = false` executes synchronously inside `_host.Close` before it returns + `true`. `!_host.IsOpen` is therefore already true when the suppression is evaluated. + +Bypass test seam already exists and needs no new plumbing: `ControlledHost.SetOpen(bool)` at +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:407`, already used for exactly this +purpose at `…Part2.cs:349`. No test anywhere references `_closeCompleted` by name or reflection. + +Related: [[breadcrumb-navigation-defects-439-440-498-499]], [[issue-469-already-fixed-residual-is-629]]. diff --git a/QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs b/QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs index 17a8e13b7..4d801eb80 100644 --- a/QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs +++ b/QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs @@ -169,5 +169,45 @@ public void LatchAfterRelease_IsIgnoredAndIssuesNoOpen() opening.Result.Should().BeFalse(); harness.Host.Requests.Should().BeEmpty(); } + + /// + /// Issue #656: after a close that returned true, with the host open again by a path that + /// reaches neither RequestOpen nor Invalidate, a further close must reach the host rather + /// than being suppressed by the completed-close flag. Deterministic: one thread, explicit + /// drain, no timers, no sleeps, no second thread, no temp files. + /// + [TestMethod] + public void CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain() + { + // Arrange: open the drop-down, then drive a close that the host accepts. + var harness = new CoordinatorHarness(); + harness.Host.Enqueue(Task.FromResult(true)); + Task opening = harness.Coordinator.RequestOpen(); + harness.Context.DrainUntil(opening); + opening.Result.Should().BeTrue(); + + harness.Coordinator.SetDroppedDown(false); + harness.Context.DrainAll(); + harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.Uncommitted); + harness.Host.IsOpen.Should().BeFalse("the host accepted the close"); + + // Act: the host becomes open again by a path that bypasses RequestOpen and Invalidate. + harness.Host.SetOpen(true); + harness.SelectorOpen = true; + harness.Coordinator.SetDroppedDown(false); + harness.Context.DrainAll(); + + // Assert + harness + .Host.CloseReasons.Should() + .Equal( + new[] + { + BreadcrumbDropDownCloseReason.Uncommitted, + BreadcrumbDropDownCloseReason.Uncommitted, + }, + "the close after a bypassing reopen must reach _host.Close a second time" + ); + } } } diff --git a/QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs b/QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs index 559ae7a49..d2a6fd05f 100644 --- a/QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs +++ b/QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs @@ -43,6 +43,13 @@ internal sealed class BreadcrumbDropDownOpenCoordinator /// legitimate reopen through while still suppressing the repeated close: the single close flag /// these two replace was doing both jobs at once and could not distinguish them. /// + /// + /// Issue #656: the flag alone is not sufficient, because it is cleared only on the + /// and Invalidate paths. A host reopened by any other + /// path leaves it set, which suppressed a close the host was genuinely open for. + /// Suppression in therefore additionally requires the host to + /// report not open. + /// private bool _closeCompleted; private bool _released; @@ -305,15 +312,25 @@ private async Task RollbackAsync(int generation) /// is cleared in a finally so it reads false on the /// success, not-closed, throw and released exits alike (I-462.1). /// + /// + /// Issue #656: the completed-close guard additionally requires the host to report not + /// open, so a close is not suppressed while the host is genuinely open again. The host + /// read is hoisted above the critical section deliberately: SR-4 of #501 declined the + /// same refinement written as a read taken inside the lock, because that adds a foreign + /// call made while the coordinator lock is held. Hoisting leaves the count of such calls + /// unchanged. The host state can change between the read and the lock; both directions + /// are analysed in the spec for this change and neither corrupts state. + /// private bool CloseCore(BreadcrumbDropDownCloseReason reason) { + bool hostOpen = _host.IsOpen; lock (_sync) { if (_released) return false; if (_closeInFlight) return true; - if (_closeCompleted) + if (_closeCompleted && !hostOpen) return true; _closeInFlight = true; } diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/code-review.2026-09-01T15-03.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/code-review.2026-09-01T15-03.md new file mode 100644 index 000000000..f2f5864ce --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/code-review.2026-09-01T15-03.md @@ -0,0 +1,268 @@ +# Code Review — Issue #656 (breadcrumb `_closeCompleted` residual) + +- Timestamp: 2026-09-01T15-03 +- Branch: `bug/breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656` +- Head SHA: `65d2f22b5100588eae8ac4de40e48f1ac391db34` +- Base: `main` at `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` +- Code surface under review: 2 files, +58/-1 lines. + +## Change summary + +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` + +- Line 326: `bool hostOpen = _host.IsOpen;` hoisted above the `lock (_sync)` that opens `CloseCore`. +- Line 333: the completed-close guard narrowed from `if (_closeCompleted)` to + `if (_closeCompleted && !hostOpen)`. +- Two `` blocks added: on the `_closeCompleted` field (lines 46-52) and on `CloseCore` + (lines 315-323). + +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` + +- One added test, `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain`. + +## What the change does, stated as a truth table + +| `_closeCompleted` | `hostOpen` | Before | After | +|---|---|---|---| +| false | either | fall through to `_host.Close` | unchanged | +| true | false | suppress, return true | unchanged | +| true | true | suppress, return true | **fall through to `_host.Close`** | + +Exactly one cell changes. That is the minimum edit that achieves the stated objective, and it is +the right shape: it does not clear `_closeCompleted` on the successful-close path, which +`issue.md` records would break two standing tests, and it does not replace the flag with a bare +`!_host.IsOpen` gate, which would break +`PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen`. + +## Positive observations + +1. **Lock discipline is respected, and for a stated reason.** SR-4 of #501 declined this refinement + when it was written as a read taken inside the lock. Hoisting the read keeps the count of foreign + calls made under `_sync` unchanged. Verified directly: across all twelve `lock (_sync)` bodies in + the file, the only `_host` member invocation is the pre-existing + `if (_closeInFlight && _host.IsOpen)` at line 119 in `RequestOpen`. Lines 200, 205, 216, 265-266 + and 340 all sit outside any lock. No new foreign call under the lock was introduced. + +2. **The added `` explain why, not what.** The `CloseCore` block records the SR-4 precedent + and the reason the read is outside the critical section. A future reader will not re-litigate the + inside-the-lock variant, which is precisely the failure mode that produced this issue. + +3. **The regression test is genuinely red-first.** The recorded red run is an assertion failure with + the verbatim message `... but {BreadcrumbDropDownCloseReason.Uncommitted {value: 1}} contains 1 + item(s) less.` — not a compile failure and not a missing-symbol failure. That is the strong form + of red-first evidence. + +4. **The test is deterministic by construction.** One thread, an explicitly pumped context via + `DrainUntil`/`DrainAll`, no timers, no sleeps, no temporary files. It satisfies the determinism + infrastructure requirements in `.claude/rules/general-unit-test.md` without needing a fake clock, + because it reads no clock. + +5. **Footprint discipline held under pressure.** `issue.md` explicitly directs the fix at feature + #488's host-surface files (`BreadcrumbItemViewerLifecycleCoordinator.cs`, + `BreadcrumbDropDownHost.cs`, `ItemViewer.Breadcrumb.cs`). The research artifact established that + touching them was unnecessary, and the diff touches none of them. That is the correct + resolution of a directive that would otherwise have widened the change substantially. + +## Findings + +### CR-1 — The production-unreachability claim is overstated. The new branch is reachable on the shipped host without any seam substitution. + +**Severity: Major. Non-blocking. Not merge-method-dependent.** + +`spec.md` classifies this work as latent-correctness hardening and states, in **Mitigations and +rollbacks**, that "no production path can currently reach the changed state" and that a rollback +"restores behavior that is observationally identical to the fixed build on every shipped path." +That conclusion rests entirely on the reopen-path enumeration. + +The enumeration itself is correct. I re-derived it independently: + +- `OpenState = true` occurs at exactly one place in production, `BreadcrumbDropDownOpenLifetime.cs:268`. +- Every other production assignment sets it false (`BreadcrumbDropDownHost.cs:334, 402, 434, 460`). +- The only production callers of a host `OpenAsync` are `BreadcrumbDropDownOpenCoordinator.cs:265-266`, + inside `BeginOpenCore`, reachable only from `RequestOpen`, which clears `_closeCompleted` at line 121. +- `IBreadcrumbDropDownHost` has exactly one production implementation. + +So no *reopen* path bypasses both entry points. That much is established. + +But the guard's new branch does not require a reopen. It requires only the state +`_closeCompleted == true && _host.IsOpen == true`, and the production host can occupy that state +through a mechanism the enumeration never examines: **`Close` returns `true` before `OpenState` +becomes `false`.** + +``` +BreadcrumbDropDownHost.cs:251-254 + if (OpenState) + { + _openLifetime.InvalidateAndSchedule(() => CompleteClose(reason, true)); + return true; + } +``` + +`OpenState = false` is set inside `CompleteClose` (`BreadcrumbDropDownHost.cs:402`), and +`CompleteClose` is *scheduled*, not called. `InvalidateAndSchedule` reaches +`BreadcrumbDropDownOpenLifetime.ScheduleInvalidating`, which ends in `ScheduleObserved` → +`RunOnOwnerAsync` → `_uiOperations.PostAsync(...)` — the same `BreadcrumbPopupUiOperations` +dispatcher the coordinator posts its own `SetDroppedDown` and `HandleSelectorOpenStateChanged` +continuations to. + +Therefore, when `CloseCore` observes `closed == true` and sets `_closeCompleted = true`, the host +still reports `IsOpen == true` until the queued `CompleteClose` runs. Any `CloseCore` dispatched in +that window reads `hostOpen == true` and now takes the new branch, on the shipped host, with no +substituted implementation. + +Concrete consequence in that window. Suppose `CloseCore(Uncommitted)` completes and a second +`CloseCore(ExplicitCommit)` is dequeued before the pending `CompleteClose`: + +- The second `_host.Close(ExplicitCommit)` re-enters the `OpenState == true` branch, so it calls + `InvalidateAndSchedule` again. That bumps the lifetime generation via `InvalidateCore`, which makes + the first scheduled `CompleteClose(Uncommitted, true)` fail its `IsLifecycleCurrent(lease, ...)` + check and be skipped; the second, carrying `ExplicitCommit`, runs instead. +- `FinishClose` calls `_cancelSelection()` only when the reason is `Uncommitted` + (`BreadcrumbDropDownHost.cs:449-451`). So the selection cancellation the pre-change code performed + would be skipped. + +**What I did and did not establish.** I established that the state is occupiable on the production +host and that the reason-substitution consequence follows if a second `CloseCore` is dequeued inside +the window. I did **not** establish that any real user gesture produces that interleaving. The +plausible trigger — the selector-close event that drives `HandleSelectorOpenStateChanged` — appears +to be raised from inside `CloseNative()`/`_cancelSelection()`, which run within `CompleteClose` +itself; if that is always the ordering, the second `CloseCore` is queued *after* the `CompleteClose` +and the window never opens. Settling that requires host-lifecycle analysis this change deliberately +stayed out of, and I am not going to assert either way on static reading alone. + +**Why this is non-blocking.** No shipped-path defect is demonstrated. The direction of the change is +toward correctness — a close reaching a host that reports itself open is the behaviour the issue +asks for. The full suite is green. The affected sequence is exercised by no test either before or +after the change, so nothing regressed relative to what was verified. + +**What is actually wrong** is the strength of the claim, not the code. The spec answers "can the host +be *reopened* without `RequestOpen`?" and then reports the answer as if it settled "can +`_closeCompleted && IsOpen` both be true in production?". Those are different questions, and the +second one has a different answer. The `` on `CloseCore` inherits the same gap: it says +"the host state can change between the read and the lock; both directions are analysed in the spec +... and neither corrupts state", which is a narrower claim about the race window and does not cover +the asynchronous-close window at all. + +**Recommendation (follow-up, not a merge condition):** amend the spec's Rollout and Risks sections to +state that the guard's new branch is reachable on the production host inside the +`Close`-returns-before-`CompleteClose`-runs window, and either demonstrate the event ordering that +closes the window or add a regression test whose fake defers `IsOpen = false` the way the real host +does. See CR-3. + +### CR-2 — R-1's description of the not-open `Close` branch is inaccurate. + +**Severity: Minor. Non-blocking. Not merge-method-dependent.** + +`spec.md` R-1 states that a redundant close on an already-closed host is something +"`BreadcrumbDropDownHost.Close` (`:247-257`) handles by returning `false` without closing." + +That is not what line 256 does: + +``` +return _openLifetime.TryCancelPendingOpen(() => CompleteClose(reason, OpenState)); +``` + +`TryCancelPendingOpen` returns `false` only when `_disposed`, or `_openCompletion == null`, or a +close is already pending. Otherwise it invalidates the lifetime, schedules `CompleteClose`, and +returns **true** — and because it sets `_pendingCloseCompletion` first, that `CompleteClose` passes +its `if (!OpenState && !_openLifetime.IsPendingClose) return;` guard and runs `FinishClose(reason)`, +which cancels the selection for an `Uncommitted` reason. + +In the specific state R-1 is reasoning about — immediately after a completed close — +`ScheduleInvalidating` has already nulled `_openCompletion`, so `TryCancelPendingOpen` does return +`false` and R-1's *conclusion* survives. But the stated reason is wrong, and a future reader who +applies R-1's rule to a state where an open is pending will reach the wrong answer. Since the +explicit purpose of the recorded analysis is "so a future reader does not re-derive it", an +inaccurate rule is worth correcting. + +**Recommendation:** correct the R-1 sentence to name the actual mechanism (`_openCompletion` is null +after a completed close, so `TryCancelPendingOpen` short-circuits) rather than describing `Close` as +unconditionally returning false when not open. + +### CR-3 — Line coverage of the new conjunct does not imply coverage of the behaviour that makes it reachable. + +**Severity: Minor. Non-blocking. Not merge-method-dependent.** + +Every fake host in the suite clears its open state synchronously inside `Close`: + +``` +QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:436-437 + if (CloseResult) + IsOpen = false; +``` + +The real host does not (CR-1). So the assertion in `evidence/qa-gates/coverage-delta` that "both +outcomes of the new conjunct are covered" is true at the branch level and is correctly evidenced — +`!hostOpen == true` by three standing guards, `!hostOpen == false` by the new test — but it does not +extend to the production timing. The new test reaches `!hostOpen == false` by calling +`harness.Host.SetOpen(true)` explicitly, which models the seam-substitution scenario, not the +asynchronous-close window. + +This is the general pattern of a coverage figure being read as stronger evidence than it is: 100 +percent line coverage on the changed lines and a 91.18 percent branch rate on the class say the code +was executed, not that the reachable production interleaving was represented. + +**Recommendation:** if CR-1 is pursued, the cheapest closing move is a harness variant whose `Close` +returns `true` but defers `IsOpen = false` to the drained queue, then a test asserting the intended +behaviour for an `Uncommitted`-then-`ExplicitCommit` pair. + +### CR-4 — Stale line references in `spec.md`. + +**Severity: Informational. Non-blocking.** + +Several `spec.md` citations use pre-change coordinates: + +| Citation in spec.md | Actual post-change location | +|---|---| +| AC-19: `_closeCompleted` docs at `:38-46` | `:38-52` (the added `` extends the block) | +| AC-19: `CloseCore` summary at `:302-307` | `:309-314`, with the added `` at `:315-323` | +| Scope & Non-Goals: `OpenAsync` calls at `:258-259` | `:265-266` | +| Scope & Non-Goals: `RequestOpen` at `:115`, clears at `:114` | `:111`, clears at `:121` | + +Every cited block exists and says what the spec says it says; only the numbers drifted, by exactly +the seven lines the field `` added. This is normal for a spec authored before the edit. It +is worth a note only because AC-19 is phrased as a line-range check, and a reader running that check +literally against the post-change file will land slightly off. + +### CR-5 — Four pre-existing uncovered lines in the coordinator. + +**Severity: Informational. Non-blocking. Pre-existing, not introduced.** + +The coordinator's only zero-hit lines are 120, 166, 247 and 330. Line 330 is `return false;` on the +`_released` exit of `CloseCore` — the method this change edits. It was uncovered before the change +and remains uncovered. Nothing in this change made it harder to cover; noted only so that a future +reader of the 98.32 percent figure knows where the gap sits. + +## Design assessment + +**Is the hoisted read the right design?** Yes, given the constraints. The three candidate shapes +were: clear `_closeCompleted` on the successful-close path (rejected in `issue.md` because it breaks +two standing tests); read `_host.IsOpen` inside the lock (rejected as SR-4 of #501 because it adds a +foreign call under `_sync`); hoist the read (chosen). The chosen shape is the only one that satisfies +both prior constraints, and the reasoning is recorded in-code rather than only in the spec. + +**Does the unconditional read create a disposal hazard?** No. The read at line 326 precedes the +`_released` check, so `_host.IsOpen` is touched even after `Release()`. On the sole production +implementation `IsOpen` is `public bool IsOpen => OpenState;` — a plain auto-property read with no +dispose guard, so it cannot throw post-disposal. I verified this directly rather than relying on the +spec's R-3. The residual exposure is to a *future* implementation that throws or counts reads on +`IsOpen`; R-3 records that, which is the appropriate level of treatment. + +**Is `_generation++` on the redundant close a problem?** In the CR-1 window the second successful +close increments `_generation` a second time and re-sets `_closeCompleted`. Since no open is in +flight in that state, the extra increment invalidates nothing. It is benign, but it is a second +observable delta that the spec's race analysis does not mention. + +## Verdict + +The code change is small, correctly shaped, well documented in-code, minimally scoped, and backed by +genuine red-first evidence and a green full suite. Merge is not blocked. + +The one finding of substance is CR-1: the delivered artifacts state a stronger unreachability +conclusion than the analysis supports, because the reopen enumeration answers a narrower question +than the one the guard actually depends on. That is a defect in the recorded reasoning, not in the +two changed lines, and it is best resolved by amending the spec and, optionally, adding the deferred +`IsOpen` harness variant described in CR-3. + +**Blocking findings: 0.** CR-1 Major non-blocking; CR-2 and CR-3 Minor non-blocking; CR-4 and CR-5 +informational. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/analyzer-gate.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/analyzer-gate.2026-08-31T20-40.md new file mode 100644 index 000000000..3aeb8e146 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/analyzer-gate.2026-08-31T20-40.md @@ -0,0 +1,52 @@ +# Baseline — Analyzer Gate (Issue #656) + +Timestamp: 2026-09-01T14-37 +Task: [P0-T9] + +Gate Start: 2026-09-01T14:37:04.6422606-04:00 +Gate End: 2026-09-01T14:37:18.6623695-04:00 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\msbuild\p0-t8-analyzer.log;Verbosity=normal" +``` + +Resolved msbuild: Visual Studio 18 Community, `MSBuild\Current\Bin\MSBuild.exe`. + +EXIT_CODE: 0 + +Build summary: `5 Warning(s)` / `0 Error(s)`, elapsed 00:00:13.90. + +## Baseline Warning Codes For BreadcrumbDropDownOpenCoordinator.cs: + +none + +Derivation: +``` +Select-String -Path TestResults\msbuild\p0-t8-analyzer.log -SimpleMatch 'BreadcrumbDropDownOpenCoordinator.cs' | Select-String -SimpleMatch 'warning' +``` +returned a match count of **0**. The pre-change file therefore carries no analyzer or compiler +warning attributed to it, so the post-change subset condition asserted by P4-T5 reduces to requiring +zero warnings for that file after the change as well. + +## Baseline solution warnings (context, not attributed to the file under change) + +All five baseline warnings are the same diagnostic emitted once per affected project by +`System.Reactive.PackagesConfigCheck.targets`: the project contains a `packages.config` file, which +is unsupported by System.Reactive v7.0 or later. Affected projects: `ToDoModel`, `QuickFiler`, +`TaskMaster`, `UtilitiesCS.Test`, and one further project reported in the same form. This is a +pre-existing repository-wide condition unrelated to this item and outside its authorized footprint. + +## Non-vacuity of this baseline + +`Select-String -SimpleMatch 'Skipping target "CoreCompile"'` over the log returned **0**, so no +project skipped compilation and the analyzers actually ran. `/t:Rebuild` is mandatory for this +reason: MSBuild's up-to-date check does not invalidate on a command-line `/p:` change, so a warm +`/t:Build` exits 0 with `CoreCompile` skipped on every project and runs no analyzers. +`/p:Nullable=enable` was not passed, in line with `.claude/rules/csharp.md` and CI. + +Output Summary: Baseline analyzer gate passed with `0 Error(s)` and 5 pre-existing System.Reactive +`packages.config` warnings, none attributed to `BreadcrumbDropDownOpenCoordinator.cs`. The baseline +warning-code set for the file under change is empty. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/base-ref.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/base-ref.2026-08-31T20-40.md new file mode 100644 index 000000000..6042608f6 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/base-ref.2026-08-31T20-40.md @@ -0,0 +1,50 @@ +# Baseline — Base Ref Resolution (Issue #656) + +Timestamp: 2026-09-01T14-34 +Task: [P0-T2] + +Command: +``` +git rev-parse 2b85134b42872e405602e6064e02dc9cda6c319b +git rev-parse HEAD +git rev-parse --abbrev-ref HEAD +``` + +EXIT_CODE: 0 + +Resolved values: + +- Plan base ref `2b85134b42872e405602e6064e02dc9cda6c319b` resolves to the 40-character object id + `2b85134b42872e405602e6064e02dc9cda6c319b`. +- HEAD: `119a89f017e0787e8aa62914333d0a5bc04576fb` +- Branch: `bug/breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656` + +## Base-Ref Discrepancy (recorded, not silently adapted) + +The plan pins all diff assertions to `2b85134b42872e405602e6064e02dc9cda6c319b`. That base is stale +relative to the current branch. Measured in this worktree at the timestamp above: + +- `git merge-base 2b85134b42872e405602e6064e02dc9cda6c319b HEAD` returns + `2b85134b42872e405602e6064e02dc9cda6c319b`, so the three-dot form degenerates to a two-dot diff + against that commit. +- `git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD` lists **299** paths. + Nine of those are under `QuickFiler/` or `QuickFiler.Test/`, and one of them is + `QuickFiler.Test/QuickFiler.Test.csproj`. None of the nine is this item's work: the branch was + reconciled against `origin/main` before execution began, and the pinned base predates that merge, + so the diff conflates every change `main` gained in the interval with this item's change set. +- `origin/main` resolves to `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` and is an ancestor of HEAD + (`git merge-base --is-ancestor origin/main HEAD` exits 0). +- `git diff --name-only origin/main...HEAD` lists exactly ten paths: six under + `.claude/agent-memory/`, plus this feature folder's `issue.md`, `plan.2026-08-31T20-10.md`, + `spec.md`, and `research/2026-08-31T20-15-*.md`. No path under `QuickFiler/` or + `QuickFiler.Test/`, and no build-configuration path. + +Consequence: the footprint acceptance stated by P4-T11 through P4-T14 and by AC-10, AC-11 and AC-12 +cannot be evaluated against the pinned base, because that base reports nine unrelated pre-existing +paths and one `.csproj`. The authoritative footprint base for this run is therefore `origin/main`, +per the execution directive for this run, and the footprint tasks record both measurements so the +substitution is auditable rather than silent. + +Output Summary: All three commands exited 0. The pinned base ref resolves. The pinned base is stale +by 299 paths relative to HEAD and is not usable as the footprint baseline; `origin/main` is used for +the footprint criteria and both measurements are recorded in the Phase 4 footprint artifacts. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-dotnet-coverage.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-dotnet-coverage.2026-08-31T20-40.md new file mode 100644 index 000000000..8c1b3773d --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-dotnet-coverage.2026-08-31T20-40.md @@ -0,0 +1,24 @@ +# Baseline — dotnet-coverage Availability (Issue #656) + +Timestamp: 2026-09-01T14-37 +Task: [P0-T6] + +Command: +``` +dotnet-coverage --version +``` + +EXIT_CODE: 0 + +Results: + +- `dotnet-coverage` resolved on the first attempt; no install was required, so the conditional + `dotnet tool install --global dotnet-coverage` branch of this task was not taken. +- Reported version: `18.10.0+f4cc39224845ffa74bf246c9da2399d50e5d6342`. + +This check is required because `scripts/vscode/Invoke-MSTestWithCoverage.ps1` throws when +`dotnet-coverage` is absent, which would surface as a test-gate failure rather than as the missing +prerequisite it actually is. + +Output Summary: Bootstrap satisfied. The coverage collector is present and reports version 18.10.0. +This is a bootstrap step, not a toolchain gate. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-restore.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-restore.2026-08-31T20-40.md new file mode 100644 index 000000000..00afecbe2 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-restore.2026-08-31T20-40.md @@ -0,0 +1,28 @@ +# Baseline — NuGet Restore Bootstrap (Issue #656) + +Timestamp: 2026-09-01T14-36 +Task: [P0-T4] + +Command: +``` +pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-Restore.ps1 +pwsh -NoProfile -Command "(Get-ChildItem -Directory packages).Count" +``` + +EXIT_CODE: 0 + +Results: + +- Pre-state: no `packages/` directory existed in this worktree. +- The restore resolved MSBuild through vswhere and reported `Installed: 172 package(s) to + packages.config projects`, then `Build succeeded. 0 Warning(s) 0 Error(s)`. +- Recorded directory count: `(Get-ChildItem -Directory packages).Count` = **172**, which is greater + than 0 as the acceptance requires. + +Rationale for this step: every first-party project declares `EnsureNuGetPackageBuildImports` whose +`` fires at `BeforeTargets="PrepareForBuild"`, and `.claude/rules/csharp.md` wires each of the +five analyzers through an explicit `..\packages\...` path. msbuild therefore hard-fails without a +populated `packages/`. `packages/` is git-ignored and does not enter the change set. + +Output Summary: Bootstrap succeeded. 172 packages restored, 0 errors, 0 warnings. This is a +bootstrap step, not a toolchain gate. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-sdk.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-sdk.2026-08-31T20-40.md new file mode 100644 index 000000000..78893894b --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-sdk.2026-08-31T20-40.md @@ -0,0 +1,26 @@ +# Baseline — Repo-Local .NET SDK Bootstrap (Issue #656) + +Timestamp: 2026-09-01T14-36 +Task: [P0-T3] + +Command: +``` +pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Install-RepoDotNetSdk.ps1 +dotnet --version +``` + +EXIT_CODE: 0 + +Results: + +- Pre-state: no `.dotnet-sdk` directory existed in this worktree, as the plan's environment + preconditions predicted. +- The installer downloaded .NET SDK `8.0.205` and installed it to `/.dotnet-sdk`. +- `Test-Path .dotnet-sdk\dotnet.exe` is True: the executable is present. +- `dotnet --version` printed `8.0.205` and exited 0. Before this step the same command would have + printed the `global.json` `errorMessage` instead of a version, because `global.json` pins + `8.0.205` with `paths` `[".dotnet-sdk", "$host$"]`. +- `.dotnet-sdk/` is git-ignored, so it does not enter the change set. + +Output Summary: Bootstrap succeeded. Repo-local SDK 8.0.205 installed and resolving; `dotnet +--version` exits 0 and reports the pinned version. This is a bootstrap step, not a toolchain gate. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-testresults.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-testresults.2026-08-31T20-40.md new file mode 100644 index 000000000..af4af9e9c --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-testresults.2026-08-31T20-40.md @@ -0,0 +1,36 @@ +# Baseline — Raw-Output Directory Bootstrap (Issue #656) + +Timestamp: 2026-09-01T14-38 +Task: [P0-T7] + +Command: +``` +New-Item -ItemType Directory -Force -Path 'TestResults\msbuild','TestResults\p0-t10','TestResults\p1-t3','TestResults\p3-t2','TestResults\p3-t4','TestResults\p4-t7','TestResults\p4-t8-repeat' | Out-Null +(Test-Path 'TestResults\msbuild') +(Test-Path 'TestResults\p0-t10') +(Test-Path 'TestResults\p1-t3') +(Test-Path 'TestResults\p3-t2') +(Test-Path 'TestResults\p3-t4') +(Test-Path 'TestResults\p4-t7') +(Test-Path 'TestResults\p4-t8-repeat') +``` + +EXIT_CODE: 0 + +Test-Path results (all seven): + +- `TestResults\msbuild` = True +- `TestResults\p0-t10` = True +- `TestResults\p1-t3` = True +- `TestResults\p3-t2` = True +- `TestResults\p3-t4` = True +- `TestResults\p4-t7` = True +- `TestResults\p4-t8-repeat` = True + +This step precedes every task that writes a raw log because the msbuild file logger opens its log +with a `StreamWriter` and fails the build with an invalid-file-logger-file error when the parent +directory is missing, and `Tee-Object -FilePath` likewise does not create a missing parent. +`TestResults/` is git-ignored, so none of these paths enters the change set. + +Output Summary: Bootstrap succeeded. All seven raw-output directories created and confirmed present. +This is a bootstrap step, not a toolchain gate. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-tool-restore.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-tool-restore.2026-08-31T20-40.md new file mode 100644 index 000000000..107d14e34 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-tool-restore.2026-08-31T20-40.md @@ -0,0 +1,23 @@ +# Baseline — dotnet tool restore Bootstrap (Issue #656) + +Timestamp: 2026-09-01T14-37 +Task: [P0-T5] + +Command: +``` +dotnet tool restore +``` +(run from the worktree root) + +EXIT_CODE: 0 + +Results: + +- `Tool 'csharpier' (version '1.2.6') was restored. Available commands: csharpier` +- `Restore was successful.` +- The manifest is `dotnet-tools.json` at the worktree root and pins `csharpier` to `1.2.6`. Every + formatting command in this plan is issued through `dotnet tool run` so the manifest-pinned version + is the one that runs, matching the CI format step. + +Output Summary: Bootstrap succeeded. CSharpier 1.2.6 restored from the local tool manifest. This is +a bootstrap step, not a toolchain gate. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/format-check.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/format-check.2026-08-31T20-40.md new file mode 100644 index 000000000..d4fed6746 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/format-check.2026-08-31T20-40.md @@ -0,0 +1,23 @@ +# Baseline — Format Gate (read-only) (Issue #656) + +Timestamp: 2026-09-01T14-39 +Task: [P0-T8] + +Command: +``` +dotnet tool run csharpier check . +``` + +EXIT_CODE: 0 + +Output Summary: `Checked 1566 files in 4614ms.` — the final summary line of the command output, +transcribed verbatim. CSharpier reported no file requiring formatting and exited 0. + +Notes: + +- `check` is read-only, so the exit code alone is a genuine observation of tree state. The + write-mode `format` command is deliberately excluded from the baseline so that the baseline cannot + become a blanket waiver for pre-existing formatting drift. +- A non-zero exit here would have meant pre-existing repository-wide format drift, which would place + AC-14 outside this item's two-file footprint. That did not occur: the pre-change tree is already + format-clean, so any later `csharpier check` failure is attributable to this item's own edits. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/phase0-instructions-read.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/phase0-instructions-read.md new file mode 100644 index 000000000..8d405c21f --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/phase0-instructions-read.md @@ -0,0 +1,25 @@ +# Phase 0 — Policy Instructions Read (Issue #656) + +Timestamp: 2026-09-01T14-33 +Task: [P0-T1] +Policy Order: as required by `.claude/skills/policy-compliance-order/SKILL.md` — repository standing +instructions first, then the cross-language code-change policy, then the cross-language unit-test +policy, then the language-specific rules for the files in scope (C#). + +Files read, in the required order: + +- `CLAUDE.md` +- `.claude/rules/general-code-change.md` +- `.claude/rules/general-unit-test.md` +- `.claude/rules/csharp.md` + +EXIT_CODE: 0 + +Output Summary: All four policy files were read in the order listed above. The first three are +auto-loaded into the session as project instructions; `.claude/rules/csharp.md` was read explicitly +from disk in this session. Controlling constraints extracted for this item: C# toolchain order is +format -> analyze -> type-check -> test with a restart on any failure or file rewrite; CSharpier is +invoked only through `dotnet tool run`; msbuild gates use `/t:Rebuild` and never `/t:Build`; the +nullable gate must not carry `/p:Nullable=enable`; tests use MSTest with FluentAssertions and Moq; +no production file may be excluded from coverage; the 500-line file-size limit applies to both files +in this item's footprint. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/source-baseline.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/source-baseline.2026-08-31T20-40.md new file mode 100644 index 000000000..445fb4ad5 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/source-baseline.2026-08-31T20-40.md @@ -0,0 +1,75 @@ +# Baseline — Pre-Change Source Measurements (Issue #656) + +Timestamp: 2026-09-01T14-39 +Task: [P0-T12] + +Command: +``` +(Get-Content -LiteralPath QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs).Count +(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs).Count +(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part2.cs).Count +(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.cs).Count +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s*[^/\s].*_host\.').Count +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s').Count +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'lock (_sync)').Count +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count +@(Get-ChildItem QuickFiler.Test -Recurse -Filter *.cs | Select-String -SimpleMatch 'CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain').Count +``` + +EXIT_CODE: 0 + +## Measured values against the plan's expected values + +| Measurement | Expected | Measured | Match | +|---|---|---|---| +| `BreadcrumbDropDownOpenCoordinator.cs` line count | 378 | 378 | yes | +| `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` line count | 173 | 173 | yes | +| `BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` line count | 455 | 455 | yes | +| `BreadcrumbDropDownOpenCoordinatorTests.cs` line count | 463 | 463 | yes | +| Non-comment `_host.` lines in the coordinator | 5 | 5 | yes | +| Declared `internal`/`public` member lines in the coordinator | 12 | 12 | yes | +| `lock (_sync)` occurrences in the coordinator | 12 | 12 | yes | +| `if (_closeCompleted)` occurrences in the coordinator | 1 | 1 | yes | +| Pre-existing occurrences of the new test name in `QuickFiler.Test` | 0 | 0 | yes | + +Every expected value in the plan is confirmed against the working tree. `Select-String` has no +`-Recurse` parameter, so the final measurement's file set is produced by `Get-ChildItem` and piped +in, as the plan requires. + +## Enumerated baseline line numbers (used as the comparison set by P2-T5 and P2-T6) + +Non-comment `_host.` lines (5): + +- L112: `if (_closeInFlight && _host.IsOpen)` +- L193: `(!_host.IsOpen || !_host.Close(BreadcrumbDropDownCloseReason.Uncommitted))` +- L258: `? _host.OpenAsync(anchor, workingArea(), size)` +- L259: `: _host.OpenAsync(anchor, workingArea(), size, takeFocus: false);` +- L323: `closed = _host.Close(reason);` + +`lock (_sync)` lines (12): L84, L96, L106, L134, L147, L237, L310, L327, L332, L346, L360, L368. + +Declared member lines (12): L12, L51, L78, L80, L89, L104, L132, L143, L152, L171, L186, L202. + +## Additional plan citations re-derived against the working tree + +- `CloseCore` summary documentation occupies `:302-307`; the declaration + `private bool CloseCore(BreadcrumbDropDownCloseReason reason)` is at `:308`; its opening brace is + at `:309`; the `lock (_sync)` that opens the critical section is at `:310`; the completed-close + guard `if (_closeCompleted)` is at `:316`. +- The `_closeCompleted` field documentation occupies `:38-46`, with the declaration + `private bool _closeCompleted;` at `:46`. +- `internal void SetDroppedDown(bool droppedDown)` is at `:152`. +- In `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, `LatchAfterRelease_IsIgnoredAndIssuesNoOpen` + ends at `:171` and the closing brace of the partial class is at `:172`. Lines `:1-4` import + `System.Threading.Tasks`, `FluentAssertions`, + `Microsoft.VisualStudio.TestTools.UnitTesting`, and `QuickFiler.Viewers`, so the new test needs no + added `using` directive. +- Harness members used by the new test, in `BreadcrumbDropDownOpenCoordinatorTests.cs`: + `CoordinatorHarness` at `:323`, `SelectorOpen` at `:352`, `ControlledHost.IsOpen` at `:378`, + `CloseReasons` at `:395`, `Enqueue` at `:402`, `SetOpen` at `:407`. All exist on the unmodified + tree, so the new test compiles against unmodified production code and its Phase 1 failure is a + runtime red rather than a compile red. + +Output Summary: All nine expected baseline values matched exactly, and every plan citation into the +production and test files was re-derived and confirmed against the current working tree. No +discrepancy found in this task. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md new file mode 100644 index 000000000..a77c67d92 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md @@ -0,0 +1,61 @@ +# Baseline — Test and Coverage State (Issue #656) + +Timestamp: 2026-09-01T14-39 +Task: [P0-T11] + +Run Start: 2026-09-01T14:38:47.4405464-04:00 +Run End: 2026-09-01T14:39:35.3403481-04:00 + +Command: +``` +pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +``` +with console output tee'd to `TestResults\p0-t10\coverage-run.log`. + +EXIT_CODE: 0 + +## Baseline run results + +- Baseline Total Tests: 6925 +- Baseline Passed Tests: 6925 +- Baseline Failed Tests: 0 +- Baseline Failure Set: none + +`EXIT_CODE: 0` strictly implies zero failed tests, because +`scripts/vscode/Invoke-MSTestWithCoverage.ps1` throws when the inner vstest exit code is non-zero. +The run summary in the tee'd log reports `Total tests: 6925` and `Passed: 6925` with no `Failed:` +line, which agrees. The `BASELINE FAILURES PRESENT` branch of this task was not taken, and the +`BLOCKED` branch for a failing `QuickFiler.Test` was not taken. + +The wrapper produces no TRX: it passes `/Settings:`, `/InIsolation` and +`/TestCaseFilter:TestCategory!=LiveOutlook` and no `/Logger:trx`, and the referenced runsettings +declares no logger. Test counts and failure names for a wrapper run are therefore read from the +tee'd console log, as the plan's execution preconditions record. + +## Baseline coverage values + +- Baseline Repo Line Rate: 0.853792 +- Baseline Repo Lines Covered: 54968 +- Baseline Repo Lines Valid: 64381 +- Baseline Coordinator Line Rate: 0.983122 +- Baseline Coordinator Lines Covered: 233 +- Baseline Coordinator Lines Valid: 237 + +Derivation: the three repository values are the `line-rate`, `lines-covered` and `lines-valid` +attributes of the root `/coverage` element of `coverage\coverage.cobertura.xml`. The three +coordinator values come from the single `class` node whose `filename` attribute equals +`QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs`; exactly one such node exists, because the +wrapper's post-processing collapses a file to a single `class` node and rewrites `filename` to the +repo-relative backslash form. `Coordinator Lines Valid` is the count of `line` elements selected by +the class-relative XPath `./lines/line`, and `Coordinator Lines Covered` is the count of those whose +`hits` attribute exceeds 0. The class-relative rollup is used rather than the descendant axis +because Cobertura repeats every line under `./methods/method/lines`, which would roughly double a +descendant count. No `lines-covered` or `lines-valid` attribute exists on a `class` node; those two +attributes are set on the root `coverage` node only. + +The repository line rate of 0.853792 is above the 0.80 floor that P4-T8 asserts and above the 0.85 +floor in `.claude/rules/general-unit-test.md`. + +Output Summary: Baseline test-and-coverage run passed. 6925 tests total, 6925 passed, 0 failed, no +failure set. Repository line rate 0.853792 (54968 of 64381 lines). Coordinator line rate 0.983122 +(233 of 237 lines). All six numeric coverage fields and all four run fields are present and numeric. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/typecheck-gate.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/typecheck-gate.2026-08-31T20-40.md new file mode 100644 index 000000000..2634cfb89 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/typecheck-gate.2026-08-31T20-40.md @@ -0,0 +1,42 @@ +# Baseline — Type-Check / Nullable Gate (Issue #656) + +Timestamp: 2026-09-01T14-38 +Task: [P0-T10] + +Gate Start: 2026-09-01T14:38:05.7352079-04:00 +Gate End: 2026-09-01T14:38:18.2526653-04:00 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\msbuild\p0-t9-typecheck.log;Verbosity=normal" +``` + +EXIT_CODE: 0 + +Command-shape verification (acceptance condition of this task): + +- The `Command:` string above contains `/t:Rebuild`. +- The `Command:` string above contains **no** `/p:Nullable=enable`. +- The `Command:` string above contains **no** `/t:Build`. + +Both omissions are deliberate and are required by `.claude/rules/csharp.md` and by CI parity. No +project in this repository carries a `` element and there is no `Directory.Build.props`, +so `/p:Nullable=enable` is a solution-wide opt-in that conscripts files which never adopted the +pragma and can never pass. `/t:Build` would let MSBuild's up-to-date check skip `CoreCompile` and +exit 0 without running the compiler, making the gate vacuous. + +Build summary: `5 Warning(s)` / `0 Error(s)`, elapsed 00:00:12.40. The five warnings are the +pre-existing System.Reactive `packages.config` diagnostic emitted once per affected project; they +are not promoted to errors because they are emitted by an imported targets file rather than by the +compiler. + +Nullable coverage of the file under change: `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` +carries `#nullable enable` on line 1, so it participates in nullable analysis and its `CS86xx` +diagnostics are promoted to errors by this gate. The baseline is therefore a genuine per-file +nullable gate for the file this item edits. + +Output Summary: Baseline type-check gate passed with `0 Error(s)` under +`/p:TreatWarningsAsErrors=true`. The command carries `/t:Rebuild` and carries neither +`/p:Nullable=enable` nor `/t:Build`. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/issue-updates/ac-status.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/issue-updates/ac-status.2026-08-31T20-40.md new file mode 100644 index 000000000..ccbd83e83 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/issue-updates/ac-status.2026-08-31T20-40.md @@ -0,0 +1,50 @@ +# Acceptance Criteria Status Summary (Issue #656) + +Timestamp: 2026-09-01T14-58 +Task: [P5-T21] + +- Work Mode: full-bug +- AC source: `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md` +- Total AC items: 20 +- Checked off (delivered and verified): 20 +- Remaining (unchecked): 0 +- Items remaining: none + +Verified in the source file after check-off: +`@(Select-String -Path spec.md -Pattern '^- \[x\] AC-\d+ ').Count` = 20 and +`@(Select-String -Path spec.md -Pattern '^- \[ \] AC-\d+ ').Count` = 0. + +## Per-criterion status and establishing evidence + +AC-1 PASS — hoisted local before the lock, guard narrowed to `if (_closeCompleted && !hostOpen)`. Evidence: `evidence/other/lock-discipline.2026-08-31T20-40.md`; hoist at line 326, first lock after the `CloseCore` declaration at 327, guard literal count 1 and old-guard count 0. +AC-2 PASS — no `_host`/`IBreadcrumbDropDownHost` call added inside any `lock (_sync)` body. Evidence: `evidence/other/lock-discipline.2026-08-31T20-40.md`; exactly one such call remains, the pre-existing `if (_closeInFlight && _host.IsOpen)` in `RequestOpen`. +AC-3 PASS — `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` exists in `Part3.cs` and asserts two `Uncommitted` entries. Evidence: `evidence/qa-gates/green-run.2026-08-31T20-40.md`. +AC-4 PASS — fail-before and pass-after both recorded, with both outputs present under `evidence/qa-gates/`. Evidence: `evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md`; two departures reconciled in its `AC-4 Reconciliation:` section. +AC-5 PASS — `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` passes and its file is absent from the diff. Evidence: `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. +AC-6 PASS — `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` passes and its file is absent from the diff. Evidence: `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. +AC-7 PASS — `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` passes and its file is absent from the diff. Evidence: `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. +AC-8 PASS — `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` passes and its file is absent from the diff. Evidence: `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. +AC-9 PASS — `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` passes, confirming a close while the host reports not open still reaches `_host.Close`. Evidence: `evidence/qa-gates/standing-guards.2026-08-31T20-40.md`. +AC-10 PASS — the only changed file under `QuickFiler/` is `BreadcrumbDropDownOpenCoordinator.cs`. Evidence: `evidence/qa-gates/footprint-production.2026-08-31T20-40.md`. +AC-11 PASS — no `.csproj`, `.props`, `.targets` or `packages.config` path in the change set. Evidence: `evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md`. +AC-12 PASS — the only changed file under `QuickFiler.Test/` is `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`. Evidence: `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. +AC-13 PASS — 395 and 213 lines, both under 500. Evidence: `evidence/qa-gates/file-size.2026-08-31T20-40.md`. +AC-14 PASS — `dotnet tool run csharpier check .` exits 0 over 1566 files with no file requiring formatting. Evidence: `evidence/qa-gates/format-check.2026-08-31T20-40.md`. +AC-15 PASS — analyzer gate reports `0 Error(s)` and no warning attributed to the changed file; post-change warning set is empty and a subset of the empty baseline set. Evidence: `evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md`. +AC-16 PASS — zero `Skipping target "CoreCompile"` lines, with both assembly write times later than the recorded gate start as the positive control. Evidence: the `Non-Vacuity:` section of `evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md`. +AC-17 PASS — type-check gate reports `0 Error(s)`; the command carries `/t:Rebuild` and neither `/p:Nullable=enable` nor `/t:Build`. Evidence: `evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md`. +AC-18 PASS — 6926 tests, 6926 passed, 0 failed, with `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` both confirmed at line 76 of the wrapper. Evidence: `evidence/qa-gates/test-coverage.2026-08-31T20-40.md`. +AC-19 PASS — both documentation blocks record the new suppression condition, and the `CloseCore` block records why the host read is taken outside `_sync`. Evidence: the P2-T3 and P2-T4 verification recorded in `evidence/other/lock-discipline.2026-08-31T20-40.md` and the file's two `Issue #656` documentation lines. +AC-20 PASS — no new `internal`/`public` member on the coordinator (count unchanged at 12) and `IBreadcrumbDropDownHost.cs` absent from the changed-file list. Evidence: `evidence/qa-gates/no-new-seam.2026-08-31T20-40.md`. + +## Footprint-base note + +AC-10, AC-11 and AC-12 are footprint criteria. They were evaluated against `origin/main` +(`5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723`), which is an ancestor of HEAD, rather than against the +plan's pinned base `2b85134b42872e405602e6064e02dc9cda6c319b`. The pinned base predates this +branch's reconciliation merge with `main` and therefore conflates 299 inherited paths with this +item's change set, including nine under `QuickFiler/` and `QuickFiler.Test/` and one `.csproj`. +Both measurements are recorded verbatim in each footprint artifact so the substitution is auditable. + +Output Summary: All 20 acceptance criteria are delivered, verified against named evidence, and +checked off in `spec.md`. None remain outstanding. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/commit.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/commit.2026-08-31T20-40.md new file mode 100644 index 000000000..8bb8256db --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/commit.2026-08-31T20-40.md @@ -0,0 +1,52 @@ +# Commit Record (Issue #656) + +Timestamp: 2026-09-01T14-54 +Task: [P4-T10] + +Command: +``` +git add QuickFiler QuickFiler.Test docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656 +git commit -m "fix(breadcrumb): stop suppressing a close while the host reports open (#656)" +git rev-parse HEAD +git status --porcelain -- QuickFiler QuickFiler.Test +``` + +EXIT_CODE: 0 + +Resulting commit id: `d3dd9fe00180c449c80b2771a4befcc474f512bb` + +Post-commit `git status --porcelain -- QuickFiler QuickFiler.Test` produced **no output**, which is +the acceptance condition: both production trees are clean and everything this change touched under +them is committed. + +## Staged-set verification before the commit + +`git diff --cached --name-only` listed 29 paths and no others: + +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` (the sole production file) +- `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (the sole test file) +- 26 evidence artifacts under this feature folder's `evidence/baseline/`, + `evidence/regression-testing/`, `evidence/qa-gates/` and `evidence/other/` +- this feature folder's `plan.2026-08-31T20-10.md`, carrying the task check-offs + +No path under `.claude/agent-memory/` and no path under `artifacts/orchestration/` appears in the +staged set. + +## Why the pathspec is scoped + +The `git add` names three explicit pathspecs rather than using `-A`, `.` or `-u`. Two categories of +tracked file in this repository would otherwise be swept into this commit and corrupt the change +footprint: `.claude/agent-memory/**`, which is tracked and is written concurrently by other +processes during a run, and `artifacts/orchestration/orchestrator-state.json`, which is tracked and +carries an unrelated item's checkpoint. Neither is part of this item's change set. No +`git update-index` command was run by this task or by any other task in this plan. + +## Purpose + +This commit exists so that the anchored three-dot diff assertions in P4-T11 through P4-T14 are +non-vacuous. A name-listing diff against a base ref reports committed changes only, so running those +assertions before this commit would have returned an empty list regardless of what the change +actually touched. + +Output Summary: Commit `d3dd9fe00180c449c80b2771a4befcc474f512bb` created from an explicitly scoped +staged set of 29 paths. The scoped production-tree status is clean after the commit. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/final-commit.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/final-commit.2026-08-31T20-40.md new file mode 100644 index 000000000..2dda84f00 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/final-commit.2026-08-31T20-40.md @@ -0,0 +1,57 @@ +# Final Commit Record (Issue #656) + +Timestamp: 2026-09-01T14-59 +Task: [P5-T22] + +Command: +``` +git add docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656 +git commit -m "docs(issue-656): record QA gate and acceptance evidence" +git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656 +``` + +EXIT_CODE: 0 + +Resulting commit id: `145ee2568b6e93089e2f5276da5b36c33d48b98f` + +The scoped `git status --porcelain` output at the moment the status command ran was **empty**, which +is the acceptance condition. + +## Staged set + +Eight paths, all inside this feature folder: + +- `evidence/issue-updates/ac-status.2026-08-31T20-40.md` +- `evidence/other/commit.2026-08-31T20-40.md` +- `evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md` +- `evidence/qa-gates/footprint-production.2026-08-31T20-40.md` +- `evidence/qa-gates/footprint-test.2026-08-31T20-40.md` +- `evidence/qa-gates/no-new-seam.2026-08-31T20-40.md` +- `plan.2026-08-31T20-10.md` (carrying the P4-T11..T14 and P5-T1..T21 check-offs) +- `spec.md` (carrying the AC-1 through AC-20 check-offs) + +No path under `.claude/agent-memory/` and no path under `artifacts/orchestration/` was staged. + +## Why the status assertion is scoped by pathspec + +`.claude/agent-memory/**` is tracked in this repository and is written concurrently by other +processes during a run, and `artifacts/orchestration/orchestrator-state.json` is tracked despite the +`artifacts/` entry in `.gitignore`. An unscoped clean-tree assertion would be unsatisfiable for +reasons unrelated to this item's change set. No `git update-index` command was run by this task or +by any other task in this plan. + +## Expected residuals after this task + +Two residuals exist by design and neither invalidates the acceptance above, which was evaluated at +the moment the status command ran and therefore before either existed: + +1. `plan.2026-08-31T20-10.md` is modified again, because the check-off for P5-T22 itself is written + after this task's commit. +2. `evidence/other/final-commit.2026-08-31T20-40.md` — this artifact — is created after the status + command ran. + +Both are committed by the orchestrator at its next checkpoint. + +Output Summary: Final evidence commit `145ee2568b6e93089e2f5276da5b36c33d48b98f` created from an +explicitly scoped staged set of eight feature-folder paths. The scoped tree was clean when the +status command ran. Two expected residuals remain, as the plan anticipates. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/lock-discipline.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/lock-discipline.2026-08-31T20-40.md new file mode 100644 index 000000000..faa03064c --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/lock-discipline.2026-08-31T20-40.md @@ -0,0 +1,121 @@ +# SR-4 Lock-Discipline Invariant After the Production Edit (Issue #656) + +Timestamp: 2026-09-01T14-45 +Task: [P2-T5] + +Command: +``` +Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s*[^/\s].*_host\.' +Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'lock (_sync)' +``` + +EXIT_CODE: 0 + +## Counts against the P0-T12 baseline + +| Measurement | Baseline (P0-T12) | Post-change | Expected | Match | +|---|---|---|---|---| +| Non-comment `_host.` lines | 5 | 6 | 6 (baseline 5 plus the one hoisted read) | yes | +| `lock (_sync)` occurrences | 12 | 12 | 12 (unchanged) | yes | + +## Enumerated non-comment `_host.` lines (6) + +- L119: `if (_closeInFlight && _host.IsOpen)` +- L200: `(!_host.IsOpen || !_host.Close(BreadcrumbDropDownCloseReason.Uncommitted))` +- L265: `? _host.OpenAsync(anchor, workingArea(), size)` +- L266: `: _host.OpenAsync(anchor, workingArea(), size, takeFocus: false);` +- L326: `bool hostOpen = _host.IsOpen;` <- the line added by this change +- L340: `closed = _host.Close(reason);` + +## Enumerated `lock (_sync)` lines (12) + +L91, L103, L113, L141, L154, L244, L327, L344, L349, L363, L377, L385. + +## Lock-body membership of each non-comment `_host.` line + +Determined by reading each enclosing region directly rather than by a brace-counting heuristic, +because a heuristic misclassified three of these lines on a first attempt. + +| Line | Enclosing member | Inside a `lock (_sync)` body? | Basis | +|---|---|---|---| +| L119 | `RequestOpen` | **yes** | The lock opens at L113 and closes at L124; L119 lies between them. | +| L200 | `Reset` | no | Inside the `_operations.PostAsync` lambda opened at L197. No lock is held there. | +| L265 | `OpenCoreAsync` | no | The lock opened at L244 closes at L254; L265 follows it. | +| L266 | `OpenCoreAsync` | no | Same block as L265, after the lock closed at L254. | +| L326 | `CloseCore` | no | It is the first statement of the method body (opening brace L325) and precedes the `lock (_sync)` at L327. | +| L340 | `CloseCore` | no | The lock opened at L327 closes at L336; L340 sits in the `try` block that follows. | + +**Exactly one** non-comment `_host.` line sits inside a `lock (_sync)` body: the pre-existing +`if (_closeInFlight && _host.IsOpen)` at L119 in `RequestOpen`. Every other such line, including the +line this change added, is outside every lock body. + +## Why this is the SR-4 invariant + +SR-4 of #501 declined the refinement written as an `_host.IsOpen` read taken *inside* `_sync`, on +the ground that it enlarges the set of foreign calls made while the coordinator's lock is held. +`IsOpen` is an interface member and the coordinator holds an `IBreadcrumbDropDownHost` rather than +the concrete class, so a substituted implementation could take its own lock or re-enter the +coordinator from inside `_sync`. + +This change places the read at L326, before the lock is acquired at L327, and only a `bool` local +crosses into the critical section. The count of foreign calls made while `_sync` is held is +therefore unchanged at one, and it is the same pre-existing call it was before the change. SR-4 is +neither overridden nor contradicted: its stated objection does not apply to the hoisted form, which +was not among the shapes SR-4 evaluated. + +## Note on the search pattern's coverage + +The pattern `^\s*[^/\s].*_host\.` requires a character before the `_host.` token on the same line, +so a line whose first non-whitespace token *is* `_host.` does not match it. One such line exists in +the file, `_host.Reset();` in the `Reset` continuation, and it is absent from both the baseline +count of 5 and the post-change count of 6. This does not affect the invariant: that line is inside +the same `_operations.PostAsync` lambda as L200 and is likewise outside every lock body, so the +"exactly one inside a lock" conclusion holds whether or not it is counted. The same pattern was used +for the baseline and for this measurement, so the delta of exactly one is a like-for-like +comparison. + +Output Summary: The lock-discipline invariant holds. Non-comment `_host.` lines went from 5 to 6, +exactly the one hoisted read; `lock (_sync)` count is unchanged at 12; and exactly one non-comment +`_host.` line sits inside a lock body, the pre-existing one at L119 in `RequestOpen`. No new foreign +call under `_sync` was introduced. + + +## Declared Member Lines: + +Task: [P2-T6] +Timestamp: 2026-09-01T14-46 + +Command: +``` +Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s' +``` + +EXIT_CODE: 0 + +Count: **12**, unchanged from the P0-T12 baseline of 12. + +Enumerated lines: + +- L12: `internal sealed class BreadcrumbDropDownOpenCoordinator` +- L58: `internal BreadcrumbDropDownOpenCoordinator(` +- L85: `internal IBreadcrumbDropDownHost Host => _host;` +- L87: `internal Task CurrentOpenTask` +- L96: `internal void UpdateRequestProviders(` +- L111: `internal Task RequestOpen()` +- L139: `internal void LatchNextOpenTakesNoFocus()` +- L150: `internal bool NextOpenTakesNoFocus` +- L159: `internal void SetDroppedDown(bool droppedDown)` +- L178: `internal void HandleSelectorOpenStateChanged()` +- L193: `internal void Reset()` +- L209: `internal void Release()` + +The member set is identical to the baseline set; only the line numbers shifted, by the number of +documentation lines this change inserted above each declaration. No `internal` or `public` member +was added, so no new production seam was introduced. `CloseCore` remains `private`, and the change +adds only a method-local `bool`. + +The pattern excludes XML documentation lines because a `///` line's first non-whitespace character +is a forward slash, which `^\s+(internal|public)\s` cannot match. The `remarks` blocks added by +P2-T3 and P2-T4 therefore cannot inflate this count. + +Output Summary: Declared member count is 12, unchanged from baseline. No new production seam. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md new file mode 100644 index 000000000..48e73feee --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md @@ -0,0 +1,82 @@ +# QA Gate — Analyzer Gate (Issue #656) + +Timestamp: 2026-09-01T14-50 +Task: [P4-T3] (toolchain loop pass 1, step 2) +Satisfies: AC-15 (together with the P4-T5 section below), AC-16 (the Non-Vacuity section below) + +Gate Start: 2026-09-01T14:49:50.4380965-04:00 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\msbuild\p4-t3-analyzer.log;Verbosity=normal" +``` + +EXIT_CODE: 0 + +Acceptance measurement: +`@(Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch '0 Error(s)').Count` = **1**, +which is greater than 0 as required. Elapsed 00:00:11.80. + +Output Summary: Analyzer gate passed. `0 Error(s)` under `/p:EnableNETAnalyzers=true` +`/p:EnforceCodeStyleInBuild=true` with `/t:Rebuild`. No analyzer diagnostic is attributed to the +changed production file. + +--- + +## Non-Vacuity: + +Task: [P4-T4] +Satisfies: AC-16 + +Measurements: + +- `@(Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch 'Skipping target "CoreCompile"').Count` = **0** +- `(Get-Item QuickFiler\bin\Debug\QuickFiler.dll).LastWriteTime` = `2026-09-01T14:49:56.4605766-04:00` +- `(Get-Item QuickFiler.Test\bin\Debug\QuickFiler.Test.dll).LastWriteTime` = `2026-09-01T14:49:58.6941383-04:00` + +Both assembly timestamps are later than the `Gate Start:` value of +`2026-09-01T14:49:50.4380965-04:00` — by roughly 6.0 and 8.3 seconds respectively. + +Why both measurements are needed: the zero skip-count is the assertion AC-16 states, but a zero +count is also what an empty log, a mis-scoped log, or a log written by a build that never ran would +produce. The two `LastWriteTime` values are the positive control. They prove both assemblies were +actually recompiled inside this gate's window, so the zero count reports a genuinely absent +`Skipping target "CoreCompile"` line rather than an absent log. The changed files therefore really +were compiled and really were seen by the analyzers. + +`/t:Rebuild` is what makes this hold. 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 at all. + +--- + +## Post-Change Warning Codes For BreadcrumbDropDownOpenCoordinator.cs: + +Task: [P4-T5] +Satisfies: AC-15 (together with the P4-T3 result above) + +none + +Derivation: +``` +Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch 'BreadcrumbDropDownOpenCoordinator.cs' | Select-String -SimpleMatch 'warning' +``` +returned a match count of **0**, and the matched-line list is empty. + +Subset comparison required by this task: + +| Set | Value | +|---|---| +| `Baseline Warning Codes For BreadcrumbDropDownOpenCoordinator.cs:` (from `evidence/baseline/analyzer-gate.2026-08-31T20-40.md`) | none (empty set) | +| `Post-Change Warning Codes For BreadcrumbDropDownOpenCoordinator.cs:` | none (empty set) | + +The empty set is a subset of the empty set, so the acceptance condition holds. The change introduced +no new warning attributed to the changed production file. This is the strongest form the comparison +can take: because the baseline set was empty, any single new warning on the changed file would have +broken the subset relation. + +Solution-wide warning count is unchanged from the Phase 0 baseline; the pre-existing System.Reactive +`packages.config` diagnostics are the only warnings the build reports, and none of them names a file +in this item's footprint. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/coverage-delta.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/coverage-delta.2026-08-31T20-40.md new file mode 100644 index 000000000..c11383d7d --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/coverage-delta.2026-08-31T20-40.md @@ -0,0 +1,91 @@ +# QA Gate — Changed-Line Coverage and Coverage Delta (Issue #656) + +Timestamp: 2026-09-01T14-53 +Task: [P4-T8] + +Command: +``` +$A = (Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'bool hostOpen = _host.IsOpen;').LineNumber +$B = (Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted && !hostOpen)').LineNumber +[xml]$c = Get-Content -LiteralPath coverage\coverage.cobertura.xml -Raw -Encoding UTF8 +# hits of the ./lines/line nodes with number = $A and $B, under the class node whose +# filename = 'QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs' +``` + +EXIT_CODE: 0 + +## Changed-line coverage + +Both line numbers are derived mechanically from the file itself rather than asserted, so a later +reformat cannot invalidate them. + +- Changed Line A: **326** — `bool hostOpen = _host.IsOpen;` +- Changed Line A Hits: **1** +- Changed Line B: **333** — `if (_closeCompleted && !hostOpen)` +- Changed Line B Hits: **1** + +Both hit counts are greater than or equal to 1, which is the acceptance condition. Exactly one +`line` node matched each number under the coordinator's single `class` node, so neither figure is an +artifact of duplicate node selection. Changed-line coverage is therefore 100 percent: both lines +this change introduced are executed by the suite. + +Both outcomes of the new conjunct are exercised, not merely both lines: `!hostOpen == true` +(suppression retained) by the standing guards +`PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose`, +`SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` and +`CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce`; and `!hostOpen == false` (suppression +released) by the new regression test +`CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain`. All four passed in P3-T4 and +P3-T2. + +## Coverage delta + +| Metric | Baseline (P0-T11) | Post-change (P4-T7) | Direction | +|---|---|---|---| +| Baseline Repo Line Rate | 0.853792 | — | — | +| Post-Change Repo Line Rate | — | 0.853732 | -0.000060 | +| Baseline Coordinator Line Rate | 0.983122 | — | — | +| Post-Change Coordinator Line Rate | — | 0.983193 | +0.000071 | + +Acceptance conditions: + +- Post-change repo line rate **0.853732 >= 0.80**: satisfied, with substantial margin. It also + remains above the 0.85 floor in `.claude/rules/general-unit-test.md`. +- Post-change coordinator line rate **0.983193 >= baseline 0.983122**: satisfied. The coordinator + rate rose, so **PASS is recorded on the first measurement** and the conditional single repeat run + authorized by this task was **not** executed. `TestResults\p4-t8-repeat\` remains empty and no + `Repeat Coordinator Line Rate:` value exists, because no second measurement was needed. + +The repository rate moved down by 6.0e-5, which is six thousandths of one percentage point. This is +within the per-run nondeterminism band this repository exhibits for `lines-covered` and is not a +coverage regression on the changed lines: both changed lines are covered, as recorded above. The +gate that this task actually applies to the repository figure is the 0.80 floor, which is met. + +## Lines-valid delta — the deterministic quantity + +- Baseline Coordinator Lines Valid: **237** +- Post-Change Coordinator Lines Valid: **238** + +The post value equals the baseline value plus **exactly one**, which is the required relation. This +is the deterministic measurement and it is what a genuine instrumented-size change would move: the +change adds exactly one executable statement to the coordinator, `bool hostOpen = _host.IsOpen;`. +The narrowed guard replaced an existing statement in place and the two `remarks` blocks are XML +documentation, so neither adds an instrumented line. The observed difference is exactly one, so the +`LINES-VALID DELTA UNEXPECTED` branch of this task was not taken. + +Per-file `lines-covered` is not deterministic in this repository between two runs against the same +tree, while `lines-valid` is; the measurement supporting that is recorded in +`.claude/agent-memory/orchestrator/coverage-lines-covered-is-nondeterministic.md`, where two +Cobertura documents of the same tree carry identical `lines-valid` for all 550 files while per-file +`lines-covered` moves by up to four lines. The coordinator-rate comparison above is therefore +treated as a candidate signal rather than a hard gate on a single measurement, exactly as this task +specifies; it happened to pass on the first measurement, so that distinction did not need to be +exercised. + +Coordinator covered lines moved from 233 to 234, consistent with the one added statement being +covered. + +Output Summary: Both changed lines are covered (hits 1 and 1, at lines 326 and 333). Post-change +repository line rate 0.853732 is above the 0.80 floor. Post-change coordinator line rate 0.983193 is +at or above the baseline 0.983122, so PASS was recorded on the first measurement and no repeat run +was executed. Coordinator `lines-valid` moved from 237 to 238, exactly the expected delta of one. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/file-size.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/file-size.2026-08-31T20-40.md new file mode 100644 index 000000000..bd28969b1 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/file-size.2026-08-31T20-40.md @@ -0,0 +1,42 @@ +# QA Gate — File-Size Limit (Issue #656) + +Timestamp: 2026-09-01T14-53 +Task: [P4-T9] +Satisfies: AC-13 + +Command: +``` +(Get-Content -LiteralPath QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs).Count +(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs).Count +``` + +EXIT_CODE: 0 + +## Measured line counts + +| File | Baseline (P0-T12) | Post-change | Limit | Under limit | +|---|---|---|---|---| +| `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` | 378 | **395** | 500 | yes, by 105 lines | +| `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` | 173 | **213** | 500 | yes, by 287 lines | + +Both counts are strictly less than 500, satisfying the file-size limit in +`.claude/rules/general-code-change.md` and AC-13. + +Growth accounting: + +- The coordinator grew by 17 lines: one hoisted statement, a 7-line `remarks` block on the + `_closeCompleted` field, and a 9-line `remarks` block on `CloseCore`. The narrowed guard replaced + an existing line in place and added none. +- `Part3.cs` grew by 40 lines: the added regression test method with its XML documentation. + +This task runs after the final format pass in P4-T1, so both counts measure the CSharpier-formatted +files and no later formatting can change them. + +Note on file placement: the regression test was appended to `Part3.cs` rather than to +`BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` precisely because of this limit. `Part2.cs` stands +at 455 lines, so a roughly 40-line test would have brought it to about 495 — within a few lines of +the ceiling. `Part3.cs` is the same `public sealed partial class` and shares the same fixtures, so +the test lands in the correct class either way and no new file was created. + +Output Summary: Both files in the authorized footprint are under the 500-line limit after the +change: 395 and 213 lines. AC-13 is satisfied. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md new file mode 100644 index 000000000..163686a89 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md @@ -0,0 +1,65 @@ +# QA Gate — Build-Configuration Footprint (Issue #656) + +Timestamp: 2026-09-01T14-55 +Task: [P4-T12] +Satisfies: AC-11 + +Command (authoritative): +``` +git diff --name-only origin/main...HEAD -- '*.csproj' '*.props' '*.targets' '*packages.config' +git status --porcelain -- '*.csproj' '*.props' '*.targets' '*packages.config' +``` + +EXIT_CODE: 0 + +## Authoritative diff output (base `origin/main`, verbatim) + +``` +``` + +(empty) + +## Porcelain output (verbatim) + +``` +``` + +(empty) + +Both outputs are empty, which is the acceptance condition. AC-11 is satisfied: no `.csproj`, +`.props`, `.targets` or `packages.config` path appears in this item's change set, and none is +modified or untracked in the working tree. + +This is a meaningful result rather than a trivially empty one. Two mechanisms could have added a +build-configuration file to this change set and neither did: + +- The new test method was appended to an **existing** test file, + `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`. Creating a new `.cs` + file would have required a `` entry in `QuickFiler.Test.csproj`, because these + are legacy non-SDK projects that enumerate their sources explicitly. Appending to the existing + partial avoided that entirely. +- The plan's execution preconditions forbid `scripts/vscode/Invoke-VSBuild.ps1`, which calls + `Sync-PackageReferences.ps1` over every `.csproj` and rewrites `HintPath` values. Every msbuild + invocation in this run resolved msbuild through vswhere directly instead, so no build script + rewrote a project file as a side effect. The NuGet restore in P0-T4 used + `scripts/vscode/Invoke-Restore.ps1`, which populates `packages/` without editing project files. + +## Base-ref substitution (recorded, not silent) + +Against the plan's stale pinned base the same query is **not** empty: + +``` +git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- '*.csproj' '*.props' '*.targets' '*packages.config' +QuickFiler.Test/QuickFiler.Test.csproj +``` + +That single path is a pre-existing change that arrived on `main` and was merged into this branch +before execution began; it is not this item's work. Its presence is precisely why the pinned base +cannot be used to evaluate AC-11: it would report a build-configuration edit that this item did not +make. `origin/main` (`5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723`, an ancestor of HEAD) isolates this +branch's own contribution and is used as authoritative. Both measurements are recorded so the +substitution is auditable. + +Output Summary: Build-configuration footprint verified empty against `origin/main` for both the +anchored diff and the porcelain status. AC-11 is satisfied. The plan's pinned base reports one +unrelated `.csproj` inherited from `main`, recorded here for audit. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-production.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-production.2026-08-31T20-40.md new file mode 100644 index 000000000..ee54850da --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-production.2026-08-31T20-40.md @@ -0,0 +1,65 @@ +# QA Gate — Production Footprint (Issue #656) + +Timestamp: 2026-09-01T14-55 +Task: [P4-T11] +Satisfies: AC-10 + +Command (authoritative): +``` +git diff --name-only origin/main...HEAD -- QuickFiler +git status --porcelain -- QuickFiler +``` + +EXIT_CODE: 0 + +## Authoritative diff output (base `origin/main`, verbatim) + +``` +QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs +``` + +## Porcelain output (verbatim) + +``` +``` + +(empty) + +The diff output is exactly the single line `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` +and the porcelain output is empty. AC-10 is satisfied: no file under `QuickFiler/` other than the +sole authorized production file appears in the change set. + +The porcelain span is the required companion because a name-listing diff enumerates tracked changes +only and can never report a newly created untracked file. Its emptiness confirms there is no +untracked or uncommitted file under `QuickFiler/` that the diff would have missed. + +## Base-ref substitution (recorded, not silent) + +The plan anchors this assertion to the pinned base `2b85134b42872e405602e6064e02dc9cda6c319b`. That +base is stale: it predates the reconciliation of this branch against `origin/main`, and because it +is an ancestor of HEAD the three-dot form degenerates to a plain two-dot diff against it, which +conflates every change `main` gained in the interval with this item's change set. Measured here: + +``` +git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- QuickFiler +QuickFiler/Controllers/FilerQueue.cs +QuickFiler/Controllers/QfcFormController.EventHandlers.cs +QuickFiler/Controllers/QfcHomeController.Metrics.cs +QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs +``` + +Three of those four paths are pre-existing changes that arrived on `main` and were merged into this +branch before execution began. None was touched by this item; the working tree confirms it, since +the scoped porcelain output is empty and the staged set recorded in +`evidence/other/commit.2026-08-31T20-40.md` names only +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` under `QuickFiler/`. + +`origin/main` resolves to `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` and is an ancestor of HEAD, so +`origin/main...HEAD` isolates this branch's own contribution. It is therefore the correct footprint +base and is used as authoritative here. Both measurements are recorded above so the substitution is +auditable. The same substitution applies to P4-T12, P4-T13 and P4-T14. + +Output Summary: Production footprint verified. Against `origin/main`, exactly one file under +`QuickFiler/` changed: `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`. The scoped +porcelain output is empty. AC-10 is satisfied. The plan's pinned base is stale and its output is +recorded alongside for audit. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-test.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-test.2026-08-31T20-40.md new file mode 100644 index 000000000..07727df32 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-test.2026-08-31T20-40.md @@ -0,0 +1,82 @@ +# QA Gate — Test Footprint (Issue #656) + +Timestamp: 2026-09-01T14-56 +Task: [P4-T13] +Satisfies: AC-12; together with P3-T4 also satisfies AC-5, AC-6, AC-7 and AC-8 + +Command (authoritative): +``` +git diff --name-only origin/main...HEAD -- QuickFiler.Test +git status --porcelain -- QuickFiler.Test +``` + +EXIT_CODE: 0 + +## Authoritative diff output (base `origin/main`, verbatim) + +``` +QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs +``` + +## Porcelain output (verbatim) + +``` +``` + +(empty) + +The diff output is exactly the single line +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` and the porcelain output +is empty. AC-12 is satisfied: no file under `QuickFiler.Test/` other than the sole authorized test +file appears in the change set. + +## Mechanical proof that the standing guards were not edited + +Neither `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs` nor +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` appears in the diff +output. Those two files hold all five standing-guard tests: + +| Standing guard | File | AC | +|---|---|---| +| `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` | `BreadcrumbDropDownOpenCoordinatorTests.cs` | AC-5 | +| `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` | `BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` | AC-6 | +| `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` | `BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` | AC-7 | +| `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` | `BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` | AC-8 | +| `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` | `BreadcrumbDropDownOpenCoordinatorTests.cs` | AC-9 | + +Because neither file is in the change set, no assertion text in any of those tests was altered. That +is the "unchanged in the diff" half of AC-5 through AC-8. The "passes" half is established by +`evidence/qa-gates/standing-guards.2026-08-31T20-40.md`, where all five ran and all five passed. +The two artifacts together satisfy AC-5, AC-6, AC-7 and AC-8; the pass alone satisfies AC-9. + +This matters because the remedy was chosen specifically to avoid a regression trade. Options that +cleared `_closeCompleted` on the successful-close path would have required editing the very tests +listed above; the fact that those files are absent from the diff is the evidence that no such trade +was made. + +## Base-ref substitution (recorded, not silent) + +Against the plan's stale pinned base the same query lists seven paths: + +``` +git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- QuickFiler.Test +QuickFiler.Test/Controllers/FilerQueueTests.cs +QuickFiler.Test/Controllers/QfcFormControllerUndoHandoffTests.cs +QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs +QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs +QuickFiler.Test/Controllers/WpfUiDispatcherTests.cs +QuickFiler.Test/QuickFiler.Test.csproj +QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs +``` + +Six of those seven are pre-existing changes inherited from `main` through the pre-execution +reconciliation merge; none was touched by this item. `origin/main` +(`5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723`, an ancestor of HEAD) isolates this branch's own +contribution and is used as authoritative. Both measurements are recorded so the substitution is +auditable. + +Output Summary: Test footprint verified. Against `origin/main`, exactly one file under +`QuickFiler.Test/` changed: +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`. The scoped porcelain +output is empty. Neither file holding a standing guard appears. AC-12 is satisfied, and AC-5 through +AC-8 are satisfied jointly with the standing-guards run. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-apply.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-apply.2026-08-31T20-40.md new file mode 100644 index 000000000..bf8992bd4 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-apply.2026-08-31T20-40.md @@ -0,0 +1,43 @@ +# QA Gate — Format Apply (Issue #656) + +Timestamp: 2026-09-01T14-49 +Task: [P4-T1] (toolchain loop pass 1, step 1) + +Command: +``` +dotnet tool run csharpier format . +git status --porcelain -- QuickFiler QuickFiler.Test +``` + +EXIT_CODE: 0 + +Command output: `Formatted 1566 files in 4759ms.` + +## Porcelain After Format: + +``` + M QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs + M QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs +``` + +The output contains exactly the two lines naming +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, and no other path. Both +carry the ` M` status, meaning modified-in-worktree and not staged. No untracked file and no +additional modified file appears under either production tree. + +## Why the tree observation is recorded in addition to the exit code + +`csharpier format` is a write-mode command: it exits 0 whether or not it rewrote a file, and its +summary line reports the number of files *checked*, not the number changed. The exit code alone +therefore cannot distinguish a clean run from a repairing one. The porcelain span is the +observation that carries the real signal, and it confirms the change footprint is still the two +authorized files after the formatter ran across the whole tree. + +The porcelain span is scoped by pathspec to `QuickFiler` and `QuickFiler.Test` because +`.claude/agent-memory` is tracked in this repository and `artifacts/orchestration/orchestrator-state.json` +is tracked despite the `artifacts/` entry in `.gitignore`. An unscoped porcelain assertion would be +unsatisfiable for reasons unrelated to this item's change set. + +Output Summary: Format applied across 1566 files, exit 0. The scoped porcelain output lists exactly +the two authorized files and nothing else. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-check.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-check.2026-08-31T20-40.md new file mode 100644 index 000000000..0fbfbb9b6 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-check.2026-08-31T20-40.md @@ -0,0 +1,40 @@ +# QA Gate — Format Verify, Read-Only (Issue #656) + +Timestamp: 2026-09-01T14-49 +Task: [P4-T2] (toolchain loop pass 1, step 1 verification) +Satisfies: AC-14 + +Command: +``` +dotnet tool run csharpier check . +``` + +EXIT_CODE: 0 + +Output Summary: `Checked 1566 files in 4732ms.` — the final summary line of the command output, +transcribed verbatim. CSharpier reported no file requiring formatting and exited 0, which is what +AC-14 requires. + +The command is invoked through `dotnet tool run` so the manifest-pinned CSharpier 1.2.6 is the +version that runs, matching the version CI uses after `dotnet tool restore`. A globally installed +CSharpier of a different version would produce diffs that disagree with the CI format step. + +## Loop-restart determination + +`check` is read-only and rewrote nothing, so this step did not trigger a restart of the toolchain +loop. The preceding write-mode `format` step also required no restart: the literals this change +introduced survived it byte-for-byte, verified immediately after the format pass by + +``` +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'bool hostOpen = _host.IsOpen;').Count = 1 +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted && !hostOpen)').Count = 1 +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count = 0 +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'Issue #656').Count = 2 +``` + +Each of the four values equals the value asserted by the Phase 2 tasks, so the formatter did not +reflow any asserted literal onto a second line and the Phase 2 acceptance conditions still hold +against the formatted files. Post-format line counts are 395 for the coordinator and 213 for +`Part3.cs`, both under the 500-line limit; those are the counts recorded by P4-T9. + +Output Summary: Format gate passes read-only with exit 0 across 1566 files. AC-14 is satisfied. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-build.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-build.2026-08-31T20-40.md new file mode 100644 index 000000000..754f91467 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-build.2026-08-31T20-40.md @@ -0,0 +1,25 @@ +# QA Gate — Rebuild After the Production Edit (Issue #656) + +Timestamp: 2026-09-01T14-47 +Task: [P3-T1] + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=TestResults\msbuild\p3-t1-build.log;Verbosity=normal" +``` + +EXIT_CODE: 0 + +Results: `5 Warning(s)` / `0 Error(s)`, elapsed 00:00:11.94. The warning count is identical to the +Phase 0 baselines and to the pre-edit build in P1-T2, and all five are the pre-existing +System.Reactive `packages.config` diagnostic. The production edit and the two `remarks` blocks +introduced no compiler warning and no analyzer diagnostic at this stage. + +The two `remarks` blocks use ``, `` and +`Invalidate`. A `cref` that failed to resolve would raise CS1574, so the clean build confirms +both cross-references resolve to real members of the class. + +Output Summary: Solution rebuilt successfully with 0 errors after the `CloseCore` edit and the +documentation updates. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-run.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-run.2026-08-31T20-40.md new file mode 100644 index 000000000..c4ad0b76f --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-run.2026-08-31T20-40.md @@ -0,0 +1,39 @@ +# QA Gate — Pass-After Run (Issue #656) + +Timestamp: 2026-09-01T14-47 +Task: [P3-T2] + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 +New-Item -ItemType Directory -Force -Path 'TestResults\p3-t2' | Out-Null +& $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p3-t2' +``` + +This command is identical to the P1-T3 red-run command apart from the results directory. + +EXIT_CODE: 0 + +## TRX counter values + +- `total` = 1 +- `passed` = 1 +- `failed` = 0 + +Read from `TestRun/ResultSummary/Counters` of the TRX written to +`/TestResults/p3-t2/__2026-09-01_14_46_44_net481.trx`. + +Per-test outcome from the TRX `UnitTestResult` node: +`Passed :: CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` + +Console summary: `Test Run Successful.` / `Total tests: 1` / `Passed: 1`. + +The only change between the red run and this run is the Phase 2 edit to +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`: the hoisted `bool hostOpen = _host.IsOpen;` +and the narrowed guard `if (_closeCompleted && !hostOpen)`. The test text was not altered between +the two runs, and no other file was touched, so the transition from failing to passing is +attributable to the production fix alone. + +Output Summary: The new regression test passes after the production edit. Exit code 0, 1 total, 1 +passed, 0 failed. Together with the P1-T3 red run this establishes the fail-before / pass-after pair. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/no-new-seam.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/no-new-seam.2026-08-31T20-40.md new file mode 100644 index 000000000..e66d98dd2 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/no-new-seam.2026-08-31T20-40.md @@ -0,0 +1,55 @@ +# QA Gate — No New Production Seam (Issue #656) + +Timestamp: 2026-09-01T14-56 +Task: [P4-T14] +Satisfies: AC-20 + +Command (authoritative): +``` +git diff --name-only origin/main...HEAD -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs +git status --porcelain -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs +@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s').Count +``` + +EXIT_CODE: 0 + +## Results + +| Check | Required | Observed | Met | +|---|---|---|---| +| `git diff --name-only origin/main...HEAD -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs` | empty | empty | yes | +| `git status --porcelain -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs` | empty | empty | yes | +| Declared `internal`/`public` member count in the coordinator | 12 | **12** | yes | + +The declared-member count is unchanged from the P0-T12 baseline of 12, and the same twelve +declarations are present; only their line numbers shifted, by the number of documentation lines the +change inserted above them. The enumeration is recorded under `Declared Member Lines:` in +`evidence/other/lock-discipline.2026-08-31T20-40.md`. + +Both halves of AC-20 therefore hold: + +- **No new member on `BreadcrumbDropDownOpenCoordinator`.** The count is identical and the member + set is identical. The change adds one method-local `bool` inside a `private` method; `CloseCore` + itself remains `private`. The search pattern cannot be inflated by the two `remarks` blocks + because a `///` line's first non-whitespace character is a forward slash, which + `^\s+(internal|public)\s` cannot match. +- **No member added to `IBreadcrumbDropDownHost`.** The interface file is absent from the changed + file list and is clean in the working tree. + +No new seam was needed because `[assembly: InternalsVisibleTo("QuickFiler.Test")]` already exists in +`QuickFiler/Properties/AssemblyInfo.cs` and the test host already exposes the required bypass +through `ControlledHost.SetOpen`. The regression test uses only members that existed on the +unmodified tree, which is also why it compiled cleanly in P1-T2 before any production change. + +## Base-ref substitution (recorded, not silent) + +For this task the two bases agree: the pinned base +`2b85134b42872e405602e6064e02dc9cda6c319b` also returns an empty diff for +`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs`, because that file was not touched by this item nor +by the changes inherited from `main`. The authoritative base remains `origin/main` +(`5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723`) for consistency with P4-T11 through P4-T13; here the +substitution changes nothing. + +Output Summary: No new production seam. The interface file is absent from the change set and clean +in the tree, and the coordinator's declared `internal`/`public` member count is unchanged at 12. +AC-20 is satisfied. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md new file mode 100644 index 000000000..5f277b64d --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md @@ -0,0 +1,108 @@ +# QA Gate — Fail-Before / Pass-After Comparison (Issue #656) + +Timestamp: 2026-09-01T14-48 +Task: [P3-T3] + +Test under comparison: `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` + +Source artifacts named by this comparison: + +- Red run: `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md` +- Green run: `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-run.2026-08-31T20-40.md` + +## Result pair + +| Run | Phase | `total` | `passed` | `failed` | Exit | +|---|---|---|---|---|---| +| Red (before the production edit) | P1-T3 | 1 | **0** | **1** | 1 | +| Green (after the production edit) | P3-T2 | 1 | **1** | **0** | 0 | + +The red run recorded `failed=1, passed=0`; the green run recorded `failed=0, passed=1`. Both are for +the same test name and the same test assembly, under commands identical apart from the results +directory. + +--- + +## Embedded verbatim blocks — red artifact + +Source: `evidence/regression-testing/red-run.2026-08-31T20-40.md` + +Timestamp: 2026-09-01T14-42 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 +New-Item -ItemType Directory -Force -Path 'TestResults\p1-t3' | Out-Null +& $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p1-t3' +``` + +EXIT_CODE: 1 +ExpectedExitCode: 1 + +Output Summary: The new regression test ran alone and failed as expected. Exit code 1, which equals +the declared expectation, so this gate is normalized to pass. The TRX reports 1 total, 0 passed, 1 +failed. The failure is a runtime assertion failure, not a compile failure: the assembly built +cleanly in P1-T2 against unmodified production code. + +Failure message from the red TRX: +``` +Expected harness.Host.CloseReasons to be equal to {BreadcrumbDropDownCloseReason.Uncommitted {value: 1}, BreadcrumbDropDownCloseReason.Uncommitted {value: 1}} because the close after a bypassing reopen must reach _host.Close a second time, but {BreadcrumbDropDownCloseReason.Uncommitted {value: 1}} contains 1 item(s) less. +``` + +--- + +## Embedded verbatim blocks — green artifact + +Source: `evidence/qa-gates/green-run.2026-08-31T20-40.md` + +Timestamp: 2026-09-01T14-47 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 +New-Item -ItemType Directory -Force -Path 'TestResults\p3-t2' | Out-Null +& $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p3-t2' +``` + +EXIT_CODE: 0 + +Output Summary: The new regression test passes after the production edit. Exit code 0, 1 total, 1 +passed, 0 failed. Together with the P1-T3 red run this establishes the fail-before / pass-after pair. + +--- + +## AC-4 Reconciliation: + +AC-4 states the pair should be recorded "in the feature evidence folder under `evidence/qa-gates/`" +and checked "by comparing the two recorded `Invoke-MSTestWithCoverage.ps1` outputs for that test +name". Two departures from that stated check method were necessary. Each is recorded here with its +reason. + +**(a) Storage location of the red run.** The red run is stored under +`evidence/regression-testing/`, not under `evidence/qa-gates/`. That is the canonical fail-before +location required by `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`, whose evidence +path scheme is non-overridable and which directs a fail-before artifact search at +`/evidence/regression-testing/`. Writing the red run to `evidence/qa-gates/` instead would +place it outside the location a later audit searches for fail-before evidence. This artifact +resolves the tension by embedding the red run's `Timestamp:`, `Command:`, `EXIT_CODE:` and +`Output Summary:` blocks verbatim above, so both run outputs are present under `evidence/qa-gates/` +exactly as AC-4 requires, while the authoritative red artifact remains in its canonical folder. + +**(b) Runner used for both single-test runs.** Both runs use `vstest.console.exe` directly rather +than `scripts/vscode/Invoke-MSTestWithCoverage.ps1`. Neither wrapper accepts a `TestCaseFilter` +override — `scripts/vscode/Invoke-MSTest.ps1:54` and +`scripts/vscode/Invoke-MSTestWithCoverage.ps1:76` each pin the filter — and editing either script is +outside this item's authorized two-file footprint. A wrapper run would therefore have executed the +entire suite, which cannot exit 0 while a test is deliberately failing and so could not have +produced a scoped red record at all. Both wrapper protections are reproduced explicitly in the +direct invocation: `/InIsolation` is passed, and `TestCategory!=LiveOutlook` is the first conjunct +of the filter, so no real Outlook process can be launched. The full-suite wrapper run that AC-18 +requires is executed separately in P4-T7 and covers this test along with every other. + +Output Summary: The fail-before / pass-after pair is established for +`CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain`: failed=1/passed=0 before the +production edit, failed=0/passed=1 after it. Both source artifacts are named and both run outputs +are embedded verbatim. Two departures from AC-4's stated check method are recorded above with their +reasons. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/standing-guards.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/standing-guards.2026-08-31T20-40.md new file mode 100644 index 000000000..0a4f1bd88 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/standing-guards.2026-08-31T20-40.md @@ -0,0 +1,55 @@ +# QA Gate — Standing-Guard Regression Run (Issue #656) + +Timestamp: 2026-09-01T14-48 +Task: [P3-T4] + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 +New-Item -ItemType Directory -Force -Path 'TestResults\p3-t4' | Out-Null +& $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&(FullyQualifiedName~PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose|FullyQualifiedName~SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired|FullyQualifiedName~RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync|FullyQualifiedName~CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce|FullyQualifiedName~PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen)' '/Logger:trx' '/ResultsDirectory:TestResults\p3-t4' +``` + +EXIT_CODE: 0 + +## TRX counter values + +- `total` = 5 +- `passed` = 5 +- `failed` = 0 + +Console summary: `Test Run Successful.` / `Total tests: 5` / `Passed: 5`. + +## Per-test outcomes (from the TRX `UnitTestResult` nodes) + +| Test | Outcome | Contract it guards | +|---|---|---| +| `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` | Passed | Repeated-close suppression while the host open is still pending. `hostOpen` is `false` on the second drive, the added conjunct is `true`, and suppression is retained. | +| `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` | Passed | Repeated-close suppression after an accepted close. `ControlledHost.Close` sets `IsOpen = false`, so `hostOpen` is `false` on the second drive and suppression is retained. | +| `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` | Passed | The `RequestOpen` path after the same `SetOpen(true)` bypass the new test uses. Exercises `RequestOpen`, not `CloseCore`, and is unaffected by the change. | +| `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` | Passed | The standing guard that rules out clearing the flag on the successful-close path. No reopen occurs, `hostOpen` is `false`, and the close reaches the host exactly once. | +| `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` | Passed | Closing while the host reports not open is required behavior. `_closeCompleted` is `false` on that first close, so the added conjunct cannot suppress it. This is why a bare `!_host.IsOpen` gate would have been wrong. | + +All five recorded names appear in the list above, and every outcome is `Passed`. + +## Filter shape + +The `FullyQualifiedName` disjunction is parenthesised so that the `TestCategory!=LiveOutlook` +conjunct applies to the whole group rather than binding only to the final disjunct. Without the +parentheses the category exclusion would have covered one test out of five, and the remaining four +could have selected a `LiveOutlook`-categorised test. `/InIsolation` is passed as well, matching the +wrapper's protections. + +## Relationship to the footprint proof + +Passing these five tests shows the guards were not broken. That they were not *edited* is proved +separately and mechanically by P4-T13: the test-footprint diff lists only +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, and neither +`BreadcrumbDropDownOpenCoordinatorTests.cs` nor `BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` +appears, which is where four of these five tests live. The two together satisfy AC-5 through AC-8; +this artifact alone satisfies AC-9. + +Output Summary: All five standing-guard tests pass after the production change. Exit code 0, 5 +total, 5 passed, 0 failed. No repeated-close suppression contract regressed, and the required close +while the host reports not open still reaches the host. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/test-coverage.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/test-coverage.2026-08-31T20-40.md new file mode 100644 index 000000000..5abf508f8 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/test-coverage.2026-08-31T20-40.md @@ -0,0 +1,81 @@ +# QA Gate — Test Gate with Coverage (Issue #656) + +Timestamp: 2026-09-01T14-52 +Task: [P4-T7] (toolchain loop pass 1, step 4) +Satisfies: AC-18 + +Run Start: 2026-09-01T14:51:30.6252415-04:00 +Run End: 2026-09-01T14:52:18.0655700-04:00 + +Command: +``` +pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +``` +with console output tee'd to `TestResults\p4-t7\coverage-run.log`. + +EXIT_CODE: 0 + +## Post-change run results + +- Post-Change Total Tests: 6926 +- Post-Change Passed Tests: 6926 +- Post-Change Failed Tests: 0 +- Post-Change Failure Set: none + +Test-count reconciliation required by this task: `Baseline Total Tests:` was **6925**, and +6926 = 6925 + 1. This change adds exactly one test and removes none, so the observed total is +exactly the expected total. The added test is +`CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain`. + +Failure-set condition: the recorded `Post-Change Failure Set:` is `none`, so it contains no test in +`QuickFiler.Test` and is trivially a subset of the `Baseline Failure Set:` of `none` recorded in +`evidence/baseline/test-coverage.2026-08-31T20-40.md`. As the plan notes, when the baseline failure +set is `none` this subset condition reduces to `EXIT_CODE: 0`, which is the case observed, and +`EXIT_CODE: 0` strictly implies zero failed tests because the wrapper throws when the inner vstest +exit code is non-zero. + +## Post-change coverage values + +- Post-Change Repo Line Rate: 0.853732 +- Post-Change Repo Lines Covered: 54965 +- Post-Change Repo Lines Valid: 64382 +- Post-Change Coordinator Line Rate: 0.983193 +- Post-Change Coordinator Lines Covered: 234 +- Post-Change Coordinator Lines Valid: 238 + +Derivation is identical to the baseline artifact: the three repository values are the `line-rate`, +`lines-covered` and `lines-valid` attributes of the root `/coverage` element of +`coverage\coverage.cobertura.xml`; the three coordinator values come from the single `class` node +whose `filename` equals `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs`, with the line +counts taken from the class-relative XPath `./lines/line` and, for covered, those whose `hits` +attribute exceeds 0. Exactly one such `class` node exists. No `lines-covered` or `lines-valid` +attribute exists on a `class` node; those two attributes are set on the root `coverage` node only. + +## Wrapper Filter Lines: + +- `/TestCaseFilter:TestCategory!=LiveOutlook` — line **76** of `scripts/vscode/Invoke-MSTestWithCoverage.ps1` +- `/InIsolation` — line **76** of `scripts/vscode/Invoke-MSTestWithCoverage.ps1` + +Both `Select-String` results report line 76, which is the acceptance condition. Both switches appear +on the same source line: + +``` +) + @($TestAssembly) + @("/Settings:$RunSettingsPath", '/InIsolation', '/TestCaseFilter:TestCategory!=LiveOutlook') +``` + +This is the recorded evidence that AC-18's "`/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` +in effect" condition holds for this run: both are unconditionally appended to the vstest argument +list by the wrapper this gate invoked. + +## No TRX for this run + +The wrapper passes `/Settings:`, `/InIsolation` and `/TestCaseFilter:` and no `/Logger:trx`, and the +referenced runsettings declares no logger, so a wrapper run writes no `.trx` file. The four run +fields above are read from the vstest run summary in the tee'd console log +`TestResults\p4-t7\coverage-run.log`, which reports `Total tests: 6926` and `Passed: 6926` with no +`Failed:` line. + +Output Summary: Full test gate passed. 6926 tests total, 6926 passed, 0 failed, no failure set, +which is the baseline total plus exactly the one added test. Repository line rate 0.853732 (54965 of +64382 lines); coordinator line rate 0.983193 (234 of 238 lines). Both wrapper protections confirmed +present at line 76. AC-18 is satisfied. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md new file mode 100644 index 000000000..ea21459d6 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md @@ -0,0 +1,57 @@ +# QA Gate — Type-Check / Nullable Gate (Issue #656) + +Timestamp: 2026-09-01T14-51 +Task: [P4-T6] (toolchain loop pass 1, step 3) +Satisfies: AC-17 + +Gate Start: 2026-09-01T14:50:46.1887917-04:00 + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\msbuild\p4-t6-typecheck.log;Verbosity=normal" +``` + +EXIT_CODE: 0 + +## Acceptance measurements + +| Condition | Required | Observed | Met | +|---|---|---|---| +| Exit code | 0 | 0 | yes | +| `@(Select-String -Path TestResults\msbuild\p4-t6-typecheck.log -SimpleMatch '0 Error(s)').Count` | > 0 | 1 | yes | +| `Command:` contains `/t:Rebuild` | yes | yes | yes | +| `Command:` contains `/p:Nullable=enable` | no | no | yes | +| `Command:` contains `/t:Build` | no | no | yes | + +Elapsed 00:00:11.79. + +## Why the two omissions are load-bearing + +**No `/p:Nullable=enable`.** Nullable enforcement in this repository is per-file opt-in: a file +participates when it carries a `#nullable enable` directive, and `/p:TreatWarningsAsErrors=true` +then promotes its `CS86xx` diagnostics to build errors. No project carries a `` element +and there is no `Directory.Build.props`, so `/p:Nullable=enable` would be a solution-wide opt-in +conscripting every file that has never adopted the pragma. CI omits it deliberately, and this +command is character-for-character CI's nullable step. + +**No `/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 having skipped `CoreCompile` on every project: the gate could not +fail. The log confirms this did not happen here — +`@(Select-String -Path TestResults\msbuild\p4-t6-typecheck.log -SimpleMatch 'Skipping target "CoreCompile"').Count` +is **0**, so every project was genuinely recompiled under warnings-as-errors. + +## Nullable coverage of the changed file + +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` carries `#nullable enable` on line 1, so +it is inside the per-file nullable gate and its `CS86xx` diagnostics are promoted to errors by this +command. The statement this change added, `bool hostOpen = _host.IsOpen;`, declares a non-nullable +`bool` from a non-nullable `bool` property and introduces no null state, and the narrowed guard +`if (_closeCompleted && !hostOpen)` reads two non-nullable `bool` values. The clean result is +therefore a genuine observation about the changed lines rather than a vacuous pass over an unopted +file. + +Output Summary: Type-check gate passed with `0 Error(s)` under `/p:TreatWarningsAsErrors=true` and +`/t:Rebuild`, with no `CoreCompile` skipped. The command carries neither `/p:Nullable=enable` nor +`/t:Build`. AC-17 is satisfied. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-build.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-build.2026-08-31T20-40.md new file mode 100644 index 000000000..1930d0185 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-build.2026-08-31T20-40.md @@ -0,0 +1,32 @@ +# Regression Testing — Build Before the Red Run (Issue #656) + +Timestamp: 2026-09-01T14-41 +Task: [P1-T2] + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +& $msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=TestResults\msbuild\p1-t2-build.log;Verbosity=normal" +``` + +EXIT_CODE: 0 + +Results: + +- Build summary: `5 Warning(s)` / `0 Error(s)`, elapsed 00:00:11.93. The five warnings are the same + pre-existing System.Reactive `packages.config` diagnostic recorded in the Phase 0 baselines. +- `Test-Path QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` is True, so the scoped red run in P1-T3 + has a current test assembly containing the newly added test. + +Significance: the new test was added to `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` +before any production change, and it compiles against unmodified production code. It uses only +members that already exist on the unmodified tree — `CoordinatorHarness`, `ControlledHost.Enqueue`, +`ControlledHost.SetOpen`, `ControlledHost.CloseReasons`, `ControlledHost.IsOpen`, +`CoordinatorHarness.SelectorOpen`, and `BreadcrumbDropDownOpenCoordinator.SetDroppedDown` — and the +file's existing `using` directives already cover every type it references, so no `using` was added. +A non-zero exit here would have indicated a defect in the test text rather than the expected red. +The red observed in P1-T3 is therefore a runtime failure, not a compile failure. + +Output Summary: Solution rebuilt successfully with 0 errors. The test assembly exists and contains +the new test. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md new file mode 100644 index 000000000..f10f64926 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md @@ -0,0 +1,65 @@ +# Regression Testing — Fail-Before Run (Issue #656) + +Timestamp: 2026-09-01T14-42 +Task: [P1-T3] [expect-fail] + +Command: +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 +New-Item -ItemType Directory -Force -Path 'TestResults\p1-t3' | Out-Null +& $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p1-t3' +``` + +EXIT_CODE: 1 +ExpectedExitCode: 1 + +## TRX counter values + +- `total` = 1 +- `passed` = 0 +- `failed` = 1 + +Read from `TestRun/ResultSummary/Counters` of the TRX written to +`/TestResults/p1-t3/__2026-09-01_14_42_14_net481.trx`. + +## Failure message (copied from the TRX `UnitTestResult` node) + +``` +Expected harness.Host.CloseReasons to be equal to {BreadcrumbDropDownCloseReason.Uncommitted {value: 1}, BreadcrumbDropDownCloseReason.Uncommitted {value: 1}} because the close after a bypassing reopen must reach _host.Close a second time, but {BreadcrumbDropDownCloseReason.Uncommitted {value: 1}} contains 1 item(s) less. +``` + +## Why a direct vstest invocation rather than the wrapper + +Neither `scripts/vscode/Invoke-MSTest.ps1` nor `scripts/vscode/Invoke-MSTestWithCoverage.ps1` +accepts a `TestCaseFilter` override, and editing either script is outside this item's authorized +footprint. The direct invocation reproduces both wrapper protections explicitly: `/InIsolation` is +passed, and the `TestCategory!=LiveOutlook` conjunct is the first term of the filter, so no real +Outlook process can be launched. The run is scoped to the single new test by name because the full +suite is not run while a test is deliberately failing — a full-suite gate could not exit 0 in that +state. + +Output Summary: The new regression test ran alone and failed as expected. Exit code 1, which equals +the declared expectation, so this gate is normalized to pass. The TRX reports 1 total, 0 passed, 1 +failed. The failure is a runtime assertion failure, not a compile failure: the assembly built +cleanly in P1-T2 against unmodified production code. + +## Red Cause: + +The observed `CloseReasons` collection held exactly **one** element (`Uncommitted`) where **two** +were expected, which is the outcome the fail-before requirement predicts. The suppressing guard is +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:316`, the line `if (_closeCompleted)` +inside `CloseCore`'s `lock (_sync)` critical section, followed by `return true;` at `:317`. + +Mechanism: the test drives a successful open, then a close the host accepts, which sets +`_closeCompleted` to `true` at `:335`. It then reopens the host through +`harness.Host.SetOpen(true)`, a path that reaches neither `RequestOpen` nor `Invalidate` and +therefore does not clear `_closeCompleted`. The second `SetDroppedDown(false)` reaches `CloseCore`, +the guard at `:316` observes `_closeCompleted == true`, and the method returns `true` without ever +calling `_host.Close(reason)` at `:323`. No second reason is appended to `CloseReasons`, so the +collection is short by exactly one element — precisely what the failure message reports. + +Production file still unmodified at this point: +`@(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count` +equals **1**, unchanged from the P0-T12 baseline. The red is therefore observed against HEAD +production code, and no part of the Phase 2 fix had been applied when it was recorded. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/feature-audit.2026-09-01T15-03.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/feature-audit.2026-09-01T15-03.md new file mode 100644 index 000000000..5cd30ab48 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/feature-audit.2026-09-01T15-03.md @@ -0,0 +1,240 @@ +# Feature Audit — Issue #656 (breadcrumb `_closeCompleted` residual) + +- Timestamp: 2026-09-01T15-03 +- Work mode: `full-bug` (marker at `issue.md:12`) +- AC source: `spec.md` only. `user-story.md` correctly absent for this mode. +- Baseline: `main` at `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` +- Head: `65d2f22b5100588eae8ac4de40e48f1ac391db34` + +## Acceptance Criteria Status + +``` +### Acceptance Criteria Status +- Source: docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md +- Total AC items: 20 +- Checked off (delivered): 20 +- Remaining (unchecked): 0 +- Items remaining: none +``` + +Checkbox state re-counted directly: `^- \[x\]` matches in `spec.md` = 21, `^- \[ \]` = 4. Of those, +20 checked and 0 unchecked are AC items; the remaining 1 checked and 4 unchecked are the bug-report +template's severity radio group (`- [x] Medium`) and the `- [ ] Attached minimal logs or screenshot` +line, which are not acceptance criteria. So 20 of 20 AC boxes are checked and none is unchecked. +This matches the executor's `evidence/issue-updates/ac-status.2026-08-31T20-40.md`. + +Plan checklist: `plan.2026-08-31T20-10.md` carries 62 checked and 0 unchecked task boxes. + +No AC box was newly checked by this reviewer; all 20 were already checked and all 20 verify as PASS. + +## AC Evaluation Table + +| AC | Requirement (abbreviated) | Verification performed by this reviewer | Verdict | +|---|---|---|---| +| AC-1 | Hoisted `_host.IsOpen` local before the lock; guard is `if (_closeCompleted && !)` | Read the file: line 326 `bool hostOpen = _host.IsOpen;`, line 327 `lock (_sync)`, line 333 `if (_closeCompleted && !hostOpen)`. The read precedes the lock. | PASS | +| AC-2 | No `_host`/`IBreadcrumbDropDownHost` call added or modified inside any `lock (_sync)` body | Enumerated all 12 `lock (_sync)` sites (lines 91, 103, 113, 141, 154, 244, 327, 344, 349, 363, 377, 385) and all 8 `_host.` usages (119, 200, 205, 216, 265, 266, 326, 340). The only `_host` call inside a lock body is the pre-existing line 119 in `RequestOpen`. The diff adds none. | PASS | +| AC-3 | Named test exists in `Part3.cs`, drives open, close, `SetOpen(true)`, second close, asserts two `Uncommitted` entries | Read the test body in the diff. All four steps present in order; assertion is `.Equal(new[] { Uncommitted, Uncommitted }, "...")`. | PASS | +| AC-4 | Test demonstrated failing before and passing after, both outputs recorded under `evidence/qa-gates/` | `evidence/qa-gates/red-green-comparison` records red 1/0/1 exit 1 and green 1/1/0 exit 0, with both `Timestamp:`/`Command:`/`EXIT_CODE:`/`Output Summary:` blocks embedded verbatim and the red assertion message quoted. Two method departures are declared and justified in an `AC-4 Reconciliation:` section — see the deviation note below. | PASS | +| AC-5 | `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` passes and its file unchanged | Scoped run recorded 5/5 passed including this test. `git diff --name-only origin/main...HEAD -- QuickFiler.Test` returns only `...Part3.cs`, so `BreadcrumbDropDownOpenCoordinatorTests.cs` is unchanged. Re-run by me. | PASS | +| AC-6 | `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` passes, assertion text unchanged | Same scoped run; `...Part2.cs` absent from the diff, so no assertion text could have changed. | PASS | +| AC-7 | `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` passes, assertion text unchanged | Same scoped run; `...Part2.cs` absent from the diff. | PASS | +| AC-8 | `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` passes, assertion text unchanged | Same scoped run; `...Part2.cs` absent from the diff. | PASS | +| AC-9 | `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` passes | Same scoped run, recorded `Passed`. Also covered by the 6926/6926 full-suite run. | PASS | +| AC-10 | No file under `QuickFiler/` other than the coordinator | Re-ran `git diff --name-only origin/main...HEAD -- QuickFiler`: exactly `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`. | PASS | +| AC-11 | No `*.csproj`, `*.props`, `*.targets`, `*packages.config` in the diff | Re-ran the pathspec-scoped diff: empty output. | PASS | +| AC-12 | No file under `QuickFiler.Test/` other than `...Part3.cs` | Re-ran: exactly `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`. | PASS | +| AC-13 | Both changed files under 500 lines | `awk 'END{print NR}'`: coordinator 395 (baseline 378), test part 213 (baseline 173). Both under 500. | PASS | +| AC-14 | `dotnet tool run csharpier check .` exits 0, no file needs formatting | Re-executed in this session: `Checked 1566 files in 4543ms.`, exit 0, no file listed. | PASS | +| AC-15 | Analyzer gate `0 Error(s)`, no new warning on the changed file | `p4-t3-analyzer.log`: `0 Error(s)`, `5 Warning(s)`, zero warning lines naming `BreadcrumbDropDownOpenCoordinator.cs`. Baseline `p0-t8-analyzer.log` also `5 Warning(s)` — count unchanged. | PASS | +| AC-16 | No `Skipping target "CoreCompile"` in the analyzer log | `grep -c` over `p4-t3-analyzer.log` returns 0. The gate was non-vacuous. | PASS | +| AC-17 | Type-check gate `0 Error(s)`, no `/p:Nullable=enable`, `/t:Rebuild` | `p4-t6-typecheck.log`: `0 Error(s)`; `grep -c "Nullable=enable"` returns 0; command uses `/t:Rebuild`. | PASS | +| AC-18 | Wrapper run, zero failed tests, `/InIsolation` and `TestCategory!=LiveOutlook` in effect | `TestResults/p4-t7/coverage-run.log` lines 6948-6949: `Total tests: 6926`, `Passed: 6926`, no `Failed:` line. Both switches are unconditionally appended at line 76 of `Invoke-MSTestWithCoverage.ps1`. Count reconciles as baseline 6925 + 1 added test. | PASS | +| AC-19 | Field doc and `CloseCore` summary state the new suppression condition and why the read is outside `_sync` | Read both blocks. Field `` at lines 46-52 states the flag is cleared only on `RequestOpen`/`Invalidate` and that suppression now additionally requires the host to report not open. `CloseCore` `` at lines 315-323 states the same and gives the SR-4 rationale for hoisting. Content requirement met; cited line ranges are stale (CR-4). | PASS | +| AC-20 | No new `internal`/`public` member on the coordinator; no member on `IBreadcrumbDropDownHost` | Declared member count 12 at base and 12 at head. `QuickFiler/Viewers/IBreadcrumbDropDownHost.cs` absent from the diff. | PASS | + +**Totals: 20 PASS, 0 PARTIAL, 0 FAIL, 0 UNVERIFIED.** + +### Declared deviation on AC-4 + +The executor recorded two departures from AC-4's literal check method rather than silently +substituting. Both are accepted: + +1. The red run is stored under `evidence/regression-testing/`, which is the canonical fail-before + location in `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`, while AC-4 asks for it + under `evidence/qa-gates/`. The conflict is resolved by embedding the red run's four required + fields verbatim in the `evidence/qa-gates/red-green-comparison` artifact, so both outputs are + present where AC-4 asks while the authoritative artifact stays where a later audit will look. +2. Both single-test runs use `vstest.console.exe` directly rather than the wrapper, because neither + wrapper accepts a `TestCaseFilter` override (both pin it) and editing a wrapper is outside the + authorized two-file footprint. A wrapper run would have executed the whole suite and could not + have produced a scoped red record at all. Both wrapper protections were reproduced explicitly: + `/InIsolation` is passed and `TestCategory!=LiveOutlook` is the first conjunct of the filter. + +The substance AC-4 exists to guarantee — a recorded failing run before the fix and a recorded passing +run after — is fully delivered. + +## Baseline delta assessment + +The issue's Expected Behavior is: "The close request reaches `_host.Close`, because the host is +genuinely open again." Relative to `main`, the branch delivers exactly that for the state +`_closeCompleted == true && IsOpen == true`, and changes nothing else. All other guard states are +bit-identical to baseline (see the truth table in `code-review.2026-09-01T15-03.md`). + +The three "Proposed Fix / Validation Ideas" in `issue.md` are addressed as follows: the reopen-path +enumeration was performed (research artifact); no bypassing production path was found, so the second +idea — routing such a path through `RequestOpen`/`Invalidate` — was correctly moot and the guard was +hardened instead; and a regression test was added with the three named must-pass tests unedited. + +## Honesty of the delivered classification + +The caller asked specifically whether the latent-correctness classification is honestly represented. +Assessment, in three parts: + +**Honestly represented, part 1 — the issue's literal premise.** `spec.md` does not pretend the +reported scenario occurs. Its **Scope & Non-Goals** opens with an explicit heading, "Classification: +latent-correctness hardening, not an observed failure", and states that no production reopen path +bypassing both entry points exists in the tree today. That is a direct contradiction of the issue's +Steps to Reproduce, stated plainly rather than buried. The **Manual validation steps** section says +"None. The residual is not reachable through the shipped UI ... so there is no manual gesture that +exercises it." The **Post-fix monitoring** section says "No telemetry to monitor: the changed state +is unreachable from shipped UI, so there is no production signal to watch." The classification is +foregrounded, not concealed, and the change's impact is not inflated anywhere I could find. + +**Honestly represented, part 2 — severity.** Both `issue.md` and `spec.md` carry `- [x] Medium` with +the same justification text. The spec neither raises nor lowers the inherited severity. Given the +spec's own finding that the scenario is not reachable through shipped UI, Medium is arguably higher +than the evidence supports; but leaving an inherited severity untouched rather than editing it +downward is the conservative and defensible choice, and the qualifying sentence +("a latent correctness gap rather than an observed user-facing failure") sits directly beneath it. +No finding. + +**Not fully accurate, part 3 — the reachability conclusion is stated too strongly.** This is CR-1 in +`code-review.2026-09-01T15-03.md` and it is the one place the delivered artifacts overreach. The +enumeration proves that the *host cannot be reopened* without `RequestOpen`. The spec then reports +that as though it settled a different proposition — that `_closeCompleted == true` and +`_host.IsOpen == true` cannot both hold in production. They can: `BreadcrumbDropDownHost.Close` +returns `true` after only *scheduling* `CompleteClose`, and `CompleteClose` is what sets +`OpenState = false`, dispatched onto the same UI operations queue the coordinator posts to. So the +guard's new branch is reachable on the shipped host with no substituted implementation. The +statement in **Mitigations and rollbacks** that a rollback "restores behavior that is +observationally identical to the fixed build on every shipped path" is therefore not established. + +To be precise about what this does and does not mean: the change was scoped deliberately as latent +hardening, and I am not upgrading its claimed impact or failing it for being latent. The defect is +in the recorded *reasoning* — an enumeration that answers a narrower question than the conclusion +drawn from it. The fix direction remains correct in the newly-reachable window as well. + +## Independent verification of execution-evidence claims + +The caller flagged one integrity issue and asked that it be treated as a reason to re-derive other +factual claims rather than accept them. Both checks below were performed from git, not from prose. + +### INTEGRITY-1 — Confirmed. The executor's claim about the agent-memory file is false. + +The executor reported that `.claude/agent-memory/atomic-executor/project_baseline_sha_diff_conflates_merged_base.md` +"was already in the baseline diff". It was not. + +| Check | Result | +|---|---| +| `git ls-tree origin/main -- ` | present, blob `418576a14e3a1153c16032cf7f6329df1a472474` — the file is **tracked on the base** | +| `git diff --name-only origin/main...119a89f0` (pre-execution HEAD, 10 paths) | the path is **absent** — it was unmodified at the point execution began | +| `git log --name-status origin/main..HEAD` | appears once, as `M`, in the final commit `65d2f22b` | +| `git diff origin/main...HEAD -- ` | +10/-0, one appended paragraph | + +So the file was tracked and unmodified on both `origin/main` and the pre-execution HEAD. It entered +the branch diff because the executor modified it during the run and it was committed afterwards in +`65d2f22b`. The caller's reading is correct. + +Two mitigating observations. First, the false statement appears only in the executor's +conversational report; no committed artifact repeats it. `evidence/other/final-commit` accurately +records that "No path under `.claude/agent-memory/` and no path under `artifacts/orchestration/` was +staged" for commit `145ee256`, which is true of that commit. Second, the appended content is +factually accurate — I verified its central measurement below. The defect is a misattribution of +provenance, not fabricated content, and it is not a code defect. **Severity: Minor, non-blocking.** +The follow-up worth recording is procedural: an agent-memory write during a run is a real branch +mutation and should be reported as such, not folded into "already in the baseline". + +### INTEGRITY-2 — Confirmed sound. The stale-pinned-base substitution is correct and the recorded evidence is not self-contradictory. + +The plan anchored every footprint gate to `2b85134b42872e405602e6064e02dc9cda6c319b`. Verified: + +| Check | Result | +|---|---| +| `git merge-base --is-ancestor 2b85134b HEAD` | true — the pinned SHA is an ancestor of HEAD | +| `git merge-base 2b85134b HEAD` | `2b85134b...` — equals the pinned SHA itself | +| `git diff --name-only 2b85134b...HEAD` vs `2b85134b..HEAD` | 335 and 335 — identical, so the three-dot form has degenerated to the two-dot form | + +The executor's reasoning is therefore exactly right: because the pinned SHA is an ancestor, +`merge-base(PINNED, HEAD) == PINNED`, so `PINNED...HEAD` silently becomes `PINNED..HEAD` and +conflates everything `main` gained in the interval with this item's change set. Four footprint gates +asserting "exactly the single line" and "both outputs empty" were unsatisfiable as written. The +executor detected this, substituted `origin/main...HEAD` (a genuine merge-base of the branch), and +recorded both measurements verbatim in each footprint artifact rather than substituting silently. +That is the correct handling. + +The recorded numbers reconcile exactly against their measurement point, which is the test that +matters for self-consistency: + +| Recorded figure | Measured at pre-execution HEAD `119a89f0` | Match | +|---|---|---| +| `PINNED...HEAD` = 299 paths | `git diff --name-only 2b85134b...119a89f0` = **299** | yes | +| 9 under `QuickFiler/` + `QuickFiler.Test/` | **9** | yes | +| one `.csproj` | **1** | yes | +| `origin/main...HEAD` = 10 paths | `git diff --name-only origin/main...119a89f0` = **10** | yes | + +The 299 figure differs from today's 335 solely because 36 further evidence and documentation files +were committed after the measurement. The `footprint-production` artifact's own stale-base listing +of four `QuickFiler/` paths reproduces exactly against the current tree. No contradiction found. + +### Other re-derived claims + +Because of INTEGRITY-1 I re-derived every load-bearing figure rather than accepting the executor's +summaries. All of the following reproduced exactly: the 46-path branch diff and its composition; all +three footprint pathspec results; both file line counts and their baselines; the declared member +count 12 before and after; the analyzer `0 Error(s)`, `5 Warning(s)` and zero-CoreCompile-skips; the +type-check `0 Error(s)` and absent `/p:Nullable=enable`; the full-suite 6926/6926; the CSharpier +result, re-executed live; the repository line rate 0.853732 and branch rate 0.793761 from the +Cobertura root element; the coordinator class rate 0.983193 and its 234/238 line split; and hits of 1 +on both changed lines 326 and 333. **No further inaccuracy was found in any committed artifact.** + +## Non-blocking follow-ups (text only — no issue filed, per instruction) + +Recorded here for the maintainer's consolidated post-merge issue. None of these is a merge condition. + +1. **Amend the spec's reachability conclusion (CR-1).** State that the guard's new branch is + reachable on the production host inside the window between `Close` returning `true` and the + scheduled `CompleteClose` setting `OpenState = false`, and withdraw or qualify the claim that a + rollback is observationally identical on every shipped path. Optionally settle the open question: + does any real gesture dispatch a second `CloseCore` inside that window, given that the + selector-close event appears to be raised from within `CompleteClose` itself? +2. **Correct R-1's description of the not-open `Close` branch (CR-2).** `BreadcrumbDropDownHost.cs:256` + returns `TryCancelPendingOpen(...)`, which can invalidate, schedule a `CompleteClose` and return + true; it does not unconditionally return false. +3. **Add a deferred-`IsOpen` harness variant (CR-3).** Every current fake clears `IsOpen` + synchronously inside `Close`, so no test represents the production timing in item 1. A harness + whose `Close` returns true but defers `IsOpen = false` to the drained queue would close the gap. +4. **Refresh the stale line citations in `spec.md` (CR-4).** AC-19's `:38-46` / `:302-307` and the + Scope section's `:258-259` / `:114-115` all drifted by the seven lines the field `` + added. +5. **Provenance reporting for agent-memory writes (INTEGRITY-1).** A memory file modified during a + run is a branch mutation and should be reported as one. +6. **Reconcile the two coverage floor definitions.** `CLAUDE.md` specifies >= 80 percent repo-wide + and >= 90 percent for new code; `.claude/rules/general-unit-test.md` and + `.claude/rules/quality-tiers.md` specify >= 85 percent line and >= 75 percent branch uniformly. + Both are live in the repository and they disagree. This branch clears every one of those numbers, + so nothing turned on it here, but the conflict will eventually decide a marginal case. +7. **Optional documentation follow-up already identified in the spec.** `breadcrumb-coordinator-hub-defects-501/spec.md` + records this residual as "shipped as designed" at `:1062` and as a known limitation at `:432-437`. + Once this merges those records become historical. The spec correctly declines to widen this + footprint to amend them. + +## Verdict + +All 20 acceptance criteria are delivered and independently verified. The full toolchain passed in a +single ordered pass, re-verified against retained raw logs and one live re-execution. Coverage clears +every floor in force in this repository. The footprint is exactly the two files the spec authorizes. +The one substantive finding, CR-1, concerns the strength of a claim in the spec rather than the +correctness of the code. + +**Blocking findings across all three artifacts: 0** (0 FAIL, 0 blocking PARTIAL). No +`remediation-inputs` artifact was produced. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/issue.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/issue.md new file mode 100644 index 000000000..5b8b77935 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/issue.md @@ -0,0 +1,88 @@ +# breadcrumb-closecompleted-residual-outside-requestopen-invalidate (Issue #656) + +- Date captured: 2026-08-27 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/breadcrumb-closecompleted-residual-outside-requestopen-invalidate/ (Issue #656) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #656 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/656 +- Last Updated: 2026-08-27 +- Work Mode: full-bug + +## Summary + +`BreadcrumbDropDownOpenCoordinator._closeCompleted` stays `true` when the drop-down host is reopened by +a path that reaches neither `RequestOpen` nor `Invalidate`, so a subsequent close is wrongly suppressed. +This is the known residual of the SR-4 two-flag close fix shipped for #462 under #501, recorded against +the host paths owned by feature #488. + +## Environment + +- OS/version: Windows 11, Outlook VSTO add-in host +- Python version: n/a (C#, .NET Framework 4.8) +- Command/flags used: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"` +- Data source or fixture: `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` harness + +## Steps to Reproduce + +1. Open the breadcrumb drop-down host and close it through `CloseCore`, so `_closeCompleted` becomes `true`. +2. Reopen the host through a path that reaches neither `RequestOpen` nor `Invalidate`. +3. Request a close. + +## Expected Behavior + +The close request reaches `_host.Close`, because the host is genuinely open again. + +## Actual Behavior + +The coordinator still treats the host as already closed and suppresses the close. `_closeCompleted` was +never cleared, because it is cleared only on the `RequestOpen` and `Invalidate` paths. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: no runtime log; the residual is established by source inspection of the flag-clearing paths. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Medium: it requires a reopen path that bypasses both entry points, which the currently exercised UI +flows do not take. It is a latent correctness gap rather than an observed user-facing failure. + +## Suspected Cause / Notes + +#462 was fixed by replacing the single `_closePending` flag with two flags, `_closeInFlight` and +`_closeCompleted`. `_closeCompleted` is cleared on `RequestOpen` and `Invalidate` only. + +The two-flag form was chosen deliberately. The naive alternative, clearing the close flag on the +successful-close path, makes two existing must-pass tests fail by letting a second `CloseCore` reach +`_host.Close`: `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` and +`SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired`. Both encode the repeated-close +suppression contract. The two-flag form passes all three must-pass tests with no test edit, so it was +shipped and this residual recorded rather than traded for a regression. + +This belongs to feature #488, not #501: the reopen paths that bypass `RequestOpen` and `Invalidate` +live in the ItemViewer breadcrumb lifecycle host surface. #501 was not permitted to write +`BreadcrumbItemViewerLifecycleCoordinator.cs`, `BreadcrumbDropDownHost.cs` or `ItemViewer.Breadcrumb.cs`. + +## Proposed Fix / Validation Ideas + +- [ ] Enumerate every path that reopens the drop-down host +- [ ] For any path reaching neither `RequestOpen` nor `Invalidate`, route it through one of them or clear `_closeCompleted` explicitly +- [ ] Add a regression test driving that path, keeping the three must-pass tests unedited + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch + +## References + +- Split out of #501 / #462; see `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md`, SR-4 and `## Implementation Notes` +- Evidence: `docs/features/active/breadcrumb-coordinator-hub-defects-501/evidence/qa-gates/closepending-split.2026-08-27T20-53.md` diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/plan.2026-08-31T20-10.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/plan.2026-08-31T20-10.md new file mode 100644 index 000000000..443e48c48 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/plan.2026-08-31T20-10.md @@ -0,0 +1,315 @@ +# 2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate (Plan) + +- **Issue:** #656 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-31T20-40 +- **Status:** Ready for execution +- **Version:** 1.0 +- **Work Mode:** full-bug (requirements source is `spec.md`; `user-story.md` does not exist and is not required) + +## Execution Preconditions and Conventions + +**Shell.** Every command in this plan runs in **PowerShell** from the worktree root. The Bash tool mangles MSBuild switches (`/m` is rewritten to `M:/`, producing MSB1008), so no msbuild, vstest, or csharpier command in this plan may be issued through Bash. + +**MSBuild resolution.** `msbuild` is not assumed to be on `PATH`. Every msbuild task resolves it through vswhere first, matching `scripts/vscode/Invoke-Restore.ps1:22-30`: + +``` +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +``` + +`scripts/vscode/Invoke-VSBuild.ps1` must **not** be used: it calls `Sync-PackageReferences.ps1` over every `.csproj` and rewrites `HintPath` values, which would breach the build-configuration footprint boundary that AC-11 pins. + +**Base ref.** All diff assertions are anchored to the explicit base commit `2b85134b42872e405602e6064e02dc9cda6c319b`. No unanchored `git diff` appears in this plan. + +**Artifact stamp.** Every evidence filename in this plan uses the fixed stamp `2026-08-31T20-40` so that each asserted path is a concrete literal. The `Timestamp:` field **inside** each artifact carries the actual ISO-8601 execution time. + +**Evidence locations.** All evidence is written under `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/` in the sub-kinds `baseline/`, `regression-testing/`, `qa-gates/`, and `other/`. No `artifacts/` path is used for evidence. Every command-step artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`. Baseline and final-QC test artifacts additionally carry numeric coverage headline values. No helper script is placed under `evidence/`. + +**Raw tool output.** Raw msbuild logs and raw TRX files are large intermediates, not evidence records. They are written under `TestResults\` (git-ignored by `.gitignore:39`, pattern `[Tt]est[Rr]esult*/`) and the evidence markdown records the command, exit code, and the extracted values. The raw Cobertura XML is written by the runner to `coverage\coverage.cobertura.xml` (git-ignored by `.gitignore:144`); the numeric values are transcribed into the evidence markdown. + +**Raw-output directories are created first.** `TestResults\` does not exist in this worktree. The msbuild file logger opens its log with a `StreamWriter` and fails the build with an invalid-file-logger-file error when the parent directory is missing, and `Tee-Object -FilePath` likewise does not create a missing parent directory. P0-T7 therefore creates every `TestResults\` subdirectory this plan writes into, before the first task that writes one. + +**`TestResults\` path literals are not renumbered.** The subdirectory and log-file names under `TestResults\` are fixed literals pinned by P0-T7's creation list, and they are deliberately left unchanged when a Phase 0 task identifier shifts. `TestResults\p0-t10\coverage-run.log` is therefore the raw-output path of the baseline coverage task that carries the identifier P0-T11, `TestResults\msbuild\p0-t8-analyzer.log` is the raw-output path of the baseline analyzer task that carries the identifier P0-T9, and `TestResults\msbuild\p0-t9-typecheck.log` is the raw-output path of the baseline type-check task that carries the identifier P0-T10. Each such literal is written and read only inside the single task that owns it, plus P0-T7's creation list, so no cross-task reference depends on the name matching a task identifier. + +**The coverage wrapper produces no TRX.** `scripts/vscode/Invoke-MSTestWithCoverage.ps1:70-76` passes `/Settings:`, `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` and no `/Logger:trx`, and `scripts/vscode/TaskMaster.cli.runsettings` declares no logger, so a wrapper run writes no `.trx` file. Test counts and failing test names for a wrapper run are read from that run's tee'd console log. Only the two direct `vstest.console.exe` invocations in this plan (P1-T3 and P3-T2) and the scoped run in P3-T4 pass `/Logger:trx`, and only those tasks read a TRX. + +**Fail-closed evidence rule.** If any required baseline artifact, QA artifact, or coverage-comparison artifact is missing or incomplete, the outcome is BLOCKED or INCOMPLETE, never PASS. A checklist box stays unchecked when its artifact is absent or its fields are incomplete. + +**Bootstrap versus gate.** This worktree contains no `.dotnet-sdk` directory (verified: no files match `.dotnet-sdk/**`) and no `packages/` directory (verified: no files match `packages/*/`). A failure in P0-T3, P0-T4, P0-T5, or P0-T6 is a **bootstrap failure** and must be recorded as such, never as a toolchain gate failure. + +**Count expressions.** Every `Select-String` count stated as an acceptance value in this plan is evaluated with the array subexpression form so that a zero-match result is the number `0` rather than an absent value: write `@(Select-String ...).Count`, not `(Select-String ...).Count`. Where a task text below shows the parenthesised form, the array subexpression form is the one to run. + +**Toolchain order.** Format, then analyze, then type-check, then test. Phase 4 runs the full loop. If any step in P4-T1 through P4-T7 fails or rewrites a tracked file, restart the loop at P4-T1. + +**Literals this plan instructs the executor to create.** These are quoted here, outside every command span, so a search for them is understood as an instruction rather than an existing-tree claim: `bool hostOpen = _host.IsOpen;`, `if (_closeCompleted && !hostOpen)`, `public void CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain()`, `harness.Host.SetOpen(true);`, `Issue #656`, and `hoisted`. The literal `Skipping target "CoreCompile"` is quoted here for the same reason: P4-T4 asserts its **absence** from a log this plan creates. + +**Authorized footprint (hard boundary, from `spec.md` Scope & Non-Goals).** Exactly two code files may change: + +| Kind | File | +|---|---| +| Production | `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` | +| Test | `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` | + +Plus this feature folder. No other file may appear in the change set. + +**Files that must not appear in the diff at all.** `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs`, `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs`, `QuickFiler/Viewers/IBreadcrumbDropDownHost.cs`, and every `.csproj`, `.props`, `.targets`, and `packages.config`. + +**Why no seam task precedes the failing test.** The new test uses only members that already exist: `CoordinatorHarness` (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:323`), `ControlledHost.Enqueue` (`:402`), `ControlledHost.SetOpen` (`:407`), `ControlledHost.CloseReasons` (`:395`), `ControlledHost.IsOpen` (`:378`), `CoordinatorHarness.SelectorOpen` (`:352`), and `BreadcrumbDropDownOpenCoordinator.SetDroppedDown` (`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:152`). `Part3.cs:1-4` already imports `System.Threading.Tasks`, `FluentAssertions`, `Microsoft.VisualStudio.TestTools.UnitTesting`, and `QuickFiler.Viewers`. The test therefore compiles against unmodified production code, and its failure in P1-T3 is a **runtime** red, not a compile red. No production seam, no new file, and no `InternalsVisibleTo` change is required. + + +### Phase 0 — Policy Reads, Environment Bootstrap, and Baseline Capture + +- [x] [P0-T1] Read the four policy files in the order required by `policy-compliance-order` — `CLAUDE.md`, then `.claude/rules/general-code-change.md`, then `.claude/rules/general-unit-test.md`, then `.claude/rules/csharp.md` — and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/phase0-instructions-read.md`. Acceptance: that file exists and contains a `Timestamp:` field, a `Policy Order:` field, and four bullet lines naming the four paths above in that order. + +- [x] [P0-T2] Confirm the base ref resolves and record the current head. Run `git rev-parse 2b85134b42872e405602e6064e02dc9cda6c319b` and `git rev-parse HEAD` and `git rev-parse --abbrev-ref HEAD`, and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/base-ref.2026-08-31T20-40.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`. Acceptance: `EXIT_CODE: 0` for the base-ref command and the artifact records a 40-character object id for `2b85134b42872e405602e6064e02dc9cda6c319b`. + +- [x] [P0-T3] Bootstrap the repo-local .NET SDK by running `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Install-RepoDotNetSdk.ps1`, then run `dotnet --version` from the worktree root. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-sdk.2026-08-31T20-40.md` with the four required fields. Acceptance: the path `.dotnet-sdk\dotnet.exe` exists and `dotnet --version` records `EXIT_CODE: 0`. A failure here is a bootstrap failure, recorded with the header `BOOTSTRAP FAILURE`, never as a gate failure. Note: `global.json:3-10` pins SDK `8.0.205` with `paths` `[".dotnet-sdk", "$host$"]`, so before this step `dotnet --version` prints the `global.json` `errorMessage` instead of a version. `.dotnet-sdk/` is git-ignored by `.gitignore:350` (`.dotnet*/`) and therefore never enters the change set. + +- [x] [P0-T4] Populate `packages/` by running `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-Restore.ps1`, then run `(Get-ChildItem -Directory packages).Count`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-restore.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0` and the recorded directory count is greater than 0. Rationale recorded in the artifact: every first-party project declares `EnsureNuGetPackageBuildImports` whose `` fires at `BeforeTargets="PrepareForBuild"`, and `.claude/rules/csharp.md:77` wires each analyzer through an explicit `..\packages\...` path, so msbuild hard-fails without `packages/`. `packages/` is git-ignored by `.gitignore:191`. + +- [x] [P0-T5] Restore the manifest-pinned CSharpier by running `dotnet tool restore` from the worktree root. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-tool-restore.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0`. The manifest is `dotnet-tools.json` at the worktree root and pins `csharpier` to `1.2.6` (`dotnet-tools.json:5-11`). + +- [x] [P0-T6] Confirm the coverage collector is available by running `dotnet-coverage --version`. If the command does not resolve, install it with `dotnet tool install --global dotnet-coverage` and re-run `dotnet-coverage --version` until it succeeds. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-dotnet-coverage.2026-08-31T20-40.md` with the four required fields. Acceptance: the final recorded `dotnet-coverage --version` invocation has `EXIT_CODE: 0`. This is required because `scripts/vscode/Invoke-MSTestWithCoverage.ps1:292-294` throws when `dotnet-coverage` is absent. + +- [x] [P0-T7] Create the raw-tool-output directories this plan writes into, so that no msbuild file logger and no `Tee-Object` redirection fails for a missing parent directory. Run `New-Item -ItemType Directory -Force -Path 'TestResults\msbuild','TestResults\p0-t10','TestResults\p1-t3','TestResults\p3-t2','TestResults\p3-t4','TestResults\p4-t7','TestResults\p4-t8-repeat' | Out-Null` and then `(Test-Path 'TestResults\msbuild')`, `(Test-Path 'TestResults\p0-t10')`, `(Test-Path 'TestResults\p1-t3')`, `(Test-Path 'TestResults\p3-t2')`, `(Test-Path 'TestResults\p3-t4')`, `(Test-Path 'TestResults\p4-t7')`, and `(Test-Path 'TestResults\p4-t8-repeat')`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/bootstrap-testresults.2026-08-31T20-40.md` with the four required fields plus the seven `Test-Path` results. Acceptance: `EXIT_CODE: 0` and all seven recorded `Test-Path` results are `True`. `TestResults\` is git-ignored by `.gitignore:39` (`[Tt]est[Rr]esult*/`), so it never enters the change set. This is a bootstrap task: a failure here is recorded with the header `BOOTSTRAP FAILURE`, never as a gate failure. + +- [x] [P0-T8] Capture the baseline format state, read-only, by running `dotnet tool run csharpier check .` and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/format-check.2026-08-31T20-40.md` with the four required fields, transcribing the final summary line of the command output verbatim into `Output Summary:`. Acceptance: `EXIT_CODE: 0`. `check` is read-only, so its exit code alone is a real observation; the write-mode `format` command is deliberately kept out of the baseline so the baseline cannot become a blanket waiver for pre-existing drift. A non-zero exit here means pre-existing repository-wide format drift, which makes AC-14 unreachable inside this item's footprint; record it as `BLOCKED` and report to the orchestrator before proceeding. + +- [x] [P0-T9] Capture the baseline analyzer gate. Record the wall-clock start, then run the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\msbuild\p0-t8-analyzer.log;Verbosity=normal"`. Then run `Select-String -Path TestResults\msbuild\p0-t8-analyzer.log -SimpleMatch 'BreadcrumbDropDownOpenCoordinator.cs' | Select-String -SimpleMatch 'warning'` and record every distinct warning code it reports. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/analyzer-gate.2026-08-31T20-40.md` with the four required fields plus a `Baseline Warning Codes For BreadcrumbDropDownOpenCoordinator.cs:` list (write `none` when the list is empty). Acceptance: `EXIT_CODE: 0` and the artifact carries that warning-code list. `/t:Rebuild` is mandatory: MSBuild's up-to-date check does not invalidate on a command-line `/p:` change, so a warm `/t:Build` exits 0 with `CoreCompile` skipped and runs no analyzers. `/p:Nullable=enable` must not be added; no project carries a `` element and there is no `Directory.Build.props`, so it can never pass. + +- [x] [P0-T10] Capture the baseline type-check gate by running the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\msbuild\p0-t9-typecheck.log;Verbosity=normal"`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/typecheck-gate.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0` and the recorded `Command:` string contains neither `/p:Nullable=enable` nor `/t:Build`. `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:1` carries `#nullable enable`, so this gate already covers that file per-file. + +- [x] [P0-T11] Capture the baseline test-and-coverage state by running `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` with its console output tee'd to `TestResults\p0-t10\coverage-run.log`, then read `coverage\coverage.cobertura.xml` and record: the root `/coverage` `line-rate`, `lines-covered`, and `lines-valid` attributes; and for the single `class` node whose `filename` attribute equals `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs`, its `line-rate` attribute, the count of `line` elements selected by the class-relative XPath `./lines/line` (record as coordinator total lines), and the count of those whose `hits` attribute is greater than `0` (record as coordinator covered lines). Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md` with the four required fields plus `Baseline Repo Line Rate:`, `Baseline Repo Lines Covered:`, `Baseline Repo Lines Valid:`, `Baseline Coordinator Line Rate:`, `Baseline Coordinator Lines Covered:`, and `Baseline Coordinator Lines Valid:`, each a numeric value and not a placeholder. Also record `Baseline Total Tests:`, `Baseline Passed Tests:`, `Baseline Failed Tests:`, and `Baseline Failure Set:` — `none` when the failed count is `0`, otherwise the fully qualified name of every failing test. Those four run fields are read from the vstest run summary and the failed-result lines in `TestResults\p0-t10\coverage-run.log`, whose final summary line is transcribed verbatim into `Output Summary:`; the wrapper produces no TRX, for the reason recorded under **Execution Preconditions and Conventions**. Acceptance: the six numeric coverage fields are present, and the four run fields above are present. If the wrapper exits non-zero, record the observed exit code and the failure set, mark the artifact `BASELINE FAILURES PRESENT`, and continue; the recorded failure set becomes the tolerance floor for P4-T7. If the failure set contains any test in `QuickFiler.Test`, record `BLOCKED` and report to the orchestrator before proceeding, because AC-18 cannot then be satisfied. On a non-zero exit the six coverage fields cannot be read from the emitted file as-is, because the throw at `scripts/vscode/Invoke-MSTestWithCoverage.ps1:235-237` precedes the post-processing at `:339-343` that rewrites `class/@filename` to the repo-relative form and sets the root `lines-covered` and `lines-valid`; recover them by dot-sourcing `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1`, converting the emitted document in memory with `ConvertTo-KoverageCoberturaXml -XmlContent (Get-Content coverage\coverage.cobertura.xml -Raw -Encoding UTF8) -RepoRoot (Get-Location).Path`, and reading the same six values from the converted document. If the emitted document is absent or that conversion does not yield the six values, record all six as `NOT PRODUCED` with that reason, record `BLOCKED`, and report to the orchestrator, because P4-T8 then has no baseline to compare against. On the expected zero-exit path, `EXIT_CODE: 0` strictly implies zero failed tests, because `scripts/vscode/Invoke-MSTestWithCoverage.ps1:235-237` throws when the inner vstest exit code is non-zero. The class-relative `./lines` rollup is used rather than the descendant axis because Cobertura repeats every line under `./methods/method/lines`, so a descendant count is roughly double; `Get-CoberturaClassLineSummary` (`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:162`) applies the same reduction and may be used as a cross-check. No `lines-covered` or `lines-valid` attribute exists on a `class` node: `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:374-380` sets exactly `line-rate`, `branch-rate` and `complexity` on a class node, and `:442-445` sets `lines-covered` and `lines-valid` on the root `coverage` node only. The `filename` form is repo-relative with backslash separators because `ConvertTo-KoverageRelativePath` (`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:50-97`) strips the repo-root prefix and normalizes to the native separator, and `Merge-CoberturaClassesByFilename` collapses the file to a single `class` node. + +- [x] [P0-T12] Capture the pre-change source baseline. Record all of the following and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/source-baseline.2026-08-31T20-40.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` plus one labelled line per value. Acceptance: every one of the following recorded values matches the stated expected value. + - `(Get-Content -LiteralPath QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs).Count` equals `378`. + - `(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs).Count` equals `173`. + - `(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part2.cs).Count` equals `455`. + - `(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.cs).Count` equals `463`. + - `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s*[^/\s].*_host\.').Count` equals `5`. + - `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s').Count` equals `12`. + - `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'lock (_sync)').Count` equals `12`. + - `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count` equals `1`. + - `@(Get-ChildItem QuickFiler.Test -Recurse -Filter *.cs | Select-String -SimpleMatch 'CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain').Count` equals `0`. `Select-String` has no `-Recurse` parameter, so the file set is produced by `Get-ChildItem` and piped in. + + +### Phase 1 — Failing Regression Test + +The test is added before the production change so the recorded red-then-green pair required by AC-4 is obtainable. The red run in P1-T3 is deliberately scoped to the single new test by name; the full suite is not run while a test is deliberately failing, because a full-suite gate could not exit 0 in that state. + +- [x] [P1-T1] Append the regression test method to the existing `public sealed partial class BreadcrumbDropDownOpenCoordinatorTests` in `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, immediately after `LatchAfterRelease_IsIgnoredAndIssuesNoOpen` (which ends at `Part3.cs:171`) and before the closing brace of the class at `Part3.cs:172`. Do not repeat the `[TestClass]` attribute; it is declared once on the primary partial at `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:17`. Do not add any `using` directive. The method text: + + ``` + /// + /// Issue #656: after a close that returned true, with the host open again by a path that + /// reaches neither RequestOpen nor Invalidate, a further close must reach the host rather + /// than being suppressed by the completed-close flag. Deterministic: one thread, explicit + /// drain, no timers, no sleeps, no second thread, no temp files. + /// + [TestMethod] + public void CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain() + { + // Arrange: open the drop-down, then drive a close that the host accepts. + var harness = new CoordinatorHarness(); + harness.Host.Enqueue(Task.FromResult(true)); + Task opening = harness.Coordinator.RequestOpen(); + harness.Context.DrainUntil(opening); + opening.Result.Should().BeTrue(); + + harness.Coordinator.SetDroppedDown(false); + harness.Context.DrainAll(); + harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.Uncommitted); + harness.Host.IsOpen.Should().BeFalse("the host accepted the close"); + + // Act: the host becomes open again by a path that bypasses RequestOpen and Invalidate. + harness.Host.SetOpen(true); + harness.SelectorOpen = true; + harness.Coordinator.SetDroppedDown(false); + harness.Context.DrainAll(); + + // Assert + harness + .Host.CloseReasons.Should() + .Equal( + new[] + { + BreadcrumbDropDownCloseReason.Uncommitted, + BreadcrumbDropDownCloseReason.Uncommitted, + }, + "the close after a bypassing reopen must reach _host.Close a second time" + ); + } + ``` + + Acceptance: `(Select-String -Path QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs -SimpleMatch 'public void CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain()').Count` equals `1`, and `(Select-String -Path QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs -SimpleMatch 'harness.Host.SetOpen(true);').Count` equals `1`. + +- [x] [P1-T2] Build the solution so the scoped red run has a current `QuickFiler.Test.dll`. Run the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=TestResults\msbuild\p1-t2-build.log;Verbosity=normal"`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-build.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0` and the file `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` exists. The test compiles against unmodified production code, so a non-zero exit here is a defect in the test text, not an expected red. + +- [x] [P1-T3] [expect-fail] Run the new test alone and record the failing result. Commands: + + ``` + $vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' + $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 + New-Item -ItemType Directory -Force -Path 'TestResults\p1-t3' | Out-Null + & $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p1-t3' + ``` + + Then parse the produced TRX: `[xml]$t = Get-Content -LiteralPath (Get-ChildItem TestResults\p1-t3 -Filter *.trx | Select-Object -First 1).FullName -Raw` and read `$t.TestRun.ResultSummary.Counters` attributes `total`, `passed`, `failed`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `ExpectedExitCode: 1`, `Output Summary:`, and the three TRX counter values, plus the failure message text copied from the TRX `UnitTestResult` node. Acceptance: `EXIT_CODE: 1`, TRX `total` equals `1`, TRX `failed` equals `1`, TRX `passed` equals `0`. This is a direct vstest invocation rather than a wrapper call because neither `scripts/vscode/Invoke-MSTest.ps1:54` nor `scripts/vscode/Invoke-MSTestWithCoverage.ps1:76` accepts a `TestCaseFilter` override, and editing either script is outside the authorized footprint; the invocation reproduces both wrapper protections explicitly — `/InIsolation` and the `TestCategory!=LiveOutlook` conjunct — so no real Outlook process can be launched. + +- [x] [P1-T4] Record why the red is the expected red. Confirm from the P1-T3 failure message that the observed `CloseReasons` collection held exactly one element while two were expected, and append a `Red Cause:` section to `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/regression-testing/red-run.2026-08-31T20-40.md` naming `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:316` as the suppressing guard. Acceptance: that artifact contains a `Red Cause:` section and `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count` still equals `1`, proving the production file is still unmodified at this point. + + +### Phase 2 — Production Fix in CloseCore + +Only `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` changes in this phase. The guard order (released, then in-flight, then completed) is preserved, the `finally` that clears `_closeInFlight` is untouched, and the success block that increments `_generation` and sets `_closeCompleted` is untouched. + +- [x] [P2-T1] Hoist the host read out of the critical section. In `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`, insert the single statement `bool hostOpen = _host.IsOpen;` as the first statement of `CloseCore`, on its own line immediately after the opening brace of the method (currently `:309`) and immediately before the `lock (_sync)` that currently sits at `:310`. Acceptance: `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'bool hostOpen = _host.IsOpen;').Count` equals `1`, and that match's `LineNumber` is strictly less than the `LineNumber` of the first `lock (_sync)` match whose `LineNumber` is greater than the `LineNumber` of the `private bool CloseCore(` match. + +- [x] [P2-T2] Narrow the completed-close suppression. Replace the guard line currently reading `if (_closeCompleted)` inside `CloseCore` with `if (_closeCompleted && !hostOpen)`. Change nothing else on that line or the `return true;` beneath it. Acceptance: `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted && !hostOpen)').Count` equals `1` and `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted)').Count` equals `0`. The zero-hit half is meaningful because the replacement text is not a superstring of the searched literal: the searched literal ends with a closing parenthesis directly after `_closeCompleted`, which the replacement does not contain. The comment text added by P2-T3 and P2-T4 must not contain the literal `if (_closeCompleted)`. + +- [x] [P2-T3] Replace the `_closeCompleted` field XML documentation (currently `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:38-46`) so it records the new suppression condition, by appending this `remarks` block between the existing `` line and the `private bool _closeCompleted;` declaration: + + ``` + /// + /// Issue #656: the flag alone is not sufficient, because it is cleared only on the + /// and Invalidate paths. A host reopened by any other + /// path leaves it set, which suppressed a close the host was genuinely open for. + /// Suppression in therefore additionally requires the host to + /// report not open. + /// + ``` + + Acceptance: the contiguous run of `///` lines immediately preceding the `private bool _closeCompleted;` line contains exactly one line matching `Select-String -SimpleMatch 'Issue #656'`, and that block contains no line matching `Select-String -Pattern '_host\.'`. + +- [x] [P2-T4] Replace the `CloseCore` summary documentation (currently `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:302-307`) so it records the new guard and the reason the host read is taken outside `_sync`, by appending this `remarks` block between the existing `` line and the `private bool CloseCore(BreadcrumbDropDownCloseReason reason)` declaration: + + ``` + /// + /// Issue #656: the completed-close guard additionally requires the host to report not + /// open, so a close is not suppressed while the host is genuinely open again. The host + /// read is hoisted above the critical section deliberately: SR-4 of #501 declined the + /// same refinement written as a read taken inside the lock, because that adds a foreign + /// call made while the coordinator lock is held. Hoisting leaves the count of such calls + /// unchanged. The host state can change between the read and the lock; both directions + /// are analysed in the spec for this change and neither corrupts state. + /// + ``` + + Acceptance: the contiguous run of `///` lines immediately preceding the `private bool CloseCore(` line contains exactly one line matching `Select-String -SimpleMatch 'Issue #656'` and exactly one line matching `Select-String -SimpleMatch 'hoisted'`, and that block contains no line matching `Select-String -Pattern '_host\.'`. Across the whole file, `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'Issue #656').Count` equals `2`. + +- [x] [P2-T5] Verify the SR-4 lock-discipline invariant holds after the edits. Run `Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s*[^/\s].*_host\.'` and `Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'lock (_sync)'`, and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/lock-discipline.2026-08-31T20-40.md` with the four required fields plus the enumerated line numbers of both result sets. Acceptance: the non-comment `_host.` line count equals `6` (baseline `5` plus the one hoisted read), the `lock (_sync)` count equals `12` (unchanged from the P0-T12 baseline), and the artifact records that exactly one non-comment `_host.` line sits inside a `lock (_sync)` body — the pre-existing `if (_closeInFlight && _host.IsOpen)` in `RequestOpen` — with every other such line outside every lock body. + +- [x] [P2-T6] Verify no production seam was added. Run `Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s'` and append the count and the enumerated line numbers to `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/lock-discipline.2026-08-31T20-40.md` under a `Declared Member Lines:` section. Acceptance: the count equals `12`, unchanged from the P0-T12 baseline. The pattern excludes XML documentation lines because a `///` line's first non-whitespace character is a forward slash. + + +### Phase 3 — Pass-After Verification and Standing-Guard Regression + +- [x] [P3-T1] Rebuild the solution after the production edit. Run the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=TestResults\msbuild\p3-t1-build.log;Verbosity=normal"`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-build.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0`. + +- [x] [P3-T2] Re-run the new test alone and record the passing result. Commands, identical to P1-T3 apart from the results directory: + + ``` + $vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' + $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 + New-Item -ItemType Directory -Force -Path 'TestResults\p3-t2' | Out-Null + & $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName~CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain' '/Logger:trx' '/ResultsDirectory:TestResults\p3-t2' + ``` + + Parse the produced TRX for `ResultSummary/Counters` `total`, `passed`, `failed`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/green-run.2026-08-31T20-40.md` with `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and the three counter values. Acceptance: `EXIT_CODE: 0`, TRX `total` equals `1`, TRX `passed` equals `1`, TRX `failed` equals `0`. + +- [x] [P3-T3] Write the fail-before / pass-after comparison record required by AC-4 at `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md`. It must embed, verbatim, the `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:` blocks of both the red artifact (`evidence/regression-testing/red-run.2026-08-31T20-40.md`) and the green artifact (`evidence/qa-gates/green-run.2026-08-31T20-40.md`), and must name both source paths. Acceptance: the file exists, names both source paths, and records the pair `failed=1, passed=0` for the red run and `failed=0, passed=1` for the green run, both for the test `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain`. The artifact must additionally carry an `AC-4 Reconciliation:` section recording two departures from AC-4's stated check method and the reason for each: (a) the red run is stored under the canonical fail-before location `evidence/regression-testing/` required by `evidence-and-timestamp-conventions`, and this qa-gates comparison artifact embeds it verbatim so both outputs are present under `evidence/qa-gates/` as AC-4 requires; (b) both single-test runs use `vstest.console.exe` directly rather than `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, because neither wrapper accepts a `TestCaseFilter` override (`scripts/vscode/Invoke-MSTest.ps1:54`, `scripts/vscode/Invoke-MSTestWithCoverage.ps1:76`) and editing either is outside the authorized footprint; both wrapper protections, `/InIsolation` and `TestCategory!=LiveOutlook`, are reproduced explicitly in the direct invocation. Acceptance additionally requires that the artifact contains an `AC-4 Reconciliation:` section naming both departures. + +- [x] [P3-T4] Run the five standing-guard tests that must not regress, in one scoped invocation. Commands: + + ``` + $vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' + $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatform\vstest.console.exe' | Select-Object -First 1 + New-Item -ItemType Directory -Force -Path 'TestResults\p3-t4' | Out-Null + & $vstest 'QuickFiler.Test\bin\Debug\QuickFiler.Test.dll' '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/TestCaseFilter:TestCategory!=LiveOutlook&(FullyQualifiedName~PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose|FullyQualifiedName~SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired|FullyQualifiedName~RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync|FullyQualifiedName~CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce|FullyQualifiedName~PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen)' '/Logger:trx' '/ResultsDirectory:TestResults\p3-t4' + ``` + + The `FullyQualifiedName` disjunction is parenthesised so the `TestCategory!=LiveOutlook` conjunct applies to the whole group rather than to the final disjunct alone. `spec.md` **Test Strategy** makes that conjunct mandatory for any direct `vstest.console.exe` call. Parse the TRX counters. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/standing-guards.2026-08-31T20-40.md` with the four required fields plus the per-test outcome list read from the TRX `UnitTestResult` nodes. Acceptance: `EXIT_CODE: 0`, TRX `total` equals `5`, TRX `passed` equals `5`, TRX `failed` equals `0`, and the recorded per-test list names all five test names above. + + +### Phase 4 — Full QC Toolchain, Footprint, and Coverage + +This phase runs the full four-step toolchain unconditionally in order. There is no `IN_SCOPE` or `OUT_OF_SCOPE` branch and no `SKIPPED` completion path. If any of P4-T1 through P4-T7 fails or rewrites a tracked file, restart the loop at P4-T1. + +- [x] [P4-T1] Format. Run `dotnet tool run csharpier format .`, then immediately run `git status --porcelain -- QuickFiler QuickFiler.Test` and record its full output. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-apply.2026-08-31T20-40.md` with the four required fields plus a `Porcelain After Format:` section holding that output verbatim. Acceptance: `EXIT_CODE: 0` and the recorded `Porcelain After Format:` output contains exactly the two lines naming `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` and no other path. The tree observation is required in addition to the exit code because `csharpier format` is a write-mode command that exits 0 whether or not it rewrote a file. The porcelain span is scoped by pathspec to the two production trees so that tracked files under `.claude/agent-memory` and `artifacts/orchestration` cannot make the assertion unsatisfiable. + +- [x] [P4-T2] Verify formatting read-only. Run `dotnet tool run csharpier check .` and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/format-check.2026-08-31T20-40.md` with the four required fields, transcribing the final summary line of the output verbatim into `Output Summary:`. Acceptance: `EXIT_CODE: 0`. This satisfies AC-14. + +- [x] [P4-T3] Analyzer gate. Record the wall-clock start into the artifact as `Gate Start:`, then run the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=TestResults\msbuild\p4-t3-analyzer.log;Verbosity=normal"`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md` with the four required fields plus `Gate Start:`. Acceptance: `EXIT_CODE: 0` and `(Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch '0 Error(s)').Count` is greater than `0`. + +- [x] [P4-T4] Prove the analyzer gate was not vacuous. Run `(Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch 'Skipping target "CoreCompile"').Count`, and read `(Get-Item QuickFiler\bin\Debug\QuickFiler.dll).LastWriteTime` and `(Get-Item QuickFiler.Test\bin\Debug\QuickFiler.Test.dll).LastWriteTime`. Append a `Non-Vacuity:` section to `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md` recording the count and both timestamps. Acceptance: the recorded count equals `0`, and both recorded `LastWriteTime` values are later than the `Gate Start:` value recorded in P4-T3. The timestamp comparison is the positive control: it proves both assemblies were recompiled during this gate, so the zero count reports a genuinely absent skip rather than an empty or mis-scoped log. This satisfies AC-16. + +- [x] [P4-T5] Prove the analyzer gate introduced no new warning for the changed production file. Run `Select-String -Path TestResults\msbuild\p4-t3-analyzer.log -SimpleMatch 'BreadcrumbDropDownOpenCoordinator.cs' | Select-String -SimpleMatch 'warning'` and record every distinct warning code. Append a `Post-Change Warning Codes For BreadcrumbDropDownOpenCoordinator.cs:` section to the P4-T3 artifact (write `none` when empty). Acceptance: the recorded post-change warning-code set is a subset of the `Baseline Warning Codes For BreadcrumbDropDownOpenCoordinator.cs:` set recorded in `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/analyzer-gate.2026-08-31T20-40.md`. Together with P4-T3 this satisfies AC-15. + +- [x] [P4-T6] Type-check gate. Run the vswhere-resolved msbuild with `TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true "/flp:LogFile=TestResults\msbuild\p4-t6-typecheck.log;Verbosity=normal"`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md` with the four required fields. Acceptance: `EXIT_CODE: 0`, `(Select-String -Path TestResults\msbuild\p4-t6-typecheck.log -SimpleMatch '0 Error(s)').Count` is greater than `0`, and the recorded `Command:` string contains `/t:Rebuild` and contains neither `/p:Nullable=enable` nor `/t:Build`. This satisfies AC-17. + +- [x] [P4-T7] Test gate with coverage. Run `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` with console output tee'd to `TestResults\p4-t7\coverage-run.log`. Then read `coverage\coverage.cobertura.xml` and record: the root `/coverage` `line-rate`, `lines-covered`, and `lines-valid` attributes; and for the single `class` node whose `filename` attribute equals `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs`, its `line-rate` attribute, the count of `line` elements selected by the class-relative XPath `./lines/line` (record as coordinator total lines), and the count of those whose `hits` attribute is greater than `0` (record as coordinator covered lines). Also run `Select-String -Path scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SimpleMatch '/TestCaseFilter:TestCategory!=LiveOutlook'` and `Select-String -Path scripts\vscode\Invoke-MSTestWithCoverage.ps1 -SimpleMatch '/InIsolation'` and record the matched line numbers. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/test-coverage.2026-08-31T20-40.md` with the four required fields plus `Post-Change Repo Line Rate:`, `Post-Change Repo Lines Covered:`, `Post-Change Repo Lines Valid:`, `Post-Change Coordinator Line Rate:`, `Post-Change Coordinator Lines Covered:`, `Post-Change Coordinator Lines Valid:`, and `Wrapper Filter Lines:`. Also record `Post-Change Total Tests:`, `Post-Change Passed Tests:`, `Post-Change Failed Tests:`, and `Post-Change Failure Set:`, read from the vstest run summary and the failed-result lines in `TestResults\p4-t7\coverage-run.log` for the same no-TRX reason recorded under **Execution Preconditions and Conventions**. Acceptance: all six numeric coverage fields present and numeric; both `Select-String` results report line `76`; the recorded `Post-Change Failure Set:` contains no test in `QuickFiler.Test` and is a subset of the `Baseline Failure Set:` recorded in `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md`; and `Post-Change Total Tests:` equals the baseline total plus one, since this change adds exactly one test and removes none. When the baseline failure set is `none`, that subset condition reduces to `EXIT_CODE: 0`, which is the expected case and strictly implies zero failed tests because `scripts/vscode/Invoke-MSTestWithCoverage.ps1:235-237` throws when the inner vstest exit code is non-zero. No `lines-covered` or `lines-valid` attribute exists on a `class` node: `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:374-380` sets exactly `line-rate`, `branch-rate` and `complexity` there, and `:442-445` sets `lines-covered` and `lines-valid` on the root `coverage` node only. This satisfies AC-18. + +- [x] [P4-T8] Verify coverage on the changed lines and the coverage delta. Derive the two changed line numbers mechanically: `A` is the `LineNumber` of `Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'bool hostOpen = _host.IsOpen;'` and `B` is the `LineNumber` of `Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -SimpleMatch 'if (_closeCompleted && !hostOpen)'`. In `coverage\coverage.cobertura.xml`, read the `hits` attribute of the `line` nodes with `number` equal to `A` and to `B` under the `class` node whose `filename` equals `QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/coverage-delta.2026-08-31T20-40.md` with the four required fields plus `Changed Line A:`, `Changed Line A Hits:`, `Changed Line B:`, `Changed Line B Hits:`, `Baseline Repo Line Rate:`, `Post-Change Repo Line Rate:`, `Baseline Coordinator Line Rate:`, and `Post-Change Coordinator Line Rate:`. Acceptance: both recorded hit counts are greater than or equal to `1`; the post-change repo line rate is greater than or equal to `0.80`; and the post-change coordinator line rate is greater than or equal to the baseline coordinator line rate recorded in `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/baseline/test-coverage.2026-08-31T20-40.md`. The coordinator-rate comparison is a candidate signal, not a hard gate on a single measurement. Per-file `lines-covered` is not deterministic in this repository between two runs against the same tree, while `lines-valid` is; the measurement is recorded in `.claude/agent-memory/orchestrator/coverage-lines-covered-is-nondeterministic.md`, where two Cobertura documents of the same tree carry identical `lines-valid` for all 550 files while per-file `lines-covered` moves by up to four lines. Therefore: if the post-change coordinator line rate is greater than or equal to the baseline, record PASS. If it is lower, re-run `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` exactly once with output tee'd to `TestResults\p4-t8-repeat\coverage-run.log`, record the second measurement in the same artifact under `Repeat Coordinator Line Rate:`, and fail only if the second measurement is also lower. The repeat run overwrites `coverage\coverage.cobertura.xml`; it is read only for the coordinator line rate, and the values already transcribed by P4-T7 stand. No third execution is authorized. Additionally record `Baseline Coordinator Lines Valid:` and `Post-Change Coordinator Lines Valid:`, both taken from the counts of `./lines/line` recorded by P0-T11 and P4-T7, and state that the post value equals the baseline value plus one, which is the deterministic quantity and is what a genuine instrumented-size change would move. If the observed difference is not exactly one, record `LINES-VALID DELTA UNEXPECTED` with both values and report to the orchestrator rather than recording PASS. + +- [x] [P4-T9] Verify the file-size limit. Run `(Get-Content -LiteralPath QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs).Count` and `(Get-Content -LiteralPath QuickFiler.Test\Viewers\BreadcrumbDropDownOpenCoordinatorTests.Part3.cs).Count` and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/file-size.2026-08-31T20-40.md` with the four required fields plus both counts. Acceptance: both recorded counts are strictly less than `500`. This task runs after the final format pass in P4-T1, so the counts measure the formatted files. This satisfies AC-13. + +- [x] [P4-T10] Commit the change so the anchored diff assertions in P4-T11 through P4-T14 are non-vacuous. Run `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656` then `git commit -m "fix(breadcrumb): stop suppressing a close while the host reports open (#656)"` then `git rev-parse HEAD`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/commit.2026-08-31T20-40.md` with the four required fields plus the resulting commit id. Acceptance: `EXIT_CODE: 0` for the commit and `git status --porcelain -- QuickFiler QuickFiler.Test` returns no output. The `git add` pathspec is scoped so that tracked files under `.claude/agent-memory` and `artifacts/orchestration` are not swept into this commit. + +- [x] [P4-T11] Verify the production footprint. Run `git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- QuickFiler` and `git status --porcelain -- QuickFiler`, and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-production.2026-08-31T20-40.md` with the four required fields plus both outputs verbatim. Acceptance: the diff output is exactly the single line `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and the porcelain output is empty. The porcelain span is the companion required because a name-listing diff cannot report an untracked path. This satisfies AC-10. + +- [x] [P4-T12] Verify the build-configuration footprint. Run `git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- '*.csproj' '*.props' '*.targets' '*packages.config'` and `git status --porcelain -- '*.csproj' '*.props' '*.targets' '*packages.config'`, and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md` with the four required fields plus both outputs verbatim. Acceptance: both outputs are empty. This satisfies AC-11. + +- [x] [P4-T13] Verify the test footprint. Run `git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- QuickFiler.Test` and `git status --porcelain -- QuickFiler.Test`, and write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/footprint-test.2026-08-31T20-40.md` with the four required fields plus both outputs verbatim. Acceptance: the diff output is exactly the single line `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` and the porcelain output is empty. Neither `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs` nor `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` appears, which is the mechanical proof that the five standing-guard tests were not edited. This satisfies AC-12, and together with P3-T4 it satisfies AC-5, AC-6, AC-7, and AC-8. + +- [x] [P4-T14] Verify no new production seam. Run `git diff --name-only 2b85134b42872e405602e6064e02dc9cda6c319b...HEAD -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs`, `git status --porcelain -- QuickFiler/Viewers/IBreadcrumbDropDownHost.cs`, and `(Select-String -Path QuickFiler\Viewers\BreadcrumbDropDownOpenCoordinator.cs -Pattern '^\s+(internal|public)\s').Count`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/qa-gates/no-new-seam.2026-08-31T20-40.md` with the four required fields plus all three results. Acceptance: both git outputs are empty and the declared-member count equals `12`, unchanged from the P0-T12 baseline. This satisfies AC-20. + + +### Phase 5 — Acceptance Criteria Check-Off and Close-Out + +Each check-off task marks exactly one acceptance criterion in `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md` by changing that criterion's leading `- [ ] AC-N —` to `- [x] AC-N —`. The trailing space and em dash are part of every match so that `AC-1` is never confused with `AC-10` through `AC-19`, and `AC-2` is never confused with `AC-20`. A criterion is checked only when its named evidence artifact exists and its acceptance was met. + +- [x] [P5-T1] Check off AC-1 against `evidence/other/lock-discipline.2026-08-31T20-40.md` and the P2-T1 and P2-T2 results. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-1 —').Count` equals `1`. + +- [x] [P5-T2] Check off AC-2 against `evidence/other/lock-discipline.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-2 —').Count` equals `1`. + +- [x] [P5-T3] Check off AC-3 against the P1-T1 result and `evidence/qa-gates/green-run.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-3 —').Count` equals `1`. + +- [x] [P5-T4] Check off AC-4 against `evidence/qa-gates/red-green-comparison.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-4 —').Count` equals `1`. + +- [x] [P5-T5] Check off AC-5 against `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-5 —').Count` equals `1`. + +- [x] [P5-T6] Check off AC-6 against `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-6 —').Count` equals `1`. + +- [x] [P5-T7] Check off AC-7 against `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-7 —').Count` equals `1`. + +- [x] [P5-T8] Check off AC-8 against `evidence/qa-gates/standing-guards.2026-08-31T20-40.md` and `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-8 —').Count` equals `1`. + +- [x] [P5-T9] Check off AC-9 against `evidence/qa-gates/standing-guards.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-9 —').Count` equals `1`. + +- [x] [P5-T10] Check off AC-10 against `evidence/qa-gates/footprint-production.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-10 —').Count` equals `1`. + +- [x] [P5-T11] Check off AC-11 against `evidence/qa-gates/footprint-buildconfig.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-11 —').Count` equals `1`. + +- [x] [P5-T12] Check off AC-12 against `evidence/qa-gates/footprint-test.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-12 —').Count` equals `1`. + +- [x] [P5-T13] Check off AC-13 against `evidence/qa-gates/file-size.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-13 —').Count` equals `1`. + +- [x] [P5-T14] Check off AC-14 against `evidence/qa-gates/format-check.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-14 —').Count` equals `1`. + +- [x] [P5-T15] Check off AC-15 against `evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-15 —').Count` equals `1`. + +- [x] [P5-T16] Check off AC-16 against the `Non-Vacuity:` section of `evidence/qa-gates/analyzer-gate.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-16 —').Count` equals `1`. + +- [x] [P5-T17] Check off AC-17 against `evidence/qa-gates/typecheck-gate.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-17 —').Count` equals `1`. + +- [x] [P5-T18] Check off AC-18 against `evidence/qa-gates/test-coverage.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-18 —').Count` equals `1`. + +- [x] [P5-T19] Check off AC-19 against the P2-T3 and P2-T4 results. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-19 —').Count` equals `1`. + +- [x] [P5-T20] Check off AC-20 against `evidence/qa-gates/no-new-seam.2026-08-31T20-40.md`. Acceptance: `(Select-String -Path docs\features\active\2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656\spec.md -SimpleMatch '- [x] AC-20 —').Count` equals `1`. + +- [x] [P5-T21] Write the acceptance-criteria status summary at `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/issue-updates/ac-status.2026-08-31T20-40.md`, listing all twenty identifiers AC-1 through AC-20 with their status and the evidence path that establishes each. Acceptance: the file exists, contains a `Timestamp:` field, and contains exactly twenty lines each beginning with one of `AC-1` through `AC-20`, with no identifier repeated. + +- [x] [P5-T22] Commit the remaining feature-folder evidence and confirm the scoped tree is clean. Run `git add docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656` then `git commit -m "docs(issue-656): record QA gate and acceptance evidence"` then `git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656`. Write `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/evidence/other/final-commit.2026-08-31T20-40.md` with the four required fields plus the resulting commit id. Acceptance: the commit records `EXIT_CODE: 0` and the scoped `git status --porcelain` output is empty. The status span is scoped by pathspec because `.claude/agent-memory` is tracked in this repository and `artifacts/orchestration/orchestrator-state.json` is tracked despite the `artifacts/` entry in `.gitignore`; an unscoped clean-tree assertion would be unsatisfiable. No `git update-index` command is run by any task in this plan. The check-offs for P5-T1 through P5-T21 are written before this task's `git add`, so they are included in this commit. The check-off for P5-T22 itself is written after the commit and therefore leaves `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/plan.2026-08-31T20-10.md` modified. The `evidence/other/final-commit.2026-08-31T20-40.md` artifact this task writes is likewise created after the status command runs, so it is a second expected residual. Both residuals are committed by the orchestrator at its next checkpoint; neither invalidates the acceptance above, which is evaluated at the moment the status command runs and therefore before either residual exists. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/policy-audit.2026-09-01T15-03.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/policy-audit.2026-09-01T15-03.md new file mode 100644 index 000000000..f3de57031 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/policy-audit.2026-09-01T15-03.md @@ -0,0 +1,265 @@ +# Policy Audit — Issue #656 (breadcrumb `_closeCompleted` residual) + +- Timestamp: 2026-09-01T15-03 +- Feature folder: `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/` +- Branch: `bug/breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656` +- Head SHA: `65d2f22b5100588eae8ac4de40e48f1ac391db34` +- Base branch: `main`; `git merge-base origin/main HEAD` = `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` +- Work mode marker in `issue.md`: `- Work Mode: full-bug` (line 12). AC source is `spec.md` only. + `user-story.md` is correctly absent and is not a defect in this mode. +- Audited scope: the full branch diff against the resolved base. 46 paths, 3762 insertions, + 1 deletion. + +## Base resolution and scope + +Independently re-derived, not taken from any supplied figure: + +| Command | Result | +|---|---| +| `git rev-parse HEAD` | `65d2f22b5100588eae8ac4de40e48f1ac391db34` | +| `git rev-parse origin/main` | `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` | +| `git merge-base origin/main HEAD` | `5670b3cfe6a52e3b890bf80f0cd85a20d4fe4723` | +| `git diff --name-only origin/main...HEAD` | 46 paths | +| `git diff --shortstat origin/main...HEAD` | 46 files changed, 3762 insertions(+), 1 deletion(-) | + +`origin/main` is an ancestor of HEAD, so the three-dot and two-dot forms coincide and both isolate +the branch's own contribution. + +Composition of the 46 paths: + +- 2 C# files: `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` (+18/-1) and + `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (+40/-0). +- 44 Markdown files: 7 agent-memory notes and 37 feature-folder documents. +- Zero build-configuration files. Zero TypeScript, Python, or PowerShell files. + +## PR context artifacts + +`artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt` were both absent at the +start of this review. No PR-context collector script exists in this repository and the MCP collector +was not available in this session, so both artifacts were regenerated by hand directly from git, +anchored to the merge-base above, and are now present at their canonical paths. Every figure in this +audit is derived from git and from on-disk build/test logs, not from the regenerated pair. + +## Rejected Scope Narrowing + +The caller prompt supplied a description of the change ("THE CHANGE UNDER REVIEW. Two code paths +only") and a set of pre-measured facts. This was treated as a claim to falsify, not as a scope +instruction. The full 46-path branch diff was audited. The caller's description of the two code +paths was independently confirmed to be exactly correct. + +One caller instruction bore on verdict formation and is recorded verbatim: + +> "COVERAGE AUTHORITY — READ THIS BEFORE RAISING ANY COVERAGE FINDING. The binding floor for this +> repository is CLAUDE.md's **>= 80% repo-wide and >= 90% for new modules/classes/methods**, with +> the COM/VSTO/WinForms testable-denominator exemption. The 85%/75% line-and-branch figures in +> `.claude/rules/general-unit-test.md` and `.claude/rules/quality-tiers.md` were imported from a +> reference repository and are NOT this repo's operative gate ... Do not synthesize a coverage XML, +> and do not compute a rival denominator and fail the feature on it." + +Justification for how this was handled: the precedence claim is consistent with +`policy-compliance-order`, which ranks `CLAUDE.md` above `.claude/rules/`, and the repository +genuinely carries both figures unreconciled. It was neither accepted nor rejected on authority. +Instead, every coverage figure below was measured independently from the on-disk Cobertura document +and is reported against **both** the CLAUDE.md floors and the `.claude/rules` floors. The measured +figures clear both, so the doc conflict does not change any verdict in this audit and no rival +denominator was needed. The unreconciled 80/90 versus 85/75 discrepancy between `CLAUDE.md` and +`.claude/rules/` is recorded as a repository-level documentation defect, not as a finding against +this branch. + +No caller instruction attempted to limit the audited file set, to mark a language as out of the +audit, or to skip a toolchain check. Nothing else was rejected. + +## Evidence Location Compliance + +`git diff --name-only origin/main...HEAD` was filtered for paths under `artifacts/baselines/`, +`artifacts/qa/`, `artifacts/evidence/` and `artifacts/coverage/`. **Zero matches.** All 33 execution +evidence artifacts are written under +`/evidence/{baseline,qa-gates,regression-testing,issue-updates,other}/`, which is the +canonical location required by `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`. + +`validate_evidence_locations.py` is not present in this repository, so the scan was performed +directly against the branch diff as described above. Verdict: **PASS**. + +This review agent wrote no evidence artifacts and therefore has no +`EVIDENCE_LOCATION_OVERRIDE_REJECTED` entries. + +## Toolchain compliance (CLAUDE.md C# order: format, analyze, type-check, test) + +Each gate below was re-verified by this reviewer against the raw logs retained under +`TestResults/`, not read from the executor's summary prose. + +| Stage | Command shape | Independent verification | Verdict | +|---|---|---|---| +| 1. Format | `dotnet tool run csharpier check .` | Re-executed by this reviewer in this session: `Checked 1566 files in 4543ms.`, exit 0, no file listed as requiring formatting. | PASS | +| 2. Analyze | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | `TestResults/msbuild/p4-t3-analyzer.log`: one `0 Error(s)` summary; `5 Warning(s)`; zero warning lines naming `BreadcrumbDropDownOpenCoordinator.cs`. Baseline log `p0-t8-analyzer.log` also reports `5 Warning(s)`, so the warning count is unchanged. | PASS | +| 2a. Analyzer non-vacuity | as above | `grep -c 'Skipping target "CoreCompile"'` over `p4-t3-analyzer.log` returns **0**, so no project skipped compilation and the analyzers actually ran on the changed files. | PASS | +| 3. Type-check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | `TestResults/msbuild/p4-t6-typecheck.log`: one `0 Error(s)` summary. `grep -c "Nullable=enable"` returns 0, confirming the prohibited property was not added. `/t:Rebuild` used, not `/t:Build`. | PASS | +| 4. Test | `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` | `TestResults/p4-t7/coverage-run.log` lines 6948-6949: `Total tests: 6926`, `Passed: 6926`, with no `Failed:` line. Wrapper pins `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` on line 76 of the script. | PASS | + +The four stages are recorded as completing in a single pass with no auto-fix and no restart. The +retained log timestamps (14:50 analyzer, 14:50 type-check, 14:51-14:52 test) are consistent with one +ordered pass over an unchanged tree. + +Toolchain command-shape rules from `CLAUDE.md` were all honoured: `dotnet tool run` used for +CSharpier rather than a global install; `/t:Rebuild` rather than `/t:Build`; no `/p:Nullable=enable`; +no bare `vstest.console.exe` for the full-suite gate. + +## Coverage verification + +Coverage was verified by inspecting the coverage document produced during execution. No coverage run +was re-executed and no coverage XML was synthesized. + +Artifact inspected: `coverage/coverage.cobertura.xml` (present in the working tree, gitignored via +`.gitignore:144`, written 2026-09-01 14:52 by the P4-T7 wrapper run). The canonical hook path +`artifacts/csharp/coverage.xml` does not exist in this checkout; the Cobertura document above is the +document the recorded figures were derived from and it was read directly for this audit. + +Root element, read verbatim from the file: + +``` + +``` + +Changed-file class node, read verbatim: + +``` + +``` + +Changed lines, extracted from that class node: + +``` += 80 percent, new modules/classes/methods >= 90 percent): repo-wide + 85.3732 percent clears 80 percent. This change adds no new class and no new method to production + code; it adds one executable statement and narrows one existing conditional, and both of those + lines are executed. The enclosing class stands at 98.3193 percent, above 90 percent. +- Against `.claude/rules/general-unit-test.md` and `.claude/rules/quality-tiers.md` (line >= 85 + percent, branch >= 75 percent, no regression on changed lines): repo-wide line 85.3732 percent + clears 85 percent and repo-wide branch 79.3761 percent clears 75 percent. + +No-regression on changed lines: both changed lines carry hits >= 1, so changed-line coverage is 100 +percent and cannot have regressed. The coordinator's own line rate moved from 0.983122 (baseline) to +0.983193 (post-change), an increase, and its `lines-valid` moved 237 to 238, exactly the one +executable statement this change adds. The repository line rate moved from 0.853792 to 0.853732, a +decrease of 6.0e-5 (six thousandths of a percentage point) that is inside this repository's known +per-run `lines-covered` nondeterminism band and is not attributable to the changed lines, all of +which are covered. + +### Coverage Exclusion Policy + +The diff adds no `[ExcludeFromCodeCoverage]` attribute, removes none, and touches no +`coverage.config` or coverage-tooling configuration. No production source path was excluded from +measurement by this change. Verdict: PASS. + +## General Code Change Policy + +| Requirement | Evidence | Verdict | +|---|---|---| +| Simplicity first | The production delta is one hoisted local and one added conjunct. No new type, no new indirection, no new abstraction. | PASS | +| Separation of concerns | Unchanged. Pure guard logic remains in the coordinator; host I/O stays behind `IBreadcrumbDropDownHost`. | PASS | +| File size < 500 lines | `BreadcrumbDropDownOpenCoordinator.cs` 395 lines (was 378); `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` 213 lines (was 173). Counted with `awk 'END{print NR}'`, not `Measure-Object -Line`. Both under 500 and both grew. | PASS | +| Public API stability | Declared `internal`/`public` member count on the coordinator is 12 before and 12 after. `IBreadcrumbDropDownHost.cs` is absent from the diff. No seam widened. | PASS | +| Error handling / fail fast | Unchanged. The `finally` that clears `_closeInFlight` is untouched. | PASS | +| Comment *why*, not what | Both added `` blocks explain the reason for the guard and the reason the host read sits outside the lock, including the SR-4 precedent that shaped it. | PASS | +| No new dependencies | None added. | PASS | +| Bugfix workflow: failing regression test first | Recorded fail-before (`evidence/regression-testing/red-run`, 1 total / 0 passed / 1 failed, with the verbatim FluentAssertions failure message) and pass-after (`evidence/qa-gates/green-run`, 1 total / 1 passed / 0 failed). The red failure is an assertion failure, not a compile failure, which is the stronger form. | PASS | +| Bugfix workflow: minimal targeted fix | Two production lines and two documentation blocks. No opportunistic refactor. Deeper design questions were routed to the spec rather than into the diff. | PASS | + +## General and C# Unit Test Policy + +| Requirement | Evidence | Verdict | +|---|---|---| +| MSTest framework | `[TestMethod]` on the added test, in an existing `[TestClass]` partial. No xUnit or NUnit introduced. | PASS | +| FluentAssertions | `.Should().Equal(...)`, `.Should().BeTrue()`, `.Should().BeFalse()` used throughout the added test. | PASS | +| Moq where mocking is needed | The added test uses the existing `CoordinatorHarness` fake rather than a mock; no new mocking. Consistent with the surrounding file. | PASS | +| Independence and isolation | The test constructs its own `CoordinatorHarness`; no shared or static state; targets one behaviour of `CloseCore`. | PASS | +| Determinism | Single thread. Explicit `DrainUntil`/`DrainAll` pumping of a controlled context. No `Thread.Sleep`, no `Task.Delay`, no timer, no wall-clock read, no second thread. The XML doc states these properties explicitly. | PASS | +| No temporary files | None created by the test. | PASS | +| No external dependencies | No filesystem, network, or Outlook interop touched. `TestCategory!=LiveOutlook` is enforced by the wrapper for the full-suite run. | PASS | +| Arrange-Act-Assert | Three sections, each labelled with a comment. | PASS | +| Descriptive intent | Method name states the scenario and the expectation; the XML doc restates it and names the issue. The `Equal(...)` assertion carries a because-reason. | PASS | +| Test file location | `QuickFiler.Test/Viewers/...` mirrors `QuickFiler/Viewers/...`. No colocation in the production tree. | PASS | +| Scenario completeness for the changed conjunct | `!hostOpen == false` (suppression released) by the new test; `!hostOpen == true` (suppression retained) by three standing guards that were run and passed. Both outcomes exercised. See CR-3 in the code review for the limit of this claim. | PASS | +| Standing regression contracts preserved | Five named guard tests all `Passed` in a scoped run, and mechanically proven unedited because neither `BreadcrumbDropDownOpenCoordinatorTests.cs` nor `...Part2.cs` appears in the branch diff. | PASS | + +## Tonality Policy + +All 33 evidence artifacts, `spec.md`, `plan.2026-08-31T20-10.md` and the research artifact were +reviewed for tone. Language is factual and measured throughout; departures from stated method are +reported plainly rather than minimised; no humour, hyperbole, or decorative metaphor was found. +Verdict: PASS. + +## Artifact hygiene + +`grep` over the entire feature folder for a user-profile directory prefix, the account name, and a +POSIX-style user-home form returns zero matches. The two absolute paths that do appear are +`C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe` and the derived +`vstest.console.exe` path, which are machine-independent product install locations carrying no +account or host name. Verdict: PASS. + +## Policy verdict summary + +| Area | Verdict | +|---|---| +| Scope and base resolution | PASS | +| Evidence location compliance | PASS | +| Toolchain: format | PASS | +| Toolchain: analyze (and non-vacuity) | PASS | +| Toolchain: type-check | PASS | +| Toolchain: test | PASS | +| C# coverage | PASS | +| Coverage exclusion policy | PASS | +| General code change policy | PASS | +| Bugfix workflow (red before green) | PASS | +| General and C# unit test policy | PASS | +| Tonality | PASS | +| Artifact hygiene | PASS | + +**FAIL verdicts: 0. Blocking PARTIAL verdicts: 0.** + +Non-blocking findings are recorded in `code-review.2026-09-01T15-03.md` (CR-1 through CR-5) and +`feature-audit.2026-09-01T15-03.md`. None of them blocks merge. No +`remediation-inputs.2026-09-01T15-03.md` was produced, because no finding meets the +remediation-required bar. diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/research/2026-08-31T20-15-closecompleted-residual-reopen-path-enumeration.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/research/2026-08-31T20-15-closecompleted-residual-reopen-path-enumeration.md new file mode 100644 index 000000000..0f7b91aaa --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/research/2026-08-31T20-15-closecompleted-residual-reopen-path-enumeration.md @@ -0,0 +1,714 @@ +# Issue #656 — `_closeCompleted` residual: reopen-path enumeration and remedy analysis + +- **Timestamp:** 2026-08-31T20-15 +- **Issue:** #656 (`docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/`) +- **Scope:** research only. No source, configuration, or project file was modified. +- **Tree state:** worktree at branch `docs/parallel-session-notes-2026-08-29`, clean at session start; every line number below was re-derived by reading the files in this worktree during this session. + +--- + +## Executive answer + +**A reopen path that reaches neither `RequestOpen` nor `Invalidate` DOES NOT exist in the shipped +production code today.** The single statement in the repository that makes the drop-down host open is +`QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:268` (`_host.OpenState = true;`), and the call +chain leading to it is closed: it is reachable only from `BreadcrumbDropDownHost.Open.cs:88`, which is +reachable only from the two `OpenAsync` overloads at `BreadcrumbDropDownHost.Open.cs:22` and `:37`, +whose only production invocations are `BreadcrumbDropDownOpenCoordinator.cs:258` and `:259` inside +`BeginOpenCore`, which is called only from `BreadcrumbDropDownOpenCoordinator.cs:218` inside +`OpenCoreAsync`, which is constructed only at `BreadcrumbDropDownOpenCoordinator.cs:115` — the +statement immediately after `_closeCompleted = false;` at `:114` in `RequestOpen`. + +Issue #656 is therefore **latent-correctness hardening, not an observed user-facing failure**, exactly +as the issue's own severity note states. + +--- + +## 1. Verified Source Facts + +All line numbers re-derived from `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` +(378 lines total) in this worktree. + +| Fact | File:line | Exact source line | +| --- | --- | --- | +| Field declaration | `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:46` | ` private bool _closeCompleted;` | +| Set site (successful-close path in `CloseCore`) | `…OpenCoordinator.cs:335` | ` _closeCompleted = true;` | +| Early-return suppression guard in `CloseCore` | `…OpenCoordinator.cs:316-317` | ` if (_closeCompleted)` / ` return true;` | +| Clear site 1 (`RequestOpen`) | `…OpenCoordinator.cs:114` | ` _closeCompleted = false;` | +| Clear site 2 (`Invalidate`) | `…OpenCoordinator.cs:352` | ` _closeCompleted = false;` | + +Enclosing context, verified: + +- `CloseCore` spans `:308-342`. Its guard block is `:310-319`: + `if (_released) return false;` (`:312-313`) → `if (_closeInFlight) return true;` (`:314-315`) → + `if (_closeCompleted) return true;` (`:316-317`) → `_closeInFlight = true;` (`:318`). +- The set site sits inside `if (closed) { lock (_sync) { _generation++; _closeCompleted = true; } return true; }` + at `:330-338`. `_generation++` is `:334`; `_closeCompleted = true` is `:335`. +- `_host.Close(reason)` is invoked at `:323`, **outside** `lock (_sync)`, with `_closeInFlight` cleared + in a `finally` at `:325-329`. +- `RequestOpen` spans `:104-118`. `_closeCompleted = false;` at `:114` is immediately followed by + `_currentOpenTask = OpenCoreAsync(_generation);` at `:115`. +- `Invalidate(bool release)` spans `:344-356`; `_closeCompleted = false;` at `:352` sits between + `_currentOpenTask = null;` (`:351`) and `_released = release;` (`:353`). +- `Invalidate` has exactly two callers, both in the same file: `Reset()` at `:188` + (`Invalidate(release: false)`) and `Release()` at `:204` (`Invalidate(release: true)`). + +The field is confined to one file. A repository-wide `*.cs` search for `_closeCompleted` returns six +hits, all in `BreadcrumbDropDownOpenCoordinator.cs`: `:41` (XML doc cross-reference), `:46` +(declaration), `:114`, `:316`, `:335`, `:352`. No test file references it by name and no reflective +write reaches it (see §7). + +--- + +## 2. Reopen Path Enumeration + +### 2.1 Method + +The enumeration is anchored on the host's own open-state variable rather than on the name `OpenAsync`, +because that variable is what `IBreadcrumbDropDownHost.IsOpen` reports and what "the drop-down host is +open" means to the coordinator. `BreadcrumbDropDownHost.IsOpen` is a get-only expression-bodied +property, `public bool IsOpen => OpenState;` (`QuickFiler/Viewers/BreadcrumbDropDownHost.cs:191`), over +`internal bool OpenState { get; set; }` (`…Host.cs:244`). Every way for the host to become open is +therefore a write of `true` to `OpenState`, and the enumeration walks the call graph upward from there. + +Three additional surfaces that could conceivably make the popup visible without that write were checked +and excluded, so the enumeration covers the whole family rather than one named method: + +- **`ShowPopup`** (`QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs:98-102`), which performs the + actual `_showPopup(DropDown, Anchor, location)` native show at `:101`. Its only invocation is + `BreadcrumbDropDownOpenLifetime.cs:276`, inside `ShowCurrentSurface`, *after* `_host.OpenState = true` + at `:268`. It is not an independent entry point. +- **The `_showPopup` delegate itself** (declared `…Host.cs:30`, assigned `…Host.cs:162`, defaulted to + `BreadcrumbPopupUiOperations.ShowOwnedPopup` at `…Host.cs:74`, whose definition is + `QuickFiler/Viewers/BreadcrumbPopupUiOperations.cs:101`). Repository-wide, `_showPopup` is invoked + exactly once, at `…Host.Open.cs:101`. +- **Native `ToolStripDropDown` events.** The host subscribes exactly one drop-down event, + `DropDown.Closed += OnDropDownClosed` (`…Host.cs:171`). There is no `Opened`, `VisibleChanged`, or + equivalent handler that could observe or cause a native reopen. + +### 2.2 Every write to `OpenState` (production) + +| # | Site | Value written | Bearing on reopen | +| --- | --- | --- | --- | +| 1 | `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:268` | `true` | **The only open transition in the repository.** | +| 2 | `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:334` (`DisposeCoreAsync`) | `false` | Close only | +| 3 | `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:402` (`CompleteClose`) | `false` | Close only | +| 4 | `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:434` (`OnDropDownClosed`) | `false` | Close only | +| 5 | `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:460` (`RestoreAfterOpenFailure`) | `false` | Close only | + +`BreadcrumbDropDownOpenLifetime.Focus.cs:37` and `:41`, and `BreadcrumbDropDownOpenLifetime.cs:277`, +`BreadcrumbDropDownHost.cs:251`, `:256`, `:310`, `:332`, `:399`, `:401`, `:428`, `:432`, `:459`, and +`BreadcrumbDropDownHost.Open.cs:61` are **reads** of `OpenState`, not writes. + +### 2.3 The enumeration table + +Column meanings: a call site "reaches `RequestOpen`" if control passes through +`BreadcrumbDropDownOpenCoordinator.RequestOpen` (`:104`) before or as part of the reopen; "reaches +`Invalidate`" likewise for `BreadcrumbDropDownOpenCoordinator.Invalidate` (`:344`). + +| # | Call site (file : line) | Reaches `RequestOpen`? | Reaches `Invalidate`? | Bypasses both? | Evidence | +| --- | --- | --- | --- | --- | --- | +| 1 | `BreadcrumbDropDownOpenLifetime.cs:268` — `_host.OpenState = true` | Yes (transitively) | n/a | **No** | Inside `ShowCurrentSurface` (`:258-279`), invoked at `:243` from `OpenCoreAsync`, whose only caller is the kickoff lambda at `:67-69` inside `BreadcrumbDropDownOpenLifetime.OpenAsync` (`:44-72`) | +| 2 | `BreadcrumbDropDownOpenLifetime.OpenAsync` (`:44`) | Yes (transitively) | n/a | **No** | Single invocation repository-wide: `BreadcrumbDropDownHost.Open.cs:88` | +| 3 | `BreadcrumbDropDownHost.OpenWithFocusIntentAsync` (`Open.cs:53-89`) | Yes (transitively) | n/a | **No** | Two invocations, both in the same file: `:22` and `:37` | +| 4 | `BreadcrumbDropDownHost.OpenAsync` 3-param, public (`Open.cs:18-22`) | Yes | n/a | **No** | Production invocation is `BreadcrumbDropDownOpenCoordinator.cs:258` | +| 5 | `IBreadcrumbDropDownHost.OpenAsync` 4-param, explicit impl (`Open.cs:32-37`) | Yes | n/a | **No** | Production invocation is `BreadcrumbDropDownOpenCoordinator.cs:259` | +| 6 | `BreadcrumbDropDownOpenCoordinator.BeginOpenCore` (`:232-264`), calls `_host.OpenAsync` at `:258` / `:259` | Yes | n/a | **No** | Single invocation: `:218`, inside `OpenCoreAsync` | +| 7 | `BreadcrumbDropDownOpenCoordinator.OpenCoreAsync` (`:213-230`) | Yes | n/a | **No** | Single invocation: `:115`, the statement after `_closeCompleted = false;` at `:114` | +| 8 | `BreadcrumbDropDownOpenCoordinator.RequestOpen` (`:104-118`) | **Is** `RequestOpen` | No | **No** | Clears the flag itself at `:114` | +| 9 | `BreadcrumbDropDownOpenCoordinator.SetDroppedDown(true)` (`:152-169`) | Yes | No | **No** | Posted body `:160-165`: `_openSelector()` at `:162`; if it reports no change and the selector is open, `RequestOpen()` at `:164`; if it reports a change, the selector raises `SelectorOpenStateChanged`, routed at `BreadcrumbItemViewerLifecycleCoordinator.cs:237-238` into `HandleSelectorOpenStateChanged` → `RequestOpen()` at `:180` | +| 10 | `BreadcrumbDropDownOpenCoordinator.HandleSelectorOpenStateChanged` (`:171-184`) | Yes | No | **No** | `_ = RequestOpen();` at `:180` | +| 11 | `BreadcrumbDropDownOpenCoordinator.LatchNextOpenTakesNoFocus` (`:132-140`) | No — but performs no open | No | **No** | Sets `_nextOpenTakesNoFocus` only (`:138`); the open still arrives via `SelectorOpenStateChanged` → `RequestOpen`, as its own remarks at `:123-131` document | +| 12 | `BreadcrumbItemViewerLifecycleCoordinator.PresentSearchResults` (`…Search.cs:34-43`) | Yes (transitively) | No | **No** | `_openCoordinator?.LatchNextOpenTakesNoFocus();` at `:40` then `_bridgeCoordinator?.PresentSearchResults(items)` at `:42`; the open reaches the coordinator through `SelectorOpenStateChanged` | +| 13 | `BreadcrumbItemViewerLifecycleCoordinator.SetDroppedDown` (`:192-205`) | Yes (transitively) | No | **No** | `_openCoordinator.SetDroppedDown(droppedDown);` at `:204`; the `_openCoordinator == null` branch (`:195-202`) only calls `Focus(focus)` and opens nothing | +| 14 | `BreadcrumbItemViewerLifecycleCoordinator.ConfigureHost` (`:112-168`) — **different host** branch | n/a — no open occurs | Yes | **No** | `ReleaseHostCore()` at `:133` → `coordinator.Release()` at `:318` → `Invalidate(release: true)`; a **new** coordinator is then constructed at `:134` with `_closeCompleted` at its `false` default | +| 15 | `BreadcrumbItemViewerLifecycleCoordinator.ConfigureHost` — **same host** branch | n/a — no open occurs | No | **No** | `_openCoordinator.UpdateRequestProviders(anchorBounds, workingArea);` at `:160`. `UpdateRequestProviders` (`…OpenCoordinator.cs:89-102`) only reassigns `_anchorBounds` and `_workingArea`; it issues no open, so a stale flag here cannot suppress a close of an open that never happened | +| 16 | `BreadcrumbItemViewerLifecycleCoordinator.Reset` (`:207-215`) | No | Yes | **No** | `_openCoordinator?.Reset();` at `:212` → `Invalidate(release: false)` at `…OpenCoordinator.cs:188` → clear at `:352` | +| 17 | `BreadcrumbItemViewerLifecycleCoordinator.Dispose` (`:217-235`) | No | Yes | **No** | `ReleaseHostCore()` at `:227` → `coordinator.Release()` at `:318` | +| 18 | `ItemViewer.SetBreadcrumbDropDownState` (`ItemViewer.Breadcrumb.cs:288-300`) | Yes (transitively) | No | **No** | `_breadcrumbLifecycleCoordinator.SetDroppedDown(droppedDown, FocusBreadcrumbCore);` at `:299`; the null branch (`:290-297`) only focuses | +| 19 | `ItemViewer.PresentBreadcrumbSearchResults` (`ItemViewer.Breadcrumb.cs:313-321`) | Yes (transitively) | No | **No** | `_breadcrumbLifecycleCoordinator.PresentSearchResults(items);` at `:320` | +| 20 | `ItemViewer.ResetBreadcrumb` (`ItemViewer.Breadcrumb.cs:323`) | No | Yes | **No** | `_breadcrumbLifecycleCoordinator?.Reset()` | +| 21 | `ItemViewer.ConfigureBreadcrumbDropDown(CoreWebView2Environment, IWebViewCoreInitializer)` (`ItemViewer.Breadcrumb.cs:167-221`) | n/a — no open occurs | Yes, on the replace path | **No** | Constructs the host at `:198-207`, then delegates to the 3-arg overload at `:213`; the outgoing concrete host is disposed at `:191` | +| 22 | `ItemViewer.ConfigureBreadcrumbDropDown(IBreadcrumbDropDownHost, Func, Func)` (`ItemViewer.Breadcrumb.cs:223-241`) | n/a — no open occurs | Depends on branch 14/15 | **No** | `lifecycle.ConfigureHost(host, anchorBounds, workingArea);` at `:240` | +| 23 | `ItemViewer.FolderSearch.cs:32` (`SetBreadcrumbDropDownState(droppedDown)`) and `:39` (`PresentBreadcrumbSearchResults(items)`) | Yes (transitively) | No | **No** | Rows 18 and 19 | +| 24 | `QfcItemController.ViewerSetup.cs:171` / `:184` (`viewer.ConfigureBreadcrumbDropDown(...)`) and `:451` (`ResetBreadcrumb()`) | n/a — no open occurs | Rows 21/22, row 20 | **No** | The controller never holds or drives an `IBreadcrumbDropDownHost` directly; a repository-wide `*.cs` search for `IBreadcrumbDropDownHost` returns six production files, all under `QuickFiler/Viewers/` plus `ItemViewer.Breadcrumb.cs` | + +### 2.4 Sub-cases inside `RequestOpen` that do **not** clear the flag — checked and excluded + +`RequestOpen` has two early returns ahead of the clear at `:114`: + +- `:110-111` — `if (_currentOpenTask != null && !_currentOpenTask.IsCompleted) return _currentOpenTask;` +- `:112-113` — `if (_closeInFlight && _host.IsOpen) return ClosedTask;` + +Neither clears `_closeCompleted`, but neither starts an open either. The `:110` branch returns the +already-running task, whose generation was invalidated by the successful close at `:334`, so +`BeginOpenCore`'s currency check at `:239-240` returns `ClosedTask` and no `_host.OpenAsync` call is +made. These are not bypassing reopen paths. + +### 2.5 Plain answer + +**A bypassing reopen path — one that makes the drop-down host open while reaching neither +`BreadcrumbDropDownOpenCoordinator.RequestOpen` (`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:104`) +nor `BreadcrumbDropDownOpenCoordinator.Invalidate` (`…:344`) — does not exist in the shipped production +code, because the repository's only open transition, +`QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:268`, is reachable only through the closed chain +`RequestOpen (…OpenCoordinator.cs:115) → OpenCoreAsync (:218) → BeginOpenCore (:258/:259) → +BreadcrumbDropDownHost.Open.cs:22/:37 → :88 → BreadcrumbDropDownOpenLifetime.cs:67-69 → :243 → :268`, +whose first link clears `_closeCompleted` at `…OpenCoordinator.cs:114`.** + +--- + +## 3. Numeric Derivation Evidence + +Two numeric claims in this document are load-bearing for the recommendation and are derived below. + +### Claim N1 — `_closeCompleted` has exactly **three** assignment sites: one set and two clears + +- **Complete family:** every assignment expression whose target is the instance field + `BreadcrumbDropDownOpenCoordinator._closeCompleted`, including reflective writes. +- **Exhaustive search scope:** all `*.cs` files in the repository (production and test), plus the + reflective-write surface (`GetField(`) in `QuickFiler.Test`. The field is `private` on a `sealed` + `internal` class, so the only non-source-visible write channel is reflection, which the scope covers. +- **Inclusion rules:** direct assignments (`= true`, `= false`), compound assignments, `ref`/`out` + usage, and `FieldInfo.SetValue` calls naming the field. +- **Exclusion rules:** reads (`if (_closeCompleted)`), and XML documentation cross-references + (``). +- **Primary search strategy / query:** identifier search `_closeCompleted` across `*.cs`, then manual + classification of each of the six hits. +- **Primary member set:** `{ …OpenCoordinator.cs:114 (= false), …OpenCoordinator.cs:335 (= true), + …OpenCoordinator.cs:352 (= false) }`. Excluded as non-assignments: `:41` (doc cref), `:46` + (declaration, no initializer), `:316` (read). +- **Primary count:** 3 assignments (1 set, 2 clears). +- **Cross-check search strategy / query:** a different, structural route — full sequential read of + `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` (all 378 lines) enumerating every mutation + of the type's state, combined with a `GetField\(` search across `QuickFiler.Test` to enumerate every + reflective field write in the test assembly and check whether any names this field. +- **Cross-check member set:** the sequential read yields the same three mutation statements — inside + `RequestOpen` (`:114`), inside the `if (closed)` block of `CloseCore` (`:335`), and inside + `Invalidate` (`:352`). The `GetField(` search returns hits in `BreadcrumbBridgeCoordinatorSupersessionTests.cs`, + `BreadcrumbCoordinatorLifecycleTests.cs`, `BreadcrumbCollapsedSurfaceReadinessTests.cs`, + `BreadcrumbDropDownHostTests.cs`, `BreadcrumbCoordinatorUpgradeLifetimeTests.cs` + (`_sync`, `_current`, `_generation`), `BreadcrumbDropDownLifecycleCoverageTests.cs` (`_openLifetime`), + and several non-breadcrumb test files; **none** names `_closeCompleted`, confirmed by the fact that + the repository-wide `_closeCompleted` identifier search returns zero hits outside + `BreadcrumbDropDownOpenCoordinator.cs`. +- **Cross-check count:** 3. +- **Member-set comparison:** normalized primary set `{114, 335, 352}` equals normalized cross-check set + `{114, 335, 352}`. Reflective-write set is empty in both records. **Agreement.** + +### Claim N2 — exactly **one** production statement makes the drop-down host open + +- **Complete family:** every production statement that can cause `IBreadcrumbDropDownHost.IsOpen` to + transition from `false` to `true` for the concrete `BreadcrumbDropDownHost`. Because + `IsOpen => OpenState` (`BreadcrumbDropDownHost.cs:191`) is get-only over + `internal bool OpenState { get; set; }` (`:244`), the family is exactly the set of writes of `true` to + `OpenState`, plus any alternative mechanism that could show the popup without that write. +- **Exhaustive search scope:** all `*.cs` in the repository, covering both the property-write channel + and the native-show channel (`ShowPopup`, the `_showPopup` delegate, `ShowOwnedPopup`, + `DropDown.Show`, `DropDown.Visible`, and drop-down event subscriptions). Every member of the + `OpenAsync` overload pair (3-param and 4-param, including the explicit interface implementation) is + in scope; the search is not restricted to a single named method. +- **Inclusion rules:** assignments to `OpenState`; native show invocations that would make the popup + visible. +- **Exclusion rules:** reads of `OpenState`/`IsOpen`; assignments of `false`; the unrelated + `BreadcrumbSelectionEffects.OpenStateChanged` / `SelectorOpenStateChanged` identifier family in + `UtilitiesCS` and `BreadcrumbBridgeCoordinator`, which concerns the selector session model and never + touches the host property; test-assembly writes. +- **Primary search strategy / query:** identifier search `OpenState` across `*.cs`, then classify each + hit as write-true / write-false / read / unrelated-identifier. +- **Primary member set (writes of `true`, production):** `{ BreadcrumbDropDownOpenLifetime.cs:268 }`. + Production writes of `false`: `{ BreadcrumbDropDownHost.cs:334, :402, :434, :460 }`. Test writes: + `{ BreadcrumbSelectorToggleUiBoundaryTests.cs:225 (compound &= on a test host), + BreadcrumbPopupBoundaryCoverageTests.Part2.cs:343 (= false) }` — neither writes `true`. +- **Primary count:** 1. +- **Cross-check search strategy / query:** a structurally different route — regex + `ShowOwnedPopup|_showPopup|DropDown\.(Show|Visible)` across `QuickFiler/`, plus a full read of + `BreadcrumbDropDownHost.cs` (498 lines), `BreadcrumbDropDownHost.Open.cs` (107 lines) and + `BreadcrumbDropDownOpenLifetime.cs` (460 lines) to enumerate every native-show and every drop-down + event subscription. +- **Cross-check member set:** the native-show family is + `{ BreadcrumbDropDownHost.Open.cs:101 (_showPopup invocation, inside ShowPopup at :98-102) }`, whose + only caller is `BreadcrumbDropDownOpenLifetime.cs:276` inside `ShowCurrentSurface`, which is preceded + in the same expression chain by `_host.OpenState = true` at `:268`. The delegate-assignment sites are + `…Host.cs:74` and `:162`; the only definition is `BreadcrumbPopupUiOperations.cs:101`; and + `DropDown.Visible` appears once, as a read at `…Host.cs:459`. The only drop-down event subscription is + `DropDown.Closed += OnDropDownClosed` (`…Host.cs:171`). The cross-check therefore also yields exactly + one open transition, located at `BreadcrumbDropDownOpenLifetime.cs:268`, with no independent + native-show entry point. +- **Cross-check count:** 1. +- **Member-set comparison:** normalized primary set `{ BreadcrumbDropDownOpenLifetime.cs:268 }` equals + the normalized cross-check set. **Agreement.** + +--- + +## 4. Existing Test Contract + +All three must-pass tests live in the `QuickFiler.Test.Viewers.BreadcrumbDropDownOpenCoordinatorTests` +partial class and share the private nested `CoordinatorHarness` / `ControlledHost` fixtures declared at +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:323-372` and `:374` onward. + +### 4.1 `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` + +- **Location:** `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:262-280` + (attribute `:262`, method `:263`). +- **Shape:** an open is started against a host whose open task is a pending + `TaskCompletionSource` (`:265-269`); `SetDroppedDown(false)` is then driven **twice** (`:271`, + `:272`) before the queue is drained. +- **Assertion encoding the contract:** + `harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.Uncommitted);` at `:278` — + a single-element sequence equality, so a second `_host.Close` call is a failure. + The companion assertion at `:279`, + `harness.CancelCount.Should().Be(0, "the accepting host owns pending rollback");`, would also fail: + a second `CloseCore` reaching a host whose `CloseResult` is `true` + (`ControlledHost.Close`, `…Tests.cs:431-439`, sets `IsOpen = false` and returns `true` on the first + call) would take the `if (closed)` branch again rather than the `_closeCompleted` early return. +- **Why a naive remedy breaks it:** under "clear `_closeCompleted` in `CloseCore` on success" the + second `SetDroppedDown(false)` at `:272` finds `_closeInFlight == false` (cleared in the `finally` at + `…OpenCoordinator.cs:325-329`) and `_closeCompleted == false`, so it passes the guard block at + `:310-319` and calls `_host.Close` a second time at `:323`. `CloseReasons` becomes + `{ Uncommitted, Uncommitted }` and the `:278` sequence equality fails. + +### 4.2 `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` + +- **Location:** `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:120-140` + (attribute `:120`, method `:121`). +- **Shape:** one successful open (`:124-130`), then `SelectorOpen = false` (`:132`) followed by **two** + `HandleSelectorOpenStateChanged()` drives (`:133`, `:135`), each fully drained. +- **Assertions encoding the contract:** `harness.Host.Requests.Should().ContainSingle();` at `:138` + and `harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.ExplicitCommit);` + at `:139`. The second is the repeated-close guard: exactly one `ExplicitCommit` close. +- **Why a naive remedy breaks it:** the first drive at `:133` reaches + `CloseCore(BreadcrumbDropDownCloseReason.ExplicitCommit)` (`…OpenCoordinator.cs:182`), the host + accepts, and `_closeCompleted` would be cleared on success. The second drive at `:135` then passes the + guard block and calls `_host.Close` again, producing + `{ ExplicitCommit, ExplicitCommit }` and failing the `:139` sequence equality. + +### 4.3 `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` + +- **Location:** `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:325-364` + (doc comment `:325-331`, attribute `:332`, method `:333`). +- **Shape:** open (`:336-341`), successful close via `SetDroppedDown(false)` (`:343-346`), then the + host is made open again through the test-only seam `harness.Host.SetOpen(true);` at `:349` — the + comment at `:348` states the intent verbatim: "The host becomes open again by a path that bypasses + CloseCore and RequestOpen." A second `RequestOpen()` follows at `:354`. +- **Assertions:** `harness.Host.Requests.Should().HaveCount(2, ...)` at `:358-360` and + `reopen.Result.Should().BeTrue(...)` at `:361-363`. +- **Why a naive remedy does not break it, and why it still matters:** this test exercises the + `RequestOpen` side, which the naive remedy leaves intact, so it would still pass. It is listed as + must-pass because any remedy that touches the guard ordering in `RequestOpen` (`:108-114`) or the + generation bookkeeping at `:334` risks regressing it. It is also the single existing precedent for + driving a synthetic host reopen through `ControlledHost.SetOpen(true)` — the seam a #656 regression + test would reuse (see §7). + +### 4.4 One further standing guard, not in the delegation list but load-bearing + +`CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` +(`…Part2.cs:374-397`; doc comment `:366-373`) is explicitly documented at `:369-371` as "the standing +guard that rules out research section 6.1 option A (clearing the flag on the successful-close path)". +Its assertion at `:391-396` is a single-element `CloseReasons` sequence equality with the reason string +"the repeated close must be suppressed, so _host.Close is reached exactly once". Any remedy must keep +this green as well; it fails under the naive remedy for the same mechanism as §4.1. + +--- + +## 5. SR-4 Rationale and the Rejected Refinement + +### 5.1 The ratified decision, quoted verbatim + +From `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md:426-437`: + +> - **SR-4 — DECIDED: minimal two-flag form (research §6.1 option D), without the `&& !_host.IsOpen` +> refinement.** +> *Rationale:* the refinement `if (_closeCompleted && !_host.IsOpen) return true;` would read +> `_host.IsOpen` under `_sync` — the very lock-ordering hazard that #462's potential document flags +> and that #500 exists to remove. Adding it here would create a new instance of the class of defect +> this feature is closing. +> **KNOWN LIMITATION (accepted, recorded, not fixed here):** if the host is reopened by a path that +> reaches neither `RequestOpen` nor `Invalidate`, `_closeCompleted` stays `true` and a subsequent +> close request returns `true` without closing. This residual is **strictly narrower** than HEAD's +> behaviour, in which the single `_closePending` flag latches after *every* successful close and +> suppresses reopen unconditionally. Closing the residual at source belongs to the host paths owned by +> sibling feature 488 (see Cross-feature note 4). + +And from the same spec's `## Implementation Notes`, `:1062-1068`: + +> ### SR-4 known limitation, shipped as designed +> +> `_closeCompleted` stays `true` when the host is reopened by a path that reaches neither `RequestOpen` +> nor `Invalidate`. The two-flag form was chosen because the naive alternative (clearing the close flag on +> the successful-close path) makes `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` +> and `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` fail. The residual is filed as a +> follow-up against feature 488's host paths rather than worked around here. + +The research that SR-4 adopted, `docs/features/active/breadcrumb-coordinator-hub-defects-501/research/2026-08-24T09-12-breadcrumb-ordering-invariants-research.md:733-737`: + +> *Residual, worth recording in `spec.md` but not fixing here:* if the host is reopened by a path that +> never reaches `RequestOpen`, `_closeCompleted` stays `true` and a subsequent close request would +> return `true` without closing. A refinement `if (_closeCompleted && !_host.IsOpen) return true;` also +> passes every existing test and removes the residual, at the cost of reading `_host.IsOpen` under +> `_sync`. The minimal form is recommended; the refinement is the fallback if review prefers it. + +### 5.2 What `_sync` guards + +`private readonly object _sync = new object();` — `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:16`. +It guards the coordinator's mutable state only: `_anchorBounds` (`:24`), `_workingArea` (`:25`), +`_currentOpenTask` (`:26`), `_generation` (`:27`), `_closeInFlight` (`:36`), `_closeCompleted` (`:46`), +`_released` (`:48`), `_nextOpenTakesNoFocus` (`:49`). Every acquisition is a short, allocation-free +critical section: `:84-85`, `:96-101`, `:106-117`, `:134-139`, `:147-148`, `:237-247`, `:310-319`, +`:327-328`, `:332-336`, `:346-355`, `:360-361`, `:368-369`. + +The file's structural discipline is that **host calls happen outside the lock**. `CloseCore` is the +clearest instance: the guard block ends at `:319`, the lock is released, and only then is +`_host.Close(reason)` invoked at `:323`; the flag is restored in a `finally` at `:325-329`. The same +discipline holds in `BeginOpenCore`, which reads the providers under the lock at `:237-247` and then +calls `anchorBounds()`, `_rowCount()`, and `_host.OpenAsync` at `:249-259` with the lock released. + +### 5.3 The concrete lock and reentrancy hazard + +Reading `_host.IsOpen` under `_sync` would call into the host while the coordinator lock is held. What +that means concretely: + +1. **The interface permits arbitrary work.** `IBreadcrumbDropDownHost.IsOpen` + (`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs:22`) is a plain `bool` getter with no documented + purity or non-blocking contract. The coordinator is written host-neutrally against the interface — + it is constructed with an injected `IBreadcrumbDropDownHost` (`…OpenCoordinator.cs:53`, `:64`) — so + the analysis cannot be confined to the concrete type. +2. **The concrete host's getter is, today, safe.** `BreadcrumbDropDownHost.IsOpen => OpenState` + (`…Host.cs:191`) over the auto-property at `:244`. It takes no lock, allocates nothing, raises no + event, and cannot reenter the coordinator. There is no deadlock against this specific + implementation. +3. **A second lock exists on the path that would be entered.** + `BreadcrumbDropDownOpenLifetime` has its own `private readonly object _sync` + (`QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:25`), acquired in `OpenAsync` (`:54`), + `TryCancelPendingOpen` (`:79`), `Schedule` (`:117`), `Dispose` (`:144`), the two completion + `finally` blocks (`:173`, `:201`), `IsLifecycleCurrent` (`:390`), and `ScheduleInvalidating` + (`:403`). `BreadcrumbDropDownHost.Close` enters that lock via `InvalidateAndSchedule` (`…Host.cs:253` + → `…OpenLifetime.cs:135-136` → `ScheduleInvalidating` at `:399-420`). Today the coordinator holds + `_sync` and the lifetime lock **disjointly**, never nested, because `_host.Close` is called at + `…OpenCoordinator.cs:323` outside the lock. Adding a host read inside the lock establishes the + ordering `coordinator._sync → host code`, which is the first half of a nesting that the current + design categorically avoids. +4. **Reentrancy into the coordinator is a live shape elsewhere in the pipeline.** The host raises + `PopupMessengerReady` (`…Host.cs:219`, published at `…Host.Open.cs:104-105`), and the lifecycle + coordinator subscribes to it at `BreadcrumbItemViewerLifecycleCoordinator.cs:145`, handling it at + `:240-256`, which reads `DropDownHost` — i.e. `_openCoordinator?.Host` (`:57`). Host events already + re-enter the coordinator graph. A host member invoked under `_sync` is therefore not obviously + isolated from that graph in general, even though `IsOpen` specifically is. + +### 5.4 An honest qualification of the SR-4 rationale + +`RequestOpen` **already reads `_host.IsOpen` under `_sync`**, at +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:112`: + +```csharp +if (_closeInFlight && _host.IsOpen) + return ClosedTask; +``` + +inside the `lock (_sync)` opened at `:106`. So "never read `_host.IsOpen` under `_sync`" is not an +invariant the shipped file holds; SR-4's objection is that the refinement would add a **second** +instance of a pattern the sibling feature was closing, not that it would be the first. This is recorded +here so that the recommendation below is not built on an overstated premise. It does not overturn SR-4: +SR-4 is a ratified project decision, and increasing the exposure surface of a pattern under active +remediation is a legitimate reason to decline, independent of whether one instance already exists. + +### 5.5 A second, independent argument against the refinement + +Under production wiring the refinement would be a **no-op**, because `_host.IsOpen` is already `false` +at the moment `_closeCompleted` is consulted after a successful close. The chain: + +- `BreadcrumbUiDispatcher.Dispatch` executes **inline** when already on the captured boundary + (`QuickFiler/Viewers/BreadcrumbUiDispatcher.cs:78-95`), and `DispatchValue` likewise when + `_executingDispatcher` is this dispatcher (`:166-178`). +- `BreadcrumbPopupUiOperations.PostAsync` is `_dispatcher.Dispatch` (`…PopupUiOperations.cs:123`) and + `RunAsync` is `_dispatcher.DispatchValue` (`:120-121`). +- `CloseCore` is always reached from inside a `Dispatch`/`DispatchValue` callback — `SetDroppedDown`'s + posted body (`…OpenCoordinator.cs:156-168`, close at `:167`), `HandleSelectorOpenStateChanged`'s + posted body (`:175-183`, close at `:182`), or `FinishOpenCore` run under `RunAsync` (`:223`, close at + `:277`). So `_executingDispatcher` is set. +- Therefore `_host.Close` → `InvalidateAndSchedule` → `ScheduleInvalidating` → `ScheduleObserved` → + `RunOnOwnerAsync` (`…OpenLifetime.cs:425-434`) runs its `PostAsync` and `RunAsync` inline, so + `CompleteClose` (`…Host.cs:397-411`) — and its `OpenState = false` at `:402` — executes + **synchronously inside** `_host.Close(reason)` before it returns `true` at `…Host.cs:254`. + +Consequently `!_host.IsOpen` is already true whenever the suppression is evaluated in production, and +the refinement changes no observable behavior on any path that exists today. Adding a lock-held host +call for zero behavioral delta is not a favorable trade. + +--- + +## 6. Option Space and Recommendation + +### (a) Route the bypassing reopen path(s) through `RequestOpen` or `Invalidate` + +**Disposition: NOT APPLICABLE — vacuous.** §2 establishes there is no such path. There is nothing to +route. Implementing this option would require inventing a path in order to redirect it. + +### (b) Clear `_closeCompleted` explicitly at the bypassing site + +**Disposition: NOT APPLICABLE — vacuous, for the same reason.** There is no bypassing site at which to +place a clear. + +A near-neighbour worth naming and rejecting explicitly: clearing `_closeCompleted` in +`UpdateRequestProviders` (`…OpenCoordinator.cs:89-102`), on the theory that the same-host +`ConfigureHost` branch (`BreadcrumbItemViewerLifecycleCoordinator.cs:160`) is a lifecycle re-adoption. +Rejected: `UpdateRequestProviders` performs no open (row 15 of the table), so the clear would protect +nothing, and it would silently weaken repeated-close suppression across a reconfiguration for no +demonstrated benefit. + +### (c) A defensive guard at a safe point, plus a regression test driving the bypass through an internal seam + +The only safe point that does not reintroduce a lock-held host call is to move the *decision* outside +`_sync` while keeping the *flag reads* inside it: read `_closeCompleted` under the lock, release, then +qualify the suppression with a lock-free `_host.IsOpen` read before re-entering the lock to latch +`_closeInFlight`. Shape: + +```csharp +bool completed; +lock (_sync) +{ + if (_released) return false; + if (_closeInFlight) return true; + completed = _closeCompleted; +} +// Issue #656: the completed-close suppression is qualified by the host's own open state, read +// OUTSIDE _sync so no host member is invoked under the coordinator lock (spec 501 SR-4). +if (completed && !_host.IsOpen) return true; +lock (_sync) +{ + if (_released) return false; + if (_closeInFlight) return true; + _closeInFlight = true; +} +``` + +- Satisfies SR-4's stated objection literally: no host member is called under `_sync`. +- Keeps all four tests in §4 green. §4.1, §4.2 and §4.4 all evaluate the suppression while + `ControlledHost.IsOpen` is `false` (`ControlledHost.Close` sets `IsOpen = false` when `CloseResult` is + `true`, `…Tests.cs:436-437`; §4.1's host is never opened at all), so `completed && !IsOpen` still + suppresses. §4.3 does not exercise `CloseCore`'s guard. +- Testable red-to-green through the existing `ControlledHost.SetOpen(true)` seam (§7). +- **Costs:** double-checked locking with a duplicated guard prologue in the file's most contested + method; a genuine TOCTOU window between the lock release and the `_host.IsOpen` read; and roughly + ten added lines in a method whose current shape was adversarially reviewed and ratified. + +**Disposition: VIABLE, and the only option that actually removes the residual without violating SR-4.** + +### (d) The rejected `&& !_host.IsOpen` refinement (read under `_sync`) + +**Disposition: STAYS REJECTED.** Three independent reasons, in descending strength: + +1. It is a no-op on every production path that exists today (§5.5), so it buys no behavior change. +2. It contradicts a ratified project decision (SR-4) whose stated rationale — not adding a second + instance of a lock-held host call while a sibling feature was removing that pattern — still holds. +3. §2 shows the residual it targets is unreachable, so even the hypothetical benefit is unrealized. + +What would have to change for it to be reconsidered: a bypassing reopen path would have to be +introduced (making the residual reachable), **and** the maintainer would have to re-open SR-4. Absent +both, reintroducing it is a regression against a reviewed decision. + +### (e) No production change; pin the enumeration invariant with a test + +Land no production edit; add one deterministic regression test that drives a full +open → successful close → synthetic host reopen → close cycle through the coordinator and asserts that +the close reaches `_host.Close`. This is red on HEAD (the close is suppressed) and would stay red +without a production change, so as a *standalone* option it is not shippable — it can only be paired +with (c). Its value is that it is the acceptance test for (c). + +**Disposition: NOT SHIPPABLE ALONE; adopted as the test half of (c).** + +### RECOMMENDATION + +**Adopt option (c): the lock-free-qualified suppression in `CloseCore`, paired with option (e)'s +red-to-green regression test.** + +Rationale, in order: + +1. It is the only option that closes the residual the issue names. +2. It honors SR-4's stated objection exactly, so it does not relitigate a ratified decision — it + satisfies the constraint SR-4 imposed rather than overriding it. +3. It leaves all four tests in §4 unedited, so no regression is traded for the fix. +4. Its footprint is minimal, which matters for the concurrent parallel run. + +**Minimum production-file footprint: exactly ONE file — +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`.** No other production file needs to change. +That file is 378 lines, giving 122 lines of headroom under the 500-line ceiling; the change adds +roughly ten. + +This footprint is not merely convenient, it is close to forced. The two files the issue's "Suspected +Cause" section nominates as owners of the fix are at the ceiling: +`QuickFiler/Viewers/BreadcrumbDropDownHost.cs` is **498** lines and +`QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs` is **497** lines. Any non-trivial edit +to either would force a partial-class split first, multiplying the change footprint and the merge +surface for no benefit — and §2 shows neither file contains a defect to fix. + +**Test footprint: exactly ONE existing file — +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`** (173 lines, 327 lines of +headroom). `…Part2.cs` is 455 lines and `…Tests.cs` is 463 lines, so neither has room for a new test +under the 500-line cap; Part3 is a partial of the same test class and has direct access to the shared +`CoordinatorHarness` and `ControlledHost` fixtures. + +**Explicitly out of scope for this remedy:** any edit to `BreadcrumbDropDownHost.cs`, +`BreadcrumbDropDownHost.Open.cs`, `BreadcrumbDropDownOpenLifetime.cs`, +`BreadcrumbItemViewerLifecycleCoordinator.cs`, or `ItemViewer.Breadcrumb.cs`. + +--- + +## 7. Test Seam Analysis + +### 7.1 The seam exists today; nothing new is required + +- **`[assembly: InternalsVisibleTo("QuickFiler.Test")]`** — `QuickFiler/Properties/AssemblyInfo.cs:5`. + `BreadcrumbDropDownOpenCoordinator` is `internal sealed` (`…OpenCoordinator.cs:12`) and every member + the test needs is `internal`, so no reflection is needed to construct or drive it. +- **Injectable host interface:** `IBreadcrumbDropDownHost` + (`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs:19`), supplied as the second constructor parameter + `IBreadcrumbDropDownHost host` (`…OpenCoordinator.cs:53`, assigned `:64`). +- **The concrete test double:** `ControlledHost`, a private nested class implementing + `IBreadcrumbDropDownHost` at `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:374`. + Its relevant members: + - `internal void SetOpen(bool value)` — `…Tests.cs:407`. **This is the bypass seam.** It writes + `IsOpen` directly, reaching neither `RequestOpen` nor `Invalidate`, which is precisely the + hypothetical the issue describes. It is already used for exactly this purpose at + `…Part2.cs:349`, under the comment at `:348`. + - `internal void Enqueue(Task result)` — `…Tests.cs:402`. + - `internal List CloseReasons { get; }` — `…Tests.cs:395-396`. + - `internal bool CloseResult { get; set; }` — `…Tests.cs:397`. + - `public bool Close(BreadcrumbDropDownCloseReason reason)` — `…Tests.cs:431-439`. +- **The deterministic pump:** `CoordinatorHarness` (`…Tests.cs:323-372`) wires the coordinator to a + `BreadcrumbPopupUiOperations` over a `BreadcrumbUiDispatcher` bound to + `BreadcrumbSelectorToggleUiBoundaryTests.CapturingSynchronizationContext` (aliased at + `…Tests.cs:12`), drained explicitly by `Context.DrainOne()`, `Context.DrainAll()` and + `Context.DrainUntil(task)`. One thread, no timers, no sleeps, no temporary files. + +**No new seam has to be added.** No `[InternalsVisibleTo]` entry, no new interface, no new injection +point, no reflection. The reflective route is unnecessary and should not be used: a repository-wide +`*.cs` search confirms no test currently reads or writes `_closeCompleted` reflectively (§3, Claim N1). + +### 7.2 Proposed regression test shape (design only; no test code authored here) + +Placed in `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, MSTest +`[TestMethod]`, FluentAssertions, Arrange–Act–Assert, no Moq needed because `ControlledHost` is the +established hand-written double for this coordinator: + +1. **Arrange.** `new CoordinatorHarness()`; `Host.Enqueue(Task.FromResult(true))`; + `RequestOpen()`; `Context.DrainUntil(opening)`; assert `opening.Result` is `true`. +2. **Arrange.** `SetDroppedDown(false)`; `Context.DrainAll()`; assert `Host.CloseReasons` equals + `{ Uncommitted }` and `Host.IsOpen` is `false` — this latches `_closeCompleted`. +3. **Act.** `harness.Host.SetOpen(true);` — the bypass, reaching neither `RequestOpen` nor + `Invalidate`. Then `harness.SelectorOpen = true;` and a second `SetDroppedDown(false)` with + `Context.DrainAll()`. +4. **Assert.** `Host.CloseReasons` should have two elements — the close of a genuinely open host must + reach `_host.Close`. **Red on HEAD** (the `:316` guard suppresses it, leaving one element), + **green after option (c)**. + +This is a genuine red-to-green regression test, not a test that codifies current behavior. It satisfies +the repository bugfix workflow's "failing regression test first" requirement, the General Unit Test +Policy's determinism and no-temporary-file rules, and the C# Unit Test Policy's MSTest + FluentAssertions +requirements (Moq is available but not needed for this shape). + +### 7.3 Ancillary check for the executor + +`QuickFiler.Test/Viewers/BreadcrumbDropDownCoverageThresholdTests.cs` exists and pins coverage-driven +guard behavior for the popup lifecycle. Option (c) adds a branch to `CloseCore`; the executor must +confirm the new branch is exercised (the test in §7.2 covers the `completed && IsOpen` arm; §4.1/§4.2/ +§4.4 cover the `completed && !IsOpen` arm) so no changed line lands uncovered. + +--- + +## 8. Severity Framing + +The issue records **Medium** and **latent**, and that framing is correct and should not be escalated. + +Stated plainly: **because §2 establishes that no bypassing reopen path exists in the shipped code, this +is latent-correctness hardening, not an observed user-facing failure.** No user can currently reach the +suppressed-close state. There is no reproduction on a running Outlook host, and the issue's own +"Logs / Screenshots" section says so: "no runtime log; the residual is established by source inspection +of the flag-clearing paths" +(`docs/features/potential/promoted/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate.md:44`). + +The value of fixing it is that the coordinator's suppression currently depends on an *external* +invariant — "nothing opens the host except `RequestOpen`" — that is held by the shape of the call graph +in three other files rather than by anything the coordinator enforces or asserts. Option (c) makes the +suppression self-sufficient, so a future change to the host or lifetime that adds an open path cannot +silently reintroduce a suppressed close. That is a real but modest benefit, consistent with Medium. + +The issue's own "Suspected Cause / Notes" claim that "the reopen paths that bypass `RequestOpen` and +`Invalidate` live in the ItemViewer breadcrumb lifecycle host surface" +(`…promoted/2026-08-27-…md:68-70`) is **not confirmed**: those files contain no such path. The claim +should be corrected in `spec.md` rather than carried forward, and the fix should not be sited in +`BreadcrumbItemViewerLifecycleCoordinator.cs`, `BreadcrumbDropDownHost.cs`, or +`ItemViewer.Breadcrumb.cs` as the issue anticipated. + +--- + +## 9. Toolchain + +The applicable C# toolchain, in this exact order, restarting from step 1 whenever any step fails or +auto-fixes files: + +1. **Format** — `dotnet tool run csharpier format .` + Verify read-only with `dotnet tool run csharpier check .`. Run `dotnet tool restore` once per clone + or worktree first. Always invoke through `dotnet tool run` so the manifest-pinned CSharpier 1.2.6 is + used; never a global install. +2. **Analyze** — + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +3. **Type-check / nullable** — + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +4. **Test** — `vstest.console.exe /EnableCodeCoverage` + +**Explicit warnings, both load-bearing:** + +- **Do NOT add `/p:Nullable=enable`.** No project in this repository carries a `` element and + there is no `Directory.Build.props`, so the property is a solution-wide opt-in that conscripts files + that never adopted the pragma. CI omits it deliberately. Note that + `BreadcrumbDropDownOpenCoordinator.cs` itself carries `#nullable enable` at line 1, so it is already + under nullable analysis on a per-file basis and step 3 does gate it. +- **Use `/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. + +**Test assembly path for `QuickFiler.Test`:** +`QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` (repository-relative). Derived from +`QuickFiler.Test/QuickFiler.Test.csproj:17` (`QuickFiler.Test`) and `:36` +(`bin\Debug\` for the Debug/Any CPU configuration), and corroborated by the +`codeBase` attribute recorded in prior TRX evidence under +`docs/features/active/itemviewer-breadcrumb-lifecycle-defects-488/evidence/regression-testing/p4-t8-d4-full-suite/488-d4-full-suite.trx`. + +Two local-run notes carried from prior sessions in this repository, to be confirmed by the executor +against the current runner configuration rather than assumed: local `vstest.console.exe` invocations +have needed CI's `/InIsolation` flag, and a filter excluding `\.claude\` worktree copies of the same +assembly, to avoid assembly-load failures that present as empty-message, sub-millisecond test failures. + +--- + +## 10. Provenance read for this research + +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` (378 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbDropDownHost.cs` (498 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs` (107 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs` (460 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.cs` (497 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbItemViewerLifecycleCoordinator.Search.cs` (45 lines, read in full) +- `QuickFiler/Viewers/ItemViewer.Breadcrumb.cs` (449 lines, read in full) +- `QuickFiler/Viewers/IBreadcrumbDropDownHost.cs` (68 lines, read in full) +- `QuickFiler/Viewers/BreadcrumbUiDispatcher.cs` (285 lines, read in full) +- `QuickFiler/Properties/AssemblyInfo.cs` +- `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs` (463 lines, read in relevant part + plus both fixture classes) +- `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` (455 lines, read in full) +- `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (173 lines, read in full) +- `QuickFiler.Test/Viewers/BreadcrumbDropDownCoverageThresholdTests.cs` (partial) +- `QuickFiler.Test/QuickFiler.Test.csproj` (output-path and assembly-name properties) +- `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md` (SR-4 at `:426-437`, + cross-feature note 4 at `:183-185`, per-defect design at `:453-472`, risk at `:994-996`, follow-up at + `:1028`, implementation note at `:1062-1068`) +- `docs/features/active/breadcrumb-coordinator-hub-defects-501/research/2026-08-24T09-12-breadcrumb-ordering-invariants-research.md` + (`§6.1` at `:692-740`, `§6.2` at `:742-779`) +- `docs/features/active/breadcrumb-coordinator-hub-defects-501/evidence/issue-updates/followup-sr4-residual.2026-08-27T23-37.md` +- `docs/features/potential/promoted/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate.md` +- `docs/features/active/itemviewer-breadcrumb-lifecycle-defects-488/spec.md` (`### Corrections to the + promoted potentials (binding)` at `:228-238`) +- `docs/features/active/itemviewer-breadcrumb-lifecycle-defects-488/research/2026-08-25T10-00-itemviewer-breadcrumb-lifecycle-defects-research.md` + (`:876-890`, the cession of the four host files by feature 501) +- `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md` diff --git a/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md new file mode 100644 index 000000000..c53878ec7 --- /dev/null +++ b/docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/spec.md @@ -0,0 +1,687 @@ +# 2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate (Spec) + +- **Issue:** #656 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-31T20-10 +- **Status:** Draft +- **Version:** 0.1 + +## Context +`BreadcrumbDropDownOpenCoordinator._closeCompleted` stays `true` when the drop-down host is reopened by +a path that reaches neither `RequestOpen` nor `Invalidate`, so a subsequent close is wrongly suppressed. +This is the known residual of the SR-4 two-flag close fix shipped for #462 under #501, recorded against +the host paths owned by feature #488. + +Environment: +- OS/version: Windows 11, Outlook VSTO add-in host +- Python version: n/a (C#, .NET Framework 4.8) +- Command/flags used: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU"` +- Data source or fixture: `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` harness + +Impact / Severity: +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Medium: it requires a reopen path that bypasses both entry points, which the currently exercised UI +flows do not take. It is a latent correctness gap rather than an observed user-facing failure. + + +## Repro & Evidence +Steps to Reproduce: +1. Open the breadcrumb drop-down host and close it through `CloseCore`, so `_closeCompleted` becomes `true`. +2. Reopen the host through a path that reaches neither `RequestOpen` nor `Invalidate`. +3. Request a close. + +Expected: +The close request reaches `_host.Close`, because the host is genuinely open again. + +Actual: +The coordinator still treats the host as already closed and suppresses the close. `_closeCompleted` was +never cleared, because it is cleared only on the `RequestOpen` and `Invalidate` paths. + +Logs / Screenshots: +- [ ] Attached minimal logs or screenshot +- Snippet: no runtime log; the residual is established by source inspection of the flag-clearing paths. + + +## Scope & Non-Goals + +### Classification: latent-correctness hardening, not an observed failure + +The reopen-path enumeration recorded in +`docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/research/2026-08-31T20-15-closecompleted-residual-reopen-path-enumeration.md` +establishes that **no production reopen path bypassing both `RequestOpen` and `Invalidate` exists in the +tree today**. The trace, re-verified against the working tree for this spec: + +1. `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:191` — `public bool IsOpen => OpenState;`. +2. `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:244` — `internal bool OpenState { get; set; }`. +3. The only production assignment of `OpenState = true` anywhere in `QuickFiler/` is + `QuickFiler/Viewers/BreadcrumbDropDownOpenLifetime.cs:268`, inside `ShowCurrentSurface`. Every other + production assignment sets it to `false` + (`BreadcrumbDropDownHost.cs:334`, `:402`, `:434`, `:460`). +4. `ShowCurrentSurface` is reached only from `BreadcrumbDropDownOpenLifetime.OpenAsync` + (`BreadcrumbDropDownOpenLifetime.cs:44`). +5. `QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs:88` — + `return _openLifetime.OpenAsync(anchorScreenBounds, workingArea, desiredSize, takeFocus);` is the only + production caller of that lifetime entry point. +6. The only production call sites of any `OpenAsync` on the host are + `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:258-259`, inside `OpenCoreAsync`, reached + only from `RequestOpen` at `:115` — and `RequestOpen` clears `_closeCompleted` at `:114` immediately + before. +7. `BreadcrumbPopupUiOperations.ShowOwnedPopup` (`QuickFiler/Viewers/BreadcrumbPopupUiOperations.cs:101`) + is referenced in production only at `QuickFiler/Viewers/BreadcrumbDropDownHost.cs:74`, where it is + supplied as the show delegate to the same open path. It is not an independent reopen entry point. +8. `IBreadcrumbDropDownHost` (`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs:19`) has exactly one + production implementation, `BreadcrumbDropDownHost` + (`QuickFiler/Viewers/BreadcrumbDropDownHost.cs:22`). + +The defect is nonetheless real and reachable: `BreadcrumbDropDownOpenCoordinator` is written against the +`IBreadcrumbDropDownHost` seam (`BreadcrumbDropDownOpenCoordinator.cs:18`, `:53`), and any substituted +implementation may report `IsOpen == true` without the coordinator's `RequestOpen` having run. The +existing suite already drives exactly that state at +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:349` +(`harness.Host.SetOpen(true)`). The issue records severity Medium and latent, and this spec does not +raise that assessment. The work is correctness hardening of a seam contract, not a user-facing fix. + +### In scope +- Narrowing the completed-close suppression in `CloseCore` + (`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:308-342`) so a close is not suppressed while + the host reports open. +- One new deterministic regression test covering the residual scenario. +- Updating the `_closeCompleted` XML documentation + (`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:38-46`) and the `CloseCore` guard-order + documentation (`:302-307`) to describe the new guard. + +### Out of scope / non-goals +- Any change to `BreadcrumbDropDownHost`, `BreadcrumbDropDownHost.Open.cs`, + `BreadcrumbDropDownOpenLifetime`, `BreadcrumbItemViewerLifecycleCoordinator`, or + `ItemViewer.Breadcrumb.cs`. The issue text attributes the residual to feature #488's host paths; the + enumeration above shows the residual is closable inside the coordinator alone, so no host-surface file + is opened. +- Introducing a new production seam. `[assembly: InternalsVisibleTo("QuickFiler.Test")]` already exists + at `QuickFiler/Properties/AssemblyInfo.cs:5`, and the test host already exposes the required bypass + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:407`). +- Editing any of the four standing guards listed under **Test Strategy**. +- Revisiting the #462 two-flag design, the `_closeInFlight` semantics, or the `_generation` counter. +- Reopening SR-4 in `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md`. That record + stays as written; this spec reconciles with it rather than amending it. + +### Explicitly excluded systems, integrations, or datasets +- No Outlook interop, WebView2, WinForms, or native popup code is touched. +- No project, build, or package file: no `.csproj`, `.props`, `.targets`, or `packages.config` edit. +- No solution-wide analyzer, nullable, or coverage configuration change. + +### Hard scope boundary (concurrency) +This item runs concurrently with other items in a parallel run. A wider footprint costs run concurrency. +The authorized footprint is: + +| Kind | File | Note | +|---|---|---| +| Production | `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` | the only production file that may change | +| Test | `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` | new test appended here | + +`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs` is **455 lines**, measured in +this worktree. A regression test of the shape described below is roughly 40 lines including its XML +documentation, which would place Part2 within a few lines of the 500-line file limit in +`.claude/rules/general-code-change.md`. `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` already exists +at **173 lines**, is the same `public sealed partial class BreadcrumbDropDownOpenCoordinatorTests` +(`Part3.cs:21`), and shares the `CoordinatorHarness` and `ControlledHost` fixtures declared in the +primary partial. The new test therefore goes in **Part3**, and no new file is created. + + +## Root Cause Analysis +#462 was fixed by replacing the single `_closePending` flag with two flags, `_closeInFlight` and +`_closeCompleted`. `_closeCompleted` is cleared on `RequestOpen` and `Invalidate` only. + +The two-flag form was chosen deliberately. The naive alternative, clearing the close flag on the +successful-close path, makes two existing must-pass tests fail by letting a second `CloseCore` reach +`_host.Close`: `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` and +`SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired`. Both encode the repeated-close +suppression contract. The two-flag form passes all three must-pass tests with no test edit, so it was +shipped and this residual recorded rather than traded for a regression. + +This belongs to feature #488, not #501: the reopen paths that bypass `RequestOpen` and `Invalidate` +live in the ItemViewer breadcrumb lifecycle host surface. #501 was not permitted to write +`BreadcrumbItemViewerLifecycleCoordinator.cs`, `BreadcrumbDropDownHost.cs` or `ItemViewer.Breadcrumb.cs`. + + +## Proposed Fix + +### Design summary (what changes where): + +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`, method `CloseCore` (declared at `:308`), is +the only production change. Two edits: + +1. Capture the host open state into a local **before** entering the critical section, i.e. before + `lock (_sync)` at `:310`. +2. Narrow the completed-close suppression at `:316` from `if (_closeCompleted) return true;` to a form + that suppresses only when the host is also not open. + +The intended shape: + +```csharp +private bool CloseCore(BreadcrumbDropDownCloseReason reason) +{ + // The host read is hoisted out of the critical section deliberately: see SR-4 reconciliation. + bool hostOpen = _host.IsOpen; + lock (_sync) + { + if (_released) + return false; + if (_closeInFlight) + return true; + if (_closeCompleted && !hostOpen) + return true; + _closeInFlight = true; + } + // ... unchanged from :320 onward +} +``` + +No other statement in the method changes. The guard order (released, in-flight, completed) is preserved, +as is the `finally` that clears `_closeInFlight` (`:325-329`) and the success block that increments +`_generation` and sets `_closeCompleted` (`:330-338`). + +### SR-4 reconciliation (load-bearing) + +`docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md:426-431` records: + +> **SR-4 — DECIDED: minimal two-flag form (research §6.1 option D), without the `&& !_host.IsOpen` +> refinement.** +> *Rationale:* the refinement `if (_closeCompleted && !_host.IsOpen) return true;` would read +> `_host.IsOpen` under `_sync` — the very lock-ordering hazard that #462's potential document flags +> and that #500 exists to remove. Adding it here would create a new instance of the class of defect +> this feature is closing. + +`:1062` records the residual as "shipped as designed". + +A precise reading matters here. `RequestOpen` **already** reads `_host.IsOpen` under `_sync`, at +`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:112` +(`if (_closeInFlight && _host.IsOpen) return ClosedTask;`). SR-4's rationale is therefore not the claim +that no such read exists in the class. It is the narrower and still-correct claim that a feature whose +purpose is to remove instances of a hazard class must not **add a new instance** of that hazard class. +That distinction is the whole of the reconciliation: + +- SR-4 declined a specific code shape — an `_host.IsOpen` read placed *inside* `_sync` — on the ground + that it enlarges the set of foreign calls made while the coordinator's lock is held. `IsOpen` is an + interface member (`QuickFiler/Viewers/IBreadcrumbDropDownHost.cs:22`); the coordinator holds a + `IBreadcrumbDropDownHost` (`BreadcrumbDropDownOpenCoordinator.cs:18`), not the concrete class, so any + substituted implementation could take its own lock or re-enter the coordinator from inside `_sync`. +- The remedy in this spec does **not** place the read inside `_sync`. The read happens before the lock is + acquired and only a `bool` local crosses into the critical section. The count of foreign calls made + while `_sync` is held is unchanged by this fix. +- SR-4 is therefore neither wrong nor overridden. Its stated objection does not apply to the hoisted + form, which was not among the shapes SR-4 evaluated. + +The coordinator already reads host state outside `_sync` on this same posted-work path: +`BreadcrumbDropDownOpenCoordinator.cs:193` reads `_host.IsOpen` inside the `Reset()` continuation with no +lock held. The hoisted read is consistent with that existing pattern. + +### Boundaries and invariants to preserve: + +- **I-462.1** — `_closeInFlight` is true only while `_host.Close(reason)` executes and is cleared in a + `finally` (`:325-329`). Unchanged. +- **I-462.3** — a repeated close of an already-closed host is suppressed. Preserved: when the host is not + open, `hostOpen` is `false`, the added conjunct is `true`, and the guard behaves exactly as on HEAD. +- **Lock discipline** — no new call to any `IBreadcrumbDropDownHost` member is made while `_sync` is + held. This is the invariant SR-4 protects. +- **Guard order** — released, then in-flight, then completed. Unchanged. +- **Generation semantics** — `_generation` is incremented by a successful close (`:334`) and by + `Invalidate` (`:350`), never by `RequestOpen`. Unchanged; the fix does not consult `_generation`. +- **Closing while the host reports not open remains permitted.** The coordinator must still be able to + reach `_host.Close` when `_host.IsOpen == false`; see + `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` under **Test Strategy**. The fix + preserves this because `!hostOpen` is a *conjunct added to an existing suppression*, never a + suppression on its own. + +### Dependencies or blocked work: + +- None. The change is self-contained in one production file plus one test file. +- Environment bootstrap only (see **Assumptions, Constraints, Dependencies**). +- Does not depend on, and must not wait for, feature #488 or #501. + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` — the sole production file. +- `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` — one added test method. + +#### Functions/classes/CLI commands impacted: +- `BreadcrumbDropDownOpenCoordinator.CloseCore(BreadcrumbDropDownCloseReason)` — the two edits above. +- The `_closeCompleted` field XML documentation (`:38-46`) and the `CloseCore` summary (`:302-307`) — text + updated to state that suppression now additionally requires the host to report not open, and to record + why the host read is hoisted. +- No public API, no CLI command, and no interface member changes. `CloseCore` is `private`. + +#### Data flow and validation changes: +- One additional read of `_host.IsOpen` per `CloseCore` invocation, taken before the lock. No writes. +- The read is unconditional, so it also occurs when the coordinator is already released. This is accepted: + `IsOpen` is a side-effect-free state read on the sole production implementation + (`BreadcrumbDropDownHost.cs:191` delegating to the auto-property at `:244`), it does not throw after + disposal, and no test in `QuickFiler.Test` uses a strict `Mock` or counts + `IsOpen` reads. The alternative — an early `IsReleased()` check before the read — would add a second + lock acquisition to every close for no behavioral gain, and is rejected on simplicity grounds. + +#### Error handling and logging updates: +- None. The coordinator has no logger, and the change introduces no new failure mode. Existing exception + routing through `BreadcrumbUiDispatcher` is untouched. + +#### Rollback/feature-flag considerations (if applicable): +- No feature flag. The change is a two-line edit to one private method; rollback is a revert of the + single production file. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: +- `CloseCore(BreadcrumbDropDownCloseReason reason) -> bool`. Signature unchanged. +- Return-value contract, restated with the fix applied: + - `false` when the coordinator is released. + - `true` without calling `_host.Close` when a close is in flight. + - `true` without calling `_host.Close` when a close has already completed **and** the host reports not + open. + - Otherwise `_host.Close(reason)` is called; `true` if the host accepted it, in which case + `_generation` is incremented and `_closeCompleted` is set; `false` if the host rejected it, in which + case an `Uncommitted` close with the selector still open cancels the selector (`:339-340`). + +#### Required configuration keys and defaults: +- None. + +#### Backward-compatibility expectations: +- The observable behavior changes in exactly one state: `_closeCompleted == true` **and** + `_host.IsOpen == true`. On HEAD that state suppresses the close; after the fix it reaches + `_host.Close`. No production path can currently produce that state (see **Scope & Non-Goals**), so no + shipped behavior changes. +- Every other state is bit-identical to HEAD, because `!hostOpen` is `true` whenever the host is not + open and the guard then evaluates exactly as before. + +#### Performance constraints (latency/throughput/memory): +- One additional property read per close request. No allocation, no additional lock acquisition, no I/O. + No measurable latency or memory impact; no performance budget applies. + +### Race analysis for the hoisted read + +The host state can change between the unlocked read and the lock acquisition. Both directions must be +stated rather than assumed benign. + +**Direction 1 — read `true`, host closes before the lock is taken.** The added conjunct evaluates +`!hostOpen == false`, so the completed-close suppression does not fire and `_host.Close(reason)` is +invoked on a host that is now closed. This is a defined, safe call: `BreadcrumbDropDownHost.Close` +(`BreadcrumbDropDownHost.cs:247-257`) returns `false` when `_disposed`, and when `OpenState` is `false` +it returns `_openLifetime.TryCancelPendingOpen(...)` rather than performing a close. `closed` is then +`false`, `_closeCompleted` is left unchanged, and the `Uncommitted` fallback at `:339-340` may cancel a +still-open selector. That fallback is the coordinator's existing and correct response to a host that +declined a close, so the outcome is a redundant call with a correct result, not a corrupted state. + +**Direction 2 — read `false`, host opens before the lock is taken.** The conjunct evaluates `true`, the +close is suppressed, and the residual persists for that interleaving. This is exactly HEAD's behavior, +so it is a narrowed residual rather than a regression. + +**Why the window is not reachable in production.** Every production invocation of `CloseCore` runs on the +`BreadcrumbPopupUiOperations` queue: `SetDroppedDown` calls it inside `_operations.PostAsync` +(`:167`), `HandleSelectorOpenStateChanged` calls it inside `_operations.PostAsync` (`:182`), and +`FinishOpenCore` — the only other caller (`:277`) — is itself invoked inside `_operations.RunAsync` +(`:223`). The production mutations of `OpenState` occur in WinForms event handling +(`BreadcrumbDropDownHost.cs:426-437`) and in work scheduled through the host's own UI operations +(`BreadcrumbDropDownOpenLifetime.cs:268`, `BreadcrumbDropDownHost.cs:397-411`). The read and the +mutations are therefore serialized on the UI thread in production, and the interleaving above requires a +host implementation that mutates open state from another thread. + +**Conclusion.** The race is tolerable in both directions: one direction produces a redundant but +correctly-handled call, the other reproduces current behavior. This is a strictly better position than +HEAD, which suppresses unconditionally. The alternative that removes the race entirely — reading +`_host.IsOpen` inside `_sync` — is the shape SR-4 declined and is not adopted. + +### Option ranking + +| Option | Verdict | Reason | +|---|---|---| +| **Hoisted host read + `if (_closeCompleted && !hostOpen)`** | **Recommended** | Closes the residual, edits no test, adds no foreign call under `_sync`, and does not add a new instance of the hazard class SR-4 declined. Its only cost is the bounded race analyzed above. | +| SR-4 refinement: `if (_closeCompleted && !_host.IsOpen)` inside `_sync` | Rejected | Removes the residual and passes every existing test, but places an `IBreadcrumbDropDownHost` call inside the critical section. Declined by SR-4 (`docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md:426-431`) on lock-hazard grounds; that ground still applies. | +| Option A — clear `_closeCompleted` on the successful-close path | Rejected | Lets a second `CloseCore` reach `_host.Close`, breaking `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose`, `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired`, and `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` (whose XML documentation at `Part2.cs:366-373` names it as the standing guard against exactly this option). A remedy requiring a test edit is a regression trade and is out of scope. | +| Option B — gate `CloseCore` on `!_host.IsOpen` alone | Rejected | `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:301-318`) proves that closing while `_host.IsOpen == false` is required behavior, so a bare `!_host.IsOpen` gate suppresses a required close. It also re-introduces the under-`_sync` read unless hoisted. | +| Option C — track `_closedAtGeneration` | Rejected | `_generation` is incremented by the successful close itself (`:334`) and by `Invalidate` (`:350`), and never by `RequestOpen`. A generation stamp would suppress the close of a genuinely new open unless additional reset state were added, which is a larger change than the residual warrants. | +| Option D — two flags with distinct meanings | Shipped, insufficient | This is HEAD (`:36`, `:46`). It is correct for every reachable production state and is the source of the residual. Retained; the fix narrows its suppression rather than replacing it. | +| Route the bypassing reopen paths through `RequestOpen` or `Invalidate` (the issue's first two proposed-fix bullets) | Not applicable | The enumeration in **Scope & Non-Goals** shows there is no such production path to route. Acting on those bullets would require editing host-surface files that this spec places out of scope, with no defect to fix at those sites. | + + +## Assumptions, Constraints, Dependencies + +### Assumptions (environment, data, access): +- The residual is closable inside `BreadcrumbDropDownOpenCoordinator` alone. Supported by the reopen-path + enumeration above; the issue's attribution to feature #488's host paths is superseded by that finding + and the reason is recorded here rather than by editing `issue.md`. +- `IBreadcrumbDropDownHost.IsOpen` is a side-effect-free state read for every implementation the + coordinator will be given. True of the sole production implementation + (`BreadcrumbDropDownHost.cs:191`, `:244`) and of every test double in `QuickFiler.Test`. +- The existing `ControlledHost` fixture is sufficient for the regression test; no new production seam is + required. `[assembly: InternalsVisibleTo("QuickFiler.Test")]` exists at + `QuickFiler/Properties/AssemblyInfo.cs:5`. + +### Constraints (budget, performance, compatibility): +- Target framework .NET Framework 4.8; C# language features must remain compatible with the existing + project settings. No `init` accessors and no `record` types (no `IsExternalInit` on net48). +- File-size limit of 500 lines per `.claude/rules/general-code-change.md`. After the change, + `BreadcrumbDropDownOpenCoordinator.cs` (378 lines on HEAD) and + `BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (173 lines on HEAD) must both remain under 500. +- `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:1` carries `#nullable enable`, so the file + participates in nullable analysis and `CS86xx` diagnostics are promoted to errors by the type-check + gate. The added local is a non-nullable `bool` and introduces no null state. +- Tests must be MSTest with FluentAssertions per `CLAUDE.md` (Moq is available but not needed here; the + hand-written `ControlledHost` fixture is used). +- Deterministic tests only: no timers, no `Thread.Sleep`/`Task.Delay`, no second thread, no temporary + files, no Outlook or WebView2. +- Parallel-run concurrency constraint: the footprint table in **Scope & Non-Goals** is a hard boundary. + +### External dependencies (services, libraries, releases): +- No new NuGet package, no package version change, no `packages.config` edit. + +### Environment preconditions (bootstrap required before any gate can run) +This worktree contains **no `.dotnet-sdk` directory and no `packages/` directory**, verified by a glob of +both paths returning no files. Both must be bootstrapped before any `msbuild` or test command can run: + +1. `scripts/vscode/Install-RepoDotNetSdk.ps1` — provisions the repo-local SDK. +2. A NuGet restore — `scripts/vscode/Invoke-Restore.ps1` populates `packages/`. + +Running `msbuild` before these complete fails for environment reasons and must not be recorded as a gate +failure. + + +## Data / API / Config Impact +- **User-facing or API changes:** none. `CloseCore` is `private`; `IBreadcrumbDropDownHost` and every + `internal` member of `BreadcrumbDropDownOpenCoordinator` keep their current signatures. No user-visible + behavior changes on any currently reachable production path. +- **Data or migration considerations:** none. The coordinator holds no persisted state; the change adds a + method-local `bool`. +- **Logging/telemetry updates (if any):** none. The class has no logger and the change adds no diagnostic + surface. +- **Compatibility notes (CLI flags, config schemas, versioning):** none. No CLI flag, no configuration + key, no schema, and no assembly version change. No `.csproj`, `.props`, `.targets`, or + `packages.config` edit. + + +## Test Strategy + +This is a C# item. The framework is **MSTest**, assertions use **FluentAssertions**, and **Moq** is +available for mocking, per `CLAUDE.md`. The template's "pytest" wording does not apply and is replaced +here. + +### Standing guards that must remain unedited + +Four tests encode the repeated-close suppression contract that the #462 two-flag design was chosen to +satisfy. Any remedy requiring an edit to any of them is a regression trade and is rejected; the rejection +reason for each is recorded in the option table above. + +1. `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:263` — + `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose`. Two consecutive + `SetDroppedDown(false)` drives; asserts + `harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.Uncommitted)` — a single close + reason (`:278`). The host open is still pending at that point (`pending.SetResult(false)` at `:274`), + so `IsOpen` is `false` during both drives. + *Under the recommended remedy:* `hostOpen` is `false` on the second drive, the conjunct is `true`, and + the second close stays suppressed. Unchanged. +2. `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:121` — + `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired`. Asserts + `harness.Host.CloseReasons.Should().Equal(BreadcrumbDropDownCloseReason.ExplicitCommit)` (`:139`) + after two `HandleSelectorOpenStateChanged` drives following a successful open then close. + *Under the recommended remedy:* the host accepted the first close and `ControlledHost.Close` sets + `IsOpen = false` (`BreadcrumbDropDownOpenCoordinatorTests.cs:436-437`), so `hostOpen` is `false` on + the second drive and it stays suppressed. Unchanged. +3. `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:333` — + `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync`. Asserts + `harness.Host.Requests.Should().HaveCount(2)` (`:358-360`). It reopens the host via + `harness.Host.SetOpen(true)` (`:349`) — the same bypass seam the new regression test uses. + *Under the recommended remedy:* this test exercises `RequestOpen`, not `CloseCore`, and is unaffected. +4. `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:375` — + `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce`. Its XML documentation + (`Part2.cs:366-373`) states it is "the standing guard that rules out research section 6.1 option A + (clearing the flag on the successful-close path)". Asserts a single `Uncommitted` close reason + (`:391-396`). + *Under the recommended remedy:* no reopen occurs, the host is closed, `hostOpen` is `false`, and the + close reaches the host exactly once. Unchanged. + +Additionally, `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:302` — +`PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` — proves that closing while +`_host.IsOpen == false` is *required* behavior, which is why a bare `!_host.IsOpen` gate (option B) is +wrong. Under the recommended remedy `_closeCompleted` is `false` on that first close, so the added +conjunct cannot suppress it. This test must not regress and must not be edited. + +### Regression test to add + +**File:** `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` (append to the +existing `public sealed partial class BreadcrumbDropDownOpenCoordinatorTests`; no `[TestClass]` attribute +is repeated, per the note at `Part3.cs:11-14`). + +**Name:** `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` + +**Scenario (Arrange–Act–Assert):** +- *Arrange:* construct `CoordinatorHarness`; `harness.Host.Enqueue(Task.FromResult(true))`; drive + `RequestOpen()` and `DrainUntil` to completion; drive `SetDroppedDown(false)` and `DrainAll` so the host + accepts a close and `_closeCompleted` becomes `true`; assert `harness.Host.IsOpen` is `false`. +- *Act:* `harness.Host.SetOpen(true)` — the reopen that bypasses both `RequestOpen` and `Invalidate` + (`BreadcrumbDropDownOpenCoordinatorTests.cs:407`); set `harness.SelectorOpen = true`; drive a second + `SetDroppedDown(false)` and `DrainAll`. +- *Assert:* `harness.Host.CloseReasons.Should().Equal(new[] { BreadcrumbDropDownCloseReason.Uncommitted, BreadcrumbDropDownCloseReason.Uncommitted })` + — the close after a bypassing reopen must reach `_host.Close` a second time. +- *Determinism:* single thread, explicit drain of the capturing synchronization context, no timers, no + sleeps, no temporary files. + +**Red-to-green requirement:** the test must be demonstrated failing on HEAD (`CloseReasons` holds one +element, because `_closeCompleted` suppresses the second close) and passing after the fix. A test that is +green before the production change does not verify the fix and does not satisfy the acceptance criteria. + +### Edge cases and negative scenarios +- Repeated close with no reopen — covered by standing guard 4; must stay suppressed. +- Close while the host reports not open and `_closeCompleted` is `false` — covered by + `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen`; must still reach `_host.Close`. +- Close after `Release()` — covered by + `SetDroppedDown_AfterRelease_PostsNothingAndLeavesHostStateUntouched` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:192`); the released guard + must still return before any host state changes. +- Host rejects the close (`CloseResult = false`) — covered by + `PendingToggleClose_RejectedHostPerformsOneFallbackCancellation` + (`BreadcrumbDropDownOpenCoordinatorTests.cs:283`) and + `ResetReleaseAndCloseResults_PreserveRetryAndBlockReleasedWork` + (`BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:143`); the selector-cancel fallback must be unchanged. +- Integration-level single-close assertions — `SetFolderDroppedDownFalse_RequestsOneUncommittedCloseAndRollback` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownIntegrationTests.cs:89`) and + `InitializationFailure_CancelsSessionWithoutDuplicateClose` (`:264`) both use a host mock whose `Close` + sets `_hostOpen = false` on success (`:353-360`), so `hostOpen` is `false` at any repeated close and + their `Times.Once()` verifications are unaffected. + +### Error handling and logging verification +- Not applicable. The change adds no exception path and no log statement. The existing + `BreadcrumbUiDispatcher` error-sink behavior is exercised by + `RequestOpen_RollbackOperationThrows_CompletesFalseWithoutSurfacingSecondary` + (`BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:302`) and must not regress. + +### Coverage impact and targets for changed lines/modules +- The changed lines are the hoisted read and the widened guard in `CloseCore`. Both are executed by the + new regression test and by every existing close test, so changed-line coverage is 100%. +- Both outcomes of the new conjunct are covered: `!hostOpen == true` (suppression retained) by standing + guards 1, 2 and 4; `!hostOpen == false` (suppression released) by the new regression test. +- Coverage for `QuickFiler` must not decrease relative to the pre-change run. + +### Toolchain commands to run (format -> lint/analyze -> type-check -> test) + +Run in this exact order; if any step fails or modifies a file, fix and restart from step 1. + +1. **Format:** `dotnet tool run csharpier format .` — verify with `dotnet tool run csharpier check .`. + Always invoke through `dotnet tool run` so the manifest-pinned version is used. +2. **Analyze:** + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +3. **Type-check:** + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +4. **Test with coverage:** `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, which supplies `/InIsolation` + and `/TestCaseFilter:TestCategory!=LiveOutlook`. + +Mandatory command-shape rules: +- **Do not add `/p:Nullable=enable`.** No project in this repository carries a `` element and + there is no `Directory.Build.props`, so the property is a solution-wide opt-in that conscripts files + that never adopted the pragma. It can never pass and CI omits it deliberately. +- **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 on every + project and the gate cannot fail. +- **Do not call `vstest.console.exe` directly.** A bare invocation omits the + `TestCategory!=LiveOutlook` filter and would launch a real Outlook process. Use the wrapper script. +- Complete the environment bootstrap in **Assumptions, Constraints, Dependencies** before step 2. + +### Manual validation steps (if required) +None. The residual is not reachable through the shipped UI (see **Scope & Non-Goals**), so there is no +manual gesture that exercises it. Verification is entirely by automated test and source inspection. + + +## Acceptance Criteria + +- [x] AC-1 — `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` reads `_host.IsOpen` into a local + declared **before** the `lock (_sync)` that opens `CloseCore`, and the completed-close guard inside + that lock is `if (_closeCompleted && !) return true;`. Checkable by reading the changed + lines of `CloseCore` in the diff. +- [x] AC-2 — SR-4 reconciliation: no statement added or modified inside any `lock (_sync)` block of + `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` invokes a member of + `_host`/`IBreadcrumbDropDownHost`. Checkable by reading every `lock (_sync)` body in the changed + file and confirming the only pre-existing such call remains the one at `RequestOpen` + (`if (_closeInFlight && _host.IsOpen) return ClosedTask;`), with no new one added. +- [x] AC-3 — A new test named `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` exists + in `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`, drives a successful + open, a successful close, `harness.Host.SetOpen(true)`, and a second close, and asserts that + `harness.Host.CloseReasons` equals two `BreadcrumbDropDownCloseReason.Uncommitted` entries. + Checkable by reading the test body. +- [x] AC-4 — `CloseCore_AfterSuccessfulCloseAndHostReopen_ReachesHostCloseAgain` is demonstrated **failing + before** the production edit and **passing after** it, with both run outputs recorded in the + feature evidence folder under `evidence/qa-gates/`. Checkable by comparing the two recorded + `Invoke-MSTestWithCoverage.ps1` outputs for that test name. +- [x] AC-5 — `PendingToggleClose_HostOwnershipSuppressesFallbackAndRepeatedClose` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:263`) passes and its file is + unchanged in the diff. Checkable by the test run output and by `git diff --stat` for that file. +- [x] AC-6 — `SelectorStateTransitions_RequestOpenThenCloseOnlyWhenRequired` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:121`) passes and its + assertion text is unchanged. Checkable by the test run output and by `git diff` for that file + showing no change to the test. +- [x] AC-7 — `RequestOpen_AfterSuccessfulCloseAndHostReopen_ReachesHostOpenAsync` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:333`) passes and its + assertion text is unchanged. Checkable by the test run output and by `git diff` for that file. +- [x] AC-8 — `CloseCore_RepeatedCloseWithoutReopen_ClosesHostExactlyOnce` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:375`) passes and its + assertion text is unchanged. Checkable by the test run output and by `git diff` for that file. +- [x] AC-9 — `PendingAutomaticClose_RequestsExplicitCommitWhenHostIsNotOpen` + (`QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.cs:302`) passes, confirming that a + close while the host reports not open still reaches `_host.Close`. Checkable by the test run + output. +- [x] AC-10 — Production footprint: `git diff --name-only ...HEAD` lists no file under + `QuickFiler/` other than `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs`. Checkable by + running that command and reading its output. +- [x] AC-11 — Build-configuration footprint: the same `git diff --name-only` output contains no path + matching `*.csproj`, `*.props`, `*.targets`, or `packages.config`. Checkable by running that + command and reading its output. +- [x] AC-12 — Test footprint: the same `git diff --name-only` output lists no file under + `QuickFiler.Test/` other than + `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs`. Checkable by running + that command and reading its output. +- [x] AC-13 — File-size limit: `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and + `QuickFiler.Test/Viewers/BreadcrumbDropDownOpenCoordinatorTests.Part3.cs` each contain fewer than + 500 lines after the change. Checkable by a line count of each file. +- [x] AC-14 — Format gate: `dotnet tool run csharpier check .` exits 0 and reports no file requiring + formatting. Checkable by the command's exit code and output. +- [x] AC-15 — Analyzer gate: + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + completes with `0 Error(s)` and introduces no new warning attributed to + `BreadcrumbDropDownOpenCoordinator.cs`. Checkable by the msbuild summary and a warning grep of the + log for that file name. +- [x] AC-16 — Analyzer-gate non-vacuity: the analyzer-gate log contains no + `Skipping target "CoreCompile"` line for `QuickFiler` or `QuickFiler.Test`, proving the changed + files were actually compiled. Checkable by grepping the captured msbuild log. +- [x] AC-17 — Type-check gate: + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + completes with `0 Error(s)`. The command must not include `/p:Nullable=enable` and must use + `/t:Rebuild`. Checkable by the msbuild summary and by the recorded command text. +- [x] AC-18 — Test gate: `scripts/vscode/Invoke-MSTestWithCoverage.ps1` completes with zero failed tests + for `QuickFiler.Test`, and its recorded invocation shows `/InIsolation` and + `/TestCaseFilter:TestCategory!=LiveOutlook` in effect. Checkable by the run summary and the + recorded command line. +- [x] AC-19 — The `_closeCompleted` field documentation + (`QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs:38-46`) and the `CloseCore` summary + (`:302-307`) state that completed-close suppression now additionally requires the host to report + not open, and record why the host read is taken outside `_sync`. Checkable by reading those comment + blocks in the diff. +- [x] AC-20 — No new production seam: the diff adds no new `internal` or `public` member to + `BreadcrumbDropDownOpenCoordinator` and no member to `IBreadcrumbDropDownHost`. Checkable by + reading the diff of `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and confirming + `QuickFiler/Viewers/IBreadcrumbDropDownHost.cs` is absent from the changed-file list. + + +## Risks & Mitigations + +### Technical or operational risks +- **R-1 — The hoisted read observes stale host state.** Analyzed in **Race analysis for the hoisted + read**. One direction yields a redundant `_host.Close` on an already-closed host, which + `BreadcrumbDropDownHost.Close` (`:247-257`) handles by returning `false` without closing; the other + direction reproduces HEAD behavior. *Mitigation:* the analysis is recorded in the spec and in the code + comment required by AC-19, so a future reader does not re-derive it; production invocations are + serialized on the UI operations queue, which closes the window in practice. +- **R-2 — An unnoticed test asserts a single close in a state where the host still reports open.** Such a + test would fail after the change. *Mitigation:* the close-count assertions across `QuickFiler.Test` + were inspected; every fake and mock host clears its open state when `Close` succeeds + (`BreadcrumbDropDownOpenCoordinatorTests.cs:436-437`, + `BreadcrumbDropDownIntegrationTests.cs:353-360`, + `BreadcrumbSubfolderActivationTests.cs:322-329`, + `BreadcrumbSelectorOpenRetryTests.cs:345-349`), and a loose `Mock` returns + `false` for `IsOpen` by default. The full-suite run required by AC-18 is the authoritative check. +- **R-3 — The unconditional read touches the host after `Release()`.** *Mitigation:* `IsOpen` is a + side-effect-free auto-property read on the sole production implementation + (`BreadcrumbDropDownHost.cs:191`, `:244`) and does not throw after disposal; no test uses a strict host + mock or counts `IsOpen` reads. `SetDroppedDown_AfterRelease_PostsNothingAndLeavesHostStateUntouched` + (`BreadcrumbDropDownOpenCoordinatorTests.Part2.cs:192`) covers the released path and is part of the + AC-18 run. +- **R-4 — Scope creep into host-surface files.** The issue text directs the fix at feature #488's host + paths, which would enlarge the footprint and reduce parallel-run concurrency. *Mitigation:* the + enumeration in **Scope & Non-Goals** removes the justification for touching those files, and AC-10 + through AC-12 pin the footprint mechanically. +- **R-5 — The regression test is authored green.** A test that passes before the production edit proves + nothing. *Mitigation:* AC-4 requires recorded before-and-after runs. +- **R-6 — Environment bootstrap is mistaken for a gate failure.** The worktree has no `.dotnet-sdk` and + no `packages/`. *Mitigation:* the bootstrap steps are recorded as a precondition in **Assumptions, + Constraints, Dependencies**. + +### Mitigations and rollbacks +- Rollback is a revert of `QuickFiler/Viewers/BreadcrumbDropDownOpenCoordinator.cs` and removal of the + added test method. There is no migration, no persisted state, and no feature flag to unwind. +- Because no production path can currently reach the changed state, a rollback restores behavior that is + observationally identical to the fixed build on every shipped path. + + +## Rollout & Follow-up + +### Release/rollout steps +1. Complete the environment bootstrap (`scripts/vscode/Install-RepoDotNetSdk.ps1`, then a NuGet restore). +2. Add the regression test and record its failing run under the feature `evidence/qa-gates/` folder. +3. Apply the two-line `CloseCore` edit and the documentation update. +4. Run the four-step toolchain in order until it passes in a single pass; record each gate output under + the feature `evidence/qa-gates/` folder. +5. Check off the acceptance criteria in this file as each is verified. +6. Open the pull request. No staged rollout, no feature flag, and no runtime configuration change is + required. + +### Post-fix monitoring or clean-up tasks +- No telemetry to monitor: the changed state is unreachable from shipped UI, so there is no production + signal to watch. +- Optional follow-up, not required by this issue: `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md` + records the SR-4 residual as "shipped as designed" at `:1062` and as a known limitation at `:432-437`. + Once this fix merges, that record becomes historical. Amending it is out of scope here; if the project + wants the #501 spec annotated, promote a separate documentation item rather than widening this + footprint. +- If a future host implementation is introduced that mutates open state off the UI thread, revisit + **R-1**; the race window analyzed here becomes reachable in that configuration. + +### Links +- Issue: https://github.com/drmoisan/TaskMaster/issues/656 +- Research artifact: + `docs/features/active/2026-08-27-breadcrumb-closecompleted-residual-outside-requestopen-invalidate-656/research/2026-08-31T20-15-closecompleted-residual-reopen-path-enumeration.md` +- Origin: split out of #501 / #462; see `docs/features/active/breadcrumb-coordinator-hub-defects-501/spec.md`, + SR-4 (`:426-437`) and the implementation note at `:1062`. +- Prior option space: `docs/features/active/breadcrumb-coordinator-hub-defects-501/research/2026-08-24T09-12-breadcrumb-ordering-invariants-research.md`, + section 6.1. +- Prior evidence: `docs/features/active/breadcrumb-coordinator-hub-defects-501/evidence/qa-gates/closepending-split.2026-08-27T20-53.md` +- PRs: to be added on submission.