fix(#232): repair QuickFiler navigation-key collision on page swap - #234
Merged
Conversation
- Route LoadControlsAndHandlers_01 page-swap through SwapItemGroups so the Unregister/Register navigation pairing runs, and guard the trailing RegisterNavigation in RemoveSpecificControlGroupAsync against double registration (fixes KbdActions "Key already exists" ArgumentException) - Add additive folder-confidence probability debug logging (item summary, score, caller) at the three scoring call sites - Add QfcCollectionController regression tests for the swap and double-registration paths Refs: #232 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y AC10 - Regenerate and persist the authoritative Cobertura coverage.xml to the canonical artifacts/csharp path and a committable evidence copy, closing the machine-readable coverage-verification gap (sole blocking finding) - Verify from the persisted XML: QfcHighConfidencePreFilter.cs line-rate 1.0 (100%); repo-wide 76.575% with no regression; flip AC10 to PASS - Correct the QfcDatamodel.cs probability-log caller-context string to name ScoreRemainingQueueMailItemAsync (the emitting method) - Add cycle-1 remediation inputs/plan, QA-gate and baseline evidence, and the initial feature-review audit artifacts Refs: #232 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Record policy-audit/code-review/feature-audit at 2026-07-03T17-25: Policy COMPLIANT, Code Go, Feature READY - Coverage verified from the persisted machine-readable Cobertura artifact; AC10 PASS; prior caller-context log-string finding resolved - Remediation loop closed at 0 blocking findings after one cycle Refs: #232 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix QuickFiler navigation-key collision on page swap (#232)
Summary
System.ArgumentException: "Cannot add key because it already exists. Key 2 SourceId Collection"that crashed active QuickFiler sessions during ordinary OK/Skip/pop-out page transitions."Collection"-sourced keys that later collided on the nextRegisterNavigation().Why
In high-confidence mode, popping out the last item on a page triggers an internal Skip that swaps in the next cached page. The swap path
QfcCollectionController.LoadControlsAndHandlers_01(TableLayoutPanel, List<QfcItemGroup>)replaced the active_itemGroupswithout callingUnregisterNavigation()for the outgoing page orRegisterNavigation()for the incoming page. Because_kbdHandler.StringActionsAsyncis a single session-lifetime, purely additive/subtractive collection shared across every page, any page abandoned via OK/Skip while it still held items left its"Collection"digit keys ("1".."N") permanently orphaned. The next unconditionalRegisterNavigation()(at the end ofRemoveSpecificControlGroupAsync) then collided on the first still-occupied slot — reported as "Key 2".This is a deterministic sequencing/bookkeeping gap, reproducible under purely sequential execution; the earlier "overlapping-removal race" hypothesis was investigated and ruled out as the primary cause. The existing (previously dead)
SwapItemGroupsmethod already encodes the correctUnregisterNavigation(); ...; RegisterNavigation();pattern.What Changed
Core fix (
QuickFiler/Controllers/QfcCollectionController.cs):LoadControlsAndHandlers_01(TableLayoutPanel, List<QfcItemGroup>)through the existingSwapItemGroups, restoring theUnregisterNavigation()/RegisterNavigation()pairing so the live registry always matches the displayed page.RemoveSpecificControlGroupAsyncso the trailing unconditionalRegisterNavigation()is skipped when the zero-item branch already registered the swapped-in page, preventing a re-introduced collision.Additive probability debug logging (no control-flow change):
QuickFiler/Controllers/QfcDatamodel.cs— onelogger.Debug(...)at the master-queue admission scoring point (ScoreRemainingQueueMailItemAsync).QuickFiler/Controllers/QfcItemController.FolderHandling.cs— logging at the four display-time scoring assignment points acrossLoadFolderHandler/LoadFolderHandlerAsync, with FromField/FromArrayOrString caller context.QuickFiler/Controllers/QfcHighConfidencePreFilter.cs— newloggerfield plus onelogger.Debug(...)in theFilterAsyncscoring lambda.Tests:
QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs— reported-repro regression (throws before fix / passes after), swap register/unregister ordering, and double-registration-guard tests.Architecture / How It Fits Together
Keyboard digit navigation is registered against
_kbdHandler.StringActionsAsync(KbdActions<string, KaStringAsync, Func<string,Task>>), a single collection owned by the session's home controller and reused across pages. Page transitions (OK, Skip, single-item pop-out) all converge on the swap path; routing that path throughSwapItemGroupsmakes the registry a strict function of the currently-displayed_itemGroups. The guard inRemoveSpecificControlGroupAsyncensures exactly one registration per incoming key when the drain-to-zero branch swaps a new page in.Verification
Completed on the branch (evidence under
docs/features/active/2026-07-03-quickfiler-navigation-key-collision-232/evidence/):QfcHighConfidencePreFilter.cschanged lines 100% (>= 90% target); repo-wide 76.575% with no regression versus baseline. Repo-wide raw coverage remains under the generic 80% floor but is governed by the ratified COM/VSTO/WinForms testable-denominator exemption (CLAUDE.md; issue Refactor: qfc-item-controller-testability #227) and is not worsened by this change.Recommended before merge:
ArgumentExceptionand thatStringActionsAsyncaccumulates no stale entries across a full session.Backward Compatibility / Migration Notes
logger.Debug(...)only, gated by the log4net root level (debug builds), with no behavioral effect.Risks and Mitigations
"Collection"entry per incoming key and noArgumentException.QfcCollectionControlleris COM/WinForms-bound and coverage-exempt, so the fix is verified through theIQfcCollectionController/reflection-injection test seams rather than a live Outlook host. Mitigation: tests assert the register/unregister interaction directly at that seam; manual verification is recommended above.Review Guide
QuickFiler/Controllers/QfcCollectionController.cs— the swap routing and theRemoveSpecificControlGroupAsyncguard (the actual fix).QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs— the four regression tests.logger.Debuglines).Follow-ups
evidence/other/follow-up-candidates.md): the fixed-batch-without-backfill pattern, the dormant issue Bug: quickfiler-high-confidence-prefilter #171 pre-filter pipeline, and theremovespecificcontrolgroupcounterreentrancy-counter hygiene.GitHub Auto-close