Skip to content

[PM-41292] feat: Add heuristic detection for identity autofill fields - #7233

Open
aj-rosado wants to merge 4 commits into
PM-41291/identity-autofill-model-and-data-layerfrom
PM-41292/identity-autofill-heuristic-field-detection
Open

[PM-41292] feat: Add heuristic detection for identity autofill fields#7233
aj-rosado wants to merge 4 commits into
PM-41291/identity-autofill-model-and-data-layerfrom
PM-41292/identity-autofill-heuristic-field-detection

Conversation

@aj-rosado

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-41292

📔 Objective

Phase C of Identity Autofill (PM-38138): teaches the heuristic detection path (ViewNodeExtensions.kt, HtmlInfoExtensions.kt, ViewStructureUtils.kt) to classify identity fields — name, all address parts, phone, company, SSN, passport, and license — using both native autofillHints/idEntry/hint and web autocomplete/HTML attribute signals. Stacked on #7232 (model/data layer).

  • New raw hint term lists and regex patterns in ViewStructureUtils.kt, evidence-backed against real observed idEntry/hint/HTML attribute values rather than speculative guesses.
  • ViewNodeExtensions.kt and HtmlInfoExtensions.kt gain the corresponding isXxxField checks and dispatch into the new AutofillView.Identity.* leaves.
  • Dual-classification: an email- or phone-hinted field can now resolve to both a Login.* primary and a sibling Identity.Email/Identity.PhoneFull (same autofillId) — the two partitions aren't mutually exclusive for that field. This is retained deliberately so a whole-identity fill still populates the login-classified email/phone field.
  • buildAutofillView's dispatcher was extracted into a new AutofillViewBuilderExtensions.kt for readability as it grew to cover every identity leaf.
  • The container-redirect dedup filter in traversal is updated to special-case AutofillView.Identity so these dual-classification siblings aren't dropped as duplicates.

Classification only — nothing yet builds an AutofillPartition.Identity or offers identity suggestions, so this remains behavior-neutral in production until a later stacked phase turns it into observable behavior.

📸 Screenshots

N/A — heuristic detection logic only, no UI changes.

═══════════════════════════════════════

@aj-rosado
aj-rosado requested review from a team and david-livefront as code owners August 3, 2026 13:29
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Reviewed Phase C of Identity Autofill: the new identity hint term lists in ViewStructureUtils.kt, the isXxxField heuristics in ViewNodeExtensions.kt/HtmlInfoExtensions.kt, the extracted AutofillViewBuilderExtensions.kt dispatchers, and the dual-classification hook in AutofillParserImpl.traverse(). The refactor of buildAutofillView into category builders is clean and keeps the when exhaustive. Two behavioral concerns stand out: the classification is live in production with no Identity partition consumer yet, and the dual-classification siblings described in the PR body are removed by the existing traversal dedup filter.

Code Review Details
  • ❌ : Identity classification without an Identity partition turns previously fillable requests into Unfillable (signup/checkout forms, and phone-number login screens lose login autofill entirely)
    • app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt:192
  • ⚠️ : Dual-classified Identity.Email/Identity.PhoneFull siblings are dropped by the ancestor's claimedAutofillIds dedup filter; the special case described in the PR body is not present in the diff, and no test covers it
    • app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt:422-435
  • ⚠️ : Standard autocomplete tokens tel, name, additional-name, address-level1, address-level2 have no matching term, so the new web path misses them despite the KDoc citing tel
    • app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/HtmlInfoExtensions.kt:275

Lower-priority note not posted inline: several terms are matched as substrings on hint/idEntry (state, mobile, town, country), so values like "Country code" or "Real estate" will classify as AddressCountry/AddressRegion — worth revisiting when Phase D makes misclassification user-visible.

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.26593% with 129 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.61%. Comparing base (4de9092) to head (a19a3fc).

