feat: ScreenTopology HD Map + graph-aware GOAP routing + NAV keyword split

FUNDAMENTAL ARCHITECTURE OVERHAUL:

1. ScreenTopology HD Map (NEW):
   Pure-data BFS pathfinding between Instagram screens.
   Zero runtime dependencies. The GOAP planner's GPS.
   Knows: HOME_FEED → tap profile tab → OWN_PROFILE → tap following list → FOLLOW_LIST

2. Graph-Aware GOAP Planning:
   GoalPlanner._plan_navigation() now consults ScreenTopology FIRST.
   From HOME_FEED, goal 'open following list' returns 'tap profile tab'
   (intermediate hop) instead of blind 'open following list' (impossible).
   Autonomous discovery and Qdrant knowledge kept as fallbacks.

3. NAV Keyword Split:
   NAV_INTENT_KEYWORDS → NAV_ZONE_BYPASS_KEYWORDS + NAV_TAB_KEYWORDS
   Ends the Structural Guard civil war where 'following' was both
   'allowed in nav zone' AND 'must be at bottom' simultaneously.

4. QNavGraph Deduplication:
   _find_path() delegates to ScreenTopology.find_route() (SSOT).
   core_nodes seed generated from ScreenTopology.TRANSITIONS.

115 unit tests pass. Zero regressions.
This commit is contained in:
2026-04-24 22:33:58 +02:00
parent 82bf931b0e
commit d69da4c974
7 changed files with 462 additions and 68 deletions

View File

@@ -3,10 +3,12 @@ from unittest.mock import MagicMock
from GramAddict.core.goap import GoalPlanner, ScreenType
def test_goal_planner_linguistic_match_blank_start():
def test_goal_planner_uses_hd_map_over_linguistic_match():
"""
Tests that the GoalPlanner correctly matches a linguistic target
during Blank Start discovery without being blocked by a known target check.
Tests that the GoalPlanner uses the HD Map (ScreenTopology) as its
primary routing strategy, even during Blank Start discovery.
The HD Map returns canonical action intents that the TelepathicEngine
interprets — not just available_actions from the XML dump.
"""
planner = GoalPlanner("test_user")
@@ -26,4 +28,8 @@ def test_goal_planner_linguistic_match_blank_start():
result = planner.plan_next_step(goal, screen, explored_nav_actions=set())
assert result == "tap explore grid", "Failed to linguistically match explore during Blank Start"
# HD Map routes HOME_FEED → EXPLORE_GRID via "tap explore tab"
assert result == "tap explore tab", (
f"Expected HD Map to route via 'tap explore tab', got '{result}'. "
"The HD Map should override linguistic matching as the primary strategy."
)

View File

@@ -0,0 +1,103 @@
"""
🔴 RED TDD: GOAP Graph-Aware Routing
The GoalPlanner must use the ScreenTopology HD Map as its PRIMARY
routing strategy. When asked to reach FollowingList from HomeFeed,
it should return "tap profile tab" (first step of the BFS route),
NOT "open following list" (impossible direct action).
"""
import pytest
import sys
import os
from unittest.mock import MagicMock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import GoalPlanner, ScreenType
class TestGoapGraphRouting:
"""GOAP planner must use the HD Map for multi-step navigation."""
@pytest.fixture
def planner(self):
p = GoalPlanner("testbot")
# Ensure blank start (no learned knowledge)
p.knowledge._goal_requirements = {}
p.knowledge._learned_screen_mappings = {}
p.knowledge._learned_traps = set()
return p
def test_planner_routes_to_profile_first_for_following_list(self, planner):
"""
From HOME_FEED, goal 'open following list' should return
'tap profile tab' (navigate to OwnProfile first),
NOT 'open following list' as a raw intent.
"""
screen = {
"screen_type": ScreenType.HOME_FEED,
"available_actions": [
"tap explore tab",
"tap home tab",
"tap messages tab",
"tap reels tab",
"press back",
],
"context": {},
"selected_tab": "feed_tab",
}
action = planner.plan_next_step("open following list", screen)
assert action == "tap profile tab", (
f"Planner returned '{action}' instead of 'tap profile tab'. "
"It should use the HD Map to route HOME_FEED → OWN_PROFILE → FOLLOW_LIST."
)
def test_planner_returns_final_action_on_intermediate_screen(self, planner):
"""
From OWN_PROFILE, goal 'open following list' should return
'tap following list' directly (we're already on the right screen).
"""
screen = {
"screen_type": ScreenType.OWN_PROFILE,
"available_actions": [
"tap explore tab",
"tap home tab",
"tap reels tab",
"tap following list",
"press back",
],
"context": {},
"selected_tab": "profile_tab",
}
action = planner.plan_next_step("open following list", screen)
assert action == "tap following list", (
f"Planner returned '{action}' instead of 'tap following list'. "
"On OWN_PROFILE, it should directly execute the final action."
)
def test_planner_detects_goal_already_achieved(self, planner):
"""On FOLLOW_LIST, goal 'open following list' should return None (achieved)."""
screen = {
"screen_type": ScreenType.FOLLOW_LIST,
"available_actions": ["press back"],
"context": {},
}
action = planner.plan_next_step("open following list", screen)
assert action is None, "Goal is already achieved — planner should return None."
def test_planner_routes_explore_to_following_list(self, planner):
"""From EXPLORE_GRID, route should be: Explore → Profile → FollowList."""
screen = {
"screen_type": ScreenType.EXPLORE_GRID,
"available_actions": [
"tap home tab",
"tap profile tab",
"tap reels tab",
],
"context": {},
"selected_tab": "explore_tab",
}
action = planner.plan_next_step("open following list", screen)
assert action == "tap profile tab", (
f"From EXPLORE_GRID, planner should route via OWN_PROFILE, got '{action}'"
)

View File

@@ -0,0 +1,98 @@
"""
🔴 RED TDD: ScreenTopology — The Instagram HD Map
Tests for BFS pathfinding between Instagram screens.
The killer test: HOME_FEED → OWN_PROFILE → FOLLOW_LIST must be a 2-step route.
"""
import pytest
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import ScreenType
from GramAddict.core.screen_topology import ScreenTopology
class TestScreenTopologyRouting:
"""BFS pathfinding must compute correct multi-step routes."""
def test_route_home_to_following_list(self):
"""THE killer test: HomeFeed → OwnProfile → FollowList (2 hops)."""
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.FOLLOW_LIST)
assert route is not None, "Route from HOME_FEED to FOLLOW_LIST must exist"
assert len(route) == 2, f"Expected 2 hops, got {len(route)}: {route}"
assert route[0] == ("tap profile tab", ScreenType.OWN_PROFILE)
assert route[1] == ("tap following list", ScreenType.FOLLOW_LIST)
def test_route_already_there(self):
"""Same screen = empty route (no steps needed)."""
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.HOME_FEED)
assert route == []
def test_route_single_hop(self):
"""Direct neighbor = 1 step."""
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.EXPLORE_GRID)
assert route is not None
assert len(route) == 1
assert route[0] == ("tap explore tab", ScreenType.EXPLORE_GRID)
def test_route_reverse_direction(self):
"""FollowList back to HomeFeed: FollowList → OwnProfile → HomeFeed (2 hops)."""
route = ScreenTopology.find_route(ScreenType.FOLLOW_LIST, ScreenType.HOME_FEED)
assert route is not None
assert len(route) == 2
assert route[0][1] == ScreenType.OWN_PROFILE
assert route[1][1] == ScreenType.HOME_FEED
def test_no_route_from_unreachable(self):
"""FOREIGN_APP has no outbound transitions — route should be None."""
route = ScreenTopology.find_route(ScreenType.FOREIGN_APP, ScreenType.FOLLOW_LIST)
assert route is None
class TestGoalToTargetScreen:
"""Goal string → target ScreenType mapping."""
def test_following_list_goal(self):
assert ScreenTopology.goal_to_target_screen("open following list") == ScreenType.FOLLOW_LIST
def test_followers_list_goal(self):
assert ScreenTopology.goal_to_target_screen("open followers list") == ScreenType.FOLLOW_LIST
def test_profile_goal(self):
assert ScreenTopology.goal_to_target_screen("open profile") == ScreenType.OWN_PROFILE
def test_home_feed_goal(self):
assert ScreenTopology.goal_to_target_screen("open home feed") == ScreenType.HOME_FEED
def test_explore_goal(self):
assert ScreenTopology.goal_to_target_screen("open explore feed") == ScreenType.EXPLORE_GRID
def test_messages_goal(self):
assert ScreenTopology.goal_to_target_screen("open messages") == ScreenType.DM_INBOX
def test_interaction_goal_returns_none(self):
"""Non-navigation goals should return None."""
assert ScreenTopology.goal_to_target_screen("like this post") is None
def test_unknown_goal_returns_none(self):
assert ScreenTopology.goal_to_target_screen("do something random") is None
class TestGetTransitions:
"""Get available transitions from a screen."""
def test_home_feed_has_profile_tab(self):
transitions = ScreenTopology.get_transitions(ScreenType.HOME_FEED)
assert "tap profile tab" in transitions
assert transitions["tap profile tab"] == ScreenType.OWN_PROFILE
def test_own_profile_has_following_list(self):
transitions = ScreenTopology.get_transitions(ScreenType.OWN_PROFILE)
assert "tap following list" in transitions
assert transitions["tap following list"] == ScreenType.FOLLOW_LIST
def test_unknown_screen_returns_empty(self):
transitions = ScreenTopology.get_transitions(ScreenType.FOREIGN_APP)
assert transitions == {}