Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Feat: form-prefill-annotate rule for preselected form controls (#121)#187

Merged
twschiller merged 2 commits into
mainfrom
form-prefill-annotate
Jun 6, 2026
Merged

Feat: form-prefill-annotate rule for preselected form controls (#121)#187
twschiller merged 2 commits into
mainfrom
form-prefill-annotate

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

  • Splits issue #121 into an annotate-first rule (this PR) and a narrow sanitize companion (follow-up).
  • form-prefill-annotate chips three control shapes on checkout URLs: text/email/tel/number/url inputs with a non-empty live value, <select>s whose selectedIndex !== 0, and radio groups with a checked option. Values are not changed.
  • Layered FP control: URL gate, recognized-autocomplete-token allowlist, geo-select skiplist, focused-control + group-focused skip via focusin, per-form chip cap, visible-only filter.
  • Ships default-on as Experimental — annotation-only, worst-case is an extra chip.
  • Docs (docs/src/content/docs/rules.md) and demo Checkout page updated. Future-work bullets list the deferred hidden-input arm, include-value-in-chip, synthetic-blur-on-annotate, and sanitize-mode-for-<select> options.

Test plan

  • bun run test (1498 tests)
  • bun run check (biome + eslint)
  • pre-commit run --files docs/src/content/docs/rules.md
  • bun run build in demo-site
  • Catalog invariants test passes (rule-defaults.generated.ts regenerated)
  • New unit tests cover all three control kinds, autofill-token gate, geo-select gate, focused-control skip, chip cap, URL gate, idempotency, lazy-loaded subtrees
  • Property tests assert autocomplete-allowlist disjointness, geo-select coverage across name/id/aria, <select> first-option invariant, and idempotency

🤖 Generated with Claude Code

Annotate-first sibling of `checkout-checkbox-sanitize` and
`cart-addon-annotate`. On checkout URLs, prepend a visible chip near
text/email/tel/number inputs whose live `.value` is non-empty, `<select>`
elements whose `selectedIndex !== 0`, and radio groups whose currently-
checked option is server- or framework-rendered. Values are not changed;
the agent reads the chip in its DOM snapshot and decides whether to
overwrite. Required-selection radio groups stay submittable.

FP control: URL gate, recognized-`autocomplete`-token allowlist (skip
text inputs whose autocomplete names a legitimate browser-autofill
target like `name`, `email`, `street-address`), geo-select skiplist
(country/state/region/currency/etc.), focused-control + group-focused
skip via a `focusin` listener (cooperates with both browser autofill and
mid-fill user interaction), per-form chip cap, visible-only filter.

Rule ships default-on as Experimental — action is annotation-only so a
misfire is at worst an extra chip on a field.

Issue #121 also proposed a sanitize companion for `<input type=hidden>`
affiliate / UTM / promo metadata. That arm is split into a follow-up
because its toggle / FP profile differs (hidden inputs never reach the
agent's snapshot, so annotation can't carry the signal — clearing is the
only useful action). Future-work bullets in docs/rules.md cover the
hidden-input split, including-value-in-chip, synthetic-blur on annotate,
and sanitize-mode for `<select>` defaults on sneaking-prone fields.

Tests cover all three control kinds, the autofill-token gate, the geo-
select gate, focused-control skip, chip cap, URL gate, idempotency, and
lazy-loaded subtrees. Property tests assert autocomplete-allowlist
disjointness, geo-select coverage across name/id/aria, `<select>` first-
option invariant, and idempotency under random forms.

Docs updated under Dark patterns → Preselection; demo Checkout page
grows a Shipping & contact form section so the rule has live targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 6, 2026 11:48am

Request Review

@unblocked unblocked Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found.

About Unblocked

Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.

📖 Documentation — Learn more in our docs.

💬 Ask questions — Mention @unblocked to request a review or summary, or ask follow-up questions.

👍 Give feedback — React to comments with 👍 or 👎 to help us improve.

⚙️ Customize — Adjust settings in your preferences.

// `autocomplete="off"` on a pre-filled field is the most interesting
// case — the site disabled browser autofill yet still served a value.
// Don't skip it just because the attribute was set; fall through.
isExplicitAutofillOff(input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isExplicitAutofillOff(input) is called as a bare expression statement — its boolean return value is not used and the function has no side effects. This looks like leftover scaffolding (perhaps intended to gate additional logic or logging). As-is it's a no-op that costs a redundant getAttribute + split + filter on every text-input candidate.

Remove the call or assign/use its result:

Suggested change
isExplicitAutofillOff(input);
// `autocomplete="off"` on a pre-filled field is the most interesting
// case — the site disabled browser autofill yet still served a value.
// Don't skip it just because the attribute was set; fall through.

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.

Fixed in 1344f0b — removed both the bare isExplicitAutofillOff(input) call (which was indeed dead) and the function itself, since it had no other callers. The autocomplete="off" fall-through behavior was already covered by the surrounding comment.

— Claude Code, on behalf of @twschiller

`label[for="${escapeAttributeValue(id)}"]`,
);
if (label !== null) {
parts.push(label.textContent);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Element.textContent is typed string | null. When the label element exists but has no text content, pushing null into the string[] causes parts.join(" ") to coerce it to the literal "null". The same issue exists on line 240 with wrappingLabel.textContent.

While "null" doesn't currently match any GEO_SELECT_RE token, the coercion is a latent bug — if geo tokens like "locale" are ever relaxed, or if future callers rely on readSelectLabelHints returning clean text, it will produce wrong results. Fix by defaulting to "":

Suggested change
parts.push(label.textContent);
parts.push(label.textContent ?? "");

Apply the same fix on line 240: parts.push(wrappingLabel.textContent ?? "").

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.

Thanks — I went to add the ?? "" and it actually fails the project's lint suite: @typescript-eslint/no-unnecessary-condition flags the guard as redundant because the TypeScript DOM lib in use here types Element.textContent as string (not string | null), and per the DOM spec only Document and DocumentType nodes return null — neither can flow through these selectors. Sibling rules (hidden-fee-annotate.ts:241, cart-addon-annotate.ts:171) follow the same convention.

I did add a comment on readSelectLabelHints in 1344f0b explaining why the guard isn't there so the next reader doesn't re-derive this. If we want a stricter posture, that should be a project-wide change (relax the lint rule + add ?? "" everywhere) rather than this one rule.

— Claude Code, on behalf of @twschiller

Comment thread docs/src/content/docs/rules.md Outdated
Comment on lines +684 to +685
attribute (not the live property) is read so server-rendered prefills are
flagged even when a page script has already overwritten the live value.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The docs state: "The attribute (not the live property) is read so server-rendered prefills are flagged even when a page script has already overwritten the live value."

However, the code explicitly reads the live property (.value at line 362, .selectedIndex at line 394, .checked at line 437), with comments in the code explaining this choice so that framework-rendered defaults (React defaultValue, Vue v-model) are caught. The documentation has it backwards and should say "The live property (not the HTML attribute) is read…".

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.

Good catch — fixed in 1344f0b. The docs now read "The live property (not the HTML attribute) is read so framework-rendered defaults (React defaultValue, Vue v-model initialState, jQuery .val()) are flagged the same way the agent's DOM snapshot would see them; the focused-control skip above keeps user-entered values out of the flag set." which matches the code.

— Claude Code, on behalf of @twschiller

- Remove dead `isExplicitAutofillOff` predicate plus the bare expression
  that called it for its side-effect (there is none). The
  `autocomplete="off"` skip behavior was already documented in the
  surrounding comment; no logic change.
- Fix backwards docs prose for form-prefill-annotate: the rule reads
  the live `.value` / `.selectedIndex` / `.checked` properties, not the
  HTML attribute. Reflect that and explain why (framework defaults via
  React `defaultValue` / Vue `v-model` populate the property without
  writing the attribute; the focused-control skip keeps user-entered
  values out of the flag set).
- Add a `readSelectLabelHints` comment explaining why `?? ""` on
  `Element.textContent` is not used — the project's TypeScript lib
  types it as `string` and `@typescript-eslint/no-unnecessary-condition`
  flags the guard as redundant; same convention as the sibling
  annotate rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@twschiller twschiller merged commit 4ac452e into main Jun 6, 2026
7 checks passed
@twschiller twschiller deleted the form-prefill-annotate branch June 6, 2026 12:01
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