Skip to content

[PM-41293] feat: Add identity field-key mapping to fill-assist - #7342

Open
aj-rosado wants to merge 7 commits into
mainfrom
PM-41293/identity-autofill-fill-assist-field-mapping
Open

[PM-41293] feat: Add identity field-key mapping to fill-assist#7342
aj-rosado wants to merge 7 commits into
mainfrom
PM-41293/identity-autofill-fill-assist-field-mapping

Conversation

@aj-rosado

@aj-rosado aj-rosado commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

PM-41293

📔 Objective

Adds identity field-key mapping to the fill-assist targeting path, extending FillAssistViewNodeExtensions to resolve the 16 identity AutofillView subtypes (name, address, phone, company, SSN, passport, license) from host-rule field keys, alongside the existing Login/Card mapping.

All identity classification produced by fill-assist — the plain identity field-key mapping and the email/phone dual-classification (offering an email- or phone-matched field as both a Login and an Identity candidate) — is gated behind FlagKey.IdentityAutofill, mirroring the heuristic detection path's gating. This keeps pre-existing behavior unchanged whenever the flag is off, even if a host's fill-assist rules declare identity field keys. The dual-classification also now considers the full set of matched field keys for a node rather than only the one that wins the Login.Username/Login.Email tie-break, so a field matched under both "email" and "phone" is classified under both Identity types.

Also retires "autocomplete" from the heuristic attribute-hint list in HtmlInfoExtensions.kt — the platform doesn't actually return this attribute to the app, so it was dead weight in the hint list. Unrelated to fill-assist, but intentionally included in this PR.

📸 Screenshots

N/A — no UI changes.

@aj-rosado aj-rosado added the ai-review Request a Claude code review label Sep 1, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the identity field-key mapping added to the fill-assist path in FillAssistViewNodeExtensions.kt, the FlagKey.IdentityAutofill gating threaded through AutofillParserImpl.parseInternal/toEffectiveViews/buildFillAssistViews, the dual-classification of email/phone matches off the full matched-key set, and the removal of "autocomplete" from the heuristic attribute-hint list. Traced the flag-off path: identity keys map to null, candidateViews contains no Identity entries, and the new preference chain collapses to the previous Username-then-first behavior, so pre-existing Login/Card classification is unchanged. Traced the flag-on path: Identity views are excluded from focused-view resolution in AutofillParserImpl.kt:186-189 and dropped by filterIsInstance during partition construction, so they cannot displace a Login/Card fill or force Unfillable. Both previously reported findings are addressed at head — the focused-view filter (commit e75aceee8) and the Login/Card-over-Identity tie-break in FillAssistViewNodeExtensions.kt:84-90 (commit d1c1bb4d4) — and unit tests lock in both, including all 16 identity field keys and the flag-off exclusions.

Code Review Details

No new findings. Prior open finding on the FillAssistViewNodeExtensions.kt tie-break is resolved by the Login.Username → non-Identity → first-candidate preference chain and its accompanying test.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.02439% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.72%. Comparing base (4ecccfd) to head (d1c1bb4).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...twarden/data/autofill/parser/AutofillParserImpl.kt 38.46% 8 Missing ⚠️
...data/autofill/util/FillAssistViewNodeExtensions.kt 98.55% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7342      +/-   ##
==========================================
- Coverage   86.16%   85.72%   -0.45%     
==========================================
  Files         928      946      +18     
  Lines       67315    67852     +537     
  Branches    10156    10199      +43     
==========================================
+ Hits        58002    58166     +164     
- Misses       5735     6106     +371     
- Partials     3578     3580       +2     
Flag Coverage Δ
app-data 17.90% <89.02%> (-0.13%) ⬇️
app-ui-auth-tools 18.66% <0.00%> (-0.16%) ⬇️
app-ui-platform 16.61% <0.00%> (+0.42%) ⬆️
app-ui-vault 27.75% <0.00%> (+0.06%) ⬆️
authenticator 6.02% <0.00%> (-0.04%) ⬇️
lib-core-network-bridge 4.05% <0.00%> (-0.03%) ⬇️
lib-data-ui 1.19% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