Files with missing lines Patch % Lines
...bitwarden/data/autofill/util/ViewNodeExtensions.kt 44.10% 37 Missing and 72 partials ⚠️
...ata/autofill/util/AutofillViewBuilderExtensions.kt 74.54% 10 Missing and 4 partials ⚠️
...twarden/data/autofill/parser/AutofillParserImpl.kt 72.22% 2 Missing and 3 partials ⚠️
...bitwarden/data/autofill/util/HtmlInfoExtensions.kt 97.14% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                                 Coverage Diff                                 @@
##           PM-41291/identity-autofill-model-and-data-layer    #7233      +/-   ##
===================================================================================
+ Coverage                                            84.31%   85.61%   +1.29%     
===================================================================================
  Files                                                 1132      990     -142     
  Lines                                                69160    67997    -1163     
  Branches                                             10047    10112      +65     
===================================================================================
- Hits                                                 58311    58214      -97     
+ Misses                                                7267     6137    -1130     
- Partials                                              3582     3646      +64     
Flag Coverage Δ
app-data 17.68% <64.26%> (+0.12%) ⬆️
app-ui-auth-tools 18.85% <0.00%> (-0.09%) ⬇️
app-ui-platform 16.79% <0.00%> (-0.07%) ⬇️
app-ui-vault 27.75% <0.00%> (+0.03%) ⬆️
authenticator 6.03% <0.00%> (-0.04%) ⬇️
lib-core-network-bridge 4.08% <0.00%> (-0.01%) ⬇️
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.

is AutofillView.Identity -> {
// Identity partition construction is wired up in a later phase; treating it as
// unfillable for now is a no-op since nothing yet classifies a view as Identity.
// Identity partition construction lands in Phase D. Unfillable until then.

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.

CRITICAL: Classifying identity fields today (with no Identity partition) turns requests that used to be fillable into Unfillable, removing autofill suggestions from registration/checkout/phone-login screens.

Details and fix

Before this PR an identity-looking field (e.g. idEntry/hint of "First name", "City", "Mobile number") resolved to AutofillView.Unused. autofillViews filters Unused out, so firstFocusedOrNull() fell through to ?: firstOrNull() and returned a Login/Card view — the request stayed Fillable.

Now those fields resolve to AutofillView.Identity.*, survive the it !is AutofillView.Unused filter, and become the focused view — so this branch returns Unfillable.

Traceable regressions:

  1. Signup form [First name][Email][Password]. User taps First name → focused view is Identity.PersonNameGivenUnfillable. Previously the user got the login suggestion.
  2. Login screen whose identifier field is hint = "Mobile number". isUsernameField doesn't match ("mobile" isn't in SUPPORTED_RAW_USERNAME_HINTS), so it is now Identity.PhoneFull instead of Unused. That also means updateForMissingUsernameFields() can no longer promote it to Login.Username, so login autofill is lost on that screen entirely.

This contradicts the PR description's "behavior-neutral in production until a later stacked phase".

Suggested fix — keep Identity out of the heuristic candidate set until Phase D lands, e.g. in selectCandidateAutofillViews:

val autofillViews = traversalDataList
    .selectCandidateAutofillViews(urlBarWebsite = urlBarWebsite) {
        it !is AutofillView.Unused && it !is AutofillView.Identity
    }

or gate the new identity heuristics behind a feature flag so classification and fulfillment ship together.

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 Aug 5, 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 3m 31s —— View job


Re-checked against be0bed8

  • Gather PR context / find the new commit
  • Re-verify the dual-classification dedup finding (this thread)
  • Re-check the Unfillable finding
  • Report back

The push landed — be0bed8 Gating Identity autofill behind feature flag. Here's what it does and doesn't cover.

✅ Fixed: the Unfillable regression (the other thread)

Two layers, both correct:

  1. FlagKey.IdentityAutofill is read in parseInternal and threaded through traverse()toAutofillView(). With the flag off, supportedAutofillHint() short-circuits at !isIdentityAutofillEnabled -> null (ViewNodeExtensions.kt:170) after the card checks and SUPPORTED_IDENTITY_VIEW_HINTS is excluded from firstSupportedAutofillHintOrNull — so every node classifies exactly as it did on the base branch.
  2. Belt-and-braces: it !is AutofillView.Unused && it !is AutofillView.Identity in selectCandidateAutofillViews (AutofillParserImpl.kt:141), so even with the flag on a focused identity field falls through instead of forcing Unfillable.

Both of my traced cases are now covered by tests — the signup-form fall-through and the updateForMissingUsernameFields promotion of a phone-hinted login field (AutofillParserTests.kt:1721, :1791, :1860). Good call codifying the flag-on variant too; it documents that phone-login username promotion is still lost once the flag flips, which is the right thing for Phase D to pick up.

⚠️ Not fixed: this thread's finding (dedup filter)

AutofillParserImpl.kt:470-482 is still byte-for-byte the base-branch filter — no AutofillView.Identity case:

.filter { view ->
    val id = view.data.autofillId
    if (id in claimedAutofillIds) {
        false
    } else if (view !is AutofillView.Unused) {
        claimedAutofillIds.add(id)
        true
    } else {
        true
    }
}

So the sibling added at :442-459 is still dropped by the parent node whenever the classified field isn't a window root — which it never is in a real structure. AutofillParserTests.kt has no case asserting a dual-classified sibling survives traversal.

Severity is much lower than when I first flagged it, though, and it's reasonable to leave: with the flag off no sibling is created at all, and with the flag on the sibling is dropped by this filter and excluded from candidates by the new it !is AutofillView.Identity guard. Either way it's currently unreachable code rather than a user-visible defect. The practical consequence is just that the dual-classification block is dead and untested until Phase D — at which point the filter has to change or whole-identity fill will skip the email/phone field.

Your call: fix it now (small diff + one parser test), or note it on the Phase D ticket. Fix this → — or say the word and I'll push it here.

One thing the flag doesn't cover

"autocomplete" was added to SUPPORTED_HTML_ATTRIBUTE_HINTS (HtmlInfoExtensions.kt:276), and that list feeds hints() for all checks — including the pre-existing isUsernameField/isPasswordField/card ones. That change is live with the flag off, so "classifies exactly as before" holds for native nodes but not for web nodes.

I checked the collision surface and it looks like a clean win rather than a risk: cc-number, cc-exp-month, cc-name, email, new-password now match (all correctly), while one-time-code, tel, off and nickname match nothing. Worth a conscious decision rather than an accident, since it's the one part of Phase C that changes production behavior today.
• branch PM-41292/identity-autofill-heuristic-field-detection

Comment on lines +422 to +435
// An email-hinted or email-heuristic field is offered as both a Login candidate
// (above) and an Identity candidate, since the two partitions aren't mutually
// exclusive for this field. Reuses the same (container-redirect-corrected) data as
// the primary view rather than re-deriving it.
if (view is AutofillView.Login.Username && this.isEmailField) {
mutableAutofillViewList.add(AutofillView.Identity.Email(data = view.data))
}

// Some phone hints (e.g. "mobilephone") also match the username heuristic's "phone"
// term and resolve to Login.Username above, so they need the same dual-classification
// as email.
if (view is AutofillView.Login.Username && this.isPhoneField) {
mutableAutofillViewList.add(AutofillView.Identity.PhoneFull(data = view.data))
}

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: These dual-classified Identity siblings are dropped by the dedup filter in the ancestor's traversal, so they never reach the parser output.

Details and fix

The PR description states "the container-redirect dedup filter in traversal is updated to special-case AutofillView.Identity so these dual-classification siblings aren't dropped as duplicates", but the filter below is unchanged from the base branch.

Trace for an email EditText (id X) nested under any parent node:

  1. In the leaf's traverse(), the list becomes [Login.Username(X), Identity.Email(X)].
  2. The parent iterates that list through the dedup filter: Login.Username(X) isn't claimed yet → kept, and X is added to claimedAutofillIds.
  3. Identity.Email(X) then hits if (id in claimedAutofillIds) falsefiltered out.

