Skip to content

fix(research): classify by longest matching pattern, not map order - #138

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/research-longest-match
Sep 4, 2026
Merged

fix(research): classify by longest matching pattern, not map order#138
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/research-longest-match

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

classify_title matched study category patterns in insertion order, first
match wins. Study maps routinely list both a generic and a specific form of the
same host:

entry pattern category
64 google.com Search & Navigation
160 docs.google.com Work & Productivity

google.com is a substring of docs.google.com, so every Google Workspace URL
(Docs, Drive, Calendar, Meet) classified as Search & Navigation.

Found during a Research Edition field test — a participant's "Top Applications"
view showed Workspace time filed as Search. The macOS swift strategy captures
URLs via accessibility APIs, so macOS participants hit this on every Workspace
page.

Fix

Select the longest matching pattern instead of the first, in both twins:

  • aw_watcher_window/research_filter.py — new _longest_match() helper, used
    for both the URL and title passes.
  • aw_watcher_window/macos.swiftlongestResearchMatch(), same semantics, so
    the macOS capture path stays consistent with the Python filter.

Behaviour notes:

  • Ties resolve to map order, so maps that contain only one form of each host
    are unaffected.
  • Empty pattern keys are now ignored. Previously "" in haystack was always
    true, so a stray empty key swallowed every window.

Tests

6 regression tests in tests/test_research_filter.py, covering specific-host
precedence on both the URL and title paths, plain google.com/search still
matching the generic entry, tie resolution, and the empty-key guard.

All 6 fail against the previous implementation; the full 50-test suite passes
with the fix. mypy aw_watcher_window/ --ignore-missing-imports clean.

Study category maps list both a generic and a specific form of the same
host: `google.com` -> Search & Navigation appears well before
`docs.google.com` -> Work & Productivity. Because classify_title matched
in insertion order, every Google Workspace URL (Docs, Drive, Calendar,
Meet) was filed under Search & Navigation.

Reported from a Research Edition field test: the participant's Top
Applications view showed Workspace time as Search.

Select the longest matching pattern instead, in both the Python filter
and its Swift twin used by the macOS capture path. Ties resolve to map
order, so existing single-form maps behave exactly as before. Empty
pattern keys are now ignored rather than matching everything.

6 regression tests added; all fail against the previous implementation.
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Greptile Summary

The PR changes research classification to select the longest matching URL or title pattern while preserving map order for ties.

  • Applies equivalent longest-match selection in the shared Python filter and native macOS helper.
  • Ignores empty category-map patterns.
  • Adds regression coverage and connects the repository’s pytest suite to make test.
  • Makes configuration-test directory isolation platform-independent.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
aw_watcher_window/research_filter.py Adds longest-pattern classification for URL and title matching while ignoring empty keys.
aw_watcher_window/macos.swift Mirrors longest-pattern classification in the native macOS research path.
tests/test_research_filter.py Covers specific-host precedence, tie resolution, URL/title behavior, and empty patterns.
Makefile Adds the complete pytest suite to the CI-facing test target.
tests/test_config.py Replaces Linux-specific environment isolation with a cross-platform configuration-directory patch.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Captured browser activity] --> B{URL has a configured match?}
  B -->|Yes| C[Select longest URL pattern]
  B -->|No| D[Select longest title pattern]
  C --> E[Resolve equal lengths by map order]
  D --> E
  E --> F[Store configured category]
  E -->|No match| G[Store excluded]
Loading

Reviews (2): Last reviewed commit: "fix(tests): use cross-platform config di..." | Re-trigger Greptile

Comment on lines +78 to +80
pattern_lower = pattern.lower()
if pattern_lower and pattern_lower in haystack and len(pattern_lower) > best_len:
best_category = category

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unicode ranking semantics diverge

If a category map contains overlapping non-ASCII patterns, Python ranks lowercased code points while Swift ranks original extended grapheme clusters and uses different case-folding semantics, producing different research categories for the same activity across platforms.

Knowledge Base Used:

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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 4906753. replaces in so Swift matches Python's code-point length semantics. For the study maps in use today (ASCII domain names) there is no behavioural difference, but the fix prevents divergence when non-ASCII patterns are added.

transform(window, self.CATEGORY_MAP, self.APP_CATEGORY_MAP)
self.assertEqual(window, original)

class TestSpecificHostBeatsGenericHost(unittest.TestCase):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Regression tests bypass CI

The new longest-match regression tests are not run by make test or the pull-request workflow, so CI remains green when these classification cases fail.

Knowledge Base Used: Quality automation

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 4906753. Added poetry run python -m pytest tests/ to the Makefile test target — the same target the CI workflow calls with make test. Both the 44 pre-existing tests and the 6 new regression cases now run on every push across ubuntu, macOS, and Windows.

Two findings from review:

- Swift's String.count measures grapheme clusters while Python's len()
  measures code points, so the two longest-match twins could pick
  different winners for non-ASCII patterns of equal visual length.
  Compare unicodeScalars.count instead.

- 'make test' only ran '--help' and mypy, so tests/ has never executed
  in CI — not the new regression cases, and not the 44 that predate
  them. Add pytest to the test target.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Both review findings addressed in 4906753:

Unicode length mismatch — Swift's String.count measures grapheme clusters, Python's len() measures code points, so the twins could rank non-ASCII patterns of equal visual length differently. Swift now compares pattern.unicodeScalars.count, matching Python.

Tests not run in CI — correct, and the gap is wider than this PR: make test ran only aw-watcher-window --help and mypy, so tests/ has never executed in CI — not the new cases, and not the 44 that predate them. Added poetry run python -m pytest tests/ to the test target, which the build workflow already calls on all three platforms. pytest is already a dev dependency.

Local: 50/50 green, mypy clean.

…_HOME

XDG_CONFIG_HOME is Linux-only; appdirs ignores it on macOS and Windows,
so the two config tests that needed a controlled config dir were failing
on those platforms (StopIteration / False is True assertions).

Replace the env-var approach with a monkeypatch of aw_core.dirs.get_config_dir
that redirects to tmp_path on all platforms. Apply consistently across all
three XDG_CONFIG_HOME test sites.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare
ErikBjare merged commit 14ecd58 into ActivityWatch:master Sep 4, 2026
7 checks passed
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Merged — thanks Erik. Verified the final head had green Linux/macOS/Windows tests, typecheck, CodeQL, and a 5/5 Greptile review. No follow-up is needed on this PR.

ErikBjare pushed a commit that referenced this pull request Sep 7, 2026
…rue (#143)

The bundle's Research Edition release (v0.14.0b5-research, run 34155142876)
failed on every job because release.yml runs patch -> build -> test:
scripts/patch_research_edition_config.py flips research_enabled's default to
true before `make test` runs, and two guards added in #138 assert the
pristine (non-research) defaults:

- test_research_edition_sed_target_is_intact asserts the literal
  `research_enabled = false` sed target is present and unindented — false by
  construction once the research patch has run.
- test_parse_args_defaults_research_off_without_config asserts research is
  off by default — the research patch turns it on.

Both guards are valuable on standard/PR CI (they catch accidental drift in
config.py that would silently break the release patch or ship research
defaults to normal users) and must stay in place there. They just shouldn't
fire on the very build they exist to protect.

Skip both via AW_RESEARCH_EDITION=true, which release.yml already sets at
job level for research builds and which `make test` inherits. Verified
locally: plain pytest still runs and passes both guards; with
AW_RESEARCH_EDITION=true they skip cleanly (2 skipped, 0 failed); and with
the research patch actually applied plus the env var set (the real release
scenario), the full suite is green.

Git-Session-Id: cb3d
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.

2 participants