feat(navigation): add structural identity and transitions for NOTIFICATIONS trap

This commit is contained in:
2026-05-05 13:05:46 +02:00
parent 5ca5777c4b
commit b626dc6488
7 changed files with 104 additions and 7 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
<node index="0" text="New post" resource-id="" class="android.widget.TextView" bounds="[0,0][100,100]" />
<node index="1" text="" resource-id="com.instagram.android:id/gallery_grid_item_thumbnail" class="android.widget.Button" content-desc="Selected media number 1 Photo thumbnail created on 3 May 2026 10:30" bounds="[0,100][500,600]" />
<node index="2" text="" resource-id="com.instagram.android:id/next_button_textview" class="android.widget.Button" content-desc="Next" bounds="[900,0][1080,100]" />
</node>
</hierarchy>
"""
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 = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
<node index="0" text="" resource-id="com.instagram.android:id/camera_cancel_button" class="android.widget.ImageView" bounds="[0,0][100,100]" />
<node index="1" text="" resource-id="com.instagram.android:id/quick_capture" class="android.widget.FrameLayout" bounds="[0,100][1080,2400]" />
</node>
</hierarchy>
"""
identity = ScreenIdentity("bot_user")
result = identity.identify(xml_dump)
assert result["screen_type"] == ScreenType.MODAL

View File

@@ -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 = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
<node index="0" text="Notifications" resource-id="com.instagram.android:id/action_bar_title" class="android.widget.TextView" bounds="[0,0][1080,100]" />
<node index="1" text="User liked your photo." resource-id="com.instagram.android:id/row_newsfeed_text" class="android.widget.TextView" bounds="[100,100][1000,200]" />
<node index="2" text="" resource-id="com.instagram.android:id/newsfeed_tab" selected="true" class="android.widget.FrameLayout" bounds="[600,2300][800,2400]" />
</node>
</hierarchy>
"""
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"]

View File

@@ -28,7 +28,8 @@ def test_sae_detects_obstacle_keyboard():
xml_with_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android" resource-id="" text="" content-desc="" />
<node index="1" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
<node index="1" class="android.widget.EditText" focused="true" />
<node index="2" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
</hierarchy>
"""
@@ -45,7 +46,8 @@ def test_obstacle_guard_dismisses_keyboard():
xml_with_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android" resource-id="" text="" content-desc="" />
<node index="1" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
<node index="1" class="android.widget.EditText" focused="true" />
<node index="2" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
</hierarchy>
"""