Summary
QfcCollectionController.UnregisterNavigation bounds its unregister loop with the current
_itemGroups.Count, while RemoveSpecificControlGroup(int) mutates _itemGroups with no
unregister/register bracket around the mutation. When a group is removed through that unbracketed
path, the count the unregister loop later reads no longer matches the count in force when the
navigation keys were registered, so the loop stops short and leaves orphaned KbdActions
navigation registrations behind. Every production call site discards KbdActions.Remove's bool
result, so the divergence is silent until a later Add or Find throws
(ArgumentException or InvalidOperationException). The unbracketed mutation is reachable from
RemoveBelowThresholdAsync via the RemoveGroupByEntryId seam, and from the 'R' char action in
QfcItemController.EventWiring.cs. This is a distinct defect from the register/unregister
digit-width mismatch filed as #472: #472 concerns the format of the keys removed, this concerns
the number of them. Fixing it requires the key-ledger design - recording the exact set of keys
registered and replaying that set on unregistration - which changes the outcome of the existing
characterisation tests in QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs, a file at
the 500-line ceiling whose [TestMethod] count issue #468 froze. It was therefore deliberately
kept out of #472's scope under the CLAUDE.md Bugfix Workflow rule that a deeper design problem
uncovered mid-fix opens a new issue instead of widening scope.
Environment
- OS/version: Windows 11, Outlook VSTO add-in host
- Python version: n/a (C# / .NET Framework)
- Command/flags used:
vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"
- Data source or fixture:
QfcCollectionController with an _itemGroups collection crossing a group removal
Steps to Reproduce
- Bring up the QuickFiler collection surface with enough item groups that navigation keys are
registered for each group.
- Remove a group through an unbracketed path - either
RemoveBelowThresholdAsync (which reaches
RemoveSpecificControlGroup through the RemoveGroupByEntryId seam) or the 'R' char action
wired in QfcItemController.EventWiring.cs. No UnregisterNavigation / RegisterNavigation
bracket surrounds this mutation.
- Trigger
UnregisterNavigation. Its loop bound is now the reduced _itemGroups.Count, so it
iterates fewer times than the registration did.
- Re-register navigation, or press a navigation key that resolves against the stale registry.
Expected Behavior
Unregistration removes exactly the set of navigation keys that registration added, regardless of any
_itemGroups mutation that occurred in between. A subsequent registration succeeds, and a navigation
keypress resolves against exactly one handler.
Actual Behavior
One or more navigation registrations are orphaned in the KbdActions registry. Because every call
site discards Remove's bool return, nothing reports the failure at the point it happens. The
symptom surfaces later as an ArgumentException from a duplicate Add, or an
InvalidOperationException from a Find that resolves against a multi-element match set.
Logs / Screenshots
Impact / Severity
Medium: the failure is silent at the point of divergence and surfaces later as a thrown exception in
a keyboard path. In the QuickFiler surface the resulting exception is caught and logged in
KeyboardHandler, so the user-visible symptom is a dead navigation key rather than a crash. It is
not a blocker because the unbracketed removal paths are not on the common navigation flow.
Source
From: docs/features/potential/2026-08-27-qfc-unregister-navigation-count-mismatch-orphan.md
Summary
QfcCollectionController.UnregisterNavigationbounds its unregister loop with the current_itemGroups.Count, whileRemoveSpecificControlGroup(int)mutates_itemGroupswith nounregister/register bracket around the mutation. When a group is removed through that unbracketed
path, the count the unregister loop later reads no longer matches the count in force when the
navigation keys were registered, so the loop stops short and leaves orphaned
KbdActionsnavigation registrations behind. Every production call site discards
KbdActions.Remove'sboolresult, so the divergence is silent until a later
AddorFindthrows(
ArgumentExceptionorInvalidOperationException). The unbracketed mutation is reachable fromRemoveBelowThresholdAsyncvia theRemoveGroupByEntryIdseam, and from the'R'char action inQfcItemController.EventWiring.cs. This is a distinct defect from the register/unregisterdigit-width mismatch filed as #472: #472 concerns the format of the keys removed, this concerns
the number of them. Fixing it requires the key-ledger design - recording the exact set of keys
registered and replaying that set on unregistration - which changes the outcome of the existing
characterisation tests in
QuickFiler.Test/Controllers/QfcCollectionControllerTests.cs, a file atthe 500-line ceiling whose
[TestMethod]count issue #468 froze. It was therefore deliberatelykept out of #472's scope under the
CLAUDE.mdBugfix Workflow rule that a deeper design problemuncovered mid-fix opens a new issue instead of widening scope.
Environment
vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"QfcCollectionControllerwith an_itemGroupscollection crossing a group removalSteps to Reproduce
registered for each group.
RemoveBelowThresholdAsync(which reachesRemoveSpecificControlGroupthrough theRemoveGroupByEntryIdseam) or the'R'char actionwired in
QfcItemController.EventWiring.cs. NoUnregisterNavigation/RegisterNavigationbracket surrounds this mutation.
UnregisterNavigation. Its loop bound is now the reduced_itemGroups.Count, so ititerates fewer times than the registration did.
Expected Behavior
Unregistration removes exactly the set of navigation keys that registration added, regardless of any
_itemGroupsmutation that occurred in between. A subsequent registration succeeds, and a navigationkeypress resolves against exactly one handler.
Actual Behavior
One or more navigation registrations are orphaned in the
KbdActionsregistry. Because every callsite discards
Remove'sboolreturn, nothing reports the failure at the point it happens. Thesymptom surfaces later as an
ArgumentExceptionfrom a duplicateAdd, or anInvalidOperationExceptionfrom aFindthat resolves against a multi-element match set.Logs / Screenshots
Bug: qfc-collection-navigation-digits-desync #472's root-cause analysis, not by a captured runtime trace. The residual orphan it produces IS
observable: Bug: qfc-collection-navigation-digits-desync #472's width-fidelity regression test in
QuickFiler.Test/Controllers/QfcCollectionControllerNavigationDigitsTests.csasserts the residualentry explicitly and attributes it, by XML documentation comment, to this follow-up issue rather
than silently absorbing it.
Impact / Severity
Medium: the failure is silent at the point of divergence and surfaces later as a thrown exception in
a keyboard path. In the QuickFiler surface the resulting exception is caught and logged in
KeyboardHandler, so the user-visible symptom is a dead navigation key rather than a crash. It isnot a blocker because the unbracketed removal paths are not on the common navigation flow.
Source
From: docs/features/potential/2026-08-27-qfc-unregister-navigation-count-mismatch-orphan.md