val fillAssistViews = assistStructure.buildFillAssistViews(
hostRules = hostRules,
urlBarWebsite = urlBarWebsite,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: With the flag on, a focused Identity view from fill-assist can force the whole request to Unfillable, unlike the heuristic path.

Details and fix

toEffectiveViews returns fillAssistViews unfiltered, and buildFillAssistViews is given all of the host's rules (including identity-category ones), so an identity-keyed node is now classified even when coversCurrentPartition matched on a login/card rule. parseInternal then does:

val effectiveFocusedView = effectiveViews.firstFocusedOrNull() ?: return AutofillRequest.Unfillable
val partition = when (effectiveFocusedView) {
    ...
    is AutofillView.Identity -> return AutofillRequest.Unfillable

Traceable path (flag on, fill-assist on, host has account-login rules plus identity field keys):

  1. User focuses a "Full name" field. Heuristics classify it Identity (or Unused), so it is not in autofillViews; focusedView falls back to autofillViews.firstOrNull() — an unfocused Login.Username.
  2. coversCurrentPartition is true (Login rule present), so fill-assist runs.
  3. fillAssistViews now contains Identity.PersonNameFull(isFocused = true) alongside Login.Username/Login.Password.
  4. firstFocusedOrNull() returns the Identity view → Unfillable.

Before this change the identity key mapped to null, the node was omitted, and firstFocusedOrNull() fell back to the first Login view → a fillable Login partition. So turning the flag on removes login suggestions on these hosts.

This is the exact case the heuristic path guards against at line 141-144 (it !is AutofillView.Unused && it !is AutofillView.Identity), covered by parse should choose AutofillPartition Login when an Identity view is focused but a Login view is fillable elsewhere. The fill-assist path has no equivalent.

Mirroring that filter until Phase D would keep the two paths aligned, e.g. in parseInternal:

val effectiveFocusedView = effectiveViews
    .filterNot { it is AutofillView.Identity }
    .firstFocusedOrNull()
    ?: return AutofillRequest.Unfillable

A parser-level test with a focused identity-keyed fill-assist node would lock this in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude fixed

@claude claude Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @aj-rosado's task in 1m 18s —— View job


Fix verified ✅

  • Read the new commit since my review (e75acee "Filter out autofill identity")
  • Check the focused-Identity → Unfillable path is closed
  • Check test coverage locks the behavior in

The fix is correct and complete. AutofillParserImpl.kt:186-189 now filters Identity out before resolving the effective focused view:

val effectiveFocusedView = effectiveViews
    .filterNot { it is AutofillView.Identity }
    .firstFocusedOrNull()
    ?: return AutofillRequest.Unfillable

Tracing the path I reported: with the flag on, the focused identity-keyed node is now skipped, firstFocusedOrNull()'s ?: firstOrNull() fallback (AutofillParserImpl.kt:337-338) lands on the fill-assist Login.Username, and the request stays a fillable Login partition. The two paths are aligned — this mirrors the heuristic filter at AutofillParserImpl.kt:141-144.

No regressions I can see from the filter itself:

  • Fill-assist off: autofillViews already excludes Identity, so filterNot is a no-op.
  • Only identity views present: filtered list is empty → Unfillable, same as before.

The new test parse should choose AutofillPartition Login when fill-assist resolves the focused view to Identity but a Login view is fillable elsewhere (AutofillParserTests.kt:1789+) exercises exactly the scenario — focused Unused node reclassified to Identity.PersonNameFull by a rule that also carries a username key — and asserts the Login partition rather than Unfillable. It would fail without the filterNot. Fixtures all check out against the existing test setup.


One nit (non-blocking)

AutofillParserImpl.kt:205-208 — the is AutofillView.Identity -> return AutofillRequest.Unfillable branch is now unreachable, since Identity can no longer reach effectiveFocusedView. The branch still has to exist for when exhaustiveness, but the comment now reads as if it's a live path. Consider matching the tone of the adjacent Unused branch:

is AutofillView.Identity -> {
    // Unreachable: Identity views are filtered out of the focused-view lookup above.
    // Identity partition construction lands in Phase D.
    return AutofillRequest.Unfillable
}

Fix this →


⚠️ I was not able to run ./gradlew :app:testStandardDebugUnitTest in this job — the Gradle command was blocked by the tool permission gate — so my verification is by code reading only. CI test results are authoritative.
PM-41293/identity-autofill-fill-assist-field-mapping

@aj-rosado
aj-rosado marked this pull request as ready for review September 2, 2026 14:25
@aj-rosado
aj-rosado requested review from a team and david-livefront as code owners September 2, 2026 14:25
Comment on lines +82 to +86
val view = candidateViews
.firstOrNull { (_, view) -> view is AutofillView.Login.Username }
?.second
?: candidateViews.firstOrNull()?.second
?: return@let null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The tie-break can now let an Identity view outrank a Login/Card view for the same node, dropping it from the partition when the flag is on.

Details and fix

candidateViews holds one entry per matched field key, in hostRules.flatMap { it.fields.entries } order. The fallback is candidateViews.firstOrNull(), which has no notion of Identity being the weakest classification.

Traceable path (flag on):

  1. A node matches both an identity key and a non-username/phone login or card key — e.g. personNameFull and email — because two of the host's clauses select it (a loose clause such as input[type=text] is enough).
  2. No Login.Username candidate exists, so candidateViews.firstOrNull() wins.
  3. If the identity key is declared first, view is Identity.PersonNameFull.
  4. parseInternal builds the partition with effectiveViews.filterIsInstance<AutofillView.Login>(), so that node is silently absent — the email field gets no suggestion.

With the flag off, toIdentityViewForFieldKey returns null, the identity key is dropped from candidateViews, and the same node classifies as Login.Email. So flipping the flag removes a login/card field from the partition, the same class of flag-on regression as the focused-Identity case fixed in AutofillParserImpl.

Login.Username is already preferred for a reason; extending that ordering to deprioritize Identity keeps Login/Card authoritative while Identity partitions are unbuilt:

val view = candidateViews
    .firstOrNull { (_, view) -> view is AutofillView.Login.Username }
    ?.second
    ?: candidateViews.firstOrNull { (_, view) -> view !is AutofillView.Identity }?.second
    ?: candidateViews.firstOrNull()?.second
    ?: return@let null

A test with a node matched under both an identity key (listed first) and email would lock this in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude fixed

@claude claude Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @aj-rosado's task in 1m 6s —— View job


Fix verified ✅

  • Read commit d1c1bb4 "Prioritize login/card classifications over identity"
  • Verify the Identity-outranks-Login/Card path is closed
  • Check test coverage locks it in

The tie-break in FillAssistViewNodeExtensions.kt:84-90 now demotes Identity below every other classification:

val view = candidateViews
    .firstOrNull { (_, view) -> view is AutofillView.Login.Username }
    ?.second
    ?: candidateViews.firstOrNull { (_, view) -> view !is AutofillView.Identity }
        ?.second
    ?: candidateViews.firstOrNull()?.second
    ?: return@let null

Tracing the path I reported — node matched under personNameFull (declared first) and email: no Login.Username candidate, so the new middle clause picks Login.Email instead of Identity.PersonNameFull. isLoginIdentifierView is then true, matchedKeys contains email, so Identity.Email is appended — the node lands in the Login partition and keeps its identity candidate. That's the behavior the new test asserts.

No regressions I can find:

  • Flag offtoIdentityViewForFieldKey is skipped, so candidateViews holds no Identity entries and the new filterNot-style clause is a no-op.
  • Only identity keys match — both preferential clauses miss, candidateViews.firstOrNull() still returns the Identity view, so plain identity-only nodes classify exactly as before.
  • Relative order among non-Identity candidatesfirstOrNull on the filtered view preserves declaration order, so existing Login/Card tie-breaks are untouched.
  • Card keys — a node matched under an identity key plus cardNumber now resolves to Card.Number, which is the intended direction.

FillAssistViewNodeExtensionsTest.kt:458+ (buildFillAssistViews should prefer Login Email over Identity PersonNameFull when identity key is listed first and matches the same node) uses linkedMapOf so the identity key genuinely comes first, and createHtmlInfo() matches any clause so both keys hit the same node. It would fail on the pre-fix code (candidateViews.firstOrNull()Identity.PersonNameFull). Fixture helpers all line up with the existing suite.


One nit (non-blocking, Phase D)

An identity classification can now be silently dropped when the same node also matches a login/card key that isn't in the dual-classify set. E.g. a node matched under phoneFull + email resolves to Login.Email + Identity.Email, and the Identity.PhoneFull candidate disappears — the dual-classify block only checks the "email" and "phone" keys (FillAssistViewNodeExtensions.kt:100-105), not "phoneFull". No user-visible effect today since identity partitions aren't built, but worth revisiting when Phase D starts consuming these views.


⚠️ I could not run ./gradlew :app:testStandardDebugUnitTest in this job — the Gradle command was blocked by the tool permission gate — so this verification is by code reading only. CI results are authoritative.
PM-41293/identity-autofill-fill-assist-field-mapping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant