From b626dc648871c0bbc860d0be65038227a76cf252 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 5 May 2026 13:05:46 +0200 Subject: [PATCH] feat(navigation): add structural identity and transitions for NOTIFICATIONS trap --- GramAddict/core/perception/screen_identity.py | 26 ++++++++++++- GramAddict/core/screen_topology.py | 8 ++++ GramAddict/core/telepathic_engine.py | 9 +++-- tests/tdd/test_context_gate_matrix.py | 1 + tests/tdd/test_gallery_trap_guard.py | 39 +++++++++++++++++++ tests/tdd/test_notifications_screen.py | 22 +++++++++++ tests/tdd/test_obstacle_keyboard.py | 6 ++- 7 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/tdd/test_gallery_trap_guard.py create mode 100644 tests/tdd/test_notifications_screen.py diff --git a/GramAddict/core/perception/screen_identity.py b/GramAddict/core/perception/screen_identity.py index c57f7bc..0f8f668 100644 --- a/GramAddict/core/perception/screen_identity.py +++ b/GramAddict/core/perception/screen_identity.py @@ -23,6 +23,7 @@ class ScreenType(Enum): COMMENTS = "comments" MODAL = "modal" FOREIGN_APP = "foreign_app" + NOTIFICATIONS = "notifications" UNKNOWN = "unknown" @@ -94,7 +95,14 @@ class ScreenIdentity: resource_ids.add(short_id) # Track which tab is selected - if selected and short_id in ("feed_tab", "search_tab", "clips_tab", "profile_tab", "direct_tab"): + if selected and short_id in ( + "feed_tab", + "search_tab", + "clips_tab", + "profile_tab", + "direct_tab", + "news_tab", + ): selected_tab = short_id if text: @@ -180,7 +188,15 @@ class ScreenIdentity: # Structural detection is O(1), zero LLM calls, and cannot be fooled. # EXCEPTION: If Qdrant has explicitly learned this screen as NORMAL (via LLM unlearning), # we skip the structural check to prevent false-positive infinite loops. - creation_flow_markers = ("quick_capture", "gallery_cancel_button", "creation_flow", "reel_camera") + creation_flow_markers = ( + "quick_capture", + "gallery_cancel_button", + "creation_flow", + "reel_camera", + "gallery_grid_item_thumbnail", + "camera_cancel_button", + "next_button_textview", + ) browser_markers = ("ig_browser_text_title", "ig_browser_close_button", "ig_chrome_subsection") if not is_normal_override and any(marker in ids_str for marker in creation_flow_markers): logger.info("🛡️ [ScreenIdentity] Content-creation overlay detected → MODAL") @@ -239,6 +255,11 @@ class ScreenIdentity: if any(marker in ids for marker in STORY_MARKERS): return ScreenType.STORY_VIEW + # Notifications / Activity structural markers + NOTIFICATIONS_MARKERS = ("row_newsfeed_text", "newsfeed_tab", "newsfeed_user_imageview") + if any(marker in ids for marker in NOTIFICATIONS_MARKERS) or selected_tab == "news_tab": + return ScreenType.NOTIFICATIONS + # DM thread detection — Structural markers (header and input fields) if "direct_thread_header" in ids or "direct_text_input" in ids or "message_composer_container" in ids: return ScreenType.DM_THREAD @@ -357,6 +378,7 @@ class ScreenIdentity: "clips_tab": "tap reels tab", "profile_tab": "tap profile tab", "direct_tab": "tap messages tab", + "news_tab": "tap activity heart icon notifications", } for tab_id, action in tab_map.items(): if tab_id in resource_ids: diff --git a/GramAddict/core/screen_topology.py b/GramAddict/core/screen_topology.py index dd483ee..b4782f8 100644 --- a/GramAddict/core/screen_topology.py +++ b/GramAddict/core/screen_topology.py @@ -33,6 +33,7 @@ class ScreenTopology: "tap profile tab": ScreenType.OWN_PROFILE, "tap reels tab": ScreenType.REELS_FEED, "tap messages tab": ScreenType.DM_INBOX, + "tap activity heart icon notifications": ScreenType.NOTIFICATIONS, "tap story ring avatar": ScreenType.STORY_VIEW, }, ScreenType.EXPLORE_GRID: { @@ -84,6 +85,12 @@ class ScreenTopology: "tap home tab": ScreenType.HOME_FEED, # NOTE: 'press back' intentionally omitted — destination is non-deterministic }, + ScreenType.NOTIFICATIONS: { + "tap home tab": ScreenType.HOME_FEED, + "press back": ScreenType.HOME_FEED, + "tap profile tab": ScreenType.OWN_PROFILE, + "tap explore tab": ScreenType.EXPLORE_GRID, + }, ScreenType.UNKNOWN: { "tap home tab": ScreenType.HOME_FEED, }, @@ -110,6 +117,7 @@ class ScreenTopology: "open user profile": ScreenType.OTHER_PROFILE, "open search": ScreenType.SEARCH_RESULTS, "view comments": ScreenType.COMMENTS, + "open notifications": ScreenType.NOTIFICATIONS, } @classmethod diff --git a/GramAddict/core/telepathic_engine.py b/GramAddict/core/telepathic_engine.py index 180a3bb..bf1ea1c 100644 --- a/GramAddict/core/telepathic_engine.py +++ b/GramAddict/core/telepathic_engine.py @@ -253,9 +253,12 @@ class TelepathicEngine: # ZERO-TRUST: NO LOCALIZED STRINGS. res_id = (node.resource_id or "").lower() - # Avoid clicking on audio/trending/camera elements unless explicitly requested - if any(trap in res_id for trap in ["album_art", "use_in_camera", "trending", "audio"]): - if not any(word in intent for word in ["audio", "trending", "camera"]): + # Avoid clicking on audio/trending/camera/creation elements unless explicitly requested + if any( + trap in res_id + for trap in ["album_art", "use_in_camera", "trending", "audio", "creation", "camera", "gallery"] + ): + if not any(word in intent for word in ["audio", "trending", "camera", "create", "gallery"]): return False # Avoid clicking share, save, menu, options unless explicitly requested diff --git a/tests/tdd/test_context_gate_matrix.py b/tests/tdd/test_context_gate_matrix.py index b0c777b..deac25b 100644 --- a/tests/tdd/test_context_gate_matrix.py +++ b/tests/tdd/test_context_gate_matrix.py @@ -141,6 +141,7 @@ class TestScreenTopologyBackTransitions: ScreenType.FOLLOW_LIST, ScreenType.STORY_VIEW, ScreenType.COMMENTS, + ScreenType.NOTIFICATIONS, ): # These are "leaf" screens with a deterministic parent — OK to keep continue diff --git a/tests/tdd/test_gallery_trap_guard.py b/tests/tdd/test_gallery_trap_guard.py new file mode 100644 index 0000000..1471744 --- /dev/null +++ b/tests/tdd/test_gallery_trap_guard.py @@ -0,0 +1,39 @@ +from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType + + +def test_gallery_screen_is_modal(): + """ + Test that the Gallery/New Post screen is correctly identified as a MODAL, + preventing it from being hallucinated as POST_DETAIL or HOME_FEED. + """ + xml_dump = """ + + + + + + + +""" + identity = ScreenIdentity("bot_user") + result = identity.identify(xml_dump) + + assert result["screen_type"] == ScreenType.MODAL + + +def test_camera_screen_is_modal(): + """ + Test that the Camera creation screen is correctly identified as a MODAL. + """ + xml_dump = """ + + + + + + +""" + identity = ScreenIdentity("bot_user") + result = identity.identify(xml_dump) + + assert result["screen_type"] == ScreenType.MODAL diff --git a/tests/tdd/test_notifications_screen.py b/tests/tdd/test_notifications_screen.py new file mode 100644 index 0000000..cab48a1 --- /dev/null +++ b/tests/tdd/test_notifications_screen.py @@ -0,0 +1,22 @@ +from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType + + +def test_notifications_screen_identification(): + """ + Test that the Notifications/Activity screen is correctly identified, + preventing it from being hallucinated as POST_DETAIL. + """ + xml_dump = """ + + + + + + + +""" + identity = ScreenIdentity("bot_user") + result = identity.identify(xml_dump) + + assert result["screen_type"] == ScreenType.NOTIFICATIONS + assert "tap home tab" in result["available_actions"] or "press back" in result["available_actions"] diff --git a/tests/tdd/test_obstacle_keyboard.py b/tests/tdd/test_obstacle_keyboard.py index ba99aca..b650d86 100644 --- a/tests/tdd/test_obstacle_keyboard.py +++ b/tests/tdd/test_obstacle_keyboard.py @@ -28,7 +28,8 @@ def test_sae_detects_obstacle_keyboard(): xml_with_keyboard = """ - + + """ @@ -45,7 +46,8 @@ def test_obstacle_guard_dismisses_keyboard(): xml_with_keyboard = """ - + + """