You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Branch 1 of KaStringAsync.KeyEquals guards on a substring test but computes its Update argument with prefix arithmetic. The two are only consistent when other is a prefix of Key.
QuickFiler/Controllers/KaStringAsync.cs:
The branch-1 guard is Key.Contains(other) — a substring test.
Its body computes Update(Key.Substring(other.Length - 1, 1)), an offset that is only meaningful when other is a prefix of Key.
For Key = "abc" and other = "b", Contains is true and the expression yields "a", which is neither the matched character nor the character following it.
Reachability
Reachable in principle whenever the registered digit width is 2. GenerateStringKbdAction (QuickFiler/Controllers/QfcCollectionController.cs:1363-1385) registers keys "01" through "12" at that width; typing "1" matches "01" at index 1, as a substring rather than as a prefix.
It has no observable effect today, because Update is null on every KaStringAsync instance production creates:
QfcCollectionController.cs:1376-1383 passes null for both update and toggleControl.
KbdActions.Add(string, TKey, VDelegate) builds its element with UClass instance = new() (KbdActions.cs:99), the parameterless constructor, which assigns neither callback.
So every Update is not null guard in KeyEquals evaluates false on every production evaluation. This is a latent defect, not a live one.
Fixing it correctly requires a design decision that #445 was not scoped to make: whether branch 1 should test StartsWith instead of Contains. That is a keyboard-filtering behaviour change, and the current substring semantics are pinned by an existing test at QuickFiler.Test/Controllers/KbdActionsTests.cs:71-76. Changing it would invert that test rather than leave it passing.
Per the CLAUDE.md Bugfix Workflow, section 2 ("If you uncover deeper design problems, open a new issue instead of widening scope"), the defect was recorded and deferred rather than folded into #445. Issue #445's acceptance criterion AC19 requires this issue to exist, and its P4-T4 gate verifies the expression was left unchanged:
Key.Substring(other.Length - 1, 1) in KaStringAsync.cs — count 1, unchanged.
Key.Contains(other) in KaStringAsync.cs — count 1, unchanged.
The .Be("b") assertion at KaStringAsyncTests.cs (for Key = "abc", other = "ab", the prefix case) — count 1, unchanged.
Options to consider
Change branch 1 to Key.StartsWith(other). Makes the guard and the offset arithmetic consistent. Requires re-deciding, and re-pinning, the keyboard matching semantics currently asserted by KbdActionsTests.cs:71-76.
Keep Contains and correct the offset. For a substring match, compute the index via Key.IndexOf(other) and derive the intended character from that, leaving matching semantics untouched.
Document the prefix precondition and constrain registration so non-prefix matches cannot arise.
Option 2 is the narrowest and does not change which rows match a probe; option 1 is the larger behavioural change. The choice should be made deliberately, with a regression test for the two-digit-width case (Key = "01", other = "1").
Acceptance Criteria
A decision is recorded between StartsWith matching, corrected offset arithmetic under Contains, or a documented prefix precondition.
KaStringAsync.KeyEquals branch 1 no longer computes a substring offset that is meaningless for a non-prefix match.
A regression test covers the two-digit-width non-prefix case (for example Key = "01" with other = "1") with a non-null Update.
QuickFiler.Test/Controllers/KbdActionsTests.cs:71-76 either still passes unchanged, or its change is explicitly justified as the intended behavioural change.
The full C# toolchain passes: csharpier check ., the analyzer build, the nullable build, and the MSTest suite.
Summary
Branch 1 of
KaStringAsync.KeyEqualsguards on a substring test but computes itsUpdateargument with prefix arithmetic. The two are only consistent whenotheris a prefix ofKey.QuickFiler/Controllers/KaStringAsync.cs:Key.Contains(other)— a substring test.Update(Key.Substring(other.Length - 1, 1)), an offset that is only meaningful whenotheris a prefix ofKey.For
Key = "abc"andother = "b",Containsistrueand the expression yields"a", which is neither the matched character nor the character following it.Reachability
Reachable in principle whenever the registered digit width is 2.
GenerateStringKbdAction(QuickFiler/Controllers/QfcCollectionController.cs:1363-1385) registers keys"01"through"12"at that width; typing"1"matches"01"at index 1, as a substring rather than as a prefix.It has no observable effect today, because
Updateisnullon everyKaStringAsyncinstance production creates:QfcCollectionController.cs:1376-1383passesnullfor bothupdateandtoggleControl.KbdActions.Add(string, TKey, VDelegate)builds its element withUClass instance = new()(KbdActions.cs:99), the parameterless constructor, which assigns neither callback.So every
Update is not nullguard inKeyEqualsevaluatesfalseon every production evaluation. This is a latent defect, not a live one.Why it was excluded from #445
Fixing it correctly requires a design decision that #445 was not scoped to make: whether branch 1 should test
StartsWithinstead ofContains. That is a keyboard-filtering behaviour change, and the current substring semantics are pinned by an existing test atQuickFiler.Test/Controllers/KbdActionsTests.cs:71-76. Changing it would invert that test rather than leave it passing.Per the CLAUDE.md Bugfix Workflow, section 2 ("If you uncover deeper design problems, open a new issue instead of widening scope"), the defect was recorded and deferred rather than folded into #445. Issue #445's acceptance criterion AC19 requires this issue to exist, and its P4-T4 gate verifies the expression was left unchanged:
Key.Substring(other.Length - 1, 1)inKaStringAsync.cs— count 1, unchanged.Key.Contains(other)inKaStringAsync.cs— count 1, unchanged..Be("b")assertion atKaStringAsyncTests.cs(forKey = "abc",other = "ab", the prefix case) — count 1, unchanged.Options to consider
Key.StartsWith(other). Makes the guard and the offset arithmetic consistent. Requires re-deciding, and re-pinning, the keyboard matching semantics currently asserted byKbdActionsTests.cs:71-76.Containsand correct the offset. For a substring match, compute the index viaKey.IndexOf(other)and derive the intended character from that, leaving matching semantics untouched.Option 2 is the narrowest and does not change which rows match a probe; option 1 is the larger behavioural change. The choice should be made deliberately, with a regression test for the two-digit-width case (
Key = "01",other = "1").Acceptance Criteria
StartsWithmatching, corrected offset arithmetic underContains, or a documented prefix precondition.KaStringAsync.KeyEqualsbranch 1 no longer computes a substring offset that is meaningless for a non-prefix match.Key = "01"withother = "1") with a non-nullUpdate.QuickFiler.Test/Controllers/KbdActionsTests.cs:71-76either still passes unchanged, or its change is explicitly justified as the intended behavioural change.csharpier check ., the analyzer build, the nullable build, and the MSTest suite.References
quickfiler-keyboard-action-contract-defects), acceptance criterion AC19.docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/spec.md, "Scope & Non-Goals" and "Rollout & Follow-up".docs/features/active/2026-08-07-quickfiler-keyboard-action-contract-defects-445/research/keyboard-action-contract-defects.2026-08-21T18-20.md.