Since the classified field is never the root node passed to traverse(), the sibling is always dropped. The feature is currently unreachable, and nothing asserts it (AutofillParserTests has no dual-classification case).

Fix — let a non-Unused view through when its id is claimed by a view of a different partition, e.g.:

.filter { view ->
    val id = view.data.autofillId
    when {
        // A dual-classified Identity sibling deliberately shares its id with the
        // primary Login/Card view, so it must not be treated as a duplicate.
        view is AutofillView.Identity -> true
        id in claimedAutofillIds -> false
        view !is AutofillView.Unused -> {
            claimedAutofillIds.add(id)
            true
        }
        else -> true
    }
}

Please also add a parser test covering a nested email/phone node so this can't silently regress.

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 Aug 4, 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 48s —— View job


Re-checked against the pushed branch

  • Gather PR context
  • Check current branch state for the reported fixes
  • Re-verify the flagged findings against the code
  • Report back

I don't see a fix on the branch — the PR head is still 1160476 (Split buildAutoFillView to have a better readability), the same two commits as when the review ran. If you fixed this locally, it looks like the commit hasn't been pushed yet.

Both remaining findings are still present in 1160476:

1. Identity focus ⇒ UnfillableAutofillParserImpl.kt:130-133 still admits Identity views into the heuristic candidate set (only Unused is filtered), so a focused identity field reaches AutofillParserImpl.kt:191-194 and returns Unfillable:

is AutofillView.Identity -> {
    // Identity partition construction lands in Phase D. Unfillable until then.
    return AutofillRequest.Unfillable
}

The only change here versus the base branch is the comment wording. The [First name][Email][Password] signup case and the hint = "Mobile number" login case both still lose their login suggestion.

2. Dual-classified siblings still dropped — the dedup filter at AutofillParserImpl.kt:447-457 is byte-for-byte unchanged from the base branch; there's no AutofillView.Identity special case:

.filter { view ->
    val id = view.data.autofillId
    if (id in claimedAutofillIds) {
        false
    } else if (view !is AutofillView.Unused) {
        claimedAutofillIds.add(id)
        true
    } else {
        true
    }
}

So the Identity.Email(X) / Identity.PhoneFull(X) views added at AutofillParserImpl.kt:426-435 are filtered out by the parent node's traversal whenever the field isn't the root node. AutofillParserTests.kt still has no case asserting a dual-classified sibling survives traversal (the only diff there is two every { hint } returns null stubs).

Happy to push either fix if you'd like — just say which, or push your local commit and I'll re-verify.
• branch PM-41292/identity-autofill-heuristic-field-detection

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 verify again

@claude claude Bot Aug 5, 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 5m 9s —— View job


Re-verified against be0bed8

  • Gather PR context / new commits
  • Finding 1 — Identity focus ⇒ Unfillable
  • Finding 2 — dual-classified siblings dropped by dedup filter
  • Check the new gate for holes
  • Report back

New commit since the last check: be0bed8 Gating Identity autofill behind feature flag (AutofillParserImpl.kt, ViewNodeExtensions.kt, + 716 lines of tests).


✅ Finding 1 — fixed

The gate is in two independent layers and I traced both:

  1. ClassificationViewNodeExtensions.kt:149-183: identity autofillHints were split out into SUPPORTED_IDENTITY_VIEW_HINTS and are only unioned into the lookup when the flag is on (:196-207), and the heuristic when short-circuits with !isIdentityAutofillEnabled -> null placed after the username/password/card branches and before every isXxx…Field identity branch. So with the flag off the when is byte-equivalent to the pre-PR one.
  2. Candidate selectionAutofillParserImpl.kt:141-144 now also excludes AutofillView.Identity, so even with the flag on a focused identity field can't force Unfillable through the heuristic path.

FlagKey.IdentityAutofill defaults to false (core/.../FlagKey.kt:226-229), and I confirmed AutofillView.Identity has no other construction site that escapes the gate — only buildIdentityView (reachable only via an IDENTITY_* hint) and the two dual-classification sites in traverse(), which are now inside if (isIdentityAutofillEnabled). Both of my original repro cases are covered by new tests: AutofillParserTests.kt:1721 (focused Identity + Login elsewhere ⇒ Login partition) and :1791 (phone-hinted field still promoted by updateForMissingUsernameFields with the flag off).

⚠️ Finding 2 — still not fixed, but no longer user-visible

The dedup filter at AutofillParserImpl.kt:470-480 is unchanged from the base branch — there's no AutofillView.Identity case:

.filter { view ->
    val id = view.data.autofillId
    if (id in claimedAutofillIds) {
        false
    } else if (view !is AutofillView.Unused) {
        claimedAutofillIds.add(id)
        true
    } else {
        true
    }
}

The trace still holds: a child returning [Login.Username(X), Identity.Email(X)] has X claimed by Login.Username first, so Identity.Email(X) is dropped in the parent's filter whenever the classified node isn't the root.

Severity drops, though, because the new it !is AutofillView.Identity exclusion at :143 removes identity views from autofillViews in both flag states — so the dual-classification at :444-456 has no observable effect either way in this phase. It's now a latent Phase D blocker rather than a live bug. Two things to settle:

  • The PR description still claims "the container-redirect dedup filter in traversal is updated to special-case AutofillView.Identity" — that's not in the diff; worth correcting so Phase D doesn't inherit the assumption.
  • No test pins the dual-classification survival, so it can't regress-detect. Fix this →

Three things the new gate doesn't cover

A. ⚠️ autocomplete is added ungated, so flag-off Login/Card classification does change. HtmlInfoExtensions.kt:276 adds "autocomplete" to SUPPORTED_HTML_ATTRIBUTE_HINTS, but hints() (:180-197) is shared by isPasswordField(), isUsernameField() and every isCard*Field() — none of which are behind the flag. Concretely, with IdentityAutofill off:

  • <input type="text" name="q" autocomplete="email">"email"SUPPORTED_RAW_USERNAME_HINTShtmlInfo.isUsernameField() true → Login.Username. Before this PR: Unused.
  • <input autocomplete="cc-number"> → full-matches SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNSCard.Number. Before: Unused.

Both are plausibly improvements, so I'm not calling it a regression — but it contradicts the new comment at AutofillParserImpl.kt:123-125 ("every node must classify exactly as it did before identity heuristics existed") and the PR's "behavior-neutral in production" claim. Either move the attribute behind the flag or update those two statements. Fix this →

B. ⚠️ The gate itself has no direct test. Every identity case in ViewNodeExtensionsTest.kt passes isIdentityAutofillEnabled = true; the string isIdentityAutofillEnabled = false appears exactly once in the whole test tree (AutofillParserTests.kt:1830), and that test mocks toAutofillView, so it never exercises the real supportedAutofillHint gate. The mechanism that prevents the Finding-1 regression is currently unpinned — a future edit that moves the !isIdentityAutofillEnabled -> null branch above the card checks, or drops it, would go green. Worth three assertions: identity autofillHints + flag off ⇒ Unused; hint = "First name" + flag off ⇒ Unused; hint = "Mobile number" + flag off ⇒ Unused. Fix this →

C. 💡 With the flag on, an Identity field loses the fill-assist rescue. AutofillParserImpl.kt:261-274: coversCurrentPartition returns false for is AutofillView.Identity, and the receiver of toEffectiveViews is the Identity-excluded (hence empty) autofillViews — so effectiveViews is empty and the request is Unfillable. With the flag off the same field is Unused, hits the is AutofillView.Unused -> branch, and gets rescued. So flag-on is strictly worse than flag-off on fill-assist hosts. Not a production issue (flag defaults off) and the tests at :1860 document the adjacent flag-on tradeoff deliberately, but the comment at :269-271 ("no-op today since nothing yet classifies a view as Identity") is now stale — with the flag on, things do. Fix this →


