Skip to content

fix(patterns): one generic-element filter for all three click detectors - #57

Open
Sarcastic-Soul wants to merge 2 commits into
nossa-y:mainfrom
Sarcastic-Soul:fix/unify-generic-element-filter
Open

fix(patterns): one generic-element filter for all three click detectors#57
Sarcastic-Soul wants to merge 2 commits into
nossa-y:mainfrom
Sarcastic-Soul:fix/unify-generic-element-filter

Conversation

@Sarcastic-Soul

Copy link
Copy Markdown
Contributor

Fixes #52

Important

Stacked on #56. Both PRs touch daily_habits, and building this one on main produced a real merge conflict — so it is branched off #56 instead. Merge #56 first; this PR's own change is the second commit (fix(patterns): one generic-element filter…), which is the one to review here. Once #56 lands, this becomes a single-commit diff automatically. Happy to re-cut it against main in either order if you'd rather.

The bug

patterns.py opens with what looks like the canonical list of elements the recorder couldn't meaningfully name:

_GENERIC_ELEMENTS = {"scroll area", "group", "cell", "text", "text field"}

Only one of the three click detectors actually used it. The other two re-implemented the filter in SQL, and none of the three agreed:

Detector Filter Case-sensitive?
action_sequences the Python set yes
repeated_clicks all five re-typed in SQL NOT IN yes
daily_habits only ('scroll area','group') yes

What that produced

One click stream — cell x5 on each of two days, plus Scroll Area x5 (recorder capitalization varies):

Detector before after
repeated_clicks Clicked 'Scroll Area' (AXScrollArea) 5x (dropped)
daily_habits 'cell' on 2 days, 10x total (dropped)
action_sequences Scroll Area -> Scroll Area -> … [AXScrollArea] -> [AXScrollArea] -> …

'Scroll Area' slipped through because SQL NOT IN is case-sensitive and the list is lowercase. 'cell' was reported as a habit because daily_habits' shorter list omitted it — while repeated_clicks, reading the same rows, filtered that exact name out.

Both results are noise in get_patterns, which is served to agents as automation suggestions. The maintenance trap is the quieter half: the constant sits at the top of the file looking authoritative, so editing it silently fixes one detector out of three.

The change

_GENERIC_ELEMENTS becomes the real single source of truth, with two derived forms:

def _is_generic(name: str | None) -> bool:
    return (name or "").strip().lower() in _GENERIC_ELEMENTS

_GENERIC_SQL = ",".join("?" * len(_GENERIC_ELEMENTS))
_GENERIC_PARAMS = tuple(sorted(_GENERIC_ELEMENTS))
  • repeated_clicks keeps filtering in SQL — deliberately. It has ORDER BY cnt DESC LIMIT 20, so generic rows filtered late would consume slots that real elements should hold. The NOT IN list is now generated from the set and matched via lower(trim(element_name)), so it can't drift and it's case-insensitive.
  • daily_habits drops its SQL list entirely and calls _is_generic in the loop it already runs. No LIMIT to protect, so the simpler form is fine.
  • action_sequences swaps its inline in _GENERIC_ELEMENTS for _is_generic, picking up strip + case-folding. Behaviour is otherwise unchanged: a generic element still keeps its slot in the sequence and stands in as [AXRole], because the click did happen — it just doesn't have a name worth printing.

Tests

Five tests added to tests/test_patterns.py:

  • test_is_generic_ignores_case_and_padding — every name in the set, upper/title/padded
  • test_repeated_clicks_drops_generic_elements_whatever_the_case — the 'Scroll Area' leak, plus a real element alongside it to confirm nothing over-filters
  • test_daily_habits_drops_every_generic_element — the 'cell' habit
  • test_action_sequences_stands_in_for_generic_elements — sequence still mined, name replaced by role
  • test_all_three_detectors_agree_on_what_is_generic — the invariant itself: one capture containing every generic name, and no detector prints any of them. This is the property the three filters used to disagree on, so it's the one guarding against future drift.

Full suite: 117 passed.

Scope

Filtering only. No threshold, ranking, or label format changes; url_patterns, app_switching, and repeated_text are untouched.

Sarcastic-Soul and others added 2 commits August 15, 2026 22:51
daily_habits grouped clicks with SQLite's date(), which buckets the stored
UTC timestamps by UTC calendar day, while the rest of the package is
local-day based. For a user far enough from UTC a single local day
straddles a UTC boundary and was counted as two -- and since the habit
threshold is days >= 2, that reported a habit out of activity that never
repeated, contradicting the module's stated guarantee.

Bucket in Python from parse_epoch + local_day_string instead, matching how
coverage() already derives local time and keeping the result independent
of the SQLite process timezone. The per-day click floor the SQL HAVING
enforced moves to MIN_CLICKS_PER_DAY, and rows whose timestamp will not
parse are now skipped rather than bucketed at epoch 0.

Adds tests/test_patterns.py (the module had no direct coverage).

Fixes nossa-y#51

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_GENERIC_ELEMENTS was defined once but used by only one detector.
repeated_clicks retyped all five names in SQL; daily_habits listed just two.
All three matched case-sensitively. So the same click stream produced
"Clicked 'Scroll Area' 5x" as a repeated click (capitalization slipped past
the lowercase SQL list) and "'cell' on 2 days" as a daily habit (cell was
missing from the shorter list) -- both of them exactly the noise the list
exists to suppress.

Make _GENERIC_ELEMENTS the single source of truth: _is_generic() strips and
lowercases before matching, and the SQL fragment is derived from the same
set rather than retyped. repeated_clicks keeps filtering in SQL so generic
rows cannot consume slots in its LIMIT 20; daily_habits and action_sequences
use the Python check.

Fixes nossa-y#52

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Three pattern detectors use three different 'generic element' filters

1 participant