fix(quickfiler): correct keyboard-action registration desync in KbdActions, collection navigation and item expansion (#444, #472, #482) - #654
Merged
drmoisan merged 17 commits intoAug 27, 2026
Conversation
Acceptance criterion AC-472-10 of the quickfiler-keyboard-action-defects spec requires the unbracketed-removal count-mismatch defect recorded in `### Downstream notes` item 3 to be promoted into a new potential entry AND a new GitHub issue, on the stated grounds that prose left in the feature folder does not survive that folder's archival. Promoted through the MCP lifecycle (bug route, full-bug mode) as issue #644, verified OPEN with its full ten-section body intact. The defect itself is NOT fixed here: it needs the key-ledger design, which changes the outcome of characterisation tests in a 500-line file whose [TestMethod] count issue #468 froze. Also corrects the agent-memory entry on promotion body handling. The promotion tool maps sections by heading NAME against the issue template: template headings survive with their content intact, and only non-template headings are dropped. The previous entry generalised a real observation into a wrong causal rule that predicted data loss where there is none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…er-keyboard-action-defects-444
Phase 4 final QA loop evidence: repository-wide csharpier check (0 unformatted of 1541 files), post-format size audit, analyzer Rebuild (0 errors, 5 pre-existing warnings, 0 skipped CoreCompile), type-check Rebuild without /p:Nullable=enable (0 errors, 0 skipped CoreCompile), full-suite vstest run (6713 of 6713 passed), TRX host-value normalization, final coverage capture (line 85.13 percent, branch 79.21 percent), SyncExpandedRegistrations line-rate 1, the KbdActions guard covered on both paths, the coverage delta report (line +0.09, branch +0.09) and the clean-pass aggregator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
Checks off AC-QA-01 through AC-QA-06, AC-QA-08, AC-QA-09, AC-QA-10 and the Phase 3 deferral AC-482-08 in spec.md, each against the Phase 4 evidence: size audit, csharpier check, analyzer Rebuild, type-check Rebuild, the 6713-of-6713 vstest run, SyncExpandedRegistrations line-rate 1, the KbdActions guard covered on the throwing and non-throwing paths, and the non-negative coverage deltas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…ions Phase 5 scope-discipline gates and acceptance-criteria reconciliation: branch file list against the re-derived merge base (three production paths, one project file), forbidden-path gate (eleven of eleven absent), upstream contract conformance (six of six rows MATCH), Remove contract (1 and 0), public-API gate (zero public added/removed/re-signed), declined timer seam (zero added Timer lines), the no-live-form guard test, the plan line-number-citation gate (both counts zero), PR-body inputs, the completion report, and the 57-row AC reconciliation. Checks off AC-SCOPE-01 through AC-SCOPE-11, AC-QA-07, AC-QA-11, AC-QA-12 and AC-QA-13 in spec.md, bringing the total to 54 of 57. The three remaining criteria (AC-472-10, AC-482-11, AC-482-12) each require the integration pull-request body and are recorded as deferred under evidence/issue-updates/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
Records the terminal git status under the feature pathspec (QuickFiler, QuickFiler.Test, and the feature folder) and closes out the plan checklist: all 167 tasks checked. Zero source, test, or project paths uncommitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…acts All three audits returned 0 Blocking findings. Non-blocking items are the pre-existing 2437-line QfcCollectionController.cs (unchanged in size by this branch), the pre-existing 80-vs-85 percent coverage-floor documentation conflict, and five code-review observations none of which are regressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…er-keyboard-action-defects-444
… criteria Re-ran the full C# toolchain in the mandated order after merging integration tip 13a22ad (#493 fan-in). All four gates pass in a single loop: csharpier check 1543 files 0 unformatted; analyzer Rebuild 0 errors with a zero Skipping-target-CoreCompile count and 15 executed CoreCompile targets; nullable Rebuild 0 errors with 11 executed CoreCompile targets; 6719 of 6719 tests pass. Line coverage 85.1326 percent, branch 79.2162 percent. Checked off the final three acceptance criteria, which were deferred pending the PR body and are now genuinely satisfied by artifacts/pr_body_444.md: the issue #644 follow-up number, the deliberate behaviour widening, and the corrected #482 trigger and severity. spec.md is now 57 of 57. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
drmoisan
merged commit Aug 27, 2026
69e8317
into
epic/quickfiler-bug-family-integration
5 checks passed
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.
Summary
Fixes three keyboard-action registration defects in QuickFiler. All three share one root cause: a registration and an unregistration disagreeing about what was registered, with the disagreement swallowed because every production call site discards the
boolthatKbdActions.Removereturns.KbdActions(IEnumerable<UClass>)bypassed the duplicate-registration guard thatAddenforces, so a seed sequence containing twoStoredKeyEquals-equal keys constructed silently and threw later.logger.Error, and throwsArgumentException.UnregisterNavigationre-read the liveDigitswidth instead of the width in force at registration, so growing or shrinking the item count between register and unregister removed keys the page never registered and left the real ones behind.QfcCollectionControllerrecords the registration width in a newprivate int _registeredDigitsfield andUnregisterNavigationreplays that recorded width, with zero reads of theDigitsproperty.ToggleExpansionandToggleExpansionAsynceach maintained a different keyboard registry, so a synchronous toggle clearedCharActions(where nothing had been added) whileCharActionsAsynckept its entries, and the next expansion threwArgumentExceptionfromAdd.private void SyncExpandedRegistrations(bool expanded), which maintains both registries together.Base branch is
epic/quickfiler-bug-family-integration(epicquickfiler-bug-family), notmain.Deliberate behaviour widening
The #482 fix routes all expansion keyboard registration through
SyncExpandedRegistrations(bool expanded), which maintains both the synchronous_kbdHandler.CharActionsregistry and the asynchronous_kbdHandler.CharActionsAsyncregistry together rather than one per toggle path. That is a deliberate widening of observable behaviour, not an incidental side effect:'B'and'D'now respond after a synchronous expansion. Previously only an asynchronous expansion populatedCharActionsAsync, and the Alt-key path that readsCharActionswas left empty after a synchronous toggle.Band Alt+Dnow respond after an asynchronous expansion. Previously only a synchronous expansion populatedCharActions.The alternative — collapsing onto a single registry — was considered and rejected. Four focus-path methods in
QfcItemController.EventWiring.csconditionally call the expansion register and unregister methods on_expanded. Under a single-registry unification one of those cleanup paths would remove from the registry that no longer holds the entries, re-creating exactly the silent-falsedivergence these three issues describe. Maintaining both registries makes every one of those four call sites operate on a registry that genuinely holds the entries.Idempotence is preserved because the two unregister calls are unconditional and
KbdActions.Removereturnsfalserather than throwing when the pair is absent. That is why repeated and interleaved toggles no longer raiseArgumentExceptionfromKbdActions.Add, without any change toAdd's contract.Correction to #482's filed trigger and severity
The filed issue's stated trigger is unreachable and its stated severity is overstated. Both corrections are recorded in
spec.mdunder### #482 — expansion registry divergenceand are repeated here so this PR does not restate an unsupported claim.The filed trigger is dead code with respect to this interleaving. The promoted document names the synchronous
ToggleExpansion()call insideActivateBySelectionAsyncas what makes the interleaving reachable in production rather than theoretical. That call is guarded byif (blExpanded), and both asynchronous callers pass a value that is alwaysfalse: one passes the literalfalse, and the other passes a value returned fromToggleOffActiveItemAsync, whose expansion branch is commented out so it returns its parameter unchanged. The guarded call therefore never executes with atrueargument, and the filed trigger cannot produce the interleaving.The live trigger is Right, then Down, then Right:
ToggleExpansionAsync(On), setting_expanded = trueand adding'B'and'D'toCharActionsAsync.SelectNextItemAsync, which marshals to the synchronousSelectNextItem, and throughChangeByIndexandToggleOffActiveItemreaches the synchronousToggleExpansion(). That clears_expandedand removes fromCharActions, where nothing was ever added, soRemovereturnsfalsesilently.CharActionsAsyncstill holds'B'and'D'._expanded == false, soToggleExpansionAsync(On)runs andCharActionsAsync.Addis called for an entry that is already present, raisingArgumentException.The severity is a dead key, not a crash. The exception surfaces through the asynchronous keyboard handler in
KeyboardHandler.cs, whosecatchblock logs it. The user-visible symptom is a'B'or'D'key that stops responding for that item — not an unhandled exception and not a crash.Known follow-up: issue #644
spec.md### Downstream notesitem 3 describes a second, distinct defect inUnregisterNavigation, which this PR deliberately leaves in place: the method bounds its unregister loop with the current_itemGroups.Count, whileRemoveSpecificControlGroup(int)mutates_itemGroupswith no unregister/register bracket. When a group is removed through that unbracketed path — reachable fromRemoveBelowThresholdAsyncvia theRemoveGroupByEntryIdseam, and from the'R'char action — the count the unregister loop later reads no longer matches the count in force at registration, so the loop stops short and leaves orphaned navigation registrations behind.That defect is tracked as issue #644 (
qfc-unregister-navigation-count-mismatch-orphan, #644), promoted through the feature-promotion lifecycle in commit12256da4with potential entrydocs/features/potential/promoted/2026-08-27-qfc-unregister-navigation-count-mismatch-orphan.md.It is out of scope here under
CLAUDE.md's Bugfix Workflow step 2 ("If you uncover deeper design problems, open a new issue instead of widening scope"): the fix requires a key-ledger redesign that breaks characterization tests inQfcCollectionControllerTests.cs, a file sitting at exactly 500 lines with a[TestMethod]count frozen by upstream #468. This PR's #472 regression test asserts the residual orphan explicitly — exactly one"10"entry remains — and carries an XML doc comment attributing that residual to issue #644, so the assertion does not silently absorb the second defect.Changes
Three production files, four test files, one project-file line.
QuickFiler/Controllers/KbdActions.cs(+36)QuickFiler/Controllers/QfcCollectionController.cs(+8 −8)QuickFiler/Controllers/QfcItemController.Navigation.cs(+28 −4)QuickFiler.Test/Controllers/QfcCollectionControllerNavigationDigitsTests.cs(new, +226)QuickFiler.Test/Controllers/QfcItemController.NavigationTests.cs(+107)QuickFiler.Test/Controllers/KbdActionsRemainingBranchesTests.cs(+91)QuickFiler.Test/Controllers/KbdActionsTests.cs(+37)QuickFiler.Test/QuickFiler.Test.csproj(+1, one<Compile Include>in theControllers\Qfc*slot)No public API was added, removed, or re-signed; the two added members are both
private.QuickFiler/QuickFiler.csprojis untouched.KeyboardHandler.cs,IQfcCollectionController.csand the nine otherQfcItemControllerpartials are unmodified.Verification
Full C# toolchain, re-run in the mandated order after merging integration tip
13a22ade. All four gates pass in a single loop; no gate rewrote a file, so no restart was triggered.dotnet tool run csharpier check .msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=truemsbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=truepwsh -NoProfile -File ./scripts/vscode/Invoke-MSTestWithCoverage.ps1/t:Rebuildwas used for both msbuild gates. Non-vacuity is proven by a zero count ofSkipping target "CoreCompile"together with 15 and 11 executedCoreCompile:targets respectively — not by a csc-invocation count, which reads 0 at this verbosity even on a real compile./p:Nullable=enablewas not added to any command.All five analyzer warnings are the pre-existing
System.Reactive 7.0.0packages.configdiagnostic, one per consuming project, identical to the Phase 0 baseline.Coverage
Unfiltered whole-run denominator, the same wrapper and denominator as the Phase 0 baseline.
KbdActions.csline-rateQfcItemController.Navigation.csline-rateSyncExpandedRegistrations(new member)Both figures moved up and clear both readings of the repository's two coverage policies — the
CLAUDE.md§UT2 floor of>= 80%line, and the.claude/rules/general-unit-test.md/quality-tiers.mdfloors of>= 85%line and>= 75%branch. No interpretation was needed to declare a pass. That two-policy conflict is pre-existing and is recorded as unresolved rather than silently decided.Regression tests
Each of the three fixes has a fail-before / pass-after record under
evidence/regression-testing/andevidence/qa-gates/. The guard's throw path and its normal-completion path are both covered.Acceptance criteria
spec.mdis the sole acceptance-criteria source (work modefull-bug;user-story.mdis intentionally absent). 57 of 57 criteria are checked. Feature review returned 0 Blocking findings across policy-audit, code-review and feature-audit.Notes for the epic capstone
Closesreferences below will not fire on this base. GitHub registers closing references only for pull requests targeting the default branch, and this PR targetsepic/quickfiler-bug-family-integration. Issues Bug: kbdactions-enumerable-ctor-bypasses-duplicate-guard #444, Bug: qfc-collection-navigation-digits-desync #472 and Bug: qfc-item-controller-expansion-registry-divergence #482 are all still OPEN and must be closed by the epic's integration-to-main PR or by hand. Nothing here should be read as asserting they are closed.Controllers\Qfc*, but Bug: uithread-dispatcher-static-swap-no-restore #493's two added includes (Controllers\QfcItemController.UiThreadDispatcherFixture*.cs) also match that pattern. No conflict arose and nothing was lost, but the declared regions are not disjoint and would benefit from being narrowed to per-file lists in future waves.QfcCollectionController.csis 2437 lines, far over the 500-line cap. It is pre-existing at the base and unchanged in size by this branch (+8 −8); remediation is forbidden to this feature by the epic's ownership rules.Closes #444
Closes #472
Closes #482
🤖 Generated with Claude Code
https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA