diff --git a/.gitignore b/.gitignore index 918787c..f7a5905 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,11 @@ coverage.xml debug/ # Bytecode & Cache (Enforce at bottom to override whitelists) -__pycache__/ -*.pyc +**/__pycache__/ **/*.pyc **/*.pyo **/*.pyd .pytest_cache/ .hypothesis/ +.coverage +htmlcov/ diff --git a/GramAddict/core/behaviors/resonance_evaluator.py b/GramAddict/core/behaviors/resonance_evaluator.py index c8b1dd0..0eea2e8 100644 --- a/GramAddict/core/behaviors/resonance_evaluator.py +++ b/GramAddict/core/behaviors/resonance_evaluator.py @@ -34,8 +34,8 @@ class ResonanceEvaluatorPlugin(BehaviorPlugin): def execute(self, ctx: BehaviorContext) -> BehaviorResult: resonance = ctx.cognitive_stack.get("resonance") if not resonance: - logger.warning("🧠 [Resonance] Engine missing. Defaulting to 1.0") - res_score = 1.0 + logger.error("🧠 [Resonance] CRITICAL: Engine missing from cognitive stack. Defaulting to 0.5 (neutral).") + res_score = 0.5 else: post_data = ctx.post_data or {} res_score = resonance.calculate_resonance(post_data) diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index 27ba52d..15667a4 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -942,14 +942,11 @@ def _run_zero_latency_feed_loop( dopamine = cognitive_stack.get("dopamine") darwin = cognitive_stack.get("darwin") - cognitive_stack.get("resonance") ai = cognitive_stack.get("active_inference") growth = cognitive_stack.get("growth_brain") - cognitive_stack.get("swarm") # Track interaction outcomes for end-of-session learning session_outcomes = [] - session_outcomes = [] shared_state = {"consecutive_marker_misses": 0, "consecutive_ads": 0, "session_outcomes": session_outcomes} from GramAddict.core.session_state import SessionState diff --git a/GramAddict/core/perception/action_memory.py b/GramAddict/core/perception/action_memory.py index 6f5c317..821de8f 100644 --- a/GramAddict/core/perception/action_memory.py +++ b/GramAddict/core/perception/action_memory.py @@ -270,7 +270,7 @@ class ActionMemory: else: # VLM returned ambiguous response (JSON, mixed signals, etc.) # Don't treat as hard failure — fall through to structural delta verification - logger.info( + logger.debug( f"🧠 [ActionMemory] VLM response for '{intent}' was not YES/NO " f"(got: '{response[:80]}...'). Falling through to structural verification." ) diff --git a/GramAddict/core/perception/screen_identity.py b/GramAddict/core/perception/screen_identity.py index 526d0cd..4fdc520 100644 --- a/GramAddict/core/perception/screen_identity.py +++ b/GramAddict/core/perception/screen_identity.py @@ -399,11 +399,6 @@ class ScreenIdentity: desc_text = " ".join(content_descs) - # Username on profile - username_match = re.search(r"(\w+)'s (?:profile|story|unseen story)", desc_text) - if username_match: - context["username"] = username_match.group(1) - # Post/follower counts for d in content_descs: m = re.match(r"([\d,.]+K?M?)(\s*)(posts?|followers?|following)", d, re.IGNORECASE) diff --git a/GramAddict/core/resonance_engine.py b/GramAddict/core/resonance_engine.py index 42d73fe..a04cce8 100644 --- a/GramAddict/core/resonance_engine.py +++ b/GramAddict/core/resonance_engine.py @@ -1,6 +1,7 @@ import logging import math import random +import re from typing import Optional from colorama import Fore @@ -108,15 +109,11 @@ class ResonanceEngine: """ username = post_content.get("username", "Unknown") description = post_content.get("description", "") + caption = post_content.get("caption", "") logger.info(f"✨ [Resonance Oracle] Evaluating content from @{username}...", extra={"color": f"{Fore.MAGENTA}"}) # Build a rich text representation of the post - - description = post_content.get("description", "") - caption = post_content.get("caption", "") - username = post_content.get("username", "") - content_text = " ".join(filter(None, [description, caption])).strip() if not content_text or len(content_text) < 5: @@ -218,7 +215,7 @@ class ResonanceEngine: ] text_lower = text.lower() - if any(f" {word} " in f" {text_lower} " for word in tragic_keywords): + if any(re.search(rf"\b{re.escape(word)}\b", text_lower) for word in tragic_keywords): logger.warning( "🛡️ [Empathy Filter] Tragic/Sensitive content detected. Suppressing resonance to prevent blind liking." ) diff --git a/GramAddict/core/screen_topology.py b/GramAddict/core/screen_topology.py index 89c87d3..3f99067 100644 --- a/GramAddict/core/screen_topology.py +++ b/GramAddict/core/screen_topology.py @@ -62,13 +62,25 @@ class ScreenTopology: "press back": ScreenType.HOME_FEED, }, ScreenType.OTHER_PROFILE: { - "press back": ScreenType.HOME_FEED, + "tap home tab": ScreenType.HOME_FEED, + "tap explore tab": ScreenType.EXPLORE_GRID, + "tap reels tab": ScreenType.REELS_FEED, + "tap profile tab": ScreenType.OWN_PROFILE, }, ScreenType.POST_DETAIL: { "tap home tab": ScreenType.HOME_FEED, "tap explore tab": ScreenType.EXPLORE_GRID, "tap profile tab": ScreenType.OWN_PROFILE, "tap reels tab": ScreenType.REELS_FEED, + "tap view all comments": ScreenType.COMMENTS, + "press back": ScreenType.EXPLORE_GRID, # Default fallback if unknown source + }, + ScreenType.COMMENTS: { + "press back": ScreenType.POST_DETAIL, + }, + ScreenType.SEARCH_RESULTS: { + "tap home tab": ScreenType.HOME_FEED, + "press back": ScreenType.EXPLORE_GRID, }, ScreenType.UNKNOWN: { "tap home tab": ScreenType.HOME_FEED, @@ -94,6 +106,8 @@ class ScreenTopology: "view the user profile": ScreenType.OTHER_PROFILE, "view user profile": ScreenType.OTHER_PROFILE, "open user profile": ScreenType.OTHER_PROFILE, + "open search": ScreenType.SEARCH_RESULTS, + "view comments": ScreenType.COMMENTS, } @classmethod diff --git a/tests/core/test_topology_completeness.py b/tests/core/test_topology_completeness.py new file mode 100644 index 0000000..0d15bf0 --- /dev/null +++ b/tests/core/test_topology_completeness.py @@ -0,0 +1,40 @@ +""" +P2-9 & P2-10: ScreenTopology Completeness + +TDD RED: These tests enforce that: +1. COMMENTS and SEARCH_RESULTS screens are reachable in the graph. +2. OTHER_PROFILE transitions are deterministic (no "press back" to HOME_FEED). +3. The pathfinder can find routes to these critical screens. +""" + +import pytest +from GramAddict.core.screen_topology import ScreenTopology +from GramAddict.core.goap import ScreenType + +class TestTopologyCompleteness: + def test_comments_reachability(self): + """RED: Pathfinder should find a route FROM COMMENTS back to HOME_FEED.""" + # This requires COMMENTS to have a transition back to somewhere reachable + route = ScreenTopology.find_route(ScreenType.COMMENTS, ScreenType.HOME_FEED) + assert route is not None + assert "press back" in [step[0] for step in route] + + def test_search_results_reachability(self): + """RED: Pathfinder should find a route FROM SEARCH_RESULTS back to HOME_FEED.""" + route = ScreenTopology.find_route(ScreenType.SEARCH_RESULTS, ScreenType.HOME_FEED) + assert route is not None + + def test_other_profile_to_home_is_not_back(self): + """RED: Moving from OTHER_PROFILE to HOME_FEED should NOT use 'press back'.""" + route = ScreenTopology.find_route(ScreenType.OTHER_PROFILE, ScreenType.HOME_FEED) + assert route is not None + steps = [step[0] for step in route] + # 'press back' is forbidden for this transition because it's non-deterministic + assert "press back" not in steps + assert "tap home tab" in steps + + def test_goal_map_contains_missing_screens(self): + """RED: Goals like 'open search' should map to SEARCH_RESULTS.""" + # Note: SEARCH_RESULTS might be reached via 'tap explore tab' + 'tap search bar' + # but the goal should still be valid. + assert ScreenTopology.goal_to_target_screen("open search") == ScreenType.SEARCH_RESULTS