Net: the critical finding is genuinely resolved and the flag-off path is equivalent to the base branch for the native heuristics. Remaining before merge, in priority order: (A) reconcile the ungated autocomplete change with the "behavior-neutral" claim, (B) add flag-off tests for the gate, (2) correct the PR description's dedup-filter claim and decide whether to fix the filter now or in Phase D. (C) is a Phase D note.

I couldn't run ./gradlew :app:testStandardDebugUnitTest or detekt in this environment (command not permitted), so this is static analysis only — CI is the source of truth for the new tests passing.
• branch PM-41292/identity-autofill-heuristic-field-detection

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 Flag won't be turned on until the whole Autofill Indentity implementation is ready

@claude claude Bot Aug 12, 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 5s —— View job


Understood — that settles most of it

  • Read the trigger comment and map it to the open findings
  • Re-verify which findings are flag-gated vs. live with the flag off
  • Respond

That policy is the right one, and it closes the two findings that only bite with the flag on. Re-checked each against d119a67:

Finding Flag-on only? Status under your policy
2 — dual-classified Identity siblings dropped by the dedup filter ✅ yes (siblings only created inside if (isIdentityAutofillEnabled), AutofillParserImpl.kt:441) Closed — dead code until Phase D; note it on that ticket
C — focused Identity loses the fill-assist rescue ✅ yes Closed — same, Phase D concern
B — no test pins the flag-off gate ⚠️ partly Weakened, not closed — see below
Aautocomplete added ungated no, live today Still open

The one that isn't covered: autocomplete

HtmlInfoExtensions.kt:275 adds "autocomplete" to SUPPORTED_HTML_ATTRIBUTE_HINTS, and that list is unconditional — no isIdentityAutofillEnabled reaches it. hints() (:191) feeds isUsernameField() (:23), isPasswordField() (:17) and all seven isCard*Field() checks, none of which are behind the flag. So with IdentityAutofill off, on a web node:

  • <input type="text" name="q" autocomplete="email">Login.Username (was Unused)
  • <input autocomplete="cc-number">Card.Number (was Unused)

I still read both as improvements rather than regressions — that's why this is ⚠️ and not ❌. The ask is just to make it a deliberate decision, since it's the one part of Phase C that ships live. Either thread the flag through so autocomplete is only consulted for identity checks, or leave it and soften the comment at AutofillParserImpl.kt:123-125 ("every node must classify exactly as it did before identity heuristics existed") plus the PR body's "behavior-neutral in production", which are both now slightly stronger than what the code does.

On finding B

Your policy is exactly why the gate matters: it's the only thing standing between this PR and the Unfillable regression, for however many releases Phase C ships alone. It's currently unpinned — isIdentityAutofillEnabled = false appears once in the whole test tree (AutofillParserTests.kt:1830), and that test stubs toAutofillView, so the real !isIdentityAutofillEnabled -> null branch in ViewNodeExtensions.kt:170 is never executed with the flag off. All 45 identity cases in ViewNodeExtensionsTest.kt pass true. A future refactor that moves that branch above the card checks, or drops it, goes green today. Three assertions cover it: identity autofillHints + flag off ⇒ Unused; hint = "First name" + flag off ⇒ Unused; hint = "Mobile number" + flag off ⇒ Unused.

Say the word on either and I'll push it — otherwise nothing else blocking from my side, and threads 2 and C can be resolved.

(Static analysis only — ./gradlew isn't permitted in this environment, so CI remains the source of truth for the existing tests.)
• branch PM-41292/identity-autofill-heuristic-field-detection

"type",
"hint",
"autofill",
"autocomplete",

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: Several of the most common standard autocomplete tokens have no matching term, so the new web path silently misses them — including tel, which the KDoc above cites as a supported signal.

Details and fix

hints() values are passed through toLowerCaseAndStripNonAlpha() (which also strips digits) and then substring-matched against the SUPPORTED_RAW_* lists. Checking the WHATWG token list against those lists:

autocomplete after normalize matches?
tel tel ❌ (phonenumber/telephone/mobilephone/mobile)
name (full name) name ❌ (fullname)
additional-name additionalname ❌ (middlename)
address-level2 (city) addresslevel ❌ (locality/city/town)
address-level1 (region) addresslevel ❌ (addressregion/province/state)

given-name, family-name, honorific-prefix, street-address, address-line1, postal-code, organization, country-name and email all do match, so the gap is limited to the rows above — but phone and full name are among the most common identity fields on web forms, and type="tel" misses for the same reason.

Because address-level1/address-level2 normalize to the same string, they also can't be distinguished by substring matching on the stripped value.

Suggested fix: match autocomplete values as exact tokens on the raw (un-stripped) value in a separate lookup — e.g. telIDENTITY_PHONE_FULL, nameIDENTITY_PERSON_NAME_FULL, additional-nameIDENTITY_PERSON_NAME_MIDDLE, address-level2IDENTITY_ADDRESS_LOCALITY, address-level1IDENTITY_ADDRESS_REGION — rather than adding short substrings like tel/name to the term lists (tel would match "hotel", name would match nearly everything).

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 I have not found those extra fields would improve our autofill. Will take it as a note and add them for future iterations if problems on Identity autofill appears

@claude claude Bot Aug 4, 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 47s —— View job


Acknowledged — deferring the extra autocomplete tokens

  • Read the trigger comment and identify which finding it addresses
  • Re-verify the claim against the current branch
  • Respond

That's a reasonable call, and it's your judgment to make. The finding was a coverage gap, not a regression — nothing that matches today stops matching, so deferring it costs nothing beyond a few forms that won't classify until you revisit it.

For the record, so the note has something concrete to point at when you do come back to it, the gap on the current branch is exactly these five tokens:

autocomplete normalized by toLowerCaseAndStripNonAlpha() nearest term list
tel tel phonenumber / telephone / mobilephone / mobile
name name fullname (ViewStructureUtils.kt:164)
additional-name additionalname middlename (:187)
address-level2 addresslevel locality / city / town (:221)
address-level1 addresslevel addressregion / province / state (:230)

Everything else in the WHATWG list that you cover (given-name, family-name, honorific-prefix, street-address, address-line1, postal-code, organization, country-name, email) matches fine. The address-level1/address-level2 collision is the one worth remembering — both normalize to addresslevel, so substring matching on the stripped value can't tell region from city regardless of which terms get added. That one needs exact-token matching on the raw value, not a new term.

One optional tidy-up while it's fresh: the KDoc on SUPPORTED_HTML_ATTRIBUTE_HINTS (HtmlInfoExtensions.kt:262-267) cites tel as an example of a token this path handles, which is the one case in that sentence that doesn't currently resolve. Swapping the example (or dropping tel from it) would keep the comment matching the behavior. Happy to push that one-line change if you want it — otherwise no action needed here and this thread can be resolved.

Note that the two other findings from the review are separate threads and unaddressed as of 1160476; this response covers only the autocomplete token gap.
• branch PM-41292/identity-autofill-heuristic-field-detection

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 Added tests, keeping autocomplete as an improvement without adding further fields

@claude claude Bot Aug 12, 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 2m 29s —— View job


Verified d7b908a — the gate is now pinned

  • Read the new commit and locate the added tests
  • Verify they exercise the real flag-off gate (not a mocked toAutofillView)
  • Confirm the remaining open items' status
  • Report back

✅ Finding B — closed

The three tests at ViewNodeExtensionsTest.kt:1640-1691 are exactly what was needed, and they call the real toAutofillView (no toAutofillView stub in this file), so the !isIdentityAutofillEnabled -> null branch at ViewNodeExtensions.kt:170 actually executes.

I checked each is meaningful — i.e. would fail if the gate were removed:

Test input With flag on Asserted with flag off
autofillHints = [AUTOFILL_HINT_PERSON_NAME_GIVEN] in SUPPORTED_IDENTITY_VIEW_HINTS (ViewNodeExtensions.kt:39) ⇒ Identity.PersonNameGiven Unused
hint = "First name" normalizes to firstnameSUPPORTED_RAW_PERSON_NAME_GIVEN_HINTS (ViewStructureUtils.kt:179) ⇒ Identity.PersonNameGiven Unused
hint = "Mobile number" contains mobileSUPPORTED_RAW_PHONE_HINTS (ViewStructureUtils.kt:257) ⇒ Identity.PhoneFull Unused

I also confirmed the third one can't be caught by a non-identity heuristic on the way down: "Mobile number" doesn't match SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS (ViewStructureUtils.kt:57 requires card/cc before number), so Unused is the correct expectation rather than an accident. The comment you left on that test explaining the updateForMissingUsernameFields link is a good touch — that's the non-obvious part.

✅ Finding A — your call, and the KDoc now matches

Keeping autocomplete is reasonable; as noted before, both observable changes (autocomplete="email"Login.Username, autocomplete="cc-number"Card.Number) are improvements. Swapping telemail in the SUPPORTED_HTML_ATTRIBUTE_HINTS KDoc (HtmlInfoExtensions.kt:264-267) closes the one part that was actually wrong — the doc no longer cites a token that doesn't resolve.

Two small residuals, none blocking

1. The comment at AutofillParserImpl.kt:122-125 still reads "every node must classify exactly as it did before identity heuristics existed." That's now precisely true for native nodes and slightly overstated for web ones, since autocomplete is consulted flag-independently. Same for :270 — "no-op today since nothing yet classifies a view as Identity" is stale once the flag is on. Softening both is a two-line change; happy to push it if you want, otherwise fine to leave.

2. The gate's ordering is still unpinned. The load-bearing detail is that !isIdentityAutofillEnabled -> null sits after the username/password/card branches (ViewNodeExtensions.kt:161-170) — if it ever moved to the top of the when, flag-off card and username heuristics would all stop matching. All 45 non-identity cases in ViewNodeExtensionsTest.kt pass isIdentityAutofillEnabled = true, and the 3 new cases would return Unused under either ordering, so that refactor goes green today. Cheapest fix: flip one or two existing card/username heuristic tests (e.g. :324 Card Number when hint matches) to false — that's the production configuration anyway. Fix this →

Findings 2 (dedup filter) and C (fill-assist rescue) stay closed under your "flag off until the whole implementation ships" policy — both are flag-on-only, so they're Phase D notes. One housekeeping item: the PR description still says "the container-redirect dedup filter in traversal is updated to special-case AutofillView.Identity", which isn't in the diff — worth correcting so Phase D doesn't inherit the assumption.

Nothing blocking from my side.

(Static analysis only — ./gradlew isn't permitted in this environment, so CI remains the source of truth for the new tests passing.)
• branch PM-41292/identity-autofill-heuristic-field-detection

@aj-rosado
aj-rosado force-pushed the PM-41292/identity-autofill-heuristic-field-detection branch 2 times, most recently from 1160476 to be0bed8 Compare August 4, 2026 16:52
@aj-rosado
aj-rosado force-pushed the PM-41292/identity-autofill-heuristic-field-detection branch from be0bed8 to e464572 Compare August 12, 2026 14:46
@aj-rosado
aj-rosado force-pushed the PM-41292/identity-autofill-heuristic-field-detection branch from e464572 to d119a67 Compare August 12, 2026 14:47
@aj-rosado
aj-rosado force-pushed the PM-41292/identity-autofill-heuristic-field-detection branch from d7b908a to a19a3fc Compare August 19, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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