Feat: hidden-fee-annotate rule for drip-pricing fees (#119)#181
Conversation
Annotates mandatory fees (resort, service, convenience, processing, etc.) in checkout order summaries with a visible chip so agents can verify the total before completing a purchase. Ships default-off pending real-world per-rule activity counts (#174) — same rollout posture as schema-trust-sanitize. Row is annotated, not removed, since many fees are legally required to be surfaced (FTC Unfair-or-Deceptive-Fees rule). FP control is layered: whole-string regex on a small leaf-ish label, required order-summary ancestor (table / [role=region] / cart-shaped container / schema.org Order), required adjacent currency amount, single-item-cart skip, legally-required-line-item exclude list, and a (launch-empty) per-host denylist. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Action is annotation-only — worst case from a misfire is an extra chip on a row, not a destructive edit. Per-rule activity counts (#174) and the per-host denylist remain the fast lever if live signal surfaces a false-positive cluster. Experimental tag retained. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
1 issue 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.
| // aria-labelledby can list multiple IDs; concatenate their visible text. | ||
| const document_ = element.ownerDocument; | ||
| const parts: string[] = []; | ||
| for (const id of labelledby.split(/\s+/)) { |
There was a problem hiding this comment.
If aria-labelledby contains leading/trailing whitespace (e.g. " id1 id2 "), split(/\s+/) produces empty strings in the array. CSS.escape("") returns "", so querySelector("#") is called, which throws a DOMException ("'#' is not a valid selector").
The existing readAccessibleName in trust-badge-annotate.ts (line 137) guards against this with if (id === "") { continue; }. The same guard is needed here.
| for (const id of labelledby.split(/\s+/)) { | |
| for (const id of labelledby.split(/\s+/)) { | |
| if (id === "") { | |
| continue; | |
| } |
There was a problem hiding this comment.
Good catch — fixed in 5af258b. Added the empty-id guard alongside the split(/\s+/), matching readAccessibleName in trust-badge-annotate.ts as suggested. Also added a regression test for the leading/trailing-whitespace case in hidden-fee-annotate.test.ts (it needs a small CSS.escape polyfill because jsdom in this project doesn't expose the CSS global — same latent test-only quirk applies to trust-badge but isn't exercised there).
— Claude Code, on behalf of @twschiller
Without the guard, an aria-labelledby attribute with leading/trailing
whitespace (e.g. " heading-a heading-b ") would split into the empty
string, which `CSS.escape` returns unchanged, and `querySelector("#")`
then throws a DOMException. Matches the same pattern in
`readAccessibleName` in trust-badge-annotate.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
hidden-fee-annotate(default off, Experimental) — annotates mandatory fees that surface only at checkout (resort / service / convenience / processing / destination / facility / handling / venue fee, delivery surcharge) so agents can verify the total before paying. Closes Rule proposal: hidden-fee-annotate — annotate drip-pricing fees added during checkout #119.<table>/[role="region"]/ cart-shaped class/id / schema.orgOrdermicrodata), required adjacent currency amount, single-item-cart skip, legally-required-line-item exclude list (tax, VAT, GST, shipping, tip, gratuity), and a (launch-empty) per-host denylist.fast-checkproperty tests (regex precision, phrase/exclude disjointness, currency regex, single-item-cart invariant, idempotency). Full suite: 1444 tests pass.Rollout
Ships default-off; promote to default-on after per-rule activity counts (#174) show the FP rate is acceptable, mirroring
schema-trust-sanitize's posture.Test plan
bun run preflight(build-site-data, build-injection-patterns, build-rule-defaults, biome, eslint, tsc, knip, jest --coverage) — cleanpre-commit run --files docs/src/content/docs/rules.md— clean (mdformat-reflowed on first run)/checkoutpage outside the order-summary container🤖 Generated with Claude Code