Skip to content

Fix KaStringAsync.KeyEquals branch-1 offset arithmetic for non-prefix Contains matches - #874

Merged
drmoisan merged 6 commits into
mainfrom
bug/kastringasync-keyequals-contains-offset-583
Sep 13, 2026
Merged

drmoisan merged 6 commits into
mainfrom
bug/kastringasync-keyequals-contains-offset-583

Conversation

@drmoisan

Copy link
Copy Markdown
Owner

Suggested title

Fix KaStringAsync.KeyEquals branch-1 offset arithmetic for non-prefix Contains matches

Summary

  • KaStringAsync.KeyEquals's Contains-guarded branch computed its Update callback argument
    from prefix-only arithmetic (Key.Substring(other.Length - 1, 1)), which only produces the
    correct matched character when other is a prefix of Key.
  • The offset is corrected to derive from the match position instead:
    Key.Substring(Key.IndexOf(other, StringComparison.Ordinal) + other.Length - 1, 1).
  • The Contains guard itself is unchanged, per the binding maintainer decision recorded in
    issue.md; no StartsWith call is introduced anywhere in the file.
  • A new regression test pins the previously-wrong non-prefix case (Key = "01", other = "1",
    now correctly yields "1" instead of "0"); one pre-existing test's explanatory string is
    reworded (its asserted value is unchanged) because it quoted the replaced literal expression.
  • The defect is latent today: every production KaStringAsync construction site passes a null
    Update callback, so the corrected branch has no observable effect on current behavior.
  • Full C# toolchain (CSharpier check, analyzer rebuild, nullable rebuild, coverage-instrumented
    MSTest run) passes in a single clean pass; changed-line coverage on the fix is 100%, with no
    regression on any previously-covered line.

Why

Branch 1 of KeyEquals guards on a substring test (Key.Contains(other)) but computed its
Update argument using an offset derived only from other's own length. That offset is only
correct when other happens to be a prefix of Key; for a substring match at any other
position, the wrong character is passed to Update. The maintainer decision recorded in
issue.md (2026-09-11) required keeping the Contains guard as-is and correcting only the
offset arithmetic, deriving it from Key.IndexOf(other) (the actual match position) instead.
The research record backing this plan confirms the two search modes (Contains, ordinal;
IndexOf, comparison-dependent) cannot disagree for this method's actual ASCII-digit input
domain, so an explicit StringComparison.Ordinal argument on the added IndexOf call was
added for textual consistency with the guard, not because any active gate requires it.

What Changed

Core fix

  • QuickFiler/Controllers/KaStringAsync.cs: corrected the branch-1 Update offset expression
    to derive from Key.IndexOf(other, StringComparison.Ordinal); reworded one doc-comment
    sentence in the "Argument contract" paragraph to describe the new derivation instead of the
    old prefix-only one.

Tests

  • QuickFiler.Test/Controllers/KaStringAsyncTests.cs: added
    KeyEquals_ContainsMatchAtNonPrefixIndex_InvokesUpdateWithLastMatchedCharacter, covering the
    two-digit-width non-prefix regression case; reworded the because-string of the pre-existing
    prefix-case test (asserted value "b" unchanged) so it no longer quotes the replaced literal
    expression.

Docs / feature folder / evidence

  • docs/features/active/kastringasync-keyequals-contains-offset-583/: issue.md, spec.md,
    the atomic plan, the research record, and 21 evidence artifacts under evidence/baseline/,
    evidence/regression-testing/, and evidence/qa-gates/ (baseline and post-change toolchain
    gate results, red-before-fix/green-after-fix regression-test captures, coverage delta, final
    QC attestation, and the acceptance-criteria status summary). No raw coverage or test-results
    file is included; all committed evidence is markdown projections per the repository's
    evidence-hygiene convention.

Architecture / How It Fits Together

KaStringAsync is a keyboard-action value object implementing IKbdAction<string, Func<string, Task>>. KeyEquals is invoked by KbdActions' keyboard-filtering methods
(ContainsKey, FilterKeys, Find, FindIndex, the indexer) to test a keystroke probe
against a registered key and, when a non-null Update callback is present, to feed it the
character it should surface. The change is confined to the internal arithmetic of one branch
of one method; the public signature, return contract, and every other branch (the single-char
non-match branch and the multi-character non-match branch) are unchanged.

Verification

Completed (see the linked evidence artifacts for full command/output detail):

  • CSharpier check: 0 files needing formatting, before and after the fix.
  • Analyzer rebuild (/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true): Build
    succeeded, 0 errors, 0 skip-CoreCompile, both baseline and post-change.
  • Nullable rebuild (/p:TreatWarningsAsErrors=true): Build succeeded, 0 errors, 0
    skip-CoreCompile, both baseline and post-change.
  • Regression test, red before the fix (exit code 1, expected): 1 test ran, Failed, observed
    "0" against expected "1".
  • Regression test, green after the fix: 13/13 KaStringAsyncTests Passed.
  • Full-assembly coverage-instrumented run: 1394/1394 Passed post-change (1393/1393 baseline);
    KaStringAsync.cs covered/total 65/65 post-change (60/60 baseline); changed-line coverage on
    the fix is 6/6 = 100%; no previously-covered line regressed.
  • Pinned keyboard-matching test (QuickFiler.Test/Controllers/KbdActionsTests.cs,
    FilterKeys_WhenDistinctStoredKeysCoexist_PreservesKeyboardMatchingSemantics): 4/4 Passed
    both before and after the fix, 0 delta; that file is untouched by this change.
  • Final-QC attestation: PASS — all seven QA-gate commands (CSharpier format/check, analyzer
    rebuild, nullable rebuild, coverage capture, KbdActions run, coverage-delta computation)
    recorded exit code 0 (CSharpier format's measured rewrite count is 0), and the anchored
    diff/status check confirms every changed path falls within this change's declared write set.
  • Acceptance Criteria: 6 of 6 checked off in spec.md (AC1-AC6); see
    evidence/qa-gates/ac-status-summary.md.

Recommended: none beyond the completed toolchain run above; no manual verification step
applies, because the defect has no observable effect in current production behavior (every
production construction path leaves Update null).

Backward Compatibility / Migration Notes

None. KeyEquals's public signature, return contract, and exception contract are all
unchanged. The corrected Update argument value differs from today's value only for match
positions that no current production construction path exercises (Update is always null in
production today), so there is no observable behavior change for any existing caller.

Risks and Mitigations

  • Risk: a future change wires a non-null Update callback into a production
    KaStringAsync construction site, newly exercising this corrected branch.
    Mitigation: the corrected behavior is now covered by a regression test and the doc
    comment describes the new derivation; spec.md's Rollout & Follow-up section flags this as
    the one condition under which the corrected path should be re-verified.
  • Rollback: a plain source revert of the two changed source files if a regression is ever
    discovered; the change is not behind a feature flag and requires no migration.

Review Guide

Suggested order:

  1. QuickFiler/Controllers/KaStringAsync.cs — the one-line arithmetic fix and its doc-comment
    reword.
  2. QuickFiler.Test/Controllers/KaStringAsyncTests.cs — the new regression test and the
    because-string reword.
  3. docs/features/active/kastringasync-keyequals-contains-offset-583/spec.md — acceptance
    criteria and design rationale.
  4. The evidence/ subtree — toolchain gate and coverage-delta artifacts, useful for confirming
    the verification claims above without re-running the toolchain locally.

The 24 markdown files are feature-folder documentation and evidence; none of them requires the
same scrutiny as the two source files.

Follow-ups

  • None identified in scope. If a future change gives a production KaStringAsync construction
    site a non-null Update callback, that change should re-verify this corrected offset
    behavior against the newly reachable code path (see spec.md's Rollout & Follow-up section).

GitHub Auto-close

drmoisan and others added 6 commits September 12, 2026 11:54
…rage figure

Per the projection-only evidence decision recorded on issue 671 on 2026-09-11, no new raw Cobertura XML and no new TRX file may be added to git. The approved plan wrote two raw coverage documents as committed evidence: coverage-baseline.cobertura.xml under evidence/baseline and coverage-postchange.cobertura.xml under evidence/qa-gates. Both are removed.

P0-T8 and P5-T5 still run dotnet-coverage, but the Cobertura output now lands in a session folder beneath the per-user temporary directory, outside the repository, and is read then deleted rather than committed. Each task records a prefix comparison proving the location is outside the repository root, a post-deletion existence check, and a directory listing free of any xml entry.

Every numeric figure is preserved. coverage-baseline.md and coverage-postchange.md now carry the root line-rate, the root branch-rate, the per-file covered and total counts, and a new per-line hits projection. P5-T7 computes the delta from those two markdown artifacts alone, naming no raw document, and still reports baseline, post-change and changed-line percentages.

P5-T7 also gained a stated denominator and a non-empty-denominator floor, so the changed-line percentage is mechanically determinate and its quantifier can fail. P5-T15 and the git-diff-anchoring reference were corrected out of footprint, because the scope list omitted issue.md and the research record, which the anchored diff necessarily reports, leaving the terminal gate ordering an unbounded phase restart.

Preflight cleared at round 13. Task count and numbering unchanged at 30.
…ence under coordinator quota hold

Committed by the parallel-orchestrator coordinator under a full hold: the active account reaches its 5-hour cap within minutes and the fallback account is also exhausted until 06:00Z, so every running child would otherwise die mid-task with this work uncommitted and unrecoverable. Working state, not a completion claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…redact host paths

Final QC loop passed in a single clean pass (csharpier check, analyzer rebuild,
nullable rebuild, coverage-instrumented MSTest run); coverage delta and
final-QC attestation confirm no regression and 100% changed-line coverage on
the KaStringAsync.cs fix. All six spec.md acceptance criteria are checked off.
Also redacts two absolute host paths left in earlier baseline evidence
(coverage-tool-probe.md, dotnet-bootstrap.md) per repository evidence-hygiene
convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@drmoisan
drmoisan merged commit 39ce289 into main Sep